diff --git a/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj b/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj index e95db20c4..09aa7214e 100644 --- a/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj +++ b/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj @@ -912,6 +912,10 @@ C650F8BD2BA4182900BDA099 /* FB_UD_MPCConnectedWalletDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = C650F8BC2BA4182900BDA099 /* FB_UD_MPCConnectedWalletDetails.swift */; }; C650F8C42BA41A2A00BDA099 /* FireblocksKeyStorageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C650F8C32BA41A2A00BDA099 /* FireblocksKeyStorageProvider.swift */; }; C650F8C82BA41C3300BDA099 /* FireblocksRPCMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C650F8C72BA41C3300BDA099 /* FireblocksRPCMessageHandler.swift */; }; + C651C6DB2C66016C0076F631 /* PurchaseDomainsOrderSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C651C6DA2C66016C0076F631 /* PurchaseDomainsOrderSummaryView.swift */; }; + C651C6DC2C66016C0076F631 /* PurchaseDomainsOrderSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C651C6DA2C66016C0076F631 /* PurchaseDomainsOrderSummaryView.swift */; }; + C651C6DE2C6609D00076F631 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C651C6DD2C6609D00076F631 /* ToastView.swift */; }; + C651C6DF2C6609D00076F631 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C651C6DD2C6609D00076F631 /* ToastView.swift */; }; C651DC51286C115400808D4C /* WatchFaceDomainImagePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C651DC50286C115400808D4C /* WatchFaceDomainImagePreviewView.swift */; }; C651DC56286C115900808D4C /* WatchFaceDomainImagePreviewView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C651DC55286C115900808D4C /* WatchFaceDomainImagePreviewView.xib */; }; C652446A2AF8A7A400673CC0 /* WalletConnectNotify in Frameworks */ = {isa = PBXBuildFile; productRef = C65244692AF8A7A400673CC0 /* WalletConnectNotify */; }; @@ -999,11 +1003,9 @@ C655CA992B16E51B00FDA063 /* EasySkeleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C655CA952B16E51B00FDA063 /* EasySkeleton.swift */; }; C655CA9C2B16E5BE00FDA063 /* UDListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C655CA9A2B16E5BE00FDA063 /* UDListItemView.swift */; }; C655CA9D2B16E5BE00FDA063 /* UDToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C655CA9B2B16E5BE00FDA063 /* UDToggleStyle.swift */; }; - C655CAA52B16E82800FDA063 /* PurchaseDomainsSelectDiscountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C655CAA02B16E82700FDA063 /* PurchaseDomainsSelectDiscountsView.swift */; }; C655CAA62B16E82800FDA063 /* PurchaseDomainsEnterDiscountCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C655CAA12B16E82700FDA063 /* PurchaseDomainsEnterDiscountCodeView.swift */; }; C655CAA72B16E82800FDA063 /* PurchaseDomainsCheckoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C655CAA22B16E82700FDA063 /* PurchaseDomainsCheckoutView.swift */; }; C655CAA82B16E82800FDA063 /* PurchaseDomainsSelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C655CAA32B16E82700FDA063 /* PurchaseDomainsSelectWalletView.swift */; }; - C655CAA92B16E82800FDA063 /* PurchaseDomainsEnterZIPCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C655CAA42B16E82700FDA063 /* PurchaseDomainsEnterZIPCodeView.swift */; }; C6568EF92B204E4C0022B598 /* UIImageBridgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6568EF82B204E4C0022B598 /* UIImageBridgeView.swift */; }; C6568EFA2B204E4C0022B598 /* UIImageBridgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6568EF82B204E4C0022B598 /* UIImageBridgeView.swift */; }; C658D44E2B16F47C0057BE12 /* FirebaseAuthTokenStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C658D44D2B16F47C0057BE12 /* FirebaseAuthTokenStorage.swift */; }; @@ -2060,8 +2062,6 @@ C6D5DAF42837866800379C38 /* SecondaryDangerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D5DAF32837866800379C38 /* SecondaryDangerButton.swift */; }; C6D645732B1D721D00D724AC /* PurchaseDomainsCheckoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C655CAA22B16E82700FDA063 /* PurchaseDomainsCheckoutView.swift */; }; C6D645742B1D721D00D724AC /* PurchaseDomainsSelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C655CAA32B16E82700FDA063 /* PurchaseDomainsSelectWalletView.swift */; }; - C6D645752B1D721D00D724AC /* PurchaseDomainsEnterZIPCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C655CAA42B16E82700FDA063 /* PurchaseDomainsEnterZIPCodeView.swift */; }; - C6D645762B1D721D00D724AC /* PurchaseDomainsSelectDiscountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C655CAA02B16E82700FDA063 /* PurchaseDomainsSelectDiscountsView.swift */; }; C6D645772B1D721D00D724AC /* PurchaseDomainsEnterDiscountCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C655CAA12B16E82700FDA063 /* PurchaseDomainsEnterDiscountCodeView.swift */; }; C6D645782B1D724500D724AC /* DeviceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C628E38127FEE31D0044E408 /* DeviceHelper.swift */; }; C6D6457A2B1D7C2F00D724AC /* PullUpError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D645792B1D7C2F00D724AC /* PullUpError.swift */; }; @@ -3368,6 +3368,8 @@ C650F8BC2BA4182900BDA099 /* FB_UD_MPCConnectedWalletDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FB_UD_MPCConnectedWalletDetails.swift; sourceTree = ""; }; C650F8C32BA41A2A00BDA099 /* FireblocksKeyStorageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireblocksKeyStorageProvider.swift; sourceTree = ""; }; C650F8C72BA41C3300BDA099 /* FireblocksRPCMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireblocksRPCMessageHandler.swift; sourceTree = ""; }; + C651C6DA2C66016C0076F631 /* PurchaseDomainsOrderSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsOrderSummaryView.swift; sourceTree = ""; }; + C651C6DD2C6609D00076F631 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; C651DC50286C115400808D4C /* WatchFaceDomainImagePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchFaceDomainImagePreviewView.swift; sourceTree = ""; }; C651DC55286C115900808D4C /* WatchFaceDomainImagePreviewView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WatchFaceDomainImagePreviewView.xib; sourceTree = ""; }; C652446B2AF8AA2600673CC0 /* WCV2DefaultCryptoProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WCV2DefaultCryptoProvider.swift; sourceTree = ""; }; @@ -3429,11 +3431,9 @@ C655CA952B16E51B00FDA063 /* EasySkeleton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EasySkeleton.swift; sourceTree = ""; }; C655CA9A2B16E5BE00FDA063 /* UDListItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UDListItemView.swift; sourceTree = ""; }; C655CA9B2B16E5BE00FDA063 /* UDToggleStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UDToggleStyle.swift; sourceTree = ""; }; - C655CAA02B16E82700FDA063 /* PurchaseDomainsSelectDiscountsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsSelectDiscountsView.swift; sourceTree = ""; }; C655CAA12B16E82700FDA063 /* PurchaseDomainsEnterDiscountCodeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsEnterDiscountCodeView.swift; sourceTree = ""; }; C655CAA22B16E82700FDA063 /* PurchaseDomainsCheckoutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsCheckoutView.swift; sourceTree = ""; }; C655CAA32B16E82700FDA063 /* PurchaseDomainsSelectWalletView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsSelectWalletView.swift; sourceTree = ""; }; - C655CAA42B16E82700FDA063 /* PurchaseDomainsEnterZIPCodeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsEnterZIPCodeView.swift; sourceTree = ""; }; C6568EF82B204E4C0022B598 /* UIImageBridgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageBridgeView.swift; sourceTree = ""; }; C658D44D2B16F47C0057BE12 /* FirebaseAuthTokenStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseAuthTokenStorage.swift; sourceTree = ""; }; C65955FA287C621100319ACC /* UIImage+Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Colors.swift"; sourceTree = ""; }; @@ -6250,10 +6250,9 @@ isa = PBXGroup; children = ( C655CAA22B16E82700FDA063 /* PurchaseDomainsCheckoutView.swift */, - C655CAA02B16E82700FDA063 /* PurchaseDomainsSelectDiscountsView.swift */, C655CAA12B16E82700FDA063 /* PurchaseDomainsEnterDiscountCodeView.swift */, C655CAA32B16E82700FDA063 /* PurchaseDomainsSelectWalletView.swift */, - C655CAA42B16E82700FDA063 /* PurchaseDomainsEnterZIPCodeView.swift */, + C651C6DA2C66016C0076F631 /* PurchaseDomainsOrderSummaryView.swift */, ); path = Checkout; sourceTree = ""; @@ -7221,6 +7220,7 @@ C6DDF2682B148A31006D1F0B /* PositionObservingView.swift */, C6E856282BBE983900AAFDE4 /* PresentAsModalPreviewView.swift */, C640D61D2C4944AB006B21C3 /* SelectionPopoverView */, + C651C6DD2C6609D00076F631 /* ToastView.swift */, C6DDF2422B147F1A006D1F0B /* UDCollectionListRowButton.swift */, C6DDF2402B147F19006D1F0B /* UDCollectionSectionBackgroundView.swift */, C655CA9A2B16E5BE00FDA063 /* UDListItemView.swift */, @@ -9259,6 +9259,7 @@ C665566A2BF70C0000F0BD7A /* MPCOnboardingPurchaseUDAuthViewController.swift in Sources */, C68F15762BD769830049BFA2 /* MPCWalletStateCardView.swift in Sources */, C6D6463A2B1DC50400D724AC /* AppReviewActionEvent.swift in Sources */, + C651C6DB2C66016C0076F631 /* PurchaseDomainsOrderSummaryView.swift in Sources */, C6109E2F28E5AB310027D5D8 /* AuthentificationServiceProtocol.swift in Sources */, C60982102823B6AC00546392 /* UDWalletsServiceProtocol.swift in Sources */, C6109E2B28E5AB310027D5D8 /* MockAuthentificationService.swift in Sources */, @@ -9294,7 +9295,6 @@ C663538B294319B400EF1DC7 /* ScrollViewOffsetListener.swift in Sources */, C632BE592BA59ECF00C95B2D /* FB_UD_MPCSuccessAuthResponse.swift in Sources */, C628E36327FDDAA00044E408 /* UILabel.swift in Sources */, - C655CAA52B16E82800FDA063 /* PurchaseDomainsSelectDiscountsView.swift in Sources */, C6A232052BEB5B1B0037E093 /* WalletDetails.swift in Sources */, C635AFF328338A080058F14F /* DomainURLActivityItemSource.swift in Sources */, 30CB2D2225C2E98C00203052 /* ApiRequestBuilder.swift in Sources */, @@ -10044,7 +10044,6 @@ C6B65F6F2B550ED5006D1812 /* NFTsAPIRequestBuilder.swift in Sources */, C6D6E4DE2817BF09008C66BB /* OnboardingProtocols.swift in Sources */, C67681312BEC807B0093AFA0 /* UINavigationBarAppearance.swift in Sources */, - C655CAA92B16E82800FDA063 /* PurchaseDomainsEnterZIPCodeView.swift in Sources */, C6D6E59928194AC9008C66BB /* TextTertiaryButton.swift in Sources */, C630BC792B18381500E29318 /* OnboardingHappyEndViewPresenter.swift in Sources */, C6D8FF1E2B82EB740094A21E /* TextMessageRowView.swift in Sources */, @@ -10193,6 +10192,7 @@ 29150FBB2975DA4D00169A1A /* PushSubscriberInfo.swift in Sources */, C666894B28116073002062B4 /* BorderedTableView.swift in Sources */, C6FECF46282D0694008DAA49 /* ResizableRoundedImageView.swift in Sources */, + C651C6DE2C6609D00076F631 /* ToastView.swift in Sources */, C61ECD792A20E8BD00E97D70 /* PushGroupChatMember.swift in Sources */, C6EECE972833A3E400978ED5 /* CoinRecord.swift in Sources */, C64F6C592C50F0FE00D89FEF /* UDMaintenanceModeFeatureFlagTracker.swift in Sources */, @@ -10363,6 +10363,7 @@ C640D61C2C491E13006B21C3 /* SelectionPopoverViewItem.swift in Sources */, C6960C4F2B19989D00B79E28 /* Env.swift in Sources */, C61807D92B19A48F0032E543 /* PreviewAuthentificationService.swift in Sources */, + C651C6DF2C6609D00076F631 /* ToastView.swift in Sources */, C632BE572BA59E8400C95B2D /* FB_UD_MPCSetupTokenResponse.swift in Sources */, C6D6457B2B1D7C3100D724AC /* PullUpError.swift in Sources */, C640F3702C0712A8009EB0F9 /* MPCWalletTakeoverError.swift in Sources */, @@ -10382,6 +10383,7 @@ C618080D2B19AA2F0032E543 /* DomainRecordsServiceProtocol.swift in Sources */, C6D646FF2B1ED7C300D724AC /* MessagingPrivateChatBlockingStatus.swift in Sources */, C6D8FF342B83180A0094A21E /* ChatListViewModel.swift in Sources */, + C651C6DC2C66016C0076F631 /* PurchaseDomainsOrderSummaryView.swift in Sources */, C6D647122B1ED7D000D724AC /* MessagingChatMessageRemoteContentTypeDisplayInfo.swift in Sources */, C6B761DA2BB3CDDE00773943 /* WalletTransactionsServiceProtocol.swift in Sources */, C6C8F8D72B21834A00A9834D /* BuyDomainsWebViewController.swift in Sources */, @@ -11070,7 +11072,6 @@ C6FAED812B8C5B4C00CC1844 /* ChatMentionSuggestionRowView.swift in Sources */, C6C8F8AA2B2182CF00A9834D /* EnterEmailViewPresenter.swift in Sources */, C6D6470E2B1ED7D000D724AC /* MessagingChatMessage.swift in Sources */, - C6D645752B1D721D00D724AC /* PurchaseDomainsEnterZIPCodeView.swift in Sources */, C6D647722B1EE07700D724AC /* CollectionViewHeaderCell.swift in Sources */, C6D647082B1ED7CA00D724AC /* MessagingNewsChannel.swift in Sources */, C6D646AB2B1ED16900D724AC /* DomainProfileTutorialItemPrivacyViewController.swift in Sources */, @@ -11370,7 +11371,6 @@ C6D645B22B1DBBCE00D724AC /* PreviewConnectedAppsImageCache.swift in Sources */, C6A89C5F2B31654E008AB043 /* HotFeatureSuggestionsService.swift in Sources */, C6B761EE2BB3F8C900773943 /* MockEntitiesFabric+Txs.swift in Sources */, - C6D645762B1D721D00D724AC /* PurchaseDomainsSelectDiscountsView.swift in Sources */, C6D646552B1ED10100D724AC /* DomainProfileWeb3WebsiteCell.swift in Sources */, C6D6462E2B1DC31700D724AC /* CollectionDashesHeaderReusableView.swift in Sources */, C64F6C542C4FAF2200D89FEF /* FullMaintenanceModeView.swift in Sources */, diff --git a/unstoppable-ios-app/domains-manager-ios/Entities/Mock/MockEntitiesFabric+Domains.swift b/unstoppable-ios-app/domains-manager-ios/Entities/Mock/MockEntitiesFabric+Domains.swift index 6e49270d7..c1a146fc6 100644 --- a/unstoppable-ios-app/domains-manager-ios/Entities/Mock/MockEntitiesFabric+Domains.swift +++ b/unstoppable-ios-app/domains-manager-ios/Entities/Mock/MockEntitiesFabric+Domains.swift @@ -109,6 +109,24 @@ extension MockEntitiesFabric { static func mockFirebaseDomainsDisplayInfo() -> [FirebaseDomainDisplayInfo] { mockFirebaseDomains().map { FirebaseDomainDisplayInfo(firebaseDomain: $0) } } + + static func mockDomainsToPurchase() -> [DomainToPurchase] { + [DomainToPurchase(name: "oleg.x", + price: 10000, + metadata: nil, + isTaken: false, + isAbleToPurchase: true), + DomainToPurchase(name: "oleg.com", + price: 10000, + metadata: nil, + isTaken: false, + isAbleToPurchase: true), + DomainToPurchase(name: "oleg.eth", + price: 10000, + metadata: nil, + isTaken: false, + isAbleToPurchase: true)] + } } } diff --git a/unstoppable-ios-app/domains-manager-ios/Entities/Toast.swift b/unstoppable-ios-app/domains-manager-ios/Entities/Toast.swift index 2cd3515f0..65daa5421 100644 --- a/unstoppable-ios-app/domains-manager-ios/Entities/Toast.swift +++ b/unstoppable-ios-app/domains-manager-ios/Entities/Toast.swift @@ -25,6 +25,8 @@ enum Toast: Hashable { case communityProfileEnabled case purchaseDomainsDiscountApplied(Int) case followedProfileAs(DomainName) + case domainRemoved + case cartCleared var message: String { switch self { @@ -68,12 +70,16 @@ enum Toast: Hashable { return String.Constants.discountAppliedToastMessage.localized(formatCartPrice(discount)) case .followedProfileAs(let domainName): return String.Constants.followedAsX.localized(domainName) + case .domainRemoved: + return String.Constants.domainRemoved.localized() + case .cartCleared: + return String.Constants.cartCleared.localized() } } var secondaryMessage: String? { switch self { - case .walletAddressCopied, .walletAdded, .iCloudBackupRestored, .walletRemoved, .walletDisconnected, .noInternetConnection, .changesConfirmed, .mintingSuccessful, .mintingUnavailable, .updatingRecords, .domainCopied, .failedToRefreshBadges, .itemSaved, .itemCopied, .userLoggedOut, .communityProfileEnabled, .purchaseDomainsDiscountApplied, .followedProfileAs: + case .walletAddressCopied, .walletAdded, .iCloudBackupRestored, .walletRemoved, .walletDisconnected, .noInternetConnection, .changesConfirmed, .mintingSuccessful, .mintingUnavailable, .updatingRecords, .domainCopied, .failedToRefreshBadges, .itemSaved, .itemCopied, .userLoggedOut, .communityProfileEnabled, .purchaseDomainsDiscountApplied, .followedProfileAs, .domainRemoved, .cartCleared: return nil case .failedToFetchDomainProfileData: return String.Constants.refresh.localized() @@ -84,7 +90,7 @@ enum Toast: Hashable { var style: Style { switch self { - case .walletAddressCopied, .walletAdded, .iCloudBackupRestored, .walletRemoved, .walletDisconnected, .changesConfirmed, .mintingSuccessful, .domainCopied, .itemSaved, .itemCopied, .userLoggedOut, .communityProfileEnabled, .purchaseDomainsDiscountApplied, .followedProfileAs: + case .walletAddressCopied, .walletAdded, .iCloudBackupRestored, .walletRemoved, .walletDisconnected, .changesConfirmed, .mintingSuccessful, .domainCopied, .itemSaved, .itemCopied, .userLoggedOut, .communityProfileEnabled, .purchaseDomainsDiscountApplied, .followedProfileAs, .domainRemoved, .cartCleared: return .success case .noInternetConnection, .updatingRecords, .mintingUnavailable, .failedToFetchDomainProfileData, .failedToUpdateProfile: return .dark @@ -95,7 +101,7 @@ enum Toast: Hashable { var image: UIImage { switch self { - case .walletAddressCopied, .walletAdded, .iCloudBackupRestored, .walletRemoved, .walletDisconnected, .changesConfirmed, .mintingSuccessful, .domainCopied, .itemSaved, .itemCopied, .userLoggedOut, .communityProfileEnabled, .purchaseDomainsDiscountApplied, .followedProfileAs: + case .walletAddressCopied, .walletAdded, .iCloudBackupRestored, .walletRemoved, .walletDisconnected, .changesConfirmed, .mintingSuccessful, .domainCopied, .itemSaved, .itemCopied, .userLoggedOut, .communityProfileEnabled, .purchaseDomainsDiscountApplied, .followedProfileAs, .domainRemoved, .cartCleared: return .checkCircleWhite case .noInternetConnection: return .connectionOffIcon @@ -152,6 +158,10 @@ enum Toast: Hashable { return true case (.followedProfileAs, .followedProfileAs): return true + case (.domainRemoved, .domainRemoved): + return true + case (.cartCleared, .cartCleared): + return true default: return false } diff --git a/unstoppable-ios-app/domains-manager-ios/Extensions/Double.swift b/unstoppable-ios-app/domains-manager-ios/Extensions/Double.swift index d6ba6da02..f48b2738f 100644 --- a/unstoppable-ios-app/domains-manager-ios/Extensions/Double.swift +++ b/unstoppable-ios-app/domains-manager-ios/Extensions/Double.swift @@ -25,6 +25,12 @@ extension Double { return (self * multiplier).rounded() / multiplier } + func formattedBalance() -> String { + formatted(toMaxNumberAfterComa: 2) + } +} + +extension Numeric where Self: LosslessStringConvertible { func formatted(toMaxNumberAfterComa maxNumberAfterComa: Int, minNumberAfterComa: Int = 2) -> String { @@ -47,10 +53,6 @@ extension Double { formatter.minimumFractionDigits = minNumberAfterComa formatter.roundingMode = .halfEven - return formatter.string(from: self as NSNumber) ?? "0.0" - } - - func formattedBalance() -> String { - formatted(toMaxNumberAfterComa: 2) + return formatter.string(from: self as! NSNumber) ?? "0.0" } } 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 150745b8b..17c246304 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 @@ -1037,6 +1037,7 @@ extension String { static let mintTo = "MINT_TO" static let applyDiscounts = "APPLY_DISCOUNTS" static let addDiscountCode = "ADD_DISCOUNT_CODE" + static let discountCodeApplied = "DISCOUNT_CODE_APPLIED" static let promoCredits = "PROMO_CREDITS" static let storeCredits = "STORE_CREDITS" static let usZIPCode = "US_ZIP_CODE" @@ -1089,6 +1090,21 @@ extension String { static let startTyping = "START_TYPING" static let buyDomainsSearchResultShowMoreTitle = "BUY_DOMAINS_SEARCH_RESULT_SHOW_MORE_TITLE" static let buyDomainsSearchResultShowLessTitle = "BUY_DOMAINS_SEARCH_RESULT_SHOW_LESS_TITLE" + static let purchaseMintingWalletTitle = "PURCHASE_MINTING_WALLET_TITLE" + static let purchaseMintingWalletPullUpTitle = "PURCHASE_MINTING_WALLET_PULL_UP_TITLE" + static let purchaseMintingWalletPullUpSubtitle = "PURCHASE_MINTING_WALLET_PULL_UP_SUBTITLE" + static let subtotal = "SUBTOTAL" + static let country = "COUNTRY" + static let usa = "USA" + static let other = "OTHER" + static let zipCodeForSalesTax = "ZIP_CODE_FOR_SALES_TAX" + static let domainRemoved = "DOMAIN_REMOVED" + static let undo = "UNDO" + static let cartCleared = "CART_CLEARED" + static let buyDomainFromWebPullUpTitle = "BUY_DOMAIN_FROM_WEB_PULL_UP_TITLE" + static let buyDomainFromWebPullUpSubtitle = "BUY_DOMAIN_FROM_WEB_PULL_UP_SUBTITLE" + static let checkoutFromWebPullUpTitle = "CHECKOUT_FROM_WEB_PULL_UP_TITLE" + static let checkoutFromWebPullUpSubtitle = "CHECKOUT_FROM_WEB_PULL_UP_SUBTITLE" // Home static let homeWalletTokensComeTitle = "HOME_WALLET_TOKENS_COME_TITLE" diff --git a/unstoppable-ios-app/domains-manager-ios/Extensions/UIImage.swift b/unstoppable-ios-app/domains-manager-ios/Extensions/UIImage.swift index 600af0c00..5526f8921 100644 --- a/unstoppable-ios-app/domains-manager-ios/Extensions/UIImage.swift +++ b/unstoppable-ios-app/domains-manager-ios/Extensions/UIImage.swift @@ -178,7 +178,10 @@ extension UIImage { static let backupICloud = UIImage(named: "backupICloud")! static let paperPlaneTopRightSend = UIImage(named: "paperPlaneTopRightSend")! static let gas = UIImage(named: "gas")! - + static let unsTLDLogo = UIImage(named: "unsTLDLogo")! + static let ensTLDLogo = UIImage(named: "ensTLDLogo")! + static let dnsTLDLogo = UIImage(named: "dnsTLDLogo")! + static let twitterIcon24 = UIImage(named: "twitterIcon24")! static let discordIcon24 = UIImage(named: "discordIcon24")! static let telegramIcon24 = UIImage(named: "telegramIcon24")! diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/ReconnectMPCWalletFlow/MPCEnterCredentialsReconnectView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/ReconnectMPCWalletFlow/MPCEnterCredentialsReconnectView.swift index 5696bb299..56d1dd0d9 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/ReconnectMPCWalletFlow/MPCEnterCredentialsReconnectView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/MPC/Activate/ReconnectMPCWalletFlow/MPCEnterCredentialsReconnectView.swift @@ -23,7 +23,6 @@ struct MPCEnterCredentialsReconnectView: View { // MARK: - Private methods private extension MPCEnterCredentialsReconnectView { func didEnterCredentials(_ credentials: MPCActivateCredentials) { - print(credentials) viewModel.handleAction(.didEnterCredentials(credentials)) } } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsCheckoutView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsCheckoutView.swift index 2910f6418..b12553e32 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsCheckoutView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsCheckoutView.swift @@ -15,46 +15,59 @@ struct PurchaseDomainsCheckoutView: View, ViewAnalyticsLogger { @EnvironmentObject var stateManagerWrapper: NavigationStateManagerWrapper @EnvironmentObject var viewModel: PurchaseDomainsViewModel - @State var domain: DomainToPurchase + @State var domains: [DomainToPurchase] @State var selectedWallet: WalletEntity @State var wallets: [WalletEntity] @State var profileChanges: DomainProfilePendingChanges - - @State private var domainAvatar: UIImage? - @State private var scrollOffset: CGPoint = .zero - @State private var checkoutData: PurchaseDomainsCheckoutData = PurchaseDomainsCheckoutData() + @State private var checkoutData: PurchaseDomainsCheckoutData = PurchaseDomainsCheckoutData() @State private var error: PullUpErrorConfiguration? @State private var pullUp: ViewPullUpConfigurationType? @State private var cartStatus: PurchaseDomainCartStatus = .ready(cart: .empty) @State private var isLoading = false + @State private var zipCode = "" + @State private var isKeyboardActive = false @State private var isSelectWalletPresented = false - @State private var isEnterZIPCodePresented = false - @State private var isSelectDiscountsPresented = false @State private var isEnterDiscountCodePresented = false + @State private var isShowingOrderSummary = false var analyticsName: Analytics.ViewName { .purchaseDomainsCheckout } - var additionalAppearAnalyticParameters: Analytics.EventParameters { [.domainName : domain.name, - .price: String(domain.price)] } + var additionalAppearAnalyticParameters: Analytics.EventParameters { + let totalPrice = domains.reduce(0, { $0 + $1.price }) + let name = domains.prefix(10).map { $0.name }.joined(separator: ",") + return [.domainName : name, + .count: String(domains.count), + .price: String(totalPrice)] + } var body: some View { ZStack { VStack(spacing: 0) { ScrollView { - LazyVStack { - headerView() + LazyVStack(spacing: 0) { + mintToRowView() + .padding(.vertical, 20) checkoutDashSeparator() - detailsSection() + usaZIPCodeView() + .padding(.vertical, 20) checkoutDashSeparator() + discountsView() + .padding(.vertical, 20) summarySection() } + .background(Color.backgroundDefault) } + .background(scrollViewBackgroundView()) + checkoutView() } + if isLoading { ProgressView() } } .allowsHitTesting(!isLoading) + .navigationTitle(title) + .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(isLoading) .background(Color.backgroundDefault) .animation(.default, value: UUID()) @@ -68,26 +81,41 @@ struct PurchaseDomainsCheckoutView: View, ViewAnalyticsLogger { .onReceive(purchaseDomainsPreferencesStorage.$checkoutData.publisher.receive(on: DispatchQueue.main), perform: { checkoutData in self.checkoutData = checkoutData }) + .onReceive(KeyboardService.shared.keyboardOpenedPublisher.receive(on: DispatchQueue.main)) { value in + isKeyboardActive = value + } .modifier(ShowingSelectWallet(isSelectWalletPresented: $isSelectWalletPresented, selectedWallet: selectedWallet, wallets: wallets, analyticsName: analyticsName, selectedWalletCallback: { wallet in warnUserIfNeededAndSelectWallet(wallet) })) - .sheet(isPresented: $isEnterZIPCodePresented, content: { - PurchaseDomainsEnterZIPCodeView() - .passViewAnalyticsDetails(logger: self) - }) .sheet(isPresented: $isEnterDiscountCodePresented, content: { PurchaseDomainsEnterDiscountCodeView() .passViewAnalyticsDetails(logger: self) + .presentationDetents([.medium]) + }) + .sheet(isPresented: $isShowingOrderSummary, content: { + PurchaseDomainsOrderSummaryView(domains: domains, + domainsUpdatedCallback: didUpdateDomainsList) + .passViewAnalyticsDetails(logger: self) }) .toolbar { ToolbarItem(placement: .topBarTrailing) { Color.clear } + ToolbarItem(placement: .keyboard) { + if !isEnterDiscountCodePresented { + UDButtonView(text: String.Constants.doneButtonTitle.localized(), + style: .large(.raisedPrimary)) { + KeyboardService.shared.hideKeyboard() + let zipCode = self.zipCode.trimmedSpaces + purchaseDomainsPreferencesStorage.checkoutData.usaZipCode = zipCode + } + .disabled(!zipCode.isEmpty && !isValidUSStateZipCode(zipCode)) + } + } } .pullUpError($error) - .modifier(ShowingSelectDiscounts(isSelectDiscountsPresented: $isSelectDiscountsPresented)) .viewPullUp($pullUp) .onAppear(perform: onAppear) } @@ -95,82 +123,102 @@ struct PurchaseDomainsCheckoutView: View, ViewAnalyticsLogger { // MARK: - Details section private extension PurchaseDomainsCheckoutView { - @ViewBuilder - func headerView() -> some View { - VStack(spacing: 32) { - Text(String.Constants.checkout.localized()) - .titleText() - if case .hasUnpaidDomains = cartStatus { - topWarningViewWith(message: .hasUnpaidDomains) { - logButtonPressedAnalyticEvents(button: .openUnpaidDomainsInfo) - openLinkExternally(.unstoppableDomainSearch(searchKey: domain.name)) - } - } + func didUpdateDomainsList(_ newDomains: [DomainToPurchase]) { + guard !newDomains.isEmpty else { + viewModel.handleAction(.didRemoveAllDomainsFromTheCart) + return + } + + self.domains = newDomains + Task { + setLoading(true) + try? await purchaseDomainsService.setDomainsToPurchase(newDomains) + setLoading(false) } - .padding(.horizontal, 16) } - @ViewBuilder - func topWarningViewWith(message: TopMessageDescription, callback: @escaping MainActorCallback) -> some View { - Button { - Task { @MainActor in - callback() - } - } label: { - HStack(spacing: 8) { - Image.infoIcon - .resizable() - .squareFrame(20) - .foregroundStyle(Color.foregroundDanger) - AttributedText(attributesList: .init(text: message.message, - font: .currentFont(withSize: 16, weight: .medium), - textColor: .foregroundDanger, - alignment: .left), - updatedAttributesList: [.init(text: message.highlightedMessage, - textColor: .foregroundAccent)]) - } - .frame(height: 48) + func isValidUSStateZipCode(_ zipCode: String) -> Bool { + let numericZipCode = zipCode.replacingOccurrences(of: "-", with: "") + + if let zipInt = Int(numericZipCode), + zipInt >= 501 && zipInt <= 99950 { // Valid zip code range for the entire USA is 00501 to 99950 + return true } + + return false + } + + + @ViewBuilder + func scrollViewBackgroundView() -> some View { + LinearGradient( + gradient: Gradient(stops: [ + .init(color: .backgroundDefault, location: 0.0), + .init(color: .backgroundDefault, location: 0.5), + .init(color: .backgroundOverlay, location: 0.5), + .init(color: .backgroundOverlay, location: 1.0) + ]), + startPoint: .top, + endPoint: .bottom + ) } @ViewBuilder - func detailsSection() -> some View { - UDCollectionSectionBackgroundView { - VStack(alignment: .center, spacing: 0) { - mintToRowView() - usaZIPCodeView() - discountView() + func mintToRowView() -> some View { + VStack(alignment: .leading) { + HStack(spacing: 16) { + Image.walletExternalIcon + .resizable() + .foregroundStyle(Color.foregroundSecondary) + .squareFrame(24) + .padding(.vertical, 10) + HStack(spacing: 8) { + Text(String.Constants.mintTo.localized()) + .textAttributes(color: .foregroundDefault, + fontSize: 16, + fontWeight: .medium) + Spacer() + selectWalletButton() + } } - .padding(EdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)) } - .padding() + .padding(.horizontal, 16) } @ViewBuilder - func mintToRowView() -> some View { - UDCollectionListRowButton(content: { - UDListItemView(title: String.Constants.mintTo.localized(), - value: selectedWalletName, - imageType: .image(.vaultIcon), - rightViewStyle: walletSelectionIndicatorStyle) - .udListItemInCollectionButtonPadding() - }, callback: { + func selectWalletButton() -> some View { + Button { + UDVibration.buttonTap.vibrate() + if !canSelectWallet, - isFailedToAuthWallet { + isFailedToAuthWallet { warnUserIfNeededAndSelectWallet(selectedWallet, forceReload: true) } else { logButtonPressedAnalyticEvents(button: .selectWallet) isSelectWalletPresented = true } - }) + } label: { + HStack(spacing: 8) { + Text(selectedWalletName) + .textAttributes(color: .foregroundSecondary, + fontSize: 16) + .lineLimit(1) + if let walletSelectionIndicatorImage { + walletSelectionIndicatorImage.resizable() + .squareFrame(24) + .foregroundStyle(Color.foregroundSecondary) + } + } + } + .buttonStyle(.plain) .allowsHitTesting(canSelectWallet || isFailedToAuthWallet) } - var walletSelectionIndicatorStyle: UDListItemView.RightViewStyle? { + var walletSelectionIndicatorImage: Image? { if case .failedToAuthoriseWallet = cartStatus { - return .errorCircle + return .infoIcon } - return canSelectWallet ? .chevron : nil + return canSelectWallet ? .chevronGrabberVertical : nil } var canSelectWallet: Bool { @@ -185,23 +233,56 @@ private extension PurchaseDomainsCheckoutView { } var selectedWalletName: String { - selectedWallet.displayName + let displayInfo = selectedWallet.displayInfo + let address = displayInfo.address.walletAddressTruncated + if displayInfo.isNameSet { + return "\(displayInfo.name) (\(address))" + } + return address } @ViewBuilder func usaZIPCodeView() -> some View { - UDCollectionListRowButton(content: { - UDListItemView(title: String.Constants.zipCode.localized(), - subtitle: String.Constants.toCalculateTaxes.localized(), - value: usaZipCodeValue, - imageType: .image(.usaFlagIcon), - imageStyle: .centred(offset: EdgeInsets(top: 6, leading: 8, bottom: 6, trailing: 8)), - rightViewStyle: .chevron) - .udListItemInCollectionButtonPadding() - }, callback: { - logButtonPressedAnalyticEvents(button: .enterUSZIPCode) - isEnterZIPCodePresented = true - }) + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 16) { + Image.planetIcon20 + .resizable() + .foregroundStyle(Color.foregroundSecondary) + .squareFrame(24) + .padding(.vertical, 10) + HStack(spacing: 8) { + VStack(alignment: .leading, spacing: 0) { + Text(String.Constants.country.localized()) + .textAttributes(color: .foregroundDefault, + fontSize: 16, + fontWeight: .medium) + .frame(height: 24) + Text(String.Constants.toCalculateTaxes.localized()) + .textAttributes(color: .foregroundSecondary, + fontSize: 14) + .frame(height: 20) + } + Spacer() + selectCountryPicker() + } + } + + if case .usa = checkoutData.purchaseLocation { + UDTextFieldView(text: $zipCode, + placeholder: "", + hint: String.Constants.zipCodeForSalesTax.localized(), + focusBehaviour: checkoutData.usaZipCode.isEmpty ? .activateOnAppear : .default, + keyboardType: .numberPad) + } + } + .padding(.horizontal, 16) + } + + @ViewBuilder + func selectCountryPicker() -> some View { + UDSegmentedControlView(selection: purchaseDomainsPreferencesStorage.$checkoutData.binding.purchaseLocation, + items: PurchaseDomainsCheckoutData.UserPurchaseLocation.allCases) + .frame(width: 157) } var usaZipCodeValue: String { @@ -212,22 +293,130 @@ private extension PurchaseDomainsCheckoutView { } } + var discountViewTitle: String { + if appliedDiscountsSum != nil { + return String.Constants.discountCodeApplied.localized() + } + return String.Constants.addDiscountCode.localized() + } + @ViewBuilder - func discountView() -> some View { - UDCollectionListRowButton(content: { - UDListItemView(title: String.Constants.creditsAndDiscounts.localized(), - value: discountValueString, - imageType: .image(.tagsCashIcon), - rightViewStyle: .chevron) - .udListItemInCollectionButtonPadding() - }, callback: { - if cartStatus.storeCreditsAvailable == 0 && cartStatus.promoCreditsAvailable == 0 { - logButtonPressedAnalyticEvents(button: .creditsAndDiscounts) + func discountsView() -> some View { + VStack(spacing: 8) { + otherDiscountsView() + promoCreditsDiscountView() + storeCreditsDiscountView() + } + } + + @ViewBuilder + func otherDiscountsView() -> some View { + Button { + logButtonPressedAnalyticEvents(button: .creditsAndDiscounts) + if checkoutData.discountCode.isEmpty { isEnterDiscountCodePresented = true } else { - isSelectDiscountsPresented = true + purchaseDomainsPreferencesStorage.checkoutData.discountCode = "" } - }) + } label: { + otherDiscountsLabelView() + .padding(.horizontal, 16) + } + .buttonStyle(.plain) + } + + @ViewBuilder + func otherDiscountsLabelView() -> some View { + HStack(spacing: 16) { + Image.tagIcon + .resizable() + .foregroundStyle(Color.foregroundSecondary) + .squareFrame(24) + .padding(.vertical, 10) + .rotationEffect(.degrees(-90)) + HStack(spacing: 8) { + VStack(alignment: .leading, spacing: 0) { + Text(discountViewTitle) + .textAttributes(color: .foregroundDefault, + fontSize: 16, + fontWeight: .medium) + .frame(height: 24) + if let discountCode = checkoutData.discountCodeIfEntered { + Text(discountCode) + .textAttributes(color: .foregroundSecondary, + fontSize: 14) + .frame(height: 20) + } + } + Spacer() + + HStack(spacing: 8) { + if cartStatus.otherDiscountsApplied > 0 { + Text("-\(formatCartPrice(cartStatus.otherDiscountsApplied))") + .textAttributes(color: .foregroundSecondary, + fontSize: 16) + } + discountRowTrailingIcon + .resizable() + .foregroundStyle(Color.foregroundSecondary) + .squareFrame(24) + } + } + } + } + + + @ViewBuilder + func promoCreditsDiscountView() -> some View { + if cartStatus.promoCreditsAvailable > 0 { + specificDiscountInfoRow(icon: .ticketIcon, + title: String.Constants.promoCredits.localized(), + value: cartStatus.promoCreditsAvailable) + } + } + + @ViewBuilder + func storeCreditsDiscountView() -> some View { + if cartStatus.storeCreditsAvailable > 0 { + specificDiscountInfoRow(icon: .starInCloudIcon, + title: String.Constants.storeCredits.localized(), + value: cartStatus.storeCreditsAvailable) + } + } + + @ViewBuilder + func specificDiscountInfoRow(icon: Image, + title: String, + value: Int) -> some View { + HStack(spacing: 16) { + icon + .resizable() + .foregroundStyle(Color.foregroundSecondary) + .squareFrame(24) + .padding(.vertical, 10) + + HStack(spacing: 8) { + Text(title) + .textAttributes(color: .foregroundDefault, + fontSize: 16, + fontWeight: .medium) + .frame(height: 24) + Spacer() + + Text("-\(formatCartPrice(value))") + .textAttributes(color: .foregroundSecondary, + fontSize: 16) + } + } + .padding(.horizontal, 16) + .padding(.trailing, cartStatus.otherDiscountsApplied > 0 ? 32 : 0) + } + + var discountRowTrailingIcon: Image { + if checkoutData.discountCode.isEmpty { + return .chevronRight + } + return .trashIcon } var discountValueString: String { @@ -248,13 +437,9 @@ private extension PurchaseDomainsCheckoutView { } @ViewBuilder - func checkoutDashSeparator() -> some View { - Line() - .stroke(style: StrokeStyle(lineWidth: 1, dash: [3])) - .foregroundColor(.black) - .opacity(0.06) - .frame(height: 1) - .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) + func checkoutDashSeparator(padding: CGFloat = 16) -> some View { + HomeExploreSeparatorView() + .padding(.horizontal, padding) } } @@ -264,55 +449,88 @@ private extension PurchaseDomainsCheckoutView { @ViewBuilder func summarySection() -> some View { LazyVStack(alignment: .leading, spacing: 16) { - Text(String.Constants.orderSummary.localized()) - .font(.currentFont(size: 20, weight: .bold)) - .foregroundStyle(Color.foregroundDefault) - UDCollectionSectionBackgroundView(backgroundColor: .backgroundSubtle) { - VStack(alignment: .center, spacing: 16) { - summaryDomainInfoView() - .padding(EdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)) - additionalCheckoutDetailsView() - .padding(EdgeInsets(top: 0, - leading: 16, - bottom: shouldShowTotalDueInSummary ? 0 : 16, - trailing: 16)) - if shouldShowTotalDueInSummary { - checkoutDashSeparator() - totalDueView() - .padding(EdgeInsets(top: 0, leading: 16, bottom: 16, trailing: 16)) - } + summarySectionHeader() + summaryDomainInfoView() + checkoutDashSeparator(padding: 0) + additionalCheckoutDetailsView() + totalDueView() + } + .padding(16) + .background(Color.backgroundOverlay) + .overlay(alignment: .top, content: { + Line() + .stroke(Color.borderDefault, lineWidth: 1.0) + }) + } + + @ViewBuilder + func summarySectionHeader() -> some View { + HStack { + Text(String.Constants.orderSummary.localized() + " (\(domains.count))") + .textAttributes(color: .foregroundDefault, + fontSize: 16, + fontWeight: .medium) + Spacer() + + if !isLoading, + case .ready = cartStatus { + Button { + UDVibration.buttonTap.vibrate() + isShowingOrderSummary = true + } label: { + Text(String.Constants.editButtonTitle.localized()) + .textAttributes(color: .foregroundAccent, + fontSize: 16, + fontWeight: .medium) + .underline() } } } - .padding() } - var avatarImage: UDListItemView.ImageType { - if let domainAvatar { - return .uiImage(domainAvatar) + @ViewBuilder + func summaryDomainInfoView() -> some View { + LazyVStack(spacing: 16) { + ForEach(domains) { domain in + domainInfoRowView(domain) + } } - return .image(.domainSharePlaceholder) } @ViewBuilder - func summaryDomainInfoView() -> some View { - ZStack { - RoundedRectangle(cornerRadius: 12) - .fill(Color.backgroundOverlay) - UDListItemView(title: domain.name, - value: formatCartPrice(domain.price), - imageType: avatarImage, - imageStyle: .full) - .padding(EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12)) + func domainInfoRowView(_ domain: DomainToPurchase) -> some View { + HStack(spacing: 16) { + HStack(spacing: 16) { + domain.tldCategory.icon + .squareFrame(24) + .foregroundStyle(Color.foregroundSecondary) + + HStack(spacing: 8) { + VStack(spacing: 0) { + Text(domain.name) + .textAttributes(color: .foregroundDefault, + fontSize: 16, + fontWeight: .medium) + .frame(height: 24) + } + Spacer() + Text(formatCartPrice(domain.price)) + .textAttributes(color: .foregroundSecondary, + fontSize: 16) + } + } } + .frame(height: 44) } @ViewBuilder func additionalCheckoutDetailsView() -> some View { if hasAdditionalCheckoutData { VStack(spacing: 8) { + additionalCheckoutDetailsRow(title: String.Constants.subtotal.localized(), value: formatCartPrice(cartStatus.subtotalPrice)) + if appliedDiscountsSum != nil { - additionalCheckoutDetailsRow(title: String.Constants.creditsAndDiscounts.localized(), value: discountValueString) + additionalCheckoutDetailsRow(title: String.Constants.discounts.localized(), value: discountValueString) } if cartStatus.taxes > 0 { additionalCheckoutDetailsRow(title: String.Constants.taxes.localized(), value: formatCartPrice(cartStatus.taxes)) @@ -345,11 +563,25 @@ private extension PurchaseDomainsCheckoutView { return false } + var title: String { + let totalDue = String.Constants.totalDue.localized() + switch cartStatus { + case .ready(let cart): + let price = formatCartPrice(cart.totalPrice) + return "\(totalDue): \(price)" + default: + return totalDue + } + } + @ViewBuilder func totalDueView() -> some View { HStack { VStack(alignment: .leading) { Text(String.Constants.totalDue.localized()) + .textAttributes(color: .foregroundDefault, + fontSize: 16, + fontWeight: .medium) if failedToLoadCalculations { HStack { Image.infoIcon @@ -380,13 +612,14 @@ private extension PurchaseDomainsCheckoutView { switch cartStatus { case .ready(let cart): Text(formatCartPrice(cart.totalPrice)) + .textAttributes(color: .foregroundDefault, + fontSize: 16, + fontWeight: .medium) default: Text("-") } } } - .font(.currentFont(size: 16, weight: .medium)) - .foregroundStyle(Color.foregroundDefault) } } @@ -394,12 +627,11 @@ private extension PurchaseDomainsCheckoutView { private extension PurchaseDomainsCheckoutView { @ViewBuilder func checkoutView() -> some View { - VStack(spacing: 0) { - if !shouldShowTotalDueInSummary { - totalDueView() - .padding(EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)) + if !isKeyboardActive { + VStack(spacing: 0) { + checkoutButton() } - checkoutButton() + .background(Color.backgroundOverlay) } } @@ -432,10 +664,6 @@ private extension PurchaseDomainsCheckoutView { .padding() } - var shouldShowTotalDueInSummary: Bool { - true - } - var isPayButtonDisabled: Bool { if case .ready = cartStatus { return false @@ -449,24 +677,13 @@ private extension PurchaseDomainsCheckoutView { var isApplePaySupported: Bool { purchaseDomainsService.isApplePaySupported } func onAppear() { checkoutData = purchaseDomainsPreferencesStorage.checkoutData + zipCode = checkoutData.usaZipCode warnUserIfNeededAndSelectWallet(selectedWallet, forceReload: true) - setDomainAvatar() if !isApplePaySupported { logAnalytic(event: .applePayNotSupported) } } - func setDomainAvatar() { - Task { - if let imageData = profileChanges.avatarData, - let avatarImage = await UIImage.createWith(anyData: imageData) { - domainAvatar = avatarImage - } else { - domainAvatar = await appContext.imageLoadingService.loadImage(from: .initials(domain.name, size: .default, style: .accent), downsampleDescription: nil) - } - } - } - func warnUserIfNeededAndSelectWallet(_ wallet: WalletEntity, forceReload: Bool = false) { switch wallet.displayInfo.source { case .external(let name, let walletMake): @@ -488,7 +705,7 @@ private extension PurchaseDomainsCheckoutView { setLoading(true) do { try await purchaseDomainsService.authoriseWithWallet(wallet.udWallet, - toPurchaseDomains: [domain]) + toPurchaseDomains: domains) } catch { Debugger.printFailure("Did fail to authorise wallet \(wallet.address) with error \(error)") } @@ -512,12 +729,11 @@ private extension PurchaseDomainsCheckoutView { parameters: [.value : String(totalPrice), .count: String(1), .isApplePaySupported: String(isApplePaySupported)]) - let pendingPurchasedDomain = PendingPurchasedDomain(name: domain.name, - walletAddress: walletToMint.address) - PurchasedDomainsStorage.setPurchasedDomains([pendingPurchasedDomain]) + let pendingPurchasedDomains: [PendingPurchasedDomain] = domains.map { PendingPurchasedDomain(name: $0.name, walletAddress: walletToMint.address) } + PurchasedDomainsStorage.setPurchasedDomains(pendingPurchasedDomains) PurchasedDomainsStorage.addPendingNonEmptyProfiles([profileChanges]) - await walletsDataService.didPurchaseDomains([pendingPurchasedDomain], + await walletsDataService.didPurchaseDomains(pendingPurchasedDomains, pendingProfiles: [profileChanges]) Task.detached { // Run in background try? await walletsDataService.refreshDataForWallet(selectedWallet) @@ -578,44 +794,10 @@ private extension PurchaseDomainsCheckoutView { let name: String let icon: UIImage } - - enum TopMessageDescription { - case hasUnpaidDomains, applePayNotSupported - - var message: String { - switch self { - case .hasUnpaidDomains: - return String.Constants.purchaseHasUnpaidVaultDomainsErrorMessage.localized() - case .applePayNotSupported: - return String.Constants.purchaseApplePayNotSupportedErrorMessage.localized() - } - } - - var highlightedMessage: String { - switch self { - case .hasUnpaidDomains: - return String.Constants.purchaseHasUnpaidVaultDomainsErrorMessageHighlighted.localized() - case .applePayNotSupported: - return String.Constants.purchaseApplePayNotSupportedErrorMessageHighlighted.localized() - } - } - } } // MARK: - Private methods private extension PurchaseDomainsCheckoutView { - struct ShowingSelectDiscounts: ViewModifier { - @Binding var isSelectDiscountsPresented: Bool - - func body(content: Content) -> some View { - content - .sheet(isPresented: $isSelectDiscountsPresented, content: { - PurchaseDomainsSelectDiscountsView() - .presentationDetents([.medium]) - }) - } - } - struct ShowingSelectWallet: ViewModifier { @Binding var isSelectWalletPresented: Bool let selectedWallet: WalletEntity @@ -682,14 +864,19 @@ private extension PullUpErrorConfiguration { } #Preview { - PurchaseDomainsCheckoutView(domain: .init(name: "oleg.x", - price: 10000, - metadata: nil, - isTaken: false, - isAbleToPurchase: true), - selectedWallet: MockEntitiesFabric.Wallet.mockEntities()[0], - wallets: Array(MockEntitiesFabric.Wallet.mockEntities().prefix(4)), - profileChanges: .init(domainName: "oleg.x", - avatarData: UIImage.Preview.previewLandscape?.dataToUpload)) - .environment(\.purchaseDomainsService, MockFirebaseInteractionsService()) + let router = MockEntitiesFabric.Home.createHomeTabRouter() + let viewModel = PurchaseDomainsViewModel(router: router) + let stateWrapper = NavigationStateManagerWrapper() + + return NavigationStack { + PurchaseDomainsCheckoutView(domains: MockEntitiesFabric.Domains.mockDomainsToPurchase(), + selectedWallet: MockEntitiesFabric.Wallet.mockEntities()[0], + wallets: Array(MockEntitiesFabric.Wallet.mockEntities().prefix(4)), + profileChanges: .init(domainName: "oleg.x", + avatarData: UIImage.Preview.previewLandscape?.dataToUpload)) + .environment(\.purchaseDomainsService, MockFirebaseInteractionsService()) + .environmentObject(stateWrapper) + .environmentObject(router) + .environmentObject(viewModel) + } } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsEnterDiscountCodeView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsEnterDiscountCodeView.swift index d1c97a3a3..edbde5153 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsEnterDiscountCodeView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsEnterDiscountCodeView.swift @@ -24,17 +24,18 @@ struct PurchaseDomainsEnterDiscountCodeView: View, ViewAnalyticsLogger { .foregroundStyle(Color.foregroundDefault) .multilineTextAlignment(.center) UDTextFieldView(text: $value, - placeholder: String.Constants.discountCode.localized(), + placeholder: "", + hint: String.Constants.discountCode.localized(), focusBehaviour: .activateOnAppear, autocapitalization: .characters) - UDButtonView(text: String.Constants.confirm.localized(), style: .large(.raisedPrimary)) { + Spacer() + UDButtonView(text: String.Constants.apply.localized(), style: .large(.raisedPrimary)) { logButtonPressedAnalyticEvents(button: .confirmDiscountCode, parameters: [.value: value.trimmedSpaces]) UDVibration.buttonTap.vibrate() purchaseDomainsPreferencesStorage.checkoutData.discountCode = value.trimmedSpaces presentationMode.wrappedValue.dismiss() enteredCallback?() } - Spacer() } .padding(EdgeInsets(top: 32, leading: 16, bottom: 16, trailing: 16)) .onAppear { diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsEnterZIPCodeView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsEnterZIPCodeView.swift deleted file mode 100644 index cc86010f3..000000000 --- a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsEnterZIPCodeView.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// PurchaseDomainsCheckoutZIPCodeView.swift -// UBTSharing -// -// Created by Oleg Kuplin on 28.11.2023. -// - -import SwiftUI - -struct PurchaseDomainsEnterZIPCodeView: View, ViewAnalyticsLogger { - - @Environment(\.analyticsViewName) private var analyticsViewName - @Environment(\.presentationMode) private var presentationMode - @Environment(\.purchaseDomainsPreferencesStorage) private var purchaseDomainsPreferencesStorage - @State private var value: String = "" - var analyticsName: Analytics.ViewName { analyticsViewName } - - var body: some View { - VStack(spacing: 24) { - Text(String.Constants.enterUSZIPCode.localized()) - .font(.currentFont(size: 22, weight: .bold)) - .foregroundStyle(Color.foregroundDefault) - .multilineTextAlignment(.center) - UDTextFieldView(text: $value, - placeholder: String.Constants.zipCode.localized(), - focusBehaviour: .activateOnAppear, - keyboardType: .numberPad) - UDButtonView(text: String.Constants.confirm.localized(), style: .large(.raisedPrimary)) { - let zipCode = value.trimmedSpaces - logButtonPressedAnalyticEvents(button: .confirmUSZIPCode, parameters: [.value: zipCode]) - UDVibration.buttonTap.vibrate() - purchaseDomainsPreferencesStorage.checkoutData.usaZipCode = zipCode - presentationMode.wrappedValue.dismiss() - } - .disabled(!value.isEmpty && !isValidUSStateZipCode(value)) - Spacer() - } - .padding(EdgeInsets(top: 32, leading: 16, bottom: 16, trailing: 16)) - .onAppear { - value = purchaseDomainsPreferencesStorage.checkoutData.usaZipCode - } - } -} - -// MARK: - Private methods -private extension PurchaseDomainsEnterZIPCodeView { - func isValidUSStateZipCode(_ zipCode: String) -> Bool { - let numericZipCode = zipCode.replacingOccurrences(of: "-", with: "") - - if let zipInt = Int(numericZipCode), - zipInt >= 501 && zipInt <= 99950 { // Valid zip code range for the entire USA is 00501 to 99950 - return true - } - - return false - } -} - -#Preview { - PurchaseDomainsEnterZIPCodeView() -} - diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsOrderSummaryView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsOrderSummaryView.swift new file mode 100644 index 000000000..1f6bf75a7 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsOrderSummaryView.swift @@ -0,0 +1,132 @@ +// +// PurchaseDomainsOrderSummaryView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 09.08.2024. +// + +import SwiftUI + +struct PurchaseDomainsOrderSummaryView: View, ViewAnalyticsLogger { + + @Environment(\.dismiss) private var dismiss + @Environment(\.analyticsViewName) private var analyticsViewName + var analyticsName: Analytics.ViewName { analyticsViewName } + + @State var domains: [DomainToPurchase] + let domainsUpdatedCallback: ([DomainToPurchase])->() + + @State private var removedDomain: RemovedDomainInfo? = nil + @State private var timer: Timer? + + var body: some View { + VStack { + ScrollView { + VStack(spacing: 32) { + headerView() + domainsListView() + } + .padding(.horizontal, 16) + } + .padding(.top, 36) + + if let removedDomain { + ToastView(toast: .changesConfirmed, + action: .init(title: String.Constants.undo.localized(), + callback: { + withAnimation { + undoRemoveDomain(removedDomain) + } + })) + .padding(.bottom, -8) + } + + UDButtonView(text: String.Constants.doneButtonTitle.localized(), + style: .large(.raisedPrimary)) { + logButtonPressedAnalyticEvents(button: .done) + domainsUpdatedCallback(domains) + dismiss() + } + .padding() + } + .presentationDetents([.medium, .large]) + } +} + + +// MARK: - Private methods +private extension PurchaseDomainsOrderSummaryView { + @ViewBuilder + func headerView() -> some View { + HStack(spacing: 4) { + Text(String.Constants.orderSummary.localized() + " (\(domains.count))") + .textAttributes(color: .foregroundDefault, + fontSize: 22, + fontWeight: .bold) + } + } + + @ViewBuilder + func domainsListView() -> some View { + UDCollectionSectionBackgroundView { + LazyVStack(spacing: 4) { + ForEach(domains) { domain in + domainListRow(domain) + .udListItemInCollectionButtonPadding() + } + } + } + } + + @ViewBuilder + func domainListRow(_ domain: DomainToPurchase) -> some View { + Button { + UDVibration.buttonTap.vibrate() + withAnimation { + removeDomain(domain) + } + } label: { + PurchaseDomainsSearchResultRowView(domain: domain, + mode: .cart) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + + func removeDomain(_ domain: DomainToPurchase) { + if let i = domains.firstIndex(of: domain) { + removedDomain = .init(domain: domains[i], + index: i) + domains.remove(at: i) + resetTimer() + } + } + + func undoRemoveDomain(_ removedDomain: RemovedDomainInfo) { + domains.insert(removedDomain.domain, + at: removedDomain.index) + self.removedDomain = nil + } + + struct RemovedDomainInfo { + let domain: DomainToPurchase + let index: Int + } + + private func resetTimer() { + // Invalidate any existing timer + timer?.invalidate() + // Start a new 5-second timer + timer = Timer.scheduledTimer(withTimeInterval: 5.0, + repeats: false) { _ in + withAnimation { + self.removedDomain = nil + } + } + } +} + +#Preview { + PurchaseDomainsOrderSummaryView(domains: MockEntitiesFabric.Domains.mockDomainsToPurchase(), + domainsUpdatedCallback: { _ in }) +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsSelectDiscountsView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsSelectDiscountsView.swift deleted file mode 100644 index df76abeb3..000000000 --- a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsSelectDiscountsView.swift +++ /dev/null @@ -1,174 +0,0 @@ -// -// PurchaseDomainsSelectDiscountsView.swift -// UBTSharing -// -// Created by Oleg Kuplin on 28.11.2023. -// - -import SwiftUI - -struct PurchaseDomainsSelectDiscountsView: View, ViewAnalyticsLogger { - - @Environment(\.analyticsViewName) private var analyticsViewName - @Environment(\.presentationMode) private var presentationMode - @Environment(\.purchaseDomainsService) private var purchaseDomainsService - @Environment(\.purchaseDomainsPreferencesStorage) private var purchaseDomainsPreferencesStorage - - @State private var isEnterDiscountCodePresented = false - @State private var isPromoCreditsOn = false - @State private var isStoreCreditsOn = false - @State private var checkoutData: PurchaseDomainsCheckoutData = PurchaseDomainsCheckoutData() - @State private var cartStatus: PurchaseDomainCartStatus = .ready(cart: .empty) - var analyticsName: Analytics.ViewName { analyticsViewName } - - var body: some View { - VStack(spacing: 24) { - Text(String.Constants.applyDiscounts.localized()) - .font(.currentFont(size: 22, weight: .bold)) - .foregroundStyle(Color.foregroundDefault) - .multilineTextAlignment(.center) - creditsSectionView() - discountSectionView() - Spacer() - } - .padding(EdgeInsets(top: 32, leading: 16, bottom: 16, trailing: 16)) - .sheet(isPresented: $isEnterDiscountCodePresented, content: { - PurchaseDomainsEnterDiscountCodeView(enteredCallback: { - presentationMode.wrappedValue.dismiss() - }) - }) - .onReceive(purchaseDomainsPreferencesStorage.$checkoutData.publisher.receive(on: DispatchQueue.main), perform: { checkoutData in - self.checkoutData = checkoutData - }) - .onReceive(purchaseDomainsService.cartStatusPublisher.receive(on: DispatchQueue.main)) { cartStatus in - self.cartStatus = cartStatus - } - .onAppear { - checkoutData = purchaseDomainsPreferencesStorage.checkoutData - isPromoCreditsOn = purchaseDomainsPreferencesStorage.checkoutData.isPromoCreditsOn - isStoreCreditsOn = purchaseDomainsPreferencesStorage.checkoutData.isStoreCreditsOn - } - } -} - -// MARK: - Private methods -private extension PurchaseDomainsSelectDiscountsView { - var hasPromoCredits: Bool { - cartStatus.promoCreditsAvailable > 0 - } - - var hasStoreCredits: Bool { - cartStatus.storeCreditsAvailable > 0 - } - - var creditsSectionHeight: CGFloat { - var height: CGFloat = 0 - if hasPromoCredits { - height += UDListItemView.height - } - if hasStoreCredits { - height += UDListItemView.height - } - return height - } -} - -// MARK: - Views -private extension PurchaseDomainsSelectDiscountsView { - @ViewBuilder - func creditsSectionView() -> some View { - UDCollectionSectionBackgroundView { - VStack(alignment: .center, spacing: 0) { - if hasPromoCredits { - Toggle("\(String.Constants.promoCredits.localized()): \(formatCartPrice(cartStatus.promoCreditsAvailable))", - isOn: $isPromoCreditsOn) - .toggleStyle(UDToggleStyle()) - .frame(minHeight: UDListItemView.height) - .onChange(of: isPromoCreditsOn) { newValue in - logButtonPressedAnalyticEvents(button: .applyPromoCredits, parameters: [.value : String(newValue)]) - purchaseDomainsPreferencesStorage.checkoutData.isPromoCreditsOn = newValue - } - } - if hasStoreCredits { - Toggle("\(String.Constants.storeCredits.localized()): \(formatCartPrice(cartStatus.storeCreditsAvailable))", - isOn: $isStoreCreditsOn) - .toggleStyle(UDToggleStyle()) - .frame(minHeight: UDListItemView.height) - .onChange(of: isStoreCreditsOn) { newValue in - logButtonPressedAnalyticEvents(button: .applyStoreCredits, parameters: [.value : String(newValue)]) - purchaseDomainsPreferencesStorage.checkoutData.isStoreCreditsOn = newValue - } - } - } - .font(.currentFont(size: 16, weight: .medium)) - .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) - } - .frame(height: creditsSectionHeight) - } - - @ViewBuilder - func discountSectionView() -> some View { - UDCollectionSectionBackgroundView { - if checkoutData.discountCode.isEmpty { - addDiscountRow() - } else { - discountAppliedRow() - } - } - .frame(height: UDListItemView.height) - } - - @ViewBuilder - func addDiscountRow() -> some View { - Button { - logButtonPressedAnalyticEvents(button: .creditsAndDiscounts) - UDVibration.buttonTap.vibrate() - isEnterDiscountCodePresented = true - } label: { - HStack(spacing: 16) { - Image.tagIcon - .resizable() - .squareFrame(20) - Text(String.Constants.addDiscountCode.localized()) - .font(.currentFont(size: 16, weight: .medium)) - Spacer() - } - } - .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) - } - - @ViewBuilder - func discountAppliedRow() -> some View { - Button { - logButtonPressedAnalyticEvents(button: .removeDiscountCode) - UDVibration.buttonTap.vibrate() - purchaseDomainsPreferencesStorage.checkoutData.discountCode = "" - } label: { - HStack(spacing: 4) { - Text("\(String.Constants.discountCode.localized()): \(formatCartPrice(cartStatus.otherDiscountsApplied))") - .foregroundStyle(Color.foregroundDefault) - Spacer() - HStack(spacing: 8) { - Text(purchaseDomainsPreferencesStorage.checkoutData.discountCode) - Image.cancelIcon - .resizable() - .squareFrame(20) - } - .foregroundStyle(Color.foregroundSecondary) - } - .font(.currentFont(size: 16, weight: .medium)) - - } - .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) - } -} - -#Preview { - PurchaseDomainsSelectDiscountsView() - .environment(\.purchaseDomainsService, MockFirebaseInteractionsService()) - -} - - - - diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsSelectWalletView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsSelectWalletView.swift index ac2d40eb1..61cd2ec1a 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsSelectWalletView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsSelectWalletView.swift @@ -21,23 +21,38 @@ struct PurchaseDomainsSelectWalletView: View, ViewAnalyticsLogger { var analyticsName: Analytics.ViewName { analyticsViewName } var body: some View { - ScrollView { - VStack(spacing: 24) { - Text(String.Constants.mintTo.localized()) - .font(.currentFont(size: 22, weight: .bold)) - .foregroundStyle(Color.foregroundDefault) + VStack { + ScrollView { + VStack(spacing: 24) { + VStack(spacing: 8) { + Text(String.Constants.purchaseMintingWalletPullUpTitle.localized()) + .textAttributes(color: .foregroundDefault, + fontSize: 22, + fontWeight: .bold) + Text(String.Constants.purchaseMintingWalletPullUpSubtitle.localized()) + .textAttributes(color: .foregroundSecondary, + fontSize: 16) + } .multilineTextAlignment(.center) - UDCollectionSectionBackgroundView { - LazyVStack { - ForEach(wallets, id: \.address) { wallet in - walletRowView(wallet) + UDCollectionSectionBackgroundView { + VStack { + ForEach(wallets, id: \.address) { wallet in + walletRowView(wallet) + } } + .padding(4) } - .padding(EdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)) } } - .padding(EdgeInsets(top: 32, leading: 16, bottom: 16, trailing: 16)) + UDButtonView(text: String.Constants.doneButtonTitle.localized(), + style: .large(.raisedPrimary)) { + userProfilesService.setActiveProfile(.wallet(selectedWallet)) + selectedWalletCallback(selectedWallet) + presentationMode.wrappedValue.dismiss() + } } + .padding(EdgeInsets(top: 32, leading: 16, bottom: 16, trailing: 16)) + .background(Color.backgroundDefault) } } @@ -57,12 +72,7 @@ private extension PurchaseDomainsSelectWalletView { .udListItemInCollectionButtonPadding() }, callback: { logButtonPressedAnalyticEvents(button: .purchaseDomainTargetWalletSelected) - - let selectedWallet = wallet - self.selectedWallet = selectedWallet - userProfilesService.setActiveProfile(.wallet(selectedWallet)) - selectedWalletCallback(selectedWallet) - presentationMode.wrappedValue.dismiss() + self.selectedWallet = wallet }) } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomains.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomains.swift index f28768515..4734752f3 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomains.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomains.swift @@ -13,6 +13,7 @@ extension PurchaseDomains { enum FlowAction { case didSelectDomains(_ domains: [DomainToPurchase]) case didFillProfileForDomain(_ domain: DomainToPurchase, profileChanges: DomainProfilePendingChanges) + case didRemoveAllDomainsFromTheCart case didPurchaseDomains case goToDomains } @@ -65,10 +66,10 @@ extension PurchaseDomains { } extension PurchaseDomains { - final class LocalCart: ObservableObject { - @Published + struct LocalCart { + private(set) var domains: [DomainToPurchase] = [] - @Published var isShowingCart = false + var isShowingCart = false var totalPrice: Int { domains.reduce(0, { $0 + $1.price })} @@ -76,19 +77,24 @@ extension PurchaseDomains { domains.firstIndex(where: { $0.name == domain.name }) != nil } - func addDomain(_ domain: DomainToPurchase) { + func canAddDomainToCart(_ domain: DomainToPurchase) -> Bool { + let totalPriceWithNewDomain = totalPrice + domain.price + return totalPriceWithNewDomain < Constants.maxPurchaseDomainsSum + } + + mutating func addDomain(_ domain: DomainToPurchase) { guard !isDomainInCart(domain) else { return } domains.append(domain) } - func removeDomain(_ domain: DomainToPurchase) { + mutating func removeDomain(_ domain: DomainToPurchase) { if let i = domains.firstIndex(where: { $0.name == domain.name }) { domains.remove(at: i) } } - func clearCart() { + mutating func clearCart() { domains.removeAll() } } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsNavigationDestination.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsNavigationDestination.swift index a94882ba1..9d3fb3a30 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsNavigationDestination.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsNavigationDestination.swift @@ -10,7 +10,7 @@ import SwiftUI extension PurchaseDomains { enum NavigationDestination: Hashable { case root(HomeTabRouter) - case checkout(_ chekoutData: CheckoutData, viewModel: PurchaseDomainsViewModel) + case checkout(_ checkoutData: CheckoutData, viewModel: PurchaseDomainsViewModel) case purchased(PurchaseDomainsViewModel) var isWithCustomTitle: Bool { @@ -65,7 +65,7 @@ extension PurchaseDomains { case .root(let router): PurchaseDomainsRootView(viewModel: PurchaseDomainsViewModel(router: router)) case .checkout(let checkoutData, let viewModel): - PurchaseDomainsCheckoutView(domain: checkoutData.domains[0], + PurchaseDomainsCheckoutView(domains: checkoutData.domains, selectedWallet: checkoutData.selectedWallet, wallets: checkoutData.wallets, profileChanges: checkoutData.profileChanges ?? .init(domainName: checkoutData.domains[0].name)) diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsViewModel.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsViewModel.swift index e47c1affc..69fd066dc 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsViewModel.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsViewModel.swift @@ -12,6 +12,7 @@ final class PurchaseDomainsViewModel: ObservableObject { @Published var isLoading = false @Published var error: Error? + @Published var localCart = PurchaseDomains.LocalCart() let id = UUID().uuidString private var purchaseData: PurchaseData = PurchaseData() private let router: HomeTabRouter @@ -57,6 +58,10 @@ final class PurchaseDomainsViewModel: ObservableObject { case .didFillProfileForDomain(let domain, let profileChanges): moveToCheckoutWith(domains: [domain], profileChanges: profileChanges) + case .didRemoveAllDomainsFromTheCart: + localCart.clearCart() + router.walletViewNavPath.removeLast() + appContext.toastMessageService.showToast(.cartCleared, isSticky: false) case .didPurchaseDomains: pushTo(.purchased(self)) case .goToDomains: diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsCartView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsCartView.swift index 01a2f2f91..22779c109 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsCartView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsCartView.swift @@ -10,10 +10,10 @@ import SwiftUI struct PurchaseDomainsCartView: View { @Environment(\.dismiss) var dismiss - @EnvironmentObject private var localCart: PurchaseDomains.LocalCart + @EnvironmentObject var viewModel: PurchaseDomainsViewModel var body: some View { - if localCart.domains.isEmpty { + if viewModel.localCart.domains.isEmpty { emptyView() .presentationDetents([.height(238)]) } else { @@ -73,14 +73,14 @@ private extension PurchaseDomainsCartView { @ViewBuilder func headerView() -> some View { HStack(spacing: 4) { - Text(String.Constants.buyDomainsCartTitle.localized(localCart.domains.count)) + Text(String.Constants.buyDomainsCartTitle.localized(viewModel.localCart.domains.count)) .textAttributes(color: .foregroundDefault, fontSize: 22, fontWeight: .bold) Spacer() Button { UDVibration.buttonTap.vibrate() - localCart.clearCart() + viewModel.localCart.clearCart() } label: { Text(String.Constants.clear.localized()) .textAttributes(color: .foregroundSecondary, @@ -95,7 +95,7 @@ private extension PurchaseDomainsCartView { func domainsListView() -> some View { UDCollectionSectionBackgroundView { LazyVStack(spacing: 4) { - ForEach(localCart.domains, id: \.name) { domain in + ForEach(viewModel.localCart.domains) { domain in domainListRow(domain) .udListItemInCollectionButtonPadding() } @@ -108,7 +108,7 @@ private extension PurchaseDomainsCartView { Button { UDVibration.buttonTap.vibrate() withAnimation { - localCart.removeDomain(domain) + viewModel.localCart.removeDomain(domain) } } label: { PurchaseDomainsSearchResultRowView(domain: domain, @@ -121,5 +121,5 @@ private extension PurchaseDomainsCartView { #Preview { PurchaseDomainsCartView() - .environmentObject(PurchaseDomains.LocalCart()) +// .environmentObject(PurchaseDomains.LocalCart()) } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsCheckoutButton.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsCheckoutButton.swift index 76715c93c..fbbcd6fe5 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsCheckoutButton.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsCheckoutButton.swift @@ -10,18 +10,17 @@ import SwiftUI struct PurchaseDomainsCheckoutButton: ViewModifier { @EnvironmentObject var viewModel: PurchaseDomainsViewModel - @EnvironmentObject private var localCart: PurchaseDomains.LocalCart func body(content: Content) -> some View { VStack { content - if !localCart.domains.isEmpty { + if !viewModel.localCart.domains.isEmpty { UDButtonView(text: String.Constants.checkout.localized(), subtext: subtitle, style: .large(.raisedPrimary)) { - if localCart.isShowingCart { - localCart.isShowingCart = false + if viewModel.localCart.isShowingCart { + viewModel.localCart.isShowingCart = false DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: didConfirmToCheckout) } else { didConfirmToCheckout() @@ -33,10 +32,10 @@ struct PurchaseDomainsCheckoutButton: ViewModifier { } private var subtitle: String { - "\(String.Constants.totalDue.localized()): \(formatCartPrice(localCart.totalPrice))" + "\(String.Constants.totalDue.localized()): \(formatCartPrice(viewModel.localCart.totalPrice))" } private func didConfirmToCheckout() { - viewModel.handleAction(.didSelectDomains(localCart.domains)) + viewModel.handleAction(.didSelectDomains(viewModel.localCart.domains)) } } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsSearchResultRowView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsSearchResultRowView.swift index 1213fe19b..06163dca1 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsSearchResultRowView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsSearchResultRowView.swift @@ -9,7 +9,7 @@ import SwiftUI struct PurchaseDomainsSearchResultRowView: View { - @EnvironmentObject private var localCart: PurchaseDomains.LocalCart + @EnvironmentObject var viewModel: PurchaseDomainsViewModel let domain: DomainToPurchase let mode: RowMode @@ -47,14 +47,20 @@ private extension PurchaseDomainsSearchResultRowView { func cartIconView() -> some View { switch mode { case .list: - if localCart.isDomainInCart(domain) { + if viewModel.localCart.isDomainInCart(domain) { Image.checkCircle .resizable() .foregroundStyle(Color.foregroundSuccess) } else { - Image.addToCartIcon - .resizable() - .foregroundStyle(Color.foregroundAccent) + if domain.isTooExpensiveToBuyInApp { + Image.arrowTopRight + .resizable() + .foregroundStyle(Color.foregroundSecondary) + } else { + Image.addToCartIcon + .resizable() + .foregroundStyle(Color.foregroundAccent) + } } case .cart: Image.trashIcon @@ -78,5 +84,5 @@ extension PurchaseDomainsSearchResultRowView { isTaken: false, isAbleToPurchase: true), mode: .list) - .environmentObject(PurchaseDomains.LocalCart()) +// .environmentObject(PurchaseDomains.LocalCart()) } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsSearchView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsSearchView.swift index 6492e3291..9c679a0b9 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsSearchView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsSearchView.swift @@ -14,7 +14,7 @@ struct PurchaseDomainsSearchView: View, ViewAnalyticsLogger { @EnvironmentObject var viewModel: PurchaseDomainsViewModel @StateObject private var debounceObject = DebounceObject() @StateObject private var ecommFlagTracker = UDMaintenanceModeFeatureFlagTracker(featureFlag: .isMaintenanceEcommEnabled) - @StateObject private var localCart = PurchaseDomains.LocalCart() + private var localCart: PurchaseDomains.LocalCart { viewModel.localCart } @State private var suggestions: [DomainToPurchaseSuggestion] = [] @State private var searchResultHolder: PurchaseDomains.SearchResultHolder = .init() @State private var isLoading: Bool = false @@ -32,10 +32,9 @@ struct PurchaseDomainsSearchView: View, ViewAnalyticsLogger { .background(Color.backgroundDefault) .viewPullUp($pullUp) .onAppear(perform: onAppear) - .sheet(isPresented: $localCart.isShowingCart, content: { + .sheet(isPresented: $viewModel.localCart.isShowingCart, content: { PurchaseDomainsCartView() }) - .environmentObject(localCart) .navigationTitle(String.Constants.buyDomainsSearchTitle.localized()) .navigationBarTitleDisplayMode(.inline) } @@ -64,7 +63,7 @@ private extension PurchaseDomainsSearchView { func cartButtonView() -> some View { Button { UDVibration.buttonTap.vibrate() - localCart.isShowingCart = true + viewModel.localCart.isShowingCart = true } label: { ZStack(alignment: .topTrailing) { Image.cartIcon @@ -170,7 +169,7 @@ private extension PurchaseDomainsSearchView { func resultDomainsListView(_ domains: [DomainToPurchase]) -> some View { LazyVStack(alignment: .leading, spacing: 20) { sectionTitleView(String.Constants.results.localized()) - ForEach(domains, id: \.name) { domain in + ForEach(domains) { domain in resultDomainRowView(domain) } } @@ -361,7 +360,7 @@ private extension PurchaseDomainsSearchView { actionButton: .main(content: .init(title: String.Constants.goToWebsite.localized(), analyticsName: .goToWebsite, action: { - openLinkExternally(.unstoppableDomainSearch(searchKey: domain.name)) + moveToPurchaseDomainsFromTheWeb(domain: domain) })), cancelButton: .gotItButton(), analyticName: .searchPurchaseDomainNotSupported)) @@ -369,12 +368,26 @@ private extension PurchaseDomainsSearchView { } func didSelectDomainToPurchase(_ domain: DomainToPurchase) { - if localCart.isDomainInCart(domain) { - localCart.removeDomain(domain) + if domain.isTooExpensiveToBuyInApp { + pullUp = .default(.buyDomainFromTheWebsite(goToWebCallback: { + moveToPurchaseDomainsFromTheWeb(domain: domain) + })) + } else if localCart.isDomainInCart(domain) { + viewModel.localCart.removeDomain(domain) } else { - localCart.addDomain(domain) + if !localCart.canAddDomainToCart(domain) { + pullUp = .default(.checkoutFromTheWebsite(goToWebCallback: { + moveToPurchaseDomainsFromTheWeb(domain: domain) + })) + } else { + viewModel.localCart.addDomain(domain) + } } } + + func moveToPurchaseDomainsFromTheWeb(domain: DomainToPurchase?) { + openLinkExternally(.unstoppableDomainSearch(searchKey: domain?.name ?? "")) + } } // MARK: - Views diff --git a/unstoppable-ios-app/domains-manager-ios/Payment/PaymentConfiguration.swift b/unstoppable-ios-app/domains-manager-ios/Payment/PaymentConfiguration.swift index 68877344e..43517f6e9 100644 --- a/unstoppable-ios-app/domains-manager-ios/Payment/PaymentConfiguration.swift +++ b/unstoppable-ios-app/domains-manager-ios/Payment/PaymentConfiguration.swift @@ -24,7 +24,7 @@ public class PaymentConfiguration { formatter.currencyCode = PaymentConfiguration.usdCurrencyLabel formatter.currencySymbol = PaymentConfiguration.usdCurrencySymbol formatter.maximumFractionDigits = 2 - formatter.minimumFractionDigits = 0 + formatter.minimumFractionDigits = 2 formatter.currencyDecimalSeparator = "." formatter.currencyGroupingSeparator = "," return formatter diff --git a/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsServiceEnvironment.swift b/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsServiceEnvironment.swift index d36fceb6f..50e106ec2 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsServiceEnvironment.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsServiceEnvironment.swift @@ -421,7 +421,7 @@ extension Analytics { case buy, receive, profile, more case connectedApps case selectProfile, profileSelected - case rrDomainAvatar, purchaseDomainAvatar + case rrDomainAvatar, purchaseDomainAvatar, purchaseSelectCountry case homeContentTypeSelected case sort, sortType case notMatchingToken, notMatchingTokensSectionHeader diff --git a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/MockFirebaseInteractionsService.swift b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/MockFirebaseInteractionsService.swift index 029f59767..1ccb0012b 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/MockFirebaseInteractionsService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/MockFirebaseInteractionsService.swift @@ -72,7 +72,7 @@ extension MockFirebaseInteractionsService: PurchaseDomainsServiceProtocol { await Task.sleep(seconds: 0.5) let key = key.lowercased() let tlds: [String] = ["x", "crypto", "nft", "wallet", "polygon", "dao", "888", "blockchain", "go", "bitcoin"] - let prices: [Int] = [40000, 20000, 8000, 4000, 500] + let prices: [Int] = [Constants.maxPurchaseDomainsSum, 4000_00, 40000, 20000, 8000, 4000, 500] let isTaken: [Bool] = [true, false] let notSupportedTLDs: [String] = ["eth", "com"] @@ -131,6 +131,11 @@ extension MockFirebaseInteractionsService: PurchaseDomainsServiceProtocol { updateCart() } + func setDomainsToPurchase(_ domains: [DomainToPurchase]) async throws { + cart = MockFirebaseInteractionsService.createMockCart() + updateCart() + } + func reset() async { cartStatus = .ready(cart: .empty) updateCart() @@ -167,7 +172,7 @@ private extension MockFirebaseInteractionsService { let storeCredits = checkoutData.isStoreCreditsOn ? 100 : 0 let promoCredits = checkoutData.isPromoCreditsOn ? 2000 : 0 let otherDiscounts = checkoutData.discountCode.isEmpty ? 0 : cart.totalPrice / 3 - cart.appliedDiscountDetails = .init(storeCredits: storeCredits, + cart.appliedDiscountDetails = .init(storeCredits: storeCredits, promoCredits: promoCredits, others: otherDiscounts) cart.totalPrice -= (storeCredits + promoCredits + otherDiscounts) diff --git a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/EcomPurchaseInteractionService.swift b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/EcomPurchaseInteractionService.swift index 0f206c3df..56c38f9c5 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/EcomPurchaseInteractionService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/EcomPurchaseInteractionService.swift @@ -133,11 +133,9 @@ extension EcomPurchaseInteractionService { } func loadUserCartCalculations() async throws -> Ecom.UserCartCalculationsResponse { - let queryComponents = ["applyPromoCredits" : String(checkoutData.isPromoCreditsOn), - "applyStoreCredits" : String(checkoutData.isStoreCreditsOn), - "discountCode" : checkoutData.discountCode.trimmedSpaces, + let queryComponents = ["discountCode" : checkoutData.discountCode.trimmedSpaces, "durationsMap" : checkoutData.getDurationsMapString(), - "zipCode" : checkoutData.usaZipCode.trimmedSpaces] + "zipCode" : checkoutData.zipCodeIfEntered?.trimmedSpaces ?? ""] let urlString = URLSList.USER_CART_CALCULATIONS_URL.appendingURLQueryComponents(queryComponents) let request = try APIRequest(urlString: urlString, @@ -198,8 +196,8 @@ extension EcomPurchaseInteractionService { struct RequestBody: Codable { let cryptoWalletId: Int? let email: String? - let applyStoreCredits: Bool - let applyPromoCredits: Bool + var applyStoreCredits: Bool? = nil + var applyPromoCredits: Bool? = nil let discountCode: String? let zipCode: String? } @@ -207,8 +205,6 @@ extension EcomPurchaseInteractionService { let urlString = URLSList.PAYMENT_STRIPE_URL let body = RequestBody(cryptoWalletId: cartDetails?.wallet?.id, email: cartDetails?.email, - applyStoreCredits: checkoutData.isStoreCreditsOn, - applyPromoCredits: checkoutData.isPromoCreditsOn, discountCode: checkoutData.discountCodeIfEntered, zipCode: checkoutData.zipCodeIfEntered) let request = try APIRequest(urlString: urlString, @@ -242,16 +238,14 @@ extension EcomPurchaseInteractionService { private func checkoutWithCredits(with cartDetails: Ecom.ProductsCartDetails?) async throws { struct RequestBody: Codable { let cryptoWalletId: Int? - let applyStoreCredits: Bool - let applyPromoCredits: Bool + var applyStoreCredits: Bool? = nil + var applyPromoCredits: Bool? = nil let discountCode: String? let zipCode: String? } let urlString = URLSList.STORE_CHECKOUT_URL let body = RequestBody(cryptoWalletId: cartDetails?.wallet?.id, - applyStoreCredits: checkoutData.isStoreCreditsOn, - applyPromoCredits: checkoutData.isPromoCreditsOn, discountCode: checkoutData.discountCodeIfEntered, zipCode: checkoutData.zipCodeIfEntered) let request = try APIRequest(urlString: urlString, diff --git a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/DomainToPurchase.swift b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/DomainToPurchase.swift index 63eb12ca7..0e2801459 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/DomainToPurchase.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/DomainToPurchase.swift @@ -7,13 +7,19 @@ import SwiftUI -struct DomainToPurchase: Hashable { +struct DomainToPurchase: Hashable, Identifiable { + + var id: String { name } + let name: String let price: Int let metadata: Data? let isTaken: Bool let isAbleToPurchase: Bool + var isTooExpensiveToBuyInApp: Bool { + price >= Constants.maxPurchaseDomainsSum + } var tld: String { name.components(separatedBy: .dotSeparator).last ?? "" } var tldCategory: TLDCategory { switch tld { diff --git a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/FirebasePurchaseDomainsService.swift b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/FirebasePurchaseDomainsService.swift index 2947eddb8..55aff1e50 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/FirebasePurchaseDomainsService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/FirebasePurchaseDomainsService.swift @@ -136,6 +136,14 @@ extension FirebasePurchaseDomainsService: PurchaseDomainsServiceProtocol { isAutoRefreshCartSuspended = false } + func setDomainsToPurchase(_ domains: [DomainToPurchase]) async throws { + isAutoRefreshCartSuspended = true + cartStatus = .ready(cart: .empty) + self.domainsToPurchase = domains + try await addDomainsToCart(domains) + isAutoRefreshCartSuspended = false + } + func reset() async { cartStatus = .ready(cart: .empty) cachedPaymentDetails = nil diff --git a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/PurchaseDomainCartStatus.swift b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/PurchaseDomainCartStatus.swift index 8f17103ff..fe184b198 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/PurchaseDomainCartStatus.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/PurchaseDomainCartStatus.swift @@ -61,5 +61,13 @@ enum PurchaseDomainCartStatus { return 0 } } + var subtotalPrice: Int { + switch self { + case .ready(let cart): + return cart.subtotalPrice + default: + return 0 + } + } } diff --git a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/PurchaseDomainsCart.swift b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/PurchaseDomainsCart.swift index 504d11cd8..2b2fafbc3 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/PurchaseDomainsCart.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/PurchaseDomainsCart.swift @@ -20,11 +20,22 @@ struct PurchaseDomainsCart { var domains: [DomainToPurchase] var totalPrice: Int + let subtotalPrice: Int var taxes: Int let storeCreditsAvailable: Int let promoCreditsAvailable: Int var appliedDiscountDetails: AppliedDiscountDetails + init(domains: [DomainToPurchase], totalPrice: Int, taxes: Int, storeCreditsAvailable: Int, promoCreditsAvailable: Int, appliedDiscountDetails: AppliedDiscountDetails) { + self.domains = domains + self.totalPrice = totalPrice + self.subtotalPrice = domains.reduce(0, { $0 + $1.price }) + self.taxes = taxes + self.storeCreditsAvailable = storeCreditsAvailable + self.promoCreditsAvailable = promoCreditsAvailable + self.appliedDiscountDetails = appliedDiscountDetails + } + struct AppliedDiscountDetails { let storeCredits: Int let promoCredits: Int diff --git a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/PurchaseDomainsCheckoutData.swift b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/PurchaseDomainsCheckoutData.swift index d9ae0732e..fa1c0c2ef 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/PurchaseDomainsCheckoutData.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/PurchaseDomainsCheckoutData.swift @@ -13,6 +13,7 @@ struct PurchaseDomainsCheckoutData: Equatable { var usaZipCode: String = "" var discountCode: String = "" var durationsMap: [String : Double] = [:] + var purchaseLocation: UserPurchaseLocation = .other func getDurationsMapString() -> String { if durationsMap.isEmpty { @@ -26,7 +27,30 @@ struct PurchaseDomainsCheckoutData: Equatable { } var discountCodeIfEntered: String? { discountCode.isEmpty ? nil : discountCode } - var zipCodeIfEntered: String? { usaZipCode.isEmpty ? nil : usaZipCode } + var zipCodeIfEntered: String? { + switch purchaseLocation { + case .usa: + return usaZipCode.isEmpty ? nil : usaZipCode + case .other: + return nil + } + } + + enum UserPurchaseLocation: String, Codable, CaseIterable, UDSegmentedControlItem { + + case usa + case other + + var title: String { + switch self { + case .usa: + return String.Constants.usa.localized() + case .other: + return String.Constants.other.localized() + } + } + var analyticButton: Analytics.Button { .purchaseSelectCountry } + } } extension PurchaseDomainsCheckoutData: Codable { @@ -36,6 +60,7 @@ extension PurchaseDomainsCheckoutData: Codable { case usaZipCode case discountCode case durationsMap + case purchaseLocation } func encode(to encoder: Encoder) throws { @@ -45,6 +70,7 @@ extension PurchaseDomainsCheckoutData: Codable { try container.encode(usaZipCode, forKey: .usaZipCode) try container.encode(discountCode, forKey: .discountCode) try container.encode(durationsMap, forKey: .durationsMap) + try container.encode(purchaseLocation, forKey: .purchaseLocation) } // Implement the init(from:) method @@ -55,6 +81,13 @@ extension PurchaseDomainsCheckoutData: Codable { usaZipCode = try container.decode(String.self, forKey: .usaZipCode) discountCode = try container.decode(String.self, forKey: .discountCode) durationsMap = try container.decode([String: Double].self, forKey: .durationsMap) + + let purchaseLocation = try? container.decode(UserPurchaseLocation.self, forKey: .purchaseLocation) + if let purchaseLocation { + self.purchaseLocation = purchaseLocation + } else { + self.purchaseLocation = usaZipCode.isEmpty ? .other : .usa + } } } diff --git a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/PurchaseDomainsServiceProtocol.swift b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/PurchaseDomainsServiceProtocol.swift index 2410b3b7b..73338a36c 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/PurchaseDomainsServiceProtocol.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase domains/PurchaseDomainsServiceProtocol.swift @@ -16,6 +16,7 @@ protocol PurchaseDomainsServiceProtocol { func getDomainsSuggestions(hint: String?) async throws -> [DomainToPurchaseSuggestion] func authoriseWithWallet(_ wallet: UDWallet, toPurchaseDomains domains: [DomainToPurchase]) async throws + func setDomainsToPurchase(_ domains: [DomainToPurchase]) async throws func getSupportedWalletsToMint() async throws -> [PurchasedDomainsWalletDescription] func reset() async diff --git a/unstoppable-ios-app/domains-manager-ios/Services/ToastMessageService.swift b/unstoppable-ios-app/domains-manager-ios/Services/ToastMessageService.swift index 79bd5dcdd..a17996a10 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/ToastMessageService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/ToastMessageService.swift @@ -25,9 +25,9 @@ final class ToastMessageService { private let animationDuration: TimeInterval = 0.25 private let dismissDelay: TimeInterval = 3 // sec - private var visibleToast: ToastView? + private var visibleToast: ToastUIView? private var dismissToastWorkingItem: DispatchWorkItem? - private var stickyToastView: ToastView? + private var stickyToastView: ToastUIView? private var toastActions: [Toast : EmptyCallback] = [:] nonisolated init() { } @@ -83,7 +83,7 @@ extension ToastMessageService: ToastMessageServiceProtocol { isSticky: Bool, dismissDelay: TimeInterval?, action: EmptyCallback?) { - if let toastView = view.firstSubviewOfType(ToastView.self) { + if let toastView = view.firstSubviewOfType(ToastUIView.self) { if toastView.toast == toast { return } @@ -106,7 +106,7 @@ extension ToastMessageService: ToastMessageServiceProtocol { } func removeToast(from view: UIView) { - if let toastView = view.firstSubviewOfType(ToastView.self) { + if let toastView = view.firstSubviewOfType(ToastUIView.self) { removeAction(for: toastView) toastView.removeFromSuperview() } @@ -121,10 +121,10 @@ private extension ToastMessageService { image: UIImage, secondaryMessage: String?, style: Toast.Style, - in frame: CGRect?) -> ToastView { + in frame: CGRect?) -> ToastUIView { let windowFrame = frame ?? window?.frame ?? .zero let sideOffset: CGFloat = 12 - let view = ToastView(frame: CGRect(x: 0, y: 0, width: windowFrame.width, height: 36)) + let view = ToastUIView(frame: CGRect(x: 0, y: 0, width: windowFrame.width, height: 36)) view.backgroundColor = style.color view.layer.cornerRadius = 18 @@ -162,7 +162,7 @@ private extension ToastMessageService { return view } - func showToastView(_ toastView: ToastView, in view: UIView?, at position: Toast.Position, dismissDelay: TimeInterval) { + func showToastView(_ toastView: ToastUIView, in view: UIView?, at position: Toast.Position, dismissDelay: TimeInterval) { guard let container = view ?? self.window else { return } if toastView.isSticky == false { @@ -201,7 +201,7 @@ private extension ToastMessageService { } } - func scheduleDismissWorkingItemFor(toastView: ToastView, dismissDelay: TimeInterval) { + func scheduleDismissWorkingItemFor(toastView: ToastUIView, dismissDelay: TimeInterval) { let dismissToastWorkingItem = DispatchWorkItem { [weak self, weak toastView] in if let toastView = toastView { self?.removeToastView(toastView) @@ -211,7 +211,7 @@ private extension ToastMessageService { DispatchQueue.main.asyncAfter(deadline: .now() + dismissDelay, execute: dismissToastWorkingItem) } - func removeToastView(_ toastView: ToastView) { + func removeToastView(_ toastView: ToastUIView) { self.visibleToast = nil dismissToastWorkingItem?.cancel() dismissToastWorkingItem = nil @@ -236,26 +236,26 @@ private extension ToastMessageService { } @objc func swipeDismissView(_ gesture: UISwipeGestureRecognizer) { - guard let toastView = gesture.view as? ToastView else { return } + guard let toastView = gesture.view as? ToastUIView else { return } removeToastView(toastView) } - func add(action: EmptyCallback?, for toast: Toast, to view: ToastView) { + func add(action: EmptyCallback?, for toast: Toast, to view: ToastUIView) { guard let action else { return } toastActions[toast] = action view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapToast))) } - func removeAction(for toastView: ToastView) { + func removeAction(for toastView: ToastUIView) { if let toast = toastView.toast { toastActions[toast] = nil } } @objc func didTapToast(_ gesture: UITapGestureRecognizer) { - guard let toastView = gesture.view as? ToastView, + guard let toastView = gesture.view as? ToastUIView, let toast = toastView.toast else { return } UDVibration.buttonTap.vibrate() @@ -264,7 +264,7 @@ private extension ToastMessageService { } } -final class ToastView: UIView { +final class ToastUIView: UIView { var toast: Toast? var initialY: CGFloat = 0 { didSet { frame.origin.y = initialY } } diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Badges/planetIcon20.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Badges/planetIcon20.imageset/Contents.json index 8c920eda6..8a4af3bee 100644 --- a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Badges/planetIcon20.imageset/Contents.json +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Badges/planetIcon20.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "planetIcon20.pdf", + "filename" : "planetIcon20.svg", "idiom" : "universal" } ], diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Badges/planetIcon20.imageset/planetIcon20.pdf b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Badges/planetIcon20.imageset/planetIcon20.pdf deleted file mode 100644 index 0e5e90bb8..000000000 Binary files a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Badges/planetIcon20.imageset/planetIcon20.pdf and /dev/null differ diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Badges/planetIcon20.imageset/planetIcon20.svg b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Badges/planetIcon20.imageset/planetIcon20.svg new file mode 100644 index 000000000..9343608cd --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Badges/planetIcon20.imageset/planetIcon20.svg @@ -0,0 +1,3 @@ + + + diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/arrowTopRight.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/arrowTopRight.imageset/Contents.json index 1fe4f5b1e..7e7abd3b2 100644 --- a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/arrowTopRight.imageset/Contents.json +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/arrowTopRight.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "arrowTopRight.pdf", + "filename" : "arrowTopRight.svg", "idiom" : "universal" } ], diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/arrowTopRight.imageset/arrowTopRight.pdf b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/arrowTopRight.imageset/arrowTopRight.pdf deleted file mode 100644 index 9bc323e4a..000000000 Binary files a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/arrowTopRight.imageset/arrowTopRight.pdf and /dev/null differ diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/arrowTopRight.imageset/arrowTopRight.svg b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/arrowTopRight.imageset/arrowTopRight.svg new file mode 100644 index 000000000..7b1d23524 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/arrowTopRight.imageset/arrowTopRight.svg @@ -0,0 +1,3 @@ + + + diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/starInCloudIcon.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/starInCloudIcon.imageset/Contents.json new file mode 100644 index 000000000..77173b4bd --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/starInCloudIcon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "starInCloudIcon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/starInCloudIcon.imageset/starInCloudIcon.svg b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/starInCloudIcon.imageset/starInCloudIcon.svg new file mode 100644 index 000000000..c52e064d7 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/starInCloudIcon.imageset/starInCloudIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/ticketIcon.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/ticketIcon.imageset/Contents.json new file mode 100644 index 000000000..82ef92777 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/ticketIcon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ticketIcon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/ticketIcon.imageset/ticketIcon.svg b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/ticketIcon.imageset/ticketIcon.svg new file mode 100644 index 000000000..0c03cf218 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/ticketIcon.imageset/ticketIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/trashIcon.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/trashIcon.imageset/Contents.json index 0f71eac9a..e65a26372 100644 --- a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/trashIcon.imageset/Contents.json +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/trashIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "trashIcon.pdf", + "filename" : "trashIcon.svg", "idiom" : "universal" } ], diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/trashIcon.imageset/trashIcon.pdf b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/trashIcon.imageset/trashIcon.pdf deleted file mode 100644 index dc5b9ba61..000000000 Binary files a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/trashIcon.imageset/trashIcon.pdf and /dev/null differ diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/trashIcon.imageset/trashIcon.svg b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/trashIcon.imageset/trashIcon.svg new file mode 100644 index 000000000..28930dd87 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/trashIcon.imageset/trashIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/dnsTLDLogo.imageset/dnsTLDLogo.svg b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/dnsTLDLogo.imageset/dnsTLDLogo.svg index f6a157a19..1d110dade 100644 --- a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/dnsTLDLogo.imageset/dnsTLDLogo.svg +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/dnsTLDLogo.imageset/dnsTLDLogo.svg @@ -1,10 +1,8 @@ - - - - - - - - - + + + + + + + diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Constants.swift b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Constants.swift index 99f8b2713..f6ecf9bcc 100644 --- a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Constants.swift +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Constants.swift @@ -60,7 +60,8 @@ 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 ldApplicationIdentifier: String = "ud-ios-app" // Launch darkly id - + static let maxPurchaseDomainsSum: Int = 10_000_00 // 10.000$ + // Shake to find static let shakeToFindServiceId: String = "090DAE5A-0DD8-4327-B074-E1E09B259597" static let shakeToFindCharacteristicId: String = "3403C4D9-2C2C-4A6A-A9DB-115D10095771" 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 8bae820b5..a23d9c36f 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 @@ -932,6 +932,7 @@ "MINT_TO" = "Mint to"; "APPLY_DISCOUNTS" = "Apply discounts"; "ADD_DISCOUNT_CODE" = "Add discount code"; +"DISCOUNT_CODE_APPLIED" = "Discount code applied"; "PROMO_CREDITS" = "Promo credits"; "STORE_CREDITS" = "Store credits"; "US_ZIP_CODE" = "US ZIP code"; @@ -984,6 +985,21 @@ "START_TYPING" = "Start typing"; "BUY_DOMAINS_SEARCH_RESULT_SHOW_MORE_TITLE" = "Show more exact matches"; "BUY_DOMAINS_SEARCH_RESULT_SHOW_LESS_TITLE" = "Show less exact matches"; +"PURCHASE_MINTING_WALLET_TITLE" = "Minting Wallet"; +"PURCHASE_MINTING_WALLET_PULL_UP_TITLE" = "Select Minting Wallet"; +"PURCHASE_MINTING_WALLET_PULL_UP_SUBTITLE" = "Where your domains will be minted."; +"SUBTOTAL" = "Subtotal"; +"COUNTRY" = "Country"; +"USA" = "USA"; +"OTHER" = "Other"; +"ZIP_CODE_FOR_SALES_TAX" = "ZIP Code for Sales Tax"; +"DOMAIN_REMOVED" = "Domain removed"; +"UNDO" = "Undo"; +"CART_CLEARED" = "Cart cleared"; +"BUY_DOMAIN_FROM_WEB_PULL_UP_TITLE" = "Buy this domain from\nthe website"; +"BUY_DOMAIN_FROM_WEB_PULL_UP_SUBTITLE" = "For domains priced above $10,000, checkout is only possible using crypto. However, the app currently doesn’t support crypto checkout."; +"CHECKOUT_FROM_WEB_PULL_UP_TITLE" = "Checkout from website"; +"CHECKOUT_FROM_WEB_PULL_UP_SUBTITLE" = "For purchases above $10,000 at once, checkout is only possible using crypto. However, the app currently doesn’t support crypto checkout."; // Home "HOME_WALLET_TOKENS_COME_TITLE" = "No more tokens here yet"; diff --git a/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/Buttons/UDNumberPadView.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/Buttons/UDNumberPadView.swift index 6e127ab48..93df47b0c 100644 --- a/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/Buttons/UDNumberPadView.swift +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/Buttons/UDNumberPadView.swift @@ -58,7 +58,6 @@ private extension UDNumberPadView { return UDNumberPadView(inputCallback: { inputType in interpreter.addInput(inputType) - print("Str: \(interpreter.getInput()) D: \(interpreter.getInterpretedNumber())") }) } diff --git a/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/ToastView.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/ToastView.swift new file mode 100644 index 000000000..9533643a1 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/ToastView.swift @@ -0,0 +1,66 @@ +// +// ToastView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 09.08.2024. +// + +import SwiftUI + +struct ToastView: View { + + let toast: Toast + var action: ActionDescription? = nil + + private var style: Toast.Style { toast.style } + + var body: some View { + HStack(spacing: 8) { + Image(uiImage: toast.image) + .resizable() + .squareFrame(20) + .foregroundStyle(Color(style.tintColor)) + Text(toast.message) + .textAttributes(color: Color(style.tintColor), + fontSize: 14, + fontWeight: .medium) + + if let action { + LineView(direction: .vertical, + size: 1) + .frame(height: 20) + .scaleEffect(y: 2) + .padding(.horizontal, 4) + .foregroundStyle(Color.borderDefault) + + Button { + UDVibration.buttonTap.vibrate() + action.callback() + } label: { + Text(action.title) + .textAttributes(color: .white.opacity(0.56), + fontSize: 14, + fontWeight: .medium) + } + .buttonStyle(.plain) + .padding(.trailing, 4) + } + } + .padding(8) + .background(Color(style.color)) + .clipShape(.capsule) + } +} + +// MARK: - Open methods +extension ToastView { + struct ActionDescription { + let title: String + let callback: EmptyCallback + } +} + +#Preview { + ToastView(toast: .changesConfirmed, + action: .init(title: "Undo", callback: { })) +} diff --git a/unstoppable-ios-app/domains-manager-ios/SwiftUI/Extensions/Image.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/Extensions/Image.swift index 7ccf52467..b1e612065 100644 --- a/unstoppable-ios-app/domains-manager-ios/SwiftUI/Extensions/Image.swift +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/Extensions/Image.swift @@ -74,7 +74,7 @@ extension Image { static let udLogoBlue = Image("udLogoBlue") static let arrowUp24 = Image("arrowUp24") static let plusIcon18 = Image("plusIcon18") - static let trashIcon = Image("trashIcon16") + static let trashIcon = Image("trashIcon") static let framesIcon = Image("framesIcon") static let helpIcon = Image("helpIcon24") static let docsIcon = Image("docsIcon24") @@ -130,6 +130,9 @@ extension Image { static let unsTLDLogo = Image("unsTLDLogo") static let ensTLDLogo = Image("ensTLDLogo") static let dnsTLDLogo = Image("dnsTLDLogo") + static let planetIcon20 = Image("planetIcon20") + static let ticketIcon = Image("ticketIcon") + static let starInCloudIcon = Image("starInCloudIcon") static let cryptoFaceIcon = Image("cryptoFaceIcon") static let cryptoPOAPIcon = Image("cryptoPOAPIcon") 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 ba5c1f7cb..8473777fb 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 @@ -504,6 +504,27 @@ extension ViewPullUpDefaultConfiguration { dismissCallback: nil) } + static func buyDomainFromTheWebsite(goToWebCallback: MainActorAsyncCallback?) -> ViewPullUpDefaultConfiguration { + .init(icon: .init(icon: .unsTLDLogo, + size: .small), + title: .text(String.Constants.buyDomainFromWebPullUpTitle.localized()), + subtitle: .label(.text(String.Constants.buyDomainFromWebPullUpSubtitle.localized())), + actionButton: .main(content: .init(title: String.Constants.goToWebsite.localized(), + analyticsName: .goToWebsite, + action: goToWebCallback)), + analyticName: .wcRequestNotSupported) + } + + static func checkoutFromTheWebsite(goToWebCallback: MainActorAsyncCallback?) -> ViewPullUpDefaultConfiguration { + .init(icon: .init(icon: .unsTLDLogo, + size: .small), + title: .text(String.Constants.checkoutFromWebPullUpTitle.localized()), + subtitle: .label(.text(String.Constants.checkoutFromWebPullUpSubtitle.localized())), + actionButton: .main(content: .init(title: String.Constants.goToWebsite.localized(), + analyticsName: .goToWebsite, + action: goToWebCallback)), + analyticName: .wcRequestNotSupported) + } } // MARK: - Open methods diff --git a/unstoppable-ios-app/domains-manager-ios/UI/Windows/MainWindow.swift b/unstoppable-ios-app/domains-manager-ios/UI/Windows/MainWindow.swift index 2fe8d5c19..7ba770a59 100644 --- a/unstoppable-ios-app/domains-manager-ios/UI/Windows/MainWindow.swift +++ b/unstoppable-ios-app/domains-manager-ios/UI/Windows/MainWindow.swift @@ -21,7 +21,7 @@ final class MainWindow: UIWindow { // MARK: - Private methods private extension MainWindow { func checkToastView() { - if let toastView = self.firstSubviewOfType(ToastView.self) { + if let toastView = self.firstSubviewOfType(ToastUIView.self) { bringSubviewToFront(toastView) } }