mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 19:28:26 +00:00
Finalize initial NFC implementation, fix wallet won't start when offline
This commit is contained in:
@@ -56,7 +56,7 @@ notifee.registerForegroundService(async (notification) => {
|
||||
if(notification.data.task === SYNC_STATE_WITH_ALL_MINTS_TASK) {
|
||||
log.debug(`[registerForegroundService] Submitting task ${SYNC_STATE_WITH_ALL_MINTS_TASK} to the queue.`)
|
||||
|
||||
WalletTask.syncStateWithAllMintsQueue({isPending: false})
|
||||
await WalletTask.syncStateWithAllMintsQueueAwaitable({isPending: false})
|
||||
}
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "minibits_wallet",
|
||||
"version": "0.3.4-beta.4",
|
||||
"version": "0.3.4-beta.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android:clean": "cd android && ./gradlew clean",
|
||||
@@ -63,6 +63,7 @@
|
||||
"react-native-fast-image": "^8.6.3",
|
||||
"react-native-flash-message": "^0.4.2",
|
||||
"react-native-gesture-handler": "^2.28.0",
|
||||
"react-native-indicators": "^0.17.0",
|
||||
"react-native-json-tree": "^1.5.0",
|
||||
"react-native-keychain": "^10.0.0",
|
||||
"react-native-localize": "^3.5.2",
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
diff --git a/node_modules/react-native-indicators/src/components/indicator/index.js b/node_modules/react-native-indicators/src/components/indicator/index.js
|
||||
index b2de0a8..feb3c21 100644
|
||||
--- a/node_modules/react-native-indicators/src/components/indicator/index.js
|
||||
+++ b/node_modules/react-native-indicators/src/components/indicator/index.js
|
||||
@@ -175,6 +175,7 @@ export default class Indicator extends PureComponent {
|
||||
render() {
|
||||
let { hideAnimation } = this.state;
|
||||
let { count, hidesWhenStopped, ...props } = this.props;
|
||||
+ const { key, ...rest } = props;
|
||||
|
||||
if (hidesWhenStopped) {
|
||||
props.style = []
|
||||
@@ -182,7 +183,7 @@ export default class Indicator extends PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
- <Animated.View {...props}>
|
||||
+ <Animated.View key={key} {...rest}>
|
||||
{Array.from(new Array(count), this.renderComponent, this)}
|
||||
</Animated.View>
|
||||
);
|
||||
diff --git a/node_modules/react-native-indicators/src/components/material-indicator/index.js b/node_modules/react-native-indicators/src/components/material-indicator/index.js
|
||||
index bf7edc4..0bde608 100644
|
||||
--- a/node_modules/react-native-indicators/src/components/material-indicator/index.js
|
||||
+++ b/node_modules/react-native-indicators/src/components/material-indicator/index.js
|
||||
@@ -107,7 +107,7 @@ export default class MaterialIndicator extends PureComponent {
|
||||
};
|
||||
|
||||
return (
|
||||
- <Animated.View style={styles.layer} {...{ key: index }}>
|
||||
+ <Animated.View style={styles.layer} key={index}>
|
||||
<Animated.View style={layerStyle}>
|
||||
<Animated.View style={[containerStyle, offsetStyle]} collapsable={false}>
|
||||
<Animated.View style={viewportStyle}>
|
||||
diff --git a/node_modules/react-native-indicators/src/components/pulse-indicator/index.js b/node_modules/react-native-indicators/src/components/pulse-indicator/index.js
|
||||
index 53bcf92..e452df8 100644
|
||||
--- a/node_modules/react-native-indicators/src/components/pulse-indicator/index.js
|
||||
+++ b/node_modules/react-native-indicators/src/components/pulse-indicator/index.js
|
||||
@@ -51,7 +51,7 @@ export default class PulseIndicator extends PureComponent {
|
||||
};
|
||||
|
||||
return (
|
||||
- <Animated.View style={styles.layer} {...{ key: index }}>
|
||||
+ <Animated.View style={styles.layer} key={index}>
|
||||
<Animated.View style={pulseStyle} />
|
||||
</Animated.View>
|
||||
);
|
||||
diff --git a/node_modules/react-native-indicators/src/components/skype-indicator/index.js b/node_modules/react-native-indicators/src/components/skype-indicator/index.js
|
||||
index 8a988df..1a0304f 100644
|
||||
--- a/node_modules/react-native-indicators/src/components/skype-indicator/index.js
|
||||
+++ b/node_modules/react-native-indicators/src/components/skype-indicator/index.js
|
||||
@@ -68,7 +68,7 @@ export default class SkypeIndicator extends PureComponent {
|
||||
};
|
||||
|
||||
return (
|
||||
- <Animated.View style={[styles.layer, layerStyle]} {...{ key: index }}>
|
||||
+ <Animated.View style={[styles.layer, layerStyle]} key={index}>
|
||||
<Animated.View style={ballStyle} />
|
||||
</Animated.View>
|
||||
);
|
||||
diff --git a/node_modules/react-native-indicators/src/components/ui-activity-indicator/index.js b/node_modules/react-native-indicators/src/components/ui-activity-indicator/index.js
|
||||
index dcde1e5..468e560 100644
|
||||
--- a/node_modules/react-native-indicators/src/components/ui-activity-indicator/index.js
|
||||
+++ b/node_modules/react-native-indicators/src/components/ui-activity-indicator/index.js
|
||||
@@ -61,7 +61,7 @@ export default class UIActivityIndicator extends PureComponent {
|
||||
};
|
||||
|
||||
return (
|
||||
- <Animated.View style={[styles.layer, layerStyle]} {...{ key: index }}>
|
||||
+ <Animated.View style={[styles.layer, layerStyle]} key={index}>
|
||||
<Animated.View style={barStyle} />
|
||||
</Animated.View>
|
||||
);
|
||||
diff --git a/node_modules/react-native-indicators/src/components/wave-indicator/index.js b/node_modules/react-native-indicators/src/components/wave-indicator/index.js
|
||||
index 2de37d5..1fbe5ce 100644
|
||||
--- a/node_modules/react-native-indicators/src/components/wave-indicator/index.js
|
||||
+++ b/node_modules/react-native-indicators/src/components/wave-indicator/index.js
|
||||
@@ -64,7 +64,7 @@ export default class WaveIndicator extends PureComponent {
|
||||
};
|
||||
|
||||
return (
|
||||
- <Animated.View style={styles.layer} {...{ key: index }}>
|
||||
+ <Animated.View style={styles.layer} key={index}>
|
||||
<Animated.View style={waveStyle} />
|
||||
</Animated.View>
|
||||
);
|
||||
+3
-6
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import FlashMessage from "react-native-flash-message"
|
||||
import {
|
||||
SafeAreaProvider,
|
||||
@@ -20,15 +20,12 @@ import { spacing, typography } from './theme'
|
||||
import { displayName } from '../app.json'
|
||||
import { Text } from './components/Text'
|
||||
import useIsInternetReachable from './utils/useIsInternetReachable'
|
||||
import * as Sentry from '@sentry/react-native'
|
||||
import { ANDROID_VERSION_NAME, APP_ENV, JS_BUNDLE_VERSION, SENTRY_DSN } from '@env'
|
||||
|
||||
/* Set default size ratio for styling */
|
||||
setSizeMattersBaseWidth(375)
|
||||
setSizeMattersBaseHeight(812)
|
||||
|
||||
function App() {
|
||||
|
||||
const {userSettingsStore, relaysStore, authStore, walletStore, walletProfileStore} = useStores()
|
||||
const [isUserAuthenticated, setIsUserAuthenticated] = useState(false)
|
||||
const [isDeviceAuthenticated, setIsDeviceAuthenticated] = useState(false)
|
||||
@@ -65,8 +62,8 @@ function App() {
|
||||
}
|
||||
|
||||
// re-enroll device for JWT authentication if refresh token expired
|
||||
if(authStore.isRefreshTokenExpired && isInternetReachable && userSettingsStore.isOnboarded) {
|
||||
log.trace('[useInitialRootStore]', 'Re-enrolling device for JWT authentication')
|
||||
if(authStore.isRefreshTokenExpired && isInternetReachable === true && userSettingsStore.isOnboarded) {
|
||||
log.trace('[useInitialRootStore]', 'Re-enrolling device for JWT authentication', {isInternetReachable})
|
||||
try {
|
||||
const walletKeys: WalletKeys = await walletStore.getCachedWalletKeys()
|
||||
const deviceId = walletProfileStore.device
|
||||
|
||||
@@ -7,6 +7,7 @@ import { isObj } from '@cashu/cashu-ts/src/utils'
|
||||
import JSONTree from 'react-native-json-tree'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import { useStores } from '../models'
|
||||
import useIsInternetReachable from '../utils/useIsInternetReachable'
|
||||
|
||||
type ErrorModalProps = {
|
||||
error: AppError
|
||||
@@ -41,9 +42,11 @@ export const ErrorModal: FC<ErrorModalProps> = function ({ error }) {
|
||||
}
|
||||
}
|
||||
|
||||
const isInternetReachable = useIsInternetReachable()
|
||||
|
||||
const enrollDevice = async () => {
|
||||
const keys = await walletStore.getCachedWalletKeys()
|
||||
if (keys.NOSTR) {
|
||||
if (keys.NOSTR && isInternetReachable === true) {
|
||||
authStore.enrollDevice(keys.NOSTR, walletProfileStore.device)
|
||||
.then(() => {
|
||||
setIsErrorVisible(false)
|
||||
|
||||
@@ -168,7 +168,7 @@ function FocusAwareStatusBar(props: StatusBarProps) {
|
||||
* - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Components-Header.md)
|
||||
*/
|
||||
export function Header(props: HeaderProps) {
|
||||
const isInternetReachable = useIsInternetReachable()
|
||||
//const isInternetReachable = useIsInternetReachable()
|
||||
|
||||
const {
|
||||
backgroundColor = useThemeColor('header'),
|
||||
|
||||
@@ -42,7 +42,7 @@ type Props = StaticScreenProps<{
|
||||
}>
|
||||
|
||||
export const CashuPaymentRequestScreen = observer(function CashuPaymentRequestScreen({ route }: Props) {
|
||||
const isInternetReachable = useIsInternetReachable()
|
||||
//const isInternetReachable = useIsInternetReachable()
|
||||
const navigation = useNavigation()
|
||||
const {
|
||||
proofsStore,
|
||||
|
||||
@@ -34,7 +34,7 @@ import RNExitApp from 'react-native-exit-app'
|
||||
import { TransactionStatus } from '../models/Transaction'
|
||||
import { maxTransactionsInHistory } from '../models/TransactionsStore'
|
||||
import { StaticScreenProps, useNavigation } from '@react-navigation/native'
|
||||
// refresh // refresh
|
||||
// refresh // refresh // refresh
|
||||
|
||||
type Props = StaticScreenProps<undefined>
|
||||
|
||||
|
||||
+254
-64
@@ -7,6 +7,17 @@ import {
|
||||
ColorValue,
|
||||
Platform,
|
||||
} from 'react-native'
|
||||
import {
|
||||
BallIndicator,
|
||||
BarIndicator,
|
||||
DotIndicator,
|
||||
MaterialIndicator,
|
||||
PacmanIndicator,
|
||||
PulseIndicator,
|
||||
SkypeIndicator,
|
||||
UIActivityIndicator,
|
||||
WaveIndicator,
|
||||
} from 'react-native-indicators'
|
||||
import { PaymentRequest as CashuPaymentRequest, MeltQuoteResponse, PaymentRequestTransport, PaymentRequestTransportType, decodePaymentRequest, getDecodedToken } from '@cashu/cashu-ts'
|
||||
import NfcManager, { NfcTech, Ndef, NfcEvents, TagEvent } from 'react-native-nfc-manager'
|
||||
import { colors, spacing, typography, useThemeColor } from '../theme'
|
||||
@@ -14,7 +25,7 @@ import EventEmitter from '../utils/eventEmitter'
|
||||
import { log } from '../services/logService'
|
||||
import { IncomingDataType, IncomingParser } from '../services/incomingParser'
|
||||
import AppError, { Err } from '../utils/AppError'
|
||||
import { BottomModal, Button, Card, ErrorModal, Icon, InfoModal, ListItem, ScanIcon, Screen, Text } from '../components'
|
||||
import { AmountInput, BottomModal, Button, Card, ErrorModal, Icon, InfoModal, ListItem, ScanIcon, Screen, Text } from '../components'
|
||||
import { infoMessage } from '../utils/utils'
|
||||
import { SvgXml } from 'react-native-svg'
|
||||
import { formatCurrency, getCurrency, MintUnit, MintUnits } from '../services/wallet/currency'
|
||||
@@ -48,6 +59,7 @@ import Animated, {
|
||||
Easing,
|
||||
} from 'react-native-reanimated';
|
||||
import { NfcService } from '../services/nfcService'
|
||||
import { TranItem } from './TranDetailScreen'
|
||||
//import Animated from 'react-native-reanimated'
|
||||
|
||||
const ContactlessIcon = (color: ColorValue | string) => `<?xml version="1.0" encoding="utf-8"?>
|
||||
@@ -110,9 +122,11 @@ type Props = StaticScreenProps<{
|
||||
unit: MintUnit
|
||||
}>
|
||||
|
||||
const SYNC_STATE_WITH_MINT_TIMEOUT = 10 * 1000
|
||||
|
||||
export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
const navigation = useNavigation<any>()
|
||||
const { mintsStore, walletStore, proofsStore } = useStores()
|
||||
const { mintsStore, walletStore, proofsStore, transactionsStore } = useStores()
|
||||
const unitRef = useRef<MintUnit>('sat')
|
||||
|
||||
const isInternetReachable = useIsInternetReachable()
|
||||
@@ -139,6 +153,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
const [isNfcEnabled, setIsNfcEnabled] = useState(false)
|
||||
const [isNfcSupported, setIsNfcSupported] = useState(false)
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const [isPaid, setIsPaid] = useState(false)
|
||||
const [nfcInfo, setNfcInfo] = useState<string | undefined>()
|
||||
const [readNfcData, setReadNfcData] = useState<string | undefined>()
|
||||
const [mint, setMint] = useState<Mint | undefined>()
|
||||
@@ -164,7 +179,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
return
|
||||
}
|
||||
|
||||
setNfcInfo('Tap your device to the NFC reader to pay')
|
||||
setNfcInfo('Hold your device close to the NFC reader.')
|
||||
setIsNfcEnabled(true)
|
||||
|
||||
// Start listening for tags immediately
|
||||
@@ -196,6 +211,39 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
}, [navigation, isNfcEnabled, isProcessing])
|
||||
|
||||
|
||||
const showSuccessModal = function () {
|
||||
const amountSentInt = Math.round(
|
||||
(toNumber(amountToPay || '0') * getCurrency(unitRef.current).precision)
|
||||
);
|
||||
|
||||
const currency = getCurrency(unitRef.current);
|
||||
|
||||
setResultModalInfo({
|
||||
status: TransactionStatus.COMPLETED,
|
||||
title: 'That was fast!',
|
||||
message: `${formatCurrency(amountSentInt, currency.code)} ${currency.code} were received by the payee.`,
|
||||
});
|
||||
setTransactionStatus(TransactionStatus.COMPLETED);
|
||||
setIsResultModalVisible(true);
|
||||
//setNfcInfo('Payment completed successfully!');
|
||||
}
|
||||
|
||||
|
||||
const showPendingModal = function () {
|
||||
const amountSentInt = Math.round(
|
||||
(toNumber(amountToPay || '0') * getCurrency(unitRef.current).precision)
|
||||
);
|
||||
|
||||
const currency = getCurrency(unitRef.current);
|
||||
|
||||
setResultModalInfo({
|
||||
status: TransactionStatus.PENDING,
|
||||
//title: 'Payment is pending',
|
||||
message: `${formatCurrency(amountSentInt, currency.code)} ${currency.code} were sent successfully, but the payee has not yet claimed the ecash . You can wait for confirmation or revert the transaction.`,
|
||||
});
|
||||
setTransactionStatus(TransactionStatus.PENDING);
|
||||
setIsResultModalVisible(true)
|
||||
}
|
||||
|
||||
// Stable handler — only recreated when dependencies actually change
|
||||
const handleSyncStateResult = useCallback(async (result: SyncStateTaskResult) => {
|
||||
@@ -208,21 +256,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
// ——— SUCCESS: Receiver claimed the ecash ———
|
||||
if (completedTransactionIds?.includes(transactionId)) {
|
||||
log.debug('[NfcScreen.handleSyncStateResult] Ecash claimed successfully by the payee.', { transactionId });
|
||||
|
||||
const amountSentInt = Math.round(
|
||||
(toNumber(amountToPay || '0') * getCurrency(unitRef.current).precision)
|
||||
);
|
||||
|
||||
const currency = getCurrency(unitRef.current);
|
||||
|
||||
setResultModalInfo({
|
||||
status: TransactionStatus.COMPLETED,
|
||||
title: 'That was fast!',
|
||||
message: `${formatCurrency(amountSentInt, currency.code)} ${currency.code} were received by the payee.`,
|
||||
});
|
||||
setTransactionStatus(TransactionStatus.COMPLETED);
|
||||
setIsResultModalVisible(true);
|
||||
setNfcInfo('Payment completed successfully!');
|
||||
showSuccessModal()
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -250,8 +284,10 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
|
||||
// One-time listener with auto-cleanup using ref
|
||||
const syncStateListenerRef = useRef<((r: SyncStateTaskResult) => void) | null>(null)
|
||||
const fallbackTimeoutRef = useRef<number>(null)
|
||||
|
||||
useEffect(() => {
|
||||
log.trace('[useEffect] Triggered to set syncStateListenerRef', {transactionId})
|
||||
// If no transactionId → nothing to listen for
|
||||
if (!transactionId) {
|
||||
// Clean up any dangling listener
|
||||
@@ -259,10 +295,33 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
EventEmitter.off(`ev_${SYNC_STATE_WITH_MINT_TASK}_result`, syncStateListenerRef.current)
|
||||
syncStateListenerRef.current = null
|
||||
}
|
||||
|
||||
if (fallbackTimeoutRef.current) {
|
||||
clearTimeout(fallbackTimeoutRef.current)
|
||||
fallbackTimeoutRef.current = null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const checkFallback = () => {
|
||||
const currentTx = transactionsStore.findById(transactionId)
|
||||
log.trace('[checkFallback] Check after timeout returned transaction', {status: currentTx?.status})
|
||||
if (currentTx?.status === TransactionStatus.COMPLETED) {
|
||||
showSuccessModal()
|
||||
}
|
||||
|
||||
if(currentTx?.status === TransactionStatus.PENDING) {
|
||||
showPendingModal()
|
||||
}
|
||||
return
|
||||
// If still PENDING or ERROR → do nothing, user can close manually
|
||||
}
|
||||
|
||||
const eventName = `ev_${SYNC_STATE_WITH_MINT_TASK}_result`
|
||||
// Start fallback timer — 15 seconds after token write. Skip if offline.
|
||||
if(isInternetReachable) {
|
||||
fallbackTimeoutRef.current = setTimeout(checkFallback, SYNC_STATE_WITH_MINT_TIMEOUT)
|
||||
}
|
||||
|
||||
// Remove previous listener (defensive)
|
||||
if (syncStateListenerRef.current) {
|
||||
@@ -270,7 +329,13 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
}
|
||||
|
||||
const handler = (result: SyncStateTaskResult) => {
|
||||
handleSyncStateResult(result);
|
||||
handleSyncStateResult(result)
|
||||
|
||||
// Clear fallback — event won
|
||||
if (fallbackTimeoutRef.current) {
|
||||
clearTimeout(fallbackTimeoutRef.current)
|
||||
fallbackTimeoutRef.current = null
|
||||
}
|
||||
|
||||
// "keep listening" for poller if our transaction is still pending
|
||||
const shouldStaySubscribed = (() => {
|
||||
@@ -297,13 +362,15 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
// Cleanup on unmount or when transactionId changes/becomes null
|
||||
return () => {
|
||||
if (syncStateListenerRef.current) {
|
||||
EventEmitter.off(eventName, syncStateListenerRef.current)
|
||||
syncStateListenerRef.current = null
|
||||
EventEmitter.off(eventName, syncStateListenerRef.current)
|
||||
syncStateListenerRef.current = null
|
||||
}
|
||||
if (fallbackTimeoutRef.current) {
|
||||
clearTimeout(fallbackTimeoutRef.current)
|
||||
fallbackTimeoutRef.current = null
|
||||
}
|
||||
}
|
||||
}, [
|
||||
handleSyncStateResult, // ← stable thanks to useCallback
|
||||
])
|
||||
}, [transactionId, handleSyncStateResult])
|
||||
|
||||
|
||||
// ====================== Lightning Payment (Transfer Task) Result Handler ======================
|
||||
@@ -319,6 +386,8 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsProcessing(true)
|
||||
|
||||
//log.info('NFC Tag found', {tag});
|
||||
const bytes = new Uint8Array(tag.ndefMessage[0]?.payload ?? [])
|
||||
decoded = Ndef.text.decodePayload(bytes)
|
||||
@@ -337,7 +406,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
return
|
||||
}
|
||||
|
||||
setNfcInfo('Processing payment request...')
|
||||
setNfcInfo('Processing, keep your device still...')
|
||||
|
||||
log.info('NFC tag decoded', { decoded })
|
||||
|
||||
@@ -352,7 +421,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
})
|
||||
handleError(e)
|
||||
} finally {
|
||||
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,7 +477,13 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
)
|
||||
|
||||
if(!selectedBalance) { // we might not find suitable balance if offline
|
||||
setInfo('Wallet do')
|
||||
setResultModalInfo({
|
||||
status: (transaction?.status || TransactionStatus.ERROR) as TransactionStatus,
|
||||
title: 'Offline payment failed',
|
||||
message: 'Wallet is offline and does not have ecash denominations matching requested amount',
|
||||
});
|
||||
toggleResultModal()
|
||||
setNfcInfo('Can not pay requested amount')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -489,7 +564,19 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
|
||||
log.trace('[NfcScreen] handleSendTaskResult: Write completed.')
|
||||
setNfcInfo('Ecash token sent successfully.')
|
||||
setIsPaid(true)
|
||||
expandHeader()
|
||||
|
||||
// Show explanatory modal immediately when offline
|
||||
if (!isInternetReachable) {
|
||||
setResultModalInfo({
|
||||
status: TransactionStatus.PENDING,
|
||||
title: translate('commonOfflinePretty'),
|
||||
message: 'Your wallet sent this payment while offline. Consult the payee that the funds have been claimed.',
|
||||
})
|
||||
|
||||
toggleResultModal()
|
||||
}
|
||||
} catch (err: any) {
|
||||
log.error('Error in handleSendTaskResult', {error: err, caller: 'NfcScreen.handleSendTaskResult'})
|
||||
|
||||
@@ -500,6 +587,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
})
|
||||
|
||||
toggleResultModal()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,7 +595,13 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
|
||||
const handleLightningInvoice = async (encodedInvoice: string) => {
|
||||
if (!isInternetReachable) {
|
||||
setInfo(translate('commonOfflinePretty'))
|
||||
setResultModalInfo({
|
||||
status: TransactionStatus.ERROR,
|
||||
title: translate('commonOfflinePretty'),
|
||||
message: 'Can not use Lightning payment method while offline.',
|
||||
})
|
||||
|
||||
toggleResultModal()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -524,19 +618,24 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
const balances = proofsStore.getMintBalancesWithEnoughBalance(totalRequired, unitRef.current)
|
||||
|
||||
if (balances.length === 0) {
|
||||
setInfo(
|
||||
translate('transferScreen_insufficientFunds', {
|
||||
setResultModalInfo({
|
||||
status: TransactionStatus.ERROR,
|
||||
title: 'Not enough funds',
|
||||
message: translate('transferScreen_insufficientFunds', {
|
||||
currency: getCurrency(unitRef.current).code,
|
||||
amount: totalRequired,
|
||||
})
|
||||
)
|
||||
}),
|
||||
})
|
||||
|
||||
toggleResultModal()
|
||||
return
|
||||
}
|
||||
|
||||
const selectedBalance = balances[0]
|
||||
setMintBalanceToSendFrom(selectedBalance)
|
||||
|
||||
setInfo('Creating Lightning payment quote...')
|
||||
setNfcInfo('Creating Lightning payment quote...')
|
||||
|
||||
const quote = await walletStore.createLightningMeltQuote(
|
||||
selectedBalance.mintUrl,
|
||||
unitRef.current,
|
||||
@@ -552,8 +651,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
if (description) setMemo(description)
|
||||
|
||||
try {
|
||||
setInfo('Paying Lightning invoice...')
|
||||
setIsLoading(true)
|
||||
setNfcInfo('Paying Lightning invoice...')
|
||||
|
||||
const result = await WalletTask.transferQueueAwaitable(
|
||||
selectedBalance,
|
||||
@@ -601,12 +699,15 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Success or settled pending
|
||||
setResultModalInfo({
|
||||
status,
|
||||
message: message || 'Lightning payment successful!',
|
||||
title: status === TransactionStatus.COMPLETED ? 'Payment sent!' : undefined,
|
||||
});
|
||||
// Success or settled pending
|
||||
setResultModalInfo({
|
||||
status,
|
||||
message: message || 'Lightning payment successful!',
|
||||
title: status === TransactionStatus.COMPLETED ? 'Payment sent!' : undefined,
|
||||
});
|
||||
setIsPaid(true)
|
||||
expandHeader()
|
||||
setNfcInfo(transaction.memo || 'Lightning payment settled.')
|
||||
}
|
||||
} else {
|
||||
// Fallback (shouldn't happen)
|
||||
@@ -616,11 +717,10 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
message: 'No transaction data received',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
toggleResultModal()
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setInfo('')
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -720,36 +820,123 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
setInfo('')
|
||||
setError(undefined)
|
||||
setIsLoading(false)
|
||||
setIsProcessing(false)
|
||||
setIsResultModalVisible(false)
|
||||
setResultModalInfo(undefined)
|
||||
setIsPaid(false)
|
||||
}
|
||||
|
||||
const toggleResultModal = () => setIsResultModalVisible(previousState => !previousState)
|
||||
|
||||
const headerHeight = useSharedValue(spacing.screenHeight * 0.15); // Initial height
|
||||
const collapseHeader = () => {
|
||||
headerHeight.value = spacing.screenHeight * 0.15
|
||||
}
|
||||
|
||||
const expandHeader = () => {
|
||||
headerHeight.value = spacing.screenHeight * 0.20
|
||||
}
|
||||
|
||||
const animatedHeader = useAnimatedStyle(() => {
|
||||
return {
|
||||
height: withTiming(headerHeight.value, { duration: 300 }),
|
||||
// opacity: withTiming(isVisible.value ? 1 : 0, { duration: 300 }),
|
||||
}
|
||||
})
|
||||
|
||||
// Theme
|
||||
const hintText = useThemeColor('textDim')
|
||||
const headerBg = useThemeColor('header')
|
||||
const headerTitle = useThemeColor('headerTitle')
|
||||
const scanIcon = useThemeColor('text')
|
||||
const scanButtonColor = useThemeColor('card')
|
||||
const indicatorStandby = useThemeColor('headerTitle')
|
||||
const indicatorProcessing = useThemeColor('button')
|
||||
|
||||
return (
|
||||
<Screen preset="fixed" contentContainerStyle={$screen}>
|
||||
<MintHeader mint={mint} unit={unitRef.current} />
|
||||
<View style={[$headerContainer, { backgroundColor: headerBg }]}>
|
||||
{/*<Icon
|
||||
icon='faNfcSymbol'
|
||||
size={verticalScale(35)}
|
||||
color={headerTitle}
|
||||
/>*/}
|
||||
</View>
|
||||
|
||||
<Animated.View style={[animatedHeader, $headerContainer, {backgroundColor: headerBg}]}>
|
||||
<View style={{}}>
|
||||
{isPaid && (
|
||||
<AmountInput
|
||||
//ref={amountInputRef}
|
||||
value={`${amountToPay}`}
|
||||
onChangeText={() => {}}
|
||||
unit={unitRef.current}
|
||||
editable={false}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
<View style={$contentContainer}>
|
||||
<Card
|
||||
ContentComponent={
|
||||
{isPaid && transaction ? (
|
||||
<Card
|
||||
style={{padding: spacing.medium}}
|
||||
ContentComponent={
|
||||
<>
|
||||
<TranItem
|
||||
label="tranDetailScreen_trasferredTo"
|
||||
isFirst={true}
|
||||
value={
|
||||
mintsStore.findByUrl(transaction.mint)
|
||||
?.shortname as string
|
||||
}
|
||||
/>
|
||||
{transaction?.memo && (
|
||||
<TranItem
|
||||
label="tranDetailScreen_memoFromInvoice"
|
||||
value={transaction.memo as string}
|
||||
/>
|
||||
)}
|
||||
<TranItem
|
||||
label="transactionCommon_feePaid"
|
||||
value={transaction.fee || 0}
|
||||
unit={unitRef.current}
|
||||
isCurrency={true}
|
||||
/>
|
||||
<TranItem
|
||||
label="tranDetailScreen_status"
|
||||
value={transaction.status as string}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Card
|
||||
ContentComponent={
|
||||
<>
|
||||
<PulsingContactlessIcon isNfcEnabled={isNfcEnabled} />
|
||||
<Text
|
||||
{isProcessing ? (
|
||||
<WaveIndicator
|
||||
color={indicatorProcessing}
|
||||
size={verticalScale(120)}
|
||||
animating={resultModalInfo ? false : true}
|
||||
animationDuration={2000}
|
||||
/>
|
||||
) : isNfcEnabled ? (
|
||||
|
||||
<PulseIndicator
|
||||
color={indicatorStandby}
|
||||
size={verticalScale(120)}
|
||||
animating={resultModalInfo ? false : true}
|
||||
hidesWhenStopped={false}
|
||||
animationDuration={2000}
|
||||
/>
|
||||
|
||||
) : (
|
||||
<PulseIndicator
|
||||
color={hintText}
|
||||
size={verticalScale(120)}
|
||||
animating={false}
|
||||
useNativeDriver={true}
|
||||
hidesWhenStopped={false}
|
||||
//animationDuration={2000}
|
||||
/>
|
||||
)}
|
||||
|
||||
</>
|
||||
}
|
||||
FooterComponent={
|
||||
<Text
|
||||
text={
|
||||
nfcInfo ? nfcInfo : ''
|
||||
}
|
||||
@@ -757,17 +944,20 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
textAlign: 'center',
|
||||
marginBottom: spacing.medium,
|
||||
//fontFamily: typography.code,
|
||||
color: isNfcEnabled ? colors.palette.success200 : colors.palette.neutral500,
|
||||
color: isProcessing ? indicatorProcessing : isNfcEnabled ? indicatorStandby : hintText,
|
||||
fontSize: 18,
|
||||
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
/>
|
||||
}
|
||||
style={{height: spacing.screenHeight * 0.3}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
<View style={$bottomContainer}>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
{!isProcessing && (<Button
|
||||
preset='tertiary'
|
||||
LeftAccessory={() => (
|
||||
<SvgXml
|
||||
@@ -781,7 +971,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
onPress={gotoScan}
|
||||
//style={{backgroundColor: scanButtonColor}}
|
||||
text='Scan QR instead'
|
||||
/>
|
||||
/>)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -854,7 +1044,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
<ResultModalInfo
|
||||
icon="faTriangleExclamation"
|
||||
iconColor={colors.palette.iconYellow300}
|
||||
title={translate('payCommon_isPending')}
|
||||
title={resultModalInfo?.title || translate('payCommon_isPending')}
|
||||
message={resultModalInfo?.message}
|
||||
/>
|
||||
<View style={$buttonContainer}>
|
||||
|
||||
+23
-16
@@ -433,23 +433,30 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
|
||||
|
||||
|
||||
|
||||
// Offline send
|
||||
useEffect(() => {
|
||||
if(isInternetReachable) return
|
||||
log.trace('[Offline send]')
|
||||
|
||||
// if offline we set all non-zero mint balances as available to allow ecash selection
|
||||
const availableBalances = proofsStore.getMintBalancesWithEnoughBalance(1, unitRef.current)
|
||||
|
||||
if (availableBalances.length === 0) {
|
||||
setInfo('Not enough funds to send')
|
||||
return
|
||||
useEffect(() => {
|
||||
log.trace('Offline send effect', { isInternetReachable })
|
||||
|
||||
if (isInternetReachable === true) {
|
||||
// We are definitively online → force online mode
|
||||
log.trace('[Online send] Switching to online mode')
|
||||
setIsOfflineSend(false)
|
||||
setAvailableMintBalances([]) // or whatever your online default is
|
||||
setInfo('') // clear any offline-specific messages
|
||||
} else if (isInternetReachable === false) {
|
||||
// We are definitively offline → enable offline send
|
||||
log.trace('[Offline send] Enabling offline mode')
|
||||
|
||||
const availableBalances = proofsStore.getMintBalancesWithEnoughBalance(1, unitRef.current)
|
||||
|
||||
if (availableBalances.length === 0) {
|
||||
setInfo('Not enough funds to send')
|
||||
return // keep isOfflineSend false or as-is
|
||||
}
|
||||
|
||||
setIsOfflineSend(true)
|
||||
setAvailableMintBalances(availableBalances)
|
||||
}
|
||||
|
||||
log.trace('[Offline send] Setting availableBalances')
|
||||
|
||||
setIsOfflineSend(true)
|
||||
setAvailableMintBalances(availableBalances)
|
||||
// If isInternetReachable is null (still checking), do nothing
|
||||
}, [isInternetReachable])
|
||||
|
||||
|
||||
|
||||
@@ -1143,7 +1143,8 @@ const SendInfoBlock = function (props: {
|
||||
throw new AppError(Err.VALIDATION_ERROR, message)
|
||||
}
|
||||
|
||||
const pendingProofs = proofsStore.getByTransactionId(transaction.id)
|
||||
const allProofs = proofsStore.getByTransactionId(transaction.id)
|
||||
const pendingProofs = allProofs.filter(p => p.isPending === true)
|
||||
|
||||
if(pendingProofs.length === 0) {
|
||||
const message = 'Could not get proofs related to the transaction from wallet state.'
|
||||
@@ -1160,9 +1161,10 @@ const SendInfoBlock = function (props: {
|
||||
})
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
const result = await WalletTask.revertQueueAwaitable(transaction)
|
||||
await handleRevertTaskResult(result)
|
||||
setIsLoading(true)
|
||||
|
||||
|
||||
} catch (e: any) {
|
||||
setResultModalInfo({status: TransactionStatus.ERROR, message: e.message})
|
||||
@@ -1186,7 +1188,6 @@ const SendInfoBlock = function (props: {
|
||||
message: result.message,
|
||||
})
|
||||
}
|
||||
setIsLoading(false)
|
||||
toggleResultModal()
|
||||
}
|
||||
|
||||
|
||||
@@ -634,7 +634,7 @@ const transfer = async function () {
|
||||
)
|
||||
|
||||
await handleTransferTaskResult(result)
|
||||
|
||||
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
|
||||
@@ -684,17 +684,23 @@ export const WalletScreen = observer(function WalletScreen({ route }: Props) {
|
||||
/>}
|
||||
TitleActionComponent={<HeaderTitle />}
|
||||
RightActionComponent={
|
||||
<>
|
||||
<View style={{justifyContent: 'flex-end', flexDirection: 'row'}}>
|
||||
{pendingCount > 0 && (
|
||||
<Pressable
|
||||
style={{flexDirection: 'row', alignItems:'center', marginRight: spacing.medium}}
|
||||
style={{flexDirection: 'row', alignItems:'center', marginRight: spacing.small}}
|
||||
onPress={gotoPendingTransactions}
|
||||
>
|
||||
<Icon icon='faClock' color={headerTitleColor}/>
|
||||
<Text text={`${pendingCount}`} style={{color: headerTitleColor}} />
|
||||
</Pressable>
|
||||
)}
|
||||
</>
|
||||
<Pressable
|
||||
style={{marginRight: spacing.medium}}
|
||||
onPress={gotoNfcPay}
|
||||
>
|
||||
<Icon icon='faNfcSymbol' color={headerTitleColor}/>
|
||||
</Pressable>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
{groupedMints.length > 0 && (
|
||||
@@ -846,14 +852,14 @@ export const WalletScreen = observer(function WalletScreen({ route }: Props) {
|
||||
tx="walletScreen_payWithLightning"
|
||||
subTx="walletScreen_payWithLightningDesc"
|
||||
onPress={() => gotoLightningPay()}
|
||||
bottomSeparator={true}
|
||||
//bottomSeparator={true}
|
||||
/>
|
||||
<ListItem
|
||||
{/*<ListItem
|
||||
leftIcon='faNfcSymbol'
|
||||
text="Pay with NFC"
|
||||
subText="Tap NFC-enabled wallet or POS to pay"
|
||||
onPress={() => gotoNfcPay()}
|
||||
/>
|
||||
/>*/}
|
||||
</>
|
||||
}
|
||||
onBackButtonPress={toggleSendModal}
|
||||
|
||||
@@ -291,6 +291,7 @@ export const WelcomeScreen = function ({ route }: Props) {
|
||||
const $dotsContainer: ViewStyle ={
|
||||
height: 50,
|
||||
justifyContent: 'space-evenly',
|
||||
marginBottom: 50
|
||||
}
|
||||
|
||||
const $dotContainer: ViewStyle ={
|
||||
|
||||
+25
-20
@@ -3,6 +3,7 @@ import NfcManager, { NfcTech, Ndef, NfcEvents, TagEvent } from 'react-native-nfc
|
||||
import { log } from './logService'
|
||||
import AppError, { Err } from '../utils/AppError'
|
||||
|
||||
|
||||
const NFC_STALE_DELAY_MS = 120
|
||||
|
||||
const init = async function () {
|
||||
@@ -26,7 +27,7 @@ const goToNfcSetting = function () {
|
||||
* Ensures a fresh NFC connection by cancelling any previous session
|
||||
* and adding a tiny delay on Android
|
||||
*/
|
||||
const withFreshNfcConnection = async <T>(action: () => Promise<T>): Promise<T> => {
|
||||
/* const withFreshNfcConnection = async <T>(action: () => Promise<T>): Promise<T> => {
|
||||
// Cancel any previous session
|
||||
await NfcManager.cancelTechnologyRequest().catch(() => {})
|
||||
log.trace('[withFreshNfcConnection] Pre-cancel completed')
|
||||
@@ -36,37 +37,41 @@ const withFreshNfcConnection = async <T>(action: () => Promise<T>): Promise<T> =
|
||||
}
|
||||
|
||||
try {
|
||||
return await action() // Only requestTechnology + getTag / writeNdef
|
||||
return await action()
|
||||
} finally {
|
||||
// This now runs IMMEDIATELY after read/write
|
||||
await NfcManager.cancelTechnologyRequest().catch(() => {})
|
||||
log.trace('[withFreshNfcConnection] Post-cancel completed')
|
||||
}
|
||||
}*/
|
||||
|
||||
|
||||
const readNdefTag = async () => {
|
||||
log.trace('[readNdefTag] start')
|
||||
try {
|
||||
await NfcManager.requestTechnology(NfcTech.Ndef) // needs to run only once per read/write session
|
||||
log.trace('[readNdefTag] requestTechnology completed')
|
||||
const tag = await NfcManager.getTag()
|
||||
log.trace('[readNdefTag] tag read') // do not close session
|
||||
|
||||
return tag
|
||||
} catch(e: any) {
|
||||
throw new AppError(Err.NFC_ERROR, e.message, {caller: 'readNdefTag', error: String(e)})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const readNdefTag = async () => {
|
||||
return await withFreshNfcConnection(async () => {
|
||||
log.trace('[readNdefTag] requesting technology')
|
||||
await NfcManager.requestTechnology(NfcTech.Ndef)
|
||||
log.trace('[readNdefTag] requestTechnology completed')
|
||||
const tag = await NfcManager.getTag()
|
||||
|
||||
return tag
|
||||
})
|
||||
}
|
||||
|
||||
const writeNdefMessage = async (text: string) => {
|
||||
return await withFreshNfcConnection(async () => {
|
||||
await NfcManager.requestTechnology(NfcTech.Ndef)
|
||||
log.trace('[writeNdefMessage] requestTechnology completed')
|
||||
try {
|
||||
log.trace('[writeNdefMessage] start') // no more requestTechnology, it requires second tap then
|
||||
const bytes = Ndef.encodeMessage([Ndef.textRecord('en', text)])
|
||||
await NfcManager.ndefHandler.writeNdefMessage(bytes)
|
||||
})
|
||||
log.trace('[writeNdefMessage] write completed')
|
||||
await NfcManager.cancelTechnologyRequest().catch(() => {}) // close session
|
||||
} catch(e: any) {
|
||||
throw new AppError(Err.NFC_ERROR, e.message, {caller: 'writeNdefMessage', error: String(e)})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const NfcService = {
|
||||
init,
|
||||
isEnabled,
|
||||
|
||||
@@ -611,7 +611,6 @@ const getLastTransactionBy = function (
|
||||
throw new AppError(
|
||||
Err.DATABASE_ERROR,
|
||||
'Exactly one of paymentId, quote, or paymentRequest must be provided',
|
||||
'Invalid criteria'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -888,6 +887,11 @@ const addOrUpdateProofs = function (
|
||||
throw new Error('Conflicting proof states')
|
||||
}
|
||||
|
||||
if(proofs.length === 0) {
|
||||
log.error('[addOrUpdateProofs] Empty proof array passed')
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
for (const proof of proofs) {
|
||||
insertQueries.push([
|
||||
|
||||
@@ -35,7 +35,8 @@ const unit = transaction.unit as MintUnit
|
||||
|
||||
try {
|
||||
|
||||
const pendingProofs = proofsStore.getByTransactionId(transaction.id)
|
||||
const allProofs = proofsStore.getByTransactionId(transaction.id)
|
||||
const pendingProofs = allProofs.filter(p => p.isPending === true)
|
||||
|
||||
if(pendingProofs.length === 0) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Missing proofs to swap')
|
||||
|
||||
@@ -118,12 +118,12 @@ export const sendTask = async function (
|
||||
outputToken
|
||||
})
|
||||
|
||||
const balanceAfter = proofsStore.getUnitBalance(unit)?.unitBalance
|
||||
|
||||
transactionData.push({
|
||||
status: TransactionStatus.PENDING,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
const balanceAfter = proofsStore.getUnitBalance(unit)?.unitBalance
|
||||
|
||||
transaction.update({
|
||||
status: TransactionStatus.PENDING,
|
||||
@@ -143,13 +143,14 @@ export const sendTask = async function (
|
||||
const wsWallet = new CashuWallet(wsMint)
|
||||
|
||||
try {
|
||||
log.trace('[send] Subscribing to onProofStateUpdates for proof', {secret: proofsToSend[0]})
|
||||
const unsub = await wsWallet.onProofStateUpdates(
|
||||
[proofsToSend[0]],
|
||||
async (proofState: ProofState) => {
|
||||
log.trace(`Websocket: proof state updated: ${proofState.state}`)
|
||||
log.trace(`Websocket: proof state updated: ${proofState.state} with secret: ${proofsToSend[0].secret}`)
|
||||
|
||||
if (proofState.state == CheckStateEnum.SPENT) {
|
||||
await WalletTask.syncStateWithMintQueueAwaitable({proofsToSync, mintUrl, isPending: true})
|
||||
WalletTask.syncStateWithMintQueueAwaitable({proofsToSync, mintUrl, isPending: true})
|
||||
unsub()
|
||||
}
|
||||
},
|
||||
@@ -171,7 +172,7 @@ export const sendTask = async function (
|
||||
maxPolls: 3,
|
||||
maxErrors: 1
|
||||
},
|
||||
{proofsToSync, mintUrl, isPending: true}
|
||||
{proofsToSend, mintUrl, isPending: true}
|
||||
)
|
||||
.then(() => log.trace('[syncStateWithMintPoller]', 'polling completed', {mintUrl}))
|
||||
}
|
||||
@@ -222,7 +223,8 @@ export const sendFromMintSync = async function (
|
||||
) {
|
||||
const mintUrl = mintBalance.mintUrl
|
||||
const mintInstance = mintsStore.findByUrl(mintUrl)
|
||||
let proofsToSendFrom: Proof[] = []
|
||||
let proofsToSendFrom: Proof[] = []
|
||||
let proofsToSend: CashuProof[] | Proof[] = []
|
||||
|
||||
try {
|
||||
if (!mintInstance) {
|
||||
@@ -322,7 +324,6 @@ export const sendFromMintSync = async function (
|
||||
let swapFeeReserve: number = 0
|
||||
let returnedAmount = 0
|
||||
let swapFeePaid: number = 0
|
||||
let proofsToSend: CashuProof[] | Proof[] = []
|
||||
let returnedProofs: CashuProof[] = []
|
||||
let isSwapNeeded: boolean = false
|
||||
|
||||
@@ -463,7 +464,7 @@ export const sendFromMintSync = async function (
|
||||
log.error('[sendFromMintSync] Going to clean spent proofs from proofsToSendFrom and from pending', {transactionId})
|
||||
|
||||
await WalletTask.syncStateWithMintTask({
|
||||
proofsToSync: proofsToSendFrom,
|
||||
proofsToSync: proofsToSend as Proof[],
|
||||
mintUrl,
|
||||
isPending: true
|
||||
})
|
||||
|
||||
@@ -1109,7 +1109,7 @@ const syncStateWithMintQueueAwaitable = function (
|
||||
})
|
||||
|
||||
const now = Date.now()
|
||||
const taskId = `syncStateWithMintTask-${now}`
|
||||
const taskId = `${SYNC_STATE_WITH_MINT_TASK}-${now}`
|
||||
|
||||
return new Promise<SyncStateTaskResult>((resolve, reject) => {
|
||||
let resolved = false
|
||||
@@ -1126,7 +1126,7 @@ const syncStateWithMintQueueAwaitable = function (
|
||||
reject(error)
|
||||
}
|
||||
|
||||
const eventName = `ev_syncStateWithMintTask_result`
|
||||
const eventName = `ev_${SYNC_STATE_WITH_MINT_TASK}_result`
|
||||
const handler = (result: SyncStateTaskResult) => {
|
||||
EventEmitter.off(eventName, handler)
|
||||
resolveOnce(result)
|
||||
@@ -1159,7 +1159,7 @@ const syncStateWithMintQueueAwaitable = function (
|
||||
EventEmitter.off(eventName, handler)
|
||||
rejectOnce(new AppError(Err.TIMEOUT_ERROR, 'Sync mint state timed out'))
|
||||
}
|
||||
}, 20000)
|
||||
}, TASK_QUEUE_TIMEOUT)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1429,7 +1429,7 @@ const syncStateWithMintTask = async function (
|
||||
revertedTransactionIds: revertedTxIds,
|
||||
}
|
||||
} catch (e: any) {
|
||||
log.error('[syncStateWithMintTask] failed', { mintUrl, error: e.message })
|
||||
log.error('[syncStateWithMintTask] failed', { mintUrl, error: e.message, stack: e.stack })
|
||||
|
||||
if (mint && e.name === Err.MINT_ERROR && e.message.includes('network')) {
|
||||
mint.setStatus(MintStatus.OFFLINE)
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
// utils/useIsInternetReachable.ts
|
||||
import { useEffect, useState } from 'react'
|
||||
import NetInfo, { NetInfoState } from '@react-native-community/netinfo'
|
||||
import {log} from '../services/logService'
|
||||
import NetInfo from '@react-native-community/netinfo'
|
||||
import { log } from '../services/logService'
|
||||
|
||||
export default function useIsInternetReachable(): boolean {
|
||||
// Start as false (conservative/safe default — assume offline until proven otherwise)
|
||||
const [isInternetReachable, setIsInternetReachable] = useState<boolean>(false)
|
||||
|
||||
export default function useIsInternetReachable() {
|
||||
// useState hook for setting netInfo
|
||||
// const [netInfo, setNetInfo] = useState({} as NetInfoState)
|
||||
const [isInternetReachable, setIsInternetReachable] = useState<boolean>(true)
|
||||
|
||||
useEffect(() => {
|
||||
// Whenever connection status changes below event fires
|
||||
const unsubscribe = NetInfo.addEventListener((state: NetInfoState) => {
|
||||
setIsInternetReachable(state.isInternetReachable as boolean)
|
||||
useEffect(() => {
|
||||
// Force an immediate check on mount — this reduces delay significantly
|
||||
NetInfo.refresh().then((state) => {
|
||||
log.trace('useIsInternetReachable', 'Initial refresh', {
|
||||
isConnected: state.isConnected,
|
||||
isInternetReachable: state.isInternetReachable,
|
||||
})
|
||||
if (state.isInternetReachable !== null) {
|
||||
setIsInternetReachable(state.isInternetReachable)
|
||||
}
|
||||
})
|
||||
|
||||
// Subscribe to future changes
|
||||
const unsubscribe = NetInfo.addEventListener((state) => {
|
||||
// Only update when we have a definitive boolean (ignore null)
|
||||
if (state.isInternetReachable !== null) {
|
||||
log.trace('useIsInternetReachable', 'Connection change', {
|
||||
isInternetReachable: state.isInternetReachable,
|
||||
})
|
||||
// Event cleanup function
|
||||
return () => {
|
||||
unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return isInternetReachable
|
||||
}
|
||||
setIsInternetReachable(state.isInternetReachable)
|
||||
}
|
||||
})
|
||||
|
||||
return () => unsubscribe()
|
||||
}, [])
|
||||
|
||||
return isInternetReachable
|
||||
}
|
||||
@@ -13604,6 +13604,7 @@ __metadata:
|
||||
react-native-fast-image: ^8.6.3
|
||||
react-native-flash-message: ^0.4.2
|
||||
react-native-gesture-handler: ^2.28.0
|
||||
react-native-indicators: ^0.17.0
|
||||
react-native-json-tree: ^1.5.0
|
||||
react-native-keychain: ^10.0.0
|
||||
react-native-localize: ^3.5.2
|
||||
@@ -15041,7 +15042,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prop-types@npm:^15.7.2, prop-types@npm:^15.8.0, prop-types@npm:^15.8.1":
|
||||
"prop-types@npm:^15.5.10, prop-types@npm:^15.7.2, prop-types@npm:^15.8.0, prop-types@npm:^15.8.1":
|
||||
version: 15.8.1
|
||||
resolution: "prop-types@npm:15.8.1"
|
||||
dependencies:
|
||||
@@ -15485,6 +15486,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-indicators@npm:^0.17.0":
|
||||
version: 0.17.0
|
||||
resolution: "react-native-indicators@npm:0.17.0"
|
||||
dependencies:
|
||||
prop-types: ^15.5.10
|
||||
peerDependencies:
|
||||
react: "*"
|
||||
react-native: "*"
|
||||
checksum: 69d0d485b6a4d81f2daaa98ca9029d0ed760470a5ae9c5cce2b81132224b5116ea9bd6829c6bc0d7b0203db4c43e876020005122c306f1a5734c4629c4bb7933
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-iphone-screen-helper@npm:^2.0.2":
|
||||
version: 2.1.2
|
||||
resolution: "react-native-iphone-screen-helper@npm:2.1.2"
|
||||
|
||||
Reference in New Issue
Block a user