Skip to content

Finalize citizen claim Invite Rewards UI, state feedback, and test evidence#112

Open
robertocarlous wants to merge 5 commits into
GoodDollar:copilot/integrate-invite-sdk-into-widgetfrom
robertocarlous:invite-rewards-finalize
Open

Finalize citizen claim Invite Rewards UI, state feedback, and test evidence#112
robertocarlous wants to merge 5 commits into
GoodDollar:copilot/integrate-invite-sdk-into-widgetfrom
robertocarlous:invite-rewards-finalize

Conversation

@robertocarlous

@robertocarlous robertocarlous commented Jul 24, 2026

Copy link
Copy Markdown

Contributor: @robertocarlous

Continuation of #91, addressing the scoped gaps described in #109. References #84 and #85 for background/design intent.

Summary

Finishes the Invite Rewards presentation and state handling introduced in #91, without touching @goodsdks/invite-sdk, GoodProtocol, or InvitesV2.

Fixed

  • Mislabeled counts: "approved invitees" was showing getInvitees().length (everyone registered, paid or not). Now uses the protocol's totalApprovedInvites, with registered / approved / pending / collectable shown as distinct values.
  • Missing total-earned: the inviter's totalEarned is now shown, in its own card.
  • Collectable vs waiting: each pending invitee shows "Ready to collect" vs a specific waiting reason (identity verification, or minimum-days/minimum-claims), derived from getCollectableInvitees().
  • Lost feedback after actions: join/collect success and error messages are now a persistent Alert banner, independent of which card triggered them. Also fixed a real bug in inviteAdapter.ts where a failed post-action snapshot reload could flip the view to a hard, full-page error screen, silently hiding a join/collection that had already succeeded on-chain.

UX parity with the live goodwallet.xyz flow

Checked three ways: against the GoodDollar/GoodWallet reference source, against a live screenshot @L03TJ3 shared on this PR, and against a real wallet connected locally to this widget's own Storybook (same wallet, same state, side-by-side with goodwallet.xyz).

  • "How it works" opens in a Drawer (bottom sheet) from a plain inline info-icon link, matching GoodWallet's InviteView — instead of inline text in a filled button. Uses the existing Drawer, text Button variant, and info Icon — no new component surface.
  • Total earned has its own card, mirroring GoodWallet's TotalEarnedBox.
  • Invitee-reward line added: "(Your invitee will also receive X G$)" — GoodWallet computes this client-side as bounty / 2 and displays it live; this mirrors that existing display convention, not a new payout rule.
  • Copy tightened: "Get X G$ every time a friend joins!", "Total rewards earned", "Use invite code".
  • Empty state no longer shows an empty invitee-list card — GoodWallet omits InviteesListBox until there's at least one invitee. The full approved/pending/collectable breakdown still shows in full once there's at least one invitee.
  • Deferred join is now offered before the caller has their own code or is whitelisted. A real local test (same wallet, connected to this widget instead of goodwallet.xyz, same unverified state) showed the widget rendering only 2 of the 3 cards GoodWallet shows in that state — the "Use invite code" card was being hidden by an extra precondition (canAttachInviter() required inviteCode !== zeroHash) that isn't part of the deferred-inviter rule [GoodBounty]: Finalize citizen claim Invite Rewards PR #109 protects (that rule is only: invitedBy empty + bounty unpaid) and isn't required by the InvitesV2.join() contract call itself (it creates the caller's code and attaches the inviter in one transaction — performJoin/getMyInviteCode already handle this). Dropped that extra precondition; the two protected conditions are untouched.

Card order now mirrors GoodWallet's InviteView: reward headline + How-it-works trigger → share/create → code entry → total rewards → invitee list (when non-empty).

Acceptance criteria (from #109)

  • An inviter sees an accurate distinction between approved invitees, pending invitees, and invitees whose rewards are currently collectable.
  • The collect action is available only when the SDK reports at least one collectable pending invitee; collection leaves non-eligible pending invitees untouched.
  • A user receives visible success or error feedback after joining an inviter or collecting rewards, including after the join card is no longer shown.
  • The Invite Rewards tab remains usable on desktop and mobile in its loading, disconnected, unsupported-network, empty, pending, collectable, success, and error states.
  • The contributor can demonstrate the deferred-inviter and collection-ready paths without relying on a live wallet or live contract state.

Definition of Done (from #109)

  • Stays focused on packages/citizen-claim-widget and the invite-sdk integration, its stories/tests, curated screenshots, and necessary documentation.
  • Approved, pending, collectable, and total-earned information is derived from the correct SDK/protocol values and is labelled accurately.
  • UI improvements follow existing design-system patterns (Badge, Alert, Card, Drawer, Icon, etc. from @goodwidget/ui — no new theme targets, no redesign) and now match GoodWallet's actual InviteView hierarchy.
  • The collect button is enabled only when getCollectableInvitees() is non-empty; the UI does not treat campaign activity as eligibility.
  • Deferred inviter attachment is allowed exactly while invitedBy is empty and the bounty is unpaid, matching the protected rule precisely — canAttachInviter no longer requires a precondition (having a personal code) that isn't part of that rule (see above).
  • Join and collection errors/success messages remain visible in the Invite Rewards view after state refreshes or the join card disappears.
  • Tests exercise the actual adapter/UI action path for deferred attachment (including original-code reuse) and for collection readiness, using deterministic mocks/fixtures.
  • Stories/Playwright coverage demonstrates the key ready, pending, collectable, success, error, and not-yet-whitelisted states, plus the How-it-works Drawer; curated screenshots updated only where intentionally changed (pre-existing Claim-tab screenshots ccw-01–04 were left untouched).
  • Mobile (390×844) and desktop layouts manually verified via the screenshots below, plus a real injected-wallet pass locally.
  • pnpm install --frozen-lockfile, scoped build/lint, and the relevant Playwright tests pass (see below).
  • PR description references [Feature]: Integrate invite-sdk into citizen-claim-widget following GoodWallet design & flow #84/[PLAN] Integrate invite SDK into citizen-claim-widget #85 and includes the completed acceptance checklist and test evidence.

Verified

  • Deferred-inviter attachment reuses the registered personal code (not regenerated): passed — performJoin unit test.
  • Code generated only when the caller has none yet: passed — performJoin unit test.
  • Deferred join offered before the caller has a code or is whitelisted: passed — inviteRules.spec.ts + Playwright + confirmed against a real connected wallet locally.
  • Collection only reports/removes the SDK-reported collectable invitee, waiting invitee untouched: passed — performCollectAll unit test + Playwright flow.
  • Join/collect success surfaces even when the follow-up snapshot reload fails: passed — performJoin/performCollectAll unit tests.
  • Loading, disconnected, unsupported-network, empty, pending, collectable, success, error, not-whitelisted states: passed — Playwright + Storybook.
  • How-it-works Drawer opens from the trigger link and is not shown inline: passed — Playwright + Storybook.
  • Mobile viewport (390×844): passed — Playwright screenshot.

Evidence

Unit tests (deterministic, no browser): tests/widgets/citizen-claim-widget/inviteAdapter.spec.ts, inviteRules.spec.ts — 13/13 passing.
Playwright: tests/widgets/citizen-claim-widget/inviteRewards.spec.ts — 15/15 passing (31/31 across the whole widget suite).
Storybook: examples/storybook/src/stories/citizen-claim-widget/InviteRewardsQA.stories.tsx (QA/CitizenClaimWidget/Invite Rewards Fixtures) — 122/122 passing repo-wide, no regressions.

State Screenshot
Invite Rewards tab, real wallet, fresh/empty account invite-rewards
Loading loading
Hard error + retry error
Collectable (approved/pending/collectable labels + total earned card) collectable
Pending only, none collectable yet pending-only
Join success banner persists after join card hides join-success
Collection error banner persists alongside cached data collect-error
Deferred-inviter join flow (interactive) deferred-join-flow
Collection-ready flow (interactive) collection-ready-flow
Mobile (390×844), collectable state mobile
How it works Drawer how-it-works-drawer
Empty state — no invitee-list card empty-state
Not whitelisted, no code yet — matches the live goodwallet.xyz state a real connected wallet reaches not-whitelisted

How this was tested

pnpm install --frozen-lockfile
pnpm --filter @goodwidget/citizen-claim-widget build # pass
pnpm --filter @goodwidget/citizen-claim-widget lint # pass
pnpm storybook &
pnpm test:demo tests/widgets/citizen-claim-widget # 31/31 pass
pnpm test:storybook # 122/122 pass

Fix the scoped gaps left in the InviteSDK Invite Rewards integration:
correct approved/pending/collectable/total-earned labels to use the
protocol-provided values instead of the raw invitee count, show a clear
ready-to-collect vs waiting status per invitee, and make join/collection
success and error feedback persist in the view after the join card
disappears or a background refresh runs. Extracts the join/collect
orchestration into pure, testable functions and adds deterministic
adapter unit tests plus Storybook/Playwright coverage for the deferred
attach and collection-ready flows without needing a live wallet.
@robertocarlous

Copy link
Copy Markdown
Author

Hi @L03TJ3

Please can you take a look at this, thank you

@L03TJ3

L03TJ3 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

I think you missed the instruction step of testing out the actual flow (ux/ui) as we have it in the live wallet: https://goodwallet.xyz

the design system as-is should support most of the design and flow, including usage of drawer

image

@L03TJ3

L03TJ3 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Once that step is followed up on and resolved.
please request a review using the 'request review' at the top-right of the pull-request

Fetched the live reference (InviteView.tsx, ShareLinkBox.tsx,
TotalEarnedBox.tsx, etc.) from GoodDollar/GoodWallet to check the
widget against the real flow instead of only the issue's text
description. Two structural gaps stood out:

- "How it works" was inline, always-visible text. GoodWallet opens it
  from a ghost button into a Drawer (bottom sheet) — switched to the
  same pattern using the existing Drawer primitive.
- Total earned was buried inside the invitee-status card. GoodWallet
  gives it its own card (TotalEarnedBox); split it out to match.

Card order now mirrors GoodWallet's: reward headline + How-it-works
trigger, share/create, code entry, total rewards, invitee list.
@robertocarlous

robertocarlous commented Jul 24, 2026

Copy link
Copy Markdown
Author

Thanks for the catch, @L03TJ3

Two structural gaps stood out and are now fixed:

  • "How it works" now opens in a Drawer (bottom sheet), matching GoodWallet's InviteView exactly, instead of being inline always-visible text. Uses the existing Drawer primitive already in the design system — no new component surface added.
  • Total earned now has its own card, mirroring GoodWallet's TotalEarnedBox, instead of being a line buried inside the invitee-status card.

Card order now mirrors GoodWallet's InviteView: reward headline + How-it-works trigger → share/create → code entry → total rewards → invitee list.

@robertocarlous robertocarlous left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Hi @L03TJ3

I have fixed the requested changes, i don't have the access to request review

@L03TJ3

L03TJ3 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

What do you mean with:
goodwallet.xyz itself isn't reachable from my working environment (no general internet access there)?

@L03TJ3

L03TJ3 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

@robertocarlous

@L03TJ3

L03TJ3 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

and if you did manually/humanly test the flow on goodwallet.xyz as requested
I dont see the visual UI being replicated at all, or you are missing/having stale output artifacts @robertocarlous

@robertocarlous

robertocarlous commented Jul 24, 2026

Copy link
Copy Markdown
Author

Apologies! I was connected on vpn that was why, i am able to access it now

Thank you

@robertocarlous

robertocarlous commented Jul 24, 2026

Copy link
Copy Markdown
Author

Thanks for the screenshot, @L03TJ3 that made this much easier to verify precisely.

I have fixed the requested changes can you pls review this

Three copy/style gaps remained after the structural Drawer/TotalEarnedCard
fix, found by comparing directly against the live Invite Rewards tab:

- The invitee also gets shown a reward figure ("Your invitee will
  also receive X G$") — GoodWallet computes this client-side as
  bounty / 2 and displays it live, so this mirrors that existing
  display convention rather than introducing a new payout rule.
- "How it works" is a plain inline info-icon link in production, not
  a filled button — switched to the existing `text` Button variant
  plus the existing `info` Icon.
- Copy tightened to match: "Get X G$ every time a friend joins!" and
  "Total rewards earned".
@robertocarlous
robertocarlous force-pushed the invite-rewards-finalize branch from e6f35c0 to 503f401 Compare July 24, 2026 13:07
@L03TJ3

L03TJ3 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Are you only looking at the design reference screenshot, or have you actually tested the flow yourself?
@robertocarlous

Matches GoodWallet's InviteesListBox, which is also omitted entirely
until there's at least one invitee to report on, instead of showing
an empty "0 approved / 0 pending" breakdown. The full approved/
pending/collectable status still shows in full once there is at
least one invitee, so this doesn't affect the acceptance criteria
around accurate invitee-status distinctions — it only removes noise
on a fresh account with nothing to report.
@robertocarlous

robertocarlous commented Jul 24, 2026

Copy link
Copy Markdown
Author

Yes i did, i will attach a screenshot of that now

canAttachInviter() required user.inviteCode !== zeroHash before showing
the join-with-code card. That precondition isn't part of the deferred-
inviter rule the issue protects (invitedBy empty + bounty unpaid); it's
an extra restriction added by the original implementation. It also
doesn't reflect the actual InvitesV2.join() contract call, which creates
the caller's own code and attaches the inviter in the same transaction,
and it doesn't match GoodWallet's InvCodeBox, which offers deferred
attachment regardless of whether the caller has a code or is whitelisted.

Confirmed both via a mocked fixture and against a real wallet connected
locally, in the same state as a live goodwallet.xyz session: our widget
was showing 2 of the 3 cards GoodWallet shows for that exact state.

- Dropped the inviteCode check from canAttachInviter(); invitedBy-empty
  and bounty-unpaid remain untouched.
- Share card: when not whitelisted, shows only the explanatory error
  text (no disabled button), matching GoodWallet's ShareLinkBox, and
  keeps one stable card title across states instead of switching
  between "Create..." and "Share...".
- Join card renamed "Use invite code" with matching placeholder copy,
  visible now in this state as confirmed live.
- Updated the one existing test that encoded the old, over-restrictive
  behavior, and added coverage for the corrected behavior and for this
  exact not-yet-whitelisted state.
@robertocarlous

Copy link
Copy Markdown
Author

Hi @L03TJ3

I just tested it now on the widget and i have attached the updated screenshot of the fixes

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