Bring the SwiftUI app in line with the Apple-native mock and keep pending approvals actionable from Live Activities and watch complications.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import SwiftUI
|
||||
|
||||
struct PrimaryActionStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
PrimaryActionBody(configuration: configuration)
|
||||
}
|
||||
|
||||
private struct PrimaryActionBody: View {
|
||||
let configuration: Configuration
|
||||
@Environment(\.isEnabled) private var isEnabled
|
||||
|
||||
var body: some View {
|
||||
configuration.label
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.vertical, 14)
|
||||
.foregroundStyle(.white)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: IdP.controlRadius, style: .continuous)
|
||||
.fill(isEnabled ? IdP.tint : Color.secondary.opacity(0.25))
|
||||
)
|
||||
.opacity(configuration.isPressed ? 0.92 : 1)
|
||||
.scaleEffect(configuration.isPressed ? 0.985 : 1)
|
||||
.animation(.easeOut(duration: 0.16), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SecondaryActionStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.vertical, 14)
|
||||
.foregroundStyle(.primary)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: IdP.controlRadius, style: .continuous)
|
||||
.fill(Color.idpSecondaryGroupedBackground)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: IdP.controlRadius, style: .continuous)
|
||||
.stroke(Color.idpSeparator.opacity(0.55), lineWidth: 1)
|
||||
)
|
||||
.opacity(configuration.isPressed ? 0.9 : 1)
|
||||
.scaleEffect(configuration.isPressed ? 0.985 : 1)
|
||||
.animation(.easeOut(duration: 0.16), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
||||
struct DestructiveStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.vertical, 14)
|
||||
.foregroundStyle(.red)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: IdP.controlRadius, style: .continuous)
|
||||
.fill(Color.red.opacity(0.10))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: IdP.controlRadius, style: .continuous)
|
||||
.stroke(Color.red.opacity(0.18), lineWidth: 1)
|
||||
)
|
||||
.opacity(configuration.isPressed ? 0.9 : 1)
|
||||
.scaleEffect(configuration.isPressed ? 0.985 : 1)
|
||||
.animation(.easeOut(duration: 0.16), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ApprovalCardModifier: ViewModifier {
|
||||
var highlighted = false
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.padding(18)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: IdP.cardRadius, style: .continuous)
|
||||
.fill(Color.idpSecondaryGroupedBackground)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: IdP.cardRadius, style: .continuous)
|
||||
.stroke(highlighted ? IdP.tint.opacity(0.7) : Color.idpSeparator.opacity(0.55), lineWidth: highlighted ? 1.5 : 1)
|
||||
)
|
||||
.overlay {
|
||||
if highlighted {
|
||||
RoundedRectangle(cornerRadius: IdP.cardRadius, style: .continuous)
|
||||
.stroke(IdP.tint.opacity(0.12), lineWidth: 6)
|
||||
.padding(-2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func approvalCard(highlighted: Bool = false) -> some View {
|
||||
modifier(ApprovalCardModifier(highlighted: highlighted))
|
||||
}
|
||||
|
||||
func deviceRowStyle() -> some View {
|
||||
modifier(DeviceRowStyle())
|
||||
}
|
||||
}
|
||||
|
||||
struct RequestHeroCard: View {
|
||||
let request: ApprovalRequest
|
||||
let handle: String
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 16) {
|
||||
MonogramAvatar(title: request.source, size: 64)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("\(request.source) wants to sign in as you")
|
||||
.font(.title3.weight(.semibold))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text("Continue as \(Text(handle).foregroundStyle(IdP.tint))")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Label(request.kind.title, systemImage: request.kind.systemImage)
|
||||
Text(request.createdAt, style: .relative)
|
||||
}
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.approvalCard(highlighted: true)
|
||||
}
|
||||
}
|
||||
|
||||
struct MonogramAvatar: View {
|
||||
let title: String
|
||||
var size: CGFloat = 40
|
||||
var tint: Color = IdP.tint
|
||||
|
||||
private var monogram: String {
|
||||
String(title.trimmingCharacters(in: .whitespacesAndNewlines).first ?? "I").uppercased()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: size * 0.34, style: .continuous)
|
||||
.fill(tint.opacity(0.14))
|
||||
|
||||
Image("AppMonogram")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: size * 0.44, height: size * 0.44)
|
||||
.opacity(0.18)
|
||||
|
||||
Text(monogram)
|
||||
.font(.system(size: size * 0.42, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(tint)
|
||||
}
|
||||
.frame(width: size, height: size)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
struct DeviceRowStyle: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import SwiftUI
|
||||
|
||||
public extension View {
|
||||
@ViewBuilder
|
||||
func idpGlassChrome() -> some View {
|
||||
if #available(iOS 26, macOS 26, *) {
|
||||
self.glassEffect(.regular)
|
||||
} else {
|
||||
self.background(.regularMaterial)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct IdPGlassCapsule<Content: View>: View {
|
||||
let padding: EdgeInsets
|
||||
let content: Content
|
||||
|
||||
init(
|
||||
padding: EdgeInsets = EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16),
|
||||
@ViewBuilder content: () -> Content
|
||||
) {
|
||||
self.padding = padding
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
content
|
||||
.padding(padding)
|
||||
.background(
|
||||
Capsule(style: .continuous)
|
||||
.fill(.clear)
|
||||
.idpGlassChrome()
|
||||
)
|
||||
.overlay(
|
||||
Capsule(style: .continuous)
|
||||
.stroke(Color.white.opacity(0.16), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import SwiftUI
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
enum Haptics {
|
||||
static func success() {
|
||||
#if os(iOS)
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||
#elseif os(macOS)
|
||||
NSHapticFeedbackManager.defaultPerformer.perform(.levelChange, performanceTime: .now)
|
||||
#endif
|
||||
}
|
||||
|
||||
static func warning() {
|
||||
#if os(iOS)
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.warning)
|
||||
#elseif os(macOS)
|
||||
NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .now)
|
||||
#endif
|
||||
}
|
||||
|
||||
static func selection() {
|
||||
#if os(iOS)
|
||||
UISelectionFeedbackGenerator().selectionChanged()
|
||||
#elseif os(macOS)
|
||||
NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .now)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import SwiftUI
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#elseif canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
public enum IdP {
|
||||
public static let tint = Color("IdPTint")
|
||||
public static let cardRadius: CGFloat = 22
|
||||
public static let controlRadius: CGFloat = 14
|
||||
public static let badgeRadius: CGFloat = 8
|
||||
|
||||
static func horizontalPadding(compact: Bool) -> CGFloat {
|
||||
compact ? 16 : 24
|
||||
}
|
||||
|
||||
static func verticalPadding(compact: Bool) -> CGFloat {
|
||||
compact ? 16 : 24
|
||||
}
|
||||
}
|
||||
|
||||
extension Color {
|
||||
static var idpGroupedBackground: Color {
|
||||
#if os(macOS)
|
||||
Color(nsColor: .windowBackgroundColor)
|
||||
#elseif os(watchOS)
|
||||
.black
|
||||
#else
|
||||
Color(uiColor: .systemGroupedBackground)
|
||||
#endif
|
||||
}
|
||||
|
||||
static var idpSecondaryGroupedBackground: Color {
|
||||
#if os(macOS)
|
||||
Color(nsColor: .controlBackgroundColor)
|
||||
#elseif os(watchOS)
|
||||
Color.white.opacity(0.08)
|
||||
#else
|
||||
Color(uiColor: .secondarySystemGroupedBackground)
|
||||
#endif
|
||||
}
|
||||
|
||||
static var idpTertiaryFill: Color {
|
||||
#if os(macOS)
|
||||
Color(nsColor: .quaternaryLabelColor).opacity(0.08)
|
||||
#elseif os(watchOS)
|
||||
Color.white.opacity(0.12)
|
||||
#else
|
||||
Color(uiColor: .tertiarySystemFill)
|
||||
#endif
|
||||
}
|
||||
|
||||
static var idpSeparator: Color {
|
||||
#if os(macOS)
|
||||
Color(nsColor: .separatorColor)
|
||||
#elseif os(watchOS)
|
||||
Color.white.opacity(0.14)
|
||||
#else
|
||||
Color(uiColor: .separator)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func idpScreenPadding(compact: Bool) -> some View {
|
||||
padding(.horizontal, IdP.horizontalPadding(compact: compact))
|
||||
.padding(.vertical, IdP.verticalPadding(compact: compact))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func idpInlineNavigationTitle() -> some View {
|
||||
#if os(macOS)
|
||||
self
|
||||
#else
|
||||
navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func idpTabBarChrome() -> some View {
|
||||
#if os(macOS)
|
||||
self
|
||||
#else
|
||||
toolbarBackground(.visible, for: .tabBar)
|
||||
.toolbarBackground(.regularMaterial, for: .tabBar)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func idpSearchable(text: Binding<String>, isPresented: Binding<Bool>) -> some View {
|
||||
#if os(macOS)
|
||||
searchable(text: text, isPresented: isPresented)
|
||||
#else
|
||||
searchable(text: text, isPresented: isPresented, placement: .navigationBarDrawer(displayMode: .always))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
extension ToolbarItemPlacement {
|
||||
static var idpTrailingToolbar: ToolbarItemPlacement {
|
||||
#if os(macOS)
|
||||
.primaryAction
|
||||
#else
|
||||
.topBarTrailing
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import SwiftUI
|
||||
|
||||
struct StatusDot: View {
|
||||
let color: Color
|
||||
|
||||
var body: some View {
|
||||
Circle()
|
||||
.fill(color)
|
||||
.frame(width: 10, height: 10)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color.white.opacity(0.65), lineWidth: 1)
|
||||
)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,31 @@ import CryptoKit
|
||||
import Foundation
|
||||
|
||||
enum AppSection: String, CaseIterable, Identifiable, Hashable, Codable {
|
||||
case overview
|
||||
case requests
|
||||
case activity
|
||||
case account
|
||||
case inbox
|
||||
case notifications
|
||||
case devices
|
||||
case identity
|
||||
case settings
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .overview: "Passport"
|
||||
case .requests: "Requests"
|
||||
case .activity: "Activity"
|
||||
case .account: "Account"
|
||||
case .inbox: "Inbox"
|
||||
case .notifications: "Notifications"
|
||||
case .devices: "Devices"
|
||||
case .identity: "Identity"
|
||||
case .settings: "Settings"
|
||||
}
|
||||
}
|
||||
|
||||
var systemImage: String {
|
||||
switch self {
|
||||
case .overview: "person.crop.square.fill"
|
||||
case .requests: "checklist.checked"
|
||||
case .activity: "clock.arrow.trianglehead.counterclockwise.rotate.90"
|
||||
case .account: "person.crop.circle.fill"
|
||||
case .inbox: "tray.full.fill"
|
||||
case .notifications: "bell.badge.fill"
|
||||
case .devices: "desktopcomputer"
|
||||
case .identity: "person.crop.rectangle.stack.fill"
|
||||
case .settings: "gearshape.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,8 +304,8 @@ enum ApprovalStatus: String, Hashable, Codable {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .pending: "Pending"
|
||||
case .approved: "Verified"
|
||||
case .rejected: "Declined"
|
||||
case .approved: "Approved"
|
||||
case .rejected: "Denied"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import Foundation
|
||||
|
||||
#if canImport(ActivityKit) && os(iOS)
|
||||
import ActivityKit
|
||||
#endif
|
||||
|
||||
struct ApprovalActivityPayload: Codable, Hashable {
|
||||
let requestID: String
|
||||
let title: String
|
||||
let appName: String
|
||||
let source: String
|
||||
let handle: String
|
||||
let location: String
|
||||
let createdAt: Date
|
||||
}
|
||||
|
||||
extension ApprovalRequest {
|
||||
var activityAppName: String {
|
||||
source
|
||||
.replacingOccurrences(of: "auth.", with: "")
|
||||
.replacingOccurrences(of: ".idp.global", with: ".idp.global")
|
||||
}
|
||||
|
||||
var activityLocation: String {
|
||||
"Berlin, DE"
|
||||
}
|
||||
|
||||
var activityExpiryDate: Date {
|
||||
createdAt.addingTimeInterval(risk == .elevated ? 180 : 300)
|
||||
}
|
||||
|
||||
func activityPayload(handle: String) -> ApprovalActivityPayload {
|
||||
ApprovalActivityPayload(
|
||||
requestID: id.uuidString,
|
||||
title: title,
|
||||
appName: activityAppName,
|
||||
source: source,
|
||||
handle: handle,
|
||||
location: activityLocation,
|
||||
createdAt: createdAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(ActivityKit) && os(iOS)
|
||||
struct ApprovalActivityAttributes: ActivityAttributes {
|
||||
struct ContentState: Codable, Hashable {
|
||||
let requestID: String
|
||||
let title: String
|
||||
let appName: String
|
||||
let source: String
|
||||
let handle: String
|
||||
let location: String
|
||||
}
|
||||
|
||||
let requestID: String
|
||||
let createdAt: Date
|
||||
}
|
||||
#endif
|
||||
@@ -1,5 +1,13 @@
|
||||
import Foundation
|
||||
|
||||
enum SharedDefaults {
|
||||
static let appGroupIdentifier = "group.global.idp.app"
|
||||
|
||||
static var userDefaults: UserDefaults {
|
||||
UserDefaults(suiteName: appGroupIdentifier) ?? .standard
|
||||
}
|
||||
}
|
||||
|
||||
struct PersistedAppState: Codable, Equatable {
|
||||
let session: AuthSession
|
||||
let profile: MemberProfile
|
||||
@@ -19,7 +27,7 @@ final class UserDefaultsAppStateStore: AppStateStoring {
|
||||
private let encoder = JSONEncoder()
|
||||
private let decoder = JSONDecoder()
|
||||
|
||||
init(defaults: UserDefaults = .standard, storageKey: String = "persisted-app-state") {
|
||||
init(defaults: UserDefaults = SharedDefaults.userDefaults, storageKey: String = "persisted-app-state") {
|
||||
self.defaults = defaults
|
||||
self.storageKey = storageKey
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ protocol IDPServicing {
|
||||
}
|
||||
|
||||
actor MockIDPService: IDPServicing {
|
||||
static let shared = MockIDPService()
|
||||
|
||||
private let profile = MemberProfile(
|
||||
name: "Phil Kunz",
|
||||
handle: "phil@idp.global",
|
||||
@@ -20,15 +22,24 @@ actor MockIDPService: IDPServicing {
|
||||
recoverySummary: "Recovery kit healthy with 2 of 3 backup paths verified."
|
||||
)
|
||||
|
||||
private let appStateStore: AppStateStoring
|
||||
private var requests: [ApprovalRequest] = []
|
||||
private var notifications: [AppNotification] = []
|
||||
|
||||
init() {
|
||||
requests = Self.seedRequests()
|
||||
notifications = Self.seedNotifications()
|
||||
init(appStateStore: AppStateStoring = UserDefaultsAppStateStore()) {
|
||||
self.appStateStore = appStateStore
|
||||
|
||||
if let state = appStateStore.load() {
|
||||
requests = state.requests.sorted { $0.createdAt > $1.createdAt }
|
||||
notifications = state.notifications.sorted { $0.sentAt > $1.sentAt }
|
||||
} else {
|
||||
requests = Self.seedRequests()
|
||||
notifications = Self.seedNotifications()
|
||||
}
|
||||
}
|
||||
|
||||
func bootstrap() async throws -> BootstrapContext {
|
||||
restoreSharedState()
|
||||
try await Task.sleep(for: .milliseconds(120))
|
||||
return BootstrapContext(
|
||||
suggestedPairingPayload: "idp.global://pair?token=swiftapp-demo-berlin&origin=code.foss.global&device=Safari%20on%20Berlin%20MBP"
|
||||
@@ -36,6 +47,7 @@ actor MockIDPService: IDPServicing {
|
||||
}
|
||||
|
||||
func signIn(with request: PairingAuthenticationRequest) async throws -> SignInResult {
|
||||
restoreSharedState()
|
||||
try await Task.sleep(for: .milliseconds(260))
|
||||
|
||||
try validateSignedGPSPosition(in: request)
|
||||
@@ -51,6 +63,8 @@ actor MockIDPService: IDPServicing {
|
||||
at: 0
|
||||
)
|
||||
|
||||
persistSharedStateIfAvailable()
|
||||
|
||||
return SignInResult(
|
||||
session: session,
|
||||
snapshot: snapshot()
|
||||
@@ -58,6 +72,7 @@ actor MockIDPService: IDPServicing {
|
||||
}
|
||||
|
||||
func identify(with request: PairingAuthenticationRequest) async throws -> DashboardSnapshot {
|
||||
restoreSharedState()
|
||||
try await Task.sleep(for: .milliseconds(180))
|
||||
|
||||
try validateSignedGPSPosition(in: request)
|
||||
@@ -73,15 +88,19 @@ actor MockIDPService: IDPServicing {
|
||||
at: 0
|
||||
)
|
||||
|
||||
persistSharedStateIfAvailable()
|
||||
|
||||
return snapshot()
|
||||
}
|
||||
|
||||
func refreshDashboard() async throws -> DashboardSnapshot {
|
||||
restoreSharedState()
|
||||
try await Task.sleep(for: .milliseconds(180))
|
||||
return snapshot()
|
||||
}
|
||||
|
||||
func approveRequest(id: UUID) async throws -> DashboardSnapshot {
|
||||
restoreSharedState()
|
||||
try await Task.sleep(for: .milliseconds(150))
|
||||
|
||||
guard let index = requests.firstIndex(where: { $0.id == id }) else {
|
||||
@@ -100,10 +119,13 @@ actor MockIDPService: IDPServicing {
|
||||
at: 0
|
||||
)
|
||||
|
||||
persistSharedStateIfAvailable()
|
||||
|
||||
return snapshot()
|
||||
}
|
||||
|
||||
func rejectRequest(id: UUID) async throws -> DashboardSnapshot {
|
||||
restoreSharedState()
|
||||
try await Task.sleep(for: .milliseconds(150))
|
||||
|
||||
guard let index = requests.firstIndex(where: { $0.id == id }) else {
|
||||
@@ -122,10 +144,13 @@ actor MockIDPService: IDPServicing {
|
||||
at: 0
|
||||
)
|
||||
|
||||
persistSharedStateIfAvailable()
|
||||
|
||||
return snapshot()
|
||||
}
|
||||
|
||||
func simulateIncomingRequest() async throws -> DashboardSnapshot {
|
||||
restoreSharedState()
|
||||
try await Task.sleep(for: .milliseconds(120))
|
||||
|
||||
let syntheticRequest = ApprovalRequest(
|
||||
@@ -151,10 +176,13 @@ actor MockIDPService: IDPServicing {
|
||||
at: 0
|
||||
)
|
||||
|
||||
persistSharedStateIfAvailable()
|
||||
|
||||
return snapshot()
|
||||
}
|
||||
|
||||
func markNotificationRead(id: UUID) async throws -> DashboardSnapshot {
|
||||
restoreSharedState()
|
||||
try await Task.sleep(for: .milliseconds(80))
|
||||
|
||||
guard let index = notifications.firstIndex(where: { $0.id == id }) else {
|
||||
@@ -162,6 +190,7 @@ actor MockIDPService: IDPServicing {
|
||||
}
|
||||
|
||||
notifications[index].isUnread = false
|
||||
persistSharedStateIfAvailable()
|
||||
return snapshot()
|
||||
}
|
||||
|
||||
@@ -227,6 +256,30 @@ actor MockIDPService: IDPServicing {
|
||||
return "An identity proof was completed for \(context.deviceName) on \(context.originHost)."
|
||||
}
|
||||
|
||||
private func restoreSharedState() {
|
||||
guard let state = appStateStore.load() else {
|
||||
requests = Self.seedRequests()
|
||||
notifications = Self.seedNotifications()
|
||||
return
|
||||
}
|
||||
|
||||
requests = state.requests.sorted { $0.createdAt > $1.createdAt }
|
||||
notifications = state.notifications.sorted { $0.sentAt > $1.sentAt }
|
||||
}
|
||||
|
||||
private func persistSharedStateIfAvailable() {
|
||||
guard let state = appStateStore.load() else { return }
|
||||
|
||||
appStateStore.save(
|
||||
PersistedAppState(
|
||||
session: state.session,
|
||||
profile: state.profile,
|
||||
requests: requests,
|
||||
notifications: notifications
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private static func seedRequests() -> [ApprovalRequest] {
|
||||
[
|
||||
ApprovalRequest(
|
||||
|
||||
Reference in New Issue
Block a user