Skip to content

iOS App Clip: scan the pairing QR, pair instantly, hand off to the full app#706

Merged
arul28 merged 5 commits into
mainfrom
ade/qr-app-clip-d537f2cb
Jul 7, 2026
Merged

iOS App Clip: scan the pairing QR, pair instantly, hand off to the full app#706
arul28 merged 5 commits into
mainfrom
ade/qr-app-clip-d537f2cb

Conversation

@arul28

@arul28 arul28 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

What

A native iOS App Clip (ADEClip, 1.4 MB) that makes ADE pairing instant for someone who doesn't have the app yet: scan the desktop's pairing QR with the iPhone camera → the clip card slides up → enter the PIN → paired. When the full app is installed (the clip promotes it via the App Store overlay), it adopts the pairing automatically on first launch — no re-pair, no PIN re-entry.

How

  • ADEClip target (apps/ios/ADEClip/): pairing-only WebSocket client speaking the v1 sync envelope (pairing_requestpairing_result), walking the QR's direct candidates then relay URLs, mirroring the full app's preference order. Compiles PairingQrPayload.swift (same v3 codec, fragment-carried payload, PIN never in the QR).
  • Handoff: one-shot credential blob in the shared App Group (.completeFileProtection); the full app's new SyncService.adoptClipPairingHandoffIfPresent() consumes it at bootstrap, persists the machine exactly like an in-app pairing (saved profile + keychain tokens), and connects. Stale blobs (>7 days) are discarded; the blob never survives its first read.
  • DPoP composition: the clip deliberately pairs without a device key — the host's existing TOFU upgrade adopts the full app's Secure-Enclave key on its first paired hello, so Mobile Push Relay Platform #705's security model is preserved.
  • Web (apps/web): /.well-known/apple-app-site-association (appclipsVQ372F39G6.com.ade.ios.Clip, served as application/json via vercel.json) + a /pair smart landing with the apple-itunes-app clip banner.
  • ASC (done via asc): registered com.ade.ios.Clip (97ZL5TPJB8) with App Groups + Associated Domains.

Verification

Follow-ups (not in this PR)

  • Advanced App Clip experience in ASC (QR → clip without Safari banner) — configurable only after a build with the clip is uploaded to TestFlight.
  • On-device E2E (camera scan invocation; simulator can't exercise clip invocation from QR).

🤖 Generated with Claude Code

ADE   Open in ADE  ·  ade/qr-app-clip-d537f2cb branch  ·  PR #706

Summary by CodeRabbit

  • New Features
    • Added an iPhone App Clip pairing flow with a QR-based entry point, PIN entry, and pairing progress feedback.
    • Added a web pairing landing page that launches the App Clip with pairing instructions.
    • Enabled an automatic App Clip → full app handoff after successful pairing.
  • Bug Fixes
    • Improved pairing handoff reliability by using one-time, time-limited shared handoff data and skipping adoption when pairing is already present.
  • Tests
    • Added automated coverage for one-shot handoff consumption and validation behavior.

Greptile Summary

This PR adds an iOS App Clip flow for pairing ADE from a scanned QR code. The main changes are:

  • New ADEClip target with QR invocation handling, PIN entry, WebSocket pairing, and App Store overlay promotion.
  • Shared App Group handoff that stores clip pairing credentials once and lets the full app adopt them on first launch.
  • Full app bootstrap changes that consume the handoff before the first sync connection and persist the paired profile/token.
  • Web /pair landing page, Apple App Site Association file, and Vercel routing/header updates for App Clip invocation.
  • Tests for handoff validation, stale payload rejection, malformed payload rejection, and one-shot deletion.

Confidence Score: 5/5

Safe to merge with low risk.

The changed code follows existing pairing and profile persistence patterns, validates and deletes the handoff blob on first read, bounds clip WebSocket handshakes, and includes focused tests for the handoff decoder.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • We inspected the actual rendered pairing landing page from repository HTML/CSS using the after image, confirming the content and steps appear as intended.
  • We reviewed the Playwright navigation log to confirm the page exposes the iOS app-clip metadata (app-id, app-clip bundle-id, and app-clip display) as reported by the browser log.
  • We validated the meta tag, AASA parse, appclips value, and /pair rewrites via the validation log, which shows PASS results and the expected values.
  • We examined the Vite server output to verify that Vite itself does not perform the /pair rewrite, indicating that rewrite behavior is configured at the hosting level.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
apps/ios/ADE/Services/SyncService.swift Adopts valid clip handoff credentials into the existing profile/keychain persistence path and reconnects.
apps/ios/ADE/Services/ClipPairingHandoff.swift Defines the one-shot App Group handoff payload decoder with version, required-field, and age validation.
apps/ios/ADEClip/ClipPairingClient.swift Implements the minimal WebSocket pairing client for direct and relay QR candidates with bounded handshake attempts.
apps/ios/ADEClip/ClipHandoff.swift Writes successful pairing credentials to the shared App Group container with complete file protection.
apps/ios/ADEClip/ClipPairingView.swift Adds the App Clip pairing UI state machine for invocation parsing, PIN entry, pairing, handoff, and Store overlay promotion.
apps/web/vercel.json Serves the AASA file as JSON and rewrites /pair routes to the landing page.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Camera as iPhone Camera/Safari
participant Clip as ADEClip
participant Host as ADE desktop host
participant Group as Shared App Group file
participant App as Full ADE app
participant Keychain as iOS Keychain

Camera->>Clip: "Open https://ade-app.dev/pair#payload"
Clip->>Clip: Parse PairingQrPayload and collect candidates
Clip->>Host: WebSocket pairing_request + PIN + clip peer id
Host-->>Clip: pairing_result(secret, deviceId)
Clip->>Group: Write clip-pairing-handoff.v1.json
Clip->>Camera: Show App Store overlay
App->>Group: consume() handoff on first launch
Group-->>App: Return payload and delete file
App->>Keychain: Save device id and pairing token
App->>App: Save HostConnectionProfile
App->>Host: Reconnect as paired device
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Camera as iPhone Camera/Safari
participant Clip as ADEClip
participant Host as ADE desktop host
participant Group as Shared App Group file
participant App as Full ADE app
participant Keychain as iOS Keychain

Camera->>Clip: "Open https://ade-app.dev/pair#payload"
Clip->>Clip: Parse PairingQrPayload and collect candidates
Clip->>Host: WebSocket pairing_request + PIN + clip peer id
Host-->>Clip: pairing_result(secret, deviceId)
Clip->>Group: Write clip-pairing-handoff.v1.json
Clip->>Camera: Show App Store overlay
App->>Group: consume() handoff on first launch
Group-->>App: Return payload and delete file
App->>Keychain: Save device id and pairing token
App->>App: Save HostConnectionProfile
App->>Host: Reconnect as paired device
Loading

Comments Outside Diff (1)

  1. General comment

    P1 /pair App Clip landing page is shadowed by SPA fallback and renders 404

    • Bug
      • When the built web app is served with Vite preview, navigating to http://127.0.0.1:4180/pair/#samplepairpayload returns the React app's not-found page instead of public/pair/index.html. The executed Playwright proof captured title ADE — Not found, body text 404 Page not found, and PAIR_META_APPLE_ITUNES_APP: <missing>, so iPhone users would not see the intended App Clip smart banner on /pair.
    • Cause
      • The static file was added at apps/web/public/pair/index.html, but the app/Vite routing being exercised resolves /pair/ through the SPA entry and not the nested static index.html, so the catch-all app route renders its 404 page before the static App Clip landing content is used.
    • Fix
      • Ensure /pair and /pair/ are served directly from the static landing page before the SPA fallback. For example, add an explicit Vercel rewrite/route from /pair and /pair/ to /pair/index.html, or move/generate the landing asset at a path that the hosting layer serves before the React catch-all.

    T-Rex Ran code and verified through T-Rex

Reviews (6): Last reviewed commit: "clip: adopt handoff deviceId on fresh in..." | Re-trigger Greptile

@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview Jul 7, 2026 12:07am

@cursor

cursor Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@arul28

arul28 commented Jul 5, 2026

Copy link
Copy Markdown
Owner Author

@copilot review

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds an App Clip pairing flow, shared handoff storage between the App Clip and the full app, bootstrap adoption in the main app, and web metadata and routing for App Clip invocation.

Changes

App Clip pairing and handoff

Layer / File(s) Summary
Pairing WebSocket client and errors
apps/ios/ADEClip/ClipPairingClient.swift
Defines pairing success/error types and implements candidate-walking pairing over WebSocket with per-host attempts, URL construction, and envelope decoding.
Clip-side identity and handoff storage
apps/ios/ADEClip/ClipHandoff.swift
Defines the handoff payload shape, persisted clip identity accessors, and atomic write of pairing credentials to the App Group container.
Pairing UI state machine and views
apps/ios/ADEClip/ClipPairingView.swift
Implements the phase-driven ClipPairingModel and ClipPairingView covering invocation, PIN entry, pairing, and post-pairing states with a StoreKit overlay.
App Clip entry point and configuration
apps/ios/ADEClip/ADEClipApp.swift, apps/ios/ADEClip/ADEClip.entitlements, apps/ios/ADEClip/Info.plist
Adds the App Clip's @main entry wiring web-activity invocation, plus entitlements and Info.plist configuration.
Main app handoff consumption
apps/ios/ADE/Services/ClipPairingHandoff.swift, apps/ios/ADETests/ClipPairingHandoffTests.swift, apps/ios/ADE/Services/SyncService.swift, apps/ios/ADE/App/ADEApp.swift
Adds the one-shot handoff model, tests its validation/deletion behavior, and adopts the handoff during app bootstrap to build and persist a HostConnectionProfile.
Web routing and metadata for App Clip invocation
apps/web/public/.well-known/apple-app-site-association, apps/web/public/pair/index.html, apps/web/vercel.json
Adds the appclips association entry, the /pair landing page, and related Vercel route and header rules.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested labels: ios, web

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the new iOS App Clip pairing flow and handoff to the full app.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade/qr-app-clip-d537f2cb

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/ios/ADEClip/ClipPairingClient.swift (1)

38-40: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Instance-level session/task are only safe due to caller's re-entrancy guard.

Storing session/task as class properties (rather than locals within pairOnce) means a concurrent pair() invocation on the same ClipPairingClient instance would race on these fields, with the defer block nil-ing out state that may belong to a different in-flight call. This is currently mitigated by ClipPairingView.submitPin()'s phase == .enterPin guard, but it's fragile against future refactors that might reuse the client differently. Consider scoping these as local variables inside pairOnce.

Also applies to: 78-85

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADEClip/ClipPairingClient.swift` around lines 38 - 40, The shared
instance properties session and task in ClipPairingClient make pairOnce()
stateful and race-prone if pair() is ever called concurrently on the same
instance. Move these websocket/session variables into pairOnce() as local
variables, and update the cleanup/defer logic to clear only the local references
so each invocation owns its own connection state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/ios/ADEClip/ClipPairingClient.swift`:
- Around line 69-152: The receive loop in ClipPairingClient.pairOnce can still
wait indefinitely because timeoutIntervalForRequest only applies to opening the
websocket, not to task.receive(). Add an explicit time-based deadline or cancel
the URLSessionWebSocketTask after a fixed number of seconds while iterating for
the pairing_result, and make sure the timeout path throws a pairing failure
instead of leaving the caller stuck in .pairing.

In `@apps/ios/ADEClip/Info.plist`:
- Around line 16-21: The App Clip’s pairing flow uses
`ClipPairingClient.socketURL` to connect with `ws://` local-network candidates,
but `Info.plist` is missing the required App Transport Security exception. Add
an `NSAppTransportSecurity` entry with `NSAllowsLocalNetworking` enabled in the
same plist so the local WebSocket pairing path can run at runtime.

---

Nitpick comments:
In `@apps/ios/ADEClip/ClipPairingClient.swift`:
- Around line 38-40: The shared instance properties session and task in
ClipPairingClient make pairOnce() stateful and race-prone if pair() is ever
called concurrently on the same instance. Move these websocket/session variables
into pairOnce() as local variables, and update the cleanup/defer logic to clear
only the local references so each invocation owns its own connection state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c4cff202-36e3-40b9-8d9c-72bd4d8fe421

📥 Commits

Reviewing files that changed from the base of the PR and between 0c28314 and bc7efa3.

⛔ Files ignored due to path filters (1)
  • apps/ios/ADE.xcodeproj/project.pbxproj is excluded by !**/*.xcodeproj/project.pbxproj
📒 Files selected for processing (12)
  • apps/ios/ADE/App/ADEApp.swift
  • apps/ios/ADE/Services/ClipPairingHandoff.swift
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADEClip/ADEClip.entitlements
  • apps/ios/ADEClip/ADEClipApp.swift
  • apps/ios/ADEClip/ClipHandoff.swift
  • apps/ios/ADEClip/ClipPairingClient.swift
  • apps/ios/ADEClip/ClipPairingView.swift
  • apps/ios/ADEClip/Info.plist
  • apps/web/public/.well-known/apple-app-site-association
  • apps/web/public/pair/index.html
  • apps/web/vercel.json

Comment thread apps/ios/ADEClip/ClipPairingClient.swift
Comment thread apps/ios/ADEClip/Info.plist
@arul28

arul28 commented Jul 5, 2026

Copy link
Copy Markdown
Owner Author

@codex review

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.

Pull request overview

Adds an iOS App Clip (“ADEClip”) to enable QR-driven instant pairing, plus a web landing page/AASA plumbing for App Clip invocation and a one-shot App Group handoff that the full iOS app adopts on first launch.

Changes:

  • Introduces the ADEClip target with a minimal WebSocket pairing client + SwiftUI PIN flow and an App Group handoff writer.
  • Adds full-app adoption of the clip handoff at bootstrap (persist profile + keychain token, then reconnect).
  • Adds web assets for App Clip invocation (/.well-known/apple-app-site-association, /pair landing) and Vercel headers.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
apps/web/vercel.json Adds JSON content-type header for AASA (but currently lacks /pair rewrite protection from SPA fallback).
apps/web/public/pair/index.html Adds a static /pair landing page with App Clip smart banner metadata and pairing instructions.
apps/web/public/.well-known/apple-app-site-association Adds App Clip association payload for ade-app.dev.
apps/ios/ADEClip/Info.plist App Clip Info.plist (local networking ATS posture + local network usage text).
apps/ios/ADEClip/ClipPairingView.swift SwiftUI state machine + PIN entry UI + App Store overlay presentation.
apps/ios/ADEClip/ClipPairingClient.swift Minimal v1 sync-envelope WebSocket pairing client with candidate walking and handshake timeout.
apps/ios/ADEClip/ClipHandoff.swift Writes one-shot credential blob to shared App Group with .completeFileProtection.
apps/ios/ADEClip/ADEClipApp.swift App Clip entrypoint handling NSUserActivityTypeBrowsingWeb invocation URLs.
apps/ios/ADEClip/ADEClip.entitlements App Clip entitlements for App Groups + Associated Domains.
apps/ios/ADE/Services/SyncService.swift Adds adoptClipPairingHandoffIfPresent() to persist adopted pairing and reconnect.
apps/ios/ADE/Services/ClipPairingHandoff.swift Adds handoff blob reader/deleter with version + age validation.
apps/ios/ADE/App/ADEApp.swift Calls handoff adoption before first foreground sync connect.
apps/ios/ADE.xcodeproj/project.pbxproj Adds the ADEClip target, sources, embed phase, and build settings wiring.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/web/vercel.json
Comment on lines +22 to 30
{
"source": "/.well-known/apple-app-site-association",
"headers": [
{
"key": "Content-Type",
"value": "application/json"
}
]
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 51fad04 — /pair and /pair/ now rewrite to /pair/index.html ahead of the SPA catch-all, so the clip-banner landing always serves.

Comment thread apps/ios/ADEClip/ClipPairingView.swift Outdated
Comment on lines +40 to +41
let code = pin.trimmingCharacters(in: .whitespacesAndNewlines)
guard code.count >= 4 else { return }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 51fad04 — submit now requires exactly 6 digits (pinIsComplete), matching the desktop generator.

Comment thread apps/ios/ADEClip/ClipPairingView.swift Outdated
}
.buttonStyle(.borderedProminent)
.buttonBorderShape(.roundedRectangle(radius: 14))
.disabled(model.phase == .pairing || model.pin.trimmingCharacters(in: .whitespaces).count < 4)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 51fad04 — the Pair button disables until pinIsComplete (6 digits), same gate as submit.

Comment thread apps/web/public/pair/index.html Outdated
<body>
<div class="card">
<h1>Pair your iPhone with ADE</h1>
<p id="lead">Scan this page's QR code with your iPhone camera to pair instantly.</p>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 51fad04 — lead copy now says to scan the pairing code shown in ADE on the Mac; no claim that this page displays a QR.

Comment on lines +3521 to +3528
/// Adopts pairing credentials handed off by the ADE App Clip (scan QR →
/// pair before the full app is installed). The clip writes a one-shot blob
/// into the shared App Group container; this reads it, persists the machine
/// exactly like a successful in-app pairing (saved profile + keychain
/// tokens), and connects. Returns true when a handoff was adopted.
@discardableResult
func adoptClipPairingHandoffIfPresent() async -> Bool {
guard let handoff = ClipPairingHandoff.consume() else { return false }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Added in 51fad04 — ClipPairingHandoffTests (7 cases: decode gates for version/deviceId/secret, one-shot delete even on invalid blobs, stale-age boundary via injectable now, malformed JSON, missing file). consume() gained an injectable location/now for testability; ran green on simulator alongside PairingAndDpopTests. Skip-when-already-paired lives in SyncService.adoptClipPairingHandoffIfPresent and reuses savedProfileForPairingQr, which PairingAndDpopTests already exercises.

@cursor

cursor Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@arul28

arul28 commented Jul 5, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (2)
apps/web/vercel.json (1)

45-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Correctly ordered before the SPA fallback.

Placing the /pair and /pair/ rewrites ahead of the catch-all /((?!.*\\.).*) rule (line 53) ensures they aren't swallowed by the SPA fallback, matching the stated intent to serve the static landing page before the SPA takes over.

Optionally, the two rules could be consolidated into one using an optional trailing slash pattern, since Vercel's rewrite source supports path-to-regexp syntax:

♻️ Optional consolidation
     {
-      "source": "/pair",
-      "destination": "/pair/index.html"
-    },
-    {
-      "source": "/pair/",
+      "source": "/pair/?",
       "destination": "/pair/index.html"
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/vercel.json` around lines 45 - 52, Move the /pair and /pair/
rewrites in vercel.json so they appear before the catch-all SPA fallback
rewrite, using the existing rewrite entries in the Vercel config to preserve the
intended static landing page behavior. If you want to simplify the config, you
can also consolidate the two pair routes into one optional trailing-slash rule,
but the key fix is ensuring the /pair mapping is ordered ahead of the
/((?!.*\\.).*) rule.
apps/ios/ADEClip/ClipPairingClient.swift (1)

88-116: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Drop the unused session/task instance state self.session and self.task aren’t read anywhere else; the local session/task values already cover lifetime and cancellation, so these properties can be removed to simplify the client.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADEClip/ClipPairingClient.swift` around lines 88 - 116, The
`ClipPairingClient.pairOnceInner(host:port:pin:)` method is storing `session`
and `task` into unused instance properties via `self.session` and `self.task`,
but those values are never read elsewhere. Remove the `session`/`task` state
from the class and keep the existing local `URLSession` and web socket task
lifecycle management inside `pairOnceInner`, including the `defer` cleanup and
cancellation handler.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/ios/ADEClip/ClipPairingClient.swift`:
- Around line 88-116: The `ClipPairingClient.pairOnceInner(host:port:pin:)`
method is storing `session` and `task` into unused instance properties via
`self.session` and `self.task`, but those values are never read elsewhere.
Remove the `session`/`task` state from the class and keep the existing local
`URLSession` and web socket task lifecycle management inside `pairOnceInner`,
including the `defer` cleanup and cancellation handler.

In `@apps/web/vercel.json`:
- Around line 45-52: Move the /pair and /pair/ rewrites in vercel.json so they
appear before the catch-all SPA fallback rewrite, using the existing rewrite
entries in the Vercel config to preserve the intended static landing page
behavior. If you want to simplify the config, you can also consolidate the two
pair routes into one optional trailing-slash rule, but the key fix is ensuring
the /pair mapping is ordered ahead of the /((?!.*\\.).*) rule.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9b7c7455-0c23-45ad-b021-c9aa4292818e

📥 Commits

Reviewing files that changed from the base of the PR and between bc7efa3 and 51fad04.

⛔ Files ignored due to path filters (1)
  • apps/ios/ADE.xcodeproj/project.pbxproj is excluded by !**/*.xcodeproj/project.pbxproj
📒 Files selected for processing (7)
  • apps/ios/ADE/Services/ClipPairingHandoff.swift
  • apps/ios/ADEClip/ClipPairingClient.swift
  • apps/ios/ADEClip/ClipPairingView.swift
  • apps/ios/ADEClip/Info.plist
  • apps/ios/ADETests/ClipPairingHandoffTests.swift
  • apps/web/public/pair/index.html
  • apps/web/vercel.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/web/public/pair/index.html
  • apps/ios/ADEClip/Info.plist
  • apps/ios/ADE/Services/ClipPairingHandoff.swift
  • apps/ios/ADEClip/ClipPairingView.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 51fad04e34

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

siteId: syncNonEmpty(handoff.siteId),
port: handoff.port,
authKind: "paired",
pairedDeviceId: handoff.deviceId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use the handed-off device id for the first full-app hello

When a fresh full app adopts a clip pairing, this saves the clip's deviceId as pairedDeviceId, but hello() still sends peer.deviceId from SyncService.deviceId, which is independently generated from the full app keychain during init. The desktop paired-auth path rejects any hello where auth.deviceId !== hello.peer.deviceId, so the App Clip handoff scenario always reconnects with mismatched ids and gets auth_failed (then the saved pairing can be forgotten) instead of being adopted. Seed/update the full app's device id from the handoff before reconnecting, or otherwise pair the clip under the same id the full app will advertise.

Useful? React with 👍 / 👎.

@arul28 arul28 closed this Jul 6, 2026
@arul28 arul28 deleted the ade/qr-app-clip-d537f2cb branch July 6, 2026 18:54
@arul28 arul28 restored the ade/qr-app-clip-d537f2cb branch July 6, 2026 23:32
@arul28 arul28 reopened this Jul 6, 2026
arul28 and others added 3 commits July 6, 2026 19:33
New ADEClip target (1.4MB, on-demand-install product embedded in ADE.app/AppClips):
slim pairing-only WS client speaking the v1 sync envelope (pairing_request/
pairing_result), PIN card UI, one-shot credential handoff through the shared
App Group; the full app adopts it on first launch (SyncService
.adoptClipPairingHandoffIfPresent) and connects without re-pairing. Clip pairs
without a DPoP key on purpose — the host's TOFU upgrade adopts the app's
enclave key on its first hello.

Web: /.well-known/apple-app-site-association (appclips) + /pair smart landing
with the apple-itunes-app clip banner; AASA served as application/json.

ASC: registered com.ade.ios.Clip (97ZL5TPJB8) with APP_GROUPS +
ASSOCIATED_DOMAINS via asc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rking

Review round 1 (CodeRabbit): a host that accepts the socket but never answers
pairing_request no longer pins the clip in .pairing — 15s hard deadline racing
the handshake, with the pending receive() unblocked via socket cancel; and the
clip now carries the same NSAllowsLocalNetworking ATS posture as the full app
so ws:// LAN pairing works at runtime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ests

Review round 2 (Copilot): PIN submit/button now require exactly 6 digits
(matching the desktop generator) instead of >=4; /pair and /pair/ rewrite to
the static landing ahead of the extensionless SPA catch-all so the App Clip
banner page always serves; landing copy no longer claims the page shows a QR;
ClipPairingHandoff.consume gains an injectable location/now and a 7-case
XCTest suite (decode gates, one-shot delete, stale rejection).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/ios/ADEClip/Info.plist
Greptile P1: the clip advertised Tailscale direct candidates but its ATS only
allowed local networking, so a tailnet-only pairing route (ws://100.x.x.x or
*.ts.net) would be blocked at runtime. Copies the app's exception domains
(100.64.0.0/10, fd7a:115c:a1e0::/48, ade-sync, ts.net) verbatim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@arul28

arul28 commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0623e1faab

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

siteId: syncNonEmpty(handoff.siteId),
port: handoff.port,
authKind: "paired",
pairedDeviceId: handoff.deviceId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Align the adopted device ID before reconnecting

When a fresh full-app install adopts a clip handoff, this stores the App Clip's paired device id in the profile but leaves SyncService.deviceId as the full app's separately generated id. The paired hello then sends auth.deviceId = handoff.deviceId while currentPeerMetadata() sends peer.deviceId = deviceId, and the host rejects paired hellos when those two ids differ (apps/ade-cli/src/services/sync/syncHostService.ts:4218), so clip-adopted pairings immediately fail to authenticate instead of connecting. Persist the handoff id as the local device id before reconnecting, or make the clip pair with the id the full app will use.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in e1e3209 — the adopter now takes over the clip's deviceId as the install's identity (persisted to keychain + legacy defaults) before reconnecting, so auth.deviceId and peer.deviceId agree at the host check. Gated to fresh installs: if any saved machine already exists under the app's own id, the handoff is dropped instead of rewriting identity out from under existing pairings.

"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.ade.ios.Clip;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Map the App Clip bundle in manual exports

When exporting with the existing apps/ios/ExportOptions.plist, signing is manual and the provisioning profile map still covers only com.ade.ios and com.ade.ios.widgets. Adding this embedded com.ade.ios.Clip product without adding its App Store provisioning profile means any release/export path that uses that plist will fail at archive export when Xcode tries to sign the clip. Add the clip bundle id to that profile map or use the automatic export plist for App Clip builds.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in e1e3209 — ExportOptions.plist now maps com.ade.ios.Clip → 'ADE App Clip App Store' alongside the app/widgets entries (profile to be minted in the signing setup pass; the auto plist needs no change).

DE00988229EE6003DBF8D8FA /* Frameworks */,
7A00A99FE2965CB60C5BE5BF /* Resources */,
CC0000000000000000000001 /* Embed Foundation Extensions */,
AC5000000000000000000001 /* Embed App Clips */,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add the parent-side App Clip entitlement

Once the ADE target embeds an App Clip, the containing app also needs the associated App Clip identifier entitlement for $(AppIdentifierPrefix)com.ade.ios.Clip; this change only adds the child target's parent-application-identifiers, while ADE/ADE.entitlements still lacks com.apple.developer.associated-appclip-app-identifiers. TestFlight/App Store validation can reject or warn on the containing app/clip relationship without that parent-side entitlement, so add it alongside this embed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in e1e3209 — ADE/ADE.entitlements now declares com.apple.developer.associated-appclip-app-identifiers = [$(AppIdentifierPrefix)com.ade.ios.Clip].

…lumbing

Codex round: (P1) the host binds the pairing secret to the clip's deviceId and
rejects paired hellos when auth.deviceId != peer.deviceId — the adopter now
takes over the clip's device id as the install's identity, gated to fresh
installs (any existing saved machine keeps its identity and the handoff is
dropped instead). (P2) manual ExportOptions.plist maps com.ade.ios.Clip to an
App Store profile so archive export doesn't fail on the embedded clip; (P2)
the containing app declares com.apple.developer.associated-appclip-app-identifiers
for TestFlight/App Store validation of the app↔clip relationship.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@arul28

arul28 commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e1e3209167

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = VQ372F39G6;
GENERATE_INFOPLIST_FILE = NO;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Generate or define the App Clip bundle metadata

With GENERATE_INFOPLIST_FILE = NO, the ADEClip target relies entirely on ADEClip/Info.plist; that new plist only defines the display name/App Clip settings and never supplies required bundle keys such as CFBundleIdentifier, CFBundleExecutable, CFBundlePackageType, CFBundleShortVersionString, or CFBundleVersion. The main app and widget plists define these explicitly, so an App Clip built or archived from this target is missing required metadata and will fail install/archive validation; add the standard CFBundle keys or enable generated Info.plist while preserving the App Clip/ATS entries.

Useful? React with 👍 / 👎.

@arul28 arul28 merged commit d3bfcc4 into main Jul 7, 2026
59 of 61 checks passed
@arul28 arul28 deleted the ade/qr-app-clip-d537f2cb branch July 7, 2026 00:32
arul28 added a commit that referenced this pull request Jul 7, 2026
The ADE app embeds an App Clip (com.ade.ios.Clip, PR #706), so a manual-signing
export must provision three bundles, not two. Document the minted
'ADE App Clip App Store' profile (+ re-mint command) and the auto-plist escape
hatch in both AGENTS.md and the release skill's 7c gotchas, so a context-free
release agent doesn't hit a surprise signing failure on the clip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
arul28 added a commit that referenced this pull request Jul 7, 2026
The ADE app embeds an App Clip (com.ade.ios.Clip, PR #706), so a manual-signing
export must provision three bundles, not two. Document the minted
'ADE App Clip App Store' profile (+ re-mint command) and the auto-plist escape
hatch in both AGENTS.md and the release skill's 7c gotchas, so a context-free
release agent doesn't hit a surprise signing failure on the clip.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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