Skip to content

feat(web): link the mobile app's store listings in the page footer - #665

Merged
krishagel merged 1 commit into
mainfrom
claude/app-store-footer
Nov 2, 2026
Merged

krishagel merged 1 commit into
mainfrom
claude/app-store-footer

Conversation

@krishagel

Copy link
Copy Markdown
Member

What

Every operator/admin page and every sign-in page now ends with a footer: Get the mobile app: App Store · Google Play.

How

  • Google Play is derived from the existing psdEoc:androidApplicationId.
  • App Store needs the numeric Apple ID, so there is a new optional tenant key psdEoc:appleAppStoreId. It is passed to App Runner as PSD_EOC_APPLE_APP_STORE_ID only when set, and read by appleAppStoreId(), which fails closed on a malformed value.
  • mobileStoreListings() builds both URLs. Each one is null when unconfigured, and the footer renders nothing when both are null.
  • StoreLinksFooter awaits connection() so the config is read at request time. Without that, a page prerendered at build would keep the build's empty values.

Evidence

  • bun run check passed: 6/6 shards, 520 pass, 0 fail; mobile native 88/88.
  • New tests:
    • store-listings.test.ts: unconfigured, configured, one store only, malformed id.
    • store-links-footer.test.tsx: both stores, one store, none.
    • The stack config test was extended to cover the new key.
  • Local /login rendered the footer with https://apps.apple.com/app/id6801607849 and https://play.google.com/store/apps/details?id=net.psd401.eoc. Both listings return 200.

Deploy note

The App Store link appears in production only after a cdk deploy. psdEoc:appleAppStoreId: "6801607849" has been added to the git-ignored infra/cdk.local.json. The Play link appears on the next deploy with no config change.

Staff on the web had no in-app way to find the mobile app. Every operator
and admin page (OperatorShell) and every sign-in page ((auth) layout) now
ends with a footer: "Get the mobile app: App Store · Google Play".

Store links are tenant configuration, not literals:
- Google Play is derived from the existing psdEoc:androidApplicationId
  (https://play.google.com/store/apps/details?id=<id>).
- App Store needs the numeric Apple ID, which cannot be derived from the
  bundle id, so a new optional tenant key psdEoc:appleAppStoreId is read
  in infra/src/stack/config.ts (pattern ^[1-9][0-9]{5,11}$), passed to App
  Runner as PSD_EOC_APPLE_APP_STORE_ID only when set, and read by
  appleAppStoreId() in packages/server/lib/config/deployment.ts, which
  fails closed on a malformed value like its siblings.
- mobileStoreListings() (lib/config/store-listings.ts) builds both URLs;
  each is null when unconfigured, and the footer renders nothing when
  both are null, so a district without store builds links to nothing.

StoreLinksFooter awaits connection() so the env is read at request time
and never baked into a page prerendered at build. The footer is rendered
without a Suspense boundary, consistent with OperatorShell's note about
keeping the shell in the initial HTML. Links open in a new tab with
rel="noopener noreferrer".

Example config (cdk.local.example.json, synthesize-example.ts), the docs
contract index and prose in docs/CONFIGURATION.md, and the stack config
test cover the new key. New tests: store-listings.test.ts (unconfigured,
configured, independent, malformed) and store-links-footer.test.tsx
(both, one, none).
@claude

claude Bot commented Nov 2, 2026

Copy link
Copy Markdown

Review: PR #665, store links in the page footer

IMPORTANT: none. The diff is clean on correctness, security, student data, and test/CI weakening.

What I checked by reading (I did not run tests, lint, or the build):

  • Config handling: appleAppStoreId trims and rejects malformed values (packages/server/lib/config/deployment.ts, optional() at :217 and the new reader). The Play URL is built with encodeURIComponent, and React escapes the rest, so no injection path.
  • Infra wiring: the App Runner env var is added only when set (infra/src/stack/psd-eoc-stack.ts, the PSD_EOC_APPLE_APP_STORE_ID block). The example synthesis asserts it with hasResourceProperties (infra/bin/synthesize-example.ts), and synth:example runs from typecheck and CI (infra/package.json:12, .github/workflows/ci.yml:184), so that wiring is gated.
  • Footer placement: OperatorShell and the (auth) layout are separate trees, so the footer does not render twice. connection() keeps the config read at request time.
  • Tests: the new tests assert specific output (exact URLs, '' for no stores, rejection of id1234567890, net.example.eoc, 0123456). The stack identity test covers both the unset and set cases.
  • Data handling: no student data, no logging, no external calls beyond rendered links.

NIT

  1. openwiki/operations/infrastructure.md:65 and openwiki/operations/configuration-and-readiness.md:59 list the env vars the web service receives. Neither mentions PSD_EOC_APPLE_APP_STORE_ID, so both are now incomplete.
  2. packages/server/app/nav/store-links-footer.tsx:38 opens links with target="_blank" and nothing tells assistive tech that a new tab opens. A visually hidden "(opens in new tab)" would fix that.

Nothing else to report.

@krishagel
krishagel merged commit 3632b93 into main Nov 2, 2026
17 checks passed
@krishagel
krishagel deleted the claude/app-store-footer branch November 2, 2026 19:52
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