diff --git a/Navigation.ts b/Navigation.ts index 12a653501..9b7662f88 100644 --- a/Navigation.ts +++ b/Navigation.ts @@ -61,6 +61,7 @@ import ProductDetails from './views/POS/ProductDetails'; import PaymentsSettings from './views/Settings/PaymentsSettings'; import InvoicesSettings from './views/Settings/InvoicesSettings'; import LSP from './views/Settings/LSP'; +import Bolt12Address from './views/Settings/Bolt12Address'; // Lightning address import LightningAddress from './views/Settings/LightningAddress'; @@ -404,6 +405,9 @@ const AppScenes = { ContactDetails: { screen: ContactDetails }, + Bolt12Address: { + screen: Bolt12Address + }, NostrKeys: { screen: NostrKeys }, diff --git a/assets/images/SVG/AtSign.svg b/assets/images/SVG/AtSign.svg new file mode 100644 index 000000000..33fbb5cc6 --- /dev/null +++ b/assets/images/SVG/AtSign.svg @@ -0,0 +1,3 @@ + + + diff --git a/backends/CLightningREST.ts b/backends/CLightningREST.ts index 668d266d4..f9cf0360b 100644 --- a/backends/CLightningREST.ts +++ b/backends/CLightningREST.ts @@ -167,6 +167,18 @@ export default class CLightningREST extends LND { payments: data.pays })); getNewAddress = () => this.getRequest('/v1/newaddr?addrType=bech32'); + getNewOffer = () => + this.postRequest('/v1/offers/offer', { + amount: 'any', + description: 'Bolt12 Payment Address' + }); + fetchInvoiceFromOffer = async (bolt12: string, amountSatoshis: string) => { + return await this.postRequest('/v1/offers/fetchInvoice', { + offer: bolt12, + msatoshi: Number(amountSatoshis) * 1000, + timeout: 60 + }); + }; openChannel = (data: OpenChannelRequest) => { let request: any; if (data.utxos && data.utxos.length > 0) { @@ -195,7 +207,7 @@ export default class CLightningREST extends LND { id: `${data.addr.pubkey}@${data.addr.host}` }); decodePaymentRequest = (urlParams?: Array) => - this.getRequest(`/v1/pay/decodePay/${urlParams && urlParams[0]}`); + this.getRequest(`/v1/utility/decode/${urlParams && urlParams[0]}`); payLightningInvoice = (data: any) => this.postRequest('/v1/pay', { invoice: data.payment_request, @@ -268,4 +280,9 @@ export default class CLightningREST extends LND { supportsCustomPreimages = () => false; supportsSweep = () => true; isLNDBased = () => false; + supportsOffers = async () => { + const res = await this.getRequest('/v1/utility/listConfigs'); + const supportsOffers: boolean = res['experimental-offers'] || false; + return supportsOffers; + }; } diff --git a/backends/Eclair.ts b/backends/Eclair.ts index 83bf42903..908d456ca 100644 --- a/backends/Eclair.ts +++ b/backends/Eclair.ts @@ -505,6 +505,7 @@ export default class Eclair { supportsCustomPreimages = () => false; supportsSweep = () => false; isLNDBased = () => false; + supportsOffers = () => false; } const mapInvoice = diff --git a/backends/EmbeddedLND.ts b/backends/EmbeddedLND.ts index 5376792cd..356cca995 100644 --- a/backends/EmbeddedLND.ts +++ b/backends/EmbeddedLND.ts @@ -197,4 +197,5 @@ export default class EmbeddedLND extends LND { supportsCustomPreimages = () => true; supportsSweep = () => true; isLNDBased = () => true; + supportsOffers = () => false; } diff --git a/backends/LND.ts b/backends/LND.ts index a7011aeeb..c38209678 100644 --- a/backends/LND.ts +++ b/backends/LND.ts @@ -460,4 +460,5 @@ export default class LND { supportsCustomPreimages = () => true; supportsSweep = () => true; isLNDBased = () => true; + supportsOffers = async () => false; } diff --git a/backends/LightningNodeConnect.ts b/backends/LightningNodeConnect.ts index 6e89cf5a5..70f098d4f 100644 --- a/backends/LightningNodeConnect.ts +++ b/backends/LightningNodeConnect.ts @@ -394,4 +394,5 @@ export default class LightningNodeConnect { supportsCustomPreimages = () => true; supportsSweep = () => true; isLNDBased = () => true; + supportsOffers = () => false; } diff --git a/backends/LndHub.ts b/backends/LndHub.ts index dfe585935..a2965559e 100644 --- a/backends/LndHub.ts +++ b/backends/LndHub.ts @@ -153,4 +153,5 @@ export default class LndHub extends LND { supportsCustomPreimages = () => false; supportsSweep = () => false; isLNDBased = () => false; + supportsOffers = () => false; } diff --git a/backends/Spark.ts b/backends/Spark.ts index bf6f0fcad..4aac9e681 100644 --- a/backends/Spark.ts +++ b/backends/Spark.ts @@ -379,4 +379,5 @@ export default class Spark { supportsCustomPreimages = () => false; supportsSweep = () => false; isLNDBased = () => false; + supportsOffers = () => false; } diff --git a/ios/.xcode.env b/ios/.xcode.env index 3d5782c71..b28fab570 100644 --- a/ios/.xcode.env +++ b/ios/.xcode.env @@ -7,5 +7,5 @@ # # Customize the NODE_BINARY variable here. # For example, to use nvm with brew, add the following line -# . "$(brew --prefix nvm)/nvm.sh" --no-use +. "$(brew --prefix nvm)/nvm.sh" --no-use export NODE_BINARY=$(command -v node) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f71681283..174effe84 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -342,6 +342,8 @@ PODS: - React-Core - react-native-notifications (5.1.0): - React-Core + - react-native-qrcode-local-image (1.0.4): + - React - react-native-randombytes (3.5.3): - React - react-native-restart (0.0.27): @@ -465,8 +467,6 @@ PODS: - React-perflogger (= 0.72.5) - ReactNativeCameraKit (13.0.0): - React-Core - - RemobileReactNativeQrcodeLocalImage (1.0.4): - - React - RNCAsyncStorage (1.19.3): - React-Core - RNCClipboard (1.13.2): @@ -563,6 +563,7 @@ DEPENDENCIES: - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-nfc-manager (from `../node_modules/react-native-nfc-manager`) - react-native-notifications (from `../node_modules/react-native-notifications`) + - "react-native-qrcode-local-image (from `../node_modules/@remobile/react-native-qrcode-local-image`)" - react-native-randombytes (from `../node_modules/react-native-randombytes`) - react-native-restart (from `../node_modules/react-native-restart`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) @@ -586,7 +587,6 @@ DEPENDENCIES: - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - ReactNativeCameraKit (from `../node_modules/react-native-camera-kit`) - - "RemobileReactNativeQrcodeLocalImage (from `../node_modules/@remobile/react-native-qrcode-local-image`)" - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" - "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)" @@ -681,6 +681,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-nfc-manager" react-native-notifications: :path: "../node_modules/react-native-notifications" + react-native-qrcode-local-image: + :path: "../node_modules/@remobile/react-native-qrcode-local-image" react-native-randombytes: :path: "../node_modules/react-native-randombytes" react-native-restart: @@ -727,8 +729,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" ReactNativeCameraKit: :path: "../node_modules/react-native-camera-kit" - RemobileReactNativeQrcodeLocalImage: - :path: "../node_modules/@remobile/react-native-qrcode-local-image" RNCAsyncStorage: :path: "../node_modules/@react-native-async-storage/async-storage" RNCClipboard: @@ -798,6 +798,7 @@ SPEC CHECKSUMS: react-native-netinfo: fefd4e98d75cbdd6e85fc530f7111a8afdf2b0c5 react-native-nfc-manager: 250424ac5f6b2827f98bec7a1ed7f27615852ed4 react-native-notifications: 4601a5a8db4ced6ae7cfc43b44d35fe437ac50c4 + react-native-qrcode-local-image: 35ccb306e4265bc5545f813e54cc830b5d75bcfc react-native-randombytes: 3638d24759d67c68f6ccba60c52a7a8a8faa6a23 react-native-restart: 7595693413fe3ca15893702f2c8306c62a708162 react-native-safe-area-context: 52342d2d80ea8faadd0ffa76d83b6051f20c5329 @@ -821,7 +822,6 @@ SPEC CHECKSUMS: React-utils: 7a9918a1ffdd39aba67835d42386f592ea3f8e76 ReactCommon: 91ece8350ebb3dd2be9cef662abd78b6948233c0 ReactNativeCameraKit: 9d46a5d7dd544ca64aa9c03c150d2348faf437eb - RemobileReactNativeQrcodeLocalImage: 57aadc12896b148fb5e04bc7c6805f3565f5c3fa RNCAsyncStorage: c913ede1fa163a71cea118ed4670bbaaa4b511bb RNCClipboard: 60fed4b71560d7bfe40e9d35dea9762b024da86d RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489 @@ -842,4 +842,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 643f83a7955aa123651bac4a54204e22598914df -COCOAPODS: 1.12.1 +COCOAPODS: 1.14.3 diff --git a/ios/zeus.xcodeproj/project.pbxproj b/ios/zeus.xcodeproj/project.pbxproj index c4baaab02..054812895 100644 --- a/ios/zeus.xcodeproj/project.pbxproj +++ b/ios/zeus.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ @@ -1060,7 +1060,6 @@ }; 13B07F861A680F5B00A75B9A = { LastSwiftMigration = 1210; - ProvisioningStyle = Automatic; }; 2D02E47A1E0B4A5D006451C7 = { CreatedOnToolsVersion = 8.2.1; @@ -1772,7 +1771,6 @@ OTHER_LDFLAGS = ( "-ObjC", "-lc++", - "-ld_classic", ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1804,7 +1802,6 @@ OTHER_LDFLAGS = ( "-ObjC", "-lc++", - "-ld_classic", ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1824,7 +1821,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 22; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = 9TU7M3555F; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; EXCLUDED_ARCHS = ""; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; @@ -1873,7 +1870,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 22; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = 9TU7M3555F; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; GCC_NO_COMMON_BLOCKS = NO; @@ -2114,33 +2111,7 @@ OTHER_CPLUSPLUSFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( "$(inherited)", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl -ld_classic ", + " ", ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; @@ -2198,33 +2169,7 @@ OTHER_CPLUSPLUSFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( "$(inherited)", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl", - "-ld_classic", - "-Wl -ld_classic ", + " ", ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; diff --git a/ios/zeus/zeus.entitlements b/ios/zeus/zeus.entitlements index 8536ab733..0c67376eb 100644 --- a/ios/zeus/zeus.entitlements +++ b/ios/zeus/zeus.entitlements @@ -1,13 +1,5 @@ - - aps-environment - development - com.apple.developer.nfc.readersession.formats - - NDEF - TAG - - + diff --git a/ios/zeus/zeusRelease.entitlements b/ios/zeus/zeusRelease.entitlements index 8536ab733..0c67376eb 100644 --- a/ios/zeus/zeusRelease.entitlements +++ b/ios/zeus/zeusRelease.entitlements @@ -1,13 +1,5 @@ - - aps-environment - development - com.apple.developer.nfc.readersession.formats - - NDEF - TAG - - + diff --git a/locales/en.json b/locales/en.json index 72089f6b0..0f166d104 100644 --- a/locales/en.json +++ b/locales/en.json @@ -568,6 +568,7 @@ "views.Receive.createLightningAddress": "Create lightning address", "views.Send.title": "Send", "views.Send.lnPayment": "Lightning payment request", + "views.Send.bolt12Address": "Bolt 12 address", "views.Send.btcAddress": "Bitcoin address", "views.Send.keysendAddress": "keysend address (if enabled)", "views.Send.mustBeValid": "Must be a valid", @@ -839,6 +840,10 @@ "views.Settings.Contacts.noAddress": "No Address", "views.Settings.Contacts.to": "To", "views.Settings.Contacts.deleteAllContacts": "Delete all contacts", + "views.Settings.Bolt12Address.bolt12Address": "Bolt12 Address", + "views.Settings.Bolt12Address.requestButton": "Request Paycode", + "views.Settings.Bolt12Address.changeButton": "Change Paycode", + "views.Settings.Bolt12Address.handle": "Your handle", "views.Transaction.title": "Transaction", "views.Transaction.totalFees": "Total Fees", "views.Transaction.transactionHash": "Transaction Hash", diff --git a/models/Contact.ts b/models/Contact.ts index e86332ddb..406190e79 100644 --- a/models/Contact.ts +++ b/models/Contact.ts @@ -6,6 +6,7 @@ export default class Contact extends BaseModel { id: string; // deprecated public contactId: string; public lnAddress: Array; + public bolt12Address: Array; public onchainAddress: Array; public pubkey: Array; public nip05: Array; @@ -25,6 +26,18 @@ export default class Contact extends BaseModel { this.lnAddress && this.lnAddress.length === 1 && this.lnAddress[0] !== '' && + (!this.bolt12Address[0] || this.bolt12Address[0] === '') && + (!this.onchainAddress[0] || this.onchainAddress[0] === '') && + (!this.pubkey[0] || this.pubkey[0] === '') + ); + } + + @computed public get isSingleBolt12Address(): boolean { + return ( + this.bolt12Address && + this.bolt12Address.length === 1 && + this.bolt12Address[0] !== '' && + (!this.lnAddress[0] || this.lnAddress[0] === '') && (!this.onchainAddress[0] || this.onchainAddress[0] === '') && (!this.pubkey[0] || this.pubkey[0] === '') ); @@ -36,6 +49,7 @@ export default class Contact extends BaseModel { this.onchainAddress.length === 1 && this.onchainAddress[0] !== '' && (!this.lnAddress[0] || this.lnAddress[0] === '') && + (!this.bolt12Address[0] || this.bolt12Address[0] === '') && (!this.pubkey[0] || this.pubkey[0] === '') ); } @@ -46,6 +60,7 @@ export default class Contact extends BaseModel { this.pubkey.length === 1 && this.pubkey[0] !== '' && (!this.lnAddress[0] || this.lnAddress[0] === '') && + (!this.bolt12Address[0] || this.bolt12Address[0] === '') && (!this.onchainAddress[0] || this.onchainAddress[0] === '') ); } @@ -54,6 +69,10 @@ export default class Contact extends BaseModel { return this.lnAddress?.length > 0 && this.lnAddress[0] !== ''; } + @computed public get hasBolt12Address(): boolean { + return this.bolt12Address?.length > 0 && this.bolt12Address[0] !== ''; + } + @computed public get hasOnchainAddress(): boolean { return this.onchainAddress?.length > 0 && this.onchainAddress[0] !== ''; } @@ -67,6 +86,9 @@ export default class Contact extends BaseModel { this.lnAddress.forEach((address) => { if (address && address !== '') count++; }); + this.bolt12Address.forEach((address) => { + if (address && address !== '') count++; + }); this.onchainAddress.forEach((address) => { if (address && address !== '') count++; }); diff --git a/models/Invoice.ts b/models/Invoice.ts index afd449a4d..452e113ec 100644 --- a/models/Invoice.ts +++ b/models/Invoice.ts @@ -172,6 +172,10 @@ export default class Invoice extends BaseModel { const msatoshi = this.millisatoshis; return Number(msatoshi) / 1000; } + if (this.invoice_amount_msat) { + const msatoshi = this.invoice_amount_msat; + return Number(msatoshi) / 1000; + } return Number(this.num_satoshis || 0); } diff --git a/stores/SettingsStore.ts b/stores/SettingsStore.ts index a29d7c63b..0f80311cf 100644 --- a/stores/SettingsStore.ts +++ b/stores/SettingsStore.ts @@ -90,6 +90,10 @@ interface LightningAddressSettings { notifications: number; } +interface Bolt12AddressSettings { + localPart: string; +} + export interface Settings { nodes?: Array; selectedNode?: number; @@ -133,6 +137,7 @@ export interface Settings { requestSimpleTaproot: boolean; // Lightning Address lightningAddress: LightningAddressSettings; + bolt12Address: Bolt12AddressSettings; } export const FIAT_RATES_SOURCE_KEYS = [ @@ -982,6 +987,9 @@ export default class SettingsStore { nostrPrivateKey: '', nostrRelays: DEFAULT_NOSTR_RELAYS, notifications: 0 + }, + bolt12Address: { + localPart: '' } }; @observable public posStatus: string = 'unselected'; diff --git a/utils/AddressUtils.ts b/utils/AddressUtils.ts index 1c0cabadb..34bdd05b3 100644 --- a/utils/AddressUtils.ts +++ b/utils/AddressUtils.ts @@ -146,6 +146,7 @@ class AddressUtils { lndHubAddress.test(input) || blueWalletAddress.test(input); isValidLightningAddress = (input: string) => lightningAddress.test(input); + isValidBolt12Address = (input: string) => lightningAddress.test(input); isValidNpub = (input: string) => npubFormat.test(input); } diff --git a/utils/BackendUtils.ts b/utils/BackendUtils.ts index fc310aacb..ebb23003e 100644 --- a/utils/BackendUtils.ts +++ b/utils/BackendUtils.ts @@ -143,6 +143,10 @@ class BackendUtils { supportsCustomPreimages = () => this.call('supportsCustomPreimages'); supportsSweep = () => this.call('supportsSweep'); isLNDBased = () => this.call('isLNDBased'); + supportsOffers = () => this.call('supportsOffers'); + getNewOffer = () => this.call('getNewOffer'); + fetchInvoiceFromOffer = (...args: any[]) => + this.call('fetchInvoiceFromOffer', args); // LNC initLNC = (...args: any[]) => this.call('initLNC', args); diff --git a/utils/handleAnything.ts b/utils/handleAnything.ts index 95598bafb..2262f5b72 100644 --- a/utils/handleAnything.ts +++ b/utils/handleAnything.ts @@ -192,6 +192,37 @@ const handleAnything = async ( ]; } else if (hasAt && AddressUtils.isValidLightningAddress(value)) { if (isClipboardValue) return true; + + if (BackendUtils.supportsOffers()) { + 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=/, ''); + + return [ + 'Send', + { + destination: value, + bolt12, + transactionType: 'Bolt12', + isValid: true + } + ]; + } catch (e: any) {} + } + const [username, domain] = value.split('@'); const url = `https://${domain}/.well-known/lnurlp/${username.toLowerCase()}`; const error = localeString( diff --git a/views/ContactDetails.tsx b/views/ContactDetails.tsx index 7f937a88d..e2ef30341 100644 --- a/views/ContactDetails.tsx +++ b/views/ContactDetails.tsx @@ -47,6 +47,7 @@ export default class ContactDetails extends React.Component< this.state = { contact: { lnAddress: [''], + bolt12Address: [''], onchainAddress: [''], pubkey: [''], nip05: [''], @@ -418,6 +419,43 @@ export default class ContactDetails extends React.Component< )} + {contact.hasBolt12Address && ( + + {contact.bolt12Address.map( + (address: string, index: number) => ( + + this.sendAddress(address) + } + > + + + + {address.length > 23 + ? `${address.substring( + 0, + 10 + )}...${address.substring( + address.length - + 10 + )}` + : address} + + + + ) + )} + + )} + {contact.hasPubkey && ( {contact.pubkey.map( diff --git a/views/PaymentRequest.tsx b/views/PaymentRequest.tsx index d90ebc665..98301a72d 100644 --- a/views/PaymentRequest.tsx +++ b/views/PaymentRequest.tsx @@ -62,6 +62,7 @@ interface InvoiceProps { NodeInfoStore: NodeInfoStore; LnurlPayStore: LnurlPayStore; SettingsStore: SettingsStore; + bolt12: string; } interface InvoiceState { diff --git a/views/Send.tsx b/views/Send.tsx index abff13627..d0ae9aa7e 100644 --- a/views/Send.tsx +++ b/views/Send.tsx @@ -75,6 +75,7 @@ interface SendProps { interface SendState { isValid: boolean; transactionType: string | null; + bolt12: string | null; destination: string; amount: string; satAmount: string | number; @@ -114,7 +115,7 @@ export default class Send extends React.Component { const transactionType = navigation.getParam('transactionType', null); const isValid = navigation.getParam('isValid', false); const contactName = navigation.getParam('contactName', null); - + const bolt12 = navigation.getParam('bolt12', null); if (transactionType === 'Lightning') { this.props.InvoicesStore.getPayReq(destination); } @@ -122,6 +123,7 @@ export default class Send extends React.Component { this.state = { isValid: isValid || false, transactionType, + bolt12, destination: destination || '', amount: amount || '', satAmount: '', @@ -163,6 +165,7 @@ export default class Send extends React.Component { const destination = navigation.getParam('destination', null); const amount = navigation.getParam('amount', null); const transactionType = navigation.getParam('transactionType', null); + const bolt12 = navigation.getParam('bolt12', null); const contactName = navigation.getParam('contactName', null); if (transactionType === 'Lightning') { @@ -171,6 +174,7 @@ export default class Send extends React.Component { this.setState({ transactionType, + bolt12, destination, isValid: true, contactName @@ -340,6 +344,26 @@ export default class Send extends React.Component { }); }; + payBolt12 = async () => { + if (this.state.amount === '0' || !this.state.bolt12) { + return; + } + try { + const res = await BackendUtils.fetchInvoiceFromOffer( + this.state.bolt12, + this.state.amount + ); + if (!res.invoice) { + return; + } + this.props.InvoicesStore.getPayReq(res.invoice); + this.props.navigation.navigate('PaymentRequest'); + } catch (e) { + console.error(e); + return; + } + }; + sendCoins = (satAmount: string | number) => { const { TransactionsStore, navigation } = this.props; const { destination, fee, utxos, confirmationTarget } = this.state; @@ -415,6 +439,7 @@ export default class Send extends React.Component { const contact = new Contact(item); const { hasLnAddress, + hasBolt12Address, hasOnchainAddress, hasPubkey, hasMultiplePayableAddresses @@ -433,6 +458,15 @@ export default class Send extends React.Component { : item.lnAddress[0]; } + if (hasBolt12Address) { + return item.bolt12Address[0].length > 23 + ? `${item.bolt12Address[0].slice( + 0, + 10 + )}...${item.bolt12Address[0].slice(-10)}` + : item.bolt12Address[0]; + } + if (hasOnchainAddress) { return item.onchainAddress[0].length > 23 ? `${item.onchainAddress[0].slice( @@ -459,6 +493,8 @@ export default class Send extends React.Component { onPress={() => { if (contact.isSingleLnAddress) { this.validateAddress(item.lnAddress[0]); + } else if (contact.isSingleBolt12Address) { + this.validateAddress(item.bolt12Address[0]); } else if (contact.isSingleOnchainAddress) { this.validateAddress(item.onchainAddress[0]); } else if (contact.isSinglePubkey) { @@ -542,6 +578,9 @@ export default class Send extends React.Component { const paymentOptions = [localeString('views.Send.lnPayment')]; + if (BackendUtils.supportsOffers()) { + paymentOptions.push(localeString('views.Send.bolt12Address')); + } if (BackendUtils.supportsOnchainSends()) { paymentOptions.push(localeString('views.Send.btcAddress')); } @@ -645,7 +684,8 @@ export default class Send extends React.Component { )} {!!destination && (transactionType === 'Lightning' || - transactionType === 'Keysend') && + transactionType === 'Keysend' || + transactionType === 'Bolt12') && lightningBalance === 0 && ( { )} + {transactionType === 'Bolt12' && + BackendUtils.supportsOffers() && ( + + { + this.setState({ + amount, + satAmount + }); + }} + /> + + )} {transactionType === 'Keysend' && BackendUtils.supportsKeysend() && ( @@ -1083,6 +1141,15 @@ export default class Send extends React.Component { )} + {destination && transactionType === 'Bolt12' && ( + +