diff --git a/package.json b/package.json index 516283aa..7ec1dad1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minibits_wallet", - "version": "0.4.2-beta.2", + "version": "0.4.2-beta.3", "private": true, "scripts": { "android:clean": "cd android && ./gradlew clean", diff --git a/src/i18n_messages/en.json b/src/i18n_messages/en.json index 1e5e010a..5a76a4d8 100644 --- a/src/i18n_messages/en.json +++ b/src/i18n_messages/en.json @@ -534,6 +534,7 @@ "settingsScreen_preferredUnit": "Preferred unit", "settingsScreen_privacy": "Privacy", "settingsScreen_recovery": "Recovery", + "settingsScreen_optimizeEcash": "Optimize ecash", "settingsScreen_relaysCount": { "one": "%{count} relay", "other": "%{count} relays" diff --git a/src/i18n_messages/es.json b/src/i18n_messages/es.json index 04793fc2..a1722dfa 100644 --- a/src/i18n_messages/es.json +++ b/src/i18n_messages/es.json @@ -533,6 +533,7 @@ "settingsScreen_preferredUnit": "Unidad preferida", "settingsScreen_privacy": "Privacidad", "settingsScreen_recovery": "Recuperación", + "settingsScreen_optimizeEcash": "Optimizar ecash", "settingsScreen_relaysCount": { "one": "Relé %{count}", "other": "%{count} relés" diff --git a/src/i18n_messages/pt.json b/src/i18n_messages/pt.json index 4cb3888d..96a27a73 100644 --- a/src/i18n_messages/pt.json +++ b/src/i18n_messages/pt.json @@ -534,6 +534,7 @@ "settingsScreen_preferredUnit": "Unidade preferida", "settingsScreen_privacy": "Privacidade", "settingsScreen_recovery": "Recuperação", + "settingsScreen_optimizeEcash": "Otimizar ecash", "settingsScreen_relaysCount": { "one": "%{count} relay", "other": "%{count} relays" diff --git a/src/i18n_messages/sk.json b/src/i18n_messages/sk.json index 12722551..c21125fe 100644 --- a/src/i18n_messages/sk.json +++ b/src/i18n_messages/sk.json @@ -534,6 +534,7 @@ "settingsScreen_preferredUnit": "Preferovaná mena", "settingsScreen_privacy": "Súkromie", "settingsScreen_recovery": "Obnova", + "settingsScreen_optimizeEcash": "Optimalizovať ecash", "settingsScreen_relaysCount": { "one": "%{count} relay", "other": "%{count} relays" diff --git a/src/models/MintsStore.ts b/src/models/MintsStore.ts index d4190a6d..5f839c80 100644 --- a/src/models/MintsStore.ts +++ b/src/models/MintsStore.ts @@ -61,7 +61,8 @@ export const MintsStoreModel = types .actions(withSetPropAction) .actions(self => ({ mintExists: (mintUrl: string | URL) => { - const mint = self.mints.find(m => m.mintUrl === mintUrl) + const normalized = String(mintUrl).replace(/\/$/, '') + const mint = self.mints.find(m => m.mintUrl.replace(/\/$/, '') === normalized) if(mint) {return true} else {return false} }, addOrUpdateCounterBackup(mintToRemove: Mint) { @@ -109,7 +110,10 @@ export const MintsStoreModel = types if(!mintUrl) { throw new AppError(Err.VALIDATION_ERROR, 'Mint URL is required.') } - + + // Cashu spec: mint URL must be stripped of trailing slashes + mintUrl = mintUrl.replace(/\/$/, '') + if(!mintUrl.includes('.onion') && !mintUrl.startsWith('https')) { throw new AppError(Err.VALIDATION_ERROR, 'Mint URL needs to start with https.') } @@ -119,7 +123,7 @@ export const MintsStoreModel = types } log.trace('[addMint] start') - + const newMint = new CashuMint(mintUrl) // get fresh keysets const keySetResult: GetKeysetsResponse = yield newMint.getKeySets() diff --git a/src/models/ProofsStore.ts b/src/models/ProofsStore.ts index 43936d90..e4de5a6a 100644 --- a/src/models/ProofsStore.ts +++ b/src/models/ProofsStore.ts @@ -411,7 +411,7 @@ import { })) .views(self => ({ - getMintBalance: (mintUrl: string) => self.balances.mintBalances.find(b => b.mintUrl === mintUrl), + getMintBalance: (mintUrl: string) => self.balances.mintBalances.find(b => b.mintUrl.replace(/\/$/, '') === mintUrl.replace(/\/$/, '')), getMintBalancesWithEnoughBalance: (amount: number, unit: MintUnit) => self.balances.mintBalances .filter(b => (b.balances[unit] || 0) >= amount) diff --git a/src/models/WalletStore.ts b/src/models/WalletStore.ts index 1a2edd19..ee71fc45 100644 --- a/src/models/WalletStore.ts +++ b/src/models/WalletStore.ts @@ -29,6 +29,7 @@ import { InFlightRequest, Mint } from './Mint' import { getRootStore } from './helpers/getRootStore' import { Transaction } from './Transaction' +// /* 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/screens/DeveloperScreen.tsx b/src/screens/DeveloperScreen.tsx index bc816a4a..cc92449e 100644 --- a/src/screens/DeveloperScreen.tsx +++ b/src/screens/DeveloperScreen.tsx @@ -35,6 +35,8 @@ import { TransactionStatus } from '../models/Transaction' import { maxTransactionsInHistory } from '../models/TransactionsStore' import { StaticScreenProps, useNavigation } from '@react-navigation/native' +// + type Props = StaticScreenProps export const DeveloperScreen = observer(function DeveloperScreen({ route }: Props) { diff --git a/src/screens/OptimizeEcashScreen.tsx b/src/screens/OptimizeEcashScreen.tsx index 4075e567..7cdcdec8 100644 --- a/src/screens/OptimizeEcashScreen.tsx +++ b/src/screens/OptimizeEcashScreen.tsx @@ -1,11 +1,14 @@ -import React, {useState, useEffect} from 'react' +import React, {useState, useEffect, useLayoutEffect} from 'react' import { TextStyle, ViewStyle, View, Platform, - ScrollView, } from 'react-native' +import Animated, { + useSharedValue, + useAnimatedScrollHandler, +} from 'react-native-reanimated' import notifee from '@notifee/react-native' import {useThemeColor, spacing, colors} from '../theme' import { @@ -18,6 +21,7 @@ import { Loading, BottomModal, Header, + AnimatedHeader, } from '../components' import AppError from '../utils/AppError' import {useStores} from '../models' @@ -35,6 +39,23 @@ type Props = StaticScreenProps export const OptimizeEcashScreen = function OptimizeEcash(_props: Props) { const navigation = useNavigation() const {proofsStore} = useStores() + const scrollY = useSharedValue(0) + const HEADER_SCROLL_DISTANCE = spacing.screenHeight * 0.15 + + useLayoutEffect(() => { + navigation.setOptions({ + headerShown: true, + header: () => ( +
navigation.goBack()} + title="Optimize ecash" + scrollY={scrollY} + scrollDistance={HEADER_SCROLL_DISTANCE} + /> + ), + }) + }, []) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState() @@ -131,17 +152,29 @@ export const OptimizeEcashScreen = function OptimizeEcash(_props: Props) { setError(e) } - const headerBg = useThemeColor('header') - const headerTitle = useThemeColor('headerTitle') const hint = useThemeColor('textDim') + const scrollHandler = useAnimatedScrollHandler({ + onScroll: (event) => { + scrollY.value = event.contentOffset.y + }, + }) + return ( -
navigation.goBack()} /> - - - - + + + @@ -177,15 +210,7 @@ export const OptimizeEcashScreen = function OptimizeEcash(_props: Props) { } style={$card} /> - - - - + {isLoading && } {error && } { try { + + /*const cashuPaymentRequest = new CashuPaymentRequest( + [ + { + type: PaymentRequestTransportType.POST, + target: 'https://shuestand.mountainlake.io/api/v1/withdrawals/wd_62b13c9d-1082-4b2d-809e-1e6723f74c52/nut18' + } + ], + 'test1234', + 10, + 'sat', + ['https://m7.mountainlake.io'], + 'memo' + ) + + const encoded = cashuPaymentRequest.toEncodedRequest() + log.trace('[handlePaymentRequest] TEST payment request', encoded)*/ + setPaymentOption(SendOption.PAY_CASHU_PAYMENT_REQUEST) setContactToSendFrom(getContactFrom()) @@ -357,25 +375,32 @@ export const SendScreen = observer(function SendScreen({ route }: Props) { } if (availableBalances.length === 0) { - throw new AppError(Err.NOTFOUND_ERROR, 'Wallet does not have any of the mints accepted by Cashu payment request.', {mints: pr.mints}) + throw new AppError(Err.VALIDATION_ERROR, 'None of the mints accepted by this payment request are in your wallet.', {mints: pr.mints}) } - + const withEnoughBalance = availableBalances.filter(balance => { const unitBalance = balance.balances[unitRef.current] if(!pr.amount) { return balance } - + if(pr.amount && pr.amount > 0 && unitBalance && unitBalance >= pr.amount) { return balance } - return null + return null }) - + + if (pr.amount && withEnoughBalance.length === 0) { + throw new AppError(Err.VALIDATION_ERROR, `Not enough balance to pay this payment request. Required: ${pr.amount} ${unitRef.current}.`) + + } + setAvailableMintBalances(withEnoughBalance) - setMintBalanceToSendFrom(withEnoughBalance[0]) + setMintBalanceToSendFrom(withEnoughBalance[0]) + + log.trace('[handlePaymentRequest] available mint balances for this payment request', {availableBalances, withEnoughBalance}) } else { let withEnoughBalance: MintBalance[] = [] @@ -443,7 +468,6 @@ export const SendScreen = observer(function SendScreen({ route }: Props) { setIsOnline(isInternetReachable) if(isInternetReachable) { - setAvailableMintBalances([]) setInfo('') // clear any offline-specific messages } else { const availableBalances = proofsStore.getMintBalancesWithEnoughBalance(1, unitRef.current) diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 8872def9..8f31877e 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -216,6 +216,11 @@ export const SettingsScreen = observer(function SettingsScreen({ route }: Props) navigation.navigate('RecoveryOptions') } + const gotoOptimizeEcash = function() { + // @ts-ignore + navigation.navigate('OptimizeEcash') + } + const gotoUpdate = function() { // @ts-ignore navigation.navigate('Update', { @@ -303,7 +308,7 @@ export const SettingsScreen = observer(function SettingsScreen({ route }: Props) } - const $itemRight = {color: useThemeColor('textDim'), fontFamily: typography.light} as TextStyle + const $itemRight = {color: useThemeColor('textDim'), fontFamily: typography.primary?.light} as TextStyle const colorScheme = useColorScheme() const defaultThemeColor = colorScheme === 'dark' ? Themes[ThemeCode.DARK]?.color : Themes[ThemeCode.LIGHT]?.color @@ -468,9 +473,18 @@ export const SettingsScreen = observer(function SettingsScreen({ route }: Props) leftIcon='faHeartPulse' leftIconColor={colors.palette.angry300} leftIconInverse={true} - style={$item} + style={$item} + bottomSeparator={true} onPress={gotoRecoveryOptions} - /> + /> + } /> diff --git a/src/screens/WalletScreen.tsx b/src/screens/WalletScreen.tsx index a573f451..835312a7 100644 --- a/src/screens/WalletScreen.tsx +++ b/src/screens/WalletScreen.tsx @@ -830,7 +830,7 @@ export const WalletScreen = observer(function WalletScreen({ route }: Props) { LeftComponent={ diff --git a/src/services/logService.ts b/src/services/logService.ts index deeb3dd1..93abceda 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({ diff --git a/src/services/walletService.ts b/src/services/walletService.ts index 66ed866f..d8e553d9 100644 --- a/src/services/walletService.ts +++ b/src/services/walletService.ts @@ -2766,7 +2766,8 @@ const handleReceivedEventTask = async function (encryptedEvent: NostrEvent): Pro // We do it defensively only after cash is received // and asynchronously so we speed up queue _sendReceiveNotification( - receivedAmount, + transaction.amount, + transaction.fee, transaction.unit, isZap, sentFrom, @@ -2986,7 +2987,8 @@ const handleReceivedEventTask = async function (encryptedEvent: NostrEvent): Pro // We do it defensively only after cash is received // and asynchronously so we speed up queue _sendReceiveNotification( - receivedAmount, + transaction.amount, + transaction.fee, transaction.unit, false, sentFrom, @@ -3021,6 +3023,7 @@ const handleReceivedEventTask = async function (encryptedEvent: NostrEvent): Pro const _sendReceiveNotification = async function ( receivedAmount: number, + feePaid: number, unit: MintUnit, isZap: boolean, sentFrom: string, @@ -3036,8 +3039,8 @@ const _sendReceiveNotification = async function ( ? `⚡${formatCurrency(amount, currency)} ${currency} received!` : `⚡${formatCurrency(amount, currency)} ${currency} received!` const body = Platform.OS === 'android' - ? `${zap ? 'Zap' : 'Ecash'} from ${sender || 'unknown payer'} is now in your wallet.` - : `${zap ? 'Zap' : 'Ecash'} from ${sender || 'unknown payer'} is now in your wallet.` + ? `${zap ? 'Zap' : 'Ecash'} from ${sender || 'unknown payer'} is now in your wallet.${feePaid > 0 ? ` Fee paid: ${formatCurrency(feePaid, currency)} ${currency}.` : ''}` + : `${zap ? 'Zap' : 'Ecash'} from ${sender || 'unknown payer'} is now in your wallet.${feePaid > 0 ? ` Fee paid: ${formatCurrency(feePaid, currency)} ${currency}.` : ''}` return { title, body }; }