diff --git a/Mixin/UserInterface/Controllers/Setting/Diagnose/TIPDiagnosticViewController.swift b/Mixin/UserInterface/Controllers/Setting/Diagnose/TIPDiagnosticViewController.swift index c59a5092d9..7b2d0e7fae 100644 --- a/Mixin/UserInterface/Controllers/Setting/Diagnose/TIPDiagnosticViewController.swift +++ b/Mixin/UserInterface/Controllers/Setting/Diagnose/TIPDiagnosticViewController.swift @@ -11,6 +11,7 @@ class TIPDiagnosticViewController: SettingsTableViewController { SettingsRow(title: "Fail PIN Update Client Once", accessory: .switch(isOn: TIPDiagnostic.failPINUpdateClientSideOnce, isEnabled: true)), SettingsRow(title: "Fail Watch Once", accessory: .switch(isOn: TIPDiagnostic.failCounterWatchOnce, isEnabled: true)), SettingsRow(title: "Crash After PIN Update", accessory: .switch(isOn: TIPDiagnostic.crashAfterUpdatePIN, isEnabled: true)), + SettingsRow(title: "Invalid Nonce Once", accessory: .switch(isOn: TIPDiagnostic.invalidNonceOnce, isEnabled: true)), ]), SettingsSection(header: "UI Test", rows: [ SettingsRow(title: "UI Test On", accessory: .switch(isOn: TIPDiagnostic.uiTestOnly, isEnabled: true)), @@ -45,6 +46,8 @@ class TIPDiagnosticViewController: SettingsTableViewController { TIPDiagnostic.failCounterWatchOnce.toggle() case dataSource.sections[0].rows[4]: TIPDiagnostic.crashAfterUpdatePIN.toggle() + case dataSource.sections[0].rows[5]: + TIPDiagnostic.invalidNonceOnce.toggle() case dataSource.sections[1].rows[0]: TIPDiagnostic.uiTestOnly.toggle() default: diff --git a/Mixin/UserInterface/Controllers/Wallet/TIPActionViewController.swift b/Mixin/UserInterface/Controllers/Wallet/TIPActionViewController.swift index 3b848bedbf..1df6c73819 100644 --- a/Mixin/UserInterface/Controllers/Wallet/TIPActionViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/TIPActionViewController.swift @@ -63,6 +63,9 @@ class TIPActionViewController: UIViewController { } private func performAction() { + guard let accountCounterBefore = LoginManager.shared.account?.tipCounter else { + return + } switch action { case let .create(pin): titleLabel.text = R.string.localizable.create_pin() @@ -84,7 +87,7 @@ class TIPActionViewController: UIViewController { finish() } } catch { - await handle(error: error) + await handle(error: error, accountCounterBefore: accountCounterBefore) } } case let .change(old, new): @@ -119,7 +122,7 @@ class TIPActionViewController: UIViewController { finish() } } catch { - await handle(error: error) + await handle(error: error, accountCounterBefore: accountCounterBefore) } } case let .migrate(pin): @@ -142,7 +145,7 @@ class TIPActionViewController: UIViewController { finish() } } catch { - await handle(error: error) + await handle(error: error, accountCounterBefore: accountCounterBefore) } } } @@ -165,7 +168,7 @@ class TIPActionViewController: UIViewController { } } - private func handle(error: Error) async { + private func handle(error: Error, accountCounterBefore: UInt64) async { Logger.tip.error(category: "TIPAction", message: "Failed with: \(error)") do { if let context = try await TIP.checkCounter() { @@ -174,9 +177,18 @@ class TIPActionViewController: UIViewController { navigationController?.setViewControllers([intro], animated: true) } } else { - await MainActor.run { - Logger.tip.warn(category: "TIPAction", message: "No interruption is detected") - finish() + try await MainActor.run { + guard let accountCounterAfter = LoginManager.shared.account?.tipCounter else { + throw MixinAPIError.unauthorized + } + if accountCounterAfter == accountCounterBefore { + Logger.tip.error(category: "TIPAction", message: "Nothing changed") + let intro = TIPIntroViewController(action: action, changedNothingWith: error) + tipNavigationController?.setViewControllers([intro], animated: true) + } else { + Logger.tip.warn(category: "TIPAction", message: "No interruption is detected") + finish() + } } } } catch { diff --git a/Mixin/UserInterface/Controllers/Wallet/TIPIntroViewController.swift b/Mixin/UserInterface/Controllers/Wallet/TIPIntroViewController.swift index 323aa5ac6a..b56406e4ba 100644 --- a/Mixin/UserInterface/Controllers/Wallet/TIPIntroViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/TIPIntroViewController.swift @@ -7,7 +7,8 @@ class TIPIntroViewController: UIViewController { enum Interruption { case unknown case none - case confirmed(TIP.InterruptionContext) + case inputNeeded(TIP.InterruptionContext) + case noInputNeeded(TIPActionViewController.Action, Error) } private enum Status { @@ -44,26 +45,41 @@ class TIPIntroViewController: UIViewController { navigationController as? TIPNavigationViewController } - init(intent: TIP.Action) { + convenience init(intent: TIP.Action) { Logger.tip.info(category: "TIPIntro", message: "Init with intent: \(intent)") - self.intent = intent - self.interruption = .unknown - let nib = R.nib.tipIntroView - super.init(nibName: nib.name, bundle: nib.bundle) + self.init(intent: intent, interruption: .unknown) } - init(context: TIP.InterruptionContext) { + convenience init(context: TIP.InterruptionContext) { Logger.tip.info(category: "TIPIntro", message: "Init with context: \(context)") - self.intent = context.action - self.interruption = .confirmed(context) - let nib = R.nib.tipIntroView - super.init(nibName: nib.name, bundle: nib.bundle) + self.init(intent: context.action, interruption: .inputNeeded(context)) + } + + convenience init(action: TIPActionViewController.Action, changedNothingWith error: Error) { + Logger.tip.info(category: "TIPIntro", message: "Init with action: \(action.debugDescription), error: \(error)") + let intent: TIP.Action + switch action { + case .create: + intent = .create + case .change: + intent = .change + case .migrate: + intent = .migrate + } + self.init(intent: intent, interruption: .noInputNeeded(action, error)) } required init?(coder: NSCoder) { fatalError("Storyboard is not supported") } + private init(intent: TIP.Action, interruption: Interruption) { + self.intent = intent + self.interruption = interruption + let nib = R.nib.tipIntroView + super.init(nibName: nib.name, bundle: nib.bundle) + } + override func viewDidLoad() { super.viewDidLoad() contentStackView.setCustomSpacing(24, after: iconImageView) @@ -76,7 +92,7 @@ class TIPIntroViewController: UIViewController { switch interruption { case .unknown, .none: description = R.string.localizable.tip_creation_introduction() - case .confirmed: + case .inputNeeded, .noInputNeeded: description = R.string.localizable.creating_wallet_terminated_unexpectedly() } setNoticeHidden(false) @@ -85,7 +101,7 @@ class TIPIntroViewController: UIViewController { switch interruption { case .unknown, .none: description = R.string.localizable.tip_introduction() - case .confirmed: + case .inputNeeded, .noInputNeeded: description = R.string.localizable.changing_pin_terminated_unexpectedly() } setNoticeHidden(false) @@ -94,7 +110,7 @@ class TIPIntroViewController: UIViewController { switch interruption { case .unknown, .none: description = R.string.localizable.tip_introduction() - case .confirmed: + case .inputNeeded, .noInputNeeded: description = R.string.localizable.upgrading_tip_terminated_unexpectedly() } setNoticeHidden(false) @@ -112,11 +128,11 @@ class TIPIntroViewController: UIViewController { case .unknown: checkCounter() descriptionTextLabel.additionalLinksMap = linksMap - case .confirmed: - updateNextButtonAndStatusLabel(with: .waitingForUser) case .none: descriptionTextLabel.additionalLinksMap = linksMap updateNextButtonAndStatusLabel(with: .waitingForUser) + case .inputNeeded, .noInputNeeded: + updateNextButtonAndStatusLabel(with: .waitingForUser) } } @@ -157,7 +173,7 @@ class TIPIntroViewController: UIViewController { })) present(validator, animated: true) } - case .confirmed(let context): + case .inputNeeded(let context): switch context.action { case .migrate: let validator = TIPPopupInputViewController(action: .migrate({ pin in @@ -174,6 +190,9 @@ class TIPIntroViewController: UIViewController { })) present(validator, animated: true) } + case let .noInputNeeded(action, _): + let viewController = TIPActionViewController(action: action) + navigationController?.setViewControllers([viewController], animated: true) } } @@ -267,11 +286,20 @@ extension TIPIntroViewController { switch interruption { case .unknown, .none: setNextButtonTitleByIntent() - case .confirmed: + actionDescriptionLabel.text = nil + case .inputNeeded: nextButton.setTitle(R.string.localizable.continue(), for: .normal) + actionDescriptionLabel.text = nil + case let .noInputNeeded(_, error): + nextButton.setTitle(R.string.localizable.retry(), for: .normal) + if let error = error as? TIPNode.Error { + actionDescriptionLabel.text = error.description + } else { + actionDescriptionLabel.text = error.localizedDescription + } + actionDescriptionLabel.textColor = .mixinRed } nextButton.isBusy = false - actionDescriptionLabel.text = nil } } diff --git a/MixinServices/MixinServices/Services/Crypto/TIP/TIP.swift b/MixinServices/MixinServices/Services/Crypto/TIP/TIP.swift index abac284c76..ed01e26692 100644 --- a/MixinServices/MixinServices/Services/Crypto/TIP/TIP.swift +++ b/MixinServices/MixinServices/Services/Crypto/TIP/TIP.swift @@ -284,7 +284,9 @@ extension TIP { account = freshAccount } else { account = try await AccountAPI.me() - LoginManager.shared.setAccount(account) + await MainActor.run { + LoginManager.shared.setAccount(account) + } } guard let pinToken = AppGroupKeychain.pinToken else { throw Error.missingPINToken diff --git a/MixinServices/MixinServices/Services/Crypto/TIP/TIPDiagnostic.swift b/MixinServices/MixinServices/Services/Crypto/TIP/TIPDiagnostic.swift index 3bdf8d9f5e..7dbef4803a 100644 --- a/MixinServices/MixinServices/Services/Crypto/TIP/TIPDiagnostic.swift +++ b/MixinServices/MixinServices/Services/Crypto/TIP/TIPDiagnostic.swift @@ -32,14 +32,21 @@ public enum TIPDiagnostic { } @MainActor - public static var uiTestOnly = false { + public static var crashAfterUpdatePIN = false { didSet { updateDashboard() } } @MainActor - public static var crashAfterUpdatePIN = false { + public static var invalidNonceOnce = false { + didSet { + updateDashboard() + } + } + + @MainActor + public static var uiTestOnly = false { didSet { updateDashboard() } @@ -80,6 +87,7 @@ public enum TIPDiagnostic { Fail PIN Update Client: \(failPINUpdateClientSideOnce ? "ONCE" : " OFF") Fail Watch: \(failCounterWatchOnce ? "ONCE" : " OFF") Crash After PIN Update: \(crashAfterUpdatePIN ? " ON" : " OFF") + Invalid Nonce: \(invalidNonceOnce ? "ONCE" : " OFF") UI Test: \(uiTestOnly ? " ON" : " OFF") """ } diff --git a/MixinServices/MixinServices/Services/Crypto/TIP/TIPNode.swift b/MixinServices/MixinServices/Services/Crypto/TIP/TIPNode.swift index a26c12685a..71f13f82be 100644 --- a/MixinServices/MixinServices/Services/Crypto/TIP/TIPNode.swift +++ b/MixinServices/MixinServices/Services/Crypto/TIP/TIPNode.swift @@ -206,7 +206,18 @@ public enum TIPNode { assignee: Data?, progressHandler: (@MainActor (TIP.Progress) -> Void)? ) async throws -> [TIPSignResponseData] { +#if DEBUG + let nonce: UInt64 = await MainActor.run { + if TIPDiagnostic.invalidNonceOnce { + TIPDiagnostic.invalidNonceOnce = false + return 20 + } else { + return UInt64(Date().timeIntervalSince1970) + } + } +#else let nonce = UInt64(Date().timeIntervalSince1970) +#endif let grace = ephemeralGrace return await withTaskGroup(of: Result.self) { group in let retries = Accumulator(maxValue: maximumRetries)