Skip to content

agents-mobile: store-readiness polish (manifests, permissions, icons, splash)#4626

Open
msfstef wants to merge 4 commits into
mainfrom
msfstef/mobile-app-polishing
Open

agents-mobile: store-readiness polish (manifests, permissions, icons, splash)#4626
msfstef wants to merge 4 commits into
mainfrom
msfstef/mobile-app-polishing

Conversation

@msfstef

@msfstef msfstef commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

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 Working with Postgres WAL format from Elixir #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 outputproduction/canary-store use distribution: "store"; keep the apk preview/canary profiles out of eas submit.
  • Play App Signing, no foreground service — fine.
  • Encryption export complianceITSAppUsesNonExemptEncryption: 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

@msfstef msfstef added the claude label Jun 18, 2026
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Electric Agents Desktop Builds

Build artifacts for commit acaecff.

Platform Status Artifact
macOS Apple Silicon Passed DMG
macOS Intel Passed DMG
Windows x64 Passed Installer
Linux x64 Passed AppImage / deb

Workflow run

@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 59.25%. Comparing base (ac2391d) to head (acaecff).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4626      +/-   ##
==========================================
+ Coverage   57.71%   59.25%   +1.53%     
==========================================
  Files         342      385      +43     
  Lines       40009    42734    +2725     
  Branches    11659    12273     +614     
==========================================
+ Hits        23091    25320    +2229     
- Misses      16881    17339     +458     
- Partials       37       75      +38     
Flag Coverage Δ
packages/agents 72.96% <ø> (ø)
packages/agents-mcp 77.70% <ø> (?)
packages/agents-mobile 80.67% <ø> (ø)
packages/agents-runtime 83.38% <ø> (+0.01%) ⬆️
packages/agents-server 75.32% <ø> (+0.02%) ⬆️
packages/agents-server-ui 7.51% <ø> (ø)
packages/electric-ax 47.62% <ø> (ø)
packages/experimental 87.73% <ø> (?)
packages/react-hooks 86.48% <ø> (?)
packages/start 82.83% <ø> (?)
packages/typescript-client 91.83% <ø> (?)
packages/y-electric 56.05% <ø> (?)
typescript 59.25% <ø> (+1.53%) ⬆️
unit-tests 59.25% <ø> (+1.53%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@claude

claude Bot commented Jun 18, 2026

Copy link
Copy Markdown

@-

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Electric Agents Mobile Build

Local mobile checks ran for commit acaecff.

The EAS Android preview build was skipped because the mobile-eas-build label is not present.
Add the mobile-eas-build label to this PR to produce an installable preview build.

Workflow run

msfstef and others added 3 commits June 18, 2026 16:52
…ash, icons

Cover binary-readiness gaps for App Store / Play review:

- Declare iOS privacy manifest (ios.privacyManifests) for the
  required-reason APIs used by AsyncStorage and Sentry. Apple
  auto-rejects uploads (ITMS-91053) that call these without a
  declaration; statically-linked pods' own manifests aren't reliably
  read, so declare at app level. NSPrivacyTracking is false and
  crash/perf data is declared not-linked / not-tracking.
- Set expo-image-picker microphonePermission:false and block
  RECORD_AUDIO + legacy READ/WRITE_EXTERNAL_STORAGE on Android. The app
  uses the system photo picker (content URIs, no broad media access)
  and never records audio, so these only bloat the Play permission
  list / Data Safety form.
- Add expo-splash-screen + assets/splash-icon.png (dark #101217
  background). There was no splash, so cold start flashed blank white
  against the dark theme.
- Ship an opaque iOS icon (flatten the transparent corners onto
  #101217) since Apple rejects icons with alpha, and add a monochrome
  adaptive-icon layer for Android 13+ themed icons.
- Add expo-system-ui so userInterfaceStyle:automatic actually applies
  on Android (prebuild warned it was a no-op without it).
- Add an app description.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two robustness fixes for flows a store reviewer is likely to hit:

- Wrap the app provider tree in Sentry.ErrorBoundary (inside
  ThemeProvider so the fallback is themed) with a "Try again" screen.
  An uncaught render error otherwise shows a blank/native crash screen,
  which is an automatic review rejection. Sentry still reports the error.
- Give the /oauth/callback route a 10s escape hatch. A cold start onto
  the callback with no pending request could spin on "Finishing
  sign-in…" forever; now it surfaces a "Back to sign-in" action.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Gate the cloud-auth console.warn calls behind __DEV__ via an exported
  devWarn helper (reused from CloudAuthContext) so token-exchange /
  HTTP-status details don't get written to production device logs.
- Point the account-deletion link at electric.ax to match the
  terms/privacy URLs (it was on a different domain).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@msfstef msfstef force-pushed the msfstef/mobile-app-polishing branch from 9c6fe6a to 1cfe0c6 Compare June 18, 2026 13:53
@msfstef

msfstef commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review + a couple of polish tweaks (branch force-pushed):

  • Lockfile churn (Important): restored pnpm-lock.yaml to main + only the expo-splash-screen/expo-system-ui entries (40 insertions, no other changes). Note: the jsdom/react-grab/agent-install/babel drift wasn't from a stale local install — a clean pnpm install --lockfile-only against main (same pinned pnpm 10.12.1) reproduces it, i.e. main's lockfile has drifted from the registry and any install re-resolves those. Trimmed it out by hand; pnpm install --frozen-lockfile passes on the result.
  • devWarn (suggestion): exported it from cloudAuth.ts and reused it in CloudAuthContext.tsx instead of the inline if (__DEV__).
  • Comments: shortened the heavier explanatory comments (privacy manifest, splash, blocked permissions, error boundary) to better match sibling density.
  • Added a patch changeset for @electric-ax/agents-mobile.

The splash dark-variant / resolveVersionCode() notes are good calls — left as-is per your read (dark inherits imageWidth intentionally; the versionCode throw-on-store-profile is a sensible fast-follow).

@msfstef msfstef requested a review from kevin-dp June 18, 2026 14:09
@msfstef msfstef marked this pull request as ready for review June 18, 2026 14:27
Independent review of the permission changes found that blocking
WRITE_EXTERNAL_STORAGE breaks photo capture on Android 7-9: expo-image-picker
17's launchCameraAsync -> ensureCameraPermissionsAreGranted() hard-requires
that permission to be GRANTED on API < 29 (ImagePickerModule.kt), and a
blocked (tools:node="remove") permission can never be granted. Library
picking and Android 10+ are unaffected.

Keep blocking RECORD_AUDIO + READ_EXTERNAL_STORAGE (genuinely unused); leave
WRITE_EXTERNAL_STORAGE in place. It's inert on Android 10+ and a benign
legacy permission, not a Play review risk.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@msfstef

msfstef commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Ran an independent adversarial review of the privacy-manifest + permissions changes. Outcome:

  • iOS privacy manifest — correct. All four reason codes (CA92.1/C617.1/35F9.1/E174.1) are valid and appropriate; nothing required is missing; collected-data-types and NSPrivacyTracking: false check out. One harmless over-declaration (DiskSpace/E174.1 — sentry-cocoa 8.56.1 no longer calls that API) left in place as defensive (Apple never rejects over-declaration).
  • Android — fixed a real bug. Blocking WRITE_EXTERNAL_STORAGE breaks launchCameraAsync on Android 7–9 (API 24–28): expo-image-picker 17's ensureCameraPermissionsAreGranted() hard-requires it on pre-Android-10, and a tools:node="remove" permission can never be granted (verified in ImagePickerModule.kt:271-292). Un-blocked it in acaecff; still blocking READ_EXTERNAL_STORAGE + RECORD_AUDIO. Confirmed via prebuild that only those two get tools:node="remove" now, and CAMERA still merges from the AAR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants