Files
Commilitia-Drop/ios/PLAN.md
T
admin eda23f79b1 docs:iOS 客户端实施计划(ios/PLAN.md)
- WKWebView 原生壳复用 web bundle + APNs 推送 + 原生剪贴板 / 分享扩展的架构与里程碑
  I0–I6;明确哪些账号无关可先做、哪些硬等 Apple Developer 账号
- APNs 服务端通道复用本次 Web Push 的「未投递才推 + 按 locale 渲染」模型,设计冻结、
  待 .p8 同期落地
2026-06-18 16:56:52 +08:00

10 KiB
Raw Blame History

cdrop iOS 客户端实施计划

WKWebView 原生壳 · 复用 web/ bundle · APNs 推送 + 原生剪贴板 / 分享 状态:规划中,待 Apple Developer 账号。本文是“账号到位即可开工”的准备文档——把架构、里程碑、以及哪些活现在就能干 / 哪些硬等签名讲清楚。 关联:根 README.mddesktop/PLAN.md(同“原生壳复用同一份 UI”思路的先例)、internal/pushWeb Push 服务端,APNs 通道与之并列)。记忆 cdrop-state / cdrop-deploy-pointers


0. 范围、定位与关键决策

定位:与桌面端同理——iOS 客户端不是“另写一套 UI”,而是“带原生外壳的同一份 UI”。UI 复用 web/src 的 React 应用(WKWebView 加载),价值全在原生层补浏览器在 iOS 上做不到的事。

为什么要原生壳(而非仅靠 PWA:任务 2 的 Web Push 已让 iOS 16.4+ 已安装的 PWA 能收推送,所以“通知”本身不必等原生。原生壳的真正价值在三处 iOS 网页端做不到的能力:

  1. 原生剪贴板iOS 对网页 navigator.clipboard 限制极严(仅用户手势触发、无法后台监听),而 cdrop 的核心是剪贴板同步。原生 UIPasteboard 才能做到顺畅读写。
  2. Share Extension:从任意 app 的分享菜单直接“发送到 cdrop”,网页无法注册。
  3. 后台与通知:原生 APNs + 后台执行窗口,配合通知点击深链回 app。

范围:常驻 iOS 客户端,复用 web UI + 原生层(剪贴板 / 分享 / APNs / 登录态 Keychain)。

非范围(推后)iPad 专属布局、Apple Watch、App Clip、原生 SwiftUI 重写 UI(明确不做——UI 复用 web)、Android。

决策 A:架构 = WKWebView 原生壳 + APNs(已定)

用户在 2026-06-18 的选型中选定WKWebView 原生壳 + APNs(而非 PWA-only,也非全原生 SwiftUI)。理由:复用现有 web UI、与桌面瘦客户端架构一致、工作量中等,同时拿到原生剪贴板 / 分享 / 后台。

决策 B:硬等 Apple Developer 账号

iOS 的真机调试、TestFlight / App Store 分发、APNs 全部要求 Apple Developer Program 成员资格 + 签名证书 + Provisioning Profile。账号到位前只能:写服务端 APNs 通道(账号无关的代码结构)、scaffold Xcode 工程、在模拟器跑 UI(模拟器不收 APNs)。账号到位后才能:真机推送、剪贴板 / 分享扩展真机验证、分发。


1. 与任务 2(Web Push)服务端模型的关系

任务 2 已落地服务端推送底座,iOS 的 APNs 通道复用同一套设计,只换发送后端:

维度 Web Push(已建) iOS APNs(待建)
凭据 自签 VAPID 公私钥 Apple APNs Auth Key.p8 + Key ID + Team ID
订阅令牌 endpoint + p256dh/auth APNs device token(每安装一枚)
存储 push_subscriptions 同表加 platform 列区分,或新 apns_tokens
收件定位 (user_id, device_name) 同上——iOS 设备登记 device_type=ios
触发门控 Hub.SendTo 未投递才推 完全相同——前台 WebView 在线即走 SSE,挂起 / 关闭才 APNs
文案渲染 服务端按订阅 locale 渲染 同上,复用 internal/pushlocalize

关键:“页面打开用应用内提示,否则系统通知”的判定在 iOS 上=app 是否前台。前台时 WebView 的 SSE 在线、Hub.SendTo 投递成功 → 应用内呈现;app 挂起 / 退后台 → SSE 断 → SendTo 失败 → 服务端发 APNs。与 Web 端逻辑同构,只是“在线”的物理含义不同。

⚠️ iOS WebView 退后台后会被系统挂起,SSE 必断。所以 iOS 的后台通知只能靠服务端 APNs不能像桌面那样靠常驻进程里的 JS(桌面 Go 进程常驻、iOS 没有等价物)。这正是必须建 APNs 通道、而非复用桌面 notifyNativeIfBackground 思路的原因。


2. 里程碑

里程碑 内容 账号依赖
I0 账号与证书前置 Apple Developer Program 加入;App IDnet.commilitia.cdrop,与桌面 bundle id 一致或加 .ios 后缀);APNs Auth Key .p8Provisioning Profile 硬等账号
I1 WKWebView 壳骨架 Xcode 工程;WKWebView 加载 https://drop.commilitia.net(或内嵌 web/dist);注入 X-Device-Type: ios;基础生命周期 模拟器可跑;真机等账号
I2 登录态 ASWebAuthenticationSession 跑 OIDC PKCE(系统级安全浏览器,复用 web client);refresh_token 存 iOS Keychain,JS 永不持有(与桌面 Go keyring、web HttpOnly cookie 同策略) 模拟器可跑
I3 APNs 推送(核心) 原生注册 APNs device token → 上报 POST /api/push/apns/register服务端 APNs 发送通道(见 §3);通知点击深链回 app 真机硬等账号
I4 原生剪贴板 UIPasteboard 读(用户手势 / 前台)+ 写;与云剪贴板 LWW 模型对接(复用 /api/clipboard);说明 iOS 后台无法监听的限制 模拟器部分可跑
I5 Share Extension 独立 extension target:从任意 app 分享文件 → 唤起 cdrop 发送流程 真机硬等账号
I6 打磨与分发 后台执行窗口;图标 / 启动屏(复用品牌资产);TestFlight → App Store 硬等账号

3. 服务端 APNs 通道(账号无关的结构,可先设计)

新增 internal/push 的兄弟实现 internal/apns(或 push.Sender 旁加一个 APNs 后端),与 Web Push 并列:

  • APNs 走 HTTP/2 + JWT.p8 的 ES256 签名),可用 github.com/sideshow/apns2 之类,或自手写(与现有 go-jose 一致)。
  • 配置koanfCDROP_APNS_*,全可选——缺失即 iOS 推送惰性关闭,类同 VAPID):APNS_KEY_PATH.p8)、APNS_KEY_IDAPNS_TEAM_IDAPNS_TOPICbundle id)、APNS_ENVsandbox / production)。
  • 存储push_subscriptionsplatform TEXT NOT NULL DEFAULT 'web' 列 + iOS 行存 APNs token(复用 endpoint 字段存 token,或加专列)。SubscriptionID 仍用 token 的哈希保持幂等 upsert。
  • 端点POST /api/push/apns/register {device_token, locale}(鉴权 + X-Device-Name,与 /api/push/subscribe 同组)。
  • 发送:在 transfer:incoming / message / transfer:state(DONE|FAILED) 的“未投递”分支里,按 platform 分流到 Web Push 或 APNs。文案复用 internal/pushlocalize(已支持 zh-CN / zh-TW / en-US)。

这部分代码结构账号无关,但 APNs 发送要 .p8 才能真发 / 真测,故与 iOS 客户端同期做(账号到位后),避免落一坨无法验证的代码。设计先定在此。


4. 复用边界与平台注入

  • UI 全复用 web/srciOS 壳不写业务 UI。
  • 平台判定web 现有 isDesktop()window.runtime + window.go(Wails 注入)。iOS 壳需类似注入一个标识(如 window.__CDROP_IOS__WKScriptMessageHandler 桥),前端加 isIOSShell(),让原生剪贴板 / 分享走桥、Web Push 入口隐藏(iOS 用 APNs 不用 VAPID)。
  • JS↔原生桥WKScriptMessageHandlerJS→Swift+ evaluateJavaScriptSwift→JS),类比 Wails 的 EventsEmit / bound methods。需要的桥方法初稿:requestApnsTokenreadClipboard / writeClipboardsaveFileshowNotification(前台)。
  • 登录回跳ASWebAuthenticationSession 的 callback URL scheme 注册(如 cdrop://oauth/callback),与 web 的 redirect_uri 并存(后端 /api/auth/config 已可按 client 下发;或复用 web client 的 loopback 思路)。

5. 风险与未决

  • R-iOS-1 WebView 后台挂起:app 退后台 SSE 必断 → 后台通知只能靠 APNs(已纳入 I3)。前台恢复时 WebView 需重连 SSEweb startHub 的重连循环已具备)。
  • R-iOS-2 剪贴板后台限制:iOS 不允许后台读剪贴板(隐私);只能前台 / 用户手势。同步语义要据此降级(前台进入时同步一次,而非桌面式实时监听)。iOS 14+ 还有“粘贴需用户确认”横幅。
  • R-iOS-3 远端 vs 内嵌 bundleWKWebView 加载线上 drop.commilitia.net(部署即更新、但离线不可用)vs 内嵌 web/dist(随 app 版本、可离线壳)。倾向内嵌 bundle + API 指向线上(与桌面一致:go:embed 的对应物是 app bundle 里的静态文件),首屏快且可控。
  • R-iOS-4 App ID 与 audienceiOS client 若用独立 OAuth client_id,后端 CDROP_OIDC_AUDIENCE 需纳入(已支持逗号分隔多值);若复用 web client(如桌面),则无需改。倾向复用 web client(最省)。
  • R-iOS-5 审核:App Store 审核对“仅是网页壳”的 app 有顾虑(Guideline 4.2)。原生剪贴板 + 分享扩展 + APNs 提供了足够的原生价值,过审风险可控,但需在审核说明里强调。

6. 现在能做 vs 等账号

账号到位前可做(账号无关)

  • 本计划(✔ 本文)。
  • 服务端 APNs 通道的设计冻结(§3)——代码待 .p8 同期落,避免无法验证的悬空实现。
  • 前端 isIOSShell() 平台注入点的预留(可与 iOS 壳同期,改动小)。

硬等 Apple Developer 账号:Xcode 工程真机签名、APNs 真发真测、Share Extension、TestFlight / App Store 分发。


附:与桌面端的策略对照(一以贯之)

维度 桌面(Wails iOSWKWebView 壳)
UI 复用 web/src 复用 web/src
refresh_token Go keyringAES-GCM iOS Keychain
登录 loopback PKCEGo 内) ASWebAuthenticationSessionPKCE
后台通知 Go 常驻进程 + 原生 API(窗口非前台才弹) 服务端 APNsapp 挂起无常驻进程)
剪贴板 Go 原生 Monitor 双向实时 UIPasteboard,前台 / 手势(后台受限)
平台判定 isDesktop()window.runtime isIOSShell()(注入标识)