[PLAN] Create staking migration widget
Parent issue: #40
Parent link: #40
Reference File Mapping
External: GoodDollar/GoodProtocol
| File |
Purpose |
releases/deployment.json |
Source of truth for the Fuse staking contract address (FuseStaking) and other protocol addresses needed by the migration adapter |
Local: GoodWidget — architecture and packaging
| File |
Purpose |
AGENTS.md |
Scope guardrails, package-boundary rules, PR requirements |
ARCHITECTURE.md |
Provider-first runtime model and package responsibilities |
docs/PACKAGING.md |
Standard widget package shape (index.ts, element.ts, register.ts, tsup.config.ts) |
docs/demo-environment.md |
Storybook and Playwright placement for widget stories/tests |
docs/architecture/theming-contract.md |
Rules for named components, theme targets, and when not to expand public theming surface |
Local: GoodWidget — reference package and runtime contracts
| File |
Purpose |
packages/citizen-claim-widget/package.json |
Reference package metadata, dependencies, and exports |
packages/citizen-claim-widget/src/CitizenClaimWidget.tsx |
Reference widget composition, named components, provider usage, and status-driven rendering |
packages/citizen-claim-widget/src/adapter.ts |
Reference adapter pattern (useWallet() → viem clients → SDK/API integration) |
packages/citizen-claim-widget/src/widgetRuntimeContract.ts |
Reference for widget state, action, and host-facing prop contracts |
packages/citizen-claim-widget/src/element.ts |
Reference web-component wrapper |
packages/citizen-claim-widget/src/register.ts |
Reference custom-element registration |
packages/citizen-claim-widget/src/index.ts |
Reference public exports |
Local: GoodWidget — shared packages likely to be reused
| File |
Purpose |
packages/core/src/provider.tsx |
GoodWidgetProvider contract for provider-first widget mounting |
packages/core/src/hooks.ts |
useWallet() integration point for wallet address, chain, provider, and connect flow |
packages/embed/src/createMiniAppElement.tsx |
Standard web-component bridge for widget packages |
packages/ui/src/index.ts |
Current reusable primitives/composites already available to new widgets |
packages/ui/src/components/Card.ts |
Primary card primitive |
packages/ui/src/components/Button.tsx |
Primary action button primitives |
packages/ui/src/components/Text.ts |
Shared text primitive |
packages/ui/src/components/Heading.ts |
Shared heading primitive |
packages/ui/src/components/TokenAmount.tsx |
Amount display primitive |
packages/ui/src/components-test/Spinner.tsx |
Existing loading indicator for migration progress |
packages/ui/src/components/Toast.tsx |
Existing transient status messaging pattern |
Local: GoodWidget — review/demo coverage references
| File |
Purpose |
examples/storybook/src/stories/citizen-claim-widget/CitizenClaimWidget.stories.tsx |
Reference widget story organization and wallet fixture usage |
tests/widgets/citizen-claim-widget/states.spec.ts |
Reference widget smoke-test structure and screenshot evidence layout |
Packages to import
@goodwidget/core — GoodWidgetProvider, wallet hooks, provider contract
@goodwidget/ui — card, text, heading, button, spinner, toast, stack/layout primitives
@goodwidget/embed — web-component bridge
viem — wallet/public client creation from the host EIP-1193 provider
- Migration API client code in the new package (no cross-package API abstraction unless a second widget needs it)
Required States, Flows, and Behaviors
Required states
| State |
Behavior |
summary |
Show staked sG$ amount, migration explanation, and the primary approve/migrate action |
approval-pending |
Show wallet approval in progress on Fuse and block duplicate submissions |
approval-failed |
Show approval failure reason and allow retry |
migrating |
Show the step timeline with one active spinner at a time |
success |
Show migration completion and final confirmation |
error |
Show the failed backend migration step and the surfaced reason |
wrong-network |
Explain the required network and prevent migration until corrected |
missing-config |
Show a clear integrator-facing error when API/operator config is absent |
User flows
- Read the connected wallet and current Fuse staking position.
- If the user has zero staked sG$, render the empty/disabled summary state.
- Validate required host config (
migrationApiBaseUrl, migrationOperator, optional migrationApiToken).
- Validate chain/network before enabling approval.
- Trigger ERC-20 approval for the migration operator on Fuse.
- Send the approval transaction hash to the migration API after approval confirms.
- Poll or subscribe to migration progress and render
unstake → bridge sent → bridge received → stake.
- Preserve completed steps while advancing the active step.
- On API or execution failure, surface the failed step and a retry path.
- On success, render a completion state that confirms the migrated position is now on Celo savings.
Migration-step behavior
| Step |
UI expectation |
unstake |
Pending spinner while active, checkmark after completion |
bridge sent |
Pending spinner while active, checkmark after completion |
bridge received |
Pending spinner while active, checkmark after completion |
stake |
Pending spinner while active, checkmark after completion |
Error and retry behavior
- Approval rejection stays in
approval-failed and does not call the migration API.
- Migration API failures transition to
error with the last known backend step.
- Retry must restart from a valid user action instead of inventing a workaround state.
- Completed steps remain visible when a later step fails.
New Components to Create
In packages/staking-migration-widget/ (widget-local by default)
| Component |
Why it belongs here |
StakingMigrationWidget.tsx |
Root widget and state-driven screen switching are migration-specific |
adapter.ts / useStakingMigrationAdapter.ts |
Wallet + approval + migration API orchestration belongs in the feature package |
widgetRuntimeContract.ts |
Host-facing props, statuses, events, and step types are feature-specific |
MigrationSummaryCard.tsx |
Summary copy and CTA are specific to Fuse staking → Celo savings |
MigrationProgressTimeline.tsx |
The four-step migration sequence is specific to this workflow |
MigrationStepRow.tsx |
The step labels/status mapping is domain-specific unless reused elsewhere later |
MigrationStatusNotice.tsx |
Missing-config / wrong-network / backend-error messaging is workflow-specific |
element.ts |
Widget package web-component entry |
register.ts |
Widget package auto-registration entry |
index.ts |
Public exports |
Candidate promotions to packages/ui only if a second widget needs the same contract
| Component |
Promotion rule |
| Generic stepper/timeline row |
Move to packages/ui only if it can be expressed without migration-specific step names or backend semantics |
| Generic empty-state card |
Move to packages/ui only if another widget needs the same structure and props |
| Generic status banner |
Move to packages/ui only if it stays semantic and not tied to staking migration copy |
Current expectation: keep the first implementation inside packages/staking-migration-widget/ and only promote truly reusable primitives after duplication pressure appears.
Execution Plan
- Create
packages/staking-migration-widget/ following the same package/export structure as packages/citizen-claim-widget/.
- Define the widget runtime contract: required host config, status enum, migration-step enum, success/error event payloads, and adapter state/actions.
- Implement the wallet/provider bridge using
useWallet() and viem clients, keeping the provider-first architecture intact.
- Add the staking-balance read path and zero-balance summary/disabled handling.
- Add approval execution for the migration operator on Fuse and gate the API call on confirmed approval success.
- Add migration API integration for submitting the approval tx hash and reading migration progress.
- Implement the state machine for
summary, approval-pending, approval-failed, migrating, success, error, wrong-network, and missing-config.
- Build the widget-local UI components for summary, progress timeline, error notices, and completion messaging.
- Add
element.ts, register.ts, and index.ts so the package ships as React + Web Component like the reference widget.
- Add Storybook stories under
examples/storybook/src/stories/staking-migration-widget/ covering empty, wrong-network, approval, migrating, success, and error states.
- Add Playwright smoke coverage under
tests/widgets/staking-migration-widget/ with screenshots for the key visible states.
- Validate that no new public theme targets are introduced unless explicitly approved.
Acceptance Criteria
Human-Reviewer Checklist
[PLAN] Create staking migration widget
Parent issue: #40
Parent link: #40
Reference File Mapping
External:
GoodDollar/GoodProtocolreleases/deployment.jsonFuseStaking) and other protocol addresses needed by the migration adapterLocal:
GoodWidget— architecture and packagingAGENTS.mdARCHITECTURE.mddocs/PACKAGING.mdindex.ts,element.ts,register.ts,tsup.config.ts)docs/demo-environment.mddocs/architecture/theming-contract.mdLocal:
GoodWidget— reference package and runtime contractspackages/citizen-claim-widget/package.jsonpackages/citizen-claim-widget/src/CitizenClaimWidget.tsxpackages/citizen-claim-widget/src/adapter.tsuseWallet()→ viem clients → SDK/API integration)packages/citizen-claim-widget/src/widgetRuntimeContract.tspackages/citizen-claim-widget/src/element.tspackages/citizen-claim-widget/src/register.tspackages/citizen-claim-widget/src/index.tsLocal:
GoodWidget— shared packages likely to be reusedpackages/core/src/provider.tsxGoodWidgetProvidercontract for provider-first widget mountingpackages/core/src/hooks.tsuseWallet()integration point for wallet address, chain, provider, and connect flowpackages/embed/src/createMiniAppElement.tsxpackages/ui/src/index.tspackages/ui/src/components/Card.tspackages/ui/src/components/Button.tsxpackages/ui/src/components/Text.tspackages/ui/src/components/Heading.tspackages/ui/src/components/TokenAmount.tsxpackages/ui/src/components-test/Spinner.tsxpackages/ui/src/components/Toast.tsxLocal:
GoodWidget— review/demo coverage referencesexamples/storybook/src/stories/citizen-claim-widget/CitizenClaimWidget.stories.tsxtests/widgets/citizen-claim-widget/states.spec.tsPackages to import
@goodwidget/core—GoodWidgetProvider, wallet hooks, provider contract@goodwidget/ui— card, text, heading, button, spinner, toast, stack/layout primitives@goodwidget/embed— web-component bridgeviem— wallet/public client creation from the host EIP-1193 providerRequired States, Flows, and Behaviors
Required states
summaryapproval-pendingapproval-failedmigratingsuccesserrorwrong-networkmissing-configUser flows
migrationApiBaseUrl,migrationOperator, optionalmigrationApiToken).unstake→bridge sent→bridge received→stake.Migration-step behavior
unstakebridge sentbridge receivedstakeError and retry behavior
approval-failedand does not call the migration API.errorwith the last known backend step.New Components to Create
In
packages/staking-migration-widget/(widget-local by default)StakingMigrationWidget.tsxadapter.ts/useStakingMigrationAdapter.tswidgetRuntimeContract.tsMigrationSummaryCard.tsxMigrationProgressTimeline.tsxMigrationStepRow.tsxMigrationStatusNotice.tsxelement.tsregister.tsindex.tsCandidate promotions to
packages/uionly if a second widget needs the same contractpackages/uionly if it can be expressed without migration-specific step names or backend semanticspackages/uionly if another widget needs the same structure and propspackages/uionly if it stays semantic and not tied to staking migration copyCurrent expectation: keep the first implementation inside
packages/staking-migration-widget/and only promote truly reusable primitives after duplication pressure appears.Execution Plan
packages/staking-migration-widget/following the same package/export structure aspackages/citizen-claim-widget/.useWallet()and viem clients, keeping the provider-first architecture intact.summary,approval-pending,approval-failed,migrating,success,error,wrong-network, andmissing-config.element.ts,register.ts, andindex.tsso the package ships as React + Web Component like the reference widget.examples/storybook/src/stories/staking-migration-widget/covering empty, wrong-network, approval, migrating, success, and error states.tests/widgets/staking-migration-widget/with screenshots for the key visible states.Acceptance Criteria
@goodwidget/staking-migration-widgetpackage exists with standard React/Web Component exportsmissing-configstatewrong-networkstate before approvalunstake,bridge sent,bridge received, andstakewith one active spinner at a timeapproval-failedwith retryerrorwith the failed step and surfaced reasontests/widgets/staking-migration-widget/Human-Reviewer Checklist
GoodProtocol/releases/deployment.jsonpackages/uiGoodWidgetProvider/useWallet()tests/widgets/staking-migration-widget/