Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4ab72c3
refactor(mobile): put iOS-only APIs behind platform seams for Android
Kitenite Oct 29, 2026
567c518
feat(mobile): React Native composer for Android
Kitenite Oct 29, 2026
33661e6
feat(mobile): Android App Links and EAS build/submit config
Kitenite Oct 29, 2026
019ecc1
feat(mobile): real Android fallbacks for small seams, glass and sheet…
Kitenite Oct 29, 2026
c3bfc1a
feat(mobile): Android agent card as an ongoing notification
Kitenite Oct 29, 2026
ad676cd
fix(mobile): boot on Android — Chinese Intl fallback, Apple error, An…
Kitenite Oct 29, 2026
8d102b6
feat(mobile): native Google sign-in on Android, no Apple button there
Kitenite Oct 29, 2026
5f41d55
feat(mobile): Android sheet header, content inset under headers, fall…
Kitenite Oct 29, 2026
4021cad
fix(mobile): keep focus and sit above the keyboard in the Android com…
Kitenite Oct 29, 2026
a1e7b3b
Merge android/01-boot-spike (8d102b642f) into Android integration
Kitenite Oct 29, 2026
6fee791
Merge android/02-composer (4021cad233) into Android integration
Kitenite Oct 29, 2026
c60e845
Merge android/03-sheets-design (5f41d550b5) into Android integration
Kitenite Oct 29, 2026
c9a35e0
Merge android/04-release-plumbing (33661e6104) into Android integration
Kitenite Oct 29, 2026
f5b497c
Merge android/05-live-activity-push (c3bfc1ac21) into Android integra…
Kitenite Oct 29, 2026
c7ca5b4
feat(mobile): Android dev setup, launch checklist and Play listing
Kitenite Oct 29, 2026
b220ddc
docs(mobile): Android checklist progress — Play account, keystore, OA…
Kitenite Oct 30, 2026
e8448d7
docs(mobile): Play phone screenshots (drafts) and review-account fix …
Kitenite Oct 30, 2026
d781db0
feat(mobile): android-dev.sh targets one emulator via ANDROID_SERIAL
Kitenite Oct 30, 2026
9a35339
chore(mobile): Android release config (changes the iOS runtime finger…
Kitenite Oct 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions .agents/skills/mobile-android-verification/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
name: mobile-android-verification
description: Run and verify apps/mobile on a local Android emulator against this worktree's API and relay, with adb and Maestro for input. Use when asked to run, screenshot, verify, or reproduce a mobile change on Android. For iOS use mobile-sim-verification; with no local toolchain (a cloud sandbox) use mobile-eas-dev, which is iOS only.
---

# Mobile Android Verification

The Android counterpart to `mobile-sim-verification`. Its Rules apply here unchanged: use the real
screen, drive state through real services, run the edge pass, never show "unknown" as a negative,
record flashes, attribute failures correctly. This file is the Android mechanics.

`apps/mobile/scripts/android-dev.sh` does the setup. Read its header for the flags.

## Setup

```bash
apps/mobile/scripts/android-dev.sh --doctor # what is missing on this machine
apps/mobile/scripts/android-dev.sh # boot, build if needed, Metro, open the app
apps/mobile/scripts/android-dev.sh --user maya@local.test # also sign in as a dev account
```

- **Toolchain.** Android Studio's SDK (`~/Library/Android/sdk`) with an AVD, and JDK 17. Gradle for
RN 0.88 fails on the Java 8 that macOS often has on `PATH`; `brew install openjdk@17`. The script
finds both without `ANDROID_HOME` or `JAVA_HOME` set. For Google sign-in, the AVD needs a Google
Play system image and a Google account added on the device.
- **Services.** Start the API, relay and realtime as in `mobile-sim-verification` (each in a
Paradev terminal). The script reverses Metro, `API_PORT`, `RELAY_PORT`, `REALTIME_PORT` and
`WEB_PORT` with `adb reverse`, so `localhost` in the app reaches this machine. A port it does not
reverse fails in the app as a network error.
- **Build.** `android/` is generated by `expo prebuild` and gitignored. The first Gradle build takes
about 10 minutes; later ones reuse the daemon. The script builds only for the emulator's ABI.
JS/TS changes need no rebuild: `android-dev.sh --reload`.

## One emulator, many worktrees

Every worktree installs the same package, `sh.superset.mobile`, so the last install wins.

- The script writes the worktree path and native fingerprint of the installed APK to
`/data/local/tmp/superset-mobile-owner` on the device. A mismatch rebuilds, so another worktree's
APK is never tested by mistake. Check it before you report: `adb shell cat
/data/local/tmp/superset-mobile-owner`.
- Agents in parallel workspaces take turns. Ask with `superset agents` before you install or
launch, and say when you hand it back. Do not boot a second emulator next to a Gradle build: the
machine runs out of memory.
- Each worktree's Metro gets its own port (saved in `.tmp/android-dev/metro.port`). Stop it with
`--stop` when you are done.

## Traps

- **Stale Metro cache.** The transform cache is shared across worktrees. A stale entry fails the
Android bundle with a 500: ENOENT on `react-native-worklets/.worklets/<hash>.js`. The script
starts Metro with `--clear`; if you start Metro yourself, do the same.
- **Dev auto sign-in.** `EXPO_PUBLIC_DEV_ONE_TIME_TOKEN` in `.env` signs the app in at launch even
after `pm clear`. Blank it and restart Metro to see the sign-in screen.
- **Notification permission.** Nothing in the app asks for `POST_NOTIFICATIONS` yet; the script
grants it so the agent card can show on Android 13+.
- **iOS-only header items.** SF Symbol `Stack.Toolbar` buttons and `Stack.SearchBar` do not render
on Android, and a form sheet draws no native bar there (see the sheet convention in
`apps/mobile/AGENTS.md`). An empty header on Android is not always a regression.
- **The iOS fingerprint.** `app.config.ts`, `eas.json`, `apps/mobile/.gitignore` and
`packages/shared/src/app-identity.ts` feed the iOS runtime version even for Android-only edits.
Changing them rebuilds and resubmits iOS on merge. Check with
`MOBILE_EAS_BUILD=1 node_modules/.bin/fingerprint fingerprint:generate . --platform ios` in
`apps/mobile`, and keep such a change in its own commit.
- **The Mobile pane** in the desktop app shows the same emulator (local hub, scrcpy stream). It
releases an emulator it booted 60 s after its pane closes, which ends an `adb` session on it. If
you booted the emulator yourself, the pane only shows it.

## Input and evidence

```bash
adb exec-out screencap -p > shot.png # or: android-dev.sh --shot shot.png
adb shell screenrecord /sdcard/run.mp4 # stop with Ctrl-C, then adb pull
adb shell uiautomator dump /sdcard/ui.xml && adb pull /sdcard/ui.xml # element bounds for taps
adb shell input tap X Y; adb shell input text 'hello'; adb shell input keyevent 4 # 4 = back
adb shell am start -a android.intent.action.VIEW -d 'superset://settings' sh.superset.mobile
adb logcat -d -s ReactNativeJS:V # JS console
adb logcat -d -b crash # native crashes
adb shell cmd statusbar expand-notifications # the agent card lives here
adb shell cmd locale set-app-locales sh.superset.mobile --locales de-DE # per-app locale
adb shell settings put system font_scale 1.3 # large text; reset to 1.0
```

Maestro runs on Android too: `maestro --device emulator-5554 test <flow>`, with `JAVA_HOME` on JDK 17.
`apps/mobile/.maestro/flows/dev-sign-in.yml` is the sign-in flow `--user` uses.

Reset the locale and font scale, stop Metro (`--stop`) and the services, and remove test data when
done. Leave the emulator running if another agent is waiting for it.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ DESKTOP_DISPLAY_NAME=
EXPO_PUBLIC_API_URL=
EXPO_PUBLIC_ROOT_DOMAIN=
EXPO_PUBLIC_POSTHOG_KEY=
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID=
EXPO_PUBLIC_SENTRY_DSN_MOBILE=
EXPO_PUBLIC_SENTRY_ENVIRONMENT=
# App Review demo account, read by apps/mobile/store.config.js for `eas metadata:push`
Expand Down
1 change: 1 addition & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ DESKTOP_DISPLAY_NAME=
EXPO_PUBLIC_API_URL=http://localhost:3001
EXPO_PUBLIC_ROOT_DOMAIN=
EXPO_PUBLIC_POSTHOG_KEY=phc_local_dev_disabled
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID=
# A syntactically valid but inert DSN: @sentry/react-native's useNativeInit
# crashes the app on launch (SIGSEGV, before any JS runs) if this fails to
# parse as a URL, which an empty or missing value does. Needs a well-formed
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ yarn-error.*
# generated native folders
/ios
/android
# Gradle output of a local module's android/ library
modules/*/android/build/

# storybook (generated on start)
.rnstorybook/storybook.requires.ts
28 changes: 28 additions & 0 deletions apps/mobile/.maestro/flows/dev-sign-in.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Signs in through the dev email prompt as ${EMAIL}, signing out first if needed.
# Run by scripts/android-dev.sh --user; works on the iOS simulator too.
appId: sh.superset.mobile
---
- extendedWaitUntil:
visible: "Sign in with email \\(dev\\)"
timeout: 15000
optional: true
- runFlow:
when:
notVisible: "Sign in with email \\(dev\\)"
commands:
- openLink: "superset://settings"
- scrollUntilVisible:
element: "Sign out"
- tapOn: "Sign out"
- tapOn: "Log out"
- extendedWaitUntil:
visible: "Sign in with email \\(dev\\)"
timeout: 15000
- tapOn: "Sign in with email \\(dev\\)"
- eraseText: 80
- inputText: ${EMAIL}
- tapOn: "Next"
- tapOn: "Sign in"
- extendedWaitUntil:
notVisible: "Sign in with email \\(dev\\)"
timeout: 20000
32 changes: 25 additions & 7 deletions apps/mobile/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,38 @@ export default function AuthenticatedLayout() {

## Conventions

- **`apps/mobile` is iOS-only.** No Android fallbacks or platform guards for iOS-only APIs, and
Android incompatibility isn't a blocker until Android is explicitly in scope.
- **Bottom sheets are expo-router `formSheet` routes** — `...glassHeaderOptions` gives the native
title and ✕, the body is RN + uniwind (never `@expo/ui` SwiftUI content, which can't be themed to
match ours), and the list stays the screen's only layout child or it cold-mounts at zero height.
Copy `PullRequestsSheet` and its route entry.
- **iOS ships; Android is a seam, not a target yet.** Every iOS-only API sits behind a typed
contract, picked by Metro's platform extensions, never by `Platform.OS` checks in screens:
- `X.types.ts` is the contract: props, handle and function types, with no SF Symbol names or
other iOS words in what callers pass. Screens import only the folder's `index.ts`.
- `X.ios.tsx` is the native binding: an Expo module in `modules/<name>/ios`, `@expo/ui/swift-ui`,
or an iOS-only Expo package. It is the only file that may import those.
- `X.tsx` is the fallback for every other platform and for `bun test`. It does nothing or
degrades to plain RN, and it must not crash.
- Both implementations annotate their exports with the contract type, so `tsc` checks the
`.ios` file against the same shape as the fallback that callers resolve to.
- To port to Android: add `X.android.tsx`, or for a module, add `android/` and `"android"` to
`expo-module.config.json` and rename the binding `X.native.tsx` so both platforms use it.
Examples: `modules/composer/src`, `ShareAccess/components/ValueMenu`,
`sign-in/utils/signInWithApple`. Android-only the other way round: `sign-in/utils/signInWithGoogle`
(`.android.ts` on `modules/google-credential`, `.ts` everywhere else).
- **Bottom sheets are expo-router `formSheet` routes** — `...sheetHeaderOptions` gives the native
title and ✕ on iOS and a JS drag handle and title on Android, where a form sheet draws no native
bar (so `Stack.SearchBar` and SF Symbol `Stack.Toolbar` buttons do not show there). The body is
RN + uniwind (never `@expo/ui` SwiftUI content, which can't be themed to match ours), and the
list stays the screen's only layout child or it cold-mounts at zero height. Copy
`PullRequestsSheet` and its route entry.
- **Hermes ships a partial `Intl`.** `lib/intl-polyfills` lists what is missing and what is
polyfilled. A `@superset/i18n/format` helper that reaches for an API outside that list throws, and
no mobile screen has an error boundary, so the throw takes the screen down. Add the polyfill, its
per-locale data, and a case in the polyfill test.
per-locale data, and a case in the polyfill test. Android's Hermes also lacks `zh-CN`/`zh-TW` in
`NumberFormat`/`DateTimeFormat` and formats them as `en-US`; `scriptSubtagFallback.android.ts`
passes the script-qualified tag instead.
- **Pending and failed are not answers.** A query with no data yet has not said "offline" or
"empty". Keep loading while it is pending and say you could not check when it failed; Home once
painted "is offline" on every cold start because presence defaulted to `false`.
- **Verifying in the real app:** on a machine with Xcode, `.agents/skills/mobile-sim-verification/SKILL.md`.
On Android, `apps/mobile/scripts/android-dev.sh` and `.agents/skills/mobile-android-verification/SKILL.md`.
On a cloud sandbox (no local Xcode/simulator), run `apps/mobile/scripts/eas-dev.sh`; see
`.agents/skills/mobile-eas-dev/SKILL.md`. The script encodes what was found getting the app running
on an EAS simulator from a sandbox (2026-10-09), so nobody has to rediscover it:
Expand Down
4 changes: 3 additions & 1 deletion apps/mobile/RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Mobile App Release Process

iOS only. Builds and submissions go through EAS; the App Store listing and the
iOS only; Android has not shipped yet, and what it still needs is in
[`plans/20261029-android-launch-checklist.md`](./plans/20261029-android-launch-checklist.md).
Builds and submissions go through EAS; the App Store listing and the
App Review notes live in [`store.config.js`](./store.config.js) and are pushed
with `eas metadata:push`. This file is the runbook for shipping a build and for
getting unstuck when App Review rejects or stalls it.
Expand Down
31 changes: 25 additions & 6 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from "node:path";
import { SUPPORTED_LOCALES } from "@superset/i18n/locales";
import { IOS_APP } from "@superset/shared/app-identity";
import { ANDROID_APP, IOS_APP } from "@superset/shared/app-identity";
import { BRAND } from "@superset/shared/brand";
import { config } from "dotenv";
import type { ConfigContext } from "expo/config";
Expand All @@ -21,8 +21,21 @@ if (process.env.MOBILE_EAS_BUILD !== "1") {
const webUrl = new URL(
process.env.EXPO_PUBLIC_WEB_URL || "https://app.superset.sh",
);
const associatedDomains =
webUrl.protocol === "https:" ? [`applinks:${webUrl.hostname}`] : undefined;
const appLinksHost = webUrl.protocol === "https:" ? webUrl.hostname : undefined;
const associatedDomains = appLinksHost && [`applinks:${appLinksHost}`];
// Same paths as apple-app-site-association in apps/web.
const appLinkIntentFilters = appLinksHost && [
{
action: "VIEW",
autoVerify: true,
data: ["/page/", "/workspaces/"].map((pathPrefix) => ({
scheme: "https",
host: appLinksHost,
pathPrefix,
})),
category: ["BROWSABLE", "DEFAULT"],
},
];

const SIGNED_BUILD_PROFILES = ["preview", "production"];
const signedUpdates = process.env.MOBILE_SIGNED_UPDATES === "1";
Expand Down Expand Up @@ -90,10 +103,11 @@ export default ({ config }: ConfigContext) => ({
android: {
adaptiveIcon: {
foregroundImage: "./assets/adaptive-icon.png",
backgroundColor: "#ffffff",
backgroundColor: "#151110",
},
package: "sh.superset.mobile",
package: ANDROID_APP.PACKAGE,
predictiveBackGestureEnabled: false,
...(appLinkIntentFilters && { intentFilters: appLinkIntentFilters }),
},
web: {
favicon: "./assets/favicon.png",
Expand Down Expand Up @@ -138,7 +152,12 @@ export default ({ config }: ConfigContext) => ({
],
[
"expo-localization",
{ supportedLocales: { ios: [...SUPPORTED_LOCALES] } },
{
supportedLocales: {
ios: [...SUPPORTED_LOCALES],
android: [...SUPPORTED_LOCALES],
},
},
],
"expo-apple-authentication",
[
Expand Down
Loading
Loading