Skip to content

Commit 8ad6228

Browse files
adborbasclaude
andcommitted
Implement WPCom connection handler and plugin compatibility check
- Add WPComConnectionSetupHandler with full connection flow implementation - Add PluginVersionChecker for WooCommerce plugin compatibility check - Update coordinator to wire handler with credentials and plugin checker - Add 3 handler tests and reorganize test structure - Remove stub handler protocol (moved to handler file) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 88b8b67 commit 8ad6228

File tree

15 files changed

+739
-383
lines changed

15 files changed

+739
-383
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import Foundation
2+
import Yosemite
3+
import class WooFoundation.VersionHelpers
4+
5+
enum PluginVersionResult {
6+
case compatible
7+
case incompatible(currentVersion: String, requiredVersion: String)
8+
}
9+
10+
protocol PluginVersionCheckerProtocol {
11+
func checkCompatibility() async throws -> PluginVersionResult
12+
}
13+
14+
final class PluginVersionChecker: PluginVersionCheckerProtocol {
15+
private let siteID: Int64
16+
private let pluginPath: String
17+
private let minimumVersion: String
18+
private let stores: StoresManager
19+
20+
init(siteID: Int64,
21+
pluginPath: String,
22+
minimumVersion: String,
23+
stores: StoresManager = ServiceLocator.stores) {
24+
self.siteID = siteID
25+
self.pluginPath = pluginPath
26+
self.minimumVersion = minimumVersion
27+
self.stores = stores
28+
}
29+
30+
func checkCompatibility() async throws -> PluginVersionResult {
31+
let plugin = try await fetchPlugin()
32+
33+
let isSupported = VersionHelpers.isVersionSupported(
34+
version: plugin.version,
35+
minimumRequired: minimumVersion
36+
)
37+
38+
if isSupported {
39+
DDLogDebug("📱 Plugin compatibility: \(pluginPath) \(plugin.version) meets minimum \(minimumVersion)")
40+
return .compatible
41+
} else {
42+
DDLogDebug("📱 Plugin compatibility: \(pluginPath) \(plugin.version) below minimum \(minimumVersion)")
43+
return .incompatible(
44+
currentVersion: plugin.version,
45+
requiredVersion: minimumVersion
46+
)
47+
}
48+
}
49+
}
50+
51+
private extension PluginVersionChecker {
52+
func fetchPlugin() async throws -> SystemPlugin {
53+
let systemInfo = try await syncSystemInformation()
54+
55+
guard let plugin = systemInfo.systemPlugins.first(where: { $0.plugin == pluginPath }) else {
56+
throw PluginVersionError.pluginNotFound
57+
}
58+
59+
return plugin
60+
}
61+
62+
func syncSystemInformation() async throws -> SystemInformation {
63+
let stores = self.stores
64+
let siteID = self.siteID
65+
return try await withCheckedThrowingContinuation { continuation in
66+
let action = SystemStatusAction.synchronizeSystemInformation(siteID: siteID) { result in
67+
continuation.resume(with: result)
68+
}
69+
Task { @MainActor in
70+
stores.dispatch(action)
71+
}
72+
}
73+
}
74+
}
75+
76+
enum PluginVersionError: Error {
77+
case pluginNotFound
78+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import Foundation
2+
import Yosemite
3+
import class Networking.AlamofireNetwork
4+
5+
enum SetupStep: Int, CaseIterable {
6+
case connect = 0
7+
case checkPlugin = 1
8+
case enablePush = 2
9+
}
10+
11+
@MainActor
12+
protocol WPComConnectionSetupHandlerDelegate: AnyObject {
13+
func stepDidUpdate(_ step: SetupStep, status: WPComConnectionSetupStep.Status)
14+
func setupDidComplete()
15+
}
16+
17+
@MainActor
18+
protocol WPComConnectionSetupHandlerProtocol: AnyObject {
19+
var delegate: WPComConnectionSetupHandlerDelegate? { get set }
20+
func start()
21+
func retry()
22+
func cancel()
23+
}
24+
25+
@MainActor
26+
final class WPComConnectionSetupHandler: WPComConnectionSetupHandlerProtocol {
27+
weak var delegate: WPComConnectionSetupHandlerDelegate?
28+
29+
private let siteURL: String
30+
private let wpcomCredentials: Credentials?
31+
private let pluginChecker: PluginVersionCheckerProtocol
32+
private let stores: StoresManager
33+
34+
private var currentTask: Task<Void, Never>?
35+
private var lastFailedStep: SetupStep?
36+
37+
init(siteURL: String,
38+
wpcomCredentials: Credentials?,
39+
pluginChecker: PluginVersionCheckerProtocol,
40+
stores: StoresManager = ServiceLocator.stores) {
41+
self.siteURL = siteURL
42+
self.wpcomCredentials = wpcomCredentials
43+
self.pluginChecker = pluginChecker
44+
self.stores = stores
45+
}
46+
47+
func start() {
48+
startFromStep(.connect)
49+
}
50+
51+
func retry() {
52+
let startStep = lastFailedStep ?? .connect
53+
lastFailedStep = nil
54+
startFromStep(startStep)
55+
}
56+
57+
func cancel() {
58+
currentTask?.cancel()
59+
currentTask = nil
60+
}
61+
}
62+
63+
private extension WPComConnectionSetupHandler {
64+
func startFromStep(_ step: SetupStep) {
65+
guard currentTask == nil else { return }
66+
67+
currentTask = Task { [weak self] in
68+
guard let self else { return }
69+
defer { self.currentTask = nil }
70+
71+
if step.rawValue <= SetupStep.connect.rawValue {
72+
guard await executeConnectStep() else { return }
73+
}
74+
75+
if step.rawValue <= SetupStep.checkPlugin.rawValue {
76+
await executePluginCheckStep()
77+
}
78+
}
79+
}
80+
81+
func executeConnectStep() async -> Bool {
82+
guard let wpcomCredentials else {
83+
delegate?.stepDidUpdate(.connect, status: .success)
84+
return true
85+
}
86+
87+
delegate?.stepDidUpdate(.connect, status: .running)
88+
89+
do {
90+
try await performConnection(with: wpcomCredentials)
91+
delegate?.stepDidUpdate(.connect, status: .success)
92+
return true
93+
} catch {
94+
DDLogError("⛔️ WPCom connection failed: \(error)")
95+
lastFailedStep = .connect
96+
delegate?.stepDidUpdate(.connect, status: .failure(reason: Localization.connectionError))
97+
return false
98+
}
99+
}
100+
101+
func performConnection(with credentials: Credentials) async throws {
102+
let connectionData = try await dispatch(JetpackConnectionAction.fetchJetpackConnectionData)
103+
if connectionData.currentUser.wpcomUser != nil {
104+
DDLogDebug("📱 WPCom connection: Site already connected")
105+
return
106+
}
107+
108+
let blogID: Int64
109+
if let existingBlogID = connectionData.blogID, connectionData.isRegistered == true {
110+
blogID = existingBlogID
111+
} else {
112+
blogID = try await dispatch(JetpackConnectionAction.registerSite)
113+
}
114+
115+
let provisionResponse = try await dispatch(JetpackConnectionAction.provisionConnection)
116+
117+
let network = AlamofireNetwork(credentials: credentials, selectedSite: nil, appPasswordSupportState: nil)
118+
let siteURL = self.siteURL
119+
try await dispatch { completion in
120+
JetpackConnectionAction.finalizeConnection(
121+
siteID: blogID,
122+
siteURL: siteURL,
123+
provisionResponse: provisionResponse,
124+
network: network,
125+
completion: completion
126+
)
127+
}
128+
129+
let verificationData = try await dispatch(JetpackConnectionAction.fetchJetpackConnectionData)
130+
guard verificationData.currentUser.wpcomUser != nil else {
131+
throw WPComConnectionError.verificationFailed
132+
}
133+
134+
DDLogDebug("📱 WPCom connection: Successfully connected")
135+
}
136+
137+
func executePluginCheckStep() async {
138+
delegate?.stepDidUpdate(.checkPlugin, status: .running)
139+
140+
do {
141+
let result = try await pluginChecker.checkCompatibility()
142+
143+
switch result {
144+
case .compatible:
145+
delegate?.stepDidUpdate(.checkPlugin, status: .success)
146+
delegate?.setupDidComplete()
147+
148+
case .incompatible(let currentVersion, _):
149+
lastFailedStep = .checkPlugin
150+
let message = String(format: Localization.pluginOutdatedFormat, currentVersion)
151+
delegate?.stepDidUpdate(.checkPlugin, status: .failure(reason: message))
152+
}
153+
} catch {
154+
DDLogError("⛔️ Plugin compatibility check failed: \(error)")
155+
lastFailedStep = .checkPlugin
156+
delegate?.stepDidUpdate(.checkPlugin, status: .failure(reason: Localization.connectionError))
157+
}
158+
}
159+
160+
func dispatch<T>(_ actionBuilder: @escaping (@escaping (Result<T, Error>) -> Void) -> Action) async throws -> T {
161+
let stores = self.stores
162+
return try await withCheckedThrowingContinuation { continuation in
163+
let action = actionBuilder { result in
164+
continuation.resume(with: result)
165+
}
166+
Task { @MainActor in
167+
stores.dispatch(action)
168+
}
169+
}
170+
}
171+
}
172+
173+
private extension WPComConnectionSetupHandler {
174+
enum Localization {
175+
static let connectionError = NSLocalizedString(
176+
"wpComConnectionSetupHandler.connectionError",
177+
value: "There was an error completing your request. Please try again or contact support.",
178+
comment: "Generic error message for WPCom connection setup failures"
179+
)
180+
181+
static let pluginOutdatedFormat = NSLocalizedString(
182+
"wpComConnectionSetupHandler.pluginOutdated",
183+
value: "Your WooCommerce plugin version %@ needs updating to connect your store.",
184+
comment: "Error message when WooCommerce plugin is outdated. %@ is the current version."
185+
)
186+
}
187+
}
188+
189+
enum WPComConnectionError: Error {
190+
case verificationFailed
191+
}

WooCommerce/Classes/Authentication/WPComLogin/WPComConnectionSetup/WPComConnectionSetupHandlerProtocol.swift

Lines changed: 0 additions & 40 deletions
This file was deleted.

WooCommerce/Classes/Authentication/WPComLogin/WPComConnectionSetup/WPComConnectionSetupStep.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
struct WPComConnectionSetupStep: Identifiable {
4-
enum Status {
4+
enum Status: Equatable {
55
case notStarted
66
case running
77
case success

WooCommerce/Classes/Authentication/WPComLogin/WPComConnectionSetup/WPComConnectionSetupView.swift

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,25 @@ private extension WPComConnectionSetupView {
113113
}
114114

115115
#Preview {
116-
let viewModel = WPComConnectionSetupViewModel(
117-
storeName: "coffeebeans.com",
118-
handler: WPComConnectionSetupHandler(),
119-
onDismiss: {},
120-
onGoToStore: {},
121-
onUpdatePlugin: {}
122-
)
116+
let viewModel: WPComConnectionSetupViewModel = {
117+
let viewModel = WPComConnectionSetupViewModel(
118+
storeName: "awesomestore.com",
119+
handler: StaticPreviewHandler(),
120+
onDismiss: {},
121+
onGoToStore: {},
122+
onUpdatePlugin: {}
123+
)
124+
viewModel.stepDidUpdate(.connect, status: .success)
125+
viewModel.stepDidUpdate(.checkPlugin, status: .running)
126+
return viewModel
127+
}()
128+
123129
WPComConnectionSetupView(viewModel: viewModel)
124130
}
131+
132+
private final class StaticPreviewHandler: WPComConnectionSetupHandlerProtocol {
133+
weak var delegate: WPComConnectionSetupHandlerDelegate?
134+
func start() {}
135+
func retry() {}
136+
func cancel() {}
137+
}

0 commit comments

Comments
 (0)