Skip to content

Commit 5984467

Browse files
authored
Merge pull request #5382 from woocommerce/issue/5357-version-compare
[Mobile Payments] Improved minimum WCPay plugin version comparison
2 parents f2778a7 + ab06f43 commit 5984467

File tree

5 files changed

+268
-30
lines changed

5 files changed

+268
-30
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import Foundation
2+
3+
/// Helpers for working with versions (e.g. comparing two version strings)
4+
///
5+
final class VersionHelpers {
6+
/// Compares two strings as versions using the same approach as PHP `version_compare`.
7+
/// https://www.php.net/manual/en/function.version-compare.php
8+
///
9+
/// Returns `orderedAscending` if the lhs version is older than the rhs
10+
/// Returns `orderedSame` if the lhs version is the same as the rhs
11+
/// Returns `orderedDescending` if the lhs version is newer than the rhs
12+
///
13+
static func compare(_ lhs: String, _ rhs: String) -> ComparisonResult {
14+
let leftComponents = versionComponents(of: lhs)
15+
let rightComponents = versionComponents(of: rhs)
16+
let maxComponents = max(leftComponents.count, rightComponents.count)
17+
18+
for index in 0..<maxComponents {
19+
/// Treat missing components (e.g. 1.2 being compared to 1.1.3 as "0", i.e. 1.2.0)
20+
let leftComponent = index < leftComponents.count ? leftComponents[index] : "0"
21+
let rightComponent = index < rightComponents.count ? rightComponents[index] : "0"
22+
23+
let comparisonResult = compareStringComponents(leftComponent, rightComponent)
24+
if comparisonResult != .orderedSame {
25+
return comparisonResult
26+
}
27+
}
28+
29+
return .orderedSame
30+
}
31+
}
32+
33+
// MARK: - Private Helpers
34+
//
35+
private extension VersionHelpers {
36+
/// Replace _ - and + with a .
37+
///
38+
static func replaceUnderscoreDashAndPlusWithDot(_ string: String) -> String {
39+
string.replacingOccurrences(of: "([_\\-+]+)", with: ".", options: .regularExpression)
40+
}
41+
42+
/// Insert a . before and after any non number
43+
///
44+
static func insertDotsBeforeAndAfterAnyNonNumber(_ string: String) -> String {
45+
string.replacingOccurrences(of: "([^0-9.]+)", with: ".$1.", options: .regularExpression)
46+
}
47+
48+
/// Score and compare two string components
49+
///
50+
static func compareStringComponents(_ lhs: String, _ rhs: String) -> ComparisonResult {
51+
let lhsScore = VersionComponentScore(from: lhs)
52+
let rhsScore = VersionComponentScore(from: rhs)
53+
54+
if lhsScore < rhsScore {
55+
return .orderedAscending
56+
}
57+
58+
if lhsScore > rhsScore {
59+
return .orderedDescending
60+
}
61+
62+
if lhsScore == .number && rhsScore == .number {
63+
let lhsAsNumber = NSNumber(value: Int(lhs) ?? 0)
64+
let rhsAsNumber = NSNumber(value: Int(rhs) ?? 0)
65+
66+
let comparisonResult = lhsAsNumber.compare(rhsAsNumber)
67+
if comparisonResult != .orderedSame {
68+
return comparisonResult
69+
}
70+
}
71+
72+
return .orderedSame
73+
}
74+
75+
/// Process the given string into version components
76+
///
77+
static func versionComponents(of string: String) -> [String] {
78+
var stringToComponentize = replaceUnderscoreDashAndPlusWithDot(string)
79+
stringToComponentize = insertDotsBeforeAndAfterAnyNonNumber(stringToComponentize)
80+
return stringToComponentize.components(separatedBy: ".")
81+
}
82+
}
83+
84+
/// Defines the score (rank) of a component string within a version string.
85+
/// e.g. the "3" in 3.0.0beta3 should be treated as `.number`
86+
/// and the "beta" should be scored (ranked) lower as `.beta`.
87+
///
88+
/// The scores of components of version strings are used when comparing complete version strings
89+
/// to decide if one version is older, equal or newer than another.
90+
///
91+
/// Ranked per https://www.php.net/manual/en/function.version-compare.php
92+
///
93+
private enum VersionComponentScore: Comparable {
94+
case other
95+
case dev
96+
case alpha
97+
case beta
98+
case RC
99+
case number
100+
case patch
101+
}
102+
103+
private extension VersionComponentScore {
104+
init(from: String) {
105+
if from == "dev" {
106+
self = .dev
107+
return
108+
}
109+
if from == "alpha" || from == "a" {
110+
self = .alpha
111+
return
112+
}
113+
if from == "beta" || from == "b" {
114+
self = .beta
115+
return
116+
}
117+
if from == "RC" || from == "rc" {
118+
self = .RC
119+
return
120+
}
121+
let componentCharacterSet = CharacterSet(charactersIn: from)
122+
if componentCharacterSet.isSubset(of: .decimalDigits) {
123+
self = .number
124+
return
125+
}
126+
127+
if from == "pl" || from == "p" {
128+
self = .patch
129+
return
130+
}
131+
132+
self = .other
133+
}
134+
}

WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/CardPresentPaymentsOnboardingUseCase.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -197,11 +197,7 @@ private extension CardPresentPaymentsOnboardingUseCase {
197197
}
198198

199199
func isWCPayVersionSupported(plugin: SystemPlugin) -> Bool {
200-
/// NOTE: It does feel a bit risky to be using .numeric here since Apple documentation say it treats
201-
/// the string as a number, so decimal numbers with extra decimals (like versions with patches M.m.p)
202-
/// wouldn't necessarily be expected to be compared correctly, but it does seem to work.
203-
/// TODO: Implement / source a comparator that explicitly supports version strings with patches.
204-
plugin.version.compare(Constants.supportedWCPayVersion, options: .numeric) != .orderedAscending
200+
VersionHelpers.compare(plugin.version, Constants.minimumSupportedWCPayVersion) != .orderedAscending
205201
}
206202

207203
func isWCPayActivated(plugin: SystemPlugin) -> Bool {
@@ -268,6 +264,6 @@ private extension PaymentGatewayAccount {
268264

269265
private enum Constants {
270266
static let pluginName = "WooCommerce Payments"
271-
static let supportedWCPayVersion = "3.2.1"
267+
static let minimumSupportedWCPayVersion = "3.2.1"
272268
static let supportedCountryCodes = ["US"]
273269
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,7 @@
500500
314DC4BF268D183600444C9E /* CardReaderSettingsKnownReaderStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314DC4BE268D183600444C9E /* CardReaderSettingsKnownReaderStorage.swift */; };
501501
314DC4C1268D28B100444C9E /* CardReaderSettingsKnownReadersStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314DC4C0268D28B100444C9E /* CardReaderSettingsKnownReadersStorageTests.swift */; };
502502
314DC4C3268D2F1000444C9E /* MockAppSettingsStoresManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314DC4C2268D2F1000444C9E /* MockAppSettingsStoresManager.swift */; };
503+
31579028273EE2B1008CA3AF /* VersionHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31579027273EE2B1008CA3AF /* VersionHelpers.swift */; };
503504
31595CAD25E966380033F0FF /* ConnectedReaderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 31595CAC25E966380033F0FF /* ConnectedReaderTableViewCell.xib */; };
504505
315E14F42698DA24000AD5FF /* PassKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 315E14F32698DA24000AD5FF /* PassKit.framework */; };
505506
316837DA25CCA90C00E36B2F /* OrderStatusListDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316837D925CCA90C00E36B2F /* OrderStatusListDataSource.swift */; };
@@ -528,6 +529,7 @@
528529
31F21B02263C8E150035B50A /* CardReaderSettingsSearchingViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F21B01263C8E150035B50A /* CardReaderSettingsSearchingViewModelTests.swift */; };
529530
31F21B5A263CB41A0035B50A /* MockCardPresentPaymentsStoresManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F21B59263CB41A0035B50A /* MockCardPresentPaymentsStoresManager.swift */; };
530531
31F21B60263CB78A0035B50A /* MockCardReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F21B5F263CB78A0035B50A /* MockCardReader.swift */; };
532+
31F635DC273AF0B100E14F10 /* VersionHelpersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F635DB273AF0B100E14F10 /* VersionHelpersTests.swift */; };
531533
31F92DE125E85F6A00DE04DF /* ConnectedReaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F92DE025E85F6A00DE04DF /* ConnectedReaderTableViewCell.swift */; };
532534
31FE28C225E6D338003519F2 /* LearnMoreTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31FE28C125E6D338003519F2 /* LearnMoreTableViewCell.swift */; };
533535
31FE28C825E6D384003519F2 /* LearnMoreTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 31FE28C725E6D384003519F2 /* LearnMoreTableViewCell.xib */; };
@@ -1969,6 +1971,7 @@
19691971
314DC4BE268D183600444C9E /* CardReaderSettingsKnownReaderStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderSettingsKnownReaderStorage.swift; sourceTree = "<group>"; };
19701972
314DC4C0268D28B100444C9E /* CardReaderSettingsKnownReadersStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderSettingsKnownReadersStorageTests.swift; sourceTree = "<group>"; };
19711973
314DC4C2268D2F1000444C9E /* MockAppSettingsStoresManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAppSettingsStoresManager.swift; sourceTree = "<group>"; };
1974+
31579027273EE2B1008CA3AF /* VersionHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionHelpers.swift; sourceTree = "<group>"; };
19721975
31595CAC25E966380033F0FF /* ConnectedReaderTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConnectedReaderTableViewCell.xib; sourceTree = "<group>"; };
19731976
315E14F32698DA24000AD5FF /* PassKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PassKit.framework; path = System/Library/Frameworks/PassKit.framework; sourceTree = SDKROOT; };
19741977
316837D925CCA90C00E36B2F /* OrderStatusListDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderStatusListDataSource.swift; sourceTree = "<group>"; };
@@ -1997,6 +2000,7 @@
19972000
31F21B01263C8E150035B50A /* CardReaderSettingsSearchingViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderSettingsSearchingViewModelTests.swift; sourceTree = "<group>"; };
19982001
31F21B59263CB41A0035B50A /* MockCardPresentPaymentsStoresManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCardPresentPaymentsStoresManager.swift; sourceTree = "<group>"; };
19992002
31F21B5F263CB78A0035B50A /* MockCardReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCardReader.swift; sourceTree = "<group>"; };
2003+
31F635DB273AF0B100E14F10 /* VersionHelpersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionHelpersTests.swift; sourceTree = "<group>"; };
20002004
31F92DE025E85F6A00DE04DF /* ConnectedReaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedReaderTableViewCell.swift; sourceTree = "<group>"; };
20012005
31FE28C125E6D338003519F2 /* LearnMoreTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreTableViewCell.swift; sourceTree = "<group>"; };
20022006
31FE28C725E6D384003519F2 /* LearnMoreTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LearnMoreTableViewCell.xib; sourceTree = "<group>"; };
@@ -5274,6 +5278,7 @@
52745278
CE22709E2293052700C0626C /* WebviewHelper.swift */,
52755279
45D685FD23D0FB25005F87D0 /* Throttler.swift */,
52765280
262C921E26EEF8B100011F92 /* Binding.swift */,
5281+
31579027273EE2B1008CA3AF /* VersionHelpers.swift */,
52775282
);
52785283
path = Tools;
52795284
sourceTree = "<group>";
@@ -5473,6 +5478,7 @@
54735478
74F301592200EC0800931B9E /* NSDecimalNumberWooTests.swift */,
54745479
45DB706B26161F970064A6CF /* DecimalWooTests.swift */,
54755480
B541B2122189E7FD008FE7C1 /* ScannerWooTests.swift */,
5481+
31F635DB273AF0B100E14F10 /* VersionHelpersTests.swift */,
54765482
B55BC1F221A8790F0011A0C0 /* StringHTMLTests.swift */,
54775483
B5980A6421AC905C00EBF596 /* UIDeviceWooTests.swift */,
54785484
B57C745020F56EE900EEFC87 /* UITableViewCellHelpersTests.swift */,
@@ -7562,6 +7568,7 @@
75627568
B5D1AFC620BC7B7300DB0E8C /* StorePickerViewController.swift in Sources */,
75637569
02DD81FB242CAA400060E50B /* WordPressMediaLibraryPickerDataSource.swift in Sources */,
75647570
0240B3AC230A910C000A866C /* StoreStatsV4ChartAxisHelper.swift in Sources */,
7571+
31579028273EE2B1008CA3AF /* VersionHelpers.swift in Sources */,
75657572
CCD2F51C26D697860010E679 /* ShippingLabelServicePackageListViewModel.swift in Sources */,
75667573
E107FCE326C13A0D00BAF51B /* InPersonPaymentsSupportLink.swift in Sources */,
75677574
2662D90626E1571900E25611 /* ListSelector.swift in Sources */,
@@ -8401,6 +8408,7 @@
84018408
2667BFE92530ECE4008099D4 /* RefundProductsTotalViewModelTests.swift in Sources */,
84028409
D810F8F82639EDE900437C67 /* CardPresentPaymentsModalViewControllerTests.swift in Sources */,
84038410
02A275C623FE9EFC005C560F /* MockFeatureFlagService.swift in Sources */,
8411+
31F635DC273AF0B100E14F10 /* VersionHelpersTests.swift in Sources */,
84048412
FE3E427726A8545B00C596CE /* MockRoleEligibilityUseCase.swift in Sources */,
84058413
02F67FF525806E0100C3BAD2 /* ShippingLabelTrackingURLGeneratorTests.swift in Sources */,
84068414
570AAB052472FACB00516C0C /* OrderDetailsDataSourceTests.swift in Sources */,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import XCTest
2+
@testable import WooCommerce
3+
4+
/// VersionHelpers Unit Tests
5+
///
6+
final class VersionHelpersTests: XCTestCase {
7+
func test_compare_as_version() {
8+
let tests = [
9+
VersionTestCase(foundVersion: "2.8", requiredMinimumVersion: "2", meetsMinimum: true),
10+
VersionTestCase(foundVersion: "2.9", requiredMinimumVersion: "3", meetsMinimum: false),
11+
VersionTestCase(foundVersion: "2.9.1", requiredMinimumVersion: "2", meetsMinimum: true),
12+
VersionTestCase(foundVersion: "3.0", requiredMinimumVersion: "3", meetsMinimum: true),
13+
14+
VersionTestCase(foundVersion: "2.8", requiredMinimumVersion: "2.9", meetsMinimum: false),
15+
VersionTestCase(foundVersion: "2.9", requiredMinimumVersion: "2.9", meetsMinimum: true),
16+
VersionTestCase(foundVersion: "2.9.1", requiredMinimumVersion: "2.9", meetsMinimum: true),
17+
VersionTestCase(foundVersion: "3.0", requiredMinimumVersion: "2.9", meetsMinimum: true),
18+
19+
VersionTestCase(foundVersion: "2.9", requiredMinimumVersion: "2.9.0", meetsMinimum: true),
20+
21+
VersionTestCase(foundVersion: "2.9.1", requiredMinimumVersion: "2.9.1", meetsMinimum: true),
22+
VersionTestCase(foundVersion: "3.0", requiredMinimumVersion: "2.9.1", meetsMinimum: true),
23+
24+
VersionTestCase(foundVersion: "3.3.1-test-1", requiredMinimumVersion: "2.9.1", meetsMinimum: true),
25+
VersionTestCase(foundVersion: "3.3.1-test-1", requiredMinimumVersion: "3.3", meetsMinimum: true),
26+
VersionTestCase(foundVersion: "3.3.1-test-1", requiredMinimumVersion: "3.3.1", meetsMinimum: false),
27+
28+
VersionTestCase(foundVersion: "4.3.2RC1", requiredMinimumVersion: "4.3.2RC2", meetsMinimum: false),
29+
VersionTestCase(foundVersion: "4.3.2RC2", requiredMinimumVersion: "4.3.2RC1", meetsMinimum: true),
30+
31+
VersionTestCase(foundVersion: "1.0.0beta", requiredMinimumVersion: "1.0.0", meetsMinimum: false),
32+
VersionTestCase(foundVersion: "1.0.1beta", requiredMinimumVersion: "1.0.0", meetsMinimum: true),
33+
VersionTestCase(foundVersion: "1.0.0beta", requiredMinimumVersion: "1.0.0b", meetsMinimum: true),
34+
35+
VersionTestCase(foundVersion: "1.0.0-dev", requiredMinimumVersion: "1.0.0", meetsMinimum: false),
36+
VersionTestCase(foundVersion: "1.0.0-alpha", requiredMinimumVersion: "1.0.0", meetsMinimum: false),
37+
VersionTestCase(foundVersion: "1.0.0-a", requiredMinimumVersion: "1.0.0", meetsMinimum: false),
38+
VersionTestCase(foundVersion: "1.0.0-beta", requiredMinimumVersion: "1.0.0", meetsMinimum: false),
39+
VersionTestCase(foundVersion: "1.0.0-b", requiredMinimumVersion: "1.0.0", meetsMinimum: false),
40+
VersionTestCase(foundVersion: "1.0.0-RC1", requiredMinimumVersion: "1.0.0", meetsMinimum: false),
41+
VersionTestCase(foundVersion: "1.0.0-rc1", requiredMinimumVersion: "1.0.0", meetsMinimum: false),
42+
VersionTestCase(foundVersion: "1.0.0-pl", requiredMinimumVersion: "1.0.0", meetsMinimum: true),
43+
VersionTestCase(foundVersion: "1.0.0-p1", requiredMinimumVersion: "1.0.0", meetsMinimum: true),
44+
]
45+
46+
for test in tests {
47+
let meetsMinimum = VersionHelpers.compare(test.foundVersion, test.requiredMinimumVersion) != .orderedAscending
48+
XCTAssertEqual(test.meetsMinimum, meetsMinimum)
49+
}
50+
}
51+
52+
struct VersionTestCase {
53+
let foundVersion: String
54+
let requiredMinimumVersion: String
55+
let meetsMinimum: Bool
56+
}
57+
}

0 commit comments

Comments
 (0)