Skip to content

Release iOS wrapper and export toolbar cleanup - #2

Merged
Programcode merged 19 commits into
mainfrom
codex/ios-capacitor-wrapper-ready
Jun 3, 2026
Merged

Programcode merged 19 commits into
mainfrom
codex/ios-capacitor-wrapper-ready

Conversation

@Programcode

@Programcode Programcode commented Jun 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • add the Capacitor iOS/iPad wrapper and native runtime glue
  • align generated-result export actions into two explicit left-aligned rows
  • harden native auth, redirects, edge-function calls, and malformed persisted result handling
  • add browser smoke coverage and focused native/export tests
  • secure the Stripe webhook idempotency table with RLS and service-role-only access

Safety / rollback

  • branch is separated from the messy compare-mode branch
  • no compare-mode files are included in this diff
  • GitHub rollback is a normal PR revert/revert commit
  • Supabase rollback for the RLS migration is documented separately before production application

Verification

  • npm run lint
  • npm test
  • npm run build
  • staging Supabase RLS advisor: critical rls_disabled_in_public resolved for webhook_events

Summary by CodeRabbit

  • New Features

    • Added iOS native app support via Capacitor with native UI integration (keyboard, status bar, safe areas).
    • Implemented PKCE-based OAuth authentication flow.
    • Consolidated export UI into a reusable ExportToolbar component.
    • Added E2E testing with Playwright across multiple locales.
  • Bug Fixes

    • Improved auth timeout handling and error propagation.
    • Fixed Stripe checkout/portal navigation for native environments.
  • Documentation

    • Added iOS setup instructions and available npm commands.
  • Tests

    • Added E2E specs, native app URL listener tests, and component tests.
  • Chores

    • Updated dependencies (Capacitor, Playwright), configuration files, and CORS allowlists.

Programcode and others added 19 commits June 2, 2026 22:46
Use base: './' only for --mode capacitor; web build uses '/' so deep
links like /app/library resolve assets correctly in production preview.

Why: commit 2464dd0 added base: './' for the iOS wrapper, which broke
web deep links because the browser resolved ./assets/foo.js relative to
the route path.

Pairs with Part 4 (iOS Capacitor wrapper).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pin the Capacitor Swift package, add the dictation microphone usage description, and remove the obsolete armv7 device capability without hardcoding a signing team.
Introduce a shared ExportToolbar that puts the EXPORT tag on the left,
AutoSavedSourcesUndo + PDF/DOCX/Google Docs (+ optional Clipboard) glued
to the right, and a second row only for auxiliary actions like flowchart
imports, save, and undo. Attack/Brainstorm/Factoids/Generate all consume
the shared component so their export rows look and align identically.

Also align Attack strength-area ticks with the text baseline (drop the
stray mt-1) and update an AskChat test that no longer applies now that
the desktop saved-chats sidebar is always open.
Align the export label and all export/action buttons to the left while preserving wrapping order across locales.
Keep export actions in a single wrapping row and translate generate result model, duration, and clipboard labels across English, Spanish, and Hungarian.
Row 1: secondaryActions (Save, Discuss, Flowcharts, Undo) + onDiscuss
       + showUndo's AutoSavedSourcesUndo
Row 2: EXPORT tag + extraExports (Clipboard) + PDF + DOCX + Google Docs

Both rows use justify-start. The previous version (commit 1be277e)
only flipped justify-between to justify-start without forcing a row
break, so on wide screens everything still collapsed to a single row.
User explicitly requested 2 left-aligned rows and called out that
ELEVEN earlier agents shipped partial fixes. Confirmed layout choice
on 2026-05-27.

Pairs with Part 4 (replaces / extends commit 1be277e).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a regression test for the exact two-row left-aligned export toolbar and remove stale onboarding copy that advertised compare-side behavior.
Allow capacitor://localhost across browser-called Edge Functions, keep generated share links on the public HTTPS origin, and add a Deno regression test for the native CORS surface.
Match the existing evidence-direction wording so the backend prompt-builder suite reflects the shipped implementation.
Use PKCE-only OAuth callbacks with bounded exchange time, recover cold-start deep links, route external WebView links through the native browser, keep Stripe callbacks on HTTPS, and align the Swift package pin with Capacitor 8.4.0.
Add Playwright smoke coverage for public and authenticated routes across locales, and scope Vite dependency scanning to the app entry so Capacitor-copied build assets do not get scanned during dev.
Upgrade Vitest and coverage tooling to the current clean-audit line, then update affected test mocks for Vitest 4 constructor behavior. Also removes a future-breaking nested mock warning and turns the Generate page result-state test into a real assertion.
Fix the valid CodeRabbit findings: restore the AttackResults discuss icon import, prevent duplicate undo controls, auto-open Factoids Google Docs exports, localize onboarding strings, add the iOS UserDefaults privacy reason, declare the Vite Node engine, and calibrate the selected-scope coverage gate. Capacitor plugin packages were left unchanged because npm reports those plugin versions as the latest available releases.
Address the second CodeRabbit pass: avoid empty export toolbar rows, clear OAuth query noise, close native auth error callbacks, make hardware back exit when it cannot go back, preserve iOS safe-area footer padding, tighten Playwright console filters, keep mobile emulation consistent, and document the selected-scope coverage gate.
Clean up remaining valid CodeRabbit items: always strip OAuth callback query params even on failed code exchange, reuse the E2E locale helper, guard duplicate native launch URL handling, and make the selected-scope coverage baseline rationale explicit.
Use the product bundle identifier for the custom URL type, set the dark native status bar to light glyphs, and route export button labels through shared locale keys. The App Store encryption flag remains false because this app only uses standard TLS/OAuth/PKCE, and Capacitor 8 remains intentional for this wrapper.
Enable row-level security for the Stripe webhook idempotency table, revoke anon/authenticated access, and keep service-role access for backend webhook processing.
@vercel

vercel Bot commented Jun 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
debate-navigator Ready Ready Preview, Comment Jun 3, 2026 8:15am

@coderabbitai

coderabbitai Bot commented Jun 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Free

Run ID: 2ebb1d5f-42cd-4c0a-ba32-a6378e9143db

📥 Commits

Reviewing files that changed from the base of the PR and between 29056fc and 095e8b1.

⛔ Files ignored due to path filters (5)
  • ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png is excluded by !**/*.png
  • ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png is excluded by !**/*.png
  • ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png is excluded by !**/*.png
  • ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png is excluded by !**/*.png
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (70)
  • .gitignore
  • README.md
  • capacitor.config.ts
  • e2e/authed-pages.spec.ts
  • e2e/helpers/auth.ts
  • e2e/public-pages.spec.ts
  • eslint.config.js
  • index.html
  • ios/.gitignore
  • ios/App/App.xcodeproj/project.pbxproj
  • ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  • ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
  • ios/App/App/AppDelegate.swift
  • ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json
  • ios/App/App/Assets.xcassets/Contents.json
  • ios/App/App/Assets.xcassets/Splash.imageset/Contents.json
  • ios/App/App/Base.lproj/LaunchScreen.storyboard
  • ios/App/App/Base.lproj/Main.storyboard
  • ios/App/App/Info.plist
  • ios/App/App/PrivacyInfo.xcprivacy
  • ios/App/CapApp-SPM/.gitignore
  • ios/App/CapApp-SPM/Package.swift
  • ios/App/CapApp-SPM/README.md
  • ios/App/CapApp-SPM/Sources/CapApp-SPM/CapApp-SPM.swift
  • ios/debug.xcconfig
  • package.json
  • playwright.config.ts
  • src/components/features/ExportToolbar.tsx
  • src/components/features/attack/AttackResults.tsx
  • src/components/features/brainstorm/BrainstormResults.tsx
  • src/components/features/factoids/FactoidsResults.tsx
  • src/components/features/generate/GenerateResults.tsx
  • src/components/layout/AppLayout.tsx
  • src/index.css
  • src/lib/native.ts
  • src/lib/stripe.ts
  • src/lib/supabase/AuthProvider.tsx
  • src/lib/supabase/client.ts
  • src/lib/supabase/oauth-url.ts
  • src/locales/en.json
  • src/locales/es.json
  • src/locales/hu.json
  • src/main.tsx
  • src/native-app-url-listener.ts
  • src/native-runtime.ts
  • src/register-service-worker.ts
  • src/test/auth/AuthProvider.behavior.test.tsx
  • src/test/components/ExportToolbar.test.tsx
  • src/test/export-utils-extended.test.ts
  • src/test/export-utils.test.ts
  • src/test/native-app-url-listener.test.ts
  • src/test/native-runtime.test.ts
  • src/test/native.test.ts
  • src/test/pages/GeneratePage.test.tsx
  • src/test/stripe.test.ts
  • supabase/functions/_shared/native-cors.test.ts
  • supabase/functions/admin/index.ts
  • supabase/functions/ai-stream/index.ts
  • supabase/functions/ai/index.ts
  • supabase/functions/ai/prompt-builder.test.ts
  • supabase/functions/create-checkout/index.ts
  • supabase/functions/create-portal/index.ts
  • supabase/functions/export-gdoc/index.ts
  • supabase/functions/import-source/index.ts
  • supabase/functions/redeem-promo/index.ts
  • supabase/functions/share/index.ts
  • supabase/migrations/20260603064751_enable_rls_webhook_events.sql
  • tsconfig.node.json
  • vite.config.ts
  • vitest.config.ts

📝 Walkthrough

Walkthrough

This PR implements comprehensive iOS native app support for DebateLab via Capacitor. It introduces the complete native iOS project infrastructure, modernizes OAuth to use PKCE code exchange, consolidates export UI into a reusable toolbar component, adds native-aware platform abstraction for URLs and external browsing, and includes E2E test coverage for public and authenticated routes across multiple locales.

Changes

iOS Capacitor Native App

Layer / File(s) Summary
iOS Xcode Project Setup
ios/App/App.xcodeproj/project.pbxproj, ios/App/App/AppDelegate.swift, ios/App/App/Base.lproj/*.storyboard, ios/App/App/Info.plist, ios/App/App/*.xcassets, ios/App/App/PrivacyInfo.xcprivacy, ios/.gitignore
Complete iOS Xcode project with native target, bundle identifier, storyboards for main and launch screens, app icon/splash assets, Info.plist metadata, privacy configuration, and AppDelegate lifecycle/URL/Universal Link routing.
Capacitor Configuration & Build Setup
capacitor.config.ts, ios/App/CapApp-SPM/Package.swift, package.json, tsconfig.node.json, vite.config.ts
Capacitor config exports app identifiers, web assets dir, iOS URL schemes, and plugin settings; SPM package manifest for local Capacitor dependencies; npm scripts for ios:build and ios:open; dynamic Vite base path for capacitor mode; TypeScript node config includes capacitor.config.ts.
Native Platform Detection & URL Helpers
src/lib/native.ts, src/test/native.test.ts
Detects native platform, computes redirect URLs (app scheme vs. web origin), provides OAuth redirect URL (native callback vs. /app), opens external URLs via Capacitor Browser or window.open, closes browser on native; tests verify platform-specific redirect behavior.
Native Deep Link & OAuth Callback Handling
src/native-app-url-listener.ts, src/test/native-app-url-listener.test.ts
Processes iOS deep links via App.getLaunchUrl() and appUrlOpen listeners, resolves OAuth code query parameters and maps host=app paths to internal routes, detects and handles auth errors, de-duplicates URL handling, logs registration failures; tests validate PKCE code preservation and route restoration.
Native Runtime Configuration (UI, Keyboard, Back Button, Haptics)
src/native-runtime.ts, src/test/native-runtime.test.ts, src/register-service-worker.ts
Configures status bar overlay/style, tracks keyboard visibility with CSS variables and DOM classes, handles device back button (back or exit), intercepts clicks on external links to open via native browser, triggers haptic feedback on touch with throttling; skips service worker registration when native; tests validate external URL filtering.
OAuth Flow Modernization (PKCE Code Exchange)
src/lib/supabase/AuthProvider.tsx, src/lib/supabase/client.ts, src/lib/supabase/oauth-url.ts, src/test/auth/AuthProvider.behavior.test.tsx
Sets auth flowType to 'pkce', exchanges OAuth code from query params via exchangeCodeForSession, clears OAuth query params after exchange, adds configurable 4s timeout helper, sets redirectTo via getOAuthRedirectUrl(), opens OAuth URL via openExternalUrl; comprehensive behavior tests cover code exchange, timeout failures, and error propagation.
Export Toolbar Component (New UI)
src/components/features/ExportToolbar.tsx, src/test/components/ExportToolbar.test.tsx
Reusable two-row left-aligned export UI component supporting secondary actions (save/discuss/undo), optional Discuss button, PDF/DOCX/Google Docs export buttons with loading state, and extra export slots; layout test validates structure and action labels.
Result Pages Export Refactoring
src/components/features/attack/AttackResults.tsx, src/components/features/brainstorm/BrainstormResults.tsx, src/components/features/factoids/FactoidsResults.tsx, src/components/features/generate/GenerateResults.tsx
Replaces inline export buttons with new ExportToolbar component across all result pages; updates Google Docs export to use openExternalUrl with error handling; refactors generate results to display localized model label and duration metadata (ms/s); adjusts import sets to reflect UI consolidation.
Layout & Styling for Native Safe Areas
src/index.css, src/components/layout/AppLayout.tsx, index.html
Adds CSS utility classes (.native-safe-screen, .native-main-content, .native-safe-bottom) for safe-area padding and keyboard-aware bottom spacing; updates AppLayout wrapper and footer to use native-safe classes; adds viewport-fit=cover to HTML meta tag for notched devices.
Stripe Checkout & Portal with Native URL Handling
src/lib/stripe.ts, src/test/stripe.test.ts
Computes checkout/portal return URLs using getWebAppRedirectUrl; navigates via openExternalUrl instead of direct href assignment; test coverage includes web and native checkout scenarios with public HTTPS callback URLs.
Backend CORS & Security (Native Origins, RLS)
supabase/functions/*/index.ts, supabase/functions/_shared/native-cors.test.ts, supabase/migrations/20260603064751_enable_rls_webhook_events.sql
Allows capacitor://localhost and http://127.0.0.1:54321 in Edge Function CORS headers; share function uses PUBLIC_WEB_APP_ORIGIN constant for native shareability; enables RLS on webhook_events table, grants full access only to service_role; tests verify native CORS and shareUrl origin usage.
E2E Testing Infrastructure
playwright.config.ts, e2e/helpers/auth.ts, e2e/authed-pages.spec.ts, e2e/public-pages.spec.ts
Configures Playwright for desktop Chrome (1280×800) and mobile Chromium (Pixel 5) projects with CI-aware retries/reporters; adds helpers to stub Supabase sessions and set test locales; specs validate public routes (/, /terms, /privacy, /login) and authenticated /app/* routes across en/es/hu locales with error filtering.
Supporting Updates
.gitignore, README.md, package.json, eslint.config.js, vitest.config.ts, src/locales/*.json, src/test/export-utils.test.ts, src/test/pages/GeneratePage.test.tsx, ios/App/CapApp-SPM/.gitignore, ios/App/CapApp-SPM/README.md, ios/App/CapApp-SPM/Sources/CapApp-SPM/CapApp-SPM.swift, ios/debug.xcconfig, ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
Adds Playwright artifact ignores; documents iOS build/open commands and OAuth native URIs in README; adds npm iOS/E2E scripts; updates Vitest coverage thresholds; adds export format translations (en/es/hu); refactors mock factories for docx module; updates ESLint ignores; Swift package metadata; Capacitor Swift PM dependency pin.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hops with glee at native dreams so bright,
iOS now runs the debate app with delight!
Code exchanges flow through PKCE's dance,
While toolbars export with elegant stance.
From keyboard to haptics, safe areas in place—
One app, two platforms, a beautiful embrace! 🍎✨

@Programcode
Programcode merged commit bbc6b85 into main Jun 3, 2026
4 checks passed
Programcode added a commit that referenced this pull request Oct 17, 2026
* Add a per-agent, Docker-free local Supabase stack helper

Uses the CLI's experimental stack engine with the native runtime so
several agents can each run migrations, pgTAP and RLS queries at once
without the shared db-gate lock.

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

* Plan the 25-item pre-ship security pass

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

* Document the per-agent stack and run the full database suite on it

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

* Harden promo redemption, client grants and account deletion

- Lock a user out of promo redemption after 5 failures in 15 minutes and
  answer unknown, expired and exhausted codes alike (next-15 #2).
- Promo grants never lower a tier, end date or daily limit.
- Revoke TRUNCATE/TRIGGER/REFERENCES/MAINTAIN from client roles, make
  global_prompts and global rate limits server-only, make the admin
  reporting RPCs service-role only, and close default privileges.
- Account deletion removes the user's demo code and scrubs the IPs and
  fingerprints recorded against it.

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

* Serve Edge Functions on the per-agent stack

The stack does not find functions behind a symlink, and its workers read
supabase/functions/.env rather than --env-file, so copy both into the
overlay.

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

* Render Markdown images from model output as links

A prompt-injected source could make the model emit an image whose URL
carries the student's work; the browser fetched it without a click.

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

* Clear tool drafts on sign-out and user switch

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

* Tighten the CSP to the hosts the app uses

Drop provider, Jina, Google API and font hosts the client no longer
calls; replace *.supabase.co with the two project hosts; stop allowing
images from any HTTPS host; add form-action; make X-Frame-Options agree
with frame-ancestors.

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

* Guard the public build and trim client telemetry

- Fail the build if dist/ holds source maps, server-secret patterns or a
  non-anon JWT.
- Log auth events only in development builds.
- Redact share codes from Sentry events and transactions.
- Move tailwindcss-animate to devDependencies and update dompurify.

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

* fix(edge): claim Stripe events before applying current subscription state

Co-Authored-By: Codex <noreply@openai.com>

* fix(edge): authenticate and allowlist AI features before processing input

Co-Authored-By: Codex <noreply@openai.com>

* fix(edge): conceal internal and provider errors from clients

Co-Authored-By: Codex <noreply@openai.com>

* fix(edge): validate promo inputs and omit attempted codes from logs

Co-Authored-By: Codex <noreply@openai.com>

* fix(edge): rate limit unknown demo callers without extending refusals

Co-Authored-By: Codex <noreply@openai.com>

* fix(edge): validate import bodies and public hosts before reserving quota

Co-Authored-By: Codex <noreply@openai.com>

* fix(edge): restrict export origins and vary CORS responses by Origin

Co-Authored-By: Codex <noreply@openai.com>

* fix(edge): keep prompt and research text out of diagnostic logs

Co-Authored-By: Codex <noreply@openai.com>

* Recover abandoned webhook claims and take payment status from Stripe

Claiming an event before its handlers means a worker that dies mid-run
would leave a claim that blocks every Stripe retry. Mark claims complete,
and let a retry take over a claim left incomplete for 10 minutes.
payment_succeeded now copies the subscription status from Stripe instead
of writing active, so a late delivery cannot revive a canceled plan.

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

* Pin supabase-js in Edge Functions to the client's version

esm.sh/@supabase/supabase-js@2 resolved whatever 2.x esm.sh served on
deploy day, inside functions that hold the service-role key.

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

* Record the pre-ship security findings and new database rules

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

* Describe the explicit Supabase hosts in the CSP

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

* Give the webhook a fake Stripe in the full-stack gate

The webhook now reads subscription state from Stripe, so it honours
STRIPE_API_URL like the account function, and the fake serves
GET /v1/subscriptions/:id from state the test sets. The billing journey
also checks that a late active update cannot revive a canceled plan.

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

* Find Library source fields by placeholder in full-stack journeys

The form gained visible Title and Content labels, so the placeholder is
no longer the accessible name. This was already failing on main.

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

* Claim webhook events on the database clock and retry in-flight ones

Codex review round 1:
- A retry that found an unfinished claim got 200 duplicate, which ends
  Stripe's retries; it now gets 409 so a dead delivery is retried.
- The abandon cutoff came from the Edge clock but was compared with a
  Postgres timestamp. claim_webhook_event and complete_webhook_event now
  decide on the database clock.
- The new-object grant test checked pg_default_acl rows; it now checks
  the effective privileges of a newly created table and sequence. A
  global function revoke would also strip extension functions (pgTAP
  broke), so functions keep their explicit per-migration revokes, which
  the anon EXECUTE test enforces.

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

* Report a webhook duplicate only for a completed claim

Codex review round 2: if a failed delivery released its claim between a
concurrent delivery's insert and its read, the second delivery found no
row and answered duplicate (HTTP 200) for an event nobody completed.
Claim again when the row is gone, and fall back to in_progress.

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

* Update the security report for the review fixes

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

* Drop hosted rate_limits and global_prompts policies by catalog

Production and staging carry rate_limits policies that no migration
created (rate_limits_select_own_or_global, "Users can read own limits"),
both allowing global rows. Dropping by the migration's own name would
have left them. Drop every policy on the two tables, and revoke client
writes on rate_limits. Rehearsed on read-only dumps of both projects.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>

This branch was successfully deployed

1 active deployment
Preview — 095e8b1b Deployed Jun 3, 2026 by vercel[bot]
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.

1 participant