From c3806e2eff90116b29543f98f42f22b2334265ac Mon Sep 17 00:00:00 2001 From: Brandon Evans Date: Sat, 5 Dec 2020 09:36:23 -0700 Subject: [PATCH] Allow selecting a phone number interactively --- Xcodes/AppState.swift | 164 ++++++------------ Xcodes/AppleAPI/Sources/AppleAPI/Client.swift | 120 +++++++++---- Xcodes/ContentView.swift | 17 +- Xcodes/SignIn/SignIn2FAView.swift | 25 ++- Xcodes/SignIn/SignInPhoneListView.swift | 47 ++++- Xcodes/SignIn/SignInSMSView.swift | 29 +++- 6 files changed, 232 insertions(+), 170 deletions(-) diff --git a/Xcodes/AppState.swift b/Xcodes/AppState.swift index c6829a7..6d06d8e 100644 --- a/Xcodes/AppState.swift +++ b/Xcodes/AppState.swift @@ -34,7 +34,13 @@ class AppState: ObservableObject { @Published var error: AlertContent? @Published var presentingSignInAlert = false - @Published var secondFactorSessionData: AppleSessionData? + @Published var secondFactorData: SecondFactorData? + + struct SecondFactorData { + let option: TwoFactorOption + let authOptions: AuthOptionsResponse + let sessionData: AppleSessionData + } private var cancellables = Set() let client = AppleAPI.Client() @@ -55,6 +61,8 @@ class AppState: ObservableObject { // } } + // MARK: - Authentication + func validateSession() -> AnyPublisher { return client.validateSession() .handleEvents(receiveCompletion: { completion in @@ -71,129 +79,50 @@ class AppState: ObservableObject { .receive(on: DispatchQueue.main) .sink( receiveCompletion: { completion in - if case .failure = completion { - // TODO: show error - } + self.handleAuthenticationFlowCompletion(completion) }, receiveValue: { authenticationState in self.authenticationState = authenticationState - if case let AuthenticationState.waitingForSecondFactor(option, sessionData) = authenticationState { - self.handleTwoFactorOption(option, serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt) + } + ) + .store(in: &cancellables) + } + + func handleTwoFactorOption(_ option: TwoFactorOption, authOptions: AuthOptionsResponse, serviceKey: String, sessionID: String, scnt: String) { + self.presentingSignInAlert = false + self.secondFactorData = SecondFactorData( + option: option, + authOptions: authOptions, + sessionData: AppleSessionData(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt) + ) + } + + func requestSMS(to trustedPhoneNumber: AuthOptionsResponse.TrustedPhoneNumber, authOptions: AuthOptionsResponse, sessionData: AppleSessionData) { + client.requestSMSSecurityCode(to: trustedPhoneNumber, authOptions: authOptions, sessionData: sessionData) + .sink( + receiveCompletion: { completion in + self.handleAuthenticationFlowCompletion(completion) + }, + receiveValue: { authenticationState in + self.authenticationState = authenticationState + if case let AuthenticationState.waitingForSecondFactor(option, authOptions, sessionData) = authenticationState { + self.handleTwoFactorOption(option, authOptions: authOptions, serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt) } } ) .store(in: &cancellables) } - func handleTwoFactorOption(_ option: TwoFactorOption, serviceKey: String, sessionID: String, scnt: String) { -// Current.logging.log("Two-factor authentication is enabled for this account.\n") - switch option { - case let .smsSent(codeLength, phoneNumber): - break -// return Result { -// let code = self.promptForSMSSecurityCode(length: codeLength, for: phoneNumber) -// return try URLRequest.submitSecurityCode(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, code: code) -// } -// .publisher -// .flatMap { request in -// return Current.network.dataTask(with: request) -// .validateSecurityCodeResponse() -// .mapError { $0 as Error } -// } -// .flatMap { (data, response) in -// self.updateSession(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt) -// } -// .eraseToAnyPublisher() - case let .smsPendingChoice(codeLength, trustedPhoneNumbers): - break -// return handleWithPhoneNumberSelection(codeLength: codeLength, trustedPhoneNumbers: trustedPhoneNumbers, serviceKey: serviceKey, sessionID: sessionID, scnt: scnt) - case let .codeSent(codeLength): - self.presentingSignInAlert = false - self.secondFactorSessionData = AppleSessionData(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt) -// let code = Current.shell.readLine(""" -// Enter "sms" without quotes to exit this prompt and choose a phone number to send an SMS security code to. -// Enter the \(codeLength) digit code from one of your trusted devices: -// """) ?? "" -// -// if code == "sms" { - // return handleWithPhoneNumberSelection(codeLength: codeLength, trustedPhoneNumbers: authOp, serviceKey: serviceKey, sessionID: sessionID, scnt: scnt) -// } - } + func choosePhoneNumberForSMS(authOptions: AuthOptionsResponse, sessionData: AppleSessionData) { + secondFactorData = SecondFactorData(option: .smsPendingChoice, authOptions: authOptions, sessionData: sessionData) } -// func selectPhoneNumberInteractively(from trustedPhoneNumbers: [AuthOptionsResponse.TrustedPhoneNumber]) -> AnyPublisher { -// return Result { -// Current.logging.log("Trusted phone numbers:") -// trustedPhoneNumbers.enumerated().forEach { (index, phoneNumber) in -// Current.logging.log("\(index + 1): \(phoneNumber.numberWithDialCode)") -// } -// -// let possibleSelectionNumberString = Current.shell.readLine("Select a trusted phone number to receive a code via SMS: ") -// guard -// let selectionNumberString = possibleSelectionNumberString, -// let selectionNumber = Int(selectionNumberString) , -// trustedPhoneNumbers.indices.contains(selectionNumber - 1) -// else { -// throw AuthenticationError.invalidPhoneNumberIndex(min: 1, max: trustedPhoneNumbers.count, given: possibleSelectionNumberString) -// } -// -// return trustedPhoneNumbers[selectionNumber - 1] -// } -// .publisher -// .catch { error -> AnyPublisher in -// guard case AuthenticationError.invalidPhoneNumberIndex = error else { -// return Fail(error: error).eraseToAnyPublisher() -// } -// Current.logging.log("\(error.localizedDescription)\n") -// return self.selectPhoneNumberInteractively(from: trustedPhoneNumbers) -// } -// .eraseToAnyPublisher() -// } -// -// func promptForSMSSecurityCode(length: Int, for trustedPhoneNumber: AuthOptionsResponse.TrustedPhoneNumber) -> SecurityCode { -// let code = Current.shell.readLine("Enter the \(length) digit code sent to \(trustedPhoneNumber.numberWithDialCode): ") ?? "" -// return .sms(code: code, phoneNumberId: trustedPhoneNumber.id) -// } - -// func handleWithPhoneNumberSelection(codeLength: Int, trustedPhoneNumbers: [AuthOptionsResponse.TrustedPhoneNumber]?, serviceKey: String, sessionID: String, scnt: String) -> AnyPublisher { -// // I don't think this should ever be nil or empty, because 2FA requires at least one trusted phone number, -// // but if it is nil or empty it's better to inform the user so they can try to address it instead of crashing. -// guard let trustedPhoneNumbers = trustedPhoneNumbers, trustedPhoneNumbers.isEmpty == false else { -// return Fail(error: AuthenticationError.noTrustedPhoneNumbers) -// .eraseToAnyPublisher() -// } -// -// return selectPhoneNumberInteractively(from: trustedPhoneNumbers) -// .flatMap { trustedPhoneNumber in -// Current.network.dataTask(with: try URLRequest.requestSecurityCode(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, trustedPhoneID: trustedPhoneNumber.id)) -// .map { _ in -// self.promptForSMSSecurityCode(length: codeLength, for: trustedPhoneNumber) -// } -// } -// .flatMap { code in -// Current.network.dataTask(with: try URLRequest.submitSecurityCode(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, code: code)) -// .validateSecurityCodeResponse() -// } -// .flatMap { (data, response) -> AnyPublisher in -// self.updateSession(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt) -// } -// .eraseToAnyPublisher() -// } - - func submit2FACode(_ code: String, sessionData: AppleSessionData) { + func submitSecurityCode(_ code: SecurityCode, sessionData: AppleSessionData) { client.submitSecurityCode(code, sessionData: sessionData) .receive(on: DispatchQueue.main) .sink( receiveCompletion: { completion in - switch completion { - case let .failure(error): - self.error = AlertContent(title: "Error logging in", message: error.legibleLocalizedDescription) - case .finished: - if case .authenticated = self.authenticationState { - self.presentingSignInAlert = false - self.secondFactorSessionData = nil - } - } + self.handleAuthenticationFlowCompletion(completion) }, receiveValue: { authenticationState in self.authenticationState = authenticationState @@ -202,6 +131,23 @@ class AppState: ObservableObject { .store(in: &cancellables) } + private func handleAuthenticationFlowCompletion(_ completion: Subscribers.Completion) { + switch completion { + case let .failure(error): + self.error = AlertContent(title: "Error signing in", message: error.legibleLocalizedDescription) + case .finished: + switch self.authenticationState { + case .authenticated, .unauthenticated: + self.presentingSignInAlert = false + self.secondFactorData = nil + case let .waitingForSecondFactor(option, authOptions, sessionData): + self.handleTwoFactorOption(option, authOptions: authOptions, serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt) + } + } + } + + // MARK: - + public func update() -> AnyPublisher<[Xcode], Error> { // return firstly { () -> Promise in // validateSession() diff --git a/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift b/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift index a3f1dbd..6cae3db 100644 --- a/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift +++ b/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift @@ -83,27 +83,39 @@ public class Client { // SMS was sent automatically if authOptions.smsAutomaticallySent { - option = .smsSent(authOptions.securityCode.length, authOptions.trustedPhoneNumbers!.first!) + option = .smsSent(authOptions.trustedPhoneNumbers!.first!) // SMS wasn't sent automatically because user needs to choose a phone to send to } else if authOptions.canFallBackToSMS { - option = .smsPendingChoice(authOptions.securityCode.length, authOptions.trustedPhoneNumbers ?? []) + option = .smsPendingChoice // Code is shown on trusted devices } else { - option = .codeSent(authOptions.securityCode.length) + option = .codeSent } let sessionData = AppleSessionData(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt) - return Just(AuthenticationState.waitingForSecondFactor(option, sessionData)) + return Just(AuthenticationState.waitingForSecondFactor(option, authOptions, sessionData)) .setFailureType(to: Error.self) .eraseToAnyPublisher() - // return handleTwoFactorOption(option, serviceKey: serviceKey, sessionID: sessionID, scnt: scnt) } // MARK: - Continue 2FA - public func submitSecurityCode(_ code: String, sessionData: AppleSessionData) -> AnyPublisher { + public func requestSMSSecurityCode(to trustedPhoneNumber: AuthOptionsResponse.TrustedPhoneNumber, authOptions: AuthOptionsResponse, sessionData: AppleSessionData) -> AnyPublisher { Result { - try URLRequest.submitSecurityCode(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt, code: .device(code: code)) + try URLRequest.requestSecurityCode(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt, trustedPhoneID: trustedPhoneNumber.id) + } + .publisher + .flatMap { request in + Current.network.dataTask(with: request) + .mapError { $0 as Error } + } + .map { _ in AuthenticationState.waitingForSecondFactor(.smsSent(trustedPhoneNumber), authOptions, sessionData) } + .eraseToAnyPublisher() + } + + public func submitSecurityCode(_ code: SecurityCode, sessionData: AppleSessionData) -> AnyPublisher { + Result { + try URLRequest.submitSecurityCode(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt, code: code) } .publisher .flatMap { request in @@ -146,7 +158,7 @@ public class Client { public enum AuthenticationState: Equatable { case unauthenticated - case waitingForSecondFactor(TwoFactorOption, AppleSessionData) + case waitingForSecondFactor(TwoFactorOption, AuthOptionsResponse, AppleSessionData) case authenticated } @@ -157,7 +169,6 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable { case incorrectSecurityCode case unexpectedSignInResponse(statusCode: Int, message: String?) case appleIDAndPrivacyAcknowledgementRequired - case noTrustedPhoneNumbers case accountUsesTwoStepAuthentication case accountUsesUnknownAuthenticationKind(String?) @@ -169,8 +180,6 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable { return "You must sign in to https://appstoreconnect.apple.com and acknowledge the Apple ID & Privacy agreement." case .invalidPhoneNumberIndex(let min, let max, let given): return "Not a valid phone number index. Expecting a whole number between \(min)-\(max), but was given \(given ?? "nothing")." - case .noTrustedPhoneNumbers: - return "Your account doesn't have any trusted phone numbers, but they're required for two-factor authentication. See https://support.apple.com/en-ca/HT204915." case .accountUsesTwoStepAuthentication: return "Received a response from Apple that indicates this account has two-step authentication enabled. xcodes currently only supports the newer two-factor authentication, though. Please consider upgrading to two-factor authentication, or open an issue on GitHub explaining why this isn't an option for you here: https://github.com/RobotsAndPencils/xcodes/issues/new" case .accountUsesUnknownAuthenticationKind: @@ -214,9 +223,9 @@ struct SignInResponse: Decodable { } public enum TwoFactorOption: Equatable { - case smsSent(Int, AuthOptionsResponse.TrustedPhoneNumber) - case codeSent(Int) - case smsPendingChoice(Int, [AuthOptionsResponse.TrustedPhoneNumber]) + case smsSent(AuthOptionsResponse.TrustedPhoneNumber) + case codeSent + case smsPendingChoice } public extension Publisher where Output == (data: Data, response: URLResponse) { @@ -239,13 +248,27 @@ public extension Publisher where Output == (data: Data, response: URLResponse) { } public struct AuthOptionsResponse: Equatable, Decodable { - let trustedPhoneNumbers: [TrustedPhoneNumber]? - let trustedDevices: [TrustedDevice]? - let securityCode: SecurityCodeInfo - let noTrustedDevices: Bool? - let serviceErrors: [ServiceError]? + public let trustedPhoneNumbers: [TrustedPhoneNumber]? + public let trustedDevices: [TrustedDevice]? + public let securityCode: SecurityCodeInfo + public let noTrustedDevices: Bool? + public let serviceErrors: [ServiceError]? - var kind: Kind { + public init( + trustedPhoneNumbers: [AuthOptionsResponse.TrustedPhoneNumber]?, + trustedDevices: [AuthOptionsResponse.TrustedDevice]?, + securityCode: AuthOptionsResponse.SecurityCodeInfo, + noTrustedDevices: Bool? = nil, + serviceErrors: [ServiceError]? = nil + ) { + self.trustedPhoneNumbers = trustedPhoneNumbers + self.trustedDevices = trustedDevices + self.securityCode = securityCode + self.noTrustedDevices = noTrustedDevices + self.serviceErrors = serviceErrors + } + + public var kind: Kind { if trustedDevices != nil { return .twoStep } else if trustedPhoneNumbers != nil { @@ -259,31 +282,56 @@ public struct AuthOptionsResponse: Equatable, Decodable { // This should have been a situation where an SMS security code was sent automatically. // This resolved itself either after some time passed, or by signing into appleid.apple.com with the account. // Not sure if it's worth explicitly handling this case or if it'll be really rare. - var canFallBackToSMS: Bool { + public var canFallBackToSMS: Bool { noTrustedDevices == true } - var smsAutomaticallySent: Bool { + public var smsAutomaticallySent: Bool { trustedPhoneNumbers?.count == 1 && canFallBackToSMS } - public struct TrustedPhoneNumber: Equatable, Decodable { - let id: Int - let numberWithDialCode: String + public struct TrustedPhoneNumber: Equatable, Decodable, Identifiable { + public let id: Int + public let numberWithDialCode: String + + public init(id: Int, numberWithDialCode: String) { + self.id = id + self.numberWithDialCode = numberWithDialCode + } } - public struct TrustedDevice: Equatable, Decodable { - let id: String - let name: String - let modelName: String + public struct TrustedDevice: Equatable, Decodable { + public let id: String + public let name: String + public let modelName: String + + public init(id: String, name: String, modelName: String) { + self.id = id + self.name = name + self.modelName = modelName + } } - public struct SecurityCodeInfo: Equatable, Decodable { - let length: Int - let tooManyCodesSent: Bool - let tooManyCodesValidated: Bool - let securityCodeLocked: Bool - let securityCodeCooldown: Bool + public struct SecurityCodeInfo: Equatable, Decodable { + public let length: Int + public let tooManyCodesSent: Bool + public let tooManyCodesValidated: Bool + public let securityCodeLocked: Bool + public let securityCodeCooldown: Bool + + public init( + length: Int, + tooManyCodesSent: Bool = false, + tooManyCodesValidated: Bool = false, + securityCodeLocked: Bool = false, + securityCodeCooldown: Bool = false + ) { + self.length = length + self.tooManyCodesSent = tooManyCodesSent + self.tooManyCodesValidated = tooManyCodesValidated + self.securityCodeLocked = securityCodeLocked + self.securityCodeCooldown = securityCodeCooldown + } } public enum Kind: Equatable { @@ -296,7 +344,7 @@ public struct ServiceError: Decodable, Equatable { let message: String } -enum SecurityCode { +public enum SecurityCode { case device(code: String) case sms(code: String, phoneNumberId: Int) diff --git a/Xcodes/ContentView.swift b/Xcodes/ContentView.swift index 0f35eff..324b49f 100644 --- a/Xcodes/ContentView.swift +++ b/Xcodes/ContentView.swift @@ -107,11 +107,22 @@ struct ContentView: View { primaryButton: .destructive(Text("Uninstall"), action: { self.appState.uninstall(id: row.id) }), secondaryButton: .cancel(Text("Cancel"))) } - .sheet(item: $appState.secondFactorSessionData) { sessionData in - SignIn2FAView(isPresented: $appState.secondFactorSessionData.isNotNil, sessionData: sessionData) + .sheet(isPresented: $appState.secondFactorData.isNotNil) { + secondFactorView(appState.secondFactorData!) .environmentObject(appState) } - + } + + @ViewBuilder + func secondFactorView(_ secondFactorData: AppState.SecondFactorData) -> some View { + switch secondFactorData.option { + case .codeSent: + SignIn2FAView(isPresented: $appState.secondFactorData.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData) + case .smsSent(let trustedPhoneNumber): + SignInSMSView(isPresented: $appState.secondFactorData.isNotNil, trustedPhoneNumber: trustedPhoneNumber, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData) + case .smsPendingChoice: + SignInPhoneListView(isPresented: $appState.secondFactorData.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData) + } } } diff --git a/Xcodes/SignIn/SignIn2FAView.swift b/Xcodes/SignIn/SignIn2FAView.swift index e64e2e5..329a108 100644 --- a/Xcodes/SignIn/SignIn2FAView.swift +++ b/Xcodes/SignIn/SignIn2FAView.swift @@ -5,29 +5,28 @@ struct SignIn2FAView: View { @EnvironmentObject var appState: AppState @Binding var isPresented: Bool @State private var code: String = "" + let authOptions: AuthOptionsResponse let sessionData: AppleSessionData - // TODO: dynamic number of digits - let numberOfDigits = 6 var body: some View { VStack(alignment: .leading) { - Text("Enter the \(6) digit code from one of your trusted devices:") + Text("Enter the \(authOptions.securityCode.length) digit code from one of your trusted devices:") HStack { Spacer() - PinCodeTextField(code: $code, numberOfDigits: numberOfDigits) + PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode.length) Spacer() } .padding() HStack { - Button("Cancel", action: { isPresented = false }) + Button("Cancel", action: { isPresented = false }) .keyboardShortcut(.cancelAction) - Button("Send SMS", action: {}) + Button("Send SMS", action: { appState.choosePhoneNumberForSMS(authOptions: authOptions, sessionData: sessionData) }) Spacer() - Button("Continue", action: { appState.submit2FACode(code, sessionData: sessionData) }) + Button("Continue", action: { appState.submitSecurityCode(.device(code: code), sessionData: sessionData) }) .keyboardShortcut(.defaultAction) - .disabled(code.count != numberOfDigits) + .disabled(code.count != authOptions.securityCode.length) } } .padding() @@ -36,7 +35,15 @@ struct SignIn2FAView: View { struct SignIn2FAView_Previews: PreviewProvider { static var previews: some View { - SignIn2FAView(isPresented: .constant(true), sessionData: AppleSessionData(serviceKey: "", sessionID: "", scnt: "")) + SignIn2FAView( + isPresented: .constant(true), + authOptions: AuthOptionsResponse( + trustedPhoneNumbers: nil, + trustedDevices: nil, + securityCode: .init(length: 6) + ), + sessionData: AppleSessionData(serviceKey: "", sessionID: "", scnt: "") + ) .environmentObject(AppState()) } } diff --git a/Xcodes/SignIn/SignInPhoneListView.swift b/Xcodes/SignIn/SignInPhoneListView.swift index 5348a06..b9c9f3a 100644 --- a/Xcodes/SignIn/SignInPhoneListView.swift +++ b/Xcodes/SignIn/SignInPhoneListView.swift @@ -1,23 +1,36 @@ import SwiftUI +import AppleAPI struct SignInPhoneListView: View { @EnvironmentObject var appState: AppState @Binding var isPresented: Bool - var phoneNumbers: [String] + @State private var selectedPhoneNumberID: AuthOptionsResponse.TrustedPhoneNumber.ID? + let authOptions: AuthOptionsResponse + let sessionData: AppleSessionData var body: some View { VStack(alignment: .leading) { - Text("Select a trusted phone number to receive a code via SMS: ") - - List(phoneNumbers, id: \.self) { - Text($0) + if let phoneNumbers = authOptions.trustedPhoneNumbers, !phoneNumbers.isEmpty { + Text("Select a trusted phone number to receive a \(authOptions.securityCode.length) digit code via SMS:") + + List(phoneNumbers, selection: $selectedPhoneNumberID) { + Text($0.numberWithDialCode) + } + .frame(height: 200) + } else { + // TODO: This should be a clickable hyperlink + Text("Your account doesn't have any trusted phone numbers, but they're required for two-factor authentication. See https://support.apple.com/en-ca/HT204915.") + // lineLimit doesn't work, fixedSize(horizontal: false, vertical: true) is too large in an Alert + .frame(height: 50) } - .frame(height: 200) HStack { Button("Cancel", action: { isPresented = false }) + .keyboardShortcut(.cancelAction) Spacer() - Button("Continue", action: {}) + Button("Continue", action: { appState.requestSMS(to: authOptions.trustedPhoneNumbers!.first { $0.id == selectedPhoneNumberID }!, authOptions: authOptions, sessionData: sessionData) }) + .keyboardShortcut(.defaultAction) + .disabled(selectedPhoneNumberID == nil) } } .padding() @@ -26,6 +39,24 @@ struct SignInPhoneListView: View { struct SignInPhoneListView_Previews: PreviewProvider { static var previews: some View { - SignInPhoneListView(isPresented: .constant(true), phoneNumbers: ["123-456-7890"]) + Group { + SignInPhoneListView( + isPresented: .constant(true), + authOptions: AuthOptionsResponse( + trustedPhoneNumbers: [.init(id: 0, numberWithDialCode: "(•••) •••-••90")], + trustedDevices: nil, + securityCode: .init(length: 6)), + sessionData: AppleSessionData(serviceKey: "", sessionID: "", scnt: "") + ) + + SignInPhoneListView( + isPresented: .constant(true), + authOptions: AuthOptionsResponse( + trustedPhoneNumbers: [], + trustedDevices: nil, + securityCode: .init(length: 6)), + sessionData: AppleSessionData(serviceKey: "", sessionID: "", scnt: "") + ) + } } } diff --git a/Xcodes/SignIn/SignInSMSView.swift b/Xcodes/SignIn/SignInSMSView.swift index 07f5a97..11804b5 100644 --- a/Xcodes/SignIn/SignInSMSView.swift +++ b/Xcodes/SignIn/SignInSMSView.swift @@ -1,22 +1,32 @@ import SwiftUI +import AppleAPI struct SignInSMSView: View { @EnvironmentObject var appState: AppState @Binding var isPresented: Bool @State private var code: String = "" + let trustedPhoneNumber: AuthOptionsResponse.TrustedPhoneNumber + let authOptions: AuthOptionsResponse + let sessionData: AppleSessionData var body: some View { VStack(alignment: .leading) { - Text("Enter the \(6) digit code sent to \("phone number"): ") + Text("Enter the \(authOptions.securityCode.length) digit code sent to \(trustedPhoneNumber.numberWithDialCode): ") HStack { - TextField("\(6) digit code", text: $code) + Spacer() + PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode.length) + Spacer() } + .padding() HStack { Button("Cancel", action: { isPresented = false }) + .keyboardShortcut(.cancelAction) Spacer() - Button("Continue", action: {}) + Button("Continue", action: { appState.submitSecurityCode(.sms(code: code, phoneNumberId: trustedPhoneNumber.id), sessionData: sessionData) }) + .keyboardShortcut(.defaultAction) + .disabled(code.count != authOptions.securityCode.length) } } .padding() @@ -25,7 +35,16 @@ struct SignInSMSView: View { struct SignInSMSView_Previews: PreviewProvider { static var previews: some View { - SignInSMSView(isPresented: .constant(true)) - .environmentObject(AppState()) + SignInSMSView( + isPresented: .constant(true), + trustedPhoneNumber: .init(id: 0, numberWithDialCode: "(•••) •••-••90"), + authOptions: AuthOptionsResponse( + trustedPhoneNumbers: nil, + trustedDevices: nil, + securityCode: .init(length: 6) + ), + sessionData: AppleSessionData(serviceKey: "", sessionID: "", scnt: "") + ) + .environmentObject(AppState()) } }