Skip to content
Merged
2 changes: 2 additions & 0 deletions Experiments/Experiments/DefaultFeatureFlagService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
return buildConfig == .localDeveloper || buildConfig == .alpha
case .inAppPurchases:
return buildConfig == .localDeveloper || buildConfig == .alpha
case .storeCreationMVP:
return buildConfig == .localDeveloper || buildConfig == .alpha
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 @@ -81,4 +81,8 @@ public enum FeatureFlag: Int {
/// Enables In-app purchases for buying Hosted WooCommerce plans
///
case inAppPurchases

/// Store creation MVP.
///
case storeCreationMVP
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ final class AuthenticatedWebViewController: UIViewController {
let webView = WKWebView(frame: .zero)
webView.translatesAutoresizingMaskIntoConstraints = false
webView.navigationDelegate = self
webView.uiDelegate = self
return webView
}()

Expand Down Expand Up @@ -156,3 +157,17 @@ extension AuthenticatedWebViewController: WKNavigationDelegate {
progressBar.setProgress(0, animated: false)
}
}

extension AuthenticatedWebViewController: WKUIDelegate {
func webView(_ webView: WKWebView,
createWebViewWith configuration: WKWebViewConfiguration,
for navigationAction: WKNavigationAction,
windowFeatures: WKWindowFeatures) -> WKWebView? {
// Allows `target=_blank` links by opening them in the same view, otherwise tapping on these links is no-op.
// Reference: https://stackoverflow.com/a/25853806/9185596
if navigationAction.targetFrame == nil {
webView.load(navigationAction.request)
}
return nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ final class StorePickerViewController: UIViewController {
}
}

/// Create store button.
@IBOutlet private weak var createStoreButton: FancyAnimatedButton!

/// New To Woo button
///
@IBOutlet var newToWooButton: UIButton! {
Expand Down Expand Up @@ -157,9 +160,13 @@ final class StorePickerViewController: UIViewController {
self?.restartAuthentication()
}

@Published private var possibleSiteURLsFromStoreCreation: Set<String> = []
private var possibleSiteURLsFromStoreCreationSubscription: AnyCancellable?

private let appleIDCredentialChecker: AppleIDCredentialCheckerProtocol
private let stores: StoresManager
private let featureFlagService: FeatureFlagService
private let isStoreCreationEnabled: Bool

init(configuration: StorePickerConfiguration,
appleIDCredentialChecker: AppleIDCredentialCheckerProtocol = AppleIDCredentialChecker(),
Expand All @@ -170,6 +177,7 @@ final class StorePickerViewController: UIViewController {
self.stores = stores
self.featureFlagService = featureFlagService
self.viewModel = StorePickerViewModel(configuration: configuration)
self.isStoreCreationEnabled = featureFlagService.isFeatureFlagEnabled(.storeCreationMVP)
super.init(nibName: Self.nibName, bundle: nil)
}

Expand All @@ -185,8 +193,10 @@ final class StorePickerViewController: UIViewController {
setupMainView()
setupAccountHeader()
setupTableView()
setupCreateStoreButton()
refreshResults()
observeStateChange()
observeSiteURLsFromStoreCreation()

switch configuration {
case .login:
Expand Down Expand Up @@ -276,6 +286,17 @@ private extension StorePickerViewController {
}
}

func setupCreateStoreButton() {
createStoreButton.isHidden = isStoreCreationEnabled == false
createStoreButton.isPrimary = false
createStoreButton.backgroundColor = .clear
createStoreButton.titleFont = StyleManager.actionButtonTitleFont
createStoreButton.setTitle(Localization.createStore, for: .normal)
createStoreButton.on(.touchUpInside) { [weak self] _ in
self?.createStoreButtonPressed()
}
}

func refreshResults() {
viewModel.refreshSites(currentlySelectedSiteID: currentlySelectedSite?.siteID)
viewModel.trackScreenView()
Expand All @@ -296,6 +317,28 @@ private extension StorePickerViewController {
func presentHelp() {
ServiceLocator.authenticationManager.presentSupport(from: self, screen: .storePicker)
}

func observeSiteURLsFromStoreCreation() {
possibleSiteURLsFromStoreCreationSubscription = $possibleSiteURLsFromStoreCreation
.filter { $0.isEmpty == false }
.removeDuplicates()
// There are usually three URLs in the webview that return a site URL - two with `*.wordpress.com` and the other the final URL.
.debounce(for: .seconds(5), scheduler: DispatchQueue.main)
.asyncMap { [weak self] possibleSiteURLs -> Site? in
// Waits for 5 seconds before syncing sites every time.
try await Task.sleep(nanoseconds: 5_000_000_000)
return try await self?.syncSites(forSiteThatMatchesPossibleURLs: possibleSiteURLs)
}
// Retries 10 times with 5 seconds pause in between to wait for the newly created site to be available as a Jetpack site
// in the WPCOM `/me/sites` response.
.retry(10)
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.sink { [weak self] site in
guard let self, let site else { return }
self.continueWithSelectedSite(site: site)
}
}
}


Expand Down Expand Up @@ -540,7 +583,7 @@ extension StorePickerViewController: UIViewControllerTransitioningDelegate {

// MARK: - Action Handlers
//
extension StorePickerViewController {
private extension StorePickerViewController {

/// Proceeds with the Login Flow.
///
Expand Down Expand Up @@ -583,6 +626,67 @@ extension StorePickerViewController {
@IBAction func secondaryActionWasPressed() {
restartAuthentication()
}

func createStoreButtonPressed() {
// TODO-7879: analytics

let viewModel = StoreCreationWebViewModel { [weak self] result in
self?.handleStoreCreationResult(result)
}
possibleSiteURLsFromStoreCreation = []
let webViewController = AuthenticatedWebViewController(viewModel: viewModel)
webViewController.addCloseNavigationBarButton(target: self, action: #selector(handleStoreCreationCloseAction))
let navigationController = WooNavigationController(rootViewController: webViewController)
// Disables interactive dismissal of the store creation modal.
navigationController.isModalInPresentation = true
present(navigationController, animated: true)
}

@objc func handleStoreCreationCloseAction() {
// TODO-7879: show a confirmation alert before closing the store creation view
// TODO-7879: analytics
dismiss(animated: true)
}

func handleStoreCreationResult(_ result: Result<String, Error>) {
switch result {
case .success(let siteURL):
// TODO-7879: analytics

// There could be multiple site URLs from the completion URL in the webview, and only one
// of them matches the final site URL from WPCOM `/me/sites` endpoint.
possibleSiteURLsFromStoreCreation.insert(siteURL)
case .failure(let error):
// TODO-7879: analytics
DDLogError("Store creation error: \(error)")
}
}

@MainActor
func syncSites(forSiteThatMatchesPossibleURLs possibleURLs: Set<String>) async throws -> Site {
return try await withCheckedThrowingContinuation { [weak self] continuation in
viewModel.refreshSites(currentlySelectedSiteID: nil) { [weak self] in
guard let self else { return }
// The newly created site often has `isJetpackThePluginInstalled=false` initially,
// which results in a JCP site.
// In this case, we want to retry sites syncing.
guard let site = self.viewModel.site(thatMatchesPossibleURLs: possibleURLs) else {
return continuation.resume(throwing: StoreCreationError.newSiteUnavailable)
}
guard site.isJetpackConnected && site.isJetpackThePluginInstalled else {
return continuation.resume(throwing: StoreCreationError.newSiteIsNotJetpackSite)
}
continuation.resume(returning: site)
}
}
}

func continueWithSelectedSite(site: Site) {
currentlySelectedSite = site
dismiss(animated: true) { [weak self] in
self?.checkRoleEligibility(for: site)
}
}
}


Expand Down Expand Up @@ -759,6 +863,8 @@ private extension StorePickerViewController {
comment: "Button to input a site address in store picker when there are no stores found")
static let newToWooCommerce = NSLocalizedString("New to WooCommerce?",
comment: "Title of button on the site picker screen for users who are new to WooCommerce.")
static let createStore = NSLocalizedString("Create a new store",
comment: "Button to create a new store from the store picker")
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21179.7" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21225" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_5" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21169.4"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21207"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="StorePickerViewController" customModule="WooCommerce" customModuleProvider="target">
<connections>
<outlet property="actionButton" destination="0KD-hY-YaS" id="mJR-Pi-S0z"/>
<outlet property="createStoreButton" destination="JzA-jw-LX7" id="bKC-9a-pp5"/>
<outlet property="enterSiteAddressButton" destination="mXN-0a-h6C" id="AR4-GV-obp"/>
<outlet property="newToWooButton" destination="X1y-bd-PXx" id="Rhc-sE-XIZ"/>
<outlet property="secondaryActionButton" destination="Slo-h4-7qY" id="YFU-eL-sGS"/>
Expand All @@ -24,18 +25,18 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" contentInsetAdjustmentBehavior="never" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" translatesAutoresizingMaskIntoConstraints="NO" id="oVs-XS-592" userLabel="Sites Table View">
<rect key="frame" x="0.0" y="44" width="414" height="533.66666666666663"/>
<rect key="frame" x="0.0" y="44.000000000000028" width="414" height="463.66666666666674"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<connections>
<outlet property="dataSource" destination="-1" id="m9y-Av-zfJ"/>
<outlet property="delegate" destination="-1" id="srI-Ru-BMi"/>
</connections>
</tableView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="H5J-Qa-DXp">
<rect key="frame" x="0.0" y="577.66666666666663" width="414" height="318.33333333333337"/>
<rect key="frame" x="0.0" y="507.66666666666674" width="414" height="388.33333333333326"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="0a6-Ee-KF8">
<rect key="frame" x="20" y="20.000000000000014" width="374" height="244.33333333333337"/>
<rect key="frame" x="20" y="19.999999999999943" width="374" height="314.33333333333331"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="X1y-bd-PXx">
<rect key="frame" x="0.0" y="0.0" width="374" height="34.333333333333336"/>
Expand Down Expand Up @@ -91,6 +92,18 @@
<action selector="secondaryActionWasPressed" destination="-1" eventType="touchUpInside" id="Dbs-az-84p"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="JzA-jw-LX7" userLabel="Create Store Button" customClass="FancyAnimatedButton" customModule="WooCommerce" customModuleProvider="target">
<rect key="frame" x="0.0" y="264.33333333333337" width="374" height="50"/>
<color key="backgroundColor" red="0.58823529409999997" green="0.34509803919999998" blue="0.54117647059999996" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="50" id="wwz-3k-mxi"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<state key="normal" title="Create a new store"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="isPrimary" value="YES"/>
</userDefinedRuntimeAttributes>
</button>
</subviews>
</stackView>
</subviews>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,12 @@ final class StorePickerViewModel {
])
}

func refreshSites(currentlySelectedSiteID: Int64?) {
func refreshSites(currentlySelectedSiteID: Int64?, completion: (() -> Void)? = nil) {
refetchSitesAndUpdateState()

synchronizeSites(selectedSiteID: currentlySelectedSiteID) { [weak self] _ in
self?.refetchSitesAndUpdateState()
completion?()
}
}

Expand Down Expand Up @@ -159,6 +160,20 @@ extension StorePickerViewModel {
}
return resultsController.safeObject(at: indexPath)
}

/// Returns the site that matches the given URL.
///
func site(thatMatchesPossibleURLs possibleURLs: Set<String>) -> Site? {
guard resultsController.numberOfObjects > 0 else {
return nil
}
return resultsController.fetchedObjects.first(where: { site in
guard let siteURL = URL(string: site.url)?.host else {
return false
}
return possibleURLs.contains(siteURL)
})
}
}

private extension StorePickerViewModel {
Expand Down
Loading