From 7225fadfbcff53c554db9a38b58038d6cc6ca7ce Mon Sep 17 00:00:00 2001 From: KKA11010 Date: Wed, 3 Apr 2024 15:03:26 +0200 Subject: [PATCH 01/18] Add history context and pending invoices --- src/components/App.tsx | 31 +++-- src/components/Balance.tsx | 59 +++----- src/components/Icons.tsx | 8 ++ src/context/History.tsx | 162 ++++++++++++++++++++++ src/model/index.ts | 1 + src/screens/History/index.tsx | 41 +----- src/screens/Payment/Receive/Invoice.tsx | 82 ++++++----- src/storage/store/HistoryStore.ts | 30 ++-- src/storage/store/latestHistoryEntries.ts | 9 ++ 9 files changed, 271 insertions(+), 152 deletions(-) create mode 100644 src/context/History.tsx diff --git a/src/components/App.tsx b/src/components/App.tsx index e64b7c48..427898e6 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -33,6 +33,7 @@ import { SafeAreaProvider } from 'react-native-safe-area-context' import Blank from './Blank' import ClipboardModal from './ClipboardModal' import Toaster from './Toaster' +import { HistoryProvider } from '@src/context/History' LogBox.ignoreLogs(['is deprecated']) // LogBox.ignoreLogs([/expo-image/gmi]) @@ -48,7 +49,7 @@ interface ILockData { l('[APP] Starting app...') void SplashScreen.preventAutoHideAsync() -function App(_: { exp: Record} ) { +function App(_: { exp: Record }) { if (!env?.SENTRY_DSN) { return ( @@ -208,19 +209,21 @@ function _App() { - - - - - - + + + + + + + + diff --git a/src/components/Balance.tsx b/src/components/Balance.tsx index 416e023f..6191a51a 100644 --- a/src/components/Balance.tsx +++ b/src/components/Balance.tsx @@ -1,18 +1,17 @@ -import { CheckmarkIcon, EcashIcon, SwapCurrencyIcon, ZapIcon } from '@comps/Icons' +import { CheckmarkIcon, ClockIcon, EcashIcon, SwapCurrencyIcon, ZapIcon } from '@comps/Icons' import { setPreferences } from '@db' -import { type IHistoryEntry, TTXType,txType } from '@model' +import { type TTXType, txType } from '@model' import type { RootStackParamList } from '@model/nav' import type { NativeStackNavigationProp } from '@react-navigation/native-stack' import EntryTime from '@screens/History/entryTime' -import { useFocusClaimContext } from '@src/context/FocusClaim' +import { useHistoryContext } from '@src/context/History' import { usePrivacyContext } from '@src/context/Privacy' import { useThemeContext } from '@src/context/Theme' import { NS } from '@src/i18n' -import { getLatestHistory } from '@store/latestHistoryEntries' import { globals, highlight as hi } from '@styles' import { getColor } from '@styles/colors' import { formatBalance, formatInt, formatSatStr, isBool } from '@util' -import { useEffect, useState } from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Text, TouchableOpacity, View } from 'react-native' import { s, ScaledSheet } from 'react-native-size-matters' @@ -29,16 +28,9 @@ interface IBalanceProps { export default function Balance({ balance, nav }: IBalanceProps) { const { t } = useTranslation([NS.common]) const { pref, color, highlight } = useThemeContext() - // State to indicate token claim from clipboard after app comes to the foreground, to re-render total balance - const { claimed } = useFocusClaimContext() const { hidden, handleLogoPress } = usePrivacyContext() const [formatSats, setFormatSats] = useState(pref?.formatBalance) - const [history, setHistory] = useState([]) - - const setHistoryEntries = async () => { - const stored = (await getLatestHistory()).reverse() - setHistory(stored) - } + const { latestHistory } = useHistoryContext() const toggleBalanceFormat = () => { setFormatSats(prev => !prev) @@ -54,23 +46,6 @@ export default function Balance({ balance, nav }: IBalanceProps) { return t('seedBackup') } - useEffect(() => { - void setHistoryEntries() - }, []) - - // get history after navigating to this page - useEffect(() => { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - const focusHandler = nav?.addListener('focus', async () => { - await setHistoryEntries() - }) - return focusHandler - }, [nav]) - - useEffect(() => { - void setHistoryEntries() - }, [claimed]) - return ( } {/* No transactions yet */} - {!history.length && + {!latestHistory.length && } {/* latest 3 history entries */} - {history.length > 0 && !hidden.txs && - history.map(h => ( + {latestHistory.length > 0 && !hidden.txs && + latestHistory.map(h => ( - : - h.type === txType.RESTORE ? - + icon={ + h.isPending ? + : - + h.type === txType.RESTORE ? + + : + h.type === txType.LIGHTNING || h.type === txType.SWAP ? + + : + } isSwap={h.type === txType.SWAP} txType={getTxTypeStr(h.type)} @@ -126,7 +105,7 @@ export default function Balance({ balance, nav }: IBalanceProps) { /> )) } - {(history.length === 3 || (history.length > 0 && hidden.txs)) && + {(latestHistory.length === 3 || (latestHistory.length > 0 && hidden.txs)) && nav?.navigate('history')} diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index c162ff95..eb929890 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -639,6 +639,14 @@ export function ConnectionErrorIcon({ width, height, color }: TIconProps) { ) } +export function ClockIcon({ width, height, color }: TIconProps) { + return ( + + + + + ) +} const styles = StyleSheet.create({ nostrIcon: { marginLeft: -5 diff --git a/src/context/History.tsx b/src/context/History.tsx new file mode 100644 index 00000000..f1ceaad6 --- /dev/null +++ b/src/context/History.tsx @@ -0,0 +1,162 @@ +/* eslint-disable no-await-in-loop */ + +import { delInvoice, getAllInvoices } from '@db' +import { l } from '@log' +import type { IHistoryEntry } from '@model' +import { NS } from '@src/i18n' +import { historyStore, store } from '@store' +import { STORE_KEYS } from '@store/consts' +import { getHistory, getHistoryEntriesByInvoices, getHistoryEntryByInvoice } from '@store/HistoryStore' +import { addToHistory, getLatestHistory, updateHistory } from '@store/latestHistoryEntries' +import { decodeLnInvoice } from '@util' +import { requestToken } from '@wallet' +import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { useFocusClaimContext } from './FocusClaim' +import { usePromptContext } from './Prompt' + +const useHistory = () => { + const { t } = useTranslation([NS.common]) + const [history, setHistory] = useState>({}) + const [latestHistory, setLatestHistory] = useState([]) + // State to indicate token claim from clipboard after app comes to the foreground, to re-render total balance + const { claimed } = useFocusClaimContext() + const { openPromptAutoClose } = usePromptContext() + const intervalRef = useRef(null) + const allHisoryEntries = useRef([]) + const hasEntries = useMemo(() => Object.keys(history).length > 0, [history]) + + const startGlobalInvoiceInterval = () => { + intervalRef.current = setInterval(() => { + void handlePendingInvoices() + }, 5000) + } + + const clearInvoiceInterval = () => { + if (intervalRef.current) { + clearInterval(intervalRef.current) + allHisoryEntries.current = [] + } + } + + const setHistoryEntries = async () => { + const [all, latest] = await Promise.all([getHistory(), getLatestHistory()]) + setHistory(all) + setLatestHistory(latest.reverse()) + } + + const handlePendingInvoices = async () => { + const invoices = await getAllInvoices() + if (!invoices.length) { return clearInvoiceInterval() } + if (!allHisoryEntries.current.length) { + const historyEntries = await getHistoryEntriesByInvoices(invoices) + allHisoryEntries.current = historyEntries + } + let paid = { count: 0, amount: 0 } + for (const invoice of invoices) { + try { + const success = await requestToken(invoice.mintUrl, invoice.amount, invoice.hash) + if (success.success) { + paid.count++ + paid.amount += invoice.amount + const entry = getHistoryEntryByInvoice(allHisoryEntries.current, invoice.pr) + if (entry) { + await updateHistoryEntry(entry, { ...entry, isPending: false }) + } + // TODO update balance + await delInvoice(invoice.hash) + continue + } + } catch (_) {/* ignore */ } + const { expiry } = decodeLnInvoice(invoice.pr) + const date = new Date((invoice.time * 1000) + (expiry * 1000)).getTime() + if (Date.now() > date) { await delInvoice(invoice.hash) } + } + // notify user + if (paid.count > 0) { + openPromptAutoClose({ + msg: 'Test message', // t(paid.count > 1 ? 'paidInvoices' : 'paidInvoice', { count: paid.count, total: paid.amount }) + success: true + }) + paid = { count: 0, amount: 0 } + } + } + + const addHistoryEntry = async (entry: Omit) => { + const resp = await addToHistory(entry) + await setHistoryEntries() + return resp + } + + const updateHistoryEntry = async (oldEntry: IHistoryEntry, newEntry: IHistoryEntry) => { + await updateHistory(oldEntry, newEntry) + await setHistoryEntries() + } + + const deleteHistory = async () => { + const [success] = await Promise.all([ + historyStore.clear(), + store.delete(STORE_KEYS.latestHistory), + ]) + setHistory({}) + setLatestHistory([]) + openPromptAutoClose({ + msg: success ? t('historyDeleted') : t('delHistoryErr'), + success + }) + } + + useEffect(() => { + void setHistoryEntries() + // request token of pending invoices in interval until all are paid or expired + startGlobalInvoiceInterval() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + useEffect(() => void setHistoryEntries(), [claimed]) + + return { + history, + latestHistory, + hasEntries, + addHistoryEntry, + updateHistoryEntry, + deleteHistory, + startGlobalInvoiceInterval, + } +} +type useHistoryType = ReturnType + +const HistoryCtx = createContext({ + history: {}, + latestHistory: [], + hasEntries: false, + // eslint-disable-next-line require-await, @typescript-eslint/require-await + addHistoryEntry: async () => ({ + timestamp: 0, + amount: 0, + value: '', + mints: [], + fee: 0, + sender: '', + recipient: '', + type: 1, + preImage: '', + isSpent: false, + isPending: false + }), + // eslint-disable-next-line no-return-await, @typescript-eslint/await-thenable + updateHistoryEntry: async () => await l(''), + // eslint-disable-next-line no-return-await, @typescript-eslint/await-thenable + deleteHistory: async () => await l(''), + startGlobalInvoiceInterval: () => l(''), +}) + +export const useHistoryContext = () => useContext(HistoryCtx) + +export const HistoryProvider = ({ children }: { children: React.ReactNode }) => ( + + {children} + +) \ No newline at end of file diff --git a/src/model/index.ts b/src/model/index.ts index aaf9c7c8..12fceb2f 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -118,6 +118,7 @@ export interface IHistoryEntry { preImage?: string, fee?: number, isSpent?: boolean // is token spendable + isPending?: boolean // is LN invoice pending } diff --git a/src/screens/History/index.tsx b/src/screens/History/index.tsx index d4136de0..601c2589 100644 --- a/src/screens/History/index.tsx +++ b/src/screens/History/index.tsx @@ -4,19 +4,14 @@ import { BottomModal } from '@comps/modal/Question' import Separator from '@comps/Separator' import Txt from '@comps/Txt' import { isIOS } from '@consts' -import type { IHistoryEntry } from '@model' import type { THistoryPageProps } from '@model/nav' import TopNav from '@nav/TopNav' import { FlashList } from '@shopify/flash-list' -import { useFocusClaimContext } from '@src/context/FocusClaim' -import { usePromptContext } from '@src/context/Prompt' +import { useHistoryContext } from '@src/context/History' import { useThemeContext } from '@src/context/Theme' import { NS } from '@src/i18n' -import { store } from '@src/storage/store' -import { STORE_KEYS } from '@store/consts' -import { getHistory, historyStore } from '@store/HistoryStore' import { globals } from '@styles' -import { useEffect, useState } from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { View } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' @@ -28,40 +23,14 @@ export default function HistoryPage({ navigation, route }: THistoryPageProps) { const insets = useSafeAreaInsets() const { t } = useTranslation([NS.common]) const { color } = useThemeContext() - const { claimed } = useFocusClaimContext() - const [data, setData] = useState>({}) - const { openPromptAutoClose } = usePromptContext() + const { history, hasEntries, deleteHistory } = useHistoryContext() const [confirm, setConfirm] = useState(false) - const hasEntries = Object.keys(data).length > 0 - const handleDeleteHistory = async () => { - const success = await historyStore.clear() - await store.delete(STORE_KEYS.latestHistory) - setData({}) - openPromptAutoClose({ - msg: success ? t('historyDeleted') : t('delHistoryErr'), - success - }) + await deleteHistory() setConfirm(false) } - // update history after claiming from clipboard when the app comes to the foreground - useEffect(() => { - void (async () => { - setData(await getHistory()) - })() - }, [claimed]) - - // update history after navigating to this page - useEffect(() => { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - const focusHandler = navigation.addListener('focus', async () => { - setData(await getHistory()) - }) - return focusHandler - }, [navigation]) - return ( {/* History list grouped by settled date */} ( <> diff --git a/src/screens/Payment/Receive/Invoice.tsx b/src/screens/Payment/Receive/Invoice.tsx index fe40f475..76e56bb2 100644 --- a/src/screens/Payment/Receive/Invoice.tsx +++ b/src/screens/Payment/Receive/Invoice.tsx @@ -5,14 +5,14 @@ import Loading from '@comps/Loading' import QR from '@comps/QR' import Txt from '@comps/Txt' import { _testmintUrl, isIOS } from '@consts' -import { getAllInvoices } from '@db' import { l } from '@log' +import type { IHistoryEntry } from '@model' import type { TMintInvoicePageProps } from '@model/nav' import TopNav from '@nav/TopNav' +import { useHistoryContext } from '@src/context/History' import { usePromptContext } from '@src/context/Prompt' import { useThemeContext } from '@src/context/Theme' import { NS } from '@src/i18n' -import { addToHistory } from '@store/latestHistoryEntries' import { globals, highlight as hi, mainColors } from '@styles' import { getColor } from '@styles/colors' import { formatMintUrl, formatSeconds, isErr, openUrl, share } from '@util' @@ -27,6 +27,11 @@ export default function InvoiceScreen({ navigation, route }: TMintInvoicePagePro const { openPromptAutoClose } = usePromptContext() const { t } = useTranslation([NS.common]) const { color, highlight } = useThemeContext() + const { + addHistoryEntry, + updateHistoryEntry, + startGlobalInvoiceInterval, + } = useHistoryContext() const intervalRef = useRef(null) const [expire, setExpire] = useState(expiry) const [expiryTime,] = useState(expire * 1000 + Date.now()) @@ -38,48 +43,23 @@ export default function InvoiceScreen({ navigation, route }: TMintInvoicePagePro } } - const handlePayment = async (isCancelling?: boolean) => { + const handlePaidInvoice = async (entry: IHistoryEntry) => { + clearInvoiceInterval() + await updateHistoryEntry(entry, { ...entry, isPending: false }) + navigation.navigate('success', { amount, mint: formatMintUrl(mintUrl) }) + } + + const handlePayment = async (entry: IHistoryEntry) => { try { - const allInvoices = (await getAllInvoices()).map(i => i.pr) const { success } = await requestToken(mintUrl, amount, hash) - /* - it is possible that success is false but invoice has - been paid and token have been issued due to the double - check in the background...(requestTokenLoop()) - So we check if the invoice is in the db and if it is - not then we check if the invoice has expired and if - it has not then we assume that the invoice has been - paid and token have been issued. - TODO we need a global context that handles invoices - payments so the frontend can handle updates accordingly - */ - if (success || (!allInvoices.includes(paymentRequest) && expire > 0)) { - // add as history entry - await addToHistory({ - amount, - type: 2, - value: paymentRequest, - mints: [mintUrl], - }) - clearInvoiceInterval() - navigation.navigate('success', { amount, mint: formatMintUrl(mintUrl) }) + if (success) { + await handlePaidInvoice(entry) } } catch (e) { if (isErr(e) && e.message === 'tokens already issued for this invoice.') { - await addToHistory({ - amount, - type: 2, - value: paymentRequest, - mints: [mintUrl], - }) - clearInvoiceInterval() - navigation.navigate('success', { amount, mint: formatMintUrl(mintUrl) }) - return + await handlePaidInvoice(entry) } setPaid('unpaid') - if (isCancelling) { - navigation.navigate('dashboard') - } } } @@ -87,8 +67,7 @@ export default function InvoiceScreen({ navigation, route }: TMintInvoicePagePro useEffect(() => { const timeLeft = Math.ceil((expiryTime - Date.now()) / 1000) if (timeLeft < 0 || paid === 'paid') { - setExpire(0) - return + return setExpire(0) } if (expire && expire > 0) { setTimeout(() => setExpire(timeLeft - 1), 1000) @@ -98,9 +77,20 @@ export default function InvoiceScreen({ navigation, route }: TMintInvoicePagePro // auto check payment in intervals useEffect(() => { - intervalRef.current = setInterval(() => { - void handlePayment() - }, 3000) + void (async () => { + // add as pending history entry + const entry = await addHistoryEntry({ + amount, + type: 2, + value: paymentRequest, + mints: [mintUrl], + isPending: true + }) + // start checking for payment in 3s intervals + intervalRef.current = setInterval(() => { + void handlePayment(entry) + }, 3000) + })() return () => clearInvoiceInterval() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -110,7 +100,13 @@ export default function InvoiceScreen({ navigation, route }: TMintInvoicePagePro void handlePayment(true)} + handlePress={() => { + // clear interval for current invoice check + clearInvoiceInterval() + // start global invoice check + startGlobalInvoiceInterval() + navigation.navigate('dashboard') + }} /> >, - mints: string[], - amount: number, - invoice: string -) { - await historyStore.add({ - amount, - type: 2, - value: invoice, - mints, - fee: payResp?.realFee, - timestamp: Math.ceil(Date.now() / 1000) - }) -} - export async function getHistory({ order = 'DESC', start = 0, count = -1, orderBy = 'insertionOrder' }: ISelectParams = {}) { const history = await historyStore.getHistory({ order, start, count, orderBy }) return groupEntries(history) } +export function getHistoryEntryByInvoice(entries: IHistoryEntry[], invoice: string) { + return entries.find(i => i.value === invoice) +} + +export async function getHistoryEntriesByInvoices(invoices: IInvoice[]) { + const history = await historyStore.getHistory() + return history.filter(h => invoices.map(i => i.pr).includes(h.value)) +} + function groupEntries(history: IHistoryEntry[]) { return groupBy(history, i => getHistoryGroupDate(new Date(i.timestamp * 1000))) } @@ -114,4 +106,4 @@ function groupBy(arr: IHistoryEntry[], key: (i: IHistoryEntry) => string) { (groups[key(item)] ??= []).push(item) return groups }, {} as Record) -} +} \ No newline at end of file diff --git a/src/storage/store/latestHistoryEntries.ts b/src/storage/store/latestHistoryEntries.ts index e8e74e9b..57f48cef 100644 --- a/src/storage/store/latestHistoryEntries.ts +++ b/src/storage/store/latestHistoryEntries.ts @@ -32,4 +32,13 @@ export async function addToHistory(entry: Omit) { // latest 3 history entries // TODO provide a historyStore method to retreive the 3 latest entries await updateLatestHistory(item) return item +} + +export async function updateHistory(oldEntry: IHistoryEntry, newEntry: IHistoryEntry) { + await historyStore.updateHistoryEntry(oldEntry, newEntry) + const stored = await getLatestHistory() + const idx = stored.findIndex(i => i.value === oldEntry.value) + if (idx === -1) { return } + stored[idx] = newEntry + await store.setObj(STORE_KEYS.latestHistory, stored) } \ No newline at end of file From f15fcc770a00e7b7369753dd8965ed02bf2801b0 Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Wed, 3 Apr 2024 16:09:57 +0200 Subject: [PATCH 02/18] Update version to 0.4.0 in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index feedfd3f..1cfa49a5 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,7 @@ "blind-signatures", "lightning-network" ], - "version": "0.3.1", + "version": "0.4.0", "license": "AGPL-3.0-only", "bugs": { "url": "https://github.com/cashubtc/eNuts/issues" From 9530bceea57811404df9c6c3063e72817007b857 Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Wed, 3 Apr 2024 16:18:24 +0200 Subject: [PATCH 03/18] set invoice interval to 15s --- src/context/History.tsx | 4 +++- src/screens/Payment/Receive/Invoice.tsx | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/context/History.tsx b/src/context/History.tsx index f1ceaad6..2b9118c1 100644 --- a/src/context/History.tsx +++ b/src/context/History.tsx @@ -16,6 +16,8 @@ import { useTranslation } from 'react-i18next' import { useFocusClaimContext } from './FocusClaim' import { usePromptContext } from './Prompt' +export const INVOICE_INTERVAL = 15_000 + const useHistory = () => { const { t } = useTranslation([NS.common]) const [history, setHistory] = useState>({}) @@ -30,7 +32,7 @@ const useHistory = () => { const startGlobalInvoiceInterval = () => { intervalRef.current = setInterval(() => { void handlePendingInvoices() - }, 5000) + }, INVOICE_INTERVAL) } const clearInvoiceInterval = () => { diff --git a/src/screens/Payment/Receive/Invoice.tsx b/src/screens/Payment/Receive/Invoice.tsx index 76e56bb2..bd1b3eed 100644 --- a/src/screens/Payment/Receive/Invoice.tsx +++ b/src/screens/Payment/Receive/Invoice.tsx @@ -9,7 +9,7 @@ import { l } from '@log' import type { IHistoryEntry } from '@model' import type { TMintInvoicePageProps } from '@model/nav' import TopNav from '@nav/TopNav' -import { useHistoryContext } from '@src/context/History' +import { INVOICE_INTERVAL, useHistoryContext } from '@src/context/History' import { usePromptContext } from '@src/context/Prompt' import { useThemeContext } from '@src/context/Theme' import { NS } from '@src/i18n' @@ -89,7 +89,7 @@ export default function InvoiceScreen({ navigation, route }: TMintInvoicePagePro // start checking for payment in 3s intervals intervalRef.current = setInterval(() => { void handlePayment(entry) - }, 3000) + }, INVOICE_INTERVAL) })() return () => clearInvoiceInterval() // eslint-disable-next-line react-hooks/exhaustive-deps From bfad604aaa380580022bf4bc0800cae90f7aa363 Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Wed, 3 Apr 2024 16:26:06 +0200 Subject: [PATCH 04/18] add translations --- assets/translations/de.json | 4 +++- assets/translations/en.json | 4 +++- assets/translations/es.json | 4 +++- assets/translations/fr.json | 4 +++- assets/translations/hu.json | 4 +++- assets/translations/sw.json | 4 +++- src/context/History.tsx | 4 ++-- 7 files changed, 20 insertions(+), 8 deletions(-) diff --git a/assets/translations/de.json b/assets/translations/de.json index ad839d4f..2b8ff22d 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -250,7 +250,9 @@ "swapHint": "Diese Option erfordert eine Lightning-Zahlung, bringt Gebühren mit sich und kann die unbekannte Mint dennoch zur Liste hinzufügen, wenn eine Gebühren-Rückzahlung erfolgt.", "trustHint": "Die mit dem Token verbundene Mint wird zu Ihrer Vertrauensliste hinzugefügt.", "noDefaultHint": "Sie müssen eine Standard-Mint einrichten, um einen automatischen Tausch durchzuführen.", - "autoSwapSuccess": "Tausch erfolgreich!" + "autoSwapSuccess": "Tausch erfolgreich!", + "paidInvoice": "{{ count }} Rechnung wurde mit einem Gesamtbetrag von {{ total }} Sats bezahlt", + "paidInvoices": "{{ count }} Rechnungen wurden mit einem Gesamtbetrag von {{ total }} Sats bezahlt" }, "error": { "checkSpendableErr": "Fehler beim Überprüfen, ob der Token ausgegeben werden kann", diff --git a/assets/translations/en.json b/assets/translations/en.json index 4ad3ab1b..bb1a012a 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -250,7 +250,9 @@ "swapHint": "This option requires a Lightning payment, involves fees, and may still add the unknown mint to the list if a fee refund occurs.", "trustHint": "The mint associated with the token will be added to your trusted list.", "noDefaultHint": "You need to setup a default mint to perform an auto swap.", - "autoSwapSuccess": "Swap successful!" + "autoSwapSuccess": "Swap successful!", + "paidInvoice": "{{ count }} invoice has been paid with a total amount of {{ total }} Sats", + "paidInvoices": "{{ count }} invoices have been paid with a total amount of {{ total }} Sats" }, "error": { "checkSpendableErr": "Error while checking if token is spendable", diff --git a/assets/translations/es.json b/assets/translations/es.json index 18b9c0a1..a0a7db01 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -250,7 +250,9 @@ "swapHint": "Esta opción requiere un pago Lightning, implica una tarifa y aún puede agregar la ceca desconocida a la lista si se produce un reembolso de tarifa.", "trustHint": "La ceca asociada al token se añadirá a tu lista de confianza.", "noDefaultHint": "Necesitas configurar una ceca predeterminada para realizar un intercambio automático.", - "autoSwapSuccess": "¡Intercambio exitoso!" + "autoSwapSuccess": "¡Intercambio exitoso!", + "paidInvoice": "Se ha pagado {{ count }} factura con un importe total de {{ total }} Sats", + "paidInvoices": "Se han pagado {{ count }} facturas con un importe total de {{ total }} Sats" }, "error": { "checkSpendableErr": "Error al comprobar si el token puede ser gastado", diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 1b4d9b7c..121c5d5f 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -250,7 +250,9 @@ "swapHint": "Ce choix nécessite un paiement Lightning et peut entraîner des frais associés.", "trustHint": "La menthe associée au jeton sera ajoutée à votre liste de confiance.", "noDefaultHint": "Vous devez configurer une mint par défaut pour effectuer un échange automatique.", - "autoSwapSuccess": "Échange réussi!" + "autoSwapSuccess": "Échange réussi!", + "paidInvoice": "{{ count }} facture a été payée pour un montant total de {{ total }} Sats", + "paidInvoices": "{{ count }} factures ont été payées pour un montant total de {{ total }} Sats" }, "error": { "checkSpendableErr": "Erreur lors de la vérification si le token est dépensable", diff --git a/assets/translations/hu.json b/assets/translations/hu.json index 5a992d92..482f99a1 100644 --- a/assets/translations/hu.json +++ b/assets/translations/hu.json @@ -250,7 +250,9 @@ "swapHint": "Ez az opció egy Lightning-fizetést igényel ami költségekkel jár, és még akkor is hozzáadhatja az ismeretlen verdét a listádhoz, ha költségvisszatérítésre kerül sor.", "trustHint": "A tokenhez kapcsolódó verde hozzá lesz adva a megbízott listához.", "noDefaultHint": "Be kell állítanod egy alapértelmezett verdét az automatikus cseréhez.", - "autoSwapSuccess": "Csere sikeres!" + "autoSwapSuccess": "Csere sikeres!", + "paidInvoice": "{{ count }} számla kifizetésre került, összesen {{ total }} Sats összegben", + "paidInvoices": "{{ count }} számla kifizetésre került, összesen {{ total }} Sats összegben" }, "error": { "checkSpendableErr": "Hiba a token elkölthetőségének ellenőrzése közben", diff --git a/assets/translations/sw.json b/assets/translations/sw.json index b39ac54f..21943270 100644 --- a/assets/translations/sw.json +++ b/assets/translations/sw.json @@ -250,7 +250,9 @@ "swapHint": "Chaguo hili linahitaji malipo ya Lightning, linajumuisha ada, na linaweza bado kuongeza mint isiyojulikana kwenye orodha ikiwa kuna marejesho ya ada.", "trustHint": "Minti inayohusiana na alama itaongezwa kwenye orodha yako ya kuaminika.", "noDefaultHint": "Unahitaji kuweka kalibu ya kufanya ubadilishaji wa moja kwa moja.", - "autoSwapSuccess": "Kubadilishana kufanikiwa!" + "autoSwapSuccess": "Kubadilishana kufanikiwa!", + "paidInvoice": "{{ count }} ankara imelipwa kwa jumla ya {{ total }} Sats", + "paidInvoices": "Bilansi ya ankara {{ count }} zimelipwa kwa jumla ya {{ total }} Sats" }, "error": { "checkSpendableErr": "Kumetokea kosa wakati wa kuangalia ikiwa kijenzi kina pesa za kutumiwa", diff --git a/src/context/History.tsx b/src/context/History.tsx index 2b9118c1..3b4887f7 100644 --- a/src/context/History.tsx +++ b/src/context/History.tsx @@ -8,7 +8,7 @@ import { historyStore, store } from '@store' import { STORE_KEYS } from '@store/consts' import { getHistory, getHistoryEntriesByInvoices, getHistoryEntryByInvoice } from '@store/HistoryStore' import { addToHistory, getLatestHistory, updateHistory } from '@store/latestHistoryEntries' -import { decodeLnInvoice } from '@util' +import { decodeLnInvoice, formatInt } from '@util' import { requestToken } from '@wallet' import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -78,7 +78,7 @@ const useHistory = () => { // notify user if (paid.count > 0) { openPromptAutoClose({ - msg: 'Test message', // t(paid.count > 1 ? 'paidInvoices' : 'paidInvoice', { count: paid.count, total: paid.amount }) + msg: t(paid.count > 1 ? 'paidInvoices' : 'paidInvoice', { count: paid.count, total: formatInt(paid.amount) }), success: true }) paid = { count: 0, amount: 0 } From 960acd6f8060151cef0b266418268492d36fb183 Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Wed, 3 Apr 2024 16:26:50 +0200 Subject: [PATCH 05/18] destructuring assignment in useHistory --- src/context/History.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/context/History.tsx b/src/context/History.tsx index 3b4887f7..5e996a66 100644 --- a/src/context/History.tsx +++ b/src/context/History.tsx @@ -58,8 +58,8 @@ const useHistory = () => { let paid = { count: 0, amount: 0 } for (const invoice of invoices) { try { - const success = await requestToken(invoice.mintUrl, invoice.amount, invoice.hash) - if (success.success) { + const { success } = await requestToken(invoice.mintUrl, invoice.amount, invoice.hash) + if (success) { paid.count++ paid.amount += invoice.amount const entry = getHistoryEntryByInvoice(allHisoryEntries.current, invoice.pr) From 1f06325f2d7cb61d34de63e1df2f26ab326dd36a Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Wed, 3 Apr 2024 16:35:06 +0200 Subject: [PATCH 06/18] increase invoice interval from 15s to 1 minute --- src/context/History.tsx | 5 ++--- src/screens/Payment/Receive/Invoice.tsx | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/context/History.tsx b/src/context/History.tsx index 5e996a66..42312fd2 100644 --- a/src/context/History.tsx +++ b/src/context/History.tsx @@ -1,5 +1,6 @@ /* eslint-disable no-await-in-loop */ +import { MinuteInMs } from '@consts' import { delInvoice, getAllInvoices } from '@db' import { l } from '@log' import type { IHistoryEntry } from '@model' @@ -16,8 +17,6 @@ import { useTranslation } from 'react-i18next' import { useFocusClaimContext } from './FocusClaim' import { usePromptContext } from './Prompt' -export const INVOICE_INTERVAL = 15_000 - const useHistory = () => { const { t } = useTranslation([NS.common]) const [history, setHistory] = useState>({}) @@ -32,7 +31,7 @@ const useHistory = () => { const startGlobalInvoiceInterval = () => { intervalRef.current = setInterval(() => { void handlePendingInvoices() - }, INVOICE_INTERVAL) + }, MinuteInMs) } const clearInvoiceInterval = () => { diff --git a/src/screens/Payment/Receive/Invoice.tsx b/src/screens/Payment/Receive/Invoice.tsx index bd1b3eed..2a6e0e34 100644 --- a/src/screens/Payment/Receive/Invoice.tsx +++ b/src/screens/Payment/Receive/Invoice.tsx @@ -4,12 +4,12 @@ import { ShareIcon, WalletIcon } from '@comps/Icons' import Loading from '@comps/Loading' import QR from '@comps/QR' import Txt from '@comps/Txt' -import { _testmintUrl, isIOS } from '@consts' +import { _testmintUrl, isIOS, MinuteInMs } from '@consts' import { l } from '@log' import type { IHistoryEntry } from '@model' import type { TMintInvoicePageProps } from '@model/nav' import TopNav from '@nav/TopNav' -import { INVOICE_INTERVAL, useHistoryContext } from '@src/context/History' +import { useHistoryContext } from '@src/context/History' import { usePromptContext } from '@src/context/Prompt' import { useThemeContext } from '@src/context/Theme' import { NS } from '@src/i18n' @@ -89,7 +89,7 @@ export default function InvoiceScreen({ navigation, route }: TMintInvoicePagePro // start checking for payment in 3s intervals intervalRef.current = setInterval(() => { void handlePayment(entry) - }, INVOICE_INTERVAL) + }, MinuteInMs) })() return () => clearInvoiceInterval() // eslint-disable-next-line react-hooks/exhaustive-deps From eeb7f6e78afafbf2250aa949655d9e12344712ef Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Wed, 3 Apr 2024 16:36:55 +0200 Subject: [PATCH 07/18] check pending invoices on app start --- src/context/History.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/context/History.tsx b/src/context/History.tsx index 42312fd2..0f3f8e2a 100644 --- a/src/context/History.tsx +++ b/src/context/History.tsx @@ -109,6 +109,7 @@ const useHistory = () => { } useEffect(() => { + void handlePendingInvoices() void setHistoryEntries() // request token of pending invoices in interval until all are paid or expired startGlobalInvoiceInterval() From 68680f9669875165ec89adb623a847d151c87cd2 Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Wed, 3 Apr 2024 16:41:54 +0200 Subject: [PATCH 08/18] update logs and icon color in latest history entries --- src/components/Balance.tsx | 2 +- src/context/History.tsx | 1 + src/screens/Payment/Receive/Invoice.tsx | 1 + src/wallet/index.ts | 2 -- 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Balance.tsx b/src/components/Balance.tsx index 6191a51a..b364e00c 100644 --- a/src/components/Balance.tsx +++ b/src/components/Balance.tsx @@ -87,7 +87,7 @@ export default function Balance({ balance, nav }: IBalanceProps) { key={h.timestamp} icon={ h.isPending ? - + : h.type === txType.RESTORE ? diff --git a/src/context/History.tsx b/src/context/History.tsx index 0f3f8e2a..47abae2b 100644 --- a/src/context/History.tsx +++ b/src/context/History.tsx @@ -30,6 +30,7 @@ const useHistory = () => { const startGlobalInvoiceInterval = () => { intervalRef.current = setInterval(() => { + l('checking pending invoices in interval of history context') void handlePendingInvoices() }, MinuteInMs) } diff --git a/src/screens/Payment/Receive/Invoice.tsx b/src/screens/Payment/Receive/Invoice.tsx index 2a6e0e34..96eafc02 100644 --- a/src/screens/Payment/Receive/Invoice.tsx +++ b/src/screens/Payment/Receive/Invoice.tsx @@ -88,6 +88,7 @@ export default function InvoiceScreen({ navigation, route }: TMintInvoicePagePro }) // start checking for payment in 3s intervals intervalRef.current = setInterval(() => { + l('checking pending invoices in invoice screen') void handlePayment(entry) }, MinuteInMs) })() diff --git a/src/wallet/index.ts b/src/wallet/index.ts index 33950626..7e4334f5 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -308,8 +308,6 @@ export async function getCounterByMintUrl(mintUrl: string) { return 0 } l('[getCounterByMintUrl] ', { mintUrl, keysetId, storedCounter: counter }) - // await store.set(storeKey, counter) - l('[getCounterByMintUrl] ', { keysetId, counter: counter }) return +counter } catch (e) { l('[getCounterByMintUrl] Error while getCounter: ', e) From 6a154136bc58c8b105fc209175cd1ed47d014bba Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Wed, 3 Apr 2024 17:24:19 +0200 Subject: [PATCH 09/18] update history details screen and entry component --- src/screens/History/Details.tsx | 40 +++++++++++++++++++-------------- src/screens/History/Entry.tsx | 19 +++++++++++----- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/screens/History/Details.tsx b/src/screens/History/Details.tsx index aaee0a46..47ed2e58 100644 --- a/src/screens/History/Details.tsx +++ b/src/screens/History/Details.tsx @@ -8,12 +8,11 @@ import Txt from '@comps/Txt' import type { THistoryEntryPageProps } from '@model/nav' import TopNav from '@nav/TopNav' import { truncateStr } from '@nostr/util' +import { useHistoryContext } from '@src/context/History' import { usePromptContext } from '@src/context/Prompt' import { useThemeContext } from '@src/context/Theme' import { NS } from '@src/i18n' import { txType } from '@src/model' -import { historyStore } from '@store' -import { addToHistory } from '@store/latestHistoryEntries' import { getCustomMintNames } from '@store/mintStore' import { globals, mainColors } from '@styles' import { copyStrToClipboard, formatInt, formatMintUrl, formatSatStr, getLnInvoiceInfo, isNum, isUndef } from '@util' @@ -24,7 +23,6 @@ import { ScrollView, Text, TouchableOpacity, View } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { s, ScaledSheet } from 'react-native-size-matters' - const initialCopyState = { value: false, hash: false, @@ -43,9 +41,11 @@ export default function DetailsPage({ navigation, route }: THistoryEntryPageProp sender, recipient, fee, - isSpent + isSpent, + isPending, } = route.params.entry const { color } = useThemeContext() + const { addHistoryEntry, updateHistoryEntry } = useHistoryContext() const [copy, setCopy] = useState(initialCopyState) const [spent, setSpent] = useState(isSpent) const { loading, startLoading, stopLoading } = useLoading() @@ -108,7 +108,7 @@ export default function DetailsPage({ navigation, route }: THistoryEntryPageProp const isSpendable = await isTokenSpendable(value) setSpent(!isSpendable) // update history item - await historyStore.updateHistoryEntry(route.params.entry, { ...route.params.entry, isSpent: !isSpendable }) + await updateHistoryEntry(route.params.entry, { ...route.params.entry, isSpent: !isSpendable }) stopLoading() } @@ -118,13 +118,12 @@ export default function DetailsPage({ navigation, route }: THistoryEntryPageProp if (!success) { openPromptAutoClose({ msg: t('invalidOrSpent') }) setSpent(true) - stopLoading() - return + return stopLoading() } // entry.isSpent can only be false here and is not undefined anymore - await historyStore.updateHistoryEntry({ ...route.params.entry, isSpent: false }, { ...route.params.entry, isSpent: true }) + await updateHistoryEntry({ ...route.params.entry, isSpent: false }, { ...route.params.entry, isSpent: true }) setSpent(true) - await addToHistory({ ...route.params.entry, amount: Math.abs(route.params.entry.amount), isSpent: true }) + await addHistoryEntry({ ...route.params.entry, amount: Math.abs(route.params.entry.amount), isSpent: true }) stopLoading() openPromptAutoClose({ msg: t( @@ -161,14 +160,14 @@ export default function DetailsPage({ navigation, route }: THistoryEntryPageProp // used in interval to check if token is spent while qr sheet is open const checkPayment = async () => { + if (type === txType.SEND_RECEIVE) { return clearTokenInterval() } const isSpendable = await isTokenSpendable(value) setSpent(!isSpendable) if (!isSpendable) { clearTokenInterval() setQr({ ...qr, open: false }) openPromptAutoClose({ msg: t('isSpent', { ns: NS.history }), success: true }) - // update history item - await historyStore.updateHistoryEntry(route.params.entry, { ...route.params.entry, isSpent: true }) + await updateHistoryEntry(route.params.entry, { ...route.params.entry, isSpent: true }) } } @@ -187,7 +186,7 @@ export default function DetailsPage({ navigation, route }: THistoryEntryPageProp // auto check payment in intervals useEffect(() => { - if (!qr.open || spent) { return clearTokenInterval() } + if (!qr.open || spent || type === txType.SEND_RECEIVE) { return clearTokenInterval() } startTokenInterval() return () => clearTokenInterval() // eslint-disable-next-line react-hooks/exhaustive-deps @@ -203,6 +202,9 @@ export default function DetailsPage({ navigation, route }: THistoryEntryPageProp + {isPending && + + } {getAmount()} @@ -344,11 +346,15 @@ export default function DetailsPage({ navigation, route }: THistoryEntryPageProp {/* LN payment fees */} - - - - - + {!isPending && + <> + + + + + + + } } {/* QR code */} diff --git a/src/screens/History/Entry.tsx b/src/screens/History/Entry.tsx index dd3b5c42..846800ea 100644 --- a/src/screens/History/Entry.tsx +++ b/src/screens/History/Entry.tsx @@ -1,6 +1,6 @@ -import { IncomingArrowIcon, OutgoingArrowIcon } from '@comps/Icons' +import { ClockIcon, IncomingArrowIcon, OutgoingArrowIcon } from '@comps/Icons' import Txt from '@comps/Txt' -import { type IHistoryEntry,txType } from '@model' +import { type IHistoryEntry, txType } from '@model' import type { THistoryPageProps } from '@model/nav' import { useThemeContext } from '@src/context/Theme' import { NS } from '@src/i18n' @@ -36,7 +36,12 @@ export default function HistoryEntry({ nav, item }: IHistoryEntryProps) { const getIcon = () => item.amount < 0 ? : - + item.isPending ? + + + + : + return ( - + 0 ? 0 : vs(10) }]}> 0 && item.type < txType.SWAP ? '+' : ''}${formatSatStr(item.type === txType.SWAP || item.type === txType.RESTORE ? Math.abs(item.amount) : item.amount, 'standard')}`} styles={[{ color: getTxColor(), marginBottom: vs(5), textAlign: 'right' }]} /> - {isNum(item.fee) && + {isNum(item.fee) && item.fee > 0 && {t('fee', { ns: NS.common })}: {item.fee} } - ) @@ -87,4 +91,7 @@ const styles = ScaledSheet.create({ position: 'absolute', right: 0, }, + clockIconWrap: { + marginLeft: '-5@s', + }, }) \ No newline at end of file From 421b2b2906d44218c886c0b602a9f039936b58f7 Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Wed, 3 Apr 2024 18:39:45 +0200 Subject: [PATCH 10/18] use history context functions, handle expired invoices --- src/components/Balance.tsx | 31 +++++++++++++------ src/components/ClipboardModal.tsx | 6 ++-- src/components/Icons.tsx | 7 +++++ src/components/hooks/Restore.tsx | 8 +++-- src/context/History.tsx | 10 ++++-- src/model/index.ts | 1 + src/screens/Dashboard.tsx | 5 +-- src/screens/History/Details.tsx | 23 +++++++++----- src/screens/History/Entry.tsx | 16 +++++++--- src/screens/Onboarding.tsx | 4 +-- src/screens/Payment/Processing.tsx | 27 ++++++---------- src/screens/Payment/Receive/Invoice.tsx | 4 +-- src/screens/Payment/Receive/nostrDM/Token.tsx | 5 +-- src/screens/QRScan/QRProcessing.tsx | 7 +++-- 14 files changed, 96 insertions(+), 58 deletions(-) diff --git a/src/components/Balance.tsx b/src/components/Balance.tsx index b364e00c..ac162b66 100644 --- a/src/components/Balance.tsx +++ b/src/components/Balance.tsx @@ -1,4 +1,4 @@ -import { CheckmarkIcon, ClockIcon, EcashIcon, SwapCurrencyIcon, ZapIcon } from '@comps/Icons' +import { CheckmarkIcon, ClockIcon, CloseCircleIcon, EcashIcon, SwapCurrencyIcon, ZapIcon } from '@comps/Icons' import { setPreferences } from '@db' import { type TTXType, txType } from '@model' import type { RootStackParamList } from '@model/nav' @@ -86,21 +86,25 @@ export default function Balance({ balance, nav }: IBalanceProps) { : - h.type === txType.RESTORE ? - + h.isExpired ? + : - h.type === txType.LIGHTNING || h.type === txType.SWAP ? - + h.type === txType.RESTORE ? + : - + h.type === txType.LIGHTNING || h.type === txType.SWAP ? + + : + } isSwap={h.type === txType.SWAP} txType={getTxTypeStr(h.type)} timestamp={h.timestamp} amount={h.amount} + isExpired={h.isExpired} onPress={() => nav?.navigate('history entry details', { entry: h })} /> )) @@ -123,10 +127,11 @@ interface IHistoryEntryProps { isSwap?: boolean timestamp: number amount: number + isExpired?: boolean onPress: () => void } -function HistoryEntry({ icon, txType, isSwap, timestamp, amount, onPress }: IHistoryEntryProps) { +function HistoryEntry({ icon, txType, isSwap, timestamp, amount, isExpired, onPress }: IHistoryEntryProps) { const { t } = useTranslation([NS.history]) const { color, highlight } = useThemeContext() @@ -148,7 +153,15 @@ function HistoryEntry({ icon, txType, isSwap, timestamp, amount, onPress }: IHis - + ) } diff --git a/src/components/ClipboardModal.tsx b/src/components/ClipboardModal.tsx index f0d6f13e..bac68009 100644 --- a/src/components/ClipboardModal.tsx +++ b/src/components/ClipboardModal.tsx @@ -2,10 +2,10 @@ import { getEncodedToken } from '@cashu/cashu-ts' import { type RootStackParamList } from '@model/nav' import { type NavigationProp, useNavigation } from '@react-navigation/core' import { useFocusClaimContext } from '@src/context/FocusClaim' +import { useHistoryContext } from '@src/context/History' import { usePromptContext } from '@src/context/Prompt' import { useThemeContext } from '@src/context/Theme' import { NS } from '@src/i18n' -import { addToHistory } from '@store/latestHistoryEntries' import { globals, mainColors } from '@styles' import { copyStrToClipboard, formatInt, formatMintUrl, formatSatStr, isErr } from '@util' import { claimToken } from '@wallet' @@ -28,6 +28,7 @@ export default function ClipboardModal() { const { tokenInfo, claimOpen, setClaimOpen, setClaimed, closeModal } = useFocusClaimContext() const { loading, startLoading, stopLoading } = useLoading() const { openPromptAutoClose } = usePromptContext() + const { addHistoryEntry } = useHistoryContext() const handleRedeem = async () => { startLoading() @@ -56,8 +57,7 @@ export default function ClipboardModal() { } stopLoading() setClaimOpen(false) - // add as history entry (receive ecash) - await addToHistory({ + await addHistoryEntry({ amount: info.value, type: 1, value: encoded, diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index eb929890..25936655 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -647,6 +647,13 @@ export function ClockIcon({ width, height, color }: TIconProps) { ) } +export function CloseCircleIcon({ width, height, color }: TIconProps) { + return ( + + + + ) +} const styles = StyleSheet.create({ nostrIcon: { marginLeft: -5 diff --git a/src/components/hooks/Restore.tsx b/src/components/hooks/Restore.tsx index b2a02b78..71e88f29 100644 --- a/src/components/hooks/Restore.tsx +++ b/src/components/hooks/Restore.tsx @@ -4,9 +4,9 @@ import { addToken, getMintBalance } from '@db' import { l } from '@log' import type { RootStackParamList } from '@model/nav' import { type NavigationProp, useNavigation } from '@react-navigation/core' +import { useHistoryContext } from '@src/context/History' import { usePromptContext } from '@src/context/Prompt' import { NS } from '@src/i18n' -import { addToHistory } from '@store/latestHistoryEntries' import { saveSeed } from '@store/restore' import { isErr } from '@util' import { _setKeys, getCounterByMintUrl, getSeedWalletByMnemonic, incrementCounterByMintUrl } from '@wallet' @@ -35,6 +35,7 @@ export function useRestore({ mintUrl, mnemonic, comingFromOnboarding }: IUseRest const navigation = useNavigation() const { t } = useTranslation([NS.common]) const { openPromptAutoClose } = usePromptContext() + const { addHistoryEntry } = useHistoryContext() const [restored, setRestored] = useState({ ...defaultRestoreState }) @@ -52,7 +53,7 @@ export function useRestore({ mintUrl, mnemonic, comingFromOnboarding }: IUseRest return navigation.navigate('dashboard') } const bal = await getMintBalance(mintUrl) - await addToHistory({ + await addHistoryEntry({ mints: [mintUrl], amount: bal, type: 4, @@ -130,7 +131,8 @@ export function useRestore({ mintUrl, mnemonic, comingFromOnboarding }: IUseRest } } void restore() - }, [comingFromOnboarding, mintUrl, mnemonic, navigation, openPromptAutoClose, t]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mintUrl]) return { ...restored } } diff --git a/src/context/History.tsx b/src/context/History.tsx index 47abae2b..c80bc8b5 100644 --- a/src/context/History.tsx +++ b/src/context/History.tsx @@ -57,12 +57,12 @@ const useHistory = () => { } let paid = { count: 0, amount: 0 } for (const invoice of invoices) { + const entry = getHistoryEntryByInvoice(allHisoryEntries.current, invoice.pr) try { const { success } = await requestToken(invoice.mintUrl, invoice.amount, invoice.hash) if (success) { paid.count++ paid.amount += invoice.amount - const entry = getHistoryEntryByInvoice(allHisoryEntries.current, invoice.pr) if (entry) { await updateHistoryEntry(entry, { ...entry, isPending: false }) } @@ -73,7 +73,13 @@ const useHistory = () => { } catch (_) {/* ignore */ } const { expiry } = decodeLnInvoice(invoice.pr) const date = new Date((invoice.time * 1000) + (expiry * 1000)).getTime() - if (Date.now() > date) { await delInvoice(invoice.hash) } + if (Date.now() > date) { + l('INVOICE EXPIRED!', invoice.pr) + await delInvoice(invoice.hash) + if (entry) { + await updateHistoryEntry(entry, { ...entry, isExpired: true }) + } + } } // notify user if (paid.count > 0) { diff --git a/src/model/index.ts b/src/model/index.ts index 12fceb2f..a3bc27e9 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -119,6 +119,7 @@ export interface IHistoryEntry { fee?: number, isSpent?: boolean // is token spendable isPending?: boolean // is LN invoice pending + isExpired?: boolean // is LN invoice expired } diff --git a/src/screens/Dashboard.tsx b/src/screens/Dashboard.tsx index 8df578e3..ecdb4a32 100644 --- a/src/screens/Dashboard.tsx +++ b/src/screens/Dashboard.tsx @@ -15,6 +15,7 @@ import type { TBeforeRemoveEvent, TDashboardPageProps } from '@model/nav' import BottomNav from '@nav/BottomNav' import { preventBack } from '@nav/utils' import { useFocusClaimContext } from '@src/context/FocusClaim' +import { useHistoryContext } from '@src/context/History' import { useInitialURL } from '@src/context/Linking' import { useNostrContext } from '@src/context/Nostr' import { usePromptContext } from '@src/context/Prompt' @@ -22,7 +23,6 @@ import { useThemeContext } from '@src/context/Theme' import { NS } from '@src/i18n' import { store } from '@store' import { STORE_KEYS } from '@store/consts' -import { addToHistory } from '@store/latestHistoryEntries' import { getDefaultMint } from '@store/mintStore' import { highlight as hi, mainColors } from '@styles' import { extractStrFromURL, getStrFromClipboard, hasTrustedMint, isCashuToken, isLnInvoice, isStr } from '@util' @@ -46,6 +46,7 @@ export default function Dashboard({ navigation, route }: TDashboardPageProps) { const { loading, startLoading, stopLoading } = useLoading() // Prompt modal const { openPromptAutoClose } = usePromptContext() + const { addHistoryEntry } = useHistoryContext() // Cashu token hook const { token, @@ -131,7 +132,7 @@ export default function Dashboard({ navigation, route }: TDashboardPageProps) { return } // add as history entry (receive ecash) - await addToHistory({ + await addHistoryEntry({ amount: info.value, type: 1, value: encodedToken, diff --git a/src/screens/History/Details.tsx b/src/screens/History/Details.tsx index 47ed2e58..67f14dc5 100644 --- a/src/screens/History/Details.tsx +++ b/src/screens/History/Details.tsx @@ -43,6 +43,7 @@ export default function DetailsPage({ navigation, route }: THistoryEntryPageProp fee, isSpent, isPending, + isExpired } = route.params.entry const { color } = useThemeContext() const { addHistoryEntry, updateHistoryEntry } = useHistoryContext() @@ -202,16 +203,22 @@ export default function DetailsPage({ navigation, route }: THistoryEntryPageProp - {isPending && + {isPending && !isExpired && } - - {getAmount()} - - + {isExpired ? + + : + <> + + {getAmount()} + + + + } {/* Settle Time */} diff --git a/src/screens/History/Entry.tsx b/src/screens/History/Entry.tsx index 846800ea..58ab31c9 100644 --- a/src/screens/History/Entry.tsx +++ b/src/screens/History/Entry.tsx @@ -1,4 +1,4 @@ -import { ClockIcon, IncomingArrowIcon, OutgoingArrowIcon } from '@comps/Icons' +import { ClockIcon, CloseCircleIcon, IncomingArrowIcon, OutgoingArrowIcon } from '@comps/Icons' import Txt from '@comps/Txt' import { type IHistoryEntry, txType } from '@model' import type { THistoryPageProps } from '@model/nav' @@ -36,12 +36,15 @@ export default function HistoryEntry({ nav, item }: IHistoryEntryProps) { const getIcon = () => item.amount < 0 ? : - item.isPending ? + item.isPending && !item.isExpired ? : - + item.isExpired ? + + : + return ( 0 ? 0 : vs(10) }]}> 0 && item.type < txType.SWAP ? '+' : ''}${formatSatStr(item.type === txType.SWAP || item.type === txType.RESTORE ? Math.abs(item.amount) : item.amount, 'standard')}`} + txt={ + item.isExpired ? + t('expired', { ns: NS.common }) + : + `${item.amount > 0 && item.type < txType.SWAP ? '+' : ''}${formatSatStr(item.type === txType.SWAP || item.type === txType.RESTORE ? Math.abs(item.amount) : item.amount, 'standard')}` + } styles={[{ color: getTxColor(), marginBottom: vs(5), textAlign: 'right' }]} /> {isNum(item.fee) && item.fee > 0 && diff --git a/src/screens/Onboarding.tsx b/src/screens/Onboarding.tsx index 36dda72c..b68d3c06 100644 --- a/src/screens/Onboarding.tsx +++ b/src/screens/Onboarding.tsx @@ -4,7 +4,7 @@ import type { TOnboardingPageProps } from '@model/nav' import { NS } from '@src/i18n' import { store } from '@src/storage/store' import { STORE_KEYS } from '@src/storage/store/consts' -import { H_Colors } from '@styles/colors' +import { H_Colors, mainColors } from '@styles/colors' import { useTranslation } from 'react-i18next' import { Image, TouchableOpacity } from 'react-native' import Onboarding from 'react-native-onboarding-swiper' @@ -53,7 +53,7 @@ export default function OnboardingScreen({ navigation }: TOnboardingPageProps) { style={{ marginRight: s(20) }} testID='onboarding-done' > - + )} /> diff --git a/src/screens/Payment/Processing.tsx b/src/screens/Payment/Processing.tsx index db365a72..b546cdaa 100644 --- a/src/screens/Payment/Processing.tsx +++ b/src/screens/Payment/Processing.tsx @@ -6,13 +6,12 @@ import type { TBeforeRemoveEvent, TProcessingPageProps } from '@model/nav' import { preventBack } from '@nav/utils' import { pool } from '@nostr/class/Pool' import { getNostrUsername } from '@nostr/util' +import { useHistoryContext } from '@src/context/History' import { useInitialURL } from '@src/context/Linking' import { useNostrContext } from '@src/context/Nostr' import { useThemeContext } from '@src/context/Theme' import { NS } from '@src/i18n' import { isLnurlOrAddress } from '@src/util/lnurl' -import { addLnPaymentToHistory } from '@store/HistoryStore' -import { addToHistory, updateLatestHistory } from '@store/latestHistoryEntries' import { getDefaultMint } from '@store/mintStore' import { globals } from '@styles' import { decodeLnInvoice, getInvoiceFromLnurl, isErr, isNum, uniqByIContacts } from '@util' @@ -32,6 +31,7 @@ export default function ProcessingScreen({ navigation, route }: TProcessingPageP const { color } = useThemeContext() const { setNostr } = useNostrContext() const { clearUrl } = useInitialURL() + const { addHistoryEntry } = useHistoryContext() const { mint, tokenInfo, @@ -118,21 +118,12 @@ export default function ProcessingScreen({ navigation, route }: TProcessingPageP // here it could be a routing path finding issue return handleError({ e: isErr(res.error) ? res.error : undefined }) } - // payment success, add as history entry - await addLnPaymentToHistory( - res, - [mint.mintUrl], - -amount, - target - ) - // update latest 3 history entries - await updateLatestHistory({ - amount: -amount, - fee: res.realFee, + await addHistoryEntry({ + amount, type: 2, - value: target, + value: invoice, mints: [mint.mintUrl], - timestamp: Math.ceil(Date.now() / 1000) + fee: res.realFee }) // reset zap deep link clearUrl() @@ -157,7 +148,7 @@ export default function ProcessingScreen({ navigation, route }: TProcessingPageP // TODO this process can take a while, we need to add it as pending transaction const res = await autoMintSwap(mint.mintUrl, targetMint.mintUrl, amount, estFee ?? 0, proofs) // add as history entry (multimint swap) - await addToHistory({ + await addHistoryEntry({ amount: -amount, fee: res.payResult.realFee, type: 3, @@ -193,7 +184,7 @@ export default function ProcessingScreen({ navigation, route }: TProcessingPageP } const amountSent = tokenInfo.value - estFeeResp // add as history entry (multimint swap) - await addToHistory({ + await addHistoryEntry({ amount: -amountSent, fee: payResult.realFee, type: 3, @@ -213,7 +204,7 @@ export default function ProcessingScreen({ navigation, route }: TProcessingPageP try { const token = await sendToken(mint.mintUrl, amount, memo || '', proofs) // add as history entry (send ecash) - const entry = await addToHistory({ + const entry = await addHistoryEntry({ amount: -amount, type: 1, value: token, diff --git a/src/screens/Payment/Receive/Invoice.tsx b/src/screens/Payment/Receive/Invoice.tsx index 96eafc02..1a8c22bc 100644 --- a/src/screens/Payment/Receive/Invoice.tsx +++ b/src/screens/Payment/Receive/Invoice.tsx @@ -4,7 +4,7 @@ import { ShareIcon, WalletIcon } from '@comps/Icons' import Loading from '@comps/Loading' import QR from '@comps/QR' import Txt from '@comps/Txt' -import { _testmintUrl, isIOS, MinuteInMs } from '@consts' +import { _testmintUrl, isIOS } from '@consts' import { l } from '@log' import type { IHistoryEntry } from '@model' import type { TMintInvoicePageProps } from '@model/nav' @@ -90,7 +90,7 @@ export default function InvoiceScreen({ navigation, route }: TMintInvoicePagePro intervalRef.current = setInterval(() => { l('checking pending invoices in invoice screen') void handlePayment(entry) - }, MinuteInMs) + }, 20_000) })() return () => clearInvoiceInterval() // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/screens/Payment/Receive/nostrDM/Token.tsx b/src/screens/Payment/Receive/nostrDM/Token.tsx index 0dcc346a..2b1a5f21 100644 --- a/src/screens/Payment/Receive/nostrDM/Token.tsx +++ b/src/screens/Payment/Receive/nostrDM/Token.tsx @@ -9,11 +9,11 @@ import { l } from '@log' import type { ITokenInfo } from '@model' import type { IContact, INostrDm } from '@model/nostr' import { getNostrUsername, truncateStr } from '@nostr/util' +import { useHistoryContext } from '@src/context/History' import { useNostrContext } from '@src/context/Nostr' import { usePromptContext } from '@src/context/Prompt' import { useThemeContext } from '@src/context/Theme' import { NS } from '@src/i18n' -import { addToHistory } from '@store/latestHistoryEntries' import { getDefaultMint } from '@store/mintStore' import { updateNostrRedeemed } from '@store/nostrDms' import { highlight as hi, mainColors } from '@styles' @@ -42,6 +42,7 @@ export default function Token({ sender, token, id, dms, setDms, mints }: ITokenP const [info, setInfo] = useState() const { trustModal, setTrustModal } = useCashuToken() const { loading, startLoading, stopLoading } = useLoading() + const { addHistoryEntry } = useHistoryContext() const handleStoreRedeemed = async () => { await updateNostrRedeemed(id) @@ -89,7 +90,7 @@ export default function Token({ sender, token, id, dms, setDms, mints }: ITokenP return stopLoading() } // add as history entry (receive ecash from nostr) - await addToHistory({ + await addHistoryEntry({ amount: info.value, type: 1, value: token, diff --git a/src/screens/QRScan/QRProcessing.tsx b/src/screens/QRScan/QRProcessing.tsx index e65dbb43..5d292bd2 100644 --- a/src/screens/QRScan/QRProcessing.tsx +++ b/src/screens/QRScan/QRProcessing.tsx @@ -4,11 +4,11 @@ import { getMintsBalances } from '@db' import { l } from '@log' import type { TBeforeRemoveEvent, TQRProcessingPageProps } from '@model/nav' import { preventBack } from '@nav/utils' +import { useHistoryContext } from '@src/context/History' import { useThemeContext } from '@src/context/Theme' import { NS } from '@src/i18n' import { isErr } from '@src/util' import { getLnurlData } from '@src/util/lnurl' -import { addToHistory } from '@store/latestHistoryEntries' import { getCustomMintNames } from '@store/mintStore' import { globals } from '@styles' import { checkFees, claimToken } from '@wallet' @@ -18,9 +18,10 @@ import { View } from 'react-native' import { ScaledSheet } from 'react-native-size-matters' export default function QRProcessingScreen({ navigation, route }: TQRProcessingPageProps) { + const { tokenInfo, token, ln, lnurl, scanned } = route.params const { t } = useTranslation([NS.mints]) const { color } = useThemeContext() - const { tokenInfo, token, ln, lnurl, scanned } = route.params + const { addHistoryEntry } = useHistoryContext() const getProcessingtxt = () => { if (token && tokenInfo) { return 'claiming' } @@ -42,7 +43,7 @@ export default function QRProcessingScreen({ navigation, route }: TQRProcessingP return } // add as history entry (receive ecash) - await addToHistory({ + await addHistoryEntry({ amount: tokenInfo.value, type: 1, value: token, From 38a0c5f74581cca64f4ca69ea3de38c8aa5e9617 Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Wed, 3 Apr 2024 19:01:14 +0200 Subject: [PATCH 11/18] lint and disable invoice check in interval --- src/components/App.tsx | 2 +- src/context/History.tsx | 38 ++++++++++++------------- src/screens/Payment/Receive/Invoice.tsx | 7 ++--- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 427898e6..b3ca053e 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -9,6 +9,7 @@ import { CustomErrorBoundary } from '@screens/ErrorScreen/ErrorBoundary' import { ErrorDetails } from '@screens/ErrorScreen/ErrorDetails' import * as Sentry from '@sentry/react-native' import { FocusClaimProvider } from '@src/context/FocusClaim' +import { HistoryProvider } from '@src/context/History' import { KeyboardProvider } from '@src/context/Keyboard' import { NostrProvider } from '@src/context/Nostr' import { PinCtx } from '@src/context/Pin' @@ -33,7 +34,6 @@ import { SafeAreaProvider } from 'react-native-safe-area-context' import Blank from './Blank' import ClipboardModal from './ClipboardModal' import Toaster from './Toaster' -import { HistoryProvider } from '@src/context/History' LogBox.ignoreLogs(['is deprecated']) // LogBox.ignoreLogs([/expo-image/gmi]) diff --git a/src/context/History.tsx b/src/context/History.tsx index c80bc8b5..aa4010fa 100644 --- a/src/context/History.tsx +++ b/src/context/History.tsx @@ -1,6 +1,4 @@ /* eslint-disable no-await-in-loop */ - -import { MinuteInMs } from '@consts' import { delInvoice, getAllInvoices } from '@db' import { l } from '@log' import type { IHistoryEntry } from '@model' @@ -24,23 +22,23 @@ const useHistory = () => { // State to indicate token claim from clipboard after app comes to the foreground, to re-render total balance const { claimed } = useFocusClaimContext() const { openPromptAutoClose } = usePromptContext() - const intervalRef = useRef(null) + // const intervalRef = useRef(null) const allHisoryEntries = useRef([]) const hasEntries = useMemo(() => Object.keys(history).length > 0, [history]) - const startGlobalInvoiceInterval = () => { - intervalRef.current = setInterval(() => { - l('checking pending invoices in interval of history context') - void handlePendingInvoices() - }, MinuteInMs) - } + // const startGlobalInvoiceInterval = () => { + // intervalRef.current = setInterval(() => { + // l('checking pending invoices in interval of history context') + // void handlePendingInvoices() + // }, MinuteInMs) + // } - const clearInvoiceInterval = () => { - if (intervalRef.current) { - clearInterval(intervalRef.current) - allHisoryEntries.current = [] - } - } + // const clearGlobalInvoiceInterval = () => { + // if (intervalRef.current) { + // clearInterval(intervalRef.current) + // allHisoryEntries.current = [] + // } + // } const setHistoryEntries = async () => { const [all, latest] = await Promise.all([getHistory(), getLatestHistory()]) @@ -50,7 +48,7 @@ const useHistory = () => { const handlePendingInvoices = async () => { const invoices = await getAllInvoices() - if (!invoices.length) { return clearInvoiceInterval() } + if (!invoices.length) { return } if (!allHisoryEntries.current.length) { const historyEntries = await getHistoryEntriesByInvoices(invoices) allHisoryEntries.current = historyEntries @@ -119,7 +117,7 @@ const useHistory = () => { void handlePendingInvoices() void setHistoryEntries() // request token of pending invoices in interval until all are paid or expired - startGlobalInvoiceInterval() + // startGlobalInvoiceInterval() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -132,7 +130,8 @@ const useHistory = () => { addHistoryEntry, updateHistoryEntry, deleteHistory, - startGlobalInvoiceInterval, + // startGlobalInvoiceInterval, + // clearGlobalInvoiceInterval } } type useHistoryType = ReturnType @@ -159,7 +158,8 @@ const HistoryCtx = createContext({ updateHistoryEntry: async () => await l(''), // eslint-disable-next-line no-return-await, @typescript-eslint/await-thenable deleteHistory: async () => await l(''), - startGlobalInvoiceInterval: () => l(''), + // startGlobalInvoiceInterval: () => l(''), + // clearGlobalInvoiceInterval: () => l('') }) export const useHistoryContext = () => useContext(HistoryCtx) diff --git a/src/screens/Payment/Receive/Invoice.tsx b/src/screens/Payment/Receive/Invoice.tsx index 1a8c22bc..16437af6 100644 --- a/src/screens/Payment/Receive/Invoice.tsx +++ b/src/screens/Payment/Receive/Invoice.tsx @@ -30,7 +30,6 @@ export default function InvoiceScreen({ navigation, route }: TMintInvoicePagePro const { addHistoryEntry, updateHistoryEntry, - startGlobalInvoiceInterval, } = useHistoryContext() const intervalRef = useRef(null) const [expire, setExpire] = useState(expiry) @@ -86,7 +85,8 @@ export default function InvoiceScreen({ navigation, route }: TMintInvoicePagePro mints: [mintUrl], isPending: true }) - // start checking for payment in 3s intervals + + // start checking for payment in intervals intervalRef.current = setInterval(() => { l('checking pending invoices in invoice screen') void handlePayment(entry) @@ -102,10 +102,7 @@ export default function InvoiceScreen({ navigation, route }: TMintInvoicePagePro screenName={t('payInvoice', { ns: NS.wallet })} txt={t('cancel')} handlePress={() => { - // clear interval for current invoice check clearInvoiceInterval() - // start global invoice check - startGlobalInvoiceInterval() navigation.navigate('dashboard') }} /> From 615261ee87237c52892aac2c8e898cb0aabd1494 Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Wed, 3 Apr 2024 19:51:21 +0200 Subject: [PATCH 12/18] update history entry & lint --- assets/translations/de.json | 3 +- assets/translations/en.json | 3 +- assets/translations/es.json | 3 +- assets/translations/fr.json | 3 +- assets/translations/hu.json | 3 +- assets/translations/sw.json | 3 +- src/components/Toaster.tsx | 2 +- src/context/History.tsx | 58 ++++++++++++++++++++------------- src/screens/History/Details.tsx | 25 ++++++++++---- src/screens/History/Entry.tsx | 22 +++++++------ src/storage/db/index.ts | 10 +++++- 11 files changed, 89 insertions(+), 46 deletions(-) diff --git a/assets/translations/de.json b/assets/translations/de.json index 2b8ff22d..aa817c99 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -252,7 +252,8 @@ "noDefaultHint": "Sie müssen eine Standard-Mint einrichten, um einen automatischen Tausch durchzuführen.", "autoSwapSuccess": "Tausch erfolgreich!", "paidInvoice": "{{ count }} Rechnung wurde mit einem Gesamtbetrag von {{ total }} Sats bezahlt", - "paidInvoices": "{{ count }} Rechnungen wurden mit einem Gesamtbetrag von {{ total }} Sats bezahlt" + "paidInvoices": "{{ count }} Rechnungen wurden mit einem Gesamtbetrag von {{ total }} Sats bezahlt", + "checkPayment": "Zahlung überprüfen" }, "error": { "checkSpendableErr": "Fehler beim Überprüfen, ob der Token ausgegeben werden kann", diff --git a/assets/translations/en.json b/assets/translations/en.json index bb1a012a..42230b00 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -252,7 +252,8 @@ "noDefaultHint": "You need to setup a default mint to perform an auto swap.", "autoSwapSuccess": "Swap successful!", "paidInvoice": "{{ count }} invoice has been paid with a total amount of {{ total }} Sats", - "paidInvoices": "{{ count }} invoices have been paid with a total amount of {{ total }} Sats" + "paidInvoices": "{{ count }} invoices have been paid with a total amount of {{ total }} Sats", + "checkPayment": "Check payment" }, "error": { "checkSpendableErr": "Error while checking if token is spendable", diff --git a/assets/translations/es.json b/assets/translations/es.json index a0a7db01..a2021ddd 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -252,7 +252,8 @@ "noDefaultHint": "Necesitas configurar una ceca predeterminada para realizar un intercambio automático.", "autoSwapSuccess": "¡Intercambio exitoso!", "paidInvoice": "Se ha pagado {{ count }} factura con un importe total de {{ total }} Sats", - "paidInvoices": "Se han pagado {{ count }} facturas con un importe total de {{ total }} Sats" + "paidInvoices": "Se han pagado {{ count }} facturas con un importe total de {{ total }} Sats", + "checkPayment": "Comprobar pago" }, "error": { "checkSpendableErr": "Error al comprobar si el token puede ser gastado", diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 121c5d5f..346f116b 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -252,7 +252,8 @@ "noDefaultHint": "Vous devez configurer une mint par défaut pour effectuer un échange automatique.", "autoSwapSuccess": "Échange réussi!", "paidInvoice": "{{ count }} facture a été payée pour un montant total de {{ total }} Sats", - "paidInvoices": "{{ count }} factures ont été payées pour un montant total de {{ total }} Sats" + "paidInvoices": "{{ count }} factures ont été payées pour un montant total de {{ total }} Sats", + "checkPayment": "Vérifier le paiement" }, "error": { "checkSpendableErr": "Erreur lors de la vérification si le token est dépensable", diff --git a/assets/translations/hu.json b/assets/translations/hu.json index 482f99a1..c7621912 100644 --- a/assets/translations/hu.json +++ b/assets/translations/hu.json @@ -252,7 +252,8 @@ "noDefaultHint": "Be kell állítanod egy alapértelmezett verdét az automatikus cseréhez.", "autoSwapSuccess": "Csere sikeres!", "paidInvoice": "{{ count }} számla kifizetésre került, összesen {{ total }} Sats összegben", - "paidInvoices": "{{ count }} számla kifizetésre került, összesen {{ total }} Sats összegben" + "paidInvoices": "{{ count }} számla kifizetésre került, összesen {{ total }} Sats összegben", + "checkPayment": "Fizetés ellenőrzése" }, "error": { "checkSpendableErr": "Hiba a token elkölthetőségének ellenőrzése közben", diff --git a/assets/translations/sw.json b/assets/translations/sw.json index 21943270..a887f8d3 100644 --- a/assets/translations/sw.json +++ b/assets/translations/sw.json @@ -252,7 +252,8 @@ "noDefaultHint": "Unahitaji kuweka kalibu ya kufanya ubadilishaji wa moja kwa moja.", "autoSwapSuccess": "Kubadilishana kufanikiwa!", "paidInvoice": "{{ count }} ankara imelipwa kwa jumla ya {{ total }} Sats", - "paidInvoices": "Bilansi ya ankara {{ count }} zimelipwa kwa jumla ya {{ total }} Sats" + "paidInvoices": "Bilansi ya ankara {{ count }} zimelipwa kwa jumla ya {{ total }} Sats", + "checkPayment": "Angalia malipo" }, "error": { "checkSpendableErr": "Kumetokea kosa wakati wa kuangalia ikiwa kijenzi kina pesa za kutumiwa", diff --git a/src/components/Toaster.tsx b/src/components/Toaster.tsx index bc3c86b6..a5f26d42 100644 --- a/src/components/Toaster.tsx +++ b/src/components/Toaster.tsx @@ -25,7 +25,7 @@ export default function Toaster() { style={styles.txtWrap} testID={`${prompt.success ? 'success' : 'error'}-toaster`} > - + ) diff --git a/src/context/History.tsx b/src/context/History.tsx index aa4010fa..1e2f9831 100644 --- a/src/context/History.tsx +++ b/src/context/History.tsx @@ -1,5 +1,5 @@ /* eslint-disable no-await-in-loop */ -import { delInvoice, getAllInvoices } from '@db' +import { delInvoice, getAllInvoices, getInvoiceByPr } from '@db' import { l } from '@log' import type { IHistoryEntry } from '@model' import { NS } from '@src/i18n' @@ -22,23 +22,9 @@ const useHistory = () => { // State to indicate token claim from clipboard after app comes to the foreground, to re-render total balance const { claimed } = useFocusClaimContext() const { openPromptAutoClose } = usePromptContext() - // const intervalRef = useRef(null) const allHisoryEntries = useRef([]) const hasEntries = useMemo(() => Object.keys(history).length > 0, [history]) - - // const startGlobalInvoiceInterval = () => { - // intervalRef.current = setInterval(() => { - // l('checking pending invoices in interval of history context') - // void handlePendingInvoices() - // }, MinuteInMs) - // } - - // const clearGlobalInvoiceInterval = () => { - // if (intervalRef.current) { - // clearInterval(intervalRef.current) - // allHisoryEntries.current = [] - // } - // } + const lastCalled = useRef(0) const setHistoryEntries = async () => { const [all, latest] = await Promise.all([getHistory(), getLatestHistory()]) @@ -89,6 +75,37 @@ const useHistory = () => { } } + const checkLnPr = async (pr: string) => { + const delay = 20_000 + const now = Date.now() + const timeSinceLastCall = now - lastCalled.current + const remainingSeconds = Math.ceil((delay - timeSinceLastCall) / 1000) + // restrict usage to 20 seconds + if (timeSinceLastCall < delay) { + return openPromptAutoClose({ msg: `Please wait ${remainingSeconds} seconds to avoid spamming the mint.`, success: false }) + } + lastCalled.current = now + const invoice = await getInvoiceByPr(pr) + const entry = getHistoryEntryByInvoice(allHisoryEntries.current, pr) + if (!invoice) { + if (entry) { + await updateHistoryEntry(entry, { ...entry, isExpired: true }) + } + return openPromptAutoClose({ msg: t('invoiceExpired'), success: false }) + } + const { success } = await requestToken(invoice.mintUrl, invoice.amount, invoice.hash) + if (success) { + openPromptAutoClose({ msg: t('paidInvoice', { count: 1, total: formatInt(invoice.amount) }), success: true }) + if (entry) { + await updateHistoryEntry(entry, { ...entry, isPending: false }) + } + // TODO update balance + await delInvoice(invoice.hash) + } else { + openPromptAutoClose({ msg: t('paymentPending'), success: false }) + } + } + const addHistoryEntry = async (entry: Omit) => { const resp = await addToHistory(entry) await setHistoryEntries() @@ -116,8 +133,6 @@ const useHistory = () => { useEffect(() => { void handlePendingInvoices() void setHistoryEntries() - // request token of pending invoices in interval until all are paid or expired - // startGlobalInvoiceInterval() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -130,8 +145,7 @@ const useHistory = () => { addHistoryEntry, updateHistoryEntry, deleteHistory, - // startGlobalInvoiceInterval, - // clearGlobalInvoiceInterval + checkLnPr } } type useHistoryType = ReturnType @@ -158,8 +172,8 @@ const HistoryCtx = createContext({ updateHistoryEntry: async () => await l(''), // eslint-disable-next-line no-return-await, @typescript-eslint/await-thenable deleteHistory: async () => await l(''), - // startGlobalInvoiceInterval: () => l(''), - // clearGlobalInvoiceInterval: () => l('') + // eslint-disable-next-line no-return-await, @typescript-eslint/await-thenable + checkLnPr: async () => await l('') }) export const useHistoryContext = () => useContext(HistoryCtx) diff --git a/src/screens/History/Details.tsx b/src/screens/History/Details.tsx index 67f14dc5..15fe907d 100644 --- a/src/screens/History/Details.tsx +++ b/src/screens/History/Details.tsx @@ -46,7 +46,7 @@ export default function DetailsPage({ navigation, route }: THistoryEntryPageProp isExpired } = route.params.entry const { color } = useThemeContext() - const { addHistoryEntry, updateHistoryEntry } = useHistoryContext() + const { addHistoryEntry, updateHistoryEntry, checkLnPr } = useHistoryContext() const [copy, setCopy] = useState(initialCopyState) const [spent, setSpent] = useState(isSpent) const { loading, startLoading, stopLoading } = useLoading() @@ -69,7 +69,7 @@ export default function DetailsPage({ navigation, route }: THistoryEntryPageProp }, [mints]) const getTxColor = () => { - if (type === txType.SWAP || type === txType.RESTORE) { return color.TEXT } + if (type === txType.SWAP || type === txType.RESTORE || isPending) { return color.TEXT } return amount < 0 ? mainColors.ERROR : mainColors.VALID } @@ -160,8 +160,8 @@ export default function DetailsPage({ navigation, route }: THistoryEntryPageProp } // used in interval to check if token is spent while qr sheet is open - const checkPayment = async () => { - if (type === txType.SEND_RECEIVE) { return clearTokenInterval() } + const checkEcashPayment = async () => { + if (type > txType.SEND_RECEIVE) { return clearTokenInterval() } const isSpendable = await isTokenSpendable(value) setSpent(!isSpendable) if (!isSpendable) { @@ -173,9 +173,9 @@ export default function DetailsPage({ navigation, route }: THistoryEntryPageProp } const startTokenInterval = () => { - if (spent) { return } + if (spent || type > txType.SEND_RECEIVE) { return } intervalRef.current = setInterval(() => { - void checkPayment() + void checkEcashPayment() }, 3000) } @@ -221,6 +221,19 @@ export default function DetailsPage({ navigation, route }: THistoryEntryPageProp } + {/* Manual check of pending invoice */} + {isPending && !isExpired && + <> + void checkLnPr(value)} + > + + + + + + } {/* Settle Time */} diff --git a/src/screens/History/Entry.tsx b/src/screens/History/Entry.tsx index 58ab31c9..bc5cac69 100644 --- a/src/screens/History/Entry.tsx +++ b/src/screens/History/Entry.tsx @@ -8,7 +8,7 @@ import { globals, mainColors } from '@styles' import { formatSatStr, isNum } from '@util' import { useTranslation } from 'react-i18next' import { Text, TouchableOpacity, View } from 'react-native' -import { ScaledSheet, vs } from 'react-native-size-matters' +import { s, ScaledSheet } from 'react-native-size-matters' import EntryTime from './entryTime' @@ -29,7 +29,7 @@ export default function HistoryEntry({ nav, item }: IHistoryEntryProps) { } const getTxColor = () => { - if (item.type === txType.SWAP || item.type === txType.RESTORE) { return color.TEXT } + if (item.type === txType.SWAP || item.type === txType.RESTORE || item.isPending || item.isExpired) { return color.TEXT } return item.amount < 0 ? mainColors.ERROR : mainColors.VALID } @@ -42,7 +42,9 @@ export default function HistoryEntry({ nav, item }: IHistoryEntryProps) { : item.isExpired ? - + + + : @@ -51,17 +53,17 @@ export default function HistoryEntry({ nav, item }: IHistoryEntryProps) { style={styles.listItem} onPress={() => nav.navigation.navigate('history entry details', { entry: item })} > - + {getIcon()} - - + + - 0 ? 0 : vs(10) }]}> + 0 ? 0 : s(10) }]}> 0 && item.type < txType.SWAP ? '+' : ''}${formatSatStr(item.type === txType.SWAP || item.type === txType.RESTORE ? Math.abs(item.amount) : item.amount, 'standard')}` } - styles={[{ color: getTxColor(), marginBottom: vs(5), textAlign: 'right' }]} + styles={[{ color: getTxColor(), marginBottom: s(5), textAlign: 'right' }]} /> {isNum(item.fee) && item.fee > 0 && - + {t('fee', { ns: NS.common })}: {item.fee} } @@ -90,7 +92,7 @@ const styles = ScaledSheet.create({ }, infoWrap: { alignItems: 'center', - paddingBottom: '10@vs', + paddingBottom: '10@s', }, placeholder: { width: '30@s', diff --git a/src/storage/db/index.ts b/src/storage/db/index.ts index 7d552817..cd3e60fc 100644 --- a/src/storage/db/index.ts +++ b/src/storage/db/index.ts @@ -318,7 +318,7 @@ export async function setPreferences(p: IPreferences) { // ################################ Invoices ################################ export async function addInvoice({ pr, hash, amount, mintUrl }: Omit) { const result = await db.execInsert( - 'INSERT OR IGNORE INTO invoices (amount,pr,hash,mintUrl) VALUES (?, ?, ?,?)', + 'INSERT OR IGNORE INTO invoices (amount,pr,hash,mintUrl) VALUES (?, ?, ?, ?)', [amount, pr, hash, mintUrl] ) l('[addInvoice]', result, { pr, hash, amount, mintUrl }) @@ -347,6 +347,14 @@ export async function getInvoice(hash: string) { l('[getInvoice]', result, { hash }) return result?.item?.(0) } +export async function getInvoiceByPr(pr: string) { + const result = await db.execSelect( + 'SELECT * from invoices Where pr = ?', + [pr] + ) + l('[getInvoice]', result, { pr }) + return result?.item?.(0) +} // ################################ Contacts ################################ export async function getContacts(): Promise { From ab3003333e6f93f37b56c5f4a1db541025c317fb Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Wed, 3 Apr 2024 20:02:23 +0200 Subject: [PATCH 13/18] add translations --- assets/translations/de.json | 3 ++- assets/translations/en.json | 3 ++- assets/translations/es.json | 3 ++- assets/translations/fr.json | 3 ++- assets/translations/hu.json | 3 ++- assets/translations/sw.json | 3 ++- src/context/History.tsx | 2 +- 7 files changed, 13 insertions(+), 7 deletions(-) diff --git a/assets/translations/de.json b/assets/translations/de.json index aa817c99..eef38634 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -253,7 +253,8 @@ "autoSwapSuccess": "Tausch erfolgreich!", "paidInvoice": "{{ count }} Rechnung wurde mit einem Gesamtbetrag von {{ total }} Sats bezahlt", "paidInvoices": "{{ count }} Rechnungen wurden mit einem Gesamtbetrag von {{ total }} Sats bezahlt", - "checkPayment": "Zahlung überprüfen" + "checkPayment": "Zahlung überprüfen", + "lnPaymentSpamHint": "Bitte warten Sie {{remainingSeconds}} Sekunden um die Mint zu entlasten." }, "error": { "checkSpendableErr": "Fehler beim Überprüfen, ob der Token ausgegeben werden kann", diff --git a/assets/translations/en.json b/assets/translations/en.json index 42230b00..161a562e 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -253,7 +253,8 @@ "autoSwapSuccess": "Swap successful!", "paidInvoice": "{{ count }} invoice has been paid with a total amount of {{ total }} Sats", "paidInvoices": "{{ count }} invoices have been paid with a total amount of {{ total }} Sats", - "checkPayment": "Check payment" + "checkPayment": "Check payment", + "lnPaymentSpamHint": "Please wait {{remainingSeconds}} seconds to avoid spamming the mint." }, "error": { "checkSpendableErr": "Error while checking if token is spendable", diff --git a/assets/translations/es.json b/assets/translations/es.json index a2021ddd..490c51b1 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -253,7 +253,8 @@ "autoSwapSuccess": "¡Intercambio exitoso!", "paidInvoice": "Se ha pagado {{ count }} factura con un importe total de {{ total }} Sats", "paidInvoices": "Se han pagado {{ count }} facturas con un importe total de {{ total }} Sats", - "checkPayment": "Comprobar pago" + "checkPayment": "Comprobar pago", + "lnPaymentSpamHint": "Por favor, espere {{remainingSeconds}} segundos para aliviar la Mint." }, "error": { "checkSpendableErr": "Error al comprobar si el token puede ser gastado", diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 346f116b..aa6e5762 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -253,7 +253,8 @@ "autoSwapSuccess": "Échange réussi!", "paidInvoice": "{{ count }} facture a été payée pour un montant total de {{ total }} Sats", "paidInvoices": "{{ count }} factures ont été payées pour un montant total de {{ total }} Sats", - "checkPayment": "Vérifier le paiement" + "checkPayment": "Vérifier le paiement", + "lnPaymentSpamHint": "Veuillez attendre {{remainingSeconds}} secondes pour soulager la Mint." }, "error": { "checkSpendableErr": "Erreur lors de la vérification si le token est dépensable", diff --git a/assets/translations/hu.json b/assets/translations/hu.json index c7621912..cfc8e87a 100644 --- a/assets/translations/hu.json +++ b/assets/translations/hu.json @@ -253,7 +253,8 @@ "autoSwapSuccess": "Csere sikeres!", "paidInvoice": "{{ count }} számla kifizetésre került, összesen {{ total }} Sats összegben", "paidInvoices": "{{ count }} számla kifizetésre került, összesen {{ total }} Sats összegben", - "checkPayment": "Fizetés ellenőrzése" + "checkPayment": "Fizetés ellenőrzése", + "lnPaymentSpamHint": "Kérjük, várjon {{remainingSeconds}} másodpercet a Mint tehermentesítése érdekében." }, "error": { "checkSpendableErr": "Hiba a token elkölthetőségének ellenőrzése közben", diff --git a/assets/translations/sw.json b/assets/translations/sw.json index a887f8d3..a371d474 100644 --- a/assets/translations/sw.json +++ b/assets/translations/sw.json @@ -253,7 +253,8 @@ "autoSwapSuccess": "Kubadilishana kufanikiwa!", "paidInvoice": "{{ count }} ankara imelipwa kwa jumla ya {{ total }} Sats", "paidInvoices": "Bilansi ya ankara {{ count }} zimelipwa kwa jumla ya {{ total }} Sats", - "checkPayment": "Angalia malipo" + "checkPayment": "Angalia malipo", + "lnPaymentSpamHint": "Tafadhali subiri {{remainingSeconds}} sekunde ili kupunguza mzigo kwa Mint." }, "error": { "checkSpendableErr": "Kumetokea kosa wakati wa kuangalia ikiwa kijenzi kina pesa za kutumiwa", diff --git a/src/context/History.tsx b/src/context/History.tsx index 1e2f9831..c00865ce 100644 --- a/src/context/History.tsx +++ b/src/context/History.tsx @@ -82,7 +82,7 @@ const useHistory = () => { const remainingSeconds = Math.ceil((delay - timeSinceLastCall) / 1000) // restrict usage to 20 seconds if (timeSinceLastCall < delay) { - return openPromptAutoClose({ msg: `Please wait ${remainingSeconds} seconds to avoid spamming the mint.`, success: false }) + return openPromptAutoClose({ msg: t('lnPaymentSpamHint', { remainingSeconds }), success: false }) } lastCalled.current = now const invoice = await getInvoiceByPr(pr) From c95d4618e4f28ff6b9ffc89af4a5ace006b9bffc Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Wed, 3 Apr 2024 20:44:55 +0200 Subject: [PATCH 14/18] add Balance context --- src/components/App.tsx | 41 +++++++++++---------- src/components/Balance.tsx | 5 ++- src/context/Balance.tsx | 45 +++++++++++++++++++++++ src/context/History.tsx | 5 ++- src/screens/Dashboard.tsx | 22 +++-------- src/screens/Payment/Send/EncodedToken.tsx | 10 +++-- src/screens/Payment/Success.tsx | 11 ++++-- 7 files changed, 94 insertions(+), 45 deletions(-) create mode 100644 src/context/Balance.tsx diff --git a/src/components/App.tsx b/src/components/App.tsx index b3ca053e..0e732792 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -8,6 +8,7 @@ import { NavigationContainer, NavigationContainerRef } from '@react-navigation/n import { CustomErrorBoundary } from '@screens/ErrorScreen/ErrorBoundary' import { ErrorDetails } from '@screens/ErrorScreen/ErrorDetails' import * as Sentry from '@sentry/react-native' +import { BalanceProvider } from '@src/context/Balance' import { FocusClaimProvider } from '@src/context/FocusClaim' import { HistoryProvider } from '@src/context/History' import { KeyboardProvider } from '@src/context/Keyboard' @@ -207,25 +208,27 @@ function _App() { - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/src/components/Balance.tsx b/src/components/Balance.tsx index ac162b66..83294e6f 100644 --- a/src/components/Balance.tsx +++ b/src/components/Balance.tsx @@ -4,6 +4,7 @@ import { type TTXType, txType } from '@model' import type { RootStackParamList } from '@model/nav' import type { NativeStackNavigationProp } from '@react-navigation/native-stack' import EntryTime from '@screens/History/entryTime' +import { useBalanceContext } from '@src/context/Balance' import { useHistoryContext } from '@src/context/History' import { usePrivacyContext } from '@src/context/Privacy' import { useThemeContext } from '@src/context/Theme' @@ -21,15 +22,15 @@ import Logo from './Logo' import Txt from './Txt' interface IBalanceProps { - balance: number nav?: NativeStackNavigationProp } -export default function Balance({ balance, nav }: IBalanceProps) { +export default function Balance({ nav }: IBalanceProps) { const { t } = useTranslation([NS.common]) const { pref, color, highlight } = useThemeContext() const { hidden, handleLogoPress } = usePrivacyContext() const [formatSats, setFormatSats] = useState(pref?.formatBalance) + const { balance } = useBalanceContext() const { latestHistory } = useHistoryContext() const toggleBalanceFormat = () => { diff --git a/src/context/Balance.tsx b/src/context/Balance.tsx new file mode 100644 index 00000000..488ad44b --- /dev/null +++ b/src/context/Balance.tsx @@ -0,0 +1,45 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable require-await */ +import { getBalance } from '@db' +import { l } from '@log' +import { createContext, useContext, useEffect, useState } from 'react' + +import { useFocusClaimContext } from './FocusClaim' + +// Total Balance state (all mints) +const useBalance = () => { + const [balance, setBalance] = useState(0) + const { claimed } = useFocusClaimContext() + + const updateBalance = async () => { + const bal = await getBalance() + setBalance(bal) + } + + useEffect(() => { + void updateBalance() + }, []) + + useEffect(() => { + void updateBalance() + }, [claimed]) + + return { + balance, + updateBalance + } +} +type useBalanceType = ReturnType + +const BalanceCtx = createContext({ + balance: 0, + updateBalance: async () => l(''), +}) + +export const useBalanceContext = () => useContext(BalanceCtx) + +export const BalanceProvider = ({ children }: { children: React.ReactNode }) => ( + + {children} + +) \ No newline at end of file diff --git a/src/context/History.tsx b/src/context/History.tsx index c00865ce..c6c33c45 100644 --- a/src/context/History.tsx +++ b/src/context/History.tsx @@ -12,6 +12,7 @@ import { requestToken } from '@wallet' import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { useBalanceContext } from './Balance' import { useFocusClaimContext } from './FocusClaim' import { usePromptContext } from './Prompt' @@ -22,6 +23,7 @@ const useHistory = () => { // State to indicate token claim from clipboard after app comes to the foreground, to re-render total balance const { claimed } = useFocusClaimContext() const { openPromptAutoClose } = usePromptContext() + const { updateBalance } = useBalanceContext() const allHisoryEntries = useRef([]) const hasEntries = useMemo(() => Object.keys(history).length > 0, [history]) const lastCalled = useRef(0) @@ -99,7 +101,6 @@ const useHistory = () => { if (entry) { await updateHistoryEntry(entry, { ...entry, isPending: false }) } - // TODO update balance await delInvoice(invoice.hash) } else { openPromptAutoClose({ msg: t('paymentPending'), success: false }) @@ -109,12 +110,14 @@ const useHistory = () => { const addHistoryEntry = async (entry: Omit) => { const resp = await addToHistory(entry) await setHistoryEntries() + await updateBalance() return resp } const updateHistoryEntry = async (oldEntry: IHistoryEntry, newEntry: IHistoryEntry) => { await updateHistory(oldEntry, newEntry) await setHistoryEntries() + await updateBalance() } const deleteHistory = async () => { diff --git a/src/screens/Dashboard.tsx b/src/screens/Dashboard.tsx index ecdb4a32..4e4f5ce4 100644 --- a/src/screens/Dashboard.tsx +++ b/src/screens/Dashboard.tsx @@ -8,7 +8,7 @@ import OptsModal from '@comps/modal/OptsModal' import { PromptModal } from '@comps/modal/Prompt' import Txt from '@comps/Txt' import { _testmintUrl, env } from '@consts' -import { addMint, getBalance, getMintsUrls, hasMints } from '@db' +import { addMint, getMintsUrls, hasMints } from '@db' import { l } from '@log' import TrustMintModal from '@modal/TrustMint' import type { TBeforeRemoveEvent, TDashboardPageProps } from '@model/nav' @@ -56,8 +56,6 @@ export default function Dashboard({ navigation, route }: TDashboardPageProps) { trustModal, setTrustModal } = useCashuToken() - // Total Balance state (all mints) - const [balance, setBalance] = useState(0) const [hasMint, setHasMint] = useState(false) // modals const [modal, setModal] = useState({ @@ -236,7 +234,6 @@ export default function Dashboard({ navigation, route }: TDashboardPageProps) { })) clearTimeout(t) }, 1000) - })() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -244,16 +241,13 @@ export default function Dashboard({ navigation, route }: TDashboardPageProps) { // check for available mints of the user useEffect(() => { void (async () => { - const [userHasMints, explainerSeen, balance] = await Promise.all([ + const [userHasMints, explainerSeen] = await Promise.all([ hasMints(), store.get(STORE_KEYS.explainer), - getBalance(), ]) setHasMint(userHasMints) setModal(prev => ({ ...prev, mint: !userHasMints && explainerSeen !== '1' })) - setBalance(balance) })() - // eslint-disable-next-line react-hooks/exhaustive-deps }, [claimed]) // handle deep links @@ -274,15 +268,11 @@ export default function Dashboard({ navigation, route }: TDashboardPageProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [url]) - // get balance after navigating to this page + // update states after navigating to this page useEffect(() => { const focusHandler = navigation.addListener('focus', async () => { - const data = await Promise.all([ - getBalance(), - hasMints() - ]) - setBalance(data[0]) - setHasMint(data[1]) + const data = await hasMints() + setHasMint(data) }) return focusHandler // eslint-disable-next-line react-hooks/exhaustive-deps @@ -298,7 +288,7 @@ export default function Dashboard({ navigation, route }: TDashboardPageProps) { return ( {/* Balance, Disclaimer & History */} - + {/* Receive/send/mints buttons */} {/* Send button or add first mint */} diff --git a/src/screens/Payment/Send/EncodedToken.tsx b/src/screens/Payment/Send/EncodedToken.tsx index 4cd9e153..983ebe0d 100644 --- a/src/screens/Payment/Send/EncodedToken.tsx +++ b/src/screens/Payment/Send/EncodedToken.tsx @@ -7,9 +7,11 @@ import type { TBeforeRemoveEvent, TEncodedTokenPageProps } from '@model/nav' import TopNav from '@nav/TopNav' import { preventBack } from '@nav/utils' import { isIOS } from '@src/consts' +import { useBalanceContext } from '@src/context/Balance' +import { useHistoryContext } from '@src/context/History' import { useThemeContext } from '@src/context/Theme' import { NS } from '@src/i18n' -import { historyStore, store } from '@store' +import { store } from '@store' import { STORE_KEYS } from '@store/consts' import { globals, highlight as hi, mainColors } from '@styles' import { formatInt, formatSatStr, share, vib } from '@util' @@ -27,6 +29,8 @@ export default function EncodedTokenPage({ navigation, route }: TEncodedTokenPag const { value, amount } = route.params.entry const { t } = useTranslation([NS.common]) const { color, highlight } = useThemeContext() + const { updateHistoryEntry } = useHistoryContext() + const { updateBalance } = useBalanceContext() const [error, setError] = useState({ msg: '', open: false }) const [spent, setSpent] = useState(false) const { copied, copy } = useCopy() @@ -43,8 +47,7 @@ export default function EncodedTokenPage({ navigation, route }: TEncodedTokenPag setSpent(!isSpendable) if (!isSpendable) { clearTokenInterval() - // update history item - await historyStore.updateHistoryEntry(route.params.entry, { ...route.params.entry, isSpent: true }) + await updateHistoryEntry(route.params.entry, { ...route.params.entry, isSpent: true }) } } @@ -63,6 +66,7 @@ export default function EncodedTokenPage({ navigation, route }: TEncodedTokenPag // auto check payment in intervals useEffect(() => { + void updateBalance() intervalRef.current = setInterval(() => { void checkPayment() }, 3000) diff --git a/src/screens/Payment/Success.tsx b/src/screens/Payment/Success.tsx index 76b7ff3b..f4acefbf 100644 --- a/src/screens/Payment/Success.tsx +++ b/src/screens/Payment/Success.tsx @@ -5,9 +5,9 @@ import { isIOS } from '@consts' import type { TBeforeRemoveEvent, TSuccessPageProps } from '@model/nav' import { preventBack } from '@nav/utils' import ProfilePic from '@screens/Addressbook/ProfilePic' +import { useBalanceContext } from '@src/context/Balance' import { useThemeContext } from '@src/context/Theme' import { NS } from '@src/i18n' -import { l } from '@src/logger' import { formatSatStr, isNum, vib } from '@util' import LottieView from 'lottie-react-native' import { useEffect } from 'react' @@ -33,9 +33,14 @@ export default function SuccessPage({ navigation, route }: TSuccessPageProps) { } = route.params const { t } = useTranslation([NS.common]) const { color } = useThemeContext() + const { updateBalance } = useBalanceContext() const insets = useSafeAreaInsets() - useEffect(() => vib(400), []) + useEffect(() => { + vib(400) + void updateBalance() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) // prevent back navigation - https://reactnavigation.org/docs/preventing-going-back/ useEffect(() => { @@ -44,8 +49,6 @@ export default function SuccessPage({ navigation, route }: TSuccessPageProps) { return () => navigation.removeListener('beforeRemove', backHandler) }, [navigation]) - l({ amount, memo, fee, mint, isClaim, isMelt, nostr, isScanned }) - return ( {nostr && nostr.contact && nostr.contact.picture ? From 576c830469a4cb7d19c61773ccf3aef3948006f6 Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Wed, 3 Apr 2024 21:03:54 +0200 Subject: [PATCH 15/18] update test --- test/components/Balance.test.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/components/Balance.test.tsx b/test/components/Balance.test.tsx index aa276d6e..39dda406 100644 --- a/test/components/Balance.test.tsx +++ b/test/components/Balance.test.tsx @@ -15,15 +15,15 @@ describe('Basic test of the Txt.tsx component', () => { beforeEach(() => jest.clearAllMocks()) // Start tests it('renders the expected string', () => { - render() - const textElement = screen.getByText('69') + render() + const textElement = screen.getByText('0') expect(textElement).toBeDefined() }) it('updates the balance format state on press', () => { - render() - const touchableElement = screen.getByText('69') + render() + const touchableElement = screen.getByText('0') // Simulate press event fireEvent.press(touchableElement) - expect(touchableElement.props.children).toBe('0.00000069') + expect(screen.getByText('0.00000000')).toBeDefined() }) -}) +}) \ No newline at end of file From 2f3f4b2606166377ca1907b517508751145573f6 Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Wed, 3 Apr 2024 23:56:09 +0200 Subject: [PATCH 16/18] fix updating history entry --- src/context/History.tsx | 12 +++--------- src/screens/Payment/Receive/Invoice.tsx | 4 ++-- src/storage/store/HistoryStore.ts | 5 +++-- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/context/History.tsx b/src/context/History.tsx index c6c33c45..dec56d2f 100644 --- a/src/context/History.tsx +++ b/src/context/History.tsx @@ -5,7 +5,7 @@ import type { IHistoryEntry } from '@model' import { NS } from '@src/i18n' import { historyStore, store } from '@store' import { STORE_KEYS } from '@store/consts' -import { getHistory, getHistoryEntriesByInvoices, getHistoryEntryByInvoice } from '@store/HistoryStore' +import { getHistory, getHistoryEntryByInvoice } from '@store/HistoryStore' import { addToHistory, getLatestHistory, updateHistory } from '@store/latestHistoryEntries' import { decodeLnInvoice, formatInt } from '@util' import { requestToken } from '@wallet' @@ -24,7 +24,6 @@ const useHistory = () => { const { claimed } = useFocusClaimContext() const { openPromptAutoClose } = usePromptContext() const { updateBalance } = useBalanceContext() - const allHisoryEntries = useRef([]) const hasEntries = useMemo(() => Object.keys(history).length > 0, [history]) const lastCalled = useRef(0) @@ -37,13 +36,9 @@ const useHistory = () => { const handlePendingInvoices = async () => { const invoices = await getAllInvoices() if (!invoices.length) { return } - if (!allHisoryEntries.current.length) { - const historyEntries = await getHistoryEntriesByInvoices(invoices) - allHisoryEntries.current = historyEntries - } let paid = { count: 0, amount: 0 } for (const invoice of invoices) { - const entry = getHistoryEntryByInvoice(allHisoryEntries.current, invoice.pr) + const entry = await getHistoryEntryByInvoice(invoice.pr) try { const { success } = await requestToken(invoice.mintUrl, invoice.amount, invoice.hash) if (success) { @@ -52,7 +47,6 @@ const useHistory = () => { if (entry) { await updateHistoryEntry(entry, { ...entry, isPending: false }) } - // TODO update balance await delInvoice(invoice.hash) continue } @@ -88,7 +82,7 @@ const useHistory = () => { } lastCalled.current = now const invoice = await getInvoiceByPr(pr) - const entry = getHistoryEntryByInvoice(allHisoryEntries.current, pr) + const entry = await getHistoryEntryByInvoice(pr) if (!invoice) { if (entry) { await updateHistoryEntry(entry, { ...entry, isExpired: true }) diff --git a/src/screens/Payment/Receive/Invoice.tsx b/src/screens/Payment/Receive/Invoice.tsx index 16437af6..d26f610d 100644 --- a/src/screens/Payment/Receive/Invoice.tsx +++ b/src/screens/Payment/Receive/Invoice.tsx @@ -83,9 +83,9 @@ export default function InvoiceScreen({ navigation, route }: TMintInvoicePagePro type: 2, value: paymentRequest, mints: [mintUrl], - isPending: true + isPending: true, + isExpired: false, }) - // start checking for payment in intervals intervalRef.current = setInterval(() => { l('checking pending invoices in invoice screen') diff --git a/src/storage/store/HistoryStore.ts b/src/storage/store/HistoryStore.ts index 95782f29..e1ee304f 100644 --- a/src/storage/store/HistoryStore.ts +++ b/src/storage/store/HistoryStore.ts @@ -87,8 +87,9 @@ export async function getHistory({ order = 'DESC', start = 0, count = -1, orderB return groupEntries(history) } -export function getHistoryEntryByInvoice(entries: IHistoryEntry[], invoice: string) { - return entries.find(i => i.value === invoice) +export async function getHistoryEntryByInvoice(invoice: string) { + const history = await historyStore.getHistory() + return history.find(i => i.value === invoice) } export async function getHistoryEntriesByInvoices(invoices: IInvoice[]) { From 4169e03cb8de9c373ba10d1c6b2d3753633ee686 Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Thu, 4 Apr 2024 00:09:19 +0200 Subject: [PATCH 17/18] add fees to amount in history --- src/screens/Payment/Processing.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/screens/Payment/Processing.tsx b/src/screens/Payment/Processing.tsx index b546cdaa..d751fe21 100644 --- a/src/screens/Payment/Processing.tsx +++ b/src/screens/Payment/Processing.tsx @@ -119,7 +119,7 @@ export default function ProcessingScreen({ navigation, route }: TProcessingPageP return handleError({ e: isErr(res.error) ? res.error : undefined }) } await addHistoryEntry({ - amount, + amount: -amount - (isNum(res.realFee) ? res.realFee : 0), type: 2, value: invoice, mints: [mint.mintUrl], @@ -149,7 +149,7 @@ export default function ProcessingScreen({ navigation, route }: TProcessingPageP const res = await autoMintSwap(mint.mintUrl, targetMint.mintUrl, amount, estFee ?? 0, proofs) // add as history entry (multimint swap) await addHistoryEntry({ - amount: -amount, + amount: -amount - (isNum(res.payResult.realFee) ? res.payResult.realFee : 0), fee: res.payResult.realFee, type: 3, value: res.requestTokenResult.invoice?.pr || '', @@ -185,7 +185,7 @@ export default function ProcessingScreen({ navigation, route }: TProcessingPageP const amountSent = tokenInfo.value - estFeeResp // add as history entry (multimint swap) await addHistoryEntry({ - amount: -amountSent, + amount: -amountSent - (isNum(payResult.realFee) ? payResult.realFee : 0), fee: payResult.realFee, type: 3, value: requestTokenResult.invoice?.pr || '', @@ -298,7 +298,6 @@ export default function ProcessingScreen({ navigation, route }: TProcessingPageP // start payment process useEffect(() => { - if (isZap) { if (payZap) { return void handleMelting() } return void handleZap() From e1ac3cc733dd5ece023c23cbd8dcf21a3d9cc1ea Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Thu, 4 Apr 2024 00:25:33 +0200 Subject: [PATCH 18/18] update translation and payment overview screen --- assets/translations/de.json | 2 +- assets/translations/en.json | 2 +- assets/translations/es.json | 2 +- assets/translations/fr.json | 2 +- assets/translations/hu.json | 2 +- assets/translations/sw.json | 2 +- src/screens/Payment/Send/CoinSelection.tsx | 17 ++++++++++++----- 7 files changed, 18 insertions(+), 11 deletions(-) diff --git a/assets/translations/de.json b/assets/translations/de.json index eef38634..3082c0ee 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -61,7 +61,7 @@ "back": "Zurück", "backToDashboard": "Zurück zu Wallet", "balance": "Guthaben", - "balanceAfterTX": "Guthaben nach Zahlung", + "balanceAfterTX": "Mint Guthaben nach Zahlung", "balTooLow": "Nicht genug Guthaben", "bigQrMsg": "Die Datenmenge ist zu groß für einen QR-Code.", "calculateFeeEst": "Gebühr wird geschätzt", diff --git a/assets/translations/en.json b/assets/translations/en.json index 161a562e..e622ae59 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -61,7 +61,7 @@ "back": "Back", "backToDashboard": "Back to dashboard", "balance": "Balance", - "balanceAfterTX": "Balance after TX", + "balanceAfterTX": "Mint balance after TX", "balTooLow": "Balance too low", "bigQrMsg": "The amount of data is too big for a QR code.", "calculateFeeEst": "Calculating fee", diff --git a/assets/translations/es.json b/assets/translations/es.json index 490c51b1..4a3c4035 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -61,7 +61,7 @@ "back": "Volver", "backToDashboard": "Volver al panel de control", "balance": "Saldo", - "balanceAfterTX": "Saldo tras la transacción", + "balanceAfterTX": "Mint saldo tras la transacción", "balTooLow": "Saldo insuficiente", "bigQrMsg": "La cantidad de datos es demasiado grande para un código QR.", "calculateFeeEst": "Calculando comisión", diff --git a/assets/translations/fr.json b/assets/translations/fr.json index aa6e5762..943dc60a 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -61,7 +61,7 @@ "back": "Retour", "backToDashboard": "Retour au tableau de bord", "balance": "Solde", - "balanceAfterTX": "Solde après paiement", + "balanceAfterTX": "Mint solde après paiement", "balTooLow": "Solde insuffisant", "bigQrMsg": "La quantité de données est trop grand pour un code QR.", "calculateFeeEst": "Calcul des frais", diff --git a/assets/translations/hu.json b/assets/translations/hu.json index cfc8e87a..54a1b474 100644 --- a/assets/translations/hu.json +++ b/assets/translations/hu.json @@ -61,7 +61,7 @@ "back": "Vissza", "backToDashboard": "Vissza a kezdőképernyőre", "balance": "Egyenleg", - "balanceAfterTX": "Utalás utáni egyenleg", + "balanceAfterTX": "Verde utalás utáni egyenleg", "balTooLow": "Egyenleg túl alacsony", "bigQrMsg": "Az adatmennyiség túl nagy egy QR kód számára.", "calculateFeeEst": "Díjszámítás", diff --git a/assets/translations/sw.json b/assets/translations/sw.json index a371d474..459d70cc 100644 --- a/assets/translations/sw.json +++ b/assets/translations/sw.json @@ -61,7 +61,7 @@ "back": "Rudi", "backToDashboard": "Rudi kwenye dashibodi", "balance": "Salio", - "balanceAfterTX": "Salio baada ya TX", + "balanceAfterTX": "Mint salio baada ya TX", "balTooLow": "Salio ni dogo mno", "bigQrMsg": "Kiasi cha data ni kikubwa sana kwa nambari ya QR.", "calculateFeeEst": "Kuhesabu ada", diff --git a/src/screens/Payment/Send/CoinSelection.tsx b/src/screens/Payment/Send/CoinSelection.tsx index b4cc0b99..ace2ffba 100644 --- a/src/screens/Payment/Send/CoinSelection.tsx +++ b/src/screens/Payment/Send/CoinSelection.tsx @@ -17,7 +17,7 @@ import { isLightningAddress } from '@util/lnurl' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { ScrollView, View } from 'react-native' -import { ScaledSheet } from 'react-native-size-matters' +import { s, ScaledSheet } from 'react-native-size-matters' import { CoinSelectionModal, CoinSelectionResume, OverviewRow } from './ProofList' @@ -141,10 +141,17 @@ export default function CoinSelectionScreen({ navigation, route }: TCoinSelectio {isNum(estFee) && !nostr && !isSendEcash && } - 0 ? `${formatInt(balance - amount - estFee)} ${t('to')} ${formatSatStr(balance - amount)}` : `${formatSatStr(balance - amount)}`} - /> + + + 0 ? `${formatInt(balance - amount - estFee)} ${t('to')} ${formatSatStr(balance - amount)}` : `${formatSatStr(balance - amount)}`} + styles={[{ color: color.TEXT_SECONDARY }]} + /> + + {memo && memo.length > 0 && }