Better offline sending UX (#163)

Co-authored-by: minibits-cash <mail@minibits.cash>
This commit is contained in:
KraXen72
2025-07-23 20:22:47 +02:00
committed by GitHub
co-authored by minibits-cash
parent 176622500e
commit b433b21307
8 changed files with 329 additions and 205 deletions
+2
View File
@@ -5,4 +5,6 @@ module.exports = {
singleQuote: true,
trailingComma: 'all',
semi: false,
useTabs: false,
tabWidth: 2,
};
+6 -4
View File
@@ -51,10 +51,12 @@ export const AmountInput = forwardRef<TextInput, AmountInputProps>(
onEndEditing()
}
// Default formatting behavior
const formattedValue = numbro(value).format({
thousandSeparated: true,
mantissa: getCurrency(unit).mantissa
})
const formattedValue = typeof value === "undefined"
? ""
: numbro(value).format({
thousandSeparated: true,
mantissa: getCurrency(unit).mantissa
})
onChangeText(formattedValue)
}
+13 -1
View File
@@ -1,5 +1,9 @@
{
"acceptClosestMatch": "Accept closest",
"addMints": "Add mints",
"amountClosestMatch": "Closest match available",
"amountExactMatch": "Exact match found!",
"amountMatchExplanation": "You don't have the exact amount, but you can use the closest available amount or select custom banknotes.",
"amountRequested": "Requested amount",
"amountSend": "Amount to send",
"amountTopup": "Topup amount",
@@ -458,24 +462,32 @@
"seedRecoveryMints": "Mints",
"seedRecoveryScreenTitle": "Wallet recovery",
"seedRecoveryStep2": "2",
"selectCustomAmount": "Select custom",
"sendContinue": "Continue",
"sendCreateToken": "Create token",
"sendEnterEcashToSend": "Enter ecash to send",
"sendLock": "Lock",
"sendLockEcash": "Lock ecash",
"sendLockEcashDesc": "Lock ecash token to one of your contacts or to a public key. Only the receiver will be able to unlock it.",
"sendLocked": "Locked",
"sendLockFor": "Lock for",
"sendOffline_usageHint": "Tap the ecash notes to adjust the amount",
"sendOfflineApproxMatch": "No exact match for your requested amount was found.",
"sendOfflineExactDenoms": "You can only send exact ecash denominations while you are offline.",
"sendScreen_createTokenBtn": "Create token",
"sendScreen_memo": "Memo for recipient",
"sendScreen_offlinemode": "OFFLINE MODE",
"sendScreen_pasteToSend": "Paste",
"sendScreen_pasteToSendDescription": "Paste lightning invoice from clipboard to pay it from your wallet.",
"sendScreen_scanToPay": "Scan to pay",
"sendScreen_scanToPayDescription": "Scan Lightning invoice, LNURL pay code or Lightning address to pay.",
"sendScreen_sendFromMintBalanceSel": "Send from mint",
"sendScreen_sendOfflineBtn": "Send offline",
"sendScreen_sendToContact": "Send to contact",
"sendScreen_sendToContactDescription": "Send your ecash to one of your contacts stored in your contact list.",
"sendScreen_showOrShareToken": "Share ecash",
"sendScreen_showOrShareTokenDescription": "Present or share ecash token for an amount you want to send.",
"sendScreen_title": "Send",
"sendSelectEcashToSend": "Select ecash to send",
"sendToContact": "Send to contact",
"setStartIndex": "Set start index",
"settingsScreen_backup": "Backup",
+13 -1
View File
@@ -1,5 +1,9 @@
{
"acceptClosestMatch": "Aceptar más cercano",
"addMints": "Agregar mints",
"amountClosestMatch": "Coincidencia más cercana disponible",
"amountExactMatch": "¡Coincidencia exacta encontrada!",
"amountMatchExplanation": "No tienes el monto exacto, pero puedes utilizar el monto disponible más cercano o seleccionar billetes personalizados.",
"amountRequested": "Cantidad solicitada",
"amountSend": "Cantidad a enviar",
"amountTopup": "Cantidad de recarga",
@@ -458,24 +462,32 @@
"seedRecoveryMints": "Mentas",
"seedRecoveryScreenTitle": "Recuperación de billetera",
"seedRecoveryStep2": "2",
"selectCustomAmount": "Seleccione personalizado",
"sendContinue": "Continuar",
"sendCreateToken": "Crear token",
"sendEnterEcashToSend": "Ingrese ecash para enviar",
"sendLock": "Cerrar",
"sendLockEcash": "Bloquear efectivo",
"sendLockEcashDesc": "Bloquea el token de ecash con uno de tus contactos o con una clave pública. Solo el receptor podrá desbloquearlo.",
"sendLocked": "Bloqueado",
"sendLockFor": "Bloquear para",
"sendOffline_usageHint": "Toque las notas de ecash para ajustar el monto",
"sendOfflineApproxMatch": "No se encontró una coincidencia exacta para la cantidad solicitada.",
"sendOfflineExactDenoms": "Solo puedes enviar denominaciones exactas de efectivo mientras estás desconectado.",
"sendScreen_createTokenBtn": "Crear token",
"sendScreen_memo": "Memo para el destinatario",
"sendScreen_offlinemode": "MODO OFFLINE",
"sendScreen_pasteToSend": "Pasta",
"sendScreen_pasteToSendDescription": "Pegue la factura de Lightning desde el portapapeles para pagarla desde su billetera.",
"sendScreen_scanToPay": "Escanear para pagar",
"sendScreen_scanToPayDescription": "Escanee la factura Lightning, el código de pago LNURL o la dirección Lightning para pagar.",
"sendScreen_sendFromMintBalanceSel": "Enviar desde menta",
"sendScreen_sendOfflineBtn": "Enviar offline",
"sendScreen_sendToContact": "Enviar a contacto",
"sendScreen_sendToContactDescription": "Envía tu ecash a uno de tus contactos almacenados en tu lista de contactos.",
"sendScreen_showOrShareToken": "Compartir ecash",
"sendScreen_showOrShareTokenDescription": "Presenta o comparte un token ecash por el monto que desees enviar.",
"sendScreen_title": "Enviar",
"sendSelectEcashToSend": "Seleccione el ecash que desea enviar",
"sendToContact": "Enviar a contacto",
"setStartIndex": "Establecer índice de inicio",
"settingsScreen_backup": "Respaldo",
+13 -1
View File
@@ -1,5 +1,9 @@
{
"acceptClosestMatch": "Aceitar o mais próximo",
"addMints": "Adicionar mints",
"amountClosestMatch": "Correspondência mais próxima disponível",
"amountExactMatch": "Correspondência exata encontrada!",
"amountMatchExplanation": "Você não tem o valor exato, mas pode usar o valor mais próximo disponível ou selecionar notas personalizadas.",
"amountRequested": "Valor solicitado",
"amountSend": "Valor a enviar",
"amountTopup": "Valor da recarga",
@@ -458,24 +462,32 @@
"seedRecoveryMints": "Mints",
"seedRecoveryScreenTitle": "Recuperação de carteira",
"seedRecoveryStep2": "2",
"selectCustomAmount": "Selecione personalizado",
"sendContinue": "Continuar",
"sendCreateToken": "Criar token",
"sendEnterEcashToSend": "Digite ecash para enviar",
"sendLock": "Bloquear",
"sendLockEcash": "Bloquear ecash",
"sendLockEcashDesc": "Bloqueie token ecash para um contato ou pubkey. Apenas o destinatário poderá desbloqueá-lo.",
"sendLocked": "Bloqueado",
"sendLockFor": "Bloquear para",
"sendOffline_usageHint": "Toque nas notas de ecash para ajustar o valor",
"sendOfflineApproxMatch": "Não foi encontrada nenhuma correspondência exata para o valor solicitado.",
"sendOfflineExactDenoms": "Você só pode enviar denominações exatas de ecash offline.",
"sendScreen_createTokenBtn": "Criar token",
"sendScreen_memo": "Memorando para destinatário",
"sendScreen_offlinemode": "MODO OFFLINE",
"sendScreen_pasteToSend": "Colar",
"sendScreen_pasteToSendDescription": "Cole invoice Lightning da área de transferência para pagar da carteira.",
"sendScreen_scanToPay": "Escanear para pagar",
"sendScreen_scanToPayDescription": "Escanear invoice Lightning, código de pagamento LNURL ou endereço Lightning para pagar.",
"sendScreen_sendFromMintBalanceSel": "Enviar da casa da moeda",
"sendScreen_sendOfflineBtn": "Enviar offline",
"sendScreen_sendToContact": "Enviar para contato",
"sendScreen_sendToContactDescription": "Envie ecash para um contato armazenado na sua lista.",
"sendScreen_showOrShareToken": "Compartilhar ecash",
"sendScreen_showOrShareTokenDescription": "Apresente ou compartilhe token ecash para o valor que deseja enviar.",
"sendScreen_title": "Enviar",
"sendSelectEcashToSend": "Selecionar ecash para enviar",
"sendToContact": "Enviar para contato",
"setStartIndex": "Definir índice inicial",
"settingsScreen_backup": "Backup",
+14 -2
View File
@@ -1,5 +1,9 @@
{
"acceptClosestMatch": "Prijať najbližšiu zhodu",
"addMints": "Pridať minty",
"amountClosestMatch": "Najbližšia dostupná zhoda",
"amountExactMatch": "Nájdená presná zhoda!",
"amountMatchExplanation": "Nemáte presnú sumu, ale môžete použiť najbližšiu dostupnú sumu alebo si vybrať vlastné bankovky.",
"amountRequested": "Požadovaná suma",
"amountSend": "Suma na odoslanie",
"amountTopup": "Pridať sumu",
@@ -458,24 +462,32 @@
"seedRecoveryMints": "Minty",
"seedRecoveryScreenTitle": "Obnova peňaženky",
"seedRecoveryStep2": "2",
"selectCustomAmount": "Vybrať vlastné",
"sendContinue": "Pokračovať",
"sendCreateToken": "Vytvoriť token",
"sendEnterEcashToSend": "Zadaj ecash na odoslanie",
"sendLock": "Zamknúť",
"sendLockEcash": "Zamknúť ecash",
"sendLockEcashDesc": "Zamkni ecash token pre jeden z tvojich kontaktov alebo na verejný kľúč. Iba príjemca ho bude môcť odomknúť.",
"sendLocked": "Zamknuté",
"sendLockFor": "Zamknúť pre",
"sendOfflineExactDenoms": "Offline môžeš poslať len presné denominácie ecash.",
"sendOffline_usageHint": "Klepnutím na ecash bankovky upravte sumu",
"sendOfflineApproxMatch": "Nenašla sa presná zhoda pre požadovanú sumu.",
"sendOfflineExactDenoms": "Keď si offline, môžeš poslať len presné ecash denominácie.",
"sendScreen_createTokenBtn": "Vytvoriť token",
"sendScreen_memo": "Správa pre prijímateľa",
"sendScreen_offlinemode": "OFFLINE REŽIM",
"sendScreen_pasteToSend": "Vložiť",
"sendScreen_pasteToSendDescription": "Vlož Lightning invoice zo schránky, aby si ju zaplatil.",
"sendScreen_scanToPay": "Skenuj na zaplatenie",
"sendScreen_scanToPayDescription": "Skenuj invoice, LNURL kód, alebo Lightning address kam chceš platiť.",
"sendScreen_sendFromMintBalanceSel": "Odoslať z mintu",
"sendScreen_sendOfflineBtn": "Odoslať offline",
"sendScreen_sendToContact": "Pošli na kontakt",
"sendScreen_sendToContactDescription": "Pošli ecash na kontakt uložený v tvojom zozname",
"sendScreen_showOrShareToken": "Zdieľaj ecash",
"sendScreen_showOrShareTokenDescription": "Zobraz alebo zdieľaj ecash token na sumu, ktorú chceš poslať",
"sendScreen_title": "Poslať",
"sendSelectEcashToSend": "Vyber ecash na odoslanie",
"sendToContact": "Poslať kontaktu",
"setStartIndex": "Nastav začiatočný index",
"settingsScreen_backup": "Záloha",
+265 -194
View File
@@ -159,8 +159,9 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
unitRef.current = unit
if(mintUrl) {
const mintBalance = proofsStore.getMintBalance(mintUrl)
const mintBalance = proofsStore.getMintBalance(mintUrl)
setMintBalanceToSendFrom(mintBalance)
log.trace('[setUnitAndMint] mintBalanceToSendFrom', mintBalance)
}
} catch (e: any) {
handleError(e)
@@ -425,16 +426,14 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
const availableBalances = proofsStore.getMintBalancesWithEnoughBalance(1, unitRef.current)
if (availableBalances.length === 0) {
setInfo('There are not enough funds to send')
setInfo('Not enough funds to send')
return
}
log.trace('Setting availableBalances')
log.trace('[Offline send] Setting availableBalances')
setIsOfflineSend(true)
setAvailableMintBalances(availableBalances)
setMintBalanceToSendFrom(availableBalances[0])
setIsMintSelectorVisible(true)
setAvailableMintBalances(availableBalances)
}, [isInternetReachable])
@@ -563,67 +562,79 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
const toggleNostrDMModal = () => setIsNostrDMModalVisible(previousState => !previousState)
const toggleProofSelectorModal = () => setIsProofSelectorModalVisible(previousState => !previousState)
const toggleResultModal = () => setIsResultModalVisible(previousState => !previousState)
// const toggleIsLockedToPubkey = () => setIsLockedToPubkey(previousState => !previousState)
const togglePubkeySelectorModal = () => setIsPubkeySelectorModalVisible(previousState => !previousState)
const validateAndProcessAmount = function (amountString: string, unit: MintUnit) {
// Normalize empty string to "0"
const normalizedAmount = amountString.trim() === "" ? "0" : amountString.trim()
const precision = getCurrency(unit).precision
const amount = round(toNumber(normalizedAmount) * precision, 0)
const isValid = typeof amount === "number" && !Number.isNaN(amount) && amount > 0
return {
amount: isValid ? amount : 0,
amountString: normalizedAmount
}
}
const onAmountEndEditing = function () {
try {
const precision = getCurrency(unitRef.current).precision
const amount = round(toNumber(amountToSend) * precision, 0)
try {
const { amount, amountString } = validateAndProcessAmount(amountToSend, unitRef.current)
log.trace('[onAmountEndEditing]', amount, amountString)
log.trace('[onAmountEndEditing]', amount)
if (amount && amount > 0) {
if (!amount || amount === 0) {
infoMessage(translate('payCommon_amountZeroOrNegative'))
return
}
const availableBalances = proofsStore.getMintBalancesWithEnoughBalance(amount, unitRef.current)
if (availableBalances.length === 0) {
log.trace('[onAmountEndEditing] payCommon_insufficientFunds')
infoMessage(translate('payCommon_insufficientFunds'))
return
}
LayoutAnimation.easeInEaseOut()
setAvailableMintBalances(availableBalances)
// Default mint if not set from route params is the one with the highest balance
if(!mintBalanceToSendFrom) {
if (!mintBalanceToSendFrom) {
setMintBalanceToSendFrom(availableBalances[0])
}
}
LayoutAnimation.easeInEaseOut()
setIsMintSelectorVisible(true)
} else {
infoMessage(translate('payCommon_amountZeroOrNegative'))
return
}
} catch (e: any) {
handleError(e)
handleError(e)
}
}
const onMemoEndEditing = function () {
LayoutAnimation.easeInEaseOut()
// Show mint selector
if (availableMintBalances.length > 0) {
setIsMintSelectorVisible(true)
}
setIsMintSelectorVisible(true)
}
}
const onMemoDone = function () {
if (parseInt(amountToSend) > 0) {
memoInputRef && memoInputRef.current
memoInputRef && memoInputRef.current
? memoInputRef.current.blur()
: false
amountInputRef && amountInputRef.current
amountInputRef && amountInputRef.current
? amountInputRef.current.blur()
: false
onMemoEndEditing()
onMemoEndEditing()
} else {
amountInputRef && amountInputRef.current
amountInputRef && amountInputRef.current
? amountInputRef.current.focus()
: false
}
@@ -634,43 +645,43 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
setMintBalanceToSendFrom(balance)
}
const onLockPubkeyStart = function () {
togglePubkeySelectorModal()
}
const onLockPubkeySelect = function () {
if(!lockedPubkey || lockedPubkey.length === 0) {
onLockPubkeyCancel()
return
if (!lockedPubkey || lockedPubkey.length === 0) {
onLockPubkeyCancel()
return
}
if(lockedPubkey.startsWith('nsec')) {
throw new AppError(Err.VALIDATION_ERROR, 'Invalid key. Please provide public key in NPUB or HEX format.')
if (lockedPubkey.startsWith('nsec')) {
throw new AppError(Err.VALIDATION_ERROR, 'Invalid key. Please provide public key in NPUB or HEX format.')
}
const contact = contactsStore.findByNpub(lockedPubkey) || contactsStore.findByPubkey(lockedPubkey)
if(contact) {
log.trace('[onLockPubkeySelect] Provided pubkey belongs to a contact', {contactName: contact.name})
let relays: string[] = []
if (contact) {
log.trace('[onLockPubkeySelect] Provided pubkey belongs to a contact', { contactName: contact.name })
let relays: string[] = []
if(contact?.type === ContactType.PUBLIC) {
relays = relaysStore.allPublicUrls
} else {
relays = relaysStore.allUrls
}
if (relays.length === 0) {
throw new AppError(Err.VALIDATION_ERROR, 'Missing NOSTR relays')
}
setPaymentOption(SendOption.SEND_TOKEN)
setContactToSendFrom(getContactFrom())
setContactToSendTo(contact)
setRelaysToShareTo(relays)
if (contact?.type === ContactType.PUBLIC) {
relays = relaysStore.allPublicUrls
} else {
relays = relaysStore.allUrls
}
if (relays.length === 0) {
throw new AppError(Err.VALIDATION_ERROR, 'Missing NOSTR relays')
}
setPaymentOption(SendOption.SEND_TOKEN)
setContactToSendFrom(getContactFrom())
setContactToSendTo(contact)
setRelaysToShareTo(relays)
}
togglePubkeySelectorModal()
}
@@ -682,38 +693,64 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
const onMintBalanceConfirm = async function () {
if (!mintBalanceToSendFrom) {
return
}
const { amount: amountToSendInt } = validateAndProcessAmount(amountToSend, unitRef.current)
const exactMatchProofs: Proof[] = []
if(isOfflineSend) {
const availableProofs = proofsStore.getByMint(mintBalanceToSendFrom.mintUrl, { isPending: false, unit: unitRef.current });
const autoSelectedProofs = CashuUtils.getProofsToSend(amountToSendInt, availableProofs)
const autoSelectedAmount = CashuUtils.getProofsAmount(autoSelectedProofs)
const isExactMatch = autoSelectedAmount === amountToSendInt
log.trace("[onMintBalanceConfirm]", {isOfflineSend, amountToSendInt, autoSelectedAmount, isExactMatch})
// setSelectedProofs(autoSelectedProofs) //
if(!isExactMatch) {
// TODO need to improve algo auto-selected proofs
/* setAmountToSend(numbro(selectedAmount / getCurrency(unitRef.current).precision).format({
thousandSeparated: true,
mantissa: getCurrency(unitRef.current).mantissa
})) */
resetSelectedProofs()
// show proof selector modal
setIsProofSelectorModalVisible(true)
return
} else {
exactMatchProofs.push(...autoSelectedProofs)
}
}
setIsLoading(true)
const amountToSendInt = round(toNumber(amountToSend) * getCurrency(unitRef.current).precision, 0)
//@ts-ignore
const p2pk: {
pubkey: string;
locktime?: number;
refundKeys?: Array<string>
const p2pk: {
pubkey: string;
locktime?: number;
refundKeys?: Array<string>
} | undefined = undefined
log.trace('[onMintBalanceConfirm] lockedPubkey', {lockedPubkey})
log.trace('[onMintBalanceConfirm] lockedPubkey', { lockedPubkey })
if(lockedPubkey && lockedPubkey.length > 0) {
if(lockedPubkey.startsWith('npub')) {
if (lockedPubkey && lockedPubkey.length > 0) {
if (lockedPubkey.startsWith('npub')) {
p2pk.pubkey = '02' + NostrClient.getHexkey(lockedPubkey)
} else {
if(lockedPubkey.length === 64) {
if (lockedPubkey.length === 64) {
p2pk.pubkey = '02' + lockedPubkey
} else if(lockedPubkey.length === 66) {
} else if (lockedPubkey.length === 66) {
p2pk.pubkey = lockedPubkey
} else {
throw new AppError(Err.VALIDATION_ERROR, 'Invalid key. Please provide public key in NPUB or HEX format.')
}
throw new AppError(Err.VALIDATION_ERROR, 'Invalid key. Please provide public key in NPUB or HEX format.')
}
}
if(lockTime && lockTime > 0) {
if (lockTime && lockTime > 0) {
p2pk.locktime = getUnixTime(new Date(Date.now() + lockTime * 24 * 60 * 60))
log.trace('[onMintBalanceConfirm] Locktime', {pubkey: p2pk.pubkey, locktime: p2pk.locktime})
log.trace('[onMintBalanceConfirm] Locktime', { pubkey: p2pk.pubkey, locktime: p2pk.locktime })
}
}
@@ -724,13 +761,16 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
amountToSendInt,
unitRef.current,
memo,
selectedProofs,
exactMatchProofs.length > 0 ? exactMatchProofs : selectedProofs, // autoSelected proofs are not yet in state
p2pk,
draftTransactionIdRef.current
)
}
const increaseProofsCounterAndRetry = async function () {
try {
const walletInstance = await walletStore.getWallet(
@@ -764,15 +804,6 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
}
const onSelectProofsOffline = async function () {
if (!mintBalanceToSendFrom) {
return
}
setIsProofSelectorModalVisible(true)
}
const onMintBalanceCancel = async function () {
resetState()
gotoWallet()
@@ -867,18 +898,31 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
const toggleSelectedProof = function (proof: Proof) {
const precision = getCurrency(unitRef.current).precision
setSelectedProofs(prevSelectedProofs => {
const isSelected = prevSelectedProofs.some(
p => p.secret === proof.secret
)
// validate amountToSend s.t. it does not crash numbro
const _amountToSend = (!amountToSend || !amountToSend.trim() || Number.isNaN(parseInt(amountToSend)))
? 0
: parseInt(amountToSend);
if (isSelected) {
// If the proof is already selected, remove it from the array
setAmountToSend(`${parseInt(amountToSend) - proof.amount}`)
setAmountToSend(`${numbro(_amountToSend - proof.amount / precision).format({
thousandSeparated: true,
mantissa: getCurrency(unitRef.current).mantissa
})}`)
return prevSelectedProofs.filter(p => p.secret !== proof.secret)
} else {
// If the proof is not selected, add it to the array
setAmountToSend(`${(parseInt(amountToSend) || 0) + proof.amount}`)
setAmountToSend(`${numbro(_amountToSend + proof.amount / precision).format({
thousandSeparated: true,
mantissa: getCurrency(unitRef.current).mantissa
})}`)
return [...prevSelectedProofs, proof]
}
})
@@ -891,7 +935,18 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
const onOfflineSendConfirm = function () {
// Update amountToSend to match exactly the selected proofs amount before proceeding
// const selectedAmount = CashuUtils.getProofsAmount(selectedProofs)
// const precision = getCurrency(unitRef.current).precision
// const formattedAmount = numbro(selectedAmount / precision)
// .format({
// thousandSeparated: true,
// mantissa: getCurrency(unitRef.current).mantissa
// })
// setAmountToSend(formattedAmount)
toggleProofSelectorModal() // close
// Pass the exact selected amount directly to onMintBalanceConfirm
onMintBalanceConfirm()
}
@@ -1010,8 +1065,8 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
value={amountToSend}
onChangeText={amount => setAmountToSend(amount)}
unit={unitRef.current}
onEndEditing={onAmountEndEditing}
editable={(transactionStatus === TransactionStatus.PENDING || isOfflineSend || isCashuPrWithAmount)
onEndEditing={transactionStatus !== TransactionStatus.PENDING ? onAmountEndEditing : undefined}
editable={(transactionStatus === TransactionStatus.PENDING || isCashuPrWithAmount)
? false
: true
}
@@ -1077,13 +1132,16 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
mintBalances={availableMintBalances}
selectedMintBalance={mintBalanceToSendFrom as MintBalance}
unit={unitRef.current}
title='Send from mint'
confirmTitle={isOfflineSend ? 'Send offline' : 'Create token'}
title={translate("sendScreen_sendFromMintBalanceSel")}
confirmTitle={isOfflineSend
? translate("sendScreen_sendOfflineBtn")
: translate("sendScreen_createTokenBtn")
}
secondaryConfirmTitle='Lock'
onMintBalanceSelect={onMintBalanceSelect}
onSecondaryMintBalanceSelect={onLockPubkeyStart}
onCancel={onMintBalanceCancel}
onMintBalanceConfirm={isOfflineSend ? onSelectProofsOffline : onMintBalanceConfirm}
onMintBalanceConfirm={onMintBalanceConfirm}
/>
)}
{transactionStatus === TransactionStatus.PENDING && encodedTokenToSend && paymentOption && (
@@ -1149,8 +1207,7 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
mintBalanceToSendFrom={mintBalanceToSendFrom as MintBalance}
unit={unitRef.current}
selectedProofs={selectedProofs}
// isLockedToPubkey={isLockedToPubkey}
// toggleIsLockedToPubkey={toggleIsLockedToPubkey}
showNoExactMatchMessage={true}
toggleProofSelectorModal={toggleProofSelectorModal}
toggleSelectedProof={toggleSelectedProof}
resetSelectedProofs={resetSelectedProofs}
@@ -1417,113 +1474,127 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
)
const SelectProofsBlock = observer(function (props: {
mintBalanceToSendFrom: MintBalance
unit: MintUnit
selectedProofs: Proof[]
// isLockedToPubkey: boolean
toggleProofSelectorModal: any
toggleSelectedProof: any
// toggleIsLockedToPubkey: any
resetSelectedProofs: any
onOfflineSendConfirm: any
}) {
/**
* allows you to manually select the ecash banknotes for offline sending
*/
const SelectProofsBlock = observer(function (props: {
mintBalanceToSendFrom: MintBalance
unit: MintUnit
selectedProofs: Proof[]
showNoExactMatchMessage?: boolean
toggleProofSelectorModal: any
toggleSelectedProof: any
resetSelectedProofs: any
onOfflineSendConfirm: any
}) {
const {proofsStore} = useStores()
const hintColor = useThemeColor('textDim')
const statusColor = useThemeColor('header')
const { proofsStore } = useStores()
const hintColor = useThemeColor('textDim')
const statusColor = useThemeColor('header')
const onCancel = function () {
props.resetSelectedProofs()
props.toggleProofSelectorModal()
}
return (
<View style={$bottomModal}>
<View
style={[
{
alignSelf: 'center',
marginTop: spacing.tiny,
paddingHorizontal: spacing.tiny,
borderRadius: spacing.tiny,
backgroundColor: colors.palette.primary200,
},
]}>
<Text
text={'OFFLINE MODE'}
style={[
{
color: statusColor,
fontSize: 10,
fontFamily: typography.primary?.light,
padding: 0,
lineHeight: 16,
}
]}
/>
</View>
<Text tx='sendSelectEcashToSend' style={{marginTop: spacing.large}}/>
<Text
tx='sendOfflineExactDenoms'
style={{color: hintColor, paddingHorizontal: spacing.small, textAlign: 'center'}}
size='xs'
/>
<CurrencyAmount
amount={CashuUtils.getProofsAmount(props.selectedProofs)}
mintUnit={props.unit}
size='extraLarge'
containerStyle={{marginTop: spacing.large, marginBottom: spacing.small, alignItems: 'center'}}
/>
<View style={{
maxHeight: spacing.screenHeight * 0.45,
borderWidth: 1,
borderColor: hintColor,
borderRadius: spacing.medium,
marginTop: spacing.small
}}>
<FlatList<Proof>
data={proofsStore.getByMint(props.mintBalanceToSendFrom.mintUrl, {isPending: false, unit: props.unit})}
renderItem={({ item }) => {
const isSelected = props.selectedProofs.some(
p => p.secret === item.secret
)
const onCancel = function () {
props.resetSelectedProofs()
props.toggleProofSelectorModal()
}
return (
<Button
preset={isSelected ? 'default' : 'secondary'}
onPress={() => props.toggleSelectedProof(item)}
text={`${item.amount}`}
style={{minWidth: 80, margin: spacing.small}}
/>
)
}}
numColumns={3}
keyExtractor={(item) => item.secret}
/>
</View>
<View style={[$bottomContainer, {marginTop: spacing.extraLarge}]}>
<View style={[$buttonContainer]}>
<Button
tx="sendCreateToken"
onPress={props.onOfflineSendConfirm}
style={{marginRight: spacing.medium}}
/>
<Button
preset="secondary"
tx="commonCancel"
onPress={onCancel}
/>
</View>
</View>
const $informStyle: TextStyle = {
paddingHorizontal: spacing.small,
textAlign: 'center',
marginTop: spacing.extraSmall,
fontWeight: '500'
}
return (
<View style={$bottomModal}>
<View
style={[
{
alignSelf: 'center',
marginTop: spacing.tiny,
paddingHorizontal: spacing.tiny,
borderRadius: spacing.tiny,
backgroundColor: colors.palette.primary200,
},
]}>
<Text
tx="sendScreen_offlinemode"
style={[
{
color: statusColor,
fontSize: 10,
fontFamily: typography.primary?.light,
padding: 0,
lineHeight: 16,
}
]}
/>
</View>
<Text tx='sendCreateToken' style={{ marginTop: spacing.large }} />
{props.showNoExactMatchMessage && (<Text
tx="sendOfflineApproxMatch"
style={$informStyle}
size='xs'
/>)}
<Text
tx='sendOfflineExactDenoms'
style={{ color: hintColor, paddingHorizontal: spacing.tiny, marginTop: spacing.extraSmall, textAlign: 'center' }}
size='xs'
/>
<CurrencyAmount
amount={CashuUtils.getProofsAmount(props.selectedProofs)}
mintUnit={props.unit}
size='extraLarge'
containerStyle={{ marginTop: spacing.large, marginBottom: spacing.small, alignItems: 'center' }}
/>
<View style={{
maxHeight: spacing.screenHeight * 0.45,
borderWidth: 1,
borderColor: hintColor,
borderRadius: spacing.medium,
marginTop: spacing.small
}}>
<FlatList<Proof>
data={proofsStore.getByMint(props.mintBalanceToSendFrom.mintUrl, { isPending: false, unit: props.unit })}
renderItem={({ item }) => {
const isSelected = props.selectedProofs.some(
p => p.secret === item.secret
)
return (
<Button
preset={isSelected ? 'default' : 'secondary'}
onPress={() => props.toggleSelectedProof(item)}
text={`${item.amount}`}
style={{ minWidth: 80, margin: spacing.small }}
/>
)
}}
numColumns={3}
keyExtractor={(item) => item.secret}
/>
</View>
<Text tx="sendOffline_usageHint" style={{ color: hintColor, marginVertical: spacing.extraSmall, textAlign: 'center' }} size="xs" />
<View style={[$bottomContainer, { marginTop: spacing.extraLarge }]}>
<View style={[$buttonContainer]}>
<Button
tx="sendCreateToken"
onPress={props.onOfflineSendConfirm}
style={{ marginRight: spacing.medium }}
/>
<Button
preset="secondary"
tx="commonCancel"
onPress={onCancel}
/>
</View>
)
})
</View>
</View>
)
})
const TokenOptionsBlock = observer(function (props: {
const TokenOptionsBlock = observer(function (props: {
toggleNostrDMModal: any
contactToSendTo?: Contact
gotoContacts: any
@@ -1724,7 +1795,7 @@ const NostDMInfoBlock = observer(function (props: {
contactToSendTo: Contact
}) {
const {walletProfileStore} = useStores()
const tokenTextColor = useThemeColor('textDim')
const amountToSendInt = round(toNumber(props.amountToSend) * getCurrency(props.unit).precision, 0)
const amountToSendDisplay = formatCurrency(amountToSendInt, getCurrency(props.unit).code)
+3 -2
View File
@@ -224,14 +224,14 @@ const getProofsToSend = function (requestedAmount: number, proofs: Proof[]): Pro
'There is not enough funds to send this amount.',
{requestedAmount, proofsAmount, caller: 'getProofsToSend'})
}
const exactMatch = findExactMatch(requestedAmount, proofs);
const exactMatch = findExactMatch(requestedAmount, proofs)
if (exactMatch) {
log.trace('[getProofsToSend] found exact match')
return exactMatch;
}
log.trace('[getProofsToSend] no exact match, fallback to findMinExcess')
return findMinExcess(requestedAmount, proofs);
return findMinExcess(requestedAmount, proofs)
}
@@ -348,6 +348,7 @@ export const CashuUtils = {
extractEncodedCashuPaymentRequest,
getProofsAmount,
getMintsFromToken,
findExactMatch,
findMinExcess,
getProofsToSend,
getProofsSubset,