mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
Allow selecting a phone number interactively
This commit is contained in:
parent
fb6ed58b3e
commit
c3806e2eff
6 changed files with 232 additions and 170 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: "")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue