Topup and Send rewrite, fix balances

This commit is contained in:
minibits-cash
2024-05-01 21:32:38 +02:00
parent 69475b5ab0
commit 143e78c6a5
31 changed files with 1138 additions and 822 deletions
+34 -1
View File
@@ -13,7 +13,7 @@ index f2809bd..48176f1 100644
"type": "git",
"url": "git+https://github.com/cashubtc/cashu-ts"
diff --git a/node_modules/@cashu/cashu-ts/src/CashuMint.ts b/node_modules/@cashu/cashu-ts/src/CashuMint.ts
index 1491934..7a88aea 100644
index 1491934..7852bd1 100644
--- a/node_modules/@cashu/cashu-ts/src/CashuMint.ts
+++ b/node_modules/@cashu/cashu-ts/src/CashuMint.ts
@@ -17,9 +17,9 @@ import type {
@@ -29,6 +29,39 @@ index 1491934..7a88aea 100644
/**
* Class represents Cashu Mint API. This class contains Lower level functions that are implemented by CashuWallet.
@@ -81,6 +81,32 @@ class CashuMint {
async mintQuote(mintQuotePayload: MintQuotePayload): Promise<MintQuoteResponse> {
return CashuMint.mintQuote(this._mintUrl, mintQuotePayload, this._customRequest);
}
+ /**
+ * Retrieve if a mint quote has been paid
+ * @param mintUrl
+ * @param quote Quote ID.
+ * @param customRequest
+ * @returns MintQuoteResponse with paid boolean
+ */
+ public static async mintQuotePaid(
+ mintUrl: string,
+ quote: string,
+ customRequest?: typeof request
+ ): Promise<MintQuoteResponse & {paid: boolean}> {
+ const requestInstance = customRequest || request;
+ return requestInstance<MintQuoteResponse & {paid: boolean}>({
+ endpoint: joinUrls(mintUrl, '/v1/mint/quote/bolt11', quote)
+ });
+ }
+
+ /**
+ * Retrieve if a mint quote has been paid
+ * @param quote Quote ID.
+ * @returns MintQuoteResponse with paid boolean
+ */
+ async mintQuotePaid(quote: string): Promise<MintQuoteResponse & {paid: boolean}> {
+ return CashuMint.mintQuotePaid(this._mintUrl, quote, this._customRequest);
+ }
/**
* Requests the mint to perform token minting after the LN invoice has been paid
* @param mintUrl
diff --git a/node_modules/@cashu/cashu-ts/src/CashuWallet.ts b/node_modules/@cashu/cashu-ts/src/CashuWallet.ts
index 6d3a0b4..02ddb59 100644
--- a/node_modules/@cashu/cashu-ts/src/CashuWallet.ts
+4 -2
View File
@@ -101,6 +101,7 @@ export function Button(props: ButtonProps) {
{ backgroundColor: defaultBg },
preset === "secondary" && { backgroundColor: secondaryBg },
preset === "tertiary" && { backgroundColor: tertiaryBg },
!!LeftAccessory && {paddingLeft: spacing.tiny},
$viewStyleOverride,
!!pressed && [$pressedViewPresets[preset], $pressedViewStyleOverride],
]
@@ -141,8 +142,9 @@ const $baseViewStyle: ViewStyle = {
justifyContent: "center",
alignItems: "center",
flexDirection: "row",
paddingVertical: moderateVerticalScale(spacing.small),
paddingHorizontal: spacing.small,
paddingVertical: spacing.small,
paddingLeft: spacing.small,
paddingRight: spacing.small,
overflow: "hidden",
}
+1
View File
@@ -0,0 +1 @@
export const MintIcon = `<svg width="256px" height="256px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" d="M10.8321 1.24802C11.5779 0.917327 12.4221 0.917327 13.1679 1.24802L21.7995 5.0754C23.7751 5.95141 23.1703 9 21.0209 9H2.97906C0.829669 9 0.224891 5.9514 2.20047 5.0754L10.8321 1.24802ZM12.3893 3.12765C12.1407 3.01742 11.8593 3.01742 11.6107 3.12765L3.41076 6.76352C3.31198 6.80732 3.34324 6.95494 3.45129 6.95494H20.5487C20.6568 6.95494 20.688 6.80732 20.5892 6.76352L12.3893 3.12765Z"></path> <path d="M2 22C2 21.4477 2.44772 21 3 21H21C21.5523 21 22 21.4477 22 22C22 22.5523 21.5523 23 21 23H3C2.44772 23 2 22.5523 2 22Z"></path> <path d="M11 19C11 19.5523 11.4477 20 12 20C12.5523 20 13 19.5523 13 19V11C13 10.4477 12.5523 10 12 10C11.4477 10 11 10.4477 11 11V19Z"></path> <path d="M6 20C5.44772 20 5 19.5523 5 19L5 11C5 10.4477 5.44771 10 6 10C6.55228 10 7 10.4477 7 11L7 19C7 19.5523 6.55229 20 6 20Z"></path> <path d="M17 19C17 19.5523 17.4477 20 18 20C18.5523 20 19 19.5523 19 19V11C19 10.4477 18.5523 10 18 10C17.4477 10 17 10.4477 17 11V19Z" ></path> </g></svg>`
-28
View File
@@ -1,28 +0,0 @@
export const NutIcon = `<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!-- License: CC0. Made by SVG Repo: https://www.svgrepo.com/svg/101787/bean -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g transform="translate(400, -50) rotate(75)">
<g>
<path d="M498.915,156.87c-13.95-28.684-38.087-47.156-69.797-53.417c-57.873-11.43-86.542,7.008-111.835,23.278
c-18.19,11.701-33.895,21.805-61.264,21.805c-0.012,0-0.023,0-0.037,0c-27.359,0-43.07-10.106-61.259-21.807
c-25.29-16.269-53.955-34.714-111.814-23.277c-31.728,6.263-55.875,24.742-69.829,53.441
C-5.704,195.525-2.47,247.03,11.506,274.495c16.768,33.004,45.727,63.463,83.74,88.081
c48.582,31.476,107.141,49.524,160.683,49.524c0.023,0,0.051,0,0.073,0c53.567-0.002,112.158-18.05,160.744-49.515
c38.033-24.63,66.991-55.081,83.74-88.053C514.472,247.037,517.707,195.508,498.915,156.87z M470.724,259.406
c-13.887,27.339-39.501,54.031-72.125,75.157c-43.328,28.059-95.305,44.151-142.604,44.151h-0.006c-0.023,0-0.041,0-0.065,0
c-47.266,0-99.213-16.093-142.531-44.158c-32.608-21.118-58.222-47.818-72.129-75.192c-8.547-16.794-12.808-57.749,1.838-87.871
c9.335-19.196,24.901-31.068,46.274-35.287c10.209-2.018,19.11-2.909,27.052-2.909c26.791,0,42.587,10.158,60.232,21.511
c19.757,12.71,42.148,27.116,79.315,27.115c0.012-0.001,0.03,0,0.048,0c37.171,0,59.564-14.404,79.32-27.113
c22.877-14.718,42.637-27.428,87.304-18.603c21.354,4.217,36.911,16.082,46.242,35.266
C483.545,201.601,479.28,242.586,470.724,259.406z"/>
</g>
</g>
<g>
<g transform="translate(400, -80) rotate(80)">
<path d="M302.428,214.517c-36.571,16.131-59.38,7.84-72.075-1.954l-20.397,26.431c15.503,11.963,33.912,17.977,54.072,17.977
c16.349,0,33.851-3.958,51.873-11.908L302.428,214.517z"/>
</g>
</g>
</svg>`
+1 -1
View File
@@ -17,5 +17,5 @@ export * from "./ScanIcon"
export * from "./BtcIcon"
export * from "./UsdIcon"
export * from "./EurIcon"
export * from "./NutIcon"
export * from "./MintIcon"
export * from "./HeaderBg"
+9 -1
View File
@@ -6,6 +6,7 @@ import {
destroy,
isStateTreeNode,
detach,
getSnapshot,
} from 'mobx-state-tree'
import {withSetPropAction} from './helpers/withSetPropAction'
import {
@@ -23,7 +24,8 @@ import { MINIBITS_NIP05_DOMAIN } from '@env'
contacts: types.array(ContactModel),
publicPubkey: types.maybe(types.string),
lastPendingReceivedCheck: types.maybe(types.number), // UNIX timestamp
receivedEventIds: types.optional(types.array(types.string), [])
receivedEventIds: types.optional(types.array(types.string), []),
selectedContact: types.maybe(types.frozen<Contact>())
})
.actions(withSetPropAction)
.views(self => ({
@@ -99,6 +101,12 @@ import { MINIBITS_NIP05_DOMAIN } from '@env'
log.debug('[removeContact]', 'Contact removed from MintsStore')
}
},
selectContact(contact: Contact) {
self.selectedContact = getSnapshot(contact)
},
unselectContact() {
self.selectedContact = undefined
},
removeAllContacts() {
self.contacts.clear()
log.debug('[removeAllContacts]', 'Removed all Contacts from ContactsStore')
+4 -4
View File
@@ -31,9 +31,9 @@ export const PaymentRequestModel = types
status: types.frozen<PaymentRequestStatus>(),
mint: types.maybe(types.string),
mintQuote: types.maybe(types.string),
mintUnit: types.frozen<MintUnit>(), // TODO migration to set default unit for existing PRs
mintUnit: types.maybe(types.frozen<MintUnit>()), // TODO migration to set default unit for existing PRs
mintMethod: types.optional(types.frozen<MintMethod>(), MintMethod.BOLT11),
amountToTopup: types.number,
amountToTopup: types.maybe(types.number),
encodedInvoice: types.string,
invoicedAmount: types.number,
invoicedUnit: types.optional(types.frozen<MintUnit>(), 'sat'),
@@ -58,10 +58,10 @@ export type PaymentRequest = {
type: PaymentRequestType
status: PaymentRequestStatus
mint?: string
mintUnit: MintUnit
mintUnit?: MintUnit
mintQuote?: string
mintMethod?: MintMethod
amountToTopup: number
amountToTopup?: number
encodedInvoice: string
invoicedAmount: number
description?: string
+31 -9
View File
@@ -51,6 +51,7 @@ export const ProofsStoreModel = types
): Proof[] | undefined {
const proofs = options.isPending ? self.pendingProofs : self.proofs
if (options.unit) {
return proofs.filter(proof => proof.mintUrl === mintUrl && proof.unit === options.unit)
}
@@ -188,7 +189,7 @@ export const ProofsStoreModel = types
return self.pendingProofs
},
}))
.views(self => ({ // move to MintsStore?
.views(self => ({
getBalances() {
const mintBalancesMap: Map<string, MintBalance> = new Map()
const unitBalancesMap: Map<MintUnit, number> = new Map()
@@ -200,15 +201,22 @@ export const ProofsStoreModel = types
// make sure balances are defined even if we have no proofs
for (const mint of mints) {
const {mintUrl, units} = mint
const zeroBalances = Object.fromEntries(units!.map(unit => [unit, 0])) as { [key in MintUnit]: number };
mintBalancesMap.set(mintUrl, { mintUrl, balances: zeroBalances})
mintPendingBalancesMap.set(mintUrl, { mintUrl, balances: zeroBalances})
const zeroBalances = Object.fromEntries(units!.map(unit => [unit, 0])) as { [key in MintUnit]: number };
// make distinct copies of zeroBalances so that balances are not summed by reference!!!
mintBalancesMap.set(mintUrl, { mintUrl, balances: JSON.parse(JSON.stringify(zeroBalances))})
mintPendingBalancesMap.set(mintUrl, { mintUrl, balances: JSON.parse(JSON.stringify(zeroBalances))})
for (const unit of mint.units!) {
unitBalancesMap.set(unit, 0)
}
}
log.trace('[getBalances] zero balances', [...mintBalancesMap.entries()])
log.trace('[getBalances] zero pendingBalances', [...mintPendingBalancesMap.entries()])
log.trace('[getBalances] proofs count', self.proofs.length)
for (const proof of self.proofs) {
const { mintUrl, unit, amount } = proof
@@ -225,16 +233,20 @@ export const ProofsStoreModel = types
const mintBalances: MintBalance[] = Array.from(mintBalancesMap.values())
log.trace('[getBalances] filled balances', [...mintBalancesMap.entries()])
log.trace('[getBalances] zero pendingBalances', [...mintPendingBalancesMap.entries()])
// Convert map to array of UnitBalance objects
const unitBalances: UnitBalance[] = Array.from(unitBalancesMap.entries()).map(([unit, unitBalance]) => ({
unitBalance,
unit
}))
}))
for (const proof of self.pendingProofs) {
const { mintUrl, unit, amount } = proof
log.trace('adding pending proof amount', proof.amount)
// Make sure to not cause madness from orphaned proofs if it would happen
if (!mintPendingBalancesMap.has(mintUrl)) {
@@ -242,10 +254,12 @@ export const ProofsStoreModel = types
}
// Update balance for the unit
const mintBalance = mintPendingBalancesMap.get(mintUrl)!
mintBalance.balances[unit] = (mintBalance.balances[unit] || 0) + amount
const mintPendingBalance = mintPendingBalancesMap.get(mintUrl)!
mintPendingBalance.balances[unit] = (mintPendingBalance.balances[unit] || 0) + amount
unitPendingBalancesMap.set(unit, (unitPendingBalancesMap.get(unit) || 0) + amount)
}
log.trace('[getBalances] pendingBalances', [...mintPendingBalancesMap.entries()])
const mintPendingBalances: MintBalance[] = Array.from(mintPendingBalancesMap.values())
@@ -324,6 +338,14 @@ export const ProofsStoreModel = types
log.debug('[getMintBalanceWithMaxBalance]', maxBalance)
return maxBalance
},
getUnitBalance: (unit: MintUnit) => {
const balances = self.getBalances().unitBalances
const unitBalance = balances
.find((balance: UnitBalance) => balance.unit === unit)
return unitBalance
},
getProofsToSend: (amount: number, proofs: Proof[]) => {
let proofsAmount = 0
const proofSubset = proofs.filter(proof => {
+38 -2
View File
@@ -18,10 +18,15 @@ import {Database, MintClient} from '../services'
import {log} from '../services/logService'
import { getRootStore } from './helpers/getRootStore'
import { formatDistance } from 'date-fns'
import { MintUnit } from '../services/wallet/currency'
import { Mint } from './Mint'
import { ReceiveOption, SendOption } from '../screens'
import { Contact } from './Contact'
export const maxTransactionsInModel = 10
export const maxTransactionsByMint = 10
export const maxTransactionsByHostname = 3
export const maxTransactionsByUnit = 3
export type GroupedByTimeAgo = {
[timeAgo: string]: Transaction[];
@@ -30,6 +35,10 @@ export type GroupedByTimeAgo = {
export const TransactionsStoreModel = types
.model('TransactionsStore', {
transactions: types.array(TransactionModel),
/*tranUnit: types.maybe(types.frozen<MintUnit>()),
tranMint: types.maybe(types.frozen<Mint>()),
tranOption: types.maybe(types.frozen<ReceiveOption | SendOption>()),
tranContact: types.maybe(types.frozen<Contact>())*/
})
.actions(withSetPropAction)
.views(self => ({
@@ -53,7 +62,7 @@ export const TransactionsStoreModel = types
return this.all.filter(t => t.status === TransactionStatus.PENDING)
},
get groupedByTimeAgo() {
return this.all.reduce((groups: GroupedByTimeAgo, transaction: Transaction) => {
return this.all.reduce((groups: GroupedByTimeAgo, transaction: Transaction) => {
const timeAgo = formatDistance(transaction.createdAt as Date, new Date(), {addSuffix: true})
if (!groups[timeAgo]) {
groups[timeAgo] = []
@@ -63,7 +72,7 @@ export const TransactionsStoreModel = types
}, {})
},
get groupedPendingByTimeAgo() {
return this.pending.reduce((groups: GroupedByTimeAgo, transaction: Transaction) => {
return this.pending.reduce((groups: GroupedByTimeAgo, transaction: Transaction) => {
const timeAgo = formatDistance(transaction.createdAt as Date, new Date(), {addSuffix: true})
if (!groups[timeAgo]) {
groups[timeAgo] = []
@@ -79,6 +88,9 @@ export const TransactionsStoreModel = types
recentByHostname(mintHostname: string) {
return this.all.filter(t => getHostname(t.mint as string) === mintHostname).slice(0, maxTransactionsByHostname)
},
recentByUnit(unit: MintUnit) {
return this.all.filter(t => t.unit === unit).slice(0, maxTransactionsByUnit)
},
recentByHostnameGroupedByTimeAgo(mintHostname: string) {
const recentByHostname = this.recentByHostname(mintHostname)
@@ -328,6 +340,30 @@ export const TransactionsStoreModel = types
self.transactions.clear()
log.debug('[removeAllTransactions]', 'Removed all transactions from TransactionsStore')
},
/* setTranUnit(unit: MintUnit) {
self.tranUnit = unit
},
setTranMint(mint: Mint) {
self.tranMint = mint
},
setTranOption(option: ReceiveOption | SendOption) {
self.tranOption = option
},
setTranContact(contact: Contact) {
self.tranContact = contact
},
unsetTranUnit() {
self.tranUnit = undefined
},
unsetTranMint(mint: Mint) {
self.tranMint = undefined
},
unsetTranOption(option: ReceiveOption | SendOption) {
self.tranOption = undefined
},
unsetTranContact(contact: Contact) {
self.tranContact = undefined
},*/
}))
const getHostname = function (mintUrl: string) {
+19 -15
View File
@@ -120,33 +120,37 @@ export function TabsNavigator() {
export type WalletStackParamList = {
Wallet: {scannedMintUrl? : string, returnWithNavigationReset?: boolean}
ReceiveOptions: undefined
ReceiveOptions: {unit: MintUnit}
Receive: {encodedToken? : string}
SendOptions: undefined
Send: {contact?: Contact, relays?: string[], paymentOption?: SendOption}
SendOptions: {unit: MintUnit}
Send: {
unit: MintUnit,
paymentOption?: SendOption,
contact?: Contact,
mintUrl?: string,
}
Scan: undefined
LightningPay: {
mintUrl?: string,
unit?: MintUnit
unit: MintUnit
mintUrl?: string,
}
TranDetail: {id: number}
TranHistory: undefined
PaymentRequests: undefined
Transfer: {
encodedInvoice?: string,
unit: MintUnit,
encodedInvoice?: string,
paymentRequest?: PaymentRequest,
lnurlParams?: LNURLPayParams & {address?: string},
paymentOption?: SendOption,
mintUrl?: string,
unit?: MintUnit
mintUrl?: string,
}
Topup: {
contact?: Contact,
relays?: string[],
paymentOption?: ReceiveOption,
Topup: {
unit: MintUnit,
paymentOption?: ReceiveOption,
contact?: Contact,
lnurlParams?: LNURLWithdrawParams,
mintUrl?: string,
unit?: MintUnit
mintUrl?: string,
}
ContactsNavigator: {screen: string, params: any}
SettingsNavigator: {screen: string, params: any}
@@ -192,7 +196,7 @@ export type ContactsStackParamList = {
WalletName: undefined
RandomName: {navigation: any}
OwnName: {navigation: any}
ContactDetail: {contact: Contact, relays: string[]}
ContactDetail: {contact: Contact}
OwnKeys: undefined
WalletNavigator: {screen: string, params: any}
SettingsNavigator: {screen: string}
+3 -3
View File
@@ -40,7 +40,7 @@ export const BackupScreen: FC<SettingsStackScreenProps<'Backup'>> = observer(fun
const [info, setInfo] = useState('')
const [isBackupModalVisible, setIsBackupModalVisible] =
useState<boolean>(false)
const [isHandleSpentFromSpendavleSentToQueue, setIsHandleSpentFromSpendavleSentToQueue] = useState<boolean>(false)
const [isHandleSpentFromSpendableSentToQueue, setIsHandleSpentFromSpendableSentToQueue] = useState<boolean>(false)
const [backupResultMessage, setBackupResultMessage] = useState<string>()
const [totalSpentCount, setTotalSpentCount] = useState<number>(0)
const [totalSpentAmount, setTotalSpentAmount] = useState<number>(0)
@@ -72,7 +72,7 @@ export const BackupScreen: FC<SettingsStackScreenProps<'Backup'>> = observer(fun
return () => {
EventEmitter.off('ev__handleSpentByMintTask_result', handleSpentByMintTaskResult)
}
}, [isHandleSpentFromSpendavleSentToQueue])
}, [isHandleSpentFromSpendableSentToQueue])
const toggleBackupSwitch = () => {
try {
@@ -124,7 +124,7 @@ export const BackupScreen: FC<SettingsStackScreenProps<'Backup'>> = observer(fun
const checkSpent = async function () {
setIsLoading(true)
setIsHandleSpentFromSpendavleSentToQueue(true)
setIsHandleSpentFromSpendableSentToQueue(true)
WalletTask.handleSpentFromSpendable()
}
+7 -7
View File
@@ -21,7 +21,7 @@ interface ContactDetailScreenProps extends ContactsStackScreenProps<'ContactDeta
export const ContactDetailScreen: FC<ContactDetailScreenProps> = observer(
function ContactScreen({route, navigation}) {
const {contact, relays} = route.params
const {contact} = route.params
const {contactsStore} = useStores()
const noteInputRef = useRef<TextInput>(null)
@@ -50,24 +50,24 @@ export const ContactDetailScreen: FC<ContactDetailScreenProps> = observer(
const gotoTopup = () => {
navigation.navigate('WalletNavigator', {
screen: 'Topup',
params: {
paymentOption: ReceiveOption.SEND_PAYMENT_REQUEST,
contact,
relays
paymentOption: ReceiveOption.SEND_PAYMENT_REQUEST,
contact
},
})
}
const gotoSend = () => {
navigation.navigate('WalletNavigator', {
screen: 'Send',
params: {
paymentOption: SendOption.SEND_TOKEN,
contact,
relays
paymentOption: SendOption.SEND_TOKEN,
contact
},
})
}
+15 -27
View File
@@ -17,12 +17,13 @@ import { ReceiveOption } from '../ReceiveOptionsScreen'
import { SendOption } from '../SendOptionsScreen'
import { infoMessage, warningMessage } from '../../utils/utils'
import { IncomingDataType, IncomingParser } from '../../services/incomingParser'
import { RouteProp } from '@react-navigation/native'
export const PrivateContacts = observer(function (props: {
navigation: StackNavigationProp<ContactsStackParamList, "Contacts", undefined>,
paymentOption: ReceiveOption | SendOption | undefined}
navigation: StackNavigationProp<ContactsStackParamList, "Contacts", undefined>,
paymentOption: ReceiveOption | SendOption | undefined},
) {
const {contactsStore, relaysStore} = useStores()
const {navigation} = props
@@ -39,15 +40,15 @@ export const PrivateContacts = observer(function (props: {
const { paymentOption } = props
if (paymentOption && paymentOption === ReceiveOption.SEND_PAYMENT_REQUEST) {
infoMessage('Select contact to send your payment request to.')
// infoMessage('Select contact to send your payment request to.')
}
if (paymentOption && paymentOption === SendOption.SEND_TOKEN) {
infoMessage('Select contact to send your ecash to.')
// infoMessage('Select contact to send your ecash to.')
}
if (paymentOption && paymentOption === SendOption.LNURL_ADDRESS) {
infoMessage('Select contact to send Lightning payment to.')
// infoMessage('Select contact to send Lightning payment to.')
}
}, [])
@@ -160,28 +161,22 @@ export const PrivateContacts = observer(function (props: {
log.trace('paymentOption', {paymentOption}, 'gotoContactDetail')
const relays = relaysStore.allUrls
if(paymentOption && paymentOption === ReceiveOption.SEND_PAYMENT_REQUEST) { // Topup tx contact selection
setIsLoading(true)
if(contact.nip05) {
await NostrClient.verifyNip05(contact.nip05 as string, contact.pubkey) // throws
}
// contactsStore.selectContact(contact)
navigation.navigate('WalletNavigator', {
screen: 'Topup',
params: {
paymentOption,
contact,
relays // TODO remove, switch to relaysStore
},
})
//reset
navigation.setParams({
paymentOption: undefined,
paymentOption,
contact
},
})
setIsLoading(false)
return
}
@@ -198,15 +193,10 @@ export const PrivateContacts = observer(function (props: {
screen: 'Send',
params: {
paymentOption,
contact,
relays // TODO remove, switch to relaysStore
contact
},
})
//reset
navigation.setParams({
paymentOption: undefined,
})
setIsLoading(false)
return
}
@@ -231,10 +221,8 @@ export const PrivateContacts = observer(function (props: {
return
}
navigation.navigate('ContactDetail', {
contact,
relays // TODO remove, switch to relaysStore
})
navigation.navigate('ContactDetail', {contact})
} catch (e: any) {
// reset so that invalid contact can be deleted
navigation.setParams({
+23 -29
View File
@@ -17,6 +17,7 @@ import { ReceiveOption } from '../ReceiveOptionsScreen'
import { useSafeAreaInsetsStyle } from '../../utils/useSafeAreaInsetsStyle'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { IncomingDataType, IncomingParser } from '../../services/incomingParser'
import { RouteProp } from '@react-navigation/native'
// const defaultPublicNpub = 'npub14n7frsyufzqsxlvkx8vje22cjah3pcwnnyqncxkuj2243jvt9kmqsdgs52'
@@ -341,44 +342,38 @@ export const PublicContacts = observer(function (props: {
const gotoContactDetail = async function (contact: Contact) {
const {paymentOption} = props
contact.type = ContactType.PUBLIC // ???
const relays = relaysStore.allPublicUrls
contact.type = ContactType.PUBLIC
if(paymentOption && paymentOption === ReceiveOption.SEND_PAYMENT_REQUEST) {
navigation.navigate('WalletNavigator', {
screen: 'Topup',
params: {
paymentOption,
contact,
relays // TODO remove, switch to relaysStore
},
})
//reset
navigation.setParams({
paymentOption: undefined,
})
if(paymentOption && paymentOption === ReceiveOption.SEND_PAYMENT_REQUEST) { // Topup tx contact selection
/*if(contact.nip05) { // test ifworks and makes sense
await NostrClient.verifyNip05(contact.nip05 as string, contact.pubkey) // throws
}*/
if(paymentOption === ReceiveOption.SEND_PAYMENT_REQUEST) {
navigation.navigate('WalletNavigator', {
screen: 'Topup',
params: {
paymentOption,
contact
},
})
}
return
}
if(paymentOption && paymentOption === SendOption.SEND_TOKEN) {
if(paymentOption && paymentOption === SendOption.SEND_TOKEN) {
navigation.navigate('WalletNavigator', {
screen: 'Send',
params: {
paymentOption,
contact,
relays // TODO remove, switch to relaysStore
contact
},
})
//reset
navigation.setParams({
paymentOption: undefined,
})
return
}
@@ -410,8 +405,7 @@ export const PublicContacts = observer(function (props: {
log.trace('[gotoContactDetail]', contact)
navigation.navigate('ContactDetail', {
contact,
relays // TODO remove, switch to relaysStore
contact
})
}
+47 -17
View File
@@ -11,22 +11,25 @@ import {useHeader} from '../utils/useHeader'
import {log} from '../services/logService'
import { IncomingDataType, IncomingParser } from '../services/incomingParser'
import AppError, { Err } from '../utils/AppError'
import { Button, Card, ErrorModal, Icon, ListItem, ScanIcon, Screen, Text } from '../components'
import { Button, Card, ErrorModal, Header, Icon, ListItem, ScanIcon, Screen, Text } from '../components'
import { LnurlUtils } from '../services/lnurl/lnurlUtils'
import { infoMessage } from '../utils/utils'
import Clipboard from '@react-native-clipboard/clipboard'
import { SvgXml } from 'react-native-svg'
import { SendOption } from './SendOptionsScreen'
import { CurrencyCode, MintUnit } from '../services/wallet/currency'
import { CurrencySign } from './Wallet/CurrencySign'
import { useStores } from '../models'
import { setMinutes } from 'date-fns'
import { Mint } from '../models/Mint'
import { CurrencyAmount } from './Wallet/CurrencyAmount'
import { MintHeader } from './Mints/MintHeader'
export const LightningPayScreen: FC<WalletStackScreenProps<'LightningPay'>> = function LightningPayScreen(_props) {
const {navigation} = _props
useHeader({
leftIcon: 'faArrowLeft',
onLeftPress: () => navigation.goBack(),
})
const lightningInputRef = useRef<TextInput>(null)
const {navigation, route} = _props
const lightningInputRef = useRef<TextInput>(null)
const {mintsStore} = useStores()
useEffect(() => {
const focus = () => {
@@ -40,10 +43,28 @@ export const LightningPayScreen: FC<WalletStackScreenProps<'LightningPay'>> = fu
}
}, [])
useEffect(() => {
const setUnitAndMint = () => {
if(route.params && route.params.unit && route.params.mintUrl) {
const mint = mintsStore.findByUrl(route.params.mintUrl)
const unit = route.params.unit
setMint(mint)
setUnit(unit)
}
}
setUnitAndMint()
return () => {}
}, [])
const [prevRouteName, setPrevRouteName] = useState<string>('')
const [lightningData, setLightningData] = useState<string | undefined>(undefined)
const [lightningData, setLightningData] = useState<string | undefined>(undefined)
const [unit, setUnit] = useState<MintUnit | undefined>(undefined)
const [mint, setMint] = useState<Mint | undefined>(undefined)
const [error, setError] = useState<AppError | undefined>()
@@ -80,7 +101,7 @@ export const LightningPayScreen: FC<WalletStackScreenProps<'LightningPay'>> = fu
try {
const invoiceResult = IncomingParser.findAndExtract(lightningData as string, IncomingDataType.INVOICE)
return IncomingParser.navigateWithIncomingData(invoiceResult, navigation)
return IncomingParser.navigateWithIncomingData(invoiceResult, navigation, mint && unit && {mintUrl: mint.mintUrl, unit})
} catch (e: any) {
const maybeLnurlAddress = LnurlUtils.findEncodedLnurlAddress(lightningData as string)
@@ -93,8 +114,8 @@ export const LightningPayScreen: FC<WalletStackScreenProps<'LightningPay'>> = fu
if(validAddress) {
await IncomingParser.navigateWithIncomingData({
type: IncomingDataType.LNURL_ADDRESS,
encoded: validAddress
}, navigation)
encoded: validAddress,
}, navigation, mint && unit && {mintUrl: mint.mintUrl, unit})
}
return
} catch (e3: any) {
@@ -114,7 +135,7 @@ export const LightningPayScreen: FC<WalletStackScreenProps<'LightningPay'>> = fu
await IncomingParser.navigateWithIncomingData({
type: IncomingDataType.LNURL,
encoded: encodedLnurl
}, navigation)
}, navigation, mint && unit && {mintUrl: mint.mintUrl, unit})
}
return
} catch (e2: any) {
@@ -136,11 +157,20 @@ export const LightningPayScreen: FC<WalletStackScreenProps<'LightningPay'>> = fu
}
const hintText = useThemeColor('textDim')
const scanIcon = useThemeColor('text')
const inputBg = useThemeColor('background')
const contactIcon = useThemeColor('button')
const headerBg = useThemeColor('header')
return (
<Screen preset="fixed" contentContainerStyle={$screen}>
<MintHeader
mint={mint}
unit={unit}
navigation={navigation}
/>
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
<Text
preset="heading"
@@ -160,10 +190,10 @@ export const LightningPayScreen: FC<WalletStackScreenProps<'LightningPay'>> = fu
RightComponent={
<Button
preset='tertiary'
LeftAccessory={() => <Icon containerStyle={{paddingVertical: 0}} icon='faAddressBook' />}
LeftAccessory={() => <Icon color={contactIcon} containerStyle={{paddingVertical: 0}} icon='faAddressBook' />}
onPress={gotoContacts}
text='Contacts'
textStyle={{fontSize: 12, color: hintText}}
textStyle={{fontSize: 12, color: contactIcon}}
/>
}
/>
@@ -203,7 +233,7 @@ export const LightningPayScreen: FC<WalletStackScreenProps<'LightningPay'>> = fu
tx={'common.confirm'}
onPress={onConfirm}
style={{marginLeft: spacing.small}}
LeftAccessory={() => <Icon icon='faCheckCircle'/>}
LeftAccessory={() => <Icon icon='faCheckCircle' color='white'/>}
/>
<Button
preset='secondary'
@@ -216,7 +246,7 @@ export const LightningPayScreen: FC<WalletStackScreenProps<'LightningPay'>> = fu
width={spacing.medium}
height={spacing.medium}
xml={ScanIcon}
fill='white'
fill={scanIcon}
style={{marginHorizontal: spacing.extraSmall}}
/>
)
+129
View File
@@ -0,0 +1,129 @@
import React, { useEffect, useState } from "react"
import { FlatList, LayoutAnimation, View, ViewStyle } from "react-native"
import { observer } from "mobx-react-lite"
import { Button, Card} from "../../components"
import { spacing } from "../../theme"
import { MintBalance } from "../../models/Mint"
import { MintUnit } from "../../services/wallet/currency"
import { useStores } from "../../models"
import { log } from "../../services"
import { MintListItem } from "./MintListItem"
export const MintBalanceSelector = observer(function (props: {
mintBalances: MintBalance[]
selectedMintBalance: MintBalance
unit: MintUnit
title: string
confirmTitle: string
onMintBalanceSelect: any
onCancel: any
onMintBalanceConfirm: any
}) {
const {mintsStore} = useStores()
log.trace('[MintBalanceSelector]', props.selectedMintBalance.mintUrl)
useEffect(() => {
if(!props.selectedMintBalance) {
LayoutAnimation.Presets.easeInEaseOut
setAllVisible(true)
}
}, [])
const [allVisible, setAllVisible] = useState<boolean>(false)
const toggleAllVisible = function () {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
setAllVisible(!allVisible)
}
const onMintSelect = function (balance: MintBalance) {
log.trace('onMintBalanceSelect', balance.mintUrl)
return props.onMintBalanceSelect(balance)
}
return (
<View style={{flex: 1}}>
<Card
style={$card}
heading={props.title}
headingStyle={{textAlign: 'center', padding: spacing.small}}
ContentComponent={
<>
<FlatList<MintBalance>
data={props.mintBalances}
renderItem={({ item, index }) => {
return(
<MintListItem
key={item.mintUrl}
mint={mintsStore.findByUrl(item.mintUrl)!}
mintBalance={item}
selectedUnit={props.unit}
onMintSelect={() => onMintSelect(item)}
isSelectable={true}
isSelected={props.selectedMintBalance.mintUrl === item.mintUrl}
separator={'top'}
style={(!allVisible && props.selectedMintBalance.mintUrl !== item.mintUrl) ? {display: 'none'} : {}}
/>
)
}}
keyExtractor={(item) => item.mintUrl}
style={{ flexGrow: 0, maxHeight: spacing.screenHeight * 0.35 }}
ListFooterComponent={props.mintBalances.length > 1 ? (
<View>
<Button
text={`${allVisible ? 'Hide' : 'Show'} other`}
onPress={toggleAllVisible}
preset='tertiary'
/>
</View>
) : undefined}
/>
</>
}
/>
<View style={$bottomContainer}>
<View style={[$buttonContainer, {marginTop: spacing.large}]}>
<Button
text={props.confirmTitle}
onPress={props.onMintBalanceConfirm}
style={{marginRight: spacing.medium}}
/>
<Button
preset="secondary"
tx={'common.cancel'}
onPress={props.onCancel}
/>
</View>
</View>
</View>
)
})
const $card: ViewStyle = {
marginBottom: spacing.small,
paddingTop: 0,
}
const $buttonContainer: ViewStyle = {
flexDirection: 'row',
alignSelf: 'center',
}
const $bottomContainer: ViewStyle = {
// position: 'absolute',
bottom: 0,
left: 0,
right: 0,
flex: 1,
justifyContent: 'flex-end',
marginBottom: spacing.medium,
alignSelf: 'stretch',
// opacity: 0,
}
+51
View File
@@ -0,0 +1,51 @@
import React from "react"
import { TextStyle, View, ViewStyle } from "react-native"
import { Button, Header, Icon, IconTypes, ListItem, Screen, Text } from "../../components"
import { colors, spacing, typography, useThemeColor } from "../../theme"
import { Mint } from "../../models/Mint"
import { MintUnit } from "../../services/wallet/currency"
import { CurrencySign } from "../Wallet/CurrencySign"
import { CurrencyAmount } from "../Wallet/CurrencyAmount"
import { observer } from "mobx-react-lite"
import { StackNavigationProp } from "@react-navigation/stack"
export const MintHeader = observer(function(props: {
mint?: Mint,
unit?: MintUnit,
navigation: StackNavigationProp<any>
}
) {
const {mint, unit, navigation} = props
return (
<Header
TitleActionComponent={
<>
<Text
text={mint && mint.shortname}
style={{color: 'white'}}
size='xxs'
/>
<CurrencySign
mintUnit={unit && unit}
textStyle={{color: 'white'}}
/>
</>
}
leftIcon='faArrowLeft'
onLeftPress={() => navigation.goBack()}
RightActionComponent={mint && unit ? (
<CurrencyAmount
mintUnit={unit}
amount={mint?.balances?.balances[unit as MintUnit] || 0}
amountStyle={{color: 'white'}}
symbolStyle={{color: 'white'}}
containerStyle={{marginRight: spacing.medium}}
size='medium'
/>
) : undefined}
/>
)
})
+8 -7
View File
@@ -4,7 +4,7 @@ import { ListItem, Text } from "../../components"
import { Mint, MintStatus } from "../../models/Mint"
import { MintBalance } from "../../models/Mint"
import { colors, spacing, typography, useThemeColor } from "../../theme"
import { TextStyle } from "react-native"
import { TextStyle, ViewStyle } from "react-native"
import { CurrencySign } from "../Wallet/CurrencySign"
import { CurrencyCode, MintUnit, MintUnitCurrencyPairs, MintUnits } from "../../services/wallet/currency"
import { log } from "../../services"
@@ -18,7 +18,8 @@ export const MintListItem = observer(function(props: {
isSelectable: boolean,
isBlocked?: boolean,
isUnitVisible?: boolean,
separator: 'bottom' | 'top' | 'both' | undefined
separator?: 'bottom' | 'top' | 'both'
style?: ViewStyle
}) {
const iconSelectedColor = useThemeColor('button')
@@ -34,26 +35,26 @@ export const MintListItem = observer(function(props: {
margin: spacing.extraSmall
}
const {mint, mintBalance, selectedUnit, onMintSelect, isSelected, isSelectable, isBlocked, isUnitVisible, separator} = props
const {mint, mintBalance, selectedUnit, onMintSelect, isSelected, isSelectable, isBlocked, isUnitVisible, separator, style} = props
// log.trace('[MintListItem]', props)
return (
<ListItem
key={mint.mintUrl}
text={mint.hostname || ''}
subText={mint.shortname || ''}
text={mint.shortname}
subText={mint.hostname}
leftIcon={isSelectable ? isSelected ? 'faCheckCircle' : 'faCircle' : undefined}
leftIconColor={isSelected ? iconSelectedColor as string : iconColor as string}
rightIcon={isBlocked ? 'faShieldHalved' : mint.status === MintStatus.OFFLINE ? 'faTriangleExclamation' : undefined}
rightIconColor={isBlocked ? iconBlockedColor : iconColor as string}
onPress={onMintSelect ? () => onMintSelect(mint, mintBalance) : undefined}
RightComponent={mintBalance && selectedUnit && <Text text={`${mintBalance?.balances[selectedUnit]}`} style={{alignSelf: 'center', marginRight: spacing.medium}}/>}
BottomComponent={isUnitVisible && mint.units ? (<>{mint.units.map(unit => <CurrencySign containerStyle={{paddingLeft: 0, marginRight: spacing.small}} key={unit} currencyCode={MintUnitCurrencyPairs[unit]}/>)}</>) : undefined}
style={{paddingHorizontal: spacing.tiny}}
BottomComponent={isUnitVisible && mint.units ? (<>{mint.units.map(unit => <CurrencySign containerStyle={{paddingLeft: 0, marginRight: spacing.small}} key={unit} currencyCode={MintUnitCurrencyPairs[unit]}/>)}</>) : undefined}
containerStyle={{alignSelf: 'stretch'}}
bottomSeparator={separator === 'bottom' || separator === 'both'}
topSeparator={separator === 'top' || separator === 'both'}
style={[{paddingHorizontal: spacing.tiny}, style]}
/>
)})
+5 -1
View File
@@ -19,6 +19,7 @@ import AppError from '../utils/AppError'
import useIsInternetReachable from '../utils/useIsInternetReachable'
export enum ReceiveOption {
// CREATE_AND_SEND_PAYMENT_REQUEST = 'CREATE_AND_SEND_PAYMENT_REQUEST',
SEND_PAYMENT_REQUEST = 'SEND_PAYMENT_REQUEST',
PASTE_OR_SCAN_TOKEN = 'PASTE_OR_SCAN_TOKEN',
SHOW_INVOICE = 'SHOW_INVOICE',
@@ -41,7 +42,10 @@ export const ReceiveOptionsScreen: FC<WalletStackScreenProps<'ReceiveOptions'>>
const gotoContacts = function () {
navigation.navigate('ContactsNavigator', {
screen: 'Contacts',
params: {paymentOption: ReceiveOption.SEND_PAYMENT_REQUEST}})
params: {
paymentOption: ReceiveOption.SEND_PAYMENT_REQUEST
}
})
}
+179 -244
View File
@@ -54,13 +54,15 @@ import EventEmitter from '../utils/eventEmitter'
import {ResultModalInfo} from './Wallet/ResultModalInfo'
import useIsInternetReachable from '../utils/useIsInternetReachable'
import { Proof } from '../models/Proof'
import { Contact } from '../models/Contact'
import { Contact, ContactType } from '../models/Contact'
import { getImageSource, infoMessage } from '../utils/utils'
import { NotificationService } from '../services/notificationService'
import { SendOption } from './SendOptionsScreen'
import { moderateVerticalScale, verticalScale } from '@gocodingnow/rn-size-matters'
import { CurrencySign } from './Wallet/CurrencySign'
import { CurrencyCode, MintUnit, MintUnits } from "../services/wallet/currency"
import { MintHeader } from './Mints/MintHeader'
import { MintBalanceSelector } from './Mints/MintBalanceSelector'
if (Platform.OS === 'android' &&
@@ -71,14 +73,9 @@ if (Platform.OS === 'android' &&
export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
function SendScreen({route, navigation}) {
useHeader({
leftIcon: 'faArrowLeft',
onLeftPress: () => navigation.goBack(),
})
const isInternetReachable = useIsInternetReachable()
const {proofsStore, walletProfileStore, transactionsStore} = useStores()
const {proofsStore, walletProfileStore, transactionsStore, mintsStore, relaysStore} = useStores()
const amountInputRef = useRef<TextInput>(null)
const memoInputRef = useRef<TextInput>(null)
@@ -125,46 +122,78 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
}, [])
useEffect(() => {
const { mintUrl, unit } = route.params
const setSelectedMintandUnit = () => {
log.trace('[setSelectedMintandUnit]', mintUrl)
setUnit(unit)
setMintBalanceToSendFrom(proofsStore.getMintBalance(mintUrl!))
}
if(mintUrl && unit) {
setSelectedMintandUnit()
}
}, [])
// Send to contact
useFocusEffect(
useCallback(() => {
const {paymentOption} = route.params
const {paymentOption, contact} = route.params
const prepareSendToken = () => {
log.trace('prepareSendToken start')
const {contact, relays} = route.params
const prepareSendToContact = () => {
try {
let relays: string[] = []
log.trace('[prepareSendToContact] selected contact', contact, paymentOption)
if(contact?.type === ContactType.PUBLIC) {
relays = relaysStore.allPublicUrls
} else {
relays = relaysStore.allUrls
}
if (!relays) {
throw new AppError(Err.VALIDATION_ERROR, 'Missing NOSTR relays')
}
const {
pubkey,
npub,
name,
picture,
} = walletProfileStore
if (!contact || !relays) {
throw new AppError(Err.VALIDATION_ERROR, 'Missing contact or relay')
const contactFrom: Contact = {
pubkey,
npub,
name,
picture
}
setPaymentOption(SendOption.SEND_TOKEN)
setContactToSendFrom(contactFrom)
setContactToSendTo(contact)
setRelaysToShareTo(relays)
if(encodedTokenToSend) {
toggleNostrDMModal() // open if we already have an invoice
}
//reset
navigation.setParams({
paymentOption: undefined,
contact: undefined
})
} catch(e: any) {
handleError(e)
}
const {
pubkey,
npub,
name,
picture,
} = walletProfileStore
const contactFrom: Contact = {
pubkey,
npub,
name,
picture
}
setPaymentOption(SendOption.SEND_TOKEN)
setContactToSendFrom(contactFrom)
setContactToSendTo(contact)
setRelaysToShareTo(relays)
navigation.setParams({contact: undefined})
navigation.setParams({relays: undefined})
}
if(paymentOption && paymentOption === SendOption.SEND_TOKEN) {
prepareSendToken()
if(paymentOption && contact && paymentOption === SendOption.SEND_TOKEN) {
prepareSendToContact()
}
}, [route.params?.paymentOption])
@@ -307,8 +336,8 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
setAvailableMintBalances(availableBalances)
// Default mint with highest balance to topup
setMintBalanceToSendFrom(availableBalances[0])
// Default mint if not set from route params is with the one with highest balance
if(!mintBalanceToSendFrom) {setMintBalanceToSendFrom(availableBalances[0])}
setIsAmountEndEditing(true)
// We do not make memo focus mandatory
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
@@ -520,6 +549,15 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
}
const gotoContacts = function () {
navigation.navigate('ContactsNavigator', {
screen: 'Contacts',
params: {
paymentOption: SendOption.SEND_TOKEN
}})
}
const resetState = function () {
// reset state so it does not interfere next payment
setAmountToSend('')
@@ -555,12 +593,13 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
return (
<Screen preset="fixed" contentContainerStyle={$screen}>
<MintHeader
mint={mintBalanceToSendFrom ? mintsStore.findByUrl(mintBalanceToSendFrom?.mintUrl) : undefined}
unit={unit}
navigation={navigation}
/>
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
<View style={$amountContainer}>
<CurrencySign
currencyCode={MintUnits[unit as any] as CurrencyCode}
textStyle={{color: 'white'}}
/>
<TextInput
ref={amountInputRef}
onChangeText={amount => setAmountToSend(amount)}
@@ -584,6 +623,7 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
</View>
</View>
<View style={$contentContainer}>
{!encodedTokenToSend && (
<Card
style={$memoCard}
ContentComponent={
@@ -618,6 +658,7 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
</View>
}
/>
)}
{!isInternetReachable && !isMemoEndEditing && (
<Button
preset="secondary"
@@ -632,25 +673,31 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
/>
)}
{isMintSelectorVisible &&(
{isMintSelectorVisible && (
<MintBalanceSelector
availableMintBalances={availableMintBalances}
mintBalanceToSendFrom={mintBalanceToSendFrom as MintBalance}
mintBalances={availableMintBalances}
selectedMintBalance={mintBalanceToSendFrom as MintBalance}
unit={unit}
title='Send from mint'
confirmTitle='Create token'
onMintBalanceSelect={onMintBalanceSelect}
onCancel={onMintBalanceCancel}
onCancel={onMintBalanceCancel}
onMintBalanceConfirm={onMintBalanceConfirm}
/>
)}
{transactionStatus === TransactionStatus.PENDING && encodedTokenToSend && paymentOption && (
<SelectedMintBlock
toggleNostrDMModal={toggleNostrDMModal}
toggleQRModal={toggleQRModal}
paymentOption={paymentOption}
encodedTokenToSend={encodedTokenToSend}
mintBalanceToSendFrom={mintBalanceToSendFrom as MintBalance}
gotoWallet={resetState}
/>
<>
<SendAsQRCodeBlock
encodedTokenToSend={encodedTokenToSend as string}
onShareToApp={onShareToApp}
onCopy={onCopy}
/>
<TokenOptionsBlock
toggleNostrDMModal={toggleNostrDMModal}
contactToSendTo={contactToSendTo}
gotoContacts={gotoContacts}
/>
</>
)}
{(transactionStatus === TransactionStatus.COMPLETED) && (
<View style={$bottomContainer}>
@@ -683,20 +730,6 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
onBackButtonPress={toggleProofSelectorModal}
onBackdropPress={toggleProofSelectorModal}
/>
<BottomModal
isVisible={isQRModalVisible ? true : false}
ContentComponent={
<SendAsQRCodeBlock
toggleQRModal={toggleQRModal}
encodedTokenToSend={encodedTokenToSend as string}
onCopy={onCopy}
onShareToApp={onShareToApp}
// handleError={handleError}
/>
}
onBackButtonPress={toggleQRModal}
onBackdropPress={toggleQRModal}
/>
<BottomModal
isVisible={isNostrDMModalVisible ? true : false}
ContentComponent={
@@ -777,77 +810,6 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
}
)
const MintBalanceSelector = observer(function (props: {
availableMintBalances: MintBalance[]
mintBalanceToSendFrom: MintBalance
unit: MintUnit
onMintBalanceSelect: any
onCancel: any
onMintBalanceConfirm: any
}) {
const onMintSelect = function (balance: MintBalance) {
log.trace('onMintBalanceSelect', balance.mintUrl)
return props.onMintBalanceSelect(balance)
}
const {mintsStore} = useStores()
return (
<View style={{flex: 1}}>
<Card
style={$card}
heading={'Pay from'}
headingStyle={{textAlign: 'center', padding: spacing.small}}
ContentComponent={
<>
<FlatList<MintBalance>
data={props.availableMintBalances}
renderItem={({ item, index }) => {
return(
<MintListItem
key={item.mintUrl}
mint={mintsStore.findByUrl(item.mintUrl) as Mint}
mintBalance={item}
selectedUnit={props.unit}
onMintSelect={() => onMintSelect(item)}
isSelectable={true}
isSelected={props.mintBalanceToSendFrom.mintUrl === item.mintUrl}
separator={'top'}
/>
)
}}
keyExtractor={(item) => item.mintUrl}
style={{ flexGrow: 0, maxHeight: spacing.screenHeight * 0.35 }}
/>
</>
}
/>
<View style={$bottomContainer}>
<View style={[$buttonContainer, {marginTop: spacing.large}]}>
<Button
text="Send now"
onPress={props.onMintBalanceConfirm}
style={{marginRight: spacing.medium}}
LeftAccessory={() => (
<Icon
icon="faArrowUp"
color="white"
size={spacing.medium}
/>
)}
// LeftAccessory={() => <Icon icon="faCoins" color="white" size={spacing.medium} containerStyle={{marginRight: spacing.small}}/>}
/>
<Button
preset="secondary"
tx={'common.cancel'}
onPress={props.onCancel}
/>
</View>
</View>
</View>
)
})
const SelectProofsBlock = observer(function (props: {
availableMintBalances: MintBalance[]
@@ -951,93 +913,60 @@ const SelectProofsBlock = observer(function (props: {
</View>
</View>
)
}
}
})
const SelectedMintBlock = observer(function (props: {
const TokenOptionsBlock = observer(function (props: {
toggleNostrDMModal: any
toggleQRModal: any
encodedTokenToSend: string
paymentOption: SendOption
mintBalanceToSendFrom: MintBalance
gotoWallet: any
contactToSendTo?: Contact
gotoContacts: any
}) {
const {mintsStore} = useStores()
const sendBg = useThemeColor('card')
const tokenTextColor = useThemeColor('textDim')
return (
<View style={{flex: 1}}>
<Card
style={$card}
heading={'Send from'}
headingStyle={{textAlign: 'center', padding: spacing.small}}
ContentComponent={
<MintListItem
mint={
mintsStore.findByUrl(
props.mintBalanceToSendFrom?.mintUrl as string,
) as Mint
}
isSelectable={false}
separator={'top'}
/>
}
/>
return (
<View style={{flex: 1}}>
<View style={$bottomContainer}>
<View style={$buttonContainer}>
<Button
text='QR code'
preset='secondary'
onPress={props.toggleQRModal}
LeftAccessory={() => (
<Icon
icon='faQrcode'
//color="white"
size={spacing.medium}
/>
)}
/>
{props.paymentOption === SendOption.SEND_TOKEN && (
{props.contactToSendTo ? (
<Button
text='Send to contact'
text={`Send to ${props.contactToSendTo.nip05}`}
preset='secondary'
onPress={props.toggleNostrDMModal}
style={{marginLeft: spacing.small}}
style={{marginLeft: spacing.medium}}
LeftAccessory={() => (
<Icon
icon='faPaperPlane'
//color="white"
// color="white"
size={spacing.medium}
/>
)}
/>
)}
) : (
<Button
preset="secondary"
tx={'common.close'}
onPress={props.gotoWallet}
style={{marginLeft: spacing.small}}
text='Send to contact'
preset='secondary'
onPress={props.gotoContacts}
style={{marginLeft: spacing.medium}}
LeftAccessory={() => (
<Icon
icon='faPaperPlane'
// color="white"
size={spacing.medium}
/>
)}
/>
)}
</View>
</View>
</View>
</View>
)
)
})
const SendAsQRCodeBlock = observer(function (props: {
toggleQRModal: any
const SendAsQRCodeBlock = observer(function (props: {
encodedTokenToSend: string
onCopy: any
onShareToApp: any
// handleError: any
onShareToApp: any
}) {
const [qrError, setQrError] = useState<Error | undefined>()
@@ -1047,49 +976,53 @@ const SendAsQRCodeBlock = observer(function (props: {
}
return (
<View style={[{marginHorizontal: spacing.small}]}>
<Text text={'Scan to receive'} style={{alignSelf: 'center'}}/>
{qrError ? (
<ListItem
text='Could not display QR code, copy token instead.'
subText={qrError ? qrError.message : ''}
leftIcon='faTriangleExclamation'
containerStyle={{marginVertical: spacing.large}}
leftIconColor={colors.palette.angry500}
/>
) : (
<View style={$qrCodeContainer}>
<QRCode
size={spacing.screenWidth - spacing.large * 2} value={props.encodedTokenToSend}
onError={(error: any) => handleQrError(error)}
/>
</View>
)}
<View style={$buttonContainer}>
<Button
text="Share"
onPress={props.onShareToApp}
style={{marginRight: spacing.medium}}
LeftAccessory={() => (
<Icon
icon="faShareFromSquare"
color="white"
size={spacing.medium}
// containerStyle={{marginRight: spacing.small}}
<Card
heading='Token to share'
headingStyle={{textAlign: 'center'}}
ContentComponent={qrError ? (
<ListItem
text='Could not display QR code, copy token instead.'
subText={qrError ? qrError.message : ''}
leftIcon='faTriangleExclamation'
containerStyle={{marginVertical: spacing.large}}
leftIconColor={colors.palette.angry500}
/>
)}
/>
<Button preset="secondary" text="Copy" onPress={props.onCopy} />
<Button
preset="tertiary"
text="Close"
onPress={props.toggleQRModal}
/>
</View>
</View>
) : (
<View style={$qrCodeContainer}>
<QRCode
size={spacing.screenWidth - spacing.large * 2} value={props.encodedTokenToSend}
onError={(error: any) => handleQrError(error)}
/>
</View>
)
}
FooterComponent={
<View style={$buttonContainer}>
<Button
text="Share"
preset="secondary"
onPress={props.onShareToApp}
textStyle={{fontSize: 14}}
style={{
minHeight: moderateVerticalScale(40),
paddingVertical: moderateVerticalScale(spacing.tiny),
marginRight: spacing.tiny
}}
/>
<Button
preset="secondary"
text="Copy"
onPress={props.onCopy}
textStyle={{fontSize: 14}}
style={{
minHeight: moderateVerticalScale(40),
paddingVertical: moderateVerticalScale(spacing.tiny),
marginRight: spacing.tiny
}}
/>
</View>
}
/>
)
})
@@ -1281,6 +1214,7 @@ const $amountInput: TextStyle = {
const $contentContainer: TextStyle = {
flex: 1,
padding: spacing.extraSmall,
marginTop: -spacing.large * 2
}
const $memoCard: ViewStyle = {
@@ -1334,7 +1268,8 @@ const $qrCodeContainer: ViewStyle = {
backgroundColor: 'white',
padding: spacing.small,
margin: spacing.small,
borderRadius: spacing.small
borderRadius: spacing.small,
alignItems: 'center',
}
const $buttonContainer: ViewStyle = {
+220 -274
View File
@@ -33,26 +33,26 @@ import {
} from '../components'
import {TransactionStatus, Transaction} from '../models/Transaction'
import {useStores} from '../models'
import {useHeader} from '../utils/useHeader'
import {NostrClient, NostrProfile, NostrUnsignedEvent, TransactionTaskResult, WalletTask} from '../services'
import {log} from '../services/logService'
import AppError, { Err } from '../utils/AppError'
import {Mint, MintBalance} from '../models/Mint'
import {MintBalance} from '../models/Mint'
import {MintListItem} from './Mints/MintListItem'
import EventEmitter from '../utils/eventEmitter'
import {ResultModalInfo} from './Wallet/ResultModalInfo'
import {PaymentRequest} from '../models/PaymentRequest'
import { useFocusEffect } from '@react-navigation/native'
import { Contact } from '../models/Contact'
import { Contact, ContactType } from '../models/Contact'
import { getImageSource, infoMessage } from '../utils/utils'
import { ReceiveOption } from './ReceiveOptionsScreen'
import { LNURLWithdrawParams } from 'js-lnurl'
import { roundDown, roundUp, toNumber } from '../utils/number'
import { LnurlClient, LnurlWithdrawResult } from '../services/lnurlService'
import { moderateVerticalScale, verticalScale } from '@gocodingnow/rn-size-matters'
import { CurrencySign } from './Wallet/CurrencySign'
import { Currencies, CurrencyCode, MintUnit, MintUnitCurrencyPairs, MintUnits } from "../services/wallet/currency"
import { Currencies, MintUnit, MintUnitCurrencyPairs, MintUnits } from "../services/wallet/currency"
import { MintHeader } from './Mints/MintHeader'
import useIsInternetReachable from '../utils/useIsInternetReachable'
import { MintBalanceSelector } from './Mints/MintBalanceSelector'
if (
Platform.OS === 'android' &&
@@ -63,12 +63,10 @@ if (
export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
function TopupScreen({navigation, route}) {
useHeader({
leftIcon: 'faArrowLeft',
onLeftPress: () => navigation.goBack(),
})
const {proofsStore, mintsStore, walletProfileStore, transactionsStore} = useStores()
const isInternetReachable = useIsInternetReachable()
const {proofsStore, mintsStore, walletProfileStore, transactionsStore, relaysStore} = useStores()
const amountInputRef = useRef<TextInput>(null)
const memoInputRef = useRef<TextInput>(null)
// const tokenInputRef = useRef<TextInput>(null)
@@ -124,7 +122,7 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
const { mintUrl, unit } = route.params
const setSelectedMintandUnit = () => {
setUnit(unit!)
setUnit(unit)
setMintBalanceToTopup(proofsStore.getMintBalance(mintUrl!))
}
@@ -133,46 +131,59 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
}
}, [])
// Send to contact
// Send to contact and LNURL withdraw topup inititalization
useFocusEffect(
useCallback(() => {
const { paymentOption } = route.params
const prepareSendPaymentRequest = () => {
try {
const {contact, relays} = route.params
const { paymentOption, contact } = route.params
if (!contact || !relays) {
throw new AppError(Err.VALIDATION_ERROR, 'Missing contact or relay')
}
const {
pubkey,
npub,
name,
picture,
} = walletProfileStore
const contactFrom: Contact = {
pubkey,
npub,
name,
picture
}
setPaymentOption(ReceiveOption.SEND_PAYMENT_REQUEST)
setContactToSendFrom(contactFrom)
setContactToSendTo(contact)
setRelaysToShareTo(relays)
navigation.setParams({contact: undefined})
navigation.setParams({relays: undefined})
} catch(e: any) {
handleError(e)
const prepareSendToContact = () => {
try {
let relays: string[] = []
log.trace('[prepareSendToContact] selected contact', contact, paymentOption)
if(contact?.type === ContactType.PUBLIC) {
relays = relaysStore.allPublicUrls
} else {
relays = relaysStore.allUrls
}
if (!relays) {
throw new AppError(Err.VALIDATION_ERROR, 'Missing NOSTR relays')
}
const {
pubkey,
npub,
name,
picture,
} = walletProfileStore
const contactFrom: Contact = {
pubkey,
npub,
name,
picture
}
setPaymentOption(paymentOption!)
setContactToSendFrom(contactFrom)
setContactToSendTo(contact)
setRelaysToShareTo(relays)
if(invoiceToPay) {
toggleNostrDMModal() // open if we already have an invoice
}
//reset
navigation.setParams({
paymentOption: undefined,
contact: undefined
})
} catch(e: any) {
handleError(e)
}
}
const prepareLnurlWithdraw = () => {
try {
@@ -186,22 +197,19 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
setAmountToTopup(`${amountSats}`)
setLnurlWithdrawParams(lnurlParams)
setMemo(lnurlParams.defaultDescription)
setPaymentOption(ReceiveOption.LNURL_WITHDRAW)
// onAmountEndEditing(`${amountSats}`)
setPaymentOption(ReceiveOption.LNURL_WITHDRAW)
} catch(e: any) {
handleError(e)
}
}
if(paymentOption && paymentOption === ReceiveOption.SEND_PAYMENT_REQUEST) {
prepareSendPaymentRequest()
if(paymentOption && contact && paymentOption === ReceiveOption.SEND_PAYMENT_REQUEST) {
prepareSendToContact()
}
if(paymentOption && paymentOption === ReceiveOption.LNURL_WITHDRAW) {
prepareLnurlWithdraw()
prepareLnurlWithdraw()
}
}, [route.params?.paymentOption])
)
@@ -229,10 +237,6 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
setIsResultModalVisible(true)
return
}
if (paymentOption === ReceiveOption.SHOW_INVOICE) {
toggleQRModal()
}
if (paymentOption === ReceiveOption.SEND_PAYMENT_REQUEST) {
toggleNostrDMModal()
@@ -291,14 +295,11 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
EventEmitter.off('ev__handlePendingTopupTask_result', handlePendingTopupTaskResult)
}
}, [transactionId])
const toggleQRModal = () => setIsQRModalVisible(previousState => !previousState)
const toggleNostrDMModal = () => setIsNostrDMModalVisible(previousState => !previousState)
const toggleWithdrawModal = () => setIsWithdrawModalVisible(previousState => !previousState)
const toggleResultModal = () => setIsResultModalVisible(previousState => !previousState)
const onAmountEndEditing = function () {
try {
const precision = Currencies[MintUnitCurrencyPairs[unit]]!.precision
@@ -327,8 +328,8 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
setAmountToTopup(`${roundUp(toNumber(amountToTopup), mantissa)}`) // round amount based on currency format
setAvailableMintBalances(availableBalances)
// Default mint is the one with the highest balance to topup
if(!mintBalanceToTopup) setMintBalanceToTopup(availableBalances[0])
// Default mint if not set from route params is the one with the highest balance to topup
if(!mintBalanceToTopup) {setMintBalanceToTopup(availableBalances[0])}
setIsAmountEndEditing(true)
// We do not make memo focus mandatory
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
@@ -492,6 +493,16 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
}
const gotoContacts = function () {
navigation.navigate('ContactsNavigator', {
screen: 'Contacts',
params: {
paymentOption: ReceiveOption.SEND_PAYMENT_REQUEST
}})
}
const onLnurlWithdraw = async function () {
try {
setIsWithdrawRequestSending(true) // replace, not working
@@ -575,14 +586,14 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
const satsColor = colors.palette.primary200
return (
<Screen preset="fixed" contentContainerStyle={$screen}>
<Screen preset="fixed" contentContainerStyle={$screen}>
<MintHeader
mint={mintBalanceToTopup ? mintsStore.findByUrl(mintBalanceToTopup?.mintUrl) : undefined}
unit={unit}
navigation={navigation}
/>
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
<View style={$amountContainer}>
<CurrencySign
currencyCode={MintUnitCurrencyPairs[unit]}
textStyle={{color: 'white'}}
containerStyle={{alignSelf: 'center'}}
/>
<TextInput
ref={amountInputRef}
onChangeText={amount => setAmountToTopup(amount)}
@@ -593,7 +604,9 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
keyboardType="numeric"
selectTextOnFocus={true}
editable={
transactionStatus === TransactionStatus.PENDING ? false : true
(transactionStatus === TransactionStatus.PENDING || !isInternetReachable)
? false
: true
}
/>
<Text
@@ -604,6 +617,7 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
</View>
</View>
<View style={$contentContainer}>
{!invoiceToPay && (
<Card
style={$memoCard}
ContentComponent={
@@ -637,43 +651,51 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
/>
</View>
}
/>
{isMintSelectorVisible && (
<MintBalanceSelector
availableMintBalances={availableMintBalances}
mintBalanceToTopup={mintBalanceToTopup as MintBalance}
unit={unit}
onMintBalanceSelect={onMintBalanceSelect}
onCancel={onMintBalanceCancel}
findByUrl={mintsStore.findByUrl}
onMintBalanceConfirm={onMintBalanceConfirm}
/>
)}
{isMintSelectorVisible && (
<MintBalanceSelector
mintBalances={availableMintBalances}
selectedMintBalance={mintBalanceToTopup as MintBalance}
unit={unit}
title='Topup mint'
confirmTitle='Create invoice'
onMintBalanceSelect={onMintBalanceSelect}
onCancel={onMintBalanceCancel}
onMintBalanceConfirm={onMintBalanceConfirm}
/>
)}
{transactionStatus === TransactionStatus.PENDING && invoiceToPay && paymentOption && (
<>
<ShareAsQRCodeBlock
invoiceToPay={invoiceToPay as string}
onShareToApp={onShareToApp}
onCopy={onCopy}
onError={handleError}
/>
<InvoiceOptionsBlock
toggleNostrDMModal={toggleNostrDMModal}
toggleWithdrawModal={toggleWithdrawModal}
paymentOption={paymentOption}
contactToSendTo={contactToSendTo}
gotoContacts={gotoContacts}
/>
</>
)}
{(transactionStatus === TransactionStatus.COMPLETED) && (
<View style={$bottomContainer}>
<View style={$buttonContainer}>
<Button
preset="secondary"
tx={'common.close'}
onPress={resetState}
/>
)}
{transactionStatus === TransactionStatus.PENDING && invoiceToPay && paymentOption && (
<SelectedMintBlock
toggleNostrDMModal={toggleNostrDMModal}
toggleQRModal={toggleQRModal}
toggleWithdrawModal={toggleWithdrawModal}
paymentOption={paymentOption}
invoiceToPay={invoiceToPay}
mintBalanceToTopup={mintBalanceToTopup as MintBalance}
gotoWallet={resetState}
/>
)}
{(transactionStatus === TransactionStatus.COMPLETED) && (
<View style={$bottomContainer}>
<View style={$buttonContainer}>
<Button
preset="secondary"
tx={'common.close'}
onPress={resetState}
/>
</View>
</View>
)}
{isLoading && <Loading />}
</View>
)}
{isLoading && <Loading />}
</View>
<BottomModal
{/*<BottomModal
isVisible={isQRModalVisible}
ContentComponent={
<ShareAsQRCodeBlock
@@ -686,7 +708,7 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
}
onBackButtonPress={toggleQRModal}
onBackdropPress={toggleQRModal}
/>
/>*/}
<BottomModal
isVisible={isNostrDMModalVisible ? true : false}
ContentComponent={
@@ -795,199 +817,121 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
},
)
const MintBalanceSelector = observer(function (props: {
availableMintBalances: MintBalance[]
mintBalanceToTopup: MintBalance
unit: MintUnit
onMintBalanceSelect: any
onCancel: any
findByUrl: any
onMintBalanceConfirm: any
}) {
const onMintSelect = function (balance: MintBalance) {
log.trace('onMintBalanceSelect', balance.mintUrl)
return props.onMintBalanceSelect(balance)
}
return (
<View style={{flex: 1}}>
<Card
style={$card}
heading={'Select mint to top-up'}
headingStyle={{textAlign: 'center', padding: spacing.small}}
ContentComponent={
<>
<FlatList<MintBalance>
data={props.availableMintBalances}
renderItem={({ item, index }) => {
return(
<MintListItem
key={item.mintUrl}
mint={props.findByUrl(item.mintUrl)}
mintBalance={item}
selectedUnit={props.unit}
onMintSelect={() => onMintSelect(item)}
isSelectable={true}
isSelected={props.mintBalanceToTopup.mintUrl === item.mintUrl}
separator={'top'}
/>
)
}}
keyExtractor={(item) => item.mintUrl}
style={{ flexGrow: 0, maxHeight: spacing.screenHeight * 0.35 }}
/>
</>
}
/>
<View style={$bottomContainer}>
<View style={[$buttonContainer, {marginTop: spacing.large}]}>
<Button
text="Create invoice"
onPress={props.onMintBalanceConfirm}
style={{marginRight: spacing.medium}}
/>
<Button
preset="secondary"
tx={'common.cancel'}
onPress={props.onCancel}
/>
</View>
</View>
</View>
)
})
const SelectedMintBlock = observer(function (props: {
toggleNostrDMModal: any
toggleQRModal: any
toggleWithdrawModal: any
invoiceToPay: string
const InvoiceOptionsBlock = observer(function (props: {
toggleNostrDMModal: any
toggleWithdrawModal: any
contactToSendTo?: Contact
paymentOption: ReceiveOption
mintBalanceToTopup: MintBalance
gotoWallet: any
gotoContacts: any
}) {
const {mintsStore} = useStores()
const sendBg = useThemeColor('card')
const tokenTextColor = useThemeColor('textDim')
return (
<View style={{flex: 1}}>
<Card
style={$card}
heading={'Mint to topup'}
headingStyle={{textAlign: 'center', padding: spacing.small}}
ContentComponent={
<MintListItem
mint={
mintsStore.findByUrl(
props.mintBalanceToTopup?.mintUrl as string,
) as Mint
}
isSelectable={false}
separator={'top'}
/>
}
/>
<View style={{flex: 1}}>
<View style={$bottomContainer}>
<View style={$buttonContainer}>
{props.contactToSendTo ? (
<Button
text='QR code'
preset='secondary'
onPress={props.toggleQRModal}
LeftAccessory={() => (
<Icon
icon='faQrcode'
// color="white"
size={spacing.medium}
/>
)}
text={`Send to ${props.contactToSendTo.nip05}`}
preset='secondary'
onPress={props.toggleNostrDMModal}
style={{marginLeft: spacing.medium}}
LeftAccessory={() => (
<Icon
icon='faPaperPlane'
// color="white"
size={spacing.medium}
/>
)}
/>
{props.paymentOption === ReceiveOption.SEND_PAYMENT_REQUEST && (
<Button
text='Send to contact'
preset='secondary'
onPress={props.toggleNostrDMModal}
style={{marginLeft: spacing.medium}}
LeftAccessory={() => (
<Icon
icon='faPaperPlane'
// color="white"
size={spacing.medium}
/>
)}
/>
)}
{props.paymentOption === ReceiveOption.LNURL_WITHDRAW && (
<Button
text='Withdraw'
preset='secondary'
onPress={props.toggleWithdrawModal}
style={{marginLeft: spacing.medium}}
LeftAccessory={() => (
<Icon
icon='faArrowTurnDown'
// color="white"
size={spacing.medium}
/>
)}
/>
)}
) : (
<Button
preset="secondary"
tx={'common.close'}
onPress={props.gotoWallet}
style={{marginLeft: spacing.small}}
text='Send to contact'
preset='secondary'
onPress={props.gotoContacts}
style={{marginLeft: spacing.medium}}
LeftAccessory={() => (
<Icon
icon='faPaperPlane'
// color="white"
size={spacing.medium}
/>
)}
/>
)}
{props.paymentOption === ReceiveOption.LNURL_WITHDRAW && (
<Button
text='Withdraw'
preset='secondary'
onPress={props.toggleWithdrawModal}
style={{marginLeft: spacing.medium}}
LeftAccessory={() => (
<Icon
icon='faArrowTurnDown'
// color="white"
size={spacing.medium}
/>
)}
/>
)}
</View>
</View>
</View>
)
})
const ShareAsQRCodeBlock = observer(function (props: {
toggleQRModal: any
const ShareAsQRCodeBlock = observer(function (props: {
invoiceToPay: string
onShareToApp: any
onCopy: any
onError: any
}) {
return (
<View style={[$bottomModal, {marginHorizontal: spacing.small}]}>
<Text text={'Scan and pay to top-up'} />
<View style={$qrCodeContainer}>
<QRCode
size={270}
value={props.invoiceToPay}
onError={props.onError}
/>
</View>
<View style={$buttonContainer}>
<Button
text="Share"
onPress={props.onShareToApp}
style={{marginRight: spacing.medium}}
LeftAccessory={() => (
<Icon
icon="faShareFromSquare"
color="white"
size={spacing.medium}
// containerStyle={{marginRight: spacing.small}}
/>
)}
/>
<Button preset="secondary" text="Copy" onPress={props.onCopy} />
<Button
preset="tertiary"
text="Close"
onPress={props.toggleQRModal}
/>
</View>
</View>
<Card
heading='Invoice to pay'
headingStyle={{textAlign: 'center'}}
ContentComponent={
<View style={$qrCodeContainer}>
<QRCode
size={270}
value={props.invoiceToPay}
onError={props.onError}
/>
</View>
}
FooterComponent={
<View style={$buttonContainer}>
<Button
text="Share"
preset="secondary"
onPress={props.onShareToApp}
textStyle={{fontSize: 14}}
style={{
minHeight: moderateVerticalScale(40),
paddingVertical: moderateVerticalScale(spacing.tiny),
marginRight: spacing.tiny
}}
/>
<Button
preset="secondary"
text="Copy"
onPress={props.onCopy}
textStyle={{fontSize: 14}}
style={{
minHeight: moderateVerticalScale(40),
paddingVertical: moderateVerticalScale(spacing.tiny),
marginRight: spacing.tiny
}}
/>
</View>
}
/>
)
})
const SendAsNostrDMBlock = observer(function (props: {
toggleNostrDMModal: any
encodedInvoiceToSend: string
@@ -1258,8 +1202,9 @@ const $headerContainer: TextStyle = {
}
const $contentContainer: TextStyle = {
flex:1,
flex: 1,
padding: spacing.extraSmall,
marginTop: -spacing.large * 2
}
const $memoCard: ViewStyle = {
@@ -1311,9 +1256,10 @@ const $bottomModal: ViewStyle = {
}
const $qrCodeContainer: ViewStyle = {
alignItems: 'center',
backgroundColor: 'white',
padding: spacing.small,
margin: spacing.small,
margin: spacing.tiny,
borderRadius: spacing.small
}
+10 -16
View File
@@ -61,16 +61,6 @@ export const TranDetailScreen: FC<WalletStackScreenProps<'TranDetail'>> =
observer(function TranDetailScreen(_props) {
const {navigation, route} = _props
const {transactionsStore, userSettingsStore} = useStores()
useHeader({
leftIcon: 'faArrowLeft',
onLeftPress: () => navigation.goBack(),
TitleActionComponent:
<CurrencySign
currencyCode={CurrencyCode.SATS}
// containerStyle={{}}
textStyle={{color: 'white'}}
/>
})
const noteInputRef = useRef<TextInput>(null)
@@ -85,6 +75,16 @@ export const TranDetailScreen: FC<WalletStackScreenProps<'TranDetail'>> =
const [note, setNote] = useState<string>('')
const [savedNote, setSavedNote] = useState<string>('')
useHeader({
leftIcon: 'faArrowLeft',
onLeftPress: () => navigation.goBack(),
TitleActionComponent:
<CurrencySign
mintUnit={transaction?.unit}
textStyle={{color: 'white'}}
/>
})
useEffect(() => {
try {
const {id} = route.params
@@ -254,17 +254,11 @@ export const TranDetailScreen: FC<WalletStackScreenProps<'TranDetail'>> =
<>
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
{transaction && (
<>
<CurrencySign
currencyCode={MintUnitCurrencyPairs[transaction.unit as MintUnit]}
textStyle={{color: 'white'}}
/>
<Text
preset="heading"
text={getFormattedAmount(transaction.amount)}
style={$tranAmount}
/>
</>
)}
</View>
<View style={$contentContainer}>
@@ -6,6 +6,7 @@ import { Icon, ListItem, Text } from "../../components"
import { Transaction, TransactionStatus, TransactionType } from "../../models/Transaction"
import { colors, spacing, typography, useThemeColor } from "../../theme"
import useIsInternetReachable from "../../utils/useIsInternetReachable"
import { Currencies, MintUnit, MintUnitCurrencyPairs } from '../../services/wallet/currency'
export interface TransactionListProps {
transaction: Transaction,
@@ -103,6 +104,13 @@ export const TransactionListItem = observer(function (props: TransactionListProp
return (<Icon containerStyle={$txIconContainer} icon="faArrowTurnUp" size={spacing.medium} color={txSendColor}/>)
}
const getAmountWithPrecision = function(tx: Transaction) {
const unit: MintUnit = tx.unit
const currencyPrecision: number = Currencies[MintUnitCurrencyPairs[unit]]!.precision
return (tx.amount / currencyPrecision).toLocaleString()
}
return (
@@ -117,28 +125,28 @@ export const TransactionListItem = observer(function (props: TransactionListProp
{([TransactionType.RECEIVE, TransactionType.RECEIVE_OFFLINE].includes(tx.type)) && (
<>
{[TransactionStatus.COMPLETED].includes(tx.status) && (
<Text style={[$txAmount, {color: txReceiveColor}]}>{tx.amount.toLocaleString()}</Text>
<Text style={[$txAmount, {color: txReceiveColor}]}>{getAmountWithPrecision(tx)}</Text>
)}
{[TransactionStatus.ERROR, TransactionStatus.BLOCKED, TransactionStatus.PREPARED_OFFLINE].includes(tx.status) && (
<Text style={[$txAmount, {color: txErrorColor}]}>{tx.amount.toLocaleString()}</Text>
<Text style={[$txAmount, {color: txErrorColor}]}>{getAmountWithPrecision(tx)}</Text>
)}
</>
)}
{([TransactionType.TOPUP].includes(tx.type)) && (
<>
{[TransactionStatus.PENDING, TransactionStatus.EXPIRED].includes(tx.status) && (
<Text style={[$txAmount, {color: txPendingColor}]}>{tx.amount.toLocaleString()}</Text>
<Text style={[$txAmount, {color: txPendingColor}]}>{getAmountWithPrecision(tx)}</Text>
)}
{tx.status === TransactionStatus.COMPLETED && (
<Text style={[$txAmount, {color: txReceiveColor}]}>{tx.amount.toLocaleString()}</Text>
<Text style={[$txAmount, {color: txReceiveColor}]}>{getAmountWithPrecision(tx)}</Text>
)}
{tx.status === TransactionStatus.ERROR && (
<Text style={[$txAmount, {color: txErrorColor}]}>{tx.amount.toLocaleString()}</Text>
<Text style={[$txAmount, {color: txErrorColor}]}>{getAmountWithPrecision(tx)}</Text>
)}
</>
)}
{([TransactionType.SEND, TransactionType.TRANSFER].includes(tx.type)) && (
<Text style={[$txAmount, {color: (tx.status === TransactionStatus.ERROR) ? txErrorColor : txSendColor}]}>-{tx.amount.toLocaleString()}</Text>
<Text style={[$txAmount, {color: (tx.status === TransactionStatus.ERROR) ? txErrorColor : txSendColor}]}>-{getAmountWithPrecision(tx)}</Text>
)}
</View>
}
+6 -3
View File
@@ -9,24 +9,27 @@ import { verticalScale } from "@gocodingnow/rn-size-matters"
export const CurrencyAmount = observer(function (props: {
amount?: number
amount: number
currencyCode?: CurrencyCode,
mintUnit?: MintUnit,
containerStyle?: ViewStyle,
symbolStyle?: TextStyle,
amountStyle?: TextStyle,
amountStyle?: TextStyle | TextStyle[],
size?: Spacing
})
{
const {currencyCode, mintUnit, amount, symbolStyle, amountStyle, containerStyle, size} = props
let currencySymbol: string = Currencies.SATS!.symbol
let currencyPrecision: number = Currencies.SATS!.precision
if(!!currencyCode) {
currencySymbol = Currencies[currencyCode]!.symbol
currencyPrecision = Currencies[currencyCode]!.precision
}
if(!!mintUnit) {
currencySymbol = Currencies[MintUnitCurrencyPairs[mintUnit]]!.symbol
currencyPrecision = Currencies[MintUnitCurrencyPairs[mintUnit]]!.precision
}
const amountColor = useThemeColor('amount')
@@ -58,7 +61,7 @@ export const CurrencyAmount = observer(function (props: {
fontSize: props.size && spacing[props.size] * 1.2 || spacing.small * 1.2,
lineHeight: size && spacing[size] * 1.2 || spacing.small * 1.2
}, amountStyle || {}]}
text={`${amount || 0}`}
text={`${amount / currencyPrecision}`}
/>
</View>
)
+149 -78
View File
@@ -32,7 +32,8 @@ import {
BottomModal,
ErrorModal,
Header,
ScanIcon
ScanIcon,
MintIcon
} from '../components'
import {useStores} from '../models'
import {WalletStackScreenProps} from '../navigation'
@@ -277,12 +278,12 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
}
}
const gotoReceiveOptions = function () {
navigation.navigate('ReceiveOptions')
const gotoReceiveOptions = function (unit: MintUnit) {
navigation.navigate('ReceiveOptions', {unit})
}
const gotoSendOptions = function () {
navigation.navigate('SendOptions')
const gotoSendOptions = function (unit: MintUnit) {
navigation.navigate('SendOptions', {unit})
}
const gotoScan = function () {
@@ -359,17 +360,27 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
leftIconColor={colors.palette.primary100}
// style={{borderWidth: 1, borderColor: 'red'}}
onLeftPress={gotoTranHistory}
TitleActionComponent={
<>
{!isInternetReachable ? (
TitleActionComponent={!isInternetReachable ? (
<Text
tx={'common.offline'}
style={$offline}
size='xxs'
/>
) : (undefined)}
</>
}
) : (
groupedMints.length > 1 ? (
<ScalingDot
testID={'sliding-border'}
data={groupedMints}
inActiveDotColor={colors.palette.primary300}
activeDotColor={colors.palette.primary100}
activeDotScale={1}
containerStyle={{marginBottom: -spacing.tiny}}
//@ts-ignore
scrollX={scrollX}
dotSize={25}
/>
) : undefined
)}
RightActionComponent={
<>
{paymentRequestsStore.countNotExpired > 0 && (
@@ -395,7 +406,7 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
<AnimatedPagerView
initialPage={0}
ref={pagerRef}
style={{flexGrow: 1, zIndex: 101, elevation: 6}}
style={{flexGrow: 1}}
onPageScroll={onPageScroll}
>
{groupedMints.map((mints) => (
@@ -408,11 +419,37 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
mintsByUnit={mints}
navigation={navigation}
/>
{/*transactionsStore.recentByHostname(mints.hostname).length > 0 && (
{/*<View style={$buttonContainer}>
<Button
tx={'walletScreen.receive'}
LeftAccessory={() => (
<Icon
icon='faArrowDown'
color='white'
size={spacing.medium}
/>
)}
onPress={() => gotoReceiveOptions(mints.unit)}
style={[$buttonReceive, {borderRightColor: screenBg}]}
/>
<Button
tx={'walletScreen.send'}
RightAccessory={() => (
<Icon
icon='faArrowUp'
color='white'
size={spacing.medium}
/>
)}
onPress={() => gotoSendOptions(mints.unit)}
style={[$buttonSend, {borderLeftColor: screenBg}]}
/>
</View>*/}
{transactionsStore.recentByUnit(mints.unit).length > 0 && (
<Card
ContentComponent={
<FlatList
data={transactionsStore.recentByHostname(mints.hostname) as Transaction[]}
data={transactionsStore.recentByUnit(mints.unit) as Transaction[]}
renderItem={({item, index}) => {
return (<TransactionListItem
key={item.id}
@@ -430,55 +467,27 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
}
style={[$card, {paddingTop: spacing.extraSmall}]}
/>
)*/}
</View>
)}
</View>
</View>
))}
))}
</AnimatedPagerView>
{isLoading && <Loading />}
<View style={[$bottomContainer]}>
<View style={$buttonContainer}>
<Button
tx={'walletScreen.receive'}
LeftAccessory={() => (
<Icon
icon='faArrowDown'
color='white'
size={spacing.medium}
/>
)}
onPress={gotoReceiveOptions}
style={[$buttonReceive, {borderRightColor: screenBg}]}
/>
<View style={$bottomContainer}>
<Button
RightAccessory={() => (
<SvgXml
width={spacing.medium}
height={spacing.medium}
width={spacing.large + 10}
height={spacing.large + 10}
xml={ScanIcon}
fill='white'
fill={iconInfo}
/>
)}
onPress={gotoScan}
style={$buttonScan}
preset='tertiary'
/>
<Button
tx={'walletScreen.send'}
RightAccessory={() => (
<Icon
icon='faArrowUp'
color='white'
size={spacing.medium}
/>
)}
onPress={gotoSendOptions}
style={[$buttonSend, {borderLeftColor: screenBg}]}
/>
</View>
</View>
</View>
{isLoading && <Loading />}
<BottomModal
isVisible={isUpdateModalVisible ? true : false}
style={{alignItems: 'stretch'}}
@@ -516,19 +525,20 @@ const UnitBalanceBlock = observer(function (props: {
const currencyColor = colors.palette.primary200
const {unitBalance} = props
log.trace('[UnitBalanceBlock]', {unitBalance})
// log.trace('[UnitBalanceBlock]', {unitBalance})
return (
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
<CurrencySign
currencyCode={MintUnitCurrencyPairs[unitBalance.unit as MintUnit]}
mintUnit={unitBalance.unit}
size='small'
textStyle={{color: balanceColor}}
/>
<Text
preset='heading'
style={[$unitBalance, {color: balanceColor, marginTop: spacing.small}]}
text={unitBalance && unitBalance.unitBalance.toLocaleString() || '0'}
<CurrencyAmount
amount={unitBalance.unitBalance || 0}
mintUnit={unitBalance.unit}
symbolStyle={{display: 'none'}}
amountStyle={[$unitBalance, {color: balanceColor, marginTop: spacing.small}]}
/>
</View>
)
@@ -611,8 +621,7 @@ const MintsByUnitListItem = observer(function (props: {
const gotoTopup = function (mintUrl: string, unit: MintUnit) {
props.navigation.navigate('Topup', {
paymentOption: ReceiveOption.SHOW_INVOICE,
props.navigation.navigate('Topup', {
mintUrl,
unit
})
@@ -626,11 +635,33 @@ const MintsByUnitListItem = observer(function (props: {
})
}
const gotoSend = async function (mintUrl: string, unit: MintUnit) {
props.navigation.navigate('Send', {
mintUrl,
unit
})
}
const color = useThemeColor('textDim')
const balanceColor = useThemeColor('amount')
const {mintsByUnit} = props
const getMintColor = function (unit: MintUnit) {
if (unit === 'sat' || unit === 'msat' || unit === 'btc') {
return colors.palette.orange400
}
if (unit === 'eur') {
return colors.palette.blue400
}
if (unit === 'usd') {
return colors.palette.green400
}
}
/* const isSingleMint: boolean = mintsByUnit.mints.length === 1 || false
const singleMint: Mint = mintsByUnit.mints[0] */
@@ -645,12 +676,30 @@ const MintsByUnitListItem = observer(function (props: {
<ListItem
text={mint.shortname}
subText={mint.hostname}
leftIcon={mint.status === MintStatus.OFFLINE ? 'faTriangleExclamation' : 'faCoins'}
leftIconInverse={true}
leftIconColor={mint.color}
// leftIcon={mint.status === MintStatus.OFFLINE ? 'faTriangleExclamation' : 'faMoneyBill1'}
// leftIconInverse={true}
// leftIconColor={mint.color}
LeftComponent={
<View
style={{
marginEnd: spacing.small,
flex: 0,
borderRadius: spacing.small,
padding: spacing.extraSmall,
backgroundColor: getMintColor(mintsByUnit.unit)
}}
>
<SvgXml
width={spacing.medium}
height={spacing.medium}
xml={MintIcon}
fill='white'
/>
</View>
}
RightComponent={
<CurrencyAmount
amount={mint.balances?.balances[mintsByUnit.unit as MintUnit]}
amount={mint.balances?.balances[mintsByUnit.unit as MintUnit] || 0}
mintUnit={mintsByUnit.unit}
size='medium'
/>
@@ -662,18 +711,22 @@ const MintsByUnitListItem = observer(function (props: {
{selectedMintUrl === mint.mintUrl && (
<View style={{flexDirection: 'row', marginBottom: spacing.small}}>
<Button
text={'Add funds'}
text={'Topup'}
LeftAccessory={() => (
<Icon
icon='faPlus'
color={color}
size={spacing.medium}
size={spacing.small}
/>
)}
preset='tertiary'
preset='secondary'
textStyle={{fontSize: 14, color}}
onPress={() => gotoTopup(mint.mintUrl, mintsByUnit.unit)}
style={{minHeight: moderateVerticalScale(40), paddingVertical: moderateVerticalScale(spacing.tiny)}}
style={{
minHeight: moderateVerticalScale(40),
paddingVertical: moderateVerticalScale(spacing.tiny),
marginRight: spacing.tiny
}}
/>
{/*<Button
text={'Exchange'}
@@ -690,17 +743,35 @@ const MintsByUnitListItem = observer(function (props: {
style={{minHeight: moderateVerticalScale(40), paddingVertical: moderateVerticalScale(spacing.tiny)}}
/>*/}
<Button
text={'Cash out'}
text={'Pay'}
LeftAccessory={() => (
<Icon
icon='faArrowTurnUp'
color={color}
size={spacing.small}
/>
)}
textStyle={{fontSize: 14, color}}
preset='secondary'
onPress={() => gotoLightningPay(mint.mintUrl, mintsByUnit.unit)}
style={{
minHeight: moderateVerticalScale(40),
paddingVertical: moderateVerticalScale(spacing.tiny),
marginRight: spacing.tiny
}}
/>
<Button
text={'Send'}
LeftAccessory={() => (
<Icon
icon='faMoneyBill1'
color={color}
size={spacing.medium}
/>
)}
textStyle={{fontSize: 14, color}}
preset='tertiary'
onPress={() => gotoLightningPay(mint.mintUrl, mintsByUnit.unit)}
preset='secondary'
onPress={() => gotoSend(mint.mintUrl, mintsByUnit.unit)}
style={{minHeight: moderateVerticalScale(40), paddingVertical: moderateVerticalScale(spacing.tiny)}}
/>
</View>
@@ -730,13 +801,13 @@ const $headerContainer: TextStyle = {
const $buttonContainer: ViewStyle = {
flexDirection: 'row',
alignSelf: 'center',
marginTop: spacing.medium,
marginVertical: spacing.medium,
}
const $contentContainer: TextStyle = {
marginTop: -spacing.extraLarge * 2
marginTop: -spacing.extraLarge * 2,
// padding: spacing.extraSmall,
// flex: 0.82,
flex: 1,
// paddingTop: spacing.extraSmall - 3,
// borderWidth: 1,
// borderColor: 'green',
@@ -791,10 +862,10 @@ const $balance: TextStyle = {
}
const $bottomContainer: ViewStyle = {
flex: 0.18,
justifyContent: 'flex-start',
// flex: 0.18,
// justifyContent: 'flex-end',
marginBottom: spacing.medium,
alignSelf: 'stretch',
alignSelf: 'center',
// opacity: 0,
}
@@ -808,7 +879,7 @@ const $buttonReceive: ViewStyle = {
}
const $buttonScan: ViewStyle = {
borderRadius: 0,
// borderRadius: verticalScale(60 / 2),
minWidth: verticalScale(60),
}
+45 -6
View File
@@ -307,7 +307,7 @@ const getSpentOrPendingProofsFromMint = async function (
const spentPendingProofs = await cashuWallet.checkProofsSpent(proofs)
log.trace('[CashuMintClient.getSpentOrPendingProofsFromMint]', {spentPendingProofs})
log.trace('[CashuMintClient.getSpentOrPendingProofsFromMint]', {mintUrl, spentPendingProofs})
return spentPendingProofs as {
spent: CashuProof[]
@@ -402,19 +402,22 @@ const payLightningMelt = async function (
const getLightningMintQuote = async function (
const getBolt11MintQuote = async function (
mintUrl: string,
unit: MintUnit,
amount: number,
) {
try {
const cashuMint = getMint(mintUrl)
const {request: encodedInvoice, quote: mintQuote} = await cashuMint.mintQuote({
const {
request: encodedInvoice,
quote: mintQuote,
} = await cashuMint.mintQuote({
unit,
amount
})
log.info('[getLightningMintQuote]', {encodedInvoice, mintQuote})
log.info('[getBolt11MintQuote]', {encodedInvoice, mintQuote})
return {
encodedInvoice,
@@ -426,7 +429,42 @@ const getLightningMintQuote = async function (
'The mint could not return an invoice.',
{
message: e.message,
caller: 'getLightningMintQuote',
caller: 'getBolt11MintQuote',
mintUrl,
}
)
}
}
const getBolt11MintQuoteIsPaid = async function (
mintUrl: string,
quote: string,
) {
try {
const cashuMint = getMint(mintUrl)
const {
request: encodedInvoice,
quote: mintQuote,
paid: isPaid
} = await cashuMint.mintQuotePaid(
quote
)
log.info('[getBolt11MintQuoteIsPaid]', {encodedInvoice, mintQuote, isPaid})
return {
encodedInvoice,
mintQuote,
isPaid
}
} catch (e: any) {
throw new AppError(
Err.MINT_ERROR,
'The mint could not return the state of a mint quote.',
{
message: e.message,
caller: 'getBolt11MintQuoteIsPaid',
mintUrl,
}
)
@@ -557,7 +595,8 @@ export const MintClient = {
receiveFromMint,
sendFromMint,
getSpentOrPendingProofsFromMint,
getLightningMintQuote,
getBolt11MintQuote,
getBolt11MintQuoteIsPaid,
mintProofs,
getLightningMeltQuote,
payLightningMelt,
+15 -6
View File
@@ -6,10 +6,10 @@ import { colors } from '../theme'
import AppError, { Err } from '../utils/AppError'
import { log } from './logService'
import { CashuUtils } from './cashu/cashuUtils'
import { MintClient, MintKeys } from './cashuMintClient'
import { LightningUtils } from './lightning/lightningUtils'
import { LnurlUtils } from './lnurl/lnurlUtils'
import { LnurlClient } from './lnurlService'
import { MintUnit } from './wallet/currency'
export enum IncomingDataType {
CASHU = 'CASHU',
@@ -135,19 +135,25 @@ const navigateWithIncomingData = async function (
type: IncomingDataType,
encoded: any
},
navigation: StackNavigationProp<any>
navigation: StackNavigationProp<any>,
mintAndUnit?: {
mintUrl: string,
unit: MintUnit
}
) {
switch (incoming.type) {
case IncomingDataType.CASHU:
return navigation.navigate('Receive', {
encodedToken: incoming.encoded,
...mintAndUnit
})
case IncomingDataType.INVOICE:
return navigation.navigate('Transfer', {
encodedInvoice: incoming.encoded,
paymentOption: SendOption.PASTE_OR_SCAN_INVOICE
paymentOption: SendOption.PASTE_OR_SCAN_INVOICE,
...mintAndUnit
})
case (IncomingDataType.LNURL):
@@ -158,14 +164,16 @@ const navigateWithIncomingData = async function (
if(lnurlParams.tag === 'withdrawRequest') {
return navigation.navigate('Topup', {
lnurlParams,
paymentOption: ReceiveOption.LNURL_WITHDRAW
paymentOption: ReceiveOption.LNURL_WITHDRAW,
...mintAndUnit
})
}
if(lnurlParams.tag === 'payRequest') {
return navigation.navigate('Transfer', {
lnurlParams,
paymentOption: SendOption.LNURL_PAY
paymentOption: SendOption.LNURL_PAY,
...mintAndUnit
})
}
@@ -183,7 +191,8 @@ const navigateWithIncomingData = async function (
return navigation.navigate('Transfer', {
lnurlParams: addressParamsResult.lnurlParams,
paymentOption: SendOption.LNURL_PAY
paymentOption: SendOption.LNURL_PAY,
...mintAndUnit
})
} catch (e: any) {
throw new AppError(Err.SERVER_ERROR, 'Could not get Lightning address details from the server.', {
+10 -7
View File
@@ -40,7 +40,7 @@ export const sendTask = async function (
memo: string,
selectedProofs: Proof[]
) : Promise<TransactionTaskResult> {
const mintUrl = mintBalanceToSendFrom.mint
const mintUrl = mintBalanceToSendFrom.mintUrl
log.trace('[send]', 'mintBalanceToSendFrom', mintBalanceToSendFrom)
@@ -126,7 +126,7 @@ export const sendTask = async function (
JSON.stringify(transactionData),
)
const balanceAfter = proofsStore.getBalances().totalBalance
const balanceAfter = proofsStore.getUnitBalance(unit)?.unitBalance!
await transactionsStore.updateBalanceAfter(transactionId, balanceAfter)
@@ -138,7 +138,7 @@ export const sendTask = async function (
WalletTask.handleSpentByMint,
{
interval: 6 * 1000,
maxPolls: 20,
maxPolls: 1, // TODO 10
maxErrors: 2
},
{mintUrl, isPending: true}
@@ -188,7 +188,7 @@ export const sendFromMint = async function (
selectedProofs: Proof[],
transactionId: number,
) {
const mintUrl = mintBalance.mint
const mintUrl = mintBalance.mintUrl
const mintInstance = mintsStore.findByUrl(mintUrl)
try {
@@ -295,8 +295,9 @@ export const sendFromMint = async function (
// If we've got valid response, decrease proofsCounter and let it be increased back in next step when adding proofs
mintInstance.decreaseProofsCounter(lockedProofsCounter.keyset, countOfInFlightProofs)
// add proofs returned by the mint after the split
// add proofs returned by the mint after the split
if (returnedProofs.length > 0) {
log.trace('[sendFromMint] add returned proofs to spendable')
const { addedProofs, addedAmount } = WalletUtils.addCashuProofs(
mintUrl,
returnedProofs,
@@ -309,16 +310,18 @@ export const sendFromMint = async function (
}
// remove used proofs and move sent proofs to pending
log.trace('[sendFromMint] remove proofsToSendFrom from spendable')
proofsStore.removeProofs(proofsToSendFrom)
// these might be original proofToSendFrom if they matched the exact amount and split was not necessary
// these might be original proofToSendFrom if they matched the exact amount and split was not necessary
log.trace('[sendFromMint] add proofsToSend to pending')
const { addedProofs, addedAmount } = WalletUtils.addCashuProofs(
mintUrl,
proofsToSend,
{
unit,
transactionId,
isPending: false
isPending: true
}
)
+6 -5
View File
@@ -11,7 +11,7 @@ import { LightningUtils } from '../lightning/lightningUtils'
import { getSnapshot, isStateTreeNode } from 'mobx-state-tree'
import { PaymentRequest, PaymentRequestStatus, PaymentRequestType } from '../../models/PaymentRequest'
import { WalletUtils } from './utils'
import { MintUnit } from './currency'
import { MintUnit, MintUnits } from './currency'
const {
transactionsStore,
@@ -59,7 +59,7 @@ export const topupTask = async function (
const storedTransaction: TransactionRecord = await transactionsStore.addTransaction(newTransaction)
transactionId = storedTransaction.id as number
const {encodedInvoice, mintQuote} = await MintClient.getLightningMintQuote(mintUrl, unit, amountToTopup)
const {encodedInvoice, mintQuote} = await MintClient.getBolt11MintQuote(mintUrl, unit, amountToTopup)
const decodedInvoice = LightningUtils.decodeInvoice(encodedInvoice)
const {amount, payment_hash, expiry, timestamp} = LightningUtils.getInvoiceData(decodedInvoice)
@@ -103,7 +103,8 @@ export const topupTask = async function (
mintUnit: unit,
amountToTopup,
encodedInvoice,
invoicedAmount: amount,
invoicedAmount: amount,
invoicedUnit: 'sat',
description: memo ? memo : contactTo ? `Pay to ${walletProfileStore.nip05}` : '',
paymentHash: payment_hash,
contactFrom,
@@ -132,8 +133,8 @@ export const topupTask = async function (
WalletTask.handlePendingTopup,
{
interval: 6 * 1000,
maxPolls: 1, // TODO 20!!!
maxErrors: 5
maxPolls: 10,
maxErrors: 2
},
{paymentRequest})
.then(() => log.trace('Polling completed', [], `handlePendingTopupTaskPoller`))
+51 -22
View File
@@ -30,7 +30,7 @@ import { topupTask } from './wallet/topupTask'
import { transferTask } from './wallet/transferTask'
import { WalletUtils } from './wallet/utils'
import { NotificationService } from './notificationService'
import { MintUnit } from './wallet/currency'
import { Currencies, CurrencyCode, MintUnit, MintUnitCurrencyPairs, MintUnits } from './wallet/currency'
type WalletTaskService = {
@@ -296,7 +296,7 @@ const handleSpentByMint = async function (
}
): Promise<void> {
const {mintUrl, isPending} = options
log.trace('[handleSpentByMint] start')
log.trace('[handleSpentByMint] start', {mintUrl, isPending})
const now = new Date().getTime()
return SyncQueue.addTask(
@@ -378,7 +378,7 @@ const _handleSpentByMintTask = async function (
pendingCount = pendingProofs.length
pendingAmount = CashuUtils.getProofsAmount(pendingProofs as Proof[])
log.trace('[_handleSpentByMintTask]', `${isPending ? 'Pending' : ''} spents and pending by mint amounts`, {spentAmount, pendingAmount, isPending})
log.trace('[_handleSpentByMintTask]', `${isPending ? 'Pending' : ''} spent and pending by mint amounts`, {spentAmount, pendingAmount, isPending})
if (spentCount > 0) {
// identify txIds to update their statuses, there might be more then one tx to complete
@@ -698,10 +698,11 @@ const _handleInFlightByMintTask = async function (mint: Mint, seed: Uint8Array):
const handlePendingTopups = async function (): Promise<void> {
log.trace('[handlePendingTopups] start')
const handlePendingTopups = async function (): Promise<void> {
const paymentRequests: PaymentRequest[] = paymentRequestsStore.allOutgoing
log.trace('[handlePendingTopups] start', {paymentRequests})
if (paymentRequests.length === 0) {
log.trace('[handlePendingTopups]', 'No outgoing payment requests in store - skipping task send to the queue...')
return
@@ -743,23 +744,42 @@ const _handlePendingTopupTask = async function (params: {paymentRequest: Payment
const {paymentRequest: pr} = params
const transactionId = {...pr}.transactionId || 0// copy
const mint = {...pr}.mint // copy
const unit = {...pr}.unit // copy
const amount = {...pr}.amount // copy
const unit = {...pr}.mintUnit // copy, unit of proofs to be received
const amount = {...pr}.amountToTopup // copy, amount of proofs to be received
const paymentHash = {...pr}.paymentHash // copy
const mintQuote = {...pr}.mintQuote // copy
const mintInstance = mintsStore.findByUrl(mint as string)
try {
if(!mintInstance) {
throw new AppError(Err.VALIDATION_ERROR, 'Missing mint', {mintUrl: mint})
if(!mintInstance || !mintQuote || !unit || !amount) {
throw new AppError(Err.VALIDATION_ERROR, 'Missing mint or mint quote or mintUnit or amountToTopup', {mintUrl: mint})
}
const amountPreferences = getDefaultAmountPreference(amount)
const countOfInFlightProofs = CashuUtils.getAmountPreferencesCount(amountPreferences)
log.trace('[_handlePendingTopupTask]', 'paymentRequest', paymentHash)
log.trace('[_handlePendingTopupTask]', 'amountPreferences', amountPreferences)
log.trace('[_handlePendingTopupTask]', 'countOfInFlightProofs', countOfInFlightProofs)
log.trace('[_handlePendingTopupTask]', 'countOfInFlightProofs', countOfInFlightProofs)
// check is quote has been paid
const { isPaid, mintQuote: quote } = await MintClient.getBolt11MintQuoteIsPaid(mint!, mintQuote)
if (quote !== mintQuote) {
throw new AppError(Err.VALIDATION_ERROR, 'Returned quote is different then the one requested', {mintUrl: mint, quote, mintQuote})
}
if (isPaid !== true) {
log.trace('[_handlePendingTopupTask] Quot not paid', {mintUrl: mint, mintQuote})
return {
taskFunction: '_handlePendingTopupTask',
mintUrl: mint,
unit,
amount,
paymentHash,
message: `Quote ${mintQuote} not paid`,
} as WalletTaskResult
}
// temp increase the counter + acquire lock and set inFlight values
await WalletUtils.lockAndSetInFlight(
@@ -784,7 +804,7 @@ const _handlePendingTopupTask = async function (params: {paymentRequest: Payment
lockedProofsCounter.inFlightFrom as number
)) as CashuProof[]
log.info('[_handlePendingTopupTask]', {proofs})
// log.info('[_handlePendingTopupTask]', {proofs})
} catch (e: any) {
// decrease so that unpaid invoices does not cause counter gaps from polling
@@ -880,10 +900,12 @@ const _handlePendingTopupTask = async function (params: {paymentRequest: Payment
mintInstance.resetInFlight(transactionId as number )
stopPolling(`handlePendingTopupTaskPoller-${paymentHash}`)
if (receivedAmount !== pr.amount) {
const precision: number = Currencies[MintUnitCurrencyPairs[pr.mintUnit as MintUnit]]!.precision
if (receivedAmount !== amount) {
throw new AppError(
Err.VALIDATION_ERROR,
`Received amount ${receivedAmount} SATS is not equal to the requested amount ${amount} SATS.`,
`Received amount ${receivedAmount / precision} ${unit} is not equal to the requested amount ${amount / precision} ${unit}.`,
)
}
@@ -908,8 +930,8 @@ const _handlePendingTopupTask = async function (params: {paymentRequest: Payment
// Fire event that the TopupScreen can listen to // TODO replace by standard task result event
// EventEmitter.emit('ev_topupCompleted', {...pr})
// Update tx with current balance
const balanceAfter = proofsStore.getBalances().mintBalances[mint as any].balances[unit as MintUnit]!
// Update tx with current total balance of topup unit/currency
const balanceAfter = proofsStore.getUnitBalance(unit)?.unitBalance!
await transactionsStore.updateBalanceAfter(
transactionId as number,
@@ -919,6 +941,8 @@ const _handlePendingTopupTask = async function (params: {paymentRequest: Payment
_sendTopupNotification(pr)
paymentRequestsStore.removePaymentRequest(pr)
return {
taskFunction: '_handlePendingTopupTask',
mintUrl: mint,
@@ -926,7 +950,7 @@ const _handlePendingTopupTask = async function (params: {paymentRequest: Payment
amount,
paymentHash,
transaction: transactionsStore.findById(transactionId as number),
message: `Your invoice has been paid and your wallet balance credited with ${amount} SATS.`,
message: `Your invoice has been paid and your wallet balance credited with ${amount / precision} ${unit}.`,
} as TransactionTaskResult
} catch (e: any) {
@@ -948,10 +972,15 @@ const _handlePendingTopupTask = async function (params: {paymentRequest: Payment
}
const _sendTopupNotification = async function (pr: PaymentRequest) {
const precision: number = Currencies[MintUnitCurrencyPairs[pr.mintUnit as MintUnit]]!.precision
await NotificationService.createLocalNotification(
`⚡ ${pr.amount} SATS received!`,
`Your invoice has been paid and your wallet balance credited with ${pr.amount} SATS.`,
`⚡ ${pr.amountToTopup! / precision} ${pr.mintUnit} received!`,
`Your invoice has been paid and your wallet balance credited with ${pr.amountToTopup! / precision} ${pr.mintUnit}.`,
)
}
@@ -1174,8 +1203,8 @@ const _handleReceivedEventTask = async function (event: NostrEvent): Promise<Wal
type: PaymentRequestType.INCOMING,
status: PaymentRequestStatus.ACTIVE,
encodedInvoice: incoming.encoded,
unit: 'sat', // bolt11
amount: amount || 0,
invoicedUnit: 'sat', // bolt11
invoicedAmount: amount || 0,
description: maybeMemo ? maybeMemo : description,
paymentHash,
contactFrom: contactFrom || {pubkey: sentFromPubkey, npub: sentFromNpub},
@@ -1276,7 +1305,7 @@ const _sendReceiveNotification = async function (
const _sendPaymentRequestNotification = async function (pr: PaymentRequest) {
await NotificationService.createLocalNotification(
`⚡ Please pay ${pr.amount} SATS!`,
`⚡ Please pay ${pr.invoicedAmount} ${pr.invoicedUnit}!`,
`${pr.contactFrom.nip05 || 'Unknown'} has sent you a request to pay an invoice.`,
pr.contactFrom.picture,
)
+4 -1
View File
@@ -44,9 +44,12 @@ const palette = {
blue100: '#75B4FB',
blue200: '#818AFF',
blue400: '#2d57e1',
green400: '#599D52',
orange200: '#FFAB51',
orange400: '#F7931A',
orange400: '#FF9900',
focus100: '#FB4E9E',
focus200: '#F62586',