// The watch face complication and Smart Stack widget: what waits on you or what is working, from // `GET /v1/summary`. For the rectangular family it also reads the waiting thread, so the Smart Stack // can offer Allow and Deny. It reads the device token and the demo flag from the shared Keychain // group, and the app reloads it whenever its own feed or a notification changes the counts. import SwiftUI import WidgetKit @main struct WristCodeWidgets: WidgetBundle { var body: some Widget { WaitingWidget() } } struct WaitingWidget: Widget { var body: some WidgetConfiguration { StaticConfiguration(kind: "WaitingCount", provider: WaitingProvider()) { entry in WaitingView(entry: entry) } .configurationDisplayName("Waiting on you") .description("Threads waiting for your approval or answer, or what is working now.") .supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline, .accessoryCorner]) } } struct WaitingProvider: TimelineProvider { /// How long until WidgetKit asks again. The app reloads sooner when it learns something. static let refreshInterval: TimeInterval = 15 * 60 func placeholder(in context: Context) -> WaitingEntry { .sample } func getSnapshot(in context: Context, completion: @escaping @Sendable (WaitingEntry) -> Void) { if context.isPreview { completion(.sample) return } let family = context.family Task { completion(await Self.load(family)) } } func getTimeline(in context: Context, completion: @escaping @Sendable (Timeline) -> Void) { let family = context.family Task { let entry = await Self.load(family) completion(Timeline(entries: [entry], policy: .after(entry.date.addingTimeInterval(Self.refreshInterval)))) } } /// The summary, and for the rectangular family the waiting approval's Allow and Deny. static func load(_ family: WidgetFamily) async -> WaitingEntry { guard let api = SharedState.api else { return WaitingEntry(date: .now, content: .notPaired) } do { let summary = try await api.summary() SharedState.lastSummary = summary let approval = family == .accessoryRectangular ? await WaitingEntry.Approval.load(for: summary, api: api) : nil return WaitingEntry(date: .now, content: .counts(summary, stale: false), approval: approval) } catch GatewayError.unauthorized { return WaitingEntry(date: .now, content: .notPaired) } catch { guard let last = SharedState.lastSummary else { return WaitingEntry(date: .now, content: .unavailable) } return WaitingEntry(date: .now, content: .counts(last, stale: true)) } } } private func entry(_ needsYou: Int, _ working: Int, next: Bool = true) -> WaitingEntry { let thread = Summary.Next(hostId: "mac-mini", threadId: "t-ocr", title: "Receipt OCR", hostLabel: "Mac mini", request: "bun test --filter ocr") let current = Summary.Current( hostId: "mac-mini", threadId: "t-scaffold", title: "Watch app scaffold", activity: Activity(kind: .command, text: "bun test", startedAt: .now.addingTimeInterval(-42))) return WaitingEntry(date: .now, content: .counts( Summary(needsYou: needsYou, working: working, next: needsYou > 0 && next ? thread : nil, current: working > 0 ? current : nil, hostsOffline: 1), stale: false)) } #Preview("Circular", as: .accessoryCircular) { WaitingWidget() } timeline: { entry(2, 3) entry(0, 3) WaitingEntry(date: .now, content: .notPaired) } #Preview("Rectangular", as: .accessoryRectangular) { WaitingWidget() } timeline: { entry(2, 3) entry(0, 3) WaitingEntry(date: .now, content: .notPaired) } #Preview("Inline", as: .accessoryInline) { WaitingWidget() } timeline: { entry(2, 3) } #Preview("Corner", as: .accessoryCorner) { WaitingWidget() } timeline: { entry(2, 3) entry(0, 3) }