Skip to content

Commit

Permalink
Added support for Lnurl-Pay through copy/paste and QR code scan (#323)
Browse files Browse the repository at this point in the history
[x] Added bech32-encoded lnurl utils file
[x] Detect encoded lnurl in QR code
[ ] No new package added other than bech32
[ ] Detect lnurl in input field
[ ] Handle decoded lnurl-pay, lightning address
[ ] use camera to scan and decode lnurl-pay and lightning address
[ ] Update UI
  • Loading branch information
starbackr-dev authored Mar 25, 2024
1 parent 6a7deb8 commit a6fffdd
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 32 deletions.
29 changes: 21 additions & 8 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@react-navigation/native-stack": "^6.9.17",
"@sentry/react-native": "5.10.0",
"@shopify/flash-list": "1.4.3",
"bech32": "^2.0.0",
"crypto-js": "4.2.0",
"expo": "^49.0.21",
"expo-application": "~5.3.0",
Expand Down
4 changes: 2 additions & 2 deletions src/screens/Payment/Processing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { addLnPaymentToHistory } from '@store/HistoryStore'
import { addToHistory, updateLatestHistory } from '@store/latestHistoryEntries'
import { getDefaultMint } from '@store/mintStore'
import { globals } from '@styles'
import { decodeLnInvoice, getInvoiceFromLnurl, isErr, isLnurl, isNum, uniqByIContacts } from '@util'
import { decodeLnInvoice, getInvoiceFromLnurl, isErr, isLnurlOrAddress, 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 @@ -99,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
if (recipient?.length && isLnurl(recipient)) {
if (recipient?.length && isLnurlOrAddress(recipient)) {
try {
invoice = await getInvoiceFromLnurl(recipient, +amount)
if (!invoice?.length) {
Expand Down
4 changes: 2 additions & 2 deletions src/screens/Payment/Send/CoinSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ 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, isLnurl, isNum } from '@util'
import { formatInt, formatMintUrl, formatSatStr, getSelectedAmount, isLnurlOrAddress, isNum } from '@util'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ScrollView, View } from 'react-native'
Expand Down Expand Up @@ -60,7 +60,7 @@ export default function CoinSelectionScreen({ navigation, route }: TCoinSelectio

const getRecipient = () => {
if (recipient) {
return !isLnurl(recipient) ? truncateStr(recipient, 16) : recipient
return !isLnurlOrAddress(recipient) ? truncateStr(recipient, 16) : recipient
}
const npub = npubEncode(nostr?.contact?.hex ?? '')
const receiverName = getNostrUsername(nostr?.contact)
Expand Down
8 changes: 5 additions & 3 deletions src/screens/Payment/Send/Inputfield.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ 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, isLnInvoice, isLnurl, openUrl } from '@util'
import { decodeLnInvoice, getStrFromClipboard, isErr, isLnurlOrAddress, openUrl } from '@util'
import { checkFees } from '@wallet'
import { createRef, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
Expand Down Expand Up @@ -43,7 +43,7 @@ export default function InputfieldScreen({ navigation, route }: TMeltInputfieldP
if (!clipboard) { return }
setInput(clipboard)
// pasted LNURL address which does not need decoding
if (isLnurl(clipboard)) { return }
if (isLnurlOrAddress(clipboard)) { return }
// pasted LN invoice
await handleInvoicePaste(clipboard)
}
Expand Down Expand Up @@ -73,7 +73,7 @@ export default function InputfieldScreen({ navigation, route }: TMeltInputfieldP
openPromptAutoClose({ msg: isErr(e) ? e.message : t('deepLinkErr') }))
}
// user pasted a LNURL, we need to get the amount by the user
if (isLnurl(input)) {
if (isLnurlOrAddress(input)) {
return navigation.navigate('selectAmount', { mint, balance, isMelt: true, lnurl: input })
}
// not enough funds
Expand Down Expand Up @@ -165,9 +165,11 @@ export default function InputfieldScreen({ navigation, route }: TMeltInputfieldP
value={input}
onChangeText={text => {
setInput(text)
/* Handle when the continue button is pressed
if (isLnInvoice(text)) {
void handleInvoicePaste(text)
}
*/
}}
onSubmitEditing={() => void handleBtnPress()}
autoFocus
Expand Down
30 changes: 26 additions & 4 deletions src/screens/QRScan/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import useLoading from '@comps/hooks/Loading'
import useCashuToken from '@comps/hooks/Token'
import { CloseIcon, FlashlightOffIcon } from '@comps/Icons'
import { isIOS, QRType } from '@consts'
import { addMint, getMintsUrls } from '@db'
import { addMint, getMintsBalances, getMintsUrls } from '@db'
import TrustMintModal from '@modal/TrustMint'
import type { TQRScanPageProps } from '@model/nav'
import { isNProfile, isNpubQR } from '@nostr/util'
import { useIsFocused } from '@react-navigation/core'
import { usePromptContext } from '@src/context/Prompt'
import { useThemeContext } from '@src/context/Theme'
import { NS } from '@src/i18n'
import { getDefaultMint } from '@store/mintStore'
import { getCustomMintNames, getDefaultMint } from '@store/mintStore'
import { globals, mainColors } from '@styles'
import { decodeLnInvoice, extractStrFromURL, hasTrustedMint, isCashuToken, isNull, isStr, isUrl } from '@util'
import { decodeLnInvoice, extractStrFromURL, hasTrustedMint, isCashuToken, isLnurlOrAddress, isNull, isStr, isUrl } from '@util'
import { getTokenInfo } from '@wallet/proofs'
import { BarCodeScanner, PermissionStatus } from 'expo-barcode-scanner'
import { Camera, FlashMode } from 'expo-camera'
Expand Down Expand Up @@ -82,7 +82,7 @@ export default function QRScanPage({ navigation, route }: TQRScanPageProps) {
navigation.navigate('qr processing', { tokenInfo, token })
}

const handleBarCodeScanned = ({ type, data }: { type: string, data: string }) => {
const handleBarCodeScanned = async ({ type, data }: { type: string, data: string }) => {
setScanned(true)
const bcType = isIOS ? 'org.iso.QRCode' : +QRType
// early return if barcode is not a QR
Expand Down Expand Up @@ -113,6 +113,28 @@ export default function QRScanPage({ navigation, route }: TQRScanPageProps) {
if (isUrl(data) && new URL(data).protocol === 'https:') {
return navigation.navigate('mint confirm', { mintUrl: data })
}
// handle LNURL

if (isLnurlOrAddress(data) ) {

if (mint === undefined || balance === undefined) {

// user has not selected the mint yet (Pressed scan QR and scanned a Lightning invoice)
const mintsWithBal = await getMintsBalances()
const mints = await getCustomMintNames(mintsWithBal.map(m => ({ mintUrl: m.mintUrl })))
const nonEmptyMint = mintsWithBal.filter(m => m.amount > 0)
const mintUsing = mints.find(m => m.mintUrl === nonEmptyMint[0].mintUrl) || { mintUrl: 'N/A', customName: 'N/A' }

return navigation.navigate('selectAmount', { mint:mintUsing, balance:nonEmptyMint[0].amount, isMelt: true, lnurl: data })


}

return navigation.navigate('selectAmount', { mint, balance, isMelt: true, lnurl: data })


}

// handle LN invoice
try {
const invoice = extractStrFromURL(data) || data
Expand Down
Loading

0 comments on commit a6fffdd

Please sign in to comment.