nvrsion: Add: Align streak card lightning bolt to left side of card, remove streak freeze counter, fix push notification wording to reflect correct session status, adjust push notification logic to recall only the approval request’s notification without affecting live activities, and investigate Live Activity Session Load.
Nucleic-Promote: 1 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