diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 24acdb46c7..91f0ebe1a5 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -776,6 +776,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 */; }; @@ -803,6 +805,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 */; }; @@ -2426,6 +2429,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 = ""; }; @@ -2453,6 +2458,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 = ""; }; @@ -4556,9 +4562,11 @@ D664C7932B289AA000CBFA76 /* ViewModel */ = { isa = PBXGroup; children = ( + D6D4B77B2B5AE99500996546 /* SubscriptionFlowNavController.swift */, D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */, - D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */, D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */, + D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */, + D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -4579,6 +4587,7 @@ D664C7AE2B289AA000CBFA76 /* SubscriptionFlowView.swift */, D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */, D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */, + D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */, ); path = Views; sourceTree = ""; @@ -6525,6 +6534,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 */, @@ -6623,6 +6633,7 @@ CB258D1329A4F24E00DEBA24 /* ConfigurationStore.swift in Sources */, 85058370219F424500ED4EDB /* SearchBarExtension.swift in Sources */, 310D09212799FD1A00DC0060 /* MIMEType.swift in Sources */, + D64648AD2B59936B0033090B /* SubscriptionEmailView.swift in Sources */, BD862E032B30DA170073E2EE /* VPNFeedbackFormViewModel.swift in Sources */, F4147354283BF834004AA7A5 /* AutofillContentScopeFeatureToggles.swift in Sources */, 986DA94A24884B18004A7E39 /* WebViewTransition.swift in Sources */, @@ -6886,6 +6897,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.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/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..1e4b7730ac 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) @@ -107,13 +106,12 @@ public struct AuthService: APIService { public let externalID: String public let id: Int public let status: String - - // swiftlint:disable:next nesting + + // swiftlint:disable nesting 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 } + // swiftlint:enable nesting } } diff --git a/DuckDuckGo/Subscription/Subscription/Services/SubscriptionService.swift b/DuckDuckGo/Subscription/Subscription/Services/SubscriptionService.swift index 347cec975a..ff048cba70 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,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/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 { diff --git a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift index ba40e5890e..a481a14598 100644 --- a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift @@ -67,6 +67,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec @Published var hasActiveSubscription = false @Published var purchaseError: AppStorePurchaseFlow.Error? @Published var activateSubscription: Bool = false + @Published var emailActivationComplete: Bool = false var broker: UserScriptMessageBroker? @@ -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,8 +219,10 @@ 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 } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift new file mode 100644 index 0000000000..7e883f8e06 --- /dev/null +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -0,0 +1,80 @@ +// +// 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.activateSubscriptionViaEmail + var viewTitle = UserText.subscriptionRestoreEmail + @Published var subscriptionEmail: String? + @Published var shouldReloadWebView = false + @Published var activateSubscription = false + @Published var managingSubscriptionEmail = false + + private var cancellables = Set() + + init(userScript: SubscriptionPagesUserScript, + subFeature: SubscriptionPagesUseSubscriptionFeature, + accountManager: AccountManager) { + self.userScript = userScript + self.subFeature = subFeature + self.accountManager = accountManager + initializeView() + setupTransactionObservers() + } + + private func initializeView() { + 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 + } + } + + private func setupTransactionObservers() { + subFeature.$emailActivationComplete + .receive(on: DispatchQueue.main) + .sink { [weak self] value in + if value { + self?.completeActivation() + } + } + .store(in: &cancellables) + } + + private func completeActivation() { + subFeature.emailActivationComplete = false + activateSubscription = true + } + +} +#endif 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/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index 6fee092c1c..d0638d4862 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(), @@ -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..85998e1bd5 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -29,6 +29,8 @@ final class SubscriptionRestoreViewModel: ObservableObject { let userScript: SubscriptionPagesUserScript let subFeature: SubscriptionPagesUseSubscriptionFeature let purchaseManager: PurchaseManager + let accountManager: AccountManager + let isAddingDevice: Bool enum SubscriptionActivationResult { case unknown, activated, notFound, error @@ -36,13 +38,24 @@ final class SubscriptionRestoreViewModel: ObservableObject { @Published var transactionStatus: SubscriptionPagesUseSubscriptionFeature.TransactionStatus = .idle @Published var activationResult: SubscriptionActivationResult = .unknown + @Published var subscriptionEmail: String? + @Published var isManagingEmailSubscription: Bool = false 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 + initializeView() + } + + func initializeView() { + subscriptionEmail = accountManager.email } @MainActor @@ -64,5 +77,9 @@ final class SubscriptionRestoreViewModel: ObservableObject { } } + func manageEmailSubscription() { + isManagingEmailSubscription = 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/HeadlessWebView.swift b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift index 2b3fafaeee..e80e3f5bdc 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,35 @@ 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 + } + + // 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) { + 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 new file mode 100644 index 0000000000..714e9696ea --- /dev/null +++ b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift @@ -0,0 +1,57 @@ +// +// SubscriptionEmailView.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 + @Binding var isActivatingSubscription: Bool + @Environment(\.dismiss) var dismiss + + var body: some View { + ZStack { + VStack { + AsyncHeadlessWebView(url: $viewModel.emailURL, + userScript: viewModel.userScript, + subFeature: viewModel.subFeature, + shouldReload: $viewModel.shouldReloadWebView).background() + } + } + .onChange(of: viewModel.activateSubscription) { active in + if active { + // We just need to dismiss the current view + if viewModel.managingSubscriptionEmail { + dismiss() + } else { + // 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 + } + } + } + .navigationTitle(viewModel.viewTitle) + } + + +} +#endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index 3649d27492..6538daedea 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 { @@ -53,14 +53,15 @@ struct SubscriptionFlowView: View { } // Activation View - NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel()), + NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel(), + isActivatingSubscription: $viewModel.activatingSubscription), isActive: $viewModel.activatingSubscription) { 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 @@ -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 618223a245..3006cf4ace 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift @@ -26,10 +26,13 @@ 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 + // 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" static let appleIDIcon = "Platform-Apple-16" @@ -51,7 +54,7 @@ struct SubscriptionRestoreView: View { listView } .background(Color(designSystemColor: .container)) - .navigationTitle(UserText.subscriptionActivate) + .navigationTitle(viewModel.isAddingDevice ? UserText.subscriptionAddDeviceTitle : UserText.subscriptionActivate) .navigationBarBackButtonHidden(viewModel.transactionStatus != .idle) .applyInsetGroupedListStyle() .alert(isPresented: $isAlertVisible) { getAlert() } @@ -60,13 +63,25 @@ struct SubscriptionRestoreView: View { isAlertVisible = true } } + .onAppear { + viewModel.initializeView() + } if viewModel.transactionStatus != .idle { PurchaseInProgressView(status: getTransactionStatus()) } } - + // Activation View + NavigationLink(destination: SubscriptionEmailView( + viewModel: SubscriptionEmailViewModel( + userScript: viewModel.userScript, + subFeature: viewModel.subFeature, + accountManager: viewModel.accountManager), + isActivatingSubscription: $isActivatingSubscription), + isActive: $viewModel.isManagingEmailSubscription) { + EmptyView() + } } private var listItems: [ListItem] { @@ -74,15 +89,11 @@ struct SubscriptionRestoreView: View { .init(id: 0, content: getCellTitle(icon: Constants.appleIDIcon, text: UserText.subscriptionActivateAppleID), - expandedContent: getCellContent(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: getCellContent(description: UserText.subscriptionActivateEmailDescription, - buttonText: UserText.subscriptionRestoreEmail, - buttonAction: {})) + expandedContent: getEmailCellContent(buttonAction: viewModel.manageEmailSubscription )) ] } @@ -97,17 +108,48 @@ struct SubscriptionRestoreView: View { ) } - private func getCellContent(description: String, buttonText: String, buttonAction: @escaping () -> Void) -> AnyView { + private func getAppleIDCellContent(buttonAction: @escaping () -> Void) -> AnyView { AnyView( VStack(alignment: .leading) { - Text(description) + Text(viewModel.isAddingDevice ? UserText.subscriptionAvailableInApple : UserText.subscriptionActivateAppleIDDescription) .daxSubheadRegular() .foregroundColor(Color(designSystemColor: .textSecondary)) - getCellButton(buttonText: buttonText, action: buttonAction) + if !viewModel.isAddingDevice { + getCellButton(buttonText: UserText.subscriptionActivateAppleIDButton, action: buttonAction) + } } ) } + 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)) + HStack { + getCellButton(buttonText: UserText.subscriptionManageEmailButton, + action: buttonAction) + /* TO BE IMPLEMENTED ?? + Spacer() + Button(action: {}, label: { + Text(UserText.subscriptionManageEmailResendInstructions).daxButton().daxBodyBold() + }) + */ + } + } + } + ) + } + private func getCellButton(buttonText: String, action: @escaping () -> Void) -> AnyView { AnyView( Button(action: action, label: { @@ -142,11 +184,11 @@ struct SubscriptionRestoreView: View { private var headerView: some View { VStack(spacing: Constants.headerLineSpacing) { Image(Constants.heroImage) - Text(UserText.subscriptionActivateTitle) + Text(viewModel.isAddingDevice ? UserText.subscriptionAddDeviceHeaderTitle : UserText.subscriptionActivateTitle) .daxHeadline() .multilineTextAlignment(.center) .foregroundColor(Color(designSystemColor: .textPrimary)) - Text(UserText.subscriptionActivateDescription) + Text(viewModel.isAddingDevice ? UserText.subscriptionAddDeviceDescription : UserText.subscriptionActivateDescription) .daxFootnoteRegular() .foregroundColor(Color(designSystemColor: .textSecondary)) .multilineTextAlignment(.center) @@ -207,11 +249,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 c10bd87f44..8e44ce451c 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 isActivatingSubscription = false var body: some View { List { @@ -44,12 +45,16 @@ 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), + isActivatingSubscription: $isActivatingSubscription)) { + SettingsCustomCell(content: { + Text(UserText.subscriptionAddDeviceButton) + .daxBodyRegular() + .foregroundColor(Color.init(designSystemColor: .accent)) + }) + } SettingsCustomCell(content: { Text(UserText.subscriptionRemoveFromDevice) @@ -73,9 +78,7 @@ struct SubscriptionSettingsView: View { SettingsCustomCell(content: { Text(UserText.subscriptionFAQ) .daxBodyRegular() - }, - action: {}, - disclosureIndicator: false) + }) } } } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 6f1b1a32be..cd5f49ad45 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") @@ -990,7 +991,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") @@ -1010,8 +1011,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") @@ -1032,44 +1032,78 @@ 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 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") - 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 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") + 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 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") + 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 3f4fec34a1..f50749e44b 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"; @@ -1915,13 +1918,15 @@ 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 */ +/* 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."; /* Subscription Activation Info */ @@ -1930,6 +1935,21 @@ 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"; + +/* 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"; + /* Restore button title for AppleID */ "subscription.activate.restore.apple" = "Restore"; @@ -1940,7 +1960,31 @@ 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"; + +/* Resend activation instructions button */ +"subscription.add.device.resend.instructions" = "Resend Instructions"; + +/* 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 title 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 +2005,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 +2038,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."; diff --git a/submodules/privacy-reference-tests b/submodules/privacy-reference-tests index 6b7ad1e7f1..a3acc21947 160000 --- a/submodules/privacy-reference-tests +++ b/submodules/privacy-reference-tests @@ -1 +1 @@ -Subproject commit 6b7ad1e7f15270f9dfeb58a272199f4d57c3eb22 +Subproject commit a3acc2194758bec0f01f57dd0c5f106de01a354e