diff --git a/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewAppContext.swift b/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewAppContext.swift index e22916685..1add3bcd8 100644 --- a/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewAppContext.swift +++ b/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewAppContext.swift @@ -78,7 +78,8 @@ final class AppContext: AppContextProtocol { var domainProfilesService: DomainProfilesServiceProtocol var walletTransactionsService: WalletTransactionsServiceProtocol var mpcWalletsService: MPCWalletsServiceProtocol - + var ipVerificationService: IPVerificationServiceProtocol = IPVerificationService() + func createStripeInstance(amount: Int, using secret: String) -> StripeServiceProtocol { StripeService(paymentDetails: .init(amount: amount, paymentSecret: secret)) } diff --git a/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewGIFAnimationsService.swift b/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewGIFAnimationsService.swift index b80b45684..51d0e1360 100644 --- a/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewGIFAnimationsService.swift +++ b/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewGIFAnimationsService.swift @@ -12,6 +12,8 @@ final class GIFAnimationsService { static let shared = GIFAnimationsService() func createGIFImageWithData(_ data: Data, + id: String, + maxImageSize: CGFloat, maskingType: GIFMaskingType? = nil) async -> UIImage? { UIImage(data: data) diff --git a/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewImageLoadingService.swift b/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewImageLoadingService.swift index 8f336021d..af5825ee1 100644 --- a/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewImageLoadingService.swift +++ b/unstoppable-ios-app/domains-manager-ios-preview/AppContext/PreviewImageLoadingService.swift @@ -34,7 +34,9 @@ final class ImageLoadingService: ImageLoadingServiceProtocol { return UIImage.Preview.previewLandscape let imageData = try Data(contentsOf: url) - if let gif = await GIFAnimationsService.shared.createGIFImageWithData(imageData) { + if let gif = await GIFAnimationsService.shared.createGIFImageWithData(imageData, + id: UUID().uuidString, + maxImageSize: maxImageSize ?? Constants.downloadedImageMaxSize) { return gif } diff --git a/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewIPVerificationService.swift b/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewIPVerificationService.swift new file mode 100644 index 000000000..2f91ea76b --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewIPVerificationService.swift @@ -0,0 +1,14 @@ +// +// PreviewIPVerificationService.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 13.08.2024. +// + +import Foundation + +final class IPVerificationService: IPVerificationServiceProtocol { + func isUserInTheUS() async throws -> Bool { + false + } +} diff --git a/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewStorage.swift b/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewStorage.swift index 70d9cf7c1..5a5ff33ca 100644 --- a/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewStorage.swift +++ b/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewStorage.swift @@ -18,22 +18,28 @@ struct Storage { class SpecificStorage { let fileName: String + private var data: T? = nil + private let queue = DispatchQueue(label: "preview.serial.storage") init(fileName: String) { self.fileName = fileName } func retrieve() -> T? { - nil + queue.sync { data } } @discardableResult func store(_ data: T) -> Bool { - - return true + queue.sync { + self.data = data + return true + } } func remove() { - + queue.sync { + self.data = nil + } } } diff --git a/unstoppable-ios-app/domains-manager-ios-preview/Modules/PreviewDomainProfile/PreviewDomainProfile.swift b/unstoppable-ios-app/domains-manager-ios-preview/Modules/PreviewDomainProfile/PreviewDomainProfile.swift index fe418d288..e7d57a05f 100644 --- a/unstoppable-ios-app/domains-manager-ios-preview/Modules/PreviewDomainProfile/PreviewDomainProfile.swift +++ b/unstoppable-ios-app/domains-manager-ios-preview/Modules/PreviewDomainProfile/PreviewDomainProfile.swift @@ -10,13 +10,14 @@ import SwiftUI @available(iOS 17, *) #Preview { - let domain = DomainToPurchase(name: "oleg.x", price: 10000, metadata: nil, isAbleToPurchase: true) - let vc = DomainProfileViewController.nibInstance() - let presenter = PurchaseDomainDomainProfileViewPresenter(view: vc, - domain: domain) - vc.presenter = presenter - let nav = EmptyRootCNavigationController(rootViewController: vc) - - return nav + EmptyView() +// let domain = DomainToPurchase(name: "oleg.x", price: 10000, metadata: nil, isAbleToPurchase: true) +// let vc = DomainProfileViewController.nibInstance() +// let presenter = PurchaseDomainDomainProfileViewPresenter(view: vc, +// domain: domain) +// vc.presenter = presenter +// let nav = EmptyRootCNavigationController(rootViewController: vc) +// +// return nav } diff --git a/unstoppable-ios-app/domains-manager-ios-preview/ViewController.swift b/unstoppable-ios-app/domains-manager-ios-preview/ViewController.swift index 5bf1edf75..4e908c927 100644 --- a/unstoppable-ios-app/domains-manager-ios-preview/ViewController.swift +++ b/unstoppable-ios-app/domains-manager-ios-preview/ViewController.swift @@ -20,38 +20,15 @@ class ViewController: UIViewController { } @IBAction func runPurchaseButtonPressed() { - UDRouter().showSearchDomainToPurchase(in: self) { result in - - } + } func showPurchaseDomainsSearch() { - let view = PurchaseSearchDomainsView(domainSelectedCallback: { _ in }) + let view = PurchaseDomainsSearchView() let vc = UIHostingController(rootView: view) addChildViewController(vc, andEmbedToView: self.view) } - func showPurchaseDomainsCheckout() { - let view = PurchaseDomainsCheckoutView(domain: .init(name: "oleg.x", price: 10000, metadata: nil, isAbleToPurchase: true), - selectedWallet: MockEntitiesFabric.Wallet.mockEntities()[0], - wallets: MockEntitiesFabric.Wallet.mockEntities(), - profileChanges: .init(domainName: "oleg.x"), - delegate: nil) - - let vc = UIHostingController(rootView: view) - addChildViewController(vc, andEmbedToView: self.view) - } - - func showDomainProfile() { - let domain = DomainToPurchase(name: "oleg.x", price: 10000, metadata: nil, isAbleToPurchase: true) - let vc = DomainProfileViewController.nibInstance() - let presenter = PurchaseDomainDomainProfileViewPresenter(view: vc, - domain: domain) - vc.presenter = presenter - let nav = EmptyRootCNavigationController(rootViewController: vc) - present(nav, animated: false) - } - } diff --git a/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj b/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj index 2716202d0..61d03de81 100644 --- a/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj +++ b/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj @@ -110,6 +110,8 @@ 30D89F5B26A9EB5500251C2D /* Bugsnag in Frameworks */ = {isa = PBXBuildFile; productRef = 30D89F5A26A9EB5500251C2D /* Bugsnag */; }; 30E549C32886DA27009833FB /* DefaultsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E549C22886DA27009833FB /* DefaultsStorage.swift */; }; 30EFBBDC27F4754E00B8D667 /* MainButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFBBDB27F4754E00B8D667 /* MainButton.swift */; }; + C6008B2B2C60F77700F218B9 /* NavigationPopGestureDisabler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6008B2A2C60F77700F218B9 /* NavigationPopGestureDisabler.swift */; }; + C6008B2C2C60F77700F218B9 /* NavigationPopGestureDisabler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6008B2A2C60F77700F218B9 /* NavigationPopGestureDisabler.swift */; }; C600AB8329F807F60089107B /* DomainTransferService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C600AB8229F807F60089107B /* DomainTransferService.swift */; }; C600AB8829F807FF0089107B /* DomainTransferServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C600AB8729F807FF0089107B /* DomainTransferServiceProtocol.swift */; }; C600AB8D29F8085D0089107B /* MockDomainTransferService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C600AB8C29F8085D0089107B /* MockDomainTransferService.swift */; }; @@ -120,6 +122,13 @@ C6011AB628F4022300342666 /* DomainProfileCryptoSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6011AB528F4022300342666 /* DomainProfileCryptoSection.swift */; }; C6011ABD28F4045000342666 /* DomainProfileTopInfoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6011ABB28F4045000342666 /* DomainProfileTopInfoCell.swift */; }; C6011AC128F4045000342666 /* DomainProfileTopInfoCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C6011ABC28F4045000342666 /* DomainProfileTopInfoCell.xib */; }; + C60204182C61071B000B5553 /* DashedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C60204172C61071B000B5553 /* DashedProgressView.swift */; }; + C60204192C61071B000B5553 /* DashedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C60204172C61071B000B5553 /* DashedProgressView.swift */; }; + C602041A2C621728000B5553 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6960C1E2B19939E00B79E28 /* ViewController.swift */; }; + C602041C2C622851000B5553 /* PurchaseDomainsTitleViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C602041B2C622851000B5553 /* PurchaseDomainsTitleViewModifier.swift */; }; + C602041D2C622851000B5553 /* PurchaseDomainsTitleViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C602041B2C622851000B5553 /* PurchaseDomainsTitleViewModifier.swift */; }; + C602041F2C625315000B5553 /* PurchaseDomainsTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C602041E2C625315000B5553 /* PurchaseDomainsTitleView.swift */; }; + C60204202C625315000B5553 /* PurchaseDomainsTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C602041E2C625315000B5553 /* PurchaseDomainsTitleView.swift */; }; C60319952C2B0ED100A77109 /* FB_UD_MPC_MessageSigningType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C60319942C2B0ED100A77109 /* FB_UD_MPC_MessageSigningType.swift */; }; C60319962C2B0ED100A77109 /* FB_UD_MPC_MessageSigningType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C60319942C2B0ED100A77109 /* FB_UD_MPC_MessageSigningType.swift */; }; C60319982C2C1F7400A77109 /* BlockchainType+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C60319972C2C1F7400A77109 /* BlockchainType+Extension.swift */; }; @@ -408,7 +417,7 @@ C61808522B19BA750032E543 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6010D1E287FC99400FBD401 /* Task.swift */; }; C61808552B19BB040032E543 /* PreviewDomainTransferService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61808532B19BAD20032E543 /* PreviewDomainTransferService.swift */; }; C61808582B19BB3C0032E543 /* PreviewUDFeatureFlagsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61808562B19BB2F0032E543 /* PreviewUDFeatureFlagsService.swift */; }; - C618085A2B19BBEB0032E543 /* PurchaseSearchDomainsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DDF25E2B148300006D1F0B /* PurchaseSearchDomainsView.swift */; }; + C618085A2B19BBEB0032E543 /* PurchaseDomainsSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DDF25E2B148300006D1F0B /* PurchaseDomainsSearchView.swift */; }; C618085B2B19BBFE0032E543 /* UDTextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DDF24C2B147F1A006D1F0B /* UDTextFieldView.swift */; }; C618085C2B19BBFE0032E543 /* UDCollectionSectionBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DDF2402B147F19006D1F0B /* UDCollectionSectionBackgroundView.swift */; }; C618085E2B19BBFE0032E543 /* UDListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C655CA9A2B16E5BE00FDA063 /* UDListItemView.swift */; }; @@ -440,7 +449,6 @@ C61808792B19BC290032E543 /* ClearListBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69F99162A9F1264004B1958 /* ClearListBackground.swift */; }; C618087A2B19BC290032E543 /* SquareFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6289A462AAB06F9002C2DEC /* SquareFrame.swift */; }; C618087B2B19BC290032E543 /* UDSubtitleText.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DDF2582B147F5E006D1F0B /* UDSubtitleText.swift */; }; - C618087C2B19BC290032E543 /* SideInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69F99182A9F1264004B1958 /* SideInsets.swift */; }; C618087D2B19BC290032E543 /* AvatarStyleClipped.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69F99192A9F1264004B1958 /* AvatarStyleClipped.swift */; }; C618087E2B19BC380032E543 /* DomainAvatarImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6124CCB29223066005E6537 /* DomainAvatarImageView.swift */; }; C618087F2B19BC470032E543 /* Vibration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6456F2E2800520E00A517D5 /* Vibration.swift */; }; @@ -829,6 +837,14 @@ C6478B1829A36960006FADFE /* DeepLinksServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6478B1729A36960006FADFE /* DeepLinksServiceTests.swift */; }; C64836B8282BADB1007BD3F1 /* PullUpViewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64836B7282BADB1007BD3F1 /* PullUpViewService.swift */; }; C64836BD282BCAFB007BD3F1 /* PrimaryDangerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64836BC282BCAFB007BD3F1 /* PrimaryDangerButton.swift */; }; + C6489AC62C5D09BE004AE320 /* PurchaseDomainsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6489AC52C5D09BE004AE320 /* PurchaseDomainsViewModel.swift */; }; + C6489AC72C5D09C4004AE320 /* PurchaseDomainsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6489AC52C5D09BE004AE320 /* PurchaseDomainsViewModel.swift */; }; + C6489AC92C5D09F9004AE320 /* PurchaseDomainsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6489AC82C5D09F9004AE320 /* PurchaseDomainsRootView.swift */; }; + C6489ACA2C5D09F9004AE320 /* PurchaseDomainsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6489AC82C5D09F9004AE320 /* PurchaseDomainsRootView.swift */; }; + C6489ACC2C5D0A0C004AE320 /* PurchaseDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6489ACB2C5D0A0C004AE320 /* PurchaseDomains.swift */; }; + C6489ACD2C5D0A0C004AE320 /* PurchaseDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6489ACB2C5D0A0C004AE320 /* PurchaseDomains.swift */; }; + C6489ACF2C5D0DA9004AE320 /* PurchaseDomainsNavigationDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6489ACE2C5D0DA9004AE320 /* PurchaseDomainsNavigationDestination.swift */; }; + C6489AD02C5D0DA9004AE320 /* PurchaseDomainsNavigationDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6489ACE2C5D0DA9004AE320 /* PurchaseDomainsNavigationDestination.swift */; }; C64939EF2B63579700457363 /* HomeTabPullUpHandlerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64939EE2B63579700457363 /* HomeTabPullUpHandlerModifier.swift */; }; C64939F02B63579700457363 /* HomeTabPullUpHandlerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64939EE2B63579700457363 /* HomeTabPullUpHandlerModifier.swift */; }; C64939F22B6357A600457363 /* HomeTabRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64939F12B6357A600457363 /* HomeTabRouter.swift */; }; @@ -846,6 +862,14 @@ C64BCB712A6FA59200EF8B47 /* XMTPMessagingWebSocketsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64BCB702A6FA59200EF8B47 /* XMTPMessagingWebSocketsService.swift */; }; C64BCB762A6FA61F00EF8B47 /* XMTPServiceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64BCB752A6FA61F00EF8B47 /* XMTPServiceHelper.swift */; }; C64BCB7B2A6FB5B700EF8B47 /* MessagingChatMessageImageDataTypeDisplayInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64BCB7A2A6FB5B700EF8B47 /* MessagingChatMessageImageDataTypeDisplayInfo.swift */; }; + C64CFE452C63916600A35B9F /* PurchaseDomainsCartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64CFE442C63916600A35B9F /* PurchaseDomainsCartView.swift */; }; + C64CFE462C63916600A35B9F /* PurchaseDomainsCartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64CFE442C63916600A35B9F /* PurchaseDomainsCartView.swift */; }; + C64CFE482C6391FD00A35B9F /* PurchaseDomainsSearchResultRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64CFE472C6391FD00A35B9F /* PurchaseDomainsSearchResultRowView.swift */; }; + C64CFE492C6391FD00A35B9F /* PurchaseDomainsSearchResultRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64CFE472C6391FD00A35B9F /* PurchaseDomainsSearchResultRowView.swift */; }; + C64CFE4B2C63940C00A35B9F /* PurchaseSearchEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64CFE4A2C63940C00A35B9F /* PurchaseSearchEmptyView.swift */; }; + C64CFE4C2C63940C00A35B9F /* PurchaseSearchEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64CFE4A2C63940C00A35B9F /* PurchaseSearchEmptyView.swift */; }; + C64CFE4E2C63B03600A35B9F /* PurchaseDomainsCheckoutButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64CFE4D2C63B03600A35B9F /* PurchaseDomainsCheckoutButton.swift */; }; + C64CFE4F2C63B03600A35B9F /* PurchaseDomainsCheckoutButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64CFE4D2C63B03600A35B9F /* PurchaseDomainsCheckoutButton.swift */; }; C64F6C4F2C4E72A900D89FEF /* EthereumSendTransactionPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64F6C4E2C4E72A900D89FEF /* EthereumSendTransactionPayload.swift */; }; C64F6C502C4E72A900D89FEF /* EthereumSendTransactionPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64F6C4E2C4E72A900D89FEF /* EthereumSendTransactionPayload.swift */; }; C64F6C532C4FAF2200D89FEF /* FullMaintenanceModeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64F6C522C4FAF2200D89FEF /* FullMaintenanceModeView.swift */; }; @@ -888,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 */; }; @@ -975,12 +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 */; }; - C655CAAD2B16EFEB00FDA063 /* PurchaseDomainsCheckoutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C655CAAC2B16EFEB00FDA063 /* PurchaseDomainsCheckoutViewController.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 */; }; @@ -1010,6 +1035,24 @@ C661DC2B2C06DB6A00844AF5 /* MPCOnboardingPurchaseAlreadyHaveWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C661DC292C06DB6A00844AF5 /* MPCOnboardingPurchaseAlreadyHaveWalletViewController.swift */; }; C661DC2D2C06DBED00844AF5 /* PurchaseMPCWalletAlreadyHaveWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C661DC2C2C06DBED00844AF5 /* PurchaseMPCWalletAlreadyHaveWalletView.swift */; }; C661DC2E2C06DBED00844AF5 /* PurchaseMPCWalletAlreadyHaveWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C661DC2C2C06DBED00844AF5 /* PurchaseMPCWalletAlreadyHaveWalletView.swift */; }; + C6631BB22C69DFAC0045186D /* RecentDomainsToPurchaseSearchStorageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6631BB12C69DFAC0045186D /* RecentDomainsToPurchaseSearchStorageProtocol.swift */; }; + C6631BB32C69E0780045186D /* RecentDomainsToPurchaseSearchStorageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6631BB12C69DFAC0045186D /* RecentDomainsToPurchaseSearchStorageProtocol.swift */; }; + C6631BB52C69EA600045186D /* PurchaseDomainsSearchFiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6631BB42C69EA600045186D /* PurchaseDomainsSearchFiltersView.swift */; }; + C6631BB62C69EA600045186D /* PurchaseDomainsSearchFiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6631BB42C69EA600045186D /* PurchaseDomainsSearchFiltersView.swift */; }; + C6631BB82C69EDB50045186D /* TLDCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6631BB72C69EDB50045186D /* TLDCategory.swift */; }; + C6631BB92C69EDB50045186D /* TLDCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6631BB72C69EDB50045186D /* TLDCategory.swift */; }; + C6631BC42C6A45FC0045186D /* UDConfettiViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6631BC32C6A45FC0045186D /* UDConfettiViewModifier.swift */; }; + C6631BC52C6A45FC0045186D /* UDConfettiViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6631BC32C6A45FC0045186D /* UDConfettiViewModifier.swift */; }; + C6631BC82C6A46F70045186D /* PurchaseDomainsCompletedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6631BC72C6A46F70045186D /* PurchaseDomainsCompletedView.swift */; }; + C6631BC92C6A46F70045186D /* PurchaseDomainsCompletedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6631BC72C6A46F70045186D /* PurchaseDomainsCompletedView.swift */; }; + C6631BCB2C6B4B7F0045186D /* HomeWalletMintingInProgressSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6631BCA2C6B4B7F0045186D /* HomeWalletMintingInProgressSectionView.swift */; }; + C6631BCC2C6B4B7F0045186D /* HomeWalletMintingInProgressSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6631BCA2C6B4B7F0045186D /* HomeWalletMintingInProgressSectionView.swift */; }; + C6631BCE2C6B4C960045186D /* MintingDomainsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6631BCD2C6B4C960045186D /* MintingDomainsListView.swift */; }; + C6631BCF2C6B4C960045186D /* MintingDomainsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6631BCD2C6B4C960045186D /* MintingDomainsListView.swift */; }; + C6631BD22C6B73D60045186D /* IPVerificationServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6631BD12C6B73D60045186D /* IPVerificationServiceProtocol.swift */; }; + C6631BD32C6B73D60045186D /* IPVerificationServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6631BD12C6B73D60045186D /* IPVerificationServiceProtocol.swift */; }; + C6631BD52C6B73E00045186D /* IPVerificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6631BD42C6B73E00045186D /* IPVerificationService.swift */; }; + C6631BD92C6B73F30045186D /* PreviewIPVerificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6631BD72C6B73F30045186D /* PreviewIPVerificationService.swift */; }; C663538B294319B400EF1DC7 /* ScrollViewOffsetListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = C663538A294319B400EF1DC7 /* ScrollViewOffsetListener.swift */; }; C6637A6A2810074D0000AE56 /* SecurityWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6637A692810074D0000AE56 /* SecurityWindow.swift */; }; C6637A7128100D3A0000AE56 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6637A6F28100D3A0000AE56 /* LaunchViewController.swift */; }; @@ -1398,7 +1441,6 @@ C695C2682A57F99700B94DA8 /* PushChatsSecretKeysStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C695C2672A57F99700B94DA8 /* PushChatsSecretKeysStorage.swift */; }; C6960C1B2B19939E00B79E28 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6960C1A2B19939E00B79E28 /* AppDelegate.swift */; }; C6960C1D2B19939E00B79E28 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6960C1C2B19939E00B79E28 /* SceneDelegate.swift */; }; - C6960C1F2B19939E00B79E28 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6960C1E2B19939E00B79E28 /* ViewController.swift */; }; C6960C222B19939E00B79E28 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C6960C202B19939E00B79E28 /* Main.storyboard */; }; C6960C272B1993A100B79E28 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C6960C252B1993A100B79E28 /* LaunchScreen.storyboard */; }; C6960C2D2B19945000B79E28 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 306486892527253C00388026 /* Assets.xcassets */; }; @@ -1449,7 +1491,6 @@ C69F99232A9F1264004B1958 /* AttributedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69F99142A9F1264004B1958 /* AttributedText.swift */; }; C69F99242A9F1264004B1958 /* ClearListBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69F99162A9F1264004B1958 /* ClearListBackground.swift */; }; C69F99252A9F1264004B1958 /* UnstoppableListRowInset.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69F99172A9F1264004B1958 /* UnstoppableListRowInset.swift */; }; - C69F99262A9F1264004B1958 /* SideInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69F99182A9F1264004B1958 /* SideInsets.swift */; }; C69F99272A9F1264004B1958 /* AvatarStyleClipped.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69F99192A9F1264004B1958 /* AvatarStyleClipped.swift */; }; C69F99282A9F1264004B1958 /* AdaptiveSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69F991A2A9F1264004B1958 /* AdaptiveSheet.swift */; }; C69F99292A9F1264004B1958 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69F991C2A9F1264004B1958 /* View.swift */; }; @@ -2039,8 +2080,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 */; }; @@ -2399,19 +2438,12 @@ C6D6478F2B1EE73D00D724AC /* CoinRecordsServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D6478E2B1EE73D00D724AC /* CoinRecordsServiceProtocol.swift */; }; C6D647902B1EE73D00D724AC /* CoinRecordsServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D6478E2B1EE73D00D724AC /* CoinRecordsServiceProtocol.swift */; }; C6D647922B1EE75A00D724AC /* PreviewCoinRecordsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D647912B1EE75A00D724AC /* PreviewCoinRecordsService.swift */; }; - C6D647942B1EEEBE00D724AC /* PurchaseDomainDomainProfileViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D647932B1EEEBE00D724AC /* PurchaseDomainDomainProfileViewPresenter.swift */; }; - C6D647952B1EEEBE00D724AC /* PurchaseDomainDomainProfileViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D647932B1EEEBE00D724AC /* PurchaseDomainDomainProfileViewPresenter.swift */; }; C6D6479A2B1EF4B400D724AC /* PurchaseDomainProfileTopInfoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D647982B1EF4B400D724AC /* PurchaseDomainProfileTopInfoCell.swift */; }; C6D6479B2B1EF4B400D724AC /* PurchaseDomainProfileTopInfoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D647982B1EF4B400D724AC /* PurchaseDomainProfileTopInfoCell.swift */; }; C6D6479C2B1EF4B400D724AC /* PurchaseDomainProfileTopInfoCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C6D647992B1EF4B400D724AC /* PurchaseDomainProfileTopInfoCell.xib */; }; C6D6479D2B1EF4B400D724AC /* PurchaseDomainProfileTopInfoCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C6D647992B1EF4B400D724AC /* PurchaseDomainProfileTopInfoCell.xib */; }; C6D6479F2B1EF4F200D724AC /* BaseDomainProfileTopInfoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D6479E2B1EF4F200D724AC /* BaseDomainProfileTopInfoCell.swift */; }; C6D647A02B1EF4F200D724AC /* BaseDomainProfileTopInfoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D6479E2B1EF4F200D724AC /* BaseDomainProfileTopInfoCell.swift */; }; - C6D647A22B1F187600D724AC /* UDRouter+Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D647A12B1F187600D724AC /* UDRouter+Common.swift */; }; - C6D647A32B1F188400D724AC /* UDRouter+Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D647A12B1F187600D724AC /* UDRouter+Common.swift */; }; - C6D647A42B1F189400D724AC /* PurchaseDomainsNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DDF2622B14848F006D1F0B /* PurchaseDomainsNavigationController.swift */; }; - C6D647A52B1F189800D724AC /* PurchaseDomainsCheckoutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C655CAAC2B16EFEB00FDA063 /* PurchaseDomainsCheckoutViewController.swift */; }; - C6D647A62B1F189B00D724AC /* PurchaseSearchDomainsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DDF2602B14840D006D1F0B /* PurchaseSearchDomainsViewController.swift */; }; C6D647A72B1F18AE00D724AC /* HappyEndViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3018AEC127EB2CC200E8C37D /* HappyEndViewController.swift */; }; C6D647A82B1F18B000D724AC /* HappyEnd.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3018AEBC27EB2AF200E8C37D /* HappyEnd.storyboard */; }; C6D647A92B1F18B300D724AC /* BaseHappyEndViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C630BC762B18376100E29318 /* BaseHappyEndViewPresenter.swift */; }; @@ -2480,9 +2512,7 @@ C6DDF2592B147F5E006D1F0B /* UDTitleText.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DDF2572B147F5D006D1F0B /* UDTitleText.swift */; }; C6DDF25A2B147F5E006D1F0B /* UDSubtitleText.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DDF2582B147F5E006D1F0B /* UDSubtitleText.swift */; }; C6DDF25D2B148294006D1F0B /* DomainToPurchaseSuggestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DDF25C2B148294006D1F0B /* DomainToPurchaseSuggestion.swift */; }; - C6DDF25F2B148300006D1F0B /* PurchaseSearchDomainsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DDF25E2B148300006D1F0B /* PurchaseSearchDomainsView.swift */; }; - C6DDF2612B14840D006D1F0B /* PurchaseSearchDomainsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DDF2602B14840D006D1F0B /* PurchaseSearchDomainsViewController.swift */; }; - C6DDF2632B14848F006D1F0B /* PurchaseDomainsNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DDF2622B14848F006D1F0B /* PurchaseDomainsNavigationController.swift */; }; + C6DDF25F2B148300006D1F0B /* PurchaseDomainsSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DDF25E2B148300006D1F0B /* PurchaseDomainsSearchView.swift */; }; C6DDF26A2B148A31006D1F0B /* PositionObservingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DDF2682B148A31006D1F0B /* PositionObservingView.swift */; }; C6DDF26B2B148A31006D1F0B /* OffsetObservingScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DDF2692B148A31006D1F0B /* OffsetObservingScrollView.swift */; }; C6DEA1642BD8D1FA00838215 /* ActivateMPCWalletViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DEA1632BD8D1FA00838215 /* ActivateMPCWalletViewModel.swift */; }; @@ -2805,6 +2835,7 @@ 30EFBBDB27F4754E00B8D667 /* MainButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainButton.swift; sourceTree = ""; }; 30F526452785BB22004C7AB6 /* ResolutionInitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolutionInitTests.swift; sourceTree = ""; }; 30F6A88F277BB5310086D70B /* UnsConfigManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsConfigManagerTests.swift; sourceTree = ""; }; + C6008B2A2C60F77700F218B9 /* NavigationPopGestureDisabler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationPopGestureDisabler.swift; sourceTree = ""; }; C600AB8229F807F60089107B /* DomainTransferService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainTransferService.swift; sourceTree = ""; }; C600AB8729F807FF0089107B /* DomainTransferServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainTransferServiceProtocol.swift; sourceTree = ""; }; C600AB8C29F8085D0089107B /* MockDomainTransferService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDomainTransferService.swift; sourceTree = ""; }; @@ -2815,6 +2846,9 @@ C6011AB528F4022300342666 /* DomainProfileCryptoSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainProfileCryptoSection.swift; sourceTree = ""; }; C6011ABB28F4045000342666 /* DomainProfileTopInfoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainProfileTopInfoCell.swift; sourceTree = ""; }; C6011ABC28F4045000342666 /* DomainProfileTopInfoCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DomainProfileTopInfoCell.xib; sourceTree = ""; }; + C60204172C61071B000B5553 /* DashedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashedProgressView.swift; sourceTree = ""; }; + C602041B2C622851000B5553 /* PurchaseDomainsTitleViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsTitleViewModifier.swift; sourceTree = ""; }; + C602041E2C625315000B5553 /* PurchaseDomainsTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsTitleView.swift; sourceTree = ""; }; C60319942C2B0ED100A77109 /* FB_UD_MPC_MessageSigningType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FB_UD_MPC_MessageSigningType.swift; sourceTree = ""; }; C60319972C2C1F7400A77109 /* BlockchainType+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BlockchainType+Extension.swift"; sourceTree = ""; }; C603A53528522C0D003962E3 /* IgnoreFailureArrayElement+PropertyWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IgnoreFailureArrayElement+PropertyWrapper.swift"; sourceTree = ""; }; @@ -3310,6 +3344,10 @@ C6478B1729A36960006FADFE /* DeepLinksServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinksServiceTests.swift; sourceTree = ""; }; C64836B7282BADB1007BD3F1 /* PullUpViewService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullUpViewService.swift; sourceTree = ""; }; C64836BC282BCAFB007BD3F1 /* PrimaryDangerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryDangerButton.swift; sourceTree = ""; }; + C6489AC52C5D09BE004AE320 /* PurchaseDomainsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsViewModel.swift; sourceTree = ""; }; + C6489AC82C5D09F9004AE320 /* PurchaseDomainsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsRootView.swift; sourceTree = ""; }; + C6489ACB2C5D0A0C004AE320 /* PurchaseDomains.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseDomains.swift; sourceTree = ""; }; + C6489ACE2C5D0DA9004AE320 /* PurchaseDomainsNavigationDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsNavigationDestination.swift; sourceTree = ""; }; C64939EE2B63579700457363 /* HomeTabPullUpHandlerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTabPullUpHandlerModifier.swift; sourceTree = ""; }; C64939F12B6357A600457363 /* HomeTabRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTabRouter.swift; sourceTree = ""; }; C64939F42B6373C700457363 /* UIWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIWindow.swift; sourceTree = ""; }; @@ -3320,6 +3358,10 @@ C64BCB702A6FA59200EF8B47 /* XMTPMessagingWebSocketsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMTPMessagingWebSocketsService.swift; sourceTree = ""; }; C64BCB752A6FA61F00EF8B47 /* XMTPServiceHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMTPServiceHelper.swift; sourceTree = ""; }; C64BCB7A2A6FB5B700EF8B47 /* MessagingChatMessageImageDataTypeDisplayInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagingChatMessageImageDataTypeDisplayInfo.swift; sourceTree = ""; }; + C64CFE442C63916600A35B9F /* PurchaseDomainsCartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsCartView.swift; sourceTree = ""; }; + C64CFE472C6391FD00A35B9F /* PurchaseDomainsSearchResultRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsSearchResultRowView.swift; sourceTree = ""; }; + C64CFE4A2C63940C00A35B9F /* PurchaseSearchEmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseSearchEmptyView.swift; sourceTree = ""; }; + C64CFE4D2C63B03600A35B9F /* PurchaseDomainsCheckoutButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsCheckoutButton.swift; sourceTree = ""; }; C64F6C4E2C4E72A900D89FEF /* EthereumSendTransactionPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthereumSendTransactionPayload.swift; sourceTree = ""; }; C64F6C522C4FAF2200D89FEF /* FullMaintenanceModeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullMaintenanceModeView.swift; sourceTree = ""; }; C64F6C552C4FD48000D89FEF /* MaintenanceModeData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaintenanceModeData.swift; sourceTree = ""; }; @@ -3344,6 +3386,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 = ""; }; @@ -3405,12 +3449,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 = ""; }; - C655CAAC2B16EFEB00FDA063 /* PurchaseDomainsCheckoutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsCheckoutViewController.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 = ""; }; @@ -3429,6 +3470,16 @@ C661DC242C06D0F900844AF5 /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = ""; }; C661DC292C06DB6A00844AF5 /* MPCOnboardingPurchaseAlreadyHaveWalletViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCOnboardingPurchaseAlreadyHaveWalletViewController.swift; sourceTree = ""; }; C661DC2C2C06DBED00844AF5 /* PurchaseMPCWalletAlreadyHaveWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseMPCWalletAlreadyHaveWalletView.swift; sourceTree = ""; }; + C6631BB12C69DFAC0045186D /* RecentDomainsToPurchaseSearchStorageProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentDomainsToPurchaseSearchStorageProtocol.swift; sourceTree = ""; }; + C6631BB42C69EA600045186D /* PurchaseDomainsSearchFiltersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsSearchFiltersView.swift; sourceTree = ""; }; + C6631BB72C69EDB50045186D /* TLDCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLDCategory.swift; sourceTree = ""; }; + C6631BC32C6A45FC0045186D /* UDConfettiViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDConfettiViewModifier.swift; sourceTree = ""; }; + C6631BC72C6A46F70045186D /* PurchaseDomainsCompletedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsCompletedView.swift; sourceTree = ""; }; + C6631BCA2C6B4B7F0045186D /* HomeWalletMintingInProgressSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeWalletMintingInProgressSectionView.swift; sourceTree = ""; }; + C6631BCD2C6B4C960045186D /* MintingDomainsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MintingDomainsListView.swift; sourceTree = ""; }; + C6631BD12C6B73D60045186D /* IPVerificationServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPVerificationServiceProtocol.swift; sourceTree = ""; }; + C6631BD42C6B73E00045186D /* IPVerificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPVerificationService.swift; sourceTree = ""; }; + C6631BD72C6B73F30045186D /* PreviewIPVerificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewIPVerificationService.swift; sourceTree = ""; }; C663538A294319B400EF1DC7 /* ScrollViewOffsetListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewOffsetListener.swift; sourceTree = ""; }; C6637A692810074D0000AE56 /* SecurityWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityWindow.swift; sourceTree = ""; }; C6637A6F28100D3A0000AE56 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = ""; }; @@ -3715,7 +3766,6 @@ C69F99142A9F1264004B1958 /* AttributedText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributedText.swift; sourceTree = ""; }; C69F99162A9F1264004B1958 /* ClearListBackground.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClearListBackground.swift; sourceTree = ""; }; C69F99172A9F1264004B1958 /* UnstoppableListRowInset.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnstoppableListRowInset.swift; sourceTree = ""; }; - C69F99182A9F1264004B1958 /* SideInsets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SideInsets.swift; sourceTree = ""; }; C69F99192A9F1264004B1958 /* AvatarStyleClipped.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarStyleClipped.swift; sourceTree = ""; }; C69F991A2A9F1264004B1958 /* AdaptiveSheet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptiveSheet.swift; sourceTree = ""; }; C69F991C2A9F1264004B1958 /* View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; @@ -4022,11 +4072,9 @@ C6D6478A2B1EE5B600D724AC /* PreviewDomainProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewDomainProfile.swift; sourceTree = ""; }; C6D6478E2B1EE73D00D724AC /* CoinRecordsServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinRecordsServiceProtocol.swift; sourceTree = ""; }; C6D647912B1EE75A00D724AC /* PreviewCoinRecordsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewCoinRecordsService.swift; sourceTree = ""; }; - C6D647932B1EEEBE00D724AC /* PurchaseDomainDomainProfileViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseDomainDomainProfileViewPresenter.swift; sourceTree = ""; }; C6D647982B1EF4B400D724AC /* PurchaseDomainProfileTopInfoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseDomainProfileTopInfoCell.swift; sourceTree = ""; }; C6D647992B1EF4B400D724AC /* PurchaseDomainProfileTopInfoCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PurchaseDomainProfileTopInfoCell.xib; sourceTree = ""; }; C6D6479E2B1EF4F200D724AC /* BaseDomainProfileTopInfoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseDomainProfileTopInfoCell.swift; sourceTree = ""; }; - C6D647A12B1F187600D724AC /* UDRouter+Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UDRouter+Common.swift"; sourceTree = ""; }; C6D6E4D32817BD4B008C66BB /* TutorialViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialViewPresenter.swift; sourceTree = ""; }; C6D6E4D82817BDBD008C66BB /* OnboardingCreateWalletPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingCreateWalletPresenter.swift; sourceTree = ""; }; C6D6E4DD2817BF09008C66BB /* OnboardingProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingProtocols.swift; sourceTree = ""; }; @@ -4078,9 +4126,7 @@ C6DDF2572B147F5D006D1F0B /* UDTitleText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UDTitleText.swift; sourceTree = ""; }; C6DDF2582B147F5E006D1F0B /* UDSubtitleText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UDSubtitleText.swift; sourceTree = ""; }; C6DDF25C2B148294006D1F0B /* DomainToPurchaseSuggestion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainToPurchaseSuggestion.swift; sourceTree = ""; }; - C6DDF25E2B148300006D1F0B /* PurchaseSearchDomainsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseSearchDomainsView.swift; sourceTree = ""; }; - C6DDF2602B14840D006D1F0B /* PurchaseSearchDomainsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseSearchDomainsViewController.swift; sourceTree = ""; }; - C6DDF2622B14848F006D1F0B /* PurchaseDomainsNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsNavigationController.swift; sourceTree = ""; }; + C6DDF25E2B148300006D1F0B /* PurchaseDomainsSearchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseDomainsSearchView.swift; sourceTree = ""; }; C6DDF2682B148A31006D1F0B /* PositionObservingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PositionObservingView.swift; sourceTree = ""; }; C6DDF2692B148A31006D1F0B /* OffsetObservingScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OffsetObservingScrollView.swift; sourceTree = ""; }; C6DEA1632BD8D1FA00838215 /* ActivateMPCWalletViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivateMPCWalletViewModel.swift; sourceTree = ""; }; @@ -5191,6 +5237,7 @@ C6C8F9512B21858200A9834D /* PreviewValet.swift */, C6C8F98A2B21895A00A9834D /* PreviewWCConnectedAppsStorageV2.swift */, C63393012B73734500941C9D /* PreviewWalletPortfolioRecordsStorage.swift */, + C6631BD72C6B73F30045186D /* PreviewIPVerificationService.swift */, ); path = Entities; sourceTree = ""; @@ -5649,7 +5696,6 @@ isa = PBXGroup; children = ( C63A350928F0071B0057D34B /* DomainProfileViewPresenter.swift */, - C6D647932B1EEEBE00D724AC /* PurchaseDomainDomainProfileViewPresenter.swift */, C63A350A28F0071B0057D34B /* DomainProfileViewController.swift */, C63A350B28F0071B0057D34B /* DomainProfileViewController.xib */, C6D868182BF3218100EEBCFC /* EnterDomainEmailView.swift */, @@ -6220,8 +6266,12 @@ C655CA9E2B16E81700FDA063 /* Search */ = { isa = PBXGroup; children = ( - C6DDF2602B14840D006D1F0B /* PurchaseSearchDomainsViewController.swift */, - C6DDF25E2B148300006D1F0B /* PurchaseSearchDomainsView.swift */, + C6DDF25E2B148300006D1F0B /* PurchaseDomainsSearchView.swift */, + C64CFE472C6391FD00A35B9F /* PurchaseDomainsSearchResultRowView.swift */, + C64CFE442C63916600A35B9F /* PurchaseDomainsCartView.swift */, + C64CFE4A2C63940C00A35B9F /* PurchaseSearchEmptyView.swift */, + C64CFE4D2C63B03600A35B9F /* PurchaseDomainsCheckoutButton.swift */, + C6631BB42C69EA600045186D /* PurchaseDomainsSearchFiltersView.swift */, ); path = Search; sourceTree = ""; @@ -6229,12 +6279,10 @@ C655CA9F2B16E82700FDA063 /* Checkout */ = { isa = PBXGroup; children = ( - C655CAAC2B16EFEB00FDA063 /* PurchaseDomainsCheckoutViewController.swift */, C655CAA22B16E82700FDA063 /* PurchaseDomainsCheckoutView.swift */, - C655CAA02B16E82700FDA063 /* PurchaseDomainsSelectDiscountsView.swift */, C655CAA12B16E82700FDA063 /* PurchaseDomainsEnterDiscountCodeView.swift */, C655CAA32B16E82700FDA063 /* PurchaseDomainsSelectWalletView.swift */, - C655CAA42B16E82700FDA063 /* PurchaseDomainsEnterZIPCodeView.swift */, + C651C6DA2C66016C0076F631 /* PurchaseDomainsOrderSummaryView.swift */, ); path = Checkout; sourceTree = ""; @@ -6266,6 +6314,23 @@ path = AlreadyHaveWallet; sourceTree = ""; }; + C6631BC62C6A46E00045186D /* Purchased */ = { + isa = PBXGroup; + children = ( + C6631BC72C6A46F70045186D /* PurchaseDomainsCompletedView.swift */, + ); + path = Purchased; + sourceTree = ""; + }; + C6631BD02C6B73C20045186D /* IPVerificationService */ = { + isa = PBXGroup; + children = ( + C6631BD12C6B73D60045186D /* IPVerificationServiceProtocol.swift */, + C6631BD42C6B73E00045186D /* IPVerificationService.swift */, + ); + path = IPVerificationService; + sourceTree = ""; + }; C6637A6E28100D180000AE56 /* LaunchViewController */ = { isa = PBXGroup; children = ( @@ -6450,6 +6515,7 @@ C6952A3C2BC5084000F4B475 /* NetworkBearerAuthorisationHeaderBuilder.swift */, C66804AC280D93E5007E6390 /* ObjectWithAttributedString.swift */, C67DE1442B984031002374CE /* RecentGlobalSearchProfilesStorageProtocol.swift */, + C6631BB12C69DFAC0045186D /* RecentDomainsToPurchaseSearchStorageProtocol.swift */, C663538A294319B400EF1DC7 /* ScrollViewOffsetListener.swift */, ); path = Protocols; @@ -6654,6 +6720,7 @@ isa = PBXGroup; children = ( C669B4542B748214001D4788 /* HomeView.swift */, + C6631BCD2C6B4C960045186D /* MintingDomainsListView.swift */, C6102FC52B6A338D0098AF75 /* HomeWebAccountView */, C6B65F4E2B54DA73006D1812 /* HomeWalletView */, ); @@ -6790,7 +6857,7 @@ C6A2D527284DC45B00327C47 /* PermissionsService.swift */, C60982092822D92300546392 /* ToastMessageService.swift */, C624D79E281BCB9A00F55530 /* UDRouter.swift */, - C6D647A12B1F187600D724AC /* UDRouter+Common.swift */, + C6631BD02C6B73C20045186D /* IPVerificationService */, C61DB1132B95BCE600CDA243 /* DomainProfilesService */, C63F1D0328AF31D5000A5C12 /* AnalyticsService */, C61236EE292CADE1002BA97B /* AppGroupsBridge */, @@ -7194,6 +7261,7 @@ C607A5D32B32A02A0088ECF3 /* CloseButtonView.swift */, C639F39B2C32D133008CC1CB /* ConnectCurveLine */, C639F3952C32B9C3008CC1CB /* ConnectTransactionSign.swift */, + C60204172C61071B000B5553 /* DashedProgressView.swift */, C6DDF2412B147F1A006D1F0B /* FlowLayoutView.swift */, C6DA0B712B7BC1C1009920B5 /* ListVGrid.swift */, C63AD0992B95657C00BF8C83 /* LineView.swift */, @@ -7202,6 +7270,7 @@ C6DDF2682B148A31006D1F0B /* PositionObservingView.swift */, C6E856282BBE983900AAFDE4 /* PresentAsModalPreviewView.swift */, C640D61D2C4944AB006B21C3 /* SelectionPopoverView */, + C651C6DD2C6609D00076F631 /* ToastView.swift */, C6DDF2422B147F1A006D1F0B /* UDCollectionListRowButton.swift */, C6DDF2402B147F19006D1F0B /* UDCollectionSectionBackgroundView.swift */, C655CA9A2B16E5BE00FDA063 /* UDListItemView.swift */, @@ -7255,18 +7324,19 @@ C67681332BEC9ECD0093AFA0 /* UDListItemInCollectionButtonPaddingModifier.swift */, C62900EA2BAAC135008B35A2 /* NavigationTopSafeAreaOffset.swift */, C685803A2B357EF400907568 /* NavBarVisibleModifier.swift */, + C6008B2A2C60F77700F218B9 /* NavigationPopGestureDisabler.swift */, C6C837A02B5FF307000A6AF5 /* TabBarVisibleModifier.swift */, C663A4D82B7F2FAC0099BCE8 /* PresentationStyleCheckerModifier.swift */, C6BF6BD92B8EE724006CC2BD /* PassViewAnalyticsDetailsViewModifier.swift */, C631DFA02B7B1ED500040221 /* InfiniteRotationModifier.swift */, C6493A012B63A8B700457363 /* TrackingPressingStateModifier.swift */, C6D645792B1D7C2F00D724AC /* PullUpError.swift */, - C69F99182A9F1264004B1958 /* SideInsets.swift */, C6170EC32B7A0075008E9C93 /* ShowingWalletSelectionModifier.swift */, C6A231F42BEB22230037E093 /* TextAttributesModifier.swift */, C617FDB22B5919CC00B93433 /* OnAppearanceChange.swift */, C6102FC22B6A12010098AF75 /* OnButtonTapModifier.swift */, C6289A462AAB06F9002C2DEC /* SquareFrame.swift */, + C6631BC32C6A45FC0045186D /* UDConfettiViewModifier.swift */, C6DDF2582B147F5E006D1F0B /* UDSubtitleText.swift */, C6DDF2572B147F5D006D1F0B /* UDTitleText.swift */, C69F99172A9F1264004B1958 /* UnstoppableListRowInset.swift */, @@ -7647,6 +7717,7 @@ C669C37E29124C2600837F21 /* SocialsType.swift */, C69F99552A9F167F004B1958 /* DomainProfileSocialAccount.swift */, C6BF6BDB2B8F11CC006CC2BD /* TaskWithDeadline.swift */, + C6631BB72C69EDB50045186D /* TLDCategory.swift */, C6109EA128E6B1CC0027D5D8 /* Toast.swift */, 307EC19A25383EF600D62BA1 /* TransactionItem.swift */, C6A359342BB6BB2000B1209A /* TxHash.swift */, @@ -7743,6 +7814,7 @@ C6B65F8F2B565C70006D1812 /* HomeWalletSortingSelectorView.swift */, C6B65F552B54E42C006D1812 /* HomeWalletTokenRowView.swift */, C6B659232B68E49500CA6A68 /* HomeWalletTokenNotMatchingRowView.swift */, + C6631BCA2C6B4B7F0045186D /* HomeWalletMintingInProgressSectionView.swift */, ); path = Subviews; sourceTree = ""; @@ -8236,9 +8308,15 @@ C6DDF25B2B14821F006D1F0B /* PurchaseDomains */ = { isa = PBXGroup; children = ( - C6DDF2622B14848F006D1F0B /* PurchaseDomainsNavigationController.swift */, - C655CA9F2B16E82700FDA063 /* Checkout */, + C6489ACB2C5D0A0C004AE320 /* PurchaseDomains.swift */, + C6489ACE2C5D0DA9004AE320 /* PurchaseDomainsNavigationDestination.swift */, + C6489AC52C5D09BE004AE320 /* PurchaseDomainsViewModel.swift */, + C6489AC82C5D09F9004AE320 /* PurchaseDomainsRootView.swift */, + C602041B2C622851000B5553 /* PurchaseDomainsTitleViewModifier.swift */, + C602041E2C625315000B5553 /* PurchaseDomainsTitleView.swift */, C655CA9E2B16E81700FDA063 /* Search */, + C655CA9F2B16E82700FDA063 /* Checkout */, + C6631BC62C6A46E00045186D /* Purchased */, ); path = PurchaseDomains; sourceTree = ""; @@ -9171,20 +9249,22 @@ C6C9959E289D313D00367362 /* CInteractiveTransitioningController.swift in Sources */, C6DDF25A2B147F5E006D1F0B /* UDSubtitleText.swift in Sources */, C6DDF24F2B147F1A006D1F0B /* UDCollectionListRowButton.swift in Sources */, - C6DDF2612B14840D006D1F0B /* PurchaseSearchDomainsViewController.swift in Sources */, C669C3842912576D00837F21 /* UITextField.swift in Sources */, C6534A8F2BBFBA10008EEBB5 /* HomeExploreFollowerRelationshipTypePickerView.swift in Sources */, C67681282BEC59C90093AFA0 /* FB_UD_MPCNetworkFee.swift in Sources */, + C6631BD52C6B73E00045186D /* IPVerificationService.swift in Sources */, C6B659242B68E49500CA6A68 /* HomeWalletTokenNotMatchingRowView.swift in Sources */, C6124CCC29223066005E6537 /* DomainAvatarImageView.swift in Sources */, C69F99292A9F1264004B1958 /* View.swift in Sources */, C64F6C532C4FAF2200D89FEF /* FullMaintenanceModeView.swift in Sources */, + C64CFE482C6391FD00A35B9F /* PurchaseDomainsSearchResultRowView.swift in Sources */, C671CD062BC66D6E005DA2FB /* EcomPurchaseMPCWalletService.swift in Sources */, C6952A372BC500D200F4B475 /* FB_UD_MPCAccountAsset.swift in Sources */, C639F3992C32D10C008CC1CB /* ConnectCurveLine.swift in Sources */, C6D6E5062817E5ED008C66BB /* OnboardingAddWalletPresenter.swift in Sources */, C69F99782A9F2206004B1958 /* PublicProfileFollowersView.swift in Sources */, C617D0B32B9C390800607555 /* PublicProfileTitleView.swift in Sources */, + C6489AC92C5D09F9004AE320 /* PurchaseDomainsRootView.swift in Sources */, C60982302823EF3900546392 /* AddWalletNavigationController.swift in Sources */, C6538DBC2A32168500B63E84 /* KeychainPGPKeysStorage.swift in Sources */, C6124CD129223374005E6537 /* DomainProfileFetchFailedActionCoverViewPresenter.swift in Sources */, @@ -9214,6 +9294,7 @@ C692C36A282EADA400C31393 /* SelectAppearanceThemePullUpView.swift in Sources */, C6ECBF7A28D2C86F00E94309 /* ReverseResolutionTransactionInProgressViewPresenter.swift in Sources */, C6952A292BC4E9F700F4B475 /* MPCWalletsServiceProtocol.swift in Sources */, + C6631BC82C6A46F70045186D /* PurchaseDomainsCompletedView.swift in Sources */, C645131128095BA400413C51 /* ExistingUsersTutorialViewController.swift in Sources */, C6D6E4F72817E045008C66BB /* OnboardingConfirmWordsPresenter.swift in Sources */, C62900F82BAAC922008B35A2 /* UDNumberButtonView.swift in Sources */, @@ -9234,6 +9315,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 */, @@ -9269,7 +9351,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 */, @@ -9277,6 +9358,7 @@ C650E2962B9E9878002C120A /* UDButtonStyle+ViewModifier.swift in Sources */, C630E4B92B7F5BAC008F3269 /* Padding.swift in Sources */, C68E91C62BE517ED00576358 /* EcommAuthenticator.swift in Sources */, + C6631BCE2C6B4C960045186D /* MintingDomainsListView.swift in Sources */, C63B1F342A4595D000B1D7D1 /* Base64DataTransformer.swift in Sources */, C64513072808089400413C51 /* UserDefaults.swift in Sources */, C6C9959A289D313D00367362 /* CNavigationControllerChild.swift in Sources */, @@ -9345,7 +9427,9 @@ C62C8DC928DAF8E500967D0A /* PaymentTransactionGasOnlyCostView.swift in Sources */, 296BB61F2A309DD90068EEEC /* Encrypting.swift in Sources */, C6F4332F2BB15641000C5E46 /* DomainsTLDGroup.swift in Sources */, + C6631BC42C6A45FC0045186D /* UDConfettiViewModifier.swift in Sources */, C630C72F2BD779A500AC1662 /* RestoreWalletType.swift in Sources */, + C6631BCB2C6B4B7F0045186D /* HomeWalletMintingInProgressSectionView.swift in Sources */, C68F157C2BD76A650049BFA2 /* MPCWalletActivationError.swift in Sources */, C6526DED29D2E12000D6F2EB /* ParkedDomainsFoundInAppViewPresenter.swift in Sources */, C630C7392BD7836C00AC1662 /* OnboardingAddWalletView.swift in Sources */, @@ -9392,6 +9476,7 @@ C6102FC02B69361D0098AF75 /* HomeWalletExpandableSectionHeaderView.swift in Sources */, C6F9FBB82A25C30C00102F81 /* MessagingWebSocketsServiceProtocol.swift in Sources */, C6098315282B6F7100546392 /* EmptyRootNavigationController.swift in Sources */, + C6631BB82C69EDB50045186D /* TLDCategory.swift in Sources */, C62900EB2BAAC135008B35A2 /* NavigationTopSafeAreaOffset.swift in Sources */, C6B40E902B5F83370038CEB0 /* UserProfileSelectionRowView.swift in Sources */, C6952A432BC50D1D00F4B475 /* FireblocksWalletRPCMessageHandler.swift in Sources */, @@ -9492,12 +9577,14 @@ C64F6C4F2C4E72A900D89FEF /* EthereumSendTransactionPayload.swift in Sources */, C6ED320D295E8EDE00BC6919 /* NonEmptyArray.swift in Sources */, C63DBCCB2BE49376008F3D2C /* NetworkService+Common.swift in Sources */, + C6489AC62C5D09BE004AE320 /* PurchaseDomainsViewModel.swift in Sources */, C6BEC9532C004D2A00F21FB6 /* RequestsLimitController.swift in Sources */, C63F1CFF28AD099C000A5C12 /* EmptyRootCNavigationController.swift in Sources */, 309BD85F265679EA00CB0C49 /* PaymentConfiguration.swift in Sources */, C6B761FC2BB403F700773943 /* HomeActivity.swift in Sources */, C64939FC2B637DDB00457363 /* ReverseResolutionSelectionView.swift in Sources */, C69F99412A9F1478004B1958 /* DomainProfileBadgeDisplayInfo.swift in Sources */, + C602041C2C622851000B5553 /* PurchaseDomainsTitleViewModifier.swift in Sources */, C69F99242A9F1264004B1958 /* ClearListBackground.swift in Sources */, C6456F0B27FF299800A517D5 /* UIView.swift in Sources */, C6FFBADE2B833C3600CB442D /* ChatListChatRowView.swift in Sources */, @@ -9515,7 +9602,6 @@ C6B761CF2BB3C11800773943 /* HomeTab.swift in Sources */, C6109E3728E5AB310027D5D8 /* AppLaunchServiceProtocol.swift in Sources */, C6534A702BBFB9EF008EEBB5 /* MPCOnboardingEnterCodeViewController.swift in Sources */, - C6DDF2632B14848F006D1F0B /* PurchaseDomainsNavigationController.swift in Sources */, C672FD75286061E300D27078 /* WalletBackUpPassword.swift in Sources */, C6B65F7E2B55153E006D1812 /* HomeWalletView+Entities.swift in Sources */, C6D5DAD428374B4600379C38 /* ManageMultiChainDomainAddressesViewController.swift in Sources */, @@ -9551,7 +9637,6 @@ C64BCB712A6FA59200EF8B47 /* XMTPMessagingWebSocketsService.swift in Sources */, C66A26E628B4C22F005470B9 /* LoadPaginatedFetchableOperation.swift in Sources */, C6124CDC2923C7FF005E6537 /* DomainProfileSectionUIChangeFailedItem.swift in Sources */, - C69F99262A9F1264004B1958 /* SideInsets.swift in Sources */, C6F433422BB25DDD000C5E46 /* ConfirmSendAssetSendingInfoView.swift in Sources */, C6D646032B1DBFEF00D724AC /* WalletConnectServiceConnectionListener.swift in Sources */, C62247BF283D28C0002A0CBD /* ApplePayButton.swift in Sources */, @@ -9618,6 +9703,7 @@ C6124CEB29252E8F005E6537 /* DomainProfileSignExternalWalletViewPresenter.swift in Sources */, C692C351282EA63100C31393 /* AppearanceSettingsViewPresenter.swift in Sources */, C65A869C2A5BB7E400CD66D2 /* MessagingChatUserPullUpSelectionItem.swift in Sources */, + C60204182C61071B000B5553 /* DashedProgressView.swift in Sources */, C6670A0F28D46D6B00415EBB /* String+SHA3.swift in Sources */, C609822A2823EB1E00546392 /* BaseCreateWalletPresenter.swift in Sources */, C6534A952BBFBA10008EEBB5 /* HomeExploreSuggestedProfileRowView.swift in Sources */, @@ -9625,6 +9711,7 @@ C6E69FF1288AD9DB000A8346 /* NotificationsService.swift in Sources */, C64939EF2B63579700457363 /* HomeTabPullUpHandlerModifier.swift in Sources */, 30BA2B45252C4E2B0097817E /* DomainItem.swift in Sources */, + C6631BB52C69EA600045186D /* PurchaseDomainsSearchFiltersView.swift in Sources */, C6C4217829371446005B791B /* DomainProfileTutorialViewController.swift in Sources */, 307852642771FB030039FF40 /* DeepLinks.swift in Sources */, C6D3B9BA2A6EB8F80091B279 /* PushMessagingChannelsAPIService.swift in Sources */, @@ -9685,6 +9772,7 @@ 309FDFBC265E7C0C00AE53D3 /* StripePaymentHelper.swift in Sources */, C6E0DC5D2B71E2410069E100 /* ShareWalletInfoView.swift in Sources */, C6C8F8292B217D7C00A9834D /* WCRegistryWalletProxy.swift in Sources */, + C64CFE4E2C63B03600A35B9F /* PurchaseDomainsCheckoutButton.swift in Sources */, C6952A482BC526F800F4B475 /* FB_UD_MPCValetStorage.swift in Sources */, C6D6476B2B1EDFAA00D724AC /* WalletConnectClientUIHandler.swift in Sources */, C69F99752A9F2206004B1958 /* ProfileFollowerImageLoader.swift in Sources */, @@ -9708,6 +9796,7 @@ C6B366D72A1C715000DE512B /* PushRESTAPIService.swift in Sources */, C630E4A92B7F4959008F3269 /* ChatViewModel.swift in Sources */, C6F060C42ACAA35B00BA2E7E /* MessagingService+Tools.swift in Sources */, + C6631BB22C69DFAC0045186D /* RecentDomainsToPurchaseSearchStorageProtocol.swift in Sources */, C6534AA12BBFBA10008EEBB5 /* HomeExploreDomainSearchTypePickerView.swift in Sources */, C67213E22BAA967C0075B9C7 /* SendCryptoAssetRootView.swift in Sources */, C69F997B2A9F2206004B1958 /* PublicProfileViewModel.swift in Sources */, @@ -9749,7 +9838,6 @@ C6D646F42B1ED65200D724AC /* Extension-String+Common.swift in Sources */, C6B6B02D296D309C00D4E30F /* CGSize.swift in Sources */, C6DDF2522B147F1A006D1F0B /* UDIconButtonView.swift in Sources */, - C655CAAD2B16EFEB00FDA063 /* PurchaseDomainsCheckoutViewController.swift in Sources */, C607A5CF2B3288A40088ECF3 /* AmplitudeAnalyticsService.swift in Sources */, C6A2D54A284E3ABD00327C47 /* QRScannerDomainInfoView.swift in Sources */, C6A2D5CD2850EDCD00327C47 /* ConnectServerRequestConfirmationView.swift in Sources */, @@ -9817,7 +9905,6 @@ C6109E8E28E6AA660027D5D8 /* AppContextProtocol.swift in Sources */, C6DDF25D2B148294006D1F0B /* DomainToPurchaseSuggestion.swift in Sources */, C6B65F842B5517FB006D1812 /* HomeWalletNFTsCollectionSectionView.swift in Sources */, - C6D647A22B1F187600D724AC /* UDRouter+Common.swift in Sources */, C6010D1A287FBEFC00FBD401 /* DomainsPurchasedDetails.swift in Sources */, C6B2E8872BE9DB3300CF4BCA /* CopyMultichainWalletAddressesPullUpView.swift in Sources */, C671CD122BC67570005DA2FB /* EcomPurchaseInteractionService.swift in Sources */, @@ -9829,6 +9916,7 @@ C688C17C2B8443CA00BD233A /* ChatListEmptyStateView.swift in Sources */, 306665E12778AE74005F6F55 /* UnsConfigManager.swift in Sources */, C6B65FAD2B57A5BB006D1812 /* DismissIndicatorView.swift in Sources */, + C6631BD22C6B73D60045186D /* IPVerificationServiceProtocol.swift in Sources */, 30AE8521257837B5003A0142 /* NetworkConfig.swift in Sources */, C6B762052BB4067600773943 /* WalletTransactionDisplayInfoListItemView.swift in Sources */, C6D5DAC1283629D800379C38 /* MockDomainRecordsService.swift in Sources */, @@ -9867,7 +9955,7 @@ C6D646EB2B1ED55F00D724AC /* NFCService+Entities.swift in Sources */, C6D2F15328757C25005F4F2E /* UIImage+SVG.swift in Sources */, C65DEA7329B5837600FF142B /* WalletConnectExternalWalletHandler.swift in Sources */, - C6DDF25F2B148300006D1F0B /* PurchaseSearchDomainsView.swift in Sources */, + C6DDF25F2B148300006D1F0B /* PurchaseDomainsSearchView.swift in Sources */, C6952A502BC531E000F4B475 /* MPCWalletMetadata.swift in Sources */, C6D8FF242B82F8FE0094A21E /* RemoteContentMessageRowView.swift in Sources */, C6D646BF2B1ED1BE00D724AC /* SaveDomainImageDescription.swift in Sources */, @@ -9878,13 +9966,16 @@ C6109E4528E5ABFE0027D5D8 /* TestsEnum.swift in Sources */, C6A89C572B31645D008AB043 /* DefaultHotFeaturesSuggestionsFetcher.swift in Sources */, C661DC252C06D0F900844AF5 /* CircularProgressView.swift in Sources */, + C64CFE452C63916600A35B9F /* PurchaseDomainsCartView.swift in Sources */, C66804AD280D93E5007E6390 /* ObjectWithAttributedString.swift in Sources */, C6D3B9BF2A6EBC2B0091B279 /* PushServiceHelper.swift in Sources */, C63095F52B0DA66400205054 /* FirebaseAuthUtilitiesProtocol.swift in Sources */, C606B73C2863545000DC562B /* MintingDomainsStorage.swift in Sources */, + C64CFE4B2C63940C00A35B9F /* PurchaseSearchEmptyView.swift in Sources */, C679B611291A591500F543A7 /* DomainProfileUpdatingRecordsCell.swift in Sources */, 299713C328F692AB00743003 /* ABIValue.swift in Sources */, C617FDAA2B590B6700B93433 /* NavigationViewWithCustomTitle.swift in Sources */, + C6489ACF2C5D0DA9004AE320 /* PurchaseDomainsNavigationDestination.swift in Sources */, C6D6479F2B1EF4F200D724AC /* BaseDomainProfileTopInfoCell.swift in Sources */, C6B435002A52884F00BC644B /* MessagingPrivateChatBlockingStatus.swift in Sources */, C664B6602914FAC100A76154 /* DomainProfileNoSocialsCell.swift in Sources */, @@ -9909,6 +10000,7 @@ C6B6B023296D2CDC00D4E30F /* MintingDomain.swift in Sources */, C6D6479A2B1EF4B400D724AC /* PurchaseDomainProfileTopInfoCell.swift in Sources */, C6D5DAD028374B4600379C38 /* ManageMultiChainDomainAddressesViewPresenter.swift in Sources */, + C6489ACC2C5D0A0C004AE320 /* PurchaseDomains.swift in Sources */, C6DDF2512B147F1A006D1F0B /* UDButtonView.swift in Sources */, C631DFA72B7B530800040221 /* MessagingChatMessageReactionTypeDisplayInfo.swift in Sources */, 306486D225273DD400388026 /* Storage.swift in Sources */, @@ -9980,6 +10072,7 @@ C669C37F29124C2600837F21 /* SocialsType.swift in Sources */, C69F992B2A9F1264004B1958 /* Font.swift in Sources */, C6A2D5AB285062C200327C47 /* BaseSignTransactionView.swift in Sources */, + C602041F2C625315000B5553 /* PurchaseDomainsTitleView.swift in Sources */, C6C995DA289D313E00367362 /* CNavigationBar.swift in Sources */, C688C1762B8439F400BD233A /* ChatListChannelRowView.swift in Sources */, C679B5ED2918BCDF00F543A7 /* DomainProfileWeb3WebsiteLoadingCell.swift in Sources */, @@ -10014,7 +10107,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 */, @@ -10088,6 +10180,7 @@ C6B761CC2BB3BEB700773943 /* HomeActivityView.swift in Sources */, C6D647442B1EDA5900D724AC /* SignPaymentTransactionUIConfiguration.swift in Sources */, C6A231FF2BEB52CB0037E093 /* WalletSourceImageView.swift in Sources */, + C6008B2B2C60F77700F218B9 /* NavigationPopGestureDisabler.swift in Sources */, C6124CE12924C236005E6537 /* CachedDomainProfileInfo.swift in Sources */, C69F992C2A9F1264004B1958 /* Color.swift in Sources */, C63095EE2B0DA66400205054 /* FirebaseAuthService.swift in Sources */, @@ -10162,6 +10255,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 */, @@ -10199,7 +10293,6 @@ C664310128D8B85800A9C734 /* ConnectExternalWalletViewController.swift in Sources */, C650E2902B9E97D8002C120A /* UDButtonStyle+VerySmall.swift in Sources */, C6F4333C2BB25B1C000C5E46 /* ConfirmSendAssetReceiverInfoView.swift in Sources */, - C6D647942B1EEEBE00D724AC /* PurchaseDomainDomainProfileViewPresenter.swift in Sources */, C6C8F8252B217CDD00A9834D /* UDWallet+RecoveryType.swift in Sources */, C6B6B048297039E500D4E30F /* DateFormattingService.swift in Sources */, C6109EA728E6B55A0027D5D8 /* AppearanceTheme.swift in Sources */, @@ -10317,6 +10410,7 @@ C61808442B19B8E20032E543 /* PreviewImageLoadingService.swift in Sources */, C640F36D2C071266009EB0F9 /* MPCWalletTakeoverState.swift in Sources */, C6D645E32B1DBD9000D724AC /* BaseCollectionViewControllerProtocol.swift in Sources */, + C602041A2C621728000B5553 /* ViewController.swift in Sources */, C671CD1C2BC680A1005DA2FB /* PurchasedDomainsWalletDescription.swift in Sources */, C6170EC22B79E8E9008E9C93 /* UDListSectionView.swift in Sources */, C6C8F9792B2188FA00A9834D /* QRScannerPermissionsView.swift in Sources */, @@ -10332,6 +10426,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 */, @@ -10351,6 +10446,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 */, @@ -10453,6 +10549,7 @@ C6D646702B1ED11B00D724AC /* CryptoEditingGroupedRecordsChangesCalculator.swift in Sources */, C618085C2B19BBFE0032E543 /* UDCollectionSectionBackgroundView.swift in Sources */, C6C8F8EA2B21836400A9834D /* LoadingParkedDomainsViewPresenter.swift in Sources */, + C64CFE492C6391FD00A35B9F /* PurchaseDomainsSearchResultRowView.swift in Sources */, C6C8F81F2B217CC000A9834D /* OnboardingCreateWalletPresenter.swift in Sources */, C6D6470C2B1ED7D000D724AC /* MessagingChatMessageDisplayType.swift in Sources */, C632BE4E2BA599E900C95B2D /* FB_UD_MPCConnectionNetworkService.swift in Sources */, @@ -10482,7 +10579,6 @@ C6960C3A2B19976E00B79E28 /* PreviewUser.swift in Sources */, C67B1DE32BFF017700C2A4DA /* ReconnectMPCWalletFlowNavigationDestination.swift in Sources */, C6D646A02B1ED15A00D724AC /* PublicProfilePullUpHeaderView.swift in Sources */, - C618087C2B19BC290032E543 /* SideInsets.swift in Sources */, C6C8F84D2B217E9600A9834D /* RevealRecoveryPhrasePresenter.swift in Sources */, C6C8F8E92B21836400A9834D /* ParkedDomainsFoundInAppViewPresenter.swift in Sources */, C6D646762B1ED11B00D724AC /* CryptoEditingGroupedRecord.swift in Sources */, @@ -10532,6 +10628,7 @@ C6952A242BC4E33000F4B475 /* MPCWalletProvider.swift in Sources */, C6C8F9402B2183FA00A9834D /* UIUserInterfaceStyle.swift in Sources */, C6D647922B1EE75A00D724AC /* PreviewCoinRecordsService.swift in Sources */, + C64CFE4F2C63B03600A35B9F /* PurchaseDomainsCheckoutButton.swift in Sources */, C6D646C52B1ED24300D724AC /* GroupedCoinRecord.swift in Sources */, C68A5AD02BD8DADE003702A2 /* MPCActivateWalletInAppView.swift in Sources */, C6D8FF252B82F8FE0094A21E /* RemoteContentMessageRowView.swift in Sources */, @@ -10539,6 +10636,7 @@ C6D646322B1DC43100D724AC /* SecondaryButton.swift in Sources */, C6D6473C2B1ED9EF00D724AC /* PaymentTransactionCostView.swift in Sources */, C6D6461D2B1DC1F900D724AC /* PreviewStripePaymentHelper.swift in Sources */, + C6631BC52C6A45FC0045186D /* UDConfettiViewModifier.swift in Sources */, C6A231F62BEB22230037E093 /* TextAttributesModifier.swift in Sources */, C640F37D2C08643B009EB0F9 /* PurchaseMPCWalletAlreadyHaveWalletInAppView.swift in Sources */, C6C8F81B2B217CC000A9834D /* BaseCreateWalletPresenter.swift in Sources */, @@ -10546,10 +10644,12 @@ C631DFA22B7B1ED500040221 /* InfiniteRotationModifier.swift in Sources */, C61807F32B19A7960032E543 /* PreviewWalletConnectServiceV2.swift in Sources */, C6D6467F2B1ED12900D724AC /* DomainProfileSectionChangeUIDescription.swift in Sources */, + C6631BB62C69EA600045186D /* PurchaseDomainsSearchFiltersView.swift in Sources */, C6C8F8D92B21835800A9834D /* ProtectWalletViewController.swift in Sources */, C6D646CF2B1ED28400D724AC /* PrimaryWhiteButton.swift in Sources */, C6952A382BC500D200F4B475 /* FB_UD_MPCAccountAsset.swift in Sources */, C6D6477A2B1EE0E600D724AC /* UnstoppableImagePicker.swift in Sources */, + C6631BCC2C6B4B7F0045186D /* HomeWalletMintingInProgressSectionView.swift in Sources */, C6D646362B1DC44C00D724AC /* SecondaryDangerButton.swift in Sources */, C6D6477C2B1EE0ED00D724AC /* CropImageViewController.swift in Sources */, C6D6460B2B1DC06B00D724AC /* OnboardingData.swift in Sources */, @@ -10619,6 +10719,7 @@ C6DEA1742BD8D68100838215 /* MPCEnterCodeInAppView.swift in Sources */, C618085E2B19BBFE0032E543 /* UDListItemView.swift in Sources */, C6C8F8402B217E9600A9834D /* EnterBackupViewController.swift in Sources */, + C6631BD92C6B73F30045186D /* PreviewIPVerificationService.swift in Sources */, C6C8F95A2B21865A00A9834D /* CodeVerificationCharacterView.swift in Sources */, C61807FB2B19A81D0032E543 /* ObjectWithAttributedString.swift in Sources */, C618087A2B19BC290032E543 /* SquareFrame.swift in Sources */, @@ -10636,6 +10737,7 @@ C61808172B19AADA0032E543 /* FirebaseDomain.swift in Sources */, C6A89C5E2B316540008AB043 /* HotFeaturesSuggestionsFetcher.swift in Sources */, C6E8562A2BBE983900AAFDE4 /* PresentAsModalPreviewView.swift in Sources */, + C60204202C625315000B5553 /* PurchaseDomainsTitleView.swift in Sources */, C6D645CD2B1DBD4100D724AC /* SetupReverseResolutionNavBarPopAnimation.swift in Sources */, C6A3592F2BB68D9B00B1209A /* ConcreteCryptoSenderProtocol.swift in Sources */, C6E28F3E2B33486700026E6C /* PreviewDomainProfileActionCover.swift in Sources */, @@ -10654,6 +10756,7 @@ C6534AA22BBFBA10008EEBB5 /* HomeExploreDomainSearchTypePickerView.swift in Sources */, C6D646E22B1ED49D00D724AC /* TableViewSelectionCell.swift in Sources */, C6C8F8262B217CDD00A9834D /* UDWallet+RecoveryType.swift in Sources */, + C6489AD02C5D0DA9004AE320 /* PurchaseDomainsNavigationDestination.swift in Sources */, C6534AC62BBFBAF5008EEBB5 /* UserProfilesService.swift in Sources */, C671CD1F2BC680E2005DA2FB /* Ecom.swift in Sources */, C6D647182B1ED83800D724AC /* CollectionTextFooterReusableView.swift in Sources */, @@ -10698,13 +10801,13 @@ C617FDB42B5919CC00B93433 /* OnAppearanceChange.swift in Sources */, C6C8F8EB2B21836400A9834D /* ParkedDomainsFoundOnboardingViewPresenter.swift in Sources */, C6FFBAE22B834EF700CB442D /* HomeChatNavigationDestination.swift in Sources */, - C6960C1F2B19939E00B79E28 /* ViewController.swift in Sources */, C6C8F8382B217E9600A9834D /* EnterBackupOnboardingPresenter.swift in Sources */, C6D6466C2B1ED11B00D724AC /* CachedDomainProfileInfo.swift in Sources */, C61807DD2B19A4D50032E543 /* PreviewUDWallet.swift in Sources */, C6C8F85A2B217FA600A9834D /* CloudStorage.swift in Sources */, C688C17A2B843BC000BD233A /* ChatListRequestsRowView.swift in Sources */, C6C8F8732B21822700A9834D /* OnboardingProtocols.swift in Sources */, + C6631BB92C69EDB50045186D /* TLDCategory.swift in Sources */, C6C8F8392B217E9600A9834D /* CreateBackupPasswordForNewLocalWalletPresenter.swift in Sources */, C6D646082B1DC02800D724AC /* UDSubtitleLabel.swift in Sources */, C61808792B19BC290032E543 /* ClearListBackground.swift in Sources */, @@ -10714,6 +10817,7 @@ C6C8F9832B2188FA00A9834D /* ConnectedAppsListViewController.swift in Sources */, C61807E72B19A61D0032E543 /* DomainTransactionsServiceProtocol.swift in Sources */, C6D646F92B1ED74500D724AC /* LoadingIndicatorView.swift in Sources */, + C6489ACA2C5D09F9004AE320 /* PurchaseDomainsRootView.swift in Sources */, C6FFBAD62B832B3900CB442D /* ChatListDataTypeSelectorView.swift in Sources */, C618087F2B19BC470032E543 /* Vibration.swift in Sources */, C6B65FAB2B57A582006D1812 /* UserProfileSelectionView.swift in Sources */, @@ -10768,7 +10872,6 @@ C6D647062B1ED7CA00D724AC /* MessagingImageLoader.swift in Sources */, C6D647832B1EE23000D724AC /* UITableViewCell.swift in Sources */, C6D1E74D2BA17E2700738365 /* WalletsDataNetworkServiceProtocol.swift in Sources */, - C6D647A32B1F188400D724AC /* UDRouter+Common.swift in Sources */, C617FDA52B58E79E00B93433 /* PreviewWalletsDataService.swift in Sources */, C61807FC2B19A83E0032E543 /* QRCodeService.swift in Sources */, C6D646C42B1ED23700D724AC /* CurrencyImageLoader.swift in Sources */, @@ -10944,6 +11047,7 @@ C6C8F8742B21822700A9834D /* TutorialStepViewController.swift in Sources */, C6A359332BB699FC00B1209A /* ConfirmSendTokenDataModel.swift in Sources */, C621A4432BB1137900CB5CB9 /* QRScannerState.swift in Sources */, + C6489AC72C5D09C4004AE320 /* PurchaseDomainsViewModel.swift in Sources */, C62900FC2BAACCD2008B35A2 /* UDNumberPadView.swift in Sources */, C6C8F82A2B217D7C00A9834D /* WCRegistryWalletProxy.swift in Sources */, C6D647042B1ED7C300D724AC /* MessagingGroupChatDetails.swift in Sources */, @@ -10951,7 +11055,6 @@ C6C8F83A2B217E9600A9834D /* OnboardingRecoveryPhrasePresenter.swift in Sources */, C6C8F81D2B217CC000A9834D /* OnboardingAddWalletPresenter.swift in Sources */, C6C8F86A2B2181AE00A9834D /* WalletDisplayInfo.swift in Sources */, - C6D647A52B1F189800D724AC /* PurchaseDomainsCheckoutViewController.swift in Sources */, C6D647532B1EDB7C00D724AC /* SelectorButton.swift in Sources */, C640F3632C0709C1009EB0F9 /* MPCTakeoverCredentials.swift in Sources */, C6C8F8AB2B2182CF00A9834D /* EnterEmailViewController.swift in Sources */, @@ -11005,6 +11108,7 @@ C67DE1462B984031002374CE /* RecentGlobalSearchProfilesStorageProtocol.swift in Sources */, C61807B32B199DE50032E543 /* UDGradientCoverView.swift in Sources */, C618086B2B19BC0C0032E543 /* EasySkeleton.swift in Sources */, + C602041D2C622851000B5553 /* PurchaseDomainsTitleViewModifier.swift in Sources */, C63AAC522C0594EB001E7F23 /* PreviewEcomMPCPriceFetcher.swift in Sources */, C6D646EE2B1ED5AF00D724AC /* BaseAddWalletPresenter.swift in Sources */, C61807B92B199EFF0032E543 /* CAAnimation.swift in Sources */, @@ -11027,6 +11131,7 @@ C6D6460C2B1DC07200D724AC /* FirebaseDomainDisplayInfo.swift in Sources */, C6952A352BC4FFC200F4B475 /* FB_UD_MPCAccount.swift in Sources */, C61808102B19AA4C0032E543 /* LinkPresentationService.swift in Sources */, + C6631BB32C69E0780045186D /* RecentDomainsToPurchaseSearchStorageProtocol.swift in Sources */, C6D6469A2B1ED14E00D724AC /* ManageMultiChainDomainAddressesViewPresenter.swift in Sources */, C630C73D2BD7838900AC1662 /* OnboardingAddWalletType.swift in Sources */, C61808342B19AEEC0032E543 /* PublishingAppStorage.swift in Sources */, @@ -11036,7 +11141,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 */, @@ -11111,7 +11215,6 @@ C6F433302BB15641000C5E46 /* DomainsTLDGroup.swift in Sources */, C6D647632B1EDED500D724AC /* ResizableRoundedWalletBadgeImageView.swift in Sources */, C61808362B19AF0E0032E543 /* UILabel.swift in Sources */, - C6D647952B1EEEBE00D724AC /* PurchaseDomainDomainProfileViewPresenter.swift in Sources */, C6C8F9272B2183B700A9834D /* OnboardingPasscodeViewController.swift in Sources */, C61808582B19BB3C0032E543 /* PreviewUDFeatureFlagsService.swift in Sources */, C630E4AA2B7F4959008F3269 /* ChatViewModel.swift in Sources */, @@ -11124,6 +11227,7 @@ C6FAED872B8C684700CC1844 /* MessageMentionString.swift in Sources */, C6D646722B1ED11B00D724AC /* DomainProfileUpdatingRecordsData.swift in Sources */, C6D646FB2B1ED74E00D724AC /* GhostTertiaryWhiteButton.swift in Sources */, + C6008B2C2C60F77700F218B9 /* NavigationPopGestureDisabler.swift in Sources */, C61808702B19BC100032E543 /* ForEach+Skeleton.swift in Sources */, C6534AA02BBFBA10008EEBB5 /* HomeExploreUserWalletDomainsView.swift in Sources */, C65CEB8F2B67537900A13B34 /* SwiftUIViewPaymentHandler.swift in Sources */, @@ -11134,13 +11238,13 @@ C6960C602B199AC900B79E28 /* Version.swift in Sources */, C6952A5A2BC65B2500F4B475 /* WalletType.swift in Sources */, C6D646532B1ED10100D724AC /* DomainProfileUpdatingRecordsCell.swift in Sources */, + C64CFE4C2C63940C00A35B9F /* PurchaseSearchEmptyView.swift in Sources */, C6D646892B1ED12D00D724AC /* DomainProfileGeneralInfoSection.swift in Sources */, C6C8F86F2B21822700A9834D /* OnboardingHappyEndViewPresenter.swift in Sources */, C6D646882B1ED12D00D724AC /* DomainProfileEmptySection.swift in Sources */, C6D646A72B1ED15A00D724AC /* ProfileFollowerImageLoader.swift in Sources */, C61808662B19BC050032E543 /* UDButtonView.swift in Sources */, C618083C2B19AFB70032E543 /* PreviewNotificationsService.swift in Sources */, - C6D647A42B1F189400D724AC /* PurchaseDomainsNavigationController.swift in Sources */, C6952A302BC4EBB800F4B475 /* MPCWalletProviderSubServiceProtocol.swift in Sources */, C6D646752B1ED11B00D724AC /* DomainProfileSectionType.swift in Sources */, C6D647112B1ED7D000D724AC /* MessagingChatMessageImageBase64TypeDisplayInfo.swift in Sources */, @@ -11161,6 +11265,7 @@ C6DEA16B2BD8D2AB00838215 /* ActivateMPCWalletFlowNavigationDestination.swift in Sources */, C6D646EF2B1ED5B200D724AC /* AddWalletViewController.swift in Sources */, C6534AAE2BBFBA10008EEBB5 /* HomeExploreNavigationDestination.swift in Sources */, + C6631BC92C6A46F70045186D /* PurchaseDomainsCompletedView.swift in Sources */, C6C8F82E2B217DE300A9834D /* ExternalWalletMake.swift in Sources */, C61807D02B19A4040032E543 /* AnalyticsServiceEnvironment.swift in Sources */, C6C8F87E2B21827700A9834D /* LoginWithEmailViewController.swift in Sources */, @@ -11183,7 +11288,6 @@ C6D645DB2B1DBD4D00D724AC /* NavBarItemsTransitionPerformer.swift in Sources */, C617FDA82B58E7BB00B93433 /* WalletsDataServiceProtocol.swift in Sources */, C618086D2B19BC0C0032E543 /* HexagonShape.swift in Sources */, - C6D647A62B1F189B00D724AC /* PurchaseSearchDomainsViewController.swift in Sources */, C630C7372BD781E900AC1662 /* OnboardingAddWalletViewController.swift in Sources */, C6D645C62B1DBD3900D724AC /* CNavigationBarContentView.swift in Sources */, C68F156E2BD659740049BFA2 /* MPCActivateWalletEnterView.swift in Sources */, @@ -11191,6 +11295,7 @@ C61808722B19BC150032E543 /* Color.swift in Sources */, C61808162B19AAB70032E543 /* FirebaseAuthenticationServiceProtocol.swift in Sources */, C618081F2B19AB160032E543 /* PurchaseDomainsPreferencesStorageEnvironmentKey.swift in Sources */, + C6489ACD2C5D0A0C004AE320 /* PurchaseDomains.swift in Sources */, C6C8F8AF2B2182CF00A9834D /* EnterEmailVerificationCodeViewController.swift in Sources */, C617FD9F2B58DBCA00B93433 /* WalletsDataServiceEnvironmentKey.swift in Sources */, C60C59B62B47FC0900A2522C /* LoginProvider.swift in Sources */, @@ -11258,18 +11363,21 @@ C6C8F82F2B217E7F00A9834D /* AppUpdatedRequired.swift in Sources */, C688C1902B8474D000BD233A /* ChannelFeedRowView.swift in Sources */, C61808752B19BC290032E543 /* UDTitleText.swift in Sources */, + C64CFE462C63916600A35B9F /* PurchaseDomainsCartView.swift in Sources */, C6D646512B1ED10100D724AC /* DomainProfileTopInfoCell.swift in Sources */, C6C8F9732B2188FA00A9834D /* QRScannerSightView.swift in Sources */, C63AAC552C05B067001E7F23 /* MPCWalletPurchasingState.swift in Sources */, C6B65F9F2B57876F006D1812 /* HomeWalletNFTCellView.swift in Sources */, - C618085A2B19BBEB0032E543 /* PurchaseSearchDomainsView.swift in Sources */, + C618085A2B19BBEB0032E543 /* PurchaseDomainsSearchView.swift in Sources */, C6FEA9182BEB700E004FD740 /* SettingsCollectionViewCell.swift in Sources */, + C60204192C61071B000B5553 /* DashedProgressView.swift in Sources */, C6450CD32BEA0C3E0010F0B8 /* ShareWalletAssetInfoView.swift in Sources */, C6D645CB2B1DBD3C00D724AC /* CNavigationControllerChildTransitioning.swift in Sources */, C61808092B19A9EE0032E543 /* AppLaunchServiceProtocol.swift in Sources */, C6C8F8942B21829000A9834D /* AppearanceSettingsViewController.swift in Sources */, C6D647242B1ED91F00D724AC /* PullUpViewService+ExternalWallets.swift in Sources */, C61808732B19BC150032E543 /* View.swift in Sources */, + C6631BCF2C6B4C960045186D /* MintingDomainsListView.swift in Sources */, C6D6471E2B1ED88F00D724AC /* Date.swift in Sources */, C6F433402BB25BD4000C5E46 /* ConfirmSendTokenViewsBuilderProtocol.swift in Sources */, C67B1DE92BFF077000C2A4DA /* MPCEnterCodeReconnectView.swift in Sources */, @@ -11290,6 +11398,7 @@ C6D647022B1ED7C300D724AC /* MessagingChatConversationState.swift in Sources */, C6D647542B1EDBAE00D724AC /* WalletInfoBadgeView.swift in Sources */, C6C8F9702B21882F00A9834D /* WalletConnectServiceV2Protocol.swift in Sources */, + C6631BD32C6B73D60045186D /* IPVerificationServiceProtocol.swift in Sources */, C6D645F02B1DBEC200D724AC /* TitleVisibilityAfterLimitNavBarScrollingBehaviour.swift in Sources */, C6C8F8432B217E9600A9834D /* RestoreWalletViewController.swift in Sources */, C6C8F8AD2B2182CF00A9834D /* NoDomainsToMintViewPresenter.swift in Sources */, @@ -11334,7 +11443,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/AppContext/AppContextProtocol.swift b/unstoppable-ios-app/domains-manager-ios/AppContext/AppContextProtocol.swift index 8358f0dbf..b627e2316 100644 --- a/unstoppable-ios-app/domains-manager-ios/AppContext/AppContextProtocol.swift +++ b/unstoppable-ios-app/domains-manager-ios/AppContext/AppContextProtocol.swift @@ -46,6 +46,7 @@ protocol AppContextProtocol { var walletTransactionsService: WalletTransactionsServiceProtocol { get } var mpcWalletsService: MPCWalletsServiceProtocol { get } var ecomPurchaseMPCWalletService: EcomPurchaseMPCWalletServiceProtocol { get } + var ipVerificationService: IPVerificationServiceProtocol { get } var persistedProfileSignaturesStorage: PersistedSignaturesStorageProtocol { get } diff --git a/unstoppable-ios-app/domains-manager-ios/AppContext/GeneralAppContext.swift b/unstoppable-ios-app/domains-manager-ios/AppContext/GeneralAppContext.swift index d8d2c61a6..852959581 100644 --- a/unstoppable-ios-app/domains-manager-ios/AppContext/GeneralAppContext.swift +++ b/unstoppable-ios-app/domains-manager-ios/AppContext/GeneralAppContext.swift @@ -59,6 +59,7 @@ final class GeneralAppContext: AppContextProtocol { private(set) lazy var linkPresentationService: LinkPresentationServiceProtocol = LinkPresentationService() private(set) lazy var domainTransferService: DomainTransferServiceProtocol = DomainTransferService() private(set) lazy var hotFeatureSuggestionsService: HotFeatureSuggestionsServiceProtocol = HotFeatureSuggestionsService(fetcher: DefaultHotFeaturesSuggestionsFetcher()) + private(set) lazy var ipVerificationService: IPVerificationServiceProtocol = IPVerificationService() init() { authentificationService = AuthentificationService() diff --git a/unstoppable-ios-app/domains-manager-ios/AppContext/MockContext.swift b/unstoppable-ios-app/domains-manager-ios/AppContext/MockContext.swift index c7660bcbe..95192079a 100644 --- a/unstoppable-ios-app/domains-manager-ios/AppContext/MockContext.swift +++ b/unstoppable-ios-app/domains-manager-ios/AppContext/MockContext.swift @@ -73,6 +73,8 @@ final class MockContext: AppContextProtocol { func createStripeInstance(amount: Int, using secret: String) -> StripeServiceProtocol { MockStripeService(amount: amount) } + + private(set) lazy var ipVerificationService: IPVerificationServiceProtocol = IPVerificationService() } diff --git a/unstoppable-ios-app/domains-manager-ios/Entities/DefaultAppVersionFetcher.swift b/unstoppable-ios-app/domains-manager-ios/Entities/DefaultAppVersionFetcher.swift index a687b78cc..8575c1c8a 100644 --- a/unstoppable-ios-app/domains-manager-ios/Entities/DefaultAppVersionFetcher.swift +++ b/unstoppable-ios-app/domains-manager-ios/Entities/DefaultAppVersionFetcher.swift @@ -25,6 +25,8 @@ struct DefaultAppVersionFetcher: AppVersionApi { dotcoinDeprecationReleased: response.dotcoinDeprecationReleased, mobileUnsReleaseVersion: response.mobileUnsReleaseVersion, tlds: response.tlds, + tldsToPurchase: response.tldsToPurchase, + dnsTlds: response.dnsTlds, limits: response.limits) return appVersion } else { 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..4b29f4fb2 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 @@ -19,15 +19,27 @@ extension MockEntitiesFabric { let tlds: [String] = ["x", "nft", "unstoppable"] for tld in tlds { + + + for i in 0..<5 { + var state: DomainDisplayInfo.State = .default + if i == 0 { + state = .minting + } let domain = DomainDisplayInfo(name: "oleg_\(i)_\(ownerWallet.last ?? "a").\(tld)", ownerWallet: ownerWallet, blockchain: .Matic, + state: state, isSetForRR: i == 0) domains.append(domain) } for i in 0..<5 { + var state: DomainDisplayInfo.State = .default + if i == 0 { + state = .minting + } var name = "subdomain_\(i).oleg_0.\(tld)" if i == 3 { name = "long_long_long_long_long_" + name @@ -35,6 +47,7 @@ extension MockEntitiesFabric { let domain = DomainDisplayInfo(name: name, ownerWallet: ownerWallet, blockchain: .Matic, + state: state, isSetForRR: false) domains.append(domain) } @@ -109,6 +122,39 @@ 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: "oleg2.x", + price: 10000, + metadata: nil, + isTaken: false, + isAbleToPurchase: true), + DomainToPurchase(name: "oleg3.x", + price: 10000, + metadata: nil, + isTaken: false, + isAbleToPurchase: true), + DomainToPurchase(name: "oleg4.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/TLDCategory.swift b/unstoppable-ios-app/domains-manager-ios/Entities/TLDCategory.swift new file mode 100644 index 000000000..fe921d409 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Entities/TLDCategory.swift @@ -0,0 +1,36 @@ +// +// TLDCategory.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 12.08.2024. +// + +import SwiftUI + +enum TLDCategory: CaseIterable { + case uns + case ens + case dns + + var icon: Image { + switch self { + case .uns: + return .unsTLDLogo + case .ens: + return .ensTLDLogo + case .dns: + return .dnsTLDLogo + } + } + + static func categoryFor(tld: String) -> TLDCategory { + switch tld { + case Constants.ensDomainTLD: + return .ens + case _ where Constants.dnsDomainTLDs.contains(tld): + return .dns + default: + return .uns + } + } +} 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/Entities/Version.swift b/unstoppable-ios-app/domains-manager-ios/Entities/Version.swift index 4a663bfc5..021d118ba 100644 --- a/unstoppable-ios-app/domains-manager-ios/Entities/Version.swift +++ b/unstoppable-ios-app/domains-manager-ios/Entities/Version.swift @@ -85,6 +85,8 @@ struct AppVersionAPIResponse: Decodable { let polygonClaimingReleased: Bool let mintingZilTldOnPolygonReleased: Bool let tlds: [String] + let tldsToPurchase: [String] + let dnsTlds: [String] var dotcoinDeprecationReleased: Bool? var mobileUnsReleaseVersion: String? var limits: AppConfigurationLimits? @@ -111,5 +113,7 @@ struct AppVersionInfo: Codable { "nft", "dao", "zil"] + var tldsToPurchase: [String]? + var dnsTlds: [String]? var limits: AppConfigurationLimits? } 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 b84a03c5a..fc3e990c3 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 @@ -395,6 +395,7 @@ extension String { static let pluralNProfilesFound = "SDICT:N_PROFILES_FOUND" static let pluralNHolders = "SDICT:N_HOLDERS" static let pluralNAddresses = "SDICT:N_ADDRESSES" + static let pluralMintingNDomains = "SDICT:MINTING_N_DOMAINS" // Errors static let creationFailed = "CREATION_FAILED" @@ -1030,7 +1031,7 @@ extension String { static let getDomainCardSubtitle = "GET_DOMAIN_CARD_SUBTITLE" static let findANewDomain = "FIND_A_NEW_DOMAIN" static let findYourDomain = "FIND_YOUR_DOMAIN" - static let searchForANewDomain = "SEARCH_FOR_A_NEW_DOMAIN" + static let searchForADomain = "SEARCH_FOR_A_DOMAIN" static let trending = "TRENDING" static let noAvailableDomains = "NO_AVAILABLE_DOMAINS" static let tryEnterDifferentName = "TRY_ENTER_DIFF_NAME" @@ -1039,6 +1040,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" @@ -1082,6 +1084,33 @@ extension String { static let purchaseSearchCantButPullUpTitle = "PURCHASE_SEARCH_CANT_BUY_PULL_UP_TITLE" static let purchaseSearchCantButPullUpSubtitle = "PURCHASE_SEARCH_CANT_BUY_PULL_UP_SUBTITLE" static let payWithCredits = "PAY_WITH_CREDITS" + static let buyDomainsSearchTitle = "BUY_DOMAINS_SEARCH_TITLE" + static let buyDomainsCartEmptyTitle = "BUY_DOMAINS_CART_EMPTY_TITLE" + static let buyDomainsCartEmptySubtitle = "BUY_DOMAINS_CART_EMPTY_SUBTITLE" + static let buyDomainsCartTitle = "BUY_DOMAINS_CART_TITLE" + static let searchDomains = "SEARCH_DOMAINS" + static let clear = "CLEAR" + 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" + static let endings = "ENDINGS" + static let suggestions = "SUGGESTIONS" + static let domainsPurchasedSummaryMessage = "DOMAINS_PURCHASED_SUMMARY_MESSAGE" // Home static let homeWalletTokensComeTitle = "HOME_WALLET_TOKENS_COME_TITLE" @@ -1093,7 +1122,6 @@ extension String { static let selectPrimaryDomainTitle = "SELECT_PRIMARY_DOMAIN_TITLE" static let selectPrimaryDomainSubtitle = "SELECT_PRIMARY_DOMAIN_SUBTITLE" - static let saveToPhotos = "SAVE_TO_PHOTOS" static let refreshMetadata = "REFRESH_METADATA" static let viewOnMarketPlace = "VIEW_ON_MARKETPLACE" @@ -1405,6 +1433,8 @@ extension String { var asURL: URL? { URL(string: self) } + + } extension String { diff --git a/unstoppable-ios-app/domains-manager-ios/Extensions/UIImage.swift b/unstoppable-ios-app/domains-manager-ios/Extensions/UIImage.swift index 9e3b514fb..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")! @@ -378,7 +381,7 @@ extension UIImage { } static func createWith(anyData data: Data) async -> UIImage? { - if let gif = await GIFAnimationsService.shared.createGIFImageWithData(data, + if let gif = await GIFAnimationsService.shared.createGIFImageWithData(data, id: UUID().uuidString, maxImageSize: Constants.downloadedImageMaxSize) { return gif diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/DomainProfile/DomainProfileViewController.swift b/unstoppable-ios-app/domains-manager-ios/Modules/DomainProfile/DomainProfileViewController.swift index 054921070..076a4ff5e 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/DomainProfile/DomainProfileViewController.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/DomainProfile/DomainProfileViewController.swift @@ -792,3 +792,4 @@ struct DomainProfileViewControllerWrapper: UIViewControllerRepresentable { domain: domain) } + diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/DomainProfile/Public Profile/Public profile view/PublicProfileView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/DomainProfile/Public Profile/Public profile view/PublicProfileView.swift index 2d6afd64e..9f50b5b51 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/DomainProfile/Public Profile/Public profile view/PublicProfileView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/DomainProfile/Public Profile/Public profile view/PublicProfileView.swift @@ -202,7 +202,7 @@ private extension PublicProfileView { AvatarShapeClipper(style: avatarStyle, avatarSize: avatarSize) }) - .sideInsets(-sidePadding) + .padding(.horizontal, -sidePadding) avatarWithActionsView() @@ -222,7 +222,7 @@ private extension PublicProfileView { .offset(y: -26) Spacer() } - .sideInsets(sidePadding) + .padding(.horizontal, sidePadding) .frame(width: UIScreen.main.bounds.width) .padding(EdgeInsets(top: 0, leading: 0, bottom: 100, trailing: 0)) } @@ -463,9 +463,9 @@ private extension PublicProfileView { carouselSocialAccountsItemIfAvailable(in: profile) carouselCryptoRecordsItemIfAvailable(in: profile) } - .sideInsets(sidePadding) + .padding(.horizontal, sidePadding) } - .sideInsets(-sidePadding) + .padding(.horizontal, -sidePadding) } @ViewBuilder @@ -508,7 +508,7 @@ private extension PublicProfileView { .fill(Color.white) .opacity(0.16) content() - .sideInsets(12) + .padding(.horizontal, 12) } .frame(height: 32) diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/DomainProfile/PurchaseDomainDomainProfileViewPresenter.swift b/unstoppable-ios-app/domains-manager-ios/Modules/DomainProfile/PurchaseDomainDomainProfileViewPresenter.swift deleted file mode 100644 index 6dad9fdde..000000000 --- a/unstoppable-ios-app/domains-manager-ios/Modules/DomainProfile/PurchaseDomainDomainProfileViewPresenter.swift +++ /dev/null @@ -1,193 +0,0 @@ -// -// PurchaseDomainDomainProfileViewPresenter.swift -// domains-manager-ios -// -// Created by Oleg Kuplin on 05.12.2023. -// - -import UIKit - -@MainActor -final class PurchaseDomainDomainProfileViewPresenter: ViewAnalyticsLogger { - - var analyticsName: Analytics.ViewName { .purchaseDomainsProfile } - var additionalAppearAnalyticParameters: Analytics.EventParameters { [.domainName : domainName]} - - private weak var view: (any DomainProfileViewProtocol)? - private var sections = [any DomainProfileSection]() - private var profile: SerializedUserDomainProfile - private let domain: DomainToPurchase - private let domainDisplayInfoHolder: DomainDisplayInfoHolder - private var didDiscardChanges = false - private var domainProfileChanges: DomainProfilePendingChanges - weak var purchaseDomainsFlowManager: PurchaseDomainsFlowManager? - - init(view: any DomainProfileViewProtocol, - domain: DomainToPurchase) { - self.view = view - self.domain = domain - self.domainProfileChanges = DomainProfilePendingChanges(domainName: domain.name) - let profileAttributes = UserDomainProfileAttributes(displayNamePublic: true, - descriptionPublic: true, - locationPublic: true, - imagePathPublic: true, - coverPathPublic: true, - web2UrlPublic: true) - self.profile = SerializedUserDomainProfile(profile: profileAttributes, - messaging: .init(), - socialAccounts: .init(), - humanityCheck: .init(verified: false), - records: [:], - storage: nil, - social: nil) - domainDisplayInfoHolder = DomainDisplayInfoHolder(domainToPurchase: domain) - } - -} - -// MARK: - DomainProfileViewPresenterProtocol -extension PurchaseDomainDomainProfileViewPresenter: DomainProfileViewPresenterProtocol { - var walletName: String { "" } - var domainName: String { domain.name } - var navBackStyle: BaseViewController.NavBackIconStyle { .arrow } - var progress: Double? { 0.5 } - - func viewDidLoad() { - view?.setAvailableActionsGroups([]) - resolveChangesState() - updateSectionsData() - refreshDomainProfileDetails(animated: true) - } - - func confirmChangesButtonPressed() { - let changes = calculateChanges() - - logButtonPressedAnalyticEvents(button: .confirm, parameters: [.isSkip : String(changes.isEmpty)]) - Task { - sections.forEach { section in - section.injectChanges(in: &domainProfileChanges) - } - try? await purchaseDomainsFlowManager?.handle(action: .didFillProfileForDomain(domain, profileChanges: domainProfileChanges)) - } - } - - func shouldPopOnBackButton() -> Bool { - view?.hideKeyboard() - - if didDiscardChanges { - return true - } - - let changes = calculateChanges() - if !changes.isEmpty { - askToDiscardChanges() - UDVibration.buttonTap.vibrate() - return false - } - - return true - } - - func isNavEnabled() -> Bool { true } - func didSelectItem(_ item: DomainProfileViewController.Item) { } - func shareButtonPressed() { } - func didTapShowWalletDetailsButton() { } - func didTapViewInBrowserButton() { } - func didTapSetReverseResolutionButton() { } - func didTapCopyDomainButton() { } - func didTapAboutProfilesButton() { } - func didTapMintedOnChainButton() { } - func didTapTransferButton() { } -} - -// MARK: - DomainProfileSectionDelegate -extension PurchaseDomainDomainProfileViewPresenter: DomainProfileSectionsController { - var viewController: DomainProfileSectionViewProtocol? { view } - var generalData: DomainProfileGeneralData { domainDisplayInfoHolder } - - func sectionDidUpdate(animated: Bool) { - Task { @MainActor in - resolveChangesState() - refreshDomainProfileDetails(animated: animated) - } - } - - func backgroundImageDidUpdate(_ image: UIImage?) { - Task { @MainActor in - view?.setBackgroundImage(image) - } - } - - func avatarImageDidUpdate(_ image: UIImage?, avatarType: DomainProfileImageType) { } - - func updateAccessPreferences(attribute: ProfileUpdateRequest.Attribute, resultCallback: @escaping UpdateProfileAccessResultCallback) { } - - @MainActor - func manageDataOnTheWebsite() { } -} - -// MARK: - Private methods -private extension PurchaseDomainDomainProfileViewPresenter { - @MainActor - func updateSectionsData() { - let sectionTypes: [DomainProfileSectionType] = [.topInfo(data: .init(profile: profile)), - .generalInfo(data: .init(profile: profile))] - - if self.sections.isEmpty { - let sectionsFactory = DomainProfileSectionsFactory() - for type in sectionTypes { - let section = sectionsFactory.buildSectionOf(type: type, - state: .purchaseNew, - controller: self) - self.sections.append(section) - } - } else { - for section in self.sections { - section.update(sectionTypes: sectionTypes) - } - } - } - - func refreshDomainProfileDetails(animated: Bool) { - var snapshot = DomainProfileSnapshot() - - for section in sections { - section.fill(snapshot: &snapshot, withGeneralData: domainDisplayInfoHolder) - } - - view?.applySnapshot(snapshot, animated: animated, completion: nil) - } - - struct DomainDisplayInfoHolder: DomainProfileGeneralData { - let domain: DomainDisplayInfo - let domainWallet: WalletEntity? - - init(domainToPurchase: DomainToPurchase) { - self.domain = .init(name: domainToPurchase.name, ownerWallet: "", isSetForRR: false) - self.domainWallet = nil - } - } - - func calculateChanges() -> [DomainProfileSectionChangeDescription] { - sections.reduce([DomainProfileSectionChangeDescription](), { $0 + $1.calculateChanges() }) - } - - func resolveChangesState() { - let changes = calculateChanges() - view?.setConfirmButtonHidden(false, - style: .main(changes.count == 0 ? .skip : .confirm)) - } - - func askToDiscardChanges() { - Task { - do { - guard let view = self.view else { return } - - try await appContext.pullUpViewService.showDiscardRecordChangesConfirmationPullUp(in: view) - didDiscardChanges = true - await view.dismissPullUpMenu() - view.cNavigationController?.popViewController(animated: true) - } - } - } -} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/HomeTabRouter.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/HomeTabRouter.swift index 61d025f4b..33196ac31 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/HomeTabRouter.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/HomeTabRouter.swift @@ -15,6 +15,8 @@ final class HomeTabRouter: ObservableObject { @Published var isSelectProfilePresented: Bool = false @Published var isConnectedAppsListPresented: Bool = false @Published var showingUpdatedToWalletGreetings: Bool = false + @Published var isShowingMintingWalletsList: Bool = false + @Published var tabViewSelection: HomeTab = .wallets @Published var pullUp: ViewPullUpConfigurationType? @Published var walletViewNavPath: [HomeWalletNavigationDestination] = [] @@ -67,23 +69,20 @@ extension HomeTabRouter { tabViewSelection = .wallets } - func runPurchaseFlow() { + func runPurchaseFlow(shouldResetNavigation: Bool = true) { Task { - let currentTab = tabViewSelection - await showHomeScreenList() - await waitBeforeNextNavigationIfTabNot(currentTab) - - walletViewNavPath.append(HomeWalletNavigationDestination.purchaseDomains(domainsPurchasedCallback: { [weak self] result in - switch result { - case .cancel: - return - case .purchased: - self?.homeWalletViewCoordinator?.domainPurchased() - } - })) + if shouldResetNavigation { + await showHomeScreenList() + } + walletViewNavPath.append(.purchaseDomains(.root(self))) } } + func didPurchaseDomains() { + walletViewNavPath.removeAll() + homeWalletViewCoordinator?.domainPurchased() + } + func runBuyCryptoFlowTo(wallet: WalletEntity) { Task { await showHomeScreenList() @@ -134,8 +133,7 @@ extension HomeTabRouter { shouldResetNavigation: Bool = true, sourceScreen: DomainProfileViewPresenter.SourceScreen = .domainsCollection) async { if shouldResetNavigation { - await popToRootAndWait() - tabViewSelection = .wallets + await showHomeScreenList() } await askToFinishSetupPurchasedProfileIfNeeded(domains: wallet.domains) guard let topVC else { return } @@ -299,6 +297,7 @@ extension HomeTabRouter { isSelectProfilePresented = false isConnectedAppsListPresented = false showingUpdatedToWalletGreetings = false + isShowingMintingWalletsList = false presentedNFT = nil presentedDomain = nil presentedPublicDomain = nil diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/NavigationViewWithCustomTitle.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/NavigationViewWithCustomTitle.swift index 591b201b0..56eb96ee9 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/NavigationViewWithCustomTitle.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/NavigationViewWithCustomTitle.swift @@ -22,6 +22,7 @@ struct NavigationViewWithCustomTitle: View where Data : Mut var body: some View { NavigationStack(path: $path) { content() + .navigationPopGestureDisabled(navigationState.navigationBackDisabled) .environmentObject(navigationState) } .overlay(alignment: .top, content: { @@ -78,6 +79,7 @@ final class NavigationStateManager: ObservableObject, Hashable { @Published var isTitleVisible: Bool = false @Published var yOffset: CGFloat = 0 @Published var dismiss: Bool = false + @Published var navigationBackDisabled: Bool = false @Published private(set) var customTitle: (() -> any View)? private(set) var customViewID: String? @@ -88,3 +90,9 @@ final class NavigationStateManager: ObservableObject, Hashable { customViewID = id } } + +final class NavigationStateManagerWrapper: ObservableObject { + + @Published var navigationState: NavigationStateManager? + +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/SendCryptoAsset/SelectAssetToSend/SelectCryptoAssetToSendView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/SendCryptoAsset/SelectAssetToSend/SelectCryptoAssetToSendView.swift index ab4057dfe..f12222fe6 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/SendCryptoAsset/SelectAssetToSend/SelectCryptoAssetToSendView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/SendCryptoAsset/SelectAssetToSend/SelectCryptoAssetToSendView.swift @@ -260,7 +260,9 @@ private extension SelectCryptoAssetToSendView { domainsContentView() } else { SelectCryptoAssetToSendEmptyView(assetType: .domains, - actionCallback: tabRouter.runPurchaseFlow) + actionCallback: { [weak tabRouter] in + tabRouter?.runPurchaseFlow() + }) } } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeView.swift index 484e7e7ee..3afad8d7c 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeView.swift @@ -10,26 +10,24 @@ import SwiftUI struct HomeView: View, ViewAnalyticsLogger { @EnvironmentObject var tabRouter: HomeTabRouter - @State private var navigationState: NavigationStateManager? - @State private var isTabBarVisible: Bool = true + @StateObject private var stateManagerWrapper = NavigationStateManagerWrapper() + var analyticsName: Analytics.ViewName { .home } var additionalAppearAnalyticParameters: Analytics.EventParameters { [.profileId: tabRouter.profile.id] } var body: some View { NavigationViewWithCustomTitle(content: { currentWalletView() - .onChange(of: isTabBarVisible) { _ in - tabRouter.isTabBarVisible = isTabBarVisible - } .navigationDestination(for: HomeWalletNavigationDestination.self) { destination in HomeWalletLinkNavigationDestination.viewFor(navigationDestination: destination) + .environmentObject(stateManagerWrapper) } .trackAppearanceAnalytics(analyticsLogger: self) .passViewAnalyticsDetails(logger: self) .checkPendingEventsOnAppear() - + .environmentObject(stateManagerWrapper) }, navigationStateProvider: { state in - self.navigationState = state + self.stateManagerWrapper.navigationState = state }, path: $tabRouter.walletViewNavPath) } @@ -42,13 +40,9 @@ private extension HomeView { switch tabRouter.profile { case .wallet(let wallet): HomeWalletView(viewModel: .init(selectedWallet: wallet, - router: tabRouter), - navigationState: $navigationState, - isTabBarVisible: $isTabBarVisible) + router: tabRouter)) case .webAccount(let user): - HomeWebAccountView(user: user, - navigationState: $navigationState, - isTabBarVisible: $isTabBarVisible) + HomeWebAccountView(user: user) } } } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletNavigationDestination.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletNavigationDestination.swift index 2c1fbd4ac..dd2def4a1 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletNavigationDestination.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletNavigationDestination.swift @@ -14,7 +14,7 @@ enum HomeWalletNavigationDestination: Hashable { mintedDomains: [DomainDisplayInfo], domainsMintedCallback: MintDomainsNavigationController.DomainsMintedCallback, mintingNavProvider: (MintDomainsNavigationController)->()) - case purchaseDomains(domainsPurchasedCallback: PurchaseDomainsNavigationController.DomainsPurchasedCallback) + case purchaseDomains(PurchaseDomains.NavigationDestination) case login(mode: LoginFlowNavigationController.Mode, callback: LoginFlowNavigationController.LoggedInCallback) case walletDetails(WalletEntity) case securitySettings @@ -68,6 +68,7 @@ enum HomeWalletNavigationDestination: Hashable { struct HomeWalletLinkNavigationDestination { + @MainActor @ViewBuilder static func viewFor(navigationDestination: HomeWalletNavigationDestination) -> some View { switch navigationDestination { @@ -85,10 +86,8 @@ struct HomeWalletLinkNavigationDestination { mintingNavProvider: mintingNavProvider) .toolbar(.hidden, for: .navigationBar) .ignoresSafeArea() - case .purchaseDomains(let callback): - PurchaseDomainsNavigationControllerWrapper(domainsPurchasedCallback: callback) - .toolbar(.hidden, for: .navigationBar) - .ignoresSafeArea() + case .purchaseDomains(let destination): + PurchaseDomains.LinkNavigationDestination.viewFor(navigationDestination: destination) case .login(let mode, let callback): LoginFlowNavigationControllerWrapper(mode: mode, callback: callback) diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletView+Entities.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletView+Entities.swift index cac018e49..f887cefb3 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletView+Entities.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletView+Entities.swift @@ -303,8 +303,9 @@ extension HomeWalletView { extension HomeWalletView { struct DomainsSectionData { - private(set) var domainsGroups: [DomainsTLDGroup] - private(set) var subdomains: [DomainDisplayInfo] + private(set) var domainsGroups: [DomainsTLDGroup] = [] + private(set) var subdomains: [DomainDisplayInfo] = [] + private(set) var mintingDomains: [DomainDisplayInfo] = [] var isSubdomainsVisible: Bool = false var domainsTLDsExpandedList: Set = [] var isSearching: Bool = false @@ -312,6 +313,7 @@ extension HomeWalletView { mutating func setDomains(_ domains: [DomainDisplayInfo]) { domainsGroups = DomainsTLDGroup.createFrom(domains: domains.filter({ !$0.isSubdomain })) subdomains = domains.filter({ $0.isSubdomain }) + mintingDomains = domains.filter { $0.isMinting } } mutating func setDomainsFrom(wallet: WalletEntity) { diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletView.swift index 0813f61b1..46764b3d5 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletView.swift @@ -13,12 +13,11 @@ struct HomeWalletView: View, ViewAnalyticsLogger { @Environment(\.analyticsAdditionalProperties) var additionalAppearAnalyticParameters @EnvironmentObject var tabRouter: HomeTabRouter + @EnvironmentObject var stateManagerWrapper: NavigationStateManagerWrapper @StateObject var viewModel: HomeWalletViewModel @StateObject private var profilesAPIFlagTracker = UDMaintenanceModeFeatureFlagTracker(featureFlag: .isMaintenanceProfilesAPIEnabled) @StateObject private var mpcFlagTracker = UDMaintenanceModeFeatureFlagTracker(featureFlag: .isMaintenanceMPCEnabled) - @State private var isOtherScreenPresented: Bool = false - @Binding var navigationState: NavigationStateManager? - @Binding var isTabBarVisible: Bool + private var navigationState: NavigationStateManager? { stateManagerWrapper.navigationState } var isOtherScreenPushed: Bool { !tabRouter.walletViewNavPath.isEmpty } var body: some View { @@ -41,12 +40,17 @@ struct HomeWalletView: View, ViewAnalyticsLogger { .listRowSeparator(.hidden) .unstoppableListRowInset() + mintingDomainsSection() + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 32, leading: 16, bottom: 32, trailing: 16)) + userDataContentViewsIfAvailable() }.environment(\.defaultMinListRowHeight, 28) .onChange(of: tabRouter.walletViewNavPath) { _ in updateNavTitleVisibility() - isTabBarVisible = !isOtherScreenPushed + tabRouter.isTabBarVisible = !isOtherScreenPushed } .animation(.default, value: viewModel.selectedWallet) .listStyle(.plain) @@ -72,6 +76,9 @@ struct HomeWalletView: View, ViewAnalyticsLogger { logAnalytic(event: .didPullToRefresh) try? await appContext.walletsDataService.refreshDataForWallet(viewModel.selectedWallet) } + .sheet(isPresented: $tabRouter.isShowingMintingWalletsList, content: { + MintingDomainsListView(domains: viewModel.domainsData.mintingDomains) + }) .onAppear(perform: onAppear) } } @@ -104,6 +111,8 @@ private extension HomeWalletView { navigationState?.setCustomTitle(customTitle: { HomeProfileSelectorNavTitleView(shouldHideAvatar: true) }, id: id) DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + guard id == navigationState?.customViewID else { return } + updateNavTitleVisibility() } } @@ -118,6 +127,11 @@ private extension HomeWalletView { profilesAPIFlagTracker.maintenanceData?.isCurrentlyEnabled == true } + @ViewBuilder + func mintingDomainsSection() -> some View { + HomeWalletMintingInProgressSectionView(mintingDomains: viewModel.domainsData.mintingDomains) + } + @ViewBuilder func userDataContentViewsIfAvailable() -> some View { if isHomeInMaintenance { diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletViewModel.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletViewModel.swift index acc571238..d0823cb70 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletViewModel.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/HomeWalletViewModel.swift @@ -117,7 +117,7 @@ extension HomeWalletView { func didSelectBuyOption(_ buyOption: HomeWalletView.BuyOptions) { switch buyOption { case .domains: - router.runPurchaseFlow() + router.runPurchaseFlow(shouldResetNavigation: false) case .crypto: router.runBuyCryptoFlowTo(wallet: selectedWallet) } @@ -163,7 +163,7 @@ extension HomeWalletView { } func buyDomainPressed() { - router.runPurchaseFlow() + router.runPurchaseFlow(shouldResetNavigation: false) } func domainPurchased() { diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/Subviews/HomeWalletDomainCellView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/Subviews/HomeWalletDomainCellView.swift index aa23b74e7..11e2a8c1c 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/Subviews/HomeWalletDomainCellView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/Subviews/HomeWalletDomainCellView.swift @@ -149,7 +149,7 @@ private extension HomeWalletDomainCellView { .shadow(color: .black.opacity(0.32), radius: 8.99346, x: 0, y: 6.29542) Spacer() } - .sideInsets(8) + .padding(.horizontal, 8) } @ViewBuilder diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/Subviews/HomeWalletMintingInProgressSectionView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/Subviews/HomeWalletMintingInProgressSectionView.swift new file mode 100644 index 000000000..d13dfbdf2 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWalletView/Subviews/HomeWalletMintingInProgressSectionView.swift @@ -0,0 +1,79 @@ +// +// HomeWalletMintingInProgressSectionView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 13.08.2024. +// + +import SwiftUI + +struct HomeWalletMintingInProgressSectionView: View, ViewAnalyticsLogger { + + @Environment(\.analyticsViewName) var analyticsName + @Environment(\.analyticsAdditionalProperties) var additionalAppearAnalyticParameters + @EnvironmentObject var tabRouter: HomeTabRouter + + let mintingDomains: [DomainDisplayInfo] + + var body: some View { + if !mintingDomains.isEmpty { + Button { + logButtonPressedAnalyticEvents(button: .showMoreMintingDomains) + UDVibration.buttonTap.vibrate() + tabRouter.isShowingMintingWalletsList = true + } label: { + ZStack { + Image.mpcWalletGrid + .resizable() + Image.mpcWalletGridAccent + .resizable() + .foregroundStyle(Color.foregroundAccent) + .mask(AnimatedMPCWalletGridMask()) + + HStack(alignment: .center, spacing: 16) { + ProgressView() + .squareFrame(24) + .tint(Color.foregroundDefault) + Text(String.Constants.pluralMintingNDomains.localized(mintingDomains.count, mintingDomains.count)) + .textAttributes(color: .foregroundDefault, + fontSize: 16, + fontWeight: .medium) + .frame(height: 24) + Spacer() + Image.chevronRight + .resizable() + .squareFrame(24) + .foregroundStyle(Color.foregroundDefault) + } + .padding(.horizontal, 16) + } + .frame(height: 76) + .background( + LinearGradient( + stops: [ + Gradient.Stop(color: Color(red: 0.05, green: 0.4, blue: 1).opacity(0), location: 0.25), + Gradient.Stop(color: Color(red: 0.05, green: 0.4, blue: 1).opacity(0.16), location: 1.00), + ], + startPoint: UnitPoint(x: 0.5, y: 0), + endPoint: UnitPoint(x: 0.5, y: 1) + ) + ) + .background(Color.backgroundDefault) + .cornerRadius(12) + .overlay( + ZStack { + RoundedRectangle(cornerRadius: 12) + .stroke(Color.backgroundDefault, lineWidth: 4) + RoundedRectangle(cornerRadius: 12) + .stroke(Color.foregroundAccent, lineWidth: 1) + } + ) + } + .buttonStyle(.plain) + } + } +} + +#Preview { + HomeWalletMintingInProgressSectionView(mintingDomains: []) +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWebAccountView/HomeWebAccountView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWebAccountView/HomeWebAccountView.swift index bc549c599..caefac009 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWebAccountView/HomeWebAccountView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/HomeWebAccountView/HomeWebAccountView.swift @@ -15,10 +15,10 @@ struct HomeWebAccountView: View, ViewAnalyticsLogger { let user: FirebaseUser @EnvironmentObject var tabRouter: HomeTabRouter + @EnvironmentObject var stateManagerWrapper: NavigationStateManagerWrapper @StateObject private var ecommFlagTracker = UDMaintenanceModeFeatureFlagTracker(featureFlag: .isMaintenanceEcommEnabled) - @Binding var navigationState: NavigationStateManager? - @Binding var isTabBarVisible: Bool + private var navigationState: NavigationStateManager? { stateManagerWrapper.navigationState } @State private var domains: [FirebaseDomainDisplayInfo] = [] private let gridColumns = [ @@ -53,7 +53,7 @@ struct HomeWebAccountView: View, ViewAnalyticsLogger { } .onChange(of: tabRouter.walletViewNavPath) { _ in updateNavTitleVisibility() - isTabBarVisible = !isOtherScreenPushed + tabRouter.isTabBarVisible = !isOtherScreenPushed } .listStyle(.plain) .clearListBackground() diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/MintingDomainsListView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/MintingDomainsListView.swift new file mode 100644 index 000000000..3d4f1ecad --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Wallet/MintingDomainsListView.swift @@ -0,0 +1,105 @@ +// +// MintingDomainsListView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 13.08.2024. +// + +import SwiftUI + +struct MintingDomainsListView: View, ViewAnalyticsLogger { + + @Environment(\.walletsDataService) var walletsDataService + @Environment(\.dismiss) var dismiss + var analyticsName: Analytics.ViewName { .mintingDomainsList } + + @State var domains: [DomainDisplayInfo] + + var body: some View { + VStack { + ScrollView { + LazyVStack(spacing: 24) { + titleView() + domainsList() + } + .padding(.horizontal, 16) + } + .padding(.top, 36) + + doneButton() + } + .background(Color.backgroundDefault) + .onReceive(walletsDataService.selectedWalletPublisher.receive(on: DispatchQueue.main)) { selectedWallet in + if let selectedWallet { + setMintingDomainsFrom(wallet: selectedWallet) + } + } + .trackAppearanceAnalytics(analyticsLogger: self) + .presentationDetents([.medium, .large]) + } +} + +// MARK: - Private methods +private extension MintingDomainsListView { + func setMintingDomainsFrom(wallet: WalletEntity) { + let mintingDomains = wallet.domains.filter { $0.isMinting } + if mintingDomains.isEmpty { + dismiss() + } else { + self.domains = mintingDomains + } + } + + @ViewBuilder + func titleView() -> some View { + Text(String.Constants.pluralMintingNDomains.localized(domains.count, domains.count)) + .textAttributes(color: .foregroundDefault, fontSize: 22, fontWeight: .bold) + } + + @ViewBuilder + func domainsList() -> some View { + UDCollectionSectionBackgroundView { + LazyVStack(alignment: .center, spacing: 4) { + ForEach(domains) { domain in + domainRowView(domain) + } + } + } + } + + @ViewBuilder + func domainRowView(_ domain: DomainDisplayInfo) -> some View { + HStack(spacing: 16) { + TLDCategory.categoryFor(tld: domain.name.getTldName() ?? "") + .icon + .resizable() + .squareFrame(24) + .foregroundStyle(Color.foregroundSecondary) + + Text(domain.name) + .textAttributes(color: .foregroundDefault, + fontSize: 16, + fontWeight: .medium) + .lineLimit(1) + Spacer() + ProgressView() + .tint(Color.foregroundDefault) + } + .frame(height: 64) + .padding(.horizontal, 16) + } + + + @ViewBuilder + func doneButton() -> some View { + UDButtonView(text: String.Constants.doneButtonTitle.localized(), + style: .large(.raisedPrimary)) { + logButtonPressedAnalyticEvents(button: .done) + dismiss() + } + } +} + +#Preview { + MintingDomainsListView(domains: MockEntitiesFabric.Domains.mockDomainsDisplayInfo(ownerWallet: "1")) +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/HotFeatureSuggestion/HotFeatureSuggestionDetailsView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/HotFeatureSuggestion/HotFeatureSuggestionDetailsView.swift index 180a9d857..124de7056 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/HotFeatureSuggestion/HotFeatureSuggestionDetailsView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/HotFeatureSuggestion/HotFeatureSuggestionDetailsView.swift @@ -22,7 +22,7 @@ struct HotFeatureSuggestionDetailsView: View, ViewAnalyticsLogger { NavigationView { OffsetObservingScrollView(offset: $scrollOffset) { contentView() - .sideInsets(16) + .padding(.horizontal, 16) UDButtonView(text: String.Constants.gotIt.localized(), style: .large(.raisedTertiary)) { logButtonPressedAnalyticEvents(button: .gotIt) 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/Onboarding/HappyEnd/HappyEndViewController.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Onboarding/HappyEnd/HappyEndViewController.swift index 012ec7b95..e63d7abd1 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Onboarding/HappyEnd/HappyEndViewController.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Onboarding/HappyEnd/HappyEndViewController.swift @@ -151,3 +151,20 @@ extension HappyEndViewController { vc.presenter = presenter return vc } + +import SwiftUI +struct PurchaseDomainsHappyEndViewControllerWrapper: UIViewControllerRepresentable { + + weak var viewModel: PurchaseDomainsViewModel? + + func makeUIViewController(context: Context) -> UIViewController { + let vc = HappyEndViewController.instance() + let presenter = PurchaseDomainsHappyEndViewPresenter(view: vc) + presenter.purchaseDomainsViewModel = viewModel + vc.presenter = presenter + return vc + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { } + +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Onboarding/HappyEnd/PurchaseDomainsHappyEndViewPresenter.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Onboarding/HappyEnd/PurchaseDomainsHappyEndViewPresenter.swift index 4bf80381c..a50251980 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Onboarding/HappyEnd/PurchaseDomainsHappyEndViewPresenter.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Onboarding/HappyEnd/PurchaseDomainsHappyEndViewPresenter.swift @@ -11,7 +11,7 @@ final class PurchaseDomainsHappyEndViewPresenter: BaseHappyEndViewPresenter { override var analyticsName: Analytics.ViewName { .domainsPurchasedHappyEnd } - weak var purchaseDomainsFlowManager: PurchaseDomainsFlowManager? + weak var purchaseDomainsViewModel: PurchaseDomainsViewModel? override func viewDidLoad() { view?.setAgreement(visible: false) @@ -20,7 +20,7 @@ final class PurchaseDomainsHappyEndViewPresenter: BaseHappyEndViewPresenter { override func actionButtonPressed() { Task { - try? await purchaseDomainsFlowManager?.handle(action: .goToDomains) + purchaseDomainsViewModel?.handleAction(.goToDomains) } } } 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 a6fa95257..4f069d82d 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 @@ -7,61 +7,70 @@ import SwiftUI -protocol PurchaseDomainsCheckoutViewDelegate: AnyObject { - func purchaseViewDidPurchaseDomains() - func purchaseViewDidUpdateScrollOffset(_ scrollOffset: CGPoint) - func purchaseViewDidUpdateLoadingState(_ isLoading: Bool) -} - struct PurchaseDomainsCheckoutView: View, ViewAnalyticsLogger { @Environment(\.purchaseDomainsService) private var purchaseDomainsService @Environment(\.purchaseDomainsPreferencesStorage) private var purchaseDomainsPreferencesStorage @Environment(\.walletsDataService) private var walletsDataService - - @State var domain: DomainToPurchase + @Environment(\.userProfilesService) private var userProfilesService + @EnvironmentObject var stateManagerWrapper: NavigationStateManagerWrapper + @EnvironmentObject var viewModel: PurchaseDomainsViewModel + + @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 - - weak var delegate: PurchaseDomainsCheckoutViewDelegate? + @State private var isShowingOrderSummary = false + @State private var didCheckPreferredWalletToMint = 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) { - OffsetObservingScrollView(offset: $scrollOffset) { - LazyVStack { - headerView() + ScrollView { + 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()) .onReceive(purchaseDomainsService.cartStatusPublisher.receive(on: DispatchQueue.main)) { cartStatus in @@ -69,29 +78,52 @@ struct PurchaseDomainsCheckoutView: View, ViewAnalyticsLogger { appContext.toastMessageService.showToast(.purchaseDomainsDiscountApplied(cartStatus.otherDiscountsApplied), isSticky: false) } self.cartStatus = cartStatus + if case .ready = cartStatus, + !didCheckPreferredWalletToMint { + setPreferredWalletToMint() + } checkUpdatedCartStatus() } .onReceive(purchaseDomainsPreferencesStorage.$checkoutData.publisher.receive(on: DispatchQueue.main), perform: { checkoutData in self.checkoutData = checkoutData }) - .onChange(of: scrollOffset) { newValue in - delegate?.purchaseViewDidUpdateScrollOffset(newValue) + .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)) + } + } + } + .trackAppearanceAnalytics(analyticsLogger: self) + .passViewAnalyticsDetails(logger: self) .pullUpError($error) - .modifier(ShowingSelectDiscounts(isSelectDiscountsPresented: $isSelectDiscountsPresented)) .viewPullUp($pullUp) .onAppear(perform: onAppear) } @@ -99,82 +131,117 @@ 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) + } + } + + 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 + } + + func setPreferredWalletToMint() { + Task { + do { + let preferredWalletToMint = try await purchaseDomainsService.getPreferredWalletToMint() + let preferredWalletAddress = preferredWalletToMint.address.lowercased() + if selectedWallet.address != preferredWalletAddress, + let preferredWallet = wallets.findWithAddress(preferredWalletAddress) { + logAnalytic(event: .willChangeWalletToWebPreferred) + selectedWallet = preferredWallet + userProfilesService.setActiveProfile(.wallet(selectedWallet)) } + didCheckPreferredWalletToMint = true } } - .padding(EdgeInsets(top: 56, leading: 16, bottom: 0, trailing: 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 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 { @@ -189,23 +256,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 { @@ -216,22 +316,130 @@ private extension PurchaseDomainsCheckoutView { } } + var discountViewTitle: String { + if appliedDiscountsSum != nil { + return String.Constants.discountCodeApplied.localized() + } + return String.Constants.addDiscountCode.localized() + } + + @ViewBuilder + func discountsView() -> some View { + VStack(spacing: 8) { + otherDiscountsView() + promoCreditsDiscountView() + storeCreditsDiscountView() + } + } + @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 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.promoCreditsApplied > 0 { + specificDiscountInfoRow(icon: .ticketIcon, + title: String.Constants.promoCredits.localized(), + value: cartStatus.promoCreditsApplied) + } + } + + @ViewBuilder + func storeCreditsDiscountView() -> some View { + if cartStatus.storeCreditsApplied > 0 { + specificDiscountInfoRow(icon: .starInCloudIcon, + title: String.Constants.storeCredits.localized(), + value: cartStatus.storeCreditsApplied) + } + } + + @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 { @@ -252,13 +460,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) } } @@ -268,55 +472,89 @@ 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 { + logButtonPressedAnalyticEvents(button: .edit) + 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)) @@ -349,11 +587,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 @@ -384,13 +636,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) } } @@ -398,12 +651,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) } } @@ -436,10 +688,6 @@ private extension PurchaseDomainsCheckoutView { .padding() } - var shouldShowTotalDueInSummary: Bool { - true - } - var isPayButtonDisabled: Bool { if case .ready = cartStatus { return false @@ -453,24 +701,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): @@ -492,7 +729,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)") } @@ -516,17 +753,21 @@ 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) } - delegate?.purchaseViewDidPurchaseDomains() + + let purchasedData = PurchaseDomains.PurchasedDomainsData(domains: domains, + totalSum: formatCartPrice(totalPrice), + wallet: selectedWallet) + + viewModel.handleAction(.didPurchaseDomains(purchasedData)) } catch { logAnalytic(event: .didFailToPurchaseDomains, parameters: [.value : String(cartStatus.totalPrice), .count: String(1), @@ -541,7 +782,7 @@ private extension PurchaseDomainsCheckoutView { func setLoading(_ isLoading: Bool) { self.isLoading = isLoading - delegate?.purchaseViewDidUpdateLoadingState(isLoading) + stateManagerWrapper.navigationState?.navigationBackDisabled = isLoading } func checkUpdatedCartStatus() { @@ -581,44 +822,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 @@ -685,14 +892,19 @@ private extension PullUpErrorConfiguration { } #Preview { - PurchaseDomainsCheckoutView(domain: .init(name: "oleg.x", - price: 10000, - metadata: nil, - isAbleToPurchase: true), - selectedWallet: MockEntitiesFabric.Wallet.mockEntities()[0], - wallets: Array(MockEntitiesFabric.Wallet.mockEntities().prefix(4)), - profileChanges: .init(domainName: "oleg.x", - avatarData: UIImage.Preview.previewLandscape?.dataToUpload), - delegate: nil) - .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/PurchaseDomainsCheckoutViewController.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsCheckoutViewController.swift deleted file mode 100644 index aae7b6826..000000000 --- a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsCheckoutViewController.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// PurchaseDomainsCheckoutViewController.swift -// domains-manager-ios -// -// Created by Oleg Kuplin on 29.11.2023. -// - -import SwiftUI - -final class PurchaseDomainsCheckoutViewController: BaseViewController, ViewWithDashesProgress, UDNavigationBackButtonHandler { - - override var scrollableContentYOffset: CGFloat? { 16 } - - weak var purchaseDomainsFlowManager: PurchaseDomainsFlowManager? - private var domain: DomainToPurchase! - private var profileChanges: DomainProfilePendingChanges! - private var selectedWallet: WalletEntity! - private var wallets: [WalletEntity]! - private var isLoading = false - override var analyticsName: Analytics.ViewName { .purchaseDomainsCheckout } - override var preferredStatusBarStyle: UIStatusBarStyle { .default } - - var dashesProgressConfiguration: DashesProgressView.Configuration { .init(numberOfDashes: 3) } - var progress: Double? { 5 / 6 } - override var additionalAppearAnalyticParameters: Analytics.EventParameters { [.domainName : domain.name, - .price: String(domain.price)] } - - static func instantiate(domain: DomainToPurchase, - profileChanges: DomainProfilePendingChanges, - selectedWallet: WalletEntity, - wallets: [WalletEntity]) -> PurchaseDomainsCheckoutViewController { - let vc = PurchaseDomainsCheckoutViewController() - vc.domain = domain - vc.profileChanges = profileChanges - vc.selectedWallet = selectedWallet - vc.wallets = wallets - return vc - } - - override func viewDidLoad() { - super.viewDidLoad() - - setup() - } - - override func shouldPopOnBackButton() -> Bool { - guard !isLoading else { return false } - - Task { await appContext.purchaseDomainsService.reset() } - return true - } -} - -// MARK: - PurchaseDomainsCheckoutViewDelegate -extension PurchaseDomainsCheckoutViewController: PurchaseDomainsCheckoutViewDelegate { - func purchaseViewDidPurchaseDomains() { - Task { @MainActor in - try? await purchaseDomainsFlowManager?.handle(action: .didPurchaseDomains) - } - } - - func purchaseViewDidUpdateScrollOffset(_ scrollOffset: CGPoint) { - cNavigationController?.underlyingScrollViewDidScrollTo(offset: scrollOffset) - } - - func purchaseViewDidUpdateLoadingState(_ isLoading: Bool) { - self.isLoading = isLoading - } -} - -// MARK: - Setup methods -private extension PurchaseDomainsCheckoutViewController { - func setup() { - addProgressDashesView(configuration: .init(numberOfDashes: 3)) - addChildView() - DispatchQueue.main.async { - self.setDashesProgress(self.progress) - } - } - - func addChildView() { - let view = PurchaseDomainsCheckoutView(domain: domain, - selectedWallet: selectedWallet, - wallets: wallets, - profileChanges: profileChanges, - delegate: self) - - let vc = UIHostingController(rootView: view) - addChildViewController(vc, andEmbedToView: self.view) - } -} 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..2935ba285 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Checkout/PurchaseDomainsOrderSummaryView.swift @@ -0,0 +1,134 @@ +// +// 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: { + logButtonPressedAnalyticEvents(button: .undoRemoveDomain) + 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 { + logButtonPressedAnalyticEvents(button: .removeDomain) + 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 new file mode 100644 index 000000000..b7a364793 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomains.swift @@ -0,0 +1,254 @@ +// +// PurchaseDomains.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 02.08.2024. +// + +import SwiftUI + +enum PurchaseDomains { } // Namespace + +extension PurchaseDomains { + enum FlowAction { + case didSelectDomains(_ domains: [DomainToPurchase]) + case didFillProfileForDomain(_ domain: DomainToPurchase, profileChanges: DomainProfilePendingChanges) + case didRemoveAllDomainsFromTheCart + case didPurchaseDomains(_ purchasedDomainsData: PurchasedDomainsData) + case goToDomains + } +} + +extension PurchaseDomains { + struct CheckoutData: Hashable { + let domains: [DomainToPurchase] + let profileChanges: DomainProfilePendingChanges? + let selectedWallet: WalletEntity + let wallets: [WalletEntity] + } + + struct PurchasedDomainsData: Hashable { + let domains: [DomainToPurchase] + let totalSum: String + let wallet: WalletEntity + } + + enum EmptyStateMode { + case start + case noResults + case error + + var title: String { + switch self { + case .start: + return String.Constants.startTyping.localized() + case .noResults: + return String.Constants.noAvailableDomains.localized() + case .error: + return String.Constants.somethingWentWrong.localized() + } + } + + var subtitle: String? { + switch self { + case .start: + return nil + case .noResults: + return String.Constants.tryEnterDifferentName.localized() + case .error: + return String.Constants.pleaseCheckInternetConnection.localized() + } + } + + var icon: Image { + switch self { + case .start: + return .searchIcon + case .noResults, .error: + return .grimaseIcon + } + } + } +} + +extension PurchaseDomains { + struct LocalCart { + + private(set) var domains: [DomainToPurchase] = [] + var isShowingCart = false + + var totalPrice: Int { domains.reduce(0, { $0 + $1.price })} + + func isDomainInCart(_ domain: DomainToPurchase) -> Bool { + domains.firstIndex(where: { $0.name == domain.name }) != nil + } + + 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) + } + + mutating func removeDomain(_ domain: DomainToPurchase) { + if let i = domains.firstIndex(where: { $0.name == domain.name }) { + domains.remove(at: i) + } + } + + mutating func clearCart() { + domains.removeAll() + } + } +} + +extension PurchaseDomains { + struct SearchResultHolder { + private(set) var availableDomains: [DomainToPurchase] = [] + private(set) var takenDomains: [DomainToPurchase] = [] + private(set) var recentSearches: [String] = [] + private let recentSearchesStorage: RecentDomainsToPurchaseSearchStorageProtocol = RecentDomainsToPurchaseSearchStorage.instance + + var isShowingTakenDomains = false + + var allDomains: [DomainToPurchase] { + availableDomains + takenDomains + } + var hasTakenDomains: Bool { !takenDomains.isEmpty } + + var isEmpty: Bool { + availableDomains.isEmpty + } + + init() { + loadRecentSearches() + } + + mutating func clear() { + availableDomains.removeAll() + takenDomains.removeAll() + } + + mutating func addDomains(_ domains: [DomainToPurchase], + searchText: String) { + let sortedDomains = sortSearchResult(domains, searchText: searchText) + for domain in sortedDomains { + if domain.isTaken { + takenDomains.append(domain) + } else { + availableDomains.append(domain) + } + } + addRecentSearch(string: searchText) + } + + private func sortSearchResult(_ searchResult: [DomainToPurchase], searchText: String) -> [DomainToPurchase] { + var searchResult = searchResult + /// Move exactly matched domain to the top of the list + if let i = searchResult.firstIndex(where: { $0.name == searchText }), + i != 0 { + let matchingDomain = searchResult[i] + searchResult.remove(at: i) + searchResult.insert(matchingDomain, at: 0) + } + return searchResult + } + mutating func removeRecentSearch(string: String) { + makeChangesToRecentProfilesStorage { storage in + storage.removeDomainToPurchaseSearchToRecents(string) + } + } + + private mutating func addRecentSearch(string: String) { + makeChangesToRecentProfilesStorage { storage in + storage.addDomainToPurchaseSearchToRecents(string) + } + } + + private mutating func loadRecentSearches() { + recentSearches = recentSearchesStorage.getRecentDomainsToPurchaseSearches() + } + + private mutating func makeChangesToRecentProfilesStorage(_ block: (RecentDomainsToPurchaseSearchStorageProtocol)->()) { + block(recentSearchesStorage) + loadRecentSearches() + } + } +} + +extension PurchaseDomains { + struct SearchFiltersHolder { + private(set) var tlds: Set = [] + var isFiltersVisible = false + + var isFiltersApplied: Bool { + !tlds.isEmpty + } + + mutating func setTLDs(_ tlds: Set) { + self.tlds = tlds + } + + func filterDomains(_ domains: [DomainToPurchase]) -> [DomainToPurchase] { + if tlds.isEmpty { + return domains + } + return domains.filter({ tlds.contains($0.tld) }) + } + } +} + +// MARK: - Open methods +extension PurchaseDomains { + struct RecentDomainsToPurchaseSearchStorage: RecentDomainsToPurchaseSearchStorageProtocol { + + typealias Object = String + static private let domainPFPStorageFileName = "purchase.domains.recent.search.data" + + static var instance = RecentDomainsToPurchaseSearchStorage() + private let storage = SpecificStorage<[Object]>(fileName: RecentDomainsToPurchaseSearchStorage.domainPFPStorageFileName) + private let maxNumberOfRecentSearches = 10 + + private init() {} + + func getRecentDomainsToPurchaseSearches() -> [Object] { + storage.retrieve() ?? [] + } + + func addDomainToPurchaseSearchToRecents(_ search: Object) { + let targetProfileIndex = 0 + var profilesList = getRecentDomainsToPurchaseSearches() + if let index = profilesList.firstIndex(where: { $0 == search }) { + if index == targetProfileIndex { + return + } + profilesList.swapAt(index, targetProfileIndex) + } else { + profilesList.insert(search, at: targetProfileIndex) + profilesList = Array(profilesList.prefix(maxNumberOfRecentSearches)) + } + + set(newProfilesList: profilesList) + } + + func removeDomainToPurchaseSearchToRecents(_ search: Object) { + var profilesList = getRecentDomainsToPurchaseSearches() + if let index = profilesList.firstIndex(where: { $0 == search }) { + profilesList.remove(at: index) + set(newProfilesList: profilesList) + } + } + + private func set(newProfilesList: [Object]) { + storage.store(newProfilesList) + } + + func clearRecentDomainsToPurchaseSearches() { + storage.remove() + } + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsNavigationController.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsNavigationController.swift deleted file mode 100644 index 0e5c4d458..000000000 --- a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsNavigationController.swift +++ /dev/null @@ -1,271 +0,0 @@ -// -// PurchaseDomainsNavigationController.swift -// domains-manager-ios -// -// Created by Oleg Kuplin on 27.11.2023. -// - -import UIKit - -@MainActor -protocol PurchaseDomainsFlowManager: AnyObject { - func handle(action: PurchaseDomainsNavigationController.Action) async throws -} - -final class PurchaseDomainsNavigationController: CNavigationController { - - typealias DomainsPurchasedCallback = ((Result)->()) - typealias PurchaseDomainsResult = Result - - private var mode: Mode = .default - private var purchaseData: PurchaseData = PurchaseData() - var domainsPurchasedCallback: DomainsPurchasedCallback? - - convenience init(mode: Mode = .default) { - self.init() - self.mode = mode - } - - override func viewDidLoad() { - super.viewDidLoad() - - self.delegate = self - setup() - } - - override func popViewController(animated: Bool, completion: (()->())? = nil) -> UIViewController? { - guard let topViewController = self.topViewController else { - return super.popViewController(animated: animated) - } - - if isLastViewController(topViewController) { - if let navigationController { - return navigationController.popViewController(animated: true) - } - return cNavigationController?.popViewController(animated: true) - } - return super.popViewController(animated: animated, completion: completion) - } -} - -// MARK: - PurchaseDomainsFlowManager -extension PurchaseDomainsNavigationController: PurchaseDomainsFlowManager { - func handle(action: Action) async throws { - switch action { - case .didSelectDomain(let domain): - moveToStep(.fillProfile(domain: domain)) - case .didFillProfileForDomain(let domain, let profileChanges): - moveToCheckoutWith(domain: domain, - profileChanges: profileChanges) - case .didPurchaseDomains: - Task { - await Task.sleep(seconds: 0.5) - await appContext.purchaseDomainsService.reset() - } - moveToStep(.purchased) - case .goToDomains: - didFinishPurchase() - } - } -} - -// MARK: - CNavigationControllerDelegate -extension PurchaseDomainsNavigationController: CNavigationControllerDelegate { - func navigationController(_ navigationController: CNavigationController, didShow viewController: UIViewController, animated: Bool) { - setSwipeGestureEnabledForCurrentState() - } -} - -// MARK: - Private methods -private extension PurchaseDomainsNavigationController { - func moveToStep(_ step: Step) { - guard let vc = createStep(step) else { return } - - self.pushViewController(vc, animated: true) - } - - func didFinishPurchase() { - dismiss(result: .purchased(domainName: purchaseData.domain?.name ?? "")) - } - - func isLastViewController(_ viewController: UIViewController) -> Bool { - viewController is PurchaseSearchDomainsViewController - } - - func dismiss(result: Result) { - if let vc = presentedViewController { - vc.dismiss(animated: true) - } - cNavigationController?.transitionHandler?.isInteractionEnabled = true - let domainsPurchasedCallback = self.domainsPurchasedCallback - self.cNavigationController?.popViewController(animated: true) { - domainsPurchasedCallback?(result) - } - navigationController?.popViewController(animated: true) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { - domainsPurchasedCallback?(result) - } - } - - func setSwipeGestureEnabledForCurrentState() { - transitionHandler?.isInteractionEnabled = false - cNavigationController?.transitionHandler?.isInteractionEnabled = false - } - - func purchase(domains: [DomainToPurchase], domainsOrderInfoMap: SortDomainsOrderInfoMap?) async throws { - - } - - func createDomainsOrderInfoMap(for domains: [String]) -> SortDomainsOrderInfoMap { - var map = SortDomainsOrderInfoMap() - for (i, domain) in domains.enumerated() { - map[domain] = i - } - return map - } - - func moveToCheckoutWith(domain: DomainToPurchase, - profileChanges: DomainProfilePendingChanges) { - - let wallets = appContext.walletsDataService.wallets - let selectedWallet: WalletEntity - if let wallet = appContext.walletsDataService.selectedWallet { - selectedWallet = wallet - } else if let wallet = wallets.first { - selectedWallet = wallet - appContext.userProfilesService.setActiveProfile(.wallet(wallet)) - } else { - askUserToAddWalletToPurchase(domain: domain, - profileChanges: profileChanges) - return - } - - purchaseData.domain = domain - moveToStep(.checkout(domain: domain, - profileChanges: profileChanges, - selectedWallet: selectedWallet, - wallets: wallets)) - } - - func askUserToAddWalletToPurchase(domain: DomainToPurchase, - profileChanges: DomainProfilePendingChanges) { - Task { - do { - let action = try await appContext.pullUpViewService.showAddWalletSelectionPullUp(in: self, - presentationOptions: .addToPurchase, - actions: WalletDetailsAddWalletAction.allCases) - await dismissPullUpMenu() - - UDRouter().showAddWalletScreenForAction(action, - in: self, - addedCallback: { [weak self] result in - switch result { - case .created, .createdAndBackedUp: - self?.moveToCheckoutWith(domain: domain, - profileChanges: profileChanges) - case .cancelled, .failedToAdd: - return - } - }) - } - } - } -} - -// MARK: - Setup methods -private extension PurchaseDomainsNavigationController { - func setup() { - isModalInPresentation = true - setupBackButtonAlwaysVisible() - - switch mode { - case .default: - if let initialViewController = createStep(.searchDomain) { - setViewControllers([initialViewController], animated: false) - } - } - setSwipeGestureEnabledForCurrentState() - } - - func setupBackButtonAlwaysVisible() { - navigationBar.alwaysShowBackButton = true - navigationBar.setBackButton(hidden: false) - } - - func createStep(_ step: Step) -> UIViewController? { - switch step { - case .searchDomain: - let vc = PurchaseSearchDomainsViewController() - vc.purchaseDomainsFlowManager = self - return vc - case .fillProfile(let domain): - let vc = DomainProfileViewController.nibInstance() - let presenter = PurchaseDomainDomainProfileViewPresenter(view: vc, - domain: domain) - presenter.purchaseDomainsFlowManager = self - vc.presenter = presenter - return vc - case .checkout(let domain, let profileChanges, let selectedWallet, let wallets): - let vc = PurchaseDomainsCheckoutViewController.instantiate(domain: domain, - profileChanges: profileChanges, - selectedWallet: selectedWallet, - wallets: wallets) - vc.purchaseDomainsFlowManager = self - return vc - case .purchased: - let vc = HappyEndViewController.instance() - let presenter = PurchaseDomainsHappyEndViewPresenter(view: vc) - presenter.purchaseDomainsFlowManager = self - vc.presenter = presenter - return vc - } - } -} - -// MARK: - Private methods -private extension PurchaseDomainsNavigationController { - struct PurchaseData { - var domain: DomainToPurchase? - var wallet: UDWallet? = nil - } -} - -extension PurchaseDomainsNavigationController { - enum Mode { - case `default` - } - - enum Step { - case searchDomain - case fillProfile(domain: DomainToPurchase) - case checkout(domain: DomainToPurchase, profileChanges: DomainProfilePendingChanges, selectedWallet: WalletEntity, wallets: [WalletEntity]) - case purchased - } - - enum Action { - case didSelectDomain(_ domain: DomainToPurchase) - case didFillProfileForDomain(_ domain: DomainToPurchase, profileChanges: DomainProfilePendingChanges) - case didPurchaseDomains - case goToDomains - } - - enum Result { - case cancel - case purchased(domainName: String) - } -} - -import SwiftUI -struct PurchaseDomainsNavigationControllerWrapper: UIViewControllerRepresentable { - - let domainsPurchasedCallback: PurchaseDomainsNavigationController.DomainsPurchasedCallback - - func makeUIViewController(context: Context) -> UIViewController { - let purchaseDomainsNavigationController = PurchaseDomainsNavigationController() - purchaseDomainsNavigationController.domainsPurchasedCallback = domainsPurchasedCallback - return purchaseDomainsNavigationController - } - - func updateUIViewController(_ uiViewController: UIViewController, context: Context) { } - -} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsNavigationDestination.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsNavigationDestination.swift new file mode 100644 index 000000000..aba7bfeb5 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsNavigationDestination.swift @@ -0,0 +1,80 @@ +// +// PurchaseDomainsNavigationDestination.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 02.08.2024. +// + +import SwiftUI + +extension PurchaseDomains { + enum NavigationDestination: Hashable { + case root(HomeTabRouter) + case checkout(_ checkoutData: CheckoutData, viewModel: PurchaseDomainsViewModel) + case purchased(_ purchasedDomainsData: PurchasedDomainsData, viewModel: PurchaseDomainsViewModel) + + var isWithCustomTitle: Bool { + if case .purchased = self { + return false + } + return true + } + + var progress: Double { + switch self { + case .root: + return 1 / 6 + case .checkout: + return 5 / 6 + case .purchased: + return 1 + } + } + + static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.root, .root): + return true + case (.checkout, .checkout): + return true + case (.purchased, .purchased): + return true + default: + return false + } + } + + func hash(into hasher: inout Hasher) { + switch self { + case .root: + hasher.combine("root") + case .checkout: + hasher.combine("checkout") + case .purchased: + hasher.combine("purchased") + } + } + } + + struct LinkNavigationDestination { + + @MainActor + @ViewBuilder + static func viewFor(navigationDestination: NavigationDestination) -> some View { + switch navigationDestination { + case .root(let router): + PurchaseDomainsRootView(viewModel: PurchaseDomainsViewModel(router: router)) + case .checkout(let checkoutData, let viewModel): + PurchaseDomainsCheckoutView(domains: checkoutData.domains, + selectedWallet: checkoutData.selectedWallet, + wallets: checkoutData.wallets, + profileChanges: checkoutData.profileChanges ?? .init(domainName: checkoutData.domains[0].name)) + .environmentObject(viewModel) + case .purchased(let purchasedDomainsData, let viewModel): + PurchaseDomainsCompletedView(purchasedDomainsData: purchasedDomainsData) + .environmentObject(viewModel) + } + } + + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsRootView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsRootView.swift new file mode 100644 index 000000000..f97316370 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsRootView.swift @@ -0,0 +1,32 @@ +// +// PurchaseDomainsRootView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 02.08.2024. +// + +import SwiftUI + +struct PurchaseDomainsRootView: View { + + @StateObject var viewModel: PurchaseDomainsViewModel + @EnvironmentObject var tabRouter: HomeTabRouter + @EnvironmentObject var stateManagerWrapper: NavigationStateManagerWrapper + + var body: some View { + ZStack { + PurchaseDomainsSearchView() + .navigationBarTitleDisplayMode(.inline) + if viewModel.isLoading { + ProgressView() + } + } + .displayError($viewModel.error) + .allowsHitTesting(!viewModel.isLoading) + .environmentObject(viewModel) + } +} + +#Preview { + PurchaseDomainsRootView(viewModel: PurchaseDomainsViewModel(router: MockEntitiesFabric.Home.createHomeTabRouter())) +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsTitleView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsTitleView.swift new file mode 100644 index 000000000..beb4bdf54 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsTitleView.swift @@ -0,0 +1,30 @@ +// +// PurchaseDomainsTitleView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 06.08.2024. +// + +import SwiftUI + +struct PurchaseDomainsTitleView: View { + + @EnvironmentObject var tabRouter: HomeTabRouter + @EnvironmentObject var viewModel: PurchaseDomainsViewModel + @State private var swipeBackProgress: Double = 0.0 + + var body: some View { + DashedProgressView(configuration: .init(numberOfDashes: 3), + progress: viewModel.progressFor(swipeBackProgress: swipeBackProgress)) + .trackNavigationControllerEvents(onDidBackGestureProgress: didSwipeBackWithProgress) + .onChange(of: tabRouter.walletViewNavPath) { _ in + DispatchQueue.main.async { + swipeBackProgress = 0 + } + } + } + + private func didSwipeBackWithProgress(_ progress: Double) { + swipeBackProgress = progress + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsTitleViewModifier.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsTitleViewModifier.swift new file mode 100644 index 000000000..064bbd765 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsTitleViewModifier.swift @@ -0,0 +1,61 @@ +// +// PurchaseDomainsTitleViewModifier.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 06.08.2024. +// + +import SwiftUI + +struct PurchaseDomainsTitleViewModifier: ViewModifier { + + @EnvironmentObject var tabRouter: HomeTabRouter + @EnvironmentObject var stateManagerWrapper: NavigationStateManagerWrapper + @EnvironmentObject var viewModel: PurchaseDomainsViewModel + + func body(content: Content) -> some View { + content + .trackNavigationControllerEvents(onDidNotFinishNavigationBack: setupTitleView) + .onChange(of: tabRouter.walletViewNavPath) { _ in + DispatchQueue.main.async { + updateTitleView() + } + } + .onAppear(perform: onAppear) + } +} + +// MARK: - Private methods +private extension PurchaseDomainsTitleViewModifier { + func onAppear() { + setupTitleView() + } + + func setupTitleView() { + withAnimation { + stateManagerWrapper.navigationState?.setCustomTitle(customTitle: { + PurchaseDomainsTitleView() + .environmentObject(viewModel) + .environmentObject(tabRouter) + }, + id: viewModel.id) + updateTitleView() + } + } + + func updateTitleView() { + if case .purchaseDomains(let destination) = tabRouter.walletViewNavPath.last { + stateManagerWrapper.navigationState?.isTitleVisible = destination.isWithCustomTitle + } else { + stateManagerWrapper.navigationState?.isTitleVisible = true + } + + stateManagerWrapper.navigationState?.yOffset = 2 + } +} + +extension View { + func purchaseDomainsTitleViewModifier() -> some View { + modifier(PurchaseDomainsTitleViewModifier()) + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsViewModel.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsViewModel.swift new file mode 100644 index 000000000..2785cf551 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/PurchaseDomainsViewModel.swift @@ -0,0 +1,183 @@ +// +// PurchaseDomainsViewModel.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 02.08.2024. +// + +import Foundation + +@MainActor +final class PurchaseDomainsViewModel: ObservableObject { + + static private var didCheckLocation = false + + @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 + + var progress: Double { + if case .purchaseDomains(let navigationDestination) = router.walletViewNavPath.last { + return navigationDestination.progress + } + return 0 + } + + private var previousProgress: Double { + let path = router.walletViewNavPath + let i = path.count - 2 + guard path.indices.contains(i) else { return 0 } + if case .purchaseDomains(let navigationDestination) = path[i] { + return navigationDestination.progress + } + return 0 + } + + func progressFor(swipeBackProgress: Double) -> Double { + let previousProgress = self.previousProgress + let currentProgress = progress + let progressDiff: Double = currentProgress - previousProgress + let progressDiffAccordingToSwipeBackProgress: Double = progressDiff * (1 - swipeBackProgress) + let result: Double = previousProgress + progressDiffAccordingToSwipeBackProgress + + return result + } + + init(router: HomeTabRouter) { + self.router = router + updateUserSettingsForCurrentLocation() + } + + func handleAction(_ action: PurchaseDomains.FlowAction) { + Task { + do { + switch action { + case .didSelectDomains(let domains): + moveToCheckoutWith(domains: domains, + profileChanges: nil) + 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(let purchasedDomainsData): + pushTo(.purchased(purchasedDomainsData, viewModel: self)) + case .goToDomains: + router.didPurchaseDomains() + } + } catch { + isLoading = false + self.error = error + } + } + } +} + +// MARK: - Private methods +private extension PurchaseDomainsViewModel { + func pushTo(_ destination: PurchaseDomains.NavigationDestination) { + router.walletViewNavPath.append(.purchaseDomains(destination)) + } + + func moveToCheckoutWith(domains: [DomainToPurchase], + profileChanges: DomainProfilePendingChanges?) { + + let wallets = appContext.walletsDataService.wallets + let selectedWallet: WalletEntity + if let wallet = appContext.walletsDataService.selectedWallet { + selectedWallet = wallet + } else if let wallet = wallets.first { + selectedWallet = wallet + appContext.userProfilesService.setActiveProfile(.wallet(wallet)) + } else { + askUserToAddWalletToPurchase(domains: domains, + profileChanges: profileChanges) + return + } + + purchaseData.domains = domains + pushTo(.checkout(.init(domains: domains, + profileChanges: profileChanges, + selectedWallet: selectedWallet, + wallets: wallets), + viewModel: self)) + } + + func askUserToAddWalletToPurchase(domains: [DomainToPurchase], + profileChanges: DomainProfilePendingChanges?) { + Task { + do { + guard let topVC = appContext.coreAppCoordinator.topVC else { return } + + let action = try await appContext.pullUpViewService.showAddWalletSelectionPullUp(in: topVC, + presentationOptions: .addToPurchase, + actions: WalletDetailsAddWalletAction.allCases) + await topVC.dismissPullUpMenu() + + UDRouter().showAddWalletScreenForAction(action, + in: topVC, + addedCallback: { [weak self] result in + switch result { + case .created, .createdAndBackedUp: + self?.moveToCheckoutWith(domains: domains, + profileChanges: profileChanges) + case .cancelled, .failedToAdd: + return + } + }) + } + } + } + + func updateUserSettingsForCurrentLocation() { + guard !Self.didCheckLocation else { return } + + Task { + do { + let isInTheUS = try await appContext.ipVerificationService.isUserInTheUS() + let purchaseLocation = PurchaseDomainsPreferencesStorage.shared.checkoutData.purchaseLocation + + if isInTheUS, + case .other = purchaseLocation { + appContext.analyticsService.log(event: .willChangePurchaseDomainsLocation, + withParameters: [.value: PurchaseDomainsCheckoutData.UserPurchaseLocation.usa.rawValue]) + PurchaseDomainsPreferencesStorage.shared.checkoutData.purchaseLocation = .usa + } else if !isInTheUS, + case .usa = purchaseLocation, + PurchaseDomainsPreferencesStorage.shared.checkoutData.zipCodeIfEntered == nil { + appContext.analyticsService.log(event: .willChangePurchaseDomainsLocation, + withParameters: [.value: PurchaseDomainsCheckoutData.UserPurchaseLocation.other.rawValue]) + PurchaseDomainsPreferencesStorage.shared.checkoutData.purchaseLocation = .other + } + + Self.didCheckLocation = true + } catch { } + } + } +} + +// MARK: - Hashable +@MainActor +extension PurchaseDomainsViewModel: Hashable { + nonisolated static func == (lhs: PurchaseDomainsViewModel, rhs: PurchaseDomainsViewModel) -> Bool { + lhs.id == rhs.id + } + + nonisolated func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +// MARK: - Private methods +private extension PurchaseDomainsViewModel { + struct PurchaseData: Hashable { + var domains: [DomainToPurchase]? + var wallet: UDWallet? = nil + } +} + diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Purchased/PurchaseDomainsCompletedView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Purchased/PurchaseDomainsCompletedView.swift new file mode 100644 index 000000000..ff62aa6d2 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Purchased/PurchaseDomainsCompletedView.swift @@ -0,0 +1,180 @@ +// +// PurchaseDomainsCompletedView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 12.08.2024. +// + +import SwiftUI + +struct PurchaseDomainsCompletedView: View, ViewAnalyticsLogger { + + @EnvironmentObject var viewModel: PurchaseDomainsViewModel + + let purchasedDomainsData: PurchaseDomains.PurchasedDomainsData + @State private var offset: CGPoint = .zero + @State private var confettiCounter = 0 + var analyticsName: Analytics.ViewName { .purchaseDomainsCompleted } + + var body: some View { + VStack(spacing: 0) { + OffsetObservingScrollView(offset: $offset) { + headerView() + purchasedDomainsList() + } + + doneButtonView() + } + .navigationBarBackButtonHidden(true) + .navigationTitle(navTitle) + .navigationBarTitleDisplayMode(.inline) + .background(Color.backgroundDefault) + .toolbar { + // To keep showing top bar when scrolling + ToolbarItem(placement: .topBarTrailing) { + Color.clear + } + } + .udConfetti(counter: $confettiCounter) + .trackAppearanceAnalytics(analyticsLogger: self) + .onAppear { + confettiCounter += 1 + } + } + +} + +// MARK: - Private methods +private extension PurchaseDomainsCompletedView { + var navTitle: String { + if offset.y > 144 { + return String.Constants.youAreAllDoneTitle.localized() + } + return "" + } + + @ViewBuilder + func headerView() -> some View { + LazyVStack(spacing: 24) { + Image.checkCircle + .resizable() + .squareFrame(56) + .foregroundStyle(Color.foregroundAccent) + LazyVStack(spacing: 16) { + Text(String.Constants.youAreAllDoneTitle.localized()) + .textAttributes(color: .foregroundDefault, + fontSize: 32, + fontWeight: .bold) + .onDisappear { + print("title disappeared") + } + orderInfoView() + } + } + .padding(.top, 32) + .padding(.horizontal, 16) + } + + @ViewBuilder + func orderInfoView() -> some View { + AttributedText(attributesList: .init(text: String.Constants.domainsPurchasedSummaryMessage.localized(orderTotal, mintWalletDescription), + font: .currentFont(withSize: 16, weight: .regular), + textColor: .foregroundSecondary, + alignment: .center), + updatedAttributesList: [.init(text: mintWalletName ?? walletAddress, + font: .currentFont(withSize: 16, weight: .medium), + textColor: .foregroundDefault)]) + } + + var wallet: WalletEntity { + purchasedDomainsData.wallet + } + + var orderTotal: String { + purchasedDomainsData.totalSum + } + + var walletAddress: String { + wallet.address.walletAddressTruncated + } + + var mintWalletDescription: String { + if let mintWalletName { + return "\(mintWalletName) (\(walletAddress))" + } + return walletAddress + } + + var mintWalletName: String? { + if wallet.displayInfo.isNameSet { + return wallet.displayInfo.name + } + return nil + } + + @ViewBuilder + func purchasedDomainsList() -> some View { + UDCollectionSectionBackgroundView { + LazyVStack(alignment: .center, spacing: 4) { + ForEach(purchasedDomainsData.domains) { domain in + domainRowView(domain) + } + } + } + .padding(.top, 32) + .padding(.horizontal, 16) + } + + @ViewBuilder + func domainRowView(_ domain: DomainToPurchase) -> some View { + HStack(spacing: 16) { + domain.tldCategory.icon + .resizable() + .squareFrame(24) + .foregroundStyle(Color.foregroundSecondary) + + Text(domain.name) + .textAttributes(color: .foregroundDefault, + fontSize: 16, + fontWeight: .medium) + .lineLimit(1) + Spacer() + } + .frame(height: 64) + .padding(.horizontal, 16) + } + + @ViewBuilder + func doneButtonView() -> some View { + UDButtonView(text: String.Constants.goToDomains.localized(), + style: .large(.raisedPrimary)) { + logButtonPressedAnalyticEvents(button: .done) + viewModel.handleAction(.goToDomains) + } + .padding(.horizontal, 16) + .background(Color.backgroundDefault) + .background( + Color.backgroundDefault + .shadow(color: Color.backgroundDefault, + radius: 8, + y: -8) + .mask(Rectangle().padding(.top, -20)) + ) + } +} + +#Preview { + let router = MockEntitiesFabric.Home.createHomeTabRouter() + let viewModel = PurchaseDomainsViewModel(router: router) + let domains = MockEntitiesFabric.Domains.mockDomainsToPurchase() + let sum = formatCartPrice(domains.reduce(0, { $0 + $1.price })) + let wallet = MockEntitiesFabric.Wallet.mockEntities().randomElement()! + + return NavigationStack { + PurchaseDomainsCompletedView(purchasedDomainsData: .init(domains: domains, + totalSum: sum, + wallet: wallet)) + .environmentObject(router) + .environmentObject(viewModel) + } +} 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 new file mode 100644 index 000000000..4610517ea --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsCartView.swift @@ -0,0 +1,136 @@ +// +// PurchaseDomainsCartView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 07.08.2024. +// + +import SwiftUI + +struct PurchaseDomainsCartView: View, ViewAnalyticsLogger { + + @Environment(\.dismiss) var dismiss + @EnvironmentObject var viewModel: PurchaseDomainsViewModel + var analyticsName: Analytics.ViewName { .purchaseDomainsCart } + + var body: some View { + contentView() + .trackAppearanceAnalytics(analyticsLogger: self) + } +} + +// MARK: - Private methods +private extension PurchaseDomainsCartView { + @ViewBuilder + func contentView() -> some View { + if viewModel.localCart.domains.isEmpty { + emptyView() + .presentationDetents([.height(238)]) + } else { + cartContentView() + .modifier(PurchaseDomainsCheckoutButton()) + .passViewAnalyticsDetails(logger: self) + .presentationDetents([.medium, .large]) + } + } + + @ViewBuilder + func emptyView() -> some View { + VStack(spacing: 16) { + DismissIndicatorView() + .padding(.top, 16) + .padding(.bottom, 4) + + Image.cartFillIcon + .resizable() + .squareFrame(48) + VStack(spacing: 8) { + Text(String.Constants.buyDomainsCartEmptyTitle.localized()) + .font(.currentFont(size: 22, weight: .bold)) + .frame(height: 28) + Text(String.Constants.buyDomainsCartEmptySubtitle.localized()) + .font(.currentFont(size: 16)) + .frame(height: 24) + } + + + UDButtonView(text: String.Constants.searchDomains.localized(), + style: .medium(.ghostPrimary)) { + logButtonPressedAnalyticEvents(button: .searchDomains) + dismiss() + } + Spacer() + } + .foregroundStyle(Color.foregroundSecondary) + .padding(.horizontal, 16) + .backgroundStyle(Color.clear) + } + + @ViewBuilder + func cartContentView() -> some View { + ScrollView { + VStack(spacing: 32) { + headerView() + domainsListView() + } + .padding(.horizontal, 16) + } + .padding(.top, 36) + } + + + @ViewBuilder + func headerView() -> some View { + HStack(spacing: 4) { + Text(String.Constants.buyDomainsCartTitle.localized(viewModel.localCart.domains.count)) + .textAttributes(color: .foregroundDefault, + fontSize: 22, + fontWeight: .bold) + Spacer() + Button { + logButtonPressedAnalyticEvents(button: .clear) + UDVibration.buttonTap.vibrate() + viewModel.localCart.clearCart() + } label: { + Text(String.Constants.clear.localized()) + .textAttributes(color: .foregroundSecondary, + fontSize: 16, + fontWeight: .medium) + .underline() + } + } + } + + @ViewBuilder + func domainsListView() -> some View { + UDCollectionSectionBackgroundView { + LazyVStack(spacing: 4) { + ForEach(viewModel.localCart.domains) { domain in + domainListRow(domain) + .udListItemInCollectionButtonPadding() + } + } + } + } + + @ViewBuilder + func domainListRow(_ domain: DomainToPurchase) -> some View { + Button { + logButtonPressedAnalyticEvents(button: .removeDomain) + UDVibration.buttonTap.vibrate() + withAnimation { + viewModel.localCart.removeDomain(domain) + } + } label: { + PurchaseDomainsSearchResultRowView(domain: domain, + mode: .cart) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } +} + +#Preview { + PurchaseDomainsCartView() +// .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 new file mode 100644 index 000000000..d74d4b2e1 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsCheckoutButton.swift @@ -0,0 +1,44 @@ +// +// PurchaseDomainsCheckoutButton.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 07.08.2024. +// + +import SwiftUI + +struct PurchaseDomainsCheckoutButton: ViewModifier, ViewAnalyticsLogger { + + @Environment(\.analyticsViewName) var analyticsName + @Environment(\.analyticsAdditionalProperties) var additionalAppearAnalyticParameters + @EnvironmentObject var viewModel: PurchaseDomainsViewModel + + func body(content: Content) -> some View { + VStack { + content + + if !viewModel.localCart.domains.isEmpty { + UDButtonView(text: String.Constants.checkout.localized(), + subtext: subtitle, + style: .large(.raisedPrimary)) { + logButtonPressedAnalyticEvents(button: .checkout) + if viewModel.localCart.isShowingCart { + viewModel.localCart.isShowingCart = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: didConfirmToCheckout) + } else { + didConfirmToCheckout() + } + } + .padding(.horizontal, 16) + } + } + } + + private var subtitle: String { + "\(String.Constants.totalDue.localized()): \(formatCartPrice(viewModel.localCart.totalPrice))" + } + + private func didConfirmToCheckout() { + viewModel.handleAction(.didSelectDomains(viewModel.localCart.domains)) + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsSearchFiltersView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsSearchFiltersView.swift new file mode 100644 index 000000000..7f718e219 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsSearchFiltersView.swift @@ -0,0 +1,149 @@ +// +// PurchaseDomainsSearchFiltersView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 12.08.2024. +// + +import SwiftUI + +struct PurchaseDomainsSearchFiltersView: View, ViewAnalyticsLogger { + + @Environment(\.dismiss) var dismiss + + let appliedFilters: Set + let callback: (Set)->() + private let tlds: [String] + @State private var currentFilters: Set = [] + var analyticsName: Analytics.ViewName { .purchaseDomainsFilters } + + var body: some View { + NavigationStack { + contentView() + .navigationTitle(String.Constants.filter.localized()) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + CloseButtonView { + logButtonPressedAnalyticEvents(button: .close) + dismiss() + } + } + + ToolbarItem(placement: .topBarTrailing) { + resetButton() + } + } + .trackAppearanceAnalytics(analyticsLogger: self) + } + } + + init(appliedFilters: Set, + callback: @escaping (Set) -> Void) { + self.appliedFilters = appliedFilters + self.callback = callback + let tlds = User.instance.getAppVersionInfo().tldsToPurchase ?? [] + self.tlds = tlds.filter({ TLDCategory.categoryFor(tld: $0) == .uns }) + self._currentFilters = State(wrappedValue: appliedFilters) + } + +} + +// MARK: - Private methods +private extension PurchaseDomainsSearchFiltersView { + @ViewBuilder + func contentView() -> some View { + VStack { + ScrollView { + filtersSection() + } + doneButton() + } + } + + @ViewBuilder + func filtersSection() -> some View { + VStack(alignment: .leading, + spacing: 16) { + Text(String.Constants.endings.localized()) + .textAttributes(color: .foregroundDefault, + fontSize: 16, + fontWeight: .medium) + .frame(height: 24) + filtersListView() + } + .padding() + } + + @ViewBuilder + func filtersListView() -> some View { + UDCollectionSectionBackgroundView(withShadow: true) { + LazyVStack(alignment: .leading, + spacing: 24) { + ForEach(tlds, id: \.self) { tld in + tldRowView(tld) + } + } + .padding(16) + } + } + + @ViewBuilder + func tldRowView(_ tld: String) -> some View { + HStack(spacing: 16) { + TLDCategory.categoryFor(tld: tld) + .icon + .resizable() + .squareFrame(24) + .foregroundStyle(Color.foregroundSecondary) + Text(tld) + .textAttributes(color: .foregroundDefault, + fontSize: 16, + fontWeight: .medium) + Spacer() + + UDCheckBoxView(isOn: Binding( + get: { + currentFilters.contains(tld) + }, set: { isOn in + let analyticsButton: Analytics.Button + if isOn { + currentFilters.insert(tld) + analyticsButton = .selectTLD + } else { + currentFilters.remove(tld) + analyticsButton = .deselectTLD + } + logButtonPressedAnalyticEvents(button: analyticsButton) + }) + ) + } + .frame(height: 40) + } + + @ViewBuilder + func doneButton() -> some View { + UDButtonView(text: String.Constants.doneButtonTitle.localized(), + style: .large(.raisedPrimary)) { + logButtonPressedAnalyticEvents(button: .done) + dismiss() + callback(currentFilters) + } + .padding(.horizontal, 16) + } + + @ViewBuilder + func resetButton() -> some View { + UDButtonView(text: String.Constants.reset.localized(), + style: .medium(.ghostPrimary)) { + logButtonPressedAnalyticEvents(button: .reset) + currentFilters = [] + } + .disabled(currentFilters.isEmpty) + } +} + +#Preview { + PurchaseDomainsSearchFiltersView(appliedFilters: ["x", "crypto"], + callback: { _ in }) +} 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 new file mode 100644 index 000000000..06163dca1 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsSearchResultRowView.swift @@ -0,0 +1,88 @@ +// +// PurchaseDomainSearchResultRowView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 07.08.2024. +// + +import SwiftUI + +struct PurchaseDomainsSearchResultRowView: View { + + @EnvironmentObject var viewModel: PurchaseDomainsViewModel + let domain: DomainToPurchase + let mode: RowMode + + var body: some View { + HStack(spacing: 8) { + HStack(spacing: 16) { + domain.tldCategory.icon + .squareFrame(24) + .foregroundStyle(Color.foregroundSecondary) + VStack(alignment: .leading, spacing: 0) { + Text(domain.name) + .textAttributes(color: .foregroundDefault, + fontSize: 16, + fontWeight: .medium) + .frame(height: 24) + Text(formatCartPrice(domain.price)) + .textAttributes(color: .foregroundSecondary, + fontSize: 14) + .frame(height: 20) + } + } + Spacer() + if !domain.isTaken { + cartIconView() + .squareFrame(24) + } + } + .frame(minHeight: UDListItemView.height) + } +} + +// MARK: - Private methods +private extension PurchaseDomainsSearchResultRowView { + @ViewBuilder + func cartIconView() -> some View { + switch mode { + case .list: + if viewModel.localCart.isDomainInCart(domain) { + Image.checkCircle + .resizable() + .foregroundStyle(Color.foregroundSuccess) + } else { + if domain.isTooExpensiveToBuyInApp { + Image.arrowTopRight + .resizable() + .foregroundStyle(Color.foregroundSecondary) + } else { + Image.addToCartIcon + .resizable() + .foregroundStyle(Color.foregroundAccent) + } + } + case .cart: + Image.trashIcon + .resizable() + .foregroundStyle(Color.foregroundMuted) + } + } +} + +extension PurchaseDomainsSearchResultRowView { + enum RowMode { + case list + case cart + } +} + +#Preview { + PurchaseDomainsSearchResultRowView(domain: .init(name: "oleg.eth", + price: 199, + metadata: nil, + isTaken: false, + isAbleToPurchase: true), + mode: .list) +// .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 new file mode 100644 index 000000000..9b19ddbcd --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseDomainsSearchView.swift @@ -0,0 +1,535 @@ +// +// SearchDomainsView.swift +// UBTSharing +// +// Created by Oleg Kuplin on 27.10.2023. +// + +import SwiftUI +import Combine + +struct PurchaseDomainsSearchView: View, ViewAnalyticsLogger { + + @Environment(\.purchaseDomainsService) private var purchaseDomainsService + @EnvironmentObject var viewModel: PurchaseDomainsViewModel + @StateObject private var debounceObject = DebounceObject() + @StateObject private var ecommFlagTracker = UDMaintenanceModeFeatureFlagTracker(featureFlag: .isMaintenanceEcommEnabled) + private var localCart: PurchaseDomains.LocalCart { viewModel.localCart } + @State private var suggestions: [DomainToPurchase] = [] + @State private var searchResultHolder: PurchaseDomains.SearchResultHolder = .init() + @State private var searchFiltersHolder: PurchaseDomains.SearchFiltersHolder = .init() + @State private var isLoading: Bool = false + @State private var loadingError: Error? + @State private var searchingText: String = "" + @State private var searchResultType: SearchResultType = .userInput + @State private var skeletonItemsWidth: [CGFloat] = [] + @State private var pullUp: ViewPullUpConfigurationType? + + var analyticsName: Analytics.ViewName { .purchaseDomainsSearch } + + var body: some View { + currentContentView() + .animation(.default, value: UUID()) + .background(Color.backgroundDefault) + .viewPullUp($pullUp) + .onAppear(perform: onAppear) + .sheet(isPresented: $viewModel.localCart.isShowingCart, content: { + PurchaseDomainsCartView() + }) + .sheet(isPresented: $searchFiltersHolder.isFiltersVisible, content: { + PurchaseDomainsSearchFiltersView(appliedFilters: searchFiltersHolder.tlds, + callback: updateTLDFilters) + }) + .navigationTitle(String.Constants.buyDomainsSearchTitle.localized()) + .navigationBarTitleDisplayMode(.inline) + .trackAppearanceAnalytics(analyticsLogger: self) + .passViewAnalyticsDetails(logger: self) + } +} + +// MARK: - Views +private extension PurchaseDomainsSearchView { + @ViewBuilder + func currentContentView() -> some View { + if ecommFlagTracker.maintenanceData?.isCurrentlyEnabled == true { + MaintenanceDetailsFullView(serviceType: .purchaseDomains, + maintenanceData: ecommFlagTracker.maintenanceData) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + contentView() + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + filterButtonView() + } + ToolbarItem(placement: .topBarTrailing) { + cartButtonView() + .padding(.leading, 10) + } + } + .modifier(PurchaseDomainsCheckoutButton()) + } + } + + @ViewBuilder + func cartButtonView() -> some View { + Button { + logButtonPressedAnalyticEvents(button: .cart) + UDVibration.buttonTap.vibrate() + viewModel.localCart.isShowingCart = true + } label: { + ZStack(alignment: .topTrailing) { + Image.cartIcon + .resizable() + .squareFrame(28) + .foregroundStyle(Color.foregroundDefault) + if !localCart.domains.isEmpty { + Text("\(localCart.domains.count)") + .textAttributes(color: .foregroundDefault, + fontSize: 11, + fontWeight: .semibold) + .padding(.horizontal, 4) + .frame(height: 16) + .frame(minWidth: 16) + .background(Color.foregroundAccent) + .clipShape(.capsule) + .offset(x: 6, y: -4) + } + } + } + .buttonStyle(.plain) + .animation(.default, value: localCart.domains) + } + + @ViewBuilder + func filterButtonView() -> some View { + Button { + logButtonPressedAnalyticEvents(button: .filterOption) + UDVibration.buttonTap.vibrate() + searchFiltersHolder.isFiltersVisible = true + } label: { + ZStack(alignment: .topTrailing) { + Image.filter + .resizable() + .squareFrame(28) + .foregroundStyle(Color.foregroundDefault) + if searchFiltersHolder.isFiltersApplied { + Circle() + .squareFrame(16) + .foregroundStyle(Color.foregroundAccent) + .offset(x: 6, y: -4) + } + } + } + .buttonStyle(.plain) + .animation(.default, value: localCart.domains) + } + + @ViewBuilder + func contentView() -> some View { + ScrollView { + VStack(spacing: 10) { + searchView() + searchResultView() + .padding(.top, 16) + } + .padding(.horizontal, 16) + .padding(.top, 16) + } + } + + @ViewBuilder + func searchView() -> some View { + UDTextFieldView(text: $debounceObject.text, + placeholder: String.Constants.searchForADomain.localized(), + hint: nil, + rightViewMode: .always, + leftViewType: .search, + focusBehaviour: .activateOnAppear, + keyboardType: .alphabet, + autocapitalization: .never, + autocorrectionDisabled: true, + height: 36) + .onChange(of: debounceObject.debouncedText) { text in + search(text: text, searchType: .userInput) + } + } + + @ViewBuilder + func searchResultView() -> some View { + if isLoading && !searchingText.isEmpty && searchResultHolder.isEmpty { + loadingView() + } else if !searchResultHolder.isEmpty { + resultListView() + } else if loadingError != nil { + errorView() + } else if !searchingText.isEmpty { + noResultsView() + } else { + emptyStateView() + } + } + + @ViewBuilder + func loadingView() -> some View { + VStack { + ForEach(skeletonItemsWidth, id: \.self) { itemWidth in + domainSearchSkeletonRow(itemWidth: itemWidth) + } + .setSkeleton(.constant(true), + animationType: .solid(.backgroundSubtle)) + } + } + + @ViewBuilder + func sectionTitleView(_ title: String) -> some View { + Text(title) + .textAttributes(color: .foregroundDefault, + fontSize: 16, + fontWeight: .medium) + .frame(height: 24) + } + + @ViewBuilder + func resultListView() -> some View { + if searchResultHolder.isShowingTakenDomains { + resultDomainsListView(searchResultHolder.allDomains) + } else { + resultDomainsListView(searchResultHolder.availableDomains) + } + suggestionsSectionView() + } + + @ViewBuilder + func resultDomainsListView(_ domains: [DomainToPurchase]) -> some View { + domainsListViewWith(title: String.Constants.results.localized(), + domains: domains) + } + + @ViewBuilder + func domainsListViewWith(title: String, + domains: [DomainToPurchase]) -> some View { + LazyVStack(alignment: .leading, spacing: 20) { + sectionTitleView(title) + ForEach(domains) { domain in + resultDomainRowView(domain) + } + } + } + + @ViewBuilder + func resultDomainRowView(_ domain: DomainToPurchase) -> some View { + Button { + UDVibration.buttonTap.vibrate() + didSelectDomain(domain) + } label: { + PurchaseDomainsSearchResultRowView(domain: domain, + mode: .list) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .disabled(domain.isTaken) + } + + @ViewBuilder + func showHideTakenDomainsView() -> some View { + Button { + UDVibration.buttonTap.vibrate() + withAnimation { + searchResultHolder.isShowingTakenDomains.toggle() + } + } label: { + HStack(spacing: 18) { + currentShowHideImage + .resizable() + .squareFrame(20) + + Text(currentShowHideTitle) + .textAttributes(color: .foregroundSecondary, + fontSize: 14, + fontWeight: .medium) + .underline() + Spacer() + } + .foregroundStyle(Color.foregroundSecondary) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + + var currentShowHideImage: Image { + searchResultHolder.isShowingTakenDomains ? .chevronUp : .chevronDown + } + + var currentShowHideTitle: String { + searchResultHolder.isShowingTakenDomains ? String.Constants.buyDomainsSearchResultShowLessTitle.localized() : String.Constants.buyDomainsSearchResultShowMoreTitle.localized() + } + + @ViewBuilder + func suggestionsSectionView() -> some View { + if !suggestions.isEmpty { + domainsListViewWith(title: String.Constants.suggestedForYou.localized(), + domains: suggestions) + } + } + + @ViewBuilder + func noResultsView() -> some View { + PurchaseSearchEmptyView(mode: .noResults) + } + + @ViewBuilder + func errorView() -> some View { + PurchaseSearchEmptyView(mode: .error) + } + + @ViewBuilder + func emptyStateView() -> some View { + if searchResultHolder.recentSearches.isEmpty { + PurchaseSearchEmptyView(mode: .start) + } else { + recentSearchesSectionView() + } + } + + @ViewBuilder + func recentSearchesSectionView() -> some View { + LazyVStack(alignment: .leading, spacing: 20) { + sectionTitleView(String.Constants.recent.localized()) + ForEach(searchResultHolder.recentSearches, id: \.self) { search in + recentSearchRowView(search) + } + } + } + + @ViewBuilder + func recentSearchRowView(_ search: String) -> some View { + HStack(spacing: 16) { + Button { + logButtonPressedAnalyticEvents(button: .recentSearch, parameters: [.value: search]) + UDVibration.buttonTap.vibrate() + self.search(text: search, searchType: .recent) + debounceObject.text = search + } label: { + HStack { + Image.clock + .resizable() + .squareFrame(24) + .foregroundStyle(Color.foregroundSecondary) + Text(search) + .textAttributes(color: .foregroundDefault, + fontSize: 16, + fontWeight: .medium) + .lineLimit(1) + Spacer() + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + + Button { + logButtonPressedAnalyticEvents(button: .clearFromRecentSearch, parameters: [.value: search]) + UDVibration.buttonTap.vibrate() + searchResultHolder.removeRecentSearch(string: search) + } label: { + Image.cancelIcon + .resizable() + .squareFrame(24) + .foregroundStyle(Color.foregroundSecondary) + } + .buttonStyle(.plain) + } + .frame(height: 44) + } +} + +// MARK: - Private methods +private extension PurchaseDomainsSearchView { + func onAppear() { + setupSkeletonItemsWidth() + } + + func setupSkeletonItemsWidth() { + guard skeletonItemsWidth.isEmpty else { return } + + for _ in 0..<6 { + let width: CGFloat = 60 + CGFloat(arc4random_uniform(100)) + skeletonItemsWidth.append(width) + } + } + + func loadSuggestions() { + suggestions.removeAll() + let searchingText = self.searchingText + Task { + do { + let suggestions = try await purchaseDomainsService.getDomainsSuggestions(hint: searchingText, + tlds: searchFiltersHolder.tlds) + guard searchingText == self.searchingText else { return } + + self.suggestions = suggestions.filter({ $0.tldCategory == .uns }) + } catch { + Debugger.printFailure("Failed to load suggestions") + } + } + } + + func updateTLDFilters(_ tlds: Set) { + self.searchFiltersHolder.setTLDs(tlds) + + let searchingText = self.searchingText + if !searchingText.isEmpty { + self.searchingText = "" + self.search(text: searchingText, + searchType: self.searchResultType) + } + } + + func search(text: String, + searchType: SearchResultType) { + let text = text.trimmedSpaces.lowercased() + guard searchingText != text else { return } + searchingText = text + loadingError = nil + + searchResultHolder.clear() + + guard !searchingText.isEmpty else { return } + + loadSuggestions() + let stream = purchaseDomainsService.searchForDomains(key: text, + tlds: searchFiltersHolder.tlds) + performSearchOperation(searchingText: text, searchType: searchType, stream: stream) + } + + func performSearchOperation(searchingText: String, + searchType: SearchResultType, + stream: AsyncThrowingStream<[DomainToPurchase], Error>) { + Task { + logAnalytic(event: .didSearch, parameters: [.value: searchingText, + .searchType: searchType.rawValue]) + + isLoading = true + do { + for try await searchResult in stream { + guard searchingText == self.searchingText else { return } // Result is irrelevant, search query has changed + + searchResultHolder.addDomains(searchResult, searchText: searchingText) + self.searchResultType = searchType + } + } catch { + loadingError = error + } + isLoading = false + } + } + + func didSelectDomain(_ domain: DomainToPurchase) { + if domain.isAbleToPurchase { + didSelectDomainToPurchase(domain) + } else { + logButtonAnalyticsForDomain(domain, button: .domainUnableToPurchase) + logAnalytic(event: .didSelectNotSupportedDomainForPurchaseInSearch, parameters: [.domainName: domain.name, + .price : String(domain.price), + .searchType: searchResultType.rawValue]) + pullUp = .default(.init(icon: .init(icon: .cartIcon, size: .large), + title: .text(String.Constants.purchaseSearchCantButPullUpTitle.localized()), + subtitle: .label(.highlightedText(.init(text: String.Constants.purchaseSearchCantButPullUpSubtitle.localized(domain.tld), + highlightedText: [.init(highlightedText: domain.tld, + highlightedColor: .foregroundDefault)], + analyticsActionName: nil, + action: nil))), + actionButton: .main(content: .init(title: String.Constants.goToWebsite.localized(), + analyticsName: .goToWebsite, + action: { + moveToPurchaseDomainsFromTheWeb(domain: domain) + })), + cancelButton: .gotItButton(), + analyticName: .searchPurchaseDomainNotSupported)) + } + } + + func didSelectDomainToPurchase(_ domain: DomainToPurchase) { + if domain.isTooExpensiveToBuyInApp { + logButtonAnalyticsForDomain(domain, button: .expensiveDomain) + pullUp = .default(.buyDomainFromTheWebsite(goToWebCallback: { + moveToPurchaseDomainsFromTheWeb(domain: domain) + })) + } else if localCart.isDomainInCart(domain) { + logButtonAnalyticsForDomain(domain, button: .removeDomain) + viewModel.localCart.removeDomain(domain) + } else { + logButtonAnalyticsForDomain(domain, button: .addDomain) + if !localCart.canAddDomainToCart(domain) { + pullUp = .default(.checkoutFromTheWebsite(goToWebCallback: { + moveToPurchaseDomainsFromTheWeb(domain: domain) + })) + } else { + viewModel.localCart.addDomain(domain) + } + } + } + + func logButtonAnalyticsForDomain(_ domain: DomainToPurchase, + button: Analytics.Button) { + let analyticsParameters: Analytics.EventParameters = [.value: domain.name, + .price: String(domain.price), + .searchType: searchResultType.rawValue] + logButtonPressedAnalyticEvents(button: button, parameters: analyticsParameters) + } + + func moveToPurchaseDomainsFromTheWeb(domain: DomainToPurchase?) { + openLinkExternally(.unstoppableDomainSearch(searchKey: domain?.name ?? "")) + } +} + +// MARK: - Views +private extension PurchaseDomainsSearchView { + @ViewBuilder + func domainSearchSkeletonRow(itemWidth: CGFloat) -> some View { + HStack(spacing: 8) { + HStack(spacing: 16) { + Image.check + .squareFrame(40) + .skeletonable() + .clipShape(Circle()) + Text("") + .frame(height: 12) + .frame(minWidth: itemWidth) + .skeletonable() + .skeletonCornerRadius(6) + } + Spacer() + Text("") + .frame(width: 40, + height: 12) + .skeletonable() + .skeletonCornerRadius(6) + } + .frame(height: UDListItemView.height) + } +} + +// MARK: - Private methods +private extension PurchaseDomainsSearchView { + enum SearchResultType: String { + case userInput + case suggestion + case recent + case aiSearch + } +} + +#Preview { + PurchaseDomains.RecentDomainsToPurchaseSearchStorage.instance.addDomainToPurchaseSearchToRecents("somesearchqueue") + let router = MockEntitiesFabric.Home.createHomeTabRouter() + let viewModel = PurchaseDomainsViewModel(router: router) + let stateWrapper = NavigationStateManagerWrapper() + + return NavigationStack { + PurchaseDomainsSearchView() + .environment(\.purchaseDomainsService, MockFirebaseInteractionsService()) + .environmentObject(stateWrapper) + .environmentObject(router) + .environmentObject(viewModel) + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseSearchDomainsView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseSearchDomainsView.swift deleted file mode 100644 index c05842ac4..000000000 --- a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseSearchDomainsView.swift +++ /dev/null @@ -1,496 +0,0 @@ -// -// SearchDomainsView.swift -// UBTSharing -// -// Created by Oleg Kuplin on 27.10.2023. -// - -import SwiftUI -import Combine - -struct PurchaseSearchDomainsView: View, ViewAnalyticsLogger { - - @Environment(\.purchaseDomainsService) private var purchaseDomainsService - @StateObject private var debounceObject = DebounceObject() - @StateObject private var ecommFlagTracker = UDMaintenanceModeFeatureFlagTracker(featureFlag: .isMaintenanceEcommEnabled) - @State private var suggestions: [DomainToPurchaseSuggestion] = [] - @State private var searchResult: [DomainToPurchase] = [] - @State private var isLoading = false - @State private var isInspiring = false - @State private var loadingError: Error? - @State private var searchingText = "" - @State private var searchResultType: SearchResultType = .userInput - @State private var scrollOffset: CGPoint = .zero - @State private var skeletonItemsWidth: [CGFloat] = [] - @State private var pullUp: ViewPullUpConfigurationType? - - var domainSelectedCallback: ((DomainToPurchase)->()) - var scrollOffsetCallback: ((CGPoint)->())? = nil - var analyticsName: Analytics.ViewName { .purchaseDomainsSearch } - - var body: some View { - currentContentView() - .animation(.default, value: UUID()) - .background(Color.backgroundDefault) - .onChange(of: scrollOffset) { newValue in - scrollOffsetCallback?(newValue) - } - .viewPullUp($pullUp) - .onAppear(perform: onAppear) - } -} - -// MARK: - Views -private extension PurchaseSearchDomainsView { - @ViewBuilder - func currentContentView() -> some View { - if ecommFlagTracker.maintenanceData?.isCurrentlyEnabled == true { - MaintenanceDetailsFullView(serviceType: .purchaseDomains, - maintenanceData: ecommFlagTracker.maintenanceData) - .frame(maxWidth: .infinity, maxHeight: .infinity) - } else { - contentView() - } - } - - @ViewBuilder - func contentView() -> some View { - GeometryReader { geom in - ZStack { - OffsetObservingScrollView(offset: $scrollOffset) { - VStack { - headerView() - searchView() - searchResultView() - } - } - searchInspirationButton() - .offset(y: (geom.size.height / 2) - searchInspirationButtonOffsetToKeyboard) - } - } - } - - @ViewBuilder - func headerView() -> some View { - VStack(spacing: 16) { - Text(String.Constants.searchForANewDomain.localized()) - .titleText() - .multilineTextAlignment(.center) - } - .padding(EdgeInsets(top: 56, leading: 0, bottom: 0, trailing: 0)) - } - - @ViewBuilder - func searchView() -> some View { - UDTextFieldView(text: $debounceObject.text, - placeholder: "domain.x", - hint: nil, - rightViewMode: .always, - leftViewType: .search, - focusBehaviour: .activateOnAppear, - keyboardType: .alphabet, - autocapitalization: .never, - autocorrectionDisabled: true) - .onChange(of: debounceObject.debouncedText) { text in - search(text: text, searchType: .userInput) - } - .padding(EdgeInsets(top: 16, leading: 16, - bottom: 0, trailing: 16)) - } - - var currentSearchFieldRightViewType: UDTextFieldView.RightViewType { - .inspire { isInspiring in - self.isInspiring = isInspiring - if isInspiring { - logButtonPressedAnalyticEvents(button: .inspire) - } else { - logButtonPressedAnalyticEvents(button: .cancelInspire) - } - searchResult = [] - } - } - - @ViewBuilder - func searchResultView() -> some View { - if isLoading && !searchingText.isEmpty { - loadingView() - } else if !searchResult.isEmpty { - resultListView() - } else if loadingError != nil { - errorView() - } else if isInspiring { - inspiringHintsView() - } else if !searchingText.isEmpty { - noResultsView() - } else if !suggestions.isEmpty { - trendingListView() - } - } - - @ViewBuilder - func loadingView() -> some View { - UDCollectionSectionBackgroundView { - VStack { - ForEach(skeletonItemsWidth, id: \.self) { itemWidth in - domainSearchSkeletonRow(itemWidth: itemWidth) - } - .setSkeleton(.constant(true), - animationType: .solid(.backgroundSubtle)) - } - .padding(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16)) - } - .padding() - } - - @ViewBuilder - func resultListView() -> some View { - UDCollectionSectionBackgroundView { - LazyVStack { - ForEach(searchResult, id: \.name) { domainInfo in - UDCollectionListRowButton(content: { - domainSearchResultRow(domainInfo) - .udListItemInCollectionButtonPadding() - }, callback: { - logButtonPressedAnalyticEvents(button: .searchDomains, parameters: [.value: domainInfo.name, - .price: String(domainInfo.price), - .searchType: searchResultType.rawValue]) - didSelectDomain(domainInfo) - }) - } - } - .padding(EdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)) - } - .padding() - } - - @ViewBuilder - func trendingListView() -> some View { - UDCollectionSectionBackgroundView(withShadow: true) { - VStack(alignment: .leading, spacing: 16) { - HStack { - Image.statsIcon - .resizable() - .squareFrame(16) - Text(String.Constants.trending.localized()) - .font(.currentFont(size: 14, weight: .semibold)) - } - .foregroundColor(.foregroundDefault) - - FlowLayoutView(suggestions) { suggestion in - Button(action: { - UDVibration.buttonTap.vibrate() - logButtonPressedAnalyticEvents(button: .suggestedName, parameters: [.value: suggestion.name]) - search(text: suggestion.name, searchType: .suggestion) - debounceObject.text = suggestion.name - }, label: { - Text(suggestion.name) - .font(.currentFont(size: 14, weight: .medium)) - .foregroundColor(.foregroundDefault) - .padding(EdgeInsets(top: 8, leading: 12, - bottom: 8, trailing: 12)) - .background(Color.backgroundMuted2) - .cornerRadius(16) - }) - } - } - .padding() - } - .padding() - } - - @ViewBuilder - func noResultsView() -> some View { - UDCollectionSectionBackgroundView { - VStack(alignment: .center, spacing: 16) { - Image.grimaseIcon - .resizable() - .squareFrame(32) - .foregroundColor(.foregroundSecondary) - VStack(spacing: 8) { - Text(String.Constants.noAvailableDomains.localized()) - .font(.currentFont(size: 20, weight: .bold)) - Text(String.Constants.tryEnterDifferentName.localized()) - .font(.currentFont(size: 14)) - } - .multilineTextAlignment(.center) - .foregroundColor(.foregroundSecondary) - } - .padding(EdgeInsets(top: 24, leading: 24, bottom: 24, trailing: 24)) - } - .padding() - } - - @ViewBuilder - func errorView() -> some View { - UDCollectionSectionBackgroundView { - VStack(alignment: .center, spacing: 16) { - Image.grimaseIcon - .resizable() - .squareFrame(32) - .foregroundColor(.foregroundSecondary) - VStack(spacing: 8) { - Text(String.Constants.somethingWentWrong.localized()) - .font(.currentFont(size: 20, weight: .bold)) - Text(String.Constants.pleaseCheckInternetConnection.localized()) - .font(.currentFont(size: 14)) - } - .multilineTextAlignment(.center) - .foregroundColor(.foregroundSecondary) - } - .padding(EdgeInsets(top: 24, leading: 24, bottom: 24, trailing: 24)) - } - .padding() - } - - @ViewBuilder - func inspiringHintsView() -> some View { - UDCollectionSectionBackgroundView { - VStack(alignment: .leading, spacing: 12) { - ForEach(AIInspireHints.allCases, id: \.self) { hint in - HStack(spacing: 8) { - hint.icon - .resizable() - .squareFrame(16) - Text(hint.title) - .font(.currentFont(size: 14)) - } - .foregroundStyle(Color.foregroundSecondary) - if hint != .hint3 { - 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)) - } - } - } - .padding() - } - .padding() - } - - @ViewBuilder - func searchInspirationButton() -> some View { - if isInspiring, - KeyboardService.shared.isKeyboardOpened, - !searchingText.isEmpty { - ZStack { - Rectangle() - .foregroundStyle(Color.backgroundDefault) - .frame(height: searchInspirationButtonStyle.height + searchInspirationButtonOffset * 2) - UDButtonView(text: String.Constants.search.localized(), style: searchInspirationButtonStyle) { - aiSearch(hint: searchingText) - } - .padding() - } - } - } - - var searchInspirationButtonStyle: UDButtonStyle { .large(.raisedPrimary) } - var searchInspirationButtonOffset: CGFloat { 16 } - var searchInspirationButtonOffsetToKeyboard: CGFloat { (searchInspirationButtonStyle.height + searchInspirationButtonOffset) / 2 } -} - -// MARK: - Private methods -private extension PurchaseSearchDomainsView { - func onAppear() { - setupSkeletonItemsWidth() - loadSuggestions() - - } - - func setupSkeletonItemsWidth() { - guard skeletonItemsWidth.isEmpty else { return } - - for _ in 0..<6 { - let width: CGFloat = 60 + CGFloat(arc4random_uniform(100)) - skeletonItemsWidth.append(width) - } - } - - func loadSuggestions() { - guard suggestions.isEmpty else { return } - - Task { - do { - let suggestions = try await purchaseDomainsService.getDomainsSuggestions(hint: nil) - self.suggestions = suggestions - } catch { - Debugger.printFailure("Failed to load suggestions") - } - } - } - - func search(text: String, searchType: SearchResultType) { - let text = text.trimmedSpaces.lowercased() - guard searchingText != text else { return } - searchingText = text - loadingError = nil - - guard !isInspiring else { return } - searchResult = [] - - guard !searchingText.isEmpty else { return } - - performSearchOperation(searchingText: text, searchType: searchType) { - try await purchaseDomainsService.searchForDomains(key: text) - } - } - - func aiSearch(hint: String) { - searchResult = [] - performSearchOperation(searchingText: hint, searchType: .aiSearch) { - try await purchaseDomainsService.aiSearchForDomains(hint: hint) - } - } - - func performSearchOperation(searchingText: String, searchType: SearchResultType, _ block: @escaping () async throws -> ([DomainToPurchase])) { - Task { - logAnalytic(event: .didSearch, parameters: [.value: searchingText, - .searchType: searchType.rawValue]) - - isLoading = true - do { - let searchResult = try await block() - guard searchingText == self.searchingText else { return } // Result is irrelevant, search query has changed - - self.searchResult = sortSearchResult(searchResult, searchText: searchingText) - self.searchResultType = searchType - } catch { - loadingError = error - } - isLoading = false - } - } - - func sortSearchResult(_ searchResult: [DomainToPurchase], searchText: String) -> [DomainToPurchase] { - var searchResult = searchResult - /// Move exactly matched domain to the top of the list - if let i = searchResult.firstIndex(where: { $0.name == searchText }), - i != 0 { - let matchingDomain = searchResult[i] - searchResult.remove(at: i) - searchResult.insert(matchingDomain, at: 0) - } - return searchResult - } - - func didSelectDomain(_ domain: DomainToPurchase) { - if domain.isAbleToPurchase { - domainSelectedCallback(domain) - } else { - logAnalytic(event: .didSelectNotSupportedDomainForPurchaseInSearch, parameters: [.domainName: domain.name, - .price : String(domain.price), - .searchType: searchResultType.rawValue]) - pullUp = .default(.init(icon: .init(icon: .cartIcon, size: .large), - title: .text(String.Constants.purchaseSearchCantButPullUpTitle.localized()), - subtitle: .label(.highlightedText(.init(text: String.Constants.purchaseSearchCantButPullUpSubtitle.localized(domain.tld), - highlightedText: [.init(highlightedText: domain.tld, - highlightedColor: .foregroundDefault)], - analyticsActionName: nil, - action: nil))), - actionButton: .main(content: .init(title: String.Constants.goToWebsite.localized(), - analyticsName: .goToWebsite, - action: { - openLinkExternally(.unstoppableDomainSearch(searchKey: domain.name)) - })), - cancelButton: .gotItButton(), - analyticName: .searchPurchaseDomainNotSupported)) - } - } -} - -// MARK: - Views -private extension PurchaseSearchDomainsView { - @ViewBuilder - func domainSearchResultRow(_ domain: DomainToPurchase) -> some View { - HStack(spacing: 8) { - HStack(spacing: 16) { - Image.check - .resizable() - .squareFrame(20) - .foregroundStyle(Color.foregroundSuccess) - .padding(EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)) - .background(Color.backgroundSuccess) - .clipShape(Circle()) - Text(domain.name) - .font(.currentFont(size: 16, weight: .medium)) - .foregroundStyle(Color.foregroundDefault) - } - Spacer() - Text(formatCartPrice(domain.price)) - .font(.currentFont(size: 16)) - .foregroundStyle(Color.foregroundSecondary) - Image.chevronRight - .resizable() - .squareFrame(20) - .foregroundStyle(Color.foregroundMuted) - } - .frame(minHeight: UDListItemView.height) - } - - @ViewBuilder - func domainSearchSkeletonRow(itemWidth: CGFloat) -> some View { - HStack(spacing: 8) { - HStack(spacing: 16) { - Image.check - .squareFrame(40) - .skeletonable() - .clipShape(Circle()) - Text("") - .frame(height: 12) - .frame(minWidth: itemWidth) - .skeletonable() - .skeletonCornerRadius(6) - } - Spacer() - Text("") - .frame(width: 40, - height: 12) - .skeletonable() - .skeletonCornerRadius(6) - } - .frame(height: UDListItemView.height) - } -} - -// MARK: - Private methods -private extension PurchaseSearchDomainsView { - enum AIInspireHints: Hashable, CaseIterable { - case hint1, hint2, hint3 - - var title: String { - switch self { - case .hint1: - return String.Constants.aiSearchHint1.localized() - case .hint2: - return String.Constants.aiSearchHint2.localized() - case .hint3: - return String.Constants.aiSearchHint3.localized() - } - } - - var icon: Image { - switch self { - case .hint1: - return .magicWandIcon - case .hint2: - return .tapSingleIcon - case .hint3: - return .warningIcon - } - } - } - - enum SearchResultType: String { - case userInput - case suggestion - case aiSearch - } -} - -#Preview { - PurchaseSearchDomainsView(domainSelectedCallback: { _ in }) - .environment(\.purchaseDomainsService, MockFirebaseInteractionsService()) -} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseSearchDomainsViewController.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseSearchDomainsViewController.swift deleted file mode 100644 index 3603533d2..000000000 --- a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseSearchDomainsViewController.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// PurchaseSearchDomainsViewController.swift -// domains-manager-ios -// -// Created by Oleg Kuplin on 27.11.2023. -// - -import SwiftUI - -final class PurchaseSearchDomainsViewController: BaseViewController, ViewWithDashesProgress { - - override var scrollableContentYOffset: CGFloat? { 16 } - - weak var purchaseDomainsFlowManager: PurchaseDomainsFlowManager? - override var analyticsName: Analytics.ViewName { .purchaseDomainsSearch } - override var preferredStatusBarStyle: UIStatusBarStyle { .default } - - var dashesProgressConfiguration: DashesProgressView.Configuration { .init(numberOfDashes: 3) } - var progress: Double? { 1 / 6 } - - override func viewDidLoad() { - super.viewDidLoad() - - setup() - } -} - -// MARK: - Private methods -private extension PurchaseSearchDomainsViewController { - func didScrollTo(offset: CGPoint) { - cNavigationController?.underlyingScrollViewDidScrollTo(offset: offset) - } - - func didSelectDomain(_ domain: DomainToPurchase) { - Task { - try? await purchaseDomainsFlowManager?.handle(action: .didSelectDomain(domain)) - } - } -} - -// MARK: - Setup methods -private extension PurchaseSearchDomainsViewController { - func setup() { - addProgressDashesView(configuration: .init(numberOfDashes: 3)) - addChildView() - DispatchQueue.main.async { - self.setDashesProgress(self.progress) - } - } - - func addChildView() { - let vc = UIHostingController(rootView: PurchaseSearchDomainsView(domainSelectedCallback: { [weak self] domain in - self?.didSelectDomain(domain) - }, scrollOffsetCallback: { [weak self] offset in - self?.didScrollTo(offset: offset) - })) - addChildViewController(vc, andEmbedToView: view) - } -} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseSearchEmptyView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseSearchEmptyView.swift new file mode 100644 index 000000000..f3865e2de --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/PurchaseDomains/Search/PurchaseSearchEmptyView.swift @@ -0,0 +1,35 @@ +// +// PurchaseSearchEmptyView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 07.08.2024. +// + +import SwiftUI + +struct PurchaseSearchEmptyView: View { + + let mode: PurchaseDomains.EmptyStateMode + + var body: some View { + VStack(spacing: 16) { + mode.icon + .resizable() + .squareFrame(48) + Text(mode.title) + .font(.currentFont(size: 22, weight: .bold)) + if let subtitle = mode.subtitle { + Text(subtitle) + .font(.currentFont(size: 14)) + } + } + .foregroundStyle(Color.foregroundSecondary) + .padding(.horizontal, 16) + .backgroundStyle(Color.clear) + .padding(.top, 56) + } +} + +#Preview { + PurchaseSearchEmptyView(mode: .start) +} 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/Protocols/RecentDomainsToPurchaseSearchStorageProtocol.swift b/unstoppable-ios-app/domains-manager-ios/Protocols/RecentDomainsToPurchaseSearchStorageProtocol.swift new file mode 100644 index 000000000..e60070a01 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Protocols/RecentDomainsToPurchaseSearchStorageProtocol.swift @@ -0,0 +1,15 @@ +// +// RecentDomainsToPurchaseSearchStorageProtocol.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 12.08.2024. +// + +import Foundation + +protocol RecentDomainsToPurchaseSearchStorageProtocol { + func getRecentDomainsToPurchaseSearches() -> [String] + func addDomainToPurchaseSearchToRecents(_ search: String) + func removeDomainToPurchaseSearchToRecents(_ search: String) + func clearRecentDomainsToPurchaseSearches() +} diff --git a/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsService.swift b/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsService.swift index 2125e03d7..5b19feb03 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsService.swift @@ -97,10 +97,10 @@ private extension AnalyticsService { if case .parking = domain.state { numberOfParkedDomains += 1 } - let tld = domain.name.getTldName() + let tld = domain.name.getTldName() ?? "" if tld == Constants.ensDomainTLD { numberOfENSDomains += 1 - } else if tld == Constants.comDomainTLD { + } else if Constants.dnsDomainTLDs.contains(tld) { numberOfCOMDomains += 1 } else { numberOfUDDomains += 1 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 2d6f5df43..290f291ea 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsServiceEnvironment.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsServiceEnvironment.swift @@ -74,6 +74,7 @@ extension Analytics { case didPurchaseDomains, didFailToPurchaseDomains, accountHasUnpaidDomains, applePayNotSupported case purchaseFirebaseRequestError, purchaseGetPaymentDetailsError, purchaseWillUseCachedPaymentDetails case didSelectNotSupportedDomainForPurchaseInSearch + case willChangePurchaseDomainsLocation, willChangeWalletToWebPreferred case shareResult, didSelectHomeTab case didPullToRefresh @@ -261,6 +262,8 @@ extension Analytics { case reconnectMPCWalletPrompt case fullMaintenance + case mintingDomainsList + case purchaseDomainsCart, purchaseDomainsFilters, purchaseDomainsCompleted } } @@ -271,6 +274,7 @@ extension Analytics { case skip, `continue`, learnMore, done, update, close, confirm, clear, share, cancel, gotIt, delete, pay, later, edit, verify, open, refresh, tryAgain, next, lock, logOut, send, logIn case copyToClipboard, pasteFromClipboard case agreeCheckbox + case checkout case termsOfUse, privacyPolicy case getStarted case hidePassword, showPassword @@ -421,7 +425,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 @@ -450,6 +454,9 @@ extension Analytics { case all, income, outcome case filterOption, reset + case cart + case removeDomain, undoRemoveDomain, addDomain, selectTLD, deselectTLD, expensiveDomain, domainUnableToPurchase + case recentSearch, clearFromRecentSearch } } diff --git a/unstoppable-ios-app/domains-manager-ios/Services/AppLaunchService/AppLaunchService.swift b/unstoppable-ios-app/domains-manager-ios/Services/AppLaunchService/AppLaunchService.swift index eb1462f5a..bca297f35 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/AppLaunchService/AppLaunchService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/AppLaunchService/AppLaunchService.swift @@ -225,6 +225,8 @@ private extension AppLaunchService { Constants.deprecatedTLDs = ["coin"] } Constants.newNonInteractableTLDs = [Constants.ensDomainTLD] + Constants.dnsDomainTLDs = Set(appVersion.dnsTlds ?? ["com", "ca", "pw"]) + if !appVersion.mintingIsEnabled { appContext.toastMessageService.showToast(.mintingUnavailable, isSticky: true) } else { diff --git a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/BaseFirebaseInteractionService.swift b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/BaseFirebaseInteractionService.swift index 84f47b793..53547c83f 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/BaseFirebaseInteractionService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/BaseFirebaseInteractionService.swift @@ -21,8 +21,22 @@ class BaseFirebaseInteractionService { static var USER_PROFILE_URL: String { USER_URL.appendingURLPathComponent("profile") } static var DOMAIN_URL: String { baseAPIURL.appendingURLPathComponent("domain") } - static var DOMAIN_SEARCH_URL: String { DOMAIN_URL.appendingURLPathComponents("search", "internal") } - static var DOMAIN_AI_SUGGESTIONS_URL: String { DOMAIN_URL.appendingURLPathComponents("search", "ai-suggestions") } + static var DOMAIN_SEARCH_URL: String { DOMAIN_URL.appendingURLPathComponents("search") } + + static func DOMAIN_UD_SEARCH_URL(tld: TLDCategory) -> String { + switch tld { + case .uns: + DOMAIN_SEARCH_URL.appendingURLPathComponents("internal") + case .dns: + DOMAIN_SEARCH_URL.appendingURLPathComponents("dns") + case .ens: + DOMAIN_SEARCH_URL.appendingURLPathComponents("ens") + } + } + + + static var DOMAIN_SUGGESTIONS_URL: String { DOMAIN_SEARCH_URL.appendingURLPathComponents("suggestions") } + static var DOMAIN_AI_SUGGESTIONS_URL: String { DOMAIN_UD_SEARCH_URL(tld: .uns).appendingURLPathComponents("ai-suggestions") } static func DOMAIN_ENS_STATUS_URL(domain: String) -> String { DOMAIN_URL.appendingURLPathComponents(domain, "ens-status") } @@ -44,6 +58,7 @@ class BaseFirebaseInteractionService { static var CRYPTO_WALLETS_URL: String { baseAPIURL.appendingURLPathComponent("crypto-wallets") } static var USER_WALLET_URL: String { USER_URL.appendingURLPathComponent("wallet") } + static var USER_MINTING_WALLET_URL: String { USER_WALLET_URL.appendingURLPathComponent("minting") } static var USER_MPC_WALLET_URL: String { USER_WALLET_URL.appendingURLPathComponent("mpc") } static func USER_MPC_SETUP_URL(walletAddress: String) -> String { USER_WALLET_URL.appendingURLPathComponents(walletAddress, "mpc", "claim") 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 9a15bed1b..e6934b5a6 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/MockFirebaseInteractionsService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/MockFirebaseInteractionsService.swift @@ -68,27 +68,52 @@ extension MockFirebaseInteractionsService: FirebaseAuthenticationServiceProtocol // MARK: - PurchaseDomainsServiceProtocol extension MockFirebaseInteractionsService: PurchaseDomainsServiceProtocol { - func searchForDomains(key: String) async throws -> [DomainToPurchase] { - await Task.sleep(seconds: 0.5) - let key = key.lowercased() - let tlds = ["x", "crypto", "nft", "wallet", "polygon", "dao", "888", "blockchain", "go", "bitcoin"] - let prices = [40000, 20000, 8000, 4000, 500] - let notSupportedTLDs = ["eth", "com"] + func searchForDomains(key: String, + tlds: Set) -> AsyncThrowingStream<[DomainToPurchase], Error> { + AsyncThrowingStream { continuation in + Task { + await Task.sleep(seconds: 0.5) + let key = key.lowercased() + + let domains = createDomainsWith(name: key) + continuation.yield(domains) + continuation.finish() + } + } + } + + private func createDomainsWith(name: String) -> [DomainToPurchase] { + let tlds: [String] = ["x", "crypto", "nft", "wallet", "polygon", "dao", "888", "blockchain", "go", "bitcoin"] + let prices: [Int] = [Constants.maxPurchaseDomainsSum, 4000_00, 40000, 20000, 8000, 4000, 500] + let isTaken: [Bool] = [true, false] + let notSupportedTLDs: [String] = ["eth", "com"] - let domains = tlds.map { DomainToPurchase(name: "\(key).\($0)", price: prices.randomElement()!, metadata: nil, isAbleToPurchase: true) } - let notSupportedDomains = notSupportedTLDs.map { DomainToPurchase(name: "\(key).\($0)", price: prices.randomElement()!, metadata: nil, isAbleToPurchase: false) } + let domains = tlds.map { + DomainToPurchase(name: "\(name).\($0)", + price: prices.randomElement()!, + metadata: nil, + isTaken: isTaken.randomElement()!, + isAbleToPurchase: true) + } + let notSupportedDomains = notSupportedTLDs.map { + DomainToPurchase(name: "\(name).\($0)", + price: prices.randomElement()!, + metadata: nil, + isTaken: isTaken.randomElement()!, + isAbleToPurchase: false) + } return domains + notSupportedDomains } func aiSearchForDomains(hint: String) async throws -> [DomainToPurchase] { - try await searchForDomains(key: "ai_" + hint) + createDomainsWith(name: "ai_" + hint) } - func getDomainsSuggestions(hint: String?) async throws -> [DomainToPurchaseSuggestion] { + func getDomainsSuggestions(hint: String, tlds: Set) async throws -> [DomainToPurchase] { await Task.sleep(seconds: 0.4) - return ["greenfashion", "naturalstyle", "savvydressers", "ethicalclothes", "urbanfashions", "wearables", "consciouslook", "activegears", "minimalista", "outsizeoutfits", "styletone"].map { DomainToPurchaseSuggestion(name: $0) } + return createDomainsWith(name: "suggest_" + hint) } func addDomainsToCart(_ domains: [DomainToPurchase]) async throws { @@ -107,6 +132,13 @@ extension MockFirebaseInteractionsService: PurchaseDomainsServiceProtocol { return userWallets.map { PurchasedDomainsWalletDescription(address: $0.address, metadata: $0.jsonData()) } } + func getPreferredWalletToMint() async throws -> PurchasedDomainsWalletDescription { + let userWallets = try await loadUserCryptoWallets() + let wallet = userWallets[0] + return PurchasedDomainsWalletDescription(address: wallet.address, + metadata: wallet.jsonData()) + } + func purchaseDomainsInTheCartAndMintTo(wallet: PurchasedDomainsWalletDescription) async throws { } @@ -118,6 +150,11 @@ extension MockFirebaseInteractionsService: PurchaseDomainsServiceProtocol { updateCart() } + func setDomainsToPurchase(_ domains: [DomainToPurchase]) async throws { + cart = MockFirebaseInteractionsService.createMockCart() + updateCart() + } + func reset() async { cartStatus = .ready(cart: .empty) updateCart() @@ -142,10 +179,13 @@ private extension MockFirebaseInteractionsService { } func loadUserCryptoWallets() async throws -> [Ecom.UDUserAccountCryptWallet] { - let wallets = appContext.udWalletsService.getUserWallets() - return wallets.map { Ecom.UDUserAccountCryptWallet(id: 1, address: $0.address, type: "") } -// [.init(id: 1605, address: "0xc4a748796805dfa42cafe0901ec182936584cc6e"), -// .init(id: 1606, address: "0x8ed92xjd2793yenx837g3847d3n4dx9h")] + let wallets = MockEntitiesFabric.Wallet.mockEntities() + var cryptoWallets: [Ecom.UDUserAccountCryptWallet] = [] + for (i, wallet) in wallets.enumerated() { + let cryptoWallet = Ecom.UDUserAccountCryptWallet(id: i, address: wallet.address, type: "") + cryptoWallets.append(cryptoWallet) + } + return cryptoWallets } func updateCart() { @@ -154,7 +194,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) @@ -167,7 +207,11 @@ private extension MockFirebaseInteractionsService { } static func createMockCart() -> PurchaseDomainsCart { - .init(domains: [.init(name: "oleg.x", price: 10000, metadata: nil, isAbleToPurchase: true)], + .init(domains: [.init(name: "oleg.x", + price: 10000, + metadata: nil, + isTaken: false, + isAbleToPurchase: true)], totalPrice: 10000, taxes: 0, storeCreditsAvailable: 100, diff --git a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Ecom.swift b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Ecom.swift index 370f645d6..8c01b4ae3 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Ecom.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Ecom.swift @@ -232,7 +232,7 @@ extension Ecom { return price + productsPrice + (ensStatus?.registrationFees ?? 0) } var isENSDomain: Bool { domain.extension == Constants.ensDomainTLD } - var isCOMDomain: Bool { domain.extension == Constants.comDomainTLD } + var isCOMDomain: Bool { Constants.dnsDomainTLDs.contains(domain.extension) } var isAbleToPurchase: Bool { !isENSDomain && !isCOMDomain } var hasUDVEnabled: Bool { 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..b16a67731 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 @@ -15,7 +15,7 @@ class EcomPurchaseInteractionService: BaseFirebaseInteractionService { } } var checkoutData: PurchaseDomainsCheckoutData - var isAutoRefreshCartSuspended = false + var isAutoRefreshCartSuspended = true var cachedPaymentDetails: Ecom.StripePaymentDetails? = nil var isApplePaySupported: Bool { StripeService.isApplePaySupported } @@ -133,11 +133,12 @@ extension EcomPurchaseInteractionService { } func loadUserCartCalculations() async throws -> Ecom.UserCartCalculationsResponse { - let queryComponents = ["applyPromoCredits" : String(checkoutData.isPromoCreditsOn), - "applyStoreCredits" : String(checkoutData.isStoreCreditsOn), + /// Always apply credits + let queryComponents = ["applyPromoCredits" : String(true), + "applyStoreCredits" : String(true), "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 +199,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 +208,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 +241,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 2ad0feb8e..a46a9a40d 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 @@ -5,13 +5,23 @@ // Created by Oleg Kuplin on 16.11.2023. // -import Foundation +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 { + .categoryFor(tld: 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 ab01ad895..1f7f83c37 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 @@ -94,10 +94,30 @@ final class FirebasePurchaseDomainsService: EcomPurchaseInteractionService { // MARK: - PurchaseDomainsServiceProtocol extension FirebasePurchaseDomainsService: PurchaseDomainsServiceProtocol { - func searchForDomains(key: String) async throws -> [DomainToPurchase] { - let searchResult = try await self.searchForFBDomains(key: key) - let domains = transformDomainProductItemsToDomainsToPurchase(searchResult.exact) - return domains + func searchForDomains(key: String, + tlds: Set) -> AsyncThrowingStream<[DomainToPurchase], Error> { + AsyncThrowingStream { continuation in + Task { + try? await withThrowingTaskGroup(of: Void.self) { group in + TLDCategory.allCases.forEach { tld in + let start = Date() + group.addTask { + do { + let searchResult = try await self.searchForEcommDomains(key: key, + tlds: tlds, + tldCategory: tld) + let domains = self.transformDomainProductItemsToDomainsToPurchase(searchResult.exact) + continuation.yield(domains) + } catch { } + } + } + + for try await _ in group { } + } + + continuation.finish() + } + } } func aiSearchForDomains(hint: String) async throws -> [DomainToPurchase] { @@ -106,9 +126,11 @@ extension FirebasePurchaseDomainsService: PurchaseDomainsServiceProtocol { return domains } - func getDomainsSuggestions(hint: String?) async throws -> [DomainToPurchaseSuggestion] { - let domainProducts = try await aiSearchForFBDomains(hint: hint ?? "Anything you think is trending now") - return domainProducts.map { DomainToPurchaseSuggestion(name: $0.domain.label) } + func getDomainsSuggestions(hint: String, tlds: Set) async throws -> [DomainToPurchase] { + let domainProducts = try await getDomainsSearchSuggestions(hint: hint, + tlds: tlds) + let domains = transformDomainProductItemsToDomainsToPurchase(domainProducts) + return domains } func addDomainsToCart(_ domains: [DomainToPurchase]) async throws { @@ -136,6 +158,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 @@ -145,7 +175,15 @@ extension FirebasePurchaseDomainsService: PurchaseDomainsServiceProtocol { func getSupportedWalletsToMint() async throws -> [PurchasedDomainsWalletDescription] { let userWallets = try await loadUserCryptoWallets() - return userWallets.map { PurchasedDomainsWalletDescription(address: $0.address, metadata: $0.jsonData()) } + return userWallets.map { PurchasedDomainsWalletDescription(ecomWallet: $0) } + } + + func getPreferredWalletToMint() async throws -> PurchasedDomainsWalletDescription { + guard firebaseAuthService.isAuthorised else { throw PurchaseDomainsError.unauthorized } + + let mintingWallet = try await getEcommMintingWallet() + let wallet = PurchasedDomainsWalletDescription(ecomWallet: mintingWallet) + return wallet } func refreshCart() async throws { @@ -157,21 +195,51 @@ extension FirebasePurchaseDomainsService: PurchaseDomainsServiceProtocol { let userWallet = try Ecom.UDUserAccountCryptWallet.objectFromDataThrowing(wallet.metadata ?? Data()) try await purchaseProductsInTheCart(with: .init(wallet: userWallet), totalAmountDue: udCart.calculations.totalAmountDue) - isAutoRefreshCartSuspended = false } } // MARK: - Private methods private extension FirebasePurchaseDomainsService { - func searchForFBDomains(key: String) async throws -> SearchDomainsResponse { - var searchResponse = try await makeSearchDomainsRequestWith(key: key) + func searchForEcommDomains(key: String, + tlds: Set, + tldCategory: TLDCategory) async throws -> SearchDomainsResponse { + var searchResponse = try await makeSearchDomainsRequestWith(key: key, + tlds: tlds, + tldCategory: tldCategory) searchResponse.exact = searchResponse.exact return searchResponse } + func getEcommMintingWallet() async throws -> Ecom.UDUserAccountCryptWallet { + struct Response: Codable { + let cryptoWallet: Ecom.UDUserAccountCryptWallet + } + + let urlString = URLSList.USER_MINTING_WALLET_URL + let request = try APIRequest(urlString: urlString, + method: .get) + let response: Response = try await makeFirebaseDecodableAPIDataRequest(request) + + return response.cryptoWallet + } + + func getDomainsSearchSuggestions(hint: String, tlds: Set) async throws -> [Ecom.DomainProductItem] { + let queryComponents: [String : String] = ["q" : hint, + "page" : "1", + "rowsPerPage" : "10"] + var urlString = URLSList.DOMAIN_SUGGESTIONS_URL.appendingURLQueryComponents(queryComponents) + for tld in tlds { + urlString += "&extension[]=\(tld)" + } + let request = try APIRequest(urlString: urlString, + method: .get) + let response: SuggestDomainsResponse = try await NetworkService().makeDecodableAPIRequest(request) + return response.suggestions + } + func aiSearchForFBDomains(hint: String) async throws -> [Ecom.DomainProductItem] { - let queryComponents = ["extension" : "All", - "phrase" : hint] + let queryComponents: [String : String] = ["extension" : "All", + "phrase" : hint] let urlString = URLSList.DOMAIN_AI_SUGGESTIONS_URL.appendingURLQueryComponents(queryComponents) let request = try APIRequest(urlString: urlString, method: .get) @@ -179,9 +247,14 @@ private extension FirebasePurchaseDomainsService { return response.suggestions } - func makeSearchDomainsRequestWith(key: String) async throws -> SearchDomainsResponse { - let queryComponents = ["q" : key] - let urlString = URLSList.DOMAIN_SEARCH_URL.appendingURLQueryComponents(queryComponents) + func makeSearchDomainsRequestWith(key: String, + tlds: Set, + tldCategory: TLDCategory) async throws -> SearchDomainsResponse { + let queryComponents: [String : String] = ["q" : key] + var urlString = URLSList.DOMAIN_UD_SEARCH_URL(tld: tldCategory).appendingURLQueryComponents(queryComponents) + for tld in tlds { + urlString += "&includeDomainEndings[]=\(tld)" + } let request = try APIRequest(urlString: urlString, method: .get) let response: SearchDomainsResponse = try await NetworkService().makeDecodableAPIRequest(request) @@ -199,7 +272,6 @@ private extension FirebasePurchaseDomainsService { func transformDomainProductItemsToDomainsToPurchase(_ productItems: [Ecom.DomainProductItem]) -> [DomainToPurchase] { productItems - .filter({ $0.availability }) .map { DomainToPurchase(domainProduct: $0) } } } @@ -223,8 +295,8 @@ private extension FirebasePurchaseDomainsService { storeCreditsAvailable: udCart.discountDetails.storeCredits, promoCreditsAvailable: udCart.discountDetails.promoCredits, appliedDiscountDetails: .init(storeCredits: udCart.calculations.storeCreditsUsed, - promoCredits: udCart.calculations.promoCreditsUsed, - others: otherDiscountsSum)) + promoCredits: udCart.calculations.promoCreditsUsed, + others: otherDiscountsSum)) } func loadCartParkingProducts(in cart: Ecom.UDUserCart) async { @@ -282,6 +354,7 @@ private extension FirebasePurchaseDomainsService { enum PurchaseDomainsError: String, LocalizedError { case udAccountHasUnpaidVault + case unauthorized } } @@ -291,6 +364,15 @@ private extension DomainToPurchase { self.name = domainProduct.domain.name self.price = domainProduct.price self.metadata = domainProduct.jsonData() + self.isTaken = !domainProduct.availability self.isAbleToPurchase = domainProduct.isAbleToPurchase } } + +// MARK: - Private methods +private extension PurchasedDomainsWalletDescription { + init(ecomWallet: Ecom.UDUserAccountCryptWallet) { + self.address = ecomWallet.address + self.metadata = ecomWallet.jsonData() + } +} 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..e798cd230 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 @@ -13,18 +13,18 @@ enum PurchaseDomainCartStatus { case failedToLoadCalculations(MainActorAsyncCallback) case ready(cart: PurchaseDomainsCart) - var promoCreditsAvailable: Int { + var promoCreditsApplied: Int { switch self { case .ready(let cart): - return cart.promoCreditsAvailable + return cart.appliedDiscountDetails.promoCredits default: return 0 } } - var storeCreditsAvailable: Int { + var storeCreditsApplied: Int { switch self { case .ready(let cart): - return cart.storeCreditsAvailable + return cart.appliedDiscountDetails.storeCredits default: return 0 } @@ -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..a40a779d0 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 @@ -11,12 +11,15 @@ protocol PurchaseDomainsServiceProtocol { var cartStatusPublisher: Published.Publisher { get } var isApplePaySupported: Bool { get } - func searchForDomains(key: String) async throws -> [DomainToPurchase] + func searchForDomains(key: String, + tlds: Set) -> AsyncThrowingStream<[DomainToPurchase], Error> func aiSearchForDomains(hint: String) async throws -> [DomainToPurchase] - func getDomainsSuggestions(hint: String?) async throws -> [DomainToPurchaseSuggestion] + func getDomainsSuggestions(hint: String, tlds: Set) async throws -> [DomainToPurchase] func authoriseWithWallet(_ wallet: UDWallet, toPurchaseDomains domains: [DomainToPurchase]) async throws + func setDomainsToPurchase(_ domains: [DomainToPurchase]) async throws func getSupportedWalletsToMint() async throws -> [PurchasedDomainsWalletDescription] + func getPreferredWalletToMint() async throws -> PurchasedDomainsWalletDescription func reset() async func refreshCart() async throws diff --git a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase mpc wallet/EcomPurchaseMPCWalletService.swift b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase mpc wallet/EcomPurchaseMPCWalletService.swift index 97fd5c457..2b1b7d4b7 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase mpc wallet/EcomPurchaseMPCWalletService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/Firebase/Purchase/Purchase mpc wallet/EcomPurchaseMPCWalletService.swift @@ -27,7 +27,9 @@ final class EcomPurchaseMPCWalletService: EcomPurchaseInteractionService { preferencesService.$checkoutData.publisher .sink { [weak self] checkoutData in self?.checkoutData = checkoutData - self?.refreshUserCartAsync() + if self?.ongoingPurchaseSession != nil { + self?.refreshUserCartAsync() + } } .store(in: &cancellables) } @@ -169,7 +171,6 @@ extension EcomPurchaseMPCWalletService: EcomPurchaseMPCWalletServiceProtocol { totalAmountDue: udCart.calculations.totalAmountDue) ongoingPurchaseSession?.orderId = cachedPaymentDetails?.orderId try await waitForMPCWalletIsCreated() - isAutoRefreshCartSuspended = false } func validateCredentialsForTakeover(credentials: MPCTakeoverCredentials) async throws -> Bool { diff --git a/unstoppable-ios-app/domains-manager-ios/Services/GIFAnimationsService.swift b/unstoppable-ios-app/domains-manager-ios/Services/GIFAnimationsService.swift index 617232416..6a1b1a9e2 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/GIFAnimationsService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/GIFAnimationsService.swift @@ -133,7 +133,6 @@ private extension GIFAnimationsService { maskingType: GIFMaskingType?) throws -> UIImage { return try serialQueue.sync { do { - print("LOGO: - Will create gif") let start = Date() let count = CGImageSourceGetCount(source) let (images, delays) = try extractImagesWithDelays(from: source, maxImageSize: maxImageSize, maskingType: maskingType) @@ -171,11 +170,8 @@ private extension GIFAnimationsService { throw GIFPreparationError.failedToCreateAnimatedImage } - print("LOGO: - Did create gif") - return animation } catch { - print("LOGO: - Did fail to create gif") throw error } } diff --git a/unstoppable-ios-app/domains-manager-ios/Services/IPVerificationService/IPVerificationService.swift b/unstoppable-ios-app/domains-manager-ios/Services/IPVerificationService/IPVerificationService.swift new file mode 100644 index 000000000..1cd9607f3 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Services/IPVerificationService/IPVerificationService.swift @@ -0,0 +1,65 @@ +// +// IPVerificationService.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 13.08.2024. +// + +import Foundation + +final class IPVerificationService { } + +// MARK: - Open methods +extension IPVerificationService: IPVerificationServiceProtocol { + func isUserInTheUS() async throws -> Bool { + let countryName = try await getCountryNameForCurrentIP() + let usCountryName = "United States" + + return countryName == usCountryName + } +} + +// MARK: - Private methods +private extension IPVerificationService { + func getCountryNameForCurrentIP() async throws -> String { + let ip = try await getCurrentIP() + let countryName = try await getCountryNameFor(ip: ip) + return countryName + } + + func getCurrentIP() async throws -> String { + struct Response: Codable { + let ip: String + } + + let url = "https://api.ipify.org/?format=json" + let data = try await getDataFrom(url: url) + let response: Response = try Response.objectFromDataThrowing(data) + + return response.ip + } + + func getCountryNameFor(ip: String) async throws -> String { + let url = "https://ipapi.co/\(ip)/country_name/" + let data = try await getDataFrom(url: url) + + guard let countryName = String(data: data, encoding: .utf8) else { throw IPVerificationServiceError.failedToParseCountryName } + + return countryName + } + + func getDataFrom(url: String) async throws -> Data { + let url = URL(string: url)! + let request = URLRequest(url: url) + let (data, _) = try await URLSession.shared.data(for: request) + return data + } + + enum IPVerificationServiceError: String, LocalizedError { + case failedToParseCountryName + + public var errorDescription: String? { + return rawValue + } + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/Services/IPVerificationService/IPVerificationServiceProtocol.swift b/unstoppable-ios-app/domains-manager-ios/Services/IPVerificationService/IPVerificationServiceProtocol.swift new file mode 100644 index 000000000..4ddea016a --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Services/IPVerificationService/IPVerificationServiceProtocol.swift @@ -0,0 +1,12 @@ +// +// IPVerificationServiceProtocol.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 13.08.2024. +// + +import Foundation + +protocol IPVerificationServiceProtocol { + func isUserInTheUS() async throws -> Bool +} 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/Services/UDRouter+Common.swift b/unstoppable-ios-app/domains-manager-ios/Services/UDRouter+Common.swift deleted file mode 100644 index 203ab513a..000000000 --- a/unstoppable-ios-app/domains-manager-ios/Services/UDRouter+Common.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// UDRouter+Common.swift -// domains-manager-ios -// -// Created by Oleg Kuplin on 05.12.2023. -// - -import UIKit - -extension UDRouter { - func showSearchDomainToPurchase(in viewController: UIViewController, - domainsPurchasedCallback: @escaping PurchaseDomainsNavigationController.DomainsPurchasedCallback) { - let purchaseDomainsNavigationController = PurchaseDomainsNavigationController() - purchaseDomainsNavigationController.domainsPurchasedCallback = domainsPurchasedCallback - viewController.cNavigationController?.pushViewController(purchaseDomainsNavigationController, - animated: true) - } -} 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/addToCartIcon.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/addToCartIcon.imageset/Contents.json new file mode 100644 index 000000000..3cc37b4b4 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/addToCartIcon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "addToCartIcon.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/addToCartIcon.imageset/addToCartIcon.svg b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/addToCartIcon.imageset/addToCartIcon.svg new file mode 100644 index 000000000..eca2f890e --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/addToCartIcon.imageset/addToCartIcon.svg @@ -0,0 +1,6 @@ + + + + + + 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/cancelIcon.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/cancelIcon.imageset/Contents.json index 453167ba7..f965800cf 100644 --- a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/cancelIcon.imageset/Contents.json +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/cancelIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "cancelIcon.pdf", + "filename" : "cancelIcon.svg", "idiom" : "universal" } ], diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/cancelIcon.imageset/cancelIcon.pdf b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/cancelIcon.imageset/cancelIcon.pdf deleted file mode 100644 index 3119a5328..000000000 Binary files a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/cancelIcon.imageset/cancelIcon.pdf and /dev/null differ diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/cancelIcon.imageset/cancelIcon.svg b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/cancelIcon.imageset/cancelIcon.svg new file mode 100644 index 000000000..bda60bae3 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/cancelIcon.imageset/cancelIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/cartFillIcon.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/cartFillIcon.imageset/Contents.json new file mode 100644 index 000000000..a39c179a3 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/cartFillIcon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "cartFillIcon.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/cartFillIcon.imageset/cartFillIcon.svg b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/cartFillIcon.imageset/cartFillIcon.svg new file mode 100644 index 000000000..ed3e67d96 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/cartFillIcon.imageset/cartFillIcon.svg @@ -0,0 +1,5 @@ + + + + + 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/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/dnsTLDLogo.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/dnsTLDLogo.imageset/Contents.json new file mode 100644 index 000000000..bc374ea6e --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/dnsTLDLogo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "dnsTLDLogo.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/TLD logo/dnsTLDLogo.imageset/dnsTLDLogo.svg b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/dnsTLDLogo.imageset/dnsTLDLogo.svg new file mode 100644 index 000000000..1d110dade --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/dnsTLDLogo.imageset/dnsTLDLogo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/ensTLDLogo.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/ensTLDLogo.imageset/Contents.json new file mode 100644 index 000000000..bbb7a6111 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/ensTLDLogo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ensTLDLogo.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/TLD logo/ensTLDLogo.imageset/ensTLDLogo.svg b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/ensTLDLogo.imageset/ensTLDLogo.svg new file mode 100644 index 000000000..8f755fb73 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/ensTLDLogo.imageset/ensTLDLogo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/unsTLDLogo.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/unsTLDLogo.imageset/Contents.json new file mode 100644 index 000000000..2b46803a5 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/unsTLDLogo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "udTLDLogo.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/TLD logo/unsTLDLogo.imageset/udTLDLogo.svg b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/unsTLDLogo.imageset/udTLDLogo.svg new file mode 100644 index 000000000..52f88bbd1 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/TLD logo/unsTLDLogo.imageset/udTLDLogo.svg @@ -0,0 +1,3 @@ + + + diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Constants.swift b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Constants.swift index 55d5d4b63..579bb6f98 100644 --- a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Constants.swift +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Constants.swift @@ -51,7 +51,7 @@ struct Constants { static let shouldHideBlockedUsersLocally = true static let isCommunitiesEnabled = true static let ensDomainTLD: String = "eth" - static let comDomainTLD: String = "com" + static var dnsDomainTLDs: Set = ["com", "ca", "pw"] static let lensDomainTLD: String = "lens" static let coinbaseDomainTLD: String = "id" static let swiftUIPreviewDevices = ["iPhone 14 Pro", "iPhone 14 Pro Max", "iPhone SE (1st generation)", "iPhone SE (3rd generation)", "iPhone 13 mini"] @@ -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 662b787ca..125ee63ab 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 @@ -925,7 +925,7 @@ "GET_DOMAIN_CARD_SUBTITLE" = "Own your identity in the digital world.\nGet started with a Web3 domain."; "FIND_A_NEW_DOMAIN" = "Find a new domain"; "FIND_YOUR_DOMAIN" = "Find your domain"; -"SEARCH_FOR_A_NEW_DOMAIN" = "Search for a new\ndomain"; +"SEARCH_FOR_A_DOMAIN" = "Search for a domain"; "TRENDING" = "Trending"; "NO_AVAILABLE_DOMAINS" = "No available domains"; "TRY_ENTER_DIFF_NAME" = "Try entering a different name."; @@ -934,6 +934,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"; @@ -977,6 +978,33 @@ "PURCHASE_SEARCH_CANT_BUY_PULL_UP_TITLE" = "Coming soon"; "PURCHASE_SEARCH_CANT_BUY_PULL_UP_SUBTITLE" = ".%@ domains are currently only for sale from our website."; "PAY_WITH_CREDITS" = "Pay with credits"; +"BUY_DOMAINS_SEARCH_TITLE" = "Buy Domains"; +"BUY_DOMAINS_CART_EMPTY_TITLE" = "Your cart is empty"; +"BUY_DOMAINS_CART_EMPTY_SUBTITLE" = "But it's easy to fix!"; +"BUY_DOMAINS_CART_TITLE" = "Your cart (%i)"; +"SEARCH_DOMAINS" = "Search domains"; +"CLEAR" = "Clear"; +"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."; +"ENDINGS" = "Endings"; +"SUGGESTIONS" = "Suggestions"; +"DOMAINS_PURCHASED_SUMMARY_MESSAGE" = "Order total: %@\n\nMinting started, this can take up to 5 minutes.\nWill be minted to: %@"; // Home "HOME_WALLET_TOKENS_COME_TITLE" = "No more tokens here yet"; diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Localization/en.lproj/Localizable.stringsdict b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Localization/en.lproj/Localizable.stringsdict index eb0bbf73e..359d9104b 100644 --- a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Localization/en.lproj/Localizable.stringsdict +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Localization/en.lproj/Localizable.stringsdict @@ -554,5 +554,29 @@ followings + SDICT:MINTING_N_DOMAINS + + NSStringLocalizedFormatKey + Minting %d %#@domain@... + domain + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + zero + domains + one + domain + two + domains + few + domains + many + domains + other + domains + + diff --git a/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/Buttons/UDButtonView/UDButtonView.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/Buttons/UDButtonView/UDButtonView.swift index b63174a6e..87e597ed6 100644 --- a/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/Buttons/UDButtonView/UDButtonView.swift +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/Buttons/UDButtonView/UDButtonView.swift @@ -156,12 +156,12 @@ fileprivate extension UDButtonView { switch style { case .large: content - .sideInsets(24) + .padding(.horizontal, 24) .frame(maxWidth: .infinity) .frame(height: style.height) case .medium, .small, .verySmall: content - .sideInsets(withTitle ? 12 : 6) + .padding(.horizontal, withTitle ? 12 : 6) .frame(height: style.height) } } 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/DashedProgressView.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/DashedProgressView.swift new file mode 100644 index 000000000..b6ceda5f5 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/DashedProgressView.swift @@ -0,0 +1,79 @@ +// +// DashedProgressView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 05.08.2024. +// + +import SwiftUI + +struct DashedProgressView: View { + let configuration: DConfiguration + let progress: Double + + var body: some View { + GeometryReader { geometry in + ZStack { + dashesWithColor(color: configuration.notFilledColor, + width: geometry.size.width) + dashesWithColor(color: configuration.filledColor, + width: geometry.size.width) + .mask( + dashesMaskView(width: geometry.size.width) + ) + } + } + .frame(width: 160.0, height: configuration.dashHeight) + .animation(.default, value: progress) + } +} + +// MARK: - Private methods +private extension DashedProgressView { + @ViewBuilder + func dashesWithColor(color: UIColor, + width: CGFloat) -> some View { + HStack(spacing: configuration.dashesSpacing) { + ForEach(0.. some View { + RoundedRectangle(cornerRadius: configuration.dashHeight / 2) + .frame(width: width * progress) + .offset(x: -(width - width * progress) / 2) + } + + func dashWidth(in totalWidth: CGFloat) -> CGFloat { + let totalSpacing = CGFloat(configuration.numberOfDashes - 1) * configuration.dashesSpacing + return (totalWidth - totalSpacing) / CGFloat(configuration.numberOfDashes) + } +} + +extension DashedProgressView { + struct DConfiguration { + var notFilledColor = UIColor.foregroundSubtle + var filledColor = UIColor.foregroundAccent + var numberOfDashes = 2 + var dashHeight: CGFloat = 4 + var dashesSpacing: CGFloat = 8 + + static func white(numberOfDashes: Int) -> DConfiguration { + DConfiguration(notFilledColor: .foregroundOnEmphasis.withAlphaComponent(0.32), + filledColor: .foregroundOnEmphasis, + numberOfDashes: numberOfDashes) + } + } +} + +#Preview { + DashedProgressView( + configuration: .init(), + progress: 0.2 + ) +} 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/CommonViews/UDCheckBoxView.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/UDCheckBoxView.swift index 01773bbc5..25e52945d 100644 --- a/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/UDCheckBoxView.swift +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/UDCheckBoxView.swift @@ -30,6 +30,7 @@ struct UDCheckBoxView: View { .resizable() .foregroundStyle(Color.white) .padding(2) + .background(onLinearBackground()) .background(isEnabled ? Color.backgroundAccentEmphasis : Color.backgroundAccent) .clipShape(RoundedRectangle(cornerRadius: 6)) } else { @@ -43,6 +44,23 @@ struct UDCheckBoxView: View { } } +// MARK: - Private methods +private extension UDCheckBoxView { + @ViewBuilder + func onLinearBackground() -> some View { + if isEnabled { + LinearGradient( + stops: [ + Gradient.Stop(color: .white.opacity(0.32), location: 0.00), + Gradient.Stop(color: .white.opacity(0), location: 0.57), + ], + startPoint: UnitPoint(x: 0.49, y: 0), + endPoint: UnitPoint(x: 0.49, y: 1) + ) + } + } +} + #Preview { UDCheckBoxView(isOn: .constant(true)) } diff --git a/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/UDTextFieldView.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/UDTextFieldView.swift index 3ffbbc1ba..402aba4c9 100644 --- a/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/UDTextFieldView.swift +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/UDTextFieldView.swift @@ -39,7 +39,7 @@ struct UDTextFieldView: View, ViewAnalyticsLogger { ZStack { getTextFieldBackground() getTextFieldContent() - .sideInsets(16) + .padding(.horizontal, 16) } .frame(height: height) } 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 5b6c7b36e..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") @@ -124,6 +124,15 @@ extension Image { static let purchaseMPCIcon = Image("purchaseMPCIcon") static let mpcWalletRecoveryIllustration = Image("mpcWalletRecoveryIllustration") static let gas = Image("gas") + static let cartIcon = Image("cartIcon") + static let addToCartIcon = Image("addToCartIcon") + static let cartFillIcon = Image("cartFillIcon") + 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/NavigationPopGestureDisabler.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/NavigationPopGestureDisabler.swift new file mode 100644 index 000000000..780a64b40 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/NavigationPopGestureDisabler.swift @@ -0,0 +1,39 @@ +// +// NavigationPopGestureDisabler.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 05.08.2024. +// + +import SwiftUI + +extension UIView { + var parentViewController: UIViewController? { + sequence(first: self) { + $0.next + }.first { $0 is UIViewController } as? UIViewController + } +} + +private struct NavigationPopGestureDisabler: UIViewRepresentable { + let disabled: Bool + + func makeUIView(context: Context) -> some UIView { UIView() } + + func updateUIView(_ uiView: UIViewType, context: Context) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { + uiView + .parentViewController? + .navigationController? + .interactivePopGestureRecognizer?.isEnabled = !disabled + } + } +} +public extension View { + @ViewBuilder + func navigationPopGestureDisabled(_ disabled: Bool) -> some View { + background { + NavigationPopGestureDisabler(disabled: disabled) + } + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/NavigationTrackerViewModifier.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/NavigationTrackerViewModifier.swift index 8e8818374..5e66a248c 100644 --- a/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/NavigationTrackerViewModifier.swift +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/NavigationTrackerViewModifier.swift @@ -10,49 +10,106 @@ import SwiftUI struct NavigationTrackerViewModifier: ViewModifier { var onDidNotFinishNavigationBack: EmptyCallback? = nil - @StateObject private var navigationTracker = UINavigationViewControllerTracker() + var onDidStartBackGesture: EmptyCallback? = nil + var onDidBackGestureProgress: ((Double)->())? = nil func body(content: Content) -> some View { content .onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { if let topVC = appContext.coreAppCoordinator.topVC, - let nav = topVC.children.first as? UINavigationController { - navigationTracker.handler = self - navigationTracker.trackNavigationController(nav) + let nav = getNavigationController(from: topVC) { + UINavigationViewControllerTracker.shared.trackNavigationController(nav, + handler: self) } } } } + private func getNavigationController(from topVC: UIViewController) -> UINavigationController? { + if let nav = topVC.children.first as? UINavigationController { + return nav + } else if let tabBarVC = topVC.children.first as? UITabBarController, + let selectedVC = tabBarVC.selectedViewController { + return getNavigationController(from: selectedVC) + } + return nil + } + } +// MARK: - UINavigationControllerTrackerHandler extension NavigationTrackerViewModifier: UINavigationControllerTrackerHandler { func didNotFinishNavigationBack() { onDidNotFinishNavigationBack?() } + + func didStartBackGesture() { + onDidStartBackGesture?() + } + + func didBackGestureProgress(_ progress: Double) { + onDidBackGestureProgress?(progress) + } } extension View { - func trackNavigationControllerEvents(onDidNotFinishNavigationBack: EmptyCallback? = nil) -> some View { - modifier(NavigationTrackerViewModifier(onDidNotFinishNavigationBack: onDidNotFinishNavigationBack)) + func trackNavigationControllerEvents(onDidNotFinishNavigationBack: EmptyCallback? = nil, + onDidStartBackGesture: EmptyCallback? = nil, + onDidBackGestureProgress: ((Double)->())? = nil) -> some View { + modifier(NavigationTrackerViewModifier(onDidNotFinishNavigationBack: onDidNotFinishNavigationBack, + onDidStartBackGesture: onDidStartBackGesture, + onDidBackGestureProgress: onDidBackGestureProgress)) } } protocol UINavigationControllerTrackerHandler { func didNotFinishNavigationBack() + func didStartBackGesture() + func didBackGestureProgress(_ progress: Double) } -final class UINavigationViewControllerTracker: NSObject, ObservableObject, UINavigationControllerDelegate { - var handler: UINavigationControllerTrackerHandler? +final class UINavigationViewControllerTracker: NSObject { + + static let shared = UINavigationViewControllerTracker() - func trackNavigationController(_ navigationController: UINavigationController?) { - guard navigationController?.delegate !== self else { return } + private var navsToCallbacks: [NavigationControllerHolder : [UINavigationControllerTrackerHandler]] = [:] + + func trackNavigationController(_ navigationController: UINavigationController?, + handler: UINavigationControllerTrackerHandler) { + let holder = NavigationControllerHolder(nav: navigationController) + navsToCallbacks[holder, default: []].append(handler) + navigationController?.interactivePopGestureRecognizer?.addTarget(self, action: #selector(handleSwipeGesture)) + } + + @objc private func handleSwipeGesture(_ gesture: UIPanGestureRecognizer) { + guard let gestureView = gesture.view else { return } + + let translation: CGPoint = gesture.translation(in: gestureView) + let percentProgress: CGFloat = abs(translation.x / gestureView.bounds.size.width); - navigationController?.transitionCoordinator?.notifyWhenInteractionChanges({ [weak self] context in - if context.completionVelocity < 0 { // will restore current view controller - self?.handler?.didNotFinishNavigationBack() + for (holder, handlers) in navsToCallbacks { + guard holder.nav?.interactivePopGestureRecognizer == gesture else { continue } + + switch gesture.state { + case .began: + handlers.forEach { $0.didStartBackGesture() } + holder.nav?.transitionCoordinator?.notifyWhenInteractionChanges({ context in + + if context.completionVelocity < 0 { // will restore current view controller + handlers.forEach { $0.didNotFinishNavigationBack() } + } + }) + return + case .changed: + handlers.forEach { $0.didBackGestureProgress(percentProgress) } + default: + return } - }) + } + } + + private struct NavigationControllerHolder: Hashable { + weak var nav: UINavigationController? } } diff --git a/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/SideInsets.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/SideInsets.swift deleted file mode 100644 index fde3ffde1..000000000 --- a/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/SideInsets.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// SideInsets.swift -// UBTSharing -// -// Created by Oleg Kuplin on 21.08.2023. -// - -import SwiftUI - -struct SideInsets: ViewModifier { - let padding: CGFloat - - func body(content: Content) -> some View { - content - .padding(EdgeInsets(top: 0, leading: padding, bottom: 0, trailing: padding)) - } -} - -extension View { - func sideInsets(_ padding: CGFloat) -> some View { - modifier(SideInsets(padding: padding)) - } -} - diff --git a/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/UDConfettiViewModifier.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/UDConfettiViewModifier.swift new file mode 100644 index 000000000..463073e8c --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/ViewModifiers/UDConfettiViewModifier.swift @@ -0,0 +1,415 @@ +// +// UDConfettiViewModifier.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 12.08.2024. +// + +import SwiftUI + +struct UDConfettiViewModifier: ViewModifier { + @Binding var counter: Int + + func body(content: Content) -> some View { + content + .confettiCannon(counter: $counter, + num: 3, + colors: [.blue], + rainHeight: 188, + openingAngle: Angle(degrees: 180), + closingAngle: Angle(degrees: 360), + repetitions: 70, + repetitionInterval: 0.1, + yOffset: -(UIScreen.main.bounds.height / 2) - 188) + .onAppear { + counter += 1 + } + } +} + +public extension View { + + func udConfetti(counter: Binding) -> some View { + modifier(UDConfettiViewModifier(counter: counter)) + } + + @ViewBuilder + func confettiCannon(counter: Binding, + num: Int = 20, + confettis: [ConfettiType] = ConfettiType.allCases, + colors: [Color] = [.blue, .red, .green, .yellow, .pink, .purple, .orange], + confettiSize: CGFloat = 10.0, + rainHeight: CGFloat = 600.0, + fadesOut: Bool = true, + opacity: Double = 1.0, + openingAngle: Angle = .degrees(60), + closingAngle: Angle = .degrees(120), + radius: CGFloat = 300, + repetitions: Int = 0, + repetitionInterval: Double = 1.0, + yOffset: CGFloat = 0.0) -> some View { + ZStack { + self + ConfettiCannon( + counter: counter, + num: num, + confettis: confettis, + colors: colors, + confettiSize: confettiSize, + rainHeight: rainHeight, + fadesOut: fadesOut, + opacity: opacity, + openingAngle: openingAngle, + closingAngle: closingAngle, + radius: radius, + repetitions: repetitions, + repetitionInterval: repetitionInterval + ) + .offset(y: yOffset) + } + } +} + +public struct ConfettiCannon: View { + @Binding var counter:Int + @StateObject private var confettiConfig:ConfettiConfig + + @State var animate:[Bool] = [] + @State var finishedAnimationCounter = 0 + @State var firstAppear = false + @State var error = "" + + public init(counter:Binding, + num:Int = 20, + confettis:[ConfettiType] = ConfettiType.allCases, + colors:[Color] = [.blue, .red, .green, .yellow, .pink, .purple, .orange], + confettiSize:CGFloat = 10.0, + rainHeight: CGFloat = 600.0, + fadesOut:Bool = true, + opacity:Double = 1.0, + openingAngle:Angle = .degrees(60), + closingAngle:Angle = .degrees(120), + radius:CGFloat = 300, + repetitions:Int = 0, + repetitionInterval:Double = 1.0) { + self._counter = counter + var shapes = [AnyView]() + + for confetti in confettis{ + for color in colors{ + switch confetti { + case .shape(_): + shapes.append(AnyView(confetti.view.foregroundColor(color).frame(width: confettiSize, height: confettiSize, alignment: .center))) + case .image(_): + shapes.append(AnyView(confetti.view.frame(maxWidth:confettiSize, maxHeight: confettiSize))) + default: + shapes.append(AnyView(confetti.view.foregroundColor(color).font(.system(size: confettiSize)))) + } + } + } + + _confettiConfig = StateObject(wrappedValue: ConfettiConfig( + num: num, + shapes: shapes, + colors: colors, + confettiSize: confettiSize, + rainHeight: rainHeight, + fadesOut: fadesOut, + opacity: opacity, + openingAngle: openingAngle, + closingAngle: closingAngle, + radius: radius, + repetitions: repetitions, + repetitionInterval: repetitionInterval + )) + } + + public var body: some View { + ZStack{ + ForEach(finishedAnimationCounter.. 0 && value < animate.count){ + animate[value-1].toggle() + } + } + } + } + } + } +} + + +struct ConfettiContainer: View { + @Binding var finishedAnimationCounter:Int + @StateObject var confettiConfig:ConfettiConfig + @State var firstAppear = true + + var body: some View{ + ZStack{ + ForEach(0...confettiConfig.num-1, id:\.self){_ in + ConfettiView(confettiConfig: confettiConfig) + } + } + .onAppear(){ + if firstAppear{ + DispatchQueue.main.asyncAfter(deadline: .now() + confettiConfig.animationDuration) { + self.finishedAnimationCounter += 1 + } + firstAppear = false + } + } + } +} + +struct ConfettiView: View{ + @State var location:CGPoint = CGPoint(x: 0, y: 0) + @State var opacity:Double = 0.0 + @StateObject var confettiConfig:ConfettiConfig + + func getShape() -> AnyView { + return confettiConfig.shapes.randomElement()! + } + + func getColor() -> Color { + return confettiConfig.colors.randomElement()! + } + + func getSpinDirection() -> CGFloat { + let spinDirections:[CGFloat] = [-1.0, 1.0] + return spinDirections.randomElement()! + } + + func getRandomExplosionTimeVariation() -> CGFloat { + CGFloat((0...999).randomElement()!) / 2100 + } + + func getAnimationDuration() -> CGFloat { + return 0.2 + confettiConfig.explosionAnimationDuration + getRandomExplosionTimeVariation() + } + + func getAnimation() -> Animation { + return Animation.timingCurve(0.1, 0.8, 0, 1, duration: getAnimationDuration()) + } + + func getDistance() -> CGFloat { + return pow(CGFloat.random(in: 0.01...1), 2.0/7.0) * confettiConfig.radius + } + + func getDelayBeforeRainAnimation() -> TimeInterval { + confettiConfig.explosionAnimationDuration * 0.1 + } + + var body: some View{ + ConfettiAnimationView(shape:getShape(), color:getColor(), spinDirX: getSpinDirection(), spinDirZ: getSpinDirection()) + .offset(x: location.x, y: location.y) + .opacity(opacity) + .onAppear(){ + withAnimation(getAnimation()) { + opacity = confettiConfig.opacity + + let randomAngle:CGFloat + if confettiConfig.openingAngle.degrees <= confettiConfig.closingAngle.degrees{ + randomAngle = CGFloat.random(in: CGFloat(confettiConfig.openingAngle.degrees)...CGFloat(confettiConfig.closingAngle.degrees)) + }else{ + randomAngle = CGFloat.random(in: CGFloat(confettiConfig.openingAngle.degrees)...CGFloat(confettiConfig.closingAngle.degrees + 360)).truncatingRemainder(dividingBy: 360) + } + + let distance = getDistance() + + location.x = distance * cos(deg2rad(randomAngle)) + location.y = -distance * sin(deg2rad(randomAngle)) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + getDelayBeforeRainAnimation()) { + withAnimation(Animation.timingCurve(0.12, 0, 0.39, 0, duration: confettiConfig.rainAnimationDuration)) { + location.y += confettiConfig.rainHeight + opacity = confettiConfig.fadesOut ? 0 : confettiConfig.opacity + } + } + } + } + + func deg2rad(_ number: CGFloat) -> CGFloat { + return number * CGFloat.pi / 180 + } + +} + +struct ConfettiAnimationView: View { + @State var shape: AnyView + @State var color: Color + @State var spinDirX: CGFloat + @State var spinDirZ: CGFloat + @State var firstAppear = true + + + @State var move = false + @State var xSpeed:Double = Double.random(in: 0.501...2.201) + + @State var zSpeed = Double.random(in: 0.501...2.201) + @State var anchor = CGFloat.random(in: 0...1).rounded() + + var body: some View { + shape + .foregroundColor(color) + .rotation3DEffect(.degrees(move ? 360:0), axis: (x: spinDirX, y: 0, z: 0)) + .animation(Animation.linear(duration: xSpeed).repeatCount(10, autoreverses: false), value: move) + .rotation3DEffect(.degrees(move ? 360:0), axis: (x: 0, y: 0, z: spinDirZ), anchor: UnitPoint(x: anchor, y: anchor)) + .animation(Animation.linear(duration: zSpeed).repeatForever(autoreverses: false), value: move) + .onAppear() { + if firstAppear { + move = true + firstAppear = true + } + } + } +} + +public enum ConfettiType:CaseIterable, Hashable { + + public enum Shape { + case circle + case triangle + case square + case slimRectangle + case roundedCross + } + + case shape(Shape) + case text(String) + case sfSymbol(symbolName: String) + case image(String) + + public var view:AnyView{ + switch self { + case .shape(.square): + return AnyView(Rectangle()) + case .shape(.triangle): + return AnyView(Triangle()) + case .shape(.slimRectangle): + return AnyView(SlimRectangle()) + case .shape(.roundedCross): + return AnyView(RoundedCross()) + case let .text(text): + return AnyView(Text(text)) + case .sfSymbol(let symbolName): + return AnyView(Image(systemName: symbolName)) + case .image(let image): + return AnyView(Image(image).resizable()) + default: + return AnyView(Circle()) + } + } + + public static var allCases: [ConfettiType] { + return [.shape(.circle), .shape(.triangle), .shape(.square), .shape(.slimRectangle), .shape(.roundedCross)] + } +} + + +public struct RoundedCross: Shape { + public func path(in rect: CGRect) -> Path { + var path = Path() + + path.move(to: CGPoint(x: rect.minX, y: rect.maxY/3)) + path.addQuadCurve(to: CGPoint(x: rect.maxX/3, y: rect.minY), control: CGPoint(x: rect.maxX/3, y: rect.maxY/3)) + path.addLine(to: CGPoint(x: 2*rect.maxX/3, y: rect.minY)) + + path.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.maxY/3), control: CGPoint(x: 2*rect.maxX/3, y: rect.maxY/3)) + path.addLine(to: CGPoint(x: rect.maxX, y: 2*rect.maxY/3)) + + path.addQuadCurve(to: CGPoint(x: 2*rect.maxX/3, y: rect.maxY), control: CGPoint(x: 2*rect.maxX/3, y: 2*rect.maxY/3)) + path.addLine(to: CGPoint(x: rect.maxX/3, y: rect.maxY)) + + path.addQuadCurve(to: CGPoint(x: 2*rect.minX/3, y: 2*rect.maxY/3), control: CGPoint(x: rect.maxX/3, y: 2*rect.maxY/3)) + + return path + } +} + +public struct SlimRectangle: Shape { + public func path(in rect: CGRect) -> Path { + var path = Path() + + path.move(to: CGPoint(x: rect.minX, y: 4*rect.maxY/5)) + path.addLine(to: CGPoint(x: rect.maxX, y: 4*rect.maxY/5)) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) + path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) + + return path + } +} + +public struct Triangle: Shape { + public func path(in rect: CGRect) -> Path { + var path = Path() + + path.move(to: CGPoint(x: rect.midX, y: rect.minY)) + path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) + path.addLine(to: CGPoint(x: rect.midX, y: rect.minY)) + + return path + } +} + + +class ConfettiConfig: ObservableObject { + internal init(num: Int, shapes: [AnyView], colors: [Color], confettiSize: CGFloat, rainHeight: CGFloat, fadesOut: Bool, opacity: Double, openingAngle:Angle, closingAngle:Angle, radius:CGFloat, repetitions:Int, repetitionInterval:Double) { + self.num = num + self.shapes = shapes + self.colors = colors + self.confettiSize = confettiSize + self.rainHeight = rainHeight + self.fadesOut = fadesOut + self.opacity = opacity + self.openingAngle = openingAngle + self.closingAngle = closingAngle + self.radius = radius + self.repetitions = repetitions + self.repetitionInterval = repetitionInterval + self.explosionAnimationDuration = Double(radius / 1300) + self.rainAnimationDuration = Double((rainHeight + radius) / 200) + } + + @Published var num:Int + @Published var shapes:[AnyView] + @Published var colors:[Color] + @Published var confettiSize:CGFloat + @Published var rainHeight:CGFloat + @Published var fadesOut:Bool + @Published var opacity:Double + @Published var openingAngle:Angle + @Published var closingAngle:Angle + @Published var radius:CGFloat + @Published var repetitions:Int + @Published var repetitionInterval:Double + @Published var explosionAnimationDuration:Double + @Published var rainAnimationDuration:Double + + + var animationDuration:Double{ + return explosionAnimationDuration + rainAnimationDuration + } + + var openingAngleRad:CGFloat{ + return CGFloat(openingAngle.degrees) * 180 / .pi + } + + var closingAngleRad:CGFloat{ + return CGFloat(closingAngle.degrees) * 180 / .pi + } +} 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 b5413815a..58ad84710 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 @@ -331,10 +331,10 @@ extension ViewPullUpDefaultConfiguration { dismissCallback: nil) } - static func showFinishSetupProfilePullUp(pendingProfile: DomainProfilePendingChanges, - signCallback: @escaping MainActorAsyncCallback) -> ViewPullUpDefaultConfiguration { + static func showFinishSetupProfilePullUp(pendingProfile: DomainProfilePendingChanges, + signCallback: @escaping MainActorAsyncCallback) -> ViewPullUpDefaultConfiguration { let domainName = pendingProfile.domainName - + return .init(icon: .init(icon: .infoIcon, size: .large), title: .highlightedText(.init(text: String.Constants.finishSetupProfilePullUpTitle.localized(String(domainName.prefix(40))), @@ -363,9 +363,9 @@ extension ViewPullUpDefaultConfiguration { analyticsName: .tryAgain, action: { completion(.success(Void())) })), cancelButton: .primaryGhost(content: .init(title: String.Constants.cancelSetup.localized(), - icon: nil, - analyticsName: .cancel, - action: { completion(.failure(PullUpError.dismissed)) })), + icon: nil, + analyticsName: .cancel, + action: { completion(.failure(PullUpError.dismissed)) })), analyticName: .failedToFinishProfileForPurchasedDomains, dismissCallback: nil) } @@ -388,7 +388,7 @@ extension ViewPullUpDefaultConfiguration { static func legalSelectionPullUp(selectionCallback: @escaping (LegalType)->()) -> ViewPullUpDefaultConfiguration { var selectedItem: LegalType? - + return .init(title: .text(String.Constants.settingsLegal.localized()), items: LegalType.allCases, itemSelectedCallback: { item in @@ -444,7 +444,7 @@ extension ViewPullUpDefaultConfiguration { analyticName: .noRecordsSetToSendCrypto, dismissCallback: nil) } - + static func showSendCryptoForTheFirstTimeConfirmationPullUp(confirmCallback: @escaping MainActorAsyncCallback) -> ViewPullUpDefaultConfiguration { var icon: UIImage? if User.instance.getSettings().touchIdActivated, @@ -492,8 +492,8 @@ extension ViewPullUpDefaultConfiguration { title: .text(String.Constants.removeMPCWalletPullUpTitle.localizedMPCProduct()), subtitle: .label(.text(String.Constants.removeMPCWalletPullUpSubtitle.localized())), actionButton: .primaryDanger(content: .init(title: String.Constants.removeWallet.localized(), - analyticsName: .walletRemove, - action: { + analyticsName: .walletRemove, + action: { removeCallback() })), cancelButton: .secondary(content: .init(title: String.Constants.cancel.localized(), @@ -504,6 +504,28 @@ 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) + } + static func transferDomainsFromVaultUnavailable() -> ViewPullUpDefaultConfiguration { .init(icon: .init(icon: .hammerWrenchIcon24, size: .small), @@ -512,7 +534,6 @@ extension ViewPullUpDefaultConfiguration { cancelButton: .gotItButton(), analyticName: .transferDomainsFromVaultMaintenance) } - } // 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) } } diff --git a/unstoppable-ios-app/domains-manager-iosTests/FB_UD_MPCConnectionServiceTests.swift b/unstoppable-ios-app/domains-manager-iosTests/FB_UD_MPCConnectionServiceTests.swift index bd47a2c3f..6afff2d15 100644 --- a/unstoppable-ios-app/domains-manager-iosTests/FB_UD_MPCConnectionServiceTests.swift +++ b/unstoppable-ios-app/domains-manager-iosTests/FB_UD_MPCConnectionServiceTests.swift @@ -97,7 +97,7 @@ extension FB_UD_MPCConnectionServiceTests { let numberOfSimReqs = 5 try await withThrowingTaskGroup(of: Void.self) { group in - for i in 0..<5 { + for _ in 0..