Read App Intents Documentation
Nucleic-Session: 56C5F781-4444-466B-91CE-A7734A8E117F Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import AppIntents
|
||||
#if canImport(NucleicProtocol)
|
||||
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:)`. The widget extension links no `NucleicProtocol`, so this intent carries plain
|
||||
/// `String` ids and compiles its real work only into the app (`#if canImport(NucleicProtocol)`).
|
||||
/// 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.
|
||||
///
|
||||
/// 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 canImport(NucleicProtocol)
|
||||
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
|
||||
}
|
||||
// Best-effort bring-up; `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()
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user