Skip to content

Add PocketChange UI for Monero wallet#5941

Open
claudinethelobster wants to merge 2 commits intoEdgeApp:developfrom
claudinethelobster:feature/pocketchange-ui
Open

Add PocketChange UI for Monero wallet#5941
claudinethelobster wants to merge 2 commits intoEdgeApp:developfrom
claudinethelobster:feature/pocketchange-ui

Conversation

@claudinethelobster
Copy link

@claudinethelobster claudinethelobster commented Feb 15, 2026

Overview

Adds UI components and localization for PocketChange feature. This PR provides the user-facing interface for configuring PocketChange settings. Requires: edge-currency-monero PR #101 for core functionality.

Changes

New Components

src/components/modals/PocketChangeModal.tsx:

  • Settings modal with toggle to enable/disable PocketChange
  • Slider to select pocket amount (0.1, 0.2, 0.3, 0.5, 0.8, 1.3 XMR)
  • Integrates with Airship modal system
  • TypeScript types: PocketChangeConfig interface

src/locales/strings/enUS.json:

  • Added 11 localization strings:
    • Modal title, description, labels
    • Send confirmation breakdown text
    • Menu item labels

Integration Requirements (Not Yet Implemented)

This PR provides the UI scaffolding. The following integration work is still needed:

1. Add Settings Menu Trigger

Option A: Wallet List Menu (Recommended)

Edit: src/components/modals/WalletListMenuModal.tsx

Add menu item for Monero wallets:

{
  pluginIds: ['monero'],
  label: lstrings.pocketchange_menu_item,
  value: 'pocketChangeSettings'
}

Add action in src/actions/WalletListMenuActions.ts:

case 'pocketChangeSettings':
  const wallet = await account.waitForCurrencyWallet(walletId)
  
  // Load current setting
  const currentSetting = wallet.walletLocalData.pocketChangeSetting ?? {
    enabled: false,
    amountPiconero: '100000000000'
  }
  
  // Convert piconero to index
  const POCKET_AMOUNTS_XMR = [0.1, 0.2, 0.3, 0.5, 0.8, 1.3]
  const amountXMR = Number(currentSetting.amountPiconero) / 1e12
  const amountIndex = POCKET_AMOUNTS_XMR.findIndex(a => Math.abs(a - amountXMR) < 0.01)
  
  // Show modal
  const result = await Airship.show<PocketChangeConfig | undefined>(bridge => (
    <PocketChangeModal
      bridge={bridge}
      initialConfig={{
        enabled: currentSetting.enabled,
        amountIndex: amountIndex >= 0 ? amountIndex : 0
      }}
    />
  ))
  
  // Save result
  if (result != null) {
    const newAmountPiconero = String(POCKET_AMOUNTS_XMR[result.amountIndex] * 1e12)
    await wallet.changeWalletLocalData({
      pocketChangeSetting: {
        enabled: result.enabled,
        amountPiconero: newAmountPiconero
      }
    })
  }
  break

2. Integrate into Send Flow

Edit: src/components/scenes/SendScene.tsx or SendScene2.tsx

Before creating the spend:

// Get user's intended targets
const baseTargets = [{
  publicAddress: recipientAddress,
  nativeAmount: sendAmount
}]

// Apply PocketChange if enabled (Monero only)
let finalTargets = baseTargets
if (wallet.currencyInfo.pluginId === 'monero' && wallet.otherMethods?.getPocketChangeTargetsForSpend) {
  try {
    finalTargets = await wallet.otherMethods.getPocketChangeTargetsForSpend(baseTargets)
  } catch (error) {
    console.warn('PocketChange not available:', error)
  }
}

// Separate for display
const paymentTargets = finalTargets.filter(t => !t.otherParams?.isPocketChange)
const pocketTargets = finalTargets.filter(t => t.otherParams?.isPocketChange)

// Create spend with all targets
const edgeTransaction = await wallet.makeSpend({
  spendTargets: finalTargets,
  ...otherSpendInfo
})

3. Update Send Confirmation UI

Edit: src/components/scenes/SendConfirmationScene.tsx

Show pocket breakdown:

{pocketTargets.length > 0 && (
  <View>
    <Text>{lstrings.pocketchange_send_breakdown_title}</Text>
    <Text>{sprintf(lstrings.pocketchange_send_pocket_count, pocketTargets.length)}</Text>
    <Text>
      {sprintf(
        lstrings.pocketchange_send_total,
        formatCurrencyAmount(sumAmounts(pocketTargets))
      )}
    </Text>
  </View>
)}

Testing

After integration is complete:

  1. Enable PocketChange:

    • Open Monero wallet
    • Access PocketChange settings (via wallet menu)
    • Toggle ON, select 0.1 XMR
    • Save
  2. Send Transaction:

    • Create a send (any amount)
    • Verify confirmation shows pocket breakdown (e.g., "6 pockets, 0.6 XMR total")
    • Confirm transaction
  3. Verify Immediate Spend:

    • Immediately send again (before 20 min)
    • Should succeed using unlocked pocket funds

Related

Files Modified

  • src/components/modals/PocketChangeModal.tsx - NEW (103 lines)
  • src/locales/strings/enUS.json - 11 new strings

Files Requiring Integration (TODO)

  • src/components/modals/WalletListMenuModal.tsx OR src/components/scenes/CurrencySettingsScene.tsx
  • src/actions/WalletListMenuActions.ts
  • src/components/scenes/SendScene.tsx (or SendScene2.tsx)
  • src/components/scenes/SendConfirmationScene.tsx

Notes

This PR is intentionally minimal (UI components only) to keep the review scope manageable. Integration work can be done as a follow-up or included before merge depending on preference.


- 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
@claudinethelobster
Copy link
Author

fixup! Add PocketChange UI components and strings

Co-authored-by: Cursor <cursoragent@cursor.com>
paullinator

This comment was marked as resolved.

@paullinator paullinator dismissed their stale review February 24, 2026 04:30

Replaced by review with inline comments

paullinator

This comment was marked as outdated.

@paullinator paullinator dismissed their stale review February 24, 2026 04:33

Test review — disregard

Copy link

@eddy-edge eddy-edge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Findings

  • critical: Unsquashed fixup! commit present: 7dfda3616 fixup! Add PocketChange UI components and strings. Fixup commits must be squashed before merging.
    • Run git rebase -i --autosquash HEAD~2 to squash the fixup into acda95216, then force-push.

import { EdgeText, Paragraph } from '../themed/EdgeText'
import { EdgeModal } from './EdgeModal'

const POCKET_AMOUNTS_XMR = [0.1, 0.2, 0.3, 0.5, 0.8, 1.3]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Floating-point numbers used for crypto amounts: POCKET_AMOUNTS_XMR = [0.1, 0.2, 0.3, 0.5, 0.8, 1.3]. JavaScript IEEE-754 floats are imprecise and should never be used to represent cryptocurrency denominations.

Recommendation: Store amounts as strings in the coin's smallest unit (piconero for XMR), or at minimum as integer multipliers of a fixed base unit. Convert to display strings using the same div/bignum helpers used elsewhere in the codebase (e.g., div(nativeAmount, multiplier, DECIMAL_PRECISION)).

]

export function WalletListMenuModal(props: Props) {
export function WalletListMenuModal(props: Props): React.ReactElement {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning: WalletListMenuModal is exported as a plain function declaration (export function WalletListMenuModal(props: Props): React.ReactElement) rather than the project-convention React.FC<Props> pattern.

Recommendation: Change to export const WalletListMenuModal: React.FC<Props> = props => { ... }

const styles = getStyles(theme)

const handleCancel = () => {
const handleCancel = (): void => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning: handleCancel is a plain arrow function not wrapped in useHandler, inconsistent with all other handlers in this file and the codebase convention.

Recommendation: Wrap with useHandler: const handleCancel = useHandler((): void => { props.bridge.resolve() })

result != null &&
wallet.otherMethods?.setPocketChangeSetting != null
) {
await wallet.otherMethods.setPocketChangeSetting(result)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning: In the pocketChange success path, setLoadingOption(null) is never called before bridge.resolve(). If the modal stays mounted briefly (animation, test), the loading spinner remains stuck. The error path correctly resets it.

Recommendation: Add setLoadingOption(null) before bridge.resolve() in the success path, or use a finally block.

await wallet.otherMethods.setPocketChangeSetting(result)
}
bridge.resolve()
} catch (error) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning: catch (error) is missing the unknown type annotation, leaving error implicitly typed as any.

Recommendation: Change to catch (error: unknown).

pocketchange_per_pocket: 'per pocket',
pocketchange_explainer:
'When enabled, received funds will automatically be split into pockets of the selected size.',
pocketchange_menu_item: 'Pocket Change',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: pocketchange_menu_item and pocketchange_line_item both have the identical value 'Pocket Change'. Two keys for the same string means translators must translate it twice.

Recommendation: Evaluate whether both usage sites truly need independent strings. If not, consolidate to a single key (e.g., reuse pocketchange_title).

}
}

// Expand spend targets with pocket change outputs if supported

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The comment // Expand spend targets with pocket change outputs if supported describes what the code does (obvious) rather than why. The protocol contract (otherParams?.isPocketChange === true) is undocumented.

Recommendation: Replace with a why-comment documenting the protocol: e.g., 'The plugin may append extra change outputs to minimise UTXO dust. Plugin-added outputs are tagged with otherParams.isPocketChange === true so the UI can display them separately.'


<View style={styles.stepperRow}>
<EdgeButton
label="−"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: "−" and "+" button labels are hardcoded strings not sourced from locale strings.

Recommendation: Add pocketchange_decrease / pocketchange_increase keys (or reuse existing generic symbols) in both locale files and reference via lstrings.

setEnabled(prev => !prev)
})

const handleDecrease = useHandler((): void => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: No unit tests cover the stepper bounds logic (handleDecrease, handleIncrease) or the Save/Cancel flow.

Recommendation: Add a test file at src/__tests__/modals/PocketChangeModal.test.tsx covering: initial state, upper bound clamping, lower bound clamping, Save resolving with config, Cancel resolving with undefined.


// Expand spend targets with pocket change outputs if supported
let finalSpendInfo = spendInfo
if (coreWallet.otherMethods?.getPocketChangeTargetsForSpend != null) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The pocket-change expansion path in the makeSpend effect has no test coverage. Only the fallback (else branch) is implicitly covered by existing tests that use wallets without getPocketChangeTargetsForSpend.

Recommendation: Add a test with a fake wallet mock that provides getPocketChangeTargetsForSpend, asserting that pocketChangeTargets is populated with only isPocketChange-tagged targets and that finalSpendInfo.spendTargets is expanded correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants