diff --git a/.prettierrc.js b/.prettierrc.js index a6271df6..e6594381 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -5,4 +5,6 @@ module.exports = { singleQuote: true, trailingComma: 'all', semi: false, + useTabs: false, + tabWidth: 2, }; diff --git a/src/components/AmountInput.tsx b/src/components/AmountInput.tsx index c4addeb6..c8aae140 100644 --- a/src/components/AmountInput.tsx +++ b/src/components/AmountInput.tsx @@ -51,10 +51,12 @@ export const AmountInput = forwardRef( 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) } diff --git a/src/i18n_messages/en.json b/src/i18n_messages/en.json index 5d0b4d3c..0aae8ac1 100644 --- a/src/i18n_messages/en.json +++ b/src/i18n_messages/en.json @@ -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", diff --git a/src/i18n_messages/es.json b/src/i18n_messages/es.json index 3849e985..cb7aa3ed 100644 --- a/src/i18n_messages/es.json +++ b/src/i18n_messages/es.json @@ -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", diff --git a/src/i18n_messages/pt.json b/src/i18n_messages/pt.json index 1f36ca84..b27f3c83 100644 --- a/src/i18n_messages/pt.json +++ b/src/i18n_messages/pt.json @@ -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", diff --git a/src/i18n_messages/sk.json b/src/i18n_messages/sk.json index 19256b11..581c3ce0 100644 --- a/src/i18n_messages/sk.json +++ b/src/i18n_messages/sk.json @@ -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", diff --git a/src/screens/SendScreen.tsx b/src/screens/SendScreen.tsx index f17c5ba3..0304ac11 100644 --- a/src/screens/SendScreen.tsx +++ b/src/screens/SendScreen.tsx @@ -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 + const p2pk: { + pubkey: string; + locktime?: number; + refundKeys?: Array } | 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 ( - - - - - - - - - - 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 ( -