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' && (
+
+
+ )}
+
{!!clipboard && !destination && (