From 2bd7de28028d48248662cb268e05b4b578808f85 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Tue, 12 Aug 2025 16:33:42 +0100 Subject: [PATCH 01/10] Initial background sync experiment --- .../WooAnalyticsEvent+BackgroudUpdates.swift | 57 +++ .../Classes/Analytics/WooAnalyticsStat.swift | 12 + WooCommerce/Classes/AppDelegate.swift | 14 + .../Classes/Extensions/UserDefaults+Woo.swift | 2 + .../POSCatalogSyncBackgroundTaskManager.swift | 386 ++++++++++++++++++ .../POSCatalogSyncController.swift | 75 ++++ WooCommerce/Resources/Info.plist | 2 + .../WooCommerce.xcodeproj/project.pbxproj | 12 +- 8 files changed, 558 insertions(+), 2 deletions(-) create mode 100644 WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift create mode 100644 WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncController.swift diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsEvent+BackgroudUpdates.swift b/WooCommerce/Classes/Analytics/WooAnalyticsEvent+BackgroudUpdates.swift index d50e4a4aba3..37018185a6c 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsEvent+BackgroudUpdates.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsEvent+BackgroudUpdates.swift @@ -27,4 +27,61 @@ extension WooAnalyticsEvent { WooAnalyticsEvent(statName: .backgroundUpdatesDisabled, properties: [:]) } } + + enum POSCatalogSync { + + private enum Keys { + static let duration = "duration" + static let source = "source" + static let taskType = "task_type" + } + + // Scheduling events + static func fullSyncScheduled() -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .posFullCatalogSyncScheduled, properties: [:]) + } + + static func incrementalSyncScheduled() -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .posIncrementalSyncScheduled, properties: [:]) + } + + static func schedulingError(_ error: Error, taskType: String) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .posCatalogSyncSchedulingError, properties: [Keys.taskType: taskType], error: error) + } + + // Sync completion events + static func fullSyncCompleted(source: String, duration: TimeInterval? = nil) -> WooAnalyticsEvent { + var properties: [String: String] = [Keys.source: source] + if let duration = duration { + properties[Keys.duration] = String(duration) + } + return WooAnalyticsEvent(statName: .posFullCatalogSyncCompleted, properties: properties) + } + + static func incrementalSyncCompleted(duration: TimeInterval) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .posIncrementalSyncCompleted, properties: [Keys.duration: duration]) + } + + // Sync error events + static func fullSyncError(_ error: Error, source: String) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .posFullCatalogSyncError, properties: [Keys.source: source], error: error) + } + + static func incrementalSyncError(_ error: Error) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .posIncrementalSyncError, properties: [:], error: error) + } + + // Task management events + static func taskExpired(taskType: String) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .posCatalogSyncTaskExpired, properties: [Keys.taskType: taskType]) + } + + static func syncRecovered() -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .posCatalogSyncRecovered, properties: [:]) + } + + static func recoveryError(_ error: Error) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .posCatalogSyncRecoveryError, properties: [:], error: error) + } + } } diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift b/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift index 392dc89f3aa..58e998bd52f 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift @@ -1270,6 +1270,18 @@ enum WooAnalyticsStat: String { case pushNotificationOrderBackgroundSynced = "push_notification_order_background_synced" case pushNotificationOrderBackgroundSyncError = "push_notification_order_background_sync_error" case backgroundUpdatesDisabled = "background_updates_disabled" + + // MARK: POS Catalog Sync events + case posFullCatalogSyncScheduled = "pos_full_catalog_sync_scheduled" + case posIncrementalSyncScheduled = "pos_incremental_sync_scheduled" + case posCatalogSyncSchedulingError = "pos_catalog_sync_scheduling_error" + case posFullCatalogSyncCompleted = "pos_full_catalog_sync_completed" + case posIncrementalSyncCompleted = "pos_incremental_sync_completed" + case posFullCatalogSyncError = "pos_full_catalog_sync_error" + case posIncrementalSyncError = "pos_incremental_sync_error" + case posCatalogSyncTaskExpired = "pos_catalog_sync_task_expired" + case posCatalogSyncRecovered = "pos_catalog_sync_recovered" + case posCatalogSyncRecoveryError = "pos_catalog_sync_recovery_error" // MARK: Point of Sale events case pointOfSaleTabSelected = "main_tab_pos_selected" diff --git a/WooCommerce/Classes/AppDelegate.swift b/WooCommerce/Classes/AppDelegate.swift index cb6bf43d957..16d5457b49f 100644 --- a/WooCommerce/Classes/AppDelegate.swift +++ b/WooCommerce/Classes/AppDelegate.swift @@ -59,6 +59,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { /// Handles events to background refresh the app. /// private let appRefreshHandler = BackgroundTaskRefreshDispatcher() + + /// Manages POS catalog background synchronization. + /// + internal let posCatalogSyncManager = POSCatalogSyncBackgroundTaskManager() private var subscriptions: Set = [] @@ -127,6 +131,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Register for background app refresh events. appRefreshHandler.registerSystemTaskIdentifier() + + // Register POS catalog sync background tasks. + posCatalogSyncManager.registerBackgroundTasks() + + // Schedule initial POS catalog syncs. + posCatalogSyncManager.scheduleFullCatalogSync() + posCatalogSyncManager.scheduleIncrementalSync() + + // Recover any incomplete POS catalog syncs from previous app sessions. + posCatalogSyncManager.recoverIncompletesyncs() return true } diff --git a/WooCommerce/Classes/Extensions/UserDefaults+Woo.swift b/WooCommerce/Classes/Extensions/UserDefaults+Woo.swift index 95f9fd27f87..7970b3d69ad 100644 --- a/WooCommerce/Classes/Extensions/UserDefaults+Woo.swift +++ b/WooCommerce/Classes/Extensions/UserDefaults+Woo.swift @@ -68,6 +68,8 @@ extension UserDefaults { // Hide stores from store picker case hiddenStoreIDs + + case lastPOSIncrementalSyncTimestamp } } diff --git a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift new file mode 100644 index 00000000000..50c2636a118 --- /dev/null +++ b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift @@ -0,0 +1,386 @@ +import Foundation +import BackgroundTasks +import Yosemite + +/// Manages POS catalog background synchronization tasks. +/// Handles both full catalog downloads (BGProcessingTask) and incremental syncs (BGAppRefreshTask). +/// +final class POSCatalogSyncBackgroundTaskManager { + + // MARK: - Task Identifiers + + /// Full catalog download using BGProcessingTask - for large downloads that can wait for optimal conditions + static let fullCatalogSyncIdentifier = "com.automattic.woocommerce.pos.fullCatalogSync" + + /// Incremental sync using BGAppRefreshTask - for quick updates when app is backgrounded + static let incrementalSyncIdentifier = "com.automattic.woocommerce.pos.incrementalSync" + + // MARK: - Dependencies + + private let stores: StoresManager + private let urlSession: URLSession + + // MARK: - State Management + + /// State of ongoing sync operations - persisted to handle app lifecycle changes + private var syncState: POSSyncState { + get { + if let data = UserDefaults.standard.data(forKey: "pos_sync_state"), + let state = try? JSONDecoder().decode(POSSyncState.self, from: data) { + return state + } + return POSSyncState() + } + set { + if let data = try? JSONEncoder().encode(newValue) { + UserDefaults.standard.set(data, forKey: "pos_sync_state") + } + } + } + + // MARK: - Initialization + + init(stores: StoresManager = ServiceLocator.stores, urlSession: URLSession? = nil) { + self.stores = stores + + // Create background URLSession configuration for reliable downloads + let config = URLSessionConfiguration.background(withIdentifier: "com.automattic.woocommerce.pos.backgroundSync") + config.isDiscretionary = true // System can delay for optimal conditions + config.allowsCellularAccess = false // WiFi only for large downloads + config.sessionSendsLaunchEvents = true // Wake app when download completes + + self.urlSession = urlSession ?? URLSession(configuration: config, delegate: nil, delegateQueue: nil) + } + + // MARK: - Registration + + /// Registers all POS catalog sync tasks with the system + func registerBackgroundTasks() { + // Only register if not running tests + guard !Self.isRunningTests() else { return } + + DDLogInfo("πŸ“± Registering POS catalog background tasks...") + + // Register full catalog sync (BGProcessingTask) + BGTaskScheduler.shared.register(forTaskWithIdentifier: Self.fullCatalogSyncIdentifier, using: nil) { task in + guard let processingTask = task as? BGProcessingTask else { + DDLogError("⛔️ Failed to cast to BGProcessingTask for full catalog sync") + task.setTaskCompleted(success: false) + return + } + self.handleFullCatalogSync(task: processingTask) + } + + // Register incremental sync (BGAppRefreshTask) + BGTaskScheduler.shared.register(forTaskWithIdentifier: Self.incrementalSyncIdentifier, using: nil) { task in + guard let refreshTask = task as? BGAppRefreshTask else { + DDLogError("⛔️ Failed to cast to BGAppRefreshTask for incremental sync") + task.setTaskCompleted(success: false) + return + } + self.handleIncrementalSync(task: refreshTask) + } + + DDLogInfo("πŸ“± Successfully registered POS catalog background tasks") + } + + // MARK: - Scheduling + + /// Schedules a full catalog sync (BGProcessingTask) + /// Should be called weekly or daily, when device is idle with WiFi + func scheduleFullCatalogSync() { + guard !Self.isRunningTests() else { return } + + let request = BGProcessingTaskRequest(identifier: Self.fullCatalogSyncIdentifier) + request.requiresNetworkConnectivity = true + request.requiresExternalPower = false // Can run on battery but system will prefer charging + request.earliestBeginDate = Date(timeIntervalSinceNow: 24 * 60 * 60) // No earlier than 1 day + + do { + try BGTaskScheduler.shared.submit(request) + DDLogInfo("πŸ“± Scheduled full POS catalog sync for: \(request.earliestBeginDate?.description ?? "unknown")") + ServiceLocator.analytics.track(event: .POSCatalogSync.fullSyncScheduled()) + } catch { + DDLogError("⛔️ Failed to schedule full POS catalog sync: \(error)") + ServiceLocator.analytics.track(event: .POSCatalogSync.schedulingError(error, taskType: "full")) + } + } + + /// Schedules an incremental sync (BGAppRefreshTask) + /// Should be called more frequently for quick updates + func scheduleIncrementalSync() { + guard !Self.isRunningTests() else { return } + + let request = BGAppRefreshTaskRequest(identifier: Self.incrementalSyncIdentifier) + request.earliestBeginDate = Date(timeIntervalSinceNow: 4 * 60 * 60) // No earlier than 4 hours + + do { + try BGTaskScheduler.shared.submit(request) + DDLogInfo("πŸ“± Scheduled incremental POS catalog sync for: \(request.earliestBeginDate?.description ?? "unknown")") + ServiceLocator.analytics.track(event: .POSCatalogSync.incrementalSyncScheduled()) + } catch { + DDLogError("⛔️ Failed to schedule incremental POS catalog sync: \(error)") + ServiceLocator.analytics.track(event: .POSCatalogSync.schedulingError(error, taskType: "incremental")) + } + } + + // MARK: - Foreground Sync Support + + /// Starts a full catalog sync in the foreground that will continue in background + /// Returns a task that can be monitored for progress + func startForegroundFullSync() -> Task { + DDLogInfo("πŸ“± Starting foreground POS catalog sync that will continue in background...") + + return Task { + var currentSyncState = self.syncState + currentSyncState.isFullSyncInProgress = true + currentSyncState.lastFullSyncStartTime = Date() + self.syncState = currentSyncState + + do { + try await self.performFullCatalogSync() + + // Update successful completion state + var completedState = self.syncState + completedState.isFullSyncInProgress = false + completedState.lastFullSyncCompletionTime = Date() + self.syncState = completedState + + DDLogInfo("πŸ“± Foreground full POS catalog sync completed successfully") + ServiceLocator.analytics.track(event: .POSCatalogSync.fullSyncCompleted(source: "foreground")) + + } catch { + // Update error state but keep sync as in progress for potential resume + var errorState = self.syncState + errorState.lastFullSyncError = error.localizedDescription + self.syncState = errorState + + DDLogError("⛔️ Foreground full POS catalog sync failed: \(error)") + ServiceLocator.analytics.track(event: .POSCatalogSync.fullSyncError(error, source: "foreground")) + throw error + } + } + } + + // MARK: - App Launch Recovery + + /// Should be called on app launch to recover from incomplete syncs + func recoverIncompletesyncs() { + let currentState = syncState + + // Check for incomplete full sync that needs processing + if currentState.isFullSyncInProgress, + let downloadPath = currentState.pendingFullCatalogPath, + FileManager.default.fileExists(atPath: downloadPath) { + + DDLogInfo("πŸ“± Recovering incomplete POS catalog sync from app launch") + + Task { + do { + try await self.processDownloadedCatalog(at: URL(fileURLWithPath: downloadPath)) + + // Update completion state + var recoveredState = self.syncState + recoveredState.isFullSyncInProgress = false + recoveredState.pendingFullCatalogPath = nil + recoveredState.lastFullSyncCompletionTime = Date() + self.syncState = recoveredState + + DDLogInfo("πŸ“± Successfully recovered incomplete POS catalog sync") + ServiceLocator.analytics.track(event: .POSCatalogSync.syncRecovered()) + + } catch { + DDLogError("⛔️ Failed to recover incomplete POS catalog sync: \(error)") + ServiceLocator.analytics.track(event: .POSCatalogSync.recoveryError(error)) + } + } + } + } +} + +// MARK: - Background Task Handlers + +private extension POSCatalogSyncBackgroundTaskManager { + + /// Handles full catalog sync using BGProcessingTask + func handleFullCatalogSync(task: BGProcessingTask) { + DDLogInfo("πŸ“± Starting full POS catalog sync in background...") + + // Schedule next full sync + scheduleFullCatalogSync() + + let syncTask = Task { + do { + let startTime = Date() + try await self.performFullCatalogSync() + + let duration = Date().timeIntervalSince(startTime) + DDLogInfo("πŸ“± Full POS catalog sync completed in \(duration) seconds") + ServiceLocator.analytics.track(event: .POSCatalogSync.fullSyncCompleted(source: "background", duration: duration)) + + task.setTaskCompleted(success: true) + + } catch { + DDLogError("⛔️ Full POS catalog sync failed: \(error)") + ServiceLocator.analytics.track(event: .POSCatalogSync.fullSyncError(error, source: "background")) + task.setTaskCompleted(success: false) + } + } + + // Handle task expiration + task.expirationHandler = { + DDLogInfo("πŸ“± Full POS catalog sync task expired, cancelling...") + ServiceLocator.analytics.track(event: .POSCatalogSync.taskExpired(taskType: "full")) + syncTask.cancel() + } + } + + /// Handles incremental sync using BGAppRefreshTask + func handleIncrementalSync(task: BGAppRefreshTask) { + DDLogInfo("πŸ“± Starting incremental POS catalog sync in background...") + + // Schedule next incremental sync + scheduleIncrementalSync() + + let syncTask = Task { + do { + let startTime = Date() + try await self.performIncrementalSync() + + let duration = Date().timeIntervalSince(startTime) + DDLogInfo("πŸ“± Incremental POS catalog sync completed in \(duration) seconds") + ServiceLocator.analytics.track(event: .POSCatalogSync.incrementalSyncCompleted(duration: duration)) + + task.setTaskCompleted(success: true) + + } catch { + DDLogError("⛔️ Incremental POS catalog sync failed: \(error)") + ServiceLocator.analytics.track(event: .POSCatalogSync.incrementalSyncError(error)) + task.setTaskCompleted(success: false) + } + } + + // Handle task expiration + task.expirationHandler = { + DDLogInfo("πŸ“± Incremental POS catalog sync task expired, cancelling...") + ServiceLocator.analytics.track(event: .POSCatalogSync.taskExpired(taskType: "incremental")) + syncTask.cancel() + } + } +} + +// MARK: - Sync Implementation Stubs + +private extension POSCatalogSyncBackgroundTaskManager { + + /// Performs the full catalog download and processing + /// This would download the <100MB compressed JSON file + func performFullCatalogSync() async throws { + guard let siteID = stores.sessionManager.defaultStoreID else { + throw POSCatalogSyncError.noActiveSite + } + + DDLogInfo("πŸ“± Starting full catalog download for siteID: \(siteID)") + + // Update state to indicate download in progress + var currentState = syncState + currentState.isFullSyncInProgress = true + currentState.lastFullSyncStartTime = Date() + syncState = currentState + + // TODO: Implement actual full catalog download + // This would: + // 1. Create background URLSession download task + // 2. Download compressed JSON catalog + // 3. Store file path in syncState.pendingFullCatalogPath + // 4. Process and insert into database + // 5. Update syncState completion + + throw POSCatalogSyncError.notImplemented("Full catalog sync implementation needed") + } + + /// Performs incremental sync using REST API endpoints + /// This would fetch only changed products/variations/coupons using timestamp + func performIncrementalSync() async throws { + guard let siteID = stores.sessionManager.defaultStoreID else { + throw POSCatalogSyncError.noActiveSite + } + + let lastSync = UserDefaults.standard[.lastPOSIncrementalSyncTimestamp] as? Date ?? Date.distantPast + DDLogInfo("πŸ“± Starting incremental sync for siteID: \(siteID), last sync: \(lastSync)") + + // TODO: Implement actual incremental sync + // This would: + // 1. Fetch products modified since lastSync timestamp + // 2. Fetch variations modified since lastSync timestamp + // 3. Fetch coupons modified since lastSync timestamp + // 4. Update local database + // 5. Update lastPOSIncrementalSyncTimestamp + + throw POSCatalogSyncError.notImplemented("Incremental sync implementation needed") + } + + /// Processes a downloaded catalog file (for recovery scenarios) + func processDownloadedCatalog(at url: URL) async throws { + DDLogInfo("πŸ“± Processing downloaded catalog at: \(url.path)") + + // TODO: Implement catalog processing + // This would: + // 1. Decompress the JSON file + // 2. Parse catalog data + // 3. Insert/update database records + // 4. Clean up temporary file + + throw POSCatalogSyncError.notImplemented("Catalog processing implementation needed") + } +} + +// MARK: - State Management + +/// Represents the current state of POS catalog synchronization +private struct POSSyncState: Codable { + var isFullSyncInProgress: Bool = false + var lastFullSyncStartTime: Date? + var lastFullSyncCompletionTime: Date? + var lastFullSyncError: String? + var pendingFullCatalogPath: String? // Path to downloaded but unprocessed catalog + + var lastIncrementalSyncTime: Date? + var lastIncrementalSyncError: String? +} + +// MARK: - Error Types + +enum POSCatalogSyncError: LocalizedError { + case noActiveSite + case notImplemented(String) + case downloadFailed(Error) + case processingFailed(Error) + + var errorDescription: String? { + switch self { + case .noActiveSite: + return "No active site available for sync" + case .notImplemented(let message): + return message + case .downloadFailed(let error): + return "Download failed: \(error.localizedDescription)" + case .processingFailed(let error): + return "Processing failed: \(error.localizedDescription)" + } + } +} + +// MARK: - Helper Extensions + +private extension POSCatalogSyncBackgroundTaskManager { + static func isRunningTests() -> Bool { + return NSClassFromString("XCTestCase") != nil + } +} + +// MARK: - UserDefaults Keys Extension + +private extension UserDefaults.Key { + static let lastPOSIncrementalSyncTimestamp = UserDefaults.Key("lastPOSIncrementalSyncTimestamp") +} \ No newline at end of file diff --git a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncController.swift b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncController.swift new file mode 100644 index 00000000000..cb1971218c7 --- /dev/null +++ b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncController.swift @@ -0,0 +1,75 @@ +import Foundation + +/// Provides easy access to POS catalog sync operations from anywhere in the app. +/// This class serves as a bridge between the UI and the background sync manager. +/// +final class POSCatalogSyncController { + + /// Shared instance for app-wide access + static let shared = POSCatalogSyncController() + + private init() {} + + /// Reference to the background sync manager from AppDelegate + private var syncManager: POSCatalogSyncBackgroundTaskManager? { + return AppDelegate.shared.posCatalogSyncManager + } + + // MARK: - Public Interface + + /// Checks if a full catalog sync is currently in progress + var isFullSyncInProgress: Bool { + // This would check the sync state - implementation depends on exposing state from the manager + return false // TODO: Implement by exposing state from POSCatalogSyncBackgroundTaskManager + } + + /// Starts a foreground full catalog sync that continues in background + /// Returns a task that can be used to monitor progress or cancellation + @discardableResult + func startForegroundFullSync() -> Task? { + guard let syncManager = syncManager else { + DDLogError("⛔️ POS catalog sync manager not available") + return nil + } + + DDLogInfo("πŸ“± Starting foreground POS catalog sync via controller...") + return syncManager.startForegroundFullSync() + } + + /// Manually triggers an incremental sync (typically not needed as it's scheduled automatically) + func triggerIncrementalSync() { + guard let syncManager = syncManager else { + DDLogError("⛔️ POS catalog sync manager not available") + return + } + + syncManager.scheduleIncrementalSync() + DDLogInfo("πŸ“± Manually triggered incremental POS catalog sync") + } + + /// Re-schedules background syncs (useful after user settings changes) + func rescheduleBackgroundSyncs() { + guard let syncManager = syncManager else { + DDLogError("⛔️ POS catalog sync manager not available") + return + } + + syncManager.scheduleFullCatalogSync() + syncManager.scheduleIncrementalSync() + DDLogInfo("πŸ“± Rescheduled POS catalog background syncs") + } +} + +// MARK: - Usage Example +/* + + // In a POS view controller when user first logs in: + if POSCatalogSyncController.shared.isFullSyncInProgress == false { + let syncTask = POSCatalogSyncController.shared.startForegroundFullSync() + // Show progress UI and monitor syncTask as needed + } + + // In settings when user changes sync preferences: + POSCatalogSyncController.shared.rescheduleBackgroundSyncs() + + */ \ No newline at end of file diff --git a/WooCommerce/Resources/Info.plist b/WooCommerce/Resources/Info.plist index 4be39f02441..ef2baa650a9 100644 --- a/WooCommerce/Resources/Info.plist +++ b/WooCommerce/Resources/Info.plist @@ -5,6 +5,8 @@ BGTaskSchedulerPermittedIdentifiers com.automattic.woocommerce.refresh + com.automattic.woocommerce.pos.fullCatalogSync + com.automattic.woocommerce.pos.incrementalSync CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index d065e0c6ac9..c61b52a1241 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -934,6 +934,8 @@ 20C3DB262E1E69CF00CF7D3B /* PointOfSaleBarcodeScannerSetupModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20C3DB202E1E69CF00CF7D3B /* PointOfSaleBarcodeScannerSetupModels.swift */; }; 20C3DB272E1E69CF00CF7D3B /* PointOfSaleBarcodeScannerSetupFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20C3DB1D2E1E69CF00CF7D3B /* PointOfSaleBarcodeScannerSetupFlow.swift */; }; 20C3DB292E1E6FBA00CF7D3B /* PointOfSaleFlowButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20C3DB282E1E6FBA00CF7D3B /* PointOfSaleFlowButtonsView.swift */; }; + 20C6D8402E4B981C0000B386 /* POSCatalogSyncBackgroundTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20C6D83E2E4B981C0000B386 /* POSCatalogSyncBackgroundTaskManager.swift */; }; + 20C6D8412E4B981C0000B386 /* POSCatalogSyncController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20C6D83F2E4B981C0000B386 /* POSCatalogSyncController.swift */; }; 20C6E7512CDE4AEA00CD124C /* ItemListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20C6E7502CDE4AEA00CD124C /* ItemListState.swift */; }; 20C909962D3151FA0013BCCF /* ItemListBaseItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20C909952D3151FA0013BCCF /* ItemListBaseItem.swift */; }; 20CC1EDB2AFA8381006BD429 /* InPersonPaymentsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CC1EDA2AFA8381006BD429 /* InPersonPaymentsMenu.swift */; }; @@ -2762,9 +2764,9 @@ DEA357132ADCC4C9006380BA /* BlazeCampaignListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA357122ADCC4C9006380BA /* BlazeCampaignListViewModelTests.swift */; }; DEA64C532E40B04700791018 /* OrderDetailsProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA64C522E40B04000791018 /* OrderDetailsProduct.swift */; }; DEA64C552E41A2D000791018 /* Product+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA64C542E41A2CA00791018 /* Product+Helpers.swift */; }; + DEA65B372E41A65600791018 /* ProductListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA65B362E41A65100791018 /* ProductListItem.swift */; }; DEA66A192E41DECF00791018 /* ShippingLabelProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA66A182E41DEC200791018 /* ShippingLabelProduct.swift */; }; DEA66A1B2E41E0C000791018 /* BlazeCampaignProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA66A1A2E41E0B800791018 /* BlazeCampaignProduct.swift */; }; - DEA65B372E41A65600791018 /* ProductListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA65B362E41A65100791018 /* ProductListItem.swift */; }; DEA6BCAF2BC6A9B10017D671 /* StoreStatsChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA6BCAE2BC6A9B10017D671 /* StoreStatsChart.swift */; }; DEA6BCB12BC6AA040017D671 /* StoreStatsChartViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA6BCB02BC6AA040017D671 /* StoreStatsChartViewModel.swift */; }; DEA88F502AA9D0100037273B /* AddEditProductCategoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA88F4F2AA9D0100037273B /* AddEditProductCategoryViewModel.swift */; }; @@ -4133,6 +4135,8 @@ 20C3DB202E1E69CF00CF7D3B /* PointOfSaleBarcodeScannerSetupModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleBarcodeScannerSetupModels.swift; sourceTree = ""; }; 20C3DB212E1E69CF00CF7D3B /* PointOfSaleBarcodeScannerSetupViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleBarcodeScannerSetupViews.swift; sourceTree = ""; }; 20C3DB282E1E6FBA00CF7D3B /* PointOfSaleFlowButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleFlowButtonsView.swift; sourceTree = ""; }; + 20C6D83E2E4B981C0000B386 /* POSCatalogSyncBackgroundTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSCatalogSyncBackgroundTaskManager.swift; sourceTree = ""; }; + 20C6D83F2E4B981C0000B386 /* POSCatalogSyncController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSCatalogSyncController.swift; sourceTree = ""; }; 20C6E7502CDE4AEA00CD124C /* ItemListState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListState.swift; sourceTree = ""; }; 20C909952D3151FA0013BCCF /* ItemListBaseItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListBaseItem.swift; sourceTree = ""; }; 20CC1EDA2AFA8381006BD429 /* InPersonPaymentsMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsMenu.swift; sourceTree = ""; }; @@ -5965,9 +5969,9 @@ DEA357122ADCC4C9006380BA /* BlazeCampaignListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignListViewModelTests.swift; sourceTree = ""; }; DEA64C522E40B04000791018 /* OrderDetailsProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderDetailsProduct.swift; sourceTree = ""; }; DEA64C542E41A2CA00791018 /* Product+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Product+Helpers.swift"; sourceTree = ""; }; + DEA65B362E41A65100791018 /* ProductListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductListItem.swift; sourceTree = ""; }; DEA66A182E41DEC200791018 /* ShippingLabelProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelProduct.swift; sourceTree = ""; }; DEA66A1A2E41E0B800791018 /* BlazeCampaignProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignProduct.swift; sourceTree = ""; }; - DEA65B362E41A65100791018 /* ProductListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductListItem.swift; sourceTree = ""; }; DEA6BCAE2BC6A9B10017D671 /* StoreStatsChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreStatsChart.swift; sourceTree = ""; }; DEA6BCB02BC6AA040017D671 /* StoreStatsChartViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreStatsChartViewModel.swift; sourceTree = ""; }; DEA88F4F2AA9D0100037273B /* AddEditProductCategoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditProductCategoryViewModel.swift; sourceTree = ""; }; @@ -8824,6 +8828,8 @@ 26BCA0412C35EDBF000BE96C /* OrderListSyncBackgroundTask.swift */, 26F115AE2C49A9250019CD73 /* DashboardSyncBackgroundTask.swift */, 26DDA4A82C4839B8005FBEBF /* DashboardTimestampStore.swift */, + 20C6D83E2E4B981C0000B386 /* POSCatalogSyncBackgroundTaskManager.swift */, + 20C6D83F2E4B981C0000B386 /* POSCatalogSyncController.swift */, ); path = BackgroundTasks; sourceTree = ""; @@ -15853,6 +15859,8 @@ 02482A8B237BE8C7007E73ED /* LinkSettingsViewController.swift in Sources */, EEC099362BF3C68000FBCF6C /* MostActiveCouponsCard.swift in Sources */, CE227097228F152400C0626C /* WooBasicTableViewCell.swift in Sources */, + 20C6D8402E4B981C0000B386 /* POSCatalogSyncBackgroundTaskManager.swift in Sources */, + 20C6D8412E4B981C0000B386 /* POSCatalogSyncController.swift in Sources */, 02C27BCE282CB52F0065471A /* CardPresentPaymentReceiptEmailCoordinator.swift in Sources */, 20D5575D2DFADF5400D9EC8B /* BarcodeScannerContainer.swift in Sources */, 026826AA2BF59DF70036F959 /* CartView.swift in Sources */, From 1c461e4616e59ae3d72e30ed1f5ddced92782cac Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Tue, 12 Aug 2025 17:10:40 +0100 Subject: [PATCH 02/10] Add realistic downloads and logging to test background behaviour --- .../POSCatalogSyncBackgroundTaskManager.swift | 361 +++++++++++++----- .../POSCatalogSyncExample.swift | 148 +++++++ .../BetaFeaturesConfiguration.swift | 58 +++ .../BetaFeaturesConfigurationViewModel.swift | 19 + .../WooCommerce.xcodeproj/project.pbxproj | 4 + 5 files changed, 494 insertions(+), 96 deletions(-) create mode 100644 WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift diff --git a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift index 50c2636a118..5f53c5028d7 100644 --- a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift +++ b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift @@ -1,27 +1,28 @@ import Foundation import BackgroundTasks import Yosemite +import UIKit /// Manages POS catalog background synchronization tasks. /// Handles both full catalog downloads (BGProcessingTask) and incremental syncs (BGAppRefreshTask). /// final class POSCatalogSyncBackgroundTaskManager { - + // MARK: - Task Identifiers - + /// Full catalog download using BGProcessingTask - for large downloads that can wait for optimal conditions static let fullCatalogSyncIdentifier = "com.automattic.woocommerce.pos.fullCatalogSync" - + /// Incremental sync using BGAppRefreshTask - for quick updates when app is backgrounded static let incrementalSyncIdentifier = "com.automattic.woocommerce.pos.incrementalSync" - + // MARK: - Dependencies - + private let stores: StoresManager private let urlSession: URLSession - + // MARK: - State Management - + /// State of ongoing sync operations - persisted to handle app lifecycle changes private var syncState: POSSyncState { get { @@ -37,30 +38,30 @@ final class POSCatalogSyncBackgroundTaskManager { } } } - + // MARK: - Initialization - + init(stores: StoresManager = ServiceLocator.stores, urlSession: URLSession? = nil) { self.stores = stores - + // Create background URLSession configuration for reliable downloads let config = URLSessionConfiguration.background(withIdentifier: "com.automattic.woocommerce.pos.backgroundSync") config.isDiscretionary = true // System can delay for optimal conditions config.allowsCellularAccess = false // WiFi only for large downloads config.sessionSendsLaunchEvents = true // Wake app when download completes - + self.urlSession = urlSession ?? URLSession(configuration: config, delegate: nil, delegateQueue: nil) } - + // MARK: - Registration - + /// Registers all POS catalog sync tasks with the system func registerBackgroundTasks() { // Only register if not running tests guard !Self.isRunningTests() else { return } - + DDLogInfo("πŸ“± Registering POS catalog background tasks...") - + // Register full catalog sync (BGProcessingTask) BGTaskScheduler.shared.register(forTaskWithIdentifier: Self.fullCatalogSyncIdentifier, using: nil) { task in guard let processingTask = task as? BGProcessingTask else { @@ -70,8 +71,8 @@ final class POSCatalogSyncBackgroundTaskManager { } self.handleFullCatalogSync(task: processingTask) } - - // Register incremental sync (BGAppRefreshTask) + + // Register incremental sync (BGAppRefreshTask) BGTaskScheduler.shared.register(forTaskWithIdentifier: Self.incrementalSyncIdentifier, using: nil) { task in guard let refreshTask = task as? BGAppRefreshTask else { DDLogError("⛔️ Failed to cast to BGAppRefreshTask for incremental sync") @@ -80,22 +81,22 @@ final class POSCatalogSyncBackgroundTaskManager { } self.handleIncrementalSync(task: refreshTask) } - + DDLogInfo("πŸ“± Successfully registered POS catalog background tasks") } - + // MARK: - Scheduling - + /// Schedules a full catalog sync (BGProcessingTask) /// Should be called weekly or daily, when device is idle with WiFi func scheduleFullCatalogSync() { guard !Self.isRunningTests() else { return } - + let request = BGProcessingTaskRequest(identifier: Self.fullCatalogSyncIdentifier) request.requiresNetworkConnectivity = true request.requiresExternalPower = false // Can run on battery but system will prefer charging request.earliestBeginDate = Date(timeIntervalSinceNow: 24 * 60 * 60) // No earlier than 1 day - + do { try BGTaskScheduler.shared.submit(request) DDLogInfo("πŸ“± Scheduled full POS catalog sync for: \(request.earliestBeginDate?.description ?? "unknown")") @@ -105,15 +106,15 @@ final class POSCatalogSyncBackgroundTaskManager { ServiceLocator.analytics.track(event: .POSCatalogSync.schedulingError(error, taskType: "full")) } } - + /// Schedules an incremental sync (BGAppRefreshTask) /// Should be called more frequently for quick updates func scheduleIncrementalSync() { guard !Self.isRunningTests() else { return } - + let request = BGAppRefreshTaskRequest(identifier: Self.incrementalSyncIdentifier) request.earliestBeginDate = Date(timeIntervalSinceNow: 4 * 60 * 60) // No earlier than 4 hours - + do { try BGTaskScheduler.shared.submit(request) DDLogInfo("πŸ“± Scheduled incremental POS catalog sync for: \(request.earliestBeginDate?.description ?? "unknown")") @@ -123,72 +124,72 @@ final class POSCatalogSyncBackgroundTaskManager { ServiceLocator.analytics.track(event: .POSCatalogSync.schedulingError(error, taskType: "incremental")) } } - + // MARK: - Foreground Sync Support - + /// Starts a full catalog sync in the foreground that will continue in background /// Returns a task that can be monitored for progress func startForegroundFullSync() -> Task { DDLogInfo("πŸ“± Starting foreground POS catalog sync that will continue in background...") - + return Task { var currentSyncState = self.syncState currentSyncState.isFullSyncInProgress = true currentSyncState.lastFullSyncStartTime = Date() self.syncState = currentSyncState - + do { try await self.performFullCatalogSync() - + // Update successful completion state var completedState = self.syncState completedState.isFullSyncInProgress = false completedState.lastFullSyncCompletionTime = Date() self.syncState = completedState - + DDLogInfo("πŸ“± Foreground full POS catalog sync completed successfully") ServiceLocator.analytics.track(event: .POSCatalogSync.fullSyncCompleted(source: "foreground")) - + } catch { // Update error state but keep sync as in progress for potential resume var errorState = self.syncState errorState.lastFullSyncError = error.localizedDescription self.syncState = errorState - + DDLogError("⛔️ Foreground full POS catalog sync failed: \(error)") ServiceLocator.analytics.track(event: .POSCatalogSync.fullSyncError(error, source: "foreground")) throw error } } } - + // MARK: - App Launch Recovery - + /// Should be called on app launch to recover from incomplete syncs func recoverIncompletesyncs() { let currentState = syncState - + // Check for incomplete full sync that needs processing if currentState.isFullSyncInProgress, let downloadPath = currentState.pendingFullCatalogPath, FileManager.default.fileExists(atPath: downloadPath) { - + DDLogInfo("πŸ“± Recovering incomplete POS catalog sync from app launch") - + Task { do { try await self.processDownloadedCatalog(at: URL(fileURLWithPath: downloadPath)) - + // Update completion state var recoveredState = self.syncState recoveredState.isFullSyncInProgress = false recoveredState.pendingFullCatalogPath = nil recoveredState.lastFullSyncCompletionTime = Date() self.syncState = recoveredState - + DDLogInfo("πŸ“± Successfully recovered incomplete POS catalog sync") ServiceLocator.analytics.track(event: .POSCatalogSync.syncRecovered()) - + } catch { DDLogError("⛔️ Failed to recover incomplete POS catalog sync: \(error)") ServiceLocator.analytics.track(event: .POSCatalogSync.recoveryError(error)) @@ -201,32 +202,32 @@ final class POSCatalogSyncBackgroundTaskManager { // MARK: - Background Task Handlers private extension POSCatalogSyncBackgroundTaskManager { - + /// Handles full catalog sync using BGProcessingTask func handleFullCatalogSync(task: BGProcessingTask) { DDLogInfo("πŸ“± Starting full POS catalog sync in background...") - + // Schedule next full sync scheduleFullCatalogSync() - + let syncTask = Task { do { let startTime = Date() try await self.performFullCatalogSync() - + let duration = Date().timeIntervalSince(startTime) DDLogInfo("πŸ“± Full POS catalog sync completed in \(duration) seconds") ServiceLocator.analytics.track(event: .POSCatalogSync.fullSyncCompleted(source: "background", duration: duration)) - + task.setTaskCompleted(success: true) - + } catch { DDLogError("⛔️ Full POS catalog sync failed: \(error)") ServiceLocator.analytics.track(event: .POSCatalogSync.fullSyncError(error, source: "background")) task.setTaskCompleted(success: false) } } - + // Handle task expiration task.expirationHandler = { DDLogInfo("πŸ“± Full POS catalog sync task expired, cancelling...") @@ -234,32 +235,32 @@ private extension POSCatalogSyncBackgroundTaskManager { syncTask.cancel() } } - + /// Handles incremental sync using BGAppRefreshTask func handleIncrementalSync(task: BGAppRefreshTask) { DDLogInfo("πŸ“± Starting incremental POS catalog sync in background...") - + // Schedule next incremental sync scheduleIncrementalSync() - + let syncTask = Task { do { let startTime = Date() try await self.performIncrementalSync() - + let duration = Date().timeIntervalSince(startTime) DDLogInfo("πŸ“± Incremental POS catalog sync completed in \(duration) seconds") ServiceLocator.analytics.track(event: .POSCatalogSync.incrementalSyncCompleted(duration: duration)) - + task.setTaskCompleted(success: true) - + } catch { DDLogError("⛔️ Incremental POS catalog sync failed: \(error)") ServiceLocator.analytics.track(event: .POSCatalogSync.incrementalSyncError(error)) task.setTaskCompleted(success: false) } } - + // Handle task expiration task.expirationHandler = { DDLogInfo("πŸ“± Incremental POS catalog sync task expired, cancelling...") @@ -272,65 +273,234 @@ private extension POSCatalogSyncBackgroundTaskManager { // MARK: - Sync Implementation Stubs private extension POSCatalogSyncBackgroundTaskManager { - + /// Performs the full catalog download and processing - /// This would download the <100MB compressed JSON file + /// Downloads the 80MB JSON catalog from staging site func performFullCatalogSync() async throws { guard let siteID = stores.sessionManager.defaultStoreID else { throw POSCatalogSyncError.noActiveSite } - - DDLogInfo("πŸ“± Starting full catalog download for siteID: \(siteID)") - + + let startTime = Date() + DDLogInfo("πŸ“± [FULL-SYNC] Starting full catalog download for siteID: \(siteID)") + DDLogInfo("πŸ“± [FULL-SYNC] Available background execution time at start: \(formatBackgroundTime(UIApplication.shared.backgroundTimeRemaining))") + // Update state to indicate download in progress var currentState = syncState currentState.isFullSyncInProgress = true currentState.lastFullSyncStartTime = Date() syncState = currentState - - // TODO: Implement actual full catalog download - // This would: - // 1. Create background URLSession download task - // 2. Download compressed JSON catalog - // 3. Store file path in syncState.pendingFullCatalogPath - // 4. Process and insert into database - // 5. Update syncState completion - - throw POSCatalogSyncError.notImplemented("Full catalog sync implementation needed") + + do { + // Step 1: Download the 80MB JSON file + let downloadStartTime = Date() + let catalogURL = URL(string: "https://poslarge.mystagingwebsite.com/wp-content/uploads/pos-catalog.json")! + + DDLogInfo("πŸ“± [FULL-SYNC] Starting download from: \(catalogURL.absoluteString)") + + let (data, response) = try await URLSession.shared.data(from: catalogURL) + let downloadDuration = Date().timeIntervalSince(downloadStartTime) + let fileSizeMB = Double(data.count) / (1024 * 1024) + + DDLogInfo("πŸ“± [FULL-SYNC] Downloaded \(String(format: "%.1f", fileSizeMB))MB in \(String(format: "%.2f", downloadDuration)) seconds") + DDLogInfo("πŸ“± [FULL-SYNC] Background time remaining after download: \(formatBackgroundTime(UIApplication.shared.backgroundTimeRemaining))") + + guard let httpResponse = response as? HTTPURLResponse, + httpResponse.statusCode == 200 else { + throw POSCatalogSyncError.downloadFailed(URLError(.badServerResponse)) + } + + // Step 2: Parse JSON - simulate realistic parsing time + let parseStartTime = Date() + DDLogInfo("πŸ“± [FULL-SYNC] Starting JSON parsing...") + + // On older hardware, parsing 80MB JSON could take 10-20 seconds + // We'll simulate this since we don't actually need the parsed data yet + do { + let jsonObject = try JSONSerialization.jsonObject(with: data) + let parseDuration = Date().timeIntervalSince(parseStartTime) + DDLogInfo("πŸ“± [FULL-SYNC] JSON parsed in \(String(format: "%.2f", parseDuration)) seconds") + DDLogInfo("πŸ“± [FULL-SYNC] Background time remaining after parsing: \(formatBackgroundTime(UIApplication.shared.backgroundTimeRemaining))") + + // Simulate realistic processing time + try await simulateDatabaseInsertion() + + } catch { + DDLogError("⛔️ JSON parsing failed: \(error)") + throw POSCatalogSyncError.processingFailed(error) + } + + // Step 3: Update completion state + var completedState = syncState + completedState.isFullSyncInProgress = false + completedState.lastFullSyncCompletionTime = Date() + completedState.pendingFullCatalogPath = nil + syncState = completedState + + let totalDuration = Date().timeIntervalSince(startTime) + DDLogInfo("πŸ“± [FULL-SYNC] Full catalog sync completed in \(String(format: "%.2f", totalDuration)) seconds") + DDLogInfo("πŸ“± [FULL-SYNC] Background time remaining at completion: \(formatBackgroundTime(UIApplication.shared.backgroundTimeRemaining))") + + } catch { + DDLogError("⛔️ Full catalog sync failed: \(error)") + + // Update error state + var errorState = syncState + errorState.lastFullSyncError = error.localizedDescription + syncState = errorState + + throw error + } } - - /// Performs incremental sync using REST API endpoints - /// This would fetch only changed products/variations/coupons using timestamp + + /// Simulates realistic database insertion time for older hardware + /// Real implementation would insert parsed catalog data into Core Data + private func simulateDatabaseInsertion() async throws { + let insertStartTime = Date() + DDLogInfo("πŸ“± [FULL-SYNC] Starting database insertion simulation...") + + // Simulate inserting thousands of products/variations/coupons + // On older hardware with thousands of items, this could take 20-40 seconds + // We'll simulate a shorter time to see what actually gets done + for batch in 1...10 { + // Each batch represents processing ~1000 items + let batchStartTime = Date() + + // Check if we still have background time + let remainingTime = UIApplication.shared.backgroundTimeRemaining + DDLogInfo("πŸ“± [FULL-SYNC] Processing batch \(batch)/10, background time remaining: \(formatBackgroundTime(remainingTime))") + + if remainingTime < 5.0 { + DDLogWarn("⚠️ Background time running low, may not complete all processing") + } + + // Simulate processing time per batch (2-4 seconds per 1000 items on older hardware) + try await Task.sleep(for: .seconds(2.5)) + + let batchDuration = Date().timeIntervalSince(batchStartTime) + DDLogInfo("πŸ“± [FULL-SYNC] Batch \(batch) completed in \(String(format: "%.2f", batchDuration)) seconds") + + // Stop if we're running out of time + if UIApplication.shared.backgroundTimeRemaining < 3.0 { + DDLogWarn("⚠️ Stopping processing due to low background time remaining") + break + } + } + + let totalInsertDuration = Date().timeIntervalSince(insertStartTime) + DDLogInfo("πŸ“± [FULL-SYNC] Database insertion simulation completed in \(String(format: "%.2f", totalInsertDuration)) seconds") + } + + /// Performs incremental sync using PointOfSaleItemService + /// Fetches a few pages of products to update recent changes func performIncrementalSync() async throws { guard let siteID = stores.sessionManager.defaultStoreID else { throw POSCatalogSyncError.noActiveSite } - + + let startTime = Date() let lastSync = UserDefaults.standard[.lastPOSIncrementalSyncTimestamp] as? Date ?? Date.distantPast - DDLogInfo("πŸ“± Starting incremental sync for siteID: \(siteID), last sync: \(lastSync)") - - // TODO: Implement actual incremental sync - // This would: - // 1. Fetch products modified since lastSync timestamp - // 2. Fetch variations modified since lastSync timestamp - // 3. Fetch coupons modified since lastSync timestamp - // 4. Update local database - // 5. Update lastPOSIncrementalSyncTimestamp - - throw POSCatalogSyncError.notImplemented("Incremental sync implementation needed") + + DDLogInfo("πŸ“± [INCREMENTAL-SYNC] Starting incremental sync for siteID: \(siteID), last sync: \(lastSync)") + DDLogInfo("πŸ“± [INCREMENTAL-SYNC] Available background execution time at start: \(formatBackgroundTime(UIApplication.shared.backgroundTimeRemaining))") + + do { + // Create POS item service for fetching products + let currencySettings = ServiceLocator.currencySettings + let itemService = PointOfSaleItemService(currencySettings: currencySettings) + + // Create fetch strategy factory and then get the default strategy + let strategyFactory = PointOfSaleItemFetchStrategyFactory( + siteID: siteID, + credentials: stores.sessionManager.defaultCredentials + ) + let analytics = POSItemFetchAnalytics(itemType: .product) + let fetchStrategy = strategyFactory.defaultStrategy(analytics: analytics) + + var totalItemsFetched = 0 + let maxPages = 3 // Limit to 3 pages for incremental sync to stay within background time limits + + // Fetch a few pages of products to get recent updates + for pageNumber in 1...maxPages { + let pageStartTime = Date() + let remainingTime = UIApplication.shared.backgroundTimeRemaining + + DDLogInfo("πŸ“± [INCREMENTAL-SYNC] Fetching page \(pageNumber)/\(maxPages), background time remaining: \(formatBackgroundTime(remainingTime))") + + if remainingTime < 10.0 { + DDLogWarn("⚠️ [INCREMENTAL-SYNC] Background time running low, stopping incremental sync at page \(pageNumber)") + break + } + + do { + let pagedItems = try await itemService.providePointOfSaleItems( + pageNumber: pageNumber, + fetchStrategy: fetchStrategy + ) + + let pageDuration = Date().timeIntervalSince(pageStartTime) + totalItemsFetched += pagedItems.items.count + + DDLogInfo("πŸ“± [INCREMENTAL-SYNC] Page \(pageNumber): fetched \(pagedItems.items.count) items in \(String(format: "%.2f", pageDuration)) seconds") + DDLogInfo("πŸ“± [INCREMENTAL-SYNC] Background time remaining after page \(pageNumber): \(formatBackgroundTime(UIApplication.shared.backgroundTimeRemaining))") + + // Simulate database update time (would normally update Core Data here) + if !pagedItems.items.isEmpty { + try await simulateIncrementalDatabaseUpdate(itemCount: pagedItems.items.count) + } + + // Stop if no more pages + if !pagedItems.hasMorePages { + DDLogInfo("πŸ“± [INCREMENTAL-SYNC] Reached end of available pages at page \(pageNumber)") + break + } + + } catch { + DDLogError("⛔️ Failed to fetch page \(pageNumber): \(error)") + // Continue with next page rather than failing completely + } + } + + // Update last sync timestamp + UserDefaults.standard[.lastPOSIncrementalSyncTimestamp] = Date() + + let totalDuration = Date().timeIntervalSince(startTime) + DDLogInfo("πŸ“± [INCREMENTAL-SYNC] Incremental sync completed: \(totalItemsFetched) items in \(String(format: "%.2f", totalDuration)) seconds") + DDLogInfo("πŸ“± [INCREMENTAL-SYNC] Background time remaining at completion: \(formatBackgroundTime(UIApplication.shared.backgroundTimeRemaining))") + + } catch { + DDLogError("⛔️ Incremental sync failed: \(error)") + throw error + } } - + + /// Simulates database update time for incremental sync + /// Real implementation would update existing records or insert new ones + private func simulateIncrementalDatabaseUpdate(itemCount: Int) async throws { + let updateStartTime = Date() + + // Simulate time to update database records (much faster than full insert) + // Estimate 0.1 seconds per 100 items for incremental updates + let estimatedSeconds = Double(itemCount) / 1000.0 + let actualSeconds = max(0.1, min(estimatedSeconds, 2.0)) // Between 0.1 and 2 seconds + + try await Task.sleep(for: .seconds(actualSeconds)) + + let updateDuration = Date().timeIntervalSince(updateStartTime) + DDLogInfo("πŸ“± [INCREMENTAL-SYNC] Updated \(itemCount) items in database in \(String(format: "%.2f", updateDuration)) seconds") + } + /// Processes a downloaded catalog file (for recovery scenarios) func processDownloadedCatalog(at url: URL) async throws { DDLogInfo("πŸ“± Processing downloaded catalog at: \(url.path)") - + // TODO: Implement catalog processing // This would: // 1. Decompress the JSON file // 2. Parse catalog data // 3. Insert/update database records // 4. Clean up temporary file - + throw POSCatalogSyncError.notImplemented("Catalog processing implementation needed") } } @@ -344,7 +514,7 @@ private struct POSSyncState: Codable { var lastFullSyncCompletionTime: Date? var lastFullSyncError: String? var pendingFullCatalogPath: String? // Path to downloaded but unprocessed catalog - + var lastIncrementalSyncTime: Date? var lastIncrementalSyncError: String? } @@ -356,7 +526,7 @@ enum POSCatalogSyncError: LocalizedError { case notImplemented(String) case downloadFailed(Error) case processingFailed(Error) - + var errorDescription: String? { switch self { case .noActiveSite: @@ -377,10 +547,9 @@ private extension POSCatalogSyncBackgroundTaskManager { static func isRunningTests() -> Bool { return NSClassFromString("XCTestCase") != nil } + + /// Formats background time remaining for logging, avoiding huge numbers in foreground + func formatBackgroundTime(_ time: TimeInterval) -> String { + return time < Double.greatestFiniteMagnitude ? "\(String(format: "%.1f", time)) seconds" : "unlimited (foreground)" + } } - -// MARK: - UserDefaults Keys Extension - -private extension UserDefaults.Key { - static let lastPOSIncrementalSyncTimestamp = UserDefaults.Key("lastPOSIncrementalSyncTimestamp") -} \ No newline at end of file diff --git a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift new file mode 100644 index 00000000000..4c428cfb825 --- /dev/null +++ b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift @@ -0,0 +1,148 @@ +// +// POSCatalogSyncExample.swift +// WooCommerce +// +// Example code showing how to use the POS Catalog Sync system +// This file demonstrates the features and can be used for testing +// + +import Foundation +import UIKit + +// MARK: - Usage Examples + +/* + + ## Testing the Background Sync System + + To test the background sync functionality: + + 1. **Full Catalog Sync (BGProcessingTask)** + - Will attempt to download 80MB JSON from https://poslarge.mystagingwebsite.com/wp-content/uploads/pos-catalog.json + - Logs available background execution time at each step + - Simulates realistic database processing times + - Demonstrates how much can be completed within background time limits + + 2. **Incremental Sync (BGAppRefreshTask)** + - Uses PointOfSaleItemService to fetch a few pages of products + - Shows timing for API calls vs database updates + - Stops early if background time is running low + + ## Key Logging to Watch For: + + ``` + πŸ“± Available background execution time at start: X.X seconds + πŸ“± Downloaded 80.0MB in 12.34 seconds + πŸ“± Background time remaining after download: X.X seconds + πŸ“± JSON parsed in 5.67 seconds + πŸ“± Background time remaining after parsing: X.X seconds + πŸ“± Processing batch 3/10, background time remaining: 8.9 seconds + ⚠️ Background time running low, may not complete all processing + πŸ“± Full catalog sync completed in 45.67 seconds + πŸ“± Background time remaining at completion: 1.2 seconds + ``` + + ## Expected Results: + + - **BGProcessingTask**: Should get ~30-60 seconds typically, may complete 3-5 processing batches + - **BGAppRefreshTask**: Should get ~15-30 seconds, may complete 1-3 pages of incremental sync + - Both will log exactly how much time they receive and use + + ## Manual Testing: + + To manually test background sync behavior: + + ```swift + // Test foreground sync that continues in background + let syncTask = POSCatalogSyncController.shared.startForegroundFullSync() + + // Monitor the task and check logs for timing information + Task { + do { + try await syncTask?.value + print("βœ… Sync completed successfully") + } catch { + print("❌ Sync failed: \(error)") + } + } + + // Test scheduling + let syncManager = AppDelegate.shared.posCatalogSyncManager + syncManager.scheduleFullCatalogSync() + syncManager.scheduleIncrementalSync() + ``` + + ## Background Task Debugging: + + In Xcode, you can simulate background task execution: + 1. Set breakpoints in handleFullCatalogSync() or handleIncrementalSync() + 2. Run app in simulator + 3. Debug -> Simulate Background App Refresh + 4. Or use: e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.automattic.woocommerce.pos.fullCatalogSync"] + + This helps verify the background tasks are properly registered and can be invoked. + + */ + +// MARK: - Development Helper + +#if DEBUG +struct POSCatalogSyncDevelopmentHelper { + + /// Logs the current background sync state for debugging + static func logCurrentSyncState() { + // This could access the sync state if we exposed it + DDLogInfo("πŸ”§ [DEV] Current POS sync state check requested") + + // Log last sync times from UserDefaults + let lastIncremental = UserDefaults.standard.object(forKey: "lastPOSIncrementalSyncTimestamp") as? Date + DDLogInfo("πŸ”§ [DEV] Last incremental sync: \(lastIncremental?.description ?? "never")") + + // Log background refresh status + let refreshStatus = UIApplication.shared.backgroundRefreshStatus + switch refreshStatus { + case .available: + DDLogInfo("πŸ”§ [DEV] Background App Refresh: Available βœ…") + case .denied: + DDLogInfo("πŸ”§ [DEV] Background App Refresh: Denied ❌") + case .restricted: + DDLogInfo("πŸ”§ [DEV] Background App Refresh: Restricted ⚠️") + @unknown default: + DDLogInfo("πŸ”§ [DEV] Background App Refresh: Unknown") + } + + // Log remaining background time (only meaningful during background execution) + let remainingTime = UIApplication.shared.backgroundTimeRemaining + if remainingTime < Double.greatestFiniteMagnitude { + DDLogInfo("πŸ”§ [DEV] Background time remaining: \(String(format: "%.1f", remainingTime)) seconds") + } else { + DDLogInfo("πŸ”§ [DEV] App is running in foreground") + } + } + + /// Manually triggers sync operations for testing + static func triggerManualSync() { + DDLogInfo("πŸ”§ [DEV] Manually triggering POS catalog sync...") + + let controller = POSCatalogSyncController.shared + + // Start a foreground sync for immediate testing + let syncTask = controller.startForegroundFullSync() + + Task { + do { + try await syncTask?.value + DDLogInfo("πŸ”§ [DEV] Manual sync completed successfully") + } catch { + DDLogError("πŸ”§ [DEV] Manual sync failed: \(error)") + } + } + } + + /// Schedules fresh background tasks for testing + static func rescheduleBackgroundTasks() { + DDLogInfo("πŸ”§ [DEV] Rescheduling background tasks...") + POSCatalogSyncController.shared.rescheduleBackgroundSyncs() + } +} +#endif diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfiguration.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfiguration.swift index da2c77b3390..0930f08abf1 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfiguration.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfiguration.swift @@ -26,6 +26,46 @@ struct BetaFeaturesConfiguration: View { TitleAndToggleRow(title: feature.title, isOn: viewModel.isOn(feature: feature)) } } + + #if DEBUG + Section(footer: Text(Localization.posSyncTestingDescription)) { + Button(action: { + viewModel.triggerPOSFullSync() + }) { + HStack { + Text(Localization.triggerFullSync) + .foregroundColor(.primary) + Spacer() + Image(systemName: "arrow.down.circle") + .foregroundColor(.blue) + } + } + + Button(action: { + viewModel.triggerPOSIncrementalSync() + }) { + HStack { + Text(Localization.triggerIncrementalSync) + .foregroundColor(.primary) + Spacer() + Image(systemName: "arrow.clockwise.circle") + .foregroundColor(.blue) + } + } + + Button(action: { + viewModel.logPOSSyncStatus() + }) { + HStack { + Text(Localization.logSyncStatus) + .foregroundColor(.primary) + Spacer() + Image(systemName: "info.circle") + .foregroundColor(.green) + } + } + } + #endif } .background(Color(.listForeground(modal: false))) .listStyle(.grouped) @@ -35,6 +75,24 @@ struct BetaFeaturesConfiguration: View { private enum Localization { static let title = NSLocalizedString("Experimental Features", comment: "Experimental features navigation title") + + #if DEBUG + static let posSyncTestingDescription = NSLocalizedString( + "POS Catalog Sync Testing - Trigger background sync operations and view detailed logs to understand timing and behavior.", + comment: "Description for POS catalog sync testing section in beta features") + + static let triggerFullSync = NSLocalizedString( + "Trigger Full Catalog Sync", + comment: "Button to trigger a full POS catalog sync for testing") + + static let triggerIncrementalSync = NSLocalizedString( + "Trigger Incremental Sync", + comment: "Button to trigger an incremental POS catalog sync for testing") + + static let logSyncStatus = NSLocalizedString( + "Log Current Sync Status", + comment: "Button to log current POS sync status for debugging") + #endif } struct BetaFeaturesConfiguration_Previews: PreviewProvider { diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfigurationViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfigurationViewModel.swift index 73d9c30d9d0..be65266c0ea 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfigurationViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfigurationViewModel.swift @@ -22,4 +22,23 @@ final class BetaFeaturesConfigurationViewModel: ObservableObject { func isOn(feature: BetaFeature) -> Binding { appSettings.betaFeatureEnabledBinding(feature) } + + #if DEBUG + // MARK: - POS Catalog Sync Testing + + func triggerPOSFullSync() { + DDLogInfo("πŸ”§ [DEV] [FULL-SYNC] Triggering POS full catalog sync from beta features menu...") + POSCatalogSyncDevelopmentHelper.triggerManualSync() + } + + func triggerPOSIncrementalSync() { + DDLogInfo("πŸ”§ [DEV] [INCREMENTAL-SYNC] Triggering POS incremental sync from beta features menu...") + POSCatalogSyncController.shared.triggerIncrementalSync() + } + + func logPOSSyncStatus() { + DDLogInfo("πŸ”§ [DEV] [STATUS] Logging POS sync status from beta features menu...") + POSCatalogSyncDevelopmentHelper.logCurrentSyncState() + } + #endif } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index c61b52a1241..a927c9199fb 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -936,6 +936,7 @@ 20C3DB292E1E6FBA00CF7D3B /* PointOfSaleFlowButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20C3DB282E1E6FBA00CF7D3B /* PointOfSaleFlowButtonsView.swift */; }; 20C6D8402E4B981C0000B386 /* POSCatalogSyncBackgroundTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20C6D83E2E4B981C0000B386 /* POSCatalogSyncBackgroundTaskManager.swift */; }; 20C6D8412E4B981C0000B386 /* POSCatalogSyncController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20C6D83F2E4B981C0000B386 /* POSCatalogSyncController.swift */; }; + 20C6D8432E4B98F30000B386 /* POSCatalogSyncExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20C6D8422E4B98F30000B386 /* POSCatalogSyncExample.swift */; }; 20C6E7512CDE4AEA00CD124C /* ItemListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20C6E7502CDE4AEA00CD124C /* ItemListState.swift */; }; 20C909962D3151FA0013BCCF /* ItemListBaseItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20C909952D3151FA0013BCCF /* ItemListBaseItem.swift */; }; 20CC1EDB2AFA8381006BD429 /* InPersonPaymentsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CC1EDA2AFA8381006BD429 /* InPersonPaymentsMenu.swift */; }; @@ -4137,6 +4138,7 @@ 20C3DB282E1E6FBA00CF7D3B /* PointOfSaleFlowButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleFlowButtonsView.swift; sourceTree = ""; }; 20C6D83E2E4B981C0000B386 /* POSCatalogSyncBackgroundTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSCatalogSyncBackgroundTaskManager.swift; sourceTree = ""; }; 20C6D83F2E4B981C0000B386 /* POSCatalogSyncController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSCatalogSyncController.swift; sourceTree = ""; }; + 20C6D8422E4B98F30000B386 /* POSCatalogSyncExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSCatalogSyncExample.swift; sourceTree = ""; }; 20C6E7502CDE4AEA00CD124C /* ItemListState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListState.swift; sourceTree = ""; }; 20C909952D3151FA0013BCCF /* ItemListBaseItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListBaseItem.swift; sourceTree = ""; }; 20CC1EDA2AFA8381006BD429 /* InPersonPaymentsMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsMenu.swift; sourceTree = ""; }; @@ -8830,6 +8832,7 @@ 26DDA4A82C4839B8005FBEBF /* DashboardTimestampStore.swift */, 20C6D83E2E4B981C0000B386 /* POSCatalogSyncBackgroundTaskManager.swift */, 20C6D83F2E4B981C0000B386 /* POSCatalogSyncController.swift */, + 20C6D8422E4B98F30000B386 /* POSCatalogSyncExample.swift */, ); path = BackgroundTasks; sourceTree = ""; @@ -16732,6 +16735,7 @@ DE7E5E7F2B4BC52C002E28D2 /* MultiSelectionList.swift in Sources */, DA0DBE2F2C4FC61D00DF14C0 /* POSFloatingControlView.swift in Sources */, DE02ABAD2B55288D008E0AC4 /* BlazeTargetLocationSearchView.swift in Sources */, + 20C6D8432E4B98F30000B386 /* POSCatalogSyncExample.swift in Sources */, 45BBFBC3274FDA6400213001 /* HubMenuViewController.swift in Sources */, 20FA73882CDCC3A900554BE3 /* OrderDetailsSyncStateController.swift in Sources */, 451B1747258BD7B600836277 /* AddAttributeOptionsViewModel.swift in Sources */, From 5e686ed6de9471c1fceb104d645e9cf01659a89b Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Wed, 13 Aug 2025 12:08:52 +0100 Subject: [PATCH 03/10] Improve logging of tasks to investigate scheduling --- .../POSCatalogSyncBackgroundTaskManager.swift | 29 +++++++++++------ .../POSCatalogSyncExample.swift | 32 +++++++++++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift index 5f53c5028d7..171257c3b4a 100644 --- a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift +++ b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift @@ -63,7 +63,7 @@ final class POSCatalogSyncBackgroundTaskManager { DDLogInfo("πŸ“± Registering POS catalog background tasks...") // Register full catalog sync (BGProcessingTask) - BGTaskScheduler.shared.register(forTaskWithIdentifier: Self.fullCatalogSyncIdentifier, using: nil) { task in + let fullSyncRegistered = BGTaskScheduler.shared.register(forTaskWithIdentifier: Self.fullCatalogSyncIdentifier, using: nil) { task in guard let processingTask = task as? BGProcessingTask else { DDLogError("⛔️ Failed to cast to BGProcessingTask for full catalog sync") task.setTaskCompleted(success: false) @@ -71,9 +71,10 @@ final class POSCatalogSyncBackgroundTaskManager { } self.handleFullCatalogSync(task: processingTask) } + DDLogInfo("πŸ“± Full catalog sync registration: \(fullSyncRegistered ? "βœ… Success" : "❌ Failed")") - // Register incremental sync (BGAppRefreshTask) - BGTaskScheduler.shared.register(forTaskWithIdentifier: Self.incrementalSyncIdentifier, using: nil) { task in + // Register incremental sync (BGAppRefreshTask) + let incrementalSyncRegistered = BGTaskScheduler.shared.register(forTaskWithIdentifier: Self.incrementalSyncIdentifier, using: nil) { task in guard let refreshTask = task as? BGAppRefreshTask else { DDLogError("⛔️ Failed to cast to BGAppRefreshTask for incremental sync") task.setTaskCompleted(success: false) @@ -81,6 +82,7 @@ final class POSCatalogSyncBackgroundTaskManager { } self.handleIncrementalSync(task: refreshTask) } + DDLogInfo("πŸ“± Incremental sync registration: \(incrementalSyncRegistered ? "βœ… Success" : "❌ Failed")") DDLogInfo("πŸ“± Successfully registered POS catalog background tasks") } @@ -95,14 +97,19 @@ final class POSCatalogSyncBackgroundTaskManager { let request = BGProcessingTaskRequest(identifier: Self.fullCatalogSyncIdentifier) request.requiresNetworkConnectivity = true request.requiresExternalPower = false // Can run on battery but system will prefer charging - request.earliestBeginDate = Date(timeIntervalSinceNow: 24 * 60 * 60) // No earlier than 1 day + request.earliestBeginDate = Date(timeIntervalSinceNow: 20 * 60) // No earlier than 20 minutes do { try BGTaskScheduler.shared.submit(request) DDLogInfo("πŸ“± Scheduled full POS catalog sync for: \(request.earliestBeginDate?.description ?? "unknown")") + DDLogInfo("πŸ“± Full sync task config: network=\(request.requiresNetworkConnectivity), power=\(request.requiresExternalPower)") ServiceLocator.analytics.track(event: .POSCatalogSync.fullSyncScheduled()) } catch { DDLogError("⛔️ Failed to schedule full POS catalog sync: \(error)") + DDLogError("⛔️ Error type: \(type(of: error)), localizedDescription: \(error.localizedDescription)") + if let bgError = error as? BGTaskScheduler.Error { + DDLogError("⛔️ BGTaskScheduler error code: \(bgError.code)") + } ServiceLocator.analytics.track(event: .POSCatalogSync.schedulingError(error, taskType: "full")) } } @@ -113,7 +120,7 @@ final class POSCatalogSyncBackgroundTaskManager { guard !Self.isRunningTests() else { return } let request = BGAppRefreshTaskRequest(identifier: Self.incrementalSyncIdentifier) - request.earliestBeginDate = Date(timeIntervalSinceNow: 4 * 60 * 60) // No earlier than 4 hours + request.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // No earlier than 5 minutes do { try BGTaskScheduler.shared.submit(request) @@ -121,6 +128,10 @@ final class POSCatalogSyncBackgroundTaskManager { ServiceLocator.analytics.track(event: .POSCatalogSync.incrementalSyncScheduled()) } catch { DDLogError("⛔️ Failed to schedule incremental POS catalog sync: \(error)") + DDLogError("⛔️ Error type: \(type(of: error)), localizedDescription: \(error.localizedDescription)") + if let bgError = error as? BGTaskScheduler.Error { + DDLogError("⛔️ BGTaskScheduler error code: \(bgError.code)") + } ServiceLocator.analytics.track(event: .POSCatalogSync.schedulingError(error, taskType: "incremental")) } } @@ -207,8 +218,8 @@ private extension POSCatalogSyncBackgroundTaskManager { func handleFullCatalogSync(task: BGProcessingTask) { DDLogInfo("πŸ“± Starting full POS catalog sync in background...") - // Schedule next full sync - scheduleFullCatalogSync() + // Schedule next full sync - do this after task completion to avoid conflicts + defer { scheduleFullCatalogSync() } let syncTask = Task { do { @@ -240,8 +251,8 @@ private extension POSCatalogSyncBackgroundTaskManager { func handleIncrementalSync(task: BGAppRefreshTask) { DDLogInfo("πŸ“± Starting incremental POS catalog sync in background...") - // Schedule next incremental sync - scheduleIncrementalSync() + // Schedule next incremental sync - do this after task completion to avoid conflicts + defer { scheduleIncrementalSync() } let syncTask = Task { do { diff --git a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift index 4c428cfb825..66281b0044d 100644 --- a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift +++ b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift @@ -118,6 +118,38 @@ struct POSCatalogSyncDevelopmentHelper { } else { DDLogInfo("πŸ”§ [DEV] App is running in foreground") } + + // Log pending background tasks + logPendingBackgroundTasks() + } + + /// Logs all pending background tasks for debugging + private static func logPendingBackgroundTasks() { + DDLogInfo("πŸ”§ [DEV] Checking pending background tasks...") + + BGTaskScheduler.shared.getPendingTaskRequests { taskRequests in + DispatchQueue.main.async { + DDLogInfo("πŸ”§ [DEV] Total pending tasks: \(taskRequests.count)") + + for request in taskRequests { + let earliestDate = request.earliestBeginDate?.description ?? "immediately" + DDLogInfo("πŸ”§ [DEV] Pending task: \(request.identifier), earliest: \(earliestDate)") + + if let processingRequest = request as? BGProcessingTaskRequest { + DDLogInfo("πŸ”§ [DEV] Type: BGProcessingTask, network: \(processingRequest.requiresNetworkConnectivity), power: \(processingRequest.requiresExternalPower)") + } else if request is BGAppRefreshTaskRequest { + DDLogInfo("πŸ”§ [DEV] Type: BGAppRefreshTask") + } + } + + // Check specifically for our tasks + let ourFullSyncTask = taskRequests.first { $0.identifier == POSCatalogSyncBackgroundTaskManager.fullCatalogSyncIdentifier } + let ourIncrementalTask = taskRequests.first { $0.identifier == POSCatalogSyncBackgroundTaskManager.incrementalSyncIdentifier } + + DDLogInfo("πŸ”§ [DEV] Our full sync task pending: \(ourFullSyncTask != nil ? "βœ…" : "❌")") + DDLogInfo("πŸ”§ [DEV] Our incremental sync task pending: \(ourIncrementalTask != nil ? "βœ…" : "❌")") + } + } } /// Manually triggers sync operations for testing From 48ee2d14fcdf5c4583d7c2bafc0a8aa4a50ba2f8 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Wed, 13 Aug 2025 12:29:31 +0100 Subject: [PATCH 04/10] Add more debug logging for background tasks --- .../POSCatalogSyncBackgroundTaskManager.swift | 48 +++++++++++++++++-- .../POSCatalogSyncExample.swift | 34 +++++++++++++ 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift index 171257c3b4a..42dccf7835b 100644 --- a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift +++ b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift @@ -92,17 +92,37 @@ final class POSCatalogSyncBackgroundTaskManager { /// Schedules a full catalog sync (BGProcessingTask) /// Should be called weekly or daily, when device is idle with WiFi func scheduleFullCatalogSync() { - guard !Self.isRunningTests() else { return } + guard !Self.isRunningTests() else { + DDLogInfo("πŸ“± Skipping full sync scheduling - running tests") + return + } + + DDLogInfo("πŸ“± Attempting to schedule full catalog sync...") + + // Cancel any existing request first + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: Self.fullCatalogSyncIdentifier) + DDLogInfo("πŸ“± Cancelled existing full sync task if any") let request = BGProcessingTaskRequest(identifier: Self.fullCatalogSyncIdentifier) request.requiresNetworkConnectivity = true request.requiresExternalPower = false // Can run on battery but system will prefer charging request.earliestBeginDate = Date(timeIntervalSinceNow: 20 * 60) // No earlier than 20 minutes + + DDLogInfo("πŸ“± Full sync request created: ID=\(request.identifier), earliest=\(request.earliestBeginDate?.description ?? "now")") do { try BGTaskScheduler.shared.submit(request) - DDLogInfo("πŸ“± Scheduled full POS catalog sync for: \(request.earliestBeginDate?.description ?? "unknown")") + DDLogInfo("πŸ“± βœ… Successfully submitted full POS catalog sync") DDLogInfo("πŸ“± Full sync task config: network=\(request.requiresNetworkConnectivity), power=\(request.requiresExternalPower)") + + // Verify it was actually queued by checking immediately + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + BGTaskScheduler.shared.getPendingTaskRequests { taskRequests in + let ourTask = taskRequests.first { $0.identifier == Self.fullCatalogSyncIdentifier } + DDLogInfo("πŸ“± Full sync task verification: \(ourTask != nil ? "βœ… Found in pending queue" : "❌ NOT found in pending queue")") + } + } + ServiceLocator.analytics.track(event: .POSCatalogSync.fullSyncScheduled()) } catch { DDLogError("⛔️ Failed to schedule full POS catalog sync: \(error)") @@ -117,14 +137,34 @@ final class POSCatalogSyncBackgroundTaskManager { /// Schedules an incremental sync (BGAppRefreshTask) /// Should be called more frequently for quick updates func scheduleIncrementalSync() { - guard !Self.isRunningTests() else { return } + guard !Self.isRunningTests() else { + DDLogInfo("πŸ“± Skipping incremental sync scheduling - running tests") + return + } + + DDLogInfo("πŸ“± Attempting to schedule incremental sync...") + + // Cancel any existing request first + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: Self.incrementalSyncIdentifier) + DDLogInfo("πŸ“± Cancelled existing incremental sync task if any") let request = BGAppRefreshTaskRequest(identifier: Self.incrementalSyncIdentifier) request.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // No earlier than 5 minutes + + DDLogInfo("πŸ“± Incremental sync request created: ID=\(request.identifier), earliest=\(request.earliestBeginDate?.description ?? "now")") do { try BGTaskScheduler.shared.submit(request) - DDLogInfo("πŸ“± Scheduled incremental POS catalog sync for: \(request.earliestBeginDate?.description ?? "unknown")") + DDLogInfo("πŸ“± βœ… Successfully submitted incremental POS catalog sync") + + // Verify it was actually queued by checking immediately + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + BGTaskScheduler.shared.getPendingTaskRequests { taskRequests in + let ourTask = taskRequests.first { $0.identifier == Self.incrementalSyncIdentifier } + DDLogInfo("πŸ“± Incremental sync task verification: \(ourTask != nil ? "βœ… Found in pending queue" : "❌ NOT found in pending queue")") + } + } + ServiceLocator.analytics.track(event: .POSCatalogSync.incrementalSyncScheduled()) } catch { DDLogError("⛔️ Failed to schedule incremental POS catalog sync: \(error)") diff --git a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift index 66281b0044d..445be931e18 100644 --- a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift +++ b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift @@ -8,6 +8,7 @@ import Foundation import UIKit +import BackgroundTasks // MARK: - Usage Examples @@ -94,6 +95,9 @@ struct POSCatalogSyncDevelopmentHelper { // This could access the sync state if we exposed it DDLogInfo("πŸ”§ [DEV] Current POS sync state check requested") + // Log system conditions that might affect background tasks + logSystemConditions() + // Log last sync times from UserDefaults let lastIncremental = UserDefaults.standard.object(forKey: "lastPOSIncrementalSyncTimestamp") as? Date DDLogInfo("πŸ”§ [DEV] Last incremental sync: \(lastIncremental?.description ?? "never")") @@ -122,6 +126,36 @@ struct POSCatalogSyncDevelopmentHelper { // Log pending background tasks logPendingBackgroundTasks() } + + /// Logs system conditions that might affect background task scheduling + private static func logSystemConditions() { + DDLogInfo("πŸ”§ [DEV] === System Conditions Check ===") + + // Check Low Power Mode + if ProcessInfo.processInfo.isLowPowerModeEnabled { + DDLogInfo("πŸ”§ [DEV] ⚠️ Low Power Mode: ENABLED (severely limits background tasks)") + } else { + DDLogInfo("πŸ”§ [DEV] βœ… Low Power Mode: Disabled") + } + + // Check device state + let device = UIDevice.current + DDLogInfo("πŸ”§ [DEV] Battery level: \(device.batteryLevel * 100)%") + + switch device.batteryState { + case .charging, .full: + DDLogInfo("πŸ”§ [DEV] βœ… Battery: Charging/Full (good for BGProcessingTask)") + case .unplugged: + DDLogInfo("πŸ”§ [DEV] ⚠️ Battery: Unplugged (BGProcessingTask may be limited)") + case .unknown: + DDLogInfo("πŸ”§ [DEV] Battery state: Unknown") + @unknown default: + DDLogInfo("πŸ”§ [DEV] Battery state: Unknown") + } + + // Check network status (simplified) + DDLogInfo("πŸ”§ [DEV] === End System Conditions ===") + } /// Logs all pending background tasks for debugging private static func logPendingBackgroundTasks() { From c01b9ca511cd81d50a2824ea98873312251b8be7 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Wed, 13 Aug 2025 12:42:08 +0100 Subject: [PATCH 05/10] Further logging improvements, do full syncs with a 3 minute delay. --- .../POSCatalogSyncBackgroundTaskManager.swift | 24 ++++++++++++++++++- .../POSCatalogSyncExample.swift | 13 +++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift index 42dccf7835b..a399790f5de 100644 --- a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift +++ b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift @@ -108,7 +108,21 @@ final class POSCatalogSyncBackgroundTaskManager { request.requiresExternalPower = false // Can run on battery but system will prefer charging request.earliestBeginDate = Date(timeIntervalSinceNow: 20 * 60) // No earlier than 20 minutes - DDLogInfo("πŸ“± Full sync request created: ID=\(request.identifier), earliest=\(request.earliestBeginDate?.description ?? "now")") + DDLogInfo("πŸ“± Full sync request created:") + DDLogInfo("πŸ“± ID: \(request.identifier)") + DDLogInfo("πŸ“± Earliest: \(request.earliestBeginDate?.description ?? "immediately")") + DDLogInfo("πŸ“± Requires network: \(request.requiresNetworkConnectivity)") + DDLogInfo("πŸ“± Requires power: \(request.requiresExternalPower)") + DDLogInfo("πŸ“± Current time: \(Date().description)") + + // Let's also try with less restrictive requirements for testing + if Self.isRunningDebugBuild() { + DDLogInfo("πŸ“± πŸ§ͺ Using relaxed requirements for debug testing") + request.requiresExternalPower = false + request.requiresNetworkConnectivity = false // Try without network requirement + request.earliestBeginDate = Date(timeIntervalSinceNow: 2 * 60) // Shorter delay for testing + DDLogInfo("πŸ“± πŸ§ͺ Debug config: network=false, power=false, delay=2min") + } do { try BGTaskScheduler.shared.submit(request) @@ -599,6 +613,14 @@ private extension POSCatalogSyncBackgroundTaskManager { return NSClassFromString("XCTestCase") != nil } + static func isRunningDebugBuild() -> Bool { + #if DEBUG + return true + #else + return false + #endif + } + /// Formats background time remaining for logging, avoiding huge numbers in foreground func formatBackgroundTime(_ time: TimeInterval) -> String { return time < Double.greatestFiniteMagnitude ? "\(String(format: "%.1f", time)) seconds" : "unlimited (foreground)" diff --git a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift index 445be931e18..d1df6efb8ec 100644 --- a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift +++ b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift @@ -153,7 +153,18 @@ struct POSCatalogSyncDevelopmentHelper { DDLogInfo("πŸ”§ [DEV] Battery state: Unknown") } - // Check network status (simplified) + // Check app usage for BGProcessingTask scheduling + DDLogInfo("πŸ”§ [DEV] App state: \(UIApplication.shared.applicationState == .active ? "Active" : "Background")") + + // Check for conditions that affect BGProcessingTask specifically + if ProcessInfo.processInfo.isLowPowerModeEnabled { + DDLogInfo("πŸ”§ [DEV] 🚨 BGProcessingTask will likely be rejected due to Low Power Mode") + } + + if device.batteryState == .unplugged && device.batteryLevel < 0.5 { + DDLogInfo("πŸ”§ [DEV] ⚠️ BGProcessingTask may be rejected: unplugged + low battery") + } + DDLogInfo("πŸ”§ [DEV] === End System Conditions ===") } From 9b18cfbe4b8c3f0a53eaf2a620f5d594876a8cef Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Wed, 13 Aug 2025 12:45:35 +0100 Subject: [PATCH 06/10] Add button to force scheduling of a full sync task --- .../BetaFeaturesConfiguration.swift | 16 ++++++++++++++++ .../BetaFeaturesConfigurationViewModel.swift | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfiguration.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfiguration.swift index 0930f08abf1..fe9e7aef44d 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfiguration.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfiguration.swift @@ -64,6 +64,18 @@ struct BetaFeaturesConfiguration: View { .foregroundColor(.green) } } + + Button(action: { + viewModel.forceScheduleFullSync() + }) { + HStack { + Text(Localization.forceScheduleFullSync) + .foregroundColor(.primary) + Spacer() + Image(systemName: "calendar.badge.plus") + .foregroundColor(.orange) + } + } } #endif } @@ -92,6 +104,10 @@ private enum Localization { static let logSyncStatus = NSLocalizedString( "Log Current Sync Status", comment: "Button to log current POS sync status for debugging") + + static let forceScheduleFullSync = NSLocalizedString( + "Force Schedule Full Sync", + comment: "Button to force schedule full catalog sync task for debugging") #endif } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfigurationViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfigurationViewModel.swift index be65266c0ea..a2078a44d78 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfigurationViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfigurationViewModel.swift @@ -40,5 +40,10 @@ final class BetaFeaturesConfigurationViewModel: ObservableObject { DDLogInfo("πŸ”§ [DEV] [STATUS] Logging POS sync status from beta features menu...") POSCatalogSyncDevelopmentHelper.logCurrentSyncState() } + + func forceScheduleFullSync() { + DDLogInfo("πŸ”§ [DEV] [FORCE-SCHEDULE] Forcing full sync task to be scheduled...") + AppDelegate.shared.posCatalogSyncManager.scheduleFullCatalogSync() + } #endif } From 969dcfb392d5e07412f98805410df7c00148c8ec Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Wed, 13 Aug 2025 13:07:10 +0100 Subject: [PATCH 07/10] Add force-sync buttons for all and ensure that we can do BGProcessingTasks --- WooCommerce/Classes/AppDelegate.swift | 2 +- .../BetaFeaturesConfiguration.swift | 48 +++++++++++++++++++ .../BetaFeaturesConfigurationViewModel.swift | 17 +++++++ WooCommerce/Resources/Info.plist | 9 ++-- 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/WooCommerce/Classes/AppDelegate.swift b/WooCommerce/Classes/AppDelegate.swift index 16d5457b49f..c2281bed348 100644 --- a/WooCommerce/Classes/AppDelegate.swift +++ b/WooCommerce/Classes/AppDelegate.swift @@ -58,7 +58,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { /// Handles events to background refresh the app. /// - private let appRefreshHandler = BackgroundTaskRefreshDispatcher() + internal let appRefreshHandler = BackgroundTaskRefreshDispatcher() /// Manages POS catalog background synchronization. /// diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfiguration.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfiguration.swift index fe9e7aef44d..3b1599b5e6f 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfiguration.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfiguration.swift @@ -76,6 +76,42 @@ struct BetaFeaturesConfiguration: View { .foregroundColor(.orange) } } + + Button(action: { + viewModel.forceScheduleIncrementalSync() + }) { + HStack { + Text(Localization.forceScheduleIncrementalSync) + .foregroundColor(.primary) + Spacer() + Image(systemName: "calendar.badge.clock") + .foregroundColor(.orange) + } + } + + Button(action: { + viewModel.forceScheduleMainAppRefresh() + }) { + HStack { + Text(Localization.forceScheduleMainAppRefresh) + .foregroundColor(.primary) + Spacer() + Image(systemName: "calendar.circle") + .foregroundColor(.orange) + } + } + + Button(action: { + viewModel.forceScheduleAllTasks() + }) { + HStack { + Text(Localization.forceScheduleAllTasks) + .foregroundColor(.primary) + Spacer() + Image(systemName: "calendar.badge.exclamationmark") + .foregroundColor(.red) + } + } } #endif } @@ -108,6 +144,18 @@ private enum Localization { static let forceScheduleFullSync = NSLocalizedString( "Force Schedule Full Sync", comment: "Button to force schedule full catalog sync task for debugging") + + static let forceScheduleIncrementalSync = NSLocalizedString( + "Force Schedule Incremental Sync", + comment: "Button to force schedule incremental catalog sync task for debugging") + + static let forceScheduleMainAppRefresh = NSLocalizedString( + "Force Schedule Main App Refresh", + comment: "Button to force schedule main app refresh task for debugging") + + static let forceScheduleAllTasks = NSLocalizedString( + "Force Schedule ALL Tasks", + comment: "Button to force schedule all background tasks at once for debugging") #endif } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfigurationViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfigurationViewModel.swift index a2078a44d78..64ec9887204 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfigurationViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfigurationViewModel.swift @@ -45,5 +45,22 @@ final class BetaFeaturesConfigurationViewModel: ObservableObject { DDLogInfo("πŸ”§ [DEV] [FORCE-SCHEDULE] Forcing full sync task to be scheduled...") AppDelegate.shared.posCatalogSyncManager.scheduleFullCatalogSync() } + + func forceScheduleIncrementalSync() { + DDLogInfo("πŸ”§ [DEV] [FORCE-SCHEDULE] Forcing incremental sync task to be scheduled...") + AppDelegate.shared.posCatalogSyncManager.scheduleIncrementalSync() + } + + func forceScheduleMainAppRefresh() { + DDLogInfo("πŸ”§ [DEV] [FORCE-SCHEDULE] Forcing main app refresh task to be scheduled...") + AppDelegate.shared.appRefreshHandler.scheduleAppRefresh() + } + + func forceScheduleAllTasks() { + DDLogInfo("πŸ”§ [DEV] [FORCE-SCHEDULE] Forcing all background tasks to be scheduled...") + AppDelegate.shared.appRefreshHandler.scheduleAppRefresh() + AppDelegate.shared.posCatalogSyncManager.scheduleFullCatalogSync() + AppDelegate.shared.posCatalogSyncManager.scheduleIncrementalSync() + } #endif } diff --git a/WooCommerce/Resources/Info.plist b/WooCommerce/Resources/Info.plist index ef2baa650a9..b3b7fd9365f 100644 --- a/WooCommerce/Resources/Info.plist +++ b/WooCommerce/Resources/Info.plist @@ -8,6 +8,8 @@ com.automattic.woocommerce.pos.fullCatalogSync com.automattic.woocommerce.pos.incrementalSync + BuildConfigurationName + $(CONFIGURATION) CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -81,10 +83,10 @@ NSLocationWhenInUseUsageDescription Location access is required in order to accept payments. - NSPhotoLibraryUsageDescription - To save photos from camera for Product images, or to add photos or videos to your Products or support tickets. NSMicrophoneUsageDescription Woo uses your microphone to let you capture audio when recording videos for your store’s media library. + NSPhotoLibraryUsageDescription + To save photos from camera for Product images, or to add photos or videos to your Products or support tickets. NSUserActivityTypes UIAppFonts @@ -131,6 +133,7 @@ fetch remote-notification bluetooth-central + processing UILaunchStoryboardName LaunchScreen @@ -153,7 +156,5 @@ UIViewControllerBasedStatusBarAppearance - BuildConfigurationName - $(CONFIGURATION) From a5fc1112fcd0dbf811450fb9c5cc4715a02b8a98 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Wed, 13 Aug 2025 17:44:39 +0100 Subject: [PATCH 08/10] Add cancel buttons for tasks --- .../POSCatalogSyncBackgroundTaskManager.swift | 38 +++--- .../POSCatalogSyncController.swift | 6 +- .../POSCatalogSyncExample.swift | 16 +-- .../BetaFeaturesConfiguration.swift | 110 ++++++++++++++---- .../BetaFeaturesConfigurationViewModel.swift | 49 ++++++-- 5 files changed, 157 insertions(+), 62 deletions(-) diff --git a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift index a399790f5de..3ddb39e45d8 100644 --- a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift +++ b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift @@ -73,7 +73,7 @@ final class POSCatalogSyncBackgroundTaskManager { } DDLogInfo("πŸ“± Full catalog sync registration: \(fullSyncRegistered ? "βœ… Success" : "❌ Failed")") - // Register incremental sync (BGAppRefreshTask) + // Register incremental sync (BGAppRefreshTask) let incrementalSyncRegistered = BGTaskScheduler.shared.register(forTaskWithIdentifier: Self.incrementalSyncIdentifier, using: nil) { task in guard let refreshTask = task as? BGAppRefreshTask else { DDLogError("⛔️ Failed to cast to BGAppRefreshTask for incremental sync") @@ -92,13 +92,13 @@ final class POSCatalogSyncBackgroundTaskManager { /// Schedules a full catalog sync (BGProcessingTask) /// Should be called weekly or daily, when device is idle with WiFi func scheduleFullCatalogSync() { - guard !Self.isRunningTests() else { + guard !Self.isRunningTests() else { DDLogInfo("πŸ“± Skipping full sync scheduling - running tests") - return + return } DDLogInfo("πŸ“± Attempting to schedule full catalog sync...") - + // Cancel any existing request first BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: Self.fullCatalogSyncIdentifier) DDLogInfo("πŸ“± Cancelled existing full sync task if any") @@ -107,14 +107,14 @@ final class POSCatalogSyncBackgroundTaskManager { request.requiresNetworkConnectivity = true request.requiresExternalPower = false // Can run on battery but system will prefer charging request.earliestBeginDate = Date(timeIntervalSinceNow: 20 * 60) // No earlier than 20 minutes - + DDLogInfo("πŸ“± Full sync request created:") DDLogInfo("πŸ“± ID: \(request.identifier)") DDLogInfo("πŸ“± Earliest: \(request.earliestBeginDate?.description ?? "immediately")") DDLogInfo("πŸ“± Requires network: \(request.requiresNetworkConnectivity)") DDLogInfo("πŸ“± Requires power: \(request.requiresExternalPower)") DDLogInfo("πŸ“± Current time: \(Date().description)") - + // Let's also try with less restrictive requirements for testing if Self.isRunningDebugBuild() { DDLogInfo("πŸ“± πŸ§ͺ Using relaxed requirements for debug testing") @@ -128,7 +128,7 @@ final class POSCatalogSyncBackgroundTaskManager { try BGTaskScheduler.shared.submit(request) DDLogInfo("πŸ“± βœ… Successfully submitted full POS catalog sync") DDLogInfo("πŸ“± Full sync task config: network=\(request.requiresNetworkConnectivity), power=\(request.requiresExternalPower)") - + // Verify it was actually queued by checking immediately DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { BGTaskScheduler.shared.getPendingTaskRequests { taskRequests in @@ -136,7 +136,7 @@ final class POSCatalogSyncBackgroundTaskManager { DDLogInfo("πŸ“± Full sync task verification: \(ourTask != nil ? "βœ… Found in pending queue" : "❌ NOT found in pending queue")") } } - + ServiceLocator.analytics.track(event: .POSCatalogSync.fullSyncScheduled()) } catch { DDLogError("⛔️ Failed to schedule full POS catalog sync: \(error)") @@ -151,26 +151,26 @@ final class POSCatalogSyncBackgroundTaskManager { /// Schedules an incremental sync (BGAppRefreshTask) /// Should be called more frequently for quick updates func scheduleIncrementalSync() { - guard !Self.isRunningTests() else { + guard !Self.isRunningTests() else { DDLogInfo("πŸ“± Skipping incremental sync scheduling - running tests") - return + return } DDLogInfo("πŸ“± Attempting to schedule incremental sync...") - + // Cancel any existing request first BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: Self.incrementalSyncIdentifier) DDLogInfo("πŸ“± Cancelled existing incremental sync task if any") let request = BGAppRefreshTaskRequest(identifier: Self.incrementalSyncIdentifier) request.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // No earlier than 5 minutes - + DDLogInfo("πŸ“± Incremental sync request created: ID=\(request.identifier), earliest=\(request.earliestBeginDate?.description ?? "now")") do { try BGTaskScheduler.shared.submit(request) DDLogInfo("πŸ“± βœ… Successfully submitted incremental POS catalog sync") - + // Verify it was actually queued by checking immediately DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { BGTaskScheduler.shared.getPendingTaskRequests { taskRequests in @@ -178,7 +178,7 @@ final class POSCatalogSyncBackgroundTaskManager { DDLogInfo("πŸ“± Incremental sync task verification: \(ourTask != nil ? "βœ… Found in pending queue" : "❌ NOT found in pending queue")") } } - + ServiceLocator.analytics.track(event: .POSCatalogSync.incrementalSyncScheduled()) } catch { DDLogError("⛔️ Failed to schedule incremental POS catalog sync: \(error)") @@ -612,15 +612,15 @@ private extension POSCatalogSyncBackgroundTaskManager { static func isRunningTests() -> Bool { return NSClassFromString("XCTestCase") != nil } - + static func isRunningDebugBuild() -> Bool { - #if DEBUG +#if DEBUG return true - #else +#else return false - #endif +#endif } - + /// Formats background time remaining for logging, avoiding huge numbers in foreground func formatBackgroundTime(_ time: TimeInterval) -> String { return time < Double.greatestFiniteMagnitude ? "\(String(format: "%.1f", time)) seconds" : "unlimited (foreground)" diff --git a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncController.swift b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncController.swift index cb1971218c7..f2e9eea9db0 100644 --- a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncController.swift +++ b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncController.swift @@ -65,11 +65,11 @@ final class POSCatalogSyncController { // In a POS view controller when user first logs in: if POSCatalogSyncController.shared.isFullSyncInProgress == false { - let syncTask = POSCatalogSyncController.shared.startForegroundFullSync() - // Show progress UI and monitor syncTask as needed + let syncTask = POSCatalogSyncController.shared.startForegroundFullSync() + // Show progress UI and monitor syncTask as needed } // In settings when user changes sync preferences: POSCatalogSyncController.shared.rescheduleBackgroundSyncs() - */ \ No newline at end of file + */ diff --git a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift index d1df6efb8ec..a3a8c707b9c 100644 --- a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift +++ b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncExample.swift @@ -126,22 +126,22 @@ struct POSCatalogSyncDevelopmentHelper { // Log pending background tasks logPendingBackgroundTasks() } - + /// Logs system conditions that might affect background task scheduling private static func logSystemConditions() { DDLogInfo("πŸ”§ [DEV] === System Conditions Check ===") - + // Check Low Power Mode if ProcessInfo.processInfo.isLowPowerModeEnabled { DDLogInfo("πŸ”§ [DEV] ⚠️ Low Power Mode: ENABLED (severely limits background tasks)") } else { DDLogInfo("πŸ”§ [DEV] βœ… Low Power Mode: Disabled") } - + // Check device state let device = UIDevice.current DDLogInfo("πŸ”§ [DEV] Battery level: \(device.batteryLevel * 100)%") - + switch device.batteryState { case .charging, .full: DDLogInfo("πŸ”§ [DEV] βœ… Battery: Charging/Full (good for BGProcessingTask)") @@ -152,19 +152,19 @@ struct POSCatalogSyncDevelopmentHelper { @unknown default: DDLogInfo("πŸ”§ [DEV] Battery state: Unknown") } - + // Check app usage for BGProcessingTask scheduling DDLogInfo("πŸ”§ [DEV] App state: \(UIApplication.shared.applicationState == .active ? "Active" : "Background")") - + // Check for conditions that affect BGProcessingTask specifically if ProcessInfo.processInfo.isLowPowerModeEnabled { DDLogInfo("πŸ”§ [DEV] 🚨 BGProcessingTask will likely be rejected due to Low Power Mode") } - + if device.batteryState == .unplugged && device.batteryLevel < 0.5 { DDLogInfo("πŸ”§ [DEV] ⚠️ BGProcessingTask may be rejected: unplugged + low battery") } - + DDLogInfo("πŸ”§ [DEV] === End System Conditions ===") } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfiguration.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfiguration.swift index 3b1599b5e6f..2b90540ab08 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfiguration.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfiguration.swift @@ -26,8 +26,8 @@ struct BetaFeaturesConfiguration: View { TitleAndToggleRow(title: feature.title, isOn: viewModel.isOn(feature: feature)) } } - - #if DEBUG + +#if DEBUG Section(footer: Text(Localization.posSyncTestingDescription)) { Button(action: { viewModel.triggerPOSFullSync() @@ -40,7 +40,7 @@ struct BetaFeaturesConfiguration: View { .foregroundColor(.blue) } } - + Button(action: { viewModel.triggerPOSIncrementalSync() }) { @@ -52,7 +52,7 @@ struct BetaFeaturesConfiguration: View { .foregroundColor(.blue) } } - + Button(action: { viewModel.logPOSSyncStatus() }) { @@ -64,7 +64,7 @@ struct BetaFeaturesConfiguration: View { .foregroundColor(.green) } } - + Button(action: { viewModel.forceScheduleFullSync() }) { @@ -76,7 +76,7 @@ struct BetaFeaturesConfiguration: View { .foregroundColor(.orange) } } - + Button(action: { viewModel.forceScheduleIncrementalSync() }) { @@ -88,7 +88,7 @@ struct BetaFeaturesConfiguration: View { .foregroundColor(.orange) } } - + Button(action: { viewModel.forceScheduleMainAppRefresh() }) { @@ -100,7 +100,7 @@ struct BetaFeaturesConfiguration: View { .foregroundColor(.orange) } } - + Button(action: { viewModel.forceScheduleAllTasks() }) { @@ -113,7 +113,57 @@ struct BetaFeaturesConfiguration: View { } } } - #endif + + Section(footer: Text(Localization.cancelTasksDescription)) { + Button(action: { + viewModel.cancelAllBackgroundTasks() + }) { + HStack { + Text(Localization.cancelAllTasks) + .foregroundColor(.primary) + Spacer() + Image(systemName: "xmark.circle.fill") + .foregroundColor(.red) + } + } + + Button(action: { + viewModel.cancelFullSyncTask() + }) { + HStack { + Text(Localization.cancelFullSyncTask) + .foregroundColor(.primary) + Spacer() + Image(systemName: "xmark.circle") + .foregroundColor(.orange) + } + } + + Button(action: { + viewModel.cancelIncrementalSyncTask() + }) { + HStack { + Text(Localization.cancelIncrementalSyncTask) + .foregroundColor(.primary) + Spacer() + Image(systemName: "xmark.circle") + .foregroundColor(.orange) + } + } + + Button(action: { + viewModel.cancelMainAppRefreshTask() + }) { + HStack { + Text(Localization.cancelMainAppRefreshTask) + .foregroundColor(.primary) + Spacer() + Image(systemName: "xmark.circle") + .foregroundColor(.orange) + } + } + } +#endif } .background(Color(.listForeground(modal: false))) .listStyle(.grouped) @@ -123,40 +173,60 @@ struct BetaFeaturesConfiguration: View { private enum Localization { static let title = NSLocalizedString("Experimental Features", comment: "Experimental features navigation title") - - #if DEBUG + +#if DEBUG static let posSyncTestingDescription = NSLocalizedString( "POS Catalog Sync Testing - Trigger background sync operations and view detailed logs to understand timing and behavior.", comment: "Description for POS catalog sync testing section in beta features") - + static let triggerFullSync = NSLocalizedString( "Trigger Full Catalog Sync", comment: "Button to trigger a full POS catalog sync for testing") - + static let triggerIncrementalSync = NSLocalizedString( - "Trigger Incremental Sync", + "Trigger Incremental Sync", comment: "Button to trigger an incremental POS catalog sync for testing") - + static let logSyncStatus = NSLocalizedString( "Log Current Sync Status", comment: "Button to log current POS sync status for debugging") - + static let forceScheduleFullSync = NSLocalizedString( "Force Schedule Full Sync", comment: "Button to force schedule full catalog sync task for debugging") - + static let forceScheduleIncrementalSync = NSLocalizedString( "Force Schedule Incremental Sync", comment: "Button to force schedule incremental catalog sync task for debugging") - + static let forceScheduleMainAppRefresh = NSLocalizedString( "Force Schedule Main App Refresh", comment: "Button to force schedule main app refresh task for debugging") - + static let forceScheduleAllTasks = NSLocalizedString( "Force Schedule ALL Tasks", comment: "Button to force schedule all background tasks at once for debugging") - #endif + + static let cancelTasksDescription = NSLocalizedString( + "Cancel Background Tasks - Remove scheduled tasks from the iOS background task queue for testing purposes.", + comment: "Description for cancel background tasks section in beta features") + + static let cancelAllTasks = NSLocalizedString( + "Cancel ALL Tasks", + comment: "Button to cancel all scheduled background tasks") + + static let cancelFullSyncTask = NSLocalizedString( + "Cancel Full Sync Task", + comment: "Button to cancel full catalog sync background task") + + static let cancelIncrementalSyncTask = NSLocalizedString( + "Cancel Incremental Sync Task", + comment: "Button to cancel incremental sync background task") + + static let cancelMainAppRefreshTask = NSLocalizedString( + "Cancel Main App Refresh Task", + comment: "Button to cancel main app refresh background task") +#endif } struct BetaFeaturesConfiguration_Previews: PreviewProvider { diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfigurationViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfigurationViewModel.swift index 64ec9887204..e339c82ef8d 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfigurationViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/BetaFeaturesConfigurationViewModel.swift @@ -1,6 +1,7 @@ import SwiftUI import protocol Experiments.FeatureFlagService import struct Storage.GeneralAppSettingsStorage +import BackgroundTasks final class BetaFeaturesConfigurationViewModel: ObservableObject { @Published private(set) var availableFeatures: [BetaFeature] = [] @@ -13,8 +14,8 @@ final class BetaFeaturesConfigurationViewModel: ObservableObject { self.featureFlagService = featureFlagService availableFeatures = BetaFeature.allCases.filter { betaFeature in switch betaFeature { - case .viewAddOns: - return true + case .viewAddOns: + return true } } } @@ -22,45 +23,69 @@ final class BetaFeaturesConfigurationViewModel: ObservableObject { func isOn(feature: BetaFeature) -> Binding { appSettings.betaFeatureEnabledBinding(feature) } - - #if DEBUG + +#if DEBUG // MARK: - POS Catalog Sync Testing - + func triggerPOSFullSync() { DDLogInfo("πŸ”§ [DEV] [FULL-SYNC] Triggering POS full catalog sync from beta features menu...") POSCatalogSyncDevelopmentHelper.triggerManualSync() } - + func triggerPOSIncrementalSync() { DDLogInfo("πŸ”§ [DEV] [INCREMENTAL-SYNC] Triggering POS incremental sync from beta features menu...") POSCatalogSyncController.shared.triggerIncrementalSync() } - + func logPOSSyncStatus() { DDLogInfo("πŸ”§ [DEV] [STATUS] Logging POS sync status from beta features menu...") POSCatalogSyncDevelopmentHelper.logCurrentSyncState() } - + func forceScheduleFullSync() { DDLogInfo("πŸ”§ [DEV] [FORCE-SCHEDULE] Forcing full sync task to be scheduled...") AppDelegate.shared.posCatalogSyncManager.scheduleFullCatalogSync() } - + func forceScheduleIncrementalSync() { DDLogInfo("πŸ”§ [DEV] [FORCE-SCHEDULE] Forcing incremental sync task to be scheduled...") AppDelegate.shared.posCatalogSyncManager.scheduleIncrementalSync() } - + func forceScheduleMainAppRefresh() { DDLogInfo("πŸ”§ [DEV] [FORCE-SCHEDULE] Forcing main app refresh task to be scheduled...") AppDelegate.shared.appRefreshHandler.scheduleAppRefresh() } - + func forceScheduleAllTasks() { DDLogInfo("πŸ”§ [DEV] [FORCE-SCHEDULE] Forcing all background tasks to be scheduled...") AppDelegate.shared.appRefreshHandler.scheduleAppRefresh() AppDelegate.shared.posCatalogSyncManager.scheduleFullCatalogSync() AppDelegate.shared.posCatalogSyncManager.scheduleIncrementalSync() } - #endif + + // MARK: - Cancel Background Tasks + + func cancelFullSyncTask() { + DDLogInfo("πŸ”§ [DEV] [CANCEL] Cancelling full sync task...") + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: POSCatalogSyncBackgroundTaskManager.fullCatalogSyncIdentifier) + } + + func cancelIncrementalSyncTask() { + DDLogInfo("πŸ”§ [DEV] [CANCEL] Cancelling incremental sync task...") + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: POSCatalogSyncBackgroundTaskManager.incrementalSyncIdentifier) + } + + func cancelMainAppRefreshTask() { + DDLogInfo("πŸ”§ [DEV] [CANCEL] Cancelling main app refresh task...") + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundTaskRefreshDispatcher.taskIdentifier) + } + + func cancelAllBackgroundTasks() { + DDLogInfo("πŸ”§ [DEV] [CANCEL] Cancelling ALL background tasks...") + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundTaskRefreshDispatcher.taskIdentifier) + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: POSCatalogSyncBackgroundTaskManager.fullCatalogSyncIdentifier) + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: POSCatalogSyncBackgroundTaskManager.incrementalSyncIdentifier) + } +#endif } From df51b9da9eec92a1651983753829aed4a329a788 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Wed, 13 Aug 2025 17:50:32 +0100 Subject: [PATCH 09/10] Fix whitespace --- .../POSCatalogSyncController.swift | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncController.swift b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncController.swift index f2e9eea9db0..fb284bd1339 100644 --- a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncController.swift +++ b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncController.swift @@ -4,25 +4,25 @@ import Foundation /// This class serves as a bridge between the UI and the background sync manager. /// final class POSCatalogSyncController { - + /// Shared instance for app-wide access static let shared = POSCatalogSyncController() - + private init() {} - + /// Reference to the background sync manager from AppDelegate private var syncManager: POSCatalogSyncBackgroundTaskManager? { return AppDelegate.shared.posCatalogSyncManager } - + // MARK: - Public Interface - + /// Checks if a full catalog sync is currently in progress var isFullSyncInProgress: Bool { // This would check the sync state - implementation depends on exposing state from the manager return false // TODO: Implement by exposing state from POSCatalogSyncBackgroundTaskManager } - + /// Starts a foreground full catalog sync that continues in background /// Returns a task that can be used to monitor progress or cancellation @discardableResult @@ -31,29 +31,29 @@ final class POSCatalogSyncController { DDLogError("⛔️ POS catalog sync manager not available") return nil } - + DDLogInfo("πŸ“± Starting foreground POS catalog sync via controller...") return syncManager.startForegroundFullSync() } - + /// Manually triggers an incremental sync (typically not needed as it's scheduled automatically) func triggerIncrementalSync() { guard let syncManager = syncManager else { DDLogError("⛔️ POS catalog sync manager not available") return } - + syncManager.scheduleIncrementalSync() DDLogInfo("πŸ“± Manually triggered incremental POS catalog sync") } - + /// Re-schedules background syncs (useful after user settings changes) func rescheduleBackgroundSyncs() { guard let syncManager = syncManager else { DDLogError("⛔️ POS catalog sync manager not available") return } - + syncManager.scheduleFullCatalogSync() syncManager.scheduleIncrementalSync() DDLogInfo("πŸ“± Rescheduled POS catalog background syncs") @@ -62,14 +62,14 @@ final class POSCatalogSyncController { // MARK: - Usage Example /* - + // In a POS view controller when user first logs in: if POSCatalogSyncController.shared.isFullSyncInProgress == false { let syncTask = POSCatalogSyncController.shared.startForegroundFullSync() // Show progress UI and monitor syncTask as needed } - + // In settings when user changes sync preferences: POSCatalogSyncController.shared.rescheduleBackgroundSyncs() - + */ From 5431dfcd41ea52121299dd19f5a699de048affb5 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Wed, 13 Aug 2025 17:58:13 +0100 Subject: [PATCH 10/10] Remove relaxed requirements for full syncs --- .../POSCatalogSyncBackgroundTaskManager.swift | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift index 3ddb39e45d8..875e8bdd2bb 100644 --- a/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift +++ b/WooCommerce/Classes/Tools/BackgroundTasks/POSCatalogSyncBackgroundTaskManager.swift @@ -106,7 +106,7 @@ final class POSCatalogSyncBackgroundTaskManager { let request = BGProcessingTaskRequest(identifier: Self.fullCatalogSyncIdentifier) request.requiresNetworkConnectivity = true request.requiresExternalPower = false // Can run on battery but system will prefer charging - request.earliestBeginDate = Date(timeIntervalSinceNow: 20 * 60) // No earlier than 20 minutes + request.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60) // No earlier than 30 minutes DDLogInfo("πŸ“± Full sync request created:") DDLogInfo("πŸ“± ID: \(request.identifier)") @@ -115,15 +115,6 @@ final class POSCatalogSyncBackgroundTaskManager { DDLogInfo("πŸ“± Requires power: \(request.requiresExternalPower)") DDLogInfo("πŸ“± Current time: \(Date().description)") - // Let's also try with less restrictive requirements for testing - if Self.isRunningDebugBuild() { - DDLogInfo("πŸ“± πŸ§ͺ Using relaxed requirements for debug testing") - request.requiresExternalPower = false - request.requiresNetworkConnectivity = false // Try without network requirement - request.earliestBeginDate = Date(timeIntervalSinceNow: 2 * 60) // Shorter delay for testing - DDLogInfo("πŸ“± πŸ§ͺ Debug config: network=false, power=false, delay=2min") - } - do { try BGTaskScheduler.shared.submit(request) DDLogInfo("πŸ“± βœ… Successfully submitted full POS catalog sync")