Skip to content

v1.8.0 — bucket templates, account safety, honest privacy, and guided UX - #42

Open
ihabkhaled wants to merge 31 commits into
mainfrom
release/1.8.0/bucket-templates-ux
Open

v1.8.0 — bucket templates, account safety, honest privacy, and guided UX#42
ihabkhaled wants to merge 31 commits into
mainfrom
release/1.8.0/bucket-templates-ux

Conversation

@ihabkhaled

@ihabkhaled ihabkhaled commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Delivers the eleven requested v1.8.0 items on top of main (1.7.4 → 1.8.0).

What changed

  1. Buckets are reusable order templates. "Start round" opens an order
    session from a shared bucket, seeds the same members, and notifies each of
    them. The template is never renamed or consumed, so the same menu runs every
    week. Built on the existing ADR-0009 order-session architecture, which was
    complete but unreachable from the UI.
  2. Logout asks for confirmation and shows a busy state.
  3. Account deletion requires re-entering email and password; nothing is
    removed until they verify, in both Firebase and local-device modes.
  4. Zero is a placeholder, not a value, in every money and percentage field —
    typing 14 yields 14, not 014.
  5. Per-section loading. Headings, filters, and actions paint immediately;
    each section shows its own placeholder, and the two bucket collections
    resolve independently.
  6. OS tray notifications. In-app notifications mirror to the system tray
    while the app is backgrounded, and a tap opens the destination. Permission is
    only ever requested from a deliberate user action.
  7. Settings split into a hub plus preferences, privacy, security, and
    account pages.
  8. Privacy made real. Analytics consent now gates an actual sink: a capped
    device-local diagnostics window that never leaves the device, with a visible
    count and a clear action. Consent roams with the profile.
  9. Language and theme restore from the profile on a new device; system
    theme follows live OS changes.
  10. Guided tours on all nineteen screens, dismissible and replayable from
    Settings → Preferences.
  11. UX fixes from a manual browser pass — see below.

Evidence

  • 382 Vitest unit/component tests; 180 Playwright tests across Chromium desktop,
    Pixel 7, and iPad Mini; Firefox cross-browser suite green.
  • Firestore rules tests against the emulator (new optional analyticsConsent).
  • Firebase Functions tests (18), covering the order-round notification fan-out.
  • Android debug APK built locally against the new local-notifications plugin.
  • Manual browser walkthrough with zero console errors or warnings. It found and
    fixed two real defects: the notification prompt stacked on top of the
    first-run tour, and could interrupt a half-filled form.

Notes for review

  • Adding @capacitor/local-notifications expands the Android permission surface
    (POST_NOTIFICATIONS, RECEIVE_BOOT_COMPLETED, WAKE_LOCK) via manifest
    merging; recorded in docs/migration/native-security-audit.md.
  • Remote push (app fully closed) is not included — it needs a VAPID key,
    google-services.json, and APNs assets that live outside this repository.
    Prerequisites: docs/operations/push-notifications.md.
  • iOS is unvalidated (EXC-5); WebKit cannot launch on the Windows dev machine,
    so desktop WebKit and mobile Safari rely on this pipeline's Linux runners.
  • No new environment variables. .env.example gained the previously undocumented
    CONTACT_EMAIL_PROVIDER key to close a drift with .env.
  • The in-app brand still reads "FoodOrder" while the native app is
    "Gama3 Orderak" — left for an owner decision rather than changed unilaterally.

🤖 Generated with Claude Code

v1.8.0

Build: the branch prerelease publishes the APK once every gate passes.

Highlights

  • Buckets are reusable order templates. "Start round" opens a new order session
    from a shared bucket, seeds the same members automatically, and notifies each
    of them — the template itself is never renamed, consumed, or duplicated, so
    the same menu can run every week.
  • Duplicating a bucket now keeps its exact title; the "(copy)" suffix is gone.
  • Replaced the "0" trap in every money and percentage field: a zero renders as a
    placeholder, so typing 14 produces 14 instead of 014.
  • Logging out asks for confirmation and shows a busy state while the session
    ends, instead of signing out on a single mis-tap.
  • Deleting an account now requires re-entering the account email and password;
    nothing is deleted until those credentials verify, in both Firebase and
    local-device modes.
  • Split the single dense settings screen into a hub with four focused subpages:
    preferences, privacy, security, and data & account.
  • The friends area follows the same shape: /social is a hub with counts and
    three rows, and search + friends list, pending requests, and groups each get
    their own page. Social notifications deep-link to the right subpage — an
    invitation opens Requests, a group change opens Groups.
  • Bucket sharing too: the share screen keeps the invite code and freeze
    controls, while member roles/permissions and the activity log move to their
    own pages behind labelled rows with a member count.
  • The deployment gate no longer fails when Google throttles its unauthenticated
    smoke probes: a persistent HTTP 429 can only come from an endpoint that
    exists (a missing function returns 404), so it is recorded as a warning while
    a real 404/5xx still fails. Probes are also spaced out so five callables do
    not trip the same quota.
  • Language and theme restore from the signed-in profile on a new device, and the
    system theme now follows live operating-system light/dark changes.
  • Analytics consent controls a real, consent-gated sink: diagnostics are written
    to a capped device-local window that never leaves the device, and the privacy
    screen shows the exact stored count with a clear action.
  • Each screen loads section by section. The heading, filters, and actions paint
    immediately while individual sections show their own placeholders, and the two
    bucket collections now resolve independently of each other.
  • In-app notifications mirror into the operating-system notification tray while
    the app is in the background, and tapping one opens its destination in the
    app. Permission is only ever requested from a deliberate user action.
  • Guided tours cover all nineteen screens in 114 steps — dashboard, buckets,
    the bucket editor, sharing, collaboration, joining, orders, order details,
    order creation, order rounds and their details, friends, and every settings
    page. Each screen walks through five to eight explanations rather than a
    single hint, dims the page behind it, and highlights the element being
    described where there is one. Three deliberate exits: Got it retires that
    screen's tour, Skip ends only this visit, Skip all silences every
    screen. All of them replay from Settings → Preferences. Tour copy lives in a
    per-screen use-*-tour.hook.ts rather than inline in the containers.
  • Arabic Franco is a first-class thirteenth language. Egyptian Arabic written
    in Latin script is hand-written idiomatic copy — "tasgeel el khoroog", not a
    transliteration table — across the app UI, the six feature catalogues, Firebase
    error messages, password-reset email, and all ten public marketing pages. It is
    selectable in Settings and served at /ar-latn, with its own sitemap, RSS feed,
    and hreflang entries.
  • Remote push delivery: registered devices receive notifications through Firebase
    Cloud Messaging even with the app closed. Tokens live under
    users/{uid}/pushTokens, are pruned when they go stale, and are removed on
    sign-out so a shared phone never receives the previous account's rounds.
  • Vercel Analytics and Speed Insights ship behind the same analytics consent that
    governs local diagnostics — neither loads until consent is granted.
  • The public site has a light/dark switch beside the language menu. It defaults
    to the operating system, an explicit choice overrides it in either direction,
    and the choice is applied from an inline script before first paint so no page
    flashes the wrong palette. The prerendered pages stay free of external
    scripts.
  • The public header is one tidy row at every width: the brand, the language
    menu, and the call to action, with all seven destinations in a collapsible
    list. Seven translated labels never fitted beside them — the bar reflowed
    into three rows in most locales, and the name was being cut short. Fixed
    alongside three layout faults found while measuring it: a call-to-action that
    wrapped mid-label, dropdown panels that claimed layout space while closed and
    gave narrow phones a horizontal scrollbar, and a hero grid whose 1fr track
    took its minimum from content so one long German compound word widened the
    page. Verified across all thirteen locales at nine widths.
  • The product name is translated per locale — "Collect Your Order" in English,
    "جمع أوردرك" in Arabic, "Gama3 Orderak" in Franco, and its own rendering in
    the other ten — across the app shell, the public header and footer, the
    og:site_name tag, and the structured data. It used to read the Franco name
    in every language but English and Arabic. The square brand mark no longer
    shrinks to make room for a long name; the name wraps instead.
  • Google AdSense loads only on indexable public marketing pages, with ads.txt
    generated from ADSENSE_PUBLISHER_ID. The logged-in app shell and all noindex
    system pages stay ad-free, and npm run public:validate enforces it.

Full notes: release-notes/v1.8.0.md

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
food-order-v1 Ready Ready Preview Aug 1, 2026 11:35am

/ar-latn returned the SPA 404 shell because three routing allow-lists still
listed only the twelve original locales, and Arabic Franco was app-only: the
landing page had no Franco copy at all.

Franco is now the thirteenth public locale rather than an app-only register.
Adding the full marketing translation — ten pages, the UI chrome, the three
system pages, and the contact form — lets the locale drop the fallback that
quietly redirected Franco readers to formal Arabic.

- Route `ar-latn` in the browser prefix pattern (before `ar`, so the longer
  segment wins), the static dev server, and both vercel.json rewrites.
- Derive the sitemap URL and locale counts from the catalog instead of the
  hardcoded 120/12, so the next locale does not need a validator edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five to eight tours now cover every one of the nineteen screens, so each one
explains what it does rather than dropping a single hint and disappearing.

Each screen's copy moves into its own use-*-tour.hook.ts. Fourteen containers
were carrying their step arrays inline in JSX, which was already crowded at two
entries and unreadable at seven; the five screens that had hooks keep the
callback-ref spotlight targets they already used.

Steps that describe how a screen behaves centre their card instead of pointing
at a control, which also keeps them readable on a narrow phone.

Adds a locale-switch regression test that drives the in-app language selector
to Arabic Franco and follows it through the redirect and a reload. Verified to
fail against the previous single-part locale prefix matcher, which matched the
`ar` in `/ar-latn`, rejected the tail, and sent Franco readers back to English
on the next load.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Switching to Arabic Franco while signed in snapped back to the previous
language on the next load. The deployed Firestore rules gate the profile
`locale` field against an allow-list that predated Franco, so the write was
refused with PERMISSION_DENIED, the stored profile kept its old language, and
the post-login profile load navigated the reader back to it. Confirmed against
the live project: writing `fr` returned 200 while `ar-Latn` returned 403.

The rule already listed `ar-Latn` in this repo but had never been deployed, so
nothing in the codebase revealed the drift. Rules tests now assert that every
locale the app offers is accepted and that a wrongly-cased `ar-latn` is
refused, which turns the next such drift into a red gate.

Separately, the product name read "Gama3 Orderak" in all eleven locales other
than English and Arabic. Each locale now says "collect your order" in its own
language across the app shell, the public header and footer, `og:site_name`,
and the structured data, so the JSON-LD name matches the rendered one. The
brand name moves from the single global `site.brandName` into per-locale UI
copy, and the type now requires it so a locale cannot silently omit it.

The square brand mark had no `flex: none` and was being squeezed out of shape
by the longer name; it now holds its size and the name wraps beneath it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seven navigation labels never fitted beside the brand, language menu, and call
to action. Measuring the natural width the bar needs showed 1610px in French
and over 1470px in five other locales, so on any normal screen the row was
being squeezed until labels broke mid-word and the header grew to three lines.

The bar now carries only the brand, the language menu, and the call to action,
with every destination in a collapsible list at all widths. The full product
name is always shown — an earlier ellipsis hid it, which defeats the point of
naming the product in the reader's language.

Three further faults surfaced while measuring, all fixed here:

- The call to action had no `flex: none`, so it shrank and wrapped "Eftah el
  app" over three lines into a tall block.
- The dropdown panels set `display` unconditionally, which cancels the
  browser's hiding of closed <details> content. Both panels stayed laid out
  off-screen while closed and gave narrow phones a horizontal scrollbar.
- The narrow hero used `grid-template-columns: 1fr`. A plain `fr` track takes
  its minimum from content, so "Gruppenbestellungen" stretched the column past
  the viewport. `minmax(0, 1fr)` lets it shrink.

The last two predate this branch and reproduce on production today.

Verified across all thirteen locales at nine widths from 320px to 1600px: the
header is a single row except at 320px, the name is never clipped, and no page
scrolls sideways.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The public pages followed the operating system with no way to override it.
A switch now sits beside the language menu, with the icon showing the theme
you would move to rather than the one you are in.

An inline script applies the stored choice before first paint, so a reader who
picked light on a dark machine never sees a dark flash while the page loads.
It is inline on purpose: these pages ship no external scripts, and the
validator enforces that.

Both directions had to be made to work. The existing dark rules lived in a
prefers-color-scheme block, so scoping only the custom properties left the
background rules applying on a dark system and an explicit light choice had no
visible effect. Every selector in that block is now scoped with
:not([data-theme='light']).

Browser access goes through a new platform adapter rather than the module, and
the React public shell drives the same markup through a hook so client-side
navigation matches the prerendered output.

Verified on both a light and a dark simulated system: initial state follows the
OS, either choice survives a reload and a navigation, and the switch is present
and labelled in all thirteen locales.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The friends screen carried six sections at once; sharing carried four. Both
now follow the settings-hub shape.

/social is a hub — counts up top, one row per destination — with three
subpages: /social/friends (search plus the friends list), /social/requests
(friend, group, and bucket invitations, with a pending count on the hub row),
and /social/groups (creation, editing, membership). Social notifications now
deep-link to the right subpage instead of the old catch-all screen, in both
the local gateway and the Cloud Functions fan-out.

/buckets/:id/share keeps the invite code, enable, and freeze controls; member
roles/permissions and the activity log move to /share/members and
/share/activity behind rows that carry the member count.

The settings link-row component was about to gain its third copy, so it moves
to src/shared/ui as LinkRow with its styles in the global sheet; settings now
consumes the shared one.

Also in this commit, three green-gate fixes the PR run surfaced:

- Branch All Gates Green failed because the pre-commit eslint --fix touched a
  spec one byte after the knowledge build, leaving stale .ai manifests;
  rebuilt in the same commit that changes sources.
- The deployed-callable smoke test no longer fails on a persistent HTTP 429:
  throttling can only come from an endpoint that exists (a missing function
  returns 404), so it downgrades to a warning while 404/5xx still fail, and
  probes are spaced so five callables do not trip the same quota.
- The notification-mirroring e2e raced the subscription baseline: the first
  payload is never mirrored, so on a slow runner the test's notification was
  folded into it and the tray stayed empty. The test now proves the baseline
  landed (badge visible) before backgrounding, like its sibling always did.

Validation: 195/195 across chromium, mobile-chrome, and tablet-chrome — the
exact matrix the E2E gate runs — plus the unit, i18n, knip, and typecheck
gates, and a browser walkthrough of the new hub and subpages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three related changes to how a build identifies and describes itself.

Build numbers now count builds of one version. They came from
github.run_number, a repository-wide counter that never resets, so `1.8.0-347`
said nothing about how many times 1.8.0 had been built. The number is derived
from the release tags that already exist, which makes it per-version and
self-healing: it increases within a version and restarts at 0 when the version
changes, with nobody remembering to reset it. Branch prereleases follow the
same rule as `X.Y.Z-dev.<n>`.

Releases now publish the version's release notes. A prerelease body used to be
one generated sentence — "Automated prerelease from branch X. Every mandatory
branch gate passed for commit Y." — which describes the process and leaves a
reader no way to know what shipped. Every release body is now composed from
release-notes/vX.Y.Z.md plus that build's own provenance.

Pull requests now carry the same notes as their description, written from the
`## Highlights` section, alongside a link to the APK built from the exact head
commit. The job rewrites only the block between its markers, so anything an
author or reviewer types around it survives every later push.

Written down as a skill (skills/write-release-notes.md), a rule
(rules/20-release-gates.md forbids a generated release body and a
repository-wide build counter), and operations docs.

Two gaps found while doing this:

- tests/tooling/*.test.mjs was never executed. vitest.config.ts includes only
  .test.{ts,tsx}, and these use node:test, which vitest does not provide. The
  pre-existing versioning test had never run in CI. Added npm run test:tooling
  and wired it into both gate workflows, so the nine tooling tests now gate.
- A git worktree under .claude/ was being linted, format-checked, and indexed
  as a second copy of the repository — 1806 files instead of 943, which would
  have failed the manifest diff gate in CI where no such directory exists.
  Excluded from ESLint, the format checker, the knowledge build, and git.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant