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
2 changes: 2 additions & 0 deletions Experiments/Experiments/DefaultFeatureFlagService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
return buildConfig == .localDeveloper || buildConfig == .alpha
case .analyticsHub:
return buildConfig == .localDeveloper || buildConfig == .alpha
case .tapToPayOnIPhone:
return buildConfig == .localDeveloper
default:
return true
}
Expand Down
4 changes: 4 additions & 0 deletions Experiments/Experiments/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ public enum FeatureFlag: Int {
///
case inAppPurchases

/// Enables Tap to Pay on iPhone flow in In-Person Payments, on eligible devices.
///
case tapToPayOnIPhone

/// Store creation MVP.
///
case storeCreationMVP
Expand Down
4 changes: 4 additions & 0 deletions Hardware/Hardware.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
02B5147A28254ED300750B71 /* Codegen in Frameworks */ = {isa = PBXBuildFile; productRef = 02B5147928254ED300750B71 /* Codegen */; };
030338102705F7D400764131 /* ReceiptTotalLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0303380F2705F7D400764131 /* ReceiptTotalLine.swift */; };
035DBA3929251ED6003E5125 /* CardReaderInputOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035DBA3829251ED6003E5125 /* CardReaderInputOptions.swift */; };
035DBA41292BBEB2003E5125 /* CardReaderDiscoveryMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035DBA40292BBEB2003E5125 /* CardReaderDiscoveryMethod.swift */; };
039D948B2760C0660044EF38 /* NoOpCardReaderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039D948A2760C0660044EF38 /* NoOpCardReaderService.swift */; };
03B440AA2754DFC400759429 /* UnderlyingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B440A92754DFC400759429 /* UnderlyingError.swift */; };
03CF78D327C6710C00523706 /* interac.svg in Resources */ = {isa = PBXBuildFile; fileRef = 03CF78D227C6710B00523706 /* interac.svg */; };
Expand Down Expand Up @@ -155,6 +156,7 @@
028C39DF28255CFE0007BA25 /* Models+Copiable.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Models+Copiable.generated.swift"; sourceTree = "<group>"; };
0303380F2705F7D400764131 /* ReceiptTotalLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptTotalLine.swift; sourceTree = "<group>"; };
035DBA3829251ED6003E5125 /* CardReaderInputOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderInputOptions.swift; sourceTree = "<group>"; };
035DBA40292BBEB2003E5125 /* CardReaderDiscoveryMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderDiscoveryMethod.swift; sourceTree = "<group>"; };
039D948A2760C0660044EF38 /* NoOpCardReaderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoOpCardReaderService.swift; sourceTree = "<group>"; };
03B440A92754DFC400759429 /* UnderlyingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnderlyingError.swift; sourceTree = "<group>"; };
03CF78D227C6710B00523706 /* interac.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = interac.svg; sourceTree = "<group>"; };
Expand Down Expand Up @@ -478,6 +480,7 @@
D845BDCB262D9B7700A3E40F /* CardBrand+Stripe.swift */,
D845BDC5262D9A4200A3E40F /* CardPresentDetails+Stripe.swift */,
D8DF5F4925DD9F7A008AFE25 /* CardReader+Stripe.swift */,
035DBA40292BBEB2003E5125 /* CardReaderDiscoveryMethod.swift */,
D865C61D261CE001006717B8 /* CardReaderEvent+Stripe.swift */,
D8DF5F4D25DD9F91008AFE25 /* CardReaderType+Stripe.swift */,
D89B8F1125DDCBCD0001C726 /* Charge+Stripe.swift */,
Expand Down Expand Up @@ -831,6 +834,7 @@
D89B8F0C25DDC9D30001C726 /* ChargeStatus.swift in Sources */,
035DBA3929251ED6003E5125 /* CardReaderInputOptions.swift in Sources */,
E140F61C2668CDC900FDB5FF /* Logging.swift in Sources */,
035DBA41292BBEB2003E5125 /* CardReaderDiscoveryMethod.swift in Sources */,
03CF78D727DF9BE600523706 /* RefundParameters.swift in Sources */,
03B440AA2754DFC400759429 /* UnderlyingError.swift in Sources */,
E1E125AC26EB582B0068A9B0 /* CardReaderSoftwareUpdateState.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion Hardware/Hardware/CardReader/CardReaderService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public protocol CardReaderService {

/// Starts the service.
/// That could imply, for example, that the reader discovery process starts
func start(_ configProvider: CardReaderConfigProvider) throws
func start(_ configProvider: CardReaderConfigProvider, discoveryMethod: CardReaderDiscoveryMethod) throws

/// Cancels the discovery process.
func cancelDiscovery() -> Future<Void, Error>
Expand Down
4 changes: 4 additions & 0 deletions Hardware/Hardware/CardReader/CardReaderType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ public enum CardReaderType: CaseIterable {
case stripeM2
/// BBPOS WisePad 3
case wisepad3
/// Tap on Mobile: Apple built in reader
case appleBuiltIn
/// Other
case other
}
Expand All @@ -26,6 +28,8 @@ extension CardReaderType {
return "STRIPE_M2"
case .wisepad3:
return "WISEPAD_3"
case .appleBuiltIn:
return "BUILT_IN"
default:
return "UNKNOWN"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#if !targetEnvironment(macCatalyst)
import Foundation
import StripeTerminal

public enum CardReaderDiscoveryMethod {
case localMobile
case bluetoothProximity

func toStripe() -> DiscoveryMethod {
switch self {
case .localMobile:
return .localMobile
case .bluetoothProximity:
return .bluetoothProximity
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ extension CardReaderType {
return .stripeM2
case .wisePad3:
return .wisepad3
case .appleBuiltIn:
return appleBuiltIn
default:
return .other
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public struct NoOpCardReaderService: CardReaderService {

/// Starts the service.
/// That could imply, for example, that the reader discovery process starts
public func start(_ configProvider: CardReaderConfigProvider) throws {
public func start(_ configProvider: CardReaderConfigProvider, discoveryMethod: CardReaderDiscoveryMethod) throws {
// no-op
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ extension StripeCardReaderService: CardReaderService {

// MARK: - CardReaderService conformance. Commands

public func start(_ configProvider: CardReaderConfigProvider) throws {
public func start(_ configProvider: CardReaderConfigProvider,
discoveryMethod: CardReaderDiscoveryMethod) throws {
setConfigProvider(configProvider)

Terminal.setLogListener { message in
Expand All @@ -85,13 +86,12 @@ extension StripeCardReaderService: CardReaderService {
}

let config = DiscoveryConfiguration(
discoveryMethod: .bluetoothScan,
discoveryMethod: discoveryMethod.toStripe(),
simulated: shouldUseSimulatedCardReader
)

// If we're using the simulated reader, we don't want to check for Bluetooth permissions
// as the simulator won't have Bluetooth available.
guard shouldUseSimulatedCardReader || CBCentralManager.authorization != .denied else {
guard shouldSkipBluetoothCheck(discoveryConfiguration: config) ||
CBCentralManager.authorization != .denied else {
throw CardReaderServiceError.bluetoothDenied
}

Expand Down Expand Up @@ -123,6 +123,14 @@ extension StripeCardReaderService: CardReaderService {
})
}


// If we're using the simulated reader, we don't want to check for Bluetooth permissions
// as the simulator won't have Bluetooth available.
// If we're using the built-in reader, bluetooth is not required.
private func shouldSkipBluetoothCheck(discoveryConfiguration: DiscoveryConfiguration) -> Bool {
shouldUseSimulatedCardReader || discoveryConfiguration.discoveryMethod == .localMobile
}

public func cancelDiscovery() -> Future <Void, Error> {
Future { [weak self] promise in
/**
Expand Down Expand Up @@ -311,9 +319,20 @@ extension StripeCardReaderService: CardReaderService {
}.eraseToAnyPublisher()
}

return getBluetoothConfiguration(stripeReader).flatMap { configuration in
self.connect(stripeReader, configuration: configuration)
}.eraseToAnyPublisher()
switch stripeReader.deviceType {
case .appleBuiltIn:
return getLocalMobileConfiguration(stripeReader).flatMap { configuration in
self.connect(stripeReader, configuration: configuration)
}
.share()
.eraseToAnyPublisher()
default:
return getBluetoothConfiguration(stripeReader).flatMap { configuration in
self.connect(stripeReader, configuration: configuration)
}
.share()
.eraseToAnyPublisher()
}
}

private func getBluetoothConfiguration(_ reader: StripeTerminal.Reader) -> Future<BluetoothConnectionConfiguration, Error> {
Expand All @@ -337,6 +356,27 @@ extension StripeCardReaderService: CardReaderService {
}
}

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

// TODO - If we've recently connected to this reader, use the cached locationId from the
// Terminal SDK instead of making this fetch. See #5116 and #5087
self.readerLocationProvider?.fetchDefaultLocationID { result in
switch result {
case .success(let locationId):
return promise(.success(LocalMobileConnectionConfiguration(locationId: locationId)))
case .failure(let error):
let underlyingError = UnderlyingError(with: error)
return promise(.failure(CardReaderServiceError.connection(underlyingError: underlyingError)))
}
}
}
}

public func connect(_ reader: StripeTerminal.Reader, configuration: BluetoothConnectionConfiguration) -> Future <CardReader, Error> {
// Keep a copy of the battery level in case the connection fails due to low battery
// If that happens, the reader object won't be accessible anymore, and we want to show
Expand Down Expand Up @@ -376,6 +416,40 @@ extension StripeCardReaderService: CardReaderService {
}
}

public func connect(_ reader: StripeTerminal.Reader, configuration: LocalMobileConnectionConfiguration) -> 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
guard let self = self else {
promise(.failure(CardReaderServiceError.connection()))
return
}
// Clear cached readers, as per Stripe's documentation.
self.discoveredStripeReadersCache.clear()

if let error = error {
let underlyingError = UnderlyingError(with: error)
// Starting with StripeTerminal 2.0, required software updates happen transparently on connection
// Any error related to that will be reported here, but we don't want to treat it as a connection error
let serviceError: CardReaderServiceError = underlyingError.isSoftwareUpdateError ?
.softwareUpdate(underlyingError: underlyingError, batteryLevel: nil) :
.connection(underlyingError: underlyingError)
promise(.failure(serviceError))
}

if let reader = reader {
self.connectedReadersSubject.send([CardReader(reader: reader)])
self.switchStatusToIdle()
promise(.success(CardReader(reader: reader)))
}
}
}
}

public func installUpdate() -> Void {
Terminal.shared.installAvailableUpdate()
}
Expand Down Expand Up @@ -443,6 +517,7 @@ private extension StripeCardReaderService {

if underlyingError == .commandCancelled {
DDLogWarn("💳 Warning: collect payment error cancelled. We actively ignore this error \(error)")
promise(.failure(CardReaderServiceError.paymentCancellation(underlyingError: underlyingError)))
}

}
Expand Down Expand Up @@ -696,6 +771,46 @@ extension StripeCardReaderService: BluetoothReaderDelegate {
}
}

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

public func localMobileReader(_ 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?) {
softwareUpdateSubject.send(.started(cancelable: cancelable.map(StripeCancelable.init(cancelable:))))
}

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

public func localMobileReader(_ reader: Reader, didFinishInstallingUpdate update: ReaderSoftwareUpdate?, error: Error?) {
if let error = error {
softwareUpdateSubject.send(.failed(
error: CardReaderServiceError.softwareUpdate(underlyingError: UnderlyingError(with: error),
batteryLevel: reader.batteryLevel?.doubleValue))
)
if let requiredDate = update?.requiredAt,
requiredDate > Date() {
softwareUpdateSubject.send(.available)
} else {
softwareUpdateSubject.send(.none)
}
} else {
softwareUpdateSubject.send(.completed)
connectedReadersSubject.send([CardReader(reader: reader)])
softwareUpdateSubject.send(.none)
}
}
}

// MARK: - Terminal delegate
extension StripeCardReaderService: TerminalDelegate {
public func terminal(_ terminal: Terminal, didReportUnexpectedReaderDisconnect reader: Reader) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ extension Storage.GeneralAppSettings {
isProductSKUInputScannerSwitchEnabled: CopiableProp<Bool> = .copy,
isCouponManagementSwitchEnabled: CopiableProp<Bool> = .copy,
isInAppPurchasesSwitchEnabled: CopiableProp<Bool> = .copy,
isTapToPayOnIPhoneSwitchEnabled: CopiableProp<Bool> = .copy,
knownCardReaders: CopiableProp<[String]> = .copy,
lastEligibilityErrorInfo: NullableCopiableProp<EligibilityErrorInfo> = .copy,
lastJetpackBenefitsBannerDismissedTime: NullableCopiableProp<Date> = .copy,
Expand All @@ -38,6 +39,7 @@ extension Storage.GeneralAppSettings {
let isProductSKUInputScannerSwitchEnabled = isProductSKUInputScannerSwitchEnabled ?? self.isProductSKUInputScannerSwitchEnabled
let isCouponManagementSwitchEnabled = isCouponManagementSwitchEnabled ?? self.isCouponManagementSwitchEnabled
let isInAppPurchasesSwitchEnabled = isInAppPurchasesSwitchEnabled ?? self.isInAppPurchasesSwitchEnabled
let isTapToPayOnIPhoneSwitchEnabled = isTapToPayOnIPhoneSwitchEnabled ?? self.isTapToPayOnIPhoneSwitchEnabled
let knownCardReaders = knownCardReaders ?? self.knownCardReaders
let lastEligibilityErrorInfo = lastEligibilityErrorInfo ?? self.lastEligibilityErrorInfo
let lastJetpackBenefitsBannerDismissedTime = lastJetpackBenefitsBannerDismissedTime ?? self.lastJetpackBenefitsBannerDismissedTime
Expand All @@ -50,6 +52,7 @@ extension Storage.GeneralAppSettings {
isProductSKUInputScannerSwitchEnabled: isProductSKUInputScannerSwitchEnabled,
isCouponManagementSwitchEnabled: isCouponManagementSwitchEnabled,
isInAppPurchasesSwitchEnabled: isInAppPurchasesSwitchEnabled,
isTapToPayOnIPhoneSwitchEnabled: isTapToPayOnIPhoneSwitchEnabled,
knownCardReaders: knownCardReaders,
lastEligibilityErrorInfo: lastEligibilityErrorInfo,
lastJetpackBenefitsBannerDismissedTime: lastJetpackBenefitsBannerDismissedTime,
Expand Down
Loading