Skip to content

Commit 710eade

Browse files
authored
Merge pull request #7551 from woocommerce/try/prologue-cta-order-ab-test
Swap the order of prologue CTAs as an A/B experiment
2 parents f05a632 + 5faa93f commit 710eade

File tree

6 files changed

+97
-46
lines changed

6 files changed

+97
-46
lines changed

Experiments/Experiments/ABTest.swift

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ public enum ABTest: String, CaseIterable {
2020
/// Experiment ref: pbxNRc-1Pp-p2
2121
case linkedProductsPromo = "woocommerceios_product_details_linked_products_banner"
2222

23+
/// A/B test for the login button order on the prologues screen.
24+
/// Experiment ref: pbxNRc-1VA-p2
25+
case loginPrologueButtonOrder = "woocommerceios_login_prologue_button_order"
26+
2327
/// Returns a variation for the given experiment
2428
public var variation: Variation {
2529
ExPlat.shared?.experiment(rawValue) ?? .control
@@ -29,14 +33,30 @@ public enum ABTest: String, CaseIterable {
2933
public extension ABTest {
3034
/// Start the AB Testing platform if any experiment exists
3135
///
32-
static func start() {
33-
guard ABTest.allCases.count > 1 else {
34-
return
35-
}
36-
37-
let experimentNames = ABTest.allCases.filter { $0 != .null }.map { $0.rawValue }
38-
ExPlat.shared?.register(experiments: experimentNames)
36+
static func start() async {
37+
await withCheckedContinuation { continuation in
38+
guard ABTest.allCases.count > 1 else {
39+
return continuation.resume(returning: ())
40+
}
41+
42+
let experimentNames = ABTest.allCases.filter { $0 != .null }.map { $0.rawValue }
43+
ExPlat.shared?.register(experiments: experimentNames)
44+
45+
ExPlat.shared?.refresh {
46+
continuation.resume(returning: ())
47+
}
48+
} as Void
49+
}
50+
}
3951

40-
ExPlat.shared?.refresh()
52+
public extension Variation {
53+
/// Used in an analytics event property value.
54+
var analyticsValue: String {
55+
switch self {
56+
case .control:
57+
return "control"
58+
case .treatment(let string):
59+
return string.map { "treatment: \($0)" } ?? "treatment"
60+
}
4161
}
4262
}

WooCommerce/Classes/AppDelegate.swift

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,26 +45,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
4545
appCoordinator?.tabBarController
4646
}
4747

48-
/// Checks on whether the Apple ID credential is valid when the app is logged in and becomes active.
49-
///
50-
private lazy var appleIDCredentialChecker = AppleIDCredentialChecker()
51-
5248
private let universalLinkRouter = UniversalLinkRouter.defaultUniversalLinkRouter()
5349

5450
// MARK: - AppDelegate Methods
5551

5652
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
5753
// Setup Components
5854
setupAnalytics()
59-
setupAuthenticationManager()
6055
setupCocoaLumberjack()
6156
setupLogLevel(.verbose)
6257
setupPushNotificationsManagerIfPossible()
6358
setupAppRatingManager()
6459
setupWormholy()
6560
setupKeyboardStateProvider()
6661
handleLaunchArguments()
67-
appleIDCredentialChecker.observeLoggedInStateForAppleIDObservations()
6862
setupUserNotificationCenter()
6963

7064
// Components that require prior Auth
@@ -78,19 +72,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
7872
// ever new source code is injected into our application.
7973
Inject.animation = .interactiveSpring()
8074

75+
Task { @MainActor in
76+
await startABTesting()
77+
78+
// Upgrade check...
79+
// This has to be called after A/B testing setup in `startABTesting` if any of the Tracks events
80+
// in `checkForUpgrades` is used as an exposure event for an experiment.
81+
// For example, `application_installed` could be the exposure event for logged-out experiments.
82+
checkForUpgrades()
83+
}
84+
8185
return true
8286
}
8387

8488
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
85-
86-
startABTesting()
87-
88-
// Upgrade check...
89-
// This has to be called after A/B testing setup in `startABTesting` if any of the Tracks events
90-
// in `checkForUpgrades` is used as an exposure event for an experiment.
91-
// For example, `application_installed` could be the exposure event for logged-out experiments.
92-
checkForUpgrades()
93-
9489
// Setup the Interface!
9590
setupMainWindow()
9691
setupComponentsAppearance()
@@ -267,12 +262,6 @@ private extension AppDelegate {
267262
ServiceLocator.analytics.initialize()
268263
}
269264

270-
/// Sets up the WordPress Authenticator.
271-
///
272-
func setupAuthenticationManager() {
273-
ServiceLocator.authenticationManager.initialize()
274-
}
275-
276265
/// Sets up CocoaLumberjack logging.
277266
///
278267
func setupCocoaLumberjack() {
@@ -382,8 +371,8 @@ private extension AppDelegate {
382371

383372
/// Starts the AB testing platform
384373
///
385-
func startABTesting() {
386-
ABTest.start()
374+
func startABTesting() async {
375+
await ABTest.start()
387376
}
388377
}
389378

@@ -397,7 +386,9 @@ private extension AppDelegate {
397386
let versionOfLastRun = UserDefaults.standard[.versionOfLastRun] as? String
398387
if versionOfLastRun == nil {
399388
// First run after a fresh install
400-
ServiceLocator.analytics.track(.applicationInstalled)
389+
ServiceLocator.analytics.track(.applicationInstalled,
390+
withProperties: ["after_abtest_setup": true,
391+
"prologue_experiment_variant": ABTest.loginPrologueButtonOrder.variation.analyticsValue])
401392
} else if versionOfLastRun != currentVersion {
402393
// App was upgraded
403394
ServiceLocator.analytics.track(.applicationUpgraded, withProperties: ["previous_version": versionOfLastRun ?? String()])

WooCommerce/Classes/Authentication/AuthenticationManager.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import WordPressAuthenticator
44
import WordPressKit
55
import Yosemite
66
import class Networking.UserAgent
7+
import enum Experiments.ABTest
78
import struct Networking.Settings
89
import protocol Storage.StorageManagerType
910

@@ -45,6 +46,7 @@ class AuthenticationManager: Authentication {
4546
func initialize() {
4647
let isWPComMagicLinkPreferredToPassword = ServiceLocator.featureFlagService.isFeatureFlagEnabled(.loginMagicLinkEmphasis)
4748
let isWPComMagicLinkShownAsSecondaryActionOnPasswordScreen = ServiceLocator.featureFlagService.isFeatureFlagEnabled(.loginMagicLinkEmphasisM2)
49+
let continueWithSiteAddressFirst = ABTest.loginPrologueButtonOrder.variation == .control
4850
let configuration = WordPressAuthenticatorConfiguration(wpcomClientId: ApiCredentials.dotcomAppId,
4951
wpcomSecret: ApiCredentials.dotcomSecret,
5052
wpcomScheme: ApiCredentials.dotcomAuthScheme,
@@ -60,7 +62,7 @@ class AuthenticationManager: Authentication {
6062
enableSignInWithApple: true,
6163
enableSignupWithGoogle: false,
6264
enableUnifiedAuth: true,
63-
continueWithSiteAddressFirst: true,
65+
continueWithSiteAddressFirst: continueWithSiteAddressFirst,
6466
enableSiteCredentialsLoginForSelfHostedSites: true,
6567
isWPComLoginRequiredForSiteCredentialsLogin: true,
6668
isWPComMagicLinkPreferredToPassword: isWPComMagicLinkPreferredToPassword,

WooCommerce/Classes/Authentication/Epilogue/StorePickerViewController.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,9 @@ private extension StorePickerViewController {
548548
guard ServiceLocator.stores.isAuthenticated else {
549549
return
550550
}
551-
ABTest.start()
551+
Task { @MainActor in
552+
await ABTest.start()
553+
}
552554
}
553555
}
554556

WooCommerce/Classes/ViewRelated/AppCoordinator.swift

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ final class AppCoordinator {
2727
private var localNotificationResponsesSubscription: AnyCancellable?
2828
private var isLoggedIn: Bool = false
2929

30+
/// Checks on whether the Apple ID credential is valid when the app is logged in and becomes active.
31+
///
32+
private lazy var appleIDCredentialChecker = AppleIDCredentialChecker()
33+
3034
init(window: UIWindow,
3135
stores: StoresManager = ServiceLocator.stores,
3236
storageManager: StorageManagerType = ServiceLocator.storageManager,
@@ -64,11 +68,12 @@ final class AppCoordinator {
6468
// More details about the UI states: https://github.com/woocommerce/woocommerce-ios/pull/3498
6569
switch (isLoggedIn, needsDefaultStore) {
6670
case (false, true), (false, false):
67-
self.displayAuthenticator()
71+
self.displayAuthenticatorWithOnboardingIfNeeded()
6872
case (true, true):
6973
self.displayLoggedInStateWithoutDefaultStore()
7074
case (true, false):
7175
self.validateRoleEligibility {
76+
self.configureAuthenticator()
7277
self.displayLoggedInUI()
7378
self.synchronizeAndShowWhatsNew()
7479
}
@@ -121,34 +126,63 @@ private extension AppCoordinator {
121126

122127
/// Displays the WordPress.com Authentication UI.
123128
///
124-
func displayAuthenticator() {
129+
func displayAuthenticatorWithOnboardingIfNeeded() {
130+
if canPresentLoginOnboarding() {
131+
// Sets a placeholder view controller as the window's root view as it is required
132+
// at the end of app launch.
133+
setWindowRootViewControllerAndAnimateIfNeeded(.init())
134+
presentLoginOnboarding { [weak self] in
135+
guard let self = self else { return }
136+
// Only displays the authenticator when dismissing onboarding to allow time for A/B test setup.
137+
self.configureAndDisplayAuthenticator()
138+
}
139+
} else {
140+
configureAndDisplayAuthenticator()
141+
}
142+
}
143+
144+
/// Configures the WPAuthenticator and sets the authenticator UI as the window's root view.
145+
func configureAndDisplayAuthenticator() {
146+
configureAuthenticator()
147+
125148
let authenticationUI = authenticationManager.authenticationUI()
126149
setWindowRootViewControllerAndAnimateIfNeeded(authenticationUI) { [weak self] _ in
127150
guard let self = self else { return }
128151
self.tabBarController.removeViewControllers()
129-
130-
self.presentLoginOnboarding()
131152
}
132-
ServiceLocator.analytics.track(.openedLogin)
153+
ServiceLocator.analytics.track(.openedLogin, withProperties: ["prologue_experiment_variant": ABTest.loginPrologueButtonOrder.variation.analyticsValue])
133154
}
134155

135-
/// Presents onboarding on top of the authentication UI under certain criteria.
136-
func presentLoginOnboarding() {
156+
/// Configures the WPAuthenticator for usage in both logged-in and logged-out states.
157+
func configureAuthenticator() {
158+
authenticationManager.initialize()
159+
appleIDCredentialChecker.observeLoggedInStateForAppleIDObservations()
160+
}
161+
162+
/// Determines whether the login onboarding should be shown.
163+
func canPresentLoginOnboarding() -> Bool {
137164
// Since we cannot control the user defaults in the simulator where UI tests are run on,
138165
// login onboarding is not shown in UI tests for now.
139166
// If we want to add UI tests for the login onboarding, we can add another launch argument
140167
// so that we can show/hide the onboarding screen consistently.
141168
let isUITesting: Bool = CommandLine.arguments.contains("-ui_testing")
142169
guard isUITesting == false else {
143-
return
170+
return false
144171
}
145172

146173
guard featureFlagService.isFeatureFlagEnabled(.loginPrologueOnboarding),
147-
loggedOutAppSettings.hasFinishedOnboarding == false else {
148-
return
174+
loggedOutAppSettings.hasFinishedOnboarding == false else {
175+
return false
149176
}
177+
return true
178+
}
179+
180+
/// Presents onboarding on top of the authentication UI under certain criteria.
181+
/// - Parameter onDismiss: invoked when the onboarding is dismissed.
182+
func presentLoginOnboarding(onDismiss: @escaping () -> Void) {
150183
let onboardingViewController = LoginOnboardingViewController { [weak self] action in
151184
guard let self = self else { return }
185+
onDismiss()
152186
self.loggedOutAppSettings.setHasFinishedOnboarding(true)
153187
self.window.rootViewController?.dismiss(animated: true)
154188

@@ -182,6 +216,8 @@ private extension AppCoordinator {
182216
return
183217
}
184218

219+
configureAuthenticator()
220+
185221
let matcher = ULAccountMatcher(storageManager: storageManager)
186222
matcher.refreshStoredSites()
187223

@@ -209,7 +245,7 @@ private extension AppCoordinator {
209245
storePickerCoordinator?.onDismiss = { [weak self] in
210246
guard let self = self else { return }
211247
if self.isLoggedIn == false {
212-
self.displayAuthenticator()
248+
self.displayAuthenticatorWithOnboardingIfNeeded()
213249
}
214250
}
215251
}

WooCommerce/WooCommerceTests/AppCoordinatorTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ final class AppCoordinatorTests: XCTestCase {
222222
appCoordinator.start()
223223

224224
// Then
225-
assertThat(window.rootViewController, isAnInstanceOf: LoginNavigationController.self)
225+
assertThat(window.rootViewController, isAnInstanceOf: UIViewController.self)
226226
assertThat(window.rootViewController?.presentedViewController, isAnInstanceOf: LoginOnboardingViewController.self)
227227
}
228228

0 commit comments

Comments
 (0)