Files
OSGKeyboard/OSGKeyboardShared/Services/HostAppURLRegistry.swift
T
Cursor Agent 13cd7b30f9 feat(flow): implement ABCD session policy and host return whitelist
- Scheme A: on-demand session start, inactivity-based expiry, handoff auto-recording
- Scheme B: cold-start overlay with swipe guidance and return alert
- Scheme C+D: HostAppURLRegistry (20 apps) and sourceApplication capture
- Settings: skip app switch toggle and inactivity duration picker
- Add LSApplicationQueriesSchemes for canOpenURL checks

Co-authored-by: Rocky <hkgood@users.noreply.github.com>
2026-07-06 07:38:26 +00:00

183 lines
5.6 KiB
Swift

// HostAppURLRegistry.swift
// OSGKeyboard · Shared
//
// Public URL-scheme whitelist for returning to a known host app after a
// cold-start Flow session handoff. One bundle id maps to one preferred URL.
import Foundation
public struct HostAppEntry: Sendable, Equatable {
public let bundleId: String
public let displayNameKey: String
public let returnURLString: String
public let tier: Int
public var returnURL: URL? {
URL(string: returnURLString)
}
public init(bundleId: String, displayNameKey: String, returnURLString: String, tier: Int) {
self.bundleId = bundleId
self.displayNameKey = displayNameKey
self.returnURLString = returnURLString
self.tier = tier
}
}
public enum HostAppURLRegistry {
/// Curated whitelist for high-frequency host apps (IM, work, notes).
public static let entries: [HostAppEntry] = [
// Tier 1 — China IM / work
HostAppEntry(
bundleId: "com.tencent.xin",
displayNameKey: "hostApp.wechat",
returnURLString: "weixin://",
tier: 1
),
HostAppEntry(
bundleId: "com.tencent.mqq",
displayNameKey: "hostApp.qq",
returnURLString: "mqq://",
tier: 1
),
HostAppEntry(
bundleId: "com.tencent.wework",
displayNameKey: "hostApp.wecom",
returnURLString: "wxwork://",
tier: 1
),
HostAppEntry(
bundleId: "com.laiwang.DingTalk",
displayNameKey: "hostApp.dingtalk",
returnURLString: "dingtalk://",
tier: 1
),
HostAppEntry(
bundleId: "com.bytedance.ee.lark",
displayNameKey: "hostApp.lark",
returnURLString: "lark://",
tier: 1
),
// Tier 2 — global IM / collaboration
HostAppEntry(
bundleId: "ph.telegra.Telegraph",
displayNameKey: "hostApp.telegram",
returnURLString: "tg://",
tier: 2
),
HostAppEntry(
bundleId: "net.whatsapp.WhatsApp",
displayNameKey: "hostApp.whatsapp",
returnURLString: "whatsapp://",
tier: 2
),
HostAppEntry(
bundleId: "jp.naver.line",
displayNameKey: "hostApp.line",
returnURLString: "line://",
tier: 2
),
HostAppEntry(
bundleId: "com.facebook.Messenger",
displayNameKey: "hostApp.messenger",
returnURLString: "fb-messenger://",
tier: 2
),
HostAppEntry(
bundleId: "com.tinyspeck.chatlyio",
displayNameKey: "hostApp.slack",
returnURLString: "slack://",
tier: 2
),
HostAppEntry(
bundleId: "com.microsoft.skype.teams",
displayNameKey: "hostApp.teams",
returnURLString: "msteams://",
tier: 2
),
HostAppEntry(
bundleId: "com.hammerandchisel.discord",
displayNameKey: "hostApp.discord",
returnURLString: "discord://",
tier: 2
),
// Tier 3 — notes / mail / browser
HostAppEntry(
bundleId: "notion.id",
displayNameKey: "hostApp.notion",
returnURLString: "notion://",
tier: 3
),
HostAppEntry(
bundleId: "net.shinyfrog.bear",
displayNameKey: "hostApp.bear",
returnURLString: "bear://",
tier: 3
),
HostAppEntry(
bundleId: "md.obsidian",
displayNameKey: "hostApp.obsidian",
returnURLString: "obsidian://",
tier: 3
),
HostAppEntry(
bundleId: "com.agiletortoise.Drafts5",
displayNameKey: "hostApp.drafts",
returnURLString: "drafts5://",
tier: 3
),
HostAppEntry(
bundleId: "com.google.Gmail",
displayNameKey: "hostApp.gmail",
returnURLString: "googlegmail://",
tier: 3
),
HostAppEntry(
bundleId: "com.microsoft.Office.Outlook",
displayNameKey: "hostApp.outlook",
returnURLString: "ms-outlook://",
tier: 3
),
HostAppEntry(
bundleId: "com.google.chrome.ios",
displayNameKey: "hostApp.chrome",
returnURLString: "googlechrome://",
tier: 3
),
// Tier 4 — China social
HostAppEntry(
bundleId: "com.sina.weibo",
displayNameKey: "hostApp.weibo",
returnURLString: "sinaweibo://",
tier: 4
),
HostAppEntry(
bundleId: "com.xingin.discover",
displayNameKey: "hostApp.xiaohongshu",
returnURLString: "xhsdiscover://",
tier: 4
)
]
private static let byBundleId: [String: HostAppEntry] = {
Dictionary(uniqueKeysWithValues: entries.map { ($0.bundleId, $0) })
}()
public static func lookup(bundleId: String?) -> HostAppEntry? {
guard let bundleId, !bundleId.isEmpty else { return nil }
return byBundleId[bundleId]
}
/// URL schemes declared in `LSApplicationQueriesSchemes` for `canOpenURL`.
public static var querySchemes: [String] {
Array(
Set(
entries.compactMap { entry -> String? in
guard let url = entry.returnURL, let scheme = url.scheme else { return nil }
return scheme
}
)
).sorted()
}
}