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-preview/Entities/PreviewEIP712TypedData.swift b/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewEIP712TypedData.swift new file mode 100644 index 000000000..3ed118227 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewEIP712TypedData.swift @@ -0,0 +1,40 @@ +// +// PreviewEIP712TypedData.swift +// unstoppable-preview +// +// Created by Oleg Kuplin on 24.05.2024. +// + +import Foundation + +public struct EIP712TypedData: Codable { + public let domain: JSON + public let message: JSON +} + +public enum JSON: Equatable, Codable { + case string(String) + case number(Float) + case object([String: JSON]) + case array([JSON]) + case bool(Bool) + case null +} + +public extension JSON { + public var debugDescription: String { "" } + + subscript(index: Int) -> JSON? { + if case .array(let arr) = self, arr.indices.contains(index) { + return arr[index] + } + return nil + } + + subscript(key: String) -> JSON? { + if case .object(let dict) = self { + return dict[key] + } + return nil + } +} diff --git a/unstoppable-ios-app/domains-manager-ios-preview/Modules/PreviewWCRequests/PreviewWCRequests.swift b/unstoppable-ios-app/domains-manager-ios-preview/Modules/PreviewWCRequests/PreviewWCRequests.swift index a4d43f95b..ead8a1935 100644 --- a/unstoppable-ios-app/domains-manager-ios-preview/Modules/PreviewWCRequests/PreviewWCRequests.swift +++ b/unstoppable-ios-app/domains-manager-ios-preview/Modules/PreviewWCRequests/PreviewWCRequests.swift @@ -17,7 +17,7 @@ import SwiftUI let connectConfiguration = createConnectConfiguration() let signConfiguration = createSignConfiguration() let paymentConfiguration = createPaymentConfiguration() - _ = try? await appContext.pullUpViewService.showServerConnectConfirmationPullUp(for: connectConfiguration, + _ = try? await appContext.pullUpViewService.showWCRequestConfirmationPullUp(for: connectConfiguration, in: vc) } } diff --git a/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj b/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj index 2d53bd82b..606c04f6e 100644 --- a/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj +++ b/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj @@ -8,9 +8,7 @@ /* Begin PBXBuildFile section */ 290136B42BFBFA3F00AB126D /* EIP712View.xib in Resources */ = {isa = PBXBuildFile; fileRef = 290136B32BFBFA3F00AB126D /* EIP712View.xib */; }; - 290136B52BFBFA3F00AB126D /* EIP712View.xib in Resources */ = {isa = PBXBuildFile; fileRef = 290136B32BFBFA3F00AB126D /* EIP712View.xib */; }; 290136B72BFBFAAE00AB126D /* EIP712View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 290136B62BFBFAAE00AB126D /* EIP712View.swift */; }; - 290136B82BFBFAAE00AB126D /* EIP712View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 290136B62BFBFAAE00AB126D /* EIP712View.swift */; }; 29018C8D2BACB7BC0004545D /* JRPC_Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29018C8C2BACB7BC0004545D /* JRPC_Client.swift */; }; 290A60422950A89900882109 /* WalletConnectServiceV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 290A60412950A89900882109 /* WalletConnectServiceV2.swift */; }; 290A60482950AA1600882109 /* WalletConnect in Frameworks */ = {isa = PBXBuildFile; productRef = 290A60472950AA1600882109 /* WalletConnect */; }; @@ -1420,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 */; }; @@ -1593,6 +1593,12 @@ C6BA74742AD5013500628DC6 /* PullUpViewService+DomainProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6BA74732AD5013500628DC6 /* PullUpViewService+DomainProfile.swift */; }; C6BB08EE2BFDB50A00123465 /* FB_UD_MPCAPIBadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6BB08ED2BFDB50A00123465 /* FB_UD_MPCAPIBadResponse.swift */; }; C6BB08EF2BFDB50A00123465 /* FB_UD_MPCAPIBadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6BB08ED2BFDB50A00123465 /* FB_UD_MPCAPIBadResponse.swift */; }; + C6BEC94B2C00382900F21FB6 /* EIP712View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 290136B62BFBFAAE00AB126D /* EIP712View.swift */; }; + C6BEC94C2C00382900F21FB6 /* EIP712View.xib in Resources */ = {isa = PBXBuildFile; fileRef = 290136B32BFBFA3F00AB126D /* EIP712View.xib */; }; + C6BEC94F2C0038F700F21FB6 /* PreviewEIP712TypedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6BEC94E2C0038F700F21FB6 /* PreviewEIP712TypedData.swift */; }; + C6BEC9512C004B0700F21FB6 /* RequestsLimitControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6BEC9502C004B0700F21FB6 /* RequestsLimitControllerTests.swift */; }; + C6BEC9532C004D2A00F21FB6 /* RequestsLimitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6BEC9522C004D2A00F21FB6 /* RequestsLimitController.swift */; }; + C6BEC9542C004D2A00F21FB6 /* RequestsLimitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6BEC9522C004D2A00F21FB6 /* RequestsLimitController.swift */; }; C6BEEF3029C30C89000489B9 /* FirebaseNetworkConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6BEEF2F29C30C89000489B9 /* FirebaseNetworkConfig.swift */; }; C6BF0C5B2B8EDEB4009CB50F /* CheckPendingEventsOnAppearViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6BF0C5A2B8EDEB4009CB50F /* CheckPendingEventsOnAppearViewModifier.swift */; }; C6BF0C5C2B8EDEB4009CB50F /* CheckPendingEventsOnAppearViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6BF0C5A2B8EDEB4009CB50F /* CheckPendingEventsOnAppearViewModifier.swift */; }; @@ -3614,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 = ""; }; @@ -3725,6 +3732,9 @@ C6BA74712AD4FEE600628DC6 /* PullUpViewService+ExternalWallets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PullUpViewService+ExternalWallets.swift"; sourceTree = ""; }; C6BA74732AD5013500628DC6 /* PullUpViewService+DomainProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PullUpViewService+DomainProfile.swift"; sourceTree = ""; }; C6BB08ED2BFDB50A00123465 /* FB_UD_MPCAPIBadResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FB_UD_MPCAPIBadResponse.swift; sourceTree = ""; }; + C6BEC94E2C0038F700F21FB6 /* PreviewEIP712TypedData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewEIP712TypedData.swift; sourceTree = ""; }; + C6BEC9502C004B0700F21FB6 /* RequestsLimitControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestsLimitControllerTests.swift; sourceTree = ""; }; + C6BEC9522C004D2A00F21FB6 /* RequestsLimitController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestsLimitController.swift; sourceTree = ""; }; C6BEEF2F29C30C89000489B9 /* FirebaseNetworkConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseNetworkConfig.swift; sourceTree = ""; }; C6BF0C5A2B8EDEB4009CB50F /* CheckPendingEventsOnAppearViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckPendingEventsOnAppearViewModifier.swift; sourceTree = ""; }; C6BF6BD92B8EE724006CC2BD /* PassViewAnalyticsDetailsViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassViewAnalyticsDetailsViewModifier.swift; sourceTree = ""; }; @@ -4258,6 +4268,7 @@ 30C0F94927D8CB250060D283 /* PrivateKeyStorageTests.swift */, C62900FD2BAAD126008B35A2 /* NumberPadInputInterpreterTests.swift */, 29EDB622290A94E700A0BD08 /* ProfilesTests.swift */, + C6BEC9502C004B0700F21FB6 /* RequestsLimitControllerTests.swift */, 30F526452785BB22004C7AB6 /* ResolutionInitTests.swift */, 308B1D4025B8A75D005FE726 /* testUpdateRecords.swift */, C6D9E29D28537AC8002CDAC2 /* TransactionsDecodingTests.swift */, @@ -4941,6 +4952,7 @@ C63095C72B0DA5DE00205054 /* PurchaseDomainsPreferencesStorageEnvironmentKey.swift */, C617FD9D2B58DBCA00B93433 /* WalletsDataServiceEnvironmentKey.swift */, C65CEB8A2B674FC700A13B34 /* UDWalletsServiceEnvironmentKey.swift */, + C6A440082C0448530042FFCC /* UDFeatureFlagsServiceEnvironmentKey.swift */, ); path = EnvironmentKeys; sourceTree = ""; @@ -5001,6 +5013,7 @@ C6960C652B199B6F00B79E28 /* PreviewDomainItem.swift */, C6D6475A2B1EDCED00D724AC /* PreviewDomainProfileInfoStorage.swift */, C6D646DB2B1ED3F500D724AC /* PreviewDomainProfileSignatureValidator.swift */, + C6BEC94E2C0038F700F21FB6 /* PreviewEIP712TypedData.swift */, C6C8F95C2B21867E00A9834D /* PreviewEncrypting.swift */, C6A89C602B31657D008AB043 /* PreviewHotFeaturesSuggestionsFetcher.swift */, C6C8F8572B217F8200A9834D /* PreviewiCloudPrivateKeyStorage.swift */, @@ -7348,6 +7361,7 @@ C6DF46222AA180C000D124E7 /* PublicDomainDisplayInfo.swift */, C63095D52B0DA61600205054 /* PublishingAppStorage.swift */, C643129A2B68A1AE00BCA2A4 /* PhotoLibraryImageSaver.swift */, + C6BEC9522C004D2A00F21FB6 /* RequestsLimitController.swift */, C61FD05528FD3F540088CFDD /* ShareDomainHandler.swift */, C669C37E29124C2600837F21 /* SocialsType.swift */, C69F99552A9F167F004B1958 /* DomainProfileSocialAccount.swift */, @@ -8534,7 +8548,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 290136B52BFBFA3F00AB126D /* EIP712View.xib in Resources */, C67B6D5E2AE7F8FB00F74B0B /* Media.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -8616,6 +8629,7 @@ C6960C2D2B19945000B79E28 /* Assets.xcassets in Resources */, C6FEA9192BEB700E004FD740 /* SettingsCollectionViewCell.xib in Resources */, C6D6473D2B1ED9EF00D724AC /* PaymentTransactionCostView.xib in Resources */, + C6BEC94C2C00382900F21FB6 /* EIP712View.xib in Resources */, C6D646D42B1ED2D500D724AC /* UDTextField.xib in Resources */, C6D646F02B1ED5B200D724AC /* AddWalletViewController.xib in Resources */, C6C8F8422B217E9600A9834D /* CreatePasswordViewController.xib in Resources */, @@ -9169,6 +9183,7 @@ C61B3E72283E43D000500B6D /* EnterEmailViewPresenter.swift in Sources */, C6ED320D295E8EDE00BC6919 /* NonEmptyArray.swift in Sources */, C63DBCCB2BE49376008F3D2C /* NetworkService+Common.swift in Sources */, + C6BEC9532C004D2A00F21FB6 /* RequestsLimitController.swift in Sources */, C63F1CFF28AD099C000A5C12 /* EmptyRootCNavigationController.swift in Sources */, 309BD85F265679EA00CB0C49 /* PaymentConfiguration.swift in Sources */, C6B761FC2BB403F700773943 /* HomeActivity.swift in Sources */, @@ -9741,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 */, @@ -9909,8 +9925,8 @@ C6B2E2122B970E0900CEA1F9 /* DomainProfileSocialRelationshipDetailsTests.swift in Sources */, C617CFA42B9ED9F200663516 /* TestableDomainTransactionsService.swift in Sources */, C632C9432BA957BF00B0072B /* UserProfilesServiceTests.swift in Sources */, + C6BEC9512C004B0700F21FB6 /* RequestsLimitControllerTests.swift in Sources */, C63391782A86819600623188 /* XMTPMessagingAPIServiceTests.swift in Sources */, - 290136B82BFBFAAE00AB126D /* EIP712View.swift in Sources */, C67B6D572AE79E8400F74B0B /* ImagesCacheStorageTests.swift in Sources */, C67DE1432B983FD0002374CE /* HomeExploreViewModelTests.swift in Sources */, C6B6B8732B91A4F900565ED2 /* TaskWithDeadlineTests.swift in Sources */, @@ -9986,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 */, @@ -10006,6 +10023,7 @@ C6D6472E2B1ED9AA00D724AC /* SelectAppearanceThemePullUpView.swift in Sources */, C6C8F8722B21822700A9834D /* TutorialViewController.swift in Sources */, C6D647372B1ED9EF00D724AC /* PaymentTransactionGasOnlyCostView.swift in Sources */, + C6BEC9542C004D2A00F21FB6 /* RequestsLimitController.swift in Sources */, C61808712B19BC150032E543 /* Image.swift in Sources */, C61807F92B19A7DF0032E543 /* ImageLoadingServiceProtocol.swift in Sources */, C61808302B19AD9D0032E543 /* PreviewStripeService.swift in Sources */, @@ -10084,6 +10102,7 @@ C6F7D9D02B8D6EFC00764708 /* MessageActionReplyButtonView.swift in Sources */, C68BAC932B919D8E00001CA0 /* ChatViewScrollHandler.swift in Sources */, C618080F2B19AA420032E543 /* UserDataServiceProtocol.swift in Sources */, + C6BEC94F2C0038F700F21FB6 /* PreviewEIP712TypedData.swift in Sources */, C618083A2B19AF800032E543 /* PreviewExternalEventsService.swift in Sources */, C61808402B19B0870032E543 /* PreviewDomainTransactionsService.swift in Sources */, C6D646042B1DBFF700D724AC /* WalletConnectServiceConnectionListener.swift in Sources */, @@ -10109,6 +10128,7 @@ C6C8F8B72B2182CF00A9834D /* MintDomainsConfigurationSelectionCell.swift in Sources */, C61807C22B19A2E70032E543 /* PermissionsService.swift in Sources */, C6D646B72B1ED18F00D724AC /* SaveDomainImageTypePullUpView.swift in Sources */, + C6BEC94B2C00382900F21FB6 /* EIP712View.swift in Sources */, C6D011C62B996A5C0008BF40 /* DomainProfileSuggestion.swift in Sources */, C6C8F8CD2B21832F00A9834D /* BaseMintingTransactionInProgressViewPresenter.swift in Sources */, C6C8F8592B217F8200A9834D /* PreviewiCloudPrivateKeyStorage.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/Entities/Mock/MockEntitiesFabric+Messaging.swift b/unstoppable-ios-app/domains-manager-ios/Entities/Mock/MockEntitiesFabric+Messaging.swift index 816e05b6f..b9315ec3d 100644 --- a/unstoppable-ios-app/domains-manager-ios/Entities/Mock/MockEntitiesFabric+Messaging.swift +++ b/unstoppable-ios-app/domains-manager-ios/Entities/Mock/MockEntitiesFabric+Messaging.swift @@ -164,11 +164,10 @@ extension MockEntitiesFabric { let sender = chatSenderFor(isThisUser: false) members.append(sender.userDisplayInfo) } - let badgeInfo = BadgeDetailedInfo(badge: .init(code: chatId, - name: name, - logo: MockEntitiesFabric.ImageURLs.aiAvatar.rawValue, - description: "This is community for this badge holders."), - usage: .init(rank: 10, holders: 10, domains: 10, featured: nil)) + let badgeInfo = BadgesInfo.BadgeInfo(code: chatId, + name: name, + logo: MockEntitiesFabric.ImageURLs.aiAvatar.rawValue, + description: "This is community for this badge holders.") let type: MessagingCommunitiesChatDetails.CommunityType = .badge(badgeInfo) let chat = MessagingChatDisplayInfo(id: chatId, thisUserDetails: sender.userDisplayInfo, diff --git a/unstoppable-ios-app/domains-manager-ios/Entities/RequestsLimitController.swift b/unstoppable-ios-app/domains-manager-ios/Entities/RequestsLimitController.swift new file mode 100644 index 000000000..84548b7f7 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Entities/RequestsLimitController.swift @@ -0,0 +1,37 @@ +// +// RequestsLimitController.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 24.05.2024. +// + +import Foundation + +actor RequestsLimitController { + private var requestTimestamps: [Date] = [] + private let requestLimit: Int + private let timeInterval: TimeInterval + + init(requestLimit: Int, timeInterval: TimeInterval) { + self.requestLimit = requestLimit + self.timeInterval = timeInterval + } + + func acquirePermission() async { + let now = Date() + + // Remove timestamps older than the specified time interval + requestTimestamps = requestTimestamps.filter { now.timeIntervalSince($0) < timeInterval } + + if requestTimestamps.count >= requestLimit { + let earliestRequest = requestTimestamps.first! + let waitTime = timeInterval - now.timeIntervalSince(earliestRequest) + await Task.sleep(seconds: waitTime) + + // Recheck and clean up the timestamps after waiting + requestTimestamps = requestTimestamps.filter { now.timeIntervalSince($0) < timeInterval } + } + + requestTimestamps.append(now) + } +} 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 f218effc8..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 @@ -453,7 +453,8 @@ extension String { static let settingsAppearanceThemeDark = "SETTINGS_APPEARANCE_THEME_DARK" static let settingsAppearanceChooseTheme = "SETTINGS_APPEARANCE_CHOOSE_THEME" static let youAreUnstoppable = "YOU_ARE_UNSTOPPABLE" - + static let feedbackEmailSubject = "FEEDBACK_EMAIL_SUBJECT" + // Wallets list static let manageICloudBackups = "MANAGE_ICLOUD_BACKUPS" static let restoreFromICloudBackup = "RESTORE_FROM_ICLOUD_BACKUP" @@ -1228,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/ActivateMPCWalletFlow/ActivateMPCWalletFlow.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ActivateMPCWalletFlow/ActivateMPCWalletFlow.swift index 7ad535dc9..0abac1ab2 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ActivateMPCWalletFlow/ActivateMPCWalletFlow.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ActivateMPCWalletFlow/ActivateMPCWalletFlow.swift @@ -21,7 +21,6 @@ extension ActivateMPCWalletFlow { enum FlowResult { case activated(UDWallet) - case restart } static let viewsTopOffset: CGFloat = 30 diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ActivateMPCWalletFlow/ActivateMPCWalletRootView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ActivateMPCWalletFlow/ActivateMPCWalletRootView.swift index 8f260e076..9f230c86c 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ActivateMPCWalletFlow/ActivateMPCWalletRootView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ActivateMPCWalletFlow/ActivateMPCWalletRootView.swift @@ -15,7 +15,7 @@ struct ActivateMPCWalletRootView: View { var body: some View { NavigationViewWithCustomTitle(content: { ZStack { - MPCEnterCredentialsInAppView() + MPCEnterCredentialsInAppView(preFilledEmail: viewModel.preFilledEmail) .environmentObject(viewModel) .navigationBarTitleDisplayMode(.inline) .navigationDestination(for: ActivateMPCWalletFlow.NavigationDestination.self) { destination in @@ -39,8 +39,10 @@ struct ActivateMPCWalletRootView: View { .allowsHitTesting(!viewModel.isLoading) } - init(activationResultCallback: @escaping ActivateMPCWalletFlow.FlowResultCallback) { - self._viewModel = StateObject(wrappedValue: ActivateMPCWalletViewModel(activationResultCallback: activationResultCallback)) + init(preFilledEmail: String?, + activationResultCallback: @escaping ActivateMPCWalletFlow.FlowResultCallback) { + self._viewModel = StateObject(wrappedValue: ActivateMPCWalletViewModel(preFilledEmail: preFilledEmail, + activationResultCallback: activationResultCallback)) } } @@ -55,5 +57,6 @@ private extension ActivateMPCWalletRootView { } #Preview { - ActivateMPCWalletRootView(activationResultCallback: { _ in }) + ActivateMPCWalletRootView(preFilledEmail: nil, + activationResultCallback: { _ in }) } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ActivateMPCWalletFlow/ActivateMPCWalletViewModel.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ActivateMPCWalletFlow/ActivateMPCWalletViewModel.swift index 455a9a790..fbd89f30d 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ActivateMPCWalletFlow/ActivateMPCWalletViewModel.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ActivateMPCWalletFlow/ActivateMPCWalletViewModel.swift @@ -10,6 +10,7 @@ import SwiftUI @MainActor final class ActivateMPCWalletViewModel: ObservableObject { + let preFilledEmail: String? let activationResultCallback: ActivateMPCWalletFlow.FlowResultCallback @Published var navPath: [ActivateMPCWalletFlow.NavigationDestination] = [] @Published var navigationState: NavigationStateManager? @@ -17,7 +18,9 @@ final class ActivateMPCWalletViewModel: ObservableObject { @Published var error: Error? private var credentials: MPCActivateCredentials? - init(activationResultCallback: @escaping ActivateMPCWalletFlow.FlowResultCallback) { + init(preFilledEmail: String?, + activationResultCallback: @escaping ActivateMPCWalletFlow.FlowResultCallback) { + self.preFilledEmail = preFilledEmail self.activationResultCallback = activationResultCallback } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ActivateMPCWalletFlow/MPCEnterCredentialsInAppView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ActivateMPCWalletFlow/MPCEnterCredentialsInAppView.swift index 4589cb28a..070633f5a 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ActivateMPCWalletFlow/MPCEnterCredentialsInAppView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/ActivateMPCWalletFlow/MPCEnterCredentialsInAppView.swift @@ -10,9 +10,12 @@ import SwiftUI struct MPCEnterCredentialsInAppView: View { @EnvironmentObject var viewModel: ActivateMPCWalletViewModel - + + let preFilledEmail: String? + var body: some View { - MPCEnterCredentialsView(analyticsName: .mpcEnterCredentialsInApp, + MPCEnterCredentialsView(mode: preFilledEmail == nil ? .freeInput : .strictEmail(preFilledEmail!), + analyticsName: .mpcEnterCredentialsInApp, credentialsCallback: didEnterCredentials) .padding(.top, ActivateMPCWalletFlow.viewsTopOffset) } @@ -26,5 +29,5 @@ private extension MPCEnterCredentialsInAppView { } #Preview { - MPCEnterCredentialsInAppView() + MPCEnterCredentialsInAppView(preFilledEmail: nil) } 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/ChatNavTitleView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/Chat/ChatView/ChatNavTitleView.swift index d659da77d..5fe2485fc 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/Chat/ChatView/ChatNavTitleView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/Chat/ChatView/ChatNavTitleView.swift @@ -64,7 +64,7 @@ struct ChatNavTitleView: View { case .community(let communityDetails): switch communityDetails.type { case .badge(let badgeInfo): - let displayInfo = DomainProfileBadgeDisplayInfo(badge: badgeInfo.badge) + let displayInfo = DomainProfileBadgeDisplayInfo(badge: badgeInfo) icon = await displayInfo.loadBadgeIcon() } } 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/ChatListRows/ChatListChatRowView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/ChatsList/ChatListRows/ChatListChatRowView.swift index 0a48a4e3a..e351cef55 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/ChatsList/ChatListRows/ChatListChatRowView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/ChatsList/ChatListRows/ChatListChatRowView.swift @@ -17,6 +17,7 @@ struct ChatListChatRowView: View, ViewAnalyticsLogger { var joinCommunityCallback: EmptyCallback? = nil @State private var icon: UIImage? + @State private var asyncSubtitle: String? private let iconSize: CGFloat = 40 var body: some View { @@ -124,10 +125,14 @@ private extension ChatListChatRowView { } else if case .community(let details) = chat.type, !details.isJoined { switch details.type { - case .badge(let badgeDetailedInfo): - let holders = badgeDetailedInfo.usage.holders - let holdersKsString = holders.asFormattedKsString - return String.Constants.pluralNHolders.localized(holdersKsString, holders) + case .badge(let badge): + Task { + if let holders = try? await BadgeHoldersFetcher.shared.getNumberOfHoldersFor(badge: badge) { + let holdersKsString = holders.asFormattedKsString + asyncSubtitle = String.Constants.pluralNHolders.localized(holdersKsString, holders) + } + } + return asyncSubtitle } } return nil @@ -216,3 +221,36 @@ struct UnreadMessagesCounterView: View { #Preview { ChatListChatRowView(chat: MockEntitiesFabric.Messaging.mockPrivateChat()) } + +private actor BadgeHoldersFetcher { + + private var cache: [String : Int] = [:] + private var ongoingTasks: [String : Task] = [:] + private let requestsLimitController = RequestsLimitController(requestLimit: 20, timeInterval: 60) // 20 requests per 60 seconds + + static let shared = BadgeHoldersFetcher() + + func getNumberOfHoldersFor(badge: BadgesInfo.BadgeInfo) async throws -> Int { + let code = badge.code + + if let cachedValue = cache[code] { + return cachedValue + } else if let ongoingTask = ongoingTasks[code] { + return try await ongoingTask.value + } else { + await requestsLimitController.acquirePermission() + + let task = Task { + let badgeInfo = try await NetworkService().fetchBadgeDetailedInfo(for: badge) + return badgeInfo.usage.holders + } + + self.ongoingTasks[code] = task + let value = try await task.value + self.cache[code] = value + self.ongoingTasks[code] = nil + return value + } + } + +} 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/Messaging/Entities/Chats/MessagingChat.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/Entities/Chats/MessagingChat.swift index 63e2b0358..07723dfa5 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/Entities/Chats/MessagingChat.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/Entities/Chats/MessagingChat.swift @@ -31,7 +31,7 @@ struct MessagingChat: Hashable { switch (lhsCommunity.type, rhsCommunity.type) { case (.badge(let lhsBadge), .badge(let rhsBadge)): - return lhsBadge.badge.code == rhsBadge.badge.code + return lhsBadge.code == rhsBadge.code } default: return false diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/Entities/Chats/MessagingCommunitiesChatDetails.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/Entities/Chats/MessagingCommunitiesChatDetails.swift index b13fa7eff..2c8b2e6f8 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/Entities/Chats/MessagingCommunitiesChatDetails.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Messaging/Entities/Chats/MessagingCommunitiesChatDetails.swift @@ -21,14 +21,14 @@ struct MessagingCommunitiesChatDetails: Hashable, Codable { var displayName: String { switch type { case .badge(let badge): - return badge.badge.name + return badge.name } } var displayIconUrl: String { switch type { - case .badge(let badgeInfo): - return badgeInfo.badge.logo + case .badge(let badge): + return badge.logo } } } @@ -45,6 +45,6 @@ extension MessagingCommunitiesChatDetails { // MARK: - Open methods extension MessagingCommunitiesChatDetails { enum CommunityType: Hashable, Codable { - case badge(BadgeDetailedInfo) + case badge(BadgesInfo.BadgeInfo) } } 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/PurchaseDomains/Search/PurchaseSearchDomainsView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseSearchDomainsView.swift index c2f351960..8110c994d 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseSearchDomainsView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseSearchDomainsView.swift @@ -68,7 +68,6 @@ private extension PurchaseSearchDomainsView { UDTextFieldView(text: $debounceObject.text, placeholder: "domain.x", hint: nil, - rightViewType: currentSearchFieldRightViewType, rightViewMode: .always, leftViewType: .search, focusBehaviour: .activateOnAppear, 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 6859f16f6..276ce1aba 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 @@ -448,12 +449,21 @@ private extension SettingsView { @MainActor func openFeedbackMailForm() { - let mail = MFMailComposeViewController() - - mail.setToRecipients([Constants.UnstoppableSupportMail]) - mail.setSubject("Unstoppable Domains App Feedback - iOS (\(UserDefaults.buildVersion))") - - appContext.coreAppCoordinator.topVC?.present(mail, animated: true) + let canSendMail = MFMailComposeViewController.canSendMail() + let recipientMailAddress = Constants.UnstoppableSupportMail + let subject = String.Constants.feedbackEmailSubject.localized(UserDefaults.buildVersion) + if canSendMail { + let mail = MFMailComposeViewController() + mail.setToRecipients([recipientMailAddress]) + mail.setSubject(subject) + + appContext.coreAppCoordinator.topVC?.present(mail, animated: true) + } else { + let mailURLString = "mailto:\(recipientMailAddress)?subject=\(subject)" + guard let url = URL(string: mailURLString) else { return } + + UIApplication.shared.open(url) + } } } @@ -494,8 +504,8 @@ private extension SettingsView { connectNewWallet() case .createNewWallet: createNewWallet() - case .activateMPC: - activateMPCWallet() + case .activateMPC(let preFilledEmail): + activateMPCWallet(preFilledEmail: preFilledEmail) } } @@ -503,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, @@ -530,7 +545,7 @@ private extension SettingsView { case .connect: connectNewWallet() case .mpc: - activateMPCWallet() + activateMPCWallet(preFilledEmail: nil) } } } @@ -563,23 +578,18 @@ private extension SettingsView { } } - func activateMPCWallet() { - guard let view = appContext.coreAppCoordinator.topVC else { return } + func activateMPCWallet(preFilledEmail: String?) { + guard udFeatureFlagsService.valueFor(flag: .isMPCWalletEnabled), + let view = appContext.coreAppCoordinator.topVC else { return } - UDRouter().showActivateMPCWalletScreen(activationResultCallback: handleMPCActivationResult, in: view) + UDRouter().showActivateMPCWalletScreen(preFilledEmail: preFilledEmail, + activationResultCallback: handleMPCActivationResult, in: view) } func handleMPCActivationResult(_ result: ActivateMPCWalletFlow.FlowResult) { switch result { case .activated(let wallet): addWalletAfterAdded(wallet) - case .restart: - Task { - guard let view = appContext.coreAppCoordinator.topVC else { return } - - await view.presentingViewController?.dismiss(animated: true) - activateMPCWallet() - } } } @@ -614,7 +624,7 @@ private extension SettingsView { extension SettingsView { enum InitialAction { case none - case importWallet, connectWallet, createNewWallet, activateMPC + case importWallet, connectWallet, createNewWallet, activateMPC(preFilledEmail: String?) case showAllAddWalletOptionsPullUp, showImportWalletOptionsPullUp } } 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 eb6b7e610..a378bca81 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/CoreAppCoordinator/CoreAppCoordinator.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/CoreAppCoordinator/CoreAppCoordinator.swift @@ -324,6 +324,8 @@ private extension CoreAppCoordinator { Task { await router.showDomainProfile(domain, wallet: wallet, preRequestedAction: action) } case .showPublicDomainProfile(let publicDomainDisplayInfo, let wallet, let action): Task { await router.showPublicDomainProfileFromDeepLink(of: publicDomainDisplayInfo, by: wallet, preRequestedAction: action) } + case .activateMPCWallet(let email): + router.runAddWalletFlow(initialAction: .activateMPC(preFilledEmail: email)) } default: return } 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 51b62811e..f0baed9ce 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/DeepLinksService/DeepLinksService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/DeepLinksService/DeepLinksService.swift @@ -13,6 +13,7 @@ final class DeepLinksService { private let coreAppCoordinator: CoreAppCoordinatorProtocol private var listeners: [DeepLinkListenerHolder] = [] private let deepLinkPath = "/mobile" + private let ud_me_MPC_path = "wallet" private let wcScheme = "wc" private let customURLScheme = "unstoppabledomains" private var isExpectingWCInteraction = false @@ -38,6 +39,9 @@ extension DeepLinksService: DeepLinksServiceProtocol { } } else if let domainName = DomainProfileLinkValidator.getUDmeDomainName(in: components) { tryHandleUDDomainProfileDeepLink(domainName: domainName, params: components.queryItems, receivedState: receivedState) + } else if tryHandleActivateMPCWalletDeepLink(components: components, + receivedState: receivedState) { + return } else { tryHandleWCDeepLink(from: components, incomingURL: incomingURL, receivedState: receivedState) } @@ -190,6 +194,23 @@ private extension DeepLinksService { receivedState: receivedState) } + func tryHandleActivateMPCWalletDeepLink(components: NSURLComponents, + receivedState: ExternalEventReceivedState) -> Bool { + guard let path = components.path, + let host = components.host else { return false } + + let pathComponents = path.components(separatedBy: "/") + + 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) + return true + } + return false + } + 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 f0eeb0ded..a62291f2f 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/DeepLinksService/DeepLinksServiceProtocol.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/DeepLinksService/DeepLinksServiceProtocol.swift @@ -11,6 +11,7 @@ enum DeepLinkEvent: Equatable { case mintDomainsVerificationCode(email: String, code: String) case showUserDomainProfile(domain: DomainDisplayInfo, wallet: WalletEntity, action: PreRequestedProfileAction?) case showPublicDomainProfile(publicDomainDisplayInfo: PublicDomainDisplayInfo, wallet: WalletEntity, action: PreRequestedProfileAction?) + case activateMPCWallet(email: String?) } protocol DeepLinksServiceProtocol { 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/Services/MessagingService/Push/PushEntitiesTransformer.swift b/unstoppable-ios-app/domains-manager-ios/Services/MessagingService/Push/PushEntitiesTransformer.swift index b931f0e92..ca960b2e9 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/MessagingService/Push/PushEntitiesTransformer.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/MessagingService/Push/PushEntitiesTransformer.swift @@ -36,7 +36,7 @@ struct PushEntitiesTransformer { } struct CommunityChatDetails { - let badgeInfo: BadgeDetailedInfo + let badgeInfo: BadgesInfo.BadgeInfo let blockedUsersList: [String] } @@ -128,14 +128,14 @@ struct PushEntitiesTransformer { return chat } - static func buildEmptyCommunityChatFor(badgeInfo: BadgeDetailedInfo, + static func buildEmptyCommunityChatFor(badgeInfo: BadgesInfo.BadgeInfo, user: MessagingChatUserProfile, blockedUsersList: [String]) -> MessagingChat { let thisUserDetails = MessagingChatUserDisplayInfo(wallet: user.wallet) let blockedUsersList = prepare(blockedUsersList: blockedUsersList) - let info = MessagingChatDisplayInfo(id: "push_community_" + badgeInfo.badge.code, + let info = MessagingChatDisplayInfo(id: "push_community_" + badgeInfo.code, thisUserDetails: thisUserDetails, - avatarURL: URL(string: badgeInfo.badge.logo), + avatarURL: URL(string: badgeInfo.logo), serviceIdentifier: .push, type: .community(.init(type: .badge(badgeInfo), isJoined: false, diff --git a/unstoppable-ios-app/domains-manager-ios/Services/MessagingService/SubServices/MessagingAPI/PushMessagingAPIService.swift b/unstoppable-ios-app/domains-manager-ios/Services/MessagingService/SubServices/MessagingAPI/PushMessagingAPIService.swift index 384f21354..415fe184b 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/MessagingService/SubServices/MessagingAPI/PushMessagingAPIService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/MessagingService/SubServices/MessagingAPI/PushMessagingAPIService.swift @@ -116,21 +116,17 @@ extension PushMessagingAPIService: MessagingAPIServiceProtocol { await withTaskGroup(of: Optional.self, body: { group in for badge in badges { group.addTask { - if let badgeInfo = try? await NetworkService().fetchBadgeDetailedInfo(for: badge) { - if let groupChatId = badge.groupChatId, - let chat = try? await self.getGroupChatBy(groupChatId: groupChatId, - user: user, - badgeInfo: badgeInfo, - blockedUsersList: blockedUsersList) { - return chat - } else { - let chat = PushEntitiesTransformer.buildEmptyCommunityChatFor(badgeInfo: badgeInfo, - user: user, - blockedUsersList: blockedUsersList) - return chat - } + if let groupChatId = badge.groupChatId, + let chat = try? await self.getGroupChatBy(groupChatId: groupChatId, + user: user, + badgeInfo: badge, + blockedUsersList: blockedUsersList) { + return chat } else { - return nil + let chat = PushEntitiesTransformer.buildEmptyCommunityChatFor(badgeInfo: badge, + user: user, + blockedUsersList: blockedUsersList) + return chat } } } @@ -146,7 +142,7 @@ extension PushMessagingAPIService: MessagingAPIServiceProtocol { private func getGroupChatBy(groupChatId: String, user: MessagingChatUserProfile, - badgeInfo: BadgeDetailedInfo, + badgeInfo: BadgesInfo.BadgeInfo, blockedUsersList: [String]) async throws -> MessagingChat { let env = getCurrentPushEnvironment() guard let pushGroup = (try await Push.PushChat.getGroup(chatId: groupChatId, env: env)) else { @@ -178,7 +174,7 @@ extension PushMessagingAPIService: MessagingAPIServiceProtocol { switch details.type { case .badge(let badgeInfo): let privateKey = try await getPGPPrivateKeyFor(user: user) - let signature = try Pgp.sign(message: badgeInfo.badge.code, privateKey: privateKey) + let signature = try Pgp.sign(message: badgeInfo.code, privateKey: privateKey) let pushUser = try await getPushUser(of: user) let blockedUsersList = pushUser.profile.blockedUsersList ?? [] let approveResponse = try await NetworkService().joinBadgeCommunity(badge: badgeInfo, @@ -213,7 +209,7 @@ extension PushMessagingAPIService: MessagingAPIServiceProtocol { switch details.type { case .badge(let badgeInfo): let privateKey = try await getPGPPrivateKeyFor(user: user) - let signature = try Pgp.sign(message: badgeInfo.badge.code, privateKey: privateKey) + let signature = try Pgp.sign(message: badgeInfo.code, privateKey: privateKey) let pushUser = try await getPushUser(of: user) let blockedUsersList = pushUser.profile.blockedUsersList ?? [] diff --git a/unstoppable-ios-app/domains-manager-ios/Services/Networking/NetworkService+MessagingApi.swift b/unstoppable-ios-app/domains-manager-ios/Services/Networking/NetworkService+MessagingApi.swift index 421483375..aa684f6a3 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/Networking/NetworkService+MessagingApi.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/Networking/NetworkService+MessagingApi.swift @@ -19,11 +19,11 @@ extension NetworkService { let signature: String } - func joinBadgeCommunity(badge: BadgeDetailedInfo, + func joinBadgeCommunity(badge: BadgesInfo.BadgeInfo, by wallet: String, signature: String) async throws -> JoinBadgeCommunityResponse { let payload = JoinAndLeaveCommunityRequestPayload(address: wallet, - badgeCode: badge.badge.code, + badgeCode: badge.code, signature: signature) let body = try prepareRequestBodyFrom(entity: payload) let endpoint = Endpoint.joinBadgeCommunity(body: body) @@ -31,10 +31,10 @@ extension NetworkService { return try await fetchDecodableDataFor(endpoint: endpoint, method: .post) } - func leaveBadgeCommunity(badge: BadgeDetailedInfo, + func leaveBadgeCommunity(badge: BadgesInfo.BadgeInfo, by wallet: String, signature: String) async throws { - let payload = JoinAndLeaveCommunityRequestPayload(address: wallet, badgeCode: badge.badge.code, signature: signature) + let payload = JoinAndLeaveCommunityRequestPayload(address: wallet, badgeCode: badge.code, signature: signature) let body = try prepareRequestBodyFrom(entity: payload) let endpoint = Endpoint.leaveBadgeCommunity(body: body) diff --git a/unstoppable-ios-app/domains-manager-ios/Services/UDRouter.swift b/unstoppable-ios-app/domains-manager-ios/Services/UDRouter.swift index da36df63e..0e825f77b 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/UDRouter.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/UDRouter.swift @@ -435,9 +435,11 @@ class UDRouter: DomainProfileSignatureValidator { viewController.present(vc, animated: true) } - func showActivateMPCWalletScreen(activationResultCallback: @escaping ActivateMPCWalletFlow.FlowResultCallback, + func showActivateMPCWalletScreen(preFilledEmail: String?, + activationResultCallback: @escaping ActivateMPCWalletFlow.FlowResultCallback, in viewController: UIViewController) { - let view = ActivateMPCWalletRootView(activationResultCallback: activationResultCallback) + let view = ActivateMPCWalletRootView(preFilledEmail: preFilledEmail, + activationResultCallback: activationResultCallback) let vc = UIHostingController(rootView: view) viewController.present(vc, animated: true) } diff --git a/unstoppable-ios-app/domains-manager-ios/Services/WalletConnect/WalletConnectService_V2/WalletConnectServiceV2.swift b/unstoppable-ios-app/domains-manager-ios/Services/WalletConnect/WalletConnectService_V2/WalletConnectServiceV2.swift index 50caa7494..57147919d 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/WalletConnect/WalletConnectService_V2/WalletConnectServiceV2.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/WalletConnect/WalletConnectService_V2/WalletConnectServiceV2.swift @@ -707,24 +707,13 @@ extension WalletConnectServiceV2: WalletConnectV2RequestHandlingServiceProtocol Debugger.printInfo(topic: .WalletConnectV2, "Successfully signed TX via external wallet: \(udWallet.address)") return .response(sig) - case .mpc: print("sign with mpc") - return .error(.internalError) // TODO: mpc + case .mpc: + // we have stopped handling eth_signTransaction internally + throw WalletConnectRequestError.methodUnsupported default: // locally verified wallet - guard let privKeyString = udWallet.getPrivateKey() else { - Debugger.printFailure("No private key in \(udWallet)", critical: true) - throw WalletConnectRequestError.failedToGetPrivateKey - } - - let privateKey = try EthereumPrivateKey(hexPrivateKey: privKeyString) - - let chainId = EthereumQuantity(quantity: BigUInt(chainIdInt)) - - let signedTx = try completedTx.sign(with: privateKey, chainId: chainId) - let (r, s, v) = (signedTx.r, signedTx.s, signedTx.v) - let signature = r.hex() + s.hex().dropFirst(2) + String(v.quantity, radix: 16) - - return .response(WCAnyCodable(signature)) + // we have stopped handling eth_signTransaction internally + throw WalletConnectRequestError.methodUnsupported } } 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 1e558dee3..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 @@ -343,6 +343,7 @@ "SETTINGS_APPEARANCE_THEME_DARK" = "Dark"; "SETTINGS_APPEARANCE_CHOOSE_THEME" = "Choose theme"; "YOU_ARE_UNSTOPPABLE" = "You are Unstoppable!"; +"FEEDBACK_EMAIL_SUBJECT" = "Unstoppable Domains App Feedback - iOS (%@)"; // Wallets list "MANAGE_ICLOUD_BACKUPS" = "Manage iCloud backups"; @@ -1118,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, 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 f77c0ba1c..8cd4116a2 100644 --- a/unstoppable-ios-app/domains-manager-iosTests/FB_UD_MPCConnectionServiceTests.swift +++ b/unstoppable-ios-app/domains-manager-iosTests/FB_UD_MPCConnectionServiceTests.swift @@ -23,7 +23,7 @@ final class FB_UD_MPCConnectionServiceTests: BaseTestClass, FB_UD_MPC.Fireblocks override func setUp() async throws { try await super.setUp() - let metadata = FB_UD_MPC.UDWalletMetadata(deviceId: deviceId).jsonData() + let metadata = FB_UD_MPC.UDWalletMetadata(email: "", deviceId: deviceId).jsonData() mpcMetadata = MPCWalletMetadata(provider: .fireblocksUD, metadata: metadata) connector = MockFireblocksConnector() @@ -163,7 +163,8 @@ private struct MPCEntitiesBuilder { let asset = createAccountAsset() let accountWithAsset = FB_UD_MPC.WalletAccountWithAssets(account: account, assets: [asset]) - return .init(deviceId: deviceId, + return .init(email: "", + deviceId: deviceId, firstAccount: accountWithAsset, accounts: [accountWithAsset]) } } diff --git a/unstoppable-ios-app/domains-manager-iosTests/Helpers/TestableMPCWalletsService.swift b/unstoppable-ios-app/domains-manager-iosTests/Helpers/TestableMPCWalletsService.swift index 8f02d1aa5..e29a4c203 100644 --- a/unstoppable-ios-app/domains-manager-iosTests/Helpers/TestableMPCWalletsService.swift +++ b/unstoppable-ios-app/domains-manager-iosTests/Helpers/TestableMPCWalletsService.swift @@ -33,7 +33,7 @@ final class TestableMPCWalletsService: MPCWalletsServiceProtocol { } - func setupMPCWalletWith(code: String, recoveryPhrase: String) -> AsyncThrowingStream { + func setupMPCWalletWith(code: String, credentials: MPCActivateCredentials) -> AsyncThrowingStream { AsyncThrowingStream { continuation in continuation.finish() } diff --git a/unstoppable-ios-app/domains-manager-iosTests/RequestsLimitControllerTests.swift b/unstoppable-ios-app/domains-manager-iosTests/RequestsLimitControllerTests.swift new file mode 100644 index 000000000..31f290f81 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-iosTests/RequestsLimitControllerTests.swift @@ -0,0 +1,47 @@ +// +// RequestsLimitController.swift +// domains-manager-iosTests +// +// Created by Oleg Kuplin on 24.05.2024. +// + +@testable import domains_manager_ios +import XCTest + +final class RequestsLimitControllerTests: XCTestCase { + let requestLimit: Int = 5 + let timeInterval: TimeInterval = 1.0 // 1 second for testing + var requestsLimitController: RequestsLimitController! + + override func setUp() { + super.setUp() + requestsLimitController = RequestsLimitController(requestLimit: requestLimit, timeInterval: timeInterval) + } + + func testRateLimiterExceedingLimit() async { + for _ in 0..= timeInterval, "Exceeded limit should wait for the time interval to pass") + } + + func testRateLimiterClearsOldTimestamps() async { + for _ in 0..