Fix payment request pay with POST transport, Optimze ecash menu item.

This commit is contained in:
minibits-cash
2026-04-14 22:38:24 +02:00
parent e3ec278e28
commit a08f9945f0
15 changed files with 119 additions and 49 deletions
+1 -1
View File
@@ -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",
+1
View File
@@ -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"
+1
View File
@@ -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"
+1
View File
@@ -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"
+1
View File
@@ -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"
+7 -3
View File
@@ -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()
+1 -1
View File
@@ -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)
+1
View File
@@ -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.
+2
View File
@@ -35,6 +35,8 @@ import { TransactionStatus } from '../models/Transaction'
import { maxTransactionsInHistory } from '../models/TransactionsStore'
import { StaticScreenProps, useNavigation } from '@react-navigation/native'
//
type Props = StaticScreenProps<undefined>
export const DeveloperScreen = observer(function DeveloperScreen({ route }: Props) {
+46 -28
View File
@@ -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<undefined>
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: () => (
<Header
leftIcon="faArrowLeft"
onLeftPress={() => navigation.goBack()}
title="Optimize ecash"
scrollY={scrollY}
scrollDistance={HEADER_SCROLL_DISTANCE}
/>
),
})
}, [])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<AppError | undefined>()
@@ -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 (
<Screen contentContainerStyle={$screen} preset="fixed">
<Header leftIcon="faArrowLeft" onLeftPress={() => navigation.goBack()} />
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
<Text preset="heading" text="Optimize ecash" style={{color: headerTitle}} />
</View>
<ScrollView style={$contentContainer}>
<Animated.ScrollView
style={$contentContainer}
onScroll={scrollHandler}
scrollEventThrottle={16}
>
<AnimatedHeader
title="Optimize ecash"
scrollY={scrollY}
/>
<Card
content={`Only denominations with more than ${OPTIMIZE_DENOMINATION_THRESHOLD} proofs can be optimized - by swapping them with the mint for lower number of ecash notes with higher amounts.`}
style={[$card, {marginTop: -spacing.extraLarge * 1.5}]}
/>
<Card
ContentComponent={
<>
@@ -177,15 +210,7 @@ export const OptimizeEcashScreen = function OptimizeEcash(_props: Props) {
}
style={$card}
/>
<View style={$hintContainer}>
<Text
style={{color: hint}}
size="xs"
preset="formHelper"
text={`Denominations with more than ${OPTIMIZE_DENOMINATION_THRESHOLD} proofs can be optimized individually.`}
/>
</View>
</ScrollView>
</Animated.ScrollView>
{isLoading && <Loading />}
{error && <ErrorModal error={error} />}
<BottomModal
@@ -247,23 +272,16 @@ export const OptimizeEcashScreen = function OptimizeEcash(_props: Props) {
}
const $screen: ViewStyle = {
flex: 1,
}
const $headerContainer: TextStyle = {
alignItems: 'center',
paddingBottom: spacing.medium,
height: spacing.screenHeight * 0.15,
// flex: 1,
}
const $contentContainer: TextStyle = {
flex: 1,
marginTop: -spacing.extraLarge * 2,
padding: spacing.extraSmall,
//flex: 1,
}
const $card: ViewStyle = {
marginBottom: spacing.small,
marginHorizontal: spacing.small,
}
const $buttonContainer: ViewStyle = {
+31 -7
View File
@@ -238,6 +238,24 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
const handlePaymentRequest = async () => {
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)
+17 -3
View File
@@ -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}
/>
/>
<ListItem
tx='settingsScreen_optimizeEcash'
leftIcon='faWandMagicSparkles'
leftIconColor={colors.palette.gold200}
leftIconInverse={true}
style={$item}
onPress={gotoOptimizeEcash}
/>
</>
}
/>
+1 -1
View File
@@ -830,7 +830,7 @@ export const WalletScreen = observer(function WalletScreen({ route }: Props) {
LeftComponent={
<View style={{marginRight: spacing.medium}}>
<FastImage
source={{uri: 'https://www.minibits.cash/img/minibits_icon-192.png'}}
source={{uri: 'https://minibits.cash/icon-192.png'}}
style={{width: 40, height: 40}}
/>
</View>
+1 -1
View File
@@ -18,7 +18,7 @@ import { Platform } from "react-native"
import AppError, { Err } from "../utils/AppError"
const { userSettingsStore } = rootStoreInstance
// refresh
//
if (!__DEV__) {
Sentry.init({
+7 -4
View File
@@ -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 (
? `<b>⚡${formatCurrency(amount, currency)} ${currency}</b> received!`
: `⚡${formatCurrency(amount, currency)} ${currency} received!`
const body = Platform.OS === 'android'
? `${zap ? 'Zap' : 'Ecash'} from <b>${sender || 'unknown payer'}</b> is now in your wallet.`
: `${zap ? 'Zap' : 'Ecash'} from ${sender || 'unknown payer'} is now in your wallet.`
? `${zap ? 'Zap' : 'Ecash'} from <b>${sender || 'unknown payer'}</b> 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 };
}