From 19381835268239dbfdd426a136d3b6e22c240cb7 Mon Sep 17 00:00:00 2001 From: Oleg Date: Wed, 16 Oct 2024 13:11:42 +0300 Subject: [PATCH 1/6] MOB-2206 - Show incorrect password message when request recovery kit (#683) --- .../MPCWalletsService/MPCWalletError.swift | 1 + .../FB_UD_MPCConnectionService.swift | 4 +- .../MPC/Recovery/MPCRequestRecoveryView.swift | 37 ++++++++++++++++--- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletError.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletError.swift index ca2c5dd43..5883cff92 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletError.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletError.swift @@ -12,6 +12,7 @@ enum MPCWalletError: String, LocalizedError { case incorrectPassword case messageSignDisabled case maintenanceEnabled + case wrongRecoveryPassword public var errorDescription: String? { return rawValue diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FB_UD_MPCConnectionService.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FB_UD_MPCConnectionService.swift index c3a3ac8f3..f5807eebb 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FB_UD_MPCConnectionService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FB_UD_MPCConnectionService.swift @@ -388,8 +388,8 @@ extension FB_UD_MPC.MPCConnectionService: MPCWalletProviderSubServiceProtocol { } catch { /// Temporary solution until clarified with the BE. if case NetworkLayerError.badResponseOrStatusCode(let code, _, _) = error, - code == 500 { - return connectedWalletDetails.email + code == 400 { + throw MPCWalletError.wrongRecoveryPassword } throw error } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Recovery/MPCRequestRecoveryView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Recovery/MPCRequestRecoveryView.swift index 3a6000964..2985bffaa 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Recovery/MPCRequestRecoveryView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Recovery/MPCRequestRecoveryView.swift @@ -15,6 +15,7 @@ struct MPCRequestRecoveryView: View, ViewAnalyticsLogger { let mpcWalletMetadata: MPCWalletMetadata @State private var passwordInput: String = "" @State private var isLoading: Bool = false + @State private var isWrongPasswordEntered: Bool = false @State private var path: [String] = [] @State private var isPresentingForgotPasswordView: Bool = false @State private var error: Error? @@ -43,6 +44,7 @@ struct MPCRequestRecoveryView: View, ViewAnalyticsLogger { MPCForgotPasswordView(isModallyPresented: true) } .displayError($error) + .animation(.default, value: isWrongPasswordEntered) .navigationDestination(for: String.self, destination: { email in MPCRecoveryRequestedView(email: email, closeCallback: close) @@ -73,12 +75,32 @@ private extension MPCRequestRecoveryView { @ViewBuilder func passwordInputView() -> some View { - UDTextFieldView(text: $passwordInput, - placeholder: String.Constants.password.localized(), - focusBehaviour: .activateOnAppear, - autocapitalization: .never, - autocorrectionDisabled: true, - isSecureInput: true) + VStack(spacing: 8) { + UDTextFieldView(text: $passwordInput, + placeholder: String.Constants.password.localized(), + focusBehaviour: .activateOnAppear, + autocapitalization: .never, + autocorrectionDisabled: true, + isSecureInput: true, + isErrorState: isWrongPasswordEntered) + if isWrongPasswordEntered { + incorrectPasswordView() + } + } + } + + @ViewBuilder + func incorrectPasswordView() -> some View { + HStack { + Image.alertCircle + .resizable() + .squareFrame(12) + Text(String.Constants.wrongPassword.localized()) + .font(.currentFont(size: 12, weight: .medium)) + Spacer() + } + .foregroundStyle(Color.foregroundDanger) + .padding(.leading, 16) } @ViewBuilder @@ -111,6 +133,7 @@ private extension MPCRequestRecoveryView { func confirmButtonPressed() { logButtonPressedAnalyticEvents(button: .confirm) + isWrongPasswordEntered = false Task { isLoading = true @@ -118,6 +141,8 @@ private extension MPCRequestRecoveryView { let email = try await mpcWalletsService.requestRecovery(password: passwordInput, by: mpcWalletMetadata) path.append(email) + } catch MPCWalletError.wrongRecoveryPassword { + self.isWrongPasswordEntered = true } catch { self.error = error } From 70d468628dc2a8e389ce4fbd6412f626dead030f Mon Sep 17 00:00:00 2001 From: Oleg Date: Tue, 22 Oct 2024 14:22:45 +0300 Subject: [PATCH 2/6] MOB-2208 - Prepare for MPC operation changes (#684) --- .../Entities/FB_UD_MPCOperationReadyResponse.swift | 1 + .../Fireblocks+UD/FB_UD_MPCConnectionService.swift | 6 ++++++ .../FB_UD_DefaultMPCConnectionNetworkService.swift | 13 ++++++++----- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Entities/FB_UD_MPCOperationReadyResponse.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Entities/FB_UD_MPCOperationReadyResponse.swift index 99935621c..02c2e5247 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Entities/FB_UD_MPCOperationReadyResponse.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Entities/FB_UD_MPCOperationReadyResponse.swift @@ -12,6 +12,7 @@ extension FB_UD_MPC { enum OperationReadyResponse { case txReady(txId: String) case signed(signature: String) + case finished(txHash: String) } } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FB_UD_MPCConnectionService.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FB_UD_MPCConnectionService.swift index f5807eebb..d355a0fe1 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FB_UD_MPCConnectionService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FB_UD_MPCConnectionService.swift @@ -216,6 +216,9 @@ extension FB_UD_MPC.MPCConnectionService: MPCWalletProviderSubServiceProtocol { case .signed(let signature): logMPC("It took \(Date().timeIntervalSince(start)) to sign message") return signature + case .finished: + logMPC("It took \(Date().timeIntervalSince(start)) to sign message") + throw MPCConnectionServiceError.incorrectOperationState } } } @@ -306,6 +309,9 @@ extension FB_UD_MPC.MPCConnectionService: MPCWalletProviderSubServiceProtocol { operationId: operationId) logMPC("It took \(Date().timeIntervalSince(start)) to finish tx") return txHash + case .finished(let txHash): + logMPC("It took \(Date().timeIntervalSince(start)) to finish tx") + return txHash case .signed: logMPC("It took \(Date().timeIntervalSince(start)) to finish tx") throw MPCConnectionServiceError.incorrectOperationState diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_DefaultMPCConnectionNetworkService.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_DefaultMPCConnectionNetworkService.swift index 62569f775..026fb9919 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_DefaultMPCConnectionNetworkService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_DefaultMPCConnectionNetworkService.swift @@ -322,10 +322,12 @@ extension FB_UD_MPC { } return .txReady(txId: transactionId) } else { - guard let signature = operation.result?.signature else { - throw MPCNetworkServiceError.missingSignatureInSignTransactionOperation + if let signature = operation.result?.signature { + return .signed(signature: signature) + } else if let txHash = operation.transaction?.id { + return .finished(txHash: txHash) } - return .signed(signature: signature) + throw MPCNetworkServiceError.completedTransactionMissingResultValue } } @@ -345,10 +347,10 @@ extension FB_UD_MPC { let operation = try await waitForOperationStatuses(accessToken: accessToken, operationId: operationId, statuses: [.processing, .completed]) - guard let signature = operation.transaction?.id else { + guard let txHash = operation.transaction?.id else { throw MPCNetworkServiceError.missingTxIdInTransactionOperation } - return signature + return txHash } func fetchCryptoPortfolioForMPC(wallet: String, accessToken: String) async throws -> [WalletTokenPortfolio] { @@ -469,6 +471,7 @@ extension FB_UD_MPC { case waitForKeyOperationStatusTimeout case missingVendorIdInSignTransactionOperation case missingSignatureInSignTransactionOperation + case completedTransactionMissingResultValue case missingTxIdInTransactionOperation case badRequestData case operationFailed From 78a63151ebfd01a001ca8c86e09953beb4ff5f72 Mon Sep 17 00:00:00 2001 From: Oleg Date: Wed, 30 Oct 2024 15:41:33 +0200 Subject: [PATCH 3/6] MOB-2207 - Handle MPC recovery deep link (#685) * Add reset password event to list of known * Implemented UI for enter password view * Localize view * Present reset flow on DL * Added API request to reset password * Activate wallet function refactoring * MPC wallet service refactoring * Prepare reset mpc flow entities * Added enter code step * Added final step with wallet activation * Multiple fixes * Fixed creation of new recovery kit after recovery * Handle recovery during onboarding * Check for wallet already in the list before adding * Call class function to get fireblocks logs --- .../Entities/PreviewMPCConnector.swift | 7 + .../project.pbxproj | 68 +++++++ .../Extensions/Extension-String+Preview.swift | 2 + .../Modules/Home/HomeTabRouter.swift | 23 +++ .../Modules/Home/HomeTabView.swift | 8 + .../MPCActivateWalletInAppView.swift | 2 +- .../Activating/MPCActivateWalletView.swift | 24 ++- ...boardingActivateWalletViewController.swift | 2 +- .../MPCActivateWalletReconnectView.swift | 2 +- .../FB_UD_MPCConnectionService.swift | 59 ++++-- .../Fireblocks/FireblocksConnector.swift | 2 +- ...D_DefaultMPCConnectionNetworkService.swift | 22 ++ .../FB_UD_MPCConnectionNetworkService.swift | 4 + .../Network/FB_UD_MPCNetwork.swift | 5 +- .../MPCWalletProviderSubServiceProtocol.swift | 2 +- .../MPCWalletsService/MPCWalletsService.swift | 4 +- .../MPCWalletsServiceProtocol.swift | 2 +- .../MPC/MPCWalletsService/SetupMPCFlow.swift | 27 +++ ...MPCActivateWalletInAppAfterClaimView.swift | 2 +- ...haseActivateAfterClaimViewController.swift | 4 +- .../MPCWalletPasswordRequirements.swift | 14 ++ .../MPCWalletPasswordValidator.swift | 28 +++ ...urchaseMPCWalletTakeoverPasswordView.swift | 42 +--- .../MPCResetPasswordActivateView.swift | 37 ++++ .../ResetPassword/MPCResetPasswordData.swift | 15 ++ .../MPCResetPasswordEnterCodeView.swift | 38 ++++ .../MPCResetPasswordEnterPasswordView.swift | 189 ++++++++++++++++++ .../ResetPassword/MPCResetPasswordFlow.swift | 30 +++ ...PCResetPasswordNavigationDestination.swift | 30 +++ .../MPCResetPasswordRootView.swift | 71 +++++++ .../MPCResetPasswordViewModel.swift | 53 +++++ .../OnboardingNavigationController.swift | 26 +++ .../Modules/Settings/SettingsView.swift | 14 +- .../AnalyticsServiceEnvironment.swift | 5 +- .../CoreAppCoordinator.swift | 11 +- .../DeepLinksService/DeepLinksService.swift | 26 ++- .../DeepLinksServiceProtocol.swift | 1 + .../Localization/en.lproj/Localizable.strings | 2 + 38 files changed, 806 insertions(+), 97 deletions(-) create mode 100644 unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/SetupMPCFlow.swift create mode 100644 unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/MPCWalletPasswordRequirements.swift create mode 100644 unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordActivateView.swift create mode 100644 unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordData.swift create mode 100644 unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordEnterCodeView.swift create mode 100644 unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordEnterPasswordView.swift create mode 100644 unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordFlow.swift create mode 100644 unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordNavigationDestination.swift create mode 100644 unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordRootView.swift create mode 100644 unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordViewModel.swift diff --git a/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewMPCConnector.swift b/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewMPCConnector.swift index a23e2d069..3ecce400e 100644 --- a/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewMPCConnector.swift +++ b/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewMPCConnector.swift @@ -142,6 +142,13 @@ extension FB_UD_MPC { func requestRecovery(_ accessToken: String, password: String) async throws { await Task.sleep(seconds: 0.5) } + + func resetPassword(accessToken: String, + recoveryToken: String, + newRecoveryPhrase: String, + requestId: String) async throws { + await Task.sleep(seconds: 0.5) + } } struct MPCWalletsDefaultDataStorage: MPCWalletsDataStorage { diff --git a/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj b/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj index 0e64e262c..86fef2b26 100644 --- a/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj +++ b/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj @@ -472,6 +472,10 @@ C61C333A2B90CE6600BD11F5 /* MessagingChatUserDisplayInfoImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61C33392B90CE6600BD11F5 /* MessagingChatUserDisplayInfoImageLoader.swift */; }; C61C333B2B90CE6600BD11F5 /* MessagingChatUserDisplayInfoImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61C33392B90CE6600BD11F5 /* MessagingChatUserDisplayInfoImageLoader.swift */; }; C61C50002820E39600D1110A /* PullUpSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61C4FFF2820E39600D1110A /* PullUpSelectionView.swift */; }; + C61D0D722CC67D3B00B76BF5 /* MPCWalletPasswordRequirements.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61D0D712CC67D3B00B76BF5 /* MPCWalletPasswordRequirements.swift */; }; + C61D0D732CC67D3B00B76BF5 /* MPCWalletPasswordRequirements.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61D0D712CC67D3B00B76BF5 /* MPCWalletPasswordRequirements.swift */; }; + C61D0D752CC684B100B76BF5 /* MPCResetPasswordData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61D0D742CC684B100B76BF5 /* MPCResetPasswordData.swift */; }; + C61D0D762CC684B100B76BF5 /* MPCResetPasswordData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61D0D742CC684B100B76BF5 /* MPCResetPasswordData.swift */; }; C61DB0FE2B95872500CDA243 /* PublicProfileLargeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61DB0FD2B95872500CDA243 /* PublicProfileLargeTextView.swift */; }; C61DB0FF2B95872900CDA243 /* PublicProfileLargeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61DB0FD2B95872500CDA243 /* PublicProfileLargeTextView.swift */; }; C61DB1052B95879200CDA243 /* PublicProfilePrimaryLargeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61DB1042B95879200CDA243 /* PublicProfilePrimaryLargeTextView.swift */; }; @@ -1335,6 +1339,20 @@ C688C1AB2B84A4CF00BD233A /* UnencryptedMessageInfoPullUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C686BF212A60F6390036C7C1 /* UnencryptedMessageInfoPullUpView.swift */; }; C68953882834CCCA0046CBA3 /* CollectionGenericContentViewReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68953872834CCCA0046CBA3 /* CollectionGenericContentViewReusableView.swift */; }; C68953922834CD280046CBA3 /* CollectionTextHeaderReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68953912834CD280046CBA3 /* CollectionTextHeaderReusableView.swift */; }; + C68980732CCA320700A4CFC0 /* SetupMPCFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68980722CCA320700A4CFC0 /* SetupMPCFlow.swift */; }; + C68980742CCA320700A4CFC0 /* SetupMPCFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68980722CCA320700A4CFC0 /* SetupMPCFlow.swift */; }; + C68980762CCA33B700A4CFC0 /* MPCResetPasswordFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68980752CCA33B700A4CFC0 /* MPCResetPasswordFlow.swift */; }; + C68980772CCA33B700A4CFC0 /* MPCResetPasswordFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68980752CCA33B700A4CFC0 /* MPCResetPasswordFlow.swift */; }; + C68980792CCA33D500A4CFC0 /* MPCResetPasswordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68980782CCA33D500A4CFC0 /* MPCResetPasswordViewModel.swift */; }; + C689807A2CCA33D500A4CFC0 /* MPCResetPasswordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68980782CCA33D500A4CFC0 /* MPCResetPasswordViewModel.swift */; }; + C689807C2CCA34B100A4CFC0 /* MPCResetPasswordNavigationDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = C689807B2CCA34B100A4CFC0 /* MPCResetPasswordNavigationDestination.swift */; }; + C689807D2CCA34B100A4CFC0 /* MPCResetPasswordNavigationDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = C689807B2CCA34B100A4CFC0 /* MPCResetPasswordNavigationDestination.swift */; }; + C689807F2CCA353C00A4CFC0 /* MPCResetPasswordRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C689807E2CCA353C00A4CFC0 /* MPCResetPasswordRootView.swift */; }; + C68980802CCA353C00A4CFC0 /* MPCResetPasswordRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C689807E2CCA353C00A4CFC0 /* MPCResetPasswordRootView.swift */; }; + C68980822CCA39E300A4CFC0 /* MPCResetPasswordEnterCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68980812CCA39E300A4CFC0 /* MPCResetPasswordEnterCodeView.swift */; }; + C68980832CCA39E300A4CFC0 /* MPCResetPasswordEnterCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68980812CCA39E300A4CFC0 /* MPCResetPasswordEnterCodeView.swift */; }; + C68980852CCA3AAE00A4CFC0 /* MPCResetPasswordActivateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68980842CCA3AAE00A4CFC0 /* MPCResetPasswordActivateView.swift */; }; + C68980862CCA3AAE00A4CFC0 /* MPCResetPasswordActivateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68980842CCA3AAE00A4CFC0 /* MPCResetPasswordActivateView.swift */; }; C689C1732ADE484300AA0186 /* LaunchDarkly in Frameworks */ = {isa = PBXBuildFile; productRef = C689C1722ADE484300AA0186 /* LaunchDarkly */; }; C689C1752ADE56AF00AA0186 /* UDFeatureFlagsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C689C1742ADE56AF00AA0186 /* UDFeatureFlagsService.swift */; }; C689C1772ADE693900AA0186 /* LaunchDarklyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C689C1762ADE693900AA0186 /* LaunchDarklyService.swift */; }; @@ -2063,6 +2081,8 @@ C6D2DF132C4E4CEB00F08A6F /* IconTitleSelectionGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D2DF112C4E4CEB00F08A6F /* IconTitleSelectionGridView.swift */; }; C6D2F13028729585005F4F2E /* DomainDisplayInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D2F12F28729585005F4F2E /* DomainDisplayInfo.swift */; }; C6D2F15328757C25005F4F2E /* UIImage+SVG.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D2F15228757C25005F4F2E /* UIImage+SVG.swift */; }; + C6D396AF2CC6771100FED58A /* MPCResetPasswordEnterPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D396AE2CC6771100FED58A /* MPCResetPasswordEnterPasswordView.swift */; }; + C6D396B02CC6771100FED58A /* MPCResetPasswordEnterPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D396AE2CC6771100FED58A /* MPCResetPasswordEnterPasswordView.swift */; }; C6D3B9B52A6EB8DA0091B279 /* MessagingChannelsAPIServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D3B9B42A6EB8DA0091B279 /* MessagingChannelsAPIServiceProtocol.swift */; }; C6D3B9BA2A6EB8F80091B279 /* PushMessagingChannelsAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D3B9B92A6EB8F80091B279 /* PushMessagingChannelsAPIService.swift */; }; C6D3B9BF2A6EBC2B0091B279 /* PushServiceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D3B9BE2A6EBC2B0091B279 /* PushServiceHelper.swift */; }; @@ -3053,6 +3073,8 @@ C61C332C2B906DCE00BD11F5 /* MockEntitiesFabric+Explore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MockEntitiesFabric+Explore.swift"; sourceTree = ""; }; C61C33392B90CE6600BD11F5 /* MessagingChatUserDisplayInfoImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagingChatUserDisplayInfoImageLoader.swift; sourceTree = ""; }; C61C4FFF2820E39600D1110A /* PullUpSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullUpSelectionView.swift; sourceTree = ""; }; + C61D0D712CC67D3B00B76BF5 /* MPCWalletPasswordRequirements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCWalletPasswordRequirements.swift; sourceTree = ""; }; + C61D0D742CC684B100B76BF5 /* MPCResetPasswordData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCResetPasswordData.swift; sourceTree = ""; }; C61DB0FD2B95872500CDA243 /* PublicProfileLargeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicProfileLargeTextView.swift; sourceTree = ""; }; C61DB1042B95879200CDA243 /* PublicProfilePrimaryLargeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicProfilePrimaryLargeTextView.swift; sourceTree = ""; }; C61DB1072B9587B600CDA243 /* PublicProfileSecondaryLargeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicProfileSecondaryLargeTextView.swift; sourceTree = ""; }; @@ -3664,6 +3686,13 @@ C688C1A32B84946C00BD233A /* SelectableChatRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableChatRowView.swift; sourceTree = ""; }; C68953872834CCCA0046CBA3 /* CollectionGenericContentViewReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionGenericContentViewReusableView.swift; sourceTree = ""; }; C68953912834CD280046CBA3 /* CollectionTextHeaderReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionTextHeaderReusableView.swift; sourceTree = ""; }; + C68980722CCA320700A4CFC0 /* SetupMPCFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupMPCFlow.swift; sourceTree = ""; }; + C68980752CCA33B700A4CFC0 /* MPCResetPasswordFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCResetPasswordFlow.swift; sourceTree = ""; }; + C68980782CCA33D500A4CFC0 /* MPCResetPasswordViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCResetPasswordViewModel.swift; sourceTree = ""; }; + C689807B2CCA34B100A4CFC0 /* MPCResetPasswordNavigationDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCResetPasswordNavigationDestination.swift; sourceTree = ""; }; + C689807E2CCA353C00A4CFC0 /* MPCResetPasswordRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCResetPasswordRootView.swift; sourceTree = ""; }; + C68980812CCA39E300A4CFC0 /* MPCResetPasswordEnterCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCResetPasswordEnterCodeView.swift; sourceTree = ""; }; + C68980842CCA3AAE00A4CFC0 /* MPCResetPasswordActivateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCResetPasswordActivateView.swift; sourceTree = ""; }; C689C1742ADE56AF00AA0186 /* UDFeatureFlagsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDFeatureFlagsService.swift; sourceTree = ""; }; C689C1762ADE693900AA0186 /* LaunchDarklyService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchDarklyService.swift; sourceTree = ""; }; C689C1792ADE6D2500AA0186 /* UDFeatureFlagsServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDFeatureFlagsServiceProtocol.swift; sourceTree = ""; }; @@ -4013,6 +4042,7 @@ C6D2DF112C4E4CEB00F08A6F /* IconTitleSelectionGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconTitleSelectionGridView.swift; sourceTree = ""; }; C6D2F12F28729585005F4F2E /* DomainDisplayInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainDisplayInfo.swift; sourceTree = ""; }; C6D2F15228757C25005F4F2E /* UIImage+SVG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SVG.swift"; sourceTree = ""; }; + C6D396AE2CC6771100FED58A /* MPCResetPasswordEnterPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCResetPasswordEnterPasswordView.swift; sourceTree = ""; }; C6D3B9B42A6EB8DA0091B279 /* MessagingChannelsAPIServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagingChannelsAPIServiceProtocol.swift; sourceTree = ""; }; C6D3B9B92A6EB8F80091B279 /* PushMessagingChannelsAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessagingChannelsAPIService.swift; sourceTree = ""; }; C6D3B9BE2A6EBC2B0091B279 /* PushServiceHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushServiceHelper.swift; sourceTree = ""; }; @@ -7121,6 +7151,7 @@ C6952A222BC4E33000F4B475 /* MPCWalletProvider.swift */, C6952A4F2BC531E000F4B475 /* MPCWalletMetadata.swift */, C632BE4F2BA59D8400C95B2D /* SetupMPCWalletStep.swift */, + C68980722CCA320700A4CFC0 /* SetupMPCFlow.swift */, C6952A312BC4EDFA00F4B475 /* MPCWalletSubServices */, ); path = MPCWalletsService; @@ -7172,6 +7203,7 @@ C68F15752BD769830049BFA2 /* MPCWalletStateCardView.swift */, C640D6112C48087C006B21C3 /* AnimatedMPCWalletGridMask.swift */, C640F3542C06FC66009EB0F9 /* MPCWalletPasswordValidator.swift */, + C61D0D712CC67D3B00B76BF5 /* MPCWalletPasswordRequirements.swift */, C665567C2BF73D8200F0BD7A /* TakeoverEmail */, C6C00A1D2C7C5AF90041324E /* TakeoverPassword */, C64F9D912C7730E200262F1B /* EnterTakeoverCode */, @@ -7814,6 +7846,7 @@ C6BF801B2BA2B53F00CC4CB3 /* MPC */ = { isa = PBXGroup; children = ( + C6D396AD2CC6760400FED58A /* ResetPassword */, C69CA5CB2CB5140200C30DF7 /* Recovery */, C640F3712C082C2E009EB0F9 /* Activate */, C6952A5B2BC6648900F4B475 /* PurchaseMPCFlow */, @@ -8044,6 +8077,21 @@ path = CollectionReusableViews; sourceTree = ""; }; + C6D396AD2CC6760400FED58A /* ResetPassword */ = { + isa = PBXGroup; + children = ( + C61D0D742CC684B100B76BF5 /* MPCResetPasswordData.swift */, + C68980752CCA33B700A4CFC0 /* MPCResetPasswordFlow.swift */, + C68980782CCA33D500A4CFC0 /* MPCResetPasswordViewModel.swift */, + C689807B2CCA34B100A4CFC0 /* MPCResetPasswordNavigationDestination.swift */, + C689807E2CCA353C00A4CFC0 /* MPCResetPasswordRootView.swift */, + C6D396AE2CC6771100FED58A /* MPCResetPasswordEnterPasswordView.swift */, + C68980812CCA39E300A4CFC0 /* MPCResetPasswordEnterCodeView.swift */, + C68980842CCA3AAE00A4CFC0 /* MPCResetPasswordActivateView.swift */, + ); + path = ResetPassword; + sourceTree = ""; + }; C6D3B9B32A6EB85E0091B279 /* ChannelsAPI */ = { isa = PBXGroup; children = ( @@ -9296,6 +9344,7 @@ C64513072808089400413C51 /* UserDefaults.swift in Sources */, C6C9959A289D313D00367362 /* CNavigationControllerChild.swift in Sources */, C6DF9866290F81D40098733A /* WebsiteURLValidator.swift in Sources */, + C68980822CCA39E300A4CFC0 /* MPCResetPasswordEnterCodeView.swift in Sources */, C65DEA3E29B1B0CC00FF142B /* WalletConnectExternalWalletConnectionWaiter.swift in Sources */, C617370029D68F1B00E6686D /* BadgeLeaderboardSelectionItem.swift in Sources */, C62396862A288A9E00363F60 /* MessagingChatMessage.swift in Sources */, @@ -9435,6 +9484,7 @@ C6B65F652B550471006D1812 /* HomeWalletMoreTokensView.swift in Sources */, C6C8F9582B2185F800A9834D /* KeychainKey.swift in Sources */, C671E39F28FE974900A2B3A0 /* UDCarouselFeature.swift in Sources */, + C68980862CCA3AAE00A4CFC0 /* MPCResetPasswordActivateView.swift in Sources */, C6C8F9552B2185A900A9834D /* MintingError.swift in Sources */, C650F8BD2BA4182900BDA099 /* FB_UD_MPCConnectedWalletDetails.swift in Sources */, C6B65F6C2B550E72006D1812 /* NFTModelChain.swift in Sources */, @@ -9445,6 +9495,7 @@ C6AF12402A67C72200F89D2E /* MessagingServiceDataRefreshManager.swift in Sources */, C6668946281151F6002062B4 /* TertiaryButton.swift in Sources */, C60982462823F82000546392 /* CreateBackupPasswordBasePresenter.swift in Sources */, + C689807A2CCA33D500A4CFC0 /* MPCResetPasswordViewModel.swift in Sources */, C6952A1F2BC4DD8500F4B475 /* FB_UD_MPC.swift in Sources */, C65DEA7B29B5837600FF142B /* MockWalletConnectExternalWalletHandler.swift in Sources */, C6170EC12B79E8E9008E9C93 /* UDListSectionView.swift in Sources */, @@ -9468,9 +9519,11 @@ 290136B72BFBFAAE00AB126D /* EIP712View.swift in Sources */, C60C29912834E9C200626851 /* ResizableRoundedWalletBadgeImageView.swift in Sources */, C666E07629C9F1900003DECB /* LoginWithEmailViewController.swift in Sources */, + C68980802CCA353C00A4CFC0 /* MPCResetPasswordRootView.swift in Sources */, C6ECBF9628D2E06800E94309 /* GenericBadgeView.swift in Sources */, C6F433382BB257F7000C5E46 /* ConfirmTransferDomainView.swift in Sources */, C63095F22B0DA66400205054 /* MockFirebaseInteractionsService.swift in Sources */, + C61D0D732CC67D3B00B76BF5 /* MPCWalletPasswordRequirements.swift in Sources */, C63902B828E2FB5B00E16B42 /* PaymentError.swift in Sources */, C6109E3328E5AB310027D5D8 /* AuthentificationService.swift in Sources */, C61807BD2B19A0AA0032E543 /* Extension-String+Preview.swift in Sources */, @@ -9707,6 +9760,7 @@ C60610D929A5FEDA005DC0D5 /* MockWCRequestsHandlingService.swift in Sources */, C67213D62BAA8DB60075B9C7 /* SendCryptoAssetSelectReceiverFollowingRowView.swift in Sources */, C669C3A52912AD4200837F21 /* DomainProfileBadgeCell.swift in Sources */, + C68980762CCA33B700A4CFC0 /* MPCResetPasswordFlow.swift in Sources */, C61ECD742A20E87100E97D70 /* PushGroupChatDTO.swift in Sources */, C64F6C662C51086400D89FEF /* MaintenanceDetailsFullView.swift in Sources */, C673BA3B2A82789B001FD763 /* MessagingChannelsWebSocketsServiceProtocol.swift in Sources */, @@ -9837,6 +9891,7 @@ C68BA7592BBD10AF00FF524C /* WalletTransactionTypeIndicatorView.swift in Sources */, C640D6122C48087C006B21C3 /* AnimatedMPCWalletGridMask.swift in Sources */, C6C9DEAE2834BD9300BAC36F /* CopyWalletAddressPullUpHandler.swift in Sources */, + C61D0D752CC684B100B76BF5 /* MPCResetPasswordData.swift in Sources */, C623967C2A288A8A00363F60 /* MessagingChatMessageDisplayType.swift in Sources */, C6C57C3D2869C0890093FD8C /* UDDomainSharingWatchCardView.swift in Sources */, C6B761EA2BB3EF5A00773943 /* InMemoryWalletTransactionsCache.swift in Sources */, @@ -9855,6 +9910,7 @@ C68F15672BD645CE0049BFA2 /* MPCActivateWalletView.swift in Sources */, C6C99586289D313D00367362 /* CNavigationTransitionHandler.swift in Sources */, C62C3FA1283F60720094FC93 /* DeepLinksServiceProtocol.swift in Sources */, + C68980742CCA320700A4CFC0 /* SetupMPCFlow.swift in Sources */, C664B670291542E300A76154 /* DomainProfileWeb3WebsiteCell.swift in Sources */, C65272212A14B32F001A084C /* MessagingServiceProtocol.swift in Sources */, C688C17C2B8443CA00BD233A /* ChatListEmptyStateView.swift in Sources */, @@ -9890,6 +9946,7 @@ C67B1DD52BFEF58800C2A4DA /* ReconnectMPCWalletRootView.swift in Sources */, C6D3B9B52A6EB8DA0091B279 /* MessagingChannelsAPIServiceProtocol.swift in Sources */, C6D8FF272B82FAFB0094A21E /* UnknownMessageRowView.swift in Sources */, + C6D396B02CC6771100FED58A /* MPCResetPasswordEnterPasswordView.swift in Sources */, C655CA9C2B16E5BE00FDA063 /* UDListItemView.swift in Sources */, C679B5FB2919327100F543A7 /* DomainProfileSectionDataChangeType.swift in Sources */, C640F3732C085398009EB0F9 /* InAppAddWalletView.swift in Sources */, @@ -9956,6 +10013,7 @@ C6EECEB12833BBE300978ED5 /* DomainRecordsData.swift in Sources */, C62396772A288A5C00363F60 /* MessagingChatMessageTextTypeDisplayInfo.swift in Sources */, 3026FA1727E7678200FE058B /* CreateWalletViewController.swift in Sources */, + C689807D2CCA34B100A4CFC0 /* MPCResetPasswordNavigationDestination.swift in Sources */, C609827628251FBA00546392 /* ImportNewWalletPresenter.swift in Sources */, C630E4B62B7F58BC008F3269 /* MessageInputView.swift in Sources */, C6D2DF122C4E4CEB00F08A6F /* IconTitleSelectionGridView.swift in Sources */, @@ -10378,6 +10436,7 @@ C6A4400A2C0448530042FFCC /* UDFeatureFlagsServiceEnvironmentKey.swift in Sources */, C688C1802B845FE500BD233A /* ChatListUserRowView.swift in Sources */, C6A231FD2BEB494D0037E093 /* WalletDetailsDomainItemView.swift in Sources */, + C68980772CCA33B700A4CFC0 /* MPCResetPasswordFlow.swift in Sources */, C6B761F12BB3F9D900773943 /* WalletTransactionsResponse.swift in Sources */, C6960C3E2B19980200B79E28 /* MessagingServiceIdentifier.swift in Sources */, C61808002B19A8B80032E543 /* MessagingChatUserDisplayInfo.swift in Sources */, @@ -10503,6 +10562,7 @@ C688C1962B8483D700BD233A /* ChatMessagesEmptyView.swift in Sources */, C6D645C32B1DBD3900D724AC /* CNavigationTransitionHandler.swift in Sources */, C6C8F8B72B2182CF00A9834D /* MintDomainsConfigurationSelectionCell.swift in Sources */, + C68980792CCA33D500A4CFC0 /* MPCResetPasswordViewModel.swift in Sources */, C61807C22B19A2E70032E543 /* PermissionsService.swift in Sources */, C6D646B72B1ED18F00D724AC /* SaveDomainImageTypePullUpView.swift in Sources */, C6BEC94B2C00382900F21FB6 /* EIP712View.swift in Sources */, @@ -10525,6 +10585,7 @@ C6C00A232C7C5B600041324E /* PurchaseMPCWalletTakeoverPasswordInAppView.swift in Sources */, C6D646762B1ED11B00D724AC /* CryptoEditingGroupedRecord.swift in Sources */, C6D6467A2B1ED12100D724AC /* DomainProfileGenericChangeDescription.swift in Sources */, + C6D396AF2CC6771100FED58A /* MPCResetPasswordEnterPasswordView.swift in Sources */, C671CD172BC68070005DA2FB /* PurchaseDomainsServiceProtocol.swift in Sources */, C630E4BA2B7F5BAC008F3269 /* Padding.swift in Sources */, C6B540C22BA3F77200A41D42 /* ViewPullUpListItemView.swift in Sources */, @@ -10591,6 +10652,7 @@ C6631BCC2C6B4B7F0045186D /* HomeWalletMintingInProgressSectionView.swift in Sources */, C6D646362B1DC44C00D724AC /* SecondaryDangerButton.swift in Sources */, C6D6477C2B1EE0ED00D724AC /* CropImageViewController.swift in Sources */, + C68980732CCA320700A4CFC0 /* SetupMPCFlow.swift in Sources */, C6D6460B2B1DC06B00D724AC /* OnboardingData.swift in Sources */, C6D646CE2B1ED27900D724AC /* ViewWithDashesProgress.swift in Sources */, C6D6471D2B1ED85C00D724AC /* WalletError.swift in Sources */, @@ -10967,6 +11029,7 @@ C6D646262B1DC29E00D724AC /* PullUpViewController.swift in Sources */, C61808682B19BC050032E543 /* UDButtonImage.swift in Sources */, C6C8F9652B21876400A9834D /* SwiftUIZoomableContainer.swift in Sources */, + C68980832CCA39E300A4CFC0 /* MPCResetPasswordEnterCodeView.swift in Sources */, C6534A982BBFBA10008EEBB5 /* HomeExploreSuggestedProfilesSectionView.swift in Sources */, C6D645F62B1DBF0B00D724AC /* UDConfigurableButton.swift in Sources */, C6D646342B1DC43D00D724AC /* TextTertiaryButton.swift in Sources */, @@ -11020,6 +11083,7 @@ C62BDA422B5E4104008E21AD /* MessagingBlockUserInChatType.swift in Sources */, C63AD0AB2B957D0900BF8C83 /* PublicProfileTokensSectionView.swift in Sources */, C6D647822B1EE22D00D724AC /* UITableView.swift in Sources */, + C61D0D762CC684B100B76BF5 /* MPCResetPasswordData.swift in Sources */, C6C8F9952B218BBB00A9834D /* Double.swift in Sources */, C6D6463C2B1DC53700D724AC /* WebViewController.swift in Sources */, C6D646A22B1ED15A00D724AC /* PublicProfileSocialsListView.swift in Sources */, @@ -11133,6 +11197,7 @@ C6C8F8662B21811000A9834D /* UDWalletBackUpError.swift in Sources */, C67681352BEC9ECD0093AFA0 /* UDListItemInCollectionButtonPaddingModifier.swift in Sources */, C61807B72B199E370032E543 /* PreviewUIImage.swift in Sources */, + C61D0D722CC67D3B00B76BF5 /* MPCWalletPasswordRequirements.swift in Sources */, C671CD1E2BC680B9005DA2FB /* PendingPurchasedDomain.swift in Sources */, C6B65F852B5517FB006D1812 /* HomeWalletNFTsCollectionSectionView.swift in Sources */, C6D646F62B1ED65200D724AC /* Extension-String+Common.swift in Sources */, @@ -11274,10 +11339,12 @@ C688C1A22B8490F700BD233A /* ChatRequestsListViewModel.swift in Sources */, C6D6470B2B1ED7CA00D724AC /* MessagingChatSender.swift in Sources */, C61807BB2B199FD10032E543 /* PreviewData.swift in Sources */, + C68980852CCA3AAE00A4CFC0 /* MPCResetPasswordActivateView.swift in Sources */, C6D645772B1D721D00D724AC /* PurchaseDomainsEnterDiscountCodeView.swift in Sources */, C671CD282BC6D3C4005DA2FB /* PurchaseMPCWalletCart.swift in Sources */, C6D645E22B1DBD9000D724AC /* BasePresenterProtocol.swift in Sources */, C6960C662B199B6F00B79E28 /* PreviewDomainItem.swift in Sources */, + C689807F2CCA353C00A4CFC0 /* MPCResetPasswordRootView.swift in Sources */, C6B761D02BB3C11800773943 /* HomeTab.swift in Sources */, C618087E2B19BC380032E543 /* DomainAvatarImageView.swift in Sources */, C6D6464F2B1ED0F300D724AC /* ManageDomainLoadingCell.swift in Sources */, @@ -11400,6 +11467,7 @@ C607A5D52B32A02A0088ECF3 /* CloseButtonView.swift in Sources */, C61808522B19BA750032E543 /* Task.swift in Sources */, C6B761FA2BB4011D00773943 /* HomeActivityNavigationDestination.swift in Sources */, + C689807C2CCA34B100A4CFC0 /* MPCResetPasswordNavigationDestination.swift in Sources */, C6C8F87F2B21827700A9834D /* LoginFlowNavigationController.swift in Sources */, C6D647052B1ED7C300D724AC /* MessagingCommunitiesChatDetails.swift in Sources */, C6D647482B1EDA6E00D724AC /* PreviewSignPaymentTransactionUIConfiguration.swift in Sources */, diff --git a/unstoppable-ios-app/domains-manager-ios/Extensions/Extension-String+Preview.swift b/unstoppable-ios-app/domains-manager-ios/Extensions/Extension-String+Preview.swift index 85f6c1c77..7086d6007 100644 --- a/unstoppable-ios-app/domains-manager-ios/Extensions/Extension-String+Preview.swift +++ b/unstoppable-ios-app/domains-manager-ios/Extensions/Extension-String+Preview.swift @@ -1321,6 +1321,8 @@ extension String { static let mpcRecoveryRequestedSubtitleHighlights = "MPC_RECOVERY_REQUESTED_SUBTITLE_HIGHLIGHTS" static let mpcRecoveryRequestedHintNotShare = "MPC_RECOVERY_REQUESTED_HINT_NOT_SHARE" static let mpcRecoveryRequestedHintPreviousInactive = "MPC_RECOVERY_REQUESTED_HINT_PREVIOUS_INACTIVE" + static let mpcResetPasswordTitle = "MPC_RESET_PASSWORD_TITLE" + static let mpcResetPasswordSubtitle = "MPC_RESET_PASSWORD_SUBTITLE" // Send crypto first time static let sendCryptoFirstTimePullUpTitle = "SEND_CRYPTO_FIRST_TIME_PULL_UP_TITLE" diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/HomeTabRouter.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/HomeTabRouter.swift index 893783ef0..c058f507f 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/HomeTabRouter.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/HomeTabRouter.swift @@ -32,6 +32,7 @@ final class HomeTabRouter: ObservableObject { @Published var resolvingPrimaryDomainWallet: SelectRRPresentationDetails? @Published var showingWalletInfo: WalletEntity? @Published var sendCryptoInitialData: SendCryptoAsset.InitialData? + @Published var mpcResetPasswordData: MPCResetPasswordData? = nil weak var mintingNav: MintDomainsNavigationController? weak var chatsListCoordinator: ChatsListCoordinator? weak var homeWalletViewCoordinator: HomeWalletViewCoordinator? @@ -306,6 +307,7 @@ extension HomeTabRouter { showingWalletInfo = nil sendCryptoInitialData = nil requestingRecoveryMPC = nil + mpcResetPasswordData = nil walletViewNavPath.removeAll() chatTabNavPath.removeAll() exploreTabNavPath.removeAll() @@ -348,6 +350,27 @@ extension HomeTabRouter { appContext.pullUpViewService.showMessageSigningInMaintenancePullUp(in: topVC) } + + func runResetMPCWalletPasswordFlow(_ mpcResetPasswordData: MPCResetPasswordData) { + self.mpcResetPasswordData = mpcResetPasswordData + } + + func didAddNewWallet(_ wallet: UDWallet) { + let profiles = appContext.userProfilesService.profiles + for profile in profiles { + if case .wallet(let walletEntity) = profile, + walletEntity.address == wallet.address { + let walletName = walletEntity.displayInfo.walletSourceName + appContext.toastMessageService.showToast(.walletAdded(walletName: walletName), isSticky: false) + Task { + await popToRootAndWait() + walletViewNavPath.append(.walletDetails(walletEntity)) + } + break + } + } + AppReviewService.shared.appReviewEventDidOccurs(event: .walletAdded) + } } // MARK: - Pull up related diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/HomeTabView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/HomeTabView.swift index d46527282..9e6c5f4de 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/HomeTabView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/HomeTabView.swift @@ -53,6 +53,14 @@ struct HomeTabView: View { ShareWalletInfoView(wallet: $0) .presentationDetents([.large]) }) + .sheet(item: $router.mpcResetPasswordData, content: { + MPCResetPasswordRootView(resetPasswordData: $0, resetResultCallback: { result in + switch result { + case .restored(let wallet): + router.didAddNewWallet(wallet) + } + }) + }) .sheet(item: $router.sendCryptoInitialData, content: { initialData in SendCryptoAssetRootView(viewModel: SendCryptoAssetViewModel(initialData: initialData)) }) diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/ActivateMPCWalletFlow/MPCActivateWalletInAppView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/ActivateMPCWalletFlow/MPCActivateWalletInAppView.swift index f160b3118..7b875ab4a 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/ActivateMPCWalletFlow/MPCActivateWalletInAppView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/ActivateMPCWalletFlow/MPCActivateWalletInAppView.swift @@ -16,7 +16,7 @@ struct MPCActivateWalletInAppView: View { var body: some View { MPCActivateWalletView(analyticsName: .mpcActivationInApp, - credentials: credentials, + flow: .activate(credentials), code: code, mpcWalletCreatedCallback: didCreateMPCWallet, changeEmailCallback: didRequestToChangeEmail) diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/Activating/MPCActivateWalletView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/Activating/MPCActivateWalletView.swift index d19375847..b8cb7c60a 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/Activating/MPCActivateWalletView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/Activating/MPCActivateWalletView.swift @@ -12,7 +12,7 @@ struct MPCActivateWalletView: View, ViewAnalyticsLogger { @Environment(\.mpcWalletsService) private var mpcWalletsService let analyticsName: Analytics.ViewName - @State var credentials: MPCActivateCredentials + @State var flow: SetupMPCFlow @State var code: String var canGoBack: Bool = true let mpcWalletCreatedCallback: (UDWallet)->() @@ -48,13 +48,13 @@ struct MPCActivateWalletView: View, ViewAnalyticsLogger { .onAppear(perform: onAppear) .sheet(item: $enterDataType) { dataType in MPCActivateWalletEnterView(dataType: dataType, - email: credentials.email, + email: flow.email, confirmationCallback: { value in switch dataType { case .passcode: self.code = value case .password: - self.credentials.password = value + didEnterNewPassword(value) } activateMPCWallet() }, changeEmailCallback: changeEmailCallback) @@ -62,6 +62,16 @@ struct MPCActivateWalletView: View, ViewAnalyticsLogger { } .navigationBarBackButtonHidden(isBackButtonHidden) } + + func didEnterNewPassword(_ password: String) { + switch self.flow { + case .activate(var credentials): + credentials.password = password + self.flow = .activate(credentials) + case .resetPassword: + Debugger.printFailure("Incorrect state, new password can't be incorrect", critical: true) + } + } } // MARK: - Private methods @@ -97,8 +107,8 @@ private extension MPCActivateWalletView { isLoading = true do { - let mpcWalletStepsStream = mpcWalletsService.setupMPCWalletWith(code: code, - credentials: credentials) + let mpcWalletStepsStream: AsyncThrowingStream = mpcWalletsService.setupMPCWalletWith(code: code, + flow: flow) for try await step in mpcWalletStepsStream { updateForSetupMPCWalletStep(step) @@ -239,8 +249,8 @@ private extension MPCActivateWalletView { #Preview { MPCActivateWalletView(analyticsName: .mpcActivationOnboarding, - credentials: .init(email: "qq@qq.qq", - password: ""), + flow: .activate(.init(email: "qq@qq.qq", + password: "")), code: "", mpcWalletCreatedCallback: { _ in }, changeEmailCallback: { }) diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/Activating/MPCOnboardingActivateWalletViewController.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/Activating/MPCOnboardingActivateWalletViewController.swift index 7cd5605a8..4892a508b 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/Activating/MPCOnboardingActivateWalletViewController.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/Activating/MPCOnboardingActivateWalletViewController.swift @@ -50,7 +50,7 @@ private extension MPCOnboardingActivateWalletViewController { return } let mpcView = MPCActivateWalletView(analyticsName: .mpcActivationOnboarding, - credentials: credentials, + flow: .activate(credentials), code: code, mpcWalletCreatedCallback: { [weak self] wallet in DispatchQueue.main.async { self?.handleAction(.didImportWallet(wallet)) diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/ReconnectMPCWalletFlow/MPCActivateWalletReconnectView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/ReconnectMPCWalletFlow/MPCActivateWalletReconnectView.swift index ae7be16d8..d330b0e85 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/ReconnectMPCWalletFlow/MPCActivateWalletReconnectView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/ReconnectMPCWalletFlow/MPCActivateWalletReconnectView.swift @@ -15,7 +15,7 @@ struct MPCActivateWalletReconnectView: View { var body: some View { MPCActivateWalletView(analyticsName: .mpcActivationInApp, - credentials: credentials, + flow: .activate(credentials), code: code, mpcWalletCreatedCallback: didCreateMPCWallet) .padding(.top, ActivateMPCWalletFlow.viewsTopOffset) diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FB_UD_MPCConnectionService.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FB_UD_MPCConnectionService.swift index d355a0fe1..d0f317229 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FB_UD_MPCConnectionService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FB_UD_MPCConnectionService.swift @@ -56,16 +56,16 @@ extension FB_UD_MPC.MPCConnectionService: MPCWalletProviderSubServiceProtocol { } func setupMPCWalletWith(code: String, - credentials: MPCActivateCredentials) -> AsyncThrowingStream { + flow: SetupMPCFlow) -> AsyncThrowingStream { AsyncThrowingStream { continuation in Task { var mpcConnectorInProgress: FB_UD_MPC.FireblocksConnectorProtocol? - let email = credentials.email - let recoveryPhrase = credentials.password + let email: String = flow.email + let recoveryPhrase: String = flow.password do { continuation.yield(.submittingCode) logMPC("Will submit code \(code). recoveryPhrase: \(recoveryPhrase)") - let submitCodeResponse = try await networkService.submitBootstrapCode(code) + let submitCodeResponse: FB_UD_MPC.BootstrapCodeSubmitResponse = try await networkService.submitBootstrapCode(code) logMPC("Did submit code \(code)") let accessToken = submitCodeResponse.accessToken let deviceId = submitCodeResponse.deviceId @@ -73,20 +73,28 @@ extension FB_UD_MPC.MPCConnectionService: MPCWalletProviderSubServiceProtocol { continuation.yield(.initialiseFireblocks) logMPC("Will create fireblocks connector") - let mpcConnector = try connectorBuilder.buildBootstrapMPCConnector(deviceId: deviceId, accessToken: accessToken) + let mpcConnector: FB_UD_MPC.FireblocksConnectorProtocol = try connectorBuilder.buildBootstrapMPCConnector(deviceId: deviceId, accessToken: accessToken) mpcConnectorInProgress = mpcConnector mpcConnector.stopJoinWallet() logMPC("Did create fireblocks connector") logMPC("Will request to join existing wallet") continuation.yield(.requestingToJoinExistingWallet) - let requestId = try await mpcConnector.requestJoinExistingWallet() + let requestId: String = try await mpcConnector.requestJoinExistingWallet() logMPC("Will auth new device with request id: \(requestId)") // Once we have the key material, now it’s time to get a full access token to the Wallets API. To prove that the key material is valid, you need to create a transaction to sign // Initialize a transaction with the Wallets API continuation.yield(.authorisingNewDevice) - try await networkService.authNewDeviceWith(requestId: requestId, - recoveryPhrase: recoveryPhrase, - accessToken: accessToken) + switch flow { + case .activate(let credentials): + try await networkService.authNewDeviceWith(requestId: requestId, + recoveryPhrase: credentials.password, + accessToken: accessToken) + case .resetPassword(let data, let newPassword): + try await networkService.resetPassword(accessToken: accessToken, + recoveryToken: data.recoveryToken, + newRecoveryPhrase: newPassword, + requestId: requestId) + } logMPC("Did auth new device with request id: \(requestId)") logMPC("Will wait for key is ready") continuation.yield(.waitingForKeysIsReady) @@ -94,7 +102,7 @@ extension FB_UD_MPC.MPCConnectionService: MPCWalletProviderSubServiceProtocol { logMPC("Will init transaction with new key materials") continuation.yield(.initialiseTransaction) - let transactionDetails = try await networkService.initTransactionWithNewKeyMaterials(accessToken: accessToken) + let transactionDetails: FB_UD_MPC.SetupTokenResponse = try await networkService.initTransactionWithNewKeyMaterials(accessToken: accessToken) let txId = transactionDetails.transactionId logMPC("Did init transaction with new key materials with tx id: \(txId)") @@ -119,7 +127,7 @@ extension FB_UD_MPC.MPCConnectionService: MPCWalletProviderSubServiceProtocol { // Once it is pending a signature, sign with the Fireblocks NCW SDK and confirm with the Wallets API that you have signed. After confirmation is validated, you’ll be returned an access token, a refresh token and a bootstrap token. logMPC("Will confirm transaction is signed") continuation.yield(.confirmingTransaction) - let authTokens = try await networkService.confirmTransactionWithNewKeyMaterialsSigned(accessToken: accessToken) + let authTokens: FB_UD_MPC.AuthTokens = try await networkService.confirmTransactionWithNewKeyMaterialsSigned(accessToken: accessToken) logMPC("Did confirm transaction is signed") logMPC("Will verify final response \(authTokens)") @@ -128,19 +136,28 @@ extension FB_UD_MPC.MPCConnectionService: MPCWalletProviderSubServiceProtocol { logMPC("Did verify final response \(authTokens) success") continuation.yield(.getWalletAccountDetails) - let walletDetails = try await getWalletAccountDetailsForWalletWith(deviceId: deviceId, + let walletDetails: WalletDetails = try await getWalletAccountDetailsForWalletWith(deviceId: deviceId, accessToken: authTokens.accessToken.jwt) logMPC("Did get wallet account details") - let mpcWallet = FB_UD_MPC.ConnectedWalletDetails(email: email, + let mpcWallet: FB_UD_MPC.ConnectedWalletDetails = FB_UD_MPC.ConnectedWalletDetails(email: email, deviceId: deviceId, tokens: authTokens, firstAccount: walletDetails.firstAccount, accounts: walletDetails.accounts) continuation.yield(.storeWallet) + logMPC("Will create UD Wallet") - let udWallet = try prepareAndSaveMPCWallet(mpcWallet) + let udWallet: UDWallet = try prepareAndSaveMPCWallet(mpcWallet) logMPC("Did create UD Wallet") + if case .resetPassword = flow { + // Send a new recovery kit email to the user + Task.detached { + try? await self.requestRecoveryFor(connectedWallet: mpcWallet, + password: recoveryPhrase) + } + } + continuation.yield(.finished(udWallet)) continuation.finish() } catch { @@ -386,13 +403,19 @@ extension FB_UD_MPC.MPCConnectionService: MPCWalletProviderSubServiceProtocol { password: String) async throws -> String { let connectedWalletDetails = try getConnectedWalletDetailsFor(walletMetadata: walletMetadata) - return try await performAuthErrorCatchingBlock(connectedWalletDetails: connectedWalletDetails) { token in + return try await requestRecoveryFor(connectedWallet: connectedWalletDetails, + password: password) + } + + @discardableResult + private func requestRecoveryFor(connectedWallet: FB_UD_MPC.ConnectedWalletDetails, + password: String) async throws -> String { + try await performAuthErrorCatchingBlock(connectedWalletDetails: connectedWallet) { token in do { try await networkService.requestRecovery(token, password: password) - return connectedWalletDetails.email - + return connectedWallet.email } catch { - /// Temporary solution until clarified with the BE. + /// Temporary solution until clarified with the BE. if case NetworkLayerError.badResponseOrStatusCode(let code, _, _) = error, code == 400 { throw MPCWalletError.wrongRecoveryPassword diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FireblocksConnector/Fireblocks/FireblocksConnector.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FireblocksConnector/Fireblocks/FireblocksConnector.swift index d7b550396..9cfdf1d13 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FireblocksConnector/Fireblocks/FireblocksConnector.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FireblocksConnector/Fireblocks/FireblocksConnector.swift @@ -86,7 +86,7 @@ extension FireblocksConnector: FB_UD_MPC.FireblocksConnectorProtocol { } func getLogsURLs() -> URL? { - fireblocks.getURLForLogFiles() + Fireblocks.getURLForLogFiles() } private func waitForKeyIsReadyInternal(attempt: Int = 0) async throws { diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_DefaultMPCConnectionNetworkService.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_DefaultMPCConnectionNetworkService.swift index 026fb9919..b92fe564b 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_DefaultMPCConnectionNetworkService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_DefaultMPCConnectionNetworkService.swift @@ -433,6 +433,28 @@ extension FB_UD_MPC { try await makeAPIRequest(request) } + func resetPassword(accessToken: String, + recoveryToken: String, + newRecoveryPhrase: String, + requestId: String) async throws { + struct RequestBody: Codable { + let recoveryToken: String + let newRecoveryPassphrase: String + let walletJoinRequestId: String + } + + let url = MPCNetwork.URLSList.devicesRecoverURL + let body = RequestBody(recoveryToken: recoveryToken, + newRecoveryPassphrase: newRecoveryPhrase, + walletJoinRequestId: requestId) + let headers = buildAuthBearerHeader(token: accessToken) + let request = try APIRequest(urlString: url, + body: body, + method: .post, + headers: headers) + try await makeAPIRequest(request) + } + // MARK: - Private methods private func makeDecodableAPIRequest(_ apiRequest: APIRequest) async throws -> T { do { diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCConnectionNetworkService.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCConnectionNetworkService.swift index b73d91579..ac786ed47 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCConnectionNetworkService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCConnectionNetworkService.swift @@ -62,5 +62,9 @@ extension FB_UD_MPC { amount: String) async throws -> NetworkFeeResponse func requestRecovery(_ accessToken: String, password: String) async throws + func resetPassword(accessToken: String, + recoveryToken: String, + newRecoveryPhrase: String, + requestId: String) async throws } } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCNetwork.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCNetwork.swift index f91cdad5a..04ab9284e 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCNetwork.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCNetwork.swift @@ -19,7 +19,10 @@ extension FB_UD_MPC { static var authURL: String { v1URL.appendingURLPathComponents("auth") } static var getCodeOnEmailURL: String { authURL.appendingURLPathComponents("bootstrap", "email") } static var submitCodeURL: String { authURL.appendingURLPathComponents("bootstrap") } - static var devicesBootstrapURL: String { authURL.appendingURLPathComponents("devices", "bootstrap") } + + static var devicesURL: String { authURL.appendingURLPathComponents("devices") } + static var devicesBootstrapURL: String { devicesURL.appendingURLPathComponents("bootstrap") } + static var devicesRecoverURL: String { devicesURL.appendingURLPathComponents("recover") } static var rpcMessagesURL: String { v1URL.appendingURLPathComponents("rpc", "messages") } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/MPCWalletProviderSubServiceProtocol.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/MPCWalletProviderSubServiceProtocol.swift index efc2ced32..15f35eadb 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/MPCWalletProviderSubServiceProtocol.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/MPCWalletProviderSubServiceProtocol.swift @@ -12,7 +12,7 @@ protocol MPCWalletProviderSubServiceProtocol { func sendBootstrapCodeTo(email: String) async throws func setupMPCWalletWith(code: String, - credentials: MPCActivateCredentials) -> AsyncThrowingStream + flow: SetupMPCFlow) -> AsyncThrowingStream func signPersonalMessage(_ messageString: String, chain: BlockchainType, by walletMetadata: MPCWalletMetadata) async throws -> String diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletsService.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletsService.swift index 52863fe45..a622ebfb0 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletsService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletsService.swift @@ -34,7 +34,7 @@ extension MPCWalletsService: MPCWalletsServiceProtocol { } func setupMPCWalletWith(code: String, - credentials: MPCActivateCredentials) -> AsyncThrowingStream { + flow: SetupMPCFlow) -> AsyncThrowingStream { AsyncThrowingStream { continuation in Task { do { @@ -42,7 +42,7 @@ extension MPCWalletsService: MPCWalletsServiceProtocol { let subService = try getSubServiceFor(provider: .fireblocksUD) for try await step in subService.setupMPCWalletWith(code: code, - credentials: credentials) { + flow: flow) { continuation.yield(step) } } catch { diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletsServiceProtocol.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletsServiceProtocol.swift index e22fd8b87..9e3591050 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletsServiceProtocol.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletsServiceProtocol.swift @@ -10,7 +10,7 @@ import Foundation protocol MPCWalletsServiceProtocol { func sendBootstrapCodeTo(email: String) async throws func setupMPCWalletWith(code: String, - credentials: MPCActivateCredentials) -> AsyncThrowingStream + flow: SetupMPCFlow) -> AsyncThrowingStream func signPersonalMessage(_ messageString: String, by walletMetadata: MPCWalletMetadata) async throws -> String func signTypedDataMessage(_ message: String, diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/SetupMPCFlow.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/SetupMPCFlow.swift new file mode 100644 index 000000000..dd8453010 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/SetupMPCFlow.swift @@ -0,0 +1,27 @@ +// +// SetupMPCFlow.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 24.10.2024. +// + +import Foundation + +enum SetupMPCFlow { + case activate(MPCActivateCredentials) + case resetPassword(MPCResetPasswordData, newPassword: String) + + var email: String { + switch self { + case .activate(let credentials): return credentials.email + case .resetPassword(let data, _): return data.email + } + } + + var password: String { + switch self { + case .activate(let credentials): return credentials.password + case .resetPassword(_, let newPassword): return newPassword + } + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/ActivateWallet/MPCActivateWalletInAppAfterClaimView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/ActivateWallet/MPCActivateWalletInAppAfterClaimView.swift index 3fc7bb331..6fe9a179a 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/ActivateWallet/MPCActivateWalletInAppAfterClaimView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/ActivateWallet/MPCActivateWalletInAppAfterClaimView.swift @@ -16,7 +16,7 @@ struct MPCActivateWalletInAppAfterClaimView: View { var body: some View { MPCActivateWalletView(analyticsName: .mpcPurchaseTakeoverActivateAfterClaimInApp, - credentials: credentials, + flow: .activate(credentials), code: code, canGoBack: false, mpcWalletCreatedCallback: didCreateMPCWallet, diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/ActivateWallet/MPCOnboardingPurchaseActivateAfterClaimViewController.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/ActivateWallet/MPCOnboardingPurchaseActivateAfterClaimViewController.swift index 55c9e2d6c..2d55b1d10 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/ActivateWallet/MPCOnboardingPurchaseActivateAfterClaimViewController.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/ActivateWallet/MPCOnboardingPurchaseActivateAfterClaimViewController.swift @@ -50,8 +50,8 @@ private extension MPCOnboardingPurchaseActivateAfterClaimViewController { } let mpcView = MPCActivateWalletView(analyticsName: .mpcActivationOnboarding, - credentials: credentials, - code: code, + flow: .activate(credentials), + code: code, mpcWalletCreatedCallback: { [weak self] wallet in DispatchQueue.main.async { self?.didActivateWallet(wallet) diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/MPCWalletPasswordRequirements.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/MPCWalletPasswordRequirements.swift new file mode 100644 index 000000000..8f6cd6f12 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/MPCWalletPasswordRequirements.swift @@ -0,0 +1,14 @@ +// +// MPCWalletPasswordRequirements.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 21.10.2024. +// + +import Foundation + +enum MPCWalletPasswordRequirements: CaseIterable { + case length + case oneNumber + case specialChar +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/MPCWalletPasswordValidator.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/MPCWalletPasswordValidator.swift index 0cd291166..4693b8a5e 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/MPCWalletPasswordValidator.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/MPCWalletPasswordValidator.swift @@ -41,6 +41,34 @@ extension MPCWalletPasswordValidator { return errors } + + func isPasswordRequirementMet(_ requirement: MPCWalletPasswordRequirements, + passwordErrors: [MPCWalletPasswordValidationError]) -> Bool { + switch requirement { + case .length: + return !passwordErrors.contains(.tooShort) && !passwordErrors.contains(.tooLong) + case .oneNumber: + return !passwordErrors.contains(.missingNumber) + case .specialChar: + return !passwordErrors.contains(.missingSpecialCharacter) + } + } + + func titleForRequirement(_ requirement: MPCWalletPasswordRequirements, + passwordErrors: [MPCWalletPasswordValidationError]) -> String { + switch requirement { + case .length: + if passwordErrors.contains(.tooLong) { + String.Constants.mpcPasswordValidationTooLongTitle.localized(minMPCWalletPasswordLength, maxMPCWalletPasswordLength) + } else { + String.Constants.mpcPasswordValidationLengthTitle.localized(minMPCWalletPasswordLength) + } + case .oneNumber: + String.Constants.mpcPasswordValidationNumberTitle.localized() + case .specialChar: + String.Constants.mpcPasswordValidationSpecialCharTitle.localized() + } + } } enum MPCWalletPasswordValidationError: String, LocalizedError { diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/TakeoverPassword/PurchaseMPCWalletTakeoverPasswordView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/TakeoverPassword/PurchaseMPCWalletTakeoverPasswordView.swift index 415ad0efa..63b961039 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/TakeoverPassword/PurchaseMPCWalletTakeoverPasswordView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/PurchaseMPCFlow/TakeoverPassword/PurchaseMPCWalletTakeoverPasswordView.swift @@ -129,18 +129,18 @@ private extension PurchaseMPCWalletTakeoverPasswordView { @ViewBuilder func passwordRequirementsView() -> some View { VStack(spacing: 3) { - ForEach(PasswordRequirements.allCases, id: \.self) { requirement in + ForEach(MPCWalletPasswordRequirements.allCases, id: \.self) { requirement in passwordRequirementView(requirement) } } } @ViewBuilder - func passwordRequirementView(_ requirement: PasswordRequirements) -> some View { + func passwordRequirementView(_ requirement: MPCWalletPasswordRequirements) -> some View { HStack(spacing: 8) { Circle() .squareFrame(4) - Text(titleFor(requirement: requirement)) + Text(titleForRequirement(requirement, passwordErrors: passwordErrors)) .font(.currentFont(size: 13)) Spacer() } @@ -150,8 +150,8 @@ private extension PurchaseMPCWalletTakeoverPasswordView { .padding(.horizontal, 4) } - func foregroundStyleFor(requirement: PasswordRequirements) -> Color { - if isPasswordRequirementMet(requirement) { + func foregroundStyleFor(requirement: MPCWalletPasswordRequirements) -> Color { + if isPasswordRequirementMet(requirement, passwordErrors: passwordErrors) { .foregroundSuccess } else if isPasswordInErrorState && requirement == .length { .foregroundDanger @@ -160,38 +160,6 @@ private extension PurchaseMPCWalletTakeoverPasswordView { } } - func isPasswordRequirementMet(_ requirement: PasswordRequirements) -> Bool { - switch requirement { - case .length: - !passwordErrors.contains(.tooShort) && !passwordErrors.contains(.tooLong) - case .oneNumber: - !passwordErrors.contains(.missingNumber) - case .specialChar: - !passwordErrors.contains(.missingSpecialCharacter) - } - } - - enum PasswordRequirements: CaseIterable { - case length - case oneNumber - case specialChar - } - - func titleFor(requirement: PasswordRequirements) -> String { - switch requirement { - case .length: - if passwordErrors.contains(.tooLong) { - String.Constants.mpcPasswordValidationTooLongTitle.localized(minMPCWalletPasswordLength, maxMPCWalletPasswordLength) - } else { - String.Constants.mpcPasswordValidationLengthTitle.localized(minMPCWalletPasswordLength) - } - case .oneNumber: - String.Constants.mpcPasswordValidationNumberTitle.localized() - case .specialChar: - String.Constants.mpcPasswordValidationSpecialCharTitle.localized() - } - } - @ViewBuilder func confirmPasswordInputView() -> some View { UDTextFieldView(text: $confirmPasswordInput, diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordActivateView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordActivateView.swift new file mode 100644 index 000000000..a94ef399e --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordActivateView.swift @@ -0,0 +1,37 @@ +// +// MPCResetPasswordActivateView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 24.10.2024. +// + +import SwiftUI + +struct MPCResetPasswordActivateView: View { + @EnvironmentObject var viewModel: MPCResetPasswordViewModel + + let data: MPCResetPasswordFlow.ResetPasswordFullData + + var body: some View { + MPCActivateWalletView(analyticsName: .mpcActivationRestorePassword, + flow: .resetPassword(data.resetPasswordData, + newPassword: data.newPassword), + code: data.code, + mpcWalletCreatedCallback: didCreateMPCWallet) + .padding(.top, ActivateMPCWalletFlow.viewsTopOffset) + } +} + +// MARK: - Private methods +private extension MPCResetPasswordActivateView { + func didCreateMPCWallet(_ wallet: UDWallet) { + viewModel.handleAction(.didActivate(wallet)) + } +} + +#Preview { + MPCResetPasswordActivateView(data: .init(resetPasswordData: .init(email: "", + recoveryToken: ""), + newPassword: "", + code: "")) +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordData.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordData.swift new file mode 100644 index 000000000..a78a2a5a0 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordData.swift @@ -0,0 +1,15 @@ +// +// MPCResetPasswordData.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 21.10.2024. +// + +import Foundation + +struct MPCResetPasswordData: Hashable, Identifiable { + var id: String { email } + + let email: String + let recoveryToken: String +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordEnterCodeView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordEnterCodeView.swift new file mode 100644 index 000000000..8fc22d88f --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordEnterCodeView.swift @@ -0,0 +1,38 @@ +// +// MPCResetPasswordEnterCodeView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 24.10.2024. +// + +import SwiftUI + +struct MPCResetPasswordEnterCodeView: View { + @Environment(\.mpcWalletsService) private var mpcWalletsService + @EnvironmentObject var viewModel: MPCResetPasswordViewModel + + let email: String + + var body: some View { + MPCEnterCodeView(analyticsName: .mpcResetPasswordEnterCode, + email: email, + resendAction: resendCode, + enterCodeCallback: didEnterCode) + .padding(.top, ActivateMPCWalletFlow.viewsTopOffset) + } +} + +// MARK: - Private methods +private extension MPCResetPasswordEnterCodeView { + func didEnterCode(_ code: String) { + viewModel.handleAction(.didEnterCode(code)) + } + + func resendCode(email: String) async throws { + try await mpcWalletsService.sendBootstrapCodeTo(email: email) + } +} + +#Preview { + MPCResetPasswordEnterCodeView(email: "") +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordEnterPasswordView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordEnterPasswordView.swift new file mode 100644 index 000000000..98a5837ab --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordEnterPasswordView.swift @@ -0,0 +1,189 @@ +// +// MPCResetPasswordEnterPasswordView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 21.10.2024. +// + +import SwiftUI + +struct MPCResetPasswordEnterPasswordView: View, ViewAnalyticsLogger, MPCWalletPasswordValidator { + + @Environment(\.dismiss) var dismiss + @Environment(\.mpcWalletsService) private var mpcWalletsService + @EnvironmentObject var viewModel: MPCResetPasswordViewModel + var analyticsName: Analytics.ViewName { .mpcResetPasswordEnterPassword } + + let email: String + @State private var passwordInput: String = "" + @State private var passwordErrors: [MPCWalletPasswordValidationError] = [] + @State private var confirmPasswordInput: String = "" + @State private var keyboardHeight: CGFloat = 0 + @State private var isLoading: Bool = false + @State private var error: Error? + + var body: some View { + contentView() + .animation(.default, value: keyboardHeight) + .displayError($error) + .onChange(of: passwordInput, perform: { newValue in + validatePasswordInput() + }) + .onReceive(KeyboardService.shared.keyboardFramePublisher.receive(on: DispatchQueue.main)) { keyboardFrame in + keyboardHeight = keyboardFrame.height + } + .toolbar { + ToolbarItem(placement: .topBarLeading) { + CloseButtonView { + logButtonPressedAnalyticEvents(button: .close) + dismiss() + } + } + } + } +} + +// MARK: - Private methods +private extension MPCResetPasswordEnterPasswordView { + @ViewBuilder + func contentView() -> some View { + ScrollView { + VStack(spacing: 8) { + VStack(spacing: 32) { + headerView() + VStack(spacing: 24) { + passwordInputView() + confirmPasswordInputView() + } + } + Spacer() + actionButtonContainerView() + } + .padding(.horizontal, 16) + } + } + + @ViewBuilder + func headerView() -> some View { + VStack(spacing: 16) { + Text(String.Constants.mpcResetPasswordTitle.localized()) + .titleText() + Text(String.Constants.mpcResetPasswordSubtitle.localized()) + .subtitleText() + } + .multilineTextAlignment(.center) + } + + @ViewBuilder + func passwordInputView() -> some View { + VStack(spacing: 8) { + UDTextFieldView(text: $passwordInput, + placeholder: String.Constants.createPassword.localized(), + focusBehaviour: .activateOnAppear, + autocapitalization: .never, + autocorrectionDisabled: true, + isSecureInput: true, + isErrorState: isPasswordInErrorState) + passwordRequirementsView() + } + } + + var isPasswordInErrorState: Bool { + passwordErrors.contains(.tooLong) + } + + @ViewBuilder + func passwordRequirementsView() -> some View { + VStack(spacing: 3) { + ForEach(MPCWalletPasswordRequirements.allCases, id: \.self) { requirement in + passwordRequirementView(requirement) + } + } + } + + @ViewBuilder + func passwordRequirementView(_ requirement: MPCWalletPasswordRequirements) -> some View { + HStack(spacing: 8) { + Circle() + .squareFrame(4) + Text(titleForRequirement(requirement, passwordErrors: passwordErrors)) + .font(.currentFont(size: 13)) + Spacer() + } + .foregroundStyle(foregroundStyleFor(requirement: requirement)) + .frame(minHeight: 20) + .frame(maxWidth: .infinity) + .padding(.horizontal, 4) + } + + func foregroundStyleFor(requirement: MPCWalletPasswordRequirements) -> Color { + if passwordInput.isEmpty { + .foregroundSecondary + } else if isPasswordRequirementMet(requirement, passwordErrors: passwordErrors) { + .foregroundSuccess + } else if isPasswordInErrorState && requirement == .length { + .foregroundDanger + } else { + .foregroundSecondary + } + } + + @ViewBuilder + func confirmPasswordInputView() -> some View { + UDTextFieldView(text: $confirmPasswordInput, + placeholder: String.Constants.confirmPassword.localized(), + focusBehaviour: .default, + autocapitalization: .never, + autocorrectionDisabled: true, + isSecureInput: true) + } + + func validatePasswordInput() { + passwordErrors = validateWalletPassword(passwordInput) + } + + var isActionButtonDisabled: Bool { + !isValidPasswordEntered || passwordInput != confirmPasswordInput + } + + var isValidPasswordEntered: Bool { + passwordErrors.isEmpty + } + + @ViewBuilder + func actionButtonContainerView() -> some View { +// VStack(spacing: 0) { + continueButton() +// .padding(.bottom, keyboardHeight + 16) + .background(Color.black) +// } + } + + @ViewBuilder + func continueButton() -> some View { + UDButtonView(text: String.Constants.continue.localized(), + style: .large(.raisedPrimary), + isLoading: isLoading, + callback: actionButtonPressed) + .disabled(isActionButtonDisabled) + } + + func actionButtonPressed() { + logButtonPressedAnalyticEvents(button: .continue) + isLoading = true + Task { + do { + try await mpcWalletsService.sendBootstrapCodeTo(email: email) + viewModel.handleAction(.didEnterNewPassword(passwordInput)) + } catch { + logAnalytic(event: .sendMPCBootstrapCodeError, parameters: [.error: error.localizedDescription]) + self.error = error + } + isLoading = false + } + } +} + +#Preview { + MPCResetPasswordEnterPasswordView(email: "test@example.com") +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordFlow.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordFlow.swift new file mode 100644 index 000000000..cada1469d --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordFlow.swift @@ -0,0 +1,30 @@ +// +// MPCResetPasswordFlow.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 24.10.2024. +// + +import Foundation + +enum MPCResetPasswordFlow { } + +extension MPCResetPasswordFlow { + enum FlowAction { + case didEnterNewPassword(String) + case didEnterCode(String) + case didActivate(UDWallet) + } + + typealias FlowResultCallback = (MPCResetPasswordFlow.FlowResult)->() + + enum FlowResult { + case restored(UDWallet) + } + + struct ResetPasswordFullData: Hashable { + let resetPasswordData: MPCResetPasswordData + let newPassword: String + let code: String + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordNavigationDestination.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordNavigationDestination.swift new file mode 100644 index 000000000..9b2d29047 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordNavigationDestination.swift @@ -0,0 +1,30 @@ +// +// MPCResetPasswordNavigationDestination.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 24.10.2024. +// + +import SwiftUI + +extension MPCResetPasswordFlow { + enum NavigationDestination: Hashable { + case enterCode(email: String) + case activate(ResetPasswordFullData) + + var isWithCustomTitle: Bool { false } + } + + struct LinkNavigationDestination { + @ViewBuilder + static func viewFor(navigationDestination: NavigationDestination) -> some View { + switch navigationDestination { + case .enterCode(let email): + MPCResetPasswordEnterCodeView(email: email) + case .activate(let data): + MPCResetPasswordActivateView(data: data) + } + } + } +} + diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordRootView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordRootView.swift new file mode 100644 index 000000000..37b780f5e --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordRootView.swift @@ -0,0 +1,71 @@ +// +// MPCResetPasswordRootView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 24.10.2024. +// + +import SwiftUI + +struct MPCResetPasswordRootView: View { + + static func instantiateViewController(resetPasswordData: MPCResetPasswordData, + resetResultCallback: @escaping MPCResetPasswordFlow.FlowResultCallback) -> UIViewController { + let view = MPCResetPasswordRootView(resetPasswordData: resetPasswordData, + resetResultCallback: resetResultCallback) + let vc = UIHostingController(rootView: view) + return vc + } + + @Environment(\.presentationMode) private var presentationMode + @StateObject private var viewModel: MPCResetPasswordViewModel + + var body: some View { + NavigationViewWithCustomTitle(content: { + ZStack { + MPCResetPasswordEnterPasswordView(email: viewModel.resetPasswordData.email) + .environmentObject(viewModel) + .navigationBarTitleDisplayMode(.inline) + .navigationDestination(for: MPCResetPasswordFlow.NavigationDestination.self) { destination in + MPCResetPasswordFlow.LinkNavigationDestination.viewFor(navigationDestination: destination) + .ignoresSafeArea() + .environmentObject(viewModel) + } + .onChange(of: viewModel.navPath) { _ in + updateTitleView() + } + .trackNavigationControllerEvents(onDidNotFinishNavigationBack: updateTitleView) + if viewModel.isLoading { + ProgressView() + } + } + }, navigationStateProvider: { navigationState in + self.viewModel.navigationState = navigationState + }, path: $viewModel.navPath) + .interactiveDismissDisabled() + .displayError($viewModel.error) + .allowsHitTesting(!viewModel.isLoading) + } + + init(resetPasswordData: MPCResetPasswordData, + resetResultCallback: @escaping MPCResetPasswordFlow.FlowResultCallback) { + self._viewModel = StateObject(wrappedValue: MPCResetPasswordViewModel(resetPasswordData: resetPasswordData, + resetResultCallback: resetResultCallback)) + } +} + +// MARK: - Private methods +private extension MPCResetPasswordRootView { + func updateTitleView() { + viewModel.navigationState?.yOffset = -2 + withAnimation { + viewModel.navigationState?.isTitleVisible = viewModel.navPath.last?.isWithCustomTitle == true + } + } +} + +#Preview { + MPCResetPasswordRootView(resetPasswordData: .init(email: "", + recoveryToken: ""), + resetResultCallback: { _ in }) +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordViewModel.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordViewModel.swift new file mode 100644 index 000000000..1cbaa35b1 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ResetPassword/MPCResetPasswordViewModel.swift @@ -0,0 +1,53 @@ +// +// MPCResetPasswordViewModel.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 24.10.2024. +// + +import SwiftUI + +@MainActor +final class MPCResetPasswordViewModel: ObservableObject { + + let resetPasswordData: MPCResetPasswordData + let resetResultCallback: MPCResetPasswordFlow.FlowResultCallback + @Published var navPath: NavigationPathWrapper = .init() + @Published var navigationState: NavigationStateManager? + @Published var isLoading = false + @Published var error: Error? + private var newPassword: String? + + init(resetPasswordData: MPCResetPasswordData, + resetResultCallback: @escaping MPCResetPasswordFlow.FlowResultCallback) { + self.resetPasswordData = resetPasswordData + self.resetResultCallback = resetResultCallback + } + + func handleAction(_ action: MPCResetPasswordFlow.FlowAction) { + Task { + do { + switch action { + case .didEnterNewPassword(let newPassword): + self.newPassword = newPassword + navPath.append(.enterCode(email: resetPasswordData.email)) + case .didEnterCode(let code): + guard let newPassword else { return } + + let data = MPCResetPasswordFlow.ResetPasswordFullData(resetPasswordData: resetPasswordData, + newPassword: newPassword, + code: code) + navPath.append(.activate(data)) + case .didActivate(let wallet): + navigationState?.dismiss = true + resetResultCallback(.restored(wallet)) + } + } catch { + self.error = error + self.isLoading = false + } + } + } + +} + diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Onboarding/OnboardingNavigationController.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Onboarding/OnboardingNavigationController.swift index 77cc44c9e..439993c9c 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Onboarding/OnboardingNavigationController.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Onboarding/OnboardingNavigationController.swift @@ -64,6 +64,32 @@ final class OnboardingNavigationController: CNavigationController { notifyViewControllerWillNavigateBack(vc) } } + + func runResetMPCWalletPasswordFlow(_ mpcResetPasswordData: MPCResetPasswordData) { + let vc = MPCResetPasswordRootView.instantiateViewController(resetPasswordData: mpcResetPasswordData) { [weak self] result in + self?.handleMPCResetPasswordResult(result) + } + topVisibleViewController().present(vc, animated: true) + } + + private func handleMPCResetPasswordResult(_ result: MPCResetPasswordFlow.FlowResult) { + let isAlreadyAddedWallet = !onboardingData.wallets.isEmpty + + switch result { + case .restored(let wallet): + if isAlreadyAddedWallet { + modifyOnboardingData { + if $0.wallets.first(where: { $0.address == wallet.address }) == nil { + $0.wallets.append(wallet) + } + } + } else { + Task { + try? await handle(action: .didImportWallet(wallet)) + } + } + } + } } // MARK: - CNavigationControllerDelegate diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Settings/SettingsView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Settings/SettingsView.swift index d297a0df5..47b445426 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Settings/SettingsView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Settings/SettingsView.swift @@ -574,19 +574,7 @@ private extension SettingsView { } func addWalletAfterAdded(_ wallet: UDWallet) { - var walletName = String.Constants.wallet.localized() - if let displayInfo = WalletDisplayInfo(wallet: wallet, domainsCount: 0, udDomainsCount: 0) { - walletName = displayInfo.walletSourceName - } - appContext.toastMessageService.showToast(.walletAdded(walletName: walletName), isSticky: false) - for profile in profiles { - if case .wallet(let walletEntity) = profile, - walletEntity.address == wallet.address { - tabRouter.walletViewNavPath.append(.walletDetails(walletEntity)) - break - } - } - AppReviewService.shared.appReviewEventDidOccurs(event: .walletAdded) + tabRouter.didAddNewWallet(wallet) } func showWalletsNumberLimitReachedPullUp() { diff --git a/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsServiceEnvironment.swift b/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsServiceEnvironment.swift index 9b4085fe8..5eb456844 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsServiceEnvironment.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsServiceEnvironment.swift @@ -246,8 +246,8 @@ extension Analytics { case transferDomainSuccess, sendCryptoSuccess case mpcEnterCredentialsOnboarding, mpcEnterCredentialsInApp, mpcEnterCredentialsReconnect - case mpcEnterCodeOnboarding, mpcEnterCodeInApp, mpcConfirmCodeInApp - case mpcActivationOnboarding, mpcActivationInApp + case mpcEnterCodeOnboarding, mpcEnterCodeInApp, mpcConfirmCodeInApp, mpcResetPasswordEnterCode + case mpcActivationOnboarding, mpcActivationInApp, mpcActivationRestorePassword case mpcPurchaseUDAuthOnboarding, mpcPurchaseUDAuthInApp case mpcPurchaseCheckoutOnboarding, mpcPurchaseCheckoutInApp @@ -271,6 +271,7 @@ extension Analytics { case purchaseDomainsCart, purchaseDomainsFilters, purchaseDomainsCompleted case mpcRequestRecovery, mpcRecoveryRequested + case mpcResetPasswordEnterPassword } } diff --git a/unstoppable-ios-app/domains-manager-ios/Services/CoreAppCoordinator/CoreAppCoordinator.swift b/unstoppable-ios-app/domains-manager-ios/Services/CoreAppCoordinator/CoreAppCoordinator.swift index 5a4abd0de..180b3ac40 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/CoreAppCoordinator/CoreAppCoordinator.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/CoreAppCoordinator/CoreAppCoordinator.swift @@ -332,6 +332,12 @@ private extension CoreAppCoordinator { Task { await router.showPublicDomainProfileFromDeepLink(of: publicDomainDisplayInfo, by: wallet, preRequestedAction: action) } case .activateMPCWallet(let email): router.runAddWalletFlow(initialAction: .activateMPC(preFilledEmail: email)) + case .resetMPCWalletPassword(let data): + router.runResetMPCWalletPasswordFlow(data) + } + case .onboarding(let nav): + if case .resetMPCWalletPassword(let data) = event { + nav.runResetMPCWalletPasswordFlow(data) } default: return } @@ -356,7 +362,7 @@ private extension CoreAppCoordinator { func setOnboardingAsRoot(_ flow: OnboardingNavigationController.OnboardingFlow) { let onboardingVC = OnboardingNavigationController.instantiate(flow: flow) setRootViewController(onboardingVC) - currentRoot = .onboarding + currentRoot = .onboarding(nav: onboardingVC) } func setRootViewController(_ rootViewController: UIViewController) { @@ -401,7 +407,8 @@ extension CoreAppCoordinator { // MARK: - CurrentRoot private extension CoreAppCoordinator { enum CurrentRoot { - case none, onboarding, appUpdate, fullMaintenance + case none, appUpdate, fullMaintenance + case onboarding(nav: OnboardingNavigationController) case home(router: HomeTabRouter) } } diff --git a/unstoppable-ios-app/domains-manager-ios/Services/DeepLinksService/DeepLinksService.swift b/unstoppable-ios-app/domains-manager-ios/Services/DeepLinksService/DeepLinksService.swift index f0baed9ce..1f9726fe3 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/DeepLinksService/DeepLinksService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/DeepLinksService/DeepLinksService.swift @@ -39,7 +39,7 @@ extension DeepLinksService: DeepLinksServiceProtocol { } } else if let domainName = DomainProfileLinkValidator.getUDmeDomainName(in: components) { tryHandleUDDomainProfileDeepLink(domainName: domainName, params: components.queryItems, receivedState: receivedState) - } else if tryHandleActivateMPCWalletDeepLink(components: components, + } else if tryHandleMPCWalletDeepLink(components: components, receivedState: receivedState) { return } else { @@ -194,8 +194,8 @@ private extension DeepLinksService { receivedState: receivedState) } - func tryHandleActivateMPCWalletDeepLink(components: NSURLComponents, - receivedState: ExternalEventReceivedState) -> Bool { + func tryHandleMPCWalletDeepLink(components: NSURLComponents, + receivedState: ExternalEventReceivedState) -> Bool { guard let path = components.path, let host = components.host else { return false } @@ -203,14 +203,28 @@ private extension DeepLinksService { if Constants.udMeHosts.contains(host), pathComponents.last == ud_me_MPC_path { - let email = components.queryItems?.first(where: { $0.name == "email" })?.value - notifyWaitersWith(event: .activateMPCWallet(email: email), - receivedState: receivedState) + let email = getValueIn(components: components, withName: "email") + + if let recoveryToken = getValueIn(components: components, withName: "recoveryToken"), + let email { + let data = MPCResetPasswordData(email: email, recoveryToken: recoveryToken) + notifyWaitersWith(event: .resetMPCWalletPassword(data: data), + receivedState: receivedState) + } else { + notifyWaitersWith(event: .activateMPCWallet(email: email), + receivedState: receivedState) + } + return true } return false } + func getValueIn(components: NSURLComponents, + withName valueName: String) -> String? { + components.queryItems?.first(where: { $0.name == valueName })?.value + } + func notifyWaitersWith(event: DeepLinkEvent, receivedState: ExternalEventReceivedState) { Debugger.printInfo(topic: .UniversalLink, "Did receive deep link event \(event)") listeners.forEach { holder in diff --git a/unstoppable-ios-app/domains-manager-ios/Services/DeepLinksService/DeepLinksServiceProtocol.swift b/unstoppable-ios-app/domains-manager-ios/Services/DeepLinksService/DeepLinksServiceProtocol.swift index a62291f2f..7723aa3f2 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/DeepLinksService/DeepLinksServiceProtocol.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/DeepLinksService/DeepLinksServiceProtocol.swift @@ -12,6 +12,7 @@ enum DeepLinkEvent: Equatable { case showUserDomainProfile(domain: DomainDisplayInfo, wallet: WalletEntity, action: PreRequestedProfileAction?) case showPublicDomainProfile(publicDomainDisplayInfo: PublicDomainDisplayInfo, wallet: WalletEntity, action: PreRequestedProfileAction?) case activateMPCWallet(email: String?) + case resetMPCWalletPassword(data: MPCResetPasswordData) } protocol DeepLinksServiceProtocol { diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Localization/en.lproj/Localizable.strings b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Localization/en.lproj/Localizable.strings index 90f54bfc8..fb5619f10 100644 --- a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Localization/en.lproj/Localizable.strings +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Localization/en.lproj/Localizable.strings @@ -1215,6 +1215,8 @@ More tabs are coming in the next updates."; "MPC_RECOVERY_REQUESTED_SUBTITLE_HIGHLIGHTS" = "'Unstoppable Lite Wallet Recovery Kit'\nno-reply@unstoppabledomains.com"; "MPC_RECOVERY_REQUESTED_HINT_NOT_SHARE" = "Do not share your recovery kit\nwith anyone"; "MPC_RECOVERY_REQUESTED_HINT_PREVIOUS_INACTIVE" = "Previous recovery kits will become inactive when a new one is created"; +"MPC_RESET_PASSWORD_TITLE" = "Reset password"; +"MPC_RESET_PASSWORD_SUBTITLE" = "Do note share your password with anyone, including Unstoppable Domains staff."; // Send crypto first time "SEND_CRYPTO_FIRST_TIME_PULL_UP_TITLE" = "Sending cryptocurrency for the first time?"; From 7ff9bba7ed622b4bdfeb5e7bc857c0e38ee6d8b5 Mon Sep 17 00:00:00 2001 From: Oleg Date: Mon, 4 Nov 2024 16:02:00 +0200 Subject: [PATCH 4/6] Fixed race condition issue when accessing app context --- .../domains-manager-ios/SupportingFiles/AppDelegate.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/AppDelegate.swift b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/AppDelegate.swift index 52dc6730c..0dc98c23f 100644 --- a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/AppDelegate.swift +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/AppDelegate.swift @@ -21,9 +21,7 @@ protocol AppDelegateProtocol { @main class AppDelegate: UIResponder, UIApplicationDelegate { - private(set) lazy var appContext: AppContextProtocol = { - GeneralAppContext() - }() + private(set) var appContext: AppContextProtocol = GeneralAppContext() static let shared: AppDelegateProtocol = UIApplication.shared.delegate as! AppDelegateProtocol var syncWalletsPopupShownCount = 0 From 592e31c53cb3d87c1dc6c43d6187a253489e67be Mon Sep 17 00:00:00 2001 From: Oleg Date: Tue, 12 Nov 2024 18:16:36 +0200 Subject: [PATCH 5/6] MOB-2196 - Update Fireblocks SDK to 2.9.0 (#690) --- .../domains-manager-ios.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj b/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj index 86fef2b26..9ac074404 100644 --- a/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj +++ b/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj @@ -12393,7 +12393,7 @@ repositoryURL = "https://github.com/fireblocks/ncw-ios-sdk.git"; requirement = { kind = exactVersion; - version = 2.8.2; + version = 2.9.0; }; }; C66811F02B47B0F500BDABB0 /* XCRemoteSwiftPackageReference "xmtp-ios" */ = { diff --git a/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3776a7d15..b162ed9af 100644 --- a/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -114,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/fireblocks/ncw-ios-sdk.git", "state" : { - "revision" : "c71c7fd281ec3edba921a72d8adab7fabcf5c9bb", - "version" : "2.8.2" + "revision" : "9ed23d0128b2f86c5bfa080cf7822b4ed6a27f04", + "version" : "2.9.0" } }, { From b0f961bffddf9e7034f8d68c1b6f5c21deeafe74 Mon Sep 17 00:00:00 2001 From: Oleg Date: Thu, 28 Nov 2024 12:30:28 +0200 Subject: [PATCH 6/6] MPC wallet 2FA support (#691) * Add reset password event to list of known * Implemented UI for enter password view * Localize view * Present reset flow on DL * Added API request to reset password * Activate wallet function refactoring * MPC wallet service refactoring * Prepare reset mpc flow entities * Added enter code step * Added final step with wallet activation * Multiple fixes * Fixed creation of new recovery kit after recovery * Handle recovery during onboarding * MOB-2112 - Show 2FA status for ULW (#686) * Created API to work with 2FA * Store 2FA status locally * Implemented functionality to check current 2fa status Implemented UI in wallet details view * Fixed compile error * MOB-2113 - Enable MPC 2FA (#687) * Fixed compile error * Added interface to request and confirm OTP to setup 2FA * Implementing UI to enable 2FA * Added qr code and continue button * Start enable 2FA flow * Implemented UI to verify code. Implemented naviogation to code verification * Show toast after 2fa enabled * Update mpc wallet account details after 2fa is on * Refactoring * Refactoring * Refactoring * MOB-2114 - Disable MPC 2FA (#688) * Implemented 2FA enabled pull up * Implemented UI for pull up to confirm disable 2FA Run disable action * Restore 2FA verification * Refactoring * Implemented functionality to ask for OTP to disable 2FA * Added enter code presentation style Updated when disabling 2fa * Update wallet info after 2FA disabled * MOB-2115 - Request and pass 2FA OTP when needed (#689) * Implemented handling of OTP is required during login * Refactoring * Fixed preview and tests targets * Control 2fa accessibility via FF --- .../PreviewCoreAppCoordinator.swift | 4 + .../PreviewImageLoadingService.swift | 3 + .../Entities/PreviewMPCConnector.swift | 20 ++ .../Entities/PreviewUDWallet.swift | 2 +- .../project.pbxproj | 32 +++ .../Entities/Mock/MockEntitiesFabric.swift | 18 ++ .../domains-manager-ios/Entities/Toast.swift | 11 +- .../Extensions/Extension-String+Preview.swift | 14 ++ .../Extensions/UIImage.swift | 1 + .../Home/NavigationViewWithCustomTitle.swift | 4 + .../HomeWalletNavigationDestination.swift | 14 ++ .../MPCWallet2FASetupDetails.swift | 30 +++ .../FB_UD_MPCConnectedWalletDetails.swift | 14 +- .../FB_UD_MPCConnectionService.swift | 69 ++++++- ...D_DefaultMPCConnectionNetworkService.swift | 98 ++++++++++ .../FB_UD_MPCConnectionNetworkService.swift | 8 + .../Network/FB_UD_MPCNetwork.swift | 5 + .../Network/FB_UD_MPCOTPProvider.swift | 14 ++ .../MPCWalletProviderSubServiceProtocol.swift | 6 + .../MPCWalletsService/MPCWalletsService.swift | 22 +++ .../MPCWalletsServiceProtocol.swift | 7 + .../2FA/MPCSetup2FAConfirmCodeView.swift | 162 +++++++++++++++ .../2FA/MPCSetup2FAEnableView.swift | 184 ++++++++++++++++++ .../Modules/WalletDetails/WalletDetails.swift | 15 +- .../WalletDetails/WalletDetailsView.swift | 53 ++++- .../ApiRequestBuilder.swift | 6 +- .../AnalyticsServiceEnvironment.swift | 6 +- .../CoreAppCoordinator.swift | 13 ++ .../Services/FeatureFlags/UDFeatureFlag.swift | 5 +- .../shieldCheckmark.imageset/Contents.json | 15 ++ .../shieldCheckmark.svg | 3 + .../Contents.json | 15 ++ .../shieldCheckmarkFilled.svg | 3 + .../Common/shieldEmpty.imageset/Contents.json | 15 ++ .../shieldEmpty.imageset/shieldEmpty.svg | 3 + .../Localization/en.lproj/Localizable.strings | 14 ++ .../SwiftUI/Extensions/Image.swift | 3 + .../ViewModifiers/ViewPullUp/ViewPullUp.swift | 3 +- .../ViewPullUpDefaultConfiguration.swift | 34 +++- .../DeepLinksServiceTests.swift | 4 + .../FB_UD_MPCConnectionServiceTests.swift | 29 +++ .../Helpers/TestableMPCWalletsService.swift | 26 +++ 42 files changed, 982 insertions(+), 25 deletions(-) create mode 100644 unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWallet2FASetupDetails.swift create mode 100644 unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCOTPProvider.swift create mode 100644 unstoppable-ios-app/domains-manager-ios/Modules/WalletDetails/2FA/MPCSetup2FAConfirmCodeView.swift create mode 100644 unstoppable-ios-app/domains-manager-ios/Modules/WalletDetails/2FA/MPCSetup2FAEnableView.swift create mode 100644 unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldCheckmark.imageset/Contents.json create mode 100644 unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldCheckmark.imageset/shieldCheckmark.svg create mode 100644 unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldCheckmarkFilled.imageset/Contents.json create mode 100644 unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldCheckmarkFilled.imageset/shieldCheckmarkFilled.svg create mode 100644 unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldEmpty.imageset/Contents.json create mode 100644 unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldEmpty.imageset/shieldEmpty.svg diff --git a/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewCoreAppCoordinator.swift b/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewCoreAppCoordinator.swift index 3a4036b2f..8246a44df 100644 --- a/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewCoreAppCoordinator.swift +++ b/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewCoreAppCoordinator.swift @@ -9,6 +9,10 @@ import UIKit final class CoreAppCoordinator: CoreAppCoordinatorProtocol { + func askForMPC2FACode() async -> String? { + "" + } + private var window: UIWindow? func askToReconnectMPCWallet(_ reconnectData: MPCWalletReconnectData) async { diff --git a/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewImageLoadingService.swift b/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewImageLoadingService.swift index af5825ee1..c4e65f44b 100644 --- a/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewImageLoadingService.swift +++ b/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewImageLoadingService.swift @@ -64,6 +64,9 @@ final class ImageLoadingService: ImageLoadingServiceProtocol { } case .wcApp: return UIImage.Preview.previewSquare + case .qrCode(_ , _): + await Task.sleep(seconds: 1) + return UIImage.Preview.previewSquare default: return nil } diff --git a/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewMPCConnector.swift b/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewMPCConnector.swift index 3ecce400e..8a33b010b 100644 --- a/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewMPCConnector.swift +++ b/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewMPCConnector.swift @@ -40,6 +40,8 @@ extension FB_UD_MPC { } struct DefaultMPCConnectionNetworkService: MPCConnectionNetworkService { + var otpProvider: FB_UD_MPC.MPCOTPProvider? + func getAssetTransferEstimations(accessToken: String, accountId: String, assetId: String, destinationAddress: String, amount: String) async throws -> FB_UD_MPC.NetworkFeeResponse { .init(priority: "", status: "", networkFee: nil) } @@ -149,6 +151,24 @@ extension FB_UD_MPC { requestId: String) async throws { await Task.sleep(seconds: 0.5) } + + func get2FAStatus(accessToken: String) async throws -> Bool { + await Task.sleep(seconds: 0.5) + return false + } + + func enable2FA(accessToken: String) async throws -> String { + await Task.sleep(seconds: 0.5) + return "" + } + + func verify2FAToken(accessToken: String, token: String) async throws { + await Task.sleep(seconds: 0.5) + } + + func disable2FA(accessToken: String, token: String) async throws { + await Task.sleep(seconds: 0.5) + } } struct MPCWalletsDefaultDataStorage: MPCWalletsDataStorage { diff --git a/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewUDWallet.swift b/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewUDWallet.swift index 1105bfc27..de7978e1f 100644 --- a/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewUDWallet.swift +++ b/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewUDWallet.swift @@ -13,7 +13,7 @@ struct UDWallet: Codable, Hashable { var address: String = "0xc4a748796805dfa42cafe0901ec182936584cc6e" var type: WalletType = .generatedLocally var hasBeenBackedUp: Bool? = false - private(set) var mpcMetadata: MPCWalletMetadata? + var mpcMetadata: MPCWalletMetadata? struct WalletConnectionInfo: Codable { var externalWallet: WCWalletsProvider.WalletRecord diff --git a/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj b/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj index 9ac074404..c662dd482 100644 --- a/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj +++ b/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj @@ -1149,6 +1149,8 @@ C66A26E628B4C22F005470B9 /* LoadPaginatedFetchableOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C66A26E428B4C22F005470B9 /* LoadPaginatedFetchableOperation.swift */; }; C66A26EA28B4C22F005470B9 /* BaseOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C66A26E528B4C22F005470B9 /* BaseOperation.swift */; }; C66A9DC828BCCE6600F8BA3F /* CNavigationControllerChildNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C66A9DC728BCCE6600F8BA3F /* CNavigationControllerChildNavigationHandler.swift */; }; + C66DDDF62CCF9589000B947E /* MPCSetup2FAConfirmCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C66DDDF52CCF9589000B947E /* MPCSetup2FAConfirmCodeView.swift */; }; + C66DDDF72CCF9589000B947E /* MPCSetup2FAConfirmCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C66DDDF52CCF9589000B947E /* MPCSetup2FAConfirmCodeView.swift */; }; C66FCCF32844863E009B9525 /* UDGradientCoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C66FCCF22844863E009B9525 /* UDGradientCoverView.swift */; }; C66FCCF82844870B009B9525 /* UserDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C66FCCF72844870B009B9525 /* UserDataService.swift */; }; C66FCCFE2844871A009B9525 /* UserDataServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C66FCCFD2844871A009B9525 /* UserDataServiceProtocol.swift */; }; @@ -1353,6 +1355,8 @@ C68980832CCA39E300A4CFC0 /* MPCResetPasswordEnterCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68980812CCA39E300A4CFC0 /* MPCResetPasswordEnterCodeView.swift */; }; C68980852CCA3AAE00A4CFC0 /* MPCResetPasswordActivateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68980842CCA3AAE00A4CFC0 /* MPCResetPasswordActivateView.swift */; }; C68980862CCA3AAE00A4CFC0 /* MPCResetPasswordActivateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68980842CCA3AAE00A4CFC0 /* MPCResetPasswordActivateView.swift */; }; + C68980892CCBD74900A4CFC0 /* MPCSetup2FAEnableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68980882CCBD74900A4CFC0 /* MPCSetup2FAEnableView.swift */; }; + C689808A2CCBD74900A4CFC0 /* MPCSetup2FAEnableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68980882CCBD74900A4CFC0 /* MPCSetup2FAEnableView.swift */; }; C689C1732ADE484300AA0186 /* LaunchDarkly in Frameworks */ = {isa = PBXBuildFile; productRef = C689C1722ADE484300AA0186 /* LaunchDarkly */; }; C689C1752ADE56AF00AA0186 /* UDFeatureFlagsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C689C1742ADE56AF00AA0186 /* UDFeatureFlagsService.swift */; }; C689C1772ADE693900AA0186 /* LaunchDarklyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C689C1762ADE693900AA0186 /* LaunchDarklyService.swift */; }; @@ -1791,6 +1795,10 @@ C6C1EC5F2A3AD483005EB37D /* UIAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C1EC5E2A3AD483005EB37D /* UIAction.swift */; }; C6C421702934A839005B791B /* DomainProfileSignatureValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C4216F2934A839005B791B /* DomainProfileSignatureValidator.swift */; }; C6C4218C293A1E21005B791B /* UDConfigurableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C4218B293A1E20005B791B /* UDConfigurableButton.swift */; }; + C6C52D8F2CD0C5FA008AAB4E /* MPCWallet2FASetupDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C52D8E2CD0C5FA008AAB4E /* MPCWallet2FASetupDetails.swift */; }; + C6C52D902CD0C5FA008AAB4E /* MPCWallet2FASetupDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C52D8E2CD0C5FA008AAB4E /* MPCWallet2FASetupDetails.swift */; }; + C6C52D922CD0E7C6008AAB4E /* FB_UD_MPCOTPProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C52D912CD0E7C6008AAB4E /* FB_UD_MPCOTPProvider.swift */; }; + C6C52D932CD0E7C6008AAB4E /* FB_UD_MPCOTPProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C52D912CD0E7C6008AAB4E /* FB_UD_MPCOTPProvider.swift */; }; C6C57C3D2869C0890093FD8C /* UDDomainSharingWatchCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C57C3C2869C0890093FD8C /* UDDomainSharingWatchCardView.swift */; }; C6C57C472869C0A50093FD8C /* UDDomainSharingWatchCardView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C6C57C462869C0A50093FD8C /* UDDomainSharingWatchCardView.xib */; }; C6C57C4D2869D8C90093FD8C /* SaveDomainImageTypePullUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C57C4C2869D8C90093FD8C /* SaveDomainImageTypePullUpView.swift */; }; @@ -3567,6 +3575,7 @@ C66A26E428B4C22F005470B9 /* LoadPaginatedFetchableOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadPaginatedFetchableOperation.swift; sourceTree = ""; }; C66A26E528B4C22F005470B9 /* BaseOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseOperation.swift; sourceTree = ""; }; C66A9DC728BCCE6600F8BA3F /* CNavigationControllerChildNavigationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CNavigationControllerChildNavigationHandler.swift; sourceTree = ""; }; + C66DDDF52CCF9589000B947E /* MPCSetup2FAConfirmCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCSetup2FAConfirmCodeView.swift; sourceTree = ""; }; C66FCCF22844863E009B9525 /* UDGradientCoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDGradientCoverView.swift; sourceTree = ""; }; C66FCCF72844870B009B9525 /* UserDataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDataService.swift; sourceTree = ""; }; C66FCCFD2844871A009B9525 /* UserDataServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDataServiceProtocol.swift; sourceTree = ""; }; @@ -3693,6 +3702,7 @@ C689807E2CCA353C00A4CFC0 /* MPCResetPasswordRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCResetPasswordRootView.swift; sourceTree = ""; }; C68980812CCA39E300A4CFC0 /* MPCResetPasswordEnterCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCResetPasswordEnterCodeView.swift; sourceTree = ""; }; C68980842CCA3AAE00A4CFC0 /* MPCResetPasswordActivateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCResetPasswordActivateView.swift; sourceTree = ""; }; + C68980882CCBD74900A4CFC0 /* MPCSetup2FAEnableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCSetup2FAEnableView.swift; sourceTree = ""; }; C689C1742ADE56AF00AA0186 /* UDFeatureFlagsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDFeatureFlagsService.swift; sourceTree = ""; }; C689C1762ADE693900AA0186 /* LaunchDarklyService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchDarklyService.swift; sourceTree = ""; }; C689C1792ADE6D2500AA0186 /* UDFeatureFlagsServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDFeatureFlagsServiceProtocol.swift; sourceTree = ""; }; @@ -3969,6 +3979,8 @@ C6C42181293741A4005B791B /* DomainProfileTutorialItemViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainProfileTutorialItemViewController.swift; sourceTree = ""; }; C6C4218629379D09005B791B /* DomainProfileTutorialItemPrivacyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainProfileTutorialItemPrivacyViewController.swift; sourceTree = ""; }; C6C4218B293A1E20005B791B /* UDConfigurableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDConfigurableButton.swift; sourceTree = ""; }; + C6C52D8E2CD0C5FA008AAB4E /* MPCWallet2FASetupDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCWallet2FASetupDetails.swift; sourceTree = ""; }; + C6C52D912CD0E7C6008AAB4E /* FB_UD_MPCOTPProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FB_UD_MPCOTPProvider.swift; sourceTree = ""; }; C6C57C3C2869C0890093FD8C /* UDDomainSharingWatchCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDDomainSharingWatchCardView.swift; sourceTree = ""; }; C6C57C462869C0A50093FD8C /* UDDomainSharingWatchCardView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = UDDomainSharingWatchCardView.xib; sourceTree = ""; }; C6C57C4C2869D8C90093FD8C /* SaveDomainImageTypePullUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveDomainImageTypePullUpView.swift; sourceTree = ""; }; @@ -4708,6 +4720,7 @@ C6A231FE2BEB52CB0037E093 /* WalletSourceImageView.swift */, C6A232012BEB53310037E093 /* RenameWalletView.swift */, C6918C682CB54EFD00036C63 /* MPCWalletMetadataDisplayInfo.swift */, + C68980872CCBD71700A4CFC0 /* 2FA */, ); path = WalletDetails; sourceTree = ""; @@ -7056,6 +7069,15 @@ path = ChannelView; sourceTree = ""; }; + C68980872CCBD71700A4CFC0 /* 2FA */ = { + isa = PBXGroup; + children = ( + C68980882CCBD74900A4CFC0 /* MPCSetup2FAEnableView.swift */, + C66DDDF52CCF9589000B947E /* MPCSetup2FAConfirmCodeView.swift */, + ); + path = 2FA; + sourceTree = ""; + }; C689C1782ADE6D1700AA0186 /* FeatureFlags */ = { isa = PBXGroup; children = ( @@ -7150,6 +7172,7 @@ C6952A522BC64F8400F4B475 /* MPCWalletError.swift */, C6952A222BC4E33000F4B475 /* MPCWalletProvider.swift */, C6952A4F2BC531E000F4B475 /* MPCWalletMetadata.swift */, + C6C52D8E2CD0C5FA008AAB4E /* MPCWallet2FASetupDetails.swift */, C632BE4F2BA59D8400C95B2D /* SetupMPCWalletStep.swift */, C68980722CCA320700A4CFC0 /* SetupMPCFlow.swift */, C6952A312BC4EDFA00F4B475 /* MPCWalletSubServices */, @@ -7161,6 +7184,7 @@ isa = PBXGroup; children = ( C65330512BC3AD9700A705AE /* FB_UD_MPCNetwork.swift */, + C6C52D912CD0E7C6008AAB4E /* FB_UD_MPCOTPProvider.swift */, C632BE4C2BA599E900C95B2D /* FB_UD_MPCConnectionNetworkService.swift */, C632BE5B2BA59F0700C95B2D /* FB_UD_DefaultMPCConnectionNetworkService.swift */, ); @@ -9171,6 +9195,7 @@ C609821C2823D36100546392 /* PullUpCollectionViewCell.swift in Sources */, C6C9958E289D313D00367362 /* CNavigationTransitioningContext.swift in Sources */, C621A4422BB1137900CB5CB9 /* QRScannerState.swift in Sources */, + C6C52D922CD0E7C6008AAB4E /* FB_UD_MPCOTPProvider.swift in Sources */, C6534AA32BBFBA10008EEBB5 /* HomeExploreEmptySearchResultView.swift in Sources */, C62247B5283CFF1F002A0CBD /* DomainTransactionsService.swift in Sources */, C6534A8D2BBFBA10008EEBB5 /* HomeExploreFollowerCellView.swift in Sources */, @@ -9304,6 +9329,7 @@ C6109E2B28E5AB310027D5D8 /* MockAuthentificationService.swift in Sources */, C60CA29A2832501D00AB1B36 /* QRCodeServiceProtocol.swift in Sources */, C60C59B52B47FC0900A2522C /* LoginProvider.swift in Sources */, + C6C52D902CD0C5FA008AAB4E /* MPCWallet2FASetupDetails.swift in Sources */, C6E1188328F5709D00C6AD4D /* WalletConnectPushNotificationsSubscribeInfo.swift in Sources */, C642A6BD28411ECE00299C6E /* MintDomainsNavigationController.swift in Sources */, C6637A6A2810074D0000AE56 /* SecurityWindow.swift in Sources */, @@ -9912,6 +9938,7 @@ C62C3FA1283F60720094FC93 /* DeepLinksServiceProtocol.swift in Sources */, C68980742CCA320700A4CFC0 /* SetupMPCFlow.swift in Sources */, C664B670291542E300A76154 /* DomainProfileWeb3WebsiteCell.swift in Sources */, + C66DDDF62CCF9589000B947E /* MPCSetup2FAConfirmCodeView.swift in Sources */, C65272212A14B32F001A084C /* MessagingServiceProtocol.swift in Sources */, C688C17C2B8443CA00BD233A /* ChatListEmptyStateView.swift in Sources */, 306665E12778AE74005F6F55 /* UnsConfigManager.swift in Sources */, @@ -10130,6 +10157,7 @@ C60CA2922832501D00AB1B36 /* QRCodeMonkeyServiceProvider.swift in Sources */, C665B9AD28325282005E535C /* DomainDetailsViewPresenter.swift in Sources */, C6F4333F2BB25BD4000C5E46 /* ConfirmSendTokenViewsBuilderProtocol.swift in Sources */, + C689808A2CCBD74900A4CFC0 /* MPCSetup2FAEnableView.swift in Sources */, C6526DE829D2E06800D6F2EB /* NoParkedDomainsFoundOnboardingViewPresenter.swift in Sources */, C652722B2A14B395001A084C /* MockMessagingService.swift in Sources */, C6EECE9D2833A42600978ED5 /* CoinRecordsService.swift in Sources */, @@ -10444,6 +10472,7 @@ C61807CF2B19A3FA0032E543 /* ExternalEvent.swift in Sources */, C618080D2B19AA2F0032E543 /* DomainRecordsServiceProtocol.swift in Sources */, C6D646FF2B1ED7C300D724AC /* MessagingPrivateChatBlockingStatus.swift in Sources */, + C6C52D932CD0E7C6008AAB4E /* FB_UD_MPCOTPProvider.swift in Sources */, C6D8FF342B83180A0094A21E /* ChatListViewModel.swift in Sources */, C651C6DC2C66016C0076F631 /* PurchaseDomainsOrderSummaryView.swift in Sources */, C6D647122B1ED7D000D724AC /* MessagingChatMessageRemoteContentTypeDisplayInfo.swift in Sources */, @@ -11087,6 +11116,7 @@ C6C8F9952B218BBB00A9834D /* Double.swift in Sources */, C6D6463C2B1DC53700D724AC /* WebViewController.swift in Sources */, C6D646A22B1ED15A00D724AC /* PublicProfileSocialsListView.swift in Sources */, + C66DDDF72CCF9589000B947E /* MPCSetup2FAConfirmCodeView.swift in Sources */, C6D646122B1DC0DD00D724AC /* UnifiedConnectedAppInfoHolder.swift in Sources */, C6D645842B1DBA8600D724AC /* UIMenuDomainAvatarLoader.swift in Sources */, C6A232002BEB52CB0037E093 /* WalletSourceImageView.swift in Sources */, @@ -11167,6 +11197,7 @@ C6D646812B1ED12900D724AC /* DomainProfileSectionsController.swift in Sources */, C6D011C32B9967A30008BF40 /* UDPageControlView.swift in Sources */, C6102FC82B6A339A0098AF75 /* HomeWebAccountView.swift in Sources */, + C68980892CCBD74900A4CFC0 /* MPCSetup2FAEnableView.swift in Sources */, C6B761E02BB3D78F00773943 /* SerializedWalletTransaction.swift in Sources */, C6BF6BDD2B8F11CC006CC2BD /* TaskWithDeadline.swift in Sources */, C6534A922BBFBA10008EEBB5 /* HomeExploreFollowersSectionView.swift in Sources */, @@ -11293,6 +11324,7 @@ C618086D2B19BC0C0032E543 /* HexagonShape.swift in Sources */, C630C7372BD781E900AC1662 /* OnboardingAddWalletViewController.swift in Sources */, C6D645C62B1DBD3900D724AC /* CNavigationBarContentView.swift in Sources */, + C6C52D8F2CD0C5FA008AAB4E /* MPCWallet2FASetupDetails.swift in Sources */, C68F156E2BD659740049BFA2 /* MPCActivateWalletEnterView.swift in Sources */, C6D647A72B1F18AE00D724AC /* HappyEndViewController.swift in Sources */, C61808722B19BC150032E543 /* Color.swift in Sources */, diff --git a/unstoppable-ios-app/domains-manager-ios/Entities/Mock/MockEntitiesFabric.swift b/unstoppable-ios-app/domains-manager-ios/Entities/Mock/MockEntitiesFabric.swift index c9fde841c..6dfa7ab99 100644 --- a/unstoppable-ios-app/domains-manager-ios/Entities/Mock/MockEntitiesFabric.swift +++ b/unstoppable-ios-app/domains-manager-ios/Entities/Mock/MockEntitiesFabric.swift @@ -121,6 +121,24 @@ extension MockEntitiesFabric { } } + + static func mockMPC() -> WalletEntity { + let address = "0xmpcwallet" + let udWallet = UDWallet.createMPC(address: address, + aliasName: "Lite Wallet", + mpcMetadata: .init(provider: .fireblocksUD, + metadata: Data())) + let displayInfo = WalletDisplayInfo(wallet: udWallet, + domainsCount: 0, + udDomainsCount: 0)! + return WalletEntity(udWallet: udWallet, + displayInfo: displayInfo, + domains: [], + nfts: [], + balance: [], + rrDomain: nil, + portfolioRecords: []) + } static func createFrom(udWallet: UDWallet, hasRRDomain: Bool = true) -> WalletEntity { diff --git a/unstoppable-ios-app/domains-manager-ios/Entities/Toast.swift b/unstoppable-ios-app/domains-manager-ios/Entities/Toast.swift index ea573bfa7..40dcb8bce 100644 --- a/unstoppable-ios-app/domains-manager-ios/Entities/Toast.swift +++ b/unstoppable-ios-app/domains-manager-ios/Entities/Toast.swift @@ -27,6 +27,7 @@ enum Toast: Hashable { case followedProfileAs(DomainName) case domainRemoved case cartCleared + case enabled2FA var message: String { switch self { @@ -74,12 +75,14 @@ enum Toast: Hashable { return String.Constants.domainRemoved.localized() case .cartCleared: return String.Constants.cartCleared.localized() + case .enabled2FA: + return String.Constants.enabled2FAToastMessage.localized() } } var secondaryMessage: String? { switch self { - case .walletAddressCopied, .walletAdded, .iCloudBackupRestored, .walletRemoved, .walletDisconnected, .noInternetConnection, .changesConfirmed, .mintingSuccessful, .mintingUnavailable, .updatingRecords, .domainCopied, .failedToRefreshBadges, .itemSaved, .itemCopied, .userLoggedOut, .communityProfileEnabled, .purchaseDomainsDiscountApplied, .followedProfileAs, .domainRemoved, .cartCleared: + case .walletAddressCopied, .walletAdded, .iCloudBackupRestored, .walletRemoved, .walletDisconnected, .noInternetConnection, .changesConfirmed, .mintingSuccessful, .mintingUnavailable, .updatingRecords, .domainCopied, .failedToRefreshBadges, .itemSaved, .itemCopied, .userLoggedOut, .communityProfileEnabled, .purchaseDomainsDiscountApplied, .followedProfileAs, .domainRemoved, .cartCleared, .enabled2FA: return nil case .failedToFetchDomainProfileData: return String.Constants.refresh.localized() @@ -90,7 +93,7 @@ enum Toast: Hashable { var style: Style { switch self { - case .walletAddressCopied, .walletAdded, .iCloudBackupRestored, .walletRemoved, .walletDisconnected, .changesConfirmed, .mintingSuccessful, .domainCopied, .itemSaved, .itemCopied, .userLoggedOut, .communityProfileEnabled, .purchaseDomainsDiscountApplied, .followedProfileAs, .domainRemoved, .cartCleared: + case .walletAddressCopied, .walletAdded, .iCloudBackupRestored, .walletRemoved, .walletDisconnected, .changesConfirmed, .mintingSuccessful, .domainCopied, .itemSaved, .itemCopied, .userLoggedOut, .communityProfileEnabled, .purchaseDomainsDiscountApplied, .followedProfileAs, .domainRemoved, .cartCleared, .enabled2FA: return .success case .noInternetConnection, .updatingRecords, .mintingUnavailable, .failedToFetchDomainProfileData, .failedToUpdateProfile: return .dark @@ -101,7 +104,7 @@ enum Toast: Hashable { var image: UIImage { switch self { - case .walletAddressCopied, .walletAdded, .iCloudBackupRestored, .walletRemoved, .walletDisconnected, .changesConfirmed, .mintingSuccessful, .domainCopied, .itemSaved, .itemCopied, .userLoggedOut, .communityProfileEnabled, .purchaseDomainsDiscountApplied, .followedProfileAs, .domainRemoved, .cartCleared: + case .walletAddressCopied, .walletAdded, .iCloudBackupRestored, .walletRemoved, .walletDisconnected, .changesConfirmed, .mintingSuccessful, .domainCopied, .itemSaved, .itemCopied, .userLoggedOut, .communityProfileEnabled, .purchaseDomainsDiscountApplied, .followedProfileAs, .domainRemoved, .cartCleared, .enabled2FA: return .checkCircleWhite case .noInternetConnection: return .cloudOfflineIcon @@ -162,6 +165,8 @@ enum Toast: Hashable { return true case (.cartCleared, .cartCleared): return true + case (.enabled2FA, .enabled2FA): + return true default: return false } diff --git a/unstoppable-ios-app/domains-manager-ios/Extensions/Extension-String+Preview.swift b/unstoppable-ios-app/domains-manager-ios/Extensions/Extension-String+Preview.swift index 7086d6007..6660fe5e4 100644 --- a/unstoppable-ios-app/domains-manager-ios/Extensions/Extension-String+Preview.swift +++ b/unstoppable-ios-app/domains-manager-ios/Extensions/Extension-String+Preview.swift @@ -510,6 +510,19 @@ extension String { static let addToBackupNewWalletSubtitle = "ADD_TO_BACKUP_NEW_WALLET_SUBTITLE" static let addToCurrentBackupNewWalletTitle = "ADD_TO_CURRENT_BACKUP_NEW_WALLET_TITLE" static let createVault = "CREATE_VAULT" + static let mpc2FAEnabled = "MPC_2FA_ENABLED" + static let mpc2FAEnable = "MPC_2FA_ENABLE" + static let enable2FATitle = "ENABLE_2FA_TITLE" + static let enable2FASubtitle = "ENABLE_2FA_SUBTITLE" + static let enable2FACopySecretTitle = "ENABLE_2FA_COPY_SECRET_TITLE" + static let enable2FAOrScanQRCode = "ENABLE_2FA_OR_SCAN_QR_CODE" + static let enable2FAConfirmTitle = "ENABLE_2FA_CONFIRM_TITLE" + static let enable2FAConfirmSubtitle = "ENABLE_2FA_CONFIRM_SUBTITLE" + static let disable2FA = "DISABLE_2FA" + static let mpc2FAEnabledPullUpTitle = "MPC_2FA_ENABLED_PULLUP_TITLE" + static let mpc2FAEnabledPullUpSubtitle = "MPC_2FA_ENABLED_PULLUP_SUBTITLE" + static let mpc2FADisableConfirmationPullUpTitle = "MPC_2FA_DISABLE_CONFIRMATION_PULLUP_TITLE" + static let mpc2FADisableConfirmationPullUpSubtitle = "MPC_2FA_DISABLE_CONFIRMATION_PULLUP_SUBTITLE" // Toast messages static let toastWalletAddressCopied = "TOAST_WALLET_ADDRESS_COPIED" @@ -1113,6 +1126,7 @@ extension String { static let endings = "ENDINGS" static let suggestions = "SUGGESTIONS" static let domainsPurchasedSummaryMessage = "DOMAINS_PURCHASED_SUMMARY_MESSAGE" + static let enabled2FAToastMessage = "ENABLED_2FA_TOAST_MESSAGE" // Home static let homeWalletTokensComeTitle = "HOME_WALLET_TOKENS_COME_TITLE" diff --git a/unstoppable-ios-app/domains-manager-ios/Extensions/UIImage.swift b/unstoppable-ios-app/domains-manager-ios/Extensions/UIImage.swift index e03e56bd3..5f0ad2c1d 100644 --- a/unstoppable-ios-app/domains-manager-ios/Extensions/UIImage.swift +++ b/unstoppable-ios-app/domains-manager-ios/Extensions/UIImage.swift @@ -142,6 +142,7 @@ extension UIImage { static let unsTLDLogo = UIImage(named: "unsTLDLogo")! static let ensTLDLogo = UIImage(named: "ensTLDLogo")! static let dnsTLDLogo = UIImage(named: "dnsTLDLogo")! + static let shieldCheckmarkFilled = UIImage(named: "shieldCheckmarkFilled")! static let twitterIcon24 = UIImage(named: "twitterIcon24")! static let discordIcon24 = UIImage(named: "discordIcon24")! diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/NavigationViewWithCustomTitle.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/NavigationViewWithCustomTitle.swift index 0fbe18a6b..c3267c916 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/NavigationViewWithCustomTitle.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/NavigationViewWithCustomTitle.swift @@ -140,6 +140,10 @@ struct NavigationPathWrapper where Data : Hashable { navigationPath.removeLast() } + mutating func removeLast(_ num: Int) { + navigationPath.removeLast(num) + } + func first(where isIncluded: (Data) -> Bool) -> Data? { navigationTypedPath.first(where: isIncluded) } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletNavigationDestination.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletNavigationDestination.swift index dd2def4a1..0337f436b 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletNavigationDestination.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletNavigationDestination.swift @@ -19,6 +19,8 @@ enum HomeWalletNavigationDestination: Hashable { case walletDetails(WalletEntity) case securitySettings case setupPasscode(SetupPasscodeViewController.Mode) + case mpcSetup2FAEnable(mpcMetadata: MPCWalletMetadata) + case mpcSetup2FAEnableConfirm(mpcMetadata: MPCWalletMetadata) static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs, rhs) { @@ -38,6 +40,10 @@ enum HomeWalletNavigationDestination: Hashable { return true case (.setupPasscode, .setupPasscode): return true + case (.mpcSetup2FAEnable, .mpcSetup2FAEnable): + return true + case (.mpcSetup2FAEnableConfirm, .mpcSetup2FAEnableConfirm): + return true default: return false } @@ -61,6 +67,10 @@ enum HomeWalletNavigationDestination: Hashable { hasher.combine("securitySettings") case .setupPasscode: hasher.combine("setupPasscode") + case .mpcSetup2FAEnable: + hasher.combine("mpcSetup2FAEnable") + case .mpcSetup2FAEnableConfirm: + hasher.combine("mpcSetup2FAEnable") } } @@ -95,6 +105,10 @@ struct HomeWalletLinkNavigationDestination { .ignoresSafeArea() case .walletDetails(let wallet): WalletDetailsView(wallet: wallet, source: .settings) + case .mpcSetup2FAEnable(let mpcMetadata): + MPCSetup2FAEnableView(mpcMetadata: mpcMetadata) + case .mpcSetup2FAEnableConfirm(let mpcMetadata): + MPCSetup2FAConfirmCodeView(verificationPurpose: .enable(mpcMetadata)) case .securitySettings: SecuritySettingsView() case .setupPasscode(let mode): diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWallet2FASetupDetails.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWallet2FASetupDetails.swift new file mode 100644 index 000000000..16fb10b08 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWallet2FASetupDetails.swift @@ -0,0 +1,30 @@ +// +// MPCWallet2FASetupDetails.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 29.10.2024. +// + +import Foundation + +struct MPCWallet2FASetupDetails { + let secret: String + let email: String + private let fullSecret: String + + init(secret: String, email: String) { + self.fullSecret = secret + // Some Auth apps like Google does not work with full base64 format. + self.secret = secret.replacingOccurrences(of: "=", with: "") + self.email = email + } + + func buildAuthPath() -> String { + let issuer = "Unstoppable" + return "otpauth://totp/\(email)?secret=\(secret)&issuer=\(issuer)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + } + + func buildAuthURL() -> URL? { + URL(string: buildAuthPath()) + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Entities/FB_UD_MPCConnectedWalletDetails.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Entities/FB_UD_MPCConnectedWalletDetails.swift index aea4a160a..398f1f5a0 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Entities/FB_UD_MPCConnectedWalletDetails.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Entities/FB_UD_MPCConnectedWalletDetails.swift @@ -14,25 +14,31 @@ extension FB_UD_MPC { let tokens: AuthTokens let firstAccount: WalletAccountWithAssets let accounts: [WalletAccountWithAssets] + let is2FAEnabled: Bool init(email: String, deviceId: String, tokens: AuthTokens, firstAccount: WalletAccountWithAssets, - accounts: [WalletAccountWithAssets]) { + accounts: [WalletAccountWithAssets], + is2FAEnabled: Bool) { self.email = email self.deviceId = deviceId self.tokens = tokens self.firstAccount = firstAccount self.accounts = accounts + self.is2FAEnabled = is2FAEnabled } - init(accountDetails: ConnectedWalletAccountsDetails, tokens: AuthTokens) { + init(accountDetails: ConnectedWalletAccountsDetails, + tokens: AuthTokens, + is2FAEnabled: Bool) { self.email = accountDetails.email self.deviceId = accountDetails.deviceId self.tokens = tokens self.firstAccount = accountDetails.firstAccount self.accounts = accountDetails.accounts + self.is2FAEnabled = is2FAEnabled } func getETHWalletAddress() -> String? { @@ -43,7 +49,8 @@ extension FB_UD_MPC { ConnectedWalletAccountsDetails(email: email, deviceId: deviceId, firstAccount: firstAccount, - accounts: accounts) + accounts: accounts, + is2FAEnabled: is2FAEnabled) } } @@ -52,6 +59,7 @@ extension FB_UD_MPC { let deviceId: String let firstAccount: WalletAccountWithAssets let accounts: [WalletAccountWithAssets] + var is2FAEnabled: Bool? = nil } struct UDWalletMetadata: Codable { diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FB_UD_MPCConnectionService.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FB_UD_MPCConnectionService.swift index d0f317229..394e8c650 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FB_UD_MPCConnectionService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/FB_UD_MPCConnectionService.swift @@ -28,7 +28,7 @@ extension FB_UD_MPC { let provider: MPCWalletProvider = .fireblocksUD private let connectorBuilder: FireblocksConnectorBuilder - private let networkService: MPCConnectionNetworkService + private var networkService: MPCConnectionNetworkService private let walletsDataStorage: MPCWalletsDataStorage private let udWalletsService: UDWalletsServiceProtocol private let uiHandler: MPCWalletsUIHandler @@ -45,6 +45,7 @@ extension FB_UD_MPC { self.udWalletsService = udWalletsService self.uiHandler = uiHandler udWalletsService.addListener(self) + self.networkService.otpProvider = self } } } @@ -139,11 +140,13 @@ extension FB_UD_MPC.MPCConnectionService: MPCWalletProviderSubServiceProtocol { let walletDetails: WalletDetails = try await getWalletAccountDetailsForWalletWith(deviceId: deviceId, accessToken: authTokens.accessToken.jwt) logMPC("Did get wallet account details") + let is2FAEnabled = (try? await networkService.get2FAStatus(accessToken: authTokens.accessToken.jwt)) ?? false let mpcWallet: FB_UD_MPC.ConnectedWalletDetails = FB_UD_MPC.ConnectedWalletDetails(email: email, deviceId: deviceId, tokens: authTokens, firstAccount: walletDetails.firstAccount, - accounts: walletDetails.accounts) + accounts: walletDetails.accounts, + is2FAEnabled: is2FAEnabled) continuation.yield(.storeWallet) logMPC("Will create UD Wallet") @@ -406,6 +409,47 @@ extension FB_UD_MPC.MPCConnectionService: MPCWalletProviderSubServiceProtocol { return try await requestRecoveryFor(connectedWallet: connectedWalletDetails, password: password) } + + func is2FAEnabled(for walletMetadata: MPCWalletMetadata) throws -> Bool { + let connectedWalletDetails = try getConnectedWalletDetailsFor(walletMetadata: walletMetadata) + return connectedWalletDetails.is2FAEnabled + } + + func request2FASetupDetails(for walletMetadata: MPCWalletMetadata) async throws -> MPCWallet2FASetupDetails { + let connectedWalletDetails = try getConnectedWalletDetailsFor(walletMetadata: walletMetadata) + return try await performAuthErrorCatchingBlock(connectedWalletDetails: connectedWalletDetails) { token in + let secret = try await networkService.enable2FA(accessToken: token) + let email = connectedWalletDetails.email + let setupDetails = MPCWallet2FASetupDetails(secret: secret, + email: email) + return setupDetails + } + } + + func confirm2FAEnabled(for walletMetadata: MPCWalletMetadata, code: String) async throws { + let connectedWalletDetails = try getConnectedWalletDetailsFor(walletMetadata: walletMetadata) + + try await performAuthErrorCatchingBlock(connectedWalletDetails: connectedWalletDetails) { accessToken in + try await networkService.verify2FAToken(accessToken: accessToken, + token: code) + try updateAccountDetailsFor(deviceId: connectedWalletDetails.deviceId) { $0.is2FAEnabled = true } + } + } + + func disable2FA(for walletMetadata: MPCWalletMetadata, code: String) async throws { + let connectedWalletDetails = try getConnectedWalletDetailsFor(walletMetadata: walletMetadata) + try await performAuthErrorCatchingBlock(connectedWalletDetails: connectedWalletDetails) { accessToken in + try await networkService.disable2FA(accessToken: accessToken, token: code) + try updateAccountDetailsFor(deviceId: connectedWalletDetails.deviceId) { $0.is2FAEnabled = false } + } + } + + private func updateAccountDetailsFor(deviceId: String, + block: (inout FB_UD_MPC.ConnectedWalletAccountsDetails)->()) throws { + var accountDetails = try walletsDataStorage.retrieveAccountsDetailsFor(deviceId: deviceId) + block(&accountDetails) + try walletsDataStorage.storeAccountsDetails(accountDetails) + } @discardableResult private func requestRecoveryFor(connectedWallet: FB_UD_MPC.ConnectedWalletDetails, @@ -459,11 +503,13 @@ extension FB_UD_MPC.MPCConnectionService: MPCWalletProviderSubServiceProtocol { accessToken: token) let authTokens = try walletsDataStorage.retrieveAuthTokensFor(deviceId: deviceId) + let is2FAEnabled = (try? await networkService.get2FAStatus(accessToken: authTokens.accessToken.jwt)) ?? false let mpcWallet = FB_UD_MPC.ConnectedWalletDetails(email: email, deviceId: deviceId, tokens: authTokens, firstAccount: walletAccountDetails.firstAccount, - accounts: walletAccountDetails.accounts) + accounts: walletAccountDetails.accounts, + is2FAEnabled: is2FAEnabled) try walletsDataStorage.storeAccountsDetails(mpcWallet.createWalletAccountsDetails()) return mpcWallet @@ -525,7 +571,9 @@ extension FB_UD_MPC.MPCConnectionService: MPCWalletProviderSubServiceProtocol { private func getConnectedWalletDetailsFor(deviceId: String) throws -> FB_UD_MPC.ConnectedWalletDetails { let tokens = try walletsDataStorage.retrieveAuthTokensFor(deviceId: deviceId) let accountDetails = try walletsDataStorage.retrieveAccountsDetailsFor(deviceId: deviceId) - return FB_UD_MPC.ConnectedWalletDetails(accountDetails: accountDetails, tokens: tokens) + return FB_UD_MPC.ConnectedWalletDetails(accountDetails: accountDetails, + tokens: tokens, + is2FAEnabled: accountDetails.is2FAEnabled ?? false) } private func getConnectedWalletDetailsFor(walletMetadata: MPCWalletMetadata) throws -> FB_UD_MPC.ConnectedWalletDetails { @@ -558,6 +606,7 @@ extension FB_UD_MPC.MPCConnectionService: MPCWalletProviderSubServiceProtocol { case invalidNetworkFeeAmountFormat case failedToTrimAmount case failedToFindUDWallet + case otpRequestRejected public var errorDescription: String? { return rawValue @@ -634,7 +683,7 @@ extension FB_UD_MPC.MPCConnectionService: FB_UD_MPC.WalletAuthTokenProvider { } private func refreshAndStoreBootstrapToken(bootstrapToken: JWToken, - currentDeviceId: String) async throws -> String { + currentDeviceId: String) async throws -> String { do { let refreshBootstrapTokenResponse = try await networkService.refreshBootstrapToken(bootstrapToken.jwt) @@ -726,6 +775,16 @@ extension FB_UD_MPC.MPCConnectionService: UDWalletsServiceListener { } } +// MARK: - MPCOTPProvider +extension FB_UD_MPC.MPCConnectionService: FB_UD_MPC.MPCOTPProvider { + func getMPCOTP() async throws -> String { + guard let code = await uiHandler.askForMPC2FACode() else { + throw MPCConnectionServiceError.otpRequestRejected + } + return code + } +} + // MARK: - Queuing extension FB_UD_MPC.MPCConnectionService { actor ActionsQueuer { diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_DefaultMPCConnectionNetworkService.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_DefaultMPCConnectionNetworkService.swift index b92fe564b..07ca5cc27 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_DefaultMPCConnectionNetworkService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_DefaultMPCConnectionNetworkService.swift @@ -11,6 +11,7 @@ extension FB_UD_MPC { struct DefaultMPCConnectionNetworkService: MPCConnectionNetworkService, NetworkBearerAuthorisationHeaderBuilder { private let networkService = NetworkService() + var otpProvider: MPCOTPProvider? = nil func sendBootstrapCodeTo(email: String) async throws { @@ -77,6 +78,7 @@ extension FB_UD_MPC { let request = try APIRequest(urlString: MPCNetwork.URLSList.tokensSetupURL, method: .post, headers: headers) + let response: SetupTokenResponse = try await makeDecodableAPIRequest(request) return response @@ -364,6 +366,61 @@ extension FB_UD_MPC { statuses: [.completed]) } + func get2FAStatus(accessToken: String) async throws -> Bool { + struct Response: Codable { + let otpEnabled: Bool + } + + let headers = buildAuthBearerHeader(token: accessToken) + let request = try APIRequest(urlString: MPCNetwork.URLSList.otpURL, + method: .get, + headers: headers) + let response: Response = try await makeDecodableAPIRequest(request) + return response.otpEnabled + } + + func enable2FA(accessToken: String) async throws -> String { + struct Response: Codable { + let secret: String + } + + let headers = buildAuthBearerHeader(token: accessToken) + let request = try APIRequest(urlString: MPCNetwork.URLSList.otpURL, + method: .post, + headers: headers) + let response: Response = try await makeDecodableAPIRequest(request) + return response.secret + } + + func verify2FAToken(accessToken: String, token: String) async throws { + struct RequestBody: Codable { + let token: String + } + + let body = RequestBody(token: token) + let headers = buildAuthBearerHeader(token: accessToken) + let request = try APIRequest(urlString: MPCNetwork.URLSList.otpVerificationURL, + body: body, + method: .post, + headers: headers) + try await makeAPIRequest(request) + } + + func disable2FA(accessToken: String, + token: String) async throws { + struct RequestBody: Codable { + let token: String + } + + let body = RequestBody(token: token) + let headers = buildAuthBearerHeader(token: accessToken) + let request = try APIRequest(urlString: MPCNetwork.URLSList.otpURL, + body: body, + method: .delete, + headers: headers) + try await makeAPIRequest(request) + } + @discardableResult private func waitForOperationStatuses(accessToken: String, operationId: String, @@ -464,6 +521,11 @@ extension FB_UD_MPC { return response } catch { + if let otpFilledRequest = try await getOTPFilledRequestIfNeeded(error: error, + apiRequest: apiRequest) { + return try await makeDecodableAPIRequest(otpFilledRequest) + } + logMPC("Did fail to make request \(apiRequest) with error: \(error.localizedDescription)") throw error } @@ -479,15 +541,51 @@ extension FB_UD_MPC { return response } catch { + if let otpFilledRequest = try await getOTPFilledRequestIfNeeded(error: error, + apiRequest: apiRequest) { + return try await makeAPIRequest(otpFilledRequest) + } + logMPC("Did fail to make request \(apiRequest) with error: \(error.localizedDescription)") throw error } } + private func getOTPFilledRequestIfNeeded(error: Error, apiRequest: APIRequest) async throws -> APIRequest? { + if isNetworkError(error, codeIs: .tokenRequired) { + guard let otpProvider else { + Debugger.printFailure("OTP Provider is not set", critical: true) + return nil + } + logMPC("Will ask for OTP") + let otp = try await otpProvider.getMPCOTP() + var request = apiRequest + request.updateHeaders { $0["X-Otp-Token"] = otp } + + logMPC("Will retry request with OTP") + return request + } + return nil + } + private func isNetworkError(_ error: Error, withCode code: Int) -> Bool { error.isNetworkError(withCode: code) } + private func isNetworkError(_ error: Error, codeIs code: MPCNetworkErrorCode) -> Bool { + if let networkError = error as? NetworkLayerError, + case .badResponseOrStatusCode(_, _, let data) = networkError, + let response = APIBadResponse.objectFromData(data), + response.code == code.rawValue { + return true + } + return false + } + + private enum MPCNetworkErrorCode: String { + case tokenRequired = "OTP_TOKEN_REQUIRED" + } + private enum MPCNetworkServiceError: String, LocalizedError { case waitForKeyMaterialsTransactionTimeout case waitForKeyOperationStatusTimeout diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCConnectionNetworkService.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCConnectionNetworkService.swift index ac786ed47..042c8bb11 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCConnectionNetworkService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCConnectionNetworkService.swift @@ -9,6 +9,8 @@ import Foundation extension FB_UD_MPC { protocol MPCConnectionNetworkService { + var otpProvider: MPCOTPProvider? { get set } + func sendBootstrapCodeTo(email: String) async throws func submitBootstrapCode(_ code: String) async throws -> BootstrapCodeSubmitResponse func authNewDeviceWith(requestId: String, @@ -66,5 +68,11 @@ extension FB_UD_MPC { recoveryToken: String, newRecoveryPhrase: String, requestId: String) async throws + func get2FAStatus(accessToken: String) async throws -> Bool + func enable2FA(accessToken: String) async throws -> String + func verify2FAToken(accessToken: String, + token: String) async throws + func disable2FA(accessToken: String, + token: String) async throws } } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCNetwork.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCNetwork.swift index 04ab9284e..7225b9f41 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCNetwork.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCNetwork.swift @@ -63,6 +63,11 @@ extension FB_UD_MPC { } static var recoveryURL: String { v1URL.appendingURLPathComponents("recovery", "email") } + + // 2FA + static var otpURL: String { v1URL.appendingURLPathComponents("otp") } + static var otpVerificationURL: String { otpURL.appendingURLPathComponents("verification") } + } } } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCOTPProvider.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCOTPProvider.swift new file mode 100644 index 000000000..dc03e867c --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/Fireblocks+UD/Network/FB_UD_MPCOTPProvider.swift @@ -0,0 +1,14 @@ +// +// FB_UD_MPCOTPProvider.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 29.10.2024. +// + +import Foundation + +extension FB_UD_MPC { + protocol MPCOTPProvider { + func getMPCOTP() async throws -> String + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/MPCWalletProviderSubServiceProtocol.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/MPCWalletProviderSubServiceProtocol.swift index 15f35eadb..b1603c10a 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/MPCWalletProviderSubServiceProtocol.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletSubServices/MPCWalletProviderSubServiceProtocol.swift @@ -44,4 +44,10 @@ protocol MPCWalletProviderSubServiceProtocol { /// - Returns: Email from attached to wallet account func requestRecovery(for walletMetadata: MPCWalletMetadata, password: String) async throws -> String + + // 2FA + func is2FAEnabled(for walletMetadata: MPCWalletMetadata) throws -> Bool + func request2FASetupDetails(for walletMetadata: MPCWalletMetadata) async throws -> MPCWallet2FASetupDetails + func confirm2FAEnabled(for walletMetadata: MPCWalletMetadata, code: String) async throws + func disable2FA(for walletMetadata: MPCWalletMetadata, code: String) async throws } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletsService.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletsService.swift index a622ebfb0..b40563b0f 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletsService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletsService.swift @@ -146,6 +146,28 @@ extension MPCWalletsService: MPCWalletsServiceProtocol { return try await subService.requestRecovery(for: walletMetadata, password: password) } + + func is2FAEnabled(for walletMetadata: MPCWalletMetadata) throws -> Bool { + let subService = try getSubServiceFor(provider: walletMetadata.provider) + + return try subService.is2FAEnabled(for: walletMetadata) + } + + func request2FASetupDetails(for walletMetadata: MPCWalletMetadata) async throws -> MPCWallet2FASetupDetails { + let subService = try getSubServiceFor(provider: walletMetadata.provider) + + return try await subService.request2FASetupDetails(for: walletMetadata) + } + + func confirm2FAEnabled(for walletMetadata: MPCWalletMetadata, code: String) async throws { + let subService = try getSubServiceFor(provider: walletMetadata.provider) + try await subService.confirm2FAEnabled(for: walletMetadata, code: code) + } + + func disable2FA(for walletMetadata: MPCWalletMetadata, code: String) async throws { + let subService = try getSubServiceFor(provider: walletMetadata.provider) + try await subService.disable2FA(for: walletMetadata, code: code) + } } // MARK: - Private methods diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletsServiceProtocol.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletsServiceProtocol.swift index 9e3591050..2d5e5a86e 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletsServiceProtocol.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/MPCWalletsService/MPCWalletsServiceProtocol.swift @@ -41,11 +41,18 @@ protocol MPCWalletsServiceProtocol { /// - Returns: Email from attached to wallet account func requestRecovery(password: String, by walletMetadata: MPCWalletMetadata) async throws -> String + + // 2FA + func is2FAEnabled(for walletMetadata: MPCWalletMetadata) throws -> Bool + func request2FASetupDetails(for walletMetadata: MPCWalletMetadata) async throws -> MPCWallet2FASetupDetails + func confirm2FAEnabled(for walletMetadata: MPCWalletMetadata, code: String) async throws + func disable2FA(for walletMetadata: MPCWalletMetadata, code: String) async throws } @MainActor protocol MPCWalletsUIHandler { func askToReconnectMPCWallet(_ reconnectData: MPCWalletReconnectData) async + func askForMPC2FACode() async -> String? } struct MPCWalletReconnectData { diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/WalletDetails/2FA/MPCSetup2FAConfirmCodeView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/WalletDetails/2FA/MPCSetup2FAConfirmCodeView.swift new file mode 100644 index 000000000..f481d8d13 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/WalletDetails/2FA/MPCSetup2FAConfirmCodeView.swift @@ -0,0 +1,162 @@ +// +// MPCSetup2FAEnableConfirmView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 28.10.2024. +// + +import SwiftUI + +struct MPCSetup2FAConfirmCodeView: View, ViewAnalyticsLogger { + + @Environment(\.mpcWalletsService) private var mpcWalletsService + @Environment(\.dismiss) private var dismiss + @EnvironmentObject var tabRouter: HomeTabRouter + + let verificationPurpose: VerificationPurpose + var navigationStyle: NavigationStyle = .push + var analyticsName: Analytics.ViewName { .setup2FAEnableConfirm } + + @State private var code: String = "" + @State private var isLoading: Bool = false + @State private var error: Error? = nil + + var body: some View { + switch navigationStyle { + case .push: + contentView() + case .modal: + NavigationStack { + contentView() + .interactiveDismissDisabled() + .toolbar { + ToolbarItem(placement: .topBarLeading) { + CloseButtonView(closeCallback: closeButtonPressed) + } + } + } + } + } +} + +private extension MPCSetup2FAConfirmCodeView { + @ViewBuilder + func contentView() -> some View { + ScrollView { + VStack(spacing: 32) { + headerView() + codeInputView() + confirmButton() + } + .padding(.horizontal, 16) + } + .background(Color.backgroundDefault) + .displayError($error) + } + + @ViewBuilder + func headerView() -> some View { + VStack(spacing: 16) { + Text(String.Constants.enable2FAConfirmTitle.localized()) + .titleText() + Text(String.Constants.enable2FAConfirmSubtitle.localized()) + .subtitleText() + } + .multilineTextAlignment(.center) + } + + @ViewBuilder + func codeInputView() -> some View { + UDTextFieldView(text: $code, + placeholder: "", + hint: String.Constants.verificationCode.localized(), + rightViewType: .paste, + rightViewMode: .always, + focusBehaviour: .activateOnAppear, + keyboardType: .alphabet, + autocapitalization: .characters, + textContentType: .oneTimeCode, + autocorrectionDisabled: true) + } + + @ViewBuilder + func confirmButton() -> some View { + UDButtonView(text: String.Constants.verify.localized(), + style: .large(.raisedPrimary), + isLoading: isLoading, + callback: { + logButtonPressedAnalyticEvents(button: .verify) + verifyCode() + }) + } + + func verifyCode() { + isLoading = true + Task { + do { + let code = self.code + switch verificationPurpose { + case .enable(let mpcMetadata): + try await mpcWalletsService.confirm2FAEnabled(for: mpcMetadata, + code: code) + case .disable(let mpcMetadata): + try await mpcWalletsService.disable2FA(for: mpcMetadata, + code: code) + case .enterCode: + Void() + } + didVerifyCode(code) + } catch { + self.error = error + } + isLoading = false + } + } + + func didVerifyCode(_ code: String) { + switch verificationPurpose { + case .enable: + appContext.toastMessageService.showToast(.enabled2FA, isSticky: false) + tabRouter.walletViewNavPath.removeLast(2) + case .disable: + dismiss() + case .enterCode(let callback): + callback(code) + dismiss() + } + } + + func closeButtonPressed() { + if case .enterCode(let callback) = verificationPurpose { + callback(nil) + } + dismiss() + } +} + +extension MPCSetup2FAConfirmCodeView { + enum VerificationPurpose { + case enable(MPCWalletMetadata) + case disable(MPCWalletMetadata) + case enterCode(callback: (String?) -> Void) + } + + enum NavigationStyle { + case push + case modal + } +} + +#Preview { + let wallet = MockEntitiesFabric.Wallet.mockMPC() + let mpcMetadata = wallet.udWallet.mpcMetadata! + + return NavigationStack { + MPCSetup2FAConfirmCodeView(verificationPurpose: .enable(mpcMetadata)) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Image(systemName: "arrow.left") + } + } + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/WalletDetails/2FA/MPCSetup2FAEnableView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/WalletDetails/2FA/MPCSetup2FAEnableView.swift new file mode 100644 index 000000000..56a2dee44 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/WalletDetails/2FA/MPCSetup2FAEnableView.swift @@ -0,0 +1,184 @@ +// +// MPCSetup2FAEnableView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 25.10.2024. +// + +import SwiftUI + +struct MPCSetup2FAEnableView: View, ViewAnalyticsLogger { + + @Environment(\.mpcWalletsService) private var mpcWalletsService + @Environment(\.imageLoadingService) private var imageLoadingService + @EnvironmentObject var tabRouter: HomeTabRouter + + var analyticsName: Analytics.ViewName { .setup2FAEnable } + + let mpcMetadata: MPCWalletMetadata + @State private var setupDetails: MPCWallet2FASetupDetails? = nil + @State private var qrCodeImage: UIImage? = nil + @State private var error: Error? = nil + + var body: some View { + ZStack(alignment: .bottom) { + ScrollView { + VStack(spacing: 32) { + headerView() + secretDetailsView() + } + .padding(.bottom, 60) + } + continueButtonView() + } + .padding(.horizontal, 16) + .background(Color.backgroundDefault) + .onAppear(perform: onAppear) + .animation(.default, value: UUID()) + .displayError($error) + } +} + +private extension MPCSetup2FAEnableView { + func onAppear() { + loadSecret() + } + + func loadSecret() { + Task { + do { + let setupDetails = try await mpcWalletsService.request2FASetupDetails(for: mpcMetadata) + self.setupDetails = setupDetails + + loadQRCodeFor(setupDetails: setupDetails) + } catch { + self.error = error + } + } + } + + func loadQRCodeFor(setupDetails: MPCWallet2FASetupDetails) { + Task { + if let url = setupDetails.buildAuthURL() { + qrCodeImage = await imageLoadingService.loadImage(from: .qrCode(url: url, + options: []), + downsampleDescription: nil) + } + } + } +} + +private extension MPCSetup2FAEnableView { + @ViewBuilder + func headerView() -> some View { + VStack(spacing: 16) { + Text(String.Constants.enable2FATitle.localized()) + .titleText() + Text(String.Constants.enable2FASubtitle.localized()) + .subtitleText() + } + .multilineTextAlignment(.center) + .padding() + } + + @ViewBuilder + func secretDetailsView() -> some View { + if let setupDetails { + VStack(spacing: 24) { + copySecretView(setupDetails.secret) + orScanSeparatorView() + qrCodeView() + } + } else { + loadingSecretView() + } + } + + @ViewBuilder + func loadingSecretView() -> some View { + ProgressView() + } + + @ViewBuilder + func copySecretView(_ secret: String) -> some View { + HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 0) { + Text(String.Constants.enable2FACopySecretTitle.localized()) + .textAttributes(color: .foregroundSecondary, fontSize: 12) + Text(secret) + .textAttributes(color: .foregroundDefault, fontSize: 16) + } + .lineLimit(1) + + UDButtonView(text: String.Constants.copy.localized(), + style: .small(.raisedPrimary), callback: { + logButtonPressedAnalyticEvents(button: .copy) + UIPasteboard.general.string = secret + }) + } + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background(Color.backgroundSubtle) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .overlay(RoundedRectangle(cornerRadius: 16) + .stroke(Color.borderDefault, lineWidth: 1)) + } + + @ViewBuilder + func orScanSeparatorView() -> some View { + HStack(spacing: 8) { + HomeExploreSeparatorView() + .layoutPriority(1) + Text(String.Constants.enable2FAOrScanQRCode.localized()) + .textAttributes(color: .foregroundSecondary, + fontSize: 14, + fontWeight: .medium) + .lineLimit(1) + .layoutPriority(2) + HomeExploreSeparatorView() + .layoutPriority(1) + } + } + + @ViewBuilder + func qrCodeView() -> some View { + if let qrCodeImage = qrCodeImage { + Image(uiImage: qrCodeImage) + .resizable() + .scaledToFit() + .squareFrame(200) + .clipShape(RoundedRectangle(cornerRadius: 12)) + } else { + ProgressView() + } + } + + @ViewBuilder + func continueButtonView() -> some View { + if setupDetails != nil { + UDButtonView(text: String.Constants.continue.localized(), + style: .large(.raisedPrimary), callback: { + logButtonPressedAnalyticEvents(button: .continue) + moveToNextScreen() + }) + } + } + + func moveToNextScreen() { + tabRouter.walletViewNavPath.append(.mpcSetup2FAEnableConfirm(mpcMetadata: mpcMetadata)) + } +} + +#Preview { + let wallet = MockEntitiesFabric.Wallet.mockMPC() + let mpcMetadata = wallet.udWallet.mpcMetadata! + + return NavigationStack { + MPCSetup2FAEnableView(mpcMetadata: mpcMetadata) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Image(systemName: "arrow.left") + } + } + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/WalletDetails/WalletDetails.swift b/unstoppable-ios-app/domains-manager-ios/Modules/WalletDetails/WalletDetails.swift index 6b3cd30e9..0ba038a02 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/WalletDetails/WalletDetails.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/WalletDetails/WalletDetails.swift @@ -20,12 +20,15 @@ extension WalletDetails { return "backUp" case .more: return "more" + case .mpc2FA: + return "mpc2FA" } } case rename case backUp(WalletDisplayInfo.BackupState) case more([WalletSubAction]) + case mpc2FA(Bool) var title: String { switch self { @@ -38,6 +41,8 @@ extension WalletDetails { return String.Constants.backUp.localized() case .more: return String.Constants.more.localized() + case .mpc2FA(let isEnabled): + return isEnabled ? String.Constants.mpc2FAEnabled.localized() : String.Constants.mpc2FAEnable.localized() } } @@ -52,6 +57,8 @@ extension WalletDetails { return .cloudIcon case .more: return .dotsIcon + case .mpc2FA(let isEnabled): + return isEnabled ? .shieldCheckmark : .shieldEmpty } } @@ -62,6 +69,8 @@ extension WalletDetails { return .foregroundSuccess } return .foregroundAccent + case .mpc2FA(let isEnabled): + return isEnabled ? .foregroundSuccess : .foregroundAccent default: return .foregroundAccent } @@ -69,7 +78,7 @@ extension WalletDetails { var subActions: [WalletSubAction] { switch self { - case .backUp, .rename: + case .backUp, .rename, .mpc2FA: return [] case .more(let subActions): return subActions @@ -84,12 +93,14 @@ extension WalletDetails { return .walletBackup case .more: return .more + case .mpc2FA: + return .mpc2FA } } var isDimmed: Bool { switch self { - case .rename, .backUp, .more: + case .rename, .backUp, .more, .mpc2FA: return false } } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/WalletDetails/WalletDetailsView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/WalletDetails/WalletDetailsView.swift index a761d13de..a54681d38 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/WalletDetails/WalletDetailsView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/WalletDetails/WalletDetailsView.swift @@ -10,6 +10,8 @@ import SwiftUI struct WalletDetailsView: View, ViewAnalyticsLogger { @Environment(\.walletsDataService) private var walletsDataService + @Environment(\.mpcWalletsService) private var mpcWalletsService + @Environment(\.udFeatureFlagsService) private var udFeatureFlagsService @Environment(\.dismiss) var dismiss @EnvironmentObject var tabRouter: HomeTabRouter @@ -17,6 +19,8 @@ struct WalletDetailsView: View, ViewAnalyticsLogger { let source: UDRouter.WalletDetailsSource @State private var isRenaming = false + @State private var pullUp: ViewPullUpConfigurationType? + @State private var disabling2FAMetadata: MPCWalletMetadataWrapper? var analyticsName: Analytics.ViewName { .walletDetails } var additionalAppearAnalyticParameters: Analytics.EventParameters { [.wallet : wallet.address] } @@ -47,6 +51,7 @@ struct WalletDetailsView: View, ViewAnalyticsLogger { .listRowSeparator(.hidden) }.environment(\.defaultMinListRowHeight, 28) .listRowSpacing(0) + .viewPullUp($pullUp) .clearListBackground() .background(Color.backgroundDefault) .passViewAnalyticsDetails(logger: self) @@ -61,6 +66,10 @@ struct WalletDetailsView: View, ViewAnalyticsLogger { .sheet(isPresented: $isRenaming, content: { RenameWalletView(wallet: wallet) }) + .sheet(item: $disabling2FAMetadata, content: { mpcMetadataWrapper in + MPCSetup2FAConfirmCodeView(verificationPurpose: .disable(mpcMetadataWrapper.metadata), + navigationStyle: .modal) + }) } } @@ -188,7 +197,14 @@ private extension WalletDetailsView { } } } - if wallet.udWallet.type == .mpc { + + + if wallet.udWallet.type == .mpc, + let mpcMetadata = wallet.udWallet.mpcMetadata { + if udFeatureFlagsService.valueFor(flag: .isMPCMFAEnabled) { + let is2FAEnabled = (try? mpcWalletsService.is2FAEnabled(for: mpcMetadata)) ?? false + actions.append(.mpc2FA(is2FAEnabled)) + } subActions.append(.mpcRecoveryKit) } @@ -217,6 +233,8 @@ private extension WalletDetailsView { case .importedNotBackedUp, .locallyGeneratedNotBackedUp: showBackupWalletScreenIfAvailable() } + case .mpc2FA(let isEnabled): + mpc2FAActionPressed(isEnabled) case .more: return } @@ -251,6 +269,27 @@ private extension WalletDetailsView { } } + func mpc2FAActionPressed(_ isEnabled: Bool) { + if isEnabled { + // TODO: - Use tabRouter.pullUp + pullUp = .default(.mpc2FAEnabled(disableCallback: askToDisable2FA)) + } else { + guard let mpcMetadata = wallet.udWallet.mpcMetadata else { return } + + tabRouter.walletViewNavPath.append(.mpcSetup2FAEnable(mpcMetadata: mpcMetadata)) + } + } + + func askToDisable2FA() { + pullUp = .default(.mpc2FADisableConfirmation(disableCallback: disable2FA)) + } + + func disable2FA() { + guard let mpcMetadata = wallet.udWallet.mpcMetadata else { return } + + disabling2FAMetadata = .init(metadata: mpcMetadata) + } + func walletSubActionPressed(_ action: WalletDetails.WalletSubAction) { switch action { case .privateKey: @@ -394,9 +433,17 @@ private extension WalletDetailsView { } } +// MARK: - Private methods +private extension WalletDetailsView { + struct MPCWalletMetadataWrapper: Identifiable { + let id = UUID() + let metadata: MPCWalletMetadata + } +} + #Preview { - let wallets = MockEntitiesFabric.Wallet.mockEntities() + let wallet = MockEntitiesFabric.Wallet.mockMPC() - return WalletDetailsView(wallet: wallets[0], + return WalletDetailsView(wallet: wallet, source: .settings) } diff --git a/unstoppable-ios-app/domains-manager-ios/NetworkEnvironment/ApiRequestBuilder.swift b/unstoppable-ios-app/domains-manager-ios/NetworkEnvironment/ApiRequestBuilder.swift index 66de01b71..1e5d5cf5f 100644 --- a/unstoppable-ios-app/domains-manager-ios/NetworkEnvironment/ApiRequestBuilder.swift +++ b/unstoppable-ios-app/domains-manager-ios/NetworkEnvironment/ApiRequestBuilder.swift @@ -27,7 +27,7 @@ enum UDApiType: String { struct APIRequest { let url: URL - let headers: [String: String] + private(set) var headers: [String: String] let body: String let method: NetworkService.HttpRequestMethod @@ -58,6 +58,10 @@ struct APIRequest { self.body = bodyString self.method = method } + + mutating func updateHeaders(block: (inout [String : String])->()) { + block(&headers) + } } enum MetaTxMethod: String { diff --git a/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsServiceEnvironment.swift b/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsServiceEnvironment.swift index 5eb456844..9c8b32f2b 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsServiceEnvironment.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsServiceEnvironment.swift @@ -215,6 +215,7 @@ extension Analytics { case domainDetails case settings, securitySettings case walletDetails, renameWallet + case setup2FAEnable, setup2FAEnableConfirm case walletsList, mintingWalletsListSelection case setupReverseResolution, walletSetupReverseResolution, setupChangeReverseResolution case selectFirstDomainForReverseResolution, changeDomainForReverseResolution @@ -301,6 +302,8 @@ extension Analytics { case dontAlreadyHaveDomain case createVault case openDomainProfile + case copy + case disable2FA // Backup type case iCloud, manually @@ -342,7 +345,7 @@ extension Analytics { case securitySettingsPasscode, securitySettingsBiometric, securitySettingsRequireSAWhenOpen // Wallet details - case walletBackup, walletRecoveryPhrase, walletRename, walletDomainsList, walletRemove, showConnectedWalletInfo, walletReverseResolution, walletReconnect + case walletBackup, walletRecoveryPhrase, walletRename, walletDomainsList, walletRemove, showConnectedWalletInfo, walletReverseResolution, walletReconnect, mpc2FA // Wallets list case manageICloudBackups, walletInList, walletsMenu, mpcRecoveryKit @@ -527,6 +530,7 @@ extension Analytics { case removeMPCWalletConfirmation case transactionDetails case domainProfileMaintenance, signMessagesMaintenance, transferDomainsFromVaultMaintenance + case mpc2FAEnabled, mpc2FADisableConfirmation // Disabled case walletTransactionsSelection, copyWalletAddressSelection diff --git a/unstoppable-ios-app/domains-manager-ios/Services/CoreAppCoordinator/CoreAppCoordinator.swift b/unstoppable-ios-app/domains-manager-ios/Services/CoreAppCoordinator/CoreAppCoordinator.swift index 180b3ac40..89bbcae0d 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/CoreAppCoordinator/CoreAppCoordinator.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/CoreAppCoordinator/CoreAppCoordinator.swift @@ -316,6 +316,19 @@ extension CoreAppCoordinator: MPCWalletsUIHandler { } await waitForAppAuthorised() } + + func askForMPC2FACode() async -> String? { + guard let topVC else { return nil } + + return await withSafeCheckedMainActorContinuation { completion in + let view = MPCSetup2FAConfirmCodeView(verificationPurpose: .enterCode(callback: { code in + completion(code) + }), navigationStyle: .modal) + + let vc = UIHostingController(rootView: view) + topVC.present(vc, animated: true) + } + } } // MARK: - Passing events diff --git a/unstoppable-ios-app/domains-manager-ios/Services/FeatureFlags/UDFeatureFlag.swift b/unstoppable-ios-app/domains-manager-ios/Services/FeatureFlags/UDFeatureFlag.swift index 2c0fa0367..f82f8b0f0 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/FeatureFlags/UDFeatureFlag.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/FeatureFlags/UDFeatureFlag.swift @@ -19,6 +19,7 @@ enum UDFeatureFlag: String, CaseIterable { case isMPCWCNativeEnabled = "mobile-mpc-wc-native-enabled" case isMPCSignatureEnabled = "mobile-mpc-signature-enabled" case isMPCPurchaseEnabled = "mobile-mpc-purchase-enabled" + case isMPCMFAEnabled = "mobile-mpc-mfa-enabled" case isMaintenanceFullEnabled = "mobile-maintenance-full" case isMaintenanceOKLinkEnabled = "mobile-maintenance-oklink" @@ -29,7 +30,7 @@ enum UDFeatureFlag: String, CaseIterable { var defaultValue: Bool { switch self { - case .communityMediaEnabled, .isBuyCryptoEnabled, .isMPCMessagingEnabled, .isMPCWCNativeEnabled, .isMaintenanceFullEnabled, .isMaintenanceOKLinkEnabled, .isMaintenanceProfilesAPIEnabled, .isMaintenanceEcommEnabled, .isMaintenanceInfuraEnabled, .isMaintenanceMPCEnabled: + case .communityMediaEnabled, .isBuyCryptoEnabled, .isMPCMessagingEnabled, .isMPCWCNativeEnabled, .isMaintenanceFullEnabled, .isMaintenanceOKLinkEnabled, .isMaintenanceProfilesAPIEnabled, .isMaintenanceEcommEnabled, .isMaintenanceInfuraEnabled, .isMaintenanceMPCEnabled, .isMPCMFAEnabled: return false case .isSendCryptoEnabled, .isMPCWalletEnabled, .isMPCSendCryptoEnabled, .isMPCSignatureEnabled, .isMPCPurchaseEnabled, .isBuyDomainEnabled: return true @@ -41,7 +42,7 @@ enum UDFeatureFlag: String, CaseIterable { switch self { case .isMaintenanceFullEnabled, .isMaintenanceOKLinkEnabled, .isMaintenanceProfilesAPIEnabled, .isMaintenanceEcommEnabled, .isMaintenanceInfuraEnabled, .isMaintenanceMPCEnabled: return true - case .isSendCryptoEnabled, .isMPCWalletEnabled, .isMPCSendCryptoEnabled, .isMPCSignatureEnabled, .isMPCPurchaseEnabled, .communityMediaEnabled, .isBuyCryptoEnabled, .isMPCMessagingEnabled, .isMPCWCNativeEnabled, .isBuyDomainEnabled: + case .isSendCryptoEnabled, .isMPCWalletEnabled, .isMPCSendCryptoEnabled, .isMPCSignatureEnabled, .isMPCPurchaseEnabled, .communityMediaEnabled, .isBuyCryptoEnabled, .isMPCMessagingEnabled, .isMPCWCNativeEnabled, .isBuyDomainEnabled, .isMPCMFAEnabled: return false } } diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldCheckmark.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldCheckmark.imageset/Contents.json new file mode 100644 index 000000000..0dc35bac9 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldCheckmark.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "shieldCheckmark.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldCheckmark.imageset/shieldCheckmark.svg b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldCheckmark.imageset/shieldCheckmark.svg new file mode 100644 index 000000000..024860179 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldCheckmark.imageset/shieldCheckmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldCheckmarkFilled.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldCheckmarkFilled.imageset/Contents.json new file mode 100644 index 000000000..4513e7d20 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldCheckmarkFilled.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "shieldCheckmarkFilled.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldCheckmarkFilled.imageset/shieldCheckmarkFilled.svg b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldCheckmarkFilled.imageset/shieldCheckmarkFilled.svg new file mode 100644 index 000000000..6af74a367 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldCheckmarkFilled.imageset/shieldCheckmarkFilled.svg @@ -0,0 +1,3 @@ + + + diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldEmpty.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldEmpty.imageset/Contents.json new file mode 100644 index 000000000..a1695a92f --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldEmpty.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "shieldEmpty.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldEmpty.imageset/shieldEmpty.svg b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldEmpty.imageset/shieldEmpty.svg new file mode 100644 index 000000000..926df4850 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/shieldEmpty.imageset/shieldEmpty.svg @@ -0,0 +1,3 @@ + + + diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Localization/en.lproj/Localizable.strings b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Localization/en.lproj/Localizable.strings index fb5619f10..a1fcdffcd 100644 --- a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Localization/en.lproj/Localizable.strings +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Localization/en.lproj/Localizable.strings @@ -400,6 +400,19 @@ "WHAT_IS_EXTERNAL_WALLET" = "What is an external wallet?"; "IMPORT_CONNECTED_WALLET_DESCRIPTION" = "Manage your Web3 domains using an external wallet without importing a recovery phrase or private key. Your linked wallet will be used for message & transaction signing."; "WALLET_WAS_DISCONNECTED_MESSAGE" = "Your wallet: %@ was disconnected from %@"; +"MPC_2FA_ENABLED" = "2FA Enabled"; +"MPC_2FA_ENABLE" = "Enable 2FA"; +"ENABLE_2FA_TITLE" = "Enable two-factor authentication (2FA)"; +"ENABLE_2FA_SUBTITLE" = "You can use any authenticator app, such as Google Authenticator, Microsoft Authenticator, and Authy."; +"ENABLE_2FA_COPY_SECRET_TITLE" = "Enter this code to your authenticator app"; +"ENABLE_2FA_OR_SCAN_QR_CODE" = "Or scan this QR code"; +"ENABLE_2FA_CONFIRM_TITLE" = "Enter the 6-digit code generated by your authenticator app"; +"ENABLE_2FA_CONFIRM_SUBTITLE" = "This is to ensure you have successfully paired your authenticator app."; +"DISABLE_2FA" = "Disable 2FA"; +"MPC_2FA_ENABLED_PULLUP_TITLE" = "Two-factor authentication\n(2FA) is enabled"; +"MPC_2FA_ENABLED_PULLUP_SUBTITLE" = "You will be asked for a verification code whenever you login or request recovery kit"; +"MPC_2FA_DISABLE_CONFIRMATION_PULLUP_TITLE" = "Are you sure you want to disable two-factor authentication (2FA)?"; +"MPC_2FA_DISABLE_CONFIRMATION_PULLUP_SUBTITLE" = "Disabling it will reduce the security of your Unstoppable Lite Wallet."; // Toast messages "TOAST_WALLET_ADDRESS_COPIED" = "%@ address copied"; @@ -1007,6 +1020,7 @@ "ENDINGS" = "Endings"; "SUGGESTIONS" = "Suggestions"; "DOMAINS_PURCHASED_SUMMARY_MESSAGE" = "Order total: %@\n\nMinting started, this can take up to 5 minutes.\nWill be minted to: %@"; +"ENABLED_2FA_TOAST_MESSAGE" = "2FA enabled"; // Home "HOME_WALLET_TOKENS_COME_TITLE" = "No more tokens here yet"; diff --git a/unstoppable-ios-app/domains-manager-ios/SwiftUI/Extensions/Image.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/Extensions/Image.swift index ff9365896..442cd28ec 100644 --- a/unstoppable-ios-app/domains-manager-ios/SwiftUI/Extensions/Image.swift +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/Extensions/Image.swift @@ -125,6 +125,9 @@ extension Image { static let starInCloudIcon = Image("starInCloudIcon") static let widgetIcon = Image("widgetIcon") static let resetULWPasswordIllustration = Image("resetULWPasswordIllustration") + static let shieldEmpty = Image("shieldEmpty") + static let shieldCheckmark = Image("shieldCheckmark") + static let shieldCheckmarkFilled = Image("shieldCheckmarkFilled") static let cryptoFaceIcon = Image("cryptoFaceIcon") static let cryptoPOAPIcon = Image("cryptoPOAPIcon") diff --git a/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/ViewPullUp/ViewPullUp.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/ViewPullUp/ViewPullUp.swift index f45ff9ebd..723836537 100644 --- a/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/ViewPullUp/ViewPullUp.swift +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/ViewPullUp/ViewPullUp.swift @@ -254,7 +254,8 @@ private extension ViewPullUp { buttonWithContent(content, style: .medium(.raisedTertiary), configuration: configuration) case .primaryGhost(let content): buttonWithContent(content, style: .large(.ghostPrimary), configuration: configuration) - + case .largeRaisedTertiary(let content): + buttonWithContent(content, style: .large(.raisedTertiary), configuration: configuration) } } diff --git a/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/ViewPullUp/ViewPullUpDefaultConfiguration.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/ViewPullUp/ViewPullUpDefaultConfiguration.swift index 58ad84710..518321c12 100644 --- a/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/ViewPullUp/ViewPullUpDefaultConfiguration.swift +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/ViewPullUp/ViewPullUpDefaultConfiguration.swift @@ -140,10 +140,11 @@ struct ViewPullUpDefaultConfiguration { case textTertiary(content: ButtonContent) case applePay(content: ButtonContent) case raisedTertiary(content: ButtonContent) + case largeRaisedTertiary(content: ButtonContent) var height: CGFloat { switch self { - case .main, .secondary, .primaryDanger, .secondaryDanger, .applePay, .raisedTertiary, .primaryGhost: + case .main, .secondary, .primaryDanger, .secondaryDanger, .applePay, .raisedTertiary, .primaryGhost, .largeRaisedTertiary: return 48 case .textTertiary: return 24 @@ -153,14 +154,14 @@ struct ViewPullUpDefaultConfiguration { @MainActor func callAction() { switch self { - case .main(let content), .secondary(let content), .textTertiary(let content), .primaryDanger(let content), .secondaryDanger(let content), .applePay(let content), .raisedTertiary(let content), .primaryGhost(let content): + case .main(let content), .secondary(let content), .textTertiary(let content), .primaryDanger(let content), .secondaryDanger(let content), .applePay(let content), .raisedTertiary(let content), .primaryGhost(let content), .largeRaisedTertiary(let content): content.action?() } } var content: ButtonContent { switch self { - case .main(let content), .secondary(let content), .textTertiary(let content), .primaryDanger(let content), .secondaryDanger(let content), .applePay(let content), .raisedTertiary(let content), .primaryGhost(let content): + case .main(let content), .secondary(let content), .textTertiary(let content), .primaryDanger(let content), .secondaryDanger(let content), .applePay(let content), .raisedTertiary(let content), .primaryGhost(let content), .largeRaisedTertiary(let content): return content } } @@ -534,6 +535,33 @@ extension ViewPullUpDefaultConfiguration { cancelButton: .gotItButton(), analyticName: .transferDomainsFromVaultMaintenance) } + + static func mpc2FAEnabled(disableCallback: @escaping MainActorAsyncCallback) -> ViewPullUpDefaultConfiguration { + .init(icon: .init(icon: .shieldCheckmarkFilled, + size: .small, + tintColor: .foregroundSuccess), + title: .text(String.Constants.mpc2FAEnabledPullUpTitle.localized()), + subtitle: .label(.text(String.Constants.mpc2FAEnabledPullUpSubtitle.localized())), + actionButton: .largeRaisedTertiary(content: .init(title: String.Constants.disable2FA.localized(), + analyticsName: .disable2FA, + action: disableCallback)), + analyticName: .mpc2FAEnabled) + } + + static func mpc2FADisableConfirmation(disableCallback: @escaping MainActorAsyncCallback) -> ViewPullUpDefaultConfiguration { + .init(icon: .init(icon: .warningIcon, + size: .small), + title: .text(String.Constants.mpc2FADisableConfirmationPullUpTitle.localized()), + subtitle: .label(.text(String.Constants.mpc2FADisableConfirmationPullUpSubtitle.localized())), + actionButton: .primaryDanger(content: .init(title: String.Constants.disable2FA.localized(), + analyticsName: .disable2FA, + action: disableCallback)), + cancelButton: .secondary(content: .init(title: String.Constants.cancel.localized(), + analyticsName: .cancel, + action: nil)), + analyticName: .mpc2FADisableConfirmation) + } + } // MARK: - Open methods diff --git a/unstoppable-ios-app/domains-manager-iosTests/DeepLinksServiceTests.swift b/unstoppable-ios-app/domains-manager-iosTests/DeepLinksServiceTests.swift index 2eaa54b9e..fb4ca0e0b 100644 --- a/unstoppable-ios-app/domains-manager-iosTests/DeepLinksServiceTests.swift +++ b/unstoppable-ios-app/domains-manager-iosTests/DeepLinksServiceTests.swift @@ -165,6 +165,10 @@ private final class PrivateMockExternalEventsService: ExternalEventsServiceProto } private final class MockCoreAppCoordinator: CoreAppCoordinatorProtocol { + func askForMPC2FACode() async -> String? { + "" + } + func askToReconnectMPCWallet(_ reconnectData: MPCWalletReconnectData) async { } diff --git a/unstoppable-ios-app/domains-manager-iosTests/FB_UD_MPCConnectionServiceTests.swift b/unstoppable-ios-app/domains-manager-iosTests/FB_UD_MPCConnectionServiceTests.swift index 6afff2d15..ed88154d8 100644 --- a/unstoppable-ios-app/domains-manager-iosTests/FB_UD_MPCConnectionServiceTests.swift +++ b/unstoppable-ios-app/domains-manager-iosTests/FB_UD_MPCConnectionServiceTests.swift @@ -192,9 +192,34 @@ private final class MockFireblocksConnector: FB_UD_MPC.FireblocksConnectorProtoc } private final class MockNetworkService: FB_UD_MPC.MPCConnectionNetworkService, FailableService { + func requestRecovery(_ accessToken: String, password: String) async throws { + + } + + func resetPassword(accessToken: String, recoveryToken: String, newRecoveryPhrase: String, requestId: String) async throws { + + } + + func get2FAStatus(accessToken: String) async throws -> Bool { + true + } + + func enable2FA(accessToken: String) async throws -> String { + "" + } + + func verify2FAToken(accessToken: String, token: String) async throws { + + } + + func disable2FA(accessToken: String, token: String) async throws { + + } + var shouldFail: Bool = false var deviceId: String = "" let queue = DispatchQueue(label: "MockNetworkService") + var otpProvider: FB_UD_MPC.MPCOTPProvider? = nil func sendBootstrapCodeTo(email: String) async throws { try failIfNeeded() @@ -363,4 +388,8 @@ private final class MockMPCWalletUIHandler: MPCWalletsUIHandler { func askToReconnectMPCWallet(_ reconnectData: MPCWalletReconnectData) async { } + + func askForMPC2FACode() async -> String? { + "" + } } diff --git a/unstoppable-ios-app/domains-manager-iosTests/Helpers/TestableMPCWalletsService.swift b/unstoppable-ios-app/domains-manager-iosTests/Helpers/TestableMPCWalletsService.swift index 8fbef3114..25d89a7fb 100644 --- a/unstoppable-ios-app/domains-manager-iosTests/Helpers/TestableMPCWalletsService.swift +++ b/unstoppable-ios-app/domains-manager-iosTests/Helpers/TestableMPCWalletsService.swift @@ -9,6 +9,32 @@ import Foundation @testable import domains_manager_ios final class TestableMPCWalletsService: MPCWalletsServiceProtocol { + func setupMPCWalletWith(code: String, flow: domains_manager_ios.SetupMPCFlow) -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + continuation.finish() + } + } + + func requestRecovery(password: String, by walletMetadata: domains_manager_ios.MPCWalletMetadata) async throws -> String { + throw NSError() + } + + func is2FAEnabled(for walletMetadata: domains_manager_ios.MPCWalletMetadata) throws -> Bool { + throw NSError() + } + + func request2FASetupDetails(for walletMetadata: domains_manager_ios.MPCWalletMetadata) async throws -> domains_manager_ios.MPCWallet2FASetupDetails { + throw NSError() + } + + func confirm2FAEnabled(for walletMetadata: domains_manager_ios.MPCWalletMetadata, code: String) async throws { + + } + + func disable2FA(for walletMetadata: domains_manager_ios.MPCWalletMetadata, code: String) async throws { + + } + func getBalancesFor(walletMetadata: MPCWalletMetadata) async throws -> [WalletTokenPortfolio] { []