Skip to content

Commit 54fb760

Browse files
committed
Merge branch 'trunk' into issue/8149-placeholder-card-data
2 parents b63885c + 848f8a7 commit 54fb760

File tree

79 files changed

+3400
-194
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+3400
-194
lines changed

Experiments/Experiments/ABTest.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ public enum ABTest: String, CaseIterable {
1616
///
1717
case abTestLoginWithWPComOnly = "woocommerceios_login_wpcom_only"
1818

19+
/// A/B test to measure the sign-in success rate when native Jetpack installation experience is enabled
20+
/// Experiment ref: pbxNRc-29W-p2
21+
///
22+
case nativeJetpackSetupFlow = "woocommerceios_login_jetpack_setup_flow"
23+
1924
/// A/B test for the Products Onboarding banner on the My Store dashboard.
2025
/// Experiment ref: pbxNRc-26F-p2
2126
case productsOnboardingBanner = "woocommerceios_products_onboarding_first_product_banner"
@@ -34,7 +39,7 @@ public enum ABTest: String, CaseIterable {
3439
/// When adding a new experiment, add it to the appropriate case depending on its context (logged-in or logged-out experience).
3540
public var context: ExperimentContext {
3641
switch self {
37-
case .productsOnboardingBanner, .productsOnboardingTemplateProducts:
42+
case .productsOnboardingBanner, .productsOnboardingTemplateProducts, .nativeJetpackSetupFlow:
3843
return .loggedIn
3944
case .aaTestLoggedOut, .abTestLoginWithWPComOnly:
4045
return .loggedOut
@@ -47,6 +52,7 @@ public enum ABTest: String, CaseIterable {
4752
public extension ABTest {
4853
/// Start the AB Testing platform if any experiment exists for the provided context
4954
///
55+
@MainActor
5056
static func start(for context: ExperimentContext) async {
5157
let experiments = ABTest.allCases.filter { $0.context == context }
5258

Experiments/Experiments/DefaultFeatureFlagService.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
5151
.performanceMonitoringUserInteraction:
5252
// Disabled by default to avoid costs spikes, unless in internal testing builds.
5353
return buildConfig == .alpha
54-
case .nativeJetpackSetupFlow:
55-
return buildConfig == .localDeveloper || buildConfig == .alpha
5654
case .analyticsHub:
5755
return buildConfig == .localDeveloper || buildConfig == .alpha
56+
case .tapToPayOnIPhone:
57+
return buildConfig == .localDeveloper
5858
default:
5959
return true
6060
}

Experiments/Experiments/FeatureFlag.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ public enum FeatureFlag: Int {
7070
///
7171
case inAppPurchases
7272

73+
/// Enables Tap to Pay on iPhone flow in In-Person Payments, on eligible devices.
74+
///
75+
case tapToPayOnIPhone
76+
7377
/// Store creation MVP.
7478
///
7579
case storeCreationMVP
@@ -126,11 +130,6 @@ public enum FeatureFlag: Int {
126130
/// - Note: The app will ignore this if `performanceMonitoring` is `false`.
127131
case performanceMonitoringViewController
128132

129-
/// Temporary feature flag for the native Jetpack setup flow.
130-
/// TODO-8075: replace this with A/B test.
131-
///
132-
case nativeJetpackSetupFlow
133-
134133
/// Temporary feature flag for the native Jetpack setup flow.
135134
///
136135
case analyticsHub

Hardware/Hardware.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
02B5147A28254ED300750B71 /* Codegen in Frameworks */ = {isa = PBXBuildFile; productRef = 02B5147928254ED300750B71 /* Codegen */; };
1212
030338102705F7D400764131 /* ReceiptTotalLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0303380F2705F7D400764131 /* ReceiptTotalLine.swift */; };
1313
035DBA3929251ED6003E5125 /* CardReaderInputOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035DBA3829251ED6003E5125 /* CardReaderInputOptions.swift */; };
14+
035DBA41292BBEB2003E5125 /* CardReaderDiscoveryMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035DBA40292BBEB2003E5125 /* CardReaderDiscoveryMethod.swift */; };
1415
039D948B2760C0660044EF38 /* NoOpCardReaderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039D948A2760C0660044EF38 /* NoOpCardReaderService.swift */; };
1516
03B440AA2754DFC400759429 /* UnderlyingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B440A92754DFC400759429 /* UnderlyingError.swift */; };
1617
03CF78D327C6710C00523706 /* interac.svg in Resources */ = {isa = PBXBuildFile; fileRef = 03CF78D227C6710B00523706 /* interac.svg */; };
@@ -155,6 +156,7 @@
155156
028C39DF28255CFE0007BA25 /* Models+Copiable.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Models+Copiable.generated.swift"; sourceTree = "<group>"; };
156157
0303380F2705F7D400764131 /* ReceiptTotalLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptTotalLine.swift; sourceTree = "<group>"; };
157158
035DBA3829251ED6003E5125 /* CardReaderInputOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderInputOptions.swift; sourceTree = "<group>"; };
159+
035DBA40292BBEB2003E5125 /* CardReaderDiscoveryMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderDiscoveryMethod.swift; sourceTree = "<group>"; };
158160
039D948A2760C0660044EF38 /* NoOpCardReaderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoOpCardReaderService.swift; sourceTree = "<group>"; };
159161
03B440A92754DFC400759429 /* UnderlyingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnderlyingError.swift; sourceTree = "<group>"; };
160162
03CF78D227C6710B00523706 /* interac.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = interac.svg; sourceTree = "<group>"; };
@@ -478,6 +480,7 @@
478480
D845BDCB262D9B7700A3E40F /* CardBrand+Stripe.swift */,
479481
D845BDC5262D9A4200A3E40F /* CardPresentDetails+Stripe.swift */,
480482
D8DF5F4925DD9F7A008AFE25 /* CardReader+Stripe.swift */,
483+
035DBA40292BBEB2003E5125 /* CardReaderDiscoveryMethod.swift */,
481484
D865C61D261CE001006717B8 /* CardReaderEvent+Stripe.swift */,
482485
D8DF5F4D25DD9F91008AFE25 /* CardReaderType+Stripe.swift */,
483486
D89B8F1125DDCBCD0001C726 /* Charge+Stripe.swift */,
@@ -831,6 +834,7 @@
831834
D89B8F0C25DDC9D30001C726 /* ChargeStatus.swift in Sources */,
832835
035DBA3929251ED6003E5125 /* CardReaderInputOptions.swift in Sources */,
833836
E140F61C2668CDC900FDB5FF /* Logging.swift in Sources */,
837+
035DBA41292BBEB2003E5125 /* CardReaderDiscoveryMethod.swift in Sources */,
834838
03CF78D727DF9BE600523706 /* RefundParameters.swift in Sources */,
835839
03B440AA2754DFC400759429 /* UnderlyingError.swift in Sources */,
836840
E1E125AC26EB582B0068A9B0 /* CardReaderSoftwareUpdateState.swift in Sources */,

Hardware/Hardware/CardReader/CardReaderService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public protocol CardReaderService {
2020

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

2525
/// Cancels the discovery process.
2626
func cancelDiscovery() -> Future<Void, Error>

Hardware/Hardware/CardReader/CardReaderType.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ public enum CardReaderType: CaseIterable {
77
case stripeM2
88
/// BBPOS WisePad 3
99
case wisepad3
10+
/// Tap on Mobile: Apple built in reader
11+
case appleBuiltIn
1012
/// Other
1113
case other
1214
}
@@ -26,6 +28,8 @@ extension CardReaderType {
2628
return "STRIPE_M2"
2729
case .wisepad3:
2830
return "WISEPAD_3"
31+
case .appleBuiltIn:
32+
return "BUILT_IN"
2933
default:
3034
return "UNKNOWN"
3135
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#if !targetEnvironment(macCatalyst)
2+
import Foundation
3+
import StripeTerminal
4+
5+
public enum CardReaderDiscoveryMethod {
6+
case localMobile
7+
case bluetoothProximity
8+
9+
func toStripe() -> DiscoveryMethod {
10+
switch self {
11+
case .localMobile:
12+
return .localMobile
13+
case .bluetoothProximity:
14+
return .bluetoothProximity
15+
}
16+
}
17+
}
18+
#endif

Hardware/Hardware/CardReader/StripeCardReader/CardReaderType+Stripe.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ extension CardReaderType {
1313
return .stripeM2
1414
case .wisePad3:
1515
return .wisepad3
16+
case .appleBuiltIn:
17+
return appleBuiltIn
1618
default:
1719
return .other
1820
}

Hardware/Hardware/CardReader/StripeCardReader/NoOpCardReaderService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public struct NoOpCardReaderService: CardReaderService {
2020

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

Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift

Lines changed: 123 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ extension StripeCardReaderService: CardReaderService {
5858

5959
// MARK: - CardReaderService conformance. Commands
6060

61-
public func start(_ configProvider: CardReaderConfigProvider) throws {
61+
public func start(_ configProvider: CardReaderConfigProvider,
62+
discoveryMethod: CardReaderDiscoveryMethod) throws {
6263
setConfigProvider(configProvider)
6364

6465
Terminal.setLogListener { message in
@@ -85,13 +86,12 @@ extension StripeCardReaderService: CardReaderService {
8586
}
8687

8788
let config = DiscoveryConfiguration(
88-
discoveryMethod: .bluetoothScan,
89+
discoveryMethod: discoveryMethod.toStripe(),
8990
simulated: shouldUseSimulatedCardReader
9091
)
9192

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

@@ -123,6 +123,14 @@ extension StripeCardReaderService: CardReaderService {
123123
})
124124
}
125125

126+
127+
// If we're using the simulated reader, we don't want to check for Bluetooth permissions
128+
// as the simulator won't have Bluetooth available.
129+
// If we're using the built-in reader, bluetooth is not required.
130+
private func shouldSkipBluetoothCheck(discoveryConfiguration: DiscoveryConfiguration) -> Bool {
131+
shouldUseSimulatedCardReader || discoveryConfiguration.discoveryMethod == .localMobile
132+
}
133+
126134
public func cancelDiscovery() -> Future <Void, Error> {
127135
Future { [weak self] promise in
128136
/**
@@ -311,9 +319,20 @@ extension StripeCardReaderService: CardReaderService {
311319
}.eraseToAnyPublisher()
312320
}
313321

314-
return getBluetoothConfiguration(stripeReader).flatMap { configuration in
315-
self.connect(stripeReader, configuration: configuration)
316-
}.eraseToAnyPublisher()
322+
switch stripeReader.deviceType {
323+
case .appleBuiltIn:
324+
return getLocalMobileConfiguration(stripeReader).flatMap { configuration in
325+
self.connect(stripeReader, configuration: configuration)
326+
}
327+
.share()
328+
.eraseToAnyPublisher()
329+
default:
330+
return getBluetoothConfiguration(stripeReader).flatMap { configuration in
331+
self.connect(stripeReader, configuration: configuration)
332+
}
333+
.share()
334+
.eraseToAnyPublisher()
335+
}
317336
}
318337

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

359+
private func getLocalMobileConfiguration(_ reader: StripeTerminal.Reader) -> Future<LocalMobileConnectionConfiguration, Error> {
360+
return Future() { [weak self] promise in
361+
guard let self = self else {
362+
promise(.failure(CardReaderServiceError.connection()))
363+
return
364+
}
365+
366+
// TODO - If we've recently connected to this reader, use the cached locationId from the
367+
// Terminal SDK instead of making this fetch. See #5116 and #5087
368+
self.readerLocationProvider?.fetchDefaultLocationID { result in
369+
switch result {
370+
case .success(let locationId):
371+
return promise(.success(LocalMobileConnectionConfiguration(locationId: locationId)))
372+
case .failure(let error):
373+
let underlyingError = UnderlyingError(with: error)
374+
return promise(.failure(CardReaderServiceError.connection(underlyingError: underlyingError)))
375+
}
376+
}
377+
}
378+
}
379+
340380
public func connect(_ reader: StripeTerminal.Reader, configuration: BluetoothConnectionConfiguration) -> Future <CardReader, Error> {
341381
// Keep a copy of the battery level in case the connection fails due to low battery
342382
// If that happens, the reader object won't be accessible anymore, and we want to show
@@ -376,6 +416,40 @@ extension StripeCardReaderService: CardReaderService {
376416
}
377417
}
378418

419+
public func connect(_ reader: StripeTerminal.Reader, configuration: LocalMobileConnectionConfiguration) -> Future <CardReader, Error> {
420+
return Future { [weak self] promise in
421+
guard let self = self else {
422+
promise(.failure(CardReaderServiceError.connection()))
423+
return
424+
}
425+
426+
Terminal.shared.connectLocalMobileReader(reader, delegate: self, connectionConfig: configuration) { [weak self] (reader, error) in
427+
guard let self = self else {
428+
promise(.failure(CardReaderServiceError.connection()))
429+
return
430+
}
431+
// Clear cached readers, as per Stripe's documentation.
432+
self.discoveredStripeReadersCache.clear()
433+
434+
if let error = error {
435+
let underlyingError = UnderlyingError(with: error)
436+
// Starting with StripeTerminal 2.0, required software updates happen transparently on connection
437+
// Any error related to that will be reported here, but we don't want to treat it as a connection error
438+
let serviceError: CardReaderServiceError = underlyingError.isSoftwareUpdateError ?
439+
.softwareUpdate(underlyingError: underlyingError, batteryLevel: nil) :
440+
.connection(underlyingError: underlyingError)
441+
promise(.failure(serviceError))
442+
}
443+
444+
if let reader = reader {
445+
self.connectedReadersSubject.send([CardReader(reader: reader)])
446+
self.switchStatusToIdle()
447+
promise(.success(CardReader(reader: reader)))
448+
}
449+
}
450+
}
451+
}
452+
379453
public func installUpdate() -> Void {
380454
Terminal.shared.installAvailableUpdate()
381455
}
@@ -443,6 +517,7 @@ private extension StripeCardReaderService {
443517

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

448523
}
@@ -696,6 +771,46 @@ extension StripeCardReaderService: BluetoothReaderDelegate {
696771
}
697772
}
698773

774+
extension StripeCardReaderService: LocalMobileReaderDelegate {
775+
public func localMobileReader(_ reader: Reader, didRequestReaderInput inputOptions: ReaderInputOptions = []) {
776+
sendReaderEvent(CardReaderEvent.make(stripeReaderInputOptions: inputOptions))
777+
}
778+
779+
public func localMobileReader(_ reader: Reader, didRequestReaderDisplayMessage displayMessage: ReaderDisplayMessage) {
780+
sendReaderEvent(CardReaderEvent.make(displayMessage: displayMessage))
781+
}
782+
783+
784+
// TODO: use a specific `deviceSetup` in these three functions instead of reusing the softwareUpdateSubject
785+
// https://github.com/woocommerce/woocommerce-ios/issues/8088
786+
public func localMobileReader(_ reader: Reader, didStartInstallingUpdate update: ReaderSoftwareUpdate, cancelable: Cancelable?) {
787+
softwareUpdateSubject.send(.started(cancelable: cancelable.map(StripeCancelable.init(cancelable:))))
788+
}
789+
790+
public func localMobileReader(_ reader: Reader, didReportReaderSoftwareUpdateProgress progress: Float) {
791+
softwareUpdateSubject.send(.installing(progress: progress))
792+
}
793+
794+
public func localMobileReader(_ reader: Reader, didFinishInstallingUpdate update: ReaderSoftwareUpdate?, error: Error?) {
795+
if let error = error {
796+
softwareUpdateSubject.send(.failed(
797+
error: CardReaderServiceError.softwareUpdate(underlyingError: UnderlyingError(with: error),
798+
batteryLevel: reader.batteryLevel?.doubleValue))
799+
)
800+
if let requiredDate = update?.requiredAt,
801+
requiredDate > Date() {
802+
softwareUpdateSubject.send(.available)
803+
} else {
804+
softwareUpdateSubject.send(.none)
805+
}
806+
} else {
807+
softwareUpdateSubject.send(.completed)
808+
connectedReadersSubject.send([CardReader(reader: reader)])
809+
softwareUpdateSubject.send(.none)
810+
}
811+
}
812+
}
813+
699814
// MARK: - Terminal delegate
700815
extension StripeCardReaderService: TerminalDelegate {
701816
public func terminal(_ terminal: Terminal, didReportUnexpectedReaderDisconnect reader: Reader) {

0 commit comments

Comments
 (0)