From ab4e0391d44103bd5ccea4b900837f03a3c2d5fe Mon Sep 17 00:00:00 2001 From: Ermat Date: Tue, 25 Jul 2023 17:03:28 +0600 Subject: [PATCH] Update ActivateSubscription module to handle subscriptions list itself --- .../project.pbxproj | 2 +- .../ActivateSubscriptionModule.swift | 9 +- .../ActivateSubscriptionService.swift | 133 +++++++++++------- .../ActivateSubscriptionViewController.swift | 62 ++++++-- .../ActivateSubscriptionViewModel.swift | 80 +++++------ .../Coin/Analytics/CoinAnalyticsModule.swift | 3 +- .../Coin/Analytics/CoinAnalyticsService.swift | 14 +- .../SubscriptionInfoViewController.swift | 7 +- .../en.lproj/Localizable.strings | 1 + 9 files changed, 182 insertions(+), 129 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 08a4855272..eb9b905c76 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -10672,7 +10672,7 @@ repositoryURL = "https://github.com/horizontalsystems/MarketKit.Swift"; requirement = { kind = exactVersion; - version = 2.1.4; + version = 2.1.5; }; }; D3604E7128F03B0A0066C366 /* XCRemoteSwiftPackageReference "ScanQrKit.Swift" */ = { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ActivateSubscription/ActivateSubscriptionModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ActivateSubscription/ActivateSubscriptionModule.swift index e95b74447a..7ab6ef92d8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ActivateSubscription/ActivateSubscriptionModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ActivateSubscription/ActivateSubscriptionModule.swift @@ -4,15 +4,12 @@ import ThemeKit struct ActivateSubscriptionModule { - static func viewController(address: String) -> UIViewController? { - guard let service = ActivateSubscriptionService( - address: address, + static func viewController() -> UIViewController { + let service = ActivateSubscriptionService( marketKit: App.shared.marketKit, subscriptionManager: App.shared.subscriptionManager, accountManager: App.shared.accountManager - ) else { - return nil - } + ) let viewModel = ActivateSubscriptionViewModel(service: service) let viewController = ActivateSubscriptionViewController(viewModel: viewModel) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ActivateSubscription/ActivateSubscriptionService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ActivateSubscription/ActivateSubscriptionService.swift index 4891a8bd9a..c05a174899 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ActivateSubscription/ActivateSubscriptionService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ActivateSubscription/ActivateSubscriptionService.swift @@ -6,92 +6,115 @@ import HsToolKit import HsExtensions class ActivateSubscriptionService { - let account: Account - let evmAddress: EvmKit.Address private let marketKit: MarketKit.Kit private let subscriptionManager: SubscriptionManager private let accountManager: AccountManager private var tasks = Set() - @PostPublished private(set) var messageItem: MessageItem? - @PostPublished private(set) var state: State = .fetchingMessage + @PostPublished private(set) var state: State = .loading + @PostPublished private(set) var activationState: ActivationState = .ready - init?(address: String, marketKit: MarketKit.Kit, subscriptionManager: SubscriptionManager, accountManager: AccountManager) { - var resolvedAccount: Account? - var resolvedEvmAddress: EvmKit.Address? + private let activatedSubject = PassthroughSubject() + private let activationErrorSubject = PassthroughSubject() - for account in accountManager.accounts { - if let evmAddress = account.type.evmAddress(chain: App.shared.evmBlockchainManager.chain(blockchainType: .ethereum)), - evmAddress.hex.caseInsensitiveCompare(address) == .orderedSame { - resolvedAccount = account - resolvedEvmAddress = evmAddress - break - } - } - - guard let resolvedAccount, let resolvedEvmAddress else { - return nil - } - - account = resolvedAccount - evmAddress = resolvedEvmAddress + init(marketKit: MarketKit.Kit, subscriptionManager: SubscriptionManager, accountManager: AccountManager) { self.marketKit = marketKit self.subscriptionManager = subscriptionManager self.accountManager = accountManager - fetchMessage() + fetchSubscriptions() } - private func fetchMessage() { - state = .fetchingMessage + private func fetchSubscriptions() { + let addressItems: [AddressItem] = accountManager.accounts.compactMap { account in + guard let address = account.type.evmAddress(chain: App.shared.evmBlockchainManager.chain(blockchainType: .ethereum)) else { + return nil + } + + return AddressItem(account: account, address: address) + } - Task { [weak self, marketKit, evmAddress] in + guard !addressItems.isEmpty else { + state = .noSubscriptions + return + } + + state = .loading + + let addresses = addressItems.map { $0.address.hex } + + Task { [weak self, marketKit] in do { - let message = try await marketKit.authKey(address: evmAddress.hex) - self?.handle(message: message) + let subscriptions = try await marketKit.subscriptions(addresses: addresses) + self?.handle(subscriptions: subscriptions, addressItems: addressItems) } catch { - self?.state = .failedToFetchMessage(error: error) + self?.state = .failed(error: error) } }.store(in: &tasks) } - private func handle(message: String) { - messageItem = MessageItem( - account: account, - address: evmAddress, - message: message - ) + private func handle(subscriptions: [ProSubscription], addressItems: [AddressItem]) { + let address = subscriptions.sorted { lhs, rhs in lhs.deadline > rhs.deadline }.first?.address - state = .readyToActivate - } + guard let address else { + state = .noSubscriptions + return + } - private func handle(token: String) { - subscriptionManager.set(authToken: token) + let addressItem = addressItems.first { addressItem in + addressItem.address.hex.caseInsensitiveCompare(address) == .orderedSame + } - state = .activated + guard let addressItem else { + state = .noSubscriptions + return + } + + Task { [weak self, marketKit] in + do { + let message = try await marketKit.authKey(address: addressItem.address.hex) + self?.state = .readyToActivate(message: message, account: addressItem.account, address: addressItem.address) + } catch { + self?.state = .failed(error: error) + } + }.store(in: &tasks) } } extension ActivateSubscriptionService { + var activatedPublisher: AnyPublisher { + activatedSubject.eraseToAnyPublisher() + } + + var activationErrorPublisher: AnyPublisher { + activationErrorSubject.eraseToAnyPublisher() + } + func retry() { - fetchMessage() + fetchSubscriptions() } func sign() { - guard let messageData = messageItem?.message.data(using: .utf8), let signedData = account.type.sign(message: messageData) else { + guard case let .readyToActivate(message, account, address) = state else { + return + } + + guard let messageData = message.data(using: .utf8), let signedData = account.type.sign(message: messageData) else { return } - state = .activating + activationState = .activating - Task { [weak self, marketKit, evmAddress] in + Task { [weak self, marketKit] in do { - let token = try await marketKit.authenticate(signature: signedData.hs.hexString, address: evmAddress.hex) - self?.handle(token: token) + let token = try await marketKit.authenticate(signature: signedData.hs.hexString, address: address.hex) + self?.subscriptionManager.set(authToken: token) + self?.activatedSubject.send() } catch { - self?.state = .failedToActivate(error: error) + self?.activationState = .ready + self?.activationErrorSubject.send(error) } }.store(in: &tasks) } @@ -100,19 +123,21 @@ extension ActivateSubscriptionService { extension ActivateSubscriptionService { - struct MessageItem { + private struct AddressItem { let account: Account let address: EvmKit.Address - let message: String } enum State { - case fetchingMessage - case readyToActivate + case loading + case noSubscriptions + case readyToActivate(message: String, account: Account, address: EvmKit.Address) + case failed(error: Error) + } + + enum ActivationState { + case ready case activating - case activated - case failedToFetchMessage(error: Error) - case failedToActivate(error: Error) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ActivateSubscription/ActivateSubscriptionViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ActivateSubscription/ActivateSubscriptionViewController.swift index adadf95f08..3ce8d930e6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ActivateSubscription/ActivateSubscriptionViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ActivateSubscription/ActivateSubscriptionViewController.swift @@ -14,11 +14,12 @@ class ActivateSubscriptionViewController: ThemeViewController { private let tableView = SectionsTableView(style: .grouped) private let spinner = HUDActivityView.create(with: .medium24) private let errorView = PlaceholderViewModule.reachabilityView() + private let noSubscriptionsView = PlaceholderView() private let buttonsHolder = BottomGradientHolder() private let signButton = PrimaryButton() private let activatingButton = PrimaryButton() - private let cancelButton = PrimaryButton() + private let rejectButton = PrimaryButton() private var viewItem: ActivateSubscriptionViewModel.ViewItem? @@ -37,6 +38,9 @@ class ActivateSubscriptionViewController: ThemeViewController { title = "activate_subscription.title".localized + navigationItem.largeTitleDisplayMode = .never + navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.close".localized, style: .plain, target: self, action: #selector(onTapClose)) + view.addSubview(tableView) tableView.snp.makeConstraints { make in make.leading.top.trailing.equalToSuperview() @@ -48,7 +52,7 @@ class ActivateSubscriptionViewController: ThemeViewController { view.addSubview(spinner) spinner.snp.makeConstraints { make in - make.center.equalTo(tableView) + make.center.equalToSuperview() } spinner.startAnimating() @@ -60,22 +64,31 @@ class ActivateSubscriptionViewController: ThemeViewController { errorView.configureSyncError(action: { [weak self] in self?.viewModel.onTapRetry() }) + view.addSubview(noSubscriptionsView) + noSubscriptionsView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + noSubscriptionsView.image = UIImage(named: "sync_error_48")?.withTintColor(.themeGray) + noSubscriptionsView.text = "activate_subscription.no_subscriptions".localized + noSubscriptionsView.addPrimaryButton(style: .yellow, title: "subscription_info.get_premium".localized, target: self, action: #selector(onTapGetPremium)) + buttonsHolder.add(to: self, under: tableView) buttonsHolder.addSubview(signButton) signButton.set(style: .yellow) signButton.setTitle("activate_subscription.sign".localized, for: .normal) - signButton.addTarget(self, action: #selector(onTapSignButton), for: .touchUpInside) + signButton.addTarget(self, action: #selector(onTapSign), for: .touchUpInside) buttonsHolder.addSubview(activatingButton) activatingButton.set(style: .yellow, accessoryType: .spinner) activatingButton.isEnabled = false activatingButton.setTitle("activate_subscription.activating".localized, for: .normal) - buttonsHolder.addSubview(cancelButton) - cancelButton.set(style: .gray) - cancelButton.setTitle("button.cancel".localized, for: .normal) - cancelButton.addTarget(self, action: #selector(onTapCancelButton), for: .touchUpInside) + buttonsHolder.addSubview(rejectButton) + rejectButton.set(style: .gray) + rejectButton.setTitle("button.reject".localized, for: .normal) + rejectButton.addTarget(self, action: #selector(onTapReject), for: .touchUpInside) viewModel.$spinnerVisible .receive(on: DispatchQueue.main) @@ -87,11 +100,23 @@ class ActivateSubscriptionViewController: ThemeViewController { .sink { [weak self] visible in self?.errorView.isHidden = !visible } .store(in: &cancellables) + viewModel.$noSubscriptionsVisible + .receive(on: DispatchQueue.main) + .sink { [weak self] visible in self?.noSubscriptionsView.isHidden = !visible } + .store(in: &cancellables) + viewModel.$viewItem .receive(on: DispatchQueue.main) .sink { [weak self] viewItem in - self?.viewItem = viewItem - self?.tableView.reload() + if let viewItem { + self?.viewItem = viewItem + self?.tableView.reload() + self?.tableView.isHidden = false + self?.buttonsHolder.isHidden = false + } else { + self?.tableView.isHidden = true + self?.buttonsHolder.isHidden = true + } } .store(in: &cancellables) @@ -105,6 +130,11 @@ class ActivateSubscriptionViewController: ThemeViewController { .sink { [weak self] visible in self?.activatingButton.isHidden = !visible } .store(in: &cancellables) + viewModel.$rejectEnabled + .receive(on: DispatchQueue.main) + .sink { [weak self] enabled in self?.rejectButton.isEnabled = enabled } + .store(in: &cancellables) + viewModel.errorPublisher .receive(on: DispatchQueue.main) .sink { text in HudHelper.instance.showErrorBanner(title: text) } @@ -117,15 +147,21 @@ class ActivateSubscriptionViewController: ThemeViewController { self?.dismiss(animated: true) } .store(in: &cancellables) - - tableView.buildSections() } - @objc private func onTapSignButton() { + @objc private func onTapSign() { viewModel.onTapSign() } - @objc private func onTapCancelButton() { + @objc private func onTapReject() { + dismiss(animated: true) + } + + @objc private func onTapGetPremium() { + UrlManager.open(url: AppConfig.analyticsLink, inAppController: self) + } + + @objc private func onTapClose() { dismiss(animated: true) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ActivateSubscription/ActivateSubscriptionViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ActivateSubscription/ActivateSubscriptionViewModel.swift index 2d3813f979..ca47af8611 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ActivateSubscription/ActivateSubscriptionViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ActivateSubscription/ActivateSubscriptionViewModel.swift @@ -7,12 +7,12 @@ class ActivateSubscriptionViewModel { @Published private(set) var spinnerVisible = true @Published private(set) var errorVisible = false + @Published private(set) var noSubscriptionsVisible = false @Published private(set) var viewItem: ViewItem? + @Published private(set) var signVisible = false @Published private(set) var activatingVisible = false - - private let errorSubject = PassthroughSubject() - private let finishSubject = PassthroughSubject() + @Published private(set) var rejectEnabled = true init(service: ActivateSubscriptionService) { self.service = service @@ -21,51 +21,49 @@ class ActivateSubscriptionViewModel { .sink { [weak self] in self?.sync(state: $0) } .store(in: &cancellables) - service.$messageItem - .sink { [weak self] in self?.sync(messageItem: $0) } + service.$activationState + .sink { [weak self] in self?.sync(activationState: $0) } .store(in: &cancellables) sync(state: service.state) + sync(activationState: service.activationState) } private func sync(state: ActivateSubscriptionService.State) { switch state { - case .activated: - finishSubject.send() - return - case .failedToActivate: - errorSubject.send("activate_subscription.failed_to_activate".localized) - default: () - } - - switch state { - case .fetchingMessage: spinnerVisible = true - default: spinnerVisible = false - } - - switch state { - case .failedToFetchMessage: errorVisible = true - default: errorVisible = false - } - - switch state { - case .readyToActivate, .failedToActivate: signVisible = true - default: signVisible = false - } - - switch state { - case .activating: activatingVisible = true - default: activatingVisible = false + case .loading: + spinnerVisible = true + errorVisible = false + noSubscriptionsVisible = false + viewItem = nil + case .noSubscriptions: + spinnerVisible = false + errorVisible = false + noSubscriptionsVisible = true + viewItem = nil + case let .readyToActivate(message, account, address): + spinnerVisible = false + errorVisible = false + noSubscriptionsVisible = false + viewItem = ViewItem(walletName: account.name, address: address.eip55, message: message) + case .failed: + spinnerVisible = false + errorVisible = true + noSubscriptionsVisible = false + viewItem = nil } } - private func sync(messageItem: ActivateSubscriptionService.MessageItem?) { - viewItem = messageItem.map { - ViewItem( - walletName: $0.account.name, - address: $0.address.eip55, - message: $0.message - ) + private func sync(activationState: ActivateSubscriptionService.ActivationState) { + switch activationState { + case .ready: + signVisible = true + activatingVisible = false + rejectEnabled = true + case .activating: + signVisible = false + activatingVisible = true + rejectEnabled = false } } @@ -74,11 +72,13 @@ class ActivateSubscriptionViewModel { extension ActivateSubscriptionViewModel { var errorPublisher: AnyPublisher { - errorSubject.eraseToAnyPublisher() + service.activationErrorPublisher + .map { _ in "activate_subscription.failed_to_activate".localized } + .eraseToAnyPublisher() } var finishPublisher: AnyPublisher { - finishSubject.eraseToAnyPublisher() + service.activatedPublisher } func onTapRetry() { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsModule.swift index 8afc23246e..5e6725c062 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsModule.swift @@ -9,8 +9,7 @@ struct CoinAnalyticsModule { fullCoin: fullCoin, marketKit: App.shared.marketKit, currencyKit: App.shared.currencyKit, - subscriptionManager: App.shared.subscriptionManager, - accountManager: App.shared.accountManager + subscriptionManager: App.shared.subscriptionManager ) let technicalIndicatorService = TechnicalIndicatorService( coinUid: fullCoin.coin.uid, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsService.swift index 87b3a8406c..ed31cd9cc0 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsService.swift @@ -10,18 +10,16 @@ class CoinAnalyticsService { private let marketKit: MarketKit.Kit private let currencyKit: CurrencyKit.Kit private let subscriptionManager: SubscriptionManager - private let accountManager: AccountManager private var tasks = Set() private var cancellables = Set() @PostPublished private(set) var state: State = .loading - init(fullCoin: FullCoin, marketKit: MarketKit.Kit, currencyKit: CurrencyKit.Kit, subscriptionManager: SubscriptionManager, accountManager: AccountManager) { + init(fullCoin: FullCoin, marketKit: MarketKit.Kit, currencyKit: CurrencyKit.Kit, subscriptionManager: SubscriptionManager) { self.fullCoin = fullCoin self.marketKit = marketKit self.currencyKit = currencyKit self.subscriptionManager = subscriptionManager - self.accountManager = accountManager subscriptionManager.$isAuthenticated .sink { [weak self] isAuthenticated in @@ -32,18 +30,10 @@ class CoinAnalyticsService { .store(in: &cancellables) } - private func resolveAddresses() -> [String] { - accountManager.accounts - .compactMap { $0.type.evmAddress(chain: App.shared.evmBlockchainManager.chain(blockchainType: .ethereum)) } - .map { $0.hex } - } - private func loadPreview() { - let addresses = resolveAddresses() - Task { [weak self, marketKit, fullCoin] in do { - let analyticsPreview = try await marketKit.analyticsPreview(coinUid: fullCoin.coin.uid, addresses: addresses) + let analyticsPreview = try await marketKit.analyticsPreview(coinUid: fullCoin.coin.uid) self?.state = .preview(analyticsPreview: analyticsPreview) } catch { self?.state = .failed(error) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SubscriptionInfo/SubscriptionInfoViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SubscriptionInfo/SubscriptionInfoViewController.swift index 41d29d024f..5d0113dad8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SubscriptionInfo/SubscriptionInfoViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SubscriptionInfo/SubscriptionInfoViewController.swift @@ -56,7 +56,12 @@ class SubscriptionInfoViewController: ThemeViewController { } @objc private func onTapAlreadyHave() { - // todo + let viewController = ActivateSubscriptionModule.viewController() + let presentingViewController = presentingViewController + + dismiss(animated: true) { + presentingViewController?.present(viewController, animated: true) + } } } diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 3b0bec3fe4..68ed7edb63 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -1604,6 +1604,7 @@ Go to Settings - > %@ and allow access to the camera."; "activate_subscription.activating" = "Activating..."; "activate_subscription.failed_to_activate" = "Failed to activate subscription"; "activate_subscription.activated" = "Activated"; +"activate_subscription.no_subscriptions" = "Your wallet address does not have a subscription to premium features, you need to purchase it to activate the subscription."; // Launch