Skip to content

Commit

Permalink
Update UI to handle encoded LNURL (#324)
Browse files Browse the repository at this point in the history
* add lnurl.ts & lint

* show lnurl address in amount selection screen

* update UI & rename functions

* fix require cycle. replace onion regex with string.endsWith()

* fix ci

* remove node 16.x from CI workflow

* Add support for Node.js 21.x

* fix going back from amount selection screen

* update translations
  • Loading branch information
KKA11010 committed Mar 27, 2024
1 parent 027dff3 commit 1c8e74b
Show file tree
Hide file tree
Showing 21 changed files with 358 additions and 204 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:

strategy:
matrix:
node-version: [16.x, 18.x, 20.x]
node-version: [18.x, 20.x, 21.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

steps:
Expand Down
2 changes: 1 addition & 1 deletion assets/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@
"invoiceHint": "Das kann einige Sekunden dauern...",
"lowBal": "Kein Guthaben verfügbar",
"meltAddressbookHint": "Wählen Sie Ihre eigene LNURL oder einen anderen Kontakt als Zahlungsempfänger aus.",
"meltInputHint": "Erstellen Sie eine Lightning-Rechnung oder geben Sie eine LNURL ein.",
"meltInputHint": "Fügen Sie eine Lightning-Rechnung, LNURL oder Lightning Adresse ein.",
"meltScanQRHint": "Erstellen Sie eine Lightning-Rechnung mit einem anderen Gerät und scannen Sie sie einfach.",
"meltSwapHint": "Wählen Sie eine andere Mint aus Ihrer vertrauenswürdigen Liste als Zahlungsempfänger aus.",
"copyShareToken": "Kopieren & teilen",
Expand Down
2 changes: 1 addition & 1 deletion assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@
"invoiceHint": "This can take a few seconds...",
"lowBal": "Mint balance too low!",
"meltAddressbookHint": "Choose your own LNURL or any other contact as a payment receiver.",
"meltInputHint": "Create a Lightning invoice or paste a LNURL into an input field.",
"meltInputHint": "Paste Lightning invoice, LN-URL or Lightning address.",
"meltScanQRHint": "Create a Lightning invoice with another device and simply scan it.",
"meltSwapHint": "Pick another mint from your trusted list as the payment receiver.",
"copyShareToken": "Copy & quickshare",
Expand Down
2 changes: 1 addition & 1 deletion assets/translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@
"invoiceHint": "Esto puede llevar unos segundos...",
"lowBal": "¡Saldo de la ceca muy bajo!",
"meltAddressbookHint": "Elige tu propia LNURL o cualquier otro contacto como receptor del pago.",
"meltInputHint": "Crea un recibo Lightning o pega una LNURL en el campo de entrada.",
"meltInputHint": "Pega la factura de Lightning, la LN-URL o la dirección de Lightning.",
"meltScanQRHint": "Crea un recibo Lightning con otro dispositivo y simplemente escanéalo.",
"meltSwapHint": "Elige otra ceca de tu lista de confianza como receptora del pago.",
"copyShareToken": "Copiar y compartir rápido",
Expand Down
2 changes: 1 addition & 1 deletion assets/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@
"invoiceHint": "Cela peut prendre quelques secondes...",
"lowBal": "Solde trop bas!",
"meltAddressbookHint": "Sélectionnez votre propre LNURL ou tout autre contact comme destinataire du paiement.",
"meltInputHint": "Créez une facture Lightning ou insérez une LNURL valide.",
"meltInputHint": "Collez une facture Lightning, LN-URL ou l'adresse Lightning.",
"meltScanQRHint": "Créez une facture Lightning avec un autre appareil et scannez-la simplement.",
"meltSwapHint": "Sélectionnez une autre mint de votre liste de confiance comme destinataire du paiement.",
"copyShareToken": "Copier et partager",
Expand Down
2 changes: 1 addition & 1 deletion assets/translations/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@
"invoiceHint": "Ez eltarthat pár másodpercig...",
"lowBal": "Verde egyenlege túl alacsony!",
"meltAddressbookHint": "Válaszd ki a saját LNURL-ed vagy bármelyik névjegyed a fizetés fogadójának.",
"meltInputHint": "Hozz létre egy Lightning számlát vagy illessz be egy LNURL-t a bemeneti mezőbe.",
"meltInputHint": "Illessze be a Lightning számlát, az LN-URL vagy a Lightning címet.",
"meltScanQRHint": "Hozz létre egy Lightning számlát egy másik eszközön és csak olvasd be.",
"meltSwapHint": "Válassz egy másik verdét a listádról a fizetés fogadójának.",
"copyShareToken": "Másol & megosztás",
Expand Down
2 changes: 1 addition & 1 deletion assets/translations/sw.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@
"invoiceHint": "Hii inaweza kuchukua sekunde kadhaa...",
"lowBal": "Salio la sarafu limepungua sana!",
"meltAddressbookHint": "Chagua LNURL yako mwenyewe au mawasiliano mengine yoyote kama mpokeaji wa malipo.",
"meltInputHint": "Unda ankara ya Lightning au weka LNURL kwenye uga wa kuingiza.",
"meltInputHint": "Weka ankara ya Lightning, LN-URL au anwani ya Lightning.",
"meltScanQRHint": "Unda ankara ya Lightning kwa kutumia kifaa kingine na ui-scan tu.",
"meltSwapHint": "Chagua sarafu nyingine kutoka kwa orodha yako ya sarafu za kuaminika kama mpokeaji wa malipo.",
"copyShareToken": "Nakili & Shiriki kwa haraka",
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ export interface ILnUrl {
callback: string
pr: string
}

// TODO This interface is missing some properties?
export interface ILnUrlPayRequest {
tag: string
cb: string
minSendable: number
maxSendable: number
metadata: string
}
export interface IMint {
id: string
mintUrl: string
Expand Down
19 changes: 17 additions & 2 deletions src/model/nav.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import type { EventArg } from '@react-navigation/core'
import type { NativeStackScreenProps } from '@react-navigation/native-stack'

import type { IHistoryEntry, IMintUrl, IMintWithBalance, IProofSelection, ITokenInfo } from '.'
import type { IHistoryEntry, ILnUrlPayRequest, IMintUrl, IMintWithBalance, IProofSelection, ITokenInfo } from '.'
import type { HexKey, IContact } from './nostr'

export interface INostrSendData {
senderName: string
contact?: IContact
}

interface ILnurlNavData {
userInput: string
url?: string
data?: ILnUrlPayRequest
}
/**
* Stack Navigator
*/
Expand Down Expand Up @@ -48,6 +54,7 @@ export type RootStackParamList = {
invoice?: string
invoiceAmount?: number
estFee?: number
lnurl?: ILnurlNavData
scanned?: boolean
},
selectTarget: {
Expand All @@ -74,8 +81,9 @@ export type RootStackParamList = {
nostr?: INostrSendData
isSwap?: boolean
balance: number
lnurl?: string
lnurl?: ILnurlNavData
targetMint?: IMintUrl
scanned?: boolean
}
selectNostrAmount: {
mint: IMintUrl
Expand Down Expand Up @@ -118,12 +126,19 @@ export type RootStackParamList = {
'qr processing': {
tokenInfo?: ITokenInfo
token?: string
scanned?: boolean
ln?: {
invoice: string
mint?: IMintUrl
balance?: number
amount: number
}
lnurl?: {
mint?: IMintUrl
balance?: number
url: string
data: string
}
}
'mint confirm': {
mintUrl: string
Expand Down
4 changes: 2 additions & 2 deletions src/nostr/consts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Npub } from '@src/model/nostr'
import { isUrl } from '@src/util'
import type { Npub } from '@model/nostr'
import { isUrl } from '@util/lnurl'

import { normalizeURL } from './util'

Expand Down
3 changes: 1 addition & 2 deletions src/screens/Addressbook/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,7 @@ export default function AddressbookPage({ navigation, route }: TAddressBookPageP
openPromptAutoClose({ msg: t('receiverNoLnurl', { ns: NS.addrBook }) })
return
}
navigation.navigate('selectAmount', { isMelt, lnurl: contact.lud16, mint, balance })
return
return navigation.navigate('selectAmount', { isMelt, lnurl: { userInput: contact.lud16 }, mint, balance })
}
if (!nostrRef.current) { return }
// mint has already been selected
Expand Down
5 changes: 3 additions & 2 deletions src/screens/Payment/Processing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import { useInitialURL } from '@src/context/Linking'
import { useNostrContext } from '@src/context/Nostr'
import { useThemeContext } from '@src/context/Theme'
import { NS } from '@src/i18n'
import { isLnurlOrAddress } from '@src/util/lnurl'
import { addLnPaymentToHistory } from '@store/HistoryStore'
import { addToHistory, updateLatestHistory } from '@store/latestHistoryEntries'
import { getDefaultMint } from '@store/mintStore'
import { globals } from '@styles'
import { decodeLnInvoice, getInvoiceFromLnurl, isErr, isLnurlOrAddress, isNum, uniqByIContacts } from '@util'
import { decodeLnInvoice, getInvoiceFromLnurl, isErr, isNum, uniqByIContacts } from '@util'
import { autoMintSwap, checkFees, fullAutoMintSwap, getHighestBalMint, payLnInvoice, requestMint, sendToken } from '@wallet'
import { useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
Expand Down Expand Up @@ -98,7 +99,7 @@ export default function ProcessingScreen({ navigation, route }: TProcessingPageP

const handleMelting = async () => {
let invoice = ''
// recipient can be a LNURL (address) or a LN invoice
// recipient can be a LNURL or a LN invoice
if (recipient?.length && isLnurlOrAddress(recipient)) {
try {
invoice = await getInvoiceFromLnurl(recipient, +amount)
Expand Down
39 changes: 28 additions & 11 deletions src/screens/Payment/SelectAmount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ import { usePromptContext } from '@src/context/Prompt'
import { useThemeContext } from '@src/context/Theme'
import { NS } from '@src/i18n'
import { globals, highlight as hi, mainColors } from '@styles'
import { cleanUpNumericStr, formatSatStr, getInvoiceFromLnurl, vib } from '@util'
import { cleanUpNumericStr, formatInt, formatSatStr, getInvoiceFromLnurl, vib } from '@util'
import { getLnurlIdentifierFromMetadata, isLightningAddress } from '@util/lnurl'
import { checkFees, requestMint } from '@wallet'
import { createRef, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Animated, KeyboardAvoidingView, TextInput, View } from 'react-native'
import { s, ScaledSheet, vs } from 'react-native-size-matters'

export default function SelectAmountScreen({ navigation, route }: TSelectAmountPageProps) {
const { mint, balance, lnurl, isMelt, isSendEcash, nostr, isSwap, targetMint } = route.params
const { mint, balance, lnurl, isMelt, isSendEcash, nostr, isSwap, targetMint, scanned } = route.params
const { openPromptAutoClose } = usePromptContext()
const { t } = useTranslation([NS.wallet])
const { color, highlight } = useThemeContext()
Expand Down Expand Up @@ -53,14 +54,14 @@ export default function SelectAmountScreen({ navigation, route }: TSelectAmountP
return 'createInvoice'
}

const handleFeeEstimation = async (lnurl: string) => {
const handleFeeEstimation = async () => {
setFee(prev => ({ ...prev, isCalculating: true }))
try {
// check fee for payment to lnurl
if (lnurl.length) {
const lnurlInvoice = await getInvoiceFromLnurl(lnurl, +amount)
if (lnurl) {
const lnurlInvoice = await getInvoiceFromLnurl(lnurl.userInput, +amount)
if (!lnurlInvoice?.length) {
openPromptAutoClose({ msg: t('feeErr', { ns: NS.common, input: lnurl }) })
openPromptAutoClose({ msg: t('feeErr', { ns: NS.common, input: lnurl.url }) })
return setFee(prev => ({ ...prev, isCalculating: false }))
}
const estFee = await checkFees(mint.mintUrl, lnurlInvoice)
Expand Down Expand Up @@ -106,11 +107,12 @@ export default function SelectAmountScreen({ navigation, route }: TSelectAmountP
return
}
// estimate melting/swap fee
if (!isSendEcash && shouldEstimate && (lnurl?.length || isSwap)) {
return handleFeeEstimation(lnurl || '')
if (!isSendEcash && shouldEstimate && (lnurl || isSwap)) {
return handleFeeEstimation()
}
// send ecash / melt / swap
if (isSendingTX) {
const recipient = isLightningAddress(lnurl?.userInput || '') ? lnurl?.userInput : lnurl?.data ? getLnurlIdentifierFromMetadata(lnurl.data?.metadata) : undefined
// Check if user melts/swaps his whole mint balance, so there is no need for coin selection and that can be skipped here
if (!isSendEcash && isSendingWholeMintBal()) {
return navigation.navigate('processing', {
Expand All @@ -121,7 +123,7 @@ export default function SelectAmountScreen({ navigation, route }: TSelectAmountP
isSendEcash,
isSwap,
targetMint,
recipient: lnurl
recipient
})
}
return navigation.navigate('coinSelection', {
Expand All @@ -135,7 +137,7 @@ export default function SelectAmountScreen({ navigation, route }: TSelectAmountP
isSendEcash,
isSwap,
targetMint,
recipient: lnurl
recipient
})
}
// request new token from mint
Expand Down Expand Up @@ -169,7 +171,7 @@ export default function SelectAmountScreen({ navigation, route }: TSelectAmountP
<Screen
screenName={t(getScreenName(), { ns: NS.common })}
withBackBtn
handlePress={() => navigation.goBack()}
handlePress={() => scanned ? navigation.navigate('qr scan', {}) : navigation.goBack()}
mintBalance={balance}
disableMintBalance={isMelt || isSwap}
handleMintBalancePress={() => setAmount(`${balance}`)}
Expand All @@ -181,6 +183,21 @@ export default function SelectAmountScreen({ navigation, route }: TSelectAmountP
/>
}
<View style={[styles.overviewWrap, { marginTop: isMelt || isSwap ? 0 : vs(20) }]}>
{lnurl && (lnurl.data || lnurl.userInput) &&
<Txt
txt={
isLightningAddress(lnurl.userInput) ?
lnurl.userInput
:
lnurl.data ?
`${getLnurlIdentifierFromMetadata(lnurl.data.metadata)} requests ${lnurl.data.minSendable / 1000} to ${formatInt(lnurl.data.maxSendable / 1000)} Sats.`
:
''
}
bold
styles={[styles.sats, { marginBottom: vs(5), fontSize: s(10) }]}
/>
}
<Animated.View style={[styles.amountWrap, { transform: [{ translateX: anim.current }] }]}>
<TextInput
keyboardType='numeric'
Expand Down
5 changes: 3 additions & 2 deletions src/screens/Payment/Send/CoinSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { useInitialURL } from '@src/context/Linking'
import { useThemeContext } from '@src/context/Theme'
import { NS } from '@src/i18n'
import { globals } from '@styles'
import { formatInt, formatMintUrl, formatSatStr, getSelectedAmount, isLnurlOrAddress, isNum } from '@util'
import { formatInt, formatMintUrl, formatSatStr, getSelectedAmount, isNum } from '@util'
import { isLightningAddress } from '@util/lnurl'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ScrollView, View } from 'react-native'
Expand Down Expand Up @@ -60,7 +61,7 @@ export default function CoinSelectionScreen({ navigation, route }: TCoinSelectio

const getRecipient = () => {
if (recipient) {
return !isLnurlOrAddress(recipient) ? truncateStr(recipient, 16) : recipient
return !isLightningAddress(recipient) ? truncateStr(recipient, 16) : recipient
}
const npub = npubEncode(nostr?.contact?.hex ?? '')
const receiverName = getNostrUsername(nostr?.contact)
Expand Down
17 changes: 13 additions & 4 deletions src/screens/Payment/Send/Inputfield.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { usePromptContext } from '@src/context/Prompt'
import { useThemeContext } from '@src/context/Theme'
import { NS } from '@src/i18n'
import { globals } from '@styles'
import { decodeLnInvoice, getStrFromClipboard, isErr, isLnurlOrAddress, openUrl } from '@util'
import { decodeLnInvoice, getStrFromClipboard, isErr, openUrl } from '@util'
import { decodeUrlOrAddress, getLnurlData, isLnurlOrAddress } from '@util/lnurl'
import { checkFees } from '@wallet'
import { createRef, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
Expand Down Expand Up @@ -65,16 +66,24 @@ export default function InputfieldScreen({ navigation, route }: TMeltInputfieldP
}
}

const handleBtnPress = () => {
const handleBtnPress = async () => {
if (loading) { return }
// open user LN wallet
if (!input.length) {
return openUrl('lightning://')?.catch(e =>
openPromptAutoClose({ msg: isErr(e) ? e.message : t('deepLinkErr') }))
}
// user pasted a LNURL, we need to get the amount by the user
// user pasted an encoded LNURL, we need to get the amount by the user
if (isLnurlOrAddress(input)) {
return navigation.navigate('selectAmount', { mint, balance, isMelt: true, lnurl: input })
const decoded = decodeUrlOrAddress(input)
if (!decoded) { return openPromptAutoClose({ msg: 'Could not decode LNURL!' }) }
try {
const lnurlData = await getLnurlData(decoded)
if (!lnurlData) { return openPromptAutoClose({ msg: 'Could not fetch data from LNURL' }) }
return navigation.navigate('selectAmount', { mint, balance, isMelt: true, lnurl: { userInput: input, url: decoded, data: lnurlData } })
} catch (e) {
return openPromptAutoClose({ msg: 'Could not fetch data from LNURL' })
}
}
// not enough funds
if (decodedAmount + estFee > balance) {
Expand Down
2 changes: 1 addition & 1 deletion src/screens/Payment/Send/SelectTarget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default function SelectTargetScreen({ navigation, route }: TSelectTargetP
mint,
balance,
isMelt: true,
lnurl: lud16
lnurl: { userInput: lud16 }
})
}}
hasSeparator
Expand Down
Loading

0 comments on commit 1c8e74b

Please sign in to comment.