Skip to content

01a03535 - Add referral programme and promo-code client - #966

Open
JonnyLuca wants to merge 113 commits into
RealUnitCH:stagingfrom
JonnyLuca:01a03535-referral-promo-2
Open

01a03535 - Add referral programme and promo-code client#966
JonnyLuca wants to merge 113 commits into
RealUnitCH:stagingfrom
JonnyLuca:01a03535-referral-promo-2

Conversation

@JonnyLuca

@JonnyLuca JonnyLuca commented Sep 4, 2026

Copy link
Copy Markdown

Summary / Zusammenfassung

EN. RealUnit referral programme and promo-code client (Offerte 24.08.2026 + mail Dani 17:42). The DFX API is the decision authority; this app is the rendering layer for eligibility, terms, invites, overview, frozen-CHF prizes, the KYC invite/promo step, and App Links. This branch supersedes #942 with clean, signed, tool-neutral commits and carries the review finalisation on top of the feature. It was hardened through a cross-vendor double-review (Claude Opus 5 + Codex) to zero findings.

DE. RealUnit-Empfehlungsprogramm und Promo-Code in der App (Offerte 24.08.2026 + Mail Dani 17:42). Die DFX-API entscheidet; die App rendert Kachel, Teilnahmebedingungen, Einladungen, Übersicht, Prämien mit eingefrorenem Frankenwert, KYC-Schritt Invite/Promo und App Links. Dieser Branch ersetzt #942 mit sauberen, signierten, werkzeugneutralen Commits und enthält die Review-Finalisierung zusätzlich zum Feature. Er wurde per Cross-Vendor-Doppelreview (Claude Opus 5 + Codex) auf null Findings gehärtet.

Scope, review finalisation, measurements

Scope

  • Dashboard and settings tiles gated on GET /v1/realunit/referral/summary eligible (KYC-verified, ≥ 70 REALU, not an employee). Subtitle Erhalte 20 REALU pro Weiterempfehlung.
  • Teilnahmebedingungen from authenticated GET /v1/realunit/referral/terms (version, markdown, markdownEn), with bundled assets only as an outage fallback; create-invite only after the acceptance checkbox. Acceptance posts the exact rendered version so a newer server version reopens the gate, and the posted version always matches the text actually shown.
  • Server-generated share text, copy and send; invite URL always https://realunit.app/invite/{code}.
  • Overview: Open / Credited only (TB Ziff. 7); defensive client parsing drops Deleted/expired rows. Total tile label Aktienkurs. History uses CHF frozen at credit (TB Ziff. 6).
  • KYC registration step for invite and promo (promo Aktionstext in its own dialog), on the sticky ScrollableActionsLayout inside a SafeArea so next/skip clear system insets. No stacking with an invite prize (API rejects).
  • Universal Links / App Links; Play Install Referrer on Android; iOS re-tap / paste.
  • Prize rows: whole REALU, date, frozen CHF. History watch includes referralPayout (type 5).
  • DE/EN, handbook #spec-referral. Referral/promo goldens regenerated via golden-regenerate.yaml.

Review finalisation on top of the feature (this replaces #942)

  • Correctness: symmetric DE/EN precedence in campaignTextForLocale; a generation guard in the eligibility reload; terms acceptance reset on a language reload, adopting the API terms version only together with the text it belongs to; the overview Equatable state compares every rendered per-invite field plus the share-price label.
  • Conformity: the KYC step uses the sticky ScrollableActionsLayout in a SafeArea and is covered by a responsive-matrix test that asserts both sticky CTAs; the pure format_frozen_chf helper moved into packages/io; the frozen-CHF label uses the text theme; the unused payout merger and the orphaned getPayouts service method were removed; the referral surfaces are analyzer-clean.
  • Second review round: a further cross-vendor double-review (Claude Opus 5 logic + Codex conformity) drove the finalisation to zero findings again. Fixed: the eligibility cubit now guards every post-await emit with isClosed (no emit-after-close when a cubit is closed mid-fetch); KYC registration hands the deeplink stash to the referral field only while the referral step is the active step, preserving the do-not-refill-after-skip contract while the field keeps stash-over-clipboard precedence; frozen_chf_label moved into lib/widgets/ (only non-referral importers) with imports re-sorted per CONTRIBUTING.md; the test-only pending-code sync helpers marked @visibleForTesting; shareReferralInvite annotated for integration-test exclusion. New regression test: the eligibility cubit does not emit after being closed mid-flight.
  • Deferred with rationale (not defects): the lenient referralJson* DTO helpers are kept deliberately (locale-tolerant CHF/number parsing; strict as casts would throw on the API's locale-formatted values); a few raw TextStyle(color:) sites are left as-is (a golden-affecting style refinement tracked as follow-up, not a correctness issue); the NEEDS_TERMS retry-only screen is escapable via back and only reachable through an unusual backend state race (reviewer: not a blocker).

Measured (self-hosted / native)

Check Result
flutter test --exclude-tags golden 5951 passed
flutter analyze 0 issues (repo's enabled lints applied across the referral feature)
Referral + routing suites 545 passed
Round-2 re-verify on the Flutter host (flutter analyze + referral/kyc/routing suites at fef34be) 0 issues, 635 passed
golden-regenerate.yaml on RealUnitCH/app two successful runs updating the KYC-step + frozen-CHF baselines (33922437059fddb4ec, 3392535510769db0ea)

Post-merge / operator actions

  • Backend rollout: DFXswiss/backend#5415 implements and tests the matching routes; merge/deploy remains human-owned.
  • Human: merge RealUnitCH/web#30 so production AASA / assetlinks / /invite/* are HTTP 200.
  • Human: PRIZE_WALLET_KEY for settled 20 REALU transfers.

Related: RealUnitCH/intern#3 (do not auto-close).

@JonnyLuca
JonnyLuca marked this pull request as ready for review September 4, 2026 22:57
@JonnyLuca
JonnyLuca marked this pull request as draft September 4, 2026 23:24
@JonnyLuca
JonnyLuca marked this pull request as ready for review September 5, 2026 06:04
@JonnyLuca
JonnyLuca marked this pull request as draft September 5, 2026 06:29
@JonnyLuca
JonnyLuca marked this pull request as ready for review September 5, 2026 06:37
@JonnyLuca
JonnyLuca marked this pull request as draft September 5, 2026 06:51
@JonnyLuca
JonnyLuca marked this pull request as ready for review September 5, 2026 08:37
@JonnyLuca
JonnyLuca marked this pull request as draft September 5, 2026 08:39
@JonnyLuca
JonnyLuca marked this pull request as ready for review September 5, 2026 08:39
@JonnyLuca
JonnyLuca force-pushed the 01a03535-referral-promo-2 branch 2 times, most recently from 4d7b83c to a145ef2 Compare September 5, 2026 09:24
@TaprootFreakAI
TaprootFreakAI marked this pull request as draft September 5, 2026 09:29
@TaprootFreakAI
TaprootFreakAI marked this pull request as ready for review September 5, 2026 18:34
@TaprootFreakAI

Copy link
Copy Markdown
Collaborator

EN. Ready after the referral/promo client was driven to zero review findings across three cross-vendor review passes (the last two confirming the finalisation fixes). The full effective PR CI is green at this head — Analyze & Test, Coverage Floor Gate and Visual Regression — with the same effective job set additionally run and green on a build host at the CI-pinned toolchain.

DE. Bereit, nachdem der Referral-/Promo-Client über drei Cross-Vendor-Review-Durchläufe auf null Findings gebracht wurde (die letzten beiden bestätigten die Finalisierungs-Fixes). Die vollständige wirksame PR-CI ist auf diesem Head grün — Analyze & Test, Coverage Floor Gate und Visual Regression — und dieselbe wirksame Jobmenge wurde zusätzlich auf einem Build-Host mit der CI-fixierten Toolchain grün gefahren.

Gate evidence
  • Reviews: two parallel read-only lanes (conformity + logic) to STATUS: complete / 0 findings at head fef34be. The finalisation fixed the eligibility emit-after-close guard, gated the KYC deeplink prefill to the active step, moved frozen_chf_label into lib/widgets/, marked test-only sync helpers @visibleForTesting, sorted imports per CONTRIBUTING.md, and added an eligibility close-mid-flight regression test.
  • CI (GitHub, this head): Analyze & Test, Coverage Floor Gate, Visual Regression all green.
  • Build-host run (CI-pinned toolchain): analyze clean, full unit/widget suite green, scoped line coverage 100% ≥ floor, coverage-visibility gate green.
  • Comments: no open PR comments or unresolved review threads.
  • Mergeability: MERGEABLE.

Deferred with rationale (not defects): the lenient referralJson* DTO helpers (locale-tolerant CHF/number parsing; strict casts would throw on locale-formatted API values); a few raw TextStyle(color:) sites (golden-affecting style refinement, follow-up); the NEEDS_TERMS retry-only screen (escapable via back, reachable only through an unusual backend state race — reviewer: not a blocker).

@JonnyLuca
JonnyLuca force-pushed the 01a03535-referral-promo-2 branch 2 times, most recently from 07b16c3 to fef34be Compare September 7, 2026 07:44
…e to pop.

Staying on /settings after unlock must still bind; KYC is gated separately.
A 200 with a non-JSON body is not retried, so the code is not restashed forever.
Leaving the page mid-fetch must not throw StateError.
SharedPreferences errors after a successful submit must not skip
checkKyc or SyncWalletServices. Guard context.mounted after the await.
A crash between backend acceptance and the success listener must not
lose the looked-up code; AlreadyRegistered will not re-open the step.
findTxIdIgnoreCase already matches the lowercased hash, so a second
query with the original casing cannot find a different row.
The dashboard gate is fail-closed: 2, -1, 0.5 and "yes" no longer open it.
InvitesLoading precedes InviteUrlLabel; referrals sits with the share keys.
Widget tests already construct ReferralOverviewPage with a real tree.
Lookup, bind and terms timeouts must not leave 30-second wall-clock futures.
The invites list paragraph now reads as one sentence; the lookup spinner
copy is a single pair of guillemets.
The auth gate returns 401 for a missing PNG too, so HTTP status cannot
prove Golden assembly. docker exec test -f checks the file exists.
A 200 with a non-JSON body must drop the stash instead of retrying.
TypeError is terminal for the same reason.
Take and discard serialize on the in-flight flag; a second take returns
null and the stash is empty when they finish.
Closing mid-getSummary must keep only the synchronous Loading emit.
Unlock restore schedules bind on the next frame so staying on settings
still consumes the stash.
Move the nine widget flows out of the unwired integration_test/ harness
so coverage CI executes them. Drop the unused SDK dependency.
dart: then Flutter SDK, then other packages, then realunit_wallet,
alphabetically within each group.
Skip and invalid lookup drop the typed prefs stash so KYC exit cannot
bind against the user's will. Submit awaits that stash so a crash after
backend accept still has the code.
Walk the whole match list so a payment push on a restored KYC stack
cannot bind or drop the pending code.
…d rows.

A settled prize with missing amount/created no longer lets sync report
success while hiding the rest of the ledger.
The SDK dep was removed from pubspec.yaml; the lock still listed it as
direct dev.
A second take after the race must also return null so a lost claim fails.
Double-tap during the new prefs awaits no longer issues two getUser
calls. The cubit is captured before those awaits, a newer deeplink is
not last-write-wins overwritten, and the tax CTA disables itself before
the first await. Tests go red if the stash-before-submit await is
removed.
apiBasedSync was fire-and-forget at both wallet-load callers, so a
parse throw became an uncaught async error nobody saw. Catch at the
bloc and show a snackbar when the flag rises.
A Pending row without amount/created threw FormatException before
isSettled could skip it, so the new fail-closed path fired on every
wallet load. Status is read from the raw JSON first.
anyOf(null, the original code) allowed the loss case. After the race
the stash must be empty so a second take cannot still bind it.
Drop duplicate Cupertino delegates, dispose extra GoRouters, point
deeplink docs at kReferralCodeMaxLength, load the real de TB in the
terms matrix, DRY the create-page harness, and move widget flows out
of the e2e/ name that the repo reserves for Maestro.
integration_test is gone from pubspec.yaml; flutter pub get drops the
four leftover transitives (flutter_driver, fuchsia_remote_debug_protocol,
sync_http, webdriver) that were not on staging.
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