Allow selecting a phone number interactively

This commit is contained in:
Brandon Evans 2020-12-05 09:36:23 -07:00
parent fb6ed58b3e
commit c3806e2eff
No known key found for this signature in database
GPG key ID: D58A4B8DB64F8E93
6 changed files with 232 additions and 170 deletions

View file

@ -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<AnyCancellable>()
let client = AppleAPI.Client()
@ -55,6 +61,8 @@ class AppState: ObservableObject {
// }
}
// MARK: - Authentication
func validateSession() -> AnyPublisher<Void, Error> {
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<AuthOptionsResponse.TrustedPhoneNumber, Swift.Error> {
// 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<AuthOptionsResponse.TrustedPhoneNumber, Swift.Error> in
// guard case AuthenticationError.invalidPhoneNumberIndex = error else {
// return Fail<AuthOptionsResponse.TrustedPhoneNumber, Swift.Error>(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<AuthenticationState, Error> {
// // 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<AuthenticationState, Error> 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<Error>) {
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<Void> in
// validateSession()

View file

@ -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<AuthenticationState, Error> {
public func requestSMSSecurityCode(to trustedPhoneNumber: AuthOptionsResponse.TrustedPhoneNumber, authOptions: AuthOptionsResponse, sessionData: AppleSessionData) -> AnyPublisher<AuthenticationState, Error> {
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<AuthenticationState, Error> {
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)

View file

@ -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)
}
}
}

View file

@ -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())
}
}

View file

@ -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: "")
)
}
}
}

View file

@ -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())
}
}