From 454fb216dadbcb3f3380e28db226dbd906314e03 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Mon, 11 Mar 2024 23:56:15 +0100 Subject: [PATCH] Subscriptions: 20 - Subscription Caching (#2569) Task/Issue URL: https://app.asana.com/0/0/1206800657723184/f BSK PR: duckduckgo/BrowserServicesKit#710 Description: Bumps BSK to use subscription caching: Push Purchase Cancel update to Webview on user cancellation Minor presentation updates for sheets and subscription-related Settings cleanup Co-authored-by: Michal Smaga --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- DuckDuckGo/SettingsSubscriptionView.swift | 32 ++++++-------- DuckDuckGo/SettingsViewModel.swift | 44 ++++++++----------- ...scriptionPagesUseSubscriptionFeature.swift | 2 + .../SubscriptionSettingsViewModel.swift | 32 ++++++-------- submodules/privacy-reference-tests | 2 +- 7 files changed, 51 insertions(+), 67 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 891f03a26f..e1800adb4f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10044,7 +10044,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 122.0.0; + version = 122.1.0; }; }; B6F997C22B8F374300476735 /* XCRemoteSwiftPackageReference "apple-toolbox" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index faaf2e1be7..fde59987a6 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "83bcbbf0dace717db6e518e4d867d617c846a3b5", - "version" : "122.0.0" + "revision" : "4042a8e04396584566df24fb90c7b529bbe1c661", + "version" : "122.1.0" } }, { diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index a727b87dc0..846e20893a 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -94,6 +94,11 @@ struct SettingsSubscriptionView: View { action: { isShowingsubScriptionFlow = true }, isButton: true ) + // Subscription Restore + .sheet(isPresented: $isShowingsubScriptionFlow, + onDismiss: { Task { viewModel.onAppear() } }, + content: { SubscriptionFlowView(viewModel: subscriptionFlowViewModel).interactiveDismissDisabled() }) + SettingsCustomCell(content: { iHaveASubscriptionView }, action: { isShowingsubScriptionFlow = true @@ -139,6 +144,10 @@ struct SettingsSubscriptionView: View { subtitle: UserText.settingsPProDBPSubTitle, action: { isShowingDBP.toggle() }, isButton: true) + .sheet(isPresented: $isShowingDBP) { + SubscriptionPIRView() + } + } if viewModel.shouldShowITP { @@ -146,6 +155,10 @@ struct SettingsSubscriptionView: View { subtitle: UserText.settingsPProITRSubTitle, action: { isShowingITP.toggle() }, isButton: true) + .sheet(isPresented: $isShowingITP) { + SubscriptionITPView() + } + } NavigationLink(destination: SubscriptionSettingsView()) { @@ -153,12 +166,7 @@ struct SettingsSubscriptionView: View { } } - .sheet(isPresented: $isShowingDBP) { - SubscriptionPIRView() - } - .sheet(isPresented: $isShowingITP) { - SubscriptionITPView() - } + } var body: some View { @@ -181,18 +189,6 @@ struct SettingsSubscriptionView: View { } } - // Subscription Restore - .sheet(isPresented: $isShowingsubScriptionFlow) { - SubscriptionFlowView(viewModel: subscriptionFlowViewModel).interactiveDismissDisabled() - } - - - // Refresh subscription when dismissing the Subscription Flow - .onChange(of: isShowingsubScriptionFlow, perform: { value in - if !value { - Task { viewModel.onAppear() } - } - }) .onChange(of: viewModel.shouldNavigateToDBP, perform: { value in if value { diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index 7aaba476b6..f67e383194 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -248,8 +248,9 @@ extension SettingsViewModel { // This manual (re)initialization will go away once appSettings and // other dependencies are observable (Such as AppIcon and netP) // and we can use subscribers (Currently called from the view onAppear) - private func initState() { - self.state = SettingsState( + @MainActor + private func initState() async { + self.state = await SettingsState( appTheme: appSettings.currentThemeName, appIcon: AppIconManager.shared.appIcon, fireButtonAnimation: appSettings.currentFireButtonAnimation, @@ -275,15 +276,6 @@ extension SettingsViewModel { setupSubscribers() - #if SUBSCRIPTION - if #available(iOS 15, *) { - Task { - if state.subscription.enabled { - await setupSubscriptionEnvironment() - } - } - } - #endif } private func getNetworkProtectionState() -> SettingsState.NetworkProtection { @@ -297,14 +289,24 @@ extension SettingsViewModel { return SettingsState.NetworkProtection(enabled: enabled, status: "") } - private func getSubscriptionState() -> SettingsState.Subscription { + private func getSubscriptionState() async -> SettingsState.Subscription { var enabled = false var canPurchase = false - let hasActiveSubscription = Self.cachedHasActiveSubscription - #if SUBSCRIPTION + var hasActiveSubscription = false + +#if SUBSCRIPTION + if #available(iOS 15, *) { enabled = featureFlagger.isFeatureOn(.subscription) canPurchase = SubscriptionPurchaseEnvironment.canPurchase - #endif + await setupSubscriptionEnvironment() + if let token = AccountManager().accessToken { + let subscriptionResult = await SubscriptionService.getSubscription(accessToken: token) + if case .success(let subscription) = subscriptionResult { + hasActiveSubscription = subscription.isActive + } + } + } +#endif return SettingsState.Subscription(enabled: enabled, canPurchase: canPurchase, hasActiveSubscription: hasActiveSubscription) @@ -354,9 +356,6 @@ extension SettingsViewModel { case .success(let subscription) where subscription.isActive: - // Cache Subscription state - cacheSubscriptionState(active: true) - // Check entitlements and update UI accordingly let entitlements: [Entitlement.ProductName] = [.identityTheftRestoration, .dataBrokerProtection, .networkProtection] for entitlement in entitlements { @@ -382,18 +381,11 @@ extension SettingsViewModel { @available(iOS 15.0, *) private func signOutUser() { AccountManager().signOut() - cacheSubscriptionState(active: false) setupSubscriptionPurchaseOptions() } - private func cacheSubscriptionState(active: Bool) { - self.state.subscription.hasActiveSubscription = active - Self.cachedHasActiveSubscription = active - } - @available(iOS 15.0, *) private func setupSubscriptionPurchaseOptions() { - cacheSubscriptionState(active: false) PurchaseManager.shared.$availableProducts .receive(on: RunLoop.main) .sink { [weak self] products in @@ -470,7 +462,7 @@ extension SettingsViewModel { extension SettingsViewModel { func onAppear() { - initState() + Task { await initState() } Task { await MainActor.run { navigateOnAppear() } } } diff --git a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift index 8ad92a96ca..f2d5a01c27 100644 --- a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift @@ -225,6 +225,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec switch error { case .cancelledByUser: setTransactionError(.cancelledByUser) + await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "canceled")) + return nil case .accountCreationFailed: setTransactionError(.accountCreationFailed) case .activeSubscriptionAlreadyPresent: diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift index 5c65f88e98..102a365523 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift @@ -56,24 +56,16 @@ final class SubscriptionSettingsViewModel: ObservableObject { }() @MainActor - func fetchAndUpdateSubscriptionDetails() { + func fetchAndUpdateSubscriptionDetails(cachePolicy: SubscriptionService.CachePolicy = .returnCacheDataElseLoad) { Task { - guard let token = accountManager.accessToken else { return } - - if let cachedDate = SubscriptionService.cachedGetSubscriptionResponse?.expiresOrRenewsAt, - let cachedStatus = SubscriptionService.cachedGetSubscriptionResponse?.status, - let productID = SubscriptionService.cachedGetSubscriptionResponse?.productId { - updateSubscriptionDetails(status: cachedStatus, date: cachedDate, product: productID) - } - - if case .success(let subscription) = await SubscriptionService.getSubscription(accessToken: token) { - if !subscription.isActive { - AccountManager().signOut() - shouldDismissView = true - return - } else { - updateSubscriptionDetails(status: subscription.status, date: subscription.expiresOrRenewsAt, product: subscription.productId) - } + guard let token = self.accountManager.accessToken else { return } + let subscriptionResult = await SubscriptionService.getSubscription(accessToken: token, cachePolicy: cachePolicy) + switch subscriptionResult { + case .success(let subscription): + updateSubscriptionDetails(status: subscription.status, date: subscription.expiresOrRenewsAt, product: subscription.productId) + case .failure(let error): + AccountManager().signOut() + shouldDismissView = true } } } @@ -86,11 +78,13 @@ final class SubscriptionSettingsViewModel: ObservableObject { } } + // Re-fetch subscription from server ignoring cache + // This ensure that if the user changed something on the Apple view, state will be updated private func setupSubscriptionUpdater() { subscriptionUpdateTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { [weak self] _ in guard let strongSelf = self else { return } Task { - await strongSelf.fetchAndUpdateSubscriptionDetails() + await strongSelf.fetchAndUpdateSubscriptionDetails(cachePolicy: .reloadIgnoringLocalCacheData) } } } @@ -104,7 +98,7 @@ final class SubscriptionSettingsViewModel: ObservableObject { func removeSubscription() { AccountManager().signOut() - let messageView = ActionMessageView() + _ = ActionMessageView() ActionMessageView.present(message: UserText.subscriptionRemovalConfirmation, presentationLocation: .withoutBottomBar) } diff --git a/submodules/privacy-reference-tests b/submodules/privacy-reference-tests index 6b7ad1e7f1..a603ff9af2 160000 --- a/submodules/privacy-reference-tests +++ b/submodules/privacy-reference-tests @@ -1 +1 @@ -Subproject commit 6b7ad1e7f15270f9dfeb58a272199f4d57c3eb22 +Subproject commit a603ff9af22ca3ff7ce2e7ffbfe18c447d9f23e8