Fix error and info modal on Android

This commit is contained in:
minibits-cash
2026-03-24 15:20:39 +01:00
parent 9aca87f66a
commit 26aac2dee8
12 changed files with 71 additions and 55 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ minibits-upload-key.keystore
GoogleService-Info.plist GoogleService-Info.plist
minibits-prod-firebase-adminsdk.json minibits-prod-firebase-adminsdk.json
dist/ dist/
zapstore zsp
# OSX # OSX
# #
@@ -31,7 +31,7 @@
shouldAutocreateTestPlan = "YES"> shouldAutocreateTestPlan = "YES">
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Release" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0" launchStyle = "0"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "minibits_wallet", "name": "minibits_wallet",
"version": "0.4.0-beta.2", "version": "0.4.0-beta.3",
"private": true, "private": true,
"scripts": { "scripts": {
"android:clean": "cd android && ./gradlew clean", "android:clean": "cd android && ./gradlew clean",
+9 -3
View File
@@ -59,7 +59,7 @@ export const AmountInput = forwardRef<TextInput, AmountInputProps>(
const [hasBottomAmountFocusedOnce, setHasBottomAmountFocusedOnce] = useState(false) const [hasBottomAmountFocusedOnce, setHasBottomAmountFocusedOnce] = useState(false)
const [hasBeenFirstTimeConverted, setHasBeenFirstTimeConverted] = useState<boolean>(false) const [hasBeenFirstTimeConverted, setHasBeenFirstTimeConverted] = useState<boolean>(false)
const amountInputColor = useThemeColor("amountInput") const focusedInputColor = useThemeColor("amountInput")
const convertedAmountColor = useThemeColor("headerSubTitle") const convertedAmountColor = useThemeColor("headerSubTitle")
const symbolColor = useThemeColor("headerSubTitle") const symbolColor = useThemeColor("headerSubTitle")
@@ -220,7 +220,7 @@ export const AmountInput = forwardRef<TextInput, AmountInputProps>(
fontFamily: typography.primary?.bold, fontFamily: typography.primary?.bold,
fontWeight: 'bold', // android fontWeight: 'bold', // android
textAlign: "center", textAlign: "center",
color: amountInputColor, color: focusedInputColor,
} }
const animatedTopStyle = useAnimatedStyle(() => ({ const animatedTopStyle = useAnimatedStyle(() => ({
@@ -273,7 +273,12 @@ export const AmountInput = forwardRef<TextInput, AmountInputProps>(
onEndEditing={onAmountEndEditing} onEndEditing={onAmountEndEditing}
onFocus={handleTopFocus} onFocus={handleTopFocus}
onBlur={handleTopBlur} onBlur={handleTopBlur}
style={[defaultTopStyle, style, animatedTopStyle]} style={[
defaultTopStyle,
style,
animatedTopStyle,
{ color: focused === 'top' ? focusedInputColor : convertedAmountColor }
]}
maxLength={9} maxLength={9}
keyboardType="decimal-pad" keyboardType="decimal-pad"
returnKeyType="done" returnKeyType="done"
@@ -307,6 +312,7 @@ export const AmountInput = forwardRef<TextInput, AmountInputProps>(
style, style,
{ color: convertedAmountColor }, { color: convertedAmountColor },
animatedBottomStyle, animatedBottomStyle,
{ color: focused === 'bottom' ? focusedInputColor : convertedAmountColor }
]} ]}
maxLength={9} maxLength={9}
keyboardType="decimal-pad" keyboardType="decimal-pad"
+6 -3
View File
@@ -23,7 +23,8 @@ interface ModalProps extends ViewProps {
top?: number top?: number
onBackdropPress?: any onBackdropPress?: any
onBackButtonPress?: any onBackButtonPress?: any
backdropOpacity?: number onModalHide?: () => void
backdropOpacity?: number
/** /**
* The heading text to display if not using `headingTx`. * The heading text to display if not using `headingTx`.
*/ */
@@ -109,11 +110,12 @@ interface ModalProps extends ViewProps {
*/ */
export function BottomModal(props: ModalProps) { export function BottomModal(props: ModalProps) {
const { const {
isVisible = true, isVisible = true,
onBackdropPress, onBackdropPress,
onBackButtonPress, onBackButtonPress,
onModalHide,
//backdropOpacity = Platform.OS === 'ios' ? 0.25 : 0, //backdropOpacity = Platform.OS === 'ios' ? 0.25 : 0,
backdropOpacity = 0.25, backdropOpacity = 0.25,
content, content,
contentTx, contentTx,
contentTxOptions, contentTxOptions,
@@ -175,6 +177,7 @@ export function BottomModal(props: ModalProps) {
avoidKeyboard={Platform.OS === 'ios'} avoidKeyboard={Platform.OS === 'ios'}
onBackdropPress={onBackdropPress} onBackdropPress={onBackdropPress}
onBackButtonPress={onBackButtonPress} onBackButtonPress={onBackButtonPress}
onModalHide={onModalHide}
backdropOpacity={backdropOpacity} backdropOpacity={backdropOpacity}
useNativeDriverForBackdrop={true} useNativeDriverForBackdrop={true}
hideModalContentWhileAnimating={true} hideModalContentWhileAnimating={true}
+8 -6
View File
@@ -8,6 +8,7 @@ import JSONTree from 'react-native-json-tree'
import Clipboard from '@react-native-clipboard/clipboard' import Clipboard from '@react-native-clipboard/clipboard'
import { useStores } from '../models' import { useStores } from '../models'
import useIsInternetReachable from '../utils/useIsInternetReachable' import useIsInternetReachable from '../utils/useIsInternetReachable'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
type ErrorModalProps = { type ErrorModalProps = {
error: AppError error: AppError
@@ -58,22 +59,23 @@ export const ErrorModal: FC<ErrorModalProps> = function ({ error }) {
} }
const bg = useThemeColor('error') const bg = useThemeColor('error')
const insets = useSafeAreaInsets()
return ( return (
<BottomModal <BottomModal
isVisible={isErrorVisible} isVisible={isErrorVisible}
onBackdropPress={onClose} onBackdropPress={onClose}
onBackButtonPress={onClose} onBackButtonPress={onClose}
style={{ marginBottom: Platform.OS === 'android' ? -(insets.top + insets.bottom) : 0 }}
ContentComponent={ ContentComponent={
<> <View>
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: spacing.small }}> <View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: spacing.small }}>
<Icon icon="faTriangleExclamation" size={spacing.large} color={bg} /> <Icon icon="faTriangleExclamation" size={spacing.large} color={bg} />
<Text style={{ marginLeft: spacing.small }}>{error.name}</Text> <Text style={{ marginLeft: spacing.small }}>{error.name}</Text>
</View> </View>
<ScrollView <ScrollView
style={{ style={{
//height: spacing.screenHeight * 0.1, maxHeight: spacing.screenHeight * 0.1,
maxHeight: spacing.screenHeight * 0.07,
}}> }}>
<Text style={{ marginBottom: spacing.small }}>{error.message}</Text> <Text style={{ marginBottom: spacing.small }}>{error.message}</Text>
</ScrollView> </ScrollView>
@@ -147,7 +149,7 @@ export const ErrorModal: FC<ErrorModalProps> = function ({ error }) {
</> </>
)} )}
</> </View>
} }
/> />
) )
+4 -1
View File
@@ -1,7 +1,8 @@
import React, { FC, useState, useEffect } from 'react' import React, { FC, useState, useEffect } from 'react'
import { View } from 'react-native' import { Platform, View } from 'react-native'
import { BottomModal, Icon, Text } from '../components' import { BottomModal, Icon, Text } from '../components'
import { spacing, useThemeColor } from '../theme' import { spacing, useThemeColor } from '../theme'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
type InfoModalProps = { type InfoModalProps = {
@@ -25,12 +26,14 @@ export const InfoModal: FC<InfoModalProps> = function ({ message }) {
const backgroundColor = useThemeColor('info') const backgroundColor = useThemeColor('info')
const iconColor = useThemeColor('textDim') const iconColor = useThemeColor('textDim')
const insets = useSafeAreaInsets()
return ( return (
<BottomModal <BottomModal
isVisible={isInfoVisible} isVisible={isInfoVisible}
onBackdropPress={() => onClose()} onBackdropPress={() => onClose()}
onBackButtonPress={() => onClose()} onBackButtonPress={() => onClose()}
style={{ marginBottom: Platform.OS === 'android' ? -(insets.top + insets.bottom) : 0 }}
ContentComponent={ ContentComponent={
<View style={{ padding: spacing.small, flexDirection: 'row', alignItems: 'center', marginRight: spacing.medium}}> <View style={{ padding: spacing.small, flexDirection: 'row', alignItems: 'center', marginRight: spacing.medium}}>
<Icon icon="faInfoCircle" size={spacing.large} color={iconColor} /> <Icon icon="faInfoCircle" size={spacing.large} color={iconColor} />
+19 -10
View File
@@ -1,23 +1,21 @@
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import React, {useEffect, useRef, useState} from 'react' import React, {useEffect, useRef, useState} from 'react'
import {FlatList, TextInput, TextStyle, View, ViewStyle} from 'react-native' import {FlatList, Platform, TextInput, TextStyle, View, ViewStyle} from 'react-native'
import {verticalScale} from '@gocodingnow/rn-size-matters' import {verticalScale} from '@gocodingnow/rn-size-matters'
import {colors, spacing, useThemeColor} from '../../theme' import {colors, spacing, useThemeColor} from '../../theme'
import {BottomModal, Button, Card, ErrorModal, Icon, InfoModal, ListItem, Loading, Screen, Text} from '../../components' import {BottomModal, Button, Card, ErrorModal, Icon, InfoModal, ListItem, Loading, Screen, Text} from '../../components'
import {useStores} from '../../models' import {useStores} from '../../models'
import { MinibitsClient, NostrClient, NostrProfile } from '../../services' import { NostrClient, NostrProfile } from '../../services'
import AppError, { Err } from '../../utils/AppError' import AppError, { Err } from '../../utils/AppError'
import {MINIBITS_NIP05_DOMAIN} from '@env' import {MINIBITS_NIP05_DOMAIN} from '@env'
import { log } from '../../services/logService' import { log } from '../../services/logService'
import { ContactListItem } from './ContactListItem' import { ContactListItem } from './ContactListItem'
import { Contact, ContactType } from '../../models/Contact' import { Contact, ContactType } from '../../models/Contact'
import { StackNavigationProp } from '@react-navigation/stack'
import { ReceiveOption } from '../ReceiveScreen' import { ReceiveOption } from '../ReceiveScreen'
import { SendOption } from '../SendScreen' import { SendOption } from '../SendScreen'
import { infoMessage, warningMessage } from '../../utils/utils'
import { IncomingDataType, IncomingParser } from '../../services/incomingParser' import { IncomingDataType, IncomingParser } from '../../services/incomingParser'
import { translate } from '../../i18n' import { translate } from '../../i18n'
import { RouteProp, useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { toJS } from 'mobx' import { toJS } from 'mobx'
import { TransferOption } from '../TransferScreen' import { TransferOption } from '../TransferScreen'
@@ -32,11 +30,12 @@ export const PrivateContacts = observer(function (props: {
const contactNameInputRef = useRef<TextInput>(null) const contactNameInputRef = useRef<TextInput>(null)
const [info, setInfo] = useState('') const [info, setInfo] = useState('')
const [newContactName, setNewContactName] = useState<string>('') const [newContactName, setNewContactName] = useState<string>('')
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [isExternalDomain, setIsExternalDomain] = useState(false) const [isExternalDomain, setIsExternalDomain] = useState(false)
const [isNewContactModalVisible, setIsNewContactModalVisible] = useState(false) const [isNewContactModalVisible, setIsNewContactModalVisible] = useState(false)
const [error, setError] = useState<AppError | undefined>() const [error, setError] = useState<AppError | undefined>()
const [pendingError, setPendingError] = useState<AppError | undefined>()
useEffect(() => { useEffect(() => {
const { paymentOption } = props const { paymentOption } = props
@@ -127,7 +126,11 @@ export const PrivateContacts = observer(function (props: {
} catch(e: any) { } catch(e: any) {
setNewContactName('') setNewContactName('')
setIsExternalDomain(false) setIsExternalDomain(false)
handleError(e) if (Platform.OS === 'ios') {
setPendingError(e)
} else {
handleError(e)
}
} }
} }
@@ -308,6 +311,12 @@ export const PrivateContacts = observer(function (props: {
</View> </View>
<BottomModal <BottomModal
isVisible={isNewContactModalVisible ? true : false} isVisible={isNewContactModalVisible ? true : false}
onModalHide={() => {
if (pendingError) {
handleError(pendingError)
setPendingError(undefined)
}
}}
ContentComponent={ ContentComponent={
<View style={$newContainer}> <View style={$newContainer}>
<Text tx="contactsScreen_newTitle" preset="subheading" /> <Text tx="contactsScreen_newTitle" preset="subheading" />
+2 -5
View File
@@ -219,11 +219,8 @@ export const PublicContactsNew = observer(function (props: {
setFollowingProfiles(following) setFollowingProfiles(following)
setIsLoading(false) setIsLoading(false)
} }
InteractionManager.runAfterInteractions(async () => { loadProfiles()
loadProfiles()
})
}, [followingPubkeys]) }, [followingPubkeys])
+12 -1
View File
@@ -49,6 +49,7 @@ export const MintsScreen = observer(function MintsScreen({ route }: Props) {
const [selectedMint, setSelectedMint] = useState<Mint | undefined>() const [selectedMint, setSelectedMint] = useState<Mint | undefined>()
const [info, setInfo] = useState('') const [info, setInfo] = useState('')
const [error, setError] = useState<AppError | undefined>() const [error, setError] = useState<AppError | undefined>()
const [pendingError, setPendingError] = useState<AppError | undefined>()
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [isAddMintVisible, setIsAddMintVisible] = useState(false) const [isAddMintVisible, setIsAddMintVisible] = useState(false)
const [isMintMenuVisible, setIsMintMenuVisible] = useState(false) const [isMintMenuVisible, setIsMintMenuVisible] = useState(false)
@@ -119,7 +120,11 @@ export const MintsScreen = observer(function MintsScreen({ route }: Props) {
setInfo(translate('mintsScreen_mintAdded')) setInfo(translate('mintsScreen_mintAdded'))
} catch (e: any) { } catch (e: any) {
setMintUrl('') setMintUrl('')
handleError(e) if (Platform.OS === 'ios') {
setPendingError(e)
} else {
handleError(e)
}
} finally { } finally {
setMintUrl('') setMintUrl('')
setIsLoading(false) setIsLoading(false)
@@ -425,6 +430,12 @@ export const MintsScreen = observer(function MintsScreen({ route }: Props) {
/> />
<BottomModal <BottomModal
isVisible={isAddMintVisible ? true : false} isVisible={isAddMintVisible ? true : false}
onModalHide={() => {
if (pendingError) {
handleError(pendingError)
setPendingError(undefined)
}
}}
ContentComponent={ ContentComponent={
<View style={$bottomModal}> <View style={$bottomModal}>
<Text <Text
+7 -22
View File
@@ -27,40 +27,25 @@ export const getImageSource = function(img: string) {
export const infoMessage = function(message: string, description?: string) { export const infoMessage = function(message: string, description?: string) {
const backgroundColor = colors.palette.success300 const backgroundColor = colors.palette.neutral500
const textColor = 'white' const textColor = 'white'
return showMessage({ return showMessage({
message, message,
description, description,
duration: 3000,
backgroundColor, backgroundColor,
color: textColor, color: textColor,
style: { style: {
minHeight: spacing.screenHeight * 0.15, minHeight: spacing.screenHeight * 0.05,
borderTopLeftRadius: spacing.medium, borderRadius: spacing.medium,
borderTopRightRadius: spacing.medium margin: spacing.large,
marginBottom: spacing.large * 4,
}, },
}) })
} }
export const warningMessage = function(message: string, description?: string) {
const backgroundColor = colors.palette.accent500
return showMessage({
message,
description,
duration: 3000,
backgroundColor,
color: 'white',
style: {
minHeight: spacing.screenHeight * 0.15,
borderTopLeftRadius: spacing.medium,
borderTopRightRadius: spacing.medium
},
})
}
export const generateId = function (lengthInBytes: number) { export const generateId = function (lengthInBytes: number) {
const random = QuickCrypto.randomBytes(lengthInBytes) const random = QuickCrypto.randomBytes(lengthInBytes)
const uint8Array = new Uint8Array(random) const uint8Array = new Uint8Array(random)
+1 -1
View File
@@ -1,5 +1,5 @@
name: Minibits Wallet name: Minibits Wallet
summary: Minibits is an ecash and Lightning wallet exploring how ₿-backed ecash can enable instant, cheap, and private value transfer. summary: Minibits is a Bitcoin Lightning and ecash wallet that deliver instant, low-cost, and private value transfers — even when the payer is offline.
repository: https://github.com/minibits-cash/minibits_wallet repository: https://github.com/minibits-cash/minibits_wallet
homepage: https://minibits.cash/ homepage: https://minibits.cash/
assets: assets: