Skip to content

Tags: electric-sql/electric

Tags

agents-desktop-canary

Toggle agents-desktop-canary's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
agents-mobile: store-readiness polish (manifests, permissions, icons,…

… splash) (#4626)

## Overview

Audit + polish pass on **`packages/agents-mobile`** focused on the **app
binary's** readiness for **App Store + Google Play review**
(store-listing content like screenshots/descriptions is out of scope).
The goal was low-hanging fruit and chores — missing manifests, icons,
permissions hygiene, cold-start polish — that matter for passing review.

Done as a 4-agent investigation (2 reading the code, 2 researching
current 2025–2026 Apple/Google requirements from official + community
sources), with the high-stakes claims verified first-hand against the
code and an Android prebuild.

This PR description is the **reference record** of that investigation so
future sessions on this branch don't repeat the research.

App context: Expo SDK 54, RN 0.81.5, New Architecture, expo-router,
managed (CNG) workflow, `com.electricsql.agents.mobile`, iOS min target
16.4. OAuth sign-in (GitHub/Google via system browser), AsyncStorage,
Sentry, photo attachments (expo-image-picker), WebView/DOM chat
timeline.

---

## What this PR changes

Three commits:

**1. Harden store config: privacy manifest, permissions, splash, icons**
(`app.config.ts`, assets, deps)

- **iOS Privacy Manifest** (`ios.privacyManifests`) declaring the
required-reason APIs used by AsyncStorage + Sentry (`CA92.1`, `C617.1`,
`35F9.1`, `E174.1`), `NSPrivacyTracking: false`, and crash/perf data
types (not linked, not tracking). Apple **auto-rejects uploads
(ITMS-91053)** that call these without a declaration; statically-linked
pods' own manifests aren't reliably read, so we declare app-level.
- **`microphonePermission: false`** on expo-image-picker + Android
`blockedPermissions` for `RECORD_AUDIO` and `READ_EXTERNAL_STORAGE`
(verified stripped via `tools:node="remove"`). The app uses the system
photo picker (content URIs, no broad media access) and never records
audio, so those only inflate the Play permission list.
`WRITE_EXTERNAL_STORAGE` is intentionally **not** blocked —
expo-image-picker's pre-Android-10 (API < 29) camera path hard-requires
it, so blocking it breaks photo capture on Android 7–9 (caught in the
independent permissions review).
- **Splash screen** — added `expo-splash-screen` +
`assets/splash-icon.png` (dark `#101217`, dark variant). There was none,
so cold start flashed blank white against the dark theme.
- **Opaque iOS icon** (flattened the transparent corners onto `#101217`;
Apple rejects icons with alpha) + **monochrome adaptive-icon layer** for
Android 13+ themed icons.
- **`expo-system-ui`** so `userInterfaceStyle: automatic` actually
applies on Android (prebuild warned it was a no-op without it).
- App `description`.

**2. Add error boundary and OAuth callback timeout** (`app/_layout.tsx`,
`app/oauth/callback.tsx`)

- `Sentry.ErrorBoundary` around the provider tree (themed "Try again"
fallback) so an uncaught render error is recoverable instead of a
blank/native crash a reviewer can hit. Sentry still reports the error.
- 10s escape hatch on `/oauth/callback` so a cold start with no pending
request can't spin on "Finishing sign-in…" forever — surfaces "Back to
sign-in".

**3. Clean up auth logging and legal link** (`cloudAuth.ts`,
`CloudAuthContext.tsx`, `AccountScreen.tsx`)

- Gate cloud-auth `console.warn` calls behind `__DEV__` (exported
`devWarn` helper, reused from the context) so token-exchange /
HTTP-status details don't hit production device logs.
- Point the account-deletion link at `electric.ax` to match the
terms/privacy URLs (it was on a different domain — `electric-sql.com`).

---

## ⚠️ NOT done in this PR — needs product/backend decisions

These are real review blockers but can't be config-only fixes:

1. **In-app account deletion** (Apple 5.1.1(v) + Google Play Data
deletion). `AccountScreen` currently only opens a web page whose own
copy says "your account is not deleted by tapping the button … email
support to start the request" — the canonical *rejected* pattern. Needs
a genuine in-app deletion flow calling a backend delete endpoint (the
same admin-API `cloudAuth` already uses) with a confirm dialog + local
sign-out. A web link is fine as the *secondary* path, not the only one.
- Reviewer trap: a reviewer may delete the demo account, breaking later
reviews — special-case it and document testing steps in review notes.
2. **Sign in with Apple** (Apple 4.8). iOS offers **only** Google +
GitHub social login (`OnboardingScreen.tsx`). 4.8 generally requires an
equivalent privacy-preserving login (SIWA) when social logins are the
mechanism. Needs `expo-apple-authentication` + backend support for Apple
as an identity provider, or a defensible "client for a specific service"
argument. iOS-only; Android can stay Google/GitHub. **Decision needed.**
3. **AI content report/flag** (Apple 1.2 UGC + Google Generative-AI
policy). An AI-chat app must let users flag/report generated content
in-app. **Verify whether `agents-server-ui` already provides this**; if
not it's a likely blocker on both stores (may also need block-user + a
visible support contact).

---

## Should-fix (mostly submission-process / out-of-binary)

- **Demo account + reachable server URL in review notes.** Login-gated
app = the #1 "couldn't sign in" rejection. The reviewer can't pass
onboarding without a working Electric Cloud account *and* a reachable
agents server. Reviews run from US/China IPs — ensure the demo backend
is reachable, disable 2FA (or document the bypass), and note that
sign-in opens an external browser so it doesn't look broken.
- **Chat WebView has no error/offline/retry state** (`app/session.tsx` +
`agents-server-ui` `EmbedApp.tsx`). The DOM embed shows
"Connecting…/Loading session…" indefinitely on connection failure;
server-down only surfaces as a red dot in the overflow menu. A reviewer
on a flaky network sees a permanent spinner = "broken." **Deliberately
not fixed here:** the embed ref exposes no connection/error signal, and
a speculative native health-poll overlay on this flash-sensitive screen
(see the Expo DOM embed flash issue) risks false "can't connect" flashes
that read worse than a spinner. Proper fix: expose connection state from
the embed ref, or add an error+retry state inside the shared
`EmbedApp.tsx` (affects desktop/web too).
- **Verify the built AAB** after an EAS production build: `bundletool
dump config --bundle=app.aab` shows `PAGE_ALIGNMENT_16K`, and the merged
manifest has no stray `AD_ID` / `QUERY_ALL_PACKAGES` (reconcile with
Data Safety / Advertising ID declaration if present).
- Permission purpose strings were tightened to be specific (Apple
rejects generic ones). Confirm the camera path (`launchCameraAsync`) is
actually used; if photo-library-only ships, drop `cameraPermission`.

## Polish / nice-to-have

- A11y quick wins: `accessibilityRole="link"` on the legal Terms/Privacy
`<Text onPress>`; decorative `Icon` SVGs not hidden from screen readers;
no Dynamic Type / `allowFontScaling` (fixed px sizes).
- Cold-start theme flash: `ThemeProvider` defaults to dark, so a
light-mode device may briefly flash dark before hydrating. Splash
mitigates; could seed the initial scheme from
`Appearance.getColorScheme()` synchronously.
- `resolveVersionCode()` dev fallback (`Date.now()/1000`) is a latent
footgun if a local build ever reaches a store upload (Play caps
versionCode at 2.1e9). CI always writes `.build-info.json` so low risk;
consider throwing on store profiles when neither env nor build-info is
present.

---

## Already satisfied by the stack (verified — no action)

- **Target API 35+** (Play min since 2025-08-31 / ext. 2025-11-01) — SDK
54 targets API 36.
- **16 KB page size** (Play, since 2025-11-01) — RN 0.81 compliant;
verify on the built AAB.
- **Edge-to-edge** (forced on API 36) — `edgeToEdgeEnabled: true` +
`react-native-safe-area-context` insets handled across screens.
- **AAB output** — `production`/`canary-store` use `distribution:
"store"`; keep the `apk` preview/canary profiles out of `eas submit`.
- **Play App Signing**, **no foreground service** — fine.
- **Encryption export compliance** — `ITSAppUsesNonExemptEncryption:
false` is correct (HTTPS/OAuth/AsyncStorage). **No ATT** needed (Sentry
isn't tracking, no IDFA) — don't add `expo-tracking-transparency`.
- **iOS 26 SDK / Xcode 26** required for uploads since 2026-04-28 —
build on EAS's current image (build target ≠ deployment target; min 16.4
stays).
- **New age-rating questionnaire** (since 2026-01-31) — answer in App
Store Connect.
- **Native debug symbols** Play warning is advisory (Sentry symbolicates
separately) — safe to ignore.

---

## Verification

`expo-doctor` 18/18 · `tsc --noEmit` clean · `eslint` clean · `vitest`
88/88 · `expo prebuild --platform android` confirmed the blocked
permissions (`tools:node="remove"`), the `<monochrome>` adaptive layer,
and the `splashscreen_logo` resources (incl. `drawable-night-*`). `pnpm
install --frozen-lockfile` passes on the trimmed lockfile.

> Generated with the help of multi-agent research; key dated sources
include Apple's privacy-manifest enforcement (2024-05-01), Guideline 4.8
revision (2024-01-25), AI-consent rule 5.1.2(i) (2025-11-13), iOS 26 SDK
requirement (2026-04-28); Google's target API 35 (2025-08-31) and 16 KB
page size (2025-11-01) deadlines.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>