mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 19:28:26 +00:00
Better offline sending UX (#163)
Co-authored-by: minibits-cash <mail@minibits.cash>
This commit is contained in:
co-authored by
minibits-cash
parent
176622500e
commit
b433b21307
@@ -5,4 +5,6 @@ module.exports = {
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
semi: false,
|
||||
useTabs: false,
|
||||
tabWidth: 2,
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user