Skip to content

Commit 47d7a2b

Browse files
peachbitsclaude
andcommitted
Zcash: Add Ironwood migration entry points
Three condition-gated surfaces, all dormant until the engine reports the wallet needs (or is running/erred on) the migration - no fork-date gating needed: a warning card on the wallet scene under the sync status card, a per-wallet notification-center card (ironwoodMigration-<walletId>, armed by a new ZcashMigrationService poller; the condition mechanism automatically re-surfaces it if new Orchard funds arrive after completion), and a kebab menu item gated to the zcash plugin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f7d6e81 commit 47d7a2b

7 files changed

Lines changed: 187 additions & 0 deletions

File tree

‎src/actions/WalletListMenuActions.tsx‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export type WalletListMenuKey =
4545
| 'getRawKeys'
4646
| 'rawDelete'
4747
| 'togglePause'
48+
| 'ironwoodMigration'
4849
| string // for split keys like splitbitcoincash, splitethereum, etc.
4950

5051
export function walletListMenuAction(
@@ -76,6 +77,14 @@ export function walletListMenuAction(
7677
}
7778
}
7879

80+
case 'ironwoodMigration': {
81+
return async (dispatch, getState) => {
82+
navigation.navigate('zcashMigrationOptions' as any, {
83+
walletId
84+
})
85+
}
86+
}
87+
7988
case 'rawDelete': {
8089
return async (dispatch, getState) => {
8190
const state = getState()

‎src/components/modals/WalletListMenuModal.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ export const WALLET_LIST_MENU: Array<{
120120
label: lstrings.fragment_wallets_view_private_view_key,
121121
value: 'viewPrivateViewKey'
122122
},
123+
{
124+
pluginIds: ['zcash'],
125+
label: lstrings.zcash_migration_menu_item,
126+
value: 'ironwoodMigration'
127+
},
123128
{
124129
label: lstrings.string_get_raw_keys,
125130
value: 'getRawKeys'

‎src/components/notification/NotificationView.tsx‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { config } from '../../theme/appConfig'
2626
import { useDispatch, useSelector } from '../../types/reactRedux'
2727
import type { NavigationBase } from '../../types/routerTypes'
2828
import { getThemedIconUri } from '../../util/CdnUris'
29+
import { getWalletName } from '../../util/CurrencyWalletHelpers'
2930
import { showOtpReminderModal } from '../../util/otpReminder'
3031
import { openBrowserUri } from '../../util/WebUtils'
3132
import { styled } from '../hoc/styled'
@@ -174,6 +175,18 @@ export const NotificationView: React.FC<Props> = props => {
174175
}
175176
})
176177

178+
// Zcash Ironwood migration cards per wallet
179+
Object.keys(wallets).forEach(walletId => {
180+
const migrationKey = `ironwoodMigration-${walletId}`
181+
if (
182+
notifState[migrationKey] != null &&
183+
!notifState[migrationKey].isBannerHidden &&
184+
!notifState[migrationKey].isCompleted
185+
) {
186+
visibleIds.push(migrationKey)
187+
}
188+
})
189+
177190
if (!otpReminder.isBannerHidden && !account.isDuressAccount)
178191
visibleIds.push('otpReminder')
179192

@@ -327,9 +340,40 @@ export const NotificationView: React.FC<Props> = props => {
327340
)
328341
}
329342

343+
if (id.startsWith('ironwoodMigration-')) {
344+
const walletId = id.slice('ironwoodMigration-'.length)
345+
const wallet = wallets[walletId]
346+
if (wallet == null) return null
347+
348+
const handleCloseMigration = async (): Promise<void> => {
349+
await writeAccountNotifInfo(account, id, { isBannerHidden: true })
350+
}
351+
const handlePressMigration = async (): Promise<void> => {
352+
navigationDebounced.navigate('zcashMigrationOptions' as any, {
353+
walletId
354+
})
355+
}
356+
357+
return (
358+
<NotificationCard
359+
key={id}
360+
type="warning"
361+
title={lstrings.zcash_migration_notif_title}
362+
message={sprintf(
363+
lstrings.zcash_migration_notif_body_1s,
364+
getWalletName(wallet)
365+
)}
366+
onPress={handlePressMigration}
367+
onDismiss={handleCloseMigration}
368+
isPriority={isPriority}
369+
/>
370+
)
371+
}
372+
330373
return null
331374
},
332375
[
376+
account,
333377
dispatch,
334378
detectedTokensRedux,
335379
handle2FaEnabledClose,

‎src/components/scenes/NotificationCenterScene.tsx‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { config } from '../../theme/appConfig'
1515
import { useDispatch, useSelector } from '../../types/reactRedux'
1616
import type { EdgeAppSceneProps, NavigationBase } from '../../types/routerTypes'
1717
import { getThemedIconUri } from '../../util/CdnUris.ts'
18+
import { getWalletName } from '../../util/CurrencyWalletHelpers'
1819
import { showOtpReminderModal } from '../../util/otpReminder.tsx'
1920
import { openBrowserUri } from '../../util/WebUtils.ts'
2021
import { SceneWrapper } from '../common/SceneWrapper'
@@ -242,6 +243,31 @@ export const NotificationCenterScene = (props: Props) => {
242243
onClose={completeNotif(key)}
243244
/>
244245
)
246+
} else if (key.includes('ironwoodMigration')) {
247+
const { params } = notifState[key]
248+
if (params == null) return null
249+
const { walletId } = params
250+
if (walletId == null || wallets[walletId] == null) return null
251+
const wallet = wallets[walletId]
252+
253+
const handlePressMigration = async () => {
254+
navigation.navigate('zcashMigrationOptions' as any, { walletId })
255+
}
256+
257+
return (
258+
<NotificationCenterRow
259+
key={key}
260+
date={date}
261+
type="warning"
262+
title={lstrings.zcash_migration_notif_title}
263+
message={sprintf(
264+
lstrings.zcash_migration_notif_body_1s,
265+
getWalletName(wallet)
266+
)}
267+
onPress={handlePressMigration}
268+
onClose={completeNotif(key)}
269+
/>
270+
)
245271
} else if (key.includes('promoCard-')) {
246272
// Handle promo card notifications
247273
const { promoCard } = notifState[key].params ?? {}

‎src/components/services/Services.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { SortedWalletList } from './SortedWalletList'
4444
import { WalletConnectService } from './WalletConnectService'
4545
import { WalletLifecycle } from './WalletLifecycle'
4646
import { WipeLogsService } from './WipeLogsService'
47+
import { ZcashMigrationService } from './ZcashMigrationService'
4748

4849
interface Props {
4950
navigation: NavigationBase
@@ -160,6 +161,7 @@ export const Services: React.FC<Props> = props => {
160161
<>
161162
{ENV.BETA_FEATURES ? <ActionQueueService /> : null}
162163
{account == null ? null : <NotificationService account={account} />}
164+
{account == null ? null : <ZcashMigrationService account={account} />}
163165
<AutoLogout />
164166
<ContactsLoader />
165167
{account == null ? null : (
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { EdgeAccount } from 'edge-core-js'
2+
import * as React from 'react'
3+
4+
import { useWatch } from '../../hooks/useWatch'
5+
import { getZcashMigrationStatus } from '../../util/zcashMigration'
6+
import { updateNotificationInfo } from './NotificationService'
7+
8+
interface Props {
9+
account: EdgeAccount
10+
}
11+
12+
const POLL_DELAY_MS = 60000
13+
14+
/**
15+
* Watches Zcash wallets for the Orchard -> Ironwood migration condition and
16+
* arms the per-wallet notification-center card (`ironwoodMigration-<id>`).
17+
* The condition mechanism in updateNotificationInfo re-activates the card
18+
* automatically if new Orchard funds arrive after a completed migration.
19+
*/
20+
export const ZcashMigrationService: React.FC<Props> = props => {
21+
const { account } = props
22+
const currencyWallets = useWatch(account, 'currencyWallets')
23+
24+
React.useEffect(() => {
25+
let stopped = false
26+
27+
const check = async (): Promise<void> => {
28+
for (const walletId of Object.keys(currencyWallets)) {
29+
const wallet = currencyWallets[walletId]
30+
if (wallet.currencyInfo.pluginId !== 'zcash') continue
31+
const status = await getZcashMigrationStatus(wallet)
32+
if (stopped || status == null) continue
33+
const isActive = status.state === 'required' || status.state === 'error'
34+
// 'scheduled' is deliberately not a notification: progress lives on
35+
// the wallet scene banner; the center card is the call to action.
36+
await updateNotificationInfo(
37+
account,
38+
`ironwoodMigration-${walletId}`,
39+
isActive,
40+
{ walletId }
41+
).catch(() => {})
42+
}
43+
}
44+
45+
const timerId = setInterval(() => {
46+
check().catch(() => {})
47+
}, POLL_DELAY_MS)
48+
check().catch(() => {})
49+
50+
return () => {
51+
stopped = true
52+
clearInterval(timerId)
53+
}
54+
}, [account, currencyWallets])
55+
56+
return null
57+
}

‎src/components/themed/TransactionListTop.tsx‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { useAsyncValue } from '../../hooks/useAsyncValue'
2424
import { useHandler } from '../../hooks/useHandler'
2525
import { useWalletName } from '../../hooks/useWalletName'
2626
import { useWatch } from '../../hooks/useWatch'
27+
import { useZcashMigrationStatus } from '../../hooks/useZcashMigrationStatus'
2728
import { formatNumber, toPercentString } from '../../locales/intl'
2829
import { lstrings } from '../../locales/strings'
2930
import { getStakePlugins } from '../../plugins/stake-plugins/stakePlugins'
@@ -106,6 +107,7 @@ interface Props {
106107
export const TransactionListTop: React.FC<Props> = props => {
107108
const { isEmpty, searching, tokenId, wallet } = props
108109
const navigation = useAsyncNavigation(props.navigation)
110+
const migrationStatus = useZcashMigrationStatus(wallet)
109111

110112
const dispatch = useDispatch()
111113
const account = useSelector(state => state.core.account)
@@ -665,6 +667,47 @@ export const TransactionListTop: React.FC<Props> = props => {
665667
)
666668
}
667669

670+
/**
671+
* Orchard -> Ironwood migration banner (Zcash only; dormant until the
672+
* engine reports the wallet needs, is running, or erred on the migration).
673+
*/
674+
function renderZcashMigrationCard(): React.ReactElement | null {
675+
if (tokenId != null || migrationStatus == null) return null
676+
const { state, completedTransfers, totalTransfers } = migrationStatus
677+
if (state !== 'required' && state !== 'scheduled' && state !== 'error')
678+
return null
679+
680+
const body =
681+
state === 'required'
682+
? lstrings.zcash_migration_required_body
683+
: state === 'error'
684+
? lstrings.zcash_migration_error_body
685+
: sprintf(
686+
lstrings.zcash_migration_scheduled_body_2s,
687+
completedTransfers,
688+
totalTransfers
689+
)
690+
return (
691+
<AlertCardUi4
692+
body={body}
693+
marginRem={[0.5, 0.5, 0, 0.5]}
694+
title={lstrings.zcash_migration_required_title}
695+
type={state === 'error' ? 'error' : 'warning'}
696+
button={{
697+
label:
698+
state === 'required'
699+
? lstrings.zcash_migration_title
700+
: lstrings.zcash_migration_view_progress,
701+
onPress: () => {
702+
navigation.navigate('zcashMigrationOptions' as any, {
703+
walletId: wallet.id
704+
})
705+
}
706+
}}
707+
/>
708+
)
709+
}
710+
668711
function renderButtons(): React.ReactElement {
669712
const styles = getStyles(theme)
670713
const hideStaking = !isStakingAvailable
@@ -795,6 +838,7 @@ export const TransactionListTop: React.FC<Props> = props => {
795838
{!isStakingAvailable ? null : renderStakedBalance()}
796839
</EdgeCard>
797840
{renderSyncStatus()}
841+
{renderZcashMigrationCard()}
798842
{renderButtons()}
799843
</>
800844
)}

0 commit comments

Comments
 (0)