Skip to content

Feat/finalize goodreserve widget#51

Open
Ryjen1 wants to merge 21 commits into
GoodDollar:mainfrom
Ryjen1:feat/finalize-goodreserve-widget
Open

Feat/finalize goodreserve widget#51
Ryjen1 wants to merge 21 commits into
GoodDollar:mainfrom
Ryjen1:feat/finalize-goodreserve-widget

Conversation

@Ryjen1

@Ryjen1 Ryjen1 commented Jun 12, 2026

Copy link
Copy Markdown

Fixed:

  • Isolated the @goodsdks/good-reserve library by creating a clean typed seam (sdk.ts), keeping direct SDK/blockchain interactions decoupled from the adapter state machine.
  • Fixed state machine bugs around idle timeouts, quote expiration, error recovery, and network switching.
  • Corrected swap calculation discrepancies, success screen values, price label text, and transaction explorer URL construction.
  • Completed the styling migration to the Tamagui theme contract, resolving a sub-theme naming collision between the reserve widget and core components.
  • Adjusted padding, margins, colors, and layout hierarchies to align with the Figma specs and the Stitch prototype.
  • Added missing coverage and Storybook stories for the sdk-initializing and quote-ready-xdc states.
  • Fixed a focus/input issue on web browsers that made the swap amount field non-editable.
  • Increased Storybook test-runner timeouts to mitigate setup-phase timeouts in CI.

Verified:

  • Monorepo install, build, and lint runs cleanly (pnpm build & pnpm lint).
  • Storybook play/interaction tests: 33/33 tests pass.
  • Playwright integration tests: 17/17 tests pass.

Evidence:

  • Verified all 16 widget states (from grw-01 to grw-16) in the Playwright snapshot suite under tests/widgets/goodreserve-widget/test-results/.
  • Updated Storybook reference screenshots under examples/storybook/src/stories/goodreserve-widget/screenshots/.

Remaining risks:

  • The @goodsdks/good-reserve dependency is imported dynamically as it hasn't been published to npm yet. Playwright runs use a mocked provider; full end-to-end network validation should be performed in staging once the SDK is released

Copilot AI and others added 21 commits May 27, 2026 14:57
Align the GoodReserve widget with the issue GoodDollar#15 references and fix concrete
implementation gaps in PR GoodDollar#39.

Widget:
- Map raw stable/G$ balances onto direction-aware in/out slots so sell-mode
  shows the correct "from" balance and MAX target.
- Derive token symbols and the network label dynamically for Celo (42220) and
  XDC (50); show an explicit unsupported-chain label.
- Key swap success/error host callbacks to discrete lifecycle fields instead of
  the whole state object to stop spurious re-fires on balance/quote updates.
- Rework ReserveSwapView to the Figma structure: header pill + title, token-badge
  amount cards, transaction-detail rows, dynamic CTA labels, redesigned confirm
  drawer with a press-to-confirm button, and a dedicated success screen.

Coverage:
- Add XDC quote-ready fixture/story and align existing fixtures.
- Rewrite Playwright states spec to assert rendered text, add confirm/slippage/
  XDC coverage, retry cold-start navigation, and emit committed screenshots.
- Regenerate Storybook story screenshots.

Repo blockers (out of widget scope, required to pass verification commands):
- core: replace empty HostContextValue interface with a type alias (lint error).
- citizen-claim-widget: drop stale react-hooks/exhaustive-deps disables for a
  rule the shared eslint config does not register (lint error).
- ui Drawer: use Sheet.Overlay/Frame/Handle directly to preserve Sheet ref
  forwarding (fixes "Function components cannot be given refs") and remove a
  debug console.log; fix the Drawer story to query the sheet portal.
- Rebuild the swap-success screen to the Figma success frame order: glowing
  blue check hero, title, summary card, "View on Explorer" link, and a
  full-width pill "Do another swap" CTA.
- Make the widget responsive: fill the host container up to a 390px max width
  (mobile-frame width) instead of a fixed 360px column.
- Raise primary CTAs to the Figma 54px height.
- Widen the Storybook story frame to 390px and regenerate story + Playwright
  screenshot evidence to reflect the updated layout.
The @goodwidget/ui Input is a Tamagui `tag:'input'` Stack, which does not
translate React Native's onChangeText to a DOM change event on web. As a
result the controlled amount field rejected all typed input and the entire
type-amount -> quote -> confirm -> swap path was unreachable in a browser
(every Storybook state is pre-seeded via mockState, so existing tests missed it).

- Wire the native onChange handler and inputMode="decimal" instead of the
  RN-only onChangeText/keyboardType props.
- Sanitize input to a single decimal number at the boundary so values are
  always safe for viem parseUnits (rejects "1.2.3", separators, letters).
- Bump the amount font to 34px to match the Figma reference.
- Add a live-adapter Interactive story and a Playwright regression test that
  types into the input and asserts both editing and sanitization.

Verified empirically: before, typing left the DOM input value empty; after,
the value updates and "1.2.3x" sanitizes to "1.23".
Correctness and safety fixes in the reserve adapter:

- Derive minimumReceived in BigInt from the SDK quote and carry the exact
  minReturn (minReturnRaw) into executeSwap, so the floor shown to the user is
  precisely the floor submitted on-chain. Previously the display used float
  math (Number * (1 - slippage)) while execution used BigInt, allowing the two
  to diverge on small trades.
- Guard executeSwap against double submission while a swap is pending.
- Re-validate chain support inside executeSwap so switching to an unsupported
  chain after opening the confirm dialog is caught before signing.
- Set swap_success before refreshing balances and make the refresh best-effort,
  so an RPC blip can no longer turn a confirmed swap into swap_error.
- mapReserveError now logs unmatched errors and returns a generic fallback
  instead of surfacing raw viem output (which can leak RPC URLs / addresses).
…handling

- Make the public client chain-aware (RESERVE_CHAINS map) so the GoodReserve
  SDK constructor, which validates publicClient.chain.id, does not throw on a
  chainless client.
- Reset the cached SDK/read client on bootstrap so a Celo<->XDC switch
  re-initializes against the new chain instead of reusing stale clients.
- Read the "from" balance via a ref inside the quote effect and drop it from
  the effect deps, so a post-swap or direction-toggle balance refresh no longer
  restarts the quote debounce.
- Preserve and restore the pre-overlay status when the slippage sheet or
  confirm dialog is dismissed, instead of unconditionally forcing quote_ready.
- Render a dedicated sdk_initializing loading state (with fixture, story, and
  Playwright coverage) rather than a half-populated swap card.
Use style={{ textAlign: 'right' }} instead of the React Native textAlign prop,
which Tamagui's tag:'input' Stack would otherwise emit as an invalid lowercase
`textalign` DOM attribute (React warning). Behavior unchanged.
Rework the swap view to match the Figma GoodReserve frames (file
xsk5EiF6CvStA9mtdbA9OR), verified structurally against the Figma API:

Structure:
- Move the header (network pill, blue title, subtitle) above the dark card.
- Replace the Buy/Sell tabs with a single circular swap-direction button
  between the amount cards (preserves both buy and sell, matches Figma).
- Render the confirmation as an anchored bottom-sheet Drawer with a token hero,
  a 50px "Minimum Received" highlight, a details table (incl. Network Fee), and
  a close affordance; render slippage selection as a Drawer too.
- Reorder the success screen to title -> summary -> explorer link -> glow icon.
- Make Transaction Details and FAQ collapsible (chevron); add the second FAQ
  item and a bottom settings/slippage icon; add MAX on the swap-to row.

Colors/typography:
- Pin the exact Figma palette in the widget (card #0C0E15, input #252730,
  badge #33343C, soft #8B91A0, secondary #C1C6D6, heading/MAX #4090FF,
  positive #43E350) via widget-scoped named components, since the shared
  GoodWalletV2 preset tokens differ and altering them is out of scope.
- Detail rows now use 16/500 values and 12/600 labels; PRICE uses PER <SYM>;
  success heading 26/700; amount inputs 34/700.

Also add arrow-down/arrow-up icons to the UI Icon registry for the flip button.
…uote math

Fix remaining adapter and view correctness issues:

- Preserve the swapped output as lastSwapOutput before clearing the quote on
  success, and show it on the success screen instead of the wallet balance
  (the two are different numbers). Adds a regression test.
- Make "View on Explorer" a functional Anchor that links to celoscan / xinfin
  for the tx hash, instead of a dead element.
- Replace window.setTimeout/clearTimeout with the bare globals so the quote
  debounce works under SSR and React Native.
- Compare the input against the balance in BigInt (parseUnits) rather than via
  Number(), removing last-decimal float drift in the insufficient-balance gate.
- Read result.hash (canonical) from the swap result.
- Split the bottom settings/slippage control into its own SettingsButton
  component so it no longer shares the swap-direction sub-theme name.
- Open the confirmation drawer at full height so the hero + 50px highlight +
  details table are not clipped; reset cleanly to buy idle on "Do another swap".
…te-coverage tests

- bootstrapSdk no longer depends on state.direction (reads a directionRef), so
  toggling buy/sell after connecting no longer re-initializes the SDK.
- Hold onSwapSuccess/onSwapError in refs so inline host callbacks do not re-fire
  the lifecycle effect on unchanged success/error states.
- Surface the real exitContribution from getReserveStats in the quote, and show
  price impact as N/A instead of a misleading hardcoded ~0.01%.
- Sanitize setMaxAmount (and setInputAmount) through a shared amount.ts helper so
  formatted balances are always parseUnits-safe.
- setSlippagePercent restores the previous status instead of forcing idle_buy,
  preserving sell/quote context.
- Make the confirm-drawer Network Fee chain-aware (CELO vs XDC).
- Add Playwright coverage for the amount-editing and quote-loading states.

All 16 widget Playwright tests pass; build and lint clean.
…ror recovery

Apply correctness, error-recovery, and contract fixes:

- Rename the idle status idle_buy -> idle across the contract, integration
  manifest, and adapter, since direction is already a separate state field; the
  status no longer reports "buy" while in sell mode.
- Align fixtures with the live adapter: priceImpactPercent is N/A everywhere
  (SDK does not expose it) and renders muted rather than green; regenerate all
  story screenshots so committed evidence matches real output, and add the
  previously missing sdk-initializing.png.
- Wire the existing refresh action to a visible Retry CTA in the quote_error /
  swap_error states so users are no longer stuck behind a disabled button.
- Add a preferredChainId prop so the unsupported-chain CTA can target XDC (or
  any chain) instead of always switching to Celo.
- Guard executeSwap against stale quotes: quotes carry a quoteExpiresAt and a
  swap submitted after expiry is rejected with a refresh prompt instead of
  signing a minReturn derived from an outdated price.
- Clear txHash/lastSwapOutput on direction change so a prior swap result cannot
  leak into the next swap.

All 16 widget Playwright tests pass; build and lint clean.
Apply high-severity theming and UX fixes (SDK-dependent items
remain blocked until @goodsdks/good-reserve is published):

- H1/H2: replace the hardcoded FIGMA hex map with real, host-themable surfaces.
  The named components now resolve their background/color/shadow from registered
  light_/dark_Reserve* component sub-themes in the preset, and cross-cutting text
  colors use new $reserve* palette tokens. No raw hex remains in the JSX.
  Verified the rendered colors are unchanged (#0C0E15 shell, #252730 cards,
  #4090FF heading) while now being overridable through the normal chain.
- H3: the header pill is a network chip (e.g. "CELO") instead of duplicating the
  "Swap on CELO" heading.
- H4: remove the permanently-"N/A" price impact row (SDK exposes no price impact).
- H5: remove the fabricated "~0.001 CELO/XDC" network fee from the confirm sheet.
- M2: relabel "Final amount received" to "Estimated received" (it is the quote).
- M3: on quote expiry, keep the entered amount and re-quote instead of erroring.
- M7: switchChain now catches rejections (e.g. 4902) and surfaces a message.
- L2: explorer link uses explorer.xdc.org; L3: stable-decimals fallback is
  chain-aware (XDC=6); N5: drop the capabilitySource reference to a non-existent
  SDK export.

All 16 widget Playwright tests pass; ui + widget build and lint clean.
…ollision

Resolve the theming-regression set:

- C4: give the confirm-hero "to" badge its own ConfirmToBadge component and
  light_/dark_ReserveConfirmToBadge sub-theme (flat, no glow), distinct from the
  96x96 glowing ReserveSuccessIcon they previously shared a name with.
- C5: every named surface declares color: '$color' and the badge glyphs use
  color="$color", so a host override of a sub-theme moves surface + foreground
  together.
- H7: convert the remaining flat-token surfaces to registered sub-themes
  (ReserveSurface for success/FAQ, ReserveSurfaceInner for the confirm highlight,
  ReserveDetailsTable for the confirm table); no inline $surface/$reserveCard.
- H8: the widget is dark-only — defaultTheme is fixed to 'dark' in the contract
  and documented; light_/dark_ pairs are intentionally identical.
- H6: document the reserve palette as mirrored between theme.ts (token source)
  and the preset color map.

All 16 widget Playwright tests pass; ui + widget build and lint clean.
… typed seam

Replace the dynamic Function-based loader with a typed lazy import() of
@goodsdks/good-reserve (declared as an optionalDependency, since it is not yet
published). The seam in sdk.ts mirrors the real PR GoodDollar#35 public surface
(GoodReserveSDK / ReserveStats / ReserveTransactionResult), so every adapter
call site is type-checked against the actual contract instead of a loose shadow
type.

- Wire the onHash callback on buy/sell so swap_pending surfaces the submitted
  tx hash before the receipt resolves; read result.hash.
- Scale exitContribution from parts-per-million (/10_000) per the Mento
  convention, instead of the previous incorrect * 100.
- Re-validate the wallet's current chain via a live eth_chainId read before
  signing, rather than trusting the memoized chain flag.
- Guard the empty-input quote effect so it cannot clobber terminal swap states
  (success/error/pending) back to idle.
- Broaden mapReserveError to cover network/timeout/rejection and sanitized
  revert reasons, matching citizen-claim-widget's coverage.
- Add a deterministic fake SDK + EIP-1193 test provider and an injection seam,
  plus a LiveFakeSdk story and a Playwright test that drives the full real
  adapter flow (quote -> confirm -> buy -> success with tx hash) with no
  published SDK and no live RPC. The harness clears the injected fake on unmount.

All 17 widget Playwright tests pass; build and lint clean.
…ng claims

- Fix the swap-rate display: compute price as output-per-input and render it
  consistently as "1 <tokenIn> = <price> <tokenOut>" in both the details row and
  the confirm sheet (previously the two labels described reciprocal quantities
  with the same number).
- Use the canonical XDC explorer (xdcscan.com/tx/<hash>) instead of an
  unverified host/path; Celo stays celoscan.io/tx/.
- Clamp the unsupported-chain switch target to a supported reserve chain so a
  bad preferredChainId can't route to an unsupported network and bounce back.
- Tokenize the network pill border (use $primaryMuted) so the view contains no
  raw color literals.
- Correct the styled-components header comment: primary surface/text come from
  the sub-theme, while secondary text shades are $reserve* tokens (overridable
  at the token layer) — the comment no longer overstates per-sub-theme control.
- Document the exitContribution scaling against the SDK demo's own convention
  (reserveRatio / 10000) rather than asserting an unverified PPM basis.

All 17 widget Playwright tests pass; build and lint clean.
Add a Storybook test-runner config that sets jest.setTimeout(60000). The default
15s can be exceeded when the dev server compiles a story on first request,
intermittently failing pnpm test:storybook in CI. Additive config (no prior
test-runner setup existed); benefits the whole story suite.
Merges the upstream 'add: extend goodwallet-v2 preset with light theme'
commit into the PR branch.

Conflict resolution (packages/ui/src/presets.ts):
- Kept both the GoodReserve swap widget palette tokens (reserve*) introduced
  on this branch AND the new Governance light-mode tokens (governance*)
  introduced upstream. Both token namespaces are non-overlapping and required.

Follow-up fix (tests/design-system/smoke.spec.ts):
- The 3 ClaimWidget/CobaltBrand, ClaimWidget/TealBrand, and
  ClaimWidget/Default test cases in smoke.spec.ts referenced story IDs that
  no longer exist after the upstream commit renamed the story meta title from
  'Theme/ClaimWidgetThemeDemo' to 'Theme/ClaimWidgetThemeDemo-Light' and
  removed the Cobalt/Teal brand export variants.
- Updated ClaimWidget/Default to navigate to the correct story ID
  (theme-claimwidgetthemedemo-light--default) and removed the now-deleted
  Cobalt/Teal brand test cases, leaving the spec at 6 tests (all pass).

Updated test screenshots from the post-merge run to reflect the merged
light-theme changes visible in the design-system stories.
@Ryjen1 Ryjen1 requested review from a team and L03TJ3 June 12, 2026 15:53
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.

2 participants