diff --git a/locales/en.json b/locales/en.json index cdce1ea7f..937532424 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1151,6 +1151,7 @@ "views.PayCode.internalLabel": "Internal label", "views.PayCodes.noPayCodes": "No pay codes created yet", "views.Settings.Bolt12Address": "BOLT 12 address", + "views.Settings.Bolt12Offer": "BOLT 12 offer", "views.Settings.Bolt12Address.requestButton": "Request Paycode", "views.Settings.Bolt12Address.changeButton": "Change Paycode", "views.Settings.Bolt12Address.handle": "Your handle", @@ -1230,4 +1231,4 @@ "views.PendingHTLCs.expirationHeight": "Expiration height", "views.PendingHTLCs.recommendationIOS": "It's recommended to leave ZEUS running while there are pending HTLCs to prevent force closes.", "views.PendingHTLCs.recommendationAndroid": "It's recommended to enable Persistent LND or leave ZEUS running while there are pending HTLCs to prevent force closes." -} +} \ No newline at end of file diff --git a/models/Contact.ts b/models/Contact.ts index 87178916b..2df89c41b 100644 --- a/models/Contact.ts +++ b/models/Contact.ts @@ -9,6 +9,7 @@ export default class Contact extends BaseModel { public contactId: string; public lnAddress: Array; public bolt12Address: Array; + public bolt12Offer: Array; public onchainAddress: Array; public pubkey: Array; public nip05: Array; @@ -32,7 +33,10 @@ export default class Contact extends BaseModel { !this.bolt12Address[0] || this.bolt12Address[0] === '') && (!this.onchainAddress[0] || this.onchainAddress[0] === '') && - (!this.pubkey[0] || this.pubkey[0] === '') + (!this.pubkey[0] || this.pubkey[0] === '') && + (!this.bolt12Offer || + !this.bolt12Offer[0] || + this.bolt12Offer[0] === '') ); } @@ -43,7 +47,24 @@ export default class Contact extends BaseModel { this.bolt12Address[0] !== '' && (!this.lnAddress[0] || this.lnAddress[0] === '') && (!this.onchainAddress[0] || this.onchainAddress[0] === '') && - (!this.pubkey[0] || this.pubkey[0] === '') + (!this.pubkey[0] || this.pubkey[0] === '') && + (!this.bolt12Offer || + !this.bolt12Offer[0] || + this.bolt12Offer[0] === '') + ); + } + + @computed public get isSingleBolt12Offer(): boolean { + return ( + this.bolt12Offer && + this.bolt12Offer.length === 1 && + this.bolt12Offer[0] !== '' && + (!this.lnAddress[0] || this.lnAddress[0] === '') && + (!this.onchainAddress[0] || this.onchainAddress[0] === '') && + (!this.pubkey[0] || this.pubkey[0] === '') && + (!this.bolt12Address || + !this.bolt12Address[0] || + this.bolt12Address[0] === '') ); } @@ -56,7 +77,10 @@ export default class Contact extends BaseModel { (!this.bolt12Address || !this.bolt12Address[0] || this.bolt12Address[0] === '') && - (!this.pubkey[0] || this.pubkey[0] === '') + (!this.pubkey[0] || this.pubkey[0] === '') && + (!this.bolt12Offer || + !this.bolt12Offer[0] || + this.bolt12Offer[0] === '') ); } @@ -69,7 +93,10 @@ export default class Contact extends BaseModel { (!this.bolt12Address || !this.bolt12Address[0] || this.bolt12Address[0] === '') && - (!this.onchainAddress[0] || this.onchainAddress[0] === '') + (!this.onchainAddress[0] || this.onchainAddress[0] === '') && + (!this.bolt12Offer || + !this.bolt12Offer[0] || + this.bolt12Offer[0] === '') ); } @@ -81,6 +108,10 @@ export default class Contact extends BaseModel { return this.bolt12Address?.length > 0 && this.bolt12Address[0] !== ''; } + @computed public get hasBolt12Offer(): boolean { + return this.bolt12Offer?.length > 0 && this.bolt12Offer[0] !== ''; + } + @computed public get hasOnchainAddress(): boolean { return this.onchainAddress?.length > 0 && this.onchainAddress[0] !== ''; } @@ -97,6 +128,9 @@ export default class Contact extends BaseModel { this.bolt12Address?.forEach((address) => { if (address && address !== '') count++; }); + this.bolt12Offer?.forEach((address) => { + if (address && address !== '') count++; + }); this.onchainAddress.forEach((address) => { if (address && address !== '') count++; }); diff --git a/views/ContactDetails.tsx b/views/ContactDetails.tsx index dd16c243d..bcc93ff80 100644 --- a/views/ContactDetails.tsx +++ b/views/ContactDetails.tsx @@ -59,6 +59,7 @@ export default class ContactDetails extends React.Component< contact: { lnAddress: [''], bolt12Address: [''], + bolt12Offer: [''], onchainAddress: [''], pubkey: [''], nip05: [''], @@ -463,6 +464,43 @@ export default class ContactDetails extends React.Component< )} + {contact.hasBolt12Offer && ( + + {contact.bolt12Offer.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/Send.tsx b/views/Send.tsx index d8156a00c..d53f0992a 100644 --- a/views/Send.tsx +++ b/views/Send.tsx @@ -530,6 +530,7 @@ export default class Send extends React.Component { const { hasLnAddress, hasBolt12Address, + hasBolt12Offer, hasOnchainAddress, hasPubkey, hasMultiplePayableAddresses @@ -557,6 +558,15 @@ export default class Send extends React.Component { : item.bolt12Address[0]; } + if (hasBolt12Offer) { + return item.bolt12Offer[0].length > 23 + ? `${item.bolt12Offer[0].slice( + 0, + 10 + )}...${item.bolt12Offer[0].slice(-10)}` + : item.bolt12Offer[0]; + } + if (hasOnchainAddress) { return item.onchainAddress[0].length > 23 ? `${item.onchainAddress[0].slice( @@ -585,6 +595,8 @@ export default class Send extends React.Component { this.validateAddress(item.lnAddress[0]); } else if (contact.isSingleBolt12Address) { this.validateAddress(item.bolt12Address[0]); + } else if (contact.isSingleBolt12Offer) { + this.validateAddress(item.bolt12Offer[0]); } else if (contact.isSingleOnchainAddress) { this.validateAddress(item.onchainAddress[0]); } else if (contact.isSinglePubkey) { @@ -687,8 +699,12 @@ export default class Send extends React.Component { const paymentOptions = [localeString('views.Send.lnPayment')]; if (BackendUtils.supportsOffers()) { - paymentOptions.push(localeString('views.Settings.Bolt12Address')); + paymentOptions.push( + localeString('views.Settings.Bolt12Address'), + localeString('views.Settings.Bolt12Offer') + ); } + if (BackendUtils.supportsOnchainSends()) { paymentOptions.push(localeString('views.Send.btcAddress')); } diff --git a/views/Settings/AddContact.tsx b/views/Settings/AddContact.tsx index 6d62b7abd..97c0b2809 100644 --- a/views/Settings/AddContact.tsx +++ b/views/Settings/AddContact.tsx @@ -48,6 +48,7 @@ interface AddContactProps { interface Contact { lnAddress: string[]; bolt12Address: string[]; + bolt12Offer: string[]; onchainAddress: string[]; nip05: string[]; nostrNpub: string[]; @@ -63,6 +64,7 @@ interface AddContactState { contacts: Contact[]; lnAddress: string[]; bolt12Address: string[]; + bolt12Offer: string[]; onchainAddress: string[]; nip05: string[]; nostrNpub: string[]; @@ -76,6 +78,7 @@ interface AddContactState { isValidOnchainAddress: boolean; isValidLightningAddress: boolean; isValidBolt12Address: boolean; + isValidBolt12Offer: boolean; isValidNIP05: boolean; isValidNpub: boolean; isValidPubkey: boolean; @@ -91,6 +94,7 @@ export default class AddContact extends React.Component< contacts: [], lnAddress: [''], bolt12Address: [''], + bolt12Offer: [''], onchainAddress: [''], nip05: [''], nostrNpub: [''], @@ -104,6 +108,7 @@ export default class AddContact extends React.Component< isValidOnchainAddress: true, isValidLightningAddress: true, isValidBolt12Address: true, + isValidBolt12Offer: true, isValidNIP05: true, isValidNpub: true, isValidPubkey: true @@ -127,6 +132,7 @@ export default class AddContact extends React.Component< const { lnAddress, bolt12Address, + bolt12Offer, onchainAddress, nip05, nostrNpub, @@ -156,6 +162,7 @@ export default class AddContact extends React.Component< ...contact, lnAddress, bolt12Address, + bolt12Offer, onchainAddress, nip05, nostrNpub, @@ -187,6 +194,7 @@ export default class AddContact extends React.Component< contactId, lnAddress, bolt12Address, + bolt12Offer, onchainAddress, nip05, nostrNpub, @@ -215,6 +223,7 @@ export default class AddContact extends React.Component< contacts: updatedContacts, lnAddress: [], bolt12Address: [], + bolt12Offer: [], onchainAddress: [], nip05: [], nostrNpub: [], @@ -325,6 +334,13 @@ export default class AddContact extends React.Component< }); }; + onChangeBolt12Offer = (text: string) => { + const isValid = AddressUtils.isValidLightningOffer(text); + this.setState({ + isValidBolt12Offer: isValid + }); + }; + onChangeNIP05 = (text: string) => { const isValid = AddressUtils.isValidLightningAddress(text); this.setState({ @@ -373,6 +389,7 @@ export default class AddContact extends React.Component< this.setState({ lnAddress: prefillContact.lnAddress, bolt12Address: prefillContact.bolt12Address, + bolt12Offer: prefillContact.bolt12Offer, onchainAddress: prefillContact.onchainAddress, nip05: prefillContact.nip05, nostrNpub: prefillContact.nostrNpub, @@ -390,6 +407,7 @@ export default class AddContact extends React.Component< const { lnAddress, bolt12Address, + bolt12Offer, onchainAddress, nip05, nostrNpub, @@ -400,6 +418,7 @@ export default class AddContact extends React.Component< isValidOnchainAddress, isValidLightningAddress, isValidBolt12Address, + isValidBolt12Offer, isValidNIP05, isValidNpub, isValidPubkey @@ -416,6 +435,11 @@ export default class AddContact extends React.Component< translateKey: 'views.Settings.Bolt12Address', value: 'bolt12Address' }, + { + key: 'BOLT 12 offer', + translateKey: 'views.Settings.Bolt12Offer', + value: 'bolt12Offer' + }, { key: 'Pubkey', translateKey: 'views.NodeInfo.pubkey', @@ -874,12 +898,126 @@ export default class AddContact extends React.Component< ))} + + + + + + + { + this.onChangeBolt12Offer(text); + const updatedAddresses = bolt12Offer + ? [...bolt12Offer] + : []; + updatedAddresses[0] = text; + this.setState({ + bolt12Offer: updatedAddresses + }); + if (!text) { + this.setState({ + isValidBolt12Offer: true + }); + } + }} + value={bolt12Offer && bolt12Offer[0]} + placeholder={localeString( + 'views.Settings.Bolt12Offer' + )} + placeholderTextColor={themeColor( + 'secondaryText' + )} + style={{ + ...styles.textInput, + color: themeColor('text') + }} + autoCapitalize="none" + /> + + {bolt12Offer?.slice(1).map((address, index) => ( + <> + + + + + + + { + this.onChangeBolt12Offer(text); + const updatedAddresses = [ + ...bolt12Offer + ]; + updatedAddresses[index + 1] = + text; + this.setState({ + bolt12Offer: + updatedAddresses + }); + if (!text) { + this.setState({ + isValidBolt12Offer: true + }); + } + }} + value={address} + placeholder={localeString( + 'views.Settings.Bolt12Offer' + )} + placeholderTextColor={themeColor( + 'secondaryText' + )} + style={{ + ...styles.textInput, + color: themeColor('text') + }} + autoCapitalize="none" + /> + + {isValidBolt12Offer && ( + + + this.removeExtraField( + 'bolt12Offer', + index + ) + } + color={themeColor('text')} + underlayColor="transparent" + size={16} + /> + + )} + + + ))} @@ -1374,6 +1512,7 @@ export default class AddContact extends React.Component< isValidOnchainAddress && isValidLightningAddress && isValidBolt12Address && + isValidBolt12Offer && isValidNIP05 && isValidNpub && isValidPubkey @@ -1384,6 +1523,7 @@ export default class AddContact extends React.Component< !isValidOnchainAddress || !isValidLightningAddress || !isValidBolt12Address || + !isValidBolt12Offer || !isValidNIP05 || !isValidNpub || !isValidPubkey || @@ -1392,6 +1532,9 @@ export default class AddContact extends React.Component< (bolt12Address?.length > 1 && bolt12Address[bolt12Address.length - 1] === '') || + (bolt12Offer?.length > 1 && + bolt12Offer[bolt12Offer.length - 1] === + '') || (pubkey?.length > 1 && pubkey[pubkey.length - 1] === '') || (onchainAddress?.length > 1 && @@ -1405,6 +1548,7 @@ export default class AddContact extends React.Component< !( lnAddress[0] || bolt12Address[0] || + bolt12Offer[0] || onchainAddress[0] || pubkey[0] ) diff --git a/views/Settings/Contacts.tsx b/views/Settings/Contacts.tsx index 9e02ef50b..55bbf915c 100644 --- a/views/Settings/Contacts.tsx +++ b/views/Settings/Contacts.tsx @@ -81,6 +81,7 @@ export default class Contacts extends React.Component< const { hasLnAddress, hasBolt12Address, + hasBolt12Offer, hasOnchainAddress, hasPubkey, hasMultiplePayableAddresses @@ -108,6 +109,15 @@ export default class Contacts extends React.Component< : item.bolt12Address[0]; } + if (hasBolt12Offer) { + return item.bolt12Offer[0].length > 23 + ? `${item.bolt12Offer[0].slice( + 0, + 10 + )}...${item.bolt12Offer[0].slice(-10)}` + : item.bolt12Offer[0]; + } + if (hasOnchainAddress) { return item.onchainAddress[0].length > 23 ? `${item.onchainAddress[0].slice( @@ -143,6 +153,12 @@ export default class Contacts extends React.Component< destination: item.bolt12Address[0], contactName: item.name })) || + (contact.isSingleBolt12Offer && + this.state.SendScreen && + this.props.navigation.navigate('Send', { + destination: item.bolt12Offer[0], + contactName: item.name + })) || (contact.isSingleOnchainAddress && this.state.SendScreen && this.props.navigation.navigate('Send', { @@ -228,6 +244,7 @@ export default class Contacts extends React.Component< hasMatch('description') || hasMatch('lnAddress') || hasMatch('bolt12Address') || + hasMatch('bolt12Offer') || hasMatch('nip05') || hasMatch('onchainAddress') || hasMatch('nostrNpub') ||