import AppIntents #if NUCLEIC_APP import NucleicProtocol #endif /// The interactive approve / deny an aggregate Live Activity (or widget) button fires — the flagship /// "resolve from the lock screen without unlocking" path (docs/APP_INTENTS_OPPORTUNITIES §4.1). /// /// It lives in the **Shared** group so both the app and the widget extension can reference it in /// `Button(intent:)`. It carries plain `String` ids and compiles its real work only into the app, /// gated on `#if NUCLEIC_APP` (a custom compilation condition set on the app target only). That's /// sound because iOS runs a widget/Live-Activity button's intent in the **app's background /// process** — where `RemoteStore` owns the live E2EE channel — never in the extension. The /// extension-side copy exists solely to satisfy the `Button(intent:)` type reference. /// /// NB: the guard is `#if NUCLEIC_APP`, *not* `#if canImport(NucleicProtocol)`. `canImport` tests /// module findability, not linkage — and because the app builds `NucleicProtocol` into the shared /// DerivedData products dir, it's findable from the widget extension too. So `canImport` is `true` /// in the extension, which would compile this branch there and fail on the app-only `RemoteStore` /// / `IntentError` types. `NUCLEIC_APP` tracks target membership, which is what we actually mean. /// /// Not discoverable in Shortcuts/Spotlight: it's button-only, driven by ids embedded at render time /// (a human uses `AnswerApprovalIntent` for the spoken/Shortcuts path). struct ApproveFromActivityIntent: AppIntent { static let title: LocalizedStringResource = "Approve from Live Activity" static let isDiscoverable = false @Parameter(title: "Approval ID") var approvalID: String @Parameter(title: "Session ID") var sessionID: String @Parameter(title: "High Risk") var isHighRisk: Bool /// `true` = allow, `false` = deny. @Parameter(title: "Allow") var allow: Bool init() {} init(approvalID: String, sessionID: String, isHighRisk: Bool, allow: Bool) { self.approvalID = approvalID self.sessionID = sessionID self.isHighRisk = isHighRisk self.allow = allow } @MainActor func perform() async throws -> some IntentResult { #if NUCLEIC_APP let store = RemoteStore.shared // §3.3 — a high-risk allow is never resolved inline; route to the app's biometric-gated card. // (Surfaces shouldn't render an inline Allow for high-risk in the first place; this is the // backstop so the intent can never be a softer path than the UI.) if isHighRisk, allow { store.route(to: SessionID(rawValue: sessionID)) throw IntentError.needsAppConfirmation } // Cloud lock-screen approvals are opt-in (SyncedSettings.resolveApprovalsViaCloud). When // off, don't push the decision over Nucleic's relay — hand off to the app to resolve it // locally (the foreground app can use the local network). Read the flag from the local // cache the sync layer keeps current, so a cold background launch decides without a host. guard SyncedSettings.resolveApprovalsViaCloud() else { store.route(to: SessionID(rawValue: sessionID)) throw IntentError.resolveInApp } // On: resolve inline over the Nucleic Edge. Dial relay-first with a longer budget — a // backgrounded/locked intent can't rely on LAN, and the relay handshake (mint token + // WebSocket upgrade + Noise) needs more time than a direct dial. `respondToApproval` // queues briefly on a dropped link (§3.1); a lost first-responder race is de-duped // host-side (§3.4), so we never surface an error for it. _ = await store.awaitLiveConnection(timeout: 12, preferRelay: true) let decision: Decision = allow ? .allow(updatedInput: nil) : .deny(reason: nil) store.respondToApproval( id: ApprovalID(rawValue: approvalID), sessionID: SessionID(rawValue: sessionID), decision: decision) return .result() #else // Widget-extension build: never executed (the system performs this in the app process). return .result() #endif } }