Skip to content

依 OPass Push Gateway v1 契約整合 FCM topic 推播 #68

Description

@denny0223

OPass 已採納中央 Push Gateway v1 與 Firebase Cloud Messaging(FCM)topic 推播契約。本 issue 追蹤 CCIP-iOS 的實作範圍與驗收;未勾選的項目仍待完成並附上對應 revision 的驗證結果。架構採納、實作完成與目標環境驗收分別記錄;部署、雲端設定與真實派送須另行取得授權。

架構摘要

CCIP-Admin-Bueno -> OPass Push Gateway -> FCM topic -> CCIP-Android / CCIP-iOS
  • Gateway 由 OPass 團隊維運;活動由驗證成功的 Gateway key 決定,呼叫端不能指定 EVENT_ID 或完整 topic。
  • CCIP-Server 不取得 Gateway key,也不參與推播發送。
  • 推播內容一定是公開資訊;不建立 device registry,也不逐一向裝置發送。
  • App 只有在活動登入成功後才訂閱;每個已登入活動各保留一個角色/推播語系 topic,切換目前活動不會取消其他活動的訂閱。

實作依據

  • 已採納的共同契約:ADR 0001、OpenAPI。
  • 內容保存與部署約束:ADR 0002;Gateway 在中央 D1 保存公開推播內容與已知派送結果,活動 key 對應仍由中央 secret 管理。運算、內容保存及原生成效交付均不得要求綁定有效付款方式。
  • 跨專案驗收與環境輸入:測試與發布驗收;中央設定及 CSV 交付:操作文件。
  • 程式碼檢視基準:11edb506834e7772fbb74607822df202b06ea2b9

若實作需要改變 topic、payload 或跨 repository 責任,應先更新 Gateway 契約,再調整本 issue。

現況與風險

  • OPassApp.swift 在 App 啟動時初始化 OneSignal,並立即要求通知權限。
  • EventStore.redeem 只有在身分驗證成功後才加入 <EVENT_ID><ROLE> tag,符合「登入成功後才訂閱」的產品規則。
  • 登入新身分時,只有現有 tag 數量至少為 2 才全部移除;恰好有一個舊 tag 時會留下舊身分。signOut 也只是把 tag value 設為空字串。改成每個 FCM role topic 各送一則後,殘留訂閱可能造成重複通知。
  • iOS 已有登出功能;目前活動、token 與 role 會分別儲存在 NSUbiquitousKeyValueStore、可同步 Keychain 與 per-event UserDefaults。每個活動的 FCM topic 對應是單一裝置的狀態,不得跟著 iCloud 或 Keychain 同步到其他裝置。
  • 切換活動時可以回到過去已登入的活動,因此切換目前活動不得取消先前活動的訂閱。
  • 專案已連接與 Android 相同的 Firebase 專案 opass-8b7db,但 App target 尚未加入 Firebase Messaging product。既有 SDK 已提供 Analytics、App Check、Crashlytics 與 Performance。
  • OneSignal 另有 Swift Package、Notification Service Extension target、App Group entitlement 與 Diagnostic 畫面。Apple alert delivery export 仍需最小 FCM service extension,不能因不需要 rich media 就刪除所有 extension。
  • EventStore.loadAttendee 也會更新角色,且失敗時可能回退快取;只有成功向活動服務驗證的結果才能建立新的已驗證身分或角色,不能把另一裝置的 token/快取當成本安裝登入成功。

1. Firebase Messaging 與 OneSignal 清理

  • 從現有 Firebase Apple SDK package 加入 Firebase Messaging product,並將 Package.resolved 的 12.17.0 更新為包含 FID 相關修正的穩定相容版本:12.18.0 修復既有 token cache 阻止 FID registration,12.19.0 修復啟用 FID 後語系變更誤判 registration 失效。依官方 release notes 核對後續修正及工具鏈需求,提交實際解析的版本;不建立第二套 Firebase 設定。
  • 在 Firebase Console 確認 app.opass.ccip 的 APNs authentication key 可供開發與正式環境使用。
  • 移除 OneSignal 初始化、Swift Package product、import 與 OneSignal-XCFramework package reference。
  • 移除 extension 中的 OneSignal SDK 與專屬實作,將既有 target 調整為最小 FCM Notification Service Extension;保留正常通知顯示與到期 completion,不加入 rich media 功能。
  • 重新確認主 App 與最小 FCM extension 的 entitlement 需求;只移除已無用途的 OneSignal App Group 設定,不因移除 OneSignal 而一併刪掉仍需的簽章或 embed 設定。
  • 保留既有 remote-notification background mode 與必要的 FCM callback,但 alert delivery export 必須在 service extension 執行,不用主 App 背景 callback 取代。
  • 移除 Diagnostic 畫面的 OneSignal ID,不改成顯示或上傳 FCM registration/FID。

2. APNs、FCM registration 與通知權限

  • 讓既有 SwiftUI AppDelegate 同時擔任 UNUserNotificationCenterDelegate 與 Firebase Messaging delegate。
  • 在 App 的 Info.plist 設定 FirebaseMessagingInstallationIdEnabled = YES,採用 Firebase Messaging 目前的 FID registration 流程,不沿用已棄用的 registration token API。
  • 設定兩個 delegate、呼叫 registerForRemoteNotifications(),並在 APNs registration callback 明確把 APNs token 交給 Firebase Messaging;SwiftUI App 不依賴隱含 swizzling 完成這一步。
  • 在 MessagingDelegate.messaging(_:didReceiveRegistration:) 收到成功的 FID registration 時觸發 topic 同步,但不得把 registration、FID 或 APNs token 上傳到 Gateway 或寫入 log。
  • 先維持現行行為:App 啟動時要求通知權限。若團隊要改成登入成功後才詢問,另作產品決策,不與「登入成功後才訂閱 topic」混為一談。
  • 使用者拒絕通知權限時仍維持正確 topic 訂閱,讓日後在系統設定開啟權限後可直接收訊。

3. Topic 推導與訂閱同步

  • 建立單一、小型的 topic manager,集中處理 topic 推導、訂閱與取消訂閱;不建立自製 queue 或 device registry。
  • 驗證 EVENT_ID 與 ROLE 均符合 [A-Za-z0-9_-]{1,64},並拒絕角色 all。
  • 保留既有 App localization,依實際採用的介面語言推導推播語系。中文 zh 及其延伸標籤、nan-Hant-* 與 nan-Latn-* 使用正體中文 zh-Hant,其他使用英文 en;本 repo 的既有 nan 是漢字台語資源識別,在推播語系轉換時視同 nan-Hant。
  • 依活動分開保存已驗證登入/角色與本安裝的 topic 套用狀態、待完成轉換;套用狀態不得進入 iCloud/synchronizable Keychain,也不能直接沿用備份還原的 UserDefaults。新安裝應在驗證登入後重新訂閱,不能只以「不同步」推定不會還原。
  • 根據所有已驗證登入活動的角色推導目標 topic;跨裝置 token/快取角色先向對應活動服務驗證並確認活動 ID,再新增訂閱。明確登出或登入失效才移除該活動;Keychain 讀取失敗、離線或伺服器暫時錯誤不等於登出。僅在本安裝已確認套用且沒有待完成轉換時,才略過相同目標。
  • redeem、loadAttendee 與同步 token 驗證須綁定發起時的活動與登入身分版本;套用成功或失效結果前,確認身分未被登出、重新登入或新同步 token 取代。檢查與狀態更新須在同一主執行緒操作內完成;過期回應不得更新 attendee/role、清除新憑證或觸發訂閱,不能只序列化 SDK 操作。
  • 序列化同一活動的 SDK 操作;呼叫前先持久化待完成轉換,取消舊 topic 後才訂閱新 topic,成功後更新已套用狀態。中斷或目標再次變更後,可清理可能已訂閱但未確認寫入本機的 topic,再收斂到當前目標。
  • 在 Firebase Messaging 已完成 registration 後,從主執行緒呼叫 topic API;SDK 的持久化重試交給 Firebase Messaging,不另做 background worker。

至少在下列時機同步訂閱:

  • EventStore.redeem 驗證身分並儲存新 attendee、token 與 role 後。
  • EventStore.loadAttendee 成功驗證並更新角色後;快取 fallback 不建立新的已驗證登入,也不以暫時讀取失敗清除原訂閱。
  • iCloud/Keychain token 更新後重新驗證,以及活動服務明確判定登入失效時;不能將所有 403 直接當成 token 失效。
  • EventStore.signOut 清除登入資料時,只移除該活動的 topic。
  • App 啟動並完成 OPassStore.loadEvent 後,核對所有已登入活動;切換目前活動本身不得取消其他活動的 topic。
  • Firebase Messaging registration/FID 更新時,核對所有已登入活動。
  • App 語言變更後重新啟動時,更新所有已登入活動的 topic。

4. 前景顯示與通知點擊

  • 在 UNUserNotificationCenterDelegate 的前景 callback 顯示 list、banner 與預設提示音。
  • 在通知點擊 callback 只處理同時包含 push_id 與 event_id 的 Gateway 通知。
  • uri 是 HTTPS 時交由系統開啟;沒有 uri 時,切換至 event_id 對應的活動,再沿用現有 Router 與 FeatureDestinations.announcement 進入公告頁。
  • 使用原生 NotificationCenter 或既有狀態傳遞通知點擊,不新增第二套 navigation framework。
  • 使用 Gateway 提供的活動主辦名稱標題;不由 App 或 Gateway 管理、累加 badge,不因一小時到期移除已顯示通知。

5. Analytics 與驗證

統計蒐集的預設、詢問時機、撤回方式及與既有 Analytics 設定的關係暫緩定案,先完成不依賴此選擇的功能。Analytics/delivery export 的使用者選擇流程及依該政策進行的成效驗收,待產品決策定案後完成;不得從 SDK 或舊程式的預設推定使用者同意。這項前置決策不阻擋 Gateway、Admin、基本通知、訂閱與導頁的獨立開發;拒絕或撤回統計不得影響通知使用。

  • 保留既有 Firebase Analytics,配合中央確認 FCM reporting 所需 data sharing 與 BigQuery 連結;資料蒐集遵守隱私告知與使用者選擇,不影響拒絕 Analytics 使用者的通知功能。
  • 在最小 Notification Service Extension 的 alert 接收路徑呼叫 Firebase service extension delivery export API,確認 Gateway 的 apns.payload.aps.mutable-content 為 1;正常完成 content handler,失敗也不妨礙原通知顯示。不得把 token 或 FID 寫入 log。
  • 配合中央以 push_id 確認 Apple 送達與背景通知開啟數可匯出;Sends 不冒充送達,未取得的資料不補零,訊息/安裝實例不冒充自然人人數。CSV 由中央依 ADR 的保存與匯出流程交付,App 不新增報表或轉化追蹤。
  • 依官方 SwiftUI delegate 路徑,讓 UNUserNotificationCenterDelegate 的收到/點擊 callback 在允許統計時手動呼叫 Messaging.messaging().appDidReceiveMessage(userInfo);不能以未停用 swizzling 推定 SwiftUI 已自動傳遞 Analytics。APNs token 仍依前述 callback 明確交給 Messaging;驗證前景與背景點擊不漏報、不重複計數,不新增自製統計事件。
  • 為語系對應、topic 建構與訂閱同步的狀態轉換加入最小單元測試。
  • 在實體裝置驗證開發與正式 APNs 環境;模擬器結果不能取代實體裝置驗收。
  • 用 Xcode 建置 App 與保留的最小 FCM extension,確認 archive、簽章與 embed 正確,且不再連結 OneSignal。

人工驗收:

  • 未登入、首次登入、相同身分再次登入、切換身分與登出。
  • 跨裝置同步 token 尚未驗證、Keychain 暫時讀取失敗、attendee 角色更新與快取 fallback。
  • 舊身分的 loadAttendee/驗證回應晚於新身分登入,成功或失效都不得改寫新角色或 topic;登出後的舊回應不得恢復訂閱,登出再登入相同 token 也不得接受前一輪回應。
  • SDK 成功但本機寫入前中斷、恢復後再切角色;備份還原的新安裝驗證後重新建立訂閱。
  • 從既有 OneSignal 版本升級,覆蓋已存在 Firebase registration cache、重啟、FID 更新與切換語系;確認仍會完成 FID registration 並為所有已驗證活動建立正確訂閱,不只測乾淨安裝。
  • 切換活動及切回曾經登入的活動,確認其他已登入活動仍可收訊。
  • 既有 zh-Hant 與 nan localization 使用 zh-Hant 推播;其他介面語言使用英文 fallback。
  • 切換語言後更新所有活動;同活動切換身分後不會再收到該活動舊角色 topic 的推播;切換活動則保留其他活動的訂閱。
  • App 在前景、背景與被終止時,有 uri、無 uri 的點擊結果都正確。
  • 收到非目前活動且沒有 uri 的通知時,點擊後開啟 event_id 對應活動的公告頁。
  • 拒絕通知權限不會破壞 topic 狀態;重新開啟權限後可以收訊。
  • Firebase Console 與 BigQuery 顯示的 Apple Sends、Opens 與 delivery metrics 符合契約所述限制。

不在本 issue 範圍

  • registration、FID 或 APNs token 上傳 API。
  • App 內的 Gateway key、service account credential 或發布功能。
  • .all topic、逐裝置發送、OneSignal 相容層、UnifiedPush 或 rich media extension 功能;最小 FCM delivery-metrics extension 明確屬於本 issue。
  • 補送流程、App 內報表、表單或現場轉化追蹤。
  • 為這次遷移重構登入資料、iCloud 同步或整套 navigation architecture。

參考資料

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions