refactor: move Swaps + RefundSwap scanners to their own views

This commit is contained in:
Evan Kaloudis
2026-03-04 20:10:53 -05:00
parent 752da70cb5
commit 70356f3a71
8 changed files with 230 additions and 142 deletions
+14
View File
@@ -65,6 +65,8 @@ import LnurlAuth from './views/LnurlAuth';
import Receive from './views/Receive';
import PaymentRequest from './views/PaymentRequest';
import HandleAnythingQRScanner from './views/HandleAnythingQRScanner';
import SwapsQRScanner from './views/SwapsQRScanner';
import RefundSwapQRScanner from './views/RefundSwapQRScanner';
import NodeQRScanner from './views/NodeQRScanner';
import OpenChannel from './views/OpenChannel';
import SendingOnChain from './views/SendingOnChain';
@@ -743,6 +745,18 @@ export default class App extends React.PureComponent {
HandleAnythingQRScanner
}
/>
<Stack.Screen
name="SwapsQRScanner" // @ts-ignore:next-line
component={
SwapsQRScanner
}
/>
<Stack.Screen
name="RefundSwapQRScanner" // @ts-ignore:next-line
component={
RefundSwapQRScanner
}
/>
<Stack.Screen
name="NodeQRCodeScanner" // @ts-ignore:next-line
component={
+1 -136
View File
@@ -6,32 +6,19 @@ import { Header } from '@rneui/themed';
import { observer } from 'mobx-react';
import { URDecoder } from '@ngraveio/bc-ur';
import { StackNavigationProp } from '@react-navigation/stack';
import { Route } from '@react-navigation/native';
import { Bytes, CryptoAccount, CryptoPSBT } from '@keystonehq/bc-ur-registry';
import LoadingIndicator from '../components/LoadingIndicator';
import QRCodeScanner from '../components/QRCodeScanner';
import { nodeInfoStore } from '../stores/Stores';
import Invoice from '../models/Invoice';
import handleAnything from '../utils/handleAnything';
import Base64Utils from '../utils/Base64Utils';
import { joinQRs } from '../utils/BbqrUtils';
import { localeString } from '../utils/LocaleUtils';
import { themeColor } from '../utils/ThemeUtils';
import AddressUtils from '../utils/AddressUtils';
import BackendUtils from '../utils/BackendUtils';
interface HandleAnythingQRProps {
navigation: StackNavigationProp<any, any>;
route: Route<
'HandleAnythingQRScanner',
{
view?: string;
}
>;
}
interface HandleAnythingQRState {
@@ -68,129 +55,7 @@ export default class HandleAnythingQRScanner extends React.Component<
};
handleAnythingScanned = async (data: string) => {
const { navigation, route } = this.props;
const view = route.params?.view;
if (view === 'Swaps') {
if (this.isProcessing) return;
this.isProcessing = true;
this.setState({ loading: true });
try {
const { value, satAmount } = AddressUtils.processBIP21Uri(data);
const { nodeInfo } = nodeInfoStore;
const { isTestNet, isRegTest, isSigNet } = nodeInfo;
// Reverse Swap
if (
AddressUtils.isValidBitcoinAddress(
value,
isTestNet || isRegTest || isSigNet
)
) {
navigation.goBack();
const navigationParams: any = {
initialInvoice: value,
initialReverse: true
};
if (satAmount) {
navigationParams.initialAmountSats = satAmount;
}
navigation.navigate('Swaps', navigationParams);
return;
}
// Submarine Swap
if (AddressUtils.isValidLightningPaymentRequest(value)) {
const decodedInvoice =
await BackendUtils.decodePaymentRequest([value]);
if (!decodedInvoice) {
throw new Error(
localeString('views.Invoice.couldNotDecode')
);
}
const invoiceModel = new Invoice(decodedInvoice);
const amount = invoiceModel.getRequestAmount;
navigation.goBack();
navigation.navigate('Swaps', {
initialInvoice: value,
initialAmountSats: amount,
initialReverse: false
});
return;
}
throw new Error(
localeString('components.QRCodeScanner.notRecognized')
);
} catch (err: any) {
this.isProcessing = false;
console.error(err.message);
Alert.alert(
localeString('general.error'),
(err as Error).message ||
localeString('utils.handleAnything.notValid'),
[
{
text: localeString('general.ok'),
onPress: () => void 0
}
],
{ cancelable: false }
);
this.setState({ loading: false });
navigation.goBack();
}
return;
} else if (view === 'RefundSwap') {
if (this.isProcessing) return;
this.isProcessing = true;
this.setState({ loading: true });
try {
const { value } = AddressUtils.processBIP21Uri(data);
const { nodeInfo } = nodeInfoStore;
const { isTestNet, isRegTest, isSigNet } = nodeInfo;
if (
AddressUtils.isValidBitcoinAddress(
value,
isTestNet || isRegTest || isSigNet
)
) {
navigation.goBack();
navigation.navigate('RefundSwap', {
scannedAddress: value
});
return;
}
throw new Error(
localeString('components.QRCodeScanner.notRecognized')
);
} catch (err: any) {
this.isProcessing = false;
console.error(err.message);
Alert.alert(
localeString('general.error'),
(err as Error).message ||
localeString('utils.handleAnything.notValid'),
[
{
text: localeString('general.ok'),
onPress: () => void 0
}
],
{ cancelable: false }
);
this.setState({ loading: false });
navigation.goBack();
}
return;
}
const { navigation } = this.props;
let handleData;
+5
View File
@@ -13,8 +13,12 @@ interface NodeQRProps {
function NodeQRScanner(props: NodeQRProps) {
const { navigation } = props;
const isProcessing = React.useRef(false);
const handleNodeScanned = (data: string) => {
if (isProcessing.current) return;
isProcessing.current = true;
if (NodeUriUtils.isValidNodeUri(data)) {
const { pubkey, host } = NodeUriUtils.processNodeUri(data);
navigation.popTo('OpenChannel', {
@@ -22,6 +26,7 @@ function NodeQRScanner(props: NodeQRProps) {
host
});
} else {
isProcessing.current = false;
Alert.alert(
localeString('general.error'),
localeString('views.NodeQRScanner.error'),
+112
View File
@@ -0,0 +1,112 @@
import * as React from 'react';
import { Alert, View } from 'react-native';
import { Header } from '@rneui/themed';
import { observer } from 'mobx-react';
import { StackNavigationProp } from '@react-navigation/stack';
import LoadingIndicator from '../../components/LoadingIndicator';
import QRCodeScanner from '../../components/QRCodeScanner';
import { localeString } from '../../utils/LocaleUtils';
import { themeColor } from '../../utils/ThemeUtils';
export interface BaseQRScannerProps {
navigation: StackNavigationProp<any, any>;
}
export interface BaseQRScannerState {
loading: boolean;
}
@observer
export default abstract class BaseQRScanner<
P extends BaseQRScannerProps = BaseQRScannerProps,
S extends BaseQRScannerState = BaseQRScannerState
> extends React.Component<P, S> {
isProcessing: boolean = false;
constructor(props: P) {
super(props);
this.state = {
loading: false
} as S;
}
/**
* Process the scanned QR data. Implementations should:
* - Validate the data
* - Navigate to the appropriate screen on success
* - Throw an error if the data is invalid
*/
protected abstract processQRData(data: string): Promise<void>;
handleQRScanned = async (data: string) => {
const { navigation } = this.props;
if (this.isProcessing) return;
this.isProcessing = true;
this.setState({ loading: true } as Partial<S> as S);
try {
await this.processQRData(data);
} catch (err: any) {
this.isProcessing = false;
console.error(err.message);
Alert.alert(
localeString('general.error'),
(err as Error).message ||
localeString('utils.handleAnything.notValid'),
[
{
text: localeString('general.ok'),
onPress: () => void 0
}
],
{ cancelable: false }
);
this.setState({ loading: false } as Partial<S> as S);
navigation.goBack();
}
};
render() {
const { navigation } = this.props;
const { loading } = this.state;
if (loading) {
return (
<View
style={{
flex: 1,
backgroundColor: themeColor('background')
}}
>
<Header
centerComponent={{
text: localeString('general.loading'),
style: {
color: themeColor('text'),
fontFamily: 'PPNeueMontreal-Book'
}
}}
backgroundColor={themeColor('background')}
containerStyle={{
borderBottomWidth: 0
}}
/>
<View style={{ top: 40 }}>
<LoadingIndicator />
</View>
</View>
);
}
return (
<QRCodeScanner
handleQRScanned={this.handleQRScanned}
goBack={() => navigation.goBack()}
navigation={navigation}
/>
);
}
}
+34
View File
@@ -0,0 +1,34 @@
import { observer } from 'mobx-react';
import BaseQRScanner from './QRScanner/BaseQRScanner';
import { nodeInfoStore } from '../stores/Stores';
import { localeString } from '../utils/LocaleUtils';
import AddressUtils from '../utils/AddressUtils';
@observer
export default class RefundSwapQRScanner extends BaseQRScanner {
protected async processQRData(data: string): Promise<void> {
const { navigation } = this.props;
const { value } = AddressUtils.processBIP21Uri(data);
const { nodeInfo } = nodeInfoStore;
const { isTestNet, isRegTest, isSigNet } = nodeInfo;
if (
AddressUtils.isValidBitcoinAddress(
value,
isTestNet || isRegTest || isSigNet
)
) {
navigation.goBack();
navigation.navigate('RefundSwap', {
scannedAddress: value
});
return;
}
throw new Error(localeString('components.QRCodeScanner.notRecognized'));
}
}
+1 -4
View File
@@ -215,10 +215,7 @@ export default class RefundSwap extends React.Component<
}}
onScan={() =>
this.props.navigation.navigate(
'HandleAnythingQRScanner',
{
view: 'RefundSwap'
}
'RefundSwapQRScanner'
)
}
placeholder={fetchingAddress ? '' : 'bc1...'}
+1 -2
View File
@@ -2018,8 +2018,7 @@ export default class Swap extends React.PureComponent<SwapProps, SwapState> {
onScan={() => {
this.isNavigatingAway = true;
navigation.navigate(
'HandleAnythingQRScanner',
{ view: 'Swaps' }
'SwapsQRScanner'
);
}}
placeholder={
+62
View File
@@ -0,0 +1,62 @@
import { observer } from 'mobx-react';
import BaseQRScanner from './QRScanner/BaseQRScanner';
import { nodeInfoStore } from '../stores/Stores';
import Invoice from '../models/Invoice';
import { localeString } from '../utils/LocaleUtils';
import AddressUtils from '../utils/AddressUtils';
import BackendUtils from '../utils/BackendUtils';
@observer
export default class SwapsQRScanner extends BaseQRScanner {
protected async processQRData(data: string): Promise<void> {
const { navigation } = this.props;
const { value, satAmount } = AddressUtils.processBIP21Uri(data);
const { nodeInfo } = nodeInfoStore;
const { isTestNet, isRegTest, isSigNet } = nodeInfo;
// Reverse Swap - Bitcoin address
if (
AddressUtils.isValidBitcoinAddress(
value,
isTestNet || isRegTest || isSigNet
)
) {
navigation.goBack();
navigation.navigate('Swaps', {
initialInvoice: value,
initialAmountSats: satAmount,
initialReverse: true
});
return;
}
// Submarine Swap - Lightning invoice
if (AddressUtils.isValidLightningPaymentRequest(value)) {
const decodedInvoice = await BackendUtils.decodePaymentRequest([
value
]);
if (!decodedInvoice) {
throw new Error(localeString('views.Invoice.couldNotDecode'));
}
const invoiceModel = new Invoice(decodedInvoice);
const amount = invoiceModel.getRequestAmount;
navigation.goBack();
navigation.navigate('Swaps', {
initialInvoice: value,
initialAmountSats: amount,
initialReverse: false
});
return;
}
throw new Error(localeString('components.QRCodeScanner.notRecognized'));
}
}