Zcash: Orchard → Ironwood (NU6.3) migration UX - #6077
Conversation
dae76da to
58a1bb5
Compare
There was a problem hiding this comment.
Warning
- Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.
Pull request overview
Adds the first-pass (v1) Zcash Orchard → Ironwood (NU6.3) migration UX: detect when an Orchard-only sweep is worthwhile, surface a recommendation card on the wallet scene, and route the user into a locked “send max to self” flow using the existing SendScene2.
Changes:
- Introduces a typed migration-status util (cleaners) plus a polling hook to safely query
wallet.otherMethods.getMigrationStatus(iOS-gated). - Adds a Zcash-only “migration recommended” card to the wallet scene that navigates into
send2with locked address/amount/wallet tiles and anotherParams.ironwoodMigrationflag. - Adds localized strings + a Jest test suite for the migration util, and updates the changelog (plus dependency pin updates in excluded manifests).
Reviewed changes
Copilot reviewed 6 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/util/zcashMigration.ts | Typed + validated access to engine migration status, with platform gating and error fallback. |
| src/hooks/useZcashMigrationStatus.ts | Polling hook wrapper around getZcashMigrationStatus for unconditional use in shared UI. |
| src/components/themed/TransactionListTop.tsx | Renders the migration card and wires navigation into a locked SendScene2 send-to-self sweep flow. |
| src/locales/en_US.ts | Adds en_US string entries for the migration card and send info tile. |
| src/tests/zcashMigration.test.ts | Adds tests for the util’s capability gating and cleaner validation. |
| CHANGELOG.md | Notes the new migration card + send-to-self flow and iOS-only gating. |
| package.json | Updated (content excluded by policy). |
| package-lock.json | Updated (content excluded by policy). |
| src/locales/strings/enUS.json | Updated (content excluded by policy). |
Files excluded by content exclusion policy (3)
- package-lock.json
- package.json
- src/locales/strings/enUS.json
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
58a1bb5 to
ffae678
Compare
There was a problem hiding this comment.
Warning
- Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.
Pull request overview
Copilot reviewed 6 out of 9 changed files in this pull request and generated 1 comment.
Files excluded by content exclusion policy (3)
- package-lock.json
- package.json
- src/locales/strings/enUS.json
Comments suppressed due to low confidence (2)
src/locales/en_US.ts:776
zcash_migration_recommended_body_1ssays "%s of your ZEC", but the caller passes a formatted string that already includes the denomination (e.g. "1.23 ZEC"), resulting in awkward text like "1.23 ZEC of your ZEC".
zcash_migration_recommended_body_1s:
"Zcash's Ironwood network upgrade added a new shielded pool, and %s of your ZEC is still in the older Orchard pool. We recommend moving it with a single send to yourself. The amount you move will be publicly visible on the blockchain. Your funds stay safe and spendable either way.",
src/components/themed/TransactionListTop.tsx:706
- If
getAddressesdoesn't return aunifiedAddress, the button becomes a no-op with no user feedback. Throwing an error here will surface a modal via the existing.catch(showError)handler.
const unifiedAddress = addresses.find(
address => address.addressType === 'unifiedAddress'
)
if (unifiedAddress == null) return
const spendInfo: EdgeSpendInfo = {
ffae678 to
b3b206d
Compare
There was a problem hiding this comment.
Warning
- Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.
Pull request overview
Copilot reviewed 6 out of 9 changed files in this pull request and generated 1 comment.
Files excluded by content exclusion policy (3)
- package-lock.json
- package.json
- src/locales/strings/enUS.json
Comments suppressed due to low confidence (2)
src/hooks/useZcashMigrationStatus.ts:22
- This project generally uses the custom
useHandlerhook instead ofReact.useCallback(seesrc/hooks/useHandler.tsand e.g.src/hooks/useMount.ts:11) to keep callback identity stable without dependency churn. UsinguseHandlerhere keeps the polling callback stable even if the wallet object reference changes unexpectedly.
const refresher = React.useCallback(
async () => await getZcashMigrationStatus(wallet),
[wallet]
)
src/tests/zcashMigration.test.ts:5
- These tests implicitly rely on whatever
Platform.OSthe Jest RN environment provides. SincegetZcashMigrationStatusgates onPlatform.OS, the first test can become flaky if the test environment changes (e.g. runs as 'web'). Mockreact-nativePlatform.OS in this test to make the expectations deterministic.
import { describe, expect, it } from '@jest/globals'
import type { EdgeCurrencyWallet } from 'edge-core-js'
import { getZcashMigrationStatus } from '../util/zcashMigration'
861507d to
707b62a
Compare
There was a problem hiding this comment.
Warning
- Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.
Pull request overview
Copilot reviewed 6 out of 9 changed files in this pull request and generated no new comments.
Files excluded by content exclusion policy (3)
- package-lock.json
- package.json
- src/locales/strings/enUS.json
Comments suppressed due to low confidence (5)
src/components/themed/TransactionListTop.tsx:706
- If
getAddressesdoesn't return aunifiedAddress, the button press silently no-ops. From the user's perspective this looks like a broken button; it would be better to surface an error (caught by the existing.catch(showError)) so the user gets feedback and we get diagnostics.
const unifiedAddress = addresses.find(
address => address.addressType === 'unifiedAddress'
)
if (unifiedAddress == null) return
src/locales/en_US.ts:776
- The
%ssubstitution passed tozcash_migration_recommended_body_1salready includes the denomination (e.g. "0.123 ZEC"), but the string also says "of your ZEC". This produces awkward duplicated output like "0.123 ZEC of your ZEC".
zcash_migration_recommended_body_1s:
"Zcash's Ironwood network upgrade added a new shielded pool, and %s of your ZEC is still in the older Orchard pool. We recommend moving it with a single send to yourself. The amount you move will be publicly visible on the blockchain. Your funds stay safe and spendable either way.",
src/components/themed/TransactionListTop.tsx:698
migrationStatus.remainingOrchardZatoshiis treated as a numeric string and passed intobiggystring.div. If the engine ever returns a non-integer string (or an empty string),divcan throw during render and crash the wallet scene. Consider guarding the value (or validating it in the status cleaner) before formatting.
const orchardDisplayAmount = formatNumber(
div(
migrationStatus.remainingOrchardZatoshi,
displayDenomination.multiplier,
DECIMAL_PRECISION
src/util/zcashMigration.ts:62
getZcashMigrationStatusis intended to be polled. Logging a warning on every failed poll can spam production logs and make real issues harder to find. Consider gating this warning to dev builds (or throttling it).
console.warn('getZcashMigrationStatus failed', error)
CHANGELOG.md:5
- This entry says the migration card is "iOS only until the Android SDK ships Ironwood", but the PR description says the Android SDK is published and the feature is enabled on both platforms. The changelog line should be updated to match the actual behavior shipped by this PR.
- added: Zcash: Orchard -> Ironwood (NU6.3) migration card on the wallet scene - when the engine reports a sweep is worthwhile, it prefills a locked max send-to-self through the ordinary send scene (recommended-tone: funds stay spendable either way). iOS only until the Android SDK ships Ironwood.
707b62a to
5b0ad4a
Compare
There was a problem hiding this comment.
Warning
- Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.
Pull request overview
Copilot reviewed 6 out of 9 changed files in this pull request and generated no new comments.
Files excluded by content exclusion policy (3)
- package-lock.json
- package.json
- src/locales/strings/enUS.json
Comments suppressed due to low confidence (2)
src/hooks/useZcashMigrationStatus.ts:24
useRefresherschedules a timer and then also triggers an immediate refresh whendefaultValue == null, which results in two concurrent polling loops when called asuseRefresher(..., undefined, delay). In this hook that means double (and potentially compounding)getMigrationStatuscalls and extra renders.
const refresher = React.useCallback(
async () => await getZcashMigrationStatus(wallet),
[wallet]
)
return useRefresher(refresher, undefined, delay)
src/components/themed/TransactionListTop.tsx:727
- If
getMaxSpendablereturns'0'(e.g. Orchard balance is entirely consumed by the fee, or engine returns 0 for any reason), this will navigate tosend2with a locked zero amount. That creates a dead-end UX and may surface confusing send errors. Consider seedingnativeAmount: '0'(same pattern as SendScene2) and explicitly blocking/throwing on a'0'max spendable result before navigating.
const spendInfo: EdgeSpendInfo = {
tokenId: null,
spendTargets: [{ publicAddress: unifiedAddress.publicAddress }],
metadata: { notes: lstrings.zcash_migration_tx_notes },
// Top-level otherParams reaches the engine intact (per-target
There was a problem hiding this comment.
Warning
- Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.
Pull request overview
Copilot reviewed 8 out of 11 changed files in this pull request and generated no new comments.
Files excluded by content exclusion policy (3)
- package-lock.json
- package.json
- src/locales/strings/enUS.json
Comments suppressed due to low confidence (2)
src/components/themed/TransactionListTop.tsx:728
- If
getMaxSpendablereturns'0'(e.g. fee >= remaining Orchard balance due to a race or fee change), this will navigate to SendScene2 with the amount tile locked, leaving the user unable to adjust the amount and likely hitting an error later. Add an explicit guard before navigating.
const migrationAmount = await wallet.getMaxSpendable(spendInfo)
navigation.push('send2', {
src/util/zcashMigration.ts:58
getZcashMigrationStatusis intended to be polled; logging aconsole.warnon every failure can create noisy logs and performance/telemetry issues if the engine method throws repeatedly (e.g. during temporary engine states). Consider removing the warning or otherwise throttling it.
} catch (error) {
console.warn('getZcashMigrationStatus failed', error)
return undefined
}
There was a problem hiding this comment.
Warning
- Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.
Pull request overview
Copilot reviewed 8 out of 11 changed files in this pull request and generated no new comments.
Files excluded by content exclusion policy (3)
- package-lock.json
- package.json
- src/locales/strings/enUS.json
Comments suppressed due to low confidence (3)
src/hooks/useZcashMigrationStatus.ts:22
- This hook uses
React.useCallbackeven though the project hasuseHandlerspecifically as a betteruseCallback(stable identity without dependency arrays). UsinguseHandlerhere avoids restarting theuseRefreshereffect/timer when thewalletobject identity changes and matches the established pattern elsewhere in the repo.
const refresher = React.useCallback(
async () => await getZcashMigrationStatus(wallet),
[wallet]
)
src/util/zcashMigration.ts:41
isMigrationCapableassumeswallet.otherMethodsalways exists. In unit tests and potentially some wallet-like objects,otherMethodscan be missing, which causesgetZcashMigrationStatusto reject and surfaces an avoidable error viauseRefresher'sshowErrorpath. GuardotherMethodswith optional chaining so non-migration wallets cleanly returnundefined.
const isMigrationCapable = (wallet: EdgeCurrencyWallet): boolean =>
wallet.currencyInfo.pluginId === 'zcash' &&
typeof wallet.otherMethods.getMigrationStatus === 'function'
src/components/cards/ZcashMigrationCard.tsx:85
- The card body uses
EdgeTextwithnumberOfLines={10}and the defaultadjustsFontSizeToFitbehavior, which can shrink the paragraph (and potentially truncate the inline “Learn more” link) on smaller screens / longer locales. Since this copy is already paragraph-length, prefer unlimited lines and disable font-size-to-fit shrinking for readability.
<EdgeText style={styles.text} numberOfLines={10}>
{sprintf(
lstrings.zcash_migration_recommended_body_1s,
orchardBalanceText
)}{' '}
<EdgeText style={styles.learnMoreLink} onPress={handleLearnMore}>
{lstrings.zcash_migration_learn_more_button}
</EdgeText>
</EdgeText>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d7a04bc. Configure here.
d7a04bc to
e7ad9e6
Compare
e7ad9e6 to
33e295b
Compare
There was a problem hiding this comment.
Claude Code Review
Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.
Tip: disable this comment in your organization's Code Review settings.
There was a problem hiding this comment.
Warning
- Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.
Pull request overview
Copilot reviewed 11 out of 14 changed files in this pull request and generated no new comments.
Files excluded by content exclusion policy (3)
- package-lock.json
- package.json
- src/locales/strings/enUS.json
Suppressed comments (1)
src/util/zcashMigration.ts:28
remainingOrchardZatoshiis later passed intobiggystring.div(...), but the cleaner currently only usesasString, which allows non-numeric strings (e.g. "abc"). If the engine ever returns an unexpected value, this can survive cleaning and then throw at render-time when formatting the card. Consider validating this field as a numeric biggystring (using the repo'sasBiggystring) so malformed engine data safely fails cleaning and the UI simply suppresses the card.
export const asZcashMigrationStatus = asObject({
state: asValue('notNeeded', 'required', 'scheduled', 'complete', 'error'),
completedTransfers: asMaybe(asNumber, 0),
totalTransfers: asMaybe(asNumber, 0),
remainingOrchardZatoshi: asMaybe(asString, '0'),
| // an item leaves the ones after it parked at their old offsets, | ||
| // a full item-width off-screen. Remounting on a slot change | ||
| // establishes the transform fresh, which is always correct. | ||
| key={`${itemIndex}-${keyExtractor(item, itemIndex)}`} |
There was a problem hiding this comment.
The previous issue was a PITA too here. Great catch, thanks.
33e295b to
ea565cc
Compare
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
|
Blocking integration finding from local verification of this branch (full details, since the test evidence comment references it). The iOS link fails with 20,117 duplicate symbols when this gui branch is built against Cause: So both a CocoaPods
To finish the migration test I unlinked piratechain locally (uncommitted): A real resolution is needed before this can land, e.g. give piratechain the same vendored-binary treatment, or have zcash consume the CocoaPods gRPC-Swift rather than vendoring it, or dedupe the link inputs in the Podfile. Flagging rather than guessing at the right shape, since it spans both native modules. |
ea565cc to
a02ab5a
Compare
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
There was a problem hiding this comment.
Claude Code Review
Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.
Tip: disable this comment in your organization's Code Review settings.
There was a problem hiding this comment.
Warning
- Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.
Pull request overview
Copilot reviewed 11 out of 12 changed files in this pull request and generated no new comments.
Files excluded by content exclusion policy (1)
- src/locales/strings/enUS.json
Suppressed comments (2)
src/util/zcashMigration.ts:57
getZcashMigrationStatusis used from a polling hook (default 10s). Logging aconsole.warnon every transient engine / cleaner failure can spam device logs and error reporting, especially if the engine changes the status shape or throws repeatedly. Consider gating the warning to dev builds (or otherwise throttling) since the function already degrades toundefined.
} catch (error) {
console.warn('getZcashMigrationStatus failed', error)
return undefined
src/components/cards/ZcashMigrationCard.tsx:88
- The inline "Learn more" text is tappable but doesn’t declare a link/button role, which can make it harder to discover with screen readers. Adding
accessibilityRole="link"will improve accessibility without changing UI behavior.
<EdgeText style={styles.learnMoreLink} onPress={handleLearnMore}>
{lstrings.zcash_migration_learn_more_button}
</EdgeText>
Typed access to the engine's v1 Orchard -> Ironwood detection surface (wallet.otherMethods.getMigrationStatus, cleaner-validated) plus a polling hook safe to call unconditionally from shared scenes. iOS-gated until the Android bridge lands. Strings use a recommended tone by design: the sweep is worthwhile, not mandatory - Orchard stays spendable post-fork and drains passively through ordinary spends, so nothing is framed as "required". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Zcash wallet scene's migration card (shown when the engine reports a sweep is worthwhile) prefills the ordinary send scene with the wallet's own unified address and its max spendable amount, with the address/amount/wallet tiles locked - the account-activation locked-send pattern. Post-activation, plain proposals route outputs and change into Ironwood, so this is a normal transaction through the normal pipeline: fee and final amount come from the engine's makeSpend, and the tx is labeled via metadata notes. Copy is deliberately recommended-tone, not required: Orchard stays spendable post-fork and drains passively through ordinary spends. The card clears via the status poll once Orchard empties by any means (sweep sent, or drained organically). A max send sweeps every shielded pool into Ironwood - the strings say "shielded ZEC", not just Orchard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An item's whole on-screen position comes from an animated transform derived from its index, while its React key is the item's own id. When the card list shrinks, a surviving item shifts to a lower slot without remounting, and the transform is never re-applied: the view stays parked at its old offset, a full item-width off-screen. The container keeps its fixed height and the pagination dots do not depend on width, so what is left on screen is an empty gap with a lone dot. Make the slot part of the item's identity so a shift remounts it, which establishes the transform fresh. Verified on an Android emulator and an iOS simulator. Not Zcash-specific: the trigger is the info server's `noBalance` card being filtered out once `useIsAccountFunded` resolves, which races the carousel mounting. It surfaces here because the Ironwood migration card changes the wallet scene's height, but it can strand a card on any scene whose carousel list shrinks after mount. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a02ab5a to
7ba8593
Compare
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
There was a problem hiding this comment.
Warning
- Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.
Pull request overview
Copilot reviewed 11 out of 12 changed files in this pull request and generated no new comments.
Files excluded by content exclusion policy (1)
- src/locales/strings/enUS.json
Suppressed comments (1)
src/hooks/useZcashMigrationStatus.ts:24
useRefresherschedules asetTimeout(handleTimeout, delay)and then (whendefaultValue == null) immediately callshandleTimeout()again. WithdefaultValueset toundefinedhere, that can create two concurrent polling timers (double-callinggetZcashMigrationStatusevery interval). Consider inlining the polling logic in this hook so you can do an immediate fetch without also scheduling an initial delayed timer.
const refresher = React.useCallback(
async () => await getZcashMigrationStatus(wallet),
[wallet]
)
return useRefresher(refresher, undefined, delay)






Zcash Ironwood (NU6.3) v1: migration card → locked max send-to-self
Pairs with react-native-zcash#71 and edge-currency-accountbased#1075 (both v1), on the released SDK 2.7.0-rc.2. Restructured 2026-07-21 to v1-only (scenes/notification/kebab moved to the stacked v2 PR): on login the wallet syncs; when the engine reports a sweep is worthwhile, a card appears on the Zcash wallet scene; tapping it prefills a locked max send-to-self in the ordinary send scene; the card clears via the status poll once Orchard empties by any means.
What's here
Status util + hook — cleaner-validated access to
otherMethods.getMigrationStatus, plus a polling hook safe to call unconditionally. Enabled on both platforms: zcash-android-sdk 2.7.0-rc.2 is published, so the Android bridge calls it directly. There is no platform gate — the earlierZCASH_MIGRATION_PLATFORMSlist was an always-true check once Android shipped, so it was removed rather than left as dead configuration.The card — its own component (
ZcashMigrationCard) rather than anAlertCardUi4, because the help link is inline in the copy and that card cannot do it. Shape:EdgeTextwith its ownonPress, matching the Stealth Send treatment.status.remainingOrchardZatoshi, and discloses that the migrated amount becomes public (ZIP 315 consent).getAddresses→ own unified address →getMaxSpendable→send2with address/amount/wallet tiles locked (the account-activation locked-send pattern), an info tile, andmetadata.noteslabeling. The spendInfo carries top-levelotherParams.ironwoodMigration: true, which makes the engine quote and build an Orchard-only sweep — so the locked amount is the Orchard balance minus fee, not a whole-wallet max send.hiddenFeaturesMapdeliberately omitted (scamWarning: falsetriggers the modal).Send-scene info tile fix — the migration tile's value is paragraph-length, and
EdgeRowcaps at 3 lines by default. That cap does not clip:EdgeTextresponds withadjustsFontSizeToFit/minimumFontScale={0.65}, so the text was rendering up to 35% smaller than its already-small 0.75rem and was hard to read.infoTilesentries now accept an optionalmaximumHeight, and the migration tile passes'large'(unlimited lines). Opt-in on purpose:infoTilesis shared with GiftCardPurchase, both Fio settings scenes and WalletActions, which all pass short one-liners, so their rendering is untouched. Note this is a latent issue for any longinfoTilesvalue app-wide, worth a separate look at whether'large'should be the default.Pins — still reference the
ironwood-ffi-6e29ab42tarballs. Re-pinning is deliberately the last step before merge, since it depends on the final published SDKs.Verification
eslint .+tsc+jestgreen (562 tests, 107 snapshots), per commit.Notes for review
proposeOrchardToIronwoodMigration), so Sapling and transparent funds are untouched — matching ZIP 318's "balance at risk" being the Orchard-pool balance specifically.InfoCardCarouselvs the vertical stack), so they can coexist. In practice the asset card is capped to outdated builds bymaxBuildNum, but that is a config guarantee rather than a code one.Note
Medium Risk
Touches Zcash send quoting and max-spend with a new engine flag and user-facing migration flows, but reuses the existing send scene with locked tiles rather than a separate signing path; dependency pins add release coupling to the Ironwood SDK builds.
Overview
Adds Zcash NU6.3 (Orchard → Ironwood) UX on the wallet scene: when the engine reports migration state
required, a warning card shows the Orchard-pool balance at risk (ZIP 318) and recommended copy that migrated amounts become public on-chain (ZIP 315).Migrate prefills the ordinary Send scene with a locked max send-to-self to the wallet’s unified address, using
otherParams.ironwoodMigrationso quoting/build is an Orchard-only sweep (not a whole-wallet max). Address, amount, and wallet tiles stay locked; an info tile explains the migration.New
getZcashMigrationStatus(cleaner-validatedotherMethods.getMigrationStatus) anduseZcashMigrationStatuspolling hook drive visibility; the card hides when Orchard is empty.ZcashMigrationCardis a dedicated component (inline Learn more link).SendScene2infoTilescan setmaximumHeight: 'large'so long migration text doesn’t shrink viaEdgeTextfont scaling.Dependencies pin
react-native-zcashandedge-currency-accountbasedIronwood FFI tarballs; locale strings and optionalzcashMigrationLearnMoreUrlin app config. Unit tests cover the migration util.Reviewed by Cursor Bugbot for commit e7ad9e6. Bugbot is set up for automated code reviews on this repo. Configure here.