Skip to content

Commit c7e15c2

Browse files
Programcodeclaude
andcommitted
Use the com.pistack.debateowl bundle id, its App Group and the EAS project
The app is com.pistack.debateowl, its widget extension com.pistack.debateowl.ExpoWidgetsTarget, and both carry the App Group group.com.pistack.debateowl, the only value DEBATELAB_APP_GROUP accepts. The config links the EAS project @pi-stack/debateowl that eas init created, and eas.json gains a development-simulator profile: an unsigned development client for the iOS Simulator, with the widget extension. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 6464cea commit c7e15c2

4 files changed

Lines changed: 42 additions & 21 deletions

File tree

‎apps/mobile/AGENTS.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# DebateOwl mobile app
22

3-
The iOS app DebateOwl (`@debatelab/mobile`; the bundle id is still `com.adam.debatelab`): Expo SDK 57, React Native 0.86, React 19, Expo Router. The plan is [plans/2026-10-08-native-ios-app.md](../../plans/2026-10-08-native-ios-app.md). Read the root [AGENTS.md](../../AGENTS.md) first; this file adds only what is specific to the app.
3+
The iOS app DebateOwl (`@debatelab/mobile`; bundle id `com.pistack.debateowl`, EAS project `@pi-stack/debateowl`): Expo SDK 57, React Native 0.86, React 19, Expo Router. The plan is [plans/2026-10-08-native-ios-app.md](../../plans/2026-10-08-native-ios-app.md). Read the root [AGENTS.md](../../AGENTS.md) first; this file adds only what is specific to the app.
44

55
## Rules
66

@@ -45,7 +45,7 @@ The iOS app DebateOwl (`@debatelab/mobile`; the bundle id is still `com.adam.deb
4545
- One-off mode is session-only and never written, as on the website; only maps saved in it are kept, with no debate, as the website keeps them. The switcher turns it on with or without a Current Debate, and `OneOffNotice` shows it on the Debate tab, Prepare, Ask, Library and the Generate, Attack, Brainstorm, Factoids, Crossfire and Sparring forms (through `WorkingIn`) and the Flowchart. The Debate tab, Prepare and Library still list the Current Debate's work in one-off mode, as the website's home does. Students prepare both sides of a motion, so a debate has no side: the Debate tab's In favor / Against toggle only chooses which side's work it shows, using the website's rule (`'both'` counts for either side, neutral and side-less work for neither). It starts on In favor and is never stored.
4646
- Offline reading: `offline-cache.ts` keeps a copy of every read in `debates.ts`, `sources.ts`, `results.ts`, `flowcharts.ts`, `chats.ts`, `sparring.ts` and `history.ts` (the debates list, counts and recent work, Library sources with their text, saved results, maps, chats, judged Sparring rounds and History's lists) in AsyncStorage under the user id. A read goes to the network first and saves its answer; when no answer comes back (supabase-js status 0: no connection, DNS, timeout), or `network-status.ts` (expo-network) says the device is offline, the copy answers and `OfflineNotice` shows at the top of `Screen`, `DocumentScreen` (including its iPad outline rail), the Flowchart and the iPad Library list until each copy shown is read from the server again or the screen showing it closes, moves on or leaves view (a screen passes `useCopyHolder().mark()` with its reads). A server error (an expired session, a policy, a 5xx) is thrown, never answered from a copy; a read made of several queries runs them through `readAll` (or `pickFailure`), so one query's refusal wins over another's missing answer. A slower answer never replaces a newer copy, each group keeps its most recently fetched copies only (`LIMITS`), and the auth provider calls `setCacheOwner` before it adopts a session, so another account's copies are removed before its first read. Nothing is written offline: AI and saving fail with their network errors, and there is no write queue. A read added to those modules goes through `cachedRead`, takes an optional `CopyMark`, and throws `readFailed(error, status)`. Every write that succeeds calls `afterWrite` with the copies it changed (`debateListScopes` names a debate's counts and lists; a delete uses `deletedListScopes`, which drops every debate's when no row came back), which drops them and stops older reads from saving over them, or `updateCopy` when it knows the new value. `debateListScopes` also names History's copy (`one-off` for work outside every debate); History's add-to-debate and filing a judged Sparring round into its debate both drop the item's copies on both sides, and Delete all my data drops every copy (`ALL_COPIES`).
4747
- Push: `push.ts` asks for permission and files the Expo push token under the signed-in user with the `register_push_token` RPC; the You tab's Notifications switch turns it on and off, the root layout registers again on launch and when the language changes, and sign-out releases the registration with the account's own access token, after any registration still in flight. Every registration carries the ID of the local record, and `release_push_token` withdraws that ID and deletes only the rows it wrote, so a registration the app gave up on is refused if it reaches the database after the release. Once a release is sent for an ID, the record is marked released and the next registration gets a new ID, so a late release cannot delete it. The local record is cleared only once a release succeeds; otherwise the next change on the phone, from any account, releases it first. The server side is in [supabase/README.md](../../supabase/README.md#push-notifications). `push.web.ts` registers nothing and only flips the switch for the screenshot script.
48-
- No build can get a token until `app.config.ts` has `extra.eas.projectId`, from `eas init` on build day (see [docs/development.md](../../docs/development.md#build-day)). Until then the switch shows as unavailable, on Expo Go too. Expo Go on Android has no remote push since SDK 53, and Expo recommends a development build for remote push on iOS as well.
48+
- A token needs `app.config.ts`'s `extra.eas.projectId`, which `eas init` set to the `@pi-stack/debateowl` project (see [docs/development.md](../../docs/development.md#build-day)); without it the switch shows as unavailable, on Expo Go too. Expo Go on Android has no remote push since SDK 53, and Expo recommends a development build for remote push on iOS as well.
4949
- Prebuild applies the `expo-notifications` config plugin automatically once the package is installed, even though `app.config.ts` does not list it, and the plugin sets the `aps-environment` entitlement. `DEBATELAB_BUILD=dev` removes it again (`withoutPushEntitlement`), because the borrowed Signulous profile has no push, so a development build cannot get a token even with a project ID: turning the switch on there fails. Real push needs a provisioning profile with the Push Notifications capability and an APNs key uploaded to EAS, both from Adam's own Apple Developer account; only then can `withoutPushEntitlement` go.
5050
- iPad layouts (iPad mock A, with mock C's docked claim) come from one hook, `useLayoutClass()` in `src/lib/layout.ts`, which also holds every width they use: `phone` on every iPhone and in iPad windows under 700 points, `regular` from 700, `wide` from 1100. Phone layouts must not change, so each screen keeps its phone JSX and branches before it; compare iPhone screenshots before and after. NativeTabs is `sidebarAdaptable` (the iPadOS 18 sidebar; `app-tabs.web.tsx` draws a sidebar or a top tab bar for the preview). The Library opens a source beside its list (`SourceView`, shared with the source sheet), Ask lists its conversations beside the chat (`ask-chat-list.tsx`), the Debate tab is a two-column dashboard, saved documents get an "On this page" rail from 900 points (`DocumentScreen`'s `outline`, `components/outline.tsx`), and the Flowchart docks a card's claim beside the map (`ClaimPanel` in `claim-panel.tsx`, shared with the claim sheet). A docked claim or a source open beside its list is dropped when that map, debate, account or row is no longer the one on screen. All are plain views sized from the window, so they run in Expo Go.
5151
- `src/theme/`: the website's DebateOwl design tokens (`src/styles/tokens.css` and the roles in `src/index.css`) in light and dark. `primary` is the blue that passes as text; `primaryFill` with `primaryLedge` is the button face and the ledge it sits on. Success is mint and always comes with a check mark. Fredoka (`weight="display"`) is for titles, the motion and button labels, Nunito for everything else, loaded with expo-font from `@expo-google-fonts`. Buttons are the website's tactile ones (`components/buttons.tsx`), which sink onto their ledge while pressed. Cards and fields have 2pt edges.

‎apps/mobile/app.config.ts‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import { type ConfigPlugin, withEntitlementsPlist } from 'expo/config-plugins';
88
// DEBATELAB_BUILD=dev (set by the `development` profile in eas.json) adds the
99
// native modules Expo Go lacks. Without it the config is the one Expo Go has
1010
// always used. DEBATELAB_APP_GROUP adds the timer's widget extension (its Live
11-
// Activity, Dynamic Island clock and lock-screen widget), which needs an App
12-
// Group: set it only when the provisioning profile carries that group.
13-
// PLACEHOLDER until Adam's Apple Developer account exists: the group to create
14-
// there is `group.com.adam.debatelab` (it follows the bundle id), registered
15-
// for both `com.adam.debatelab` and `com.adam.debatelab.ExpoWidgetsTarget`.
11+
// Activity, Dynamic Island clock and lock-screen widget), which needs the App
12+
// Group `group.com.pistack.debateowl`, registered for both
13+
// `com.pistack.debateowl` and `com.pistack.debateowl.ExpoWidgetsTarget`. On a
14+
// device, set it only when the provisioning profile carries that group; a
15+
// simulator build is not signed and always can.
1616
// See docs/development.md#development-build.
1717

1818
type PermissionStrings = { speechRecognition: string; microphone: string };
@@ -26,8 +26,11 @@ if (appGroup && build !== 'dev') {
2626
throw new Error('DEBATELAB_APP_GROUP only applies to a development build (DEBATELAB_BUILD=dev).');
2727
}
2828

29-
// Kept from the DebateLab name until Adam decides on the store listing: a new id is a new app in App Store Connect.
30-
const bundleIdentifier = 'com.adam.debatelab';
29+
const bundleIdentifier = 'com.pistack.debateowl';
30+
const APP_GROUP = 'group.com.pistack.debateowl';
31+
if (appGroup && appGroup !== APP_GROUP) {
32+
throw new Error(`DEBATELAB_APP_GROUP must be "${APP_GROUP}", not "${appGroup}".`);
33+
}
3134

3235
function permissionStrings(projectRoot: string, language: 'en' | 'es' | 'hu'): PermissionStrings {
3336
const file = path.join(projectRoot, '../../src/locales', `${language}.json`);
@@ -97,8 +100,11 @@ export default ({ projectRoot }: ConfigContext): ExpoConfig => {
97100
},
98101
],
99102
],
103+
// The EAS project @pi-stack/debateowl, from `eas init`.
104+
owner: 'pi-stack',
100105
extra: {
101106
websiteUrl: 'https://debateowl.com',
107+
eas: { projectId: 'd60fc378-2e30-48b2-bcee-a1b1d1632bf9' },
102108
},
103109
experiments: {
104110
typedRoutes: true,
@@ -127,7 +133,7 @@ export default ({ projectRoot }: ConfigContext): ExpoConfig => {
127133
'expo-widgets',
128134
{
129135
bundleIdentifier: `${bundleIdentifier}.ExpoWidgetsTarget`,
130-
groupIdentifier: appGroup,
136+
groupIdentifier: APP_GROUP,
131137
enablePushNotifications: false,
132138
widgets: [
133139
// The timer's lock-screen widget (src/widgets/timer-widgets.tsx). The Live

‎apps/mobile/eas.json‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@
1212
"env": {
1313
"DEBATELAB_BUILD": "dev"
1414
}
15+
},
16+
"development-simulator": {
17+
"developmentClient": true,
18+
"distribution": "internal",
19+
"node": "22.23.2",
20+
"ios": {
21+
"simulator": true
22+
},
23+
"env": {
24+
"DEBATELAB_BUILD": "dev",
25+
"DEBATELAB_APP_GROUP": "group.com.pistack.debateowl"
26+
}
1527
}
1628
}
1729
}

‎docs/development.md‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ A development build is the app's own binary with `expo-dev-client`, so it loads
143143

144144
- No `DEBATELAB_BUILD`: the Expo Go and web export config, unchanged.
145145
- `DEBATELAB_BUILD=dev`: adds `expo-dev-client`, `expo-speech-recognition` with the microphone and speech prompts (English in Info.plist, Spanish and Hungarian as `InfoPlist.strings` from `mobile.permissions` in the catalogs) and `CFBundleAllowMixedLocalizations`. The `development` profile in [`apps/mobile/eas.json`](../apps/mobile/eas.json) sets it.
146-
- `DEBATELAB_APP_GROUP=group.<id>`, only with `dev`: adds the `expo-widgets` extension (`ExpoWidgetsTarget`, bundle `com.adam.debatelab.ExpoWidgetsTarget`) with the timer's lock-screen widget (`DebateTimerWidget`) and Live Activity, `NSSupportsLiveActivities`, and the App Group on both targets. The group to register in Adam's Apple Developer account is `group.com.adam.debatelab`, on both bundle ids. Set it only when the provisioning profile carries that group; a borrowed wildcard profile does not. EAS does not see your shell, so it goes in the profile's `env` in `eas.json`, committed with the build.
146+
- `DEBATELAB_APP_GROUP=group.<id>`, only with `dev`: adds the `expo-widgets` extension (`ExpoWidgetsTarget`, bundle `com.pistack.debateowl.ExpoWidgetsTarget`) with the timer's lock-screen widget (`DebateTimerWidget`) and Live Activity, `NSSupportsLiveActivities`, and the App Group `group.com.pistack.debateowl` (the only value the config accepts) on both targets. Register that group for both bundle ids in the Apple Developer account. For a device build, set it only when the provisioning profile carries that group; a borrowed wildcard profile does not. A simulator build is not signed, so the `development-simulator` profile always sets it. EAS does not see your shell, so it goes in the profile's `env` in `eas.json`, committed with the build.
147147

148148
`nativeCapabilities()` in `apps/mobile/src/lib/native-capabilities.ts` tells the app at runtime what the binary has. Feature code checks it and never imports a dev-build module at the top level, so Expo Go keeps working.
149149

@@ -190,21 +190,13 @@ openssl pkcs12 -legacy -in $S/cert.p12 -passin file:$S/p12-password -nokeys -clc
190190
Go on only if all of these hold:
191191

192192
- the profile has not expired, and neither has the certificate (`notAfter`);
193-
- `application-identifier` is `<team>.*` or `<team>.com.adam.debatelab`;
193+
- `application-identifier` is `<team>.*` or `<team>.com.pistack.debateowl`;
194194
- the profile is enterprise (`all devices` true) or ad hoc with Adam's iPhone UDID among its devices: ask Adam for the UDID. A development profile (`get-task-allow` true) is not what EAS internal distribution expects; ask Adam first;
195195
- the `.p12`'s SHA-1 equals one of the profile's certificate SHA-1s.
196196

197197
Set `DEBATELAB_APP_GROUP` in the profile's `env` in `eas.json` only if `app groups` lists a group, and then pass `widgets` to step 4. Don't export it in this shell: outside a build the config rejects it without `DEBATELAB_BUILD=dev`, which breaks `eas init`. Without an App Group this build has no Live Activity.
198198

199-
**2. Link the EAS project, once.**
200-
201-
```bash
202-
cd apps/mobile
203-
export EXPO_TOKEN="$(cat $S/eas-token)"
204-
npx eas-cli@24.13.0 init --account <account> --force --non-interactive
205-
```
206-
207-
Without a project ID in the config, the non-interactive init needs both `--account` (the Expo account that owns the project, `adamgaborjani` on Adam's VPS) and `--force`, which links `@<account>/debatelab` if it exists and creates it otherwise. `eas init` cannot edit `app.config.ts`, so it prints the project ID. Add `owner: '<account>'` and `extra: { eas: { projectId: '<id>' } }` to the config, commit, and build from that commit. Expo Go's config gains those two fields, which is expected.
199+
**2. The EAS project is linked.** `app.config.ts` has `owner: 'pi-stack'` and the project ID of `@pi-stack/debateowl`, set with `eas init --account pi-stack --force --non-interactive` (which cannot edit a dynamic config, so it printed the ID). Nothing to do here unless the project changes.
208200

209201
**3. Count the builds already used.**
210202

@@ -253,6 +245,17 @@ rm -f credentials.json $S/profile.plist
253245
unset EXPO_TOKEN
254246
```
255247

248+
#### Simulator build
249+
250+
`development-simulator` in `eas.json` builds the development client for the iOS Simulator, with the widget extension and App Group. It is not signed, so it needs no credentials, only a logged-in `eas` (`eas whoami`). It costs build minutes like any iOS build:
251+
252+
```bash
253+
cd apps/mobile
254+
eas build --platform ios --profile development-simulator --non-interactive --no-wait
255+
```
256+
257+
The artifact is a `.tar.gz` of the `.app`; drag the unpacked app onto a Simulator, then start Metro with `--dev-client`.
258+
256259
#### Installing it
257260

258261
The finished build's page on expo.dev has an Install button and a QR code. Adam opens it on the iPhone in Safari and installs. For an enterprise certificate, iOS then asks to trust the developer under Settings › General › VPN & Device Management. If iOS asks for Developer Mode, it is under Settings › Privacy & Security.

0 commit comments

Comments
 (0)