Skip to content

Commit 22e0580

Browse files
committed
Merge branch 'trunk' into task/WOOMOB-1640-post-app-listings-meta
2 parents bb49d02 + 20876f1 commit 22e0580

File tree

112 files changed

+5216
-416
lines changed

Some content is hidden

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

112 files changed

+5216
-416
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<!--
22
Contains editorialized release notes. Raw release notes should go into `RELEASE-NOTES.txt`.
33
-->
4+
## 23.7
5+
This update brings Tap to Pay on iPhone to UK stores using Stripe, and automatic SSO sign-ins for web admin tasks. We made card payment onboarding error tips clearer, and fixed an intermittent bug with adding variations to an order. Lastly, iPad users can share quick feedback about Point of Sale.
6+
47
## 23.6
58
This update improves app stability and usability. We’ve enhanced compatibility for stores using HTTP site addresses, optimized how tabs load based on saved states, and fixed an issue that prevented dismissing the keyboard when editing product titles.
69

Modules/Sources/Fakes/Networking.generated.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,7 @@ extension Networking.POSProduct {
814814
manageStock: .fake(),
815815
stockQuantity: .fake(),
816816
stockStatusKey: .fake(),
817+
statusKey: .fake(),
817818
variationIDs: .fake()
818819
)
819820
}

Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,6 +1371,7 @@ extension Networking.POSProduct {
13711371
manageStock: CopiableProp<Bool> = .copy,
13721372
stockQuantity: NullableCopiableProp<Decimal> = .copy,
13731373
stockStatusKey: CopiableProp<String> = .copy,
1374+
statusKey: CopiableProp<String> = .copy,
13741375
variationIDs: CopiableProp<[Int64]> = .copy
13751376
) -> Networking.POSProduct {
13761377
let siteID = siteID ?? self.siteID
@@ -1389,6 +1390,7 @@ extension Networking.POSProduct {
13891390
let manageStock = manageStock ?? self.manageStock
13901391
let stockQuantity = stockQuantity ?? self.stockQuantity
13911392
let stockStatusKey = stockStatusKey ?? self.stockStatusKey
1393+
let statusKey = statusKey ?? self.statusKey
13921394
let variationIDs = variationIDs ?? self.variationIDs
13931395

13941396
return Networking.POSProduct(
@@ -1408,6 +1410,7 @@ extension Networking.POSProduct {
14081410
manageStock: manageStock,
14091411
stockQuantity: stockQuantity,
14101412
stockStatusKey: stockStatusKey,
1413+
statusKey: statusKey,
14111414
variationIDs: variationIDs
14121415
)
14131416
}

Modules/Sources/Networking/Model/POSProduct.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ public struct POSProduct: Codable, Equatable, GeneratedCopiable, GeneratedFakeab
4343
public let stockQuantity: Decimal?
4444
public let stockStatusKey: String
4545

46+
public let statusKey: String
47+
48+
public var productStatus: ProductStatus {
49+
return ProductStatus(rawValue: statusKey)
50+
}
51+
4652
public let variationIDs: [Int64]
4753

4854
public init(siteID: Int64,
@@ -61,6 +67,7 @@ public struct POSProduct: Codable, Equatable, GeneratedCopiable, GeneratedFakeab
6167
manageStock: Bool,
6268
stockQuantity: Decimal?,
6369
stockStatusKey: String,
70+
statusKey: String,
6471
variationIDs: [Int64]) {
6572
self.siteID = siteID
6673
self.productID = productID
@@ -85,6 +92,8 @@ public struct POSProduct: Codable, Equatable, GeneratedCopiable, GeneratedFakeab
8592
self.stockQuantity = stockQuantity
8693
self.stockStatusKey = stockStatusKey
8794

95+
self.statusKey = statusKey
96+
8897
self.variationIDs = variationIDs
8998
}
9099

@@ -129,6 +138,8 @@ public struct POSProduct: Codable, Equatable, GeneratedCopiable, GeneratedFakeab
129138
let stockQuantity = container.failsafeDecodeIfPresent(decimalForKey: .stockQuantity)
130139
let stockStatusKey = try container.decode(String.self, forKey: .stockStatusKey)
131140

141+
let statusKey = try container.decode(String.self, forKey: .statusKey)
142+
132143
let variationIDs = try container.decodeIfPresent([Int64].self, forKey: .variationIDs) ?? []
133144

134145
self.init(siteID: siteID,
@@ -147,6 +158,7 @@ public struct POSProduct: Codable, Equatable, GeneratedCopiable, GeneratedFakeab
147158
manageStock: manageStock,
148159
stockQuantity: stockQuantity,
149160
stockStatusKey: stockStatusKey,
161+
statusKey: statusKey,
150162
variationIDs: variationIDs)
151163
}
152164

@@ -180,6 +192,7 @@ private extension POSProduct {
180192
case manageStock = "manage_stock"
181193
case stockQuantity = "stock_quantity"
182194
case stockStatusKey = "stock_status"
195+
case statusKey = "status"
183196
case variationIDs = "variations"
184197
}
185198
}

Modules/Sources/Networking/Model/Product/ProductStatus.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public enum ProductStatus: Codable, Hashable, GeneratedFakeable {
1010
case privateStatus // `private` is a reserved keyword
1111
case autoDraft
1212
case importing // used for placeholder products from a product import or template
13+
case trash
1314
case custom(String) // in case there are extensions modifying product statuses
1415
}
1516

@@ -34,6 +35,8 @@ extension ProductStatus: RawRepresentable {
3435
self = .autoDraft
3536
case Keys.importing:
3637
self = .importing
38+
case Keys.trash:
39+
self = .trash
3740
default:
3841
self = .custom(rawValue)
3942
}
@@ -49,6 +52,7 @@ extension ProductStatus: RawRepresentable {
4952
case .privateStatus: return Keys.privateStatus
5053
case .autoDraft: return Keys.autoDraft
5154
case .importing: return Keys.importing
55+
case .trash: return Keys.trash
5256
case .custom(let payload): return payload
5357
}
5458
}
@@ -69,6 +73,8 @@ extension ProductStatus: RawRepresentable {
6973
return "Auto Draft" // We don't need to localize this now.
7074
case .importing:
7175
return "Importing" // We don't need to localize this now.
76+
case .trash:
77+
return "Trash" // We don't need to localize this now.
7278
case .custom(let payload):
7379
return payload // unable to localize at runtime.
7480
}
@@ -85,4 +91,5 @@ private enum Keys {
8591
static let privateStatus = "private"
8692
static let autoDraft = "auto-draft"
8793
static let importing = "importing"
94+
static let trash = "trash"
8895
}

Modules/Sources/Networking/Model/Product/ProductType.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,5 @@ private enum Keys {
105105
static let variableSubscription = "variable-subscription"
106106
static let bundle = "bundle"
107107
static let composite = "composite"
108-
static let booking = "booking"
108+
static let booking = "bookable-service"
109109
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import Foundation
2+
import CocoaLumberjackSwift
3+
4+
/// Coordinates background catalog downloads, including handling app wake events.
5+
public class BackgroundCatalogDownloadCoordinator {
6+
private let backgroundDownloader: BackgroundDownloadProtocol
7+
8+
public init(backgroundDownloader: BackgroundDownloadProtocol = BackgroundDownloadService()) {
9+
self.backgroundDownloader = backgroundDownloader
10+
}
11+
12+
/// Handles a background URLSession wake event.
13+
/// Called from AppDelegate when iOS wakes the app for a completed download.
14+
/// - Parameters:
15+
/// - sessionIdentifier: The session identifier from the callback
16+
/// - completionHandler: Completion handler to call when processing is done
17+
/// - parseHandler: Closure to parse and persist the downloaded file
18+
public func handleBackgroundSessionEvent(
19+
sessionIdentifier: String,
20+
completionHandler: @escaping () -> Void,
21+
parseHandler: @escaping (URL, Int64) async throws -> Void
22+
) async {
23+
DDLogInfo("🟣 Handling background session event for: \(sessionIdentifier)")
24+
25+
// Load the saved download state to know which site this is for
26+
guard let state = BackgroundDownloadState.load(for: sessionIdentifier) else {
27+
DDLogError("⛔️ No saved state found for background download session: \(sessionIdentifier)")
28+
completionHandler()
29+
return
30+
}
31+
32+
// Reconnect to the background session and get the downloaded file
33+
guard let fileURL = await backgroundDownloader.reconnectToSession(identifier: sessionIdentifier,
34+
allowCellular: true,
35+
completionHandler: completionHandler) else {
36+
DDLogError("⛔️ Failed to reconnect to background download session")
37+
BackgroundDownloadState.clear()
38+
return
39+
}
40+
41+
DDLogInfo("🟣 Background download file ready at: \(fileURL.path)")
42+
43+
// Parse the catalog file in this background window (~30 seconds)
44+
// TODO: WOOMOB-1677 - For very large catalogs, consider hybrid approach: try immediate parse, defer if timeout.
45+
do {
46+
try await parseHandler(fileURL, state.siteID)
47+
DDLogInfo("✅ Background catalog processing completed successfully")
48+
} catch {
49+
DDLogError("⛔️ Failed to process catalog in background: \(error)")
50+
}
51+
52+
// Clean up state
53+
BackgroundDownloadState.clear()
54+
}
55+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// periphery:ignore:all
2+
import Foundation
3+
4+
/// Protocol for handling background downloads with app suspension support.
5+
public protocol BackgroundDownloadProtocol {
6+
/// Downloads a file from the specified URL in the background.
7+
/// - Parameters:
8+
/// - url: The URL to download from.
9+
/// - sessionIdentifier: Unique identifier for the background session.
10+
/// - allowCellular: Whether cellular data should be allowed for this download.
11+
/// - Returns: Local file URL where the downloaded content is stored.
12+
func downloadFile(from url: URL, sessionIdentifier: String, allowCellular: Bool) async throws -> URL
13+
14+
/// Sets up background app suspension handling.
15+
/// - Parameter completionHandler: Handler to call when background download completes.
16+
func setBackgroundCompletionHandler(_ completionHandler: @escaping () -> Void)
17+
18+
/// Reconnects to an existing background session after app wake.
19+
/// Call this from AppDelegate when iOS wakes the app for background URLSession events.
20+
/// - Parameters:
21+
/// - sessionIdentifier: The session identifier from the callback
22+
/// - allowCellular: Whether cellular data should be allowed
23+
/// - completionHandler: Completion handler to call when all events are processed
24+
/// - Returns: Downloaded file URL if download completed, nil if still in progress
25+
func reconnectToSession(identifier sessionIdentifier: String,
26+
allowCellular: Bool,
27+
completionHandler: @escaping () -> Void) async -> URL?
28+
29+
/// Cancels all active downloads for the session.
30+
/// - Parameter sessionIdentifier: The session identifier to cancel.
31+
func cancelDownloads(for sessionIdentifier: String) async
32+
}
33+
34+
/// Progress and status information for background downloads.
35+
public struct BackgroundDownloadProgress {
36+
public let bytesDownloaded: Int64
37+
public let totalBytes: Int64
38+
public let progress: Double
39+
40+
public init(bytesDownloaded: Int64, totalBytes: Int64) {
41+
self.bytesDownloaded = bytesDownloaded
42+
self.totalBytes = totalBytes
43+
self.progress = totalBytes > 0 ? Double(bytesDownloaded) / Double(totalBytes) : 0.0
44+
}
45+
}
46+
47+
/// Errors that can occur during background downloads.
48+
public enum BackgroundDownloadError: Error, LocalizedError, Equatable {
49+
case invalidURL
50+
case sessionCreationFailed
51+
case downloadFailed(Error)
52+
case fileNotFound
53+
case cancelled
54+
55+
public var errorDescription: String? {
56+
switch self {
57+
case .invalidURL:
58+
return "The provided URL is invalid"
59+
case .sessionCreationFailed:
60+
return "Failed to create background download session"
61+
case .downloadFailed(let error):
62+
return "Download failed: \(error.localizedDescription)"
63+
case .fileNotFound:
64+
return "Downloaded file not found"
65+
case .cancelled:
66+
return "Download was cancelled"
67+
}
68+
}
69+
70+
public static func == (lhs: BackgroundDownloadError, rhs: BackgroundDownloadError) -> Bool {
71+
switch (lhs, rhs) {
72+
case (.invalidURL, .invalidURL):
73+
return true
74+
case (.sessionCreationFailed, .sessionCreationFailed):
75+
return true
76+
case (.downloadFailed(let lhsError), .downloadFailed(let rhsError)):
77+
return lhsError.localizedDescription == rhsError.localizedDescription
78+
case (.fileNotFound, .fileNotFound):
79+
return true
80+
case (.cancelled, .cancelled):
81+
return true
82+
default:
83+
return false
84+
}
85+
}
86+
}

0 commit comments

Comments
 (0)