From 40aabe9166d6213d17db2f7b2d07c69eef0fd95d Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 17 Jan 2024 19:34:32 +0100 Subject: [PATCH 01/28] SubscriptionRestore View --- DuckDuckGo.xcodeproj/project.pbxproj | 8 + .../Platform-Apple-16.imageset/Contents.json | 15 ++ .../Platform-Apple-16.svg | 10 ++ .../SubscriptionRestoreViewModel.swift | 28 ++++ .../Views/SubscriptionFlowView.swift | 2 +- .../Views/SubscriptionRestoreView.swift | 155 ++++++++++++++++++ .../Views/SubscriptionSettingsView.swift | 1 + 7 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 DuckDuckGo/Assets.xcassets/Platform-Apple-16.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Platform-Apple-16.imageset/Platform-Apple-16.svg create mode 100644 DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift create mode 100644 DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7b0e2e27d2..62d06f8f59 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -778,6 +778,8 @@ D664C7CC2B289AA200CBFA76 /* SubscriptionPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7B32B289AA000CBFA76 /* SubscriptionPagesUserScript.swift */; }; D664C7CE2B289AA200CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7B52B289AA000CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift */; }; D664C7DD2B28A02800CBFA76 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D664C7DC2B28A02800CBFA76 /* StoreKit.framework */; }; + D68DF81C2B58302E0023DBEA /* SubscriptionRestoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */; }; + D68DF81E2B5830380023DBEA /* SubscriptionRestoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */; }; D69FBF762B28BE3600B505F1 /* SettingsSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69FBF752B28BE3600B505F1 /* SettingsSubscriptionView.swift */; }; D6D12C9F2B291CA90054390C /* URL+Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C8B2B291CA90054390C /* URL+Subscription.swift */; }; D6D12CA02B291CA90054390C /* SubscriptionPurchaseEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C8C2B291CA90054390C /* SubscriptionPurchaseEnvironment.swift */; }; @@ -2419,6 +2421,8 @@ D664C7B32B289AA000CBFA76 /* SubscriptionPagesUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionPagesUserScript.swift; sourceTree = ""; }; D664C7B52B289AA000CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionPagesUseSubscriptionFeature.swift; sourceTree = ""; }; D664C7DC2B28A02800CBFA76 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRestoreView.swift; sourceTree = ""; }; + D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRestoreViewModel.swift; sourceTree = ""; }; D69FBF752B28BE3600B505F1 /* SettingsSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSubscriptionView.swift; sourceTree = ""; }; D6D12C8B2B291CA90054390C /* URL+Subscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+Subscription.swift"; sourceTree = ""; }; D6D12C8C2B291CA90054390C /* SubscriptionPurchaseEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionPurchaseEnvironment.swift; sourceTree = ""; }; @@ -4525,6 +4529,7 @@ children = ( D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */, D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */, + D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -4544,6 +4549,7 @@ D664C7AD2B289AA000CBFA76 /* PurchaseInProgressView.swift */, D664C7AE2B289AA000CBFA76 /* SubscriptionFlowView.swift */, D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */, + D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */, ); path = Views; sourceTree = ""; @@ -6502,6 +6508,7 @@ F1DE78581E5CAE350058895A /* TabViewGridCell.swift in Sources */, 984D035824ACCC6F0066CFB8 /* TabViewListCell.swift in Sources */, B6BA95C328891E33004ABA20 /* BrowsingMenuAnimator.swift in Sources */, + D68DF81C2B58302E0023DBEA /* SubscriptionRestoreView.swift in Sources */, D664C7CE2B289AA200CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift in Sources */, EE9D68DC2AE16AE100B55EF4 /* NotificationsAuthorizationController.swift in Sources */, AA3D854923DA1DFB00788410 /* AppIcon.swift in Sources */, @@ -6624,6 +6631,7 @@ 98999D5922FDA41500CBBE1B /* BasicAuthenticationAlert.swift in Sources */, C13B32D22A0E750700A59236 /* AutofillSettingStatus.swift in Sources */, D6D12CA52B291CAA0054390C /* AppStorePurchaseFlow.swift in Sources */, + D68DF81E2B5830380023DBEA /* SubscriptionRestoreViewModel.swift in Sources */, F4F6DFB426E6B63700ED7E12 /* BookmarkFolderCell.swift in Sources */, D6F93E3E2B50A8A0004C268D /* SubscriptionSettingsView.swift in Sources */, 851B12CC22369931004781BC /* AtbAndVariantCleanup.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Platform-Apple-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Platform-Apple-16.imageset/Contents.json new file mode 100644 index 0000000000..16a3bd837a --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Platform-Apple-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Platform-Apple-16.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/Platform-Apple-16.imageset/Platform-Apple-16.svg b/DuckDuckGo/Assets.xcassets/Platform-Apple-16.imageset/Platform-Apple-16.svg new file mode 100644 index 0000000000..4e50b8f0d1 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Platform-Apple-16.imageset/Platform-Apple-16.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift new file mode 100644 index 0000000000..1b2a37cd58 --- /dev/null +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -0,0 +1,28 @@ +// +// SubscriptionRestoreViewModel.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +#if SUBSCRIPTION +@available(iOS 15.0, *) +final class SubscriptionRestoreViewModel: ObservableObject { + + +} +#endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index 07d7571e0a..f730d19d4c 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -74,7 +74,7 @@ struct SubscriptionFlowView: View { message: Text(UserText.subscriptionFoundText), primaryButton: .cancel(Text(UserText.subscriptionFoundCancel)) { }, - secondaryButton: .default(Text(UserText.subscriptionFoundCancel)) { + secondaryButton: .default(Text(UserText.subscriptionFoundRestore)) { viewModel.restoreAppstoreTransaction() } ) diff --git a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift new file mode 100644 index 0000000000..32ab7c0a79 --- /dev/null +++ b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift @@ -0,0 +1,155 @@ +// +// SubscriptionRestoreView.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI +import DesignResourcesKit + +#if SUBSCRIPTION +@available(iOS 15.0, *) +struct SubscriptionRestoreView: View { + + @ObservedObject var viewModel: SubscriptionRestoreViewModel + @State private var expandedItemId: Int = 0 + + private enum Constants { + static let heroImage = "SyncTurnOnSyncHero" + static let appleIDIcon = "Platform-Apple-16" + static let emailIcon = "Email-16" + static let headerLineSpacing = 10.0 + static let openIndicator = "chevron.up" + static let closedIndicator = "chevron.down" + static let buttonCornerRadius = 8.0 + static let buttonInsets = EdgeInsets(top: 10.0, leading: 16.0, bottom: 10.0, trailing: 16.0) + static let cellLineSpacing = 12.0 + static let cellPadding = 4.0 + static let headerPadding = EdgeInsets(top: 16.0, leading: 16.0, bottom: 0, trailing: 16.0) + } + + private var listItems: [ListItem] { + [ + .init(id: 0, + content: getCellTitle(icon: Constants.appleIDIcon, + text: "Apple ID"), + expandedContent: getCellContent(description: "Restore your purchase to activate your subscription on this device.", + buttonText: "Restore", + buttonAction: {})), + .init(id: 1, + content: getCellTitle(icon: Constants.emailIcon, + text: "Email"), + expandedContent: getCellContent(description: "Use your email to activate your subscription on this device.", + buttonText: "Enter Email", + buttonAction: {})), + + ] + } + + private func getCellTitle(icon: String, text: String) -> AnyView { + AnyView( + HStack { + Image(icon) + Text(text) + .daxBodyRegular() + .foregroundColor(Color(designSystemColor: .textPrimary)) + } + ) + } + + private func getCellContent(description: String, buttonText: String, buttonAction: @escaping () -> Void) -> AnyView { + AnyView( + VStack(alignment: .leading) { + Text(description) + .daxSubheadRegular() + .foregroundColor(Color(designSystemColor: .textSecondary)) + getCellButton(buttonText: buttonText, action: buttonAction) + } + ) + } + + private func getCellButton(buttonText: String, action: @escaping () -> Void) -> AnyView { + AnyView( + Button(action: action, label: { + Text(buttonText) + .daxButton() + .padding(Constants.buttonInsets) + .foregroundColor(.white) + .overlay( + RoundedRectangle(cornerRadius: Constants.buttonCornerRadius) + .stroke(Color.clear, lineWidth: 1) + ) + }) + .background(Color(designSystemColor: .accent)) + .cornerRadius(Constants.buttonCornerRadius) + .padding(.top, Constants.cellPadding) + ) + } + + + struct ListItem { + let id: Int + let content: AnyView + let expandedContent: AnyView + } + + var body: some View { + VStack { + VStack(spacing: Constants.headerLineSpacing) { + Image(Constants.heroImage) + Text("Activate your subscription on this device") + .daxHeadline() + .multilineTextAlignment(.center) + .foregroundColor(Color(designSystemColor: .textPrimary)) + Text("Access your Privacy Pro subscription on this device via Apple ID or an email address.") + .daxFootnoteRegular() + .foregroundColor(Color(designSystemColor: .textSecondary)) + .multilineTextAlignment(.center) + } + .padding(Constants.headerPadding) + List { + Section { + ForEach(Array(zip(listItems.indices, listItems)), id: \.1.id) { _, item in + VStack(alignment: .leading, spacing: Constants.cellLineSpacing) { + HStack { + item.content + Spacer() + Image(systemName: expandedItemId == item.id ? Constants.openIndicator : Constants.closedIndicator) + .foregroundColor(Color(designSystemColor: .textPrimary)) + } + .contentShape(Rectangle()) + .onTapGesture { + expandedItemId = expandedItemId == item.id ? 0 : item.id + } + if expandedItemId == item.id { + item.expandedContent + } + }.padding(Constants.cellPadding) + } + } + } + }.background(Color(designSystemColor: .container)) + } +} + +@available(iOS 15.0, *) +struct SubscriptionRestoreView_Previews: PreviewProvider { + static var previews: some View { + SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel()) + } +} +#endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift index 769041e705..c10bd87f44 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift @@ -80,6 +80,7 @@ struct SubscriptionSettingsView: View { } } .navigationTitle(UserText.settingsPProManageSubscription) + .applyInsetGroupedListStyle() // Remove subscription .alert(isPresented: $viewModel.shouldDisplayRemovalNotice) { From 343fbdcf71c424ea94ef35a330c0194a43896a7b Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Thu, 18 Jan 2024 02:41:01 +0100 Subject: [PATCH 02/28] Restore subscription via Apple ID --- ...scriptionPagesUseSubscriptionFeature.swift | 2 + .../ViewModel/SubscriptionFlowViewModel.swift | 9 +- .../SubscriptionRestoreViewModel.swift | 40 +++++ .../Subscription/Views/HeadlessWebView.swift | 9 +- .../Views/PurchaseInProgressView.swift | 2 +- .../Views/SubscriptionFlowView.swift | 15 +- .../Views/SubscriptionRestoreView.swift | 158 ++++++++++++------ DuckDuckGo/UserText.swift | 28 +++- DuckDuckGo/en.lproj/Localizable.strings | 65 +++++-- 9 files changed, 254 insertions(+), 74 deletions(-) diff --git a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift index 04f6d26d95..ba40e5890e 100644 --- a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift @@ -66,6 +66,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec @Published var transactionStatus: TransactionStatus = .idle @Published var hasActiveSubscription = false @Published var purchaseError: AppStorePurchaseFlow.Error? + @Published var activateSubscription: Bool = false var broker: UserScriptMessageBroker? @@ -221,6 +222,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec } func activateSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { + activateSubscription = true return nil } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index 440b4060de..6fee092c1c 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -39,6 +39,7 @@ final class SubscriptionFlowViewModel: ObservableObject { @Published var hasActiveSubscription = false @Published var transactionStatus: SubscriptionPagesUseSubscriptionFeature.TransactionStatus = .idle @Published var shouldReloadWebview = false + @Published var activatingSubscription = false init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), @@ -66,10 +67,13 @@ final class SubscriptionFlowViewModel: ObservableObject { } .store(in: &cancellables) - subFeature.$hasActiveSubscription + subFeature.$activateSubscription .receive(on: DispatchQueue.main) .sink { [weak self] value in - self?.hasActiveSubscription = value + if value { + self?.subFeature.activateSubscription = false + self?.activatingSubscription = true + } } .store(in: &cancellables) } @@ -81,6 +85,7 @@ final class SubscriptionFlowViewModel: ObservableObject { func initializeViewData() async { await self.setupTransactionObserver() + await MainActor.run { shouldReloadWebview = true } } func restoreAppstoreTransaction() { diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift index 1b2a37cd58..8b76e2b9d3 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -18,11 +18,51 @@ // import Foundation +import UserScript +import Combine +import Core #if SUBSCRIPTION @available(iOS 15.0, *) final class SubscriptionRestoreViewModel: ObservableObject { + let userScript: SubscriptionPagesUserScript + let subFeature: SubscriptionPagesUseSubscriptionFeature + let purchaseManager: PurchaseManager + + enum SubscriptionActivationResult { + case unknown, activated, notFound, error + } + + @Published var transactionStatus: SubscriptionPagesUseSubscriptionFeature.TransactionStatus = .idle + @Published var activationResult: SubscriptionActivationResult = .unknown + + init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), + subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), + purchaseManager: PurchaseManager = PurchaseManager.shared) { + self.userScript = userScript + self.subFeature = subFeature + self.purchaseManager = purchaseManager + } + + @MainActor + private func setTransactionStatus(_ status: SubscriptionPagesUseSubscriptionFeature.TransactionStatus) { + self.transactionStatus = status + } + + @MainActor + func restoreAppstoreTransaction() { + Task { + transactionStatus = .restoring + activationResult = .unknown + if await subFeature.restoreAccountFromAppStorePurchase() { + activationResult = .activated + } else { + activationResult = .notFound + } + transactionStatus = .idle + } + } } #endif diff --git a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift index 44fd4c026b..2b3fafaeee 100644 --- a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift +++ b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift @@ -55,7 +55,14 @@ struct HeadlessWebview: UIViewRepresentable { func updateUIView(_ uiView: WKWebView, context: Context) { if shouldReload { - uiView.reload() + reloadView(uiView: uiView) + } + } + + @MainActor + func reloadView(uiView: WKWebView) { + uiView.reload() + DispatchQueue.main.async { shouldReload = false } } diff --git a/DuckDuckGo/Subscription/Views/PurchaseInProgressView.swift b/DuckDuckGo/Subscription/Views/PurchaseInProgressView.swift index 8b4aed988e..2793d9c043 100644 --- a/DuckDuckGo/Subscription/Views/PurchaseInProgressView.swift +++ b/DuckDuckGo/Subscription/Views/PurchaseInProgressView.swift @@ -28,7 +28,7 @@ struct PurchaseInProgressView: View { static let cornerRadius = 12.0 static let shadowRadius = 10.0 static let lightShadowColor = Color.gray50 - static let darkShadowColor = Color.black + static let darkShadowColor = Color.gray95 static let spinnerScale = 2.0 static let internalZStackWidth = 220.0 static let horizontalPadding = 20.0 diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index f730d19d4c..89a99a09ec 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -25,6 +25,7 @@ import Foundation struct SubscriptionFlowView: View { @ObservedObject var viewModel: SubscriptionFlowViewModel + @State private var isAlertVisible = false private func getTransactionStatus() -> String { switch viewModel.transactionStatus { @@ -50,15 +51,21 @@ struct SubscriptionFlowView: View { if viewModel.transactionStatus != .idle { PurchaseInProgressView(status: getTransactionStatus()) } + + // Activation View + NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel()), + isActive: $viewModel.activatingSubscription) { + EmptyView() + } } .onChange(of: viewModel.shouldReloadWebview) { shouldReload in if shouldReload { viewModel.shouldReloadWebview = false } } - .onChange(of: viewModel.shouldReloadWebview) { shouldReload in - if shouldReload { - viewModel.shouldReloadWebview = false + .onChange(of: viewModel.hasActiveSubscription) { result in + if result { + isAlertVisible = true } } .onAppear(perform: { @@ -68,7 +75,7 @@ struct SubscriptionFlowView: View { .navigationBarBackButtonHidden(viewModel.transactionStatus != .idle) // Active subscription found Alert - .alert(isPresented: $viewModel.hasActiveSubscription) { + .alert(isPresented: $isAlertVisible) { Alert( title: Text(UserText.subscriptionFoundTitle), message: Text(UserText.subscriptionFoundText), diff --git a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift index 32ab7c0a79..2d223f9b08 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift @@ -25,8 +25,10 @@ import DesignResourcesKit @available(iOS 15.0, *) struct SubscriptionRestoreView: View { + @Environment(\.dismiss) var dismiss @ObservedObject var viewModel: SubscriptionRestoreViewModel @State private var expandedItemId: Int = 0 + @State private var isAlertVisible = false private enum Constants { static let heroImage = "SyncTurnOnSyncHero" @@ -42,21 +44,45 @@ struct SubscriptionRestoreView: View { static let headerPadding = EdgeInsets(top: 16.0, leading: 16.0, bottom: 0, trailing: 16.0) } + var body: some View { + ZStack { + VStack { + headerView + listView + } + .background(Color(designSystemColor: .container)) + .navigationTitle(UserText.subscriptionActivate) + .navigationBarBackButtonHidden(viewModel.transactionStatus != .idle) + .applyInsetGroupedListStyle() + .alert(isPresented: $isAlertVisible) { getAlert() } + .onChange(of: viewModel.activationResult) { result in + if result != .unknown { + isAlertVisible = true + } + } + + if viewModel.transactionStatus != .idle { + PurchaseInProgressView(status: getTransactionStatus()) + } + } + + + } + private var listItems: [ListItem] { [ .init(id: 0, content: getCellTitle(icon: Constants.appleIDIcon, - text: "Apple ID"), - expandedContent: getCellContent(description: "Restore your purchase to activate your subscription on this device.", - buttonText: "Restore", - buttonAction: {})), + text: UserText.subscriptionActivateAppleID), + expandedContent: getCellContent(description: UserText.subscriptionActivateAppleIDDescription, + buttonText: UserText.subscriptionRestoreAppleID, + buttonAction: viewModel.restoreAppstoreTransaction)), .init(id: 1, content: getCellTitle(icon: Constants.emailIcon, - text: "Email"), - expandedContent: getCellContent(description: "Use your email to activate your subscription on this device.", - buttonText: "Enter Email", - buttonAction: {})), - + text: UserText.subscriptionActivateEmail), + expandedContent: getCellContent(description: UserText.subscriptionActivateEmailDescription, + buttonText: UserText.subscriptionRestoreEmail, + buttonAction: {})) ] } @@ -99,50 +125,86 @@ struct SubscriptionRestoreView: View { .padding(.top, Constants.cellPadding) ) } - + + private func getTransactionStatus() -> String { + switch viewModel.transactionStatus { + case .polling: + return UserText.subscriptionCompletingPurchaseTitle + case .purchasing: + return UserText.subscriptionPurchasingTitle + case .restoring: + return UserText.subscriptionPestoringTitle + case .idle: + return "" + } + } - struct ListItem { - let id: Int - let content: AnyView - let expandedContent: AnyView + private var headerView: some View { + VStack(spacing: Constants.headerLineSpacing) { + Image(Constants.heroImage) + Text(UserText.subscriptionActivateTitle) + .daxHeadline() + .multilineTextAlignment(.center) + .foregroundColor(Color(designSystemColor: .textPrimary)) + Text(UserText.subscriptionActivateDescription) + .daxFootnoteRegular() + .foregroundColor(Color(designSystemColor: .textSecondary)) + .multilineTextAlignment(.center) + }.padding(Constants.headerPadding) } - var body: some View { - VStack { - VStack(spacing: Constants.headerLineSpacing) { - Image(Constants.heroImage) - Text("Activate your subscription on this device") - .daxHeadline() - .multilineTextAlignment(.center) - .foregroundColor(Color(designSystemColor: .textPrimary)) - Text("Access your Privacy Pro subscription on this device via Apple ID or an email address.") - .daxFootnoteRegular() - .foregroundColor(Color(designSystemColor: .textSecondary)) - .multilineTextAlignment(.center) - } - .padding(Constants.headerPadding) - List { - Section { - ForEach(Array(zip(listItems.indices, listItems)), id: \.1.id) { _, item in - VStack(alignment: .leading, spacing: Constants.cellLineSpacing) { - HStack { - item.content - Spacer() - Image(systemName: expandedItemId == item.id ? Constants.openIndicator : Constants.closedIndicator) - .foregroundColor(Color(designSystemColor: .textPrimary)) - } - .contentShape(Rectangle()) - .onTapGesture { - expandedItemId = expandedItemId == item.id ? 0 : item.id - } - if expandedItemId == item.id { - item.expandedContent - } - }.padding(Constants.cellPadding) - } + private var listView: some View { + List { + Section { + ForEach(Array(zip(listItems.indices, listItems)), id: \.1.id) { _, item in + VStack(alignment: .leading, spacing: Constants.cellLineSpacing) { + HStack { + item.content + Spacer() + Image(systemName: expandedItemId == item.id ? Constants.openIndicator : Constants.closedIndicator) + .foregroundColor(Color(designSystemColor: .textPrimary)) + } + .contentShape(Rectangle()) + .onTapGesture { + expandedItemId = expandedItemId == item.id ? 0 : item.id + } + if expandedItemId == item.id { + item.expandedContent + } + }.padding(Constants.cellPadding) } } - }.background(Color(designSystemColor: .container)) + } + } + + private func getAlert() -> Alert { + switch viewModel.activationResult { + case .activated: + return Alert(title: Text(UserText.subscriptionRestoreSuccessfulTitle), + message: Text(UserText.subscriptionRestoreSuccessfulMessage), + dismissButton: .default(Text(UserText.subscriptionRestoreSuccessfulButton)) { + dismiss() + } + ) + case .notFound: + return Alert(title: Text(UserText.subscriptionRestoreNotFoundTitle), + message: Text(UserText.subscriptionRestoreNotFoundMessage), + primaryButton: .default(Text(UserText.subscriptionRestoreNotFoundPlans), + action: { + dismiss() + }), + secondaryButton: .cancel()) + case .error: + return Alert(title: Text("Error"), message: Text("An error occurred during activation.")) + default: + return Alert(title: Text("Unknown"), message: Text("An unknown error occurred.")) + } + } + + struct ListItem { + let id: Int + let content: AnyView + let expandedContent: AnyView } } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index dc0705d9e9..bd17729466 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1001,12 +1001,11 @@ But if you *do* want a peek under the hood, you can find more information about public static let settingsVersion = NSLocalizedString("settings.version", value: "Version", comment: "Settings cell for Version") public static let settingsFeedback = NSLocalizedString("settings.feedback", value: "Share Feedback", comment: "Settings cell for Feedback") - // Privacy Pro Subscriptions + // Subscriptions static let subscriptionPurchasingTitle = NSLocalizedString("subscription.progress.view.purchasing.subscription", value: "Purchase in progress...", comment: "Progress view title when starting the purchase") static let subscriptionPestoringTitle = NSLocalizedString("subscription.progress.view.restoring.subscription", value: "Restoring subscription...", comment: "Progress view title when restoring past subscription purchase") static let subscriptionCompletingPurchaseTitle = NSLocalizedString("subscription.progress.view.completing.purchase", value: "Completing purchase...", comment: "Progress view title when completing the purchase") - static func subscriptionInfo(expiration: String) -> String { let localized = NSLocalizedString("subscription.subscription.active.caption", value: "Your Privacy Pro subscription renews on %@", comment: "Subscription Expiration Data") return String(format: localized, expiration) @@ -1023,9 +1022,24 @@ But if you *do* want a peek under the hood, you can find more information about public static let subscriptionRemoveFromDeviceConfirmText = NSLocalizedString("subscription.remove.from.device.text", value: "You will no longer be able to access your Privacy Pro subscription on this device. This will not cancel your subscription, and it will remain active on your other devices.", comment: "Remove from device confirmation dialog text") public static let subscriptionRemove = NSLocalizedString("subscription.remove.subscription", value: "Remove Subscription", comment: "Remove subscription button text") public static let subscriptionRemoveCancel = NSLocalizedString("subscription.remove.subscription.cancel", value: "Cancel", comment: "Remove subscription cancel button text") - public static let subscriptionFoundTitle = NSLocalizedString("subscription.subscription.found.tite", value: "Subscription Found", comment: "Title for the existing subscription dialog") - public static let subscriptionFoundText = NSLocalizedString("subscription.subscription.found.text", value: "We found a subscription associated with this Apple ID.", comment: "Message for the existing subscription dialog") - public static let subscriptionFoundCancel = NSLocalizedString("subscription.subscription.found.cancel", value: "Cancel", comment: "Cancel action for the existing subscription dialog") - public static let subscriptionFoundRestore = NSLocalizedString("subscription.subscription.found.restore", value: "Restore", comment: "Restore action for the existing subscription dialog") - + public static let subscriptionFoundTitle = NSLocalizedString("subscription.found.tite", value: "Subscription Found", comment: "Title for the existing subscription dialog") + public static let subscriptionFoundText = NSLocalizedString("subscription.found.text", value: "We found a subscription associated with this Apple ID.", comment: "Message for the existing subscription dialog") + public static let subscriptionFoundCancel = NSLocalizedString("subscription.found.cancel", value: "Cancel", comment: "Cancel action for the existing subscription dialog") + public static let subscriptionFoundRestore = NSLocalizedString("subscription.found.restore", value: "Restore", comment: "Restore action for the existing subscription dialog") + public static let subscriptionActivateTitle = NSLocalizedString("subscription.activate.title", value: "Activate your subscription on this device", comment: "Subscription Activation Title") + public static let subscriptionActivateDescription = NSLocalizedString("subscription.activate.description", value: "Access your Privacy Pro subscription on this device via Apple ID or an email address.", comment: "Subscription Activation Info") + public static let subscriptionActivate = NSLocalizedString("subscription.activate", value: "Activate Subscription.", comment: "Subscription Activation Window Title") + public static let subscriptionActivateAppleID = NSLocalizedString("subscription.activate.appleid", value: "Apple ID", comment: "Apple ID option for activation") + public static let subscriptionActivateAppleIDDescription = NSLocalizedString("subscription.activate.appleid.description", value: "Restore your purchase to activate your subscription on this device.", comment: "Description for Apple ID activation") + public static let subscriptionRestoreAppleID = NSLocalizedString("subscription.activate.restore.apple", value: "Restore", comment: "Restore button title for AppleID") + public static let subscriptionActivateEmail = NSLocalizedString("subscription.activate.email", value: "Email", comment: "Email option for activation") + public static let subscriptionActivateEmailDescription = NSLocalizedString("subscription.activate.appleid.description", value: "Use your email to activate your subscription on this device.", comment: "Description for Email activation") + public static let subscriptionRestoreEmail = NSLocalizedString("subscription.activate.restore.email", value: "Enter Email", comment: "Restore button title for Email") + public static let subscriptionRestoreNotFoundTitle = NSLocalizedString("subscription.notFound.alert.title", value: "Subscription Not Found", comment: "Alert title for not found subscription") + public static let subscriptionRestoreNotFoundMessage = NSLocalizedString("subscription.notFound.alert.message", value: "The subscription associated with this Apple ID is no longer active.", comment: "Alert content for not found subscription") + public static let subscriptionRestoreNotFoundPlans = NSLocalizedString("subscription.notFound.view.plans", value: "View Plans", comment: "View plans button text") + public static let subscriptionRestoreSuccessfulTitle = NSLocalizedString("subscription.restore.success.alert.title", value: "You’re all set.", comment: "Alert title for restored purchase") + public static let subscriptionRestoreSuccessfulMessage = NSLocalizedString("subscription.restore.success.alert.message", value: "Your purchases have been restored.", comment: "Alert message for restored purchase") + public static let subscriptionRestoreSuccessfulButton = NSLocalizedString("subscription.restore.success.alert.button", value: "OK", comment: "Alert button text for restored purchase alert") + } diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 34845d2775..90246ac001 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1914,6 +1914,31 @@ But if you *do* want a peek under the hood, you can find more information about /* No comment provided by engineer. */ "siteFeedback.urlPlaceholder" = "Which website is broken?"; +/* Subscription Activation Window Title */ +"subscription.activate" = "Activate Subscription."; + +/* Apple ID option for activation */ +"subscription.activate.appleid" = "Apple ID"; + +/* Description for Apple ID activation + Description for Email activation */ +"subscription.activate.appleid.description" = "Restore your purchase to activate your subscription on this device."; + +/* Subscription Activation Info */ +"subscription.activate.description" = "Access your Privacy Pro subscription on this device via Apple ID or an email address."; + +/* Email option for activation */ +"subscription.activate.email" = "Email"; + +/* Restore button title for AppleID */ +"subscription.activate.restore.apple" = "Restore"; + +/* Restore button title for Email */ +"subscription.activate.restore.email" = "Enter Email"; + +/* Subscription Activation Title */ +"subscription.activate.title" = "Activate your subscription on this device"; + /* Add to another device button */ "subscription.add.device" = "Add to Another Device"; @@ -1926,6 +1951,18 @@ But if you *do* want a peek under the hood, you can find more information about /* FAQ Description */ "subscription.faq.description" = "Visit our Privacy Pro help pages for answers to frequently asked questions"; +/* Cancel action for the existing subscription dialog */ +"subscription.found.cancel" = "Cancel"; + +/* Restore action for the existing subscription dialog */ +"subscription.found.restore" = "Restore"; + +/* Message for the existing subscription dialog */ +"subscription.found.text" = "We found a subscription associated with this Apple ID."; + +/* Title for the existing subscription dialog */ +"subscription.found.tite" = "Subscription Found"; + /* Help and support Section header */ "subscription.help" = "Help and support"; @@ -1935,6 +1972,15 @@ But if you *do* want a peek under the hood, you can find more information about /* Manage Plan header */ "subscription.manage.plan" = "Manage Plan"; +/* Alert content for not found subscription */ +"subscription.notFound.alert.message" = "The subscription associated with this Apple ID is no longer active."; + +/* Alert title for not found subscription */ +"subscription.notFound.alert.title" = "Subscription Not Found"; + +/* View plans button text */ +"subscription.notFound.view.plans" = "View Plans"; + /* Progress view title when completing the purchase */ "subscription.progress.view.completing.purchase" = "Completing purchase..."; @@ -1959,20 +2005,17 @@ But if you *do* want a peek under the hood, you can find more information about /* Remove subscription cancel button text */ "subscription.remove.subscription.cancel" = "Cancel"; -/* Subscription Expiration Data */ -"subscription.subscription.active.caption" = "Your Privacy Pro subscription renews on %@"; - -/* Cancel action for the existing subscription dialog */ -"subscription.subscription.found.cancel" = "Cancel"; +/* Alert button text for restored purchase alert */ +"subscription.restore.success.alert.button" = "OK"; -/* Restore action for the existing subscription dialog */ -"subscription.subscription.found.restore" = "Restore"; +/* Alert message for restored purchase */ +"subscription.restore.success.alert.message" = "Your purchases have been restored."; -/* Message for the existing subscription dialog */ -"subscription.subscription.found.text" = "We found a subscription associated with this Apple ID."; +/* Alert title for restored purchase */ +"subscription.restore.success.alert.title" = "You’re all set."; -/* Title for the existing subscription dialog */ -"subscription.subscription.found.tite" = "Subscription Found"; +/* Subscription Expiration Data */ +"subscription.subscription.active.caption" = "Your Privacy Pro subscription renews on %@"; /* Message confirming that recovery code was copied to clipboard */ "sync.code.copied" = "Recovery code copied to clipboard"; From 8f5f549138f0d1ad1f85b400f8b076c430369ebe Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Thu, 18 Jan 2024 17:46:26 +0100 Subject: [PATCH 03/28] Typo --- DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift | 2 +- DuckDuckGo/UserText.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift index 2d223f9b08..618223a245 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift @@ -133,7 +133,7 @@ struct SubscriptionRestoreView: View { case .purchasing: return UserText.subscriptionPurchasingTitle case .restoring: - return UserText.subscriptionPestoringTitle + return UserText.subscriptionRestoringTitle case .idle: return "" } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index bd17729466..3b70fc29f8 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1004,7 +1004,7 @@ But if you *do* want a peek under the hood, you can find more information about // Subscriptions static let subscriptionPurchasingTitle = NSLocalizedString("subscription.progress.view.purchasing.subscription", value: "Purchase in progress...", comment: "Progress view title when starting the purchase") - static let subscriptionPestoringTitle = NSLocalizedString("subscription.progress.view.restoring.subscription", value: "Restoring subscription...", comment: "Progress view title when restoring past subscription purchase") + static let subscriptionRestoringTitle = NSLocalizedString("subscription.progress.view.restoring.subscription", value: "Restoring subscription...", comment: "Progress view title when restoring past subscription purchase") static let subscriptionCompletingPurchaseTitle = NSLocalizedString("subscription.progress.view.completing.purchase", value: "Completing purchase...", comment: "Progress view title when completing the purchase") static func subscriptionInfo(expiration: String) -> String { let localized = NSLocalizedString("subscription.subscription.active.caption", value: "Your Privacy Pro subscription renews on %@", comment: "Subscription Expiration Data") From b1dd0019a7d9488cea87e1394e9d5d2e9a90a221 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Thu, 18 Jan 2024 17:52:17 +0100 Subject: [PATCH 04/28] NOOP --- DuckDuckGo/UserText.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 3b70fc29f8..689520285b 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1040,6 +1040,5 @@ But if you *do* want a peek under the hood, you can find more information about public static let subscriptionRestoreNotFoundPlans = NSLocalizedString("subscription.notFound.view.plans", value: "View Plans", comment: "View plans button text") public static let subscriptionRestoreSuccessfulTitle = NSLocalizedString("subscription.restore.success.alert.title", value: "You’re all set.", comment: "Alert title for restored purchase") public static let subscriptionRestoreSuccessfulMessage = NSLocalizedString("subscription.restore.success.alert.message", value: "Your purchases have been restored.", comment: "Alert message for restored purchase") - public static let subscriptionRestoreSuccessfulButton = NSLocalizedString("subscription.restore.success.alert.button", value: "OK", comment: "Alert button text for restored purchase alert") - + public static let subscriptionRestoreSuccessfulButton = NSLocalizedString("subscription.restore.success.alert.button", value: "OK", comment: "Alert button text for restored purchase alert") } From 3c5b8bb9ad2b3e02b0033ad70e8b0a52f616e7d0 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Thu, 18 Jan 2024 17:59:11 +0100 Subject: [PATCH 05/28] Typo --- DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index 89a99a09ec..3649d27492 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -34,7 +34,7 @@ struct SubscriptionFlowView: View { case .purchasing: return UserText.subscriptionPurchasingTitle case .restoring: - return UserText.subscriptionPestoringTitle + return UserText.subscriptionRestoringTitle case .idle: return "" } From d3e54f13f747c2985c9a97bec9fb81d1f7d3690c Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Thu, 18 Jan 2024 18:11:38 +0100 Subject: [PATCH 06/28] Subscription API Updates (From macOS) --- .../AppStore/AppStoreAccountManagementFlow.swift | 3 +-- .../Flows/AppStore/AppStorePurchaseFlow.swift | 6 ++++-- .../Subscription/Services/APIService.swift | 6 +++--- .../Subscription/Services/AuthService.swift | 7 +------ .../Subscription/Services/SubscriptionService.swift | 7 ++----- .../SubscriptionPurchaseEnvironment.swift | 13 ++++++++++--- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/DuckDuckGo/Subscription/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift b/DuckDuckGo/Subscription/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift index a78faa029a..4c199acf20 100644 --- a/DuckDuckGo/Subscription/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift +++ b/DuckDuckGo/Subscription/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift @@ -38,8 +38,7 @@ public final class AppStoreAccountManagementFlow { if #available(macOS 12.0, iOS 15.0, *) { // In case of invalid token attempt store based authentication to obtain a new one guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { - return .failure(.noPastTransaction) - } + return .failure(.noPastTransaction) } switch await AuthService.storeLogin(signature: lastTransactionJWSRepresentation) { case .success(let response): diff --git a/DuckDuckGo/Subscription/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift b/DuckDuckGo/Subscription/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift index 3da3548c24..28a3c0cfdd 100644 --- a/DuckDuckGo/Subscription/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift +++ b/DuckDuckGo/Subscription/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift @@ -80,7 +80,9 @@ public final class AppStorePurchaseFlow { if case let .success(accessToken) = await accountManager.exchangeAuthTokenToAccessToken(response.authToken), case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) { accountManager.storeAuthToken(token: response.authToken) - accountManager.storeAccount(token: accessToken, email: accountDetails.email, externalID: accountDetails.externalID) + accountManager.storeAccount(token: accessToken, + email: accountDetails.email, + externalID: accountDetails.externalID) } case .failure: return .failure(.accountCreationFailed) @@ -104,7 +106,7 @@ public final class AppStorePurchaseFlow { @discardableResult public static func completeSubscriptionPurchase() async -> Result { - let result = await checkForEntitlements(wait: 2.0, retry: 30) + let result = await checkForEntitlements(wait: 2.0, retry: 10) return result ? .success(PurchaseUpdate(type: "completed")) : .failure(.missingEntitlements) } diff --git a/DuckDuckGo/Subscription/Subscription/Services/APIService.swift b/DuckDuckGo/Subscription/Subscription/Services/APIService.swift index 741f3163dd..b0d036a41f 100644 --- a/DuckDuckGo/Subscription/Subscription/Services/APIService.swift +++ b/DuckDuckGo/Subscription/Subscription/Services/APIService.swift @@ -33,7 +33,6 @@ struct ErrorResponse: Decodable { } public protocol APIService { - static var logger: OSLog { get } static var baseURL: URL { get } static var session: URLSession { get } static func executeAPICall(method: String, endpoint: String, headers: [String: String]?, body: Data?) async -> Result where T: Decodable @@ -64,7 +63,7 @@ public extension APIService { } } } catch { - os_log("Service error: %{public}@", log: .error, error.localizedDescription) + os_log(.error, log: .subscription, "Service error: %{public}@", error.localizedDescription) return .failure(.connectionError) } } @@ -94,7 +93,8 @@ public extension APIService { private static func printDebugInfo(method: String, endpoint: String, data: Data, response: URLResponse) { let statusCode = (response as? HTTPURLResponse)!.statusCode let stringData = String(data: data, encoding: .utf8) ?? "" - os_log("[%d] %{public}@ /%{public}@ :: %{public}@", log: logger, statusCode, method, endpoint, stringData) + + os_log(.info, log: .subscription, "[API] %d %{public}s /%{public}s :: %{private}s", statusCode, method, endpoint, stringData) } static func makeAuthorizationHeader(for token: String) -> [String: String] { diff --git a/DuckDuckGo/Subscription/Subscription/Services/AuthService.swift b/DuckDuckGo/Subscription/Subscription/Services/AuthService.swift index 3f1d3c4088..8232a998a1 100644 --- a/DuckDuckGo/Subscription/Subscription/Services/AuthService.swift +++ b/DuckDuckGo/Subscription/Subscription/Services/AuthService.swift @@ -22,7 +22,6 @@ import Common public struct AuthService: APIService { - public static let logger: OSLog = .authService public static let session = { let configuration = URLSessionConfiguration.ephemeral return URLSession(configuration: configuration) @@ -108,12 +107,8 @@ public struct AuthService: APIService { public let id: Int public let status: String - // swiftlint:disable:next nesting enum CodingKeys: String, CodingKey { - case authToken = "authToken", - email, externalID = "externalId", - id, - status // no underscores due to keyDecodingStrategy = .convertFromSnakeCase + case authToken = "authToken", email, externalID = "externalId", id, status // no underscores due to keyDecodingStrategy = .convertFromSnakeCase } } } diff --git a/DuckDuckGo/Subscription/Subscription/Services/SubscriptionService.swift b/DuckDuckGo/Subscription/Subscription/Services/SubscriptionService.swift index 347cec975a..21970be2c4 100644 --- a/DuckDuckGo/Subscription/Subscription/Services/SubscriptionService.swift +++ b/DuckDuckGo/Subscription/Subscription/Services/SubscriptionService.swift @@ -21,8 +21,7 @@ import Foundation import Common public struct SubscriptionService: APIService { - - public static let logger: OSLog = .subscriptionService + public static let session = { let configuration = URLSessionConfiguration.ephemeral return URLSession(configuration: configuration) @@ -32,9 +31,7 @@ public struct SubscriptionService: APIService { // MARK: - public static func getSubscriptionDetails(token: String) async -> Result { - let result: Result = await executeAPICall(method: "GET", - endpoint: "subscription", - headers: makeAuthorizationHeader(for: token)) + let result: Result = await executeAPICall(method: "GET", endpoint: "subscription", headers: makeAuthorizationHeader(for: token)) switch result { case .success(let response): diff --git a/DuckDuckGo/Subscription/Subscription/SubscriptionPurchaseEnvironment.swift b/DuckDuckGo/Subscription/Subscription/SubscriptionPurchaseEnvironment.swift index 0dca730071..f8847dafaf 100644 --- a/DuckDuckGo/Subscription/Subscription/SubscriptionPurchaseEnvironment.swift +++ b/DuckDuckGo/Subscription/Subscription/SubscriptionPurchaseEnvironment.swift @@ -18,15 +18,18 @@ // import Foundation +import Common public final class SubscriptionPurchaseEnvironment { - public enum Environment { + public enum Environment: String { case appStore, stripe } public static var current: Environment = .appStore { didSet { + os_log(.info, log: .subscription, "[SubscriptionPurchaseEnvironment] Setting to %@", current.rawValue) + canPurchase = false switch current { @@ -38,8 +41,12 @@ public final class SubscriptionPurchaseEnvironment { } } - public static var canPurchase: Bool = false - + public static var canPurchase: Bool = false { + didSet { + os_log(.info, log: .subscription, "[SubscriptionPurchaseEnvironment] canPurchase %@", (canPurchase ? "true" : "false")) + } + } + private static func setupForAppStore() { if #available(macOS 12.0, iOS 15.0, *) { Task { From d61d7ba14aadb9d3152844c5814792484f6aa40b Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Thu, 18 Jan 2024 20:35:44 +0100 Subject: [PATCH 07/28] Add Email to subscription and organize Strings --- DuckDuckGo.xcodeproj/project.pbxproj | 8 +++ ...scriptionPagesUseSubscriptionFeature.swift | 19 +++-- .../SubscriptionEmailViewModel.swift | 69 +++++++++++++++++++ .../ViewModel/SubscriptionFlowViewModel.swift | 10 +-- .../SubscriptionRestoreViewModel.swift | 16 ++++- .../Views/SubscriptionEmailView.swift | 45 ++++++++++++ .../Views/SubscriptionFlowView.swift | 6 +- .../Views/SubscriptionRestoreView.swift | 31 +++++++-- .../Views/SubscriptionSettingsView.swift | 16 ++--- DuckDuckGo/UserText.swift | 53 ++++++++++---- DuckDuckGo/en.lproj/Localizable.strings | 43 ++++++++++-- 11 files changed, 265 insertions(+), 51 deletions(-) create mode 100644 DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift create mode 100644 DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d282cb5bbb..a20dfbe991 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -768,6 +768,8 @@ CBEFB9142AE0844700DEDE7B /* CriticalAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEFB9102ADFFE7900DEDE7B /* CriticalAlerts.swift */; }; CBFCB30E2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFCB30D2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift */; }; D63657192A7BAE7C001AF19D /* EmailManagerRequestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63657182A7BAE7C001AF19D /* EmailManagerRequestDelegate.swift */; }; + D64648AD2B59936B0033090B /* SubscriptionEmailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */; }; + D64648AF2B5993890033090B /* SubscriptionEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */; }; D652498E2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */; }; D664C7B62B289AA200CBFA76 /* SubscriptionFlowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */; }; D664C7B72B289AA200CBFA76 /* Subscription.storekit in Resources */ = {isa = PBXBuildFile; fileRef = D664C7952B289AA000CBFA76 /* Subscription.storekit */; }; @@ -2411,6 +2413,8 @@ CBF14FC627970C8A001D94D0 /* HomeMessageCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeMessageCollectionViewCell.swift; sourceTree = ""; }; CBFCB30D2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationURLDebugViewController.swift; sourceTree = ""; }; D63657182A7BAE7C001AF19D /* EmailManagerRequestDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmailManagerRequestDelegate.swift; sourceTree = ""; }; + D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionEmailView.swift; sourceTree = ""; }; + D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionEmailViewModel.swift; sourceTree = ""; }; D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionSettingsViewModel.swift; sourceTree = ""; }; D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowViewModel.swift; sourceTree = ""; }; D664C7952B289AA000CBFA76 /* Subscription.storekit */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Subscription.storekit; sourceTree = ""; }; @@ -4529,6 +4533,7 @@ children = ( D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */, D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */, + D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */, D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */, ); path = ViewModel; @@ -4550,6 +4555,7 @@ D664C7AE2B289AA000CBFA76 /* SubscriptionFlowView.swift */, D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */, D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */, + D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */, ); path = Views; sourceTree = ""; @@ -6590,6 +6596,7 @@ CB258D1329A4F24E00DEBA24 /* ConfigurationStore.swift in Sources */, 85058370219F424500ED4EDB /* SearchBarExtension.swift in Sources */, 310D09212799FD1A00DC0060 /* MIMEType.swift in Sources */, + D64648AD2B59936B0033090B /* SubscriptionEmailView.swift in Sources */, F4147354283BF834004AA7A5 /* AutofillContentScopeFeatureToggles.swift in Sources */, 986DA94A24884B18004A7E39 /* WebViewTransition.swift in Sources */, 31B524572715BB23002225AB /* WebJSAlert.swift in Sources */, @@ -6847,6 +6854,7 @@ F103073B1E7C91330059FEC7 /* BookmarksDataSource.swift in Sources */, EE0153E62A6FE106002A8B26 /* NetworkProtectionRootViewModel.swift in Sources */, 85864FBC24D31EF300E756FF /* SuggestionTrayViewController.swift in Sources */, + D64648AF2B5993890033090B /* SubscriptionEmailViewModel.swift in Sources */, 1EF24235273BB9D200DE3D02 /* IntervalSlider.swift in Sources */, 027F48782A4B663C001A1C6C /* AppTPFAQView.swift in Sources */, D6E83C3D2B1F2C03006C8AFB /* SettingsLoginsView.swift in Sources */, diff --git a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift index ba40e5890e..446430f35e 100644 --- a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift @@ -45,7 +45,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec static let backToSettings = "backToSettings" static let getSubscriptionOptions = "getSubscriptionOptions" static let subscriptionSelected = "subscriptionSelected" - static let activateSubscription = "activateSubscription" + static let subscriptionActive = "activateSubscription" static let featureSelected = "featureSelected" } @@ -66,7 +66,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec @Published var transactionStatus: TransactionStatus = .idle @Published var hasActiveSubscription = false @Published var purchaseError: AppStorePurchaseFlow.Error? - @Published var activateSubscription: Bool = false + @Published var subscriptionActive: Bool = false + @Published var emailActivationComplete: Bool = false var broker: UserScriptMessageBroker? @@ -90,7 +91,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec case Handlers.backToSettings: return backToSettings case Handlers.getSubscriptionOptions: return getSubscriptionOptions case Handlers.subscriptionSelected: return subscriptionSelected - case Handlers.activateSubscription: return activateSubscription + case Handlers.subscriptionActive: return subscriptionActive case Handlers.featureSelected: return featureSelected default: return nil @@ -141,7 +142,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec case .success(let subscriptionOptions): return subscriptionOptions case .failure: - + os_log(.info, log: .subscription, "Failed to obtain subscription options") return nil } @@ -206,6 +207,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) { accountManager.storeAuthToken(token: authToken) accountManager.storeAccount(token: accessToken, email: accountDetails.email, externalID: accountDetails.externalID) + } else { + os_log(.info, log: .subscription, "Failed to obtain subscription options") } return nil @@ -216,13 +219,15 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec if let accessToken = accountManager.accessToken, case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) { accountManager.storeAccount(token: accessToken, email: accountDetails.email, externalID: accountDetails.externalID) + emailActivationComplete = true + } else { + os_log(.info, log: .subscription, "Failed to restore subscription from Email") } - return nil } - func activateSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { - activateSubscription = true + func subscriptionActive(params: Any, original: WKScriptMessage) async throws -> Encodable? { + subscriptionActive = true return nil } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift new file mode 100644 index 0000000000..310a452be1 --- /dev/null +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -0,0 +1,69 @@ +// +// SubscriptionEmailViewModel.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UserScript +import Combine +import Core + +#if SUBSCRIPTION +@available(iOS 15.0, *) +final class SubscriptionEmailViewModel: ObservableObject { + + let accountManager: AccountManager + let userScript: SubscriptionPagesUserScript + let subFeature: SubscriptionPagesUseSubscriptionFeature + + var emailURL = URL.addEmailToSubscription + var viewTitle = UserText.subscriptionRestoreAddEmailTitle + @Published var subscriptionEmail: String? + @Published var shouldReloadWebView = false + @Published var subscriptionActive = false + + private var cancellables = Set() + + init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), + subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), + accountManager: AccountManager = AccountManager()) { + self.userScript = userScript + self.subFeature = subFeature + self.accountManager = accountManager + initializeView() + } + + func initializeView() { + subscriptionEmail = accountManager.email + if subscriptionEmail != nil { + emailURL = URL.activateSubscriptionViaEmail + } + } + + func setupTransactionObservers() { + subFeature.$emailActivationComplete + .receive(on: DispatchQueue.main) + .sink { [weak self] value in + if value { + self?.subFeature.emailActivationComplete = false + self?.subscriptionActive = true + } + } + .store(in: &cancellables) + } +} +#endif diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index 6fee092c1c..08e27ff3b5 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -38,7 +38,7 @@ final class SubscriptionFlowViewModel: ObservableObject { var purchaseURL = URL.purchaseSubscription @Published var hasActiveSubscription = false @Published var transactionStatus: SubscriptionPagesUseSubscriptionFeature.TransactionStatus = .idle - @Published var shouldReloadWebview = false + @Published var shouldReloadWebView = false @Published var activatingSubscription = false init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), @@ -67,11 +67,11 @@ final class SubscriptionFlowViewModel: ObservableObject { } .store(in: &cancellables) - subFeature.$activateSubscription + subFeature.$subscriptionActive .receive(on: DispatchQueue.main) .sink { [weak self] value in if value { - self?.subFeature.activateSubscription = false + self?.subFeature.subscriptionActive = false self?.activatingSubscription = true } } @@ -85,13 +85,13 @@ final class SubscriptionFlowViewModel: ObservableObject { func initializeViewData() async { await self.setupTransactionObserver() - await MainActor.run { shouldReloadWebview = true } + await MainActor.run { shouldReloadWebView = true } } func restoreAppstoreTransaction() { Task { if await subFeature.restoreAccountFromAppStorePurchase() { - await MainActor.run { shouldReloadWebview = true } + await MainActor.run { shouldReloadWebView = true } } else { await MainActor.run { } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift index 8b76e2b9d3..db846a359c 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -29,6 +29,9 @@ final class SubscriptionRestoreViewModel: ObservableObject { let userScript: SubscriptionPagesUserScript let subFeature: SubscriptionPagesUseSubscriptionFeature let purchaseManager: PurchaseManager + let accountManager: AccountManager + let isAddingDevice: Bool + var viewTitle: String enum SubscriptionActivationResult { case unknown, activated, notFound, error @@ -36,13 +39,24 @@ final class SubscriptionRestoreViewModel: ObservableObject { @Published var transactionStatus: SubscriptionPagesUseSubscriptionFeature.TransactionStatus = .idle @Published var activationResult: SubscriptionActivationResult = .unknown + @Published var subscriptionEmail: String? init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), - purchaseManager: PurchaseManager = PurchaseManager.shared) { + purchaseManager: PurchaseManager = PurchaseManager.shared, + accountManager: AccountManager = AccountManager(), + isAddingDevice: Bool = false) { self.userScript = userScript self.subFeature = subFeature self.purchaseManager = purchaseManager + self.accountManager = accountManager + self.isAddingDevice = isAddingDevice + self.viewTitle = !isAddingDevice ? UserText.subscriptionActivate : UserText.subscriptionAddDeviceButton + initializeView() + } + + func initializeView() { + subscriptionEmail = accountManager.email } @MainActor diff --git a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift new file mode 100644 index 0000000000..1c69a8f342 --- /dev/null +++ b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift @@ -0,0 +1,45 @@ +// +// SubscriptionEmailFlowView.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if SUBSCRIPTION +import SwiftUI +import Foundation + +@available(iOS 15.0, *) +struct SubscriptionEmailView: View { + + @ObservedObject var viewModel: SubscriptionEmailViewModel + @Environment(\.dismiss) var dismiss + + var body: some View { + ZStack { + AsyncHeadlessWebView(url: $viewModel.emailURL, + userScript: viewModel.userScript, + subFeature: viewModel.subFeature, + shouldReload: $viewModel.shouldReloadWebView).background() + + } + .navigationTitle(viewModel.viewTitle) + + .onChange(of: viewModel.subscriptionActive) { _ in + dismiss() + } + } +} +#endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index 3649d27492..c997b0b45f 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -45,7 +45,7 @@ struct SubscriptionFlowView: View { AsyncHeadlessWebView(url: $viewModel.purchaseURL, userScript: viewModel.userScript, subFeature: viewModel.subFeature, - shouldReload: $viewModel.shouldReloadWebview).background() + shouldReload: $viewModel.shouldReloadWebView).background() // Overlay that appears when transaction is in progress if viewModel.transactionStatus != .idle { @@ -58,9 +58,9 @@ struct SubscriptionFlowView: View { EmptyView() } } - .onChange(of: viewModel.shouldReloadWebview) { shouldReload in + .onChange(of: viewModel.shouldReloadWebView) { shouldReload in if shouldReload { - viewModel.shouldReloadWebview = false + viewModel.shouldReloadWebView = false } } .onChange(of: viewModel.hasActiveSubscription) { result in diff --git a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift index 618223a245..204155069e 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift @@ -51,7 +51,7 @@ struct SubscriptionRestoreView: View { listView } .background(Color(designSystemColor: .container)) - .navigationTitle(UserText.subscriptionActivate) + .navigationTitle(viewModel.viewTitle) .navigationBarBackButtonHidden(viewModel.transactionStatus != .idle) .applyInsetGroupedListStyle() .alert(isPresented: $isAlertVisible) { getAlert() } @@ -74,15 +74,13 @@ struct SubscriptionRestoreView: View { .init(id: 0, content: getCellTitle(icon: Constants.appleIDIcon, text: UserText.subscriptionActivateAppleID), - expandedContent: getCellContent(description: UserText.subscriptionActivateAppleIDDescription, + expandedContent: getAppleIDCellContent(description: UserText.subscriptionActivateAppleIDDescription, buttonText: UserText.subscriptionRestoreAppleID, buttonAction: viewModel.restoreAppstoreTransaction)), .init(id: 1, content: getCellTitle(icon: Constants.emailIcon, text: UserText.subscriptionActivateEmail), - expandedContent: getCellContent(description: UserText.subscriptionActivateEmailDescription, - buttonText: UserText.subscriptionRestoreEmail, - buttonAction: {})) + expandedContent: getEmailCellContent(buttonAction: {})) ] } @@ -97,7 +95,7 @@ struct SubscriptionRestoreView: View { ) } - private func getCellContent(description: String, buttonText: String, buttonAction: @escaping () -> Void) -> AnyView { + private func getAppleIDCellContent(description: String, buttonText: String, buttonAction: @escaping () -> Void) -> AnyView { AnyView( VStack(alignment: .leading) { Text(description) @@ -108,6 +106,27 @@ struct SubscriptionRestoreView: View { ) } + private func getEmailCellContent(buttonAction: @escaping () -> Void) -> AnyView { + AnyView( + VStack(alignment: .leading) { + if viewModel.subscriptionEmail == nil { + Text(UserText.subscriptionActivateEmailDescription) + .daxSubheadRegular() + .foregroundColor(Color(designSystemColor: .textSecondary)) + getCellButton(buttonText: UserText.subscriptionRestoreEmail, + action: buttonAction) + } else { + Text(viewModel.subscriptionEmail ?? "").daxSubheadSemibold() + Text(UserText.subscriptionActivateEmailDescription) + .daxSubheadRegular() + .foregroundColor(Color(designSystemColor: .textSecondary)) + getCellButton(buttonText: UserText.subscriptionManageEmail, + action: buttonAction) + } + } + ) + } + private func getCellButton(buttonText: String, action: @escaping () -> Void) -> AnyView { AnyView( Button(action: action, label: { diff --git a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift index c10bd87f44..40f4e2babe 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift @@ -44,13 +44,13 @@ struct SubscriptionSettingsView: View { .hidden() }.textCase(nil) Section(header: Text(UserText.subscriptionManageDevices)) { - SettingsCustomCell(content: { - Text(UserText.subscriptionAddDevice) - .daxBodyRegular() - .foregroundColor(Color.init(designSystemColor: .accent)) - }, - action: {}) - + NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel(isAddingDevice: true))) { + SettingsCustomCell(content: { + Text(UserText.subscriptionAddDeviceButton) + .daxBodyRegular() + .foregroundColor(Color.init(designSystemColor: .accent)) + }) + } SettingsCustomCell(content: { Text(UserText.subscriptionRemoveFromDevice) .daxBodyRegular() @@ -75,7 +75,7 @@ struct SubscriptionSettingsView: View { .daxBodyRegular() }, action: {}, - disclosureIndicator: false) + disclosureIndicator: true) } } } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 6e6f3bb0a0..3ed7e71c1f 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -959,7 +959,7 @@ But if you *do* want a peek under the hood, you can find more information about public static let settingsAutolock = NSLocalizedString("settings.autolock", value: "Application Lock", comment: "Settings screen cell text for Application Lock") public static let settingsAutoLockDescription = NSLocalizedString("settings.autolock.description", value: "If Touch ID, Face ID or a system passcode is set, you’ll be requested to unlock the app when opening.", comment: "Section footer Autolock description") - // Privacy Pro Section + // Subscription Section public static let settingsPProSection = NSLocalizedString("settings.ppro", value: "Privacy Pro", comment: "Product name for the subscription bundle") public static let settingsPProSubscribe = NSLocalizedString("settings.subscription.subscribe", value: "Subscribe to Privacy Pro", comment: "Call to action title for Privacy Pro") public static let settingsPProDescription = NSLocalizedString("settings.subscription.description", value:"More seamless privacy with three new protections, including:", comment: "Privacy pro description subtext") @@ -979,8 +979,7 @@ But if you *do* want a peek under the hood, you can find more information about public static let settingsPProDBPSubTitle = NSLocalizedString("settings.subscription.DBP.subtitle", value: "Remove your info from sites that sell it", comment: "Data Broker protection cell subtitle for privacy pro") public static let settingsPProITRTitle = NSLocalizedString("settings.subscription.ITR.title", value: "Identity Theft Restoration", comment: "Identity theft restoration cell title for privacy pro") public static let settingsPProITRSubTitle = NSLocalizedString("settings.subscription.ITR.subtitle", value: "If your identity is stolen, we'll help restore it", comment: "Identity theft restoration cell subtitle for privacy pro") - - + // Customize Section public static let settingsCustomizeSection = NSLocalizedString("settings.customize", value: "Customize", comment: "Settings title for the customize section") public static let settingsKeyboard = NSLocalizedString("settings.keyboard", value: "Keyboard", comment: "Settings screen cell for Keyboard") @@ -1001,44 +1000,70 @@ But if you *do* want a peek under the hood, you can find more information about public static let settingsVersion = NSLocalizedString("settings.version", value: "Version", comment: "Settings cell for Version") public static let settingsFeedback = NSLocalizedString("settings.feedback", value: "Share Feedback", comment: "Settings cell for Feedback") - // Subscriptions + // MARK: Subscriptions + // Loaders static let subscriptionPurchasingTitle = NSLocalizedString("subscription.progress.view.purchasing.subscription", value: "Purchase in progress...", comment: "Progress view title when starting the purchase") static let subscriptionRestoringTitle = NSLocalizedString("subscription.progress.view.restoring.subscription", value: "Restoring subscription...", comment: "Progress view title when restoring past subscription purchase") static let subscriptionCompletingPurchaseTitle = NSLocalizedString("subscription.progress.view.completing.purchase", value: "Completing purchase...", comment: "Progress view title when completing the purchase") + + // Subscription Settings static func subscriptionInfo(expiration: String) -> String { let localized = NSLocalizedString("subscription.subscription.active.caption", value: "Your Privacy Pro subscription renews on %@", comment: "Subscription Expiration Data") return String(format: localized, expiration) } - public static let subscriptionManageDevices = NSLocalizedString("subscription.manage.device", value: "Manage Devices", comment: "Header for the device management section") - public static let subscriptionAddDevice = NSLocalizedString("subscription.add.device", value: "Add to Another Device", comment: "Add to another device button") - public static let subscriptionRemoveFromDevice = NSLocalizedString("subscription.remove.from.device", value: "Remove From This Device", comment: "Remove from this device button") + public static let subscriptionManageDevices = NSLocalizedString("subscription.manage.devices", value: "Manage Devices", comment: "Header for the device management section") + public static let subscriptionAddDeviceButton = NSLocalizedString("subscription.add.device.button", value: "Add to Another Device", comment: "Add to another device button") + public static let subscriptionRemoveFromDevice = NSLocalizedString("subscription.remove.from.device.button", value: "Remove From This Device", comment: "Remove from this device button") public static let subscriptionManagePlan = NSLocalizedString("subscription.manage.plan", value: "Manage Plan", comment: "Manage Plan header") public static let subscriptionChangePlan = NSLocalizedString("subscription.change.plan", value: "Change Plan Or Billing", comment: "Change plan or billing title") public static let subscriptionHelpAndSupport = NSLocalizedString("subscription.help", value: "Help and support", comment: "Help and support Section header") public static let subscriptionFAQ = NSLocalizedString("subscription.faq", value: "Privacy Pro FAQ", comment: "FAQ Button") public static let subscriptionFAQFooter = NSLocalizedString("subscription.faq.description", value: "Visit our Privacy Pro help pages for answers to frequently asked questions", comment: "FAQ Description") + + // Remove subscription confirmation public static let subscriptionRemoveFromDeviceConfirmTitle = NSLocalizedString("subscription.remove.from.device.title", value: "Remove From This Device?", comment: "Remove from device confirmation dialog title") public static let subscriptionRemoveFromDeviceConfirmText = NSLocalizedString("subscription.remove.from.device.text", value: "You will no longer be able to access your Privacy Pro subscription on this device. This will not cancel your subscription, and it will remain active on your other devices.", comment: "Remove from device confirmation dialog text") public static let subscriptionRemove = NSLocalizedString("subscription.remove.subscription", value: "Remove Subscription", comment: "Remove subscription button text") public static let subscriptionRemoveCancel = NSLocalizedString("subscription.remove.subscription.cancel", value: "Cancel", comment: "Remove subscription cancel button text") - public static let subscriptionFoundTitle = NSLocalizedString("subscription.found.tite", value: "Subscription Found", comment: "Title for the existing subscription dialog") - public static let subscriptionFoundText = NSLocalizedString("subscription.found.text", value: "We found a subscription associated with this Apple ID.", comment: "Message for the existing subscription dialog") - public static let subscriptionFoundCancel = NSLocalizedString("subscription.found.cancel", value: "Cancel", comment: "Cancel action for the existing subscription dialog") - public static let subscriptionFoundRestore = NSLocalizedString("subscription.found.restore", value: "Restore", comment: "Restore action for the existing subscription dialog") + + // Subscription Restore + public static let subscriptionActivate = NSLocalizedString("subscription.activate", value: "Activate Subscription", comment: "Subscription Activation Window Title") public static let subscriptionActivateTitle = NSLocalizedString("subscription.activate.title", value: "Activate your subscription on this device", comment: "Subscription Activation Title") public static let subscriptionActivateDescription = NSLocalizedString("subscription.activate.description", value: "Access your Privacy Pro subscription on this device via Apple ID or an email address.", comment: "Subscription Activation Info") - public static let subscriptionActivate = NSLocalizedString("subscription.activate", value: "Activate Subscription.", comment: "Subscription Activation Window Title") public static let subscriptionActivateAppleID = NSLocalizedString("subscription.activate.appleid", value: "Apple ID", comment: "Apple ID option for activation") public static let subscriptionActivateAppleIDDescription = NSLocalizedString("subscription.activate.appleid.description", value: "Restore your purchase to activate your subscription on this device.", comment: "Description for Apple ID activation") public static let subscriptionRestoreAppleID = NSLocalizedString("subscription.activate.restore.apple", value: "Restore", comment: "Restore button title for AppleID") public static let subscriptionActivateEmail = NSLocalizedString("subscription.activate.email", value: "Email", comment: "Email option for activation") - public static let subscriptionActivateEmailDescription = NSLocalizedString("subscription.activate.appleid.description", value: "Use your email to activate your subscription on this device.", comment: "Description for Email activation") + public static let subscriptionActivateEmailDescription = NSLocalizedString("subscription.activate.email.description", value: "Use your email to activate your subscription on this device.", comment: "Description for Email activation") public static let subscriptionRestoreEmail = NSLocalizedString("subscription.activate.restore.email", value: "Enter Email", comment: "Restore button title for Email") + + // Add to other devices (AppleID / Email) + public static let subscriptionAddDeviceTitle = NSLocalizedString("subscription.add.device.title", value: "Add Device", comment: "Add to another device view title") + public static let subscriptionUseOnOtherDevices = NSLocalizedString("subscription.add.device.header.title", value: "Use your subscription on all your devices", comment: "Add subscription to other device title ") + public static let subscriptionAddDeviceDescription = NSLocalizedString("subscription.add.device.description", value: "Access your Privacy Pro subscription on any of your devices via Apple ID or by adding an email address.", comment: "Subscription Add device Info") + public static let subscriptionAvailableInApple = NSLocalizedString("subscription.available.apple", value: "Privacy Pro is available on any device signed in to the same Apple ID.", comment: "Subscription availability message on Apple devices") + + // Add Email To subscription + public static let subscriptionAddEmail = NSLocalizedString("subscription.add.email", value: "Add an email address to activate your subscription on your other devices. We’ll only use this address to verify your subscription.", comment: "Add email to an existing subscription") + public static let subscriptionRestoreAddEmailButton = NSLocalizedString("subscription.add.email.button", value: "Add Email", comment: "Button title for adding email to subscription") + public static let subscriptionRestoreAddEmailTitle = NSLocalizedString("subscription.add.email.title", value: "Add Email", comment: "View title for adding email to subscription") + + // Manage Subscription Email + public static let subscriptionManageEmailDescription = NSLocalizedString("subscription.manage.email.description", value: "You can use this email to activate your subscription on your other devices.", comment: "Description for Email Management options") + public static let subscriptionManageEmail = NSLocalizedString("subscription.activate.manage.email.button", value: "Manage", comment: "Restore button title for Managing Email") + + // Subscribe & Restore Flow + public static let subscriptionFoundTitle = NSLocalizedString("subscription.found.title", value: "Subscription Found", comment: "Title for the existing subscription dialog") + public static let subscriptionFoundText = NSLocalizedString("subscription.found.text", value: "We found a subscription associated with this Apple ID.", comment: "Message for the existing subscription dialog") + public static let subscriptionFoundCancel = NSLocalizedString("subscription.found.cancel", value: "Cancel", comment: "Cancel action for the existing subscription dialog") + public static let subscriptionFoundRestore = NSLocalizedString("subscription.found.restore", value: "Restore", comment: "Restore action for the existing subscription dialog") public static let subscriptionRestoreNotFoundTitle = NSLocalizedString("subscription.notFound.alert.title", value: "Subscription Not Found", comment: "Alert title for not found subscription") public static let subscriptionRestoreNotFoundMessage = NSLocalizedString("subscription.notFound.alert.message", value: "The subscription associated with this Apple ID is no longer active.", comment: "Alert content for not found subscription") public static let subscriptionRestoreNotFoundPlans = NSLocalizedString("subscription.notFound.view.plans", value: "View Plans", comment: "View plans button text") public static let subscriptionRestoreSuccessfulTitle = NSLocalizedString("subscription.restore.success.alert.title", value: "You’re all set.", comment: "Alert title for restored purchase") public static let subscriptionRestoreSuccessfulMessage = NSLocalizedString("subscription.restore.success.alert.message", value: "Your purchases have been restored.", comment: "Alert message for restored purchase") - public static let subscriptionRestoreSuccessfulButton = NSLocalizedString("subscription.restore.success.alert.button", value: "OK", comment: "Alert button text for restored purchase alert") + public static let subscriptionRestoreSuccessfulButton = NSLocalizedString("subscription.restore.success.alert.button", value: "OK", comment: "Alert button text for restored purchase alert") + + } diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 721303ff0f..ad0f0b1171 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1915,13 +1915,12 @@ But if you *do* want a peek under the hood, you can find more information about "siteFeedback.urlPlaceholder" = "Which website is broken?"; /* Subscription Activation Window Title */ -"subscription.activate" = "Activate Subscription."; +"subscription.activate" = "Activate Subscription"; /* Apple ID option for activation */ "subscription.activate.appleid" = "Apple ID"; -/* Description for Apple ID activation - Description for Email activation */ +/* Description for Apple ID activation */ "subscription.activate.appleid.description" = "Restore your purchase to activate your subscription on this device."; /* Subscription Activation Info */ @@ -1930,6 +1929,12 @@ But if you *do* want a peek under the hood, you can find more information about /* Email option for activation */ "subscription.activate.email" = "Email"; +/* Description for Email activation */ +"subscription.activate.email.description" = "Use your email to activate your subscription on this device."; + +/* Restore button title for Managing Email */ +"subscription.activate.manage.email.button" = "Manage"; + /* Restore button title for AppleID */ "subscription.activate.restore.apple" = "Restore"; @@ -1940,7 +1945,28 @@ But if you *do* want a peek under the hood, you can find more information about "subscription.activate.title" = "Activate your subscription on this device"; /* Add to another device button */ -"subscription.add.device" = "Add to Another Device"; +"subscription.add.device.button" = "Add to Another Device"; + +/* Subscription Add device Info */ +"subscription.add.device.description" = "Access your Privacy Pro subscription on any of your devices via Apple ID or by adding an email address."; + +/* Add subscription to other device title */ +"subscription.add.device.header.title" = "Use your subscription on all your devices"; + +/* Add to another device view title */ +"subscription.add.device.title" = "Add Device"; + +/* Add email to an existing subscription */ +"subscription.add.email" = "Add an email address to activate your subscription on your other devices. We’ll only use this address to verify your subscription."; + +/* Button title for adding email to subscription */ +"subscription.add.email.button" = "Add Email"; + +/* View itle for adding email to subscription */ +"subscription.add.email.title" = "Add Email"; + +/* Subscription availability message on Apple devices */ +"subscription.available.apple" = "Privacy Pro is available on any device signed in to the same Apple ID."; /* Change plan or billing title */ "subscription.change.plan" = "Change Plan Or Billing"; @@ -1961,13 +1987,16 @@ But if you *do* want a peek under the hood, you can find more information about "subscription.found.text" = "We found a subscription associated with this Apple ID."; /* Title for the existing subscription dialog */ -"subscription.found.tite" = "Subscription Found"; +"subscription.found.title" = "Subscription Found"; /* Help and support Section header */ "subscription.help" = "Help and support"; /* Header for the device management section */ -"subscription.manage.device" = "Manage Devices"; +"subscription.manage.devices" = "Manage Devices"; + +/* Description for Email Management options */ +"subscription.manage.email.description" = "You can use this email to activate your subscription on your other devices."; /* Manage Plan header */ "subscription.manage.plan" = "Manage Plan"; @@ -1991,7 +2020,7 @@ But if you *do* want a peek under the hood, you can find more information about "subscription.progress.view.restoring.subscription" = "Restoring subscription..."; /* Remove from this device button */ -"subscription.remove.from.device" = "Remove From This Device"; +"subscription.remove.from.device.button" = "Remove From This Device"; /* Remove from device confirmation dialog text */ "subscription.remove.from.device.text" = "You will no longer be able to access your Privacy Pro subscription on this device. This will not cancel your subscription, and it will remain active on your other devices."; From 5177005024f933ba3a93fe47e9be98dd8be797e4 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Fri, 19 Jan 2024 20:28:31 +0100 Subject: [PATCH 08/28] Activate via Email --- DuckDuckGo.xcodeproj/project.pbxproj | 8 ++++-- .../Subscription/Services/AuthService.swift | 3 +- .../Services/SubscriptionService.swift | 5 +++- .../SubscriptionEmailViewModel.swift | 7 +++-- .../SubscriptionFlowNavController.swift | 28 +++++++++++++++++++ .../SubscriptionRestoreViewModel.swift | 20 +++++++++++-- .../SubscriptionSettingsViewModel.swift | 3 +- .../Views/SubscriptionEmailView.swift | 2 +- .../Views/SubscriptionRestoreView.swift | 24 ++++++++++------ DuckDuckGo/UserText.swift | 1 + DuckDuckGo/en.lproj/Localizable.strings | 5 +++- 11 files changed, 84 insertions(+), 22 deletions(-) create mode 100644 DuckDuckGo/Subscription/ViewModel/SubscriptionFlowNavController.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a20dfbe991..41d203e794 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -797,6 +797,7 @@ D6D12CAB2B291CAA0054390C /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9C2B291CA90054390C /* APIService.swift */; }; D6D12CAC2B291CAA0054390C /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9D2B291CA90054390C /* AuthService.swift */; }; D6D12CAD2B291CAA0054390C /* PurchaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9E2B291CA90054390C /* PurchaseManager.swift */; }; + D6D4B77C2B5AE99500996546 /* SubscriptionFlowNavController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4B77B2B5AE99500996546 /* SubscriptionFlowNavController.swift */; }; D6E83C122B1E6AB3006C8AFB /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C112B1E6AB3006C8AFB /* SettingsView.swift */; }; D6E83C2E2B1EA06E006C8AFB /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C2D2B1EA06E006C8AFB /* SettingsViewModel.swift */; }; D6E83C312B1EA309006C8AFB /* SettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C302B1EA309006C8AFB /* SettingsCell.swift */; }; @@ -2442,6 +2443,7 @@ D6D12C9C2B291CA90054390C /* APIService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; D6D12C9D2B291CA90054390C /* AuthService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = ""; }; D6D12C9E2B291CA90054390C /* PurchaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseManager.swift; sourceTree = ""; }; + D6D4B77B2B5AE99500996546 /* SubscriptionFlowNavController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowNavController.swift; sourceTree = ""; }; D6E83C112B1E6AB3006C8AFB /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; D6E83C2D2B1EA06E006C8AFB /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; D6E83C302B1EA309006C8AFB /* SettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = ""; }; @@ -4531,10 +4533,11 @@ D664C7932B289AA000CBFA76 /* ViewModel */ = { isa = PBXGroup; children = ( + D6D4B77B2B5AE99500996546 /* SubscriptionFlowNavController.swift */, D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */, - D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */, - D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */, D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */, + D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */, + D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -6498,6 +6501,7 @@ 1E162610296C5C630004127F /* CustomDaxDialogViewModel.swift in Sources */, 8590CB69268A4E190089F6BF /* DebugEtagStorage.swift in Sources */, D6D12CA62B291CAA0054390C /* AppStoreRestoreFlow.swift in Sources */, + D6D4B77C2B5AE99500996546 /* SubscriptionFlowNavController.swift in Sources */, C1CDA3162AFB9C7F006D1476 /* AutofillNeverPromptWebsitesManager.swift in Sources */, F1CA3C371F045878005FADB3 /* PrivacyStore.swift in Sources */, 37FCAAC029930E26000E420A /* FailedAssertionView.swift in Sources */, diff --git a/DuckDuckGo/Subscription/Subscription/Services/AuthService.swift b/DuckDuckGo/Subscription/Subscription/Services/AuthService.swift index 8232a998a1..3bc47e2112 100644 --- a/DuckDuckGo/Subscription/Subscription/Services/AuthService.swift +++ b/DuckDuckGo/Subscription/Subscription/Services/AuthService.swift @@ -108,7 +108,8 @@ public struct AuthService: APIService { public let status: String enum CodingKeys: String, CodingKey { - case authToken = "authToken", email, externalID = "externalId", id, status // no underscores due to keyDecodingStrategy = .convertFromSnakeCase + // no underscores due to keyDecodingStrategy = .convertFromSnakeCase + case authToken = "authToken", email, externalID = "externalId", id, status } } } diff --git a/DuckDuckGo/Subscription/Subscription/Services/SubscriptionService.swift b/DuckDuckGo/Subscription/Subscription/Services/SubscriptionService.swift index 21970be2c4..ff048cba70 100644 --- a/DuckDuckGo/Subscription/Subscription/Services/SubscriptionService.swift +++ b/DuckDuckGo/Subscription/Subscription/Services/SubscriptionService.swift @@ -31,7 +31,10 @@ public struct SubscriptionService: APIService { // MARK: - public static func getSubscriptionDetails(token: String) async -> Result { - let result: Result = await executeAPICall(method: "GET", endpoint: "subscription", headers: makeAuthorizationHeader(for: token)) + let result: Result = await executeAPICall(method: "GET", + endpoint: "subscription", + headers: makeAuthorizationHeader(for: token)) switch result { case .success(let response): diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index 310a452be1..8be853ca46 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -38,13 +38,14 @@ final class SubscriptionEmailViewModel: ObservableObject { private var cancellables = Set() - init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), - subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), - accountManager: AccountManager = AccountManager()) { + init(userScript: SubscriptionPagesUserScript, + subFeature: SubscriptionPagesUseSubscriptionFeature, + accountManager: AccountManager) { self.userScript = userScript self.subFeature = subFeature self.accountManager = accountManager initializeView() + setupTransactionObservers() } func initializeView() { diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowNavController.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowNavController.swift new file mode 100644 index 0000000000..ddd207de0d --- /dev/null +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowNavController.swift @@ -0,0 +1,28 @@ +// +// SubscriptionFlowNavController.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class SubscriptionFlowNavController: ObservableObject { + @Published var shouldDisplayRestoreView: Bool = false { + didSet { + print("shouldDisplayRestoreView changed to: \(shouldDisplayRestoreView)") + } + } +} diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift index db846a359c..14223a1739 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -31,7 +31,19 @@ final class SubscriptionRestoreViewModel: ObservableObject { let purchaseManager: PurchaseManager let accountManager: AccountManager let isAddingDevice: Bool - var viewTitle: String + + + var viewTitle: String { + UserText.subscriptionActivate + } + + var headerTitle: String { + UserText.subscriptionActivateTitle + } + + var headerDescription: String { + UserText.subscriptionActivateDescription + } enum SubscriptionActivationResult { case unknown, activated, notFound, error @@ -40,6 +52,7 @@ final class SubscriptionRestoreViewModel: ObservableObject { @Published var transactionStatus: SubscriptionPagesUseSubscriptionFeature.TransactionStatus = .idle @Published var activationResult: SubscriptionActivationResult = .unknown @Published var subscriptionEmail: String? + @Published var isRestoringEmailSubscription: Bool = false init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), @@ -51,7 +64,6 @@ final class SubscriptionRestoreViewModel: ObservableObject { self.purchaseManager = purchaseManager self.accountManager = accountManager self.isAddingDevice = isAddingDevice - self.viewTitle = !isAddingDevice ? UserText.subscriptionActivate : UserText.subscriptionAddDeviceButton initializeView() } @@ -78,5 +90,9 @@ final class SubscriptionRestoreViewModel: ObservableObject { } } + func restoreEmailSubscription() { + isRestoringEmailSubscription = true + } + } #endif diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift index 243e046534..d7f5d98351 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift @@ -25,8 +25,7 @@ import StoreKit @available(iOS 15.0, *) final class SubscriptionSettingsViewModel: ObservableObject { - private let accountManager: AccountManager - + let accountManager: AccountManager var subscriptionDetails: String = "" @Published var shouldDisplayRemovalNotice: Bool = false diff --git a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift index 1c69a8f342..23e020b030 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift @@ -1,5 +1,5 @@ // -// SubscriptionEmailFlowView.swift +// SubscriptionEmailView.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. diff --git a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift index 204155069e..09dd861aab 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift @@ -66,6 +66,14 @@ struct SubscriptionRestoreView: View { } } + // Activation View + NavigationLink(destination: SubscriptionEmailView(viewModel: SubscriptionEmailViewModel(userScript: viewModel.userScript, + subFeature: viewModel.subFeature, + accountManager: viewModel.accountManager)), + isActive: $viewModel.isRestoringEmailSubscription) { + EmptyView() + } + } @@ -74,13 +82,11 @@ struct SubscriptionRestoreView: View { .init(id: 0, content: getCellTitle(icon: Constants.appleIDIcon, text: UserText.subscriptionActivateAppleID), - expandedContent: getAppleIDCellContent(description: UserText.subscriptionActivateAppleIDDescription, - buttonText: UserText.subscriptionRestoreAppleID, - buttonAction: viewModel.restoreAppstoreTransaction)), + expandedContent: getAppleIDCellContent(buttonAction: viewModel.restoreAppstoreTransaction)), .init(id: 1, content: getCellTitle(icon: Constants.emailIcon, text: UserText.subscriptionActivateEmail), - expandedContent: getEmailCellContent(buttonAction: {})) + expandedContent: getEmailCellContent(buttonAction: viewModel.restoreEmailSubscription )) ] } @@ -95,13 +101,13 @@ struct SubscriptionRestoreView: View { ) } - private func getAppleIDCellContent(description: String, buttonText: String, buttonAction: @escaping () -> Void) -> AnyView { + private func getAppleIDCellContent(buttonAction: @escaping () -> Void) -> AnyView { AnyView( VStack(alignment: .leading) { - Text(description) + Text(UserText.subscriptionActivateAppleIDDescription) .daxSubheadRegular() .foregroundColor(Color(designSystemColor: .textSecondary)) - getCellButton(buttonText: buttonText, action: buttonAction) + getCellButton(buttonText: UserText.subscriptionActivateAppleIDButton, action: buttonAction) } ) } @@ -161,11 +167,11 @@ struct SubscriptionRestoreView: View { private var headerView: some View { VStack(spacing: Constants.headerLineSpacing) { Image(Constants.heroImage) - Text(UserText.subscriptionActivateTitle) + Text(viewModel.headerTitle) .daxHeadline() .multilineTextAlignment(.center) .foregroundColor(Color(designSystemColor: .textPrimary)) - Text(UserText.subscriptionActivateDescription) + Text(viewModel.headerDescription) .daxFootnoteRegular() .foregroundColor(Color(designSystemColor: .textSecondary)) .multilineTextAlignment(.center) diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 3ed7e71c1f..6af317cf5b 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1032,6 +1032,7 @@ But if you *do* want a peek under the hood, you can find more information about public static let subscriptionActivateTitle = NSLocalizedString("subscription.activate.title", value: "Activate your subscription on this device", comment: "Subscription Activation Title") public static let subscriptionActivateDescription = NSLocalizedString("subscription.activate.description", value: "Access your Privacy Pro subscription on this device via Apple ID or an email address.", comment: "Subscription Activation Info") public static let subscriptionActivateAppleID = NSLocalizedString("subscription.activate.appleid", value: "Apple ID", comment: "Apple ID option for activation") + public static let subscriptionActivateAppleIDButton = NSLocalizedString("subscription.activate.appleid.button", value: "Restore Purchase", comment: "Button text for restoring purchase via Apple ID") public static let subscriptionActivateAppleIDDescription = NSLocalizedString("subscription.activate.appleid.description", value: "Restore your purchase to activate your subscription on this device.", comment: "Description for Apple ID activation") public static let subscriptionRestoreAppleID = NSLocalizedString("subscription.activate.restore.apple", value: "Restore", comment: "Restore button title for AppleID") public static let subscriptionActivateEmail = NSLocalizedString("subscription.activate.email", value: "Email", comment: "Email option for activation") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index ad0f0b1171..97188925cd 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1920,6 +1920,9 @@ But if you *do* want a peek under the hood, you can find more information about /* Apple ID option for activation */ "subscription.activate.appleid" = "Apple ID"; +/* Button text for restoring purchase via Apple ID */ +"subscription.activate.appleid.button" = "Restore Purchase"; + /* Description for Apple ID activation */ "subscription.activate.appleid.description" = "Restore your purchase to activate your subscription on this device."; @@ -1962,7 +1965,7 @@ But if you *do* want a peek under the hood, you can find more information about /* Button title for adding email to subscription */ "subscription.add.email.button" = "Add Email"; -/* View itle for adding email to subscription */ +/* View title for adding email to subscription */ "subscription.add.email.title" = "Add Email"; /* Subscription availability message on Apple devices */ From 319c96cda84004d45720e6f194105e3b94513690 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Fri, 19 Jan 2024 21:50:00 +0100 Subject: [PATCH 09/28] Dismiss view after email activation --- .../SubscriptionEmailViewModel.swift | 20 ++++++++++++++----- .../SubscriptionRestoreViewModel.swift | 11 ++++++++++ .../Views/SubscriptionEmailView.swift | 4 ---- .../Views/SubscriptionRestoreView.swift | 18 +++++++++++++---- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index 8be853ca46..cee1ded2c7 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -29,6 +29,7 @@ final class SubscriptionEmailViewModel: ObservableObject { let accountManager: AccountManager let userScript: SubscriptionPagesUserScript let subFeature: SubscriptionPagesUseSubscriptionFeature + var onSubscriptionActivation: (() -> Void)? var emailURL = URL.addEmailToSubscription var viewTitle = UserText.subscriptionRestoreAddEmailTitle @@ -40,31 +41,40 @@ final class SubscriptionEmailViewModel: ObservableObject { init(userScript: SubscriptionPagesUserScript, subFeature: SubscriptionPagesUseSubscriptionFeature, - accountManager: AccountManager) { + accountManager: AccountManager, + onSubscriptionActivation: @escaping (() -> Void)) { self.userScript = userScript self.subFeature = subFeature self.accountManager = accountManager + self.onSubscriptionActivation = onSubscriptionActivation initializeView() setupTransactionObservers() } - func initializeView() { + private func initializeView() { subscriptionEmail = accountManager.email if subscriptionEmail != nil { emailURL = URL.activateSubscriptionViaEmail } } - func setupTransactionObservers() { + private func setupTransactionObservers() { subFeature.$emailActivationComplete .receive(on: DispatchQueue.main) .sink { [weak self] value in if value { - self?.subFeature.emailActivationComplete = false - self?.subscriptionActive = true + self?.activateSubscription() } } .store(in: &cancellables) } + + private func activateSubscription() { + subFeature.emailActivationComplete = false + subscriptionActive = true + print("activateSubscription called, notifying parent ViewModel") + onSubscriptionActivation?() + } + } #endif diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift index 14223a1739..ec505be0db 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -53,6 +53,7 @@ final class SubscriptionRestoreViewModel: ObservableObject { @Published var activationResult: SubscriptionActivationResult = .unknown @Published var subscriptionEmail: String? @Published var isRestoringEmailSubscription: Bool = false + @Published var subscriptionActivatedViaEmail: Bool = false init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), @@ -94,5 +95,15 @@ final class SubscriptionRestoreViewModel: ObservableObject { isRestoringEmailSubscription = true } + func handleEmailSubscriptionActivation() { + DispatchQueue.main.async { + print("handleEmailSubscriptionActivation called") + self.isRestoringEmailSubscription = false + self.subscriptionActivatedViaEmail = true + print("isRestoringEmailSubscription set to false") + print("subscriptionActivatedViaEmail set to true") + } + } + } #endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift index 23e020b030..a962a3a25c 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift @@ -36,10 +36,6 @@ struct SubscriptionEmailView: View { } .navigationTitle(viewModel.viewTitle) - - .onChange(of: viewModel.subscriptionActive) { _ in - dismiss() - } } } #endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift index 09dd861aab..7c88af51ce 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift @@ -26,7 +26,7 @@ import DesignResourcesKit struct SubscriptionRestoreView: View { @Environment(\.dismiss) var dismiss - @ObservedObject var viewModel: SubscriptionRestoreViewModel + @StateObject var viewModel: SubscriptionRestoreViewModel @State private var expandedItemId: Int = 0 @State private var isAlertVisible = false @@ -60,6 +60,12 @@ struct SubscriptionRestoreView: View { isAlertVisible = true } } + .onChange(of: viewModel.subscriptionActivatedViaEmail) { activated in + print("onChange triggered with activated: \(activated)") + if activated { + dismiss() + } + } if viewModel.transactionStatus != .idle { PurchaseInProgressView(status: getTransactionStatus()) @@ -69,12 +75,16 @@ struct SubscriptionRestoreView: View { // Activation View NavigationLink(destination: SubscriptionEmailView(viewModel: SubscriptionEmailViewModel(userScript: viewModel.userScript, subFeature: viewModel.subFeature, - accountManager: viewModel.accountManager)), - isActive: $viewModel.isRestoringEmailSubscription) { + accountManager: viewModel.accountManager, + onSubscriptionActivation: { + viewModel.handleEmailSubscriptionActivation() + }) + ), + isActive: $viewModel.isRestoringEmailSubscription) { EmptyView() } - + } private var listItems: [ListItem] { From 4fa94291e5ef75d994eedb534901876a3cc079e4 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Fri, 19 Jan 2024 23:10:12 +0100 Subject: [PATCH 10/28] Another dismissal approach --- .../SubscriptionEmailViewModel.swift | 9 ++++----- .../SubscriptionRestoreViewModel.swift | 7 ++----- .../Views/SubscriptionEmailView.swift | 1 - .../Views/SubscriptionRestoreView.swift | 19 ++++++++----------- 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index cee1ded2c7..a9bf97b4fa 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -29,7 +29,7 @@ final class SubscriptionEmailViewModel: ObservableObject { let accountManager: AccountManager let userScript: SubscriptionPagesUserScript let subFeature: SubscriptionPagesUseSubscriptionFeature - var onSubscriptionActivation: (() -> Void)? + var parentViewModel: SubscriptionRestoreViewModel var emailURL = URL.addEmailToSubscription var viewTitle = UserText.subscriptionRestoreAddEmailTitle @@ -42,11 +42,11 @@ final class SubscriptionEmailViewModel: ObservableObject { init(userScript: SubscriptionPagesUserScript, subFeature: SubscriptionPagesUseSubscriptionFeature, accountManager: AccountManager, - onSubscriptionActivation: @escaping (() -> Void)) { + parentViewModel: SubscriptionRestoreViewModel) { self.userScript = userScript self.subFeature = subFeature self.accountManager = accountManager - self.onSubscriptionActivation = onSubscriptionActivation + self.parentViewModel = parentViewModel initializeView() setupTransactionObservers() } @@ -72,8 +72,7 @@ final class SubscriptionEmailViewModel: ObservableObject { private func activateSubscription() { subFeature.emailActivationComplete = false subscriptionActive = true - print("activateSubscription called, notifying parent ViewModel") - onSubscriptionActivation?() + parentViewModel.handleEmailSubscriptionActivation() } } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift index ec505be0db..bd6fd18b0a 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -96,12 +96,9 @@ final class SubscriptionRestoreViewModel: ObservableObject { } func handleEmailSubscriptionActivation() { - DispatchQueue.main.async { - print("handleEmailSubscriptionActivation called") - self.isRestoringEmailSubscription = false + DispatchQueue.main.asyncAfter(deadline: .now()) { self.subscriptionActivatedViaEmail = true - print("isRestoringEmailSubscription set to false") - print("subscriptionActivatedViaEmail set to true") + self.isRestoringEmailSubscription = false } } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift index a962a3a25c..b8fac58f2d 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift @@ -25,7 +25,6 @@ import Foundation struct SubscriptionEmailView: View { @ObservedObject var viewModel: SubscriptionEmailViewModel - @Environment(\.dismiss) var dismiss var body: some View { ZStack { diff --git a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift index 7c88af51ce..af7906da9d 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift @@ -61,7 +61,6 @@ struct SubscriptionRestoreView: View { } } .onChange(of: viewModel.subscriptionActivatedViaEmail) { activated in - print("onChange triggered with activated: \(activated)") if activated { dismiss() } @@ -73,18 +72,16 @@ struct SubscriptionRestoreView: View { } // Activation View - NavigationLink(destination: SubscriptionEmailView(viewModel: SubscriptionEmailViewModel(userScript: viewModel.userScript, - subFeature: viewModel.subFeature, - accountManager: viewModel.accountManager, - onSubscriptionActivation: { - viewModel.handleEmailSubscriptionActivation() - }) - ), - isActive: $viewModel.isRestoringEmailSubscription) { + NavigationLink(destination: SubscriptionEmailView( + viewModel: SubscriptionEmailViewModel( + userScript: viewModel.userScript, + subFeature: viewModel.subFeature, + accountManager: viewModel.accountManager, + parentViewModel: viewModel + )), + isActive: $viewModel.isRestoringEmailSubscription) { EmptyView() } - - } private var listItems: [ListItem] { From 8b96fb83d00712c8a488c78752886052da55be2e Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Sat, 20 Jan 2024 01:00:00 +0100 Subject: [PATCH 11/28] Correct navigation after activating subscription --- .../SubscriptionEmailViewModel.swift | 3 --- .../SubscriptionRestoreViewModel.swift | 8 -------- .../Views/SubscriptionEmailView.swift | 19 ++++++++++++++----- .../Views/SubscriptionFlowView.swift | 4 +++- .../Views/SubscriptionRestoreView.swift | 15 ++------------- .../Views/SubscriptionSettingsView.swift | 2 ++ 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index a9bf97b4fa..c881458c66 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -29,7 +29,6 @@ final class SubscriptionEmailViewModel: ObservableObject { let accountManager: AccountManager let userScript: SubscriptionPagesUserScript let subFeature: SubscriptionPagesUseSubscriptionFeature - var parentViewModel: SubscriptionRestoreViewModel var emailURL = URL.addEmailToSubscription var viewTitle = UserText.subscriptionRestoreAddEmailTitle @@ -46,7 +45,6 @@ final class SubscriptionEmailViewModel: ObservableObject { self.userScript = userScript self.subFeature = subFeature self.accountManager = accountManager - self.parentViewModel = parentViewModel initializeView() setupTransactionObservers() } @@ -72,7 +70,6 @@ final class SubscriptionEmailViewModel: ObservableObject { private func activateSubscription() { subFeature.emailActivationComplete = false subscriptionActive = true - parentViewModel.handleEmailSubscriptionActivation() } } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift index bd6fd18b0a..60a9bf0167 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -32,7 +32,6 @@ final class SubscriptionRestoreViewModel: ObservableObject { let accountManager: AccountManager let isAddingDevice: Bool - var viewTitle: String { UserText.subscriptionActivate } @@ -95,12 +94,5 @@ final class SubscriptionRestoreViewModel: ObservableObject { isRestoringEmailSubscription = true } - func handleEmailSubscriptionActivation() { - DispatchQueue.main.asyncAfter(deadline: .now()) { - self.subscriptionActivatedViaEmail = true - self.isRestoringEmailSubscription = false - } - } - } #endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift index b8fac58f2d..ad7c4715f6 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift @@ -25,16 +25,25 @@ import Foundation struct SubscriptionEmailView: View { @ObservedObject var viewModel: SubscriptionEmailViewModel + @Binding var isActivatingSubscription: Bool var body: some View { ZStack { - AsyncHeadlessWebView(url: $viewModel.emailURL, - userScript: viewModel.userScript, - subFeature: viewModel.subFeature, - shouldReload: $viewModel.shouldReloadWebView).background() - + VStack { + AsyncHeadlessWebView(url: $viewModel.emailURL, + userScript: viewModel.userScript, + subFeature: viewModel.subFeature, + shouldReload: $viewModel.shouldReloadWebView).background() + } + } + .onChange(of: viewModel.subscriptionActive) { active in + if active { + isActivatingSubscription = false + } } .navigationTitle(viewModel.viewTitle) } + + } #endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index c997b0b45f..bbaf83f4a4 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -26,6 +26,7 @@ struct SubscriptionFlowView: View { @ObservedObject var viewModel: SubscriptionFlowViewModel @State private var isAlertVisible = false + @State private var isRestoringSubscription = false private func getTransactionStatus() -> String { switch viewModel.transactionStatus { @@ -53,7 +54,7 @@ struct SubscriptionFlowView: View { } // Activation View - NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel()), + NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel(), isActivatingSubscription: $viewModel.activatingSubscription), isActive: $viewModel.activatingSubscription) { EmptyView() } @@ -68,6 +69,7 @@ struct SubscriptionFlowView: View { isAlertVisible = true } } + .onAppear(perform: { Task { await viewModel.initializeViewData() } }) diff --git a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift index af7906da9d..cd53883877 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift @@ -29,6 +29,7 @@ struct SubscriptionRestoreView: View { @StateObject var viewModel: SubscriptionRestoreViewModel @State private var expandedItemId: Int = 0 @State private var isAlertVisible = false + @Binding var isActivatingSubscription: Bool private enum Constants { static let heroImage = "SyncTurnOnSyncHero" @@ -60,11 +61,6 @@ struct SubscriptionRestoreView: View { isAlertVisible = true } } - .onChange(of: viewModel.subscriptionActivatedViaEmail) { activated in - if activated { - dismiss() - } - } if viewModel.transactionStatus != .idle { PurchaseInProgressView(status: getTransactionStatus()) @@ -78,7 +74,7 @@ struct SubscriptionRestoreView: View { subFeature: viewModel.subFeature, accountManager: viewModel.accountManager, parentViewModel: viewModel - )), + ), isActivatingSubscription: $isActivatingSubscription), isActive: $viewModel.isRestoringEmailSubscription) { EmptyView() } @@ -239,11 +235,4 @@ struct SubscriptionRestoreView: View { let expandedContent: AnyView } } - -@available(iOS 15.0, *) -struct SubscriptionRestoreView_Previews: PreviewProvider { - static var previews: some View { - SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel()) - } -} #endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift index 40f4e2babe..8645cb4274 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift @@ -44,6 +44,7 @@ struct SubscriptionSettingsView: View { .hidden() }.textCase(nil) Section(header: Text(UserText.subscriptionManageDevices)) { + /* NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel(isAddingDevice: true))) { SettingsCustomCell(content: { Text(UserText.subscriptionAddDeviceButton) @@ -51,6 +52,7 @@ struct SubscriptionSettingsView: View { .foregroundColor(Color.init(designSystemColor: .accent)) }) } + */ SettingsCustomCell(content: { Text(UserText.subscriptionRemoveFromDevice) .daxBodyRegular() From 1deb26cce869b3d78065762f682040d2cbb3935a Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Sat, 20 Jan 2024 01:05:39 +0100 Subject: [PATCH 12/28] Restore subscription from email --- .../Subscription/ViewModel/SubscriptionEmailViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index c881458c66..79686b7d12 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -31,7 +31,7 @@ final class SubscriptionEmailViewModel: ObservableObject { let subFeature: SubscriptionPagesUseSubscriptionFeature var emailURL = URL.addEmailToSubscription - var viewTitle = UserText.subscriptionRestoreAddEmailTitle + var viewTitle = UserText.subscriptionRestoreEmail @Published var subscriptionEmail: String? @Published var shouldReloadWebView = false @Published var subscriptionActive = false From e58f9e54939f61a4ac93e9bce4c9f95e1889c21a Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Sat, 20 Jan 2024 02:29:55 +0100 Subject: [PATCH 13/28] Restore view for existing email --- .../SubscriptionEmailViewModel.swift | 3 +- .../SubscriptionRestoreViewModel.swift | 12 ------ .../Views/SubscriptionEmailView.swift | 4 +- .../Views/SubscriptionFlowView.swift | 3 +- .../Views/SubscriptionRestoreView.swift | 38 +++++++++++-------- .../Views/SubscriptionSettingsView.swift | 11 +++--- DuckDuckGo/UserText.swift | 4 +- DuckDuckGo/en.lproj/Localizable.strings | 3 ++ 8 files changed, 38 insertions(+), 40 deletions(-) diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index 79686b7d12..f4b4eb54cf 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -40,8 +40,7 @@ final class SubscriptionEmailViewModel: ObservableObject { init(userScript: SubscriptionPagesUserScript, subFeature: SubscriptionPagesUseSubscriptionFeature, - accountManager: AccountManager, - parentViewModel: SubscriptionRestoreViewModel) { + accountManager: AccountManager) { self.userScript = userScript self.subFeature = subFeature self.accountManager = accountManager diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift index 60a9bf0167..bdd35284e2 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -32,18 +32,6 @@ final class SubscriptionRestoreViewModel: ObservableObject { let accountManager: AccountManager let isAddingDevice: Bool - var viewTitle: String { - UserText.subscriptionActivate - } - - var headerTitle: String { - UserText.subscriptionActivateTitle - } - - var headerDescription: String { - UserText.subscriptionActivateDescription - } - enum SubscriptionActivationResult { case unknown, activated, notFound, error } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift index ad7c4715f6..623280002e 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift @@ -25,7 +25,7 @@ import Foundation struct SubscriptionEmailView: View { @ObservedObject var viewModel: SubscriptionEmailViewModel - @Binding var isActivatingSubscription: Bool + @Binding var isConfiguringSubscription: Bool var body: some View { ZStack { @@ -38,7 +38,7 @@ struct SubscriptionEmailView: View { } .onChange(of: viewModel.subscriptionActive) { active in if active { - isActivatingSubscription = false + isConfiguringSubscription = false } } .navigationTitle(viewModel.viewTitle) diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index bbaf83f4a4..e1a194f140 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -26,7 +26,6 @@ struct SubscriptionFlowView: View { @ObservedObject var viewModel: SubscriptionFlowViewModel @State private var isAlertVisible = false - @State private var isRestoringSubscription = false private func getTransactionStatus() -> String { switch viewModel.transactionStatus { @@ -54,7 +53,7 @@ struct SubscriptionFlowView: View { } // Activation View - NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel(), isActivatingSubscription: $viewModel.activatingSubscription), + NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel(), isConfiguringSubscription: $viewModel.activatingSubscription), isActive: $viewModel.activatingSubscription) { EmptyView() } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift index cd53883877..89ab28dd3b 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift @@ -29,7 +29,7 @@ struct SubscriptionRestoreView: View { @StateObject var viewModel: SubscriptionRestoreViewModel @State private var expandedItemId: Int = 0 @State private var isAlertVisible = false - @Binding var isActivatingSubscription: Bool + @Binding var isConfiguringSubscription: Bool private enum Constants { static let heroImage = "SyncTurnOnSyncHero" @@ -52,7 +52,7 @@ struct SubscriptionRestoreView: View { listView } .background(Color(designSystemColor: .container)) - .navigationTitle(viewModel.viewTitle) + .navigationTitle(viewModel.isAddingDevice ? UserText.subscriptionAddDeviceTitle : UserText.subscriptionActivate) .navigationBarBackButtonHidden(viewModel.transactionStatus != .idle) .applyInsetGroupedListStyle() .alert(isPresented: $isAlertVisible) { getAlert() } @@ -69,13 +69,12 @@ struct SubscriptionRestoreView: View { // Activation View NavigationLink(destination: SubscriptionEmailView( - viewModel: SubscriptionEmailViewModel( - userScript: viewModel.userScript, - subFeature: viewModel.subFeature, - accountManager: viewModel.accountManager, - parentViewModel: viewModel - ), isActivatingSubscription: $isActivatingSubscription), - isActive: $viewModel.isRestoringEmailSubscription) { + viewModel: SubscriptionEmailViewModel( + userScript: viewModel.userScript, + subFeature: viewModel.subFeature, + accountManager: viewModel.accountManager), + isConfiguringSubscription: $isConfiguringSubscription), + isActive: $viewModel.isRestoringEmailSubscription) { EmptyView() } } @@ -107,10 +106,12 @@ struct SubscriptionRestoreView: View { private func getAppleIDCellContent(buttonAction: @escaping () -> Void) -> AnyView { AnyView( VStack(alignment: .leading) { - Text(UserText.subscriptionActivateAppleIDDescription) + Text(viewModel.isAddingDevice ? UserText.subscriptionAvailableInApple : UserText.subscriptionActivateAppleIDDescription) .daxSubheadRegular() .foregroundColor(Color(designSystemColor: .textSecondary)) - getCellButton(buttonText: UserText.subscriptionActivateAppleIDButton, action: buttonAction) + if !viewModel.isAddingDevice { + getCellButton(buttonText: UserText.subscriptionActivateAppleIDButton, action: buttonAction) + } } ) } @@ -129,8 +130,15 @@ struct SubscriptionRestoreView: View { Text(UserText.subscriptionActivateEmailDescription) .daxSubheadRegular() .foregroundColor(Color(designSystemColor: .textSecondary)) - getCellButton(buttonText: UserText.subscriptionManageEmail, - action: buttonAction) + HStack { + getCellButton(buttonText: UserText.subscriptionManageEmail, + action: buttonAction) + Spacer() + Button(action: {}, label: { + Text(UserText.subscriptionManageEmailResendInstructions).daxButton().daxBodyBold() + }) + } + } } ) @@ -170,11 +178,11 @@ struct SubscriptionRestoreView: View { private var headerView: some View { VStack(spacing: Constants.headerLineSpacing) { Image(Constants.heroImage) - Text(viewModel.headerTitle) + Text(viewModel.isAddingDevice ? UserText.subscriptionAddDeviceHeaderTitle : UserText.subscriptionActivateTitle) .daxHeadline() .multilineTextAlignment(.center) .foregroundColor(Color(designSystemColor: .textPrimary)) - Text(viewModel.headerDescription) + Text(viewModel.isAddingDevice ? UserText.subscriptionAddDeviceDescription : UserText.subscriptionActivateDescription) .daxFootnoteRegular() .foregroundColor(Color(designSystemColor: .textSecondary)) .multilineTextAlignment(.center) diff --git a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift index 8645cb4274..6496211865 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift @@ -32,6 +32,7 @@ struct SubscriptionSettingsView: View { @ObservedObject var viewModel: SubscriptionSettingsViewModel @Environment(\.presentationMode) var presentationMode @StateObject var sceneEnvironment = SceneEnvironment() + @State private var isConfiguringSubscription = false var body: some View { List { @@ -44,15 +45,15 @@ struct SubscriptionSettingsView: View { .hidden() }.textCase(nil) Section(header: Text(UserText.subscriptionManageDevices)) { - /* - NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel(isAddingDevice: true))) { + + NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel(isAddingDevice: true), isConfiguringSubscription: $isConfiguringSubscription)) { SettingsCustomCell(content: { Text(UserText.subscriptionAddDeviceButton) .daxBodyRegular() .foregroundColor(Color.init(designSystemColor: .accent)) }) } - */ + SettingsCustomCell(content: { Text(UserText.subscriptionRemoveFromDevice) .daxBodyRegular() @@ -75,9 +76,7 @@ struct SubscriptionSettingsView: View { SettingsCustomCell(content: { Text(UserText.subscriptionFAQ) .daxBodyRegular() - }, - action: {}, - disclosureIndicator: true) + }) } } } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 6af317cf5b..0d0cd067ac 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1041,9 +1041,11 @@ But if you *do* want a peek under the hood, you can find more information about // Add to other devices (AppleID / Email) public static let subscriptionAddDeviceTitle = NSLocalizedString("subscription.add.device.title", value: "Add Device", comment: "Add to another device view title") - public static let subscriptionUseOnOtherDevices = NSLocalizedString("subscription.add.device.header.title", value: "Use your subscription on all your devices", comment: "Add subscription to other device title ") + public static let subscriptionAddDeviceHeaderTitle = NSLocalizedString("subscription.add.device.header.title", value: "Use your subscription on all your devices", comment: "Add subscription to other device title ") public static let subscriptionAddDeviceDescription = NSLocalizedString("subscription.add.device.description", value: "Access your Privacy Pro subscription on any of your devices via Apple ID or by adding an email address.", comment: "Subscription Add device Info") public static let subscriptionAvailableInApple = NSLocalizedString("subscription.available.apple", value: "Privacy Pro is available on any device signed in to the same Apple ID.", comment: "Subscription availability message on Apple devices") + public static let subscriptionManageEmailResendInstructions = NSLocalizedString("subscription.add.device.resend.instructions", value: "Resend Instructions.", comment: "Resend activation instructions button") + // Add Email To subscription public static let subscriptionAddEmail = NSLocalizedString("subscription.add.email", value: "Add an email address to activate your subscription on your other devices. We’ll only use this address to verify your subscription.", comment: "Add email to an existing subscription") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 97188925cd..e87049be5e 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1956,6 +1956,9 @@ But if you *do* want a peek under the hood, you can find more information about /* Add subscription to other device title */ "subscription.add.device.header.title" = "Use your subscription on all your devices"; +/* Resend activation instructions button */ +"subscription.add.device.resend.instructions" = "Resend Instructions."; + /* Add to another device view title */ "subscription.add.device.title" = "Add Device"; From 43f3b8ebfb2fac78982b0317f2c8124ff05b9e45 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Mon, 22 Jan 2024 16:03:35 +0100 Subject: [PATCH 14/28] Manage subscription email --- .../ViewModel/SubscriptionEmailViewModel.swift | 9 +++++---- .../ViewModel/SubscriptionRestoreViewModel.swift | 6 +++--- DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift | 3 ++- .../Subscription/Views/SubscriptionRestoreView.swift | 9 +++++---- DuckDuckGo/UserText.swift | 5 +++-- DuckDuckGo/en.lproj/Localizable.strings | 5 ++++- 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index f4b4eb54cf..3b5567505d 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -30,7 +30,7 @@ final class SubscriptionEmailViewModel: ObservableObject { let userScript: SubscriptionPagesUserScript let subFeature: SubscriptionPagesUseSubscriptionFeature - var emailURL = URL.addEmailToSubscription + var emailURL = URL.activateSubscriptionViaEmail var viewTitle = UserText.subscriptionRestoreEmail @Published var subscriptionEmail: String? @Published var shouldReloadWebView = false @@ -49,9 +49,10 @@ final class SubscriptionEmailViewModel: ObservableObject { } private func initializeView() { - subscriptionEmail = accountManager.email - if subscriptionEmail != nil { - emailURL = URL.activateSubscriptionViaEmail + // If use is authenticated, we want to "Add or manage email" instead of activating + if accountManager.isUserAuthenticated { + emailURL = accountManager.email == nil ? URL.addEmailToSubscription : URL.manageSubscriptionEmail + viewTitle = accountManager.email == nil ? UserText.subscriptionAddEmail : UserText.subscriptionManageEmailTitle } } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift index bdd35284e2..15b6e55762 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -39,7 +39,7 @@ final class SubscriptionRestoreViewModel: ObservableObject { @Published var transactionStatus: SubscriptionPagesUseSubscriptionFeature.TransactionStatus = .idle @Published var activationResult: SubscriptionActivationResult = .unknown @Published var subscriptionEmail: String? - @Published var isRestoringEmailSubscription: Bool = false + @Published var isManagingEmailSubscription: Bool = false @Published var subscriptionActivatedViaEmail: Bool = false init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), @@ -78,8 +78,8 @@ final class SubscriptionRestoreViewModel: ObservableObject { } } - func restoreEmailSubscription() { - isRestoringEmailSubscription = true + func manageEmailSubscription() { + isManagingEmailSubscription = true } } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index e1a194f140..b5fbf59569 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -53,7 +53,8 @@ struct SubscriptionFlowView: View { } // Activation View - NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel(), isConfiguringSubscription: $viewModel.activatingSubscription), + NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel(), + isConfiguringSubscription: $viewModel.activatingSubscription), isActive: $viewModel.activatingSubscription) { EmptyView() } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift index 89ab28dd3b..e09423667d 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift @@ -74,7 +74,7 @@ struct SubscriptionRestoreView: View { subFeature: viewModel.subFeature, accountManager: viewModel.accountManager), isConfiguringSubscription: $isConfiguringSubscription), - isActive: $viewModel.isRestoringEmailSubscription) { + isActive: $viewModel.isManagingEmailSubscription) { EmptyView() } } @@ -88,7 +88,7 @@ struct SubscriptionRestoreView: View { .init(id: 1, content: getCellTitle(icon: Constants.emailIcon, text: UserText.subscriptionActivateEmail), - expandedContent: getEmailCellContent(buttonAction: viewModel.restoreEmailSubscription )) + expandedContent: getEmailCellContent(buttonAction: viewModel.manageEmailSubscription )) ] } @@ -131,14 +131,15 @@ struct SubscriptionRestoreView: View { .daxSubheadRegular() .foregroundColor(Color(designSystemColor: .textSecondary)) HStack { - getCellButton(buttonText: UserText.subscriptionManageEmail, + getCellButton(buttonText: UserText.subscriptionManageEmailButton, action: buttonAction) + /* TO BE IMPLEMENTED ?? Spacer() Button(action: {}, label: { Text(UserText.subscriptionManageEmailResendInstructions).daxButton().daxBodyBold() }) + */ } - } } ) diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 0d0cd067ac..46296aef32 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1044,7 +1044,7 @@ But if you *do* want a peek under the hood, you can find more information about public static let subscriptionAddDeviceHeaderTitle = NSLocalizedString("subscription.add.device.header.title", value: "Use your subscription on all your devices", comment: "Add subscription to other device title ") public static let subscriptionAddDeviceDescription = NSLocalizedString("subscription.add.device.description", value: "Access your Privacy Pro subscription on any of your devices via Apple ID or by adding an email address.", comment: "Subscription Add device Info") public static let subscriptionAvailableInApple = NSLocalizedString("subscription.available.apple", value: "Privacy Pro is available on any device signed in to the same Apple ID.", comment: "Subscription availability message on Apple devices") - public static let subscriptionManageEmailResendInstructions = NSLocalizedString("subscription.add.device.resend.instructions", value: "Resend Instructions.", comment: "Resend activation instructions button") + public static let subscriptionManageEmailResendInstructions = NSLocalizedString("subscription.add.device.resend.instructions", value: "Resend Instructions", comment: "Resend activation instructions button") // Add Email To subscription @@ -1054,7 +1054,8 @@ But if you *do* want a peek under the hood, you can find more information about // Manage Subscription Email public static let subscriptionManageEmailDescription = NSLocalizedString("subscription.manage.email.description", value: "You can use this email to activate your subscription on your other devices.", comment: "Description for Email Management options") - public static let subscriptionManageEmail = NSLocalizedString("subscription.activate.manage.email.button", value: "Manage", comment: "Restore button title for Managing Email") + public static let subscriptionManageEmailButton = NSLocalizedString("subscription.activate.manage.email.button", value: "Manage", comment: "Restore button title for Managing Email") + public static let subscriptionManageEmailTitle = NSLocalizedString("subscription.activate.manage.email.title", value: "Manage Email", comment: "View Title for managing your email account") // Subscribe & Restore Flow public static let subscriptionFoundTitle = NSLocalizedString("subscription.found.title", value: "Subscription Found", comment: "Title for the existing subscription dialog") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index e87049be5e..192414428e 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1938,6 +1938,9 @@ But if you *do* want a peek under the hood, you can find more information about /* Restore button title for Managing Email */ "subscription.activate.manage.email.button" = "Manage"; +/* View Title for managing your email account */ +"subscription.activate.manage.email.title" = "Manage Email"; + /* Restore button title for AppleID */ "subscription.activate.restore.apple" = "Restore"; @@ -1957,7 +1960,7 @@ But if you *do* want a peek under the hood, you can find more information about "subscription.add.device.header.title" = "Use your subscription on all your devices"; /* Resend activation instructions button */ -"subscription.add.device.resend.instructions" = "Resend Instructions."; +"subscription.add.device.resend.instructions" = "Resend Instructions"; /* Add to another device view title */ "subscription.add.device.title" = "Add Device"; From 52d70fc468c19056d8d0c4ef9bcb14f93c237e7a Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Mon, 22 Jan 2024 17:22:44 +0100 Subject: [PATCH 15/28] Email Management flows --- ...scriptionPagesUseSubscriptionFeature.swift | 10 +++---- .../SubscriptionEmailViewModel.swift | 12 ++++---- .../ViewModel/SubscriptionFlowViewModel.swift | 4 +-- .../Subscription/Views/HeadlessWebView.swift | 28 ++++++++++++++++++- .../Views/SubscriptionEmailView.swift | 13 +++++++-- .../Views/SubscriptionFlowView.swift | 2 +- .../Views/SubscriptionRestoreView.swift | 9 ++++-- .../Views/SubscriptionSettingsView.swift | 4 +-- DuckDuckGo/UserText.swift | 5 ++++ DuckDuckGo/en.lproj/Localizable.strings | 9 ++++++ 10 files changed, 75 insertions(+), 21 deletions(-) diff --git a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift index 446430f35e..a481a14598 100644 --- a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift @@ -45,7 +45,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec static let backToSettings = "backToSettings" static let getSubscriptionOptions = "getSubscriptionOptions" static let subscriptionSelected = "subscriptionSelected" - static let subscriptionActive = "activateSubscription" + static let activateSubscription = "activateSubscription" static let featureSelected = "featureSelected" } @@ -66,7 +66,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec @Published var transactionStatus: TransactionStatus = .idle @Published var hasActiveSubscription = false @Published var purchaseError: AppStorePurchaseFlow.Error? - @Published var subscriptionActive: Bool = false + @Published var activateSubscription: Bool = false @Published var emailActivationComplete: Bool = false var broker: UserScriptMessageBroker? @@ -91,7 +91,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec case Handlers.backToSettings: return backToSettings case Handlers.getSubscriptionOptions: return getSubscriptionOptions case Handlers.subscriptionSelected: return subscriptionSelected - case Handlers.subscriptionActive: return subscriptionActive + case Handlers.activateSubscription: return activateSubscription case Handlers.featureSelected: return featureSelected default: return nil @@ -226,8 +226,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec return nil } - func subscriptionActive(params: Any, original: WKScriptMessage) async throws -> Encodable? { - subscriptionActive = true + func activateSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { + activateSubscription = true return nil } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index 3b5567505d..c89701d0f1 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -34,7 +34,8 @@ final class SubscriptionEmailViewModel: ObservableObject { var viewTitle = UserText.subscriptionRestoreEmail @Published var subscriptionEmail: String? @Published var shouldReloadWebView = false - @Published var subscriptionActive = false + @Published var activateSubscription = false + @Published var managingSubscriptionEmail = false private var cancellables = Set() @@ -52,7 +53,8 @@ final class SubscriptionEmailViewModel: ObservableObject { // If use is authenticated, we want to "Add or manage email" instead of activating if accountManager.isUserAuthenticated { emailURL = accountManager.email == nil ? URL.addEmailToSubscription : URL.manageSubscriptionEmail - viewTitle = accountManager.email == nil ? UserText.subscriptionAddEmail : UserText.subscriptionManageEmailTitle + viewTitle = accountManager.email == nil ? UserText.subscriptionRestoreAddEmailTitle : UserText.subscriptionManageEmailTitle + managingSubscriptionEmail = true } } @@ -61,15 +63,15 @@ final class SubscriptionEmailViewModel: ObservableObject { .receive(on: DispatchQueue.main) .sink { [weak self] value in if value { - self?.activateSubscription() + self?.completeActivation() } } .store(in: &cancellables) } - private func activateSubscription() { + private func completeActivation() { subFeature.emailActivationComplete = false - subscriptionActive = true + activateSubscription = true } } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index 08e27ff3b5..d0638d4862 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -67,11 +67,11 @@ final class SubscriptionFlowViewModel: ObservableObject { } .store(in: &cancellables) - subFeature.$subscriptionActive + subFeature.$activateSubscription .receive(on: DispatchQueue.main) .sink { [weak self] value in if value { - self?.subFeature.subscriptionActive = false + self?.subFeature.activateSubscription = false self?.activatingSubscription = true } } diff --git a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift index 2b3fafaeee..944fd1fb20 100644 --- a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift +++ b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift @@ -44,6 +44,8 @@ struct HeadlessWebview: UIViewRepresentable { webView.load(URLRequest(url: url)) } + webView.uiDelegate = context.coordinator + #if DEBUG if #available(iOS 16.4, *) { @@ -80,8 +82,32 @@ struct HeadlessWebview: UIViewRepresentable { return userContentController } - class Coordinator: NSObject { + class Coordinator: NSObject, WKUIDelegate { var webView: WKWebView? + + private func topMostViewController() -> UIViewController? { + var topController: UIViewController? = UIApplication.shared.windows.filter { $0.isKeyWindow } + .first? + .rootViewController + while let presentedViewController = topController?.presentedViewController { + topController = presentedViewController + } + return topController + } + + func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, + initiatedByFrame frame: WKFrameInfo, + completionHandler: @escaping (Bool) -> Void) { + let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: UserText.actionCancel, style: .cancel, handler: { _ in completionHandler(false) })) + alertController.addAction(UIAlertAction(title: UserText.actionOK, style: .default, handler: { _ in completionHandler(true) })) + + if let topController = topMostViewController() { + topController.present(alertController, animated: true, completion: nil) + } else { + completionHandler(false) + } + } } } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift index 623280002e..8a37c3ffec 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift @@ -25,7 +25,8 @@ import Foundation struct SubscriptionEmailView: View { @ObservedObject var viewModel: SubscriptionEmailViewModel - @Binding var isConfiguringSubscription: Bool + @Binding var isActivatingSubscription: Bool + @Environment(\.dismiss) var dismiss var body: some View { ZStack { @@ -36,9 +37,15 @@ struct SubscriptionEmailView: View { shouldReload: $viewModel.shouldReloadWebView).background() } } - .onChange(of: viewModel.subscriptionActive) { active in + .onChange(of: viewModel.activateSubscription) { active in if active { - isConfiguringSubscription = false + // We just need to dismiss the current view + if viewModel.managingSubscriptionEmail { + dismiss() + } else { + // Update the binding to tear down the entire view stack (Go back to initial webview) + isActivatingSubscription = false + } } } .navigationTitle(viewModel.viewTitle) diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index b5fbf59569..6538daedea 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -54,7 +54,7 @@ struct SubscriptionFlowView: View { // Activation View NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel(), - isConfiguringSubscription: $viewModel.activatingSubscription), + isActivatingSubscription: $viewModel.activatingSubscription), isActive: $viewModel.activatingSubscription) { EmptyView() } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift index e09423667d..3006cf4ace 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift @@ -29,7 +29,9 @@ struct SubscriptionRestoreView: View { @StateObject var viewModel: SubscriptionRestoreViewModel @State private var expandedItemId: Int = 0 @State private var isAlertVisible = false - @Binding var isConfiguringSubscription: Bool + + // Binding used to dismiss the entire stack (Go back to settings from several levels down) + @Binding var isActivatingSubscription: Bool private enum Constants { static let heroImage = "SyncTurnOnSyncHero" @@ -61,6 +63,9 @@ struct SubscriptionRestoreView: View { isAlertVisible = true } } + .onAppear { + viewModel.initializeView() + } if viewModel.transactionStatus != .idle { PurchaseInProgressView(status: getTransactionStatus()) @@ -73,7 +78,7 @@ struct SubscriptionRestoreView: View { userScript: viewModel.userScript, subFeature: viewModel.subFeature, accountManager: viewModel.accountManager), - isConfiguringSubscription: $isConfiguringSubscription), + isActivatingSubscription: $isActivatingSubscription), isActive: $viewModel.isManagingEmailSubscription) { EmptyView() } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift index 6496211865..5bf604473f 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift @@ -32,7 +32,7 @@ struct SubscriptionSettingsView: View { @ObservedObject var viewModel: SubscriptionSettingsViewModel @Environment(\.presentationMode) var presentationMode @StateObject var sceneEnvironment = SceneEnvironment() - @State private var isConfiguringSubscription = false + @State private var isActivatingSubscription = false var body: some View { List { @@ -46,7 +46,7 @@ struct SubscriptionSettingsView: View { }.textCase(nil) Section(header: Text(UserText.subscriptionManageDevices)) { - NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel(isAddingDevice: true), isConfiguringSubscription: $isConfiguringSubscription)) { + NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel(isAddingDevice: true), isActivatingSubscription: $isActivatingSubscription)) { SettingsCustomCell(content: { Text(UserText.subscriptionAddDeviceButton) .daxBodyRegular() diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 46296aef32..cea1ecdfdc 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -65,6 +65,7 @@ public struct UserText { public static let actionRemoveFavorite = NSLocalizedString("action.title.remove.favorite", value: "Remove Favorite", comment: "Remove Favorite action") public static let actionManageFavorites = NSLocalizedString("action.manage.favorites", value: "Manage", comment: "Button label for managing favorites") + public static let actionOK = NSLocalizedString("action.ok", value: "OK", comment: "Button label for OK action") public static let voiceoverSuggestionTypeWebsite = NSLocalizedString("voiceover.suggestion.type.website", value: "Open website", comment: "Open suggested website action accessibility title") public static let voiceoverSuggestionTypeBookmark = NSLocalizedString("voiceover.suggestion.type.bookmark", value: "Bookmark", comment: "Voice-over title for a Bookmark suggestion. Noun") @@ -1056,6 +1057,10 @@ But if you *do* want a peek under the hood, you can find more information about public static let subscriptionManageEmailDescription = NSLocalizedString("subscription.manage.email.description", value: "You can use this email to activate your subscription on your other devices.", comment: "Description for Email Management options") public static let subscriptionManageEmailButton = NSLocalizedString("subscription.activate.manage.email.button", value: "Manage", comment: "Restore button title for Managing Email") public static let subscriptionManageEmailTitle = NSLocalizedString("subscription.activate.manage.email.title", value: "Manage Email", comment: "View Title for managing your email account") + public static let subscriptionManageEmailCancelButton = NSLocalizedString("subscription.activate.manage.email.cancel", value: "Cancel", comment: "Button title for cancelling email deletion") + public static let subscriptionManageEmailOKButton = NSLocalizedString("subscription.activate.manage.email.OK", value: "OK", comment: "Button title for confirming email deletion") + + // Subscribe & Restore Flow public static let subscriptionFoundTitle = NSLocalizedString("subscription.found.title", value: "Subscription Found", comment: "Title for the existing subscription dialog") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 192414428e..c7a75a9b57 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -10,6 +10,9 @@ /* Button label for managing favorites */ "action.manage.favorites" = "Manage"; +/* Button label for OK action */ +"action.ok" = "OK"; + /* Add action - button shown in alert */ "action.title.add" = "Add"; @@ -1938,6 +1941,12 @@ But if you *do* want a peek under the hood, you can find more information about /* Restore button title for Managing Email */ "subscription.activate.manage.email.button" = "Manage"; +/* Button title for cancelling email deletion */ +"subscription.activate.manage.email.cancel" = "Cancel"; + +/* Button title for confirming email deletion */ +"subscription.activate.manage.email.OK" = "OK"; + /* View Title for managing your email account */ "subscription.activate.manage.email.title" = "Manage Email"; From e3a4dd4cf31f5ba4cbf2e1a42f061149d6462622 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Mon, 22 Jan 2024 17:41:45 +0100 Subject: [PATCH 16/28] Lint issues and comments --- .../Subscription/Subscription/Services/AuthService.swift | 4 +++- .../Subscription/ViewModel/SubscriptionEmailViewModel.swift | 4 +++- .../Subscription/ViewModel/SubscriptionRestoreViewModel.swift | 1 - DuckDuckGo/Subscription/Views/HeadlessWebView.swift | 3 +++ DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift | 3 ++- DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift | 4 +++- 6 files changed, 14 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo/Subscription/Subscription/Services/AuthService.swift b/DuckDuckGo/Subscription/Subscription/Services/AuthService.swift index 3bc47e2112..1e4b7730ac 100644 --- a/DuckDuckGo/Subscription/Subscription/Services/AuthService.swift +++ b/DuckDuckGo/Subscription/Subscription/Services/AuthService.swift @@ -106,10 +106,12 @@ public struct AuthService: APIService { public let externalID: String public let id: Int public let status: String - + + // swiftlint:disable nesting enum CodingKeys: String, CodingKey { // no underscores due to keyDecodingStrategy = .convertFromSnakeCase case authToken = "authToken", email, externalID = "externalId", id, status } + // swiftlint:enable nesting } } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index c89701d0f1..7e883f8e06 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -50,10 +50,12 @@ final class SubscriptionEmailViewModel: ObservableObject { } private func initializeView() { - // If use is authenticated, we want to "Add or manage email" instead of activating if accountManager.isUserAuthenticated { + // If user is authenticated, we want to "Add or manage email" instead of activating emailURL = accountManager.email == nil ? URL.addEmailToSubscription : URL.manageSubscriptionEmail viewTitle = accountManager.email == nil ? UserText.subscriptionRestoreAddEmailTitle : UserText.subscriptionManageEmailTitle + + // Also we assume subscription requires managing, and not activation managingSubscriptionEmail = true } } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift index 15b6e55762..85998e1bd5 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -40,7 +40,6 @@ final class SubscriptionRestoreViewModel: ObservableObject { @Published var activationResult: SubscriptionActivationResult = .unknown @Published var subscriptionEmail: String? @Published var isManagingEmailSubscription: Bool = false - @Published var subscriptionActivatedViaEmail: Bool = false init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), diff --git a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift index 944fd1fb20..e80e3f5bdc 100644 --- a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift +++ b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift @@ -95,6 +95,9 @@ struct HeadlessWebview: UIViewRepresentable { return topController } + // MARK: WKUIDelegate + + // Enables presenting Javascript alerts via the native layer (window.confirm()) func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { diff --git a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift index 8a37c3ffec..714e9696ea 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift @@ -43,7 +43,8 @@ struct SubscriptionEmailView: View { if viewModel.managingSubscriptionEmail { dismiss() } else { - // Update the binding to tear down the entire view stack (Go back to initial webview) + // Update the binding to tear down the entire view stack + // This dismisses all views in between and takes you back to the welcome page isActivatingSubscription = false } } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift index 5bf604473f..8e44ce451c 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift @@ -46,7 +46,9 @@ struct SubscriptionSettingsView: View { }.textCase(nil) Section(header: Text(UserText.subscriptionManageDevices)) { - NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel(isAddingDevice: true), isActivatingSubscription: $isActivatingSubscription)) { + NavigationLink(destination: SubscriptionRestoreView( + viewModel: SubscriptionRestoreViewModel(isAddingDevice: true), + isActivatingSubscription: $isActivatingSubscription)) { SettingsCustomCell(content: { Text(UserText.subscriptionAddDeviceButton) .daxBodyRegular() From 144b832ba3f052fd57152f1d4d29a4d200bee499 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Mon, 22 Jan 2024 18:43:05 +0100 Subject: [PATCH 17/28] Add device from welcome screen --- .../ViewModel/SubscriptionRestoreViewModel.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift index 85998e1bd5..458be8fad4 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -30,7 +30,7 @@ final class SubscriptionRestoreViewModel: ObservableObject { let subFeature: SubscriptionPagesUseSubscriptionFeature let purchaseManager: PurchaseManager let accountManager: AccountManager - let isAddingDevice: Bool + var isAddingDevice: Bool enum SubscriptionActivationResult { case unknown, activated, notFound, error @@ -56,6 +56,9 @@ final class SubscriptionRestoreViewModel: ObservableObject { func initializeView() { subscriptionEmail = accountManager.email + if accountManager.isUserAuthenticated { + isAddingDevice = true + } } @MainActor From 1404dd3ce03a015b292178dcb6f12321d933ad87 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Tue, 23 Jan 2024 20:44:57 +0100 Subject: [PATCH 18/28] OnAppear navigation --- DuckDuckGo.xcodeproj/project.pbxproj | 4 -- DuckDuckGo/SettingsSubscriptionView.swift | 4 +- DuckDuckGo/SettingsViewModel.swift | 37 ++++++++++++++++--- ...scriptionPagesUseSubscriptionFeature.swift | 10 +++-- .../SubscriptionFlowNavController.swift | 28 -------------- .../ViewModel/SubscriptionFlowViewModel.swift | 2 + .../Views/SubscriptionFlowView.swift | 21 +++++++++-- 7 files changed, 60 insertions(+), 46 deletions(-) delete mode 100644 DuckDuckGo/Subscription/ViewModel/SubscriptionFlowNavController.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 41d203e794..e97827414a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -797,7 +797,6 @@ D6D12CAB2B291CAA0054390C /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9C2B291CA90054390C /* APIService.swift */; }; D6D12CAC2B291CAA0054390C /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9D2B291CA90054390C /* AuthService.swift */; }; D6D12CAD2B291CAA0054390C /* PurchaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9E2B291CA90054390C /* PurchaseManager.swift */; }; - D6D4B77C2B5AE99500996546 /* SubscriptionFlowNavController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4B77B2B5AE99500996546 /* SubscriptionFlowNavController.swift */; }; D6E83C122B1E6AB3006C8AFB /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C112B1E6AB3006C8AFB /* SettingsView.swift */; }; D6E83C2E2B1EA06E006C8AFB /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C2D2B1EA06E006C8AFB /* SettingsViewModel.swift */; }; D6E83C312B1EA309006C8AFB /* SettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C302B1EA309006C8AFB /* SettingsCell.swift */; }; @@ -2443,7 +2442,6 @@ D6D12C9C2B291CA90054390C /* APIService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; D6D12C9D2B291CA90054390C /* AuthService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = ""; }; D6D12C9E2B291CA90054390C /* PurchaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseManager.swift; sourceTree = ""; }; - D6D4B77B2B5AE99500996546 /* SubscriptionFlowNavController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowNavController.swift; sourceTree = ""; }; D6E83C112B1E6AB3006C8AFB /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; D6E83C2D2B1EA06E006C8AFB /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; D6E83C302B1EA309006C8AFB /* SettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = ""; }; @@ -4533,7 +4531,6 @@ D664C7932B289AA000CBFA76 /* ViewModel */ = { isa = PBXGroup; children = ( - D6D4B77B2B5AE99500996546 /* SubscriptionFlowNavController.swift */, D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */, D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */, D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */, @@ -6501,7 +6498,6 @@ 1E162610296C5C630004127F /* CustomDaxDialogViewModel.swift in Sources */, 8590CB69268A4E190089F6BF /* DebugEtagStorage.swift in Sources */, D6D12CA62B291CAA0054390C /* AppStoreRestoreFlow.swift in Sources */, - D6D4B77C2B5AE99500996546 /* SubscriptionFlowNavController.swift in Sources */, C1CDA3162AFB9C7F006D1476 /* AutofillNeverPromptWebsitesManager.swift in Sources */, F1CA3C371F045878005FADB3 /* PrivacyStore.swift in Sources */, 37FCAAC029930E26000E420A /* FailedAssertionView.swift in Sources */, diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index 53069a89c2..334d4f6860 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -52,7 +52,9 @@ struct SettingsSubscriptionView: View { private var purchaseSubscriptionView: some View { return Group { SettingsCustomCell(content: { subscriptionDescriptionView }) - NavigationLink(destination: SubscriptionFlowView(viewModel: SubscriptionFlowViewModel())) { + NavigationLink(destination: SubscriptionFlowView(viewModel: SubscriptionFlowViewModel(), onFeatureSelected: { _ in + viewModel.onAppearNavigationTarget = .netP + })) { SettingsCustomCell(content: { learnMoreView }) } } diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index bff53aae81..299c6717f9 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -56,12 +56,13 @@ final class SettingsViewModel: ObservableObject { @UserDefaultsWrapper(key: .subscriptionIsActive, defaultValue: false) static private var cachedHasActiveSubscription: Bool - // Closures to interact with legacy view controllers throught the container + // Closures to interact with legacy view controllers through the container var onRequestPushLegacyView: ((UIViewController) -> Void)? var onRequestPresentLegacyView: ((UIViewController, _ modal: Bool) -> Void)? var onRequestPopLegacyView: (() -> Void)? var onRequestDismissSettings: (() -> Void)? + // Subscription Entitlement names: TBD static let entitlementNames = ["dummy1", "dummy2", "dummy3"] // Our View State @@ -82,6 +83,13 @@ final class SettingsViewModel: ObservableObject { var shouldShowNoMicrophonePermissionAlert: Bool = false + // Used to automatically navigate on Appear to a specific section + // Add more cases as needed + enum SettingsSection: String { + case none, netP, dbp, itp + } + var onAppearNavigationTarget: SettingsSection + // MARK: Bindings var themeBinding: Binding { Binding( @@ -183,10 +191,14 @@ final class SettingsViewModel: ObservableObject { } // MARK: Default Init - init(state: SettingsState? = nil, legacyViewProvider: SettingsLegacyViewProvider, accountManager: AccountManager) { + init(state: SettingsState? = nil, + legacyViewProvider: SettingsLegacyViewProvider, + accountManager: AccountManager, + navigateOnAppearDestination: SettingsSection = .none) { self.state = SettingsState.defaults self.legacyViewProvider = legacyViewProvider self.accountManager = accountManager + self.onAppearNavigationTarget = navigateOnAppearDestination } } @@ -287,7 +299,6 @@ extension SettingsViewModel { completion(true) } } - #if SUBSCRIPTION @available(iOS 15.0, *) @@ -348,7 +359,7 @@ extension SettingsViewModel { } } #endif - + } // MARK: Subscribers @@ -365,7 +376,7 @@ extension SettingsViewModel { } .store(in: &cancellables) #endif - + } } @@ -374,6 +385,7 @@ extension SettingsViewModel { func onAppear() { initState() + Task { await MainActor.run { navigateOnAppear() } } } func setAsDefaultBrowser() { @@ -401,6 +413,20 @@ extension SettingsViewModel { onRequestDismissSettings?() } + @MainActor + private func navigateOnAppear() { + switch onAppearNavigationTarget { + case .netP: + self.presentLegacyView(.netP) + case .dbp: + break + case .itp: + break + default: + break + } + } + } // MARK: Legacy View Presentation @@ -409,6 +435,7 @@ extension SettingsViewModel { // can review and migrate extension SettingsViewModel { + // swiftlint:disable:next cyclomatic_complexity @MainActor func presentLegacyView(_ view: SettingsLegacyViewProvider.LegacyView) { diff --git a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift index a481a14598..3b85bd54b5 100644 --- a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift @@ -63,11 +63,16 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec case idle, purchasing, restoring, polling } + struct FeatureSelection: Codable { + let feature: String + } + @Published var transactionStatus: TransactionStatus = .idle @Published var hasActiveSubscription = false @Published var purchaseError: AppStorePurchaseFlow.Error? @Published var activateSubscription: Bool = false @Published var emailActivationComplete: Bool = false + @Published var selectedFeature: FeatureSelection? var broker: UserScriptMessageBroker? @@ -232,14 +237,11 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec } func featureSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? { - struct FeatureSelection: Codable { - let feature: String - } - guard let featureSelection: FeatureSelection = DecodableHelper.decode(from: params) else { assertionFailure("SubscriptionPagesUserScript: expected JSON representation of FeatureSelection") return nil } + selectedFeature = featureSelection return nil } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowNavController.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowNavController.swift deleted file mode 100644 index ddd207de0d..0000000000 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowNavController.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// SubscriptionFlowNavController.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -class SubscriptionFlowNavController: ObservableObject { - @Published var shouldDisplayRestoreView: Bool = false { - didSet { - print("shouldDisplayRestoreView changed to: \(shouldDisplayRestoreView)") - } - } -} diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index d0638d4862..97b99e0871 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -40,6 +40,7 @@ final class SubscriptionFlowViewModel: ObservableObject { @Published var transactionStatus: SubscriptionPagesUseSubscriptionFeature.TransactionStatus = .idle @Published var shouldReloadWebView = false @Published var activatingSubscription = false + @Published var showDismissView = false init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), @@ -76,6 +77,7 @@ final class SubscriptionFlowViewModel: ObservableObject { } } .store(in: &cancellables) + } @MainActor diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index 6538daedea..40610d9630 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -24,8 +24,10 @@ import Foundation @available(iOS 15.0, *) struct SubscriptionFlowView: View { + @Environment(\.dismiss) var dismiss @ObservedObject var viewModel: SubscriptionFlowViewModel @State private var isAlertVisible = false + var onFeatureSelected: ((String) -> Void) private func getTransactionStatus() -> String { switch viewModel.transactionStatus { @@ -42,10 +44,16 @@ struct SubscriptionFlowView: View { var body: some View { ZStack { - AsyncHeadlessWebView(url: $viewModel.purchaseURL, - userScript: viewModel.userScript, - subFeature: viewModel.subFeature, - shouldReload: $viewModel.shouldReloadWebView).background() + VStack { + Button(action: { + onFeatureSelected("vpn") + }, label: { Text("tap me!!!!") }) + AsyncHeadlessWebView(url: $viewModel.purchaseURL, + userScript: viewModel.userScript, + subFeature: viewModel.subFeature, + shouldReload: $viewModel.shouldReloadWebView).background() + } + // Overlay that appears when transaction is in progress if viewModel.transactionStatus != .idle { @@ -69,6 +77,11 @@ struct SubscriptionFlowView: View { isAlertVisible = true } } + .onChange(of: viewModel.showDismissView) { result in + if result { + dismiss() + } + } .onAppear(perform: { Task { await viewModel.initializeViewData() } From 4b0f086f19e2164911408cf776c0bd457e7e3014 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 24 Jan 2024 00:24:55 +0100 Subject: [PATCH 19/28] Settings onAppear Navigation --- DuckDuckGo/SettingsSubscriptionView.swift | 10 +++--- DuckDuckGo/SettingsView.swift | 1 + DuckDuckGo/SettingsViewModel.swift | 32 +++++++++++++------ .../Views/SubscriptionFlowView.swift | 3 +- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index 334d4f6860..f97e808052 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -52,8 +52,10 @@ struct SettingsSubscriptionView: View { private var purchaseSubscriptionView: some View { return Group { SettingsCustomCell(content: { subscriptionDescriptionView }) - NavigationLink(destination: SubscriptionFlowView(viewModel: SubscriptionFlowViewModel(), onFeatureSelected: { _ in - viewModel.onAppearNavigationTarget = .netP + NavigationLink(destination: SubscriptionFlowView(viewModel: SubscriptionFlowViewModel(), onFeatureSelected: { value in + if let section = SettingsViewModel.SettingsSection(rawValue: value) { + viewModel.onAppearNavigationTarget = section + } })) { SettingsCustomCell(content: { learnMoreView }) } @@ -68,11 +70,11 @@ struct SettingsSubscriptionView: View { disclosureIndicator: true, isButton: true) - NavigationLink(destination: Text("Data Broker Protection")) { + NavigationLink(destination: Text("Data Broker Protection"), isActive: $viewModel.shouldNavigateToDBP) { SettingsCellView(label: UserText.settingsPProDBPTitle, subtitle: UserText.settingsPProDBPSubTitle) } - NavigationLink(destination: Text("Identity Theft Restoration")) { + NavigationLink(destination: Text("Identity Theft Restoration"), isActive: $viewModel.shouldNavigateToITP) { SettingsCellView(label: UserText.settingsPProITRTitle, subtitle: UserText.settingsPProITRSubTitle) } diff --git a/DuckDuckGo/SettingsView.swift b/DuckDuckGo/SettingsView.swift index 0bfd697ef4..2003c6fa43 100644 --- a/DuckDuckGo/SettingsView.swift +++ b/DuckDuckGo/SettingsView.swift @@ -42,6 +42,7 @@ struct SettingsView: View { SettingsMoreView() SettingsAboutView() SettingsDebugView() + } .navigationBarTitle(UserText.settingsTitle, displayMode: .inline) .navigationBarItems(trailing: Button(UserText.navigationTitleDone) { diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index 299c6717f9..ac9444547a 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -62,6 +62,10 @@ final class SettingsViewModel: ObservableObject { var onRequestPopLegacyView: (() -> Void)? var onRequestDismissSettings: (() -> Void)? + // SwiftUI Navigation + @Published var shouldNavigateToDBP = false + @Published var shouldNavigateToITP = false + // Subscription Entitlement names: TBD static let entitlementNames = ["dummy1", "dummy2", "dummy3"] @@ -88,7 +92,8 @@ final class SettingsViewModel: ObservableObject { enum SettingsSection: String { case none, netP, dbp, itp } - var onAppearNavigationTarget: SettingsSection + @Published var onAppearNavigationTarget: SettingsSection + @Published var currentNavigationTarget: SettingsSection? // MARK: Bindings var themeBinding: Binding { @@ -415,17 +420,26 @@ extension SettingsViewModel { @MainActor private func navigateOnAppear() { - switch onAppearNavigationTarget { - case .netP: + // We need a short delay to let the SwifttUI view lifecycle complete + // Otherwise the transition can be inconsistent + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + switch self.onAppearNavigationTarget { + case .netP: self.presentLegacyView(.netP) - case .dbp: - break - case .itp: - break - default: - break + case .dbp: + self.shouldNavigateToDBP = true + case .itp: + self.shouldNavigateToITP = true + default: + break + } + self.onAppearNavigationTarget = .none } } + + func resetNavigationTarget() { + currentNavigationTarget = nil + } } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index 40610d9630..83468a1276 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -46,7 +46,8 @@ struct SubscriptionFlowView: View { ZStack { VStack { Button(action: { - onFeatureSelected("vpn") + dismiss() + onFeatureSelected("itp") }, label: { Text("tap me!!!!") }) AsyncHeadlessWebView(url: $viewModel.purchaseURL, userScript: viewModel.userScript, From 12eb88e6a73ae24a9d601d0a813117633b5462eb Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 24 Jan 2024 01:24:44 +0100 Subject: [PATCH 20/28] Navigate back to Settings and open an existing subscription feature --- DuckDuckGo/SettingsGeneralView.swift | 2 +- DuckDuckGo/SettingsSubscriptionView.swift | 12 ++--- DuckDuckGo/SettingsViewModel.swift | 9 +--- .../ViewModel/SubscriptionFlowViewModel.swift | 53 ++++++++++++++++++- .../Views/SubscriptionFlowView.swift | 7 +-- 5 files changed, 60 insertions(+), 23 deletions(-) diff --git a/DuckDuckGo/SettingsGeneralView.swift b/DuckDuckGo/SettingsGeneralView.swift index db6de8cc18..4779ef3b6d 100644 --- a/DuckDuckGo/SettingsGeneralView.swift +++ b/DuckDuckGo/SettingsGeneralView.swift @@ -34,7 +34,7 @@ struct SettingsGeneralView: View { action: { viewModel.presentLegacyView(.addToDock) }, isButton: true) - NavigationLink(destination: WidgetEducationView()) { + NavigationLink(destination: WidgetEducationView(), isActive: $viewModel.shouldNavigateAddWidget) { SettingsCellView(label: UserText.settingsAddWidget) } } diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index f97e808052..11e681147d 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -47,16 +47,14 @@ struct SettingsSubscriptionView: View { .daxBodyRegular() .foregroundColor(Color.init(designSystemColor: .accent)) } - - + private var purchaseSubscriptionView: some View { return Group { SettingsCustomCell(content: { subscriptionDescriptionView }) - NavigationLink(destination: SubscriptionFlowView(viewModel: SubscriptionFlowViewModel(), onFeatureSelected: { value in - if let section = SettingsViewModel.SettingsSection(rawValue: value) { - viewModel.onAppearNavigationTarget = section - } - })) { + let viewModel = SubscriptionFlowViewModel(onFeatureSelected: { value in + self.viewModel.onAppearNavigationTarget = value + }) + NavigationLink(destination: SubscriptionFlowView(viewModel: viewModel)) { SettingsCustomCell(content: { learnMoreView }) } } diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index ac9444547a..8980bc21af 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -62,7 +62,8 @@ final class SettingsViewModel: ObservableObject { var onRequestPopLegacyView: (() -> Void)? var onRequestDismissSettings: (() -> Void)? - // SwiftUI Navigation + // SwiftUI Navigation Variables + @Published var shouldNavigateAddWidget = false @Published var shouldNavigateToDBP = false @Published var shouldNavigateToITP = false @@ -88,12 +89,10 @@ final class SettingsViewModel: ObservableObject { var shouldShowNoMicrophonePermissionAlert: Bool = false // Used to automatically navigate on Appear to a specific section - // Add more cases as needed enum SettingsSection: String { case none, netP, dbp, itp } @Published var onAppearNavigationTarget: SettingsSection - @Published var currentNavigationTarget: SettingsSection? // MARK: Bindings var themeBinding: Binding { @@ -436,10 +435,6 @@ extension SettingsViewModel { self.onAppearNavigationTarget = .none } } - - func resetNavigationTarget() { - currentNavigationTarget = nil - } } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index 97b99e0871..b345816355 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -36,18 +36,33 @@ final class SubscriptionFlowViewModel: ObservableObject { // State variables var purchaseURL = URL.purchaseSubscription + + // Closure passed to navigate to a specific section + // after returning to settings + var onFeatureSelected: ((SettingsViewModel.SettingsSection) -> Void) + + enum FeatureName { + static let netP = "vpn" + static let itp = "identity-theft-restoration" + static let dbp = "personal-information-removal" + } + + + // Published properties @Published var hasActiveSubscription = false @Published var transactionStatus: SubscriptionPagesUseSubscriptionFeature.TransactionStatus = .idle @Published var shouldReloadWebView = false @Published var activatingSubscription = false - @Published var showDismissView = false + @Published var shouldDismissView = false init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), - purchaseManager: PurchaseManager = PurchaseManager.shared) { + purchaseManager: PurchaseManager = PurchaseManager.shared, + onFeatureSelected: @escaping ((SettingsViewModel.SettingsSection) -> Void)) { self.userScript = userScript self.subFeature = subFeature self.purchaseManager = purchaseManager + self.onFeatureSelected = onFeatureSelected } // Observe transaction status @@ -78,6 +93,27 @@ final class SubscriptionFlowViewModel: ObservableObject { } .store(in: &cancellables) + subFeature.$selectedFeature + .receive(on: DispatchQueue.main) + .sink { [weak self] value in + if value != nil { + self?.shouldDismissView = true + switch value?.feature { + case FeatureName.netP: + self?.onFeatureSelected(.netP) + case FeatureName.itp: + self?.onFeatureSelected(.itp) + case FeatureName.dbp: + self?.onFeatureSelected(.dbp) + default: + return + } + + + } + } + .store(in: &cancellables) + } @MainActor @@ -85,6 +121,19 @@ final class SubscriptionFlowViewModel: ObservableObject { self.transactionStatus = status } + private func getFeatureName(feature: String) -> SettingsViewModel.SettingsSection { + switch feature { + case "vpn": + return .netP + case "identity-theft-protection": + return .itp + case "data-broker-protection": + return .dbp + default: + return .none + } + } + func initializeViewData() async { await self.setupTransactionObserver() await MainActor.run { shouldReloadWebView = true } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index 83468a1276..8b6fcd87d2 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -27,7 +27,6 @@ struct SubscriptionFlowView: View { @Environment(\.dismiss) var dismiss @ObservedObject var viewModel: SubscriptionFlowViewModel @State private var isAlertVisible = false - var onFeatureSelected: ((String) -> Void) private func getTransactionStatus() -> String { switch viewModel.transactionStatus { @@ -45,10 +44,6 @@ struct SubscriptionFlowView: View { var body: some View { ZStack { VStack { - Button(action: { - dismiss() - onFeatureSelected("itp") - }, label: { Text("tap me!!!!") }) AsyncHeadlessWebView(url: $viewModel.purchaseURL, userScript: viewModel.userScript, subFeature: viewModel.subFeature, @@ -78,7 +73,7 @@ struct SubscriptionFlowView: View { isAlertVisible = true } } - .onChange(of: viewModel.showDismissView) { result in + .onChange(of: viewModel.shouldDismissView) { result in if result { dismiss() } From 9907bb93b9df51354ec68b0eb05cb4460c8b856a Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 24 Jan 2024 01:28:25 +0100 Subject: [PATCH 21/28] Removed whitespace --- .../Subscription/ViewModel/SubscriptionFlowViewModel.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index b345816355..0eae9ee220 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -108,8 +108,6 @@ final class SubscriptionFlowViewModel: ObservableObject { default: return } - - } } .store(in: &cancellables) From ae9d84c4860433266e1acef7b89d8607a7c425b4 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 24 Jan 2024 02:00:57 +0100 Subject: [PATCH 22/28] Cleanup + comments --- .../xcshareddata/swiftpm/Package.resolved | 2 +- DuckDuckGo/SettingsGeneralView.swift | 2 +- DuckDuckGo/SettingsView.swift | 1 - DuckDuckGo/SettingsViewModel.swift | 6 ++---- .../ViewModel/SubscriptionFlowViewModel.swift | 17 +---------------- .../Subscription/Views/HeadlessWebView.swift | 6 ++---- 6 files changed, 7 insertions(+), 27 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6c74085f32..d85be58c3b 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -156,7 +156,7 @@ { "identity" : "trackerradarkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/TrackerRadarKit.git", + "location" : "https://github.com/duckduckgo/TrackerRadarKit", "state" : { "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", "version" : "1.2.2" diff --git a/DuckDuckGo/SettingsGeneralView.swift b/DuckDuckGo/SettingsGeneralView.swift index 4779ef3b6d..db6de8cc18 100644 --- a/DuckDuckGo/SettingsGeneralView.swift +++ b/DuckDuckGo/SettingsGeneralView.swift @@ -34,7 +34,7 @@ struct SettingsGeneralView: View { action: { viewModel.presentLegacyView(.addToDock) }, isButton: true) - NavigationLink(destination: WidgetEducationView(), isActive: $viewModel.shouldNavigateAddWidget) { + NavigationLink(destination: WidgetEducationView()) { SettingsCellView(label: UserText.settingsAddWidget) } } diff --git a/DuckDuckGo/SettingsView.swift b/DuckDuckGo/SettingsView.swift index 2003c6fa43..0bfd697ef4 100644 --- a/DuckDuckGo/SettingsView.swift +++ b/DuckDuckGo/SettingsView.swift @@ -42,7 +42,6 @@ struct SettingsView: View { SettingsMoreView() SettingsAboutView() SettingsDebugView() - } .navigationBarTitle(UserText.settingsTitle, displayMode: .inline) .navigationBarItems(trailing: Button(UserText.navigationTitleDone) { diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index 8980bc21af..fd452ddeca 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -62,8 +62,8 @@ final class SettingsViewModel: ObservableObject { var onRequestPopLegacyView: (() -> Void)? var onRequestDismissSettings: (() -> Void)? - // SwiftUI Navigation Variables - @Published var shouldNavigateAddWidget = false + // SwiftUI Programatic Navigation Variables + // Add more views as needed here... @Published var shouldNavigateToDBP = false @Published var shouldNavigateToITP = false @@ -363,7 +363,6 @@ extension SettingsViewModel { } } #endif - } // MARK: Subscribers @@ -444,7 +443,6 @@ extension SettingsViewModel { // can review and migrate extension SettingsViewModel { - // swiftlint:disable:next cyclomatic_complexity @MainActor func presentLegacyView(_ view: SettingsLegacyViewProvider.LegacyView) { diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index 0eae9ee220..89cd53837b 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -29,7 +29,6 @@ final class SubscriptionFlowViewModel: ObservableObject { let userScript: SubscriptionPagesUserScript let subFeature: SubscriptionPagesUseSubscriptionFeature let purchaseManager: PurchaseManager - let viewTitle = UserText.settingsPProSection private var cancellables = Set() @@ -46,8 +45,7 @@ final class SubscriptionFlowViewModel: ObservableObject { static let itp = "identity-theft-restoration" static let dbp = "personal-information-removal" } - - + // Published properties @Published var hasActiveSubscription = false @Published var transactionStatus: SubscriptionPagesUseSubscriptionFeature.TransactionStatus = .idle @@ -119,19 +117,6 @@ final class SubscriptionFlowViewModel: ObservableObject { self.transactionStatus = status } - private func getFeatureName(feature: String) -> SettingsViewModel.SettingsSection { - switch feature { - case "vpn": - return .netP - case "identity-theft-protection": - return .itp - case "data-broker-protection": - return .dbp - default: - return .none - } - } - func initializeViewData() async { await self.setupTransactionObserver() await MainActor.run { shouldReloadWebView = true } diff --git a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift index e80e3f5bdc..cdcb36a770 100644 --- a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift +++ b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift @@ -35,11 +35,9 @@ struct HeadlessWebview: UIViewRepresentable { configuration.userContentController = makeUserContentController() let webView = WKWebView(frame: .zero, configuration: configuration) + DefaultUserAgentManager.shared.update(webView: webView, isDesktop: false, url: url) - // We're using the macOS agent as the config for iOS has not been deployed in test env - webView.customUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko)" - // DefaultUserAgentManager.shared.update(webView: webView, isDesktop: false, url: url) - + // Just add time if you need to hook the webview debugger DispatchQueue.main.asyncAfter(deadline: .now() + 0) { webView.load(URLRequest(url: url)) } From f9e0e6ad45b76dedd3db19416c3a572101a20a60 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 24 Jan 2024 02:01:16 +0100 Subject: [PATCH 23/28] Comment --- DuckDuckGo/Subscription/Views/HeadlessWebView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift index cdcb36a770..dd90be78b0 100644 --- a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift +++ b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift @@ -37,7 +37,7 @@ struct HeadlessWebview: UIViewRepresentable { let webView = WKWebView(frame: .zero, configuration: configuration) DefaultUserAgentManager.shared.update(webView: webView, isDesktop: false, url: url) - // Just add time if you need to hook the webview debugger + // Just add time if you need to hook the WebView debugger DispatchQueue.main.asyncAfter(deadline: .now() + 0) { webView.load(URLRequest(url: url)) } From 7a054a19bfd98718d4bbf5eae355e8207ae978e4 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 24 Jan 2024 02:01:58 +0100 Subject: [PATCH 24/28] Comment --- DuckDuckGo/Subscription/Views/HeadlessWebView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift index dd90be78b0..116e34f771 100644 --- a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift +++ b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift @@ -37,7 +37,7 @@ struct HeadlessWebview: UIViewRepresentable { let webView = WKWebView(frame: .zero, configuration: configuration) DefaultUserAgentManager.shared.update(webView: webView, isDesktop: false, url: url) - // Just add time if you need to hook the WebView debugger + // Just add time if you need to hook the WebView inspector DispatchQueue.main.asyncAfter(deadline: .now() + 0) { webView.load(URLRequest(url: url)) } From 2704c3ce3a74c82cd2c55e6c4ba11550f0179857 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 24 Jan 2024 02:22:12 +0100 Subject: [PATCH 25/28] Comment --- DuckDuckGo/SettingsState.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/SettingsState.swift b/DuckDuckGo/SettingsState.swift index 9d9ff181a5..e6568efc7d 100644 --- a/DuckDuckGo/SettingsState.swift +++ b/DuckDuckGo/SettingsState.swift @@ -87,7 +87,7 @@ struct SettingsState { // Subscriptions Properties var subscription: Subscription - // Sync Propertiers + // Sync Properties var sync: SyncSettings static var defaults: SettingsState { From 9a34d708b2b0f24c65349fcd80fe4f0e73887512 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 24 Jan 2024 11:53:08 +0100 Subject: [PATCH 26/28] Subscription removal confirmation Toast --- .../ViewModel/SubscriptionSettingsViewModel.swift | 1 + DuckDuckGo/UserText.swift | 5 ++--- DuckDuckGo/en.lproj/Localizable.strings | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift index d7f5d98351..4cf960c44f 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift @@ -63,6 +63,7 @@ final class SubscriptionSettingsViewModel: ObservableObject { func removeSubscription() { AccountManager().signOut() + ActionMessageView.present(message: UserText.subscriptionRemovalConfirmation) } func manageSubscription() { diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index cd5f49ad45..caaaf665b7 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1058,6 +1058,7 @@ But if you *do* want a peek under the hood, you can find more information about public static let subscriptionRemoveFromDeviceConfirmText = NSLocalizedString("subscription.remove.from.device.text", value: "You will no longer be able to access your Privacy Pro subscription on this device. This will not cancel your subscription, and it will remain active on your other devices.", comment: "Remove from device confirmation dialog text") public static let subscriptionRemove = NSLocalizedString("subscription.remove.subscription", value: "Remove Subscription", comment: "Remove subscription button text") public static let subscriptionRemoveCancel = NSLocalizedString("subscription.remove.subscription.cancel", value: "Cancel", comment: "Remove subscription cancel button text") + public static let subscriptionRemovalConfirmation = NSLocalizedString("subscription.cancel.message", value: "Your subscription has been removed from this device.", comment: "Subscription Removal confirmation message") // Subscription Restore public static let subscriptionActivate = NSLocalizedString("subscription.activate", value: "Activate Subscription", comment: "Subscription Activation Window Title") @@ -1091,8 +1092,6 @@ But if you *do* want a peek under the hood, you can find more information about public static let subscriptionManageEmailCancelButton = NSLocalizedString("subscription.activate.manage.email.cancel", value: "Cancel", comment: "Button title for cancelling email deletion") public static let subscriptionManageEmailOKButton = NSLocalizedString("subscription.activate.manage.email.OK", value: "OK", comment: "Button title for confirming email deletion") - - // Subscribe & Restore Flow public static let subscriptionFoundTitle = NSLocalizedString("subscription.found.title", value: "Subscription Found", comment: "Title for the existing subscription dialog") public static let subscriptionFoundText = NSLocalizedString("subscription.found.text", value: "We found a subscription associated with this Apple ID.", comment: "Message for the existing subscription dialog") @@ -1104,6 +1103,6 @@ But if you *do* want a peek under the hood, you can find more information about public static let subscriptionRestoreSuccessfulTitle = NSLocalizedString("subscription.restore.success.alert.title", value: "You’re all set.", comment: "Alert title for restored purchase") public static let subscriptionRestoreSuccessfulMessage = NSLocalizedString("subscription.restore.success.alert.message", value: "Your purchases have been restored.", comment: "Alert message for restored purchase") public static let subscriptionRestoreSuccessfulButton = NSLocalizedString("subscription.restore.success.alert.button", value: "OK", comment: "Alert button text for restored purchase alert") - + } diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index f50749e44b..815afd61b6 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1986,6 +1986,9 @@ But if you *do* want a peek under the hood, you can find more information about /* Subscription availability message on Apple devices */ "subscription.available.apple" = "Privacy Pro is available on any device signed in to the same Apple ID."; +/* Subscription Removal confirmation message */ +"subscription.cancel.message" = "Your subscription has been removed from this device."; + /* Change plan or billing title */ "subscription.change.plan" = "Change Plan Or Billing"; From 1c81e932052e946e3a53623e99805c1ba9cab91e Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 24 Jan 2024 11:56:15 +0100 Subject: [PATCH 27/28] Remove unwanted class --- DuckDuckGo.xcodeproj/project.pbxproj | 4 --- .../SubscriptionFlowNavController.swift | 28 ------------------- 2 files changed, 32 deletions(-) delete mode 100644 DuckDuckGo/Subscription/ViewModel/SubscriptionFlowNavController.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 91f0ebe1a5..69bb109f32 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -805,7 +805,6 @@ D6D12CAB2B291CAA0054390C /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9C2B291CA90054390C /* APIService.swift */; }; D6D12CAC2B291CAA0054390C /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9D2B291CA90054390C /* AuthService.swift */; }; D6D12CAD2B291CAA0054390C /* PurchaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9E2B291CA90054390C /* PurchaseManager.swift */; }; - D6D4B77C2B5AE99500996546 /* SubscriptionFlowNavController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4B77B2B5AE99500996546 /* SubscriptionFlowNavController.swift */; }; D6E83C122B1E6AB3006C8AFB /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C112B1E6AB3006C8AFB /* SettingsView.swift */; }; D6E83C2E2B1EA06E006C8AFB /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C2D2B1EA06E006C8AFB /* SettingsViewModel.swift */; }; D6E83C312B1EA309006C8AFB /* SettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C302B1EA309006C8AFB /* SettingsCell.swift */; }; @@ -2458,7 +2457,6 @@ D6D12C9C2B291CA90054390C /* APIService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; D6D12C9D2B291CA90054390C /* AuthService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = ""; }; D6D12C9E2B291CA90054390C /* PurchaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseManager.swift; sourceTree = ""; }; - D6D4B77B2B5AE99500996546 /* SubscriptionFlowNavController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowNavController.swift; sourceTree = ""; }; D6E83C112B1E6AB3006C8AFB /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; D6E83C2D2B1EA06E006C8AFB /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; D6E83C302B1EA309006C8AFB /* SettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = ""; }; @@ -4562,7 +4560,6 @@ D664C7932B289AA000CBFA76 /* ViewModel */ = { isa = PBXGroup; children = ( - D6D4B77B2B5AE99500996546 /* SubscriptionFlowNavController.swift */, D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */, D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */, D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */, @@ -6534,7 +6531,6 @@ 1E162610296C5C630004127F /* CustomDaxDialogViewModel.swift in Sources */, 8590CB69268A4E190089F6BF /* DebugEtagStorage.swift in Sources */, D6D12CA62B291CAA0054390C /* AppStoreRestoreFlow.swift in Sources */, - D6D4B77C2B5AE99500996546 /* SubscriptionFlowNavController.swift in Sources */, C1CDA3162AFB9C7F006D1476 /* AutofillNeverPromptWebsitesManager.swift in Sources */, F1CA3C371F045878005FADB3 /* PrivacyStore.swift in Sources */, 37FCAAC029930E26000E420A /* FailedAssertionView.swift in Sources */, diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowNavController.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowNavController.swift deleted file mode 100644 index ddd207de0d..0000000000 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowNavController.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// SubscriptionFlowNavController.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -class SubscriptionFlowNavController: ObservableObject { - @Published var shouldDisplayRestoreView: Bool = false { - didSet { - print("shouldDisplayRestoreView changed to: \(shouldDisplayRestoreView)") - } - } -} From 6dad555506486aecea8cceef6d11c48b10f256e1 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 24 Jan 2024 12:00:46 +0100 Subject: [PATCH 28/28] Cleanup --- DuckDuckGo/UserText.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index caaaf665b7..531900ac59 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1104,5 +1104,4 @@ But if you *do* want a peek under the hood, you can find more information about public static let subscriptionRestoreSuccessfulMessage = NSLocalizedString("subscription.restore.success.alert.message", value: "Your purchases have been restored.", comment: "Alert message for restored purchase") public static let subscriptionRestoreSuccessfulButton = NSLocalizedString("subscription.restore.success.alert.button", value: "OK", comment: "Alert button text for restored purchase alert") - }