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
4 changes: 4 additions & 0 deletions Hardware/Hardware.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
019A77CA2D88645100ABBB71 /* PaymentMethodType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019A77C92D88645100ABBB71 /* PaymentMethodType.swift */; };
028C39E028255CFE0007BA25 /* Models+Copiable.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028C39DF28255CFE0007BA25 /* Models+Copiable.generated.swift */; };
02B5147A28254ED300750B71 /* Codegen in Frameworks */ = {isa = PBXBuildFile; productRef = 02B5147928254ED300750B71 /* Codegen */; };
02FDAB102CEEF11F00B6C8AA /* PaymentChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FDAB0F2CEEF11D00B6C8AA /* PaymentChannel.swift */; };
Expand Down Expand Up @@ -159,6 +160,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
019A77C92D88645100ABBB71 /* PaymentMethodType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodType.swift; sourceTree = "<group>"; };
02351FF56149ADCD11338B19 /* Pods-SampleReceiptPrinter.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleReceiptPrinter.release.xcconfig"; path = "Target Support Files/Pods-SampleReceiptPrinter/Pods-SampleReceiptPrinter.release.xcconfig"; sourceTree = "<group>"; };
028C39DF28255CFE0007BA25 /* Models+Copiable.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Models+Copiable.generated.swift"; sourceTree = "<group>"; };
02FDAB0F2CEEF11D00B6C8AA /* PaymentChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentChannel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -476,6 +478,7 @@
02FDAB0F2CEEF11D00B6C8AA /* PaymentChannel.swift */,
D89B8F0125DDC7500001C726 /* PaymentIntentStatus.swift */,
D88FDB2325DD21B000CB0DBD /* PaymentIntentParameters.swift */,
019A77C92D88645100ABBB71 /* PaymentMethodType.swift */,
03CF78D627DF9BE500523706 /* RefundParameters.swift */,
D88FDB2025DD21AF00CB0DBD /* PaymentStatus.swift */,
D845BDBB262D980C00A3E40F /* CardPresentTransactionDetails.swift */,
Expand Down Expand Up @@ -843,6 +846,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
019A77CA2D88645100ABBB71 /* PaymentMethodType.swift in Sources */,
B9C4AB2327FDE133007008B8 /* Email.swift in Sources */,
D845BE59262ED84000A3E40F /* AirPrintReceiptPrinterService.swift in Sources */,
317975C2274EBC1F004357B1 /* DeclineReason+Stripe.swift in Sources */,
Expand Down
4 changes: 2 additions & 2 deletions Hardware/Hardware/CardReader/PaymentIntentParameters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public struct PaymentIntentParameters {
///
/// Can be `card_present`, `interac_present`.
///
public let paymentMethodTypes: [String]
public let paymentMethodTypes: [PaymentMethodType]

public init(amount: Decimal,
currency: String,
Expand All @@ -54,7 +54,7 @@ public struct PaymentIntentParameters {
receiptDescription: String? = nil,
statementDescription: String? = nil,
receiptEmail: String? = nil,
paymentMethodTypes: [String] = [],
paymentMethodTypes: [PaymentMethodType] = [],
metadata: [String: String]? = nil) {
self.amount = amount
self.currency = currency
Expand Down
7 changes: 7 additions & 0 deletions Hardware/Hardware/CardReader/PaymentMethodType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/// PaymentMethodType defines supported hardware payment types.
/// Mirrors supported types from StripeTerminal.PaymentMethodType https://stripe.dev/stripe-terminal-ios/docs/Enums/SCPPaymentMethodType.html
///
public enum PaymentMethodType {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: maybe we can add a comment potentially with a link to the Stripe doc, as it seems to mirror a subset of StripeTerminal.PaymentMethodType?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea. I will.

I borrowed the approach of CardReaderType and CardReaderType+Stripe, it's a similar type used in a similar way.

case cardPresent
case interacPresent
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public extension CardReaderDiscoveryMethod {
func toStripe() -> DiscoveryMethod {
switch self {
case .localMobile:
return .localMobile
return .tapToPay
case .bluetoothScan:
return .bluetoothScan
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ extension CardReaderType {
return .stripeM2
case .wisePad3:
return .wisepad3
case .appleBuiltIn:
case .tapToPay:
return appleBuiltIn
default:
return .other
Expand All @@ -29,7 +29,7 @@ extension CardReaderType {
case .wisepad3:
return .wisePad3
case .appleBuiltIn:
return .appleBuiltIn
return .tapToPay
case .other:
return nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,32 +34,32 @@ private extension Hardware.PaymentIntentParameters {

func build(_ builder: PaymentIntentParametersBuilder,
with cardReaderMetadata: CardReaderMetadata? = nil) throws -> StripeTerminal.PaymentIntentParameters? {
builder.setPaymentMethodTypes(paymentMethodTypes)
builder.setStripeDescription(receiptDescription)
_ = builder.setPaymentMethodTypes(paymentMethodTypes.map(\.stripeTerminalPaymentMethodType))
builder.setStripeDescription(receiptDescription)

/// Stripe allows the credit card statement descriptor to be nil, but not an empty string
/// https://stripe.dev/stripe-terminal-ios/docs/Classes/SCPPaymentIntentParameters.html#/c:objc(cs)SCPPaymentIntentParameters(py)statementDescriptor
builder.setStatementDescriptor(nil)
/// Stripe allows the credit card statement descriptor to be nil, but not an empty string
/// https://stripe.dev/stripe-terminal-ios/docs/Classes/SCPPaymentIntentParameters.html#/c:objc(cs)SCPPaymentIntentParameters(py)statementDescriptor
builder.setStatementDescriptor(nil)

let descriptor = statementDescription ?? ""
if !descriptor.isEmpty {
builder.setStatementDescriptor(descriptor)
}
let descriptor = statementDescription ?? ""
if !descriptor.isEmpty {
builder.setStatementDescriptor(descriptor)
}

if let applicationFee = applicationFee {
/// Stripe requires that "The amount must be provided as a boxed UInt in the currency's smallest unit."
/// Smallest-unit and UInt conversion is done in the same way as for the total amount, but that does not need to be boxed.
let applicationFeeForStripe = NSNumber(value: prepareAmountForStripe(applicationFee))
builder.setApplicationFeeAmount(applicationFeeForStripe)
}
if let applicationFee = applicationFee {
/// Stripe requires that "The amount must be provided as a boxed UInt in the currency's smallest unit."
/// Smallest-unit and UInt conversion is done in the same way as for the total amount, but that does not need to be boxed.
let applicationFeeForStripe = NSNumber(value: prepareAmountForStripe(applicationFee))
builder.setApplicationFeeAmount(applicationFeeForStripe)
}

builder.setReceiptEmail(receiptEmail)
builder.setReceiptEmail(receiptEmail)

let updatedMetadata = prepareMetadataForStripe(with: cardReaderMetadata)
builder.setMetadata(updatedMetadata)
let updatedMetadata = prepareMetadataForStripe(with: cardReaderMetadata)
builder.setMetadata(updatedMetadata)

// Return payment intent built configuration:
return try builder.build()
// Return payment intent built configuration:
return try builder.build()
}

/// Updates the existing PaymentIntentParameters metadata with our CardReader metadata, if any.
Expand All @@ -80,4 +80,16 @@ private extension Hardware.PaymentIntentParameters {
return updatedMetadata
}
}

private extension PaymentMethodType {
var stripeTerminalPaymentMethodType: StripeTerminal.PaymentMethodType {
switch self {
case .cardPresent:
return .cardPresent
case .interacPresent:
return .interacPresent
}
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,11 @@ extension StripeCardReaderService: CardReaderService {
throw error
}
case .localMobile:
let localMobileConfig = LocalMobileDiscoveryConfigurationBuilder()
let tapToPayConfig = TapToPayDiscoveryConfigurationBuilder()
do {
config = try localMobileConfig.setSimulated(shouldUseSimulatedCardReader).build()
config = try tapToPayConfig.setSimulated(shouldUseSimulatedCardReader).build()
} catch let error as UnderlyingError {
DDLogError("Failed to start LocalMobileDiscovery. Error:\(String(describing: error.failureReason))")
DDLogError("Failed to start TapToPayDiscovery. Error:\(String(describing: error.failureReason))")
throw error
} catch {
DDLogError("\(error)")
Expand All @@ -159,8 +159,6 @@ extension StripeCardReaderService: CardReaderService {
throw CardReaderServiceError.bluetoothDenied
}

Terminal.shared.delegate = self

// We're now ready to start discovery, but first we'll check that we're not starting or canceling
// another discovery process.
// If we can't grab a lock quickly, let's fail rather than wait indefinitely
Expand Down Expand Up @@ -198,7 +196,7 @@ extension StripeCardReaderService: CardReaderService {
// as the simulator won't have Bluetooth available.
// If we're using Tap to Pay on iPhone, bluetooth is not required.
private func shouldSkipBluetoothCheck(discoveryConfiguration: DiscoveryConfiguration) -> Bool {
shouldUseSimulatedCardReader || discoveryConfiguration.discoveryMethod == .localMobile
shouldUseSimulatedCardReader || discoveryConfiguration.discoveryMethod == .tapToPay
}

public func cancelDiscovery() -> Future <Void, Error> {
Expand Down Expand Up @@ -273,7 +271,7 @@ extension StripeCardReaderService: CardReaderService {
/// https://stripe.dev/stripe-terminal-ios/docs/Classes/SCPTerminal.html#/c:objc(cs)SCPTerminal(im)disconnectReader:
/// The completion block for disconnect, apparently, is called when the SDK has not really transitioned to an idle state.
/// Clients might need to dispatch operations that rely on this completion block to start a second operation on the card reader.
/// (for example, starting a `localMobile` connection after a BlueTooth reader has been disconnected)
/// (for example, starting a `tapToPay` connection after a BlueTooth reader has been disconnected)
Terminal.shared.disconnectReader { error in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if let error = error {
Expand Down Expand Up @@ -453,7 +451,7 @@ extension StripeCardReaderService: CardReaderService {

connectionAttemptInvalidated = false
switch stripeReader.deviceType {
case .appleBuiltIn:
case .tapToPay:
return getLocalMobileConfiguration(stripeReader, options: options).flatMap { configuration in
self.connect(stripeReader, configuration: configuration)
}
Expand All @@ -480,7 +478,7 @@ extension StripeCardReaderService: CardReaderService {
self.readerLocationProvider?.fetchDefaultLocationID { result in
switch result {
case .success(let locationId):
let buildConfig = BluetoothConnectionConfigurationBuilder(locationId: locationId)
let buildConfig = BluetoothConnectionConfigurationBuilder(delegate: self, locationId: locationId)
do {
let config = try buildConfig.build()
return promise(.success(config))
Expand All @@ -497,7 +495,7 @@ extension StripeCardReaderService: CardReaderService {
}

private func getLocalMobileConfiguration(_ reader: StripeTerminal.Reader,
options: CardReaderConnectionOptions?) -> Future<LocalMobileConnectionConfiguration, Error> {
options: CardReaderConnectionOptions?) -> Future<TapToPayConnectionConfiguration, Error> {
return Future() { [weak self] promise in
guard let self = self else {
promise(.failure(CardReaderServiceError.connection()))
Expand All @@ -509,7 +507,7 @@ extension StripeCardReaderService: CardReaderService {
self.readerLocationProvider?.fetchDefaultLocationID { result in
switch result {
case .success(let locationId):
let localMobileConfig = LocalMobileConnectionConfigurationBuilder(locationId: locationId)
let localMobileConfig = TapToPayConnectionConfigurationBuilder(delegate: self, locationId: locationId)
localMobileConfig.setMerchantDisplayName(nil)
localMobileConfig.setOnBehalfOf(nil)
localMobileConfig.setTosAcceptancePermitted(options?.builtInOptions?.termsOfServiceAcceptancePermitted ?? true)
Expand Down Expand Up @@ -540,7 +538,7 @@ extension StripeCardReaderService: CardReaderService {
return
}

Terminal.shared.connectBluetoothReader(reader, delegate: self, connectionConfig: configuration) { [weak self] (reader, error) in
Terminal.shared.connectReader(reader, connectionConfig: configuration) { [weak self] (reader, error) in
guard let self = self else {
promise(.failure(CardReaderServiceError.connection()))
return
Expand Down Expand Up @@ -573,14 +571,14 @@ extension StripeCardReaderService: CardReaderService {
}
}

public func connect(_ reader: StripeTerminal.Reader, configuration: LocalMobileConnectionConfiguration) -> Future <CardReader, Error> {
public func connect(_ reader: StripeTerminal.Reader, configuration: TapToPayConnectionConfiguration) -> Future <CardReader, Error> {
return Future { [weak self] promise in
guard let self = self else {
promise(.failure(CardReaderServiceError.connection()))
return
}

Terminal.shared.connectLocalMobileReader(reader, delegate: self, connectionConfig: configuration) { [weak self] (reader, error) in
Terminal.shared.connectReader(reader, connectionConfig: configuration) { [weak self] (reader, error) in
guard let self = self else {
promise(.failure(CardReaderServiceError.connection()))
return
Expand Down Expand Up @@ -873,7 +871,7 @@ extension StripeCardReaderService: DiscoveryDelegate {


// MARK: - ReaderDisplayDelegate.
extension StripeCardReaderService: BluetoothReaderDelegate {
extension StripeCardReaderService: MobileReaderDelegate {
public func reader(_ reader: Reader, didReportAvailableUpdate update: ReaderSoftwareUpdate) {
softwareUpdateSubject.send(.available)
}
Expand Down Expand Up @@ -960,29 +958,33 @@ extension StripeCardReaderService: BluetoothReaderDelegate {

connectedReadersSubject.send([connectedReaderWithUpdatedBatteryLevel])
}

public func reader(_ reader: Reader, didDisconnect reason: DisconnectReason) {
connectedReadersSubject.send([])
Copy link
Contributor

Choose a reason for hiding this comment

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

Just wondering, does TTP delegate have a similar method to didDisconnect in the bluetooth reader delegate now that TerminalDelegate is removed? Or TTP doesn't really have a "disconnecting" state?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes.

Both TapToPayReaderDelegate and MobileReaderDelegate have didDisconnect. These delegates are both SCPReaderDelegate, which defines didDisconnect.

It means that when we set delegate to self, this method is shared by both TTP and Reader delegates.

BluetoothConnectionConfigurationBuilder(delegate: self, locationId: locationId)
TapToPayConnectionConfigurationBuilder(delegate: self, locationId: locationId)

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for the explanation, makes sense now. I wasn't aware both delegates extend the same base delegate.

}
}

extension StripeCardReaderService: LocalMobileReaderDelegate {
public func localMobileReader(_ reader: Reader, didRequestReaderInput inputOptions: ReaderInputOptions = []) {
extension StripeCardReaderService: TapToPayReaderDelegate {
public func tapToPayReader(_ reader: Reader, didRequestReaderInput inputOptions: ReaderInputOptions = []) {
sendReaderEvent(CardReaderEvent.make(stripeReaderInputOptions: inputOptions))
}

public func localMobileReader(_ reader: Reader, didRequestReaderDisplayMessage displayMessage: ReaderDisplayMessage) {
public func tapToPayReader(_ reader: Reader, didRequestReaderDisplayMessage displayMessage: ReaderDisplayMessage) {
sendReaderEvent(CardReaderEvent.make(displayMessage: displayMessage))
}


// TODO: use a specific `deviceSetup` in these three functions instead of reusing the softwareUpdateSubject
// https://github.com/woocommerce/woocommerce-ios/issues/8088
public func localMobileReader(_ reader: Reader, didStartInstallingUpdate update: ReaderSoftwareUpdate, cancelable: Cancelable?) {
public func tapToPayReader(_ reader: Reader, didStartInstallingUpdate update: ReaderSoftwareUpdate, cancelable: Cancelable?) {
softwareUpdateSubject.send(.started(cancelable: cancelable.map(StripeCancelable.init(cancelable:))))
}

public func localMobileReader(_ reader: Reader, didReportReaderSoftwareUpdateProgress progress: Float) {
public func tapToPayReader(_ reader: Reader, didReportReaderSoftwareUpdateProgress progress: Float) {
softwareUpdateSubject.send(.installing(progress: progress))
}

public func localMobileReader(_ reader: Reader, didFinishInstallingUpdate update: ReaderSoftwareUpdate?, error: Error?) {
public func tapToPayReader(_ reader: Reader, didFinishInstallingUpdate update: ReaderSoftwareUpdate?, error: Error?) {
if let error = error {
let underlyingError = Self.logAndDecodeError(error)
softwareUpdateSubject.send(.failed(
Expand All @@ -1002,18 +1004,11 @@ extension StripeCardReaderService: LocalMobileReaderDelegate {
}
}

public func localMobileReaderDidAcceptTermsOfService(_ reader: Reader) {
public func tapToPayReaderDidAcceptTermsOfService(_ reader: Reader) {
builtInCardReaderAcceptToSSubject.send(())
}
}

// MARK: - Terminal delegate
extension StripeCardReaderService: TerminalDelegate {
public func terminal(_ terminal: Terminal, didReportUnexpectedReaderDisconnect reader: Reader) {
connectedReadersSubject.send([])
}
}

// MARK: - Reader events
private extension StripeCardReaderService {
func sendReaderEvent(_ event: CardReaderEvent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,23 +92,23 @@ extension UnderlyingError {
self = .processorAPIError
case .passcodeNotEnabled:
self = .passcodeNotEnabled
case .appleBuiltInReaderTOSAcceptanceRequiresiCloudSignIn:
case .tapToPayReaderTOSAcceptanceRequiresiCloudSignIn:
self = .appleBuiltInReaderTOSAcceptanceRequiresiCloudSignIn
case .nfcDisabled:
self = .nfcDisabled
case .appleBuiltInReaderFailedToPrepare:
case .tapToPayReaderFailedToPrepare:
self = .appleBuiltInReaderFailedToPrepare
case .appleBuiltInReaderTOSAcceptanceCanceled:
case .tapToPayReaderTOSAcceptanceCanceled:
self = .appleBuiltInReaderTOSAcceptanceCanceled
case .appleBuiltInReaderTOSNotYetAccepted:
case .tapToPayReaderTOSNotYetAccepted:
self = .appleBuiltInReaderTOSNotYetAccepted
case .appleBuiltInReaderTOSAcceptanceFailed:
case .tapToPayReaderTOSAcceptanceFailed:
self = .appleBuiltInReaderTOSAcceptanceFailed
case .appleBuiltInReaderMerchantBlocked:
case .tapToPayReaderMerchantBlocked:
self = .appleBuiltInReaderMerchantBlocked
case .appleBuiltInReaderInvalidMerchant:
case .tapToPayReaderInvalidMerchant:
self = .appleBuiltInReaderInvalidMerchant
case .appleBuiltInReaderDeviceBanned:
case .tapToPayReaderDeviceBanned:
self = .appleBuiltInReaderDeviceBanned
case .unsupportedMobileDeviceConfiguration:
self = .unsupportedMobileDeviceConfiguration
Expand Down Expand Up @@ -170,7 +170,7 @@ extension UnderlyingError {
self = .internetConnectTimeOut
case .bluetoothReconnectStarted:
self = .bluetoothReconnectStarted
case .appleBuiltInReaderAccountDeactivated:
case .tapToPayReaderAccountDeactivated:
self = .appleBuiltInReaderAccountDeactivated
case .readerMissingEncryptionKeys:
self = .readerMissingEncryptionKeys
Expand Down
Loading
Loading