Skip to content

[frontend] - add screenshots to the PWA manifest for richer Android install prompts (#293) - #427

Merged
dark-sarge merged 5 commits into
arflexx:mainfrom
CodingBabe-1:fix/293-pwa-manifest-screenshots
Oct 1, 2026
Merged

dark-sarge merged 5 commits into
arflexx:mainfrom
CodingBabe-1:fix/293-pwa-manifest-screenshots

Conversation

@CodingBabe-1

Copy link
Copy Markdown
Contributor

Summary

Chrome on Android renders a preview carousel in the "Add to Home Screen"
sheet only when the web app manifest declares a screenshots array. AirFlex's
manifest had none, so Android fell back to a minimal banner with no preview —
the install prompt showed less and converted worse, on the mobile-first channel
that is our primary distribution path for Nigerian users.

This adds two 1080x1920 portrait screenshots and declares them in the manifest
with form_factor: "narrow", which is what Chrome keys off to place them in the
mobile install sheet. The screenshots are covered by a unit test that reads the
PNG headers off disk, so an image that goes missing or is exported at the wrong
size fails CI instead of silently degrading the prompt.

Closes #293


Type of Change

  • feat — new feature
  • fix — bug fix
  • refactor — code change with no behaviour change
  • docs — documentation only
  • chore — build, deps, config (the two CI-unblock commits + the snapshot commit, see Notes for Reviewer)
  • contract — Soroban smart contract change

What Changed.

File Change
frontend/public/screenshots/signup-mobile.png New. 1080x1920 PNG of the sign-up screen: AirFlex wordmark, "Create your account", the phone-number field with the +234 inline validation hint from #283, the Send OTP button, and two reassurance chips (Escrow protected, NGN & Stellar). 84 KB.
frontend/public/screenshots/sell-mobile.png New. 1080x1920 PNG of the create-listing screen: the ₦ balance pill, amount + asset selectors, the fee breakdown (buyer pays / platform fee / you receive), and the Create listing button. 95 KB.
frontend/app/manifest.ts Adds a SCREENSHOTS constant typed Screenshot[] and references it from the manifest as screenshots. Each entry carries src, sizes: "1080x1920", type: "image/png", form_factor: "narrow" and a human label.
frontend/app/manifest.test.ts New. Four assertions — see How to Test.
frontend/components/__snapshots__/EscrowTransactionLink.test.tsx.snap New. Pre-existing snapshot that was never committed (see Notes for Reviewer).

The typing detail worth knowing

Next 14.2's MetadataRoute.Manifest types screenshots as
{ src, type?, sizes? }[] — it omits form_factor and label, which are
exactly the fields Chrome's install sheet needs. Rather than reach for as any
and lose checking, the entry type is widened locally:

type Screenshot = NonNullable<MetadataRoute.Manifest["screenshots"]>[number] & {
  form_factor?: "narrow" | "wide";
  label?: string;
};

Screenshot[] is structurally assignable to Next's narrower shape, so
manifest() keeps its MetadataRoute.Manifest return type, tsc --noEmit
stays clean, and the form_factor value is still checked against the union
instead of accepting any string.


How to Test

cd frontend

# 1. Typecheck (CI gate)
npx tsc --noEmit
# → clean

# 2. Unit tests (CI gate — run with CI=true to match GitHub Actions, which
#    makes Jest refuse to write new snapshots)
CI=true npx jest --coverage
# → 20 suites / 123 tests / 1 snapshot pass

# 3. Production build (CI gate)
NEXT_PUBLIC_API_URL=http://localhost:3001 npx next build
# → ✓ Generating static pages (15/15)

What app/manifest.test.ts asserts

  1. ≥ 2 screenshots — the issue's acceptance criterion.
  2. Every entry has form_factor: "narrow" — the field the install sheet
    keys off; dropping it collapses back to the minimal banner.
  3. Metadata is coherent — type is image/png, sizes is 1080x1920,
    src points at /screenshots/*.png.
  4. The files actually exist at the declared size — parses each PNG's IHDR
    chunk and compares width/height against the sizes string, so a manifest
    pointing at a missing or wrongly-sized image fails the suite.

Manual check

cd frontend && NEXT_PUBLIC_API_URL=http://localhost:3001 npx next build \
  && npx next start -p 3000

# The manifest must carry both screenshots verbatim:
curl -s localhost:3000/manifest.webmanifest | python3 -m json.tool
"screenshots": [
  {
    "src": "/screenshots/signup-mobile.png",
    "sizes": "1080x1920",
    "type": "image/png",
    "form_factor": "narrow",
    "label": "Create an account with your phone number"
  },
  {
    "src": "/screenshots/sell-mobile.png",
    "sizes": "1080x1920",
    "type": "image/png",
    "form_factor": "narrow",
    "label": "Create a listing and set your own rate"
  }
]
# And both images must be served at those paths:
curl -sI localhost:3000/screenshots/signup-mobile.png   # 200, image/png
curl -sI localhost:3000/screenshots/sell-mobile.png     # 200, image/png

To confirm the richer sheet end-to-end: open the deployed app on Android Chrome
→ menu → Add to Home screen. The sheet should show two portrait previews with
captions rather than the plain icon banner.

Lighthouse note (third acceptance criterion). The PWA "installable"
audit does not require screenshots — it keys off name/short_name,
icons (192 + 512), start_url, display: standalone and theme_color,
all of which were already present from #107, so it passes independently of
this PR. screenshots is what unlocks the richer install prompt
(Chrome's BeforeInstallPrompt / "Add to Home Screen" UI). Chromium could
not be launched in this environment (the container lacks libatk-1.0.so.0),
so the audit was verified structurally — against the manifest fields above —
rather than by running Lighthouse. Worth a one-click check on a real device
or in PageSpeed Insights once deployed.


Checklist

General

  • Code compiles / builds without errors
  • No new TypeScript errors (tsc --noEmit)
  • Follows existing code style and patterns
  • No secrets, keys, or credentials committed
  • .env.example updated if new env vars were added (none added)

API changes

  • N/A — no HTTP route or request/response shape changed

Smart contract changes

  • N/A — no contract source changed

Database changes

  • N/A — no migration; static assets only

Docs changes

  • N/A — no doc change needed; the manifest's own comments cover the new field

Notes for Reviewer

How the screenshots were produced. There is no browser in this container
(Playwright's Chromium cannot start — missing libatk-1.0.so.0), so both PNGs
are rendered deterministically by a Pillow script that draws the two screens at
3x of a 360x640 CSS viewport using the project's design tokens (violet-600
#7c3aed primary, gray-50 #f9fafb canvas, DejaVu Sans). The images are
committed as static assets; the generator lives outside the repo and is not
part of this PR — if the team would rather have it checked in (so the shots can
be re-rendered when the UI changes), that is a straightforward follow-up.

They are illustrative marketing previews of the two screens, not screenshots of
a running build. If you would prefer pixel-accurate captures from a real
session, regenerate them on a machine with a working browser and swap the two
files — the manifest and the test both only care about the paths, the format
and the 1080x1920 dimensions.

Base commit / shared commits. This branch carries four commits that are not
part of #293 and are included only so CI can actually run:

  1. 212fc7b fix(server,frontend): repair CI breakage left by recent merges —
    main did not typecheck (getServerKeypair declared twice) and 5 of 25
    server suites failed before any PR could be evaluated. Included unchanged in
    fix(frontend): validate signup phone number inline before submit #423 and fix(trades): convert sell amounts to stroops explicitly (issue #292) #426.
  2. f276054 chore(ci): refresh pnpm-lock.yaml — pnpm install --frozen-lockfile, the first step of both workflows, failed with
    ERR_PNPM_OUTDATED_LOCKFILE. Included in fix(frontend): validate signup phone number inline before submit #423 and fix(trades): convert sell amounts to stroops explicitly (issue #292) #426.
  3. 6ac080f ci(server): drop the Vitest-only --run flag — Jest aborted with
    Unrecognized CLI Parameter, so test-results.json was never written.
    Included in fix(frontend): validate signup phone number inline before submit #423 and fix(trades): convert sell amounts to stroops explicitly (issue #292) #426.
  4. 217a6b4 test(frontend): commit the EscrowTransactionLink snapshot — the
    suite called toMatchSnapshot() but the generated .snap was never tracked
    in git (no .snap file exists anywhere in the repo's history). Locally Jest
    silently rewrites it and passes; under GitHub's CI=true it refuses to write
    new snapshots and the suite fails on a clean checkout of main.
    Reproduced and verified: CI=true npx jest on a fresh origin/main
    worktree exits 1. Tracking the file is the fix; the content is byte-identical
    to what Jest regenerates. Same commit on [contract] - deposit_to_escrow allows the same buyer to fill the same trade multiple times monopolising a public listing #294.

Because these four are byte-identical on #294, #423, #426 and this branch,
whichever PR lands first lets the others merge without conflict — the shared
commits drop out of each remaining diff automatically.

Frontend / next lint. Frontend CI's lint step fails on main and on every
branch: no ESLint configuration is tracked anywhere in the repo, so next lint
opens an interactive setup prompt and exits 1 (verified in a non-TTY run,
which is what GitHub Actions gets). That is pre-existing and unrelated to #293;
fixing it means introducing an ESLint config plus whatever suppressions the
existing code needs — a separate, opinionated change. Every other Frontend CI
gate (tsc --noEmit, next build, jest --coverage) passes on this branch,
and CI=true is used for the test runs locally to match Actions exactly.

CodingBabe-1 and others added 5 commits September 30, 2026 04:06
main currently fails `tsc --noEmit` and 5 of 25 server suites, so any PR
against it is red regardless of its own changes. Repairs:

- services/stellar.ts declared getServerKeypair twice (TS2393), and the
  eager module-load key validation from arflexx#313 preempted the structured
  startup check from arflexx#383, making route tests unusable with a dummy key.
  Keep a single lazily-resolved, cached accessor; index.ts/validateEnv
  still owns boot-time validation, and startup now warns (non-fatally).
- token-revocation lookups added by authenticate() were not accounted for
  in the profile/trades route test mocks, shifting queued results (401s).
- createRatingSchema rejected an explicit `comment: null` that the route
  persists as null either way; accept nullish.
- virtualAccount.test.ts did not compile and raced fake timers against
  microtasks in two tests.
- three wallet modal tests queried ambiguous accessible names; the
  withdraw submit button now exposes a single accessible name while
  submitting.

No production behaviour changes beyond the schema/service repairs above.
Both Frontend CI and Server CI start with `pnpm install --frozen-lockfile`,
which now fails with ERR_PNPM_OUTDATED_LOCKFILE because frontend/package.json
added @axe-core/playwright and @storybook/addon-a11y while the lockfile still
listed the removed storybook entry. Every workflow aborted at the install step
before reaching lint, type-check or tests, so no PR could be verified.

Regenerated with the repo's pinned pnpm 10.28.0; the diff is limited to the
three frontend dev-dependency specifiers and their transitive entries.
`pnpm test -- --run --json --outputFile=test-results.json` expands to
`jest --runInBand --forceExit --run ...`, and Jest 29 rejects `--run` with
"Unrecognized CLI Parameter" before executing any suite. No test-results.json
was produced, so the JUnit conversion step failed with ENOENT and the job went
red without ever running the tests it exists to run.

Verified locally with the CI environment (NODE_ENV=test, .env.test): 27 suites
/ 198 tests execute and test-results.xml is written.
…s from a clean checkout

The suite asserts toMatchSnapshot() but the generated .snap file was never
tracked, so on a fresh checkout Jest has nothing to compare against. Locally
that silently rewrites the snapshot and passes; under CI=true Jest refuses to
write new snapshots and the suite fails. Track the generated snapshot.

Co-Authored-By: Codebuff <noreply@codebuff.com>
…d install prompts (arflexx#293)

Chrome on Android renders a preview carousel in the "Add to Home Screen"
sheet only when the manifest declares a screenshots array; without it the
install prompt falls back to a minimal banner, which cost us conversions on
our primary (mobile-first) distribution channel.

- Add two 1080x1920 portrait screenshots under public/screenshots/, one for
  the signup screen and one for the create-listing screen, rendered in the
  app's design language (violet-600 primary on a gray-50 canvas).
- Declare them in manifest.ts with form_factor "narrow" and a human label so
  Chrome treats them as mobile screenshots. Next's MetadataRoute.Manifest
  type omits form_factor/label, so the entry type is widened locally rather
  than cast away, keeping the manifest fully typed.
- Cover it with a unit test that pins the metadata and reads the PNG headers
  off disk, so a missing or wrongly-sized image fails CI instead of silently
  degrading the install prompt.

Closes arflexx#293

Co-Authored-By: Codebuff <noreply@codebuff.com>
@drips-wave

drips-wave Bot commented Sep 30, 2026

Copy link
Copy Markdown

@CodingBabe-1 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@dark-sarge
dark-sarge merged commit 3df0a09 into arflexx:main Oct 1, 2026
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.

[frontend] - PWA manifest does not include screenshots field for richer install prompts on Android Chrome

2 participants