diff --git a/package.json b/package.json
index 8fac2404..c82ff083 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "minibits_wallet",
- "version": "0.3.4-beta.19",
+ "version": "0.3.4-beta.20",
"private": true,
"scripts": {
"android:clean": "cd android && ./gradlew clean",
diff --git a/src/components/AnimatedHeader.tsx b/src/components/AnimatedHeader.tsx
index 87fc405a..bd5eea90 100644
--- a/src/components/AnimatedHeader.tsx
+++ b/src/components/AnimatedHeader.tsx
@@ -8,6 +8,7 @@ import Animated, {
} from 'react-native-reanimated'
import { spacing, typography, useThemeColor } from '../theme'
import { translate, TxKeyPath } from '../i18n'
+import { verticalScale } from '@gocodingnow/rn-size-matters'
export interface AnimatedHeaderProps {
/**
@@ -107,6 +108,7 @@ const $headerContainer: ViewStyle = {
}
const $headerTitle: TextStyle = {
- fontSize: spacing.extraLarge,
+ fontSize: verticalScale(32),
+ lineHeight: verticalScale(44),
fontFamily: typography.primary?.medium,
}
diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx
index 3eaacf06..1fec4ef7 100644
--- a/src/components/Icon.tsx
+++ b/src/components/Icon.tsx
@@ -41,6 +41,7 @@ import { faXmark } from '@fortawesome/free-solid-svg-icons/faXmark'
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons/faInfoCircle'
import { faBug } from '@fortawesome/free-solid-svg-icons/faBug'
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons/faCheckCircle'
+import { faCheck } from '@fortawesome/free-solid-svg-icons/faCheck'
import { faArrowTurnUp } from '@fortawesome/free-solid-svg-icons/faArrowTurnUp'
import { faArrowTurnDown } from '@fortawesome/free-solid-svg-icons/faArrowTurnDown'
import { faPencil } from '@fortawesome/free-solid-svg-icons/faPencil'
@@ -108,7 +109,7 @@ export type IconTypes = keyof typeof iconRegistry
// TODO remove need for manual iconregistry?
// would be best to just import all of them, i guess, or figure out something smart
-export const iconRegistry = { faWifi, faNfcSymbol, faAddressCard, faAddressBook, faWallet, faQrcode, faClipboard, faSliders, faCoins, faEllipsisVertical, faEllipsis, faArrowUp, faArrowDown, faArrowLeft, faXmark, faInfoCircle, faBug, faCheckCircle, faArrowTurnUp, faArrowTurnDown, faPencil, faTags, faShareFromSquare, faRotate, faCode, faBan, faCircle, faPaperPlane, faBolt, faArrowUpFromBracket, faArrowRightToBracket, faPlus, faShieldHalved, faCloudArrowUp, faPaintbrush, faCopy, faBurst, faUserShield, faLock, faLockOpen, faTriangleExclamation, faDownload, faUpload, faRecycle, faListUl, faExpand, faFingerprint, faWandMagicSparkles, faCircleUser, faComment, faKey, faCircleNodes, faBullseye, faEyeSlash, faUpRightFromSquare, faShareNodes, faPaste, faKeyboard, faMoneyBill1, faGears, faTag, faBank, faChevronDown, faChevronUp, faCircleExclamation, faCircleQuestion, faEnvelope, faTwitter, faTelegramPlane, faDiscord, faGithub, faReddit, faCircleArrowUp, faCircleArrowDown, faGlobe, faCubes, faClock, faArrowRotateLeft, faHeartPulse, faSeedling, faChevronLeft, faArrowRightArrowLeft }
+export const iconRegistry = { faWifi, faNfcSymbol, faAddressCard, faAddressBook, faWallet, faQrcode, faClipboard, faSliders, faCoins, faEllipsisVertical, faEllipsis, faArrowUp, faArrowDown, faArrowLeft, faXmark, faInfoCircle, faBug, faCheckCircle, faCheck, faArrowTurnUp, faArrowTurnDown, faPencil, faTags, faShareFromSquare, faRotate, faCode, faBan, faCircle, faPaperPlane, faBolt, faArrowUpFromBracket, faArrowRightToBracket, faPlus, faShieldHalved, faCloudArrowUp, faPaintbrush, faCopy, faBurst, faUserShield, faLock, faLockOpen, faTriangleExclamation, faDownload, faUpload, faRecycle, faListUl, faExpand, faFingerprint, faWandMagicSparkles, faCircleUser, faComment, faKey, faCircleNodes, faBullseye, faEyeSlash, faUpRightFromSquare, faShareNodes, faPaste, faKeyboard, faMoneyBill1, faGears, faTag, faBank, faChevronDown, faChevronUp, faCircleExclamation, faCircleQuestion, faEnvelope, faTwitter, faTelegramPlane, faDiscord, faGithub, faReddit, faCircleArrowUp, faCircleArrowDown, faGlobe, faCubes, faClock, faArrowRotateLeft, faHeartPulse, faSeedling, faChevronLeft, faArrowRightArrowLeft }
interface IconProps extends TouchableOpacityProps {
diff --git a/src/components/NumericKeypad.tsx b/src/components/NumericKeypad.tsx
new file mode 100644
index 00000000..a0f03982
--- /dev/null
+++ b/src/components/NumericKeypad.tsx
@@ -0,0 +1,118 @@
+import React from 'react'
+import { View, ViewStyle, TextStyle, Pressable } from 'react-native'
+import { verticalScale, moderateScale } from '@gocodingnow/rn-size-matters'
+import { spacing, useThemeColor, typography } from '../theme'
+import { Text } from './Text'
+import { Icon } from './Icon'
+
+export interface NumericKeypadProps {
+ onKeyPress: (key: string) => void
+ onClear?: () => void
+ onLongPressBackspace?: () => void
+ disabled?: boolean
+}
+
+const KEYS = [
+ ['1', '2', '3'],
+ ['4', '5', '6'],
+ ['7', '8', '9'],
+ ['clear', '0', 'backspace'],
+]
+
+export function NumericKeypad({ onKeyPress, onClear, onLongPressBackspace, disabled = false }: NumericKeypadProps) {
+ const keyBg = useThemeColor('card')
+ const keyBgPressed = useThemeColor('buttonSecondary')
+ const textColor = useThemeColor('text')
+ const textDimColor = useThemeColor('textDim')
+
+ const renderKey = (key: string) => {
+ const isBackspace = key === 'backspace'
+ const isClear = key === 'clear'
+
+ return (
+ {
+ if (disabled) return
+ if (isClear && onClear) {
+ onClear()
+ } else {
+ onKeyPress(key)
+ }
+ }}
+ onLongPress={isBackspace && onLongPressBackspace ? onLongPressBackspace : undefined}
+ delayLongPress={500}
+ disabled={disabled}
+ style={({ pressed }) => [
+ $key,
+ { backgroundColor: pressed ? keyBgPressed : keyBg },
+ disabled && $keyDisabled,
+ ]}
+ >
+ {isBackspace ? (
+
+ ) : isClear ? (
+
+ ) : (
+
+ {key}
+
+ )}
+
+ )
+ }
+
+ return (
+
+ {KEYS.map((row, rowIndex) => (
+
+ {row.map(renderKey)}
+
+ ))}
+
+ )
+}
+
+const $container: ViewStyle = {
+ width: '100%',
+ paddingHorizontal: spacing.medium,
+}
+
+const $row: ViewStyle = {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ marginVertical: spacing.small,
+}
+
+const $key: ViewStyle = {
+ width: moderateScale(80),
+ height: verticalScale(56),
+ borderRadius: spacing.small,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginHorizontal: spacing.small,
+}
+
+const $keyDisabled: ViewStyle = {
+ opacity: 0.5,
+}
+
+const $keyText: TextStyle = {
+ //fontFamily: typography.primary.medium,
+ //fontSize: moderateScale(28),
+
+}
diff --git a/src/components/PosIcon.tsx b/src/components/PosIcon.tsx
new file mode 100644
index 00000000..d5368d52
--- /dev/null
+++ b/src/components/PosIcon.tsx
@@ -0,0 +1,14 @@
+export const PosIcon = `
+`
\ No newline at end of file
diff --git a/src/components/StaticHeader.tsx b/src/components/StaticHeader.tsx
new file mode 100644
index 00000000..752f8dbe
--- /dev/null
+++ b/src/components/StaticHeader.tsx
@@ -0,0 +1,60 @@
+import React from 'react'
+import { TextStyle, View, ViewStyle } from 'react-native'
+import Animated, {
+ SharedValue,
+ useAnimatedStyle,
+ interpolate,
+ Extrapolation,
+} from 'react-native-reanimated'
+import { spacing, typography, useThemeColor } from '../theme'
+import { translate, TxKeyPath } from '../i18n'
+import { verticalScale } from '@gocodingnow/rn-size-matters'
+import { Text } from './Text'
+
+export interface StaticHeaderProps {
+ /**
+ * Title text to display.
+ */
+ title?: string
+ /**
+ * Title text which is looked up via i18n.
+ */
+ titleTx?: TxKeyPath
+
+ height?: number
+
+}
+
+
+export function StaticHeader(props: StaticHeaderProps) {
+ const {
+ title,
+ titleTx,
+ height = spacing.screenHeight * 0.15,
+ } = props
+
+ const headerBg = useThemeColor('header')
+ const headerTitleColor = useThemeColor('headerTitle')
+
+
+ const titleContent = titleTx ? translate(titleTx) : title
+
+ return (
+
+
+ {titleContent}
+
+
+ )
+}
+
+const $headerContainer: ViewStyle = {
+ alignItems: 'center',
+ paddingBottom: spacing.medium,
+}
+
+const $headerTitle: TextStyle = {
+ fontSize: verticalScale(32),
+ lineHeight: verticalScale(44),
+ fontFamily: typography.primary?.medium,
+}
diff --git a/src/components/Text.tsx b/src/components/Text.tsx
index b5482226..be03b6f2 100644
--- a/src/components/Text.tsx
+++ b/src/components/Text.tsx
@@ -76,7 +76,7 @@ export function Text(props: TextProps) {
}
export const $sizeStyles = {
- xxl: { fontSize: verticalScale(36), lineHeight: verticalScale(44) } as TextStyle,
+ xxl: { fontSize: verticalScale(32), lineHeight: verticalScale(44) } as TextStyle,
xl: { fontSize: verticalScale(24), lineHeight: verticalScale(34) } as TextStyle,
lg: { fontSize: verticalScale(20), lineHeight: verticalScale(32) } as TextStyle,
md: { fontSize: verticalScale(18), lineHeight: verticalScale(26) } as TextStyle,
diff --git a/src/components/index.ts b/src/components/index.ts
index 2c916d60..7e17d920 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -4,6 +4,7 @@ export * from "./Button"
export * from "./Card"
export * from "./Header"
export * from "./AnimatedHeader"
+export * from "./StaticHeader"
export * from "./Icon"
export * from "./ListItem"
export * from "./Screen"
@@ -25,4 +26,5 @@ export * from "./MintIcon"
export * from "./HeaderBg"
export * from "./NwcIcon"
export * from "./ContactlessIcon"
-
+export * from "./PosIcon"
+export * from "./NumericKeypad"
diff --git a/src/i18n_messages/en.json b/src/i18n_messages/en.json
index 9387ecdb..85ca33c2 100644
--- a/src/i18n_messages/en.json
+++ b/src/i18n_messages/en.json
@@ -310,6 +310,9 @@
"payCommon_requestPayment": "Request payment",
"payCommon_send": "Send",
"payCommon_withdraw": "Withdraw",
+ "pos_exchangeRateUnavailable": "Exchange rate unavailable",
+ "pos_newTransaction": "New transaction",
+ "pos_waitingForPayment": "Waiting for payment...",
"payerMemo": "Memo for the payer",
"payWithLightningDesc": "Enter or paste Lightning address, invoice or LNURL pay code you want to pay to.",
"pictureRetrieveFail": "Could not retrieve pictures from the server, try again later.",
diff --git a/src/i18n_messages/es.json b/src/i18n_messages/es.json
index 308e172b..6a328319 100644
--- a/src/i18n_messages/es.json
+++ b/src/i18n_messages/es.json
@@ -309,6 +309,9 @@
"payCommon_requestPayment": "Solicitar pago",
"payCommon_send": "Enviar",
"payCommon_withdraw": "Retirar",
+ "pos_exchangeRateUnavailable": "Tipo de cambio no disponible",
+ "pos_newTransaction": "Nueva transacción",
+ "pos_waitingForPayment": "Esperando el pago...",
"payerMemo": "Memo para el pagador",
"payWithLightningDesc": "Ingrese o pegue la dirección Lightning, la factura o el código de pago LNURL al que desea pagar.",
"pictureRetrieveFail": "No se pudieron recuperar las imágenes del servidor, inténtelo nuevamente más tarde.",
diff --git a/src/i18n_messages/pt.json b/src/i18n_messages/pt.json
index 7f3fb70c..478baac3 100644
--- a/src/i18n_messages/pt.json
+++ b/src/i18n_messages/pt.json
@@ -310,6 +310,9 @@
"payCommon_requestPayment": "Solicitar pagamento",
"payCommon_send": "Enviar",
"payCommon_withdraw": "Retirar",
+ "pos_exchangeRateUnavailable": "Taxa de câmbio indisponível",
+ "pos_newTransaction": "Nova transação",
+ "pos_waitingForPayment": "Aguardando pagamento...",
"payerMemo": "Memorando para o pagador",
"payWithLightningDesc": "Insira ou cole endereço Lightning, invoice ou código de pagamento LNURL para pagar.",
"pictureRetrieveFail": "Não foi possível recuperar imagens do servidor, tente mais tarde.",
diff --git a/src/i18n_messages/sk.json b/src/i18n_messages/sk.json
index 2adf2991..9996c800 100644
--- a/src/i18n_messages/sk.json
+++ b/src/i18n_messages/sk.json
@@ -310,6 +310,9 @@
"payCommon_requestPayment": "Vyžiadaj platbu",
"payCommon_send": "Pošli",
"payCommon_withdraw": "Vyber",
+ "pos_exchangeRateUnavailable": "Výmenný kurz nie je k dispozícii",
+ "pos_newTransaction": "Nová transakcia",
+ "pos_waitingForPayment": "Čakám na platbu...",
"payerMemo": "Poznámka pre príjemcu",
"payWithLightningDesc": "Zadaj Lightning adresu, invoice alebo LNURL kód ktorý chceš zaplatiť.",
"pictureRetrieveFail": "Nejde načítať obrázky zo servera, skús neskôr.",
diff --git a/src/models/WalletStore.ts b/src/models/WalletStore.ts
index a00f6830..a9cee1b7 100644
--- a/src/models/WalletStore.ts
+++ b/src/models/WalletStore.ts
@@ -27,7 +27,7 @@ import { Proof } from './Proof'
import { InFlightRequest, Mint } from './Mint'
import { getRootStore } from './helpers/getRootStore'
import { Transaction } from './Transaction'
-// refresh refresh
+//
/*
Not persisted, in-memory only model of the cashu-ts wallet instances and wallet keys persisted in the device secure store.
diff --git a/src/navigation/TabsNavigator.tsx b/src/navigation/TabsNavigator.tsx
index f5d86293..65e8bf87 100644
--- a/src/navigation/TabsNavigator.tsx
+++ b/src/navigation/TabsNavigator.tsx
@@ -37,7 +37,8 @@ import {
TokenReceiveScreen,
NwcScreen,
CashuPaymentRequestScreen,
- RecoveryOptionsScreen
+ RecoveryOptionsScreen,
+ POSScreen
} from "../screens"
import { colors, spacing, typography } from "../theme"
@@ -73,7 +74,8 @@ const WalletStack = createNativeStackNavigator({
NfcPay: NfcPayScreen,
Transfer: TransferScreen,
Topup: TopupScreen,
- CashuPaymentRequest: CashuPaymentRequestScreen,
+ CashuPaymentRequest: CashuPaymentRequestScreen,
+ POS: POSScreen,
}
})
diff --git a/src/screens/Contacts/PrivateContacts.tsx b/src/screens/Contacts/PrivateContacts.tsx
index 4daf0dde..223b8b87 100644
--- a/src/screens/Contacts/PrivateContacts.tsx
+++ b/src/screens/Contacts/PrivateContacts.tsx
@@ -352,6 +352,11 @@ export const PrivateContacts = observer(function (props: {
}
value={newContactName}
autoCapitalize="none"
+ autoComplete='off'
+ importantForAutofill='no'
+ autoCorrect={false}
+ spellCheck={false}
+ smartInsertDelete={false}
keyboardType="email-address"
maxLength={80}
selectTextOnFocus={true}
diff --git a/src/screens/DeveloperScreen.tsx b/src/screens/DeveloperScreen.tsx
index 91591a03..1d0bda61 100644
--- a/src/screens/DeveloperScreen.tsx
+++ b/src/screens/DeveloperScreen.tsx
@@ -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
+//
type Props = StaticScreenProps
diff --git a/src/screens/Mints/MintHeader.tsx b/src/screens/Mints/MintHeader.tsx
index e51f1c77..98008ea7 100644
--- a/src/screens/Mints/MintHeader.tsx
+++ b/src/screens/Mints/MintHeader.tsx
@@ -1,4 +1,5 @@
-import React from "react"
+import React, { useState } from "react"
+import { TouchableOpacity } from "react-native"
import { Header, Text } from "../../components"
import { spacing, useThemeColor } from "../../theme"
import { Mint } from "../../models/Mint"
@@ -14,45 +15,39 @@ export const MintHeader = observer(function(props: {
unit: MintUnit,
mint?: Mint
hideBalance?: boolean
- onBackPress?: () => void
+ onBackPress?: () => void
+ backgroundColor?: string
+ textColor?: string
+ leftIconColor?: string
}
) {
const navigation = useNavigation()
- const {mint, unit, hideBalance, onBackPress} = props
-
- const getActiveUnitColor = () => {
- /* switch (props.unit) {
- case 'usd':
- return useThemeColor('usd')
- case 'eur':
- return useThemeColor('eur')
- default:
- return useThemeColor('btc')
- } */
-
- return useThemeColor('headerTitle')
-
- }
+ const {mint, unit, hideBalance, onBackPress, backgroundColor, textColor, leftIconColor} = props
+ const [isBalanceHidden, setIsBalanceHidden] = useState(false)
const tabWidth = moderateScale(80)
const headerTitle = useThemeColor('headerTitle')
-
+
+ const resolvedTextColor = textColor || 'white'
+
return (
-
- {mint && ()}
-
@@ -61,16 +56,28 @@ export const MintHeader = observer(function(props: {
leftIcon='faArrowLeft'
onLeftPress={() => {
onBackPress ? onBackPress() : navigation.goBack()
- }}
+ }}
RightActionComponent={mint && unit && !hideBalance ? (
-
+ setIsBalanceHidden(!isBalanceHidden)}
+ style={{marginRight: spacing.medium}}
+ >
+ {isBalanceHidden ? (
+
+ ) : (
+
+ )}
+
) : undefined}
/>
)
diff --git a/src/screens/POSScreen.tsx b/src/screens/POSScreen.tsx
new file mode 100644
index 00000000..24ec5b3e
--- /dev/null
+++ b/src/screens/POSScreen.tsx
@@ -0,0 +1,494 @@
+import { observer } from 'mobx-react-lite'
+import React, { useEffect, useState, useRef } from 'react'
+import {
+ View,
+ ViewStyle,
+ TextStyle,
+} from 'react-native'
+import Animated, {
+ useSharedValue,
+ useAnimatedStyle,
+ withSequence,
+ withTiming,
+ withDelay,
+ Easing,
+} from 'react-native-reanimated'
+import { spacing, useThemeColor, colors, typography } from '../theme'
+import {
+ Button,
+ Icon,
+ Screen,
+ Loading,
+ InfoModal,
+ ErrorModal,
+ BottomModal,
+ Text,
+ NumericKeypad,
+} from '../components'
+import { TransactionStatus, Transaction } from '../models/Transaction'
+import { useStores } from '../models'
+import {
+ HANDLE_PENDING_TOPUP_TASK,
+ TransactionTaskResult,
+ WalletTask,
+} from '../services'
+import { log } from '../services/logService'
+import AppError from '../utils/AppError'
+import { MintBalance } from '../models/Mint'
+import EventEmitter from '../utils/eventEmitter'
+import { ResultModalInfo } from './Wallet/ResultModalInfo'
+import { StaticScreenProps, useNavigation } from '@react-navigation/native'
+import { infoMessage } from '../utils/utils'
+import { round } from '../utils/number'
+import { verticalScale, moderateScale } from '@gocodingnow/rn-size-matters'
+import {
+ CurrencyCode,
+ MintUnit,
+ convertToFromSats,
+ Currencies,
+ formatCurrency,
+} from '../services/wallet/currency'
+import { MintHeader } from './Mints/MintHeader'
+import useIsInternetReachable from '../utils/useIsInternetReachable'
+import { QRCodeBlock } from './Wallet/QRCode'
+import { translate } from '../i18n'
+
+type Props = StaticScreenProps<{
+ unit: MintUnit
+ mintUrl?: string
+}>
+
+export const POSScreen = observer(function POSScreen({ route }: Props) {
+ const navigation = useNavigation()
+ const isInternetReachable = useIsInternetReachable()
+
+ const {
+ proofsStore,
+ mintsStore,
+ walletStore,
+ userSettingsStore,
+ } = useStores()
+
+ const unitRef = useRef('sat')
+
+ // Amount state - stored as integer cents (e.g., 123 = $1.23)
+ const [amountCents, setAmountCents] = useState(0)
+ const [satsAmount, setSatsAmount] = useState(0)
+
+ // Max digits for amount entry (e.g., 7 = max $99,999.99)
+ const MAX_CENTS_DIGITS = 7
+
+ // Mint selection
+ const [mintBalanceToTopup, setMintBalanceToTopup] = useState()
+
+ // Transaction state
+ const [transactionStatus, setTransactionStatus] = useState()
+ const [transactionId, setTransactionId] = useState()
+ const [transaction, setTransaction] = useState()
+ const [invoiceToPay, setInvoiceToPay] = useState('')
+
+ // UI state
+ const [info, setInfo] = useState('')
+ const [error, setError] = useState()
+ const [isLoading, setIsLoading] = useState(false)
+
+ // Success icon animation
+ const iconScale = useSharedValue(0.8)
+
+ // Get fiat currency settings
+ const fiatCode = userSettingsStore.exchangeCurrency || CurrencyCode.USD
+ const fiatCurrency = Currencies[fiatCode]
+ const fiatSymbol = fiatCurrency?.symbol || '$'
+ const fiatPrecision = fiatCurrency?.precision ?? 100
+ const fiatMantissa = fiatCurrency?.mantissa ?? 2
+
+ // Initialize unit from route params
+ useEffect(() => {
+ if (route.params?.unit) {
+ unitRef.current = route.params.unit
+ }
+ }, [route.params?.unit])
+
+ // Convert cents to sats whenever amountCents or exchange rate changes
+ useEffect(() => {
+ if (!walletStore.exchangeRate) {
+ setSatsAmount(0)
+ return
+ }
+
+ const sats = convertToFromSats(amountCents, fiatCode, walletStore.exchangeRate)
+ setSatsAmount(round(sats, 0))
+ }, [amountCents, walletStore.exchangeRate, fiatCode])
+
+ // Trigger success icon animation when transaction completes
+ useEffect(() => {
+ if (transactionStatus === TransactionStatus.COMPLETED) {
+ iconScale.value = 0.8
+ iconScale.value = withDelay(
+ 300,
+ withSequence(
+ withTiming(1.15, { duration: 400, easing: Easing.out(Easing.ease) }),
+ withTiming(1, { duration: 300, easing: Easing.inOut(Easing.ease) })
+ )
+ )
+ }
+ }, [transactionStatus])
+
+ const iconAnimatedStyle = useAnimatedStyle(() => ({
+ transform: [{ scale: iconScale.value }],
+ }))
+
+ // Payment detection via EventEmitter
+ useEffect(() => {
+ const handlePendingTopupTaskResult = (result: TransactionTaskResult) => {
+ log.trace('[POSScreen] handlePendingTopupTaskResult triggered')
+
+ if (!transactionId) return
+ if (result.transaction?.id !== transactionId) return
+ if (result.transaction.status !== TransactionStatus.COMPLETED) return
+
+ log.trace('[POSScreen] Invoice paid, proofs received')
+
+ setTransactionStatus(TransactionStatus.COMPLETED)
+ setTransaction(result.transaction)
+ }
+
+ if (transactionId) {
+ EventEmitter.on(
+ `ev_${HANDLE_PENDING_TOPUP_TASK}_result`,
+ handlePendingTopupTaskResult
+ )
+ }
+
+ return () => {
+ EventEmitter.off(
+ `ev_${HANDLE_PENDING_TOPUP_TASK}_result`,
+ handlePendingTopupTaskResult
+ )
+ }
+ }, [transactionId])
+
+ // Handle keypad input - cents-first entry (each digit shifts left)
+ const handleKeyPress = (key: string) => {
+ if (transactionStatus === TransactionStatus.PENDING) return
+
+ if (key === 'backspace') {
+ // Shift right - remove last digit
+ setAmountCents(prev => Math.floor(prev / 10))
+ } else {
+ // Only process digit keys
+ const digit = parseInt(key, 10)
+ if (isNaN(digit)) return
+
+ // Check max digits limit
+ if (amountCents.toString().length >= MAX_CENTS_DIGITS) return
+
+ // Shift left and add new digit
+ setAmountCents(prev => prev * 10 + digit)
+ }
+ }
+
+ // Clear amount (also used for long-press backspace)
+ const onClearAmount = () => {
+ setAmountCents(0)
+ setSatsAmount(0)
+ }
+
+ // Confirm amount and create invoice immediately with default mint
+ const onConfirmAmount = async () => {
+ if (!isInternetReachable) {
+ infoMessage(translate('commonOfflinePretty'))
+ return
+ }
+
+ if (satsAmount <= 0) {
+ infoMessage(translate('payCommon_amountZeroOrNegative'))
+ return
+ }
+
+ const availableBalances = proofsStore.getMintBalancesWithUnit(unitRef.current)
+
+ if (availableBalances.length === 0) {
+ infoMessage(
+ translate('topup_missingMintAddFirst'),
+ translate('topup_missingMintAddFirstDesc')
+ )
+ return
+ }
+
+ // Use first available mint (highest balance) and create invoice immediately
+ const selectedMint = availableBalances[0]
+ setMintBalanceToTopup(selectedMint)
+ setIsLoading(true)
+
+ const result = await WalletTask.topupQueueAwaitable(
+ selectedMint,
+ satsAmount,
+ unitRef.current,
+ '' // empty memo for POS
+ )
+
+ await handleTopupTaskResult(result)
+ }
+
+ // Handle invoice creation result
+ const handleTopupTaskResult = async (result: TransactionTaskResult) => {
+ log.trace('[POSScreen] handleTopupTaskResult')
+
+ setIsLoading(false)
+
+ const { status, id } = result.transaction as Transaction
+ setTransactionStatus(status)
+ setTransactionId(id)
+
+ if (result.encodedInvoice) {
+ setInvoiceToPay(result.encodedInvoice)
+ }
+
+ if (result.error) {
+ setError(result.error)
+ return
+ }
+
+ }
+
+ // Reset for new transaction
+ const resetState = () => {
+ setAmountCents(0)
+ setSatsAmount(0)
+ setTransactionStatus(undefined)
+ setTransactionId(undefined)
+ setTransaction(undefined)
+ setInvoiceToPay('')
+ //setMintBalanceToTopup(undefined)
+ }
+
+
+ const gotoWallet = () => {
+ resetState()
+ navigation.goBack()
+ }
+
+ const handleError = (e: AppError) => {
+ setError(e)
+ }
+
+ // Get colors
+ const headerBg = useThemeColor('background')
+ const textColor = useThemeColor('text')
+ const textDimColor = useThemeColor('textDim')
+ const mainButtonColor = useThemeColor('button')
+
+ // Get selected mint info
+ const selectedMint = mintBalanceToTopup
+ ? mintsStore.findByUrl(mintBalanceToTopup.mintUrl)
+ : undefined
+
+ // Format display amount from cents to decimal string (e.g., 123 → "1.23")
+ const formatCentsToDisplay = (cents: number): string => {
+ const divisor = fiatPrecision // e.g., 100 for USD
+ const whole = Math.floor(cents / divisor)
+ const fraction = cents % divisor
+ return `${whole}.${fraction.toString().padStart(fiatMantissa, '0')}`
+ }
+
+ const displayFiatAmount = formatCentsToDisplay(amountCents)
+
+ return (
+
+
+
+ {/* Amount Display */}
+
+
+ {walletStore.exchangeRate && (
+
+
+
+ )}
+
+
+ {/* Content Area */}
+
+ {/* Show keypad when no invoice yet */}
+ {!invoiceToPay && (
+
+
+
+
+
+ )}
+
+ {/* Show QR code when invoice is created */}
+ {invoiceToPay && transactionStatus === TransactionStatus.PENDING && (
+
+
+
+
+ )}
+
+ {/* Show success when completed */}
+ {transactionStatus === TransactionStatus.COMPLETED && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+ {/* Error/Info modals */}
+ {error && }
+ {info && }
+ {isLoading && }
+
+ )
+})
+
+const $screen: ViewStyle = {
+ flex: 1,
+}
+
+const $amountContainer: ViewStyle = {
+ alignItems: 'center',
+ paddingVertical: spacing.extraLarge,
+ paddingHorizontal: spacing.medium,
+}
+
+const $fiatAmountText: TextStyle = {
+ fontFamily: typography.primary?.medium,
+ fontSize: moderateScale(48),
+ lineHeight: moderateScale(56),
+}
+
+const $satsAmountText: TextStyle = {
+ fontFamily: typography.primary?.normal,
+ fontSize: moderateScale(16),
+ marginTop: spacing.tiny,
+}
+
+const $contentContainer: ViewStyle = {
+ flex: 1,
+ //paddingTop: spacing.medium,
+}
+
+const $keypadContainer: ViewStyle = {
+ flex: 1,
+ justifyContent: 'center',
+}
+
+
+const $qrContainer: ViewStyle = {
+ flex: 1,
+ alignItems: 'center',
+ paddingHorizontal: spacing.medium,
+}
+
+const $waitingText: TextStyle = {
+ marginTop: spacing.small,
+ textAlign: 'center',
+}
+
+const $successContainer: ViewStyle = {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingHorizontal: spacing.large,
+ //paddingTop: spacing.large,
+}
+
+const $successText: TextStyle = {
+ fontFamily: typography.primary?.medium,
+ fontSize: moderateScale(20),
+ marginTop: spacing.medium,
+ textAlign: 'center',
+}
+
+const $buttonContainer: ViewStyle = {
+ flexDirection: 'row',
+ alignSelf: 'center',
+ marginVertical: spacing.medium,
+}
+
+const $buttonSend: ViewStyle = {
+ borderRadius: verticalScale(60 / 2),
+ height: verticalScale(60),
+ minWidth: verticalScale(140),
+}
+
+const $bottomContainer: ViewStyle = {
+ // position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ flex: 1,
+ justifyContent: 'flex-end',
+ marginBottom: spacing.medium,
+ alignSelf: 'stretch',
+ // opacity: 0,
+}
diff --git a/src/screens/TokenReceiveScreen.tsx b/src/screens/TokenReceiveScreen.tsx
index 12082310..ff38e550 100644
--- a/src/screens/TokenReceiveScreen.tsx
+++ b/src/screens/TokenReceiveScreen.tsx
@@ -10,10 +10,10 @@ import {colors, spacing, useThemeColor} from '../theme'
import {log} from '../services/logService'
import { IncomingDataType, IncomingParser } from '../services/incomingParser'
import AppError, { Err } from '../utils/AppError'
-import { Button, Card, ErrorModal, Icon, ListItem, ScanIcon, Screen, Text } from '../components'
+import { Button, Card, ErrorModal, Icon, ListItem, PosIcon, ScanIcon, Screen, StaticHeader, Text } from '../components'
import { infoMessage } from '../utils/utils'
import Clipboard from '@react-native-clipboard/clipboard'
-import { SvgXml } from 'react-native-svg'
+import Svg, { SvgXml } from 'react-native-svg'
import { useStores } from '../models'
import { Mint } from '../models/Mint'
import { MintHeader } from './Mints/MintHeader'
@@ -135,10 +135,18 @@ export const TokenReceiveScreen = function TokenReceiveScreen({ route }: Props)
//@ts-ignore
navigation.navigate('Topup', {
unit,
- mintUrl: mint?.mintUrl
+ mintUrl: mint?.mintUrl
})
}
-
+
+ const gotoPOS = async function () {
+ //@ts-ignore
+ navigation.navigate('POS', {
+ unit,
+ mintUrl: mint?.mintUrl
+ })
+ }
+
const handleError = function(e: AppError): void {
setError(e)
}
@@ -158,13 +166,9 @@ export const TokenReceiveScreen = function TokenReceiveScreen({ route }: Props)
mint={mint}
unit={unit!}
/>
-
-
-
+
-
@@ -289,12 +315,6 @@ const $contentContainer: ViewStyle = {
flex: 1
}
-const $headerContainer: TextStyle = {
- alignItems: 'center',
- paddingBottom: spacing.medium,
- height: spacing.screenHeight * 0.15,
-}
-
const $buttonContainer: ViewStyle = {
//marginTop: spacing.large,
flexDirection: 'row',
diff --git a/src/screens/TopupScreen.tsx b/src/screens/TopupScreen.tsx
index 06c5f468..0aa12db3 100644
--- a/src/screens/TopupScreen.tsx
+++ b/src/screens/TopupScreen.tsx
@@ -275,34 +275,27 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
const handlePendingTopupTaskResult = (result: TransactionTaskResult) => {
log.trace('[handlePendingTopupTaskResult] event handler triggered')
- if (!transactionId) {
- return
- }
+ if (!transactionId) return
+ if (result.transaction?.id !== transactionId) return
+ if (result.transaction.status !== TransactionStatus.COMPLETED) return
- // Filter and handle events only for this topup transactionId
- if (result.transaction?.id === transactionId) {
- // Show result modal only on completed topup
- if (result.transaction.status !== TransactionStatus.COMPLETED) {
- return
- }
+ log.trace(
+ '[handlePendingTopupTaskResult]',
+ 'Invoice has been paid and new proofs received',
+ )
- log.trace(
- '[handlePendingTopupTaskResult]',
- 'Invoice has been paid and new proofs received',
- )
+ setResultModalInfo({
+ status: result.transaction.status,
+ message: result.message,
+ })
- setResultModalInfo({
- status: result.transaction.status,
- message: result.message,
- })
-
- setTransactionStatus(TransactionStatus.COMPLETED)
- setTransaction(result.transaction)
- setIsQRModalVisible(false)
- setIsNostrDMModalVisible(false)
- setIsWithdrawModalVisible(false)
- setIsResultModalVisible(true)
- }
+ setTransactionStatus(TransactionStatus.COMPLETED)
+ setTransaction(result.transaction)
+ setIsQRModalVisible(false)
+ setIsNostrDMModalVisible(false)
+ setIsWithdrawModalVisible(false)
+ setIsResultModalVisible(true)
+
}
if(transactionId) {
diff --git a/src/screens/Wallet/QRCode.tsx b/src/screens/Wallet/QRCode.tsx
index 46538dd1..8c9936c1 100644
--- a/src/screens/Wallet/QRCode.tsx
+++ b/src/screens/Wallet/QRCode.tsx
@@ -7,12 +7,11 @@ import { infoMessage } from "../../utils/utils"
import { Button, Card, Icon, ListItem } from "../../components"
import QRCode from "react-native-qrcode-svg"
import Clipboard from "@react-native-clipboard/clipboard"
-import { moderateScale, verticalScale } from "@gocodingnow/rn-size-matters"
+import { verticalScale } from "@gocodingnow/rn-size-matters"
import { colors, spacing } from "../../theme"
import { translate, TxKeyPath } from "../../i18n"
import { log } from "../../services"
import { Token, getDecodedToken, getEncodedToken } from '@cashu/cashu-ts'
-import { isSupported } from '@react-native-firebase/messaging';
import { NfcService } from '../../services/nfcService';
diff --git a/src/screens/index.ts b/src/screens/index.ts
index acf4ca30..0faa1add 100644
--- a/src/screens/index.ts
+++ b/src/screens/index.ts
@@ -19,6 +19,7 @@ export * from './LightningPayScreen'
export * from './TokenReceiveScreen'
export * from './CashuPaymentRequestScreen'
export * from './NfcPayScreen'
+export * from './POSScreen'
// ContactsNavigator
export * from './ContactsScreen'
diff --git a/src/services/logService.ts b/src/services/logService.ts
index bb844a6f..6bc2e256 100644
--- a/src/services/logService.ts
+++ b/src/services/logService.ts
@@ -18,7 +18,7 @@ import { Platform } from "react-native"
import AppError, { Err } from "../utils/AppError"
const { userSettingsStore } = rootStoreInstance
-//
+// refresh
if (!__DEV__) {
Sentry.init({