FieldUserPresenceGate.swift (4907B)
1 import Foundation 2 import RadrootsKit 3 4 enum FieldUserPresenceAction: Equatable, Sendable { 5 case unlockIdentity 6 case saveIdentity 7 case deleteIdentity 8 9 var reason: String { 10 switch self { 11 case .unlockIdentity: 12 "Unlock your local Nostr identity." 13 case .saveIdentity: 14 "Save your local Nostr identity on this iPhone." 15 case .deleteIdentity: 16 "Delete your local Nostr identity from this iPhone." 17 } 18 } 19 20 var verifiedStatusText: String { 21 switch self { 22 case .unlockIdentity: 23 "Verified user presence to unlock identity." 24 case .saveIdentity: 25 "Verified user presence to save identity." 26 case .deleteIdentity: 27 "Verified user presence to delete identity." 28 } 29 } 30 } 31 32 struct FieldUserPresenceRequestRecord: Equatable, Sendable { 33 let action: FieldUserPresenceAction 34 let statusText: String 35 } 36 37 enum FieldUserPresenceGateError: LocalizedError, Equatable { 38 case notVerified 39 40 var errorDescription: String? { 41 switch self { 42 case .notVerified: 43 "User presence was not verified." 44 } 45 } 46 } 47 48 final class FieldUserPresenceGate: Sendable { 49 private let userPresence: any RadrootsUserPresence 50 51 init(userPresence: any RadrootsUserPresence) { 52 self.userPresence = userPresence 53 } 54 55 static func configured() -> FieldUserPresenceGate { 56 #if DEBUG 57 if FieldUITestHarness.isRequested { 58 return FieldUserPresenceGate(userPresence: uiTestUserPresence()) 59 } 60 #endif 61 return FieldUserPresenceGate(userPresence: RadrootsAppleUserPresence()) 62 } 63 64 func requirePresence(for action: FieldUserPresenceAction) async throws -> FieldUserPresenceRequestRecord { 65 let request = try RadrootsUserPresenceRequest(reason: action.reason) 66 let result = try await userPresence.verify(request) 67 guard result.verified else { 68 throw FieldUserPresenceGateError.notVerified 69 } 70 return FieldUserPresenceRequestRecord(action: action, statusText: action.verifiedStatusText) 71 } 72 73 #if DEBUG 74 private static func uiTestUserPresence() -> any RadrootsUserPresence { 75 let outcomes = uiTestOutcomes() 76 let status = uiTestStatus() 77 return FieldUITestUserPresence(status: status, outcomes: outcomes.map(\.result)) 78 } 79 80 private static func uiTestStatus() -> RadrootsUserPresenceStatus { 81 let raw = FieldUITestHarness.string("RADROOTS_FIELD_IOS_UI_TEST_USER_PRESENCE_STATUS")?.lowercased() 82 switch raw { 83 case "unavailable": 84 return .unavailable 85 case "device_credential": 86 return RadrootsUserPresenceStatus( 87 support: .deviceCredential, 88 biometryKind: .none, 89 canEvaluateDeviceCredential: true, 90 canEvaluateBiometrics: false 91 ) 92 case nil, "", "available", "biometrics": 93 return RadrootsUserPresenceStatus( 94 support: .biometricsOrDeviceCredential, 95 biometryKind: .faceID, 96 canEvaluateDeviceCredential: true, 97 canEvaluateBiometrics: true 98 ) 99 default: 100 return .unavailable 101 } 102 } 103 104 private static func uiTestOutcomes() -> [FieldUserPresenceUITestOutcome] { 105 let raw = FieldUITestHarness.string("RADROOTS_FIELD_IOS_UI_TEST_USER_PRESENCE_OUTCOME") ?? "" 106 let parts = raw 107 .split(separator: ",") 108 .map { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() } 109 .filter { !$0.isEmpty } 110 if parts.isEmpty { 111 return [.success] 112 } 113 return parts.map { FieldUserPresenceUITestOutcome(rawValue: $0) ?? .denied } 114 } 115 #endif 116 } 117 118 #if DEBUG 119 private enum FieldUserPresenceUITestOutcome: String { 120 case success 121 case unverified 122 case cancelled 123 case denied 124 case unavailable 125 case timeout 126 case transientFailure = "transient_failure" 127 case permanentFailure = "permanent_failure" 128 129 var result: Result<Bool, RadrootsUserPresenceError> { 130 switch self { 131 case .success: 132 .success(true) 133 case .unverified: 134 .success(false) 135 case .cancelled: 136 .failure(.userCancelled("User presence was cancelled.")) 137 case .denied: 138 .failure(.permissionDenied("User presence permission is denied.")) 139 case .unavailable: 140 .failure(.unavailable("User presence is unavailable.")) 141 case .timeout: 142 .failure(.timeout("User presence timed out.")) 143 case .transientFailure: 144 .failure(.transientFailure("User presence failed. Please retry.")) 145 case .permanentFailure: 146 .failure(.permanentFailure("User presence failed.")) 147 } 148 } 149 } 150 #endif