diff --git a/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewAppContext.swift b/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewAppContext.swift index 259d6766a..e22916685 100644 --- a/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewAppContext.swift +++ b/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewAppContext.swift @@ -92,7 +92,8 @@ final class AppContext: AppContextProtocol { walletTransactionsService = WalletTransactionsService(networkService: NetworkService(), cache: InMemoryWalletTransactionsCache()) - mpcWalletsService = MPCWalletsService(udWalletsService: udWalletsService, + mpcWalletsService = MPCWalletsService(udWalletsService: udWalletsService, + udFeatureFlagsService: udFeatureFlagsService, uiHandler: coreAppCoordinator) } } diff --git a/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj b/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj index a882b3f9b..0526dc43d 100644 --- a/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj +++ b/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj @@ -1418,6 +1418,8 @@ C6A359332BB699FC00B1209A /* ConfirmSendTokenDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A359312BB699FC00B1209A /* ConfirmSendTokenDataModel.swift */; }; C6A359352BB6BB2100B1209A /* TxHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A359342BB6BB2000B1209A /* TxHash.swift */; }; C6A359362BB6BB2100B1209A /* TxHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A359342BB6BB2000B1209A /* TxHash.swift */; }; + C6A440092C0448530042FFCC /* UDFeatureFlagsServiceEnvironmentKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A440082C0448530042FFCC /* UDFeatureFlagsServiceEnvironmentKey.swift */; }; + C6A4400A2C0448530042FFCC /* UDFeatureFlagsServiceEnvironmentKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A440082C0448530042FFCC /* UDFeatureFlagsServiceEnvironmentKey.swift */; }; C6A474C729D149560073415F /* LoginFlowNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A474C629D149560073415F /* LoginFlowNavigationController.swift */; }; C6A474D029D150A40073415F /* NoParkedDomainsFoundViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A474CD29D150A40073415F /* NoParkedDomainsFoundViewPresenter.swift */; }; C6A474D429D150A40073415F /* NoParkedDomainsFoundViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A474CE29D150A40073415F /* NoParkedDomainsFoundViewController.swift */; }; @@ -3618,6 +3620,7 @@ C6A359292BB5586700B1209A /* NavigationTrackerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationTrackerViewModifier.swift; sourceTree = ""; }; C6A359312BB699FC00B1209A /* ConfirmSendTokenDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmSendTokenDataModel.swift; sourceTree = ""; }; C6A359342BB6BB2000B1209A /* TxHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TxHash.swift; sourceTree = ""; }; + C6A440082C0448530042FFCC /* UDFeatureFlagsServiceEnvironmentKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDFeatureFlagsServiceEnvironmentKey.swift; sourceTree = ""; }; C6A474C629D149560073415F /* LoginFlowNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginFlowNavigationController.swift; sourceTree = ""; }; C6A474CD29D150A40073415F /* NoParkedDomainsFoundViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoParkedDomainsFoundViewPresenter.swift; sourceTree = ""; }; C6A474CE29D150A40073415F /* NoParkedDomainsFoundViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoParkedDomainsFoundViewController.swift; sourceTree = ""; }; @@ -4949,6 +4952,7 @@ C63095C72B0DA5DE00205054 /* PurchaseDomainsPreferencesStorageEnvironmentKey.swift */, C617FD9D2B58DBCA00B93433 /* WalletsDataServiceEnvironmentKey.swift */, C65CEB8A2B674FC700A13B34 /* UDWalletsServiceEnvironmentKey.swift */, + C6A440082C0448530042FFCC /* UDFeatureFlagsServiceEnvironmentKey.swift */, ); path = EnvironmentKeys; sourceTree = ""; @@ -9752,6 +9756,7 @@ C671CD2B2BC6D5F3005DA2FB /* PreviewEcomPurchaseMPCWalletService.swift in Sources */, C6B761DF2BB3D78F00773943 /* SerializedWalletTransaction.swift in Sources */, C60DCEE2282D0C4000F71C13 /* ResizableRoundedWalletImageView.swift in Sources */, + C6A440092C0448530042FFCC /* UDFeatureFlagsServiceEnvironmentKey.swift in Sources */, C61808822B19BC680032E543 /* TransactionError.swift in Sources */, C6534A912BBFBA10008EEBB5 /* HomeExploreFollowersSectionView.swift in Sources */, C6534AA92BBFBA10008EEBB5 /* HomeExploreSeparatorView.swift in Sources */, @@ -9997,6 +10002,7 @@ C6C8F9912B218B0E00A9834D /* TextWhiteButton.swift in Sources */, C6D646782B1ED12100D724AC /* RecordChangeType.swift in Sources */, C6534AB02BBFBA10008EEBB5 /* HomeExploreSuggestedProfilesListView.swift in Sources */, + C6A4400A2C0448530042FFCC /* UDFeatureFlagsServiceEnvironmentKey.swift in Sources */, C688C1802B845FE500BD233A /* ChatListUserRowView.swift in Sources */, C6A231FD2BEB494D0037E093 /* WalletDetailsDomainItemView.swift in Sources */, C6B761F12BB3F9D900773943 /* WalletTransactionsResponse.swift in Sources */, diff --git a/unstoppable-ios-app/domains-manager-ios/AppContext/GeneralAppContext.swift b/unstoppable-ios-app/domains-manager-ios/AppContext/GeneralAppContext.swift index 29ab8eab7..02eaa59e9 100644 --- a/unstoppable-ios-app/domains-manager-ios/AppContext/GeneralAppContext.swift +++ b/unstoppable-ios-app/domains-manager-ios/AppContext/GeneralAppContext.swift @@ -34,6 +34,7 @@ final class GeneralAppContext: AppContextProtocol { let walletTransactionsService: WalletTransactionsServiceProtocol let ecomPurchaseMPCWalletService: EcomPurchaseMPCWalletServiceProtocol let mpcWalletsService: MPCWalletsServiceProtocol + let udFeatureFlagsService: UDFeatureFlagsServiceProtocol private(set) lazy var coinRecordsService: CoinRecordsServiceProtocol = CoinRecordsService() private(set) lazy var imageLoadingService: ImageLoadingServiceProtocol = ImageLoadingService(qrCodeService: qrCodeService, @@ -56,7 +57,6 @@ final class GeneralAppContext: AppContextProtocol { private(set) lazy var userDataService: UserDataServiceProtocol = UserDataService() private(set) lazy var linkPresentationService: LinkPresentationServiceProtocol = LinkPresentationService() private(set) lazy var domainTransferService: DomainTransferServiceProtocol = DomainTransferService() - private(set) lazy var udFeatureFlagsService: UDFeatureFlagsServiceProtocol = UDFeatureFlagsService() private(set) lazy var hotFeatureSuggestionsService: HotFeatureSuggestionsServiceProtocol = HotFeatureSuggestionsService(fetcher: DefaultHotFeaturesSuggestionsFetcher()) init() { @@ -65,6 +65,7 @@ final class GeneralAppContext: AppContextProtocol { udDomainsService = UDDomainsService() udWalletsService = UDWalletsService() walletNFTsService = WalletNFTsService() + udFeatureFlagsService = UDFeatureFlagsService() walletTransactionsService = WalletTransactionsService(networkService: NetworkService(), cache: InMemoryWalletTransactionsCache()) @@ -77,7 +78,8 @@ final class GeneralAppContext: AppContextProtocol { let coreAppCoordinator = CoreAppCoordinator(pullUpViewService: pullUpViewService) self.coreAppCoordinator = coreAppCoordinator walletConnectServiceV2.setUIHandler(coreAppCoordinator) - mpcWalletsService = MPCWalletsService(udWalletsService: udWalletsService, + mpcWalletsService = MPCWalletsService(udWalletsService: udWalletsService, + udFeatureFlagsService: udFeatureFlagsService, uiHandler: coreAppCoordinator) // Wallets data diff --git a/unstoppable-ios-app/domains-manager-ios/AppContext/MockContext.swift b/unstoppable-ios-app/domains-manager-ios/AppContext/MockContext.swift index c10875935..c7660bcbe 100644 --- a/unstoppable-ios-app/domains-manager-ios/AppContext/MockContext.swift +++ b/unstoppable-ios-app/domains-manager-ios/AppContext/MockContext.swift @@ -64,6 +64,7 @@ final class MockContext: AppContextProtocol { private(set) lazy var walletTransactionsService: WalletTransactionsServiceProtocol = WalletTransactionsService(networkService: NetworkService(), cache: InMemoryWalletTransactionsCache()) private(set) lazy var mpcWalletsService: MPCWalletsServiceProtocol = MPCWalletsService(udWalletsService: udWalletsService, + udFeatureFlagsService: udFeatureFlagsService, uiHandler: coreAppCoordinator) private(set) lazy var ecomPurchaseMPCWalletService: EcomPurchaseMPCWalletServiceProtocol = PreviewEcomPurchaseMPCWalletService() diff --git a/unstoppable-ios-app/domains-manager-ios/Entities/EnvironmentKeys/UDFeatureFlagsServiceEnvironmentKey.swift b/unstoppable-ios-app/domains-manager-ios/Entities/EnvironmentKeys/UDFeatureFlagsServiceEnvironmentKey.swift new file mode 100644 index 000000000..49360a662 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Entities/EnvironmentKeys/UDFeatureFlagsServiceEnvironmentKey.swift @@ -0,0 +1,19 @@ +// +// UDFeatureFlagsServiceEnvironmentKey.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 27.05.2024. +// + +import SwiftUI + +private struct UDFeatureFlagsServiceEnvironmentKey: EnvironmentKey { + static let defaultValue = appContext.udFeatureFlagsService +} + +extension EnvironmentValues { + var udFeatureFlagsService: UDFeatureFlagsServiceProtocol { + get { self[UDFeatureFlagsServiceEnvironmentKey.self] } + set { self[UDFeatureFlagsServiceEnvironmentKey.self] = newValue } + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/Entities/EnvironmentKeys/UDWalletsServiceEnvironmentKey.swift b/unstoppable-ios-app/domains-manager-ios/Entities/EnvironmentKeys/UDWalletsServiceEnvironmentKey.swift index 778f0022c..1d7759b73 100644 --- a/unstoppable-ios-app/domains-manager-ios/Entities/EnvironmentKeys/UDWalletsServiceEnvironmentKey.swift +++ b/unstoppable-ios-app/domains-manager-ios/Entities/EnvironmentKeys/UDWalletsServiceEnvironmentKey.swift @@ -19,4 +19,3 @@ extension EnvironmentValues { } } - diff --git a/unstoppable-ios-app/domains-manager-ios/Extensions/Error.swift b/unstoppable-ios-app/domains-manager-ios/Extensions/Error.swift index 67ae48e66..ae65f113f 100644 --- a/unstoppable-ios-app/domains-manager-ios/Extensions/Error.swift +++ b/unstoppable-ios-app/domains-manager-ios/Extensions/Error.swift @@ -31,6 +31,10 @@ extension Error { default: title = String.Constants.somethingWentWrong.localized() message = String.Constants.pleaseTryAgain.localized() } + } else if let mpcError = self as? MPCWalletError, + case .messageSignDisabled = mpcError { + title = String.Constants.mpcWalletSigningUnavailableErrorMessage.localizedMPCProduct() + message = String.Constants.tryAgainLater.localized() } else { title = String.Constants.somethingWentWrong.localized() message = String.Constants.pleaseTryAgain.localized() 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 b5b737f3d..bcfb39694 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 @@ -1229,6 +1229,8 @@ extension String { static let reImportWallet = "RE_IMPORT_WALLET" static let removeMPCWalletPullUpTitle = "REMOVE_MPC_WALLET_PULL_UP_TITLE" static let removeMPCWalletPullUpSubtitle = "REMOVE_MPC_WALLET_PULL_UP_SUBTITLE" + static let mpcWalletMessagingUnavailableMessage = "MPC_WALLET_MESSAGING_UNAVAILABLE_MESSAGE" + static let mpcWalletSigningUnavailableErrorMessage = "MPC_WALLET_SIGNING_UNAVAILABLE_ERROR_MESSAGE" // Send crypto first time static let sendCryptoFirstTimePullUpTitle = "SEND_CRYPTO_FIRST_TIME_PULL_UP_TITLE" diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/DomainProfile/DomainProfileViewPresenter.swift b/unstoppable-ios-app/domains-manager-ios/Modules/DomainProfile/DomainProfileViewPresenter.swift index 29c6206c5..04061d4be 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/DomainProfile/DomainProfileViewPresenter.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/DomainProfile/DomainProfileViewPresenter.swift @@ -495,11 +495,14 @@ private extension DomainProfileViewPresenter { let updatedChanges = updatedRequests.reduce([DomainProfileSectionChangeDescription](), { $0 + $1.changes }) - func checkForApplePayErrorAndShowUpdateFailedPullUpFor(errors: [UpdateDomainProfileError], - requiredPullUp: ( ()async throws->())) async throws { + func checkForSpecialErrorAndShowUpdateFailedPullUpFor(errors: [UpdateDomainProfileError], + requiredPullUp: ( ()async throws->())) async throws { let paymentsError = errors.compactMap({ $0.error as? PaymentError }) - if let _ = paymentsError.first(where: { $0 == .applePayNotSupported }) { + if paymentsError.first(where: { $0 == .applePayNotSupported }) != nil { appContext.pullUpViewService.showApplePayRequiredPullUp(in: view) + } else if let error = errors.compactMap({ $0.error as? MPCWalletError }).first(where: { $0 == .messageSignDisabled }) { + view.showAlertWith(error: error, handler: nil) + throw MPCWalletError.messageSignDisabled } else { try await requiredPullUp() } @@ -530,7 +533,7 @@ private extension DomainProfileViewPresenter { await view.dismissPullUpMenu() let numberOfFailedAttempts = dataHolder.numberOfFailedToUpdateProfileAttempts - try await checkForApplePayErrorAndShowUpdateFailedPullUpFor(errors: updateErrors, requiredPullUp: { + try await checkForSpecialErrorAndShowUpdateFailedPullUpFor(errors: updateErrors, requiredPullUp: { if numberOfFailedAttempts >= 3 { try await appContext.pullUpViewService.showTryUpdateDomainProfileLaterPullUp(in: view) } else { @@ -565,7 +568,7 @@ private extension DomainProfileViewPresenter { let failedUIChanges = failedRequestsWithChanges.reduce([DomainProfileSectionChangeDescription](), { $0 + $1.changes }).map { $0.uiChange } let failedUIChangeItems = failedUIChanges.map({ DomainProfileSectionUIChangeFailedItem(failedChangeType: $0) }) - try await checkForApplePayErrorAndShowUpdateFailedPullUpFor(errors: updateErrors, requiredPullUp: { + try await checkForSpecialErrorAndShowUpdateFailedPullUpFor(errors: updateErrors, requiredPullUp: { try await appContext.pullUpViewService.showUpdateDomainProfileSomeChangesFailedPullUp(in: view, changes: failedUIChangeItems) }) diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/SendCryptoAsset/SelectAssetToSend/SelectCryptoAssetToSendEmptyView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/SendCryptoAsset/SelectAssetToSend/SelectCryptoAssetToSendEmptyView.swift index 3ce935520..98d3194ef 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/SendCryptoAsset/SelectAssetToSend/SelectCryptoAssetToSendEmptyView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/SendCryptoAsset/SelectAssetToSend/SelectCryptoAssetToSendEmptyView.swift @@ -9,6 +9,7 @@ import SwiftUI struct SelectCryptoAssetToSendEmptyView: View { + @Environment(\.udFeatureFlagsService) var udFeatureFlagsService var assetType: SendCryptoAsset.AssetType let actionCallback: () -> Void @@ -72,7 +73,7 @@ private extension SelectCryptoAssetToSendEmptyView { @ViewBuilder func actionButton() -> some View { if case .tokens = assetType, - !Constants.isBuyCryptoEnabled { + !udFeatureFlagsService.valueFor(flag: .isBuyCryptoEnabled) { EmptyView() } else { UDButtonView(text: actionButtonTitle, diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletView.swift index 888cc6b71..39321945d 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletView.swift @@ -91,7 +91,12 @@ struct HomeWalletView: View, ViewAnalyticsLogger { // MARK: - Private methods private extension HomeWalletView { func walletActions() -> [WalletAction] { - [.buy, .send, .receive, .profile(enabled: viewModel.isProfileButtonEnabled)] + var actions: [WalletAction] = [.buy] + if viewModel.isSendCryptoEnabled { + actions.append(.send) + } + actions.append(contentsOf: [.receive, .profile(enabled: viewModel.isProfileButtonEnabled)]) + return actions } func onAppear() { diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletViewModel.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletViewModel.swift index 1f042e3d3..ef73d33b2 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletViewModel.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletViewModel.swift @@ -32,7 +32,21 @@ extension HomeWalletView { private var cancellables: Set = [] private var router: HomeTabRouter private var lastVerifiedRecordsWalletAddress: String? = nil - var isWCSupported: Bool { selectedWallet.udWallet.type != .mpc } + var isWCSupported: Bool { + if selectedWallet.udWallet.type == .mpc { + return appContext.udFeatureFlagsService.valueFor(flag: .isMPCWCNativeEnabled) + } + return true + } + var isSendCryptoEnabled: Bool { + if appContext.udFeatureFlagsService.valueFor(flag: .isSendCryptoEnabled) == false { + return false + } + if selectedWallet.udWallet.type == .mpc { + return appContext.udFeatureFlagsService.valueFor(flag: .isMPCSendCryptoEnabled) + } + return true + } init(selectedWallet: WalletEntity, router: HomeTabRouter) { @@ -84,7 +98,7 @@ extension HomeWalletView { })) } case .buy: - if Constants.isBuyCryptoEnabled { + if appContext.udFeatureFlagsService.valueFor(flag: .isBuyCryptoEnabled) { router.pullUp = .default(.homeWalletBuySelectionPullUp(selectionCallback: { [weak self] buyOption in self?.router.pullUp = nil self?.didSelectBuyOption(buyOption) 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 6d44e5b5e..ef7218d59 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 @@ -10,6 +10,7 @@ import Foundation enum MPCWalletError: String, LocalizedError { case incorrectCode case incorrectPassword + case messageSignDisabled public var errorDescription: String? { return rawValue 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 5443dc355..8338a5ab8 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 @@ -12,11 +12,14 @@ final class MPCWalletsService { private var subServices = [MPCWalletProviderSubServiceProtocol]() private let udWalletsService: UDWalletsServiceProtocol + private let udFeatureFlagsService: UDFeatureFlagsServiceProtocol private let uiHandler: MPCWalletsUIHandler - + init(udWalletsService: UDWalletsServiceProtocol, + udFeatureFlagsService: UDFeatureFlagsServiceProtocol, uiHandler: MPCWalletsUIHandler) { self.udWalletsService = udWalletsService + self.udFeatureFlagsService = udFeatureFlagsService self.uiHandler = uiHandler setup() } @@ -50,6 +53,7 @@ extension MPCWalletsService: MPCWalletsServiceProtocol { func signMessage(_ messageString: String, by walletMetadata: MPCWalletMetadata) async throws -> String { + guard udFeatureFlagsService.valueFor(flag: .isMPCSignatureEnabled) else { throw MPCWalletError.messageSignDisabled } let subService = try getSubServiceFor(provider: walletMetadata.provider) return try await subService.signMessage(messageString, diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ReconnectMPCWalletFlow/ReconnectMPCWalletPromptView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ReconnectMPCWalletFlow/ReconnectMPCWalletPromptView.swift index 3cb372ae1..d850a6524 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ReconnectMPCWalletFlow/ReconnectMPCWalletPromptView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ReconnectMPCWalletFlow/ReconnectMPCWalletPromptView.swift @@ -68,7 +68,7 @@ private extension ReconnectMPCWalletPromptView { @ViewBuilder func subtitleText() -> some View { - Text(String.Constants.reImportMPCWalletPromptSubtitle.localized()) + Text(String.Constants.reImportMPCWalletPromptSubtitle.localizedMPCProduct()) .textAttributes(color: .foregroundSecondary, fontSize: 16) .multilineTextAlignment(.center) } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/Chat/ChatView/ChatViewModel.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/Chat/ChatView/ChatViewModel.swift index 62ebbd7b6..6ef48c647 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/Chat/ChatView/ChatViewModel.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/Chat/ChatView/ChatViewModel.swift @@ -1160,6 +1160,8 @@ extension ChatViewModel: UDFeatureFlagsListener { setIfUserCanSendAttachments() reloadCachedMessages() } + default: + return } } } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/ChatsList/ChatListView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/ChatsList/ChatListView.swift index cab3bca0c..fdb7bb585 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/ChatsList/ChatListView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/ChatsList/ChatListView.swift @@ -184,21 +184,31 @@ private extension ChatListView { chatsListStateContentView() case .loading: loadingStateContentView() + case .mpcUnavailable: + mpcUnavailableStateContentView() } } @ViewBuilder func noWalletStateContentView() -> some View { ChatListEmptyStateView(title: String.Constants.messagingNoWalletsTitle.localized(), - subtitle: String.Constants.messagingNoWalletsSubtitle.localized(), - icon: .walletIcon, - buttonTitle: String.Constants.addWalletTitle.localized(), - buttonIcon: .plusIcon18, - buttonStyle: .medium(.raisedPrimary), - buttonCallback: { + subtitle: String.Constants.messagingNoWalletsSubtitle.localized(), + icon: .walletIcon, + actionButtonConfiguration: .init(buttonTitle: String.Constants.addWalletTitle.localized(), + buttonIcon: .plusIcon18, + buttonStyle: .medium(.raisedPrimary), + buttonCallback: { logButtonPressedAnalyticEvents(button: .addWallet) viewModel.addWalletButtonPressed() - }) + })) + } + + @ViewBuilder + func mpcUnavailableStateContentView() -> some View { + ChatListEmptyStateView(title: String.Constants.mpcWalletMessagingUnavailableMessage.localizedMPCProduct(), + subtitle: "", + icon: .messageCircleFilledIcon, + actionButtonConfiguration: nil) } @ViewBuilder @@ -223,8 +233,8 @@ private extension ChatListView { func chatDataTypePickerView() -> some View { if !viewModel.isSearchActive { switch viewModel.chatState { - case .noWallet, .createProfile, .loading: - if true { } + case .noWallet, .createProfile, .loading, .mpcUnavailable: + EmptyView() case .chatsList: ChatListDataTypeSelectorView() .listRowSeparator(.hidden) @@ -313,17 +323,17 @@ private extension ChatListView { @ViewBuilder func chatsListEmptyView() -> some View { ChatListEmptyStateView(title: String.Constants.messagingChatsListEmptyTitle.localized(), - subtitle: String.Constants.messagingChatsListEmptySubtitle.localized(), - icon: .messageCircleIcon24, - buttonTitle: String.Constants.newMessage.localized(), - buttonIcon: .newMessageIcon, - buttonStyle: .medium(.raisedPrimary), - buttonCallback: { + subtitle: String.Constants.messagingChatsListEmptySubtitle.localized(), + icon: .messageCircleIcon24, + actionButtonConfiguration: .init(buttonTitle: String.Constants.newMessage.localized(), + buttonIcon: .newMessageIcon, + buttonStyle: .medium(.raisedPrimary), + buttonCallback: { logButtonPressedAnalyticEvents(button: .emptyMessagingAction, parameters: [.value: ChatsList.DataType.chats.rawValue]) viewModel.searchMode = .chatsOnly viewModel.isSearchActive = true - }) + })) } @ViewBuilder @@ -352,13 +362,13 @@ private extension ChatListView { ChatListEmptyStateView(title: String.Constants.messagingCommunitiesListEnableTitle.localized(), subtitle: String.Constants.messagingCommunitiesListEnableSubtitle.localized(), icon: .chatRequestsIcon, - buttonTitle: String.Constants.enable.localized(), - buttonIcon: Image(uiImage: appContext.authentificationService.biometricIcon ?? .init()), - buttonStyle: .medium(.raisedPrimary), - buttonCallback: { + actionButtonConfiguration: .init(buttonTitle: String.Constants.enable.localized(), + buttonIcon: Image(uiImage: appContext.authentificationService.biometricIcon ?? .init()), + buttonStyle: .medium(.raisedPrimary), + buttonCallback: { logButtonPressedAnalyticEvents(button: .createCommunityProfile) viewModel.createCommunitiesProfileButtonPressed() - }) + })) } @ViewBuilder @@ -366,14 +376,14 @@ private extension ChatListView { ChatListEmptyStateView(title: String.Constants.messagingCommunitiesEmptyTitle.localized(), subtitle: String.Constants.messagingCommunitiesEmptySubtitle.localized(), icon: .messageCircleIcon24, - buttonTitle: String.Constants.learnMore.localized(), - buttonIcon: .infoIcon, - buttonStyle: .medium(.raisedPrimary), - buttonCallback: { + actionButtonConfiguration: .init(buttonTitle: String.Constants.learnMore.localized(), + buttonIcon: .infoIcon, + buttonStyle: .medium(.raisedPrimary), + buttonCallback: { logButtonPressedAnalyticEvents(button: .emptyMessagingAction, parameters: [.value: ChatsList.DataType.communities.rawValue]) openLink(.communitiesInfo) - }) + })) } @ViewBuilder @@ -444,15 +454,15 @@ private extension ChatListView { ChatListEmptyStateView(title: String.Constants.messagingChannelsEmptyTitle.localized(), subtitle: String.Constants.messagingChannelsEmptySubtitle.localized(), icon: .messageCircleIcon24, - buttonTitle: String.Constants.searchApps.localized(), - buttonIcon: .searchIcon, - buttonStyle: .medium(.raisedTertiary), - buttonCallback: { + actionButtonConfiguration: .init(buttonTitle: String.Constants.searchApps.localized(), + buttonIcon: .searchIcon, + buttonStyle: .medium(.raisedTertiary), + buttonCallback: { logButtonPressedAnalyticEvents(button: .emptyMessagingAction, parameters: [.value: ChatsList.DataType.channels.rawValue]) viewModel.searchMode = .channelsOnly viewModel.isSearchActive = true - }) + })) } @ViewBuilder @@ -533,6 +543,7 @@ extension ChatListView { case createProfile case chatsList case loading + case mpcUnavailable } enum CommunitiesListState { diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/ChatsList/ChatListViewModel.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/ChatsList/ChatListViewModel.swift index 2482d2304..4ec8522b4 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/ChatsList/ChatListViewModel.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/ChatsList/ChatListViewModel.swift @@ -526,6 +526,12 @@ private extension ChatListViewModel { profileWalletPairsCache.append(chatProfile) } + if chatProfile.wallet.udWallet.type == .mpc, + appContext.udFeatureFlagsService.valueFor(flag: .isMPCMessagingEnabled) == false { + chatState = .mpcUnavailable + return + } + guard let profile = chatProfile.profile else { let state: MessagingProfileStateAnalytics = chatProfile.wallet.rrDomain == nil ? .notCreatedRRNotSet : .notCreatedRRSet logAnalytic(event: .willShowMessagingProfile, diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/ChatsList/ChatListViews/ChatListEmptyStateView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/ChatsList/ChatListViews/ChatListEmptyStateView.swift index 8be87b394..73545a929 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/ChatsList/ChatListViews/ChatListEmptyStateView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/ChatsList/ChatListViews/ChatListEmptyStateView.swift @@ -11,10 +11,7 @@ struct ChatListEmptyStateView: View { let title: String let subtitle: String let icon: Image - let buttonTitle: String - let buttonIcon: Image - let buttonStyle: UDButtonStyle - let buttonCallback: MainActorCallback + let actionButtonConfiguration: ActionButtonConfiguration? var body: some View { VStack(spacing: 24) { @@ -32,10 +29,7 @@ struct ChatListEmptyStateView: View { .foregroundStyle(Color.foregroundSecondary) .multilineTextAlignment(.center) - UDButtonView(text: buttonTitle, - icon: buttonIcon, - style: buttonStyle, - callback: buttonCallback) + actionButtonForCurrentConfiguration() } .frame(maxWidth: .infinity) .frame(height: 400) @@ -44,12 +38,35 @@ struct ChatListEmptyStateView: View { } } +// MARK: - Private methods +private extension ChatListEmptyStateView { + @ViewBuilder + func actionButtonForCurrentConfiguration() -> some View { + if let actionButtonConfiguration { + UDButtonView(text: actionButtonConfiguration.buttonTitle, + icon: actionButtonConfiguration.buttonIcon, + style: actionButtonConfiguration.buttonStyle, + callback: actionButtonConfiguration.buttonCallback) + } + } +} + +// MARK: - Open methods +extension ChatListEmptyStateView { + struct ActionButtonConfiguration { + let buttonTitle: String + let buttonIcon: Image + let buttonStyle: UDButtonStyle + let buttonCallback: MainActorCallback + } +} + #Preview { ChatListEmptyStateView(title: "Title", subtitle: "Subtitle", icon: .messageCircleIcon24, - buttonTitle: "Action", - buttonIcon: .messagesIcon, - buttonStyle: .medium(.raisedPrimary), - buttonCallback: { }) + actionButtonConfiguration: .init(buttonTitle: "Action", + buttonIcon: .messagesIcon, + buttonStyle: .medium(.raisedPrimary), + buttonCallback: { })) } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Onboarding/OnboardingRestoreWallets/RestoreWalletViewController.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Onboarding/OnboardingRestoreWallets/RestoreWalletViewController.swift index 6d1225e67..bc5dec83c 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Onboarding/OnboardingRestoreWallets/RestoreWalletViewController.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Onboarding/OnboardingRestoreWallets/RestoreWalletViewController.swift @@ -102,7 +102,11 @@ private extension RestoreWalletViewController { restoreOptions.append([.iCloud(value: iCLoudRestoreHintValue(backedUpWallets: backedUpWallets))]) } - restoreOptions.append([.mpc, .recoveryPhrase, .externalWallet, .websiteAccount]) + if appContext.udFeatureFlagsService.valueFor(flag: .isMPCWalletEnabled) { + restoreOptions.append([.mpc, .recoveryPhrase, .externalWallet, .websiteAccount]) + } else { + restoreOptions.append([.recoveryPhrase, .externalWallet, .websiteAccount]) + } let selectionView = RestoreWalletView(options: restoreOptions) { [weak self] restoreOption in self?.logButtonPressedAnalyticEvents(button: restoreOption.analyticsName) 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 98c4aca90..e5fd3b2e9 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Settings/SettingsView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Settings/SettingsView.swift @@ -10,6 +10,7 @@ import MessageUI struct SettingsView: View, ViewAnalyticsLogger { + @Environment(\.udFeatureFlagsService) var udFeatureFlagsService @Environment(\.userProfilesService) var userProfilesService @EnvironmentObject private var tabRouter: HomeTabRouter @@ -512,12 +513,17 @@ private extension SettingsView { guard let view = appContext.coreAppCoordinator.topVC else { return } Task { - let actions: [WalletDetailsAddWalletAction] + var actions: [WalletDetailsAddWalletAction] = [] if isImportOnly { actions = [.mpc, .recoveryOrKey, .connect] } else { actions = WalletDetailsAddWalletAction.allCases } + + if !udFeatureFlagsService.valueFor(flag: .isMPCWalletEnabled) { + actions.removeAll(where: { $0 == .mpc }) + } + do { let action = try await appContext.pullUpViewService.showAddWalletSelectionPullUp(in: view, presentationOptions: .default, @@ -573,7 +579,8 @@ private extension SettingsView { } func activateMPCWallet() { - guard let view = appContext.coreAppCoordinator.topVC else { return } + guard udFeatureFlagsService.valueFor(flag: .isMPCWalletEnabled), + let view = appContext.coreAppCoordinator.topVC else { return } UDRouter().showActivateMPCWalletScreen(activationResultCallback: handleMPCActivationResult, in: view) } diff --git a/unstoppable-ios-app/domains-manager-ios/Services/FeatureFlags/LaunchDarklyService.swift b/unstoppable-ios-app/domains-manager-ios/Services/FeatureFlags/LaunchDarklyService.swift index 2738c0667..77f6cd280 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/FeatureFlags/LaunchDarklyService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/FeatureFlags/LaunchDarklyService.swift @@ -22,7 +22,13 @@ final class LaunchDarklyService { Debugger.printFailure("Failed to create context for Launch darkly", critical: true) return } - let config = LDConfig(mobileKey: mobileKey, autoEnvAttributes: .enabled) + var applicationInfo = ApplicationInfo() + applicationInfo.applicationIdentifier(Constants.ldApplicationIdentifier) + applicationInfo.applicationVersion(Version.getCurrentAppVersionString()) + + var config = LDConfig(mobileKey: mobileKey, autoEnvAttributes: .enabled) + config.applicationInfo = applicationInfo + LDClient.start(config: config, context: context) } 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 1e81d037b..ba6ea9b84 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/FeatureFlags/UDFeatureFlag.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/FeatureFlags/UDFeatureFlag.swift @@ -9,11 +9,21 @@ import Foundation enum UDFeatureFlag: String, CaseIterable { case communityMediaEnabled = "ecommerce-service-users-enable-chat-community-media" + case isBuyCryptoEnabled = "mobile-buy-crypto-enabled" + case isSendCryptoEnabled = "mobile-send-crypto-enabled" + + case isMPCWalletEnabled = "mobile-mpc-wallet-enabled" + case isMPCSendCryptoEnabled = "mobile-mpc-send-crypto-enabled" + case isMPCMessagingEnabled = "mobile-mpc-messaging-enabled" + case isMPCWCNativeEnabled = "mobile-mpc-wc-native-enabled" + case isMPCSignatureEnabled = "mobile-mpc-signature-enabled" var defaultValue: Bool { switch self { - case .communityMediaEnabled: + case .communityMediaEnabled, .isBuyCryptoEnabled, .isMPCMessagingEnabled, .isMPCWCNativeEnabled: return false + case .isSendCryptoEnabled, .isMPCWalletEnabled, .isMPCSendCryptoEnabled, .isMPCSignatureEnabled: + return true } } } diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Constants.swift b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Constants.swift index 6018272ae..82a1e59b5 100644 --- a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Constants.swift +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Constants.swift @@ -61,8 +61,7 @@ struct Constants { static let popularCoinsTickers: [String] = ["BTC", "ETH", "ZIL", "LTC", "XRP"] // This is not required order to be on the UI static let additionalSupportedTokens = ["crypto.SOL.address", "crypto.BTC.address"] static let baseChainSymbol: String = "BASE" - static let isBuyCryptoEnabled = false - + static let ldApplicationIdentifier: String = "ud-ios-app" // Launch darkly id // Shake to find static let shakeToFindServiceId: String = "090DAE5A-0DD8-4327-B074-E1E09B259597" 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 16a126245..12ea47637 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 @@ -1119,10 +1119,12 @@ More tabs are coming in the next updates."; "MPC_WRONG_PASSWORD_MESSAGE" = "You’ve entered wrong password for %@"; "CHANGE" = "Change"; "RE_IMPORT_MPC_WALLET_PROMPT_TITLE" = "Please re-import your Unstoppable Wallet %@"; -"RE_IMPORT_MPC_WALLET_PROMPT_SUBTITLE" = "Periodically Unstoppable Wallets must be re-imported. This design enhances security and ensures the protection of assets in your wallet."; +"RE_IMPORT_MPC_WALLET_PROMPT_SUBTITLE" = "Periodically %@ must be re-imported. This design enhances security and ensures the protection of assets in your wallet."; "RE_IMPORT_WALLET" = "Re-import wallet"; -"REMOVE_MPC_WALLET_PULL_UP_TITLE" = "Are you sure you want to remove your Unstoppable Wallet?"; +"REMOVE_MPC_WALLET_PULL_UP_TITLE" = "Are you sure you want to remove your %@?"; "REMOVE_MPC_WALLET_PULL_UP_SUBTITLE" = "This wallet will be removed immediately. You can add the wallet to the app later."; +"MPC_WALLET_MESSAGING_UNAVAILABLE_MESSAGE" = "Messaging is coming soon for %@"; +"MPC_WALLET_SIGNING_UNAVAILABLE_ERROR_MESSAGE" = "This %@ functionality is temporarily unavailable."; // Send crypto first time "SEND_CRYPTO_FIRST_TIME_PULL_UP_TITLE" = "Sending cryptocurrency for the first time?"; 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 46bf53a87..11b5cd2b8 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 @@ -489,7 +489,7 @@ extension ViewPullUpDefaultConfiguration { removeCallback: @escaping MainActorAsyncCallback) -> ViewPullUpDefaultConfiguration { return .init(icon: .init(icon: .trashFill, size: .small), - title: .text(String.Constants.removeMPCWalletPullUpTitle.localized()), + title: .text(String.Constants.removeMPCWalletPullUpTitle.localizedMPCProduct()), subtitle: .label(.text(String.Constants.removeMPCWalletPullUpSubtitle.localized())), actionButton: .primaryDanger(content: .init(title: String.Constants.removeWallet.localized(), analyticsName: .walletRemove,