From 33d7bc07c066f4e017229ad7f2149d65b7be9209 Mon Sep 17 00:00:00 2001 From: minibits-cash Date: Sun, 14 Jun 2026 23:25:15 +0200 Subject: [PATCH] UX improvements (Wallet, NFC) --- .../xcschemes/minibits_wallet.xcscheme | 2 +- package.json | 2 +- src/components/SegmentedTabBar.tsx | 171 +++++++++++ src/components/index.ts | 1 + src/models/ProofsStore.ts | 19 +- src/screens/ContactsScreen.tsx | 16 +- src/screens/MintInfoScreen.tsx | 8 +- src/screens/Mints/MintHeader.tsx | 9 +- src/screens/NfcPayScreen.tsx | 279 +++++++++++++++--- src/screens/WalletNameScreen.tsx | 16 +- src/screens/WalletScreen.tsx | 46 ++- 11 files changed, 465 insertions(+), 104 deletions(-) create mode 100644 src/components/SegmentedTabBar.tsx diff --git a/ios/minibits_wallet.xcodeproj/xcshareddata/xcschemes/minibits_wallet.xcscheme b/ios/minibits_wallet.xcodeproj/xcshareddata/xcschemes/minibits_wallet.xcscheme index 797020b..3be76ef 100644 --- a/ios/minibits_wallet.xcodeproj/xcshareddata/xcschemes/minibits_wallet.xcscheme +++ b/ios/minibits_wallet.xcodeproj/xcshareddata/xcschemes/minibits_wallet.xcscheme @@ -31,7 +31,7 @@ shouldAutocreateTestPlan = "YES"> = SceneRendererProps & { + navigationState: NavigationState + /** Custom content for each segment. Defaults to the route's `title` as text. */ + renderItem?: (info: { route: T; focused: boolean }) => React.ReactNode + /** Fixed per-segment width; the control is then centered. Omit to fill width. */ + segmentWidth?: number + trackColor?: string + pillColor?: string + /** Label colors for the default text renderer. */ + activeColor?: string + inactiveColor?: string + containerStyle?: StyleProp +} + +// Shared so non-tab surfaces (e.g. MintHeader's selected-unit chip) can match +// the active pill exactly. +export const SEGMENTED_PILL_RADIUS = 100 +export const SEGMENTED_PILL_COLOR = "rgba(255,255,255,0.20)" + +const PILL_RADIUS = SEGMENTED_PILL_RADIUS +// No padding: the pill fills its segment edge-to-edge so no track color shows +// as a margin around the active segment. +const TRACK_PADDING = 0 +const ANIM = { duration: 220, easing: Easing.out(Easing.cubic) } + +const DEFAULT_TRACK = "rgba(255,255,255,0.08)" +const DEFAULT_PILL = SEGMENTED_PILL_COLOR + +export function SegmentedTabBar(props: SegmentedTabBarProps) { + const { + navigationState, + jumpTo, + renderItem, + segmentWidth, + trackColor, + pillColor, + activeColor = "white", + inactiveColor = "rgba(255,255,255,0.6)", + containerStyle, + } = props + + const routes = navigationState.routes + const count = routes.length + const index = navigationState.index + + // Inner content width drives the pill geometry. In fixed-width mode we seed it + // up front to avoid a first-frame flash; onLayout keeps it exact thereafter. + const innerWidth = useSharedValue(segmentWidth ? segmentWidth * count : 0) + const animIndex = useSharedValue(index) + + useEffect(() => { + animIndex.value = withTiming(index, ANIM) + }, [index]) + + const onInnerLayout = (e: LayoutChangeEvent) => { + innerWidth.value = e.nativeEvent.layout.width + } + + const pillStyle = useAnimatedStyle(() => { + const segW = count > 0 ? innerWidth.value / count : 0 + return { + width: segW, + transform: [{ translateX: animIndex.value * segW }], + } + }) + + const renderLabel = (route: T, focused: boolean) => { + if (renderItem) return renderItem({ route, focused }) + return ( + + ) + } + + return ( + + + + + {routes.map((route, i) => ( + jumpTo(route.key)} + accessibilityRole="tab" + accessibilityState={{ selected: i === index }} + > + {renderLabel(route, i === index)} + + ))} + + + + ) +} + +const $track: ViewStyle = { + borderRadius: PILL_RADIUS, + padding: TRACK_PADDING, + marginVertical: spacing.small, +} + +const $inner: ViewStyle = { + position: "relative", + alignSelf: "stretch", +} + +const $pill: ViewStyle = { + position: "absolute", + top: 0, + bottom: 0, + left: 0, + borderRadius: PILL_RADIUS, +} + +const $row: ViewStyle = { + flexDirection: "row", +} + +const $segment: ViewStyle = { + paddingVertical: spacing.extraSmall, + alignItems: "center", + justifyContent: "center", +} + +const $label: TextStyle = { + textAlign: "center", +} diff --git a/src/components/index.ts b/src/components/index.ts index 66437c2..7a03781 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 "./SegmentedTabBar" export * from "./StaticHeader" export * from "./Icon" export * from "./ListItem" diff --git a/src/models/ProofsStore.ts b/src/models/ProofsStore.ts index cbb3665..d46d2db 100644 --- a/src/models/ProofsStore.ts +++ b/src/models/ProofsStore.ts @@ -647,18 +647,13 @@ import { .filter(b => unit in b.balances) .sort((a, b) => (b.balances[unit] || 0) - (a.balances[unit] || 0)), - getMintBalanceWithMaxBalance: (unit: MintUnit) => { - let max: MintBalance | undefined - let maxAmt = -1 - for (const b of self.balances.mintBalances) { - const amt = b.balances[unit] || 0 - if (amt > maxAmt) { - maxAmt = amt - max = b - } - } - return max - }, + // Highest-balance mint that actually holds the unit (undefined if none do). + // Only mints that list the unit are candidates, so a unit whose sole mint + // has a zero balance still resolves to that mint rather than an unrelated one. + getMintBalanceWithMaxBalance: (unit: MintUnit): MintBalance | undefined => + self.balances.mintBalances + .filter(b => unit in b.balances) + .sort((a, b) => (b.balances[unit] || 0) - (a.balances[unit] || 0))[0], getUnitBalance: (unit: MintUnit) => self.balances.unitBalances.find(b => b.unit === unit) || { unit, unitBalance: 0 }, diff --git a/src/screens/ContactsScreen.tsx b/src/screens/ContactsScreen.tsx index b322c7a..dfc402c 100644 --- a/src/screens/ContactsScreen.tsx +++ b/src/screens/ContactsScreen.tsx @@ -1,9 +1,9 @@ import {observer} from 'mobx-react-lite' import React, {FC, useEffect, useState} from 'react' import {Image, Pressable, View, ViewStyle} from 'react-native' -import { TabBar, TabView, Route } from 'react-native-tab-view' -import {colors, spacing, typography, useThemeColor} from '../theme' -import {Header, Icon, Screen} from '../components' +import { TabView, Route } from 'react-native-tab-view' +import {spacing, typography, useThemeColor} from '../theme' +import {Header, Icon, Screen, SegmentedTabBar} from '../components' import {useStores} from '../models' import { PrivateContacts } from './Contacts/PrivateContacts' import { PublicContacts } from './Contacts/PublicContacts' @@ -54,16 +54,12 @@ export const ContactsScreen = observer(function ({ route }: Props) { } const headerBg = useThemeColor('header') - const activeTabIndicator = colors.palette.accent400 const {nip05} = walletProfileStore const renderTabBar = (props: any) => ( - + + + ) return ( diff --git a/src/screens/MintInfoScreen.tsx b/src/screens/MintInfoScreen.tsx index 46b58a9..0ed78c5 100644 --- a/src/screens/MintInfoScreen.tsx +++ b/src/screens/MintInfoScreen.tsx @@ -418,11 +418,11 @@ function NutsCard(props: {info: GetInfoResponse}) { // detailed nuts are separated from simple ones if we want to show more info abt them in the future for (const [nut, info] of Object.entries(props.info.nuts)) { - if (nut === '15' && Array.isArray(info) && info.length > 0) { + if (nut === '15' && 'methods' in info && Array.isArray(info.methods) && info.methods.length > 0) { // see https://github.com/cashubtc/nuts/blob/main/15.md - multipath payments // in the future, it might be nice to show for which currencies are multipath payments supported // for example by extending NutItem - nutsSimple.push(['15', info[0].mpp ?? false]) + nutsSimple.push(['15', true]) continue; } // see https://github.com/cashubtc/nutshell/issues/588 @@ -439,6 +439,10 @@ function NutsCard(props: {info: GetInfoResponse}) { nutsSimple.push(['19', info.cached_endpoints.some(e => e.method === 'POST')]) continue; } + if (nut === '29' && ('supported' in info && info.supported === true) || ('methods' in info && Array.isArray(info.methods) && info.methods.length > 0)) { + nutsSimple.push(['29', true]) + continue; + } if ('disabled' in info && info.disabled === false) { // detailed supportedNutsDetailed.push([nut, info as unknown as DetailedNutInfo]) } else if ('supported' in info && typeof info.supported !== 'undefined') { // simple diff --git a/src/screens/Mints/MintHeader.tsx b/src/screens/Mints/MintHeader.tsx index 98008ea..832980c 100644 --- a/src/screens/Mints/MintHeader.tsx +++ b/src/screens/Mints/MintHeader.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react" import { TouchableOpacity } from "react-native" -import { Header, Text } from "../../components" +import { Header, Text, SEGMENTED_PILL_COLOR, SEGMENTED_PILL_RADIUS } from "../../components" import { spacing, useThemeColor } from "../../theme" import { Mint } from "../../models/Mint" import { MintUnit } from "../../services/wallet/currency" @@ -45,9 +45,10 @@ export const MintHeader = observer(function(props: { mintUnit={unit} textStyle={{color: resolvedTextColor}} containerStyle={{ - borderBottomWidth: 2, - paddingVertical: mint ? spacing.tiny : spacing.small, - borderBottomColor: resolvedTextColor, + backgroundColor: SEGMENTED_PILL_COLOR, + borderRadius: SEGMENTED_PILL_RADIUS, + paddingVertical: spacing.extraSmall, + marginTop: mint ? spacing.tiny : 0, width: tabWidth }} /> diff --git a/src/screens/NfcPayScreen.tsx b/src/screens/NfcPayScreen.tsx index c0b5731..dbdc07e 100644 --- a/src/screens/NfcPayScreen.tsx +++ b/src/screens/NfcPayScreen.tsx @@ -1,9 +1,11 @@ -import React, { useState, useEffect, useRef, useCallback } from 'react' +import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react' import { ViewStyle, View, TextStyle, Platform, + Pressable, + StyleSheet, } from 'react-native' import { PaymentRequest as CashuPaymentRequest, MeltQuoteBolt11Response, PaymentRequestTransportType, decodePaymentRequest, getDecodedToken, getTokenMetadata } from '@cashu/cashu-ts' import NfcManager, { Ndef, NfcEvents } from 'react-native-nfc-manager' @@ -13,7 +15,7 @@ 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 { SvgXml } from 'react-native-svg' +import { SvgXml, Svg, Defs, LinearGradient as SvgLinearGradient, Stop, Rect } from 'react-native-svg' import { formatCurrency, getCurrency, MintUnit, MintUnits } from '../services/wallet/currency' import { useStores } from '../models' import { MintHeader } from './Mints/MintHeader' @@ -57,6 +59,37 @@ type Props = StaticScreenProps<{ const SYNC_STATE_WITH_MINT_TIMEOUT = 10 * 1000 +// Visual phases of the NFC session. Each drives the header color band so progress is readable +// at arm's length. `waiting` is the special Android case: the app was woken by the NFC field +// (cold/warm launch) and the OS already consumed the launch tap, so delivering the ecash needs +// a SECOND deliberate tap — we signal that with its own attention color and message, distinct +// from the initial `ready` arming. Order also defines the dev stepper sequence below. +const NFC_PHASES = ['idle', 'ready', 'processing', 'waiting', 'success', 'error'] as const +type NfcPhase = (typeof NFC_PHASES)[number] + +// Base phase colors. `ready` is a calm neutral (just darker than the screen background) and is +// resolved per-theme in the component (see `phaseColors`); the value here is only a fallback. +const PHASE_COLORS: Record = { + idle: colors.palette.neutral500, + ready: colors.palette.neutral400, + processing: colors.palette.primary400, + waiting: colors.palette.accent400, + success: colors.palette.success200, + error: colors.palette.angry300, +} + +// Representative nfcInfo text shown when previewing a phase via the dev stepper. These mirror +// the actual strings the screen sets via setNfcInfo (idle/error reuse the messages from the +// disabled-NFC and failure paths). +const PHASE_DEV_INFO: Record = { + idle: 'Please enable NFC in your device settings', + ready: 'Hold your device close to the NFC reader.', + processing: 'Processing, keep your device still...', + waiting: 'Tap the device again to send the payment.', + success: 'Ecash token sent successfully.', + error: 'NFC Payment failed', +} + export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { const navigation = useNavigation() const { mintsStore, walletStore, proofsStore, transactionsStore, walletProfileStore } = useStores() @@ -113,7 +146,13 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { const [isProcessing, setIsProcessing] = useState(false) const [isPaid, setIsPaid] = useState(false) const [isError, setIsError] = useState(false) + // True while we need the user to deliberately tap AGAIN to deliver the ecash — the Android + // wake-by-NFC case where the OS consumed the launch tap (see `waiting` phase note above). + const [isWaitingForTap, setIsWaitingForTap] = useState(false) const [nfcInfo, setNfcInfo] = useState() + // Dev-only: when set, overrides the derived phase so each state can be previewed on a + // simulator without real NFC hardware. Cleared by the "live" button in the dev stepper. + const [devPhase, setDevPhase] = useState() //const [readNfcData, setReadNfcData] = useState() const [error, setError] = useState() @@ -210,6 +249,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { setNfcInfo(needsLiveWriteBack ? 'Tap the device again to send the payment.' : 'Hold your device close to the NFC reader.') + setIsWaitingForTap(needsLiveWriteBack) setIsNfcEnabled(true) keepScanningRef.current = true @@ -472,6 +512,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { // reader-mode session alive (see finally) so the OS chooser can't pop over the // result modal, and we stop re-arming for new taps. paymentLockedRef.current = true + setIsWaitingForTap(false) setIsProcessing(true) //log.info('NFC Tag found', {tag}); @@ -674,8 +715,12 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { // from the initial read open, so it skips this. if (!readerSessionActiveRef.current) { log.trace('[NfcScreen] handleSendTaskResult: no open reader session (cold/warm launch), arming one for write-back.') - setNfcInfo('Hold your device close to the payee to deliver the ecash...') + setNfcInfo('Hold your device close to the reader...') + // Token is prepared but undelivered — surface the dedicated "tap again" phase + // (takes priority over processing) so the user knows to physically tap once more. + setIsWaitingForTap(true) await NfcService.readNdefTag() // opens requestTechnology session, keeps it alive + setIsWaitingForTap(false) readerSessionActiveRef.current = true } @@ -1043,13 +1088,6 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { }) } - - const handleError = (e: AppError) => { - setError(e) - setInfo('') - setIsProcessing(false) - } - const gotoScan = () => { navigation.navigate('Scan', { unit: unitRef.current }) } @@ -1062,7 +1100,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { } const resetState = function () { - //ßsetEncodedInvoice('') + //setEncodedInvoice('') setInvoice(undefined) setInvoiceExpiry(undefined) setMeltQuote(undefined) @@ -1075,6 +1113,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { setIsResultModalVisible(false) setResultModalInfo(undefined) setIsPaid(false) + setIsWaitingForTap(false) setPendingAsyncMeltId(undefined) } @@ -1097,14 +1136,39 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { // Theme const hintText = useThemeColor('textDim') const headerBg = useThemeColor('background') + //const headerBg = useThemeColor('header') const headerTitle = useThemeColor('headerTitle') const scanIcon = useThemeColor('text') const nfcText = useThemeColor('text') - const indicatorStandby = colors.palette.primary400 - const indicatorProcessing = useThemeColor('button') + const isLightTheme = headerBg === colors.light.background + + // Derive the current visual phase from session state. `waiting` ranks above `processing` + // so the "tap again to deliver" prompt (set while isProcessing is also true) wins. A dev + // override lets the simulator step through every phase without NFC hardware. + const realPhase: NfcPhase = + isError ? 'error' + : isPaid ? 'success' + : isWaitingForTap ? 'waiting' + : isProcessing ? 'processing' + : isNfcEnabled ? 'ready' + : 'idle' + const phase: NfcPhase = devPhase ?? realPhase + + // Resolve per-theme phase colors: `ready` is a neutral band just a step darker than the + // screen background (light → darker grey, dark → darker charcoal), fading to the background. + const phaseColors = useMemo>(() => ({ + ...PHASE_COLORS, + ready: isLightTheme ? colors.palette.neutral400 : colors.palette.neutral800, + }), [isLightTheme]) + + // Ink on the band. The band stays fully opaque through its upper portion (where the icon + // and text sit), so white reads cleanly on every phase — including the darker-neutral idle + // and ready bands in light theme. + const onPhaseColor = 'white' + // Translucent white disc behind the NFC icon for subtle depth on any band. + const phaseDiscColor = 'rgba(255, 255, 255, 0.18)' // 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 @@ -1113,28 +1177,36 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { return ( - - - {!isOnline.current && !isPaid && ( + {/* Solid phase color behind the header, cross-fading in step with the band below + so the whole top region reads as one continuous, phase-aware color block. */} + + + + + + {/* Color band that communicates the session phase, fading into the screen + background at the bottom edge. Cross-fades smoothly between phases. */} + + {!isOnline.current && !isPaid && ( <> - + )} {isPaid ? ( {}} unit={unitRef.current} editable={false} @@ -1148,14 +1220,17 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { justifyContent: 'center', width: moderateScale(80), height: moderateScale(80), - backgroundColor:isProcessing ? indicatorProcessing : isNfcEnabled ? indicatorStandby : hintText, + // Translucent well behind the icon for subtle depth against + // whatever phase band is showing (tint follows the ink). + backgroundColor: phaseDiscColor, borderRadius: moderateScale(80) / 2, - marginVertical: spacing.medium, + marginBottom: spacing.medium, }} > - )} + />)} + {__DEV__ && ( + { + setDevPhase(p) + if (p) setNfcInfo(PHASE_DEV_INFO[p]) + }} + /> + )} { + opacity.value = withTiming(active ? 1 : 0, { + duration: 600, + easing: Easing.inOut(Easing.quad), + }) + }, [active, opacity]) + + const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value })) + // Gradient ids must be unique per layer; phase colors are already distinct. + const gradientId = useMemo(() => `nfcPhaseGrad-${color.replace(/[^a-zA-Z0-9]/g, '')}`, [color]) + + if (solid) { + return ( + + ) + } + + return ( + + + + + + + + + + + + + + ) +} + +// Stacks one layer per phase and lights up the active one. Only the active layer is at full +// opacity, so the others sit invisible underneath and a phase change reads as a smooth color +// cross-fade rather than a hard swap. `solid` selects the flat (header) vs gradient variant. +const PhaseColorBand = function ({ + phase, + colorsMap, + solid, +}: { + phase: NfcPhase + colorsMap: Record + solid?: boolean +}) { + return ( + + {NFC_PHASES.map((p) => ( + + ))} + + ) +} + +// Dev-only control to preview each phase on a simulator without NFC hardware. "live" releases +// the override and returns to the real session-derived phase. +const PhaseDevStepper = function ({ + current, + colorsMap, + onSelect, +}: { + current: NfcPhase + colorsMap: Record + onSelect: (phase: NfcPhase | undefined) => void +}) { + return ( + + {NFC_PHASES.map((p) => ( + onSelect(p)} + style={[ + $devChip, + { backgroundColor: colorsMap[p], opacity: current === p ? 1 : 0.4 }, + ]} + > + + + ))} + onSelect(undefined)} + style={[$devChip, { backgroundColor: colors.palette.neutral800 }]} + > + + + + ) +} + interface PulsingContactlessIconProps { isNfcEnabled: boolean; size?: number; + color?: string; } export const PulsingContactlessIcon: React.FC = ({ isNfcEnabled, size, + color = 'white', }) => { const scale = useSharedValue(1); @@ -1474,18 +1661,14 @@ export const PulsingContactlessIcon: React.FC = ({ transform: [{ scale: scale.value }], })); - const color = isNfcEnabled - ? colors.palette.success300 - : colors.light.text - return ( ); @@ -1493,8 +1676,8 @@ export const PulsingContactlessIcon: React.FC = ({ const $screen: ViewStyle = { } -const $contentContainer: ViewStyle = { flex: 1, padding: spacing.extraSmall } -const $headerContainer: TextStyle = { alignItems: 'center', paddingVertical: spacing.medium,} +const $contentContainer: ViewStyle = { flex: 1, padding: spacing.extraSmall, marginTop: -spacing.extraLarge * 2 } +const $headerContainer: TextStyle = { alignItems: 'center', paddingVertical: spacing.medium, height: spacing.screenHeight * 0.3} const $buttonContainer: ViewStyle = { flexDirection: 'row', alignSelf: 'center' } const $bottomContainer: ViewStyle = { position: 'absolute', @@ -1565,4 +1748,20 @@ const $cardBalanceRow: ViewStyle = { } const $cardBottomRow: ViewStyle = { marginTop: spacing.small, +} +const $devStepper: ViewStyle = { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'center', + alignSelf: 'center', + marginTop: spacing.small, +} +const $devChip: ViewStyle = { + paddingHorizontal: spacing.small, + paddingVertical: spacing.tiny, + borderRadius: spacing.extraSmall, + margin: spacing.tiny, +} +const $devChipText: TextStyle = { + color: 'white', } \ No newline at end of file diff --git a/src/screens/WalletNameScreen.tsx b/src/screens/WalletNameScreen.tsx index 00c88c0..81401f2 100644 --- a/src/screens/WalletNameScreen.tsx +++ b/src/screens/WalletNameScreen.tsx @@ -1,9 +1,9 @@ import {observer} from 'mobx-react-lite' import React, {FC, useCallback, useState} from 'react' import {TextStyle, View, ViewStyle } from 'react-native' -import { TabView, Route, TabBar } from 'react-native-tab-view' -import {colors, spacing, useThemeColor} from '../theme' -import {Screen, Text} from '../components' +import { TabView, Route } from 'react-native-tab-view' +import {spacing, useThemeColor} from '../theme' +import {Screen, Text, SegmentedTabBar} from '../components' import {RandomName} from './Contacts/RandomName' import {OwnName} from './Contacts/OwnName' import {useStores} from '../models' @@ -41,15 +41,11 @@ export const WalletNameScreen = observer(function WalletNameScreen({ route }: Pr ]) const headerBg = useThemeColor('header') - const activeTabIndicator = colors.palette.accent400 const renderTabBar = (props: any) => ( - + + + ) return ( diff --git a/src/screens/WalletScreen.tsx b/src/screens/WalletScreen.tsx index 9b07e4b..dc569b6 100644 --- a/src/screens/WalletScreen.tsx +++ b/src/screens/WalletScreen.tsx @@ -14,7 +14,7 @@ import { } from 'react-native' import {moderateScale, verticalScale} from '@gocodingnow/rn-size-matters' import { SvgXml } from 'react-native-svg' -import { TabBar, TabView } from 'react-native-tab-view' +import { TabView } from 'react-native-tab-view' import {useThemeColor, spacing, colors, typography} from '../theme' import { Button, @@ -29,7 +29,8 @@ import { ErrorModal, Header, ScanIcon, - MintIcon + MintIcon, + SegmentedTabBar, } from '../components' import EventEmitter from '../utils/eventEmitter' import {useStores} from '../models' @@ -661,32 +662,27 @@ export const WalletScreen = observer(function WalletScreen({ route }: Props) { return null } - const tabWidth = moderateScale(75) + const tabWidth = moderateScale(82) const renderTabBar = useCallback((props: any) => { + // A single unit tab needs no switcher. + if (routes.length <= 1) return null return( - - - ( - - )} - indicatorStyle={{backgroundColor: headerTitleColor}} - style={{backgroundColor: headerBg, shadowColor: 'transparent'}} - /> - + + ( + + )} + /> ) - }, [headerBg, routes.length, tabWidth, headerTitleColor]) + }, [headerBg, routes.length, tabWidth]) const HeaderTitle = function (props: any) { @@ -726,7 +722,8 @@ export const WalletScreen = observer(function WalletScreen({ route }: Props) { return ( -
{ const setSelected = async () => { const balance = proofsStore.getMintBalanceWithMaxBalance(props.mintsByUnit.unit) + if(!balance) return const mint = mintsStore.findByUrl(balance?.mintUrl)