Skip to content

Commit ef1d4c5

Browse files
Add PocketChange UI components and strings
- Create PocketChangeModal component with toggle and slider - Add English localization strings for PocketChange - Modal allows enabling/disabling and configuring pocket amount (0.1-1.3 XMR) - Integrates with Airship modal system Integration TODO: - Add modal trigger to Monero wallet settings/menu - Update SendScene to call wallet.otherMethods.getPocketChangeTargetsForSpend() - Show pocket breakdown in send confirmation - Save config to wallet.walletLocalData.pocketChangeSetting Made-with: Cursor
1 parent cc8bc66 commit ef1d4c5

7 files changed

Lines changed: 259 additions & 18 deletions

File tree

eslint.config.mjs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export default [
107107
'src/actions/TransactionExportActions.tsx',
108108
'src/actions/WalletActions.tsx',
109109
'src/actions/WalletListActions.tsx',
110-
'src/actions/WalletListMenuActions.tsx',
110+
111111
'src/app.ts',
112112
'src/components/buttons/ButtonsView.tsx',
113113
'src/components/buttons/EdgeSwitch.tsx',
@@ -202,7 +202,6 @@ export default [
202202
'src/components/modals/SwapVerifyTermsModal.tsx',
203203
'src/components/modals/TextInputModal.tsx',
204204
'src/components/modals/TransferModal.tsx',
205-
'src/components/modals/WalletListMenuModal.tsx',
206205

207206
'src/components/modals/WalletListSortModal.tsx',
208207
'src/components/modals/WcSmartContractModal.tsx',

src/actions/WalletListMenuActions.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { toggleUserPausedWallet } from './SettingsActions'
3131

3232
export type WalletListMenuKey =
3333
| 'settings'
34+
| 'pocketChange'
3435
| 'rename'
3536
| 'delete'
3637
| 'resync'
@@ -65,6 +66,11 @@ export function walletListMenuAction(
6566
})
6667
}
6768
}
69+
case 'pocketChange': {
70+
return async () => {
71+
// Handled directly in WalletListMenuModal via Airship
72+
}
73+
}
6874
case 'manageTokens': {
6975
return async (dispatch, getState) => {
7076
navigation.navigate('manageTokens', {
@@ -79,7 +85,7 @@ export function walletListMenuAction(
7985
const { account } = state.core
8086
account
8187
.changeWalletStates({ [walletId]: { deleted: true } })
82-
.catch(error => {
88+
.catch((error: unknown) => {
8389
showError(error)
8490
})
8591
}
@@ -118,8 +124,8 @@ export function walletListMenuAction(
118124
try {
119125
const fioAddresses =
120126
await engine.otherMethods.getFioAddressNames()
121-
fioAddress = fioAddresses.length ? fioAddresses[0] : ''
122-
} catch (e: any) {
127+
fioAddress = fioAddresses.length > 0 ? fioAddresses[0] : ''
128+
} catch (e: unknown) {
123129
fioAddress = ''
124130
}
125131
}
@@ -129,7 +135,7 @@ export function walletListMenuAction(
129135
let additionalMsg: string | undefined
130136
let tokenCurrencyCode: string | undefined
131137
if (tokenId == null) {
132-
if (fioAddress) {
138+
if (fioAddress !== '') {
133139
additionalMsg =
134140
lstrings.fragmet_wallets_delete_fio_extra_message_mobile
135141
} else if (Object.keys(wallet.currencyConfig.allTokens).length > 0) {
@@ -155,7 +161,7 @@ export function walletListMenuAction(
155161
)} ${wallet.type} ${wallet.id}`
156162
)
157163
})
158-
.catch(error => {
164+
.catch((error: unknown) => {
159165
showError(error)
160166
})
161167

@@ -176,7 +182,7 @@ export function walletListMenuAction(
176182
} ${tokenId}`
177183
)
178184
})
179-
.catch(error => {
185+
.catch((error: unknown) => {
180186
showError(error)
181187
})
182188
}
@@ -297,8 +303,8 @@ export function walletListMenuAction(
297303
)
298304
// Add a copy button only for development
299305
let devButtons = {}
300-
// @ts-expect-error
301-
if (global.__DEV__)
306+
// @ts-expect-error - __DEV__ is a RN global not in TS types
307+
if (global.__DEV__ === true)
302308
devButtons = {
303309
copy: { label: lstrings.fragment_wallets_copy_seed }
304310
}
@@ -313,8 +319,8 @@ export function walletListMenuAction(
313319
buttons={{ ok: { label: lstrings.string_ok_cap }, ...devButtons }}
314320
/>
315321
)).then(buttonPressed => {
316-
// @ts-expect-error
317-
if (global.__DEV__ && buttonPressed === 'copy') {
322+
// @ts-expect-error - __DEV__ is a RN global not in TS types
323+
if (global.__DEV__ === true && buttonPressed === 'copy') {
318324
Clipboard.setString(privateKey)
319325
showToast(lstrings.fragment_wallets_copied_seed)
320326
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import * as React from 'react'
2+
import { View } from 'react-native'
3+
import type { AirshipBridge } from 'react-native-airship'
4+
5+
import { useHandler } from '../../hooks/useHandler'
6+
import { lstrings } from '../../locales/strings'
7+
import { EdgeButton } from '../buttons/EdgeButton'
8+
import { cacheStyles, type Theme, useTheme } from '../services/ThemeContext'
9+
import { SettingsHeaderRow } from '../settings/SettingsHeaderRow'
10+
import { SettingsSwitchRow } from '../settings/SettingsSwitchRow'
11+
import { EdgeText, Paragraph } from '../themed/EdgeText'
12+
import { EdgeModal } from './EdgeModal'
13+
14+
const POCKET_AMOUNTS_XMR = [0.1, 0.2, 0.3, 0.5, 0.8, 1.3]
15+
16+
export interface PocketChangeConfig {
17+
enabled: boolean
18+
amountIndex: number
19+
}
20+
21+
interface Props {
22+
bridge: AirshipBridge<PocketChangeConfig | undefined>
23+
initialConfig: PocketChangeConfig
24+
}
25+
26+
export const PocketChangeModal: React.FC<Props> = props => {
27+
const { bridge, initialConfig } = props
28+
const theme = useTheme()
29+
const styles = getStyles(theme)
30+
const [enabled, setEnabled] = React.useState(initialConfig.enabled)
31+
const [amountIndex, setAmountIndex] = React.useState(
32+
initialConfig.amountIndex
33+
)
34+
35+
const handleCancel = useHandler((): void => {
36+
bridge.resolve(undefined)
37+
})
38+
39+
const handleSave = useHandler((): void => {
40+
bridge.resolve({ enabled, amountIndex })
41+
})
42+
43+
const handleToggle = useHandler((): void => {
44+
setEnabled(prev => !prev)
45+
})
46+
47+
const handleDecrease = useHandler((): void => {
48+
setAmountIndex(prev => Math.max(0, prev - 1))
49+
})
50+
51+
const handleIncrease = useHandler((): void => {
52+
setAmountIndex(prev => Math.min(POCKET_AMOUNTS_XMR.length - 1, prev + 1))
53+
})
54+
55+
return (
56+
<EdgeModal
57+
bridge={bridge}
58+
onCancel={handleCancel}
59+
title={lstrings.pocketchange_title}
60+
>
61+
<View style={styles.container}>
62+
<Paragraph>{lstrings.pocketchange_description}</Paragraph>
63+
64+
<SettingsSwitchRow
65+
label={lstrings.pocketchange_enable}
66+
value={enabled}
67+
onPress={handleToggle}
68+
/>
69+
70+
{enabled ? (
71+
<>
72+
<SettingsHeaderRow label={lstrings.pocketchange_amount_header} />
73+
74+
<View style={styles.stepperRow}>
75+
<EdgeButton
76+
label="−"
77+
type="secondary"
78+
mini
79+
onPress={handleDecrease}
80+
disabled={amountIndex <= 0}
81+
/>
82+
<EdgeText style={styles.amountText}>
83+
{POCKET_AMOUNTS_XMR[amountIndex]} XMR{' '}
84+
{lstrings.pocketchange_per_pocket}
85+
</EdgeText>
86+
<EdgeButton
87+
label="+"
88+
type="secondary"
89+
mini
90+
onPress={handleIncrease}
91+
disabled={amountIndex >= POCKET_AMOUNTS_XMR.length - 1}
92+
/>
93+
</View>
94+
95+
<Paragraph>{lstrings.pocketchange_explainer}</Paragraph>
96+
</>
97+
) : null}
98+
99+
<View style={styles.saveButton}>
100+
<EdgeButton label={lstrings.pocketchange_save} onPress={handleSave} />
101+
</View>
102+
</View>
103+
</EdgeModal>
104+
)
105+
}
106+
107+
const getStyles = cacheStyles((theme: Theme) => ({
108+
container: {
109+
paddingHorizontal: theme.rem(0.5)
110+
},
111+
stepperRow: {
112+
flexDirection: 'row',
113+
alignItems: 'center',
114+
justifyContent: 'center',
115+
paddingVertical: theme.rem(0.5)
116+
},
117+
amountText: {
118+
fontSize: theme.rem(1.1),
119+
marginHorizontal: theme.rem(1)
120+
},
121+
saveButton: {
122+
marginTop: theme.rem(1),
123+
marginBottom: theme.rem(0.5)
124+
}
125+
}))

src/components/modals/WalletListMenuModal.tsx

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,12 @@ import {
2626
import { getWalletName } from '../../util/CurrencyWalletHelpers'
2727
import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity'
2828
import { CryptoIcon } from '../icons/CryptoIcon'
29-
import { showError } from '../services/AirshipInstance'
29+
import { Airship, showError } from '../services/AirshipInstance'
3030
import { cacheStyles, type Theme, useTheme } from '../services/ThemeContext'
3131
import { UnscaledText } from '../text/UnscaledText'
3232
import { ModalTitle } from '../themed/ModalParts'
3333
import { EdgeModal } from './EdgeModal'
34+
import { type PocketChangeConfig, PocketChangeModal } from './PocketChangeModal'
3435

3536
interface Option {
3637
value: WalletListMenuKey
@@ -53,6 +54,7 @@ const icons: Record<string, string> = {
5354
getSeed: 'key',
5455
goToParent: 'upcircleo',
5556
manageTokens: 'plus',
57+
pocketChange: 'wallet',
5658
rawDelete: 'warning',
5759
rename: 'edit',
5860
resync: 'sync',
@@ -75,6 +77,11 @@ export const WALLET_LIST_MENU: Array<{
7577
label: lstrings.settings_asset_settings,
7678
value: 'settings'
7779
},
80+
{
81+
pluginIds: ['monero'],
82+
label: lstrings.pocketchange_menu_item,
83+
value: 'pocketChange'
84+
},
7885
{
7986
label: lstrings.string_rename,
8087
value: 'rename'
@@ -142,7 +149,7 @@ export const WALLET_LIST_MENU: Array<{
142149
}
143150
]
144151

145-
export function WalletListMenuModal(props: Props) {
152+
export function WalletListMenuModal(props: Props): React.ReactElement {
146153
const { bridge, tokenId, navigation, walletId } = props
147154

148155
const [options, setOptions] = React.useState<Option[]>([])
@@ -161,13 +168,41 @@ export function WalletListMenuModal(props: Props) {
161168
const theme = useTheme()
162169
const styles = getStyles(theme)
163170

164-
const handleCancel = () => {
171+
const handleCancel = (): void => {
165172
props.bridge.resolve()
166173
}
167174

168175
const optionAction = useHandler(async (option: WalletListMenuKey) => {
169176
if (loadingOption != null) return // Prevent multiple actions
170177

178+
if (option === 'pocketChange' && wallet != null) {
179+
setLoadingOption(option)
180+
try {
181+
let initialConfig: PocketChangeConfig = {
182+
enabled: false,
183+
amountIndex: 2
184+
}
185+
if (wallet.otherMethods?.getPocketChangeSetting != null) {
186+
const saved = await wallet.otherMethods.getPocketChangeSetting()
187+
if (saved != null) initialConfig = saved
188+
}
189+
const result = await Airship.show<PocketChangeConfig | undefined>(b => (
190+
<PocketChangeModal bridge={b} initialConfig={initialConfig} />
191+
))
192+
if (
193+
result != null &&
194+
wallet.otherMethods?.setPocketChangeSetting != null
195+
) {
196+
await wallet.otherMethods.setPocketChangeSetting(result)
197+
}
198+
bridge.resolve()
199+
} catch (error) {
200+
setLoadingOption(null)
201+
showError(error)
202+
}
203+
return
204+
}
205+
171206
setLoadingOption(option)
172207
try {
173208
await dispatch(

0 commit comments

Comments
 (0)