New tab under the Giving hub, alongside Zakat/Sadaqah/Khairat. Two parts:
Donations: amount presets + custom amount, four frequencies (One-Time,
Monthly, Quarterly, Yearly), each mapped to its own live Polar.sh
checkout link (merchant of record — this app never touches payment
details, same 'log intent, don't move money' principle used everywhere
else). Design/copy modeled on the sibling moslem03.falahos.my app's
existing Support tab, per direct reference.
No donation product existed on the Polar account yet, so created one
via the Polar API with the user's explicit go-ahead: 'Support Nur
Falah' as 4 pay-what-you-want products (one-time + monthly/quarterly/
yearly recurring, quarterly via recurring_interval=month with count=3
since Polar has no native quarterly interval), plus a hosted checkout
link for each. Verified all four resolve to genuinely distinct, live
Stripe-backed checkout sessions before shipping — moslem03's own
checkout.polar.sh URL pattern turned out to be stale/non-resolving;
the current correct domain is buy.polar.sh via the /v1/checkout-links
API, discovered from Polar's live OpenAPI spec rather than guessed.
Partnership section: co-branding and white-label pitch with the VP
Sales contact info given directly — info@falahos.my and WhatsApp
+60132250691 — reachable via a 'View Partnership Opportunities' link
from the donation card, matching moslem03's UX pattern of surfacing
partnership discovery from the support flow.
Covered by e2e-support.cjs (14/14) — including opening all four
checkout links live and confirming each is a genuinely distinct,
resolving Stripe checkout session, not a dead or placeholder link.
Full regression: all suites pass (one confirmed transient flake on
rerun, unrelated to this change).
Implements the researched IA fix: mobile nav UX consensus caps primary
destinations at 4-5 (uxpin.com, fintech/banking 2026 UX research), and
this app had grown to 22 tabs in one horizontally-scrolling row —
exactly the anti-pattern that research flags for choice paralysis and
slower task completion.
New structure — 5 bottom-nav hubs, grouped by what the user is actually
trying to do, not build order:
- Home: Coverage (unchanged, still the landing screen)
- Estate: Assets, Faraid, Insurance, Wassiyah, Hibah, Family Waqf,
Nominate, Claims (H2)
- Giving: Zakat, Sadaqah, Khairat
- Family: Tree, Trigger, Mutawalli, Manage (was 'Family', renamed to
avoid colliding with the hub's own label), Neighbourhood
- Daily: Prayer Times, Qibla, Quran, Locate
Each hub reveals its own sub-nav one tap in, instead of every tab
competing for space in a single row. Settings moved out of the tab
strip entirely into a header gear icon, matching the banking-app pattern
of keeping settings out of primary thumb-reach real estate. The bottom
nav is now fixed (thumb-zone), sub-nav keeps the old sticky-top position.
Cross-component navigation (nav.js requestedTab, used by the Family
Tree's 'give sadaqah in memory' link) now resolves a tab label to its
owning (hub, sub-tab) pair instead of a flat index — verified live.
This touches every E2E suite: a single click on a tab's old selector no
longer reaches it (hub, then sub-tab). Added a shared gotoTab(page, label)
helper to e2e-auth-helper.cjs encapsulating the two-step navigation, and
migrated all ~22 affected test files off direct nav-button selectors —
mechanical substitution followed by manual fixes for local clickTab
wrappers, template-literal selectors, and active-state assertions that
needed to target the new .hub-tab/.subnav structure specifically.
Full regression after migration: every suite passes (one isolated
Family Tree flake confirmed clean on rerun, unrelated to navigation).
New e2e-auth-helper.cjs: shared signInFreshFamily() prelude — signs in as
the confirmed test owner account and creates a uniquely-named family per
run, so accumulated data from a previous run's assets/hibah/etc. (now
persisted in Supabase, not wiped with the browser context like localStorage
was) can never bleed into another run's percentage/coverage assertions.
All 8 suites (e2e-uat, e2e-fastpath, e2e-trust, e2e-business,
e2e-digital-vehicle, e2e-property, e2e-other, e2e-info) now call it in
place of the old anonymous page.goto(BASE).
Restoring them surfaced two real product bugs, not just test staleness:
1. WassiyahGenerator.svelte was never migrated to Supabase in the earlier
backend work — it still called the old local storage.js load()/save()
for assets, bequests, and witnesses, so the one-third meter silently
read an empty local cache and always showed 0. Migrated to family-scoped
Supabase tables (new nf_wassiyah_bequests, nf_wassiyah_settings, with
member-scoped RLS) matching the pattern used for Hibah/Nominations/etc.
2. storage.js's exportAll() did an unguarded JSON.parse on every
"nf."-prefixed localStorage key, but family.js stores activeFamilyId as
a raw string (not JSON-encoded) — one malformed parse threw and silently
aborted the whole export before the file download fired. Made exportAll
defensive: falls back to the raw string on a parse failure instead of
throwing.
The remaining test failures were async-timing gaps inherent to the move
from synchronous localStorage reads to async Supabase fetches: several
assertions checked <select> option counts or newly-created rows immediately
after a fixed short wait, before the async load/refresh had actually
landed. Fixed by replacing blind isVisible()/fixed-timeout checks with
proper waitFor()/polling in the test helpers (selectByText, corpus-select
population, row-creation checks) — not a product bug, but worth fixing
since the old timing assumptions no longer hold now that data is live and
shared instead of instant and local.
Results: e2e-uat 32/32, e2e-fastpath 16/16, e2e-trust 12/12,
e2e-business 10/10, e2e-digital-vehicle 10/10, e2e-property 9/9,
e2e-other 4/4, e2e-info 31/31 — 124/124. Re-verified e2e-family-agent
(12/12) and e2e-smoke-authed (24/24) still pass after the
WassiyahGenerator migration. 160/160 total across all ten suites.