From c938d1744628a1c3fb87563517e36a5bb24bf8a9 Mon Sep 17 00:00:00 2001 From: minibits-cash Date: Mon, 2 Feb 2026 16:20:09 +0100 Subject: [PATCH] Redesign NFC screen --- package.json | 3 +- patches/react-native-indicators+0.17.0.patch | 86 ---- src/components/NfcIcon.tsx | 6 +- src/screens/NfcPayScreen.tsx | 480 ++++++++++++------- src/screens/Wallet/QRCode.tsx | 143 ++++-- src/screens/WalletScreen.tsx | 11 +- yarn.lock | 15 +- 7 files changed, 420 insertions(+), 324 deletions(-) delete mode 100644 patches/react-native-indicators+0.17.0.patch diff --git a/package.json b/package.json index fd74be92..7b99ebb5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minibits_wallet", - "version": "0.3.4-beta.22", + "version": "0.3.4-beta.23", "private": true, "scripts": { "android:clean": "cd android && ./gradlew clean", @@ -64,7 +64,6 @@ "react-native-flash-message": "^0.4.2", "react-native-gesture-handler": "^2.28.0", "react-native-hce": "^0.3.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", diff --git a/patches/react-native-indicators+0.17.0.patch b/patches/react-native-indicators+0.17.0.patch deleted file mode 100644 index 663cb380..00000000 --- a/patches/react-native-indicators+0.17.0.patch +++ /dev/null @@ -1,86 +0,0 @@ -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 ( -- -+ - {Array.from(new Array(count), this.renderComponent, this)} - - ); -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 ( -- -+ - - - -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 ( -- -+ - - - ); -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 ( -- -+ - - - ); -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 ( -- -+ - - - ); -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 ( -- -+ - - - ); \ No newline at end of file diff --git a/src/components/NfcIcon.tsx b/src/components/NfcIcon.tsx index 320e8ba6..3b293ac8 100644 --- a/src/components/NfcIcon.tsx +++ b/src/components/NfcIcon.tsx @@ -1 +1,5 @@ -export const NfcIcon = `` \ No newline at end of file +export const NfcIcon = ` + + + +` \ No newline at end of file diff --git a/src/screens/NfcPayScreen.tsx b/src/screens/NfcPayScreen.tsx index 95a07688..d1c26db9 100644 --- a/src/screens/NfcPayScreen.tsx +++ b/src/screens/NfcPayScreen.tsx @@ -1,35 +1,27 @@ -import React, { FC, useState, useEffect, useRef, useCallback } from 'react' +import React, { useState, useEffect, useRef, useCallback } from 'react' import { ViewStyle, View, TextStyle, - Alert, - ColorValue, - Platform, } from 'react-native' -import { - PulseIndicator, - WaveIndicator, - } from 'react-native-indicators' -import { PaymentRequest as CashuPaymentRequest, MeltQuoteResponse, PaymentRequestTransportType, decodePaymentRequest, getDecodedToken } from '@cashu/cashu-ts' -import NfcManager, { NfcTech, Ndef, NfcEvents, TagEvent } from 'react-native-nfc-manager' +import { PaymentRequest as CashuPaymentRequest, MeltQuoteBolt11Response, MeltQuoteResponse, PaymentRequestTransportType, decodePaymentRequest, getDecodedToken } from '@cashu/cashu-ts' +import NfcManager, { Ndef, NfcEvents } from 'react-native-nfc-manager' import { colors, spacing, typography, useThemeColor } from '../theme' import EventEmitter from '../utils/eventEmitter' import { log } from '../services/logService' import { IncomingDataType, IncomingParser } from '../services/incomingParser' import AppError, { Err } from '../utils/AppError' 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' import { useStores } from '../models' import { MintHeader } from './Mints/MintHeader' -import { verticalScale } from '@gocodingnow/rn-size-matters' +import { moderateScale } from '@gocodingnow/rn-size-matters' import useIsInternetReachable from '../utils/useIsInternetReachable' import { translate } from '../i18n' import { StackActions, StaticScreenProps, useNavigation } from '@react-navigation/native' import { observer } from 'mobx-react-lite' -import { Mint, MintBalance } from '../models/Mint' +import { MintBalance } from '../models/Mint' import { MinibitsClient, NostrClient, SYNC_STATE_WITH_MINT_TASK, SyncStateTaskResult, TransactionTaskResult, WalletTask } from '../services' import { Transaction, TransactionStatus } from '../models/Transaction' import { Proof } from '../models/Proof' @@ -42,72 +34,17 @@ import { LIGHTNING_FEE_PERCENT, MIN_LIGHTNING_FEE } from '../models/NwcStore' import { ResultModalInfo } from './Wallet/ResultModalInfo' import Animated, { useSharedValue, - withRepeat, withTiming, useAnimatedStyle, + withRepeat, Easing, } from 'react-native-reanimated'; import { NfcService } from '../services/nfcService' import { TranItem } from './TranDetailScreen' import { ProfilePointer } from 'nostr-tools/nip19' -import { Contact } from '../models/Contact' -//import Animated from 'react-native-reanimated' +import { NfcIcon } from '../components/NfcIcon' +import FastImage from 'react-native-fast-image' -const ContactlessIcon = (color: ColorValue | string) => ` - - -` - - -interface PulsingContactlessIconProps { - isNfcEnabled: boolean; -} - -export const PulsingContactlessIcon: React.FC = ({ - isNfcEnabled, -}) => { - const scale = useSharedValue(1); - - // Start/stop pulse based on isNfcEnabled - useEffect(() => { - if (isNfcEnabled) { - scale.value = withRepeat( - withTiming(1.22, { - duration: 1200, - easing: Easing.out(Easing.quad), - }), - -1, // infinite - true // reverse (so it goes 1 → 1.22 → 1 → 1.22...) - ); - } else { - scale.value = withTiming(1, { duration: 300 }); - } - }, [isNfcEnabled, scale]); - - const animatedStyle = useAnimatedStyle(() => ({ - transform: [{ scale: scale.value }], - })); - - const color = isNfcEnabled - ? colors.palette.success200 - : colors.palette.neutral400 - - return ( - - - - ); -}; type Props = StaticScreenProps<{ unit: MintUnit @@ -131,7 +68,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { // const [encodedInvoice, setEncodedInvoice] = useState('') const [invoice, setInvoice] = useState() const [invoiceExpiry, setInvoiceExpiry] = useState() - const [meltQuote, setMeltQuote] = useState() + const [meltQuote, setMeltQuote] = useState() const [memo, setMemo] = useState('') const [mintBalanceToSendFrom, setMintBalanceToSendFrom] = useState() @@ -144,14 +81,12 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { const [isResultModalVisible, setIsResultModalVisible] = useState(false) // NFC state - const [isNfcEnabled, setIsNfcEnabled] = useState(false) - const [isNfcSupported, setIsNfcSupported] = useState(false) + const [isNfcEnabled, setIsNfcEnabled] = useState(false) const [isProcessing, setIsProcessing] = useState(false) const [isPaid, setIsPaid] = useState(false) const [isError, setIsError] = useState(false) const [nfcInfo, setNfcInfo] = useState() - const [readNfcData, setReadNfcData] = useState() - const [mint, setMint] = useState() + //const [readNfcData, setReadNfcData] = useState() const [error, setError] = useState() const [info, setInfo] = useState('') @@ -161,9 +96,22 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { const initNfc = async () => { try { const supported = await NfcService.init() // runs NfcManager.start() - setIsNfcSupported(supported) log.trace({isInternetReachable}) + const balance = proofsStore.getMintBalanceWithMaxBalance(unitRef.current) + + if (balance) { + setMintBalanceToSendFrom(balance) + } + + /*setNfcInfo('Hold your device close to the NFC reader.') + setIsNfcEnabled(true) + return + + const tx = transactionsStore.findById(1) + setTransaction(tx || undefined) + setAmountToPay(formatDisplayAmount(tx?.amount || 0, unitRef.current))*/ + if (!supported) { setNfcInfo('NFC is not supported on this device') return @@ -405,7 +353,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { log.info('NFC tag decoded', { decoded }) - setReadNfcData(decoded) + //setReadNfcData(decoded) await handleIncomingData(decoded) } catch (e: any) { @@ -493,6 +441,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { }) } + // re-set selected mint balance if default is not accepted by this PR setMintBalanceToSendFrom(selectedBalance) setAmountToPay(formatDisplayAmount(requiredAmount, unit)) @@ -564,8 +513,7 @@ 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 (!isOnline.current) { setResultModalInfo({ @@ -650,7 +598,6 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { setNfcInfo('Ecash token sent successfully.') setIsPaid(true) - expandHeader() } @@ -664,6 +611,10 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { }) } + if(!mintBalanceToSendFrom) { + throw new AppError(Err.NOTFOUND_ERROR, 'No mint balance selected for payment') + } + log.trace('[handleLightningInvoice] decoding invoice') const decoded = LightningUtils.decodeInvoice(encodedInvoice) @@ -676,9 +627,11 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { const expiresAt = addSeconds(new Date((timestamp as number) * 1000), expiry as number) const feeReserve = Math.max(MIN_LIGHTNING_FEE, Math.round(invoiceAmount * LIGHTNING_FEE_PERCENT / 100)) const totalRequired = invoiceAmount + feeReserve - const balances = proofsStore.getMintBalancesWithEnoughBalance(totalRequired, unitRef.current) + // const balances = proofsStore.getMintBalancesWithEnoughBalance(totalRequired, unitRef.current) + + const availableBalance = mintBalanceToSendFrom.balances[unitRef.current] || 0 - if (balances.length === 0) { + if (availableBalance < totalRequired) { throw new AppError(Err.NOTFOUND_ERROR, 'Not enough funds', { message: translate('transferScreen_insufficientFunds', { currency: getCurrency(unitRef.current).code, @@ -687,15 +640,15 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { }) } - const selectedBalance = balances[0] - setMintBalanceToSendFrom(selectedBalance) + // const selectedBalance = balances[0] + // setMintBalanceToSendFrom(selectedBalance) let msgQuote = 'Creating Lightning payment quote...' log.trace('[handleLightningInvoice]', msgQuote) setNfcInfo(msgQuote) const quote = await walletStore.createLightningMeltQuote( - selectedBalance.mintUrl, + mintBalanceToSendFrom.mintUrl, unitRef.current, encodedInvoice ) @@ -711,7 +664,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { setNfcInfo('Paying Lightning invoice...') const result = await WalletTask.transferQueueAwaitable( - selectedBalance, + mintBalanceToSendFrom, quote.amount, unitRef.current, quote, @@ -754,7 +707,6 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { title: status === TransactionStatus.COMPLETED ? 'Payment sent!' : undefined, }); setIsPaid(true) - expandHeader() setNfcInfo(transaction.memo || 'Lightning payment settled.') } } else { @@ -807,7 +759,6 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { title: status === TransactionStatus.COMPLETED ? 'Ecash received!' : undefined, }); setIsPaid(true) - expandHeader() setNfcInfo(transaction.memo || 'Receive over NFC completed.') setAmountToReceive(formatDisplayAmount(transaction.amount, transaction.unit)) } @@ -925,44 +876,46 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { 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 headerBg = useThemeColor('background') const headerTitle = useThemeColor('headerTitle') const scanIcon = useThemeColor('text') const nfcText = useThemeColor('text') const indicatorStandby = colors.palette.primary400 - const indicatorProcessing = useThemeColor('button') + const indicatorProcessing = useThemeColor('button') + + // Metallic card colors - subtle gradient effect via borders + const isLightTheme = headerBg === colors.light.background + const metalHighlight = isLightTheme ? 'rgba(255, 255, 255, 0.9)' : 'rgba(255, 255, 255, 0.12)' + const metalShadow = isLightTheme ? 'rgba(0, 0, 0, 0.15)' : 'rgba(0, 0, 0, 0.5)' + const metalBase = isLightTheme ? colors.palette.neutral200 : colors.palette.neutral700 + const metalShineTop = isLightTheme ? 'rgba(255, 255, 255, 0.6)' : 'rgba(255, 255, 255, 0.08)' + const metalShineBottom = isLightTheme ? 'rgba(255, 255, 255, 0)' : 'rgba(255, 255, 255, 0)' return ( - - - + + {!isOnline.current && !isPaid && ( + <> + /> + )} - {isPaid && ( + {isPaid ? ( + ) : ( + <> + + + + + + )} - - + {isPaid && transaction ? ( ) : ( - } - ContentComponent={ - <> - {isProcessing ? ( - - ) : isNfcEnabled ? ( - - - - ) : ( - - )} - - - } - FooterComponent={ - - } - style={{height: spacing.screenHeight * 0.3}} - /> - )} - + <> + {mintBalanceToSendFrom && (() => { + const mint = mintsStore.findByUrl(mintBalanceToSendFrom.mintUrl) + const balance = mintBalanceToSendFrom.balances[unitRef.current] || 0 + const currency = getCurrency(unitRef.current) + const formattedBalance = formatCurrency(balance, currency.code) + return ( + + {/* Metallic shine overlay */} + + + + {mint?.mintInfo?.icon_url ? ( + + ) : ( + + )} + + + + + + + + + + + } + style={$mintCard} + /> + + ) + })()} + + )} - {!isProcessing && (