diff --git a/WooCommerce/Classes/Extensions/UIImage+Woo.swift b/WooCommerce/Classes/Extensions/UIImage+Woo.swift index 9c0cc29dda0..3ae8ff1159c 100644 --- a/WooCommerce/Classes/Extensions/UIImage+Woo.swift +++ b/WooCommerce/Classes/Extensions/UIImage+Woo.swift @@ -492,6 +492,18 @@ extension UIImage { return UIImage(named: "card-reader-scanning")! } + static var tempBuiltInReaderCheck: UIImage { + return UIImage(named: "temp-woo-tap-on-mobile-check")! + } + + static var tempBuiltInReaderPrepare: UIImage { + return UIImage(named: "temp-woo-tap-on-mobile-prepare")! + } + + static var tempBuiltInReaderPayment: UIImage { + return UIImage(named: "temp-woo-tap-on-mobile")! + } + /// Found Card Reader /// static var cardReaderFound: UIImage { diff --git a/WooCommerce/Classes/ViewModels/CardPresentPayments/CardPresentModalBuiltInConnectingToReader.swift b/WooCommerce/Classes/ViewModels/CardPresentPayments/CardPresentModalBuiltInConnectingToReader.swift new file mode 100644 index 00000000000..4fc6b73473d --- /dev/null +++ b/WooCommerce/Classes/ViewModels/CardPresentPayments/CardPresentModalBuiltInConnectingToReader.swift @@ -0,0 +1,50 @@ +import UIKit + +/// Modal presented when we are connecting to a reader +/// +final class CardPresentModalBuiltInConnectingToReader: CardPresentPaymentsModalViewModel { + let textMode: PaymentsModalTextMode = .reducedBottomInfo + let actionsMode: PaymentsModalActionsMode = .none + + let topTitle: String = Localization.title + + var topSubtitle: String? + + let image: UIImage = .tempBuiltInReaderCheck + + let primaryButtonTitle: String? = nil + + let secondaryButtonTitle: String? = nil + + let auxiliaryButtonTitle: String? = nil + + let bottomTitle: String? = Localization.instruction + + var bottomSubtitle: String? + + var accessibilityLabel: String? { + return topTitle + Localization.instruction + } + + init() {} + + func didTapPrimaryButton(in viewController: UIViewController?) {} + + func didTapSecondaryButton(in viewController: UIViewController?) {} + + func didTapAuxiliaryButton(in viewController: UIViewController?) {} +} + +private extension CardPresentModalBuiltInConnectingToReader { + enum Localization { + static let title = NSLocalizedString( + "Preparing iPhone card reader...", + comment: "Title label for modal dialog that appears when connecting to a built in card reader" + ) + + static let instruction = NSLocalizedString( + "The first time you connect, you may be prompted to accept Apple's Terms of Service.", + comment: "Label within the modal dialog that appears when connecting to a built in card reader" + ) + } +} diff --git a/WooCommerce/Classes/ViewModels/CardPresentPayments/CardPresentModalBuiltInReaderCheckingDeviceSupport.swift b/WooCommerce/Classes/ViewModels/CardPresentPayments/CardPresentModalBuiltInReaderCheckingDeviceSupport.swift new file mode 100644 index 00000000000..9e53fbab4ae --- /dev/null +++ b/WooCommerce/Classes/ViewModels/CardPresentPayments/CardPresentModalBuiltInReaderCheckingDeviceSupport.swift @@ -0,0 +1,122 @@ +import UIKit + +/// Modal presented when we are scanning for a reader to connect to +/// +final class CardPresentModalBuiltInReaderCheckingDeviceSupport: CardPresentPaymentsModalViewModel { + /// Called when cancel button is tapped + private let cancelAction: () -> Void + + let textMode: PaymentsModalTextMode = .reducedBottomInfo + let actionsMode: PaymentsModalActionsMode = .secondaryActionAndAuxiliaryButton + + let topTitle: String = Localization.title + + var topSubtitle: String? + + let image: UIImage = .tempBuiltInReaderPrepare + + let primaryButtonTitle: String? = nil + + let secondaryButtonTitle: String? = Localization.cancel + + let auxiliaryButtonTitle: String? = nil + + let auxiliaryButtonimage: UIImage? = .infoOutlineImage + + var auxiliaryAttributedButtonTitle: NSAttributedString? { + let result = NSMutableAttributedString( + string: .localizedStringWithFormat( + Localization.learnMoreText, + Localization.learnMoreLink + ), + attributes: [.foregroundColor: UIColor.text] + ) + result.replaceFirstOccurrence( + of: Localization.learnMoreLink, + with: NSAttributedString( + string: Localization.learnMoreLink, + attributes: [ + .foregroundColor: UIColor.accent, + .underlineStyle: NSUnderlineStyle.single.rawValue + ] + )) + result.addAttribute(.font, value: UIFont.footnote, range: NSRange(location: 0, length: result.length)) + return result + } + + let bottomTitle: String? = Localization.instruction + + var bottomSubtitle: String? + + var accessibilityLabel: String? { + guard let bottomTitle = bottomTitle else { + return topTitle + } + return topTitle + bottomTitle + } + + init(cancel: @escaping () -> Void) { + self.cancelAction = cancel + } + + func didTapPrimaryButton(in viewController: UIViewController?) {} + + func didTapSecondaryButton(in viewController: UIViewController?) { + cancelAction() + } + + func didTapAuxiliaryButton(in viewController: UIViewController?) { + ServiceLocator.analytics.track(.cardPresentOnboardingLearnMoreTapped) + guard let viewController = viewController else { + return + } + WebviewHelper.launch(Constants.learnMoreURL.asURL(), with: viewController) + } +} + +private extension CardPresentModalBuiltInReaderCheckingDeviceSupport { + enum Constants { + static let learnMoreURL = WooConstants.URLs.inPersonPaymentsLearnMoreWCPay + } + + enum Localization { + static let title = NSLocalizedString( + "cardPresent.builtIn.modalCheckingDeviceSupport.title", + value: "Checking device", + comment: "Title label for modal dialog that appears when searching for a card reader" + ) + + static let instruction = NSLocalizedString( + "cardPresent.builtIn.modalCheckingDeviceSupport.instruction", + value: "Please wait while we check that your device is ready for Tap to Pay on iPhone.", + comment: "Label within the modal dialog that appears when checking the built in card reader" + ) + + static let cancel = NSLocalizedString( + "cardPresent.builtIn.modalCheckingDeviceSupport.cancelButton", + value: "Cancel", + comment: "Label for a cancel button" + ) + + static let learnMoreLink = NSLocalizedString( + "cardPresent.builtIn.modalCheckingDeviceSupport.learnMore.link", + value: "Learn more", + comment: """ + A label prompting users to learn more about In-Person Payments. + This is the link to the website, and forms part of a longer sentence which it should be considered a part of. + """ + ) + + static let learnMoreText = NSLocalizedString( + "cardPresent.builtIn.modalCheckingDeviceSupport.learnMore.text", + value: "%1$@ about In‑Person Payments", + comment: """ + A label prompting users to learn more about In-Person Payments. + The hyphen in "In‑Person" is a non-breaking hyphen (U+2011). + If your translation of that term also happens to contains a hyphen, please be sure to use the non-breaking hyphen character for it. + %1$@ is a placeholder that always replaced with \"Learn more\" string, + which should be translated separately and considered part of this sentence. + """ + ) + } +} diff --git a/WooCommerce/Classes/ViewRelated/CardPresentPayments/BuiltInCardReaderConnectionController.swift b/WooCommerce/Classes/ViewRelated/CardPresentPayments/BuiltInCardReaderConnectionController.swift index 432fdfc8dbe..68d1c1ec96e 100644 --- a/WooCommerce/Classes/ViewRelated/CardPresentPayments/BuiltInCardReaderConnectionController.swift +++ b/WooCommerce/Classes/ViewRelated/CardPresentPayments/BuiltInCardReaderConnectionController.swift @@ -74,6 +74,8 @@ final class BuiltInCardReaderConnectionController { private let alertsPresenter: CardPresentPaymentAlertsPresenting private let configuration: CardPresentPaymentsConfiguration + private let alertsProvider: CardReaderConnectionAlertsProviding + /// The reader we want the user to consider connecting to /// private var candidateReader: CardReader? @@ -105,6 +107,7 @@ final class BuiltInCardReaderConnectionController { storageManager: StorageManagerType = ServiceLocator.storageManager, stores: StoresManager = ServiceLocator.stores, alertsPresenter: CardPresentPaymentAlertsPresenting, + alertsProvider: CardReaderConnectionAlertsProviding, configuration: CardPresentPaymentsConfiguration, analyticsTracker: CardReaderConnectionAnalyticsTracker ) { @@ -113,6 +116,7 @@ final class BuiltInCardReaderConnectionController { self.stores = stores state = .idle self.alertsPresenter = alertsPresenter + self.alertsProvider = alertsProvider self.configuration = configuration self.analyticsTracker = analyticsTracker @@ -264,7 +268,7 @@ private extension BuiltInCardReaderConnectionController { /// If all else fails, display the "scanning" modal and /// stay in this state /// - alertsPresenter.present(viewModel: CardPresentModalScanningForReader(cancel: { + alertsPresenter.present(viewModel: alertsProvider.scanningForReader(cancel: { self.state = .cancel })) } @@ -288,9 +292,9 @@ private extension BuiltInCardReaderConnectionController { } alertsPresenter.present( - viewModel: CardPresentModalUpdateProgress(requiredUpdate: true, - progress: progress, - cancel: cancel)) + viewModel: alertsProvider.updateProgress(requiredUpdate: true, + progress: progress, + cancel: cancel)) } /// Retry a search for a card reader @@ -382,7 +386,7 @@ private extension BuiltInCardReaderConnectionController { } stores.dispatch(action) - alertsPresenter.present(viewModel: CardPresentModalConnectingToReader()) + alertsPresenter.present(viewModel: alertsProvider.connectingToReader()) } /// An error occurred while connecting @@ -402,23 +406,25 @@ private extension BuiltInCardReaderConnectionController { } private func onUpdateFailed(error: Error) { - guard case CardReaderServiceError.softwareUpdate(underlyingError: let underlyingError, batteryLevel: let batteryLevel) = error else { + guard case CardReaderServiceError.softwareUpdate(underlyingError: let underlyingError, batteryLevel: _) = error else { return } + // Duplication of `readerSoftwareUpdateFailedBatteryLow` and `default is left to make factoring out easier later on. switch underlyingError { case .readerSoftwareUpdateFailedInterrupted: // Update was cancelled, don't treat this as an error return case .readerSoftwareUpdateFailedBatteryLow: alertsPresenter.present( - viewModel: CardPresentModalUpdateFailedLowBattery(batteryLevel: batteryLevel, - close: { - self.state = .searching - })) + viewModel: alertsProvider.updatingFailed(tryAgain: nil, + close: { + self.state = .searching + })) default: alertsPresenter.present( - viewModel: CardPresentModalUpdateFailedNonRetryable(close: { + viewModel: alertsProvider.updatingFailed(tryAgain: nil, + close: { self.state = .searching })) } @@ -429,6 +435,7 @@ private extension BuiltInCardReaderConnectionController { self.state = .retry } + // TODO: Consider removing this in favour of retry only – continue doesn't make sense for a built-in reader let continueSearch = { self.state = .searching } @@ -439,30 +446,25 @@ private extension BuiltInCardReaderConnectionController { guard case CardReaderServiceError.connection(let underlyingError) = error else { return alertsPresenter.present( - viewModel: CardPresentModalConnectingFailed(continueSearch: continueSearch, cancelSearch: cancelSearch)) + viewModel: alertsProvider.connectingFailed(continueSearch: continueSearch, cancelSearch: cancelSearch)) } switch underlyingError { case .incompleteStoreAddress(let adminUrl): alertsPresenter.present( - viewModel: CardPresentModalConnectingFailedUpdateAddress( + viewModel: alertsProvider.connectingFailedIncompleteAddress( openWCSettings: openWCSettingsAction(adminUrl: adminUrl, retrySearch: retrySearch), retrySearch: retrySearch, cancelSearch: cancelSearch)) case .invalidPostalCode: alertsPresenter.present( - viewModel: CardPresentModalConnectingFailedUpdatePostalCode( - retrySearch: retrySearch, - cancelSearch: cancelSearch)) - case .bluetoothConnectionFailedBatteryCriticallyLow: - alertsPresenter.present( - viewModel: CardPresentModalConnectingFailedChargeReader( + viewModel: alertsProvider.connectingFailedInvalidPostalCode( retrySearch: retrySearch, cancelSearch: cancelSearch)) default: alertsPresenter.present( - viewModel: CardPresentModalConnectingFailed( + viewModel: alertsProvider.connectingFailed( continueSearch: continueSearch, cancelSearch: cancelSearch)) } @@ -521,7 +523,7 @@ private extension BuiltInCardReaderConnectionController { /// private func onDiscoveryFailed(error: Error) { alertsPresenter.present( - viewModel: CardPresentModalScanningFailed(error: error) { [weak self] in + viewModel: alertsProvider.scanningFailed(error: error) { [weak self] in self?.returnFailure(error: error) }) } diff --git a/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardPresentPaymentPreflightController.swift b/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardPresentPaymentPreflightController.swift index 75140038f50..c0d849ce9b8 100644 --- a/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardPresentPaymentPreflightController.swift +++ b/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardPresentPaymentPreflightController.swift @@ -70,12 +70,14 @@ final class CardPresentPaymentPreflightController { forSiteID: siteID, knownReaderProvider: CardReaderSettingsKnownReaderStorage(), alertsPresenter: alertsPresenter, + alertsProvider: BluetoothReaderConnectionAlertsProvider(), configuration: configuration, analyticsTracker: analyticsTracker) self.builtInConnectionController = BuiltInCardReaderConnectionController( forSiteID: siteID, alertsPresenter: alertsPresenter, + alertsProvider: BuiltInReaderConnectionAlertsProvider(), configuration: configuration, analyticsTracker: analyticsTracker) } diff --git a/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardReaderConnectionController.swift b/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardReaderConnectionController.swift index 5e70ee4baa5..ceab72a92f0 100644 --- a/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardReaderConnectionController.swift +++ b/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardReaderConnectionController.swift @@ -83,6 +83,8 @@ final class CardReaderConnectionController { private let alertsPresenter: CardPresentPaymentAlertsPresenting private let configuration: CardPresentPaymentsConfiguration + private let alertsProvider: BluetoothReaderConnnectionAlertsProviding + /// Reader(s) discovered by the card reader service /// private var foundReaders: [CardReader] @@ -134,6 +136,7 @@ final class CardReaderConnectionController { stores: StoresManager = ServiceLocator.stores, knownReaderProvider: CardReaderSettingsKnownReaderProvider, alertsPresenter: CardPresentPaymentAlertsPresenting, + alertsProvider: BluetoothReaderConnnectionAlertsProviding, configuration: CardPresentPaymentsConfiguration, analyticsTracker: CardReaderConnectionAnalyticsTracker ) { @@ -143,6 +146,7 @@ final class CardReaderConnectionController { state = .idle knownCardReaderProvider = knownReaderProvider self.alertsPresenter = alertsPresenter + self.alertsProvider = alertsProvider foundReaders = [] knownReaderID = nil skippedReaderIDs = [] @@ -419,7 +423,7 @@ private extension CardReaderConnectionController { /// If all else fails, display the "scanning" modal and /// stay in this state /// - alertsPresenter.present(viewModel: CardPresentModalScanningForReader(cancel: { + alertsPresenter.present(viewModel: alertsProvider.scanningForReader(cancel: { self.state = .cancel })) } @@ -433,7 +437,7 @@ private extension CardReaderConnectionController { } alertsPresenter.present( - viewModel: CardPresentModalFoundReader( + viewModel: alertsProvider.foundReader( name: candidateReader.id, connect: { self.state = .connectToReader @@ -444,7 +448,7 @@ private extension CardReaderConnectionController { self.pruneSkippedReaders() self.state = .searching }, - cancel: { [weak self] in + cancelSearch: { [weak self] in self?.state = .cancel })) } @@ -487,9 +491,9 @@ private extension CardReaderConnectionController { } alertsPresenter.present( - viewModel: CardPresentModalUpdateProgress(requiredUpdate: true, - progress: progress, - cancel: cancel)) + viewModel: alertsProvider.updateProgress(requiredUpdate: true, + progress: progress, + cancel: cancel)) } /// Retry a search for a card reader @@ -582,7 +586,7 @@ private extension CardReaderConnectionController { } stores.dispatch(action) - alertsPresenter.present(viewModel: CardPresentModalConnectingToReader()) + alertsPresenter.present(viewModel: alertsProvider.connectingToReader()) } /// An error occurred while connecting @@ -612,13 +616,14 @@ private extension CardReaderConnectionController { return case .readerSoftwareUpdateFailedBatteryLow: alertsPresenter.present( - viewModel: CardPresentModalUpdateFailedLowBattery(batteryLevel: batteryLevel, - close: { - self.state = .searching - })) + viewModel: alertsProvider.updatingFailedLowBattery(batteryLevel: batteryLevel, + close: { + self.state = .searching + })) default: alertsPresenter.present( - viewModel: CardPresentModalUpdateFailedNonRetryable(close: { + viewModel: alertsProvider.updatingFailed(tryAgain: nil, + close: { self.state = .searching })) } @@ -639,30 +644,30 @@ private extension CardReaderConnectionController { guard case CardReaderServiceError.connection(let underlyingError) = error else { return alertsPresenter.present( - viewModel: CardPresentModalConnectingFailed(continueSearch: continueSearch, cancelSearch: cancelSearch)) + viewModel: alertsProvider.connectingFailed(continueSearch: continueSearch, cancelSearch: cancelSearch)) } switch underlyingError { case .incompleteStoreAddress(let adminUrl): alertsPresenter.present( - viewModel: CardPresentModalConnectingFailedUpdateAddress( + viewModel: alertsProvider.connectingFailedIncompleteAddress( openWCSettings: openWCSettingsAction(adminUrl: adminUrl, retrySearch: retrySearch), retrySearch: retrySearch, cancelSearch: cancelSearch)) case .invalidPostalCode: alertsPresenter.present( - viewModel: CardPresentModalConnectingFailedUpdatePostalCode( + viewModel: alertsProvider.connectingFailedInvalidPostalCode( retrySearch: retrySearch, cancelSearch: cancelSearch)) case .bluetoothConnectionFailedBatteryCriticallyLow: alertsPresenter.present( - viewModel: CardPresentModalConnectingFailedChargeReader( + viewModel: alertsProvider.connectingFailedCriticallyLowBattery( retrySearch: retrySearch, cancelSearch: cancelSearch)) default: alertsPresenter.present( - viewModel: CardPresentModalConnectingFailed( + viewModel: alertsProvider.connectingFailed( continueSearch: continueSearch, cancelSearch: cancelSearch)) } @@ -721,7 +726,7 @@ private extension CardReaderConnectionController { /// private func onDiscoveryFailed(error: Error) { alertsPresenter.present( - viewModel: CardPresentModalScanningFailed(error: error) { [weak self] in + viewModel: alertsProvider.scanningFailed(error: error) { [weak self] in self?.returnFailure(error: error) }) } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothReaderConnectionAlertsProvider.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothReaderConnectionAlertsProvider.swift new file mode 100644 index 00000000000..3844329e89c --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothReaderConnectionAlertsProvider.swift @@ -0,0 +1,65 @@ +import Foundation +import UIKit + +struct BluetoothReaderConnectionAlertsProvider: BluetoothReaderConnnectionAlertsProviding { + func scanningForReader(cancel: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + CardPresentModalScanningForReader(cancel: cancel) + } + + func scanningFailed(error: Error, + close: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + CardPresentModalScanningFailed(error: error, primaryAction: close) + } + + func connectingToReader() -> CardPresentPaymentsModalViewModel { + CardPresentModalConnectingToReader() + } + + func connectingFailed(continueSearch: @escaping () -> Void, + cancelSearch: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + CardPresentModalConnectingFailed(continueSearch: continueSearch, cancelSearch: cancelSearch) + } + + func connectingFailedIncompleteAddress(openWCSettings: ((UIViewController) -> Void)?, + retrySearch: @escaping () -> Void, + cancelSearch: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + CardPresentModalConnectingFailedUpdateAddress(openWCSettings: openWCSettings, retrySearch: retrySearch, cancelSearch: cancelSearch) + } + + func connectingFailedInvalidPostalCode(retrySearch: @escaping () -> Void, + cancelSearch: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + CardPresentModalConnectingFailedUpdatePostalCode(retrySearch: retrySearch, cancelSearch: cancelSearch) + } + + func updatingFailed(tryAgain: (() -> Void)?, + close: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + if let tryAgain = tryAgain { + return CardPresentModalUpdateFailed(tryAgain: tryAgain, close: close) + } else { + return CardPresentModalUpdateFailedNonRetryable(close: close) + } + } + func updateProgress(requiredUpdate: Bool, + progress: Float, + cancel: (() -> Void)?) -> CardPresentPaymentsModalViewModel { + CardPresentModalUpdateProgress(requiredUpdate: requiredUpdate, progress: progress, cancel: cancel) + } + + func foundReader(name: String, + connect: @escaping () -> Void, + continueSearch: @escaping () -> Void, + cancelSearch: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + CardPresentModalFoundReader(name: name, connect: connect, continueSearch: continueSearch, cancel: cancelSearch) + } + + func connectingFailedCriticallyLowBattery(retrySearch: @escaping () -> Void, + cancelSearch: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + CardPresentModalConnectingFailedChargeReader(retrySearch: retrySearch, cancelSearch: cancelSearch) + } + + func updatingFailedLowBattery(batteryLevel: Double?, + close: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + CardPresentModalUpdateFailedLowBattery(batteryLevel: batteryLevel, close: close) + } + +} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInReaderConnectionAlertsProvider.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInReaderConnectionAlertsProvider.swift new file mode 100644 index 00000000000..cb9cbae952f --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInReaderConnectionAlertsProvider.swift @@ -0,0 +1,46 @@ +import Foundation +import UIKit + +struct BuiltInReaderConnectionAlertsProvider: CardReaderConnectionAlertsProviding { + func scanningForReader(cancel: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + CardPresentModalBuiltInReaderCheckingDeviceSupport(cancel: cancel) + } + + func scanningFailed(error: Error, + close: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + CardPresentModalScanningFailed(error: error, primaryAction: close) + } + + func connectingToReader() -> CardPresentPaymentsModalViewModel { + CardPresentModalBuiltInConnectingToReader() + } + + func connectingFailed(continueSearch: @escaping () -> Void, + cancelSearch: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + CardPresentModalConnectingFailed(continueSearch: continueSearch, cancelSearch: cancelSearch) + } + + func connectingFailedIncompleteAddress(openWCSettings: ((UIViewController) -> Void)?, + retrySearch: @escaping () -> Void, + cancelSearch: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + CardPresentModalConnectingFailedUpdateAddress(openWCSettings: openWCSettings, retrySearch: retrySearch, cancelSearch: cancelSearch) + } + + func connectingFailedInvalidPostalCode(retrySearch: @escaping () -> Void, + cancelSearch: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + CardPresentModalConnectingFailedUpdatePostalCode(retrySearch: retrySearch, cancelSearch: cancelSearch) + } + + func updatingFailed(tryAgain: (() -> Void)?, + close: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + if let tryAgain = tryAgain { + return CardPresentModalUpdateFailed(tryAgain: tryAgain, close: close) + } else { + return CardPresentModalUpdateFailedNonRetryable(close: close) + } + } + func updateProgress(requiredUpdate: Bool, progress: Float, + cancel: (() -> Void)?) -> CardPresentPaymentsModalViewModel { + CardPresentModalUpdateProgress(requiredUpdate: requiredUpdate, progress: progress, cancel: cancel) + } +} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderConnectionAlertsProviding.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderConnectionAlertsProviding.swift new file mode 100644 index 00000000000..588c9e574d1 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderConnectionAlertsProviding.swift @@ -0,0 +1,97 @@ +import Foundation +import UIKit +import Yosemite + +/// Defines a protocol for card reader connection alert providers to conform to - defining what +/// alert viewModels such a provider is expected to provide over the course of searching for +/// and connecting to a reader +/// +protocol CardReaderConnectionAlertsProviding { + /// Defines a cancellable alert indicating we are searching for a reader + /// + func scanningForReader(cancel: @escaping () -> Void) -> CardPresentPaymentsModalViewModel + + /// Defines a cancellable (closeable) alert indicating the search failed + /// + func scanningFailed(error: Error, close: @escaping () -> Void) -> CardPresentPaymentsModalViewModel + + /// Defines a non-interactive alert indicating a connection is in progress to a particular reader + /// + func connectingToReader() -> CardPresentPaymentsModalViewModel + + /// Defines an alert indicating connecting failed. The user may continue the search + /// or cancel + /// + func connectingFailed(continueSearch: @escaping () -> Void, + cancelSearch: @escaping () -> Void) -> CardPresentPaymentsModalViewModel + + /// Defines an alert indicating connecting failed because their address needs updating. + /// The user may try again or cancel + /// + func connectingFailedIncompleteAddress(openWCSettings: ((UIViewController) -> Void)?, + retrySearch: @escaping () -> Void, + cancelSearch: @escaping () -> Void) -> CardPresentPaymentsModalViewModel + + /// Defines an alert indicating connecting failed because their postal code needs updating. + /// The user may try again or cancel + /// + func connectingFailedInvalidPostalCode(retrySearch: @escaping () -> Void, + cancelSearch: @escaping () -> Void) -> CardPresentPaymentsModalViewModel + + /// Defines an alert indicating an update couldn't be installed. + /// + func updatingFailed(tryAgain: (() -> Void)?, close: @escaping () -> Void) -> CardPresentPaymentsModalViewModel + + /// Shows progress when a software update is being installed + /// + func updateProgress(requiredUpdate: Bool, progress: Float, cancel: (() -> Void)?) -> CardPresentPaymentsModalViewModel +} + + +/// Defines a protocol for card reader connection alert providers to conform to, if they support choosing +/// between several readers - defining what alert viewModels such a provider is expected to provide over +/// the course of searching for and connecting to a reader. +/// +protocol MultipleCardReaderConnectionAlertsProviding { + /// Defines an interactive alert indicating a reader has been found. The user must + /// choose to connect to that reader or continue searching + /// + func foundReader(name: String, + connect: @escaping () -> Void, + continueSearch: @escaping () -> Void, + cancelSearch: @escaping () -> Void) -> CardPresentPaymentsModalViewModel + + // TODO: implement this approach, allowing us to remove the several readers logic from CardPresentPaymentAlertsPresenter + // https://github.com/woocommerce/woocommerce-ios/issues/8296 + /// Defines an interactive alert indicating more than one reader has been found. The user must + /// choose to connect to that reader or cancel searching + /// +// func foundSeveralReaders(readerIDs: [String], +// connect: @escaping (String) -> Void, +// cancelSearch: @escaping () -> Void) -> CardPresentPaymentsModalViewModel + + /// Allows updating the list of readers found in the several readers alert + /// +// func updateSeveralReadersList(readerIDs: [String]) -> CardPresentPaymentsModalViewModel +} + +/// Defines a protocol for card reader connection alert providers to conform to, if they support battery +/// powered readers - defining what alert viewModels such a provider is expected to provide over +/// the course of searching for and connecting to a reader. +/// +protocol BatteryPoweredCardReaderConnectionAlertsProviding { + /// Defines an alert indicating connecting failed because the reader battery is critically low. + /// The user may try searching again (i.e. for a different reader) or cancel + /// + func connectingFailedCriticallyLowBattery(retrySearch: @escaping () -> Void, + cancelSearch: @escaping () -> Void) -> CardPresentPaymentsModalViewModel + + /// Defines an alert indicating an update couldn't be installed because the reader is low on battery. + /// + func updatingFailedLowBattery(batteryLevel: Double?, + close: @escaping () -> Void) -> CardPresentPaymentsModalViewModel +} + +typealias BluetoothReaderConnnectionAlertsProviding = CardReaderConnectionAlertsProviding & + MultipleCardReaderConnectionAlertsProviding & + BatteryPoweredCardReaderConnectionAlertsProviding diff --git a/WooCommerce/Resources/Images.xcassets/temp-woo-tap-on-mobile-check.imageset/Contents.json b/WooCommerce/Resources/Images.xcassets/temp-woo-tap-on-mobile-check.imageset/Contents.json new file mode 100644 index 00000000000..1a535b2ce42 --- /dev/null +++ b/WooCommerce/Resources/Images.xcassets/temp-woo-tap-on-mobile-check.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "temp-woo-tap-on-mobile-check~universal.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WooCommerce/Resources/Images.xcassets/temp-woo-tap-on-mobile-check.imageset/temp-woo-tap-on-mobile-check~universal.pdf b/WooCommerce/Resources/Images.xcassets/temp-woo-tap-on-mobile-check.imageset/temp-woo-tap-on-mobile-check~universal.pdf new file mode 100644 index 00000000000..f8699bbf868 Binary files /dev/null and b/WooCommerce/Resources/Images.xcassets/temp-woo-tap-on-mobile-check.imageset/temp-woo-tap-on-mobile-check~universal.pdf differ diff --git a/WooCommerce/Resources/Images.xcassets/temp-woo-tap-on-mobile-prepare.imageset/Contents.json b/WooCommerce/Resources/Images.xcassets/temp-woo-tap-on-mobile-prepare.imageset/Contents.json new file mode 100644 index 00000000000..f584231b453 --- /dev/null +++ b/WooCommerce/Resources/Images.xcassets/temp-woo-tap-on-mobile-prepare.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "temp-woo-tap-on-mobile-prepare~universal.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WooCommerce/Resources/Images.xcassets/temp-woo-tap-on-mobile-prepare.imageset/temp-woo-tap-on-mobile-prepare~universal.pdf b/WooCommerce/Resources/Images.xcassets/temp-woo-tap-on-mobile-prepare.imageset/temp-woo-tap-on-mobile-prepare~universal.pdf new file mode 100644 index 00000000000..b6ccedd4720 Binary files /dev/null and b/WooCommerce/Resources/Images.xcassets/temp-woo-tap-on-mobile-prepare.imageset/temp-woo-tap-on-mobile-prepare~universal.pdf differ diff --git a/WooCommerce/Resources/Images.xcassets/temp-woo-tap-on-mobile.imageset/Contents.json b/WooCommerce/Resources/Images.xcassets/temp-woo-tap-on-mobile.imageset/Contents.json new file mode 100644 index 00000000000..12dabdb3fd3 --- /dev/null +++ b/WooCommerce/Resources/Images.xcassets/temp-woo-tap-on-mobile.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "temp-woo-tap-on-mobile~universal.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WooCommerce/Resources/Images.xcassets/temp-woo-tap-on-mobile.imageset/temp-woo-tap-on-mobile~universal.pdf b/WooCommerce/Resources/Images.xcassets/temp-woo-tap-on-mobile.imageset/temp-woo-tap-on-mobile~universal.pdf new file mode 100644 index 00000000000..745b546dbdb Binary files /dev/null and b/WooCommerce/Resources/Images.xcassets/temp-woo-tap-on-mobile.imageset/temp-woo-tap-on-mobile~universal.pdf differ diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 11b4d4f7d69..7335700120d 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -495,6 +495,11 @@ 03BB9EA7292E50B600251E9E /* CardPresentModalSelectSearchType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BB9EA6292E50B600251E9E /* CardPresentModalSelectSearchType.swift */; }; 03CF78D127C3DBC000523706 /* WCPayCardBrand+IconsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CF78D027C3DBC000523706 /* WCPayCardBrand+IconsTests.swift */; }; 03E471BE29388787001A58AD /* BuiltInCardReaderConnectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E471BD29388787001A58AD /* BuiltInCardReaderConnectionController.swift */; }; + 03E471C0293A158D001A58AD /* CardReaderConnectionAlertsProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E471BF293A158C001A58AD /* CardReaderConnectionAlertsProviding.swift */; }; + 03E471C2293A1F6B001A58AD /* BluetoothReaderConnectionAlertsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E471C1293A1F6B001A58AD /* BluetoothReaderConnectionAlertsProvider.swift */; }; + 03E471C4293A1F8D001A58AD /* BuiltInReaderConnectionAlertsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E471C3293A1F8D001A58AD /* BuiltInReaderConnectionAlertsProvider.swift */; }; + 03E471C6293A2E95001A58AD /* CardPresentModalBuiltInReaderCheckingDeviceSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E471C5293A2E95001A58AD /* CardPresentModalBuiltInReaderCheckingDeviceSupport.swift */; }; + 03E471C8293A3076001A58AD /* CardPresentModalBuiltInConnectingToReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E471C7293A3075001A58AD /* CardPresentModalBuiltInConnectingToReader.swift */; }; 03EF24FA28BF5D21006A033E /* InPersonPaymentsCashOnDeliveryToggleRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EF24F928BF5D21006A033E /* InPersonPaymentsCashOnDeliveryToggleRowViewModel.swift */; }; 03EF24FC28BF996F006A033E /* InPersonPaymentsCashOnDeliveryPaymentGatewayHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EF24FB28BF996F006A033E /* InPersonPaymentsCashOnDeliveryPaymentGatewayHelpers.swift */; }; 03EF24FE28C0B356006A033E /* CardPresentPaymentsPlugin+CashOnDelivery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EF24FD28C0B356006A033E /* CardPresentPaymentsPlugin+CashOnDelivery.swift */; }; @@ -2493,6 +2498,11 @@ 03BB9EA6292E50B600251E9E /* CardPresentModalSelectSearchType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardPresentModalSelectSearchType.swift; sourceTree = ""; }; 03CF78D027C3DBC000523706 /* WCPayCardBrand+IconsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WCPayCardBrand+IconsTests.swift"; sourceTree = ""; }; 03E471BD29388787001A58AD /* BuiltInCardReaderConnectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuiltInCardReaderConnectionController.swift; sourceTree = ""; }; + 03E471BF293A158C001A58AD /* CardReaderConnectionAlertsProviding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardReaderConnectionAlertsProviding.swift; sourceTree = ""; }; + 03E471C1293A1F6B001A58AD /* BluetoothReaderConnectionAlertsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothReaderConnectionAlertsProvider.swift; sourceTree = ""; }; + 03E471C3293A1F8D001A58AD /* BuiltInReaderConnectionAlertsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuiltInReaderConnectionAlertsProvider.swift; sourceTree = ""; }; + 03E471C5293A2E95001A58AD /* CardPresentModalBuiltInReaderCheckingDeviceSupport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardPresentModalBuiltInReaderCheckingDeviceSupport.swift; sourceTree = ""; }; + 03E471C7293A3075001A58AD /* CardPresentModalBuiltInConnectingToReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardPresentModalBuiltInConnectingToReader.swift; sourceTree = ""; }; 03EF24F928BF5D21006A033E /* InPersonPaymentsCashOnDeliveryToggleRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsCashOnDeliveryToggleRowViewModel.swift; sourceTree = ""; }; 03EF24FB28BF996F006A033E /* InPersonPaymentsCashOnDeliveryPaymentGatewayHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsCashOnDeliveryPaymentGatewayHelpers.swift; sourceTree = ""; }; 03EF24FD28C0B356006A033E /* CardPresentPaymentsPlugin+CashOnDelivery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CardPresentPaymentsPlugin+CashOnDelivery.swift"; sourceTree = ""; }; @@ -5614,6 +5624,9 @@ 31F92DFE25E86F4500DE04DF /* CardReaderTableViewCells */, 311F827326CD897900DF5BAD /* CardReaderSettingsAlertsProvider.swift */, 311D21EC264AF0E700102316 /* CardReaderSettingsAlerts.swift */, + 03E471BF293A158C001A58AD /* CardReaderConnectionAlertsProviding.swift */, + 03E471C1293A1F6B001A58AD /* BluetoothReaderConnectionAlertsProvider.swift */, + 03E471C3293A1F8D001A58AD /* BuiltInReaderConnectionAlertsProvider.swift */, 3178C1F626409216000D771A /* CardReaderSettingsConnectedViewModel.swift */, 035C6DEA273EA12D00F70406 /* SoftwareUpdateTypeProperty.swift */, 314265B02645A07800500598 /* CardReaderSettingsConnectedViewController.swift */, @@ -8509,6 +8522,8 @@ isa = PBXGroup; children = ( D8815AE626383FD600EDAD62 /* CardPresentPaymentsModalViewModel.swift */, + 03E471C5293A2E95001A58AD /* CardPresentModalBuiltInReaderCheckingDeviceSupport.swift */, + 03E471C7293A3075001A58AD /* CardPresentModalBuiltInConnectingToReader.swift */, 311237ED2714DA240033C44E /* CardPresentModalDisplayMessage.swift */, D8815B0026385E3F00EDAD62 /* CardPresentModalTapCard.swift */, D8815B0C263861A400EDAD62 /* CardPresentModalSuccess.swift */, @@ -10118,6 +10133,7 @@ DE61979328A20C17005E4362 /* StorePickerViewModel.swift in Sources */, E1BE703A265E6F47006CA4D9 /* CardPresentModalScanningFailed.swift in Sources */, D89CFFDD25B44468000E4683 /* ULAccountMismatchViewController.swift in Sources */, + 03E471C6293A2E95001A58AD /* CardPresentModalBuiltInReaderCheckingDeviceSupport.swift in Sources */, B687940C27699D420092BCA0 /* RefundFeesCalculationUseCase.swift in Sources */, 02EA6BF82435E80600FFF90A /* ImageDownloader.swift in Sources */, CECC758623D21AC200486676 /* AggregateOrderItem.swift in Sources */, @@ -10277,6 +10293,7 @@ 0262DA5B23A244830029AF30 /* Product+ShippingSettingsViewModels.swift in Sources */, 0216272B2379662C000208D2 /* DefaultProductFormTableViewModel.swift in Sources */, 45E9A6E424DAE1EA00A600E8 /* ProductReviewsViewController.swift in Sources */, + 03E471C4293A1F8D001A58AD /* BuiltInReaderConnectionAlertsProvider.swift in Sources */, D8736B7522F1FE1600A14A29 /* BadgeLabel.swift in Sources */, B5F8B7E02194759100DAB7E2 /* ReviewDetailsViewController.swift in Sources */, 262A098B2628C51D0033AD20 /* OrderAddOnListViewModel.swift in Sources */, @@ -10383,6 +10400,7 @@ CE24BCD8212F25D4001CD12E /* StorageOrder+Woo.swift in Sources */, DE2FE58A2925EAAE0018040A /* LoginJetpackSetupCoordinator.swift in Sources */, 02820F3422C257B700DE0D37 /* UITableView+HeaderFooterHelpers.swift in Sources */, + 03E471C0293A158D001A58AD /* CardReaderConnectionAlertsProviding.swift in Sources */, D8C2A291231BD0FD00F503E9 /* ReviewsDataSource.swift in Sources */, CE855366209BA6A700938BDC /* CustomerInfoTableViewCell.swift in Sources */, 02DAE7FC291B7B8B009342B7 /* DomainSelectorViewModel.swift in Sources */, @@ -10623,6 +10641,7 @@ 26E1BECA251BE5390096D0A1 /* RefundItemTableViewCell.swift in Sources */, DE7B479527A38B8F0018742E /* CouponDetailsViewModel.swift in Sources */, E16058F9285876E600E471D4 /* LeftImageTitleSubtitleTableViewCell.swift in Sources */, + 03E471C2293A1F6B001A58AD /* BluetoothReaderConnectionAlertsProvider.swift in Sources */, DEC2962926C20ECB005A056B /* CollapsibleView.swift in Sources */, AEDDDA0A25CA9C980077F9B2 /* AttributePickerViewController.swift in Sources */, 7E7C5F862719A93C00315B61 /* ProductCategoryViewModelBuilder.swift in Sources */, @@ -10784,6 +10803,7 @@ EECB7EE62864647F0028C888 /* ProductImagesProductIDUpdater.swift in Sources */, FE28F7182684EE6A004465C7 /* RoleErrorViewModel.swift in Sources */, 02C0CD2C23B5BC9600F880B1 /* DefaultImageService.swift in Sources */, + 03E471C8293A3076001A58AD /* CardPresentModalBuiltInConnectingToReader.swift in Sources */, CE0F17CF22A8105800964A63 /* ReadMoreTableViewCell.swift in Sources */, 02E3B62F2906322B007E0F13 /* AccountCreationFormFieldView.swift in Sources */, 03EF24FE28C0B356006A033E /* CardPresentPaymentsPlugin+CashOnDelivery.swift in Sources */,