import SwiftUI import WidgetKit #if os(iOS) import ActivityKit import AppIntents import UIKit #endif struct ApprovalWidgetEntry: TimelineEntry { let date: Date let pendingCount: Int let topPayload: ApprovalActivityPayload? } struct ApprovalWidgetProvider: TimelineProvider { func placeholder(in context: Context) -> ApprovalWidgetEntry { ApprovalWidgetEntry( date: .now, pendingCount: 2, topPayload: ApprovalActivityPayload( requestID: UUID().uuidString, title: "github.com wants to sign in", appName: "github.com", source: "github.com", handle: "@jurgen", location: "Berlin, DE", createdAt: .now ) ) } func getSnapshot(in context: Context, completion: @escaping (ApprovalWidgetEntry) -> Void) { completion(makeEntry()) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { let entry = makeEntry() completion(Timeline(entries: [entry], policy: .after(.now.addingTimeInterval(60)))) } private func makeEntry() -> ApprovalWidgetEntry { let state = UserDefaultsAppStateStore().load() let pendingRequests = (state?.requests ?? []) .filter { $0.status == .pending } .sorted { $0.createdAt > $1.createdAt } let handle = state?.profile.handle ?? "@you" return ApprovalWidgetEntry( date: .now, pendingCount: pendingRequests.count, topPayload: pendingRequests.first?.activityPayload(handle: handle) ) } } @main struct IDPGlobalWidgetsBundle: WidgetBundle { var body: some Widget { #if os(iOS) ApprovalLiveActivityWidget() #endif #if os(watchOS) ApprovalAccessoryRectangularWidget() ApprovalAccessoryCircularWidget() ApprovalAccessoryCornerWidget() #endif } } #if os(iOS) struct ApproveLiveActivityIntent: LiveActivityIntent { static var title: LocalizedStringResource = "Approve" static var openAppWhenRun = false @Parameter(title: "Request ID") var requestID: String init() {} init(requestID: String) { self.requestID = requestID } func perform() async throws -> some IntentResult { guard let id = UUID(uuidString: requestID) else { return .result() } _ = try? await MockIDPService.shared.approveRequest(id: id) await ApprovalLiveActivityActionHandler.complete(requestID: requestID, outcome: "Approved") WidgetCenter.shared.reloadAllTimelines() return .result() } } struct DenyLiveActivityIntent: LiveActivityIntent { static var title: LocalizedStringResource = "Deny" static var openAppWhenRun = false @Parameter(title: "Request ID") var requestID: String init() {} init(requestID: String) { self.requestID = requestID } func perform() async throws -> some IntentResult { guard let id = UUID(uuidString: requestID) else { return .result() } _ = try? await MockIDPService.shared.rejectRequest(id: id) await ApprovalLiveActivityActionHandler.complete(requestID: requestID, outcome: "Denied") WidgetCenter.shared.reloadAllTimelines() return .result() } } private enum ApprovalLiveActivityActionHandler { static func complete(requestID: String, outcome: String) async { guard let activity = Activity.activities.first(where: { $0.attributes.requestID == requestID }) else { return } let state = ApprovalActivityAttributes.ContentState( requestID: requestID, title: outcome, appName: activity.content.state.appName, source: activity.content.state.source, handle: activity.content.state.handle, location: activity.content.state.location ) await activity.end(ActivityContent(state: state, staleDate: .now), dismissalPolicy: .immediate) } } struct ApprovalLiveActivityWidget: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: ApprovalActivityAttributes.self) { context in VStack(alignment: .leading, spacing: 12) { Text(context.state.title) .font(.headline) Text("Sign in as \(Text(context.state.handle).foregroundStyle(.purple))") .font(.subheadline) Text(context.state.location) .font(.caption) .foregroundStyle(.secondary) HStack(spacing: 10) { Button(intent: DenyLiveActivityIntent(requestID: context.state.requestID)) { Text("Deny") } .buttonStyle(.bordered) Button(intent: ApproveLiveActivityIntent(requestID: context.state.requestID)) { Text("Approve") } .buttonStyle(.borderedProminent) .tint(.purple) } } .padding(16) .activityBackgroundTint(Color(uiColor: .systemBackground)) .activitySystemActionForegroundColor(.purple) } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { Image(systemName: "shield.lefthalf.filled") .foregroundStyle(.purple) } DynamicIslandExpandedRegion(.trailing) { MonogramBubble(title: context.state.appName) } DynamicIslandExpandedRegion(.center) { VStack(alignment: .leading, spacing: 4) { Text(context.state.title) .font(.headline) Text(context.state.handle) .font(.caption) .foregroundStyle(.secondary) } } DynamicIslandExpandedRegion(.bottom) { HStack(spacing: 10) { Button(intent: DenyLiveActivityIntent(requestID: context.state.requestID)) { Text("Deny") } .buttonStyle(.bordered) Button(intent: ApproveLiveActivityIntent(requestID: context.state.requestID)) { Text("Approve") } .buttonStyle(.borderedProminent) .tint(.purple) } } } compactLeading: { Image(systemName: "shield.lefthalf.filled") .foregroundStyle(.purple) } compactTrailing: { MonogramBubble(title: context.state.appName) } minimal: { Image(systemName: "shield") .foregroundStyle(.purple) } } } } private struct MonogramBubble: View { let title: String private var letter: String { String(title.trimmingCharacters(in: .whitespacesAndNewlines).first ?? "I").uppercased() } var body: some View { ZStack { Circle() .fill(Color.purple.opacity(0.18)) Text(letter) .font(.caption.weight(.bold)) .foregroundStyle(.purple) } .frame(width: 24, height: 24) } } #endif #if os(watchOS) struct ApprovalAccessoryRectangularWidget: Widget { var body: some WidgetConfiguration { StaticConfiguration(kind: "IDPGlobalAccessoryRectangular", provider: ApprovalWidgetProvider()) { entry in VStack(alignment: .leading, spacing: 3) { Label("idp.global", systemImage: "shield.lefthalf.filled") .font(.caption2) Text("\(entry.pendingCount) requests") .font(.headline) Text(entry.topPayload?.appName ?? "Inbox") .font(.caption2) .foregroundStyle(.secondary) } .widgetURL(URL(string: "idpglobal://inbox")) } .configurationDisplayName("Approval Queue") .description("Pending sign-in requests.") .supportedFamilies([.accessoryRectangular]) } } struct ApprovalAccessoryCircularWidget: Widget { var body: some WidgetConfiguration { StaticConfiguration(kind: "IDPGlobalAccessoryCircular", provider: ApprovalWidgetProvider()) { entry in ZStack { AccessoryWidgetBackground() VStack(spacing: 2) { Image(systemName: "shield.lefthalf.filled") Text("\(entry.pendingCount)") .font(.caption2.weight(.bold)) } } .widgetURL(URL(string: "idpglobal://inbox")) } .configurationDisplayName("Approval Count") .supportedFamilies([.accessoryCircular]) } } struct ApprovalAccessoryCornerWidget: Widget { var body: some WidgetConfiguration { StaticConfiguration(kind: "IDPGlobalAccessoryCorner", provider: ApprovalWidgetProvider()) { entry in Text("\(entry.pendingCount)") .widgetCurvesContent() .widgetLabel { Image(systemName: "shield.lefthalf.filled") } .widgetURL(URL(string: "idpglobal://inbox")) } .configurationDisplayName("Approval Corner") .supportedFamilies([.accessoryCorner]) } } #endif