Skip to content

iOS round 6: Live Activities, Sign in with Apple, in-app purchase, Siri and Handoff - #270

Merged
Programcode merged 15 commits into
feat/native-iosfrom
native-ios/r6-apple-platform
Oct 29, 2026
Merged

Programcode merged 15 commits into
feat/native-iosfrom
native-ios/r6-apple-platform

Conversation

@Programcode

@Programcode Programcode commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Round 6 of the iOS app: the Apple platform features. It builds on #266 (rounds 3 and 4), so the base is native-ios/parity-r3r4-combined. Round 5 (native-ios/r5-speed-polish) owns quick actions, Spotlight indexing, the share extension and iPad keyboard shortcuts, so none of those are here.

Nothing here is deployed. The migration, the ai function change and the new push copy only exist in this PR. Every native part needs a new development build, and none was started.

What it adds

  • Generating Live Activity and Dynamic Island. Generate, Attack, Brainstorm and Factoids show Ollie, the step (research, write, check), the parts written so far and the elapsed time on the Lock Screen. iOS suspends the app soon after it leaves the screen. When that happens the activity goes stale and says to open DebateOwl, and the answer is read back from ai_run_results.
  • "Your case is ready" push. The ai function pushes result_ready once a streamed answer is stored. It goes only to the phone that started the run, and only for a successful answer. The app hides it while it is on screen.
  • Timer Live Activity buttons and time-up alert. From iOS 17 the timer's Live Activity has Pause/Resume and Next speech. A local time-sensitive notification fires when a speech or prep clock runs out on a locked phone. The app's own beeps cannot play then, because iOS has suspended it.
  • Sign in with Apple (Apple's button, hashed nonce, signInWithIdToken, the name saved on first sign-in) and Google (the website's OAuth flow in an authentication session, with PKCE). The Supabase client now uses flowType: 'pkce', as the website's does; email code and demo code verify directly and are unaffected.
  • In-app purchase (StoreKit 2).
    • The app's own module, modules/store-kit, plus purchases.ts. It buys with the account id as appAccountToken, posts the signed transaction to app-store-purchase, and finishes it only after a 2xx.
    • Deliveries run one at a time and never go out under another account. Unfinished transactions are sent again at launch, on sign-in and in the foreground. Every Transaction.updates version is sent too, so refunds reach the server.
    • New plans sheet: StoreKit prices, renewal terms, Restore and Manage subscription. You and the limit sheet open it.
    • A migration adds com.pistack.debateowl.{basic,pro}.monthly.
  • Siri, Shortcuts and Spotlight actions. Six App Intents with phrases in en/es/hu, compiled into the app target by a config plugin:
    • start the timer;
    • ask the coach (the question is filled in, not sent);
    • start a debate;
    • write a case for a motion;
    • open the Library;
    • spar.
  • Handoff. The open screen continues on a Mac at its debateowl.com page, or on another iPhone or iPad on the same route.
  • App Store review prompt after the third finished result, at most every 120 days. Notification taps now open the judged Sparring round or the timer.

Build switches

Expo Go's config is byte-identical to before (checked with npx expo config --json). DEBATELAB_BUILD=dev adds the native parts. The new DEBATELAB_APPLE_ACCOUNT=1 is for a build signed by DebateOwl's own Apple account. It keeps the push and Sign in with Apple entitlements, which withoutAccountEntitlements strips from every other development build because the borrowed profile has neither. It also adds Time Sensitive Notifications and activitycontinuation:debateowl.com. It goes in the eas.json profile env on build day.

What Adam sets up

docs/apple-platform.md has the full checklist. In short:

  • Apple Developer Program and the Paid Apps agreement.
  • App Group group.com.pistack.debateowl.
  • App IDs com.pistack.debateowl (App Groups, Associated Domains, Push, Sign in with Apple, Time Sensitive Notifications) and com.pistack.debateowl.ExpoWidgetsTarget (App Groups).
  • An APNs key uploaded to EAS.
  • App Store Connect: the app, a subscription group with the two products, and the server notification URL.
  • Supabase:
    • the Apple provider with client id com.pistack.debateowl;
    • the debatelab://auth/callback redirect;
    • the APP_STORE_* settings;
    • the migrations;
    • deploys of app-store-purchase, app-store-notifications, account and ai.
  • The website's apple-app-site-association file, which needs the Team ID.

Verification

  • npm run verify passed locally at the final head fb2e8035: lint and repo rules, typecheck, 902 unit tests, 30 collaboration tests, 555 edge tests, build, service worker, 47 Playwright smoke tests. This is a local pass, not CI.
  • Database gate (./.superset/db-gate.sh --db-only): 33 files, 655 pgTAP tests, PASS, with the new migration. Stack stopped.
  • New tests:
    • purchases: finish only after a 2xx, not_configured, pending and cancelled, serial delivery, an account switch mid-purchase, refund updates, a new signed version of a delivered transaction, a finish that failed;
    • the OAuth callback parser;
    • timer buttons, the time-up moment, and a paused prep bank kept on the Lock Screen;
    • Handoff and Siri link mapping;
    • result_ready scoped to the requesting install;
    • onResult runs after the result is stored and cannot break the stream.
  • Swift:
    • StoreKitLogic.swift and DebateOwlAppIntents.swift typecheck with Swift 6.4 on the Mac (macOS SDK, plus Catalyst for the UIKit branch).
    • StoreKitModule.swift and HandoffModule.swift have not been compiled against ExpoModulesCore. They mirror existing Expo modules.
  • The App Intents config plugin ran against a bare SDK 57 template and is idempotent. No prebuild of this app was run.
  • Reviews:
    • Codex round 1 (6b8790c4) had seven medium findings, all addressed in cd1b24b5. For the offer-code finding, Redeem was removed and the limit documented.
    • Rounds 2 to 4 hung and were cancelled. Round 2 had been looking at two areas, and both turned out to be real bugs. They were reproduced with failing tests and fixed in 71cc82d4, ca0cd554 and fb2e8035:
      • a delivered transaction's new signed version was skipped;
      • a prep clock paused from the Lock Screen could not be resumed.
    • The final fixes were self-reviewed only, with no second-model review.

Not verified: anything on a device or in Xcode, Live Activities, Siri, Handoff, the StoreKit sandbox, Apple and Google sign-in end to end, web-preview screenshots (expo export was skipped for memory).

Risks

  • The two Expo glue Swift files may need small fixes at the first build.
  • A Live Activity button press on an app iOS has ended may be missed.
  • An offer code redeemed through Apple, or a Family Sharing subscription, carries no account id, so the server cannot apply it.
  • App Review 5.1.1(v): deleting an account should revoke its Sign in with Apple token. That needs the Sign in with Apple key on the server and is not built yet.
  • Overlap with round 5: app.config.ts, _layout.tsx, apps/mobile/package.json, package-lock.json, apps/mobile/AGENTS.md, docs/development.md, and two lines in sparring/index.tsx.

🤖 Generated with Claude Code

Programcode and others added 11 commits October 29, 2026 11:24
Generate, Attack, Brainstorm and Factoids start a Live Activity while the
model writes: Ollie in the step's pose, research, write and check, the parts
written and the time since it started. When the app leaves the screen the
activity goes stale shortly after and says to open DebateOwl, because iOS
suspends the app and the stream with it. A finished run stays on the Lock
Screen for a while when the student is away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From iOS 17 the timer's Live Activity has Pause or Resume and Next speech,
which act on the clock it shows as the timer screen's buttons do. When the
app leaves the screen with a clock running, a time-sensitive local
notification is set for the moment it runs out, because the app's own
signals stop when iOS suspends it. Notification permission is asked the
first time a clock starts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apple's own button and sheet, with a hashed nonce and Supabase's id token
sign-in; the name Apple gives on the first sign-in is saved to the account.
Google runs the website's OAuth flow in an authentication session with PKCE.
Each shows only in builds that can finish it: app.config.ts turns Google on
for development builds and Apple for builds signed by DebateOwl's own
account (DEBATELAB_APPLE_ACCOUNT), which also keep push and add the Time
Sensitive and Handoff entitlements. Every other development build drops the
Sign in with Apple entitlement the package's plugin adds, as it drops push.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both are in Expo Go. The review prompt is Apple's own, offered a few seconds
after the third finished result opens and at most every 120 days.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app asks for it on every live run (notifyWhenReady); the ai function
pushes once the streamed answer is stored and only for an answer, with fixed
copy that names the tool. The app hides it while it is on screen. A tapped
notification now opens its screen: a judged Sparring round or the timer.
Not deployed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Additive; the placeholders stay for the database tests. Not applied anywhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A local StoreKit 2 module (modules/store-kit) and purchases.ts: Apple's sheet
with the account's id as appAccountToken, then app-store-purchase, and the
transaction is finished only once the server has it. Unfinished ones, Ask to
Buy approvals, renewals and redeemed offer codes are delivered at launch, on
sign-in, on every update and in the foreground, one at a time. The plans
sheet shows StoreKit's prices with the renewal terms, Restore, Redeem a code
and Manage subscription; You and the limit sheet open it where the build has
StoreKit. Until App Store Connect and the server are set up it says
purchases are not available yet. Strings for this round in en, es and hu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/apple-platform.md maps what runs in Expo Go, a development build and a
build signed by DebateOwl's own account, and lists what the Apple account,
App Store Connect, Supabase and the website need to switch each part on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Six App Intents with App Shortcuts phrases in English, Spanish and
Hungarian: start the timer, ask the coach (the question is filled in, not
sent), start a debate, write a case for a motion, open the Library, spar.
A config plugin compiles them into the app target, where Xcode extracts
their metadata. Handoff offers the open screen through the app's own
NSUserActivity module: a Mac continues on the matching debateowl.com page,
another iPhone or iPad opens the same route. Development builds only; the
Expo Go config is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A Transaction.updates version is sent as it comes, even for an id already
  delivered, so a refund reaches the server; the same version is not sent twice.
- Offer codes are not offered: Apple gives a redeemed code no account id, so
  the server cannot apply it. Restore counts only purchases made out to
  another account as theirs.
- A generating activity whose owls arrived while the app was off screen
  starts when it comes back, as iOS refuses to start one in the background.
- The plans sheet does nothing once closed, so a late purchase cannot pop
  another screen.
- Ask's q and Generate's motion take the first value when a link repeats them.
- Handoff opens History for a result only the other device holds, and the
  website's free tools hand back to the timer, speech time and motions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app sends its install id with notifyWhenReady, and the push goes only to
that installation's token rows, so a case written on the iPad does not ring
the iPhone. Without an install id no push is sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

@Programcode

Copy link
Copy Markdown
Owner Author

Local npm run verify at 5b11a2be passed: lint and repo rules, typecheck, 898 unit tests, 30 collaboration tests, 555 edge tests, build, service worker, and 47 Playwright smoke tests. This is a local pass, not a CI pass.

Programcode and others added 2 commits October 29, 2026 12:59
… version

The timer's round could not tell which clock was on screen once nothing ran,
so pausing prep from the Lock Screen ended the activity (or showed the paused
speech) and Resume could not restart that prep. The session now keeps the
bank last paused while it ran as held; the activity shows it paused and its
Resume restarts it, until another clock starts, the bank runs out or the
format changes. Stored with the round.

Purchases skipped any transaction whose id this run had already delivered,
so a new unfinished version of it (a refund, a new expiry) was counted as
applied without being sent or finished. The cache is now keyed by the signed
version, and updates and pending deliveries share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Programcode

Copy link
Copy Markdown
Owner Author

Local npm run verify at ca0cd554 passed (lint and repo rules, typecheck, unit, collaboration, edge, build, service worker, 47 Playwright smoke). This is a local pass, not a CI pass.

The server already has that version, so it is not sent again; an update
repeating a version the server took is skipped as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Programcode
Programcode changed the base branch from native-ios/parity-r3r4-combined to feat/native-ios October 29, 2026 18:58
# Conflicts:
#	apps/mobile/AGENTS.md
#	apps/mobile/src/app/sparring/index.tsx
#	apps/mobile/src/lib/native-capabilities.test.ts
#	apps/mobile/src/lib/native-capabilities.ts
#	docs/development.md
@Programcode
Programcode marked this pull request as ready for review October 29, 2026 19:06
Copilot AI balanced review requested due to automatic review settings October 29, 2026 19:06
@Programcode
Programcode merged commit ebbc75b into feat/native-ios Oct 29, 2026
3 of 14 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

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.

2 participants