diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index ba93be880e6..4c8c0ae15d6 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -7,6 +7,7 @@ - [*] POS: icon button with confirmation step used for clearing the cart [https://github.com/woocommerce/woocommerce-ios/pull/15829] - [*] Shipping Labels: Fixed a portion of layout issues caused by bigger accessibility content size categories. [https://github.com/woocommerce/woocommerce-ios/pull/15844] - [*] Shipping Labels: Enable the confirm button on the payment method sheet even when there are no changes. [https://github.com/woocommerce/woocommerce-ios/pull/15856] +- [*] Watch app: Fixed connection issue upon fresh install [https://github.com/woocommerce/woocommerce-ios/pull/15867] 22.7 ----- diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift b/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift index 7cbaa64acec..937fd161afb 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift @@ -1248,6 +1248,7 @@ enum WooAnalyticsStat: String { case watchAppOpened = "watch_app_opened" case watchStoreDataSynced = "watch_store_data_synced" case watchConnectingOpened = "watch_connecting_opened" + case watchSyncingFailed = "watch_syncing_failed" case watchMyStoreOpened = "watch_my_store_opened" case watchOrdersListOpened = "watch_orders_list_opened" case watchPushNotificationTapped = "watch_push_notification_tapped" diff --git a/WooCommerce/Classes/System/WatchDependenciesSynchronizer.swift b/WooCommerce/Classes/System/WatchDependenciesSynchronizer.swift index 229dc0d2458..d54d67a3104 100644 --- a/WooCommerce/Classes/System/WatchDependenciesSynchronizer.swift +++ b/WooCommerce/Classes/System/WatchDependenciesSynchronizer.swift @@ -1,6 +1,7 @@ import WatchConnectivity import Combine import Networking +import protocol WooFoundation.Analytics import class WooFoundation.CurrencySettings /// Type that syncs the necessary dependencies to the watch session. @@ -10,6 +11,8 @@ final class WatchDependenciesSynchronizer: NSObject, WCSessionDelegate { /// Current WatchKit Session private let watchSession: WCSession + private let analytics: Analytics + /// Subscriptions store for combine publishers /// private var subscriptions = Set() @@ -34,16 +37,15 @@ final class WatchDependenciesSynchronizer: NSObject, WCSessionDelegate { /// @Published var account: Account? - /// Tracks if the current watch session is active or not - /// - @Published private var isSessionActive: Bool = false - /// Toggle this value to force a credentials sync. /// @Published private var syncTrigger = false - init(watchSession: WCSession = WCSession.default, storedDependencies: WatchDependencies?) { + init(watchSession: WCSession = WCSession.default, + storedDependencies: WatchDependencies?, + analytics: Analytics = ServiceLocator.analytics) { self.watchSession = watchSession + self.analytics = analytics super.init() self.storeID = storedDependencies?.storeID @@ -80,9 +82,7 @@ final class WatchDependenciesSynchronizer: NSObject, WCSessionDelegate { let (storeID, storeName, credentials, currencySettings) = required let (enablesCrashReports, account) = configuration - guard let storeID, let storeName, let credentials else { - return nil - } + guard let storeID, let storeName, let credentials else { return nil } return .init(storeID: storeID, storeName: storeName, @@ -95,16 +95,30 @@ final class WatchDependenciesSynchronizer: NSObject, WCSessionDelegate { .debounce(for: 0.5, scheduler: DispatchQueue.main) // Syncs the dependencies to the paired counterpart when the session becomes available. - Publishers.CombineLatest3(watchDependencies, $isSessionActive, $syncTrigger) - .sink { [watchSession] dependencies, isSessionActive, forceSync in + watchDependencies.combineLatest($syncTrigger) + .sink { [weak self, watchSession] dependencies, forceSync in // Do not update the context if the session is not active, the watch is not paired or the watch app is not installed. - guard isSessionActive, watchSession.isPaired, watchSession.isWatchAppInstalled else { return } + guard watchSession.activationState == .activated, + watchSession.isPaired, + watchSession.isWatchAppInstalled else { + self?.analytics.track( + .watchSyncingFailed, + properties: [ + "session_active": watchSession.activationState == .activated, + "session_paired": watchSession.isPaired, + "watch_app_installed": watchSession.isWatchAppInstalled + ], + error: SyncError.watchSessionInactiveOrNotPaired + ) + return + } do { // If dependencies is nil, send an empty dictionary. This is most likely a logged out state guard let dependencies else { + self?.analytics.track(.watchSyncingFailed, withError: SyncError.noDependenciesFound) return try watchSession.updateApplicationContext([:]) } @@ -118,9 +132,11 @@ final class WatchDependenciesSynchronizer: NSObject, WCSessionDelegate { try watchSession.updateApplicationContext(jsonObject) } else { + self?.analytics.track(.watchSyncingFailed, withError: SyncError.encodingApplicationContextFailed) DDLogError("⛔️ Unable to encode watch dependencies for synchronization. Resulting object is not a dictionary") } } catch { + self?.analytics.track(.watchSyncingFailed, withError: error) DDLogError("⛔️ Error synchronizing credentials into watch session: \(error)") } } @@ -129,7 +145,6 @@ final class WatchDependenciesSynchronizer: NSObject, WCSessionDelegate { func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { DDLogInfo("🔵 WatchSession activated \(activationState)") - self.isSessionActive = activationState == .activated } func sessionDidBecomeInactive(_ session: WCSession) { @@ -138,7 +153,6 @@ final class WatchDependenciesSynchronizer: NSObject, WCSessionDelegate { func sessionDidDeactivate(_ session: WCSession) { // Try to guarantee an active session - self.isSessionActive = false watchSession.activate() } } @@ -149,13 +163,12 @@ extension WatchDependenciesSynchronizer { /// This is in order to not duplicate tracks configuration which involve quite a lot of information to be transmitted to the watch. /// func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) { - // The user info could contain a track event. Send it if we found one. guard let rawEvent = userInfo[WooConstants.watchTracksKey] as? String, let analyticEvent = WooAnalyticsStat(rawValue: rawEvent) else { return DDLogError("⛔️ Unsupported watch tracks event: \(userInfo)") } - ServiceLocator.analytics.track(analyticEvent) + analytics.track(analyticEvent) } } @@ -173,7 +186,14 @@ extension WatchDependenciesSynchronizer { guard message[WooConstants.watchSyncKey] as? Bool == true else { return DDLogError("⛔️ Unsupported sync request message: \(message)") } - syncTrigger.toggle() } } + +extension WatchDependenciesSynchronizer { + enum SyncError: Error { + case watchSessionInactiveOrNotPaired + case noDependenciesFound + case encodingApplicationContextFailed + } +} diff --git a/WooCommerce/Woo Watch App/ConnectView.swift b/WooCommerce/Woo Watch App/ConnectView.swift index aa12d0c581e..8d106affbca 100644 --- a/WooCommerce/Woo Watch App/ConnectView.swift +++ b/WooCommerce/Woo Watch App/ConnectView.swift @@ -7,6 +7,8 @@ struct ConnectView: View { @EnvironmentObject private var tracksProvider: WatchTracksProvider + @State private var didRequestSyncing = false + let synchronizer: PhoneDependenciesSynchronizer let message: String = Localization.connectMessage @@ -18,6 +20,12 @@ struct ConnectView: View { .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) + if didRequestSyncing { + Text(Localization.workaroundTip) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + Image(systemName: "bolt.fill") .renderingMode(.original) .resizable() @@ -26,6 +34,7 @@ struct ConnectView: View { Button(Localization.itsNotWorking) { synchronizer.requestCredentialSync() + didRequestSyncing = true } } } @@ -44,10 +53,15 @@ extension ConnectView { private enum Localization { static let connectMessage = AppLocalizedString( - "watch.connect.message", - value: "Open Woo on your iPhone, connect your store, and hold your Watch nearby", + "watch.connect.messageUpdated", + value: "Open Woo on your iPhone, log into your store, and hold your Watch nearby.", comment: "Info message when connecting your watch to the phone for the first time." ) + static let workaroundTip = AppLocalizedString( + "watch.connect.workaround", + value: "If the error persists, relaunch the app.", + comment: "Workaround when connecting the watch to the phone fails." + ) static let itsNotWorking = AppLocalizedString( "watch.connect.notworking.title", value: "It's not working",