diff --git a/App.tsx b/App.tsx
index f262a1363..bb8f567e8 100644
--- a/App.tsx
+++ b/App.tsx
@@ -78,6 +78,7 @@ import InvoicesSettings from './views/Settings/InvoicesSettings';
import LSP from './views/Settings/LSP';
import ChannelsSettings from './views/Settings/ChannelsSettings';
import SetWalletPicture from './views/Settings/SetWalletPicture';
+import ChoosePaymentMethod from './views/ChoosePaymentMethod';
// Lightning address
import LightningAddress from './views/Settings/LightningAddress';
@@ -245,7 +246,7 @@ export default class App extends React.PureComponent {
);
}
}}
- // @ts-ignore:next-line]
+ // @ts-ignore:next-line
theme={{
dark: true,
colors: {
@@ -316,6 +317,12 @@ export default class App extends React.PureComponent {
name="Accounts" // @ts-ignore:next-line
component={Accounts}
/>
+
this.fetchLnInvoice()}
+ onPress={() => (disabled ? null : this.fetchLnInvoice())}
activeOpacity={1}
>
{children}
diff --git a/components/LayerBalances/OnchainSwipeableRow.tsx b/components/LayerBalances/OnchainSwipeableRow.tsx
index 22ab3f70e..6fb504d3f 100644
--- a/components/LayerBalances/OnchainSwipeableRow.tsx
+++ b/components/LayerBalances/OnchainSwipeableRow.tsx
@@ -27,6 +27,7 @@ interface OnchainSwipeableRowProps {
account?: string;
hidden?: boolean;
children?: React.ReactNode;
+ disabled?: boolean;
}
export default class OnchainSwipeableRow extends Component<
@@ -170,11 +171,11 @@ export default class OnchainSwipeableRow extends Component<
};
render() {
- const { children, value, locked, hidden } = this.props;
+ const { children, value, locked, hidden, disabled } = this.props;
if (locked && value) {
return (
this.sendToAddress()}
+ onPress={() => (disabled ? null : this.sendToAddress())}
activeOpacity={1}
style={{ width: '100%' }}
>
diff --git a/components/LayerBalances/PaymentMethodList.tsx b/components/LayerBalances/PaymentMethodList.tsx
new file mode 100644
index 000000000..d1ed1bde5
--- /dev/null
+++ b/components/LayerBalances/PaymentMethodList.tsx
@@ -0,0 +1,263 @@
+import React, { Component } from 'react';
+import { FlatList, StyleSheet, Text, View, I18nManager } from 'react-native';
+import LinearGradient from 'react-native-linear-gradient';
+import { RectButton } from 'react-native-gesture-handler';
+import { StackNavigationProp } from '@react-navigation/stack';
+
+import { Spacer } from '../layout/Spacer';
+import OnchainSwipeableRow from './OnchainSwipeableRow';
+import LightningSwipeableRow from './LightningSwipeableRow';
+
+import BackendUtils from '../../utils/BackendUtils';
+import { localeString } from '../../utils/LocaleUtils';
+import { themeColor } from '../../utils/ThemeUtils';
+
+import OnChainSvg from '../../assets/images/SVG/DynamicSVG/OnChainSvg';
+import LightningSvg from '../../assets/images/SVG/DynamicSVG/LightningSvg';
+
+interface PaymentMethodListProps {
+ navigation: StackNavigationProp;
+ value?: string;
+ amount?: string;
+ lightning?: string;
+ offer?: string;
+}
+
+// To toggle LTR/RTL change to `true`
+I18nManager.allowRTL(false);
+
+type DataRow = {
+ layer: string;
+ subtitle?: string;
+ disabled?: boolean;
+};
+
+const Row = ({ item }: { item: DataRow }) => {
+ return (
+
+
+
+ {item.layer === 'On-chain' ? (
+
+ ) : item.layer === 'Lightning' || item.layer === 'Offer' ? (
+
+ ) : (
+
+ )}
+
+
+
+ {item.layer === 'Lightning'
+ ? localeString('general.lightning')
+ : item.layer === 'Offer'
+ ? localeString('views.Settings.Bolt12Offer')
+ : item.layer === 'On-chain'
+ ? localeString('general.onchain')
+ : item.layer}
+
+ {item.subtitle && (
+
+ {item.subtitle}
+
+ )}
+
+
+
+
+ );
+};
+
+const SwipeableRow = ({
+ item,
+ navigation,
+ value,
+ amount,
+ lightning,
+ offer
+}: {
+ item: DataRow;
+ index: number;
+ navigation: StackNavigationProp;
+ value?: string;
+ amount?: string;
+ lightning?: string;
+ offer?: string;
+}) => {
+ if (item.layer === 'Lightning') {
+ return (
+
+
+
+ );
+ }
+
+ if (item.layer === 'Offer') {
+ return (
+
+
+
+ );
+ }
+
+ if (item.layer === 'On-chain') {
+ return (
+
+
+
+ );
+ }
+};
+
+export default class PaymentMethodList extends Component<
+ PaymentMethodListProps,
+ {}
+> {
+ render() {
+ const { navigation, value, amount, lightning, offer } = this.props;
+
+ let DATA: DataRow[] = [];
+
+ if (lightning) {
+ DATA.push({
+ layer: 'Lightning',
+ subtitle: `${lightning?.slice(0, 12)}...${lightning?.slice(
+ -12
+ )}`
+ });
+ }
+
+ if (offer) {
+ DATA.push({
+ layer: 'Offer',
+ subtitle: `${offer?.slice(0, 12)}...${offer?.slice(-12)}`,
+ disabled: !BackendUtils.supportsOffers()
+ });
+ }
+
+ // Only show on-chain balance for non-Lnbank accounts
+ if (BackendUtils.supportsOnchainReceiving()) {
+ DATA.push({
+ layer: 'On-chain',
+ subtitle: value
+ ? `${value.slice(0, 12)}...${value.slice(-12)}`
+ : undefined,
+ disabled: !BackendUtils.supportsOnchainSends()
+ });
+ }
+
+ return (
+
+ (
+
+ )}
+ renderItem={({ item, index }) => (
+ // @ts-ignore:next-line
+
+ )}
+ keyExtractor={(_item, index) => `message ${index}`}
+ style={{ marginTop: 20 }}
+ refreshing={false}
+ />
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ rectButton: {
+ flex: 1,
+ height: 80,
+ paddingVertical: 10,
+ paddingLeft: 6,
+ paddingRight: 20,
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ flexDirection: 'row',
+ marginLeft: 15,
+ marginRight: 15,
+ borderRadius: 50
+ },
+ moreButton: {
+ height: 40,
+ paddingVertical: 10,
+ paddingHorizontal: 20,
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ flexDirection: 'row',
+ marginLeft: 15,
+ marginRight: 15,
+ borderRadius: 15
+ },
+ left: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'flex-start'
+ },
+ separator: {
+ backgroundColor: 'transparent',
+ height: 20
+ },
+ layerText: {
+ backgroundColor: 'transparent',
+ fontSize: 15,
+ fontFamily: 'PPNeueMontreal-Medium'
+ },
+ eyeIcon: { alignSelf: 'center', margin: 15, marginLeft: 25 }
+});
diff --git a/components/LayerBalances/index.tsx b/components/LayerBalances/index.tsx
index 70526a51f..446cf2cf2 100644
--- a/components/LayerBalances/index.tsx
+++ b/components/LayerBalances/index.tsx
@@ -170,7 +170,6 @@ const SwipeableRow = ({
item: DataRow;
index: number;
navigation: StackNavigationProp;
- // selectMode: boolean; // not used for no
value?: string;
amount?: string;
lightning?: string;
diff --git a/utils/AddressUtils.test.ts b/utils/AddressUtils.test.ts
index a5a767bed..d7c706d11 100644
--- a/utils/AddressUtils.test.ts
+++ b/utils/AddressUtils.test.ts
@@ -9,70 +9,66 @@ import AddressUtils from './AddressUtils';
import { walletrpc } from '../proto/lightning';
describe('AddressUtils', () => {
- describe('isValidBitcoinAddress', () => {
- it('validates Bitcoin Addresses properly', () => {
- expect(AddressUtils.isValidBitcoinAddress('a', false)).toBeFalsy();
+ describe('isValidBIP21Uri', () => {
+ it('validates all BIP-21 URI variations', () => {
expect(
- AddressUtils.isValidBitcoinAddress(
- 'bcrt1qqgdrlt97x4847rf85utak8gre5q7k83uwh3ajj',
- true
+ AddressUtils.isValidBIP21Uri(
+ 'BITCOIN:BC1Q7065EZYHCD3QTQLCVWCMP9T2WEAXC4SGUUVLWU?amount=0.00170003'
)
- ).toBeTruthy();
+ ).toEqual(true);
expect(
- AddressUtils.isValidBitcoinAddress(
- '1AY6gTALH7bGrbN73qqTRnkW271JvBJc9o',
- false
+ AddressUtils.isValidBIP21Uri(
+ 'BITCOIN:bc1q7065ezyhcd3qtqlcvwcmp9t2weaxc4sguuvlwu?amount=0.00170003'
)
- ).toBeTruthy();
+ ).toEqual(true);
expect(
- AddressUtils.isValidBitcoinAddress(
- '1AevDm7EU7TQn5q4QizTrbkZfEg5xpLM7s',
- false
+ AddressUtils.isValidBIP21Uri(
+ 'BITCOIN:BC1Q7065EZYHCD3QTQLCVWCMP9T2WEAXC4SGUUVLWU'
)
- ).toBeTruthy();
+ ).toEqual(true);
expect(
- AddressUtils.isValidBitcoinAddress(
- '3BZj8Xf72guNcHCvaTCyhJqzyKhNJZuSUK',
- false
+ AddressUtils.isValidBIP21Uri(
+ 'BITCOIN:bc1q7065ezyhcd3qtqlcvwcmp9t2weaxc4sguuvlwu'
)
- ).toBeTruthy();
+ ).toEqual(true);
expect(
- AddressUtils.isValidBitcoinAddress(
- '3BMEXKKbiLv8a4v6Q482EfQAtecQUDAE6w',
- false
+ AddressUtils.isValidBIP21Uri(
+ 'bitcoin:bc1q7065ezyhcd3qtqlcvwcmp9t2weaxc4sguuvlwu'
)
- ).toBeTruthy();
+ ).toEqual(true);
expect(
- AddressUtils.isValidBitcoinAddress(
- 'bc1q7065ezyhcd3qtqlcvwcmp9t2weaxc4sguuvlwu',
- false
+ AddressUtils.isValidBIP21Uri(
+ 'BITCOIN:BC1Q7065EZYHCD3QTQLCVWCMP9T2WEAXC4SGUUVLWU?amount=0.00170003'
)
- ).toBeTruthy();
+ ).toEqual(true);
expect(
- AddressUtils.isValidBitcoinAddress(
- 'bc1q073ezlgdrqj8ug8gpmlnh0qa7ztlx65cm62sck',
- false
+ AddressUtils.isValidBIP21Uri(
+ 'bitcoin:?lno=lno1pgqpvggr3l9u9ppv79mzn7g9v98cf8zw900skucuz53zr5vvjss454zrnyes'
)
- ).toBeTruthy();
+ ).toEqual(true);
expect(
- AddressUtils.isValidBitcoinAddress(
- 'bc1q073ezlgdrqj8ug8gpmlnh0qa7ztlx65cm62sck-',
- false
+ AddressUtils.isValidBIP21Uri(
+ 'bitcoin:?lno=lno1qsgqmqvgm96frzdg8m0gc6nzeqffvzsqzrxqy32afmr3jn9ggkwg3egfwch2hy0l6jut6vfd8vpsc3h89l6u3dm4q2d6nuamav3w27xvdmv3lpgklhg7l5teypqz9l53hj7zvuaenh34xqsz2sa967yzqkylfu9xtcd5ymcmfp32h083e805y7jfd236w9afhavqqvl8uyma7x77yun4ehe9pnhu2gekjguexmxpqjcr2j822xr7q34p078gzslf9wpwz5y57alxu99s0z2ql0kfqvwhzycqq45ehh58xnfpuek80hw6spvwrvttjrrq9pphh0dpydh06qqspp5uq4gpyt6n9mwexde44qv7lstzzq60nr40ff38u27un6y53aypmx0p4qruk2tf9mjwqlhxak4znvna5y'
)
- ).toBeFalsy();
-
+ ).toEqual(true);
expect(
- AddressUtils.isValidBitcoinAddress(
- 'lndhub://db48dd0a-a298-405f-b70e-a62d8df5ae45:85f8a5a56c547fd561f6d8902b0ca9d66fc0152a@https://mybtcpayserver.com/plugins/lnbank/api/lndhub',
- false
+ AddressUtils.isValidBIP21Uri(
+ 'BITCOIN:?LNO=lno1pgqpvggr3l9u9ppv79mzn7g9v98cf8zw900skucuz53zr5vvjss454zrnyes'
)
- ).toBeFalsy();
+ ).toEqual(true);
+ expect(
+ AddressUtils.isValidBIP21Uri(
+ 'shitcoin:?LNO=lno1pgqpvggr3l9u9ppv79mzn7g9v98cf8zw900skucuz53zr5vvjss454zrnyes'
+ )
+ ).toEqual(false);
});
+ });
- it('processes all Bech32 send address variations', () => {
+ describe('processBIP21Uri', () => {
+ it('processes all BIP-21 URI variations', () => {
// with fee
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'BITCOIN:BC1Q7065EZYHCD3QTQLCVWCMP9T2WEAXC4SGUUVLWU?amount=0.00170003'
)
).toEqual({
@@ -80,60 +76,45 @@ describe('AddressUtils', () => {
amount: '170003' // amount in sats
});
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'BITCOIN:bc1q7065ezyhcd3qtqlcvwcmp9t2weaxc4sguuvlwu?amount=0.00170003'
)
).toEqual({
value: 'bc1q7065ezyhcd3qtqlcvwcmp9t2weaxc4sguuvlwu',
amount: '170003' // amount in sats
});
- expect(
- AddressUtils.processSendAddress(
- 'bitcoin:BC1Q7065EZYHCD3QTQLCVWCMP9T2WEAXC4SGUUVLWU?amount=0.00170003'
- )
- ).toEqual({
- value: 'BC1Q7065EZYHCD3QTQLCVWCMP9T2WEAXC4SGUUVLWU',
- amount: '170003' // amount in sats
- });
- expect(
- AddressUtils.processSendAddress(
- 'bitcoin:bc1q7065ezyhcd3qtqlcvwcmp9t2weaxc4sguuvlwu?amount=0.00170003'
- )
- ).toEqual({
- value: 'bc1q7065ezyhcd3qtqlcvwcmp9t2weaxc4sguuvlwu',
- amount: '170003' // amount in sats
- });
+
// without fee
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'BITCOIN:BC1Q7065EZYHCD3QTQLCVWCMP9T2WEAXC4SGUUVLWU'
)
).toEqual({
value: 'BC1Q7065EZYHCD3QTQLCVWCMP9T2WEAXC4SGUUVLWU'
});
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'BITCOIN:bc1q7065ezyhcd3qtqlcvwcmp9t2weaxc4sguuvlwu'
)
).toEqual({
value: 'bc1q7065ezyhcd3qtqlcvwcmp9t2weaxc4sguuvlwu'
});
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'bitcoin:BC1Q7065EZYHCD3QTQLCVWCMP9T2WEAXC4SGUUVLWU'
)
).toEqual({
value: 'BC1Q7065EZYHCD3QTQLCVWCMP9T2WEAXC4SGUUVLWU'
});
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'bitcoin:bc1q7065ezyhcd3qtqlcvwcmp9t2weaxc4sguuvlwu'
)
).toEqual({
value: 'bc1q7065ezyhcd3qtqlcvwcmp9t2weaxc4sguuvlwu'
});
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'BITCOIN:BC1Q7065EZYHCD3QTQLCVWCMP9T2WEAXC4SGUUVLWU?amount=0.00170003'
)
).toEqual({
@@ -143,7 +124,7 @@ describe('AddressUtils', () => {
// bolt12 offers
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'bitcoin:?lno=lno1pgqpvggr3l9u9ppv79mzn7g9v98cf8zw900skucuz53zr5vvjss454zrnyes'
)
).toEqual({
@@ -151,7 +132,7 @@ describe('AddressUtils', () => {
offer: 'lno1pgqpvggr3l9u9ppv79mzn7g9v98cf8zw900skucuz53zr5vvjss454zrnyes'
});
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'BITCOIN:?lno=lno1pgqpvggr3l9u9ppv79mzn7g9v98cf8zw900skucuz53zr5vvjss454zrnyes'
)
).toEqual({
@@ -159,7 +140,7 @@ describe('AddressUtils', () => {
offer: 'lno1pgqpvggr3l9u9ppv79mzn7g9v98cf8zw900skucuz53zr5vvjss454zrnyes'
});
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'BITCOIN:?LNO=lno1pgqpvggr3l9u9ppv79mzn7g9v98cf8zw900skucuz53zr5vvjss454zrnyes'
)
).toEqual({
@@ -167,6 +148,67 @@ describe('AddressUtils', () => {
offer: 'lno1pgqpvggr3l9u9ppv79mzn7g9v98cf8zw900skucuz53zr5vvjss454zrnyes'
});
});
+ });
+
+ describe('isValidBitcoinAddress', () => {
+ it('validates Bitcoin Addresses properly', () => {
+ expect(AddressUtils.isValidBitcoinAddress('a', false)).toBeFalsy();
+ expect(
+ AddressUtils.isValidBitcoinAddress(
+ 'bcrt1qqgdrlt97x4847rf85utak8gre5q7k83uwh3ajj',
+ true
+ )
+ ).toBeTruthy();
+ expect(
+ AddressUtils.isValidBitcoinAddress(
+ '1AY6gTALH7bGrbN73qqTRnkW271JvBJc9o',
+ false
+ )
+ ).toBeTruthy();
+ expect(
+ AddressUtils.isValidBitcoinAddress(
+ '1AevDm7EU7TQn5q4QizTrbkZfEg5xpLM7s',
+ false
+ )
+ ).toBeTruthy();
+ expect(
+ AddressUtils.isValidBitcoinAddress(
+ '3BZj8Xf72guNcHCvaTCyhJqzyKhNJZuSUK',
+ false
+ )
+ ).toBeTruthy();
+ expect(
+ AddressUtils.isValidBitcoinAddress(
+ '3BMEXKKbiLv8a4v6Q482EfQAtecQUDAE6w',
+ false
+ )
+ ).toBeTruthy();
+ expect(
+ AddressUtils.isValidBitcoinAddress(
+ 'bc1q7065ezyhcd3qtqlcvwcmp9t2weaxc4sguuvlwu',
+ false
+ )
+ ).toBeTruthy();
+ expect(
+ AddressUtils.isValidBitcoinAddress(
+ 'bc1q073ezlgdrqj8ug8gpmlnh0qa7ztlx65cm62sck',
+ false
+ )
+ ).toBeTruthy();
+ expect(
+ AddressUtils.isValidBitcoinAddress(
+ 'bc1q073ezlgdrqj8ug8gpmlnh0qa7ztlx65cm62sck-',
+ false
+ )
+ ).toBeFalsy();
+
+ expect(
+ AddressUtils.isValidBitcoinAddress(
+ 'lndhub://db48dd0a-a298-405f-b70e-a62d8df5ae45:85f8a5a56c547fd561f6d8902b0ca9d66fc0152a@https://mybtcpayserver.com/plugins/lnbank/api/lndhub',
+ false
+ )
+ ).toBeFalsy();
+ });
it('validates Bech32m - P2TR properly', () => {
expect(
@@ -283,10 +325,10 @@ describe('AddressUtils', () => {
).toBeFalsy();
});
- describe('processSendAddress', () => {
+ describe('processBIP21Uri', () => {
it('process address inputed and scanned from the Send view', () => {
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'bitcoin:34K6tvoWM7k2ujeXVuimv29WyAsqzhWofb?amount=0.00170003'
)
).toEqual({
@@ -295,7 +337,7 @@ describe('AddressUtils', () => {
});
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'bitcoin:34K6tvoWM7k2ujeXVuimv29WyAsqzhWofb?label=test&amount=0.00170003'
)
).toEqual({
@@ -304,7 +346,7 @@ describe('AddressUtils', () => {
});
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'bitcoin:34K6tvoWM7k2ujeXVuimv29WyAsqzhWofb?amount=0.00170003&label=testw&randomparams=rm2'
)
).toEqual({
@@ -313,7 +355,7 @@ describe('AddressUtils', () => {
});
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'bitcoin:34K6tvoWM7k2ujeXVuimv29WyAsqzhWofb'
)
).toEqual({
@@ -322,7 +364,7 @@ describe('AddressUtils', () => {
});
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'bitcoin:34K6tvoWM7k2ujeXVuimv29WyAsqzhWofd?label=BitMEX%20Deposit%20-%20randomUser'
)
).toEqual({
@@ -333,7 +375,7 @@ describe('AddressUtils', () => {
it('processes BIP21 invoices', () => {
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'bitcoin:BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U?amount=0.00001&label=sbddesign%3A%20For%20lunch%20Tuesday&message=For%20lunch%20Tuesday&lightning=LNBC10U1P3PJ257PP5YZTKWJCZ5FTL5LAXKAV23ZMZEKAW37ZK6KMV80PK4XAEV5QHTZ7QDPDWD3XGER9WD5KWM36YPRX7U3QD36KUCMGYP282ETNV3SHJCQZPGXQYZ5VQSP5USYC4LK9CHSFP53KVCNVQ456GANH60D89REYKDNGSMTJ6YW3NHVQ9QYYSSQJCEWM5CJWZ4A6RFJX77C490YCED6PEMK0UPKXHY89CMM7SCT66K8GNEANWYKZGDRWRFJE69H9U5U0W57RRCSYSAS7GADWMZXC8C6T0SPJAZUP6'
)
).toEqual({
@@ -346,7 +388,7 @@ describe('AddressUtils', () => {
it('processes BIP21 invoices - with hanging values after lightning param', () => {
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'bitcoin:BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U?amount=0.00001&label=sbddesign%3A%20For%20lunch%20Tuesday&message=For%20lunch%20Tuesday&lightning=LNBC10U1P3PJ257PP5YZTKWJCZ5FTL5LAXKAV23ZMZEKAW37ZK6KMV80PK4XAEV5QHTZ7QDPDWD3XGER9WD5KWM36YPRX7U3QD36KUCMGYP282ETNV3SHJCQZPGXQYZ5VQSP5USYC4LK9CHSFP53KVCNVQ456GANH60D89REYKDNGSMTJ6YW3NHVQ9QYYSSQJCEWM5CJWZ4A6RFJX77C490YCED6PEMK0UPKXHY89CMM7SCT66K8GNEANWYKZGDRWRFJE69H9U5U0W57RRCSYSAS7GADWMZXC8C6T0SPJAZUP6&test=haha'
)
).toEqual({
@@ -359,7 +401,7 @@ describe('AddressUtils', () => {
it('processes BIP21 invoices - with lightning param in all caps', () => {
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'bitcoin:BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U?amount=0.00001&label=sbddesign%3A%20For%20lunch%20Tuesday&message=For%20lunch%20Tuesday&LIGHTNING=LNBC10U1P3PJ257PP5YZTKWJCZ5FTL5LAXKAV23ZMZEKAW37ZK6KMV80PK4XAEV5QHTZ7QDPDWD3XGER9WD5KWM36YPRX7U3QD36KUCMGYP282ETNV3SHJCQZPGXQYZ5VQSP5USYC4LK9CHSFP53KVCNVQ456GANH60D89REYKDNGSMTJ6YW3NHVQ9QYYSSQJCEWM5CJWZ4A6RFJX77C490YCED6PEMK0UPKXHY89CMM7SCT66K8GNEANWYKZGDRWRFJE69H9U5U0W57RRCSYSAS7GADWMZXC8C6T0SPJAZUP6&test=haha'
)
).toEqual({
@@ -372,7 +414,7 @@ describe('AddressUtils', () => {
it('processes BIP21 invoices - with amount param in all caps', () => {
expect(
- AddressUtils.processSendAddress(
+ AddressUtils.processBIP21Uri(
'bitcoin:BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U?AMOUNT=0.00001&label=sbddesign%3A%20For%20lunch%20Tuesday&message=For%20lunch%20Tuesday&LIGHTNING=LNBC10U1P3PJ257PP5YZTKWJCZ5FTL5LAXKAV23ZMZEKAW37ZK6KMV80PK4XAEV5QHTZ7QDPDWD3XGER9WD5KWM36YPRX7U3QD36KUCMGYP282ETNV3SHJCQZPGXQYZ5VQSP5USYC4LK9CHSFP53KVCNVQ456GANH60D89REYKDNGSMTJ6YW3NHVQ9QYYSSQJCEWM5CJWZ4A6RFJX77C490YCED6PEMK0UPKXHY89CMM7SCT66K8GNEANWYKZGDRWRFJE69H9U5U0W57RRCSYSAS7GADWMZXC8C6T0SPJAZUP6&test=haha'
)
).toEqual({
diff --git a/utils/AddressUtils.ts b/utils/AddressUtils.ts
index 21a5effcc..bb1460cf1 100644
--- a/utils/AddressUtils.ts
+++ b/utils/AddressUtils.ts
@@ -22,6 +22,10 @@ const lnInvoice =
/^(lnbc|lntb|lntbs|lnbcrt|LNBC|LNTB|LNTBS|LNBCRT)([0-9]{1,}[a-zA-Z0-9]+){1}$/;
const lnPubKey = /^[a-f0-9]{66}$/;
+/* BIP-21 */
+const bip21Uri =
+ /^(bitcoin|BITCOIN):([13a-zA-Z0-9]{25,42})?(\?((amount|AMOUNT)=([0-9]+(\.[0-9]+)?)|(label|LABEL|message|MESSAGE|lightning|LIGHTNING|lno|LNO)=([^&]*))((&((amount|AMOUNT)=([0-9]+(\.[0-9]+)?)|(label|LABEL|message|MESSAGE|lightning|LIGHTNING|lno|LNO)=([^&]*)))*))?/;
+
/* BOLT 12 */
const lnOffer = /^(lno|LNO)([0-9]{1,}[a-zA-Z0-9]+){1}$/;
@@ -103,7 +107,7 @@ const bitcoinQrParser = (input: string, prefix: string) => {
};
class AddressUtils {
- processSendAddress = (input: string) => {
+ processBIP21Uri = (input: string) => {
let value, amount, lightning, offer;
// handle addresses prefixed with 'bitcoin:' and
@@ -182,6 +186,8 @@ class AddressUtils {
return btcNonBech.test(input) || btcBech.test(input);
};
+ isValidBIP21Uri = (input: string) => bip21Uri.test(input);
+
isValidLightningPaymentRequest = (input: string) => lnInvoice.test(input);
isValidLightningOffer = (input: string) => lnOffer.test(input);
diff --git a/utils/handleAnything.test.ts b/utils/handleAnything.test.ts
index 756ffe995..848d2f362 100644
--- a/utils/handleAnything.test.ts
+++ b/utils/handleAnything.test.ts
@@ -1,5 +1,5 @@
jest.mock('./AddressUtils', () => ({
- processSendAddress: () => mockProcessedSendAddress,
+ processBIP21Uri: () => mockProcessedSendAddress,
isValidBitcoinAddress: () => mockIsValidBitcoinAddress,
isValidLightningPubKey: () => mockIsValidLightningPubKey,
isValidLightningPaymentRequest: () => mockIsValidLightningPaymentRequest
@@ -139,9 +139,13 @@ describe('handleAnything', () => {
const result = await handleAnything(data);
expect(result).toEqual([
- 'LnurlPay',
+ 'ChoosePaymentMethod',
{
- lnurlParams: mockGetLnurlParams
+ amount: undefined,
+ lightning:
+ 'LNURL1DP68GURN8GHJ7ARN9EJX2UN8D9NKJTNRDAKJ7SJ5GVH42J2VFE24YNP0WPSHJTMF9ATKWD622CE953JGWV6XXUMRDPXNVCJ8X4G9GF2CHDF',
+ offer: undefined,
+ value: 'BC1QUXCS7V556UTNUKU93HSZ7LHHFFLWN9NF2UTQ6N'
}
]);
});
@@ -167,20 +171,19 @@ describe('handleAnything', () => {
it('should return PaymentRequest screen and call getPayReq if not from clipboard', async () => {
const data =
'bitcoin:BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U?amount=0.00001&label=sbddesign%3A%20For%20lunch%20Tuesday&message=For%20lunch%20Tuesday&lightning=LNBC10U1P3PJ257PP5YZTKWJCZ5FTL5LAXKAV23ZMZEKAW37ZK6KMV80PK4XAEV5QHTZ7QDPDWD3XGER9WD5KWM36YPRX7U3QD36KUCMGYP282ETNV3SHJCQZPGXQYZ5VQSP5USYC4LK9CHSFP53KVCNVQ456GANH60D89REYKDNGSMTJ6YW3NHVQ9QYYSSQJCEWM5CJWZ4A6RFJX77C490YCED6PEMK0UPKXHY89CMM7SCT66K8GNEANWYKZGDRWRFJE69H9U5U0W57RRCSYSAS7GADWMZXC8C6T0SPJAZUP6';
- mockProcessedSendAddress = {
- value: 'BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U',
- lightning:
- 'LNBC10U1P3PJ257PP5YZTKWJCZ5FTL5LAXKAV23ZMZEKAW37ZK6KMV80PK4XAEV5QHTZ7QDPDWD3XGER9WD5KWM36YPRX7U3QD36KUCMGYP282ETNV3SHJCQZPGXQYZ5VQSP5USYC4LK9CHSFP53KVCNVQ456GANH60D89REYKDNGSMTJ6YW3NHVQ9QYYSSQJCEWM5CJWZ4A6RFJX77C490YCED6PEMK0UPKXHY89CMM7SCT66K8GNEANWYKZGDRWRFJE69H9U5U0W57RRCSYSAS7GADWMZXC8C6T0SPJAZUP6'
- };
- mockIsValidBitcoinAddress = true;
- mockSupportsOnchainSends = false;
const result = await handleAnything(data);
- expect(stores.invoicesStore.getPayReq).toHaveBeenCalledWith(
- 'LNBC10U1P3PJ257PP5YZTKWJCZ5FTL5LAXKAV23ZMZEKAW37ZK6KMV80PK4XAEV5QHTZ7QDPDWD3XGER9WD5KWM36YPRX7U3QD36KUCMGYP282ETNV3SHJCQZPGXQYZ5VQSP5USYC4LK9CHSFP53KVCNVQ456GANH60D89REYKDNGSMTJ6YW3NHVQ9QYYSSQJCEWM5CJWZ4A6RFJX77C490YCED6PEMK0UPKXHY89CMM7SCT66K8GNEANWYKZGDRWRFJE69H9U5U0W57RRCSYSAS7GADWMZXC8C6T0SPJAZUP6'
- );
- expect(result).toEqual(['PaymentRequest', {}]);
+ expect(result).toEqual([
+ 'ChoosePaymentMethod',
+ {
+ amount: undefined,
+ lightning:
+ 'LNURL1DP68GURN8GHJ7ARN9EJX2UN8D9NKJTNRDAKJ7SJ5GVH42J2VFE24YNP0WPSHJTMF9ATKWD622CE953JGWV6XXUMRDPXNVCJ8X4G9GF2CHDF',
+ offer: undefined,
+ value: 'BC1QUXCS7V556UTNUKU93HSZ7LHHFFLWN9NF2UTQ6N'
+ }
+ ]);
});
it('should return true if from clipboard', async () => {
diff --git a/utils/handleAnything.ts b/utils/handleAnything.ts
index ab8b8fdf5..df83fd989 100644
--- a/utils/handleAnything.ts
+++ b/utils/handleAnything.ts
@@ -97,9 +97,12 @@ const handleAnything = async (
): Promise => {
const { nodeInfo } = nodeInfoStore;
const { isTestNet, isRegTest, isSigNet } = nodeInfo;
- const { value, amount, lightning }: any =
- AddressUtils.processSendAddress(data);
+ const { value, amount, lightning, offer }: any =
+ AddressUtils.processBIP21Uri(data);
const hasAt: boolean = value.includes('@');
+ const hasMultiple: boolean =
+ (value && lightning) || (value && offer) || (lightning && offer);
+
let lnurl;
// if the value is from clipboard and looks like a url we don't want to decode it
if (!isClipboardValue || !data.match(/^https?:\/\//i)) {
@@ -108,7 +111,29 @@ const handleAnything = async (
} catch (e) {}
}
- if (
+ if (!hasAt && hasMultiple) {
+ if (isClipboardValue) return true;
+ return [
+ 'ChoosePaymentMethod',
+ {
+ value,
+ amount,
+ lightning,
+ offer
+ }
+ ];
+ } else if (offer && BackendUtils.supportsOffers()) {
+ if (isClipboardValue) return true;
+ return [
+ 'Send',
+ {
+ destination: offer,
+ bolt12: offer,
+ transactionType: 'BOLT 12',
+ isValid: true
+ }
+ ];
+ } else if (
!hasAt &&
AddressUtils.isValidBitcoinAddress(value, isTestNet || isRegTest) &&
lightning
@@ -341,66 +366,71 @@ const handleAnything = async (
if (isClipboardValue) return true;
// try BOLT 12 address first, if supported
- if (BackendUtils.supportsOffers()) {
- const [localPart, domain] = value.split('@');
- const dnsUrl = 'https://cloudflare-dns.com/dns-query';
+ const [localPart, domain] = value.split('@');
+ const dnsUrl = 'https://cloudflare-dns.com/dns-query';
- const name = `${localPart}.user._bitcoin-payment.${domain}`;
- const url = `${dnsUrl}?name=${name}&type=TXT`;
- let bolt12: string;
- try {
- const res = await fetch(url, {
- headers: {
- accept: 'application/dns-json'
- }
- });
- const json = await res.json();
- if (!json.Answer && !json.Answer[0]) throw 'Bad';
- bolt12 = json.Answer[0].data;
- bolt12 = bolt12.replace(/("|\\)/g, '');
- bolt12 = bolt12.replace(/bitcoin:b12=/, '');
+ const name = `${localPart}.user._bitcoin-payment.${domain}`;
+ let url = `${dnsUrl}?name=${name}&type=TXT`;
+ let bolt12: string;
+ try {
+ const res = await fetch(url, {
+ headers: {
+ accept: 'application/dns-json'
+ }
+ });
+ const json = await res.json();
+ if (!json.Answer && !json.Answer[0]) throw 'Bad';
+ bolt12 = json.Answer[0].data;
+ bolt12 = bolt12.replace(/("|\\)/g, '');
+ bolt12 = bolt12.replace(/bitcoin:b12=/, '');
- const { value, amount, lightning, offer }: any =
- AddressUtils.processSendAddress(bolt12);
+ const { value, amount, lightning, offer }: any =
+ AddressUtils.processBIP21Uri(bolt12);
- if (value) {
- return [
- 'Accounts',
- {
- value,
- amount,
- lightning,
- offer,
- locked: true
- }
- ];
- }
+ const hasMultiple: boolean =
+ (value && lightning) ||
+ (value && offer) ||
+ (lightning && offer);
+ if (hasMultiple) {
+ return [
+ 'ChoosePaymentMethod',
+ {
+ value,
+ amount,
+ lightning,
+ offer,
+ locked: true
+ }
+ ];
+ }
+
+ if (offer && BackendUtils.supportsOffers()) {
return [
'Send',
{
- destination: value || offer,
+ destination: offer,
bolt12,
transactionType: 'BOLT 12',
isValid: true
}
];
- } catch (e: any) {}
- }
+ }
+ } catch (e: any) {}
- const [username, domain] = value.split('@');
- let url;
- if (domain.includes('.onion')) {
- url = `http://${domain}/.well-known/lnurlp/${username.toLowerCase()}`;
+ // try BOLT 11 address
+ const [username, bolt11Domain] = value.split('@');
+ if (bolt11Domain.includes('.onion')) {
+ url = `http://${bolt11Domain}/.well-known/lnurlp/${username.toLowerCase()}`;
} else {
- url = `https://${domain}/.well-known/lnurlp/${username.toLowerCase()}`;
+ url = `https://${bolt11Domain}/.well-known/lnurlp/${username.toLowerCase()}`;
}
const error = localeString(
'utils.handleAnything.lightningAddressError'
);
// handle Tor LN addresses
- if (settingsStore.enableTor && domain.includes('.onion')) {
+ if (settingsStore.enableTor && bolt11Domain.includes('.onion')) {
await doTorRequest(url, RequestMethod.GET)
.then((response: any) => {
if (!response.callback) {
diff --git a/views/ChoosePaymentMethod.tsx b/views/ChoosePaymentMethod.tsx
new file mode 100644
index 000000000..078ea2107
--- /dev/null
+++ b/views/ChoosePaymentMethod.tsx
@@ -0,0 +1,103 @@
+import * as React from 'react';
+import { Route } from '@react-navigation/native';
+import { StackNavigationProp } from '@react-navigation/stack';
+
+import Button from '../components/Button';
+import Header from '../components/Header';
+import PaymentMethodList from '../components/LayerBalances/PaymentMethodList';
+import Screen from '../components/Screen';
+
+import { localeString } from '../utils/LocaleUtils';
+import { themeColor } from '../utils/ThemeUtils';
+
+interface ChoosePaymentMethodProps {
+ navigation: StackNavigationProp;
+ route: Route<
+ 'ChoosePaymentMethod',
+ {
+ value: string;
+ amount: string;
+ lightning: string;
+ offer: string;
+ }
+ >;
+}
+
+interface ChoosePaymentMethodState {
+ value: string;
+ amount: string;
+ lightning: string;
+ offer: string;
+}
+
+export default class ChoosePaymentMethod extends React.Component<
+ ChoosePaymentMethodProps,
+ ChoosePaymentMethodState
+> {
+ state = {
+ value: '',
+ amount: '',
+ lightning: '',
+ offer: ''
+ };
+
+ componentDidMount() {
+ const { route } = this.props;
+ const { value, amount, lightning, offer } = route.params ?? {};
+
+ if (value) {
+ this.setState({ value });
+ }
+
+ if (amount) {
+ this.setState({ amount });
+ }
+
+ if (lightning) {
+ this.setState({ lightning });
+ }
+
+ if (offer) {
+ this.setState({ offer });
+ }
+ }
+
+ render() {
+ const { navigation } = this.props;
+ const { value, amount, lightning, offer } = this.state;
+
+ return (
+
+
+
+ {!!value && !!lightning && (
+
+ );
+ }
+}