Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ extension StripeCardReaderService: CardReaderService {

self?.internalError(error)
discoveryLock.unlock()
promise(.failure(error))
let underlyingError = UnderlyingError(with: error)
promise(.failure(CardReaderServiceError.discovery(underlyingError: underlyingError)))
}
}
}
Expand Down Expand Up @@ -838,7 +839,10 @@ private extension StripeCardReaderService {

func resetDiscoveredReadersSubject(error: Error? = nil) {
if let error = error {
discoveredReadersSubject.send(completion: .failure(error))
let underlyingError = UnderlyingError(with: error)
discoveredReadersSubject.send(completion:
.failure(CardReaderServiceError.discovery(underlyingError: underlyingError))
)
}
discoveredReadersSubject.send(completion: .finished)
discoveredReadersSubject = CurrentValueSubject<[CardReader], Error>([])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,36 @@ extension UnderlyingError {
self = .readerSessionExpired
case ErrorCode.Code.stripeAPIError.rawValue:
self = .processorAPIError
case ErrorCode.Code.passcodeNotEnabled.rawValue:
self = .passcodeNotEnabled
case ErrorCode.Code.appleBuiltInReaderTOSAcceptanceRequiresiCloudSignIn.rawValue:
self = .appleBuiltInReaderTOSAcceptanceRequiresiCloudSignIn
case ErrorCode.Code.nfcDisabled.rawValue:
self = .nfcDisabled
case ErrorCode.Code.appleBuiltInReaderFailedToPrepare.rawValue:
self = .appleBuiltInReaderFailedToPrepare
case ErrorCode.Code.appleBuiltInReaderTOSAcceptanceCanceled.rawValue:
self = .appleBuiltInReaderTOSAcceptanceCanceled
case ErrorCode.Code.appleBuiltInReaderTOSNotYetAccepted.rawValue:
self = .appleBuiltInReaderTOSNotYetAccepted
case ErrorCode.Code.appleBuiltInReaderTOSAcceptanceFailed.rawValue:
self = .appleBuiltInReaderTOSAcceptanceFailed
case ErrorCode.Code.appleBuiltInReaderMerchantBlocked.rawValue:
self = .appleBuiltInReaderMerchantBlocked
case ErrorCode.Code.appleBuiltInReaderInvalidMerchant.rawValue:
self = .appleBuiltInReaderInvalidMerchant
case ErrorCode.Code.appleBuiltInReaderDeviceBanned.rawValue:
self = .appleBuiltInReaderDeviceBanned
case ErrorCode.Code.unsupportedMobileDeviceConfiguration.rawValue:
self = .unsupportedMobileDeviceConfiguration
case ErrorCode.Code.readerNotAccessibleInBackground.rawValue:
self = .readerNotAccessibleInBackground
case ErrorCode.Code.commandNotAllowedDuringCall.rawValue:
self = .commandNotAllowedDuringCall
case ErrorCode.Code.invalidAmount.rawValue:
self = .invalidAmount
case ErrorCode.Code.invalidCurrency.rawValue:
self = .invalidCurrency
default:
return nil
}
Expand Down
121 changes: 121 additions & 0 deletions Hardware/Hardware/CardReader/UnderlyingError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,72 @@ public enum UnderlyingError: Error, Equatable {

/// There was no refund in progress to cancel
case noRefundInProgress

// MARK: - Built-in reader related errors

/// The device must have a passcode in order to use the built-in reader
/// https://stripe.dev/stripe-terminal-ios/docs/Enums/SCPError.html#/c:@E@SCPError@SCPErrorPasscodeNotEnabled
case passcodeNotEnabled

/// The phone must have a signed-in iCloud account in order to accept the TOS for the built in reader.
/// The signed-in account does not need to be the one used to connect the reader.
/// https://stripe.dev/stripe-terminal-ios/docs/Enums/SCPError.html#/c:@E@SCPError@SCPErrorAppleBuiltInReaderTOSAcceptanceRequiresiCloudSignIn
case appleBuiltInReaderTOSAcceptanceRequiresiCloudSignIn

/// NFC is disabled on the device. This could be a permissions issue, in particular due to a device management profile.
/// It's unlikely that the user can directly correct this issue
/// https://stripe.dev/stripe-terminal-ios/docs/Enums/SCPError.html#/c:@E@SCPError@SCPErrorNFCDisabled
case nfcDisabled

/// Preparing the built-in reader failed. This is a retriable error
/// https://stripe.dev/stripe-terminal-ios/docs/Enums/SCPError.html#/c:@E@SCPError@SCPErrorAppleBuiltInReaderFailedToPrepare
case appleBuiltInReaderFailedToPrepare

/// The user cancelled the built-in reader Terms of Service acceptance
/// https://stripe.dev/stripe-terminal-ios/docs/Enums/SCPError.html#/c:@E@SCPError@SCPErrorAppleBuiltInReaderTOSAcceptanceCanceled
case appleBuiltInReaderTOSAcceptanceCanceled

/// The built-in reader Terms of Service have not been accepted. This error is retriable
/// https://stripe.dev/stripe-terminal-ios/docs/Enums/SCPError.html#/c:@E@SCPError@SCPErrorAppleBuiltInReaderTOSNotYetAccepted
case appleBuiltInReaderTOSNotYetAccepted

/// The built-in reader Terms of Service could not be accepted. This may indicate an issue with the Apple ID used.
/// https://stripe.dev/stripe-terminal-ios/docs/Enums/SCPError.html#/c:@E@SCPError@SCPErrorAppleBuiltInReaderTOSAcceptanceFailed
case appleBuiltInReaderTOSAcceptanceFailed

/// This (Stripe) merchant account cannot be used with the built-in reader as it has been blocked
/// https://stripe.dev/stripe-terminal-ios/docs/Enums/SCPError.html#/c:@E@SCPError@SCPErrorAppleBuiltInReaderMerchantBlocked
case appleBuiltInReaderMerchantBlocked

/// The merchant account is invalid and cannot be used with the built-in reader
/// https://stripe.dev/stripe-terminal-ios/docs/Enums/SCPError.html#/c:@E@SCPError@SCPErrorAppleBuiltInReaderInvalidMerchant
case appleBuiltInReaderInvalidMerchant

/// The built-in reader on this device cannot be used because it has been banned
/// https://stripe.dev/stripe-terminal-ios/docs/Enums/SCPError.html#/c:@E@SCPError@SCPErrorAppleBuiltInReaderDeviceBanned
case appleBuiltInReaderDeviceBanned

/// The device does not meet the minimum requirements for using the built-in reader
/// https://stripe.dev/stripe-terminal-ios/docs/Enums/SCPError.html#/c:@E@SCPError@SCPErrorUnsupportedMobileDeviceConfiguration
case unsupportedMobileDeviceConfiguration

/// The built-in reader cannot be used while the app is in the background
/// https://stripe.dev/stripe-terminal-ios/docs/Enums/SCPError.html#/c:@E@SCPError@SCPErrorReaderNotAccessibleInBackground
case readerNotAccessibleInBackground

/// The built-in reader cannot be used during a phone call
/// https://stripe.dev/stripe-terminal-ios/docs/Enums/SCPError.html#/c:@E@SCPError@SCPErrorCommandNotAllowedDuringCall
case commandNotAllowedDuringCall

/// The amount charged was not supported by the reader.
/// (This may be a different amount than the minimum for a payment with Stripe. There is a maximum too.)
/// https://stripe.dev/stripe-terminal-ios/docs/Enums/SCPError.html#/c:@E@SCPError@SCPErrorInvalidAmount
case invalidAmount

/// The currency used was not supported by the reader.
/// The reader may support a different set of currencies than WCPay or Stripe.
/// https://stripe.dev/stripe-terminal-ios/docs/Enums/SCPError.html#/c:@E@SCPError@SCPErrorInvalidCurrency
case invalidCurrency
}

extension UnderlyingError {
Expand Down Expand Up @@ -328,6 +394,61 @@ extension UnderlyingError: LocalizedError {
return NSLocalizedString("Sorry, this refund could not be canceled",
comment: "Error message shown when a refund could not be canceled (likely because " +
"it had already completed)")

// MARK: - Built-in reader errors
case .passcodeNotEnabled:
return NSLocalizedString("Your device needs a lock screen passcode set to use the built-in card reader",
comment: "Error message shown when the built-in reader cannot be used because " +
"the device does not have a passcode set.")
case .appleBuiltInReaderTOSAcceptanceRequiresiCloudSignIn:
return NSLocalizedString("Please sign in to iCloud on this device, so you can use the built-in card reader",
comment: "Error message shown when the built-in reader cannot be used because " +
"the device is not signed in to iCloud.")
case .nfcDisabled:
return NSLocalizedString("The app could not enable the card reader, because the NFC chip is disabled. " +
"Please contact support for more details.",
comment: "Error message shown when the built-in reader cannot be used because " +
"the device's NFC chipset has been disabled by a device management policy.")
case .appleBuiltInReaderFailedToPrepare, .readerNotAccessibleInBackground:
return NSLocalizedString("There was an issue preparing the built in reader for payment – please try again.",
comment: "Error message shown when the built-in reader cannot be used because " +
"there was some issue with the connection. Retryable.")
case .appleBuiltInReaderTOSAcceptanceCanceled, .appleBuiltInReaderTOSNotYetAccepted:
return NSLocalizedString("Please try again, and accept Apple's Terms of Service, so you can use the " +
"built-in card reader",
comment: "Error message shown when the built-in reader cannot be used because " +
"the merchant cancelled or did not complete the Terms of Service acceptance flow")
case .appleBuiltInReaderTOSAcceptanceFailed:
return NSLocalizedString("Please check your Apple ID is valid, and then try again. A valid Apple ID is " +
"required to accept Apple's Terms of Service",
comment: "Error message shown when the built-in reader cannot be used because " +
"the Terms of Service acceptance flow failed, possibly due to issues with " +
"the Apple ID")
case .appleBuiltInReaderMerchantBlocked, .appleBuiltInReaderInvalidMerchant, .appleBuiltInReaderDeviceBanned:
return NSLocalizedString("Please contact support – there was an issue connecting to the built-in reader",
comment: "Error message shown when the built-in reader cannot be used because " +
"there is an issue with the merchant account or device")
case .unsupportedMobileDeviceConfiguration:
return NSLocalizedString("Please check that your phone meets these requirements: " +
"iPhone XS or newer running iOS 16.0 or above. Contact support if this error " +
"shows on a supported device.",
comment: "Error message shown when the built-in reader cannot be used because " +
"the device does not meet minimum requirements.")
case .commandNotAllowedDuringCall:
return NSLocalizedString("The built-in reader cannot be used during a phone call. Please try again after " +
"you finish your call",
comment: "Error message shown when the built-in reader cannot be used because " +
"there is a call in progress")
case .invalidAmount:
return NSLocalizedString("The amount is not supported by the built in reader – please try a hardware " +
"reader or another payment method.",
comment: "Error message shown when the built-in reader cannot be used because " +
"the amount for payment is not supported by the built in reader.")
case .invalidCurrency:
return NSLocalizedString("The currency is not supported by the built in reader – please try a hardware " +
"reader or another payment method.",
comment: "Error message shown when the built-in reader cannot be used because " +
"the currency for payment is not supported by the built in reader.")
}
}
}
60 changes: 60 additions & 0 deletions Hardware/HardwareTests/ErrorCodesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,66 @@ final class CardReaderServiceErrorTests: XCTestCase {
XCTAssertEqual(.processorAPIError, domainError(stripeCode: 9020))
}

func test_stripe_passcode_not_enabled_maps_to_expected_error() {
XCTAssertEqual(.passcodeNotEnabled, domainError(stripeCode: 2920))
}

func test_stripe_TOS_requires_iCloud_signin_maps_to_expected_error() {
XCTAssertEqual(.appleBuiltInReaderTOSAcceptanceRequiresiCloudSignIn, domainError(stripeCode: 2960))
}

func test_stripe_nfc_disabled_maps_to_expected_error() {
XCTAssertEqual(.nfcDisabled, domainError(stripeCode: 3100))
}

func test_stripe_built_in_reader_failed_to_prepare_maps_to_expected_error() {
XCTAssertEqual(.appleBuiltInReaderFailedToPrepare, domainError(stripeCode: 3910))
}

func test_stripe_TOS_acceptance_cancelled_maps_to_expected_error() {
XCTAssertEqual(.appleBuiltInReaderTOSAcceptanceCanceled, domainError(stripeCode: 2970))
}

func test_stripe_TOS_not_yet_accepted_maps_to_expected_error() {
XCTAssertEqual(.appleBuiltInReaderTOSNotYetAccepted, domainError(stripeCode: 3930))
}

func test_stripe_TOS_acceptance_failed_maps_to_expected_error() {
XCTAssertEqual(.appleBuiltInReaderTOSAcceptanceFailed, domainError(stripeCode: 3940))
}

func test_stripe_merchant_blocked_maps_to_expected_error() {
XCTAssertEqual(.appleBuiltInReaderMerchantBlocked, domainError(stripeCode: 3950))
}

func test_stripe_invalid_merchant_maps_to_expected_error() {
XCTAssertEqual(.appleBuiltInReaderInvalidMerchant, domainError(stripeCode: 3960))
}

func test_stripe_device_banned_maps_to_expected_error() {
XCTAssertEqual(.appleBuiltInReaderDeviceBanned, domainError(stripeCode: 3920))
}

func test_stripe_unsupported_mobile_device_maps_to_expected_error() {
XCTAssertEqual(.unsupportedMobileDeviceConfiguration, domainError(stripeCode: 2910))
}

func test_stripe_not_accessible_in_background_maps_to_expected_error() {
XCTAssertEqual(.readerNotAccessibleInBackground, domainError(stripeCode: 3900))
}

func test_stripe_command_not_allowed_during_call_maps_to_expected_error() {
XCTAssertEqual(.commandNotAllowedDuringCall, domainError(stripeCode: 2930))
}

func test_stripe_invalid_amount_maps_to_expected_error() {
XCTAssertEqual(.invalidAmount, domainError(stripeCode: 2940))
}

func test_stripe_invalid_currency_maps_to_expected_error() {
XCTAssertEqual(.invalidCurrency, domainError(stripeCode: 2950))
}

func test_stripe_catch_all_error() {
// Any error code not mapped to an specific error will be
// mapped to `internalServiceError`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import UIKit

/// Modal presented when a firmware update is being installed
///
final class CardPresentModalBuiltInConfigurationProgress: CardPresentPaymentsModalViewModel, CardPresentModalProgressDisplaying {
/// Called when cancel button is tapped
private let cancelAction: (() -> Void)?

let textMode: PaymentsModalTextMode = .fullInfo
let actionsMode: PaymentsModalActionsMode

var topSubtitle: String? = nil

var progress: Float

let primaryButtonTitle: String? = nil

let secondaryButtonTitle: String? = Localization.cancel

let auxiliaryButtonTitle: String? = nil

var titleComplete: String

var titleInProgress: String

var messageComplete: String?

var messageInProgress: String?

var accessibilityLabel: String? {
Localization.title
}

init(progress: Float, cancel: (() -> Void)?) {
self.progress = progress
self.cancelAction = cancel

titleComplete = Localization.titleComplete
titleInProgress = Localization.title
messageComplete = Localization.messageComplete
messageInProgress = Localization.message
actionsMode = cancel != nil ? .secondaryOnlyAction : .none
}

func didTapPrimaryButton(in viewController: UIViewController?) {}

func didTapSecondaryButton(in viewController: UIViewController?) {
cancelAction?()
}

func didTapAuxiliaryButton(in viewController: UIViewController?) {}
}

private extension CardPresentModalBuiltInConfigurationProgress {
enum Localization {
static let title = NSLocalizedString(
"Configuring iPhone",
comment: "Dialog title that displays when iPhone configuration is being updated for use as a card reader"
)

static let titleComplete = NSLocalizedString(
"Configuration updated",
comment: "Dialog title that displays when a configuration update just finished installing"
)

static let message = NSLocalizedString(
"Your iPhone needs to be configured to collect payments.",
comment: "Label that displays when a configuration update is happening"
)

static let messageComplete = NSLocalizedString(
"Your phone will be ready to collect payments in a moment...",
comment: "Dialog message that displays when a configuration update just finished installing"
)

static let cancel = NSLocalizedString(
"Cancel",
comment: "Label for a cancel button"
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import UIKit

protocol CardPresentModalProgressDisplaying: CardPresentPaymentsModalViewModel {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part doesn't seem related to errors or mentioned in the description/testing, I'm wondering if this was intentionally included

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Github's being a bit strange.

Here's the first four commits from this PR:
CleanShot 2022-12-07 at 13 03 04@2x

And here's the three commits from the previous PR, which is merged to trunk:
CleanShot 2022-12-07 at 13 03 54@2x

So... they're all already in trunk with matching hashes, so I'm pretty sure this will sort itself out when I merge this PR... but I will check carefully! Thanks for the heads-up.

var progress: Float { get }
var isComplete: Bool { get }
var titleComplete: String { get }
var titleInProgress: String { get }
var messageComplete: String? { get }
var messageInProgress: String? { get }
}

extension CardPresentModalProgressDisplaying {
var image: UIImage {
.softwareUpdateProgress(progress: CGFloat(progress))
}

var isComplete: Bool {
progress == 1
}

var topTitle: String {
isComplete ? titleComplete : titleInProgress
}

var bottomTitle: String? {
String(format: CardPresentModalProgressDisplayingLocalization.percentComplete, 100 * progress)
}

var bottomSubtitle: String? {
isComplete ? messageComplete : messageInProgress
}
}

fileprivate enum CardPresentModalProgressDisplayingLocalization {
static let percentComplete = NSLocalizedString(
"%.0f%% complete",
comment: "Label that describes the completed progress of an update being installed (e.g. 15% complete). Keep the %.0f%% exactly as is"
)
}
Loading