mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 19:28:26 +00:00
Redesign NFC screen
This commit is contained in:
+1
-2
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
- <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>
|
||||
);
|
||||
@@ -1 +1,5 @@
|
||||
export const NfcIcon = `<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 120.39 122.88" style="enable-background:new 0 0 120.39 122.88" xml:space="preserve"><g><path d="M5.92,82.19C2.56,77.14,0.56,69.46,0.1,61.82c-0.46-7.6,0.61-15.42,3.37-20.81l2.8-5.47l4.79,3.84L56.59,75.9 c1.86-6.84,2.43-13.69,1.88-20.17c-0.75-8.76-4.56-18.25-9.11-24.46l8.62-5.03c5.64,7.7,9.58,18.07,10.49,28.66 c0.86,9.97-0.6,20.73-4.77,31.24l-2.5,6.3l-5.29-4.24L10.51,51.78c-0.47,2.9-0.6,6.16-0.4,9.45c0.36,6.02,1.8,11.87,4.17,15.43 L5.92,82.19L5.92,82.19z M92.57,116.75c11.23-17.56,17.82-39.92,17.81-57.3c-0.01-21.83-6.85-41.56-15.56-54.46L103.56,0 c9.52,14.26,16.82,35.8,16.82,59.45c0.01,19.12-7.47,44.1-19.84,63.43L92.57,116.75L92.57,116.75z M71.78,100.52 c8.01-11.5,12.79-27.12,12.71-41.63c-0.08-14.45-4.23-29.01-12.43-40.72l8.68-5.03C89.9,26.39,94.4,42.69,94.49,58.85 c0.09,16.53-5.59,34.62-14.78,47.83L71.78,100.52L71.78,100.52z"/></g></svg>`
|
||||
export const NfcIcon = `<svg class="w-4 h-4" viewBox="0 -1 15 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" d="M14.1019 11.6159C14.1019 7.48502 12.8129 3.63737 10.5179 0.453773C9.81482-0.532555 8.37928 0.32682 9.141 1.35221C11.2797 4.29166 12.4222 7.83659 12.4222 11.6159C12.4222 15.3952 11.2699 18.9303 9.141 21.8698C8.39881 22.8854 9.78553 23.7936 10.5179 22.778C12.8129 19.5846 14.1019 15.737 14.1019 11.6159Z" ></path>
|
||||
<path stroke-linecap="round" d="M8.77967 11.6159C8.77967 8.54948 7.81287 5.66862 6.07459 3.33463C5.29334 2.28971 3.93592 3.29557 4.66834 4.2526C6.26014 6.34245 7.10975 8.89127 7.10975 11.6159C7.10975 14.3405 6.26014 16.8893 4.66834 18.9792C3.93592 19.9362 5.29334 20.9421 6.07459 19.8874C7.81287 17.5534 8.77967 14.6823 8.77967 11.6159Z" ></path>
|
||||
<path stroke-linecap="round" d="M3.47693 11.6159C3.47693 9.60416 2.78357 7.72916 1.55311 6.26432C0.742558 5.30729-0.439082 6.39127 0.166386 7.17252C1.34803 8.6569 1.79725 9.89713 1.79725 11.6159C1.79725 13.3346 1.34803 14.5749 0.166386 16.0592C-0.429317 16.8307 0.752324 17.9049 1.55311 16.9577C2.78357 15.5026 3.47693 13.6276 3.47693 11.6159Z"></path>
|
||||
</svg>`
|
||||
+308
-172
@@ -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) => `<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg viewBox="-5 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16.3 19.5002C17.4 17.2002 18 14.7002 18 12.0002C18 9.30024 17.4 6.70024 16.3 4.50024M12.7 17.8003C13.5 16.0003 14 14.0003 14 12.0003C14 10.0003 13.5 7.90034 12.7 6.10034M9.1001 16.1001C9.7001 14.8001 10.0001 13.4001 10.0001 12.0001C10.0001 10.6001 9.7001 9.10015 9.1001 7.90015M5.5 14.3003C5.8 13.6003 6 12.8003 6 12.0003C6 11.2003 5.8 10.3003 5.5 9.60034"
|
||||
stroke="${String(color)}"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"/>
|
||||
</svg>`
|
||||
|
||||
|
||||
interface PulsingContactlessIconProps {
|
||||
isNfcEnabled: boolean;
|
||||
}
|
||||
|
||||
export const PulsingContactlessIcon: React.FC<PulsingContactlessIconProps> = ({
|
||||
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 (
|
||||
<Animated.View style={[animatedStyle, { alignSelf: 'center' }]}>
|
||||
<SvgXml
|
||||
xml={ContactlessIcon(color)}
|
||||
width={150}
|
||||
height={150}
|
||||
style={{ marginVertical: 24 }} // or your spacing.large
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = StaticScreenProps<{
|
||||
unit: MintUnit
|
||||
@@ -131,7 +68,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
// const [encodedInvoice, setEncodedInvoice] = useState<string>('')
|
||||
const [invoice, setInvoice] = useState<DecodedLightningInvoice | undefined>()
|
||||
const [invoiceExpiry, setInvoiceExpiry] = useState<Date | undefined>()
|
||||
const [meltQuote, setMeltQuote] = useState<MeltQuoteResponse | undefined>()
|
||||
const [meltQuote, setMeltQuote] = useState<MeltQuoteBolt11Response | undefined>()
|
||||
const [memo, setMemo] = useState('')
|
||||
const [mintBalanceToSendFrom, setMintBalanceToSendFrom] = useState<MintBalance | undefined>()
|
||||
|
||||
@@ -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<string | undefined>()
|
||||
const [readNfcData, setReadNfcData] = useState<string | undefined>()
|
||||
const [mint, setMint] = useState<Mint | undefined>()
|
||||
//const [readNfcData, setReadNfcData] = useState<string | undefined>()
|
||||
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
const [info, setInfo] = useState<string>('')
|
||||
@@ -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 (
|
||||
<Screen preset="fixed" contentContainerStyle={$screen}>
|
||||
<MintHeader mint={mint} unit={unitRef.current} />
|
||||
<Animated.View style={[animatedHeader, $headerContainer, {backgroundColor: headerBg}]}>
|
||||
<View style={{}}>
|
||||
<MintHeader
|
||||
mint={undefined}
|
||||
unit={unitRef.current}
|
||||
onBackPress={gotoWallet}
|
||||
textColor={nfcText as string}
|
||||
backgroundColor={headerBg as string}
|
||||
leftIconColor={nfcText as string}
|
||||
/>
|
||||
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
|
||||
{!isOnline.current && !isPaid && (
|
||||
<>
|
||||
<Text
|
||||
tx="commonOffline"
|
||||
style={$warning}
|
||||
size="xxs"
|
||||
/>
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{isPaid && (
|
||||
{isPaid ? (
|
||||
<AmountInput
|
||||
//ref={amountInputRef}
|
||||
value={`${amountToPay || amountToReceive}`}
|
||||
@@ -970,9 +923,41 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
unit={unitRef.current}
|
||||
editable={false}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
alignSelf: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: moderateScale(80),
|
||||
height: moderateScale(80),
|
||||
backgroundColor:isProcessing ? indicatorProcessing : isNfcEnabled ? indicatorStandby : hintText,
|
||||
borderRadius: moderateScale(80) / 2,
|
||||
marginVertical: spacing.medium,
|
||||
}}
|
||||
>
|
||||
<PulsingContactlessIcon
|
||||
isNfcEnabled={isNfcEnabled}
|
||||
size={moderateScale(40)}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
text={nfcInfo}
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
marginBottom: spacing.medium,
|
||||
paddingHorizontal: spacing.medium,
|
||||
//fontFamily: typography.code,
|
||||
color: isProcessing ? indicatorProcessing : isNfcEnabled ? indicatorStandby : hintText,
|
||||
}}
|
||||
preset='heading'
|
||||
size='md'
|
||||
/>
|
||||
</>
|
||||
|
||||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
<View style={$contentContainer}>
|
||||
{isPaid && transaction ? (
|
||||
<Card
|
||||
@@ -1007,63 +992,98 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Card
|
||||
HeadingComponent={<Icon icon='faNfcSymbol' size={spacing.medium} containerStyle={{alignSelf: 'center'}}/>}
|
||||
ContentComponent={
|
||||
<>
|
||||
{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 : ''
|
||||
}
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
marginBottom: spacing.medium,
|
||||
//fontFamily: typography.code,
|
||||
color: isProcessing ? indicatorProcessing : isNfcEnabled ? nfcText : hintText,
|
||||
fontSize: 18,
|
||||
|
||||
}}
|
||||
/>
|
||||
}
|
||||
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 (
|
||||
<View style={[
|
||||
$mintCardOuter,
|
||||
{
|
||||
backgroundColor: metalBase,
|
||||
borderTopColor: metalHighlight,
|
||||
borderLeftColor: metalHighlight,
|
||||
borderBottomColor: metalShadow,
|
||||
borderRightColor: metalShadow,
|
||||
}
|
||||
]}>
|
||||
{/* Metallic shine overlay */}
|
||||
<View
|
||||
style={[
|
||||
$metalShine,
|
||||
{
|
||||
backgroundColor: metalShineTop,
|
||||
borderColor: metalShineBottom,
|
||||
}
|
||||
]}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<Card
|
||||
ContentComponent={
|
||||
<View style={$cardContent}>
|
||||
<View style={$cardTopRow}>
|
||||
{mint?.mintInfo?.icon_url ? (
|
||||
<FastImage
|
||||
style={{
|
||||
width: moderateScale(28),
|
||||
height: moderateScale(28),
|
||||
borderRadius: moderateScale(14),
|
||||
}}
|
||||
source={{ uri: mint.mintInfo.icon_url }}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
) : (
|
||||
<View style={[$mintIconFallback, { backgroundColor: mint?.color || colors.palette.primary300 }]} />
|
||||
)}
|
||||
<Text
|
||||
text={mint?.shortname || 'Mint'}
|
||||
style={{ color: nfcText, marginLeft: spacing.small }}
|
||||
size="xs"
|
||||
/>
|
||||
</View>
|
||||
<View style={$cardBalanceRow}>
|
||||
<Text
|
||||
text={formattedBalance}
|
||||
style={{
|
||||
fontFamily: typography.code?.normal,
|
||||
color: nfcText,
|
||||
fontSize: moderateScale(28),
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
text={currency.code}
|
||||
style={{
|
||||
color: hintText,
|
||||
marginLeft: spacing.extraSmall,
|
||||
alignSelf: 'flex-end',
|
||||
marginBottom: spacing.tiny,
|
||||
}}
|
||||
size="xs"
|
||||
/>
|
||||
</View>
|
||||
<View style={$cardBottomRow}>
|
||||
<Text
|
||||
text={mint?.hostname || mintBalanceToSendFrom.mintUrl}
|
||||
style={{ color: hintText }}
|
||||
size="xxs"
|
||||
numberOfLines={1}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
style={$mintCard}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
})()}
|
||||
</>
|
||||
)}
|
||||
<View style={$bottomContainer}>
|
||||
<View style={$buttonContainer}>
|
||||
{!isProcessing && (<Button
|
||||
{!isProcessing && !isPaid && (<Button
|
||||
preset='tertiary'
|
||||
LeftAccessory={() => (
|
||||
<SvgXml
|
||||
@@ -1077,7 +1097,21 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
onPress={gotoScan}
|
||||
//style={{backgroundColor: scanButtonColor}}
|
||||
text='Scan QR instead'
|
||||
/>)}
|
||||
/>)}
|
||||
{isPaid && (<Button
|
||||
preset='tertiary'
|
||||
LeftAccessory={() => (
|
||||
<Icon
|
||||
icon="faXmark"
|
||||
size={spacing.medium}
|
||||
color={scanIcon as string}
|
||||
style={{marginHorizontal: spacing.extraSmall}}
|
||||
/>
|
||||
)}
|
||||
onPress={gotoWallet}
|
||||
//style={{backgroundColor: scanButtonColor}}
|
||||
text='Close'
|
||||
/>)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -1172,11 +1206,59 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
|
||||
)
|
||||
})
|
||||
|
||||
interface PulsingContactlessIconProps {
|
||||
isNfcEnabled: boolean;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const $screen: ViewStyle = { flex: 1 }
|
||||
const $contentContainer: ViewStyle = { flex: 1, marginTop: -spacing.extraLarge * 2, padding: spacing.extraSmall }
|
||||
const $headerContainer: TextStyle = { alignItems: 'center', paddingBottom: spacing.medium, height: spacing.screenHeight * 0.15 }
|
||||
const $buttonContainer: ViewStyle = { marginTop: spacing.large, flexDirection: 'row', alignSelf: 'center' }
|
||||
export const PulsingContactlessIcon: React.FC<PulsingContactlessIconProps> = ({
|
||||
isNfcEnabled,
|
||||
size,
|
||||
}) => {
|
||||
const scale = useSharedValue(1);
|
||||
|
||||
// Start/stop pulse based on nfcBroadcast
|
||||
useEffect(() => {
|
||||
if (isNfcEnabled) {
|
||||
scale.value = withRepeat(
|
||||
withTiming(1.15, {
|
||||
duration: 1000,
|
||||
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.success300
|
||||
: colors.light.text
|
||||
|
||||
return (
|
||||
<Animated.View style={[animatedStyle, { alignSelf: 'center' }]}>
|
||||
<SvgXml
|
||||
width={size || spacing.medium}
|
||||
height={size || spacing.medium}
|
||||
xml={NfcIcon}
|
||||
stroke={'white'}
|
||||
fill={'white'}
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const $screen: ViewStyle = { }
|
||||
const $contentContainer: ViewStyle = { flex: 1, padding: spacing.extraSmall }
|
||||
const $headerContainer: TextStyle = { alignItems: 'center', paddingVertical: spacing.medium,}
|
||||
const $buttonContainer: ViewStyle = { flex: 1, marginTop: spacing.large, flexDirection: 'row', alignSelf: 'center' }
|
||||
const $bottomContainer: ViewStyle = {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
@@ -1188,8 +1270,62 @@ const $bottomContainer: ViewStyle = {
|
||||
alignSelf: 'stretch',
|
||||
}
|
||||
const $warning: TextStyle = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
backgroundColor: colors.palette.orange400,
|
||||
borderRadius: spacing.extraSmall,
|
||||
paddingHorizontal: spacing.tiny,
|
||||
marginTop: spacing.medium
|
||||
alignSelf: 'center',
|
||||
zIndex: 1,
|
||||
marginVertical: spacing.small,
|
||||
}
|
||||
const $mintCardOuter: ViewStyle = {
|
||||
marginHorizontal: spacing.small,
|
||||
borderRadius: spacing.medium + 2,
|
||||
borderWidth: 1.5,
|
||||
shadowColor: colors.palette.neutral900,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 8,
|
||||
elevation: 6,
|
||||
overflow: 'hidden',
|
||||
}
|
||||
const $metalShine: ViewStyle = {
|
||||
position: 'absolute',
|
||||
top: -50,
|
||||
left: -50,
|
||||
width: '150%',
|
||||
height: '70%',
|
||||
borderBottomWidth: 1,
|
||||
transform: [{ rotate: '-15deg' }],
|
||||
}
|
||||
const $mintCard: ViewStyle = {
|
||||
height: spacing.screenHeight * 0.25,
|
||||
borderRadius: spacing.medium,
|
||||
padding: spacing.medium,
|
||||
backgroundColor: 'transparent',
|
||||
shadowOpacity: 0,
|
||||
elevation: 0,
|
||||
}
|
||||
const $cardContent: ViewStyle = {
|
||||
marginVertical: spacing.small,
|
||||
flex: 1,
|
||||
justifyContent: 'space-between',
|
||||
}
|
||||
const $cardTopRow: ViewStyle = {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}
|
||||
const $mintIconFallback: ViewStyle = {
|
||||
width: moderateScale(28),
|
||||
height: moderateScale(28),
|
||||
borderRadius: moderateScale(14),
|
||||
}
|
||||
const $cardBalanceRow: ViewStyle = {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
marginTop: spacing.medium,
|
||||
}
|
||||
const $cardBottomRow: ViewStyle = {
|
||||
marginTop: spacing.small,
|
||||
}
|
||||
@@ -13,6 +13,9 @@ import { translate, TxKeyPath } from "../../i18n"
|
||||
import { log } from "../../services"
|
||||
import { Token, getDecodedToken, getEncodedToken } from '@cashu/cashu-ts'
|
||||
import { NfcService } from '../../services/nfcService';
|
||||
import { SvgXml } from 'react-native-svg';
|
||||
import { NfcIcon } from '../../components/NfcIcon';
|
||||
import Animated, { Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated';
|
||||
|
||||
|
||||
export type QRCodeBlockTypes = 'EncodedV3Token' | 'EncodedV4Token' | 'Bolt11Invoice' | 'URL' | 'NWC' | 'PUBKEY' | 'PaymentRequest'
|
||||
@@ -203,44 +206,63 @@ export const QRCodeBlock = function (props: {
|
||||
|
||||
// NFC Functions
|
||||
const startNFCSimulation = async () => {
|
||||
const dataToBroadcast = encodedV3Token || qrCodeData
|
||||
const tag = new NFCTagType4({
|
||||
type: NFCTagType4NDEFContentType.Text,
|
||||
content: dataToBroadcast,
|
||||
writable: false,
|
||||
})
|
||||
const session = await HCESession.getInstance()
|
||||
session.setApplication(tag)
|
||||
await session.setEnabled(true)
|
||||
log.trace('[startNFCSimulation] Session enabled')
|
||||
simulationRef.current = session
|
||||
const dataToBroadcast = encodedV3Token || qrCodeData
|
||||
log.trace('[startNFCSimulation] Starting with data length:', dataToBroadcast.length)
|
||||
|
||||
const tag = new NFCTagType4({
|
||||
type: NFCTagType4NDEFContentType.Text,
|
||||
content: dataToBroadcast,
|
||||
writable: false,
|
||||
})
|
||||
|
||||
const session = await HCESession.getInstance()
|
||||
if (!session) {
|
||||
throw new Error('Failed to get HCE session instance')
|
||||
}
|
||||
|
||||
session.setApplication(tag)
|
||||
await session.setEnabled(true)
|
||||
simulationRef.current = session
|
||||
log.trace('[startNFCSimulation] Session enabled')
|
||||
}
|
||||
|
||||
const stopNFCSimulation = async () => {
|
||||
if (simulationRef.current) {
|
||||
try {
|
||||
await simulationRef.current.setEnabled(false)
|
||||
log.trace('[stopNFCSimulation] Session closed')
|
||||
} catch (error) {
|
||||
console.warn('Failed to terminate NFC session:', error)
|
||||
}
|
||||
if (!simulationRef.current) {
|
||||
log.trace('[stopNFCSimulation] No active session to stop')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await simulationRef.current.setEnabled(false)
|
||||
log.trace('[stopNFCSimulation] Session closed')
|
||||
} catch (e: unknown) {
|
||||
const message = e instanceof Error ? e.message : 'Unknown error'
|
||||
log.warn('[stopNFCSimulation] Failed to terminate session:', message)
|
||||
} finally {
|
||||
simulationRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
const toggleNFC = async () => {
|
||||
if (nfcBroadcast) {
|
||||
await stopNFCSimulation()
|
||||
setNfcBroadcast(prev => !prev)
|
||||
try {
|
||||
await stopNFCSimulation()
|
||||
} catch (e: unknown) {
|
||||
const message = e instanceof Error ? e.message : 'Failed to stop NFC'
|
||||
log.error('[toggleNFC] Stop failed:', message)
|
||||
} finally {
|
||||
setNfcBroadcast(false)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await startNFCSimulation()
|
||||
setNfcBroadcast(prev => !prev)
|
||||
} catch(e: any) {
|
||||
log.error('NFC simulation failed to start: ', e.message, {stack: e.stack})
|
||||
infoMessage(e.message)
|
||||
//await startNFCSimulation()
|
||||
setNfcBroadcast(true)
|
||||
} catch (e: unknown) {
|
||||
const message = e instanceof Error ? e.message : 'Failed to start NFC'
|
||||
log.error('[toggleNFC] Start failed:', message, e instanceof Error ? { stack: e.stack } : {})
|
||||
infoMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const qrCodeSize = size || spacing.screenWidth - spacing.large * 2
|
||||
@@ -308,17 +330,6 @@ export const QRCodeBlock = function (props: {
|
||||
pressedStyle={{backgroundColor: colors.light.buttonTertiaryPressed}}
|
||||
style={{ minHeight: verticalScale(40), paddingVertical: verticalScale(spacing.tiny) }}
|
||||
/>
|
||||
{/*type === 'EncodedV4Token' && keysetFormat === 'hex' && !isAnimating && (
|
||||
<Button
|
||||
preset="tertiary"
|
||||
tx={encodedV3Token ? "qrCodeNewFormatButton" : "qrCodeOldFormatButton"}
|
||||
onPress={switchTokenEncoding}
|
||||
LeftAccessory={() => <Icon icon='faMoneyBill1' size={spacing.small} color={colors.light.text}/>}
|
||||
textStyle={{color: colors.light.text, fontSize: 14}}
|
||||
pressedStyle={{backgroundColor: colors.light.buttonTertiaryPressed}}
|
||||
style={{ minHeight: verticalScale(40), paddingVertical: verticalScale(spacing.tiny) }}
|
||||
/>
|
||||
)*/}
|
||||
{type === 'EncodedV4Token' && !isQRCodeError && (
|
||||
<Button
|
||||
preset="tertiary"
|
||||
@@ -334,20 +345,12 @@ export const QRCodeBlock = function (props: {
|
||||
{Platform.OS === 'android' && isStringSafeForNfc && isNfcSupported && (
|
||||
<Button
|
||||
preset="tertiary"
|
||||
//text={nfcBroadcast ? "NFC active" : "NFC"}
|
||||
onPress={isNfcEnabled ? toggleNFC : () => Alert.alert('Enable NFC in device settings')}
|
||||
LeftAccessory={() => (
|
||||
<Icon
|
||||
icon='faNfcSymbol'
|
||||
size={spacing.medium}
|
||||
color={nfcBroadcast ? colors.palette.success300 : colors.light.text}
|
||||
<PulsingContactlessIcon
|
||||
nfcBroadcast={nfcBroadcast}
|
||||
/>
|
||||
)}
|
||||
textStyle={{
|
||||
color: nfcBroadcast ? colors.palette.success300 : colors.light.text,
|
||||
fontSize: 14,
|
||||
fontWeight: nfcBroadcast ? 'bold' : 'normal'
|
||||
}}
|
||||
pressedStyle={{ backgroundColor: colors.light.buttonTertiaryPressed }}
|
||||
style={{
|
||||
minHeight: verticalScale(40),
|
||||
@@ -361,6 +364,52 @@ export const QRCodeBlock = function (props: {
|
||||
)
|
||||
}
|
||||
|
||||
interface PulsingContactlessIconProps {
|
||||
nfcBroadcast: boolean;
|
||||
}
|
||||
|
||||
export const PulsingContactlessIcon: React.FC<PulsingContactlessIconProps> = ({
|
||||
nfcBroadcast,
|
||||
}) => {
|
||||
const scale = useSharedValue(1);
|
||||
|
||||
// Start/stop pulse based on nfcBroadcast
|
||||
useEffect(() => {
|
||||
if (nfcBroadcast) {
|
||||
scale.value = withRepeat(
|
||||
withTiming(1.25, {
|
||||
duration: 1000,
|
||||
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 });
|
||||
}
|
||||
}, [nfcBroadcast, scale]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: scale.value }],
|
||||
}));
|
||||
|
||||
const color = nfcBroadcast
|
||||
? colors.palette.success300
|
||||
: colors.light.text
|
||||
|
||||
return (
|
||||
<Animated.View style={[animatedStyle, { alignSelf: 'center' }]}>
|
||||
<SvgXml
|
||||
width={spacing.medium}
|
||||
height={spacing.medium}
|
||||
xml={NfcIcon}
|
||||
stroke={color}
|
||||
fill={color}
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
const $qrCodeContainer: ViewStyle = {
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'white',
|
||||
|
||||
@@ -55,6 +55,7 @@ import { LeftProfileHeader } from './ContactsScreen'
|
||||
import { getUnixTime } from 'date-fns/getUnixTime'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import { HotUpdater, getUpdateSource } from '@hot-updater/react-native'
|
||||
import { NfcIcon } from '../components/NfcIcon'
|
||||
|
||||
const MINT_CHECK_INTERVAL = 60
|
||||
|
||||
@@ -700,7 +701,7 @@ export const WalletScreen = observer(function WalletScreen({ route }: Props) {
|
||||
/>}
|
||||
TitleActionComponent={<HeaderTitle />}
|
||||
RightActionComponent={
|
||||
<View style={{justifyContent: 'flex-end', flexDirection: 'row'}}>
|
||||
<View style={{justifyContent: 'flex-end', flexDirection: 'row', alignItems: 'center'}}>
|
||||
{pendingCount > 0 && (
|
||||
<Pressable
|
||||
style={{flexDirection: 'row', alignItems:'center', marginRight: spacing.small}}
|
||||
@@ -714,7 +715,13 @@ export const WalletScreen = observer(function WalletScreen({ route }: Props) {
|
||||
style={{marginRight: spacing.medium}}
|
||||
onPress={gotoNfcPay}
|
||||
>
|
||||
<Icon icon='faNfcSymbol' color={headerTitleColor}/>
|
||||
<SvgXml
|
||||
width={spacing.medium}
|
||||
height={spacing.medium}
|
||||
xml={NfcIcon}
|
||||
stroke={headerTitleColor}
|
||||
fill={headerTitleColor}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
}
|
||||
|
||||
@@ -14503,7 +14503,6 @@ __metadata:
|
||||
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
|
||||
@@ -15948,7 +15947,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prop-types@npm:^15.5.10, prop-types@npm:^15.7.2, prop-types@npm:^15.8.0, prop-types@npm:^15.8.1":
|
||||
"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:
|
||||
@@ -16407,18 +16406,6 @@ __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