From eb1dae79175896933354849b47a609b40927a9f4 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 17 Jan 2024 02:11:02 +0100 Subject: [PATCH] Daniel/subscriptions/3.manage subscription (#2336) Task/Issue URL: https://app.asana.com/0/72649045549333/1205054784245717/f Implement Subscription Management View Adds labels to the transaction progress box and reduces the opacity of the background view. --- Core/FeatureFlag.swift | 4 +- Core/UserDefaultsPropertyWrapper.swift | 2 +- DuckDuckGo.xcodeproj/project.pbxproj | 32 +++-- .../Views/PurchaseInProgressView.swift | 45 ------- DuckDuckGo/SettingsAboutView.swift | 9 +- DuckDuckGo/SettingsAppeareanceView.swift | 8 +- DuckDuckGo/SettingsCell.swift | 121 ++++++++---------- DuckDuckGo/SettingsCustomizeView.swift | 4 +- DuckDuckGo/SettingsDebugView.swift | 4 +- DuckDuckGo/SettingsGeneralView.swift | 4 +- DuckDuckGo/SettingsLoginsView.swift | 4 +- DuckDuckGo/SettingsMoreView.swift | 16 +-- DuckDuckGo/SettingsPrivacyView.swift | 27 ++-- DuckDuckGo/SettingsState.swift | 8 +- ...w.swift => SettingsSubscriptionView.swift} | 27 ++-- DuckDuckGo/SettingsSyncView.swift | 4 +- DuckDuckGo/SettingsView.swift | 2 +- DuckDuckGo/SettingsViewModel.swift | 24 ++-- .../WKUserContentController+Handler.swift | 2 - .../Subscription.storekit} | 0 .../Subscription/AccountManager.swift | 0 .../AccountKeychainStorage.swift | 0 .../AccountStorage/AccountStorage.swift | 0 .../AppStoreAccountManagementFlow.swift | 0 .../Flows/AppStore/AppStorePurchaseFlow.swift | 0 .../Flows/AppStore/AppStoreRestoreFlow.swift | 0 .../Subscription/Flows/PurchaseFlow.swift | 0 .../Subscription/Logging.swift | 0 .../Subscription/PurchaseManager.swift | 0 .../Subscription/Services/APIService.swift | 0 .../Subscription/Services/AuthService.swift | 0 .../Services/SubscriptionService.swift | 0 .../SubscriptionPurchaseEnvironment.swift | 0 .../Subscription/URL+Subscription.swift | 4 + ...scriptionPagesUseSubscriptionFeature.swift | 82 ++++++------ .../SubscriptionPagesUserScript.swift | 0 .../ViewModel/SubscriptionFlowViewModel.swift | 19 ++- .../SubscriptionSettingsViewModel.swift | 92 +++++++++++++ .../Views/HeadlessWebView.swift | 0 .../Views/PurchaseInProgressView.swift | 92 +++++++++++++ .../Views/SubscriptionFlowView.swift | 35 +++-- .../Views/SubscriptionSettingsView.swift | 99 ++++++++++++++ DuckDuckGo/UserText.swift | 46 +++++-- DuckDuckGo/en.lproj/Localizable.strings | 92 ++++++++++--- 44 files changed, 627 insertions(+), 281 deletions(-) delete mode 100644 DuckDuckGo/PrivacyPro/Views/PurchaseInProgressView.swift rename DuckDuckGo/{SettingsPrivacyProView.swift => SettingsSubscriptionView.swift} (73%) rename DuckDuckGo/{PrivacyPro => Subscription}/Extensions/WKUserContentController+Handler.swift (98%) rename DuckDuckGo/{PrivacyPro/PrivacyPro.storekit => Subscription/Subscription.storekit} (100%) rename DuckDuckGo/{PrivacyPro => Subscription}/Subscription/AccountManager.swift (100%) rename DuckDuckGo/{PrivacyPro => Subscription}/Subscription/AccountStorage/AccountKeychainStorage.swift (100%) rename DuckDuckGo/{PrivacyPro => Subscription}/Subscription/AccountStorage/AccountStorage.swift (100%) rename DuckDuckGo/{PrivacyPro => Subscription}/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift (100%) rename DuckDuckGo/{PrivacyPro => Subscription}/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift (100%) rename DuckDuckGo/{PrivacyPro => Subscription}/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift (100%) rename DuckDuckGo/{PrivacyPro => Subscription}/Subscription/Flows/PurchaseFlow.swift (100%) rename DuckDuckGo/{PrivacyPro => Subscription}/Subscription/Logging.swift (100%) rename DuckDuckGo/{PrivacyPro => Subscription}/Subscription/PurchaseManager.swift (100%) rename DuckDuckGo/{PrivacyPro => Subscription}/Subscription/Services/APIService.swift (100%) rename DuckDuckGo/{PrivacyPro => Subscription}/Subscription/Services/AuthService.swift (100%) rename DuckDuckGo/{PrivacyPro => Subscription}/Subscription/Services/SubscriptionService.swift (100%) rename DuckDuckGo/{PrivacyPro => Subscription}/Subscription/SubscriptionPurchaseEnvironment.swift (100%) rename DuckDuckGo/{PrivacyPro => Subscription}/Subscription/URL+Subscription.swift (91%) rename DuckDuckGo/{PrivacyPro => Subscription}/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift (78%) rename DuckDuckGo/{PrivacyPro => Subscription}/UserScripts/SubscriptionPagesUserScript.swift (100%) rename DuckDuckGo/{PrivacyPro => Subscription}/ViewModel/SubscriptionFlowViewModel.swift (81%) create mode 100644 DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift rename DuckDuckGo/{PrivacyPro => Subscription}/Views/HeadlessWebView.swift (100%) create mode 100644 DuckDuckGo/Subscription/Views/PurchaseInProgressView.swift rename DuckDuckGo/{PrivacyPro => Subscription}/Views/SubscriptionFlowView.swift (61%) create mode 100644 DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index 531e694283..f5c96b04ad 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -34,13 +34,13 @@ public enum FeatureFlag: String { case networkProtection case networkProtectionWaitlistAccess case networkProtectionWaitlistActive - case privacyPro + case subscription } extension FeatureFlag: FeatureFlagSourceProviding { public var source: FeatureFlagSource { switch self { - case .debugMenu, .appTrackingProtection, .privacyPro: + case .debugMenu, .appTrackingProtection, .subscription: return .internalOnly case .sync: return .remoteReleasable(.subfeature(SyncSubfeature.level0ShowSync)) diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index 192bb83d82..850a3a9738 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -122,7 +122,7 @@ public struct UserDefaultsWrapper { case privacyConfigCustomURL = "com.duckduckgo.ios.privacyConfigCustomURL" - case privacyProHasActiveSubscription = "com.duckduckgo.ios.privacyPro.hasActiveSubscription" + case subscriptionIsActive = "com.duckduckgo.ios.subscruption.isActive" } private let key: Key diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 27cfa043c0..7b0e2e27d2 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -768,8 +768,9 @@ 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 */; }; + D652498E2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */; }; D664C7B62B289AA200CBFA76 /* SubscriptionFlowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */; }; - D664C7B72B289AA200CBFA76 /* PrivacyPro.storekit in Resources */ = {isa = PBXBuildFile; fileRef = D664C7952B289AA000CBFA76 /* PrivacyPro.storekit */; }; + D664C7B72B289AA200CBFA76 /* Subscription.storekit in Resources */ = {isa = PBXBuildFile; fileRef = D664C7952B289AA000CBFA76 /* Subscription.storekit */; }; D664C7B92B289AA200CBFA76 /* WKUserContentController+Handler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7982B289AA000CBFA76 /* WKUserContentController+Handler.swift */; }; D664C7C72B289AA200CBFA76 /* PurchaseInProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7AD2B289AA000CBFA76 /* PurchaseInProgressView.swift */; }; D664C7C82B289AA200CBFA76 /* SubscriptionFlowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7AE2B289AA000CBFA76 /* SubscriptionFlowView.swift */; }; @@ -777,7 +778,7 @@ 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 */; }; - D69FBF762B28BE3600B505F1 /* SettingsPrivacyProView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69FBF752B28BE3600B505F1 /* SettingsPrivacyProView.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 */; }; D6D12CA12B291CA90054390C /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C8D2B291CA90054390C /* Logging.swift */; }; @@ -809,6 +810,7 @@ D6E83C662B23936F006C8AFB /* SettingsDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C652B23936F006C8AFB /* SettingsDebugView.swift */; }; D6E83C682B23B6A3006C8AFB /* FontSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C672B23B6A3006C8AFB /* FontSettings.swift */; }; D6F93E3C2B4FFA97004C268D /* SubscriptionDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F93E3B2B4FFA97004C268D /* SubscriptionDebugViewController.swift */; }; + D6F93E3E2B50A8A0004C268D /* SubscriptionSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */; }; EA39B7E2268A1A35000C62CD /* privacy-reference-tests in Resources */ = {isa = PBXBuildFile; fileRef = EA39B7E1268A1A35000C62CD /* privacy-reference-tests */; }; EAB19EDA268963510015D3EA /* DomainMatchingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB19ED9268963510015D3EA /* DomainMatchingTests.swift */; }; EE0153E12A6EABE0002A8B26 /* NetworkProtectionConvenienceInitialisers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0153E02A6EABE0002A8B26 /* NetworkProtectionConvenienceInitialisers.swift */; }; @@ -2407,8 +2409,9 @@ 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 = ""; }; + 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 /* PrivacyPro.storekit */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = PrivacyPro.storekit; sourceTree = ""; }; + D664C7952B289AA000CBFA76 /* Subscription.storekit */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Subscription.storekit; sourceTree = ""; }; D664C7982B289AA000CBFA76 /* WKUserContentController+Handler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WKUserContentController+Handler.swift"; sourceTree = ""; }; D664C7AD2B289AA000CBFA76 /* PurchaseInProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseInProgressView.swift; sourceTree = ""; }; D664C7AE2B289AA000CBFA76 /* SubscriptionFlowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowView.swift; sourceTree = ""; }; @@ -2416,7 +2419,7 @@ 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; }; - D69FBF752B28BE3600B505F1 /* SettingsPrivacyProView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPrivacyProView.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 = ""; }; D6D12C8D2B291CA90054390C /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; @@ -2448,6 +2451,7 @@ D6E83C652B23936F006C8AFB /* SettingsDebugView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsDebugView.swift; sourceTree = ""; }; D6E83C672B23B6A3006C8AFB /* FontSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSettings.swift; sourceTree = ""; }; D6F93E3B2B4FFA97004C268D /* SubscriptionDebugViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionDebugViewController.swift; sourceTree = ""; }; + D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionSettingsView.swift; sourceTree = ""; }; EA39B7E1268A1A35000C62CD /* privacy-reference-tests */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "privacy-reference-tests"; path = "submodules/privacy-reference-tests"; sourceTree = SOURCE_ROOT; }; EAB19ED9268963510015D3EA /* DomainMatchingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainMatchingTests.swift; sourceTree = ""; }; EE0153E02A6EABE0002A8B26 /* NetworkProtectionConvenienceInitialisers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionConvenienceInitialisers.swift; sourceTree = ""; }; @@ -3766,7 +3770,7 @@ 85AE668C20971FCA0014CF04 /* Notifications */, F1C4A70C1E5771F800A6CA1B /* OmniBar */, F1AE54DB1F0425BB00D9A700 /* Privacy */, - D664C7922B289AA000CBFA76 /* PrivacyPro */, + D664C7922B289AA000CBFA76 /* Subscription */, F1DF09502B039E6E008CC908 /* PrivacyDashboard */, 02ECEC602A965074009F0654 /* PrivacyInfo.xcprivacy */, C1B7B51D28941F160098FD6A /* RemoteMessaging */, @@ -4503,23 +4507,24 @@ name = Resources; sourceTree = ""; }; - D664C7922B289AA000CBFA76 /* PrivacyPro */ = { + D664C7922B289AA000CBFA76 /* Subscription */ = { isa = PBXGroup; children = ( - D664C7952B289AA000CBFA76 /* PrivacyPro.storekit */, + D664C7952B289AA000CBFA76 /* Subscription.storekit */, D664C7932B289AA000CBFA76 /* ViewModel */, D664C7AC2B289AA000CBFA76 /* Views */, D664C7B02B289AA000CBFA76 /* UserScripts */, D664C7962B289AA000CBFA76 /* Extensions */, D6D12C8A2B291CA90054390C /* Subscription */, ); - path = PrivacyPro; + path = Subscription; sourceTree = ""; }; D664C7932B289AA000CBFA76 /* ViewModel */ = { isa = PBXGroup; children = ( D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */, + D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -4535,9 +4540,10 @@ D664C7AC2B289AA000CBFA76 /* Views */ = { isa = PBXGroup; children = ( + D664C7AF2B289AA000CBFA76 /* HeadlessWebView.swift */, D664C7AD2B289AA000CBFA76 /* PurchaseInProgressView.swift */, D664C7AE2B289AA000CBFA76 /* SubscriptionFlowView.swift */, - D664C7AF2B289AA000CBFA76 /* HeadlessWebView.swift */, + D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */, ); path = Views; sourceTree = ""; @@ -4612,7 +4618,7 @@ D6E83C3C2B1F2C03006C8AFB /* SettingsLoginsView.swift */, D6E83C402B1FC285006C8AFB /* SettingsAppeareanceView.swift */, D6E83C592B2213ED006C8AFB /* SettingsPrivacyView.swift */, - D69FBF752B28BE3600B505F1 /* SettingsPrivacyProView.swift */, + D69FBF752B28BE3600B505F1 /* SettingsSubscriptionView.swift */, D6E83C5D2B224676006C8AFB /* SettingsCustomizeView.swift */, D6E83C612B23298B006C8AFB /* SettingsMoreView.swift */, D6E83C632B238432006C8AFB /* SettingsAboutView.swift */, @@ -6081,7 +6087,7 @@ 1EE411FD2858B9300003FE64 /* dark-trackers-2.json in Resources */, AA4D6ABC23DE4D15007E8790 /* AppIconYellow60x60@3x.png in Resources */, 98D98A9B25ED954100D8E3DF /* BrowsingMenuButton.xib in Resources */, - D664C7B72B289AA200CBFA76 /* PrivacyPro.storekit in Resources */, + D664C7B72B289AA200CBFA76 /* Subscription.storekit in Resources */, AA4D6AA823DE4CC4007E8790 /* AppIconBlue40x40@2x.png in Resources */, AA4D6AE723DE4D33007E8790 /* AppIconGreen29x29@2x.png in Resources */, 1EE412002858B9300003FE64 /* dark-shield-dot.json in Resources */, @@ -6563,7 +6569,7 @@ 1E908BF329827C480008C8F3 /* AutoconsentManagement.swift in Sources */, CB9B8739278C8E72001F4906 /* WidgetEducationViewController.swift in Sources */, F4D9C4FA25117A0F00814B71 /* HomeMessageStorage.swift in Sources */, - D69FBF762B28BE3600B505F1 /* SettingsPrivacyProView.swift in Sources */, + D69FBF762B28BE3600B505F1 /* SettingsSubscriptionView.swift in Sources */, D664C7CC2B289AA200CBFA76 /* SubscriptionPagesUserScript.swift in Sources */, AA3D854523D9942200788410 /* AppIconSettingsViewController.swift in Sources */, 85C297042476C1FD0063A335 /* DaxDialogsSettings.swift in Sources */, @@ -6606,6 +6612,7 @@ 98AA92B32456FBE100ED4B9E /* SearchFieldContainerView.swift in Sources */, 3157B43827F4C8490042D3D7 /* FaviconsHelper.swift in Sources */, 85F200042216F5D8006BB258 /* FindInPageView.swift in Sources */, + D652498E2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift in Sources */, 8548D95E25262B1B005AAE49 /* ViewHighlighter.swift in Sources */, F4D7221026F29A70007D6193 /* BookmarkDetailsCell.swift in Sources */, D6D12CA22B291CA90054390C /* AccountManager.swift in Sources */, @@ -6618,6 +6625,7 @@ C13B32D22A0E750700A59236 /* AutofillSettingStatus.swift in Sources */, D6D12CA52B291CAA0054390C /* AppStorePurchaseFlow.swift in Sources */, F4F6DFB426E6B63700ED7E12 /* BookmarkFolderCell.swift in Sources */, + D6F93E3E2B50A8A0004C268D /* SubscriptionSettingsView.swift in Sources */, 851B12CC22369931004781BC /* AtbAndVariantCleanup.swift in Sources */, 85F2FFCF2211F8E5006BB258 /* TabSwitcherViewController+KeyCommands.swift in Sources */, 3157B43327F497E90042D3D7 /* SaveLoginView.swift in Sources */, diff --git a/DuckDuckGo/PrivacyPro/Views/PurchaseInProgressView.swift b/DuckDuckGo/PrivacyPro/Views/PurchaseInProgressView.swift deleted file mode 100644 index 796ae24342..0000000000 --- a/DuckDuckGo/PrivacyPro/Views/PurchaseInProgressView.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// PurchaseInProgressView.swift -// DuckDuckGo -// -// Copyright © 2023 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 SwiftUI - -struct PurchaseInProgressView: View { - @Environment(\.colorScheme) var colorScheme - - // TODO: Update colors and design - var body: some View { - ZStack { - Color(colorScheme == .dark ? .black : .white) - .opacity(0.0) - .edgesIgnoringSafeArea(.all) - .disabled(true) - - ZStack { - RoundedRectangle(cornerRadius: 12) - .fill(colorScheme == .dark ? .black : .white) - .frame(width: 120, height: 120) - .shadow(color: colorScheme == .dark ? .black : .gray30, radius: 10) - - SwiftUI.ProgressView() - .scaleEffect(2) - .progressViewStyle(CircularProgressViewStyle(tint: Color(colorScheme == .dark ? .gray30 : .gray70))) - } - - } - } -} diff --git a/DuckDuckGo/SettingsAboutView.swift b/DuckDuckGo/SettingsAboutView.swift index 8e5b20eb6c..c0fd44206d 100644 --- a/DuckDuckGo/SettingsAboutView.swift +++ b/DuckDuckGo/SettingsAboutView.swift @@ -29,16 +29,15 @@ struct SettingsAboutView: View { SettingsCellView(label: UserText.settingsAboutDDG, action: { viewModel.presentLegacyView(.about) }, - asLink: true, - disclosureIndicator: true) + disclosureIndicator: true, + isButton: true) SettingsCellView(label: UserText.settingsVersion, - accesory: .rightDetail(viewModel.state.version), - asLink: true) + accesory: .rightDetail(viewModel.state.version)) SettingsCellView(label: UserText.settingsFeedback, action: { viewModel.presentLegacyView(.feedback) }, - asLink: true) + isButton: true) } diff --git a/DuckDuckGo/SettingsAppeareanceView.swift b/DuckDuckGo/SettingsAppeareanceView.swift index 4a2f374f4c..d04bd1a08a 100644 --- a/DuckDuckGo/SettingsAppeareanceView.swift +++ b/DuckDuckGo/SettingsAppeareanceView.swift @@ -35,8 +35,8 @@ struct SettingsAppeareanceView: View { SettingsCellView(label: UserText.settingsIcon, action: { viewModel.presentLegacyView(.appIcon ) }, accesory: .image(image), - asLink: true, - disclosureIndicator: true) + disclosureIndicator: true, + isButton: true) SettingsPickerCellView(label: UserText.settingsFirebutton, options: FireButtonAnimationType.allCases, @@ -46,8 +46,8 @@ struct SettingsAppeareanceView: View { SettingsCellView(label: UserText.settingsText, action: { viewModel.presentLegacyView(.textSize) }, accesory: .rightDetail("\(viewModel.state.textSize.size)%"), - asLink: true, - disclosureIndicator: true) + disclosureIndicator: true, + isButton: true) } if viewModel.state.addressbar.enabled { diff --git a/DuckDuckGo/SettingsCell.swift b/DuckDuckGo/SettingsCell.swift index 0e9aac945e..c556659215 100644 --- a/DuckDuckGo/SettingsCell.swift +++ b/DuckDuckGo/SettingsCell.swift @@ -27,6 +27,7 @@ struct SettingsCellComponents { .foregroundColor(Color(UIColor.tertiaryLabel)) } } + /// Encapsulates a View representing a Cell with different configurations struct SettingsCellView: View, Identifiable { @@ -44,9 +45,9 @@ struct SettingsCellView: View, Identifiable { var action: () -> Void = {} var enabled: Bool = true var accesory: Accessory - var asLink: Bool var disclosureIndicator: Bool var id: UUID = UUID() + var isButton: Bool /// Initializes a `SettingsCellView` with the specified label and accesory. /// @@ -58,17 +59,17 @@ struct SettingsCellView: View, Identifiable { /// - action: The closure to execute when the view is tapped. (If not embedded in a NavigationLink) /// - accesory: The type of cell to display. Excludes the custom cell type. /// - enabled: A Boolean value that determines whether the cell is enabled. - /// - asLink: Wraps the view inside a Button. Used for views not wrapped in a NavigationLink /// - disclosureIndicator: Forces Adds a disclosure indicator on the right (chevron) - init(label: String, subtitle: String? = nil, image: Image? = nil, action: @escaping () -> Void = {}, accesory: Accessory = .none, enabled: Bool = true, asLink: Bool = false, disclosureIndicator: Bool = false) { + /// - isButton: Disables the tap actions on the cell if true + init(label: String, subtitle: String? = nil, image: Image? = nil, action: @escaping () -> Void = {}, accesory: Accessory = .none, enabled: Bool = true, disclosureIndicator: Bool = false, isButton: Bool = false) { self.label = label self.subtitle = subtitle self.image = image self.action = action self.enabled = enabled self.accesory = accesory - self.asLink = asLink self.disclosureIndicator = disclosureIndicator + self.isButton = isButton } /// Initializes a `SettingsCellView` for custom content. @@ -84,21 +85,24 @@ struct SettingsCellView: View, Identifiable { self.action = action self.enabled = enabled self.accesory = .custom(customView()) - self.asLink = false self.disclosureIndicator = false + self.isButton = false } var body: some View { - if asLink { - Button(action: action) { + Group { + if isButton { + Button(action: action) { + cellContent + .disabled(!enabled) + } + .buttonStyle(PlainButtonStyle()) + .contentShape(Rectangle()) + } else { cellContent - .disabled(!enabled) } - .buttonStyle(PlainButtonStyle()) - .contentShape(Rectangle()) - } else { - cellContent - } + }.frame(maxWidth: .infinity) + } private var cellContent: some View { @@ -233,35 +237,42 @@ struct SettingsPickerCellView: View { var content: Content var action: () -> Void - var asLink: Bool var disclosureIndicator: Bool + var isButton: Bool /// Initializes a `SettingsCustomCell`. /// - Parameters: /// - content: A SwiftUI View to be displayed in the cell. /// - action: The closure to execute when the view is tapped. - /// - asLink: A Boolean value that determines if the cell behaves like a link. - /// - disclosureIndicator: A Boolean value that determines if the cell shows a disclosure indicator. - init(@ViewBuilder content: () -> Content, action: @escaping () -> Void = {}, asLink: Bool = false, disclosureIndicator: Bool = false) { + /// - disclosureIndicator: A Boolean value that determines if the cell shows a disclosure + /// indicator. + /// - isButton: Disables the tap actions on the cell if true + init(@ViewBuilder content: () -> Content, action: @escaping () -> Void = {}, disclosureIndicator: Bool = false, isButton: Bool = false) { self.content = content() self.action = action - self.asLink = asLink self.disclosureIndicator = disclosureIndicator + self.isButton = isButton } var body: some View { - if asLink { - Button(action: action) { - cellContent + if isButton { + ZStack { + Button(action: action) { + cellContent + } + .buttonStyle(PlainButtonStyle()) + .contentShape(Rectangle()) + .frame(maxWidth: .infinity) + .onTapGesture { + action() // We need this to make sute tap target is expanded to frame + } } - .buttonStyle(PlainButtonStyle()) - .contentShape(Rectangle()) + .frame(maxWidth: .infinity) } else { cellContent } } - private var cellContent: some View { HStack { content @@ -288,94 +299,72 @@ struct SettingsCellView_Previews: PreviewProvider { static var previews: some View { Group { List { - SettingsCellView(label: "Nulla commodo augue nec", - asLink: true, + SettingsCellView(label: "Cell with disclosure", disclosureIndicator: true) .previewLayout(.sizeThatFits) - SettingsCellView(label: "Nulla commodo augue nec", + SettingsCellView(label: "Multi-line Cell with disclosure \nLine 2\nLine 3", subtitle: "Curabitur erat massa, cursus sed velit", - asLink: true, disclosureIndicator: true) .previewLayout(.sizeThatFits) - SettingsCellView(label: "Maecenas ac purus", + SettingsCellView(label: "Image cell with disclosure ", accesory: .image(Image(systemName: "person.circle")), - asLink: true, disclosureIndicator: true) .previewLayout(.sizeThatFits) - SettingsCellView(label: "Maecenas ac purus", - subtitle: "Curabitur erat massa", + SettingsCellView(label: "Subtitle image cell with disclosure", + subtitle: "This is the subtitle", accesory: .image(Image(systemName: "person.circle")), - asLink: true, disclosureIndicator: true) .previewLayout(.sizeThatFits) - SettingsCellView(label: "Curabitur erat", - accesory: .rightDetail("Curabi"), - asLink: true, - disclosureIndicator: true) - .previewLayout(.sizeThatFits) - - SettingsCellView(label: "Curabitur erat", - subtitle: "Nulla commodo augue", - accesory: .rightDetail("Aagittis"), - asLink: true, + SettingsCellView(label: "Right Detail cell with disclosure", + accesory: .rightDetail("Detail"), disclosureIndicator: true) .previewLayout(.sizeThatFits) - SettingsCellView(label: "Proin tempor urna", - accesory: .toggle(isOn: .constant(true)), - asLink: false, - disclosureIndicator: false) + SettingsCellView(label: "Switch Cell", + accesory: .toggle(isOn: .constant(true))) .previewLayout(.sizeThatFits) - SettingsCellView(label: "Proin tempor urna", - subtitle: "Fusce elementum quis", - accesory: .toggle(isOn: .constant(true)), - asLink: false, - disclosureIndicator: false) + SettingsCellView(label: "Switch Cell", + subtitle: "Subtitle goes here", + accesory: .toggle(isOn: .constant(true))) .previewLayout(.sizeThatFits) + @State var selectedOption: SampleOption = .optionOne SettingsPickerCellView(label: "Proin tempor urna", options: SampleOption.allCases, selectedOption: $selectedOption) .previewLayout(.sizeThatFits) - SettingsCellView(label: "Proin tempor urna", - subtitle: "Fusce elementum quis", - accesory: .toggle(isOn: .constant(true)), - asLink: false, - disclosureIndicator: false) - .previewLayout(.sizeThatFits) - let cellContent: () -> some View = { HStack(spacing: 15) { - Image(systemName: "hand.wave") + Image(systemName: "bird.circle") .foregroundColor(.orange) .imageScale(.large) - Image(systemName: "hand.wave") + Image(systemName: "bird.circle") .foregroundColor(.orange) .imageScale(.medium) Spacer() - VStack(alignment: .leading) { + VStack(alignment: .center) { Text("LOREM IPSUM") .font(.headline) } Spacer() - Image(systemName: "hand.wave") + Image(systemName: "bird.circle") .foregroundColor(.orange) .imageScale(.medium) - Image(systemName: "hand.wave") + Image(systemName: "bird.circle") .foregroundColor(.orange) .imageScale(.large) } } // For some unknown reason, this breaks on CI, but works normally // Perhaps an XCODE version issue? - // SettingsCustomCell(content: cellContent) - // .previewLayout(.sizeThatFits) + SettingsCustomCell(content: cellContent) + .previewLayout(.sizeThatFits) } diff --git a/DuckDuckGo/SettingsCustomizeView.swift b/DuckDuckGo/SettingsCustomizeView.swift index 699398f20b..034b268090 100644 --- a/DuckDuckGo/SettingsCustomizeView.swift +++ b/DuckDuckGo/SettingsCustomizeView.swift @@ -31,8 +31,8 @@ struct SettingsCustomizeView: View { SettingsCellView(label: UserText.settingsKeyboard, action: { viewModel.presentLegacyView(.keyboard) }, - asLink: true, - disclosureIndicator: true) + disclosureIndicator: true, + isButton: true) SettingsCellView(label: UserText.settingsAutocomplete, accesory: .toggle(isOn: viewModel.autocompleteBinding)) diff --git a/DuckDuckGo/SettingsDebugView.swift b/DuckDuckGo/SettingsDebugView.swift index 53a325b988..39615314c2 100644 --- a/DuckDuckGo/SettingsDebugView.swift +++ b/DuckDuckGo/SettingsDebugView.swift @@ -29,8 +29,8 @@ struct SettingsDebugView: View { SettingsCellView(label: "All debug options", action: { viewModel.presentLegacyView(.debug) }, - asLink: true, - disclosureIndicator: true) + disclosureIndicator: true, + isButton: true) } diff --git a/DuckDuckGo/SettingsGeneralView.swift b/DuckDuckGo/SettingsGeneralView.swift index 66aae4e741..db6de8cc18 100644 --- a/DuckDuckGo/SettingsGeneralView.swift +++ b/DuckDuckGo/SettingsGeneralView.swift @@ -28,11 +28,11 @@ struct SettingsGeneralView: View { Section { SettingsCellView(label: UserText.settingsSetDefault, action: { viewModel.setAsDefaultBrowser() }, - asLink: true) + isButton: true) SettingsCellView(label: UserText.settingsAddToDock, action: { viewModel.presentLegacyView(.addToDock) }, - asLink: true) + isButton: true) NavigationLink(destination: WidgetEducationView()) { SettingsCellView(label: UserText.settingsAddWidget) diff --git a/DuckDuckGo/SettingsLoginsView.swift b/DuckDuckGo/SettingsLoginsView.swift index cae2f09076..a24a3bfaaf 100644 --- a/DuckDuckGo/SettingsLoginsView.swift +++ b/DuckDuckGo/SettingsLoginsView.swift @@ -32,8 +32,8 @@ struct SettingsLoginsView: View { Section { SettingsCellView(label: UserText.settingsLogins, action: { viewModel.presentLegacyView(.logins) }, - asLink: true, - disclosureIndicator: true) + disclosureIndicator: true, + isButton: true) } } diff --git a/DuckDuckGo/SettingsMoreView.swift b/DuckDuckGo/SettingsMoreView.swift index 0f983ddaed..d3a770fa19 100644 --- a/DuckDuckGo/SettingsMoreView.swift +++ b/DuckDuckGo/SettingsMoreView.swift @@ -31,28 +31,28 @@ struct SettingsMoreView: View { SettingsCellView(label: UserText.settingsEmailProtection, subtitle: UserText.settingsEmailProtectionDescription, action: { viewModel.openEmailProtection() }, - asLink: true, - disclosureIndicator: true) + disclosureIndicator: true, + isButton: true) SettingsCellView(label: UserText.macBrowserTitle, subtitle: UserText.macWaitlistBrowsePrivately, action: { viewModel.presentLegacyView(.macApp) }, - asLink: true, - disclosureIndicator: true) + disclosureIndicator: true, + isButton: true) SettingsCellView(label: UserText.windowsWaitlistTitle, subtitle: UserText.windowsWaitlistBrowsePrivately, action: { viewModel.presentLegacyView(.windowsApp) }, - asLink: true, - disclosureIndicator: true) + disclosureIndicator: true, + isButton: true) #if NETWORK_PROTECTION if viewModel.state.networkProtection.enabled { SettingsCellView(label: UserText.netPNavTitle, subtitle: viewModel.state.networkProtection.status != "" ? viewModel.state.networkProtection.status : nil, action: { viewModel.presentLegacyView(.netP) }, - asLink: true, - disclosureIndicator: true) + disclosureIndicator: true, + isButton: true) } #endif } diff --git a/DuckDuckGo/SettingsPrivacyView.swift b/DuckDuckGo/SettingsPrivacyView.swift index df869e9bd8..deee886df0 100644 --- a/DuckDuckGo/SettingsPrivacyView.swift +++ b/DuckDuckGo/SettingsPrivacyView.swift @@ -33,36 +33,37 @@ struct SettingsPrivacyView: View { accesory: .rightDetail(viewModel.state.sendDoNotSell ? UserText.doNotSellEnabled : UserText.doNotSellDisabled), - asLink: true, - disclosureIndicator: true) + disclosureIndicator: true, + isButton: true) SettingsCellView(label: UserText.settingsCookiePopups, action: { viewModel.presentLegacyView(.autoconsent) }, accesory: .rightDetail(viewModel.state.autoconsentEnabled ? UserText.autoconsentEnabled : UserText.autoconsentDisabled), - asLink: true, - disclosureIndicator: true) + disclosureIndicator: true, + isButton: true) SettingsCellView(label: UserText.settingsUnprotectedSites, action: { viewModel.presentLegacyView(.unprotectedSites) }, - asLink: true, - disclosureIndicator: true) + disclosureIndicator: true, + isButton: true) SettingsCellView(label: UserText.settingsFireproofSites, action: { viewModel.presentLegacyView(.fireproofSites) }, - asLink: true, - disclosureIndicator: true) - + disclosureIndicator: true, + isButton: true) + SettingsCellView(label: UserText.settingsClearData, action: { viewModel.presentLegacyView(.autoclearData) }, accesory: .rightDetail(viewModel.state.autoclearDataEnabled ? UserText.autoClearAccessoryOn : UserText.autoClearAccessoryOff), - asLink: true, - disclosureIndicator: true) - - SettingsCellView(label: UserText.settingsAutolock, accesory: .toggle(isOn: viewModel.applicationLockBinding)) + disclosureIndicator: true, + isButton: true) + + SettingsCellView(label: UserText.settingsAutolock, + accesory: .toggle(isOn: viewModel.applicationLockBinding)) } } diff --git a/DuckDuckGo/SettingsState.swift b/DuckDuckGo/SettingsState.swift index 95fd708149..9d9ff181a5 100644 --- a/DuckDuckGo/SettingsState.swift +++ b/DuckDuckGo/SettingsState.swift @@ -21,7 +21,7 @@ import BrowserServicesKit struct SettingsState { - enum PrivacyProSubscriptionStatus { + enum SubscriptionStatus { case active, inactive, unknown } @@ -40,7 +40,7 @@ struct SettingsState { var status: String } - struct PrivacyPro { + struct Subscription { var enabled: Bool var canPurchase: Bool var hasActiveSubscription: Bool @@ -85,7 +85,7 @@ struct SettingsState { var networkProtection: NetworkProtection // Subscriptions Properties - var privacyPro: PrivacyPro + var subscription: Subscription // Sync Propertiers var sync: SyncSettings @@ -111,7 +111,7 @@ struct SettingsState { speechRecognitionEnabled: false, loginsEnabled: false, networkProtection: NetworkProtection(enabled: false, status: ""), - privacyPro: PrivacyPro(enabled: false, canPurchase: false, + subscription: Subscription(enabled: false, canPurchase: false, hasActiveSubscription: false), sync: SyncSettings(enabled: false, title: "") ) diff --git a/DuckDuckGo/SettingsPrivacyProView.swift b/DuckDuckGo/SettingsSubscriptionView.swift similarity index 73% rename from DuckDuckGo/SettingsPrivacyProView.swift rename to DuckDuckGo/SettingsSubscriptionView.swift index 2c439d92a9..53069a89c2 100644 --- a/DuckDuckGo/SettingsPrivacyProView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -1,5 +1,5 @@ // -// SettingsPrivacyProView.swift +// SettingsSubscriptionView.swift // DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. @@ -22,11 +22,11 @@ import UIKit #if SUBSCRIPTION @available(iOS 15.0, *) -struct SettingsPrivacyProView: View { +struct SettingsSubscriptionView: View { @EnvironmentObject var viewModel: SettingsViewModel - private var privacyProDescriptionView: some View { + private var subscriptionDescriptionView: some View { VStack(alignment: .leading) { Text(UserText.settingsPProSubscribe).daxBodyRegular() Group { @@ -51,7 +51,7 @@ struct SettingsPrivacyProView: View { private var purchaseSubscriptionView: some View { return Group { - SettingsCustomCell(content: { privacyProDescriptionView }) + SettingsCustomCell(content: { subscriptionDescriptionView }) NavigationLink(destination: SubscriptionFlowView(viewModel: SubscriptionFlowViewModel())) { SettingsCustomCell(content: { learnMoreView }) } @@ -63,13 +63,18 @@ struct SettingsPrivacyProView: View { SettingsCellView(label: UserText.settingsPProVPNTitle, subtitle: viewModel.state.networkProtection.status != "" ? viewModel.state.networkProtection.status : nil, action: { viewModel.presentLegacyView(.netP) }, - asLink: true, - disclosureIndicator: true) + disclosureIndicator: true, + isButton: true) - SettingsCellView(label: UserText.settingsPProDBPTitle, subtitle: UserText.settingsPProDBPSubTitle) - SettingsCellView(label: UserText.settingsPProITRTitle, subtitle: UserText.settingsPProITRSubTitle) + NavigationLink(destination: Text("Data Broker Protection")) { + SettingsCellView(label: UserText.settingsPProDBPTitle, subtitle: UserText.settingsPProDBPSubTitle) + } - NavigationLink(destination: SubscriptionFlowView(viewModel: SubscriptionFlowViewModel())) { + NavigationLink(destination: Text("Identity Theft Restoration")) { + SettingsCellView(label: UserText.settingsPProITRTitle, subtitle: UserText.settingsPProITRSubTitle) + } + + NavigationLink(destination: SubscriptionSettingsView(viewModel: SubscriptionSettingsViewModel())) { SettingsCustomCell(content: { manageSubscriptionView }) } } @@ -77,9 +82,9 @@ struct SettingsPrivacyProView: View { var body: some View { - if viewModel.state.privacyPro.enabled { + if viewModel.state.subscription.enabled { Section(header: Text(UserText.settingsPProSection)) { - if viewModel.state.privacyPro.hasActiveSubscription { + if viewModel.state.subscription.hasActiveSubscription { subscriptionDetailsView } else { purchaseSubscriptionView diff --git a/DuckDuckGo/SettingsSyncView.swift b/DuckDuckGo/SettingsSyncView.swift index bda8d96bcd..0cd75de4fc 100644 --- a/DuckDuckGo/SettingsSyncView.swift +++ b/DuckDuckGo/SettingsSyncView.swift @@ -36,8 +36,8 @@ struct SettingsSyncView: View { Section { SettingsCellView(label: SyncUI.UserText.syncTitle, action: { viewModel.presentLegacyView(.sync) }, - asLink: true, - disclosureIndicator: true) + disclosureIndicator: true, + isButton: true) } } diff --git a/DuckDuckGo/SettingsView.swift b/DuckDuckGo/SettingsView.swift index d5372591a4..0bfd697ef4 100644 --- a/DuckDuckGo/SettingsView.swift +++ b/DuckDuckGo/SettingsView.swift @@ -35,7 +35,7 @@ struct SettingsView: View { SettingsPrivacyView() #if SUBSCRIPTION if #available(iOS 15, *) { - SettingsPrivacyProView() + SettingsSubscriptionView() } #endif SettingsCustomizeView() diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index 41a4b964ec..bff53aae81 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -53,7 +53,7 @@ final class SettingsViewModel: ObservableObject { private var cancellables = Set() // Defaults - @UserDefaultsWrapper(key: .privacyProHasActiveSubscription, defaultValue: false) + @UserDefaultsWrapper(key: .subscriptionIsActive, defaultValue: false) static private var cachedHasActiveSubscription: Bool // Closures to interact with legacy view controllers throught the container @@ -217,7 +217,7 @@ extension SettingsViewModel { speechRecognitionEnabled: AppDependencyProvider.shared.voiceSearchHelper.isSpeechRecognizerAvailable, loginsEnabled: featureFlagger.isFeatureOn(.autofillAccessCredentialManagement), networkProtection: getNetworkProtectionState(), - privacyPro: getPrivacyProState(), + subscription: getSubscriptionState(), sync: getSyncState() ) @@ -226,7 +226,7 @@ extension SettingsViewModel { #if SUBSCRIPTION if #available(iOS 15, *) { Task { - if state.privacyPro.enabled { + if state.subscription.enabled { await setupSubscriptionEnvironment() } } @@ -245,15 +245,15 @@ extension SettingsViewModel { return SettingsState.NetworkProtection(enabled: enabled, status: "") } - private func getPrivacyProState() -> SettingsState.PrivacyPro { + private func getSubscriptionState() -> SettingsState.Subscription { var enabled = false var canPurchase = false - var hasActiveSubscription = Self.cachedHasActiveSubscription + let hasActiveSubscription = Self.cachedHasActiveSubscription #if SUBSCRIPTION - enabled = featureFlagger.isFeatureOn(.privacyPro) + enabled = featureFlagger.isFeatureOn(.subscription) canPurchase = SubscriptionPurchaseEnvironment.canPurchase #endif - return SettingsState.PrivacyPro(enabled: enabled, + return SettingsState.Subscription(enabled: enabled, canPurchase: canPurchase, hasActiveSubscription: hasActiveSubscription) } @@ -307,13 +307,13 @@ extension SettingsViewModel { // Check for valid entitlements let hasEntitlements = await AccountManager().hasEntitlement(for: Self.entitlementNames.first!) - self.state.privacyPro.hasActiveSubscription = hasEntitlements ? true : false + self.state.subscription.hasActiveSubscription = hasEntitlements ? true : false // Cache Subscription state - Self.cachedHasActiveSubscription = self.state.privacyPro.hasActiveSubscription + Self.cachedHasActiveSubscription = self.state.subscription.hasActiveSubscription // Enable Subscription purchase if there's no active subscription - if self.state.privacyPro.hasActiveSubscription == false { + if self.state.subscription.hasActiveSubscription == false { setupSubscriptionPurchaseOptions() } } @@ -324,11 +324,11 @@ extension SettingsViewModel { @available(iOS 15.0, *) private func setupSubscriptionPurchaseOptions() { - self.state.privacyPro.hasActiveSubscription = false + self.state.subscription.hasActiveSubscription = false PurchaseManager.shared.$availableProducts .receive(on: RunLoop.main) .sink { [weak self] products in - self?.state.privacyPro.canPurchase = !products.isEmpty + self?.state.subscription.canPurchase = !products.isEmpty }.store(in: &cancellables) } #endif diff --git a/DuckDuckGo/PrivacyPro/Extensions/WKUserContentController+Handler.swift b/DuckDuckGo/Subscription/Extensions/WKUserContentController+Handler.swift similarity index 98% rename from DuckDuckGo/PrivacyPro/Extensions/WKUserContentController+Handler.swift rename to DuckDuckGo/Subscription/Extensions/WKUserContentController+Handler.swift index 9bfae41627..b156804a4e 100644 --- a/DuckDuckGo/PrivacyPro/Extensions/WKUserContentController+Handler.swift +++ b/DuckDuckGo/Subscription/Extensions/WKUserContentController+Handler.swift @@ -17,8 +17,6 @@ // limitations under the License. // -// TODO: Move to BSK - import Foundation import WebKit import UserScript diff --git a/DuckDuckGo/PrivacyPro/PrivacyPro.storekit b/DuckDuckGo/Subscription/Subscription.storekit similarity index 100% rename from DuckDuckGo/PrivacyPro/PrivacyPro.storekit rename to DuckDuckGo/Subscription/Subscription.storekit diff --git a/DuckDuckGo/PrivacyPro/Subscription/AccountManager.swift b/DuckDuckGo/Subscription/Subscription/AccountManager.swift similarity index 100% rename from DuckDuckGo/PrivacyPro/Subscription/AccountManager.swift rename to DuckDuckGo/Subscription/Subscription/AccountManager.swift diff --git a/DuckDuckGo/PrivacyPro/Subscription/AccountStorage/AccountKeychainStorage.swift b/DuckDuckGo/Subscription/Subscription/AccountStorage/AccountKeychainStorage.swift similarity index 100% rename from DuckDuckGo/PrivacyPro/Subscription/AccountStorage/AccountKeychainStorage.swift rename to DuckDuckGo/Subscription/Subscription/AccountStorage/AccountKeychainStorage.swift diff --git a/DuckDuckGo/PrivacyPro/Subscription/AccountStorage/AccountStorage.swift b/DuckDuckGo/Subscription/Subscription/AccountStorage/AccountStorage.swift similarity index 100% rename from DuckDuckGo/PrivacyPro/Subscription/AccountStorage/AccountStorage.swift rename to DuckDuckGo/Subscription/Subscription/AccountStorage/AccountStorage.swift diff --git a/DuckDuckGo/PrivacyPro/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift b/DuckDuckGo/Subscription/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift similarity index 100% rename from DuckDuckGo/PrivacyPro/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift rename to DuckDuckGo/Subscription/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift diff --git a/DuckDuckGo/PrivacyPro/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift b/DuckDuckGo/Subscription/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift similarity index 100% rename from DuckDuckGo/PrivacyPro/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift rename to DuckDuckGo/Subscription/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift diff --git a/DuckDuckGo/PrivacyPro/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift b/DuckDuckGo/Subscription/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift similarity index 100% rename from DuckDuckGo/PrivacyPro/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift rename to DuckDuckGo/Subscription/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift diff --git a/DuckDuckGo/PrivacyPro/Subscription/Flows/PurchaseFlow.swift b/DuckDuckGo/Subscription/Subscription/Flows/PurchaseFlow.swift similarity index 100% rename from DuckDuckGo/PrivacyPro/Subscription/Flows/PurchaseFlow.swift rename to DuckDuckGo/Subscription/Subscription/Flows/PurchaseFlow.swift diff --git a/DuckDuckGo/PrivacyPro/Subscription/Logging.swift b/DuckDuckGo/Subscription/Subscription/Logging.swift similarity index 100% rename from DuckDuckGo/PrivacyPro/Subscription/Logging.swift rename to DuckDuckGo/Subscription/Subscription/Logging.swift diff --git a/DuckDuckGo/PrivacyPro/Subscription/PurchaseManager.swift b/DuckDuckGo/Subscription/Subscription/PurchaseManager.swift similarity index 100% rename from DuckDuckGo/PrivacyPro/Subscription/PurchaseManager.swift rename to DuckDuckGo/Subscription/Subscription/PurchaseManager.swift diff --git a/DuckDuckGo/PrivacyPro/Subscription/Services/APIService.swift b/DuckDuckGo/Subscription/Subscription/Services/APIService.swift similarity index 100% rename from DuckDuckGo/PrivacyPro/Subscription/Services/APIService.swift rename to DuckDuckGo/Subscription/Subscription/Services/APIService.swift diff --git a/DuckDuckGo/PrivacyPro/Subscription/Services/AuthService.swift b/DuckDuckGo/Subscription/Subscription/Services/AuthService.swift similarity index 100% rename from DuckDuckGo/PrivacyPro/Subscription/Services/AuthService.swift rename to DuckDuckGo/Subscription/Subscription/Services/AuthService.swift diff --git a/DuckDuckGo/PrivacyPro/Subscription/Services/SubscriptionService.swift b/DuckDuckGo/Subscription/Subscription/Services/SubscriptionService.swift similarity index 100% rename from DuckDuckGo/PrivacyPro/Subscription/Services/SubscriptionService.swift rename to DuckDuckGo/Subscription/Subscription/Services/SubscriptionService.swift diff --git a/DuckDuckGo/PrivacyPro/Subscription/SubscriptionPurchaseEnvironment.swift b/DuckDuckGo/Subscription/Subscription/SubscriptionPurchaseEnvironment.swift similarity index 100% rename from DuckDuckGo/PrivacyPro/Subscription/SubscriptionPurchaseEnvironment.swift rename to DuckDuckGo/Subscription/Subscription/SubscriptionPurchaseEnvironment.swift diff --git a/DuckDuckGo/PrivacyPro/Subscription/URL+Subscription.swift b/DuckDuckGo/Subscription/Subscription/URL+Subscription.swift similarity index 91% rename from DuckDuckGo/PrivacyPro/Subscription/URL+Subscription.swift rename to DuckDuckGo/Subscription/Subscription/URL+Subscription.swift index 8b2fded391..2451ab5055 100644 --- a/DuckDuckGo/PrivacyPro/Subscription/URL+Subscription.swift +++ b/DuckDuckGo/Subscription/Subscription/URL+Subscription.swift @@ -47,4 +47,8 @@ public extension URL { static var manageSubscriptionsInAppStoreAppURL: URL { URL(string: "macappstores://apps.apple.com/account/subscriptions")! } + + static var manageSubscriptionsIniOSAppStoreAppURL: URL { + URL(string: "https://apps.apple.com/account/subscriptions")! + } } diff --git a/DuckDuckGo/PrivacyPro/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift similarity index 78% rename from DuckDuckGo/PrivacyPro/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift rename to DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift index 97ba439af3..04f6d26d95 100644 --- a/DuckDuckGo/PrivacyPro/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift @@ -59,7 +59,11 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec static let year = "yearly" } - @Published var transactionInProgress = false + enum TransactionStatus { + case idle, purchasing, restoring, polling + } + + @Published var transactionStatus: TransactionStatus = .idle @Published var hasActiveSubscription = false @Published var purchaseError: AppStorePurchaseFlow.Error? @@ -108,9 +112,9 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec // Manage transation in progress flag private func withTransactionInProgress(_ work: () async throws -> T) async rethrows -> T { - transactionInProgress = true + transactionStatus = transactionStatus defer { - transactionInProgress = false + transactionStatus = .idle } return try await work() } @@ -129,13 +133,14 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec await withTransactionInProgress { + transactionStatus = .purchasing resetSubscriptionFlow() switch await AppStorePurchaseFlow.subscriptionOptions() { case .success(let subscriptionOptions): return subscriptionOptions case .failure: - // TODO: handle errors - no products found + return nil } @@ -146,6 +151,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec await withTransactionInProgress { + transactionStatus = .purchasing resetSubscriptionFlow() struct SubscriptionSelection: Decodable { @@ -153,42 +159,36 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec } let message = original + guard let subscriptionSelection: SubscriptionSelection = DecodableHelper.decode(from: params) else { + assertionFailure("SubscriptionPagesUserScript: expected JSON representation of SubscriptionSelection") + return nil + } - if #available(iOS 15, *) { - guard let subscriptionSelection: SubscriptionSelection = DecodableHelper.decode(from: params) else { - assertionFailure("SubscriptionPagesUserScript: expected JSON representation of SubscriptionSelection") - return nil - } - - print("Selected: \(subscriptionSelection.id)") - - // Check for active subscriptions - if await PurchaseManager.hasActiveSubscription() { - hasActiveSubscription = true - return nil - } - - let emailAccessToken = try? EmailManager().getToken() + // Check for active subscriptions + if await PurchaseManager.hasActiveSubscription() { + hasActiveSubscription = true + return nil + } + + let emailAccessToken = try? EmailManager().getToken() - switch await AppStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id, emailAccessToken: emailAccessToken) { - case .success: - break - case .failure: - purchaseError = .purchaseFailed - originalMessage = original - return nil - } - - switch await AppStorePurchaseFlow.completeSubscriptionPurchase() { - case .success(let purchaseUpdate): - await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate) - case .failure: - // TODO: handle errors - missing entitlements on post purchase check - purchaseError = .missingEntitlements - await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "completed")) - } + switch await AppStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id, emailAccessToken: emailAccessToken) { + case .success: + break + case .failure: + purchaseError = .purchaseFailed + originalMessage = original + return nil } + transactionStatus = .polling + switch await AppStorePurchaseFlow.completeSubscriptionPurchase() { + case .success(let purchaseUpdate): + await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate) + case .failure: + purchaseError = .missingEntitlements + await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "completed")) + } return nil } } @@ -217,13 +217,10 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec accountManager.storeAccount(token: accessToken, email: accountDetails.email, externalID: accountDetails.externalID) } - // TODO: Navigate back to settings - return nil } func activateSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { - print(">>> Selected to activate a subscription -- show the activation settings screen") return nil } @@ -236,8 +233,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec assertionFailure("SubscriptionPagesUserScript: expected JSON representation of FeatureSelection") return nil } - - print(">>> Selected a feature -- show the corresponding UI", featureSelection) + return nil } @@ -254,14 +250,14 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec func pushAction(method: SubscribeActionName, webView: WKWebView, params: Encodable) { let broker = UserScriptMessageBroker(context: SubscriptionPagesUserScript.context, requiresRunInPageContentWorld: true ) - - print(">>> Pushing into WebView:", method.rawValue, String(describing: params)) + broker.push(method: method.rawValue, params: params, for: self, into: webView) } func restoreAccountFromAppStorePurchase() async -> Bool { await withTransactionInProgress { + transactionStatus = .restoring switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { case .success(let update): return true diff --git a/DuckDuckGo/PrivacyPro/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUserScript.swift similarity index 100% rename from DuckDuckGo/PrivacyPro/UserScripts/SubscriptionPagesUserScript.swift rename to DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUserScript.swift diff --git a/DuckDuckGo/PrivacyPro/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift similarity index 81% rename from DuckDuckGo/PrivacyPro/ViewModel/SubscriptionFlowViewModel.swift rename to DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index 60550e87ea..440b4060de 100644 --- a/DuckDuckGo/PrivacyPro/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -37,7 +37,7 @@ final class SubscriptionFlowViewModel: ObservableObject { // State variables var purchaseURL = URL.purchaseSubscription @Published var hasActiveSubscription = false - @Published var transactionInProgress = false + @Published var transactionStatus: SubscriptionPagesUseSubscriptionFeature.TransactionStatus = .idle @Published var shouldReloadWebview = false init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), @@ -51,10 +51,18 @@ final class SubscriptionFlowViewModel: ObservableObject { // Observe transaction status private func setupTransactionObserver() async { - subFeature.$transactionInProgress + subFeature.$transactionStatus .sink { [weak self] status in guard let self = self else { return } - Task { await self.setTransactionInProgress(status) } + Task { await self.setTransactionStatus(status) } + + } + .store(in: &cancellables) + + subFeature.$hasActiveSubscription + .receive(on: DispatchQueue.main) + .sink { [weak self] value in + self?.hasActiveSubscription = value } .store(in: &cancellables) @@ -67,8 +75,8 @@ final class SubscriptionFlowViewModel: ObservableObject { } @MainActor - private func setTransactionInProgress(_ inProgress: Bool) { - self.transactionInProgress = inProgress + private func setTransactionStatus(_ status: SubscriptionPagesUseSubscriptionFeature.TransactionStatus) { + self.transactionStatus = status } func initializeViewData() async { @@ -81,7 +89,6 @@ final class SubscriptionFlowViewModel: ObservableObject { await MainActor.run { shouldReloadWebview = true } } else { await MainActor.run { - // TODO: Display error when restoring subscription } } } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift new file mode 100644 index 0000000000..243e046534 --- /dev/null +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift @@ -0,0 +1,92 @@ +// +// SubscriptionSettingsViewModel.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 StoreKit + +#if SUBSCRIPTION +@available(iOS 15.0, *) +final class SubscriptionSettingsViewModel: ObservableObject { + + private let accountManager: AccountManager + + var subscriptionDetails: String = "" + @Published var shouldDisplayRemovalNotice: Bool = false + + init(accountManager: AccountManager = AccountManager()) { + self.accountManager = accountManager + fetchAndUpdateSubscriptionDetails() + } + + private var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "MMM dd, yyyy" // Jan 12, 2024" + return formatter + }() + + private func fetchAndUpdateSubscriptionDetails() { + Task { + guard let token = accountManager.accessToken else { return } + + if let cachedDate = SubscriptionService.cachedSubscriptionDetailsResponse?.expiresOrRenewsAt { + updateSubscriptionDetails(date: cachedDate) + } + + if case .success(let response) = await SubscriptionService.getSubscriptionDetails(token: token) { + if !response.isSubscriptionActive { + AccountManager().signOut() + return + } + } + } + } + + private func updateSubscriptionDetails(date: Date) { + self.subscriptionDetails = UserText.subscriptionInfo(expiration: dateFormatter.string(from: date)) + } + + func removeSubscription() { + AccountManager().signOut() + } + + func manageSubscription() { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { + Task { + do { + try await AppStore.showManageSubscriptions(in: windowScene) + } catch { + openSubscriptionManagementURL() + } + } + } else { + openSubscriptionManagementURL() + } + } + + private func openSubscriptionManagementURL() { + let url = URL.manageSubscriptionsIniOSAppStoreAppURL + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } + + +} +#endif diff --git a/DuckDuckGo/PrivacyPro/Views/HeadlessWebView.swift b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift similarity index 100% rename from DuckDuckGo/PrivacyPro/Views/HeadlessWebView.swift rename to DuckDuckGo/Subscription/Views/HeadlessWebView.swift diff --git a/DuckDuckGo/Subscription/Views/PurchaseInProgressView.swift b/DuckDuckGo/Subscription/Views/PurchaseInProgressView.swift new file mode 100644 index 0000000000..8b4aed988e --- /dev/null +++ b/DuckDuckGo/Subscription/Views/PurchaseInProgressView.swift @@ -0,0 +1,92 @@ +// +// PurchaseInProgressView.swift +// DuckDuckGo +// +// Copyright © 2023 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 SwiftUI +import DesignResourcesKit + +struct PurchaseInProgressView: View { + @Environment(\.colorScheme) var colorScheme + + var status: String + enum DesignConstants { + static let coverOpacity = 0.6 + static let cornerRadius = 12.0 + static let shadowRadius = 10.0 + static let lightShadowColor = Color.gray50 + static let darkShadowColor = Color.black + static let spinnerScale = 2.0 + static let internalZStackWidth = 220.0 + static let horizontalPadding = 20.0 + static let verticalPadding = 100.0 + } + + @State private var viewHeight: CGFloat = 0 // State to store the height of the VStack + + var body: some View { + ZStack { + Color(designSystemColor: .background) + .opacity(Self.DesignConstants.coverOpacity) + .edgesIgnoringSafeArea(.all) + .disabled(true) + + ZStack { + RoundedRectangle(cornerRadius: DesignConstants.cornerRadius) + .fill(Color(designSystemColor: .background)) + .frame(width: DesignConstants.internalZStackWidth, height: viewHeight) // Use the dynamic height + .shadow(color: colorScheme == .dark ? DesignConstants.darkShadowColor : DesignConstants.lightShadowColor, + radius: DesignConstants.shadowRadius) + + VStack { + SwiftUI.ProgressView() + .scaleEffect(DesignConstants.spinnerScale) + .progressViewStyle(CircularProgressViewStyle(tint: Color(designSystemColor: DesignSystemColor.icons))) + .padding(.bottom, 18) + .padding(.top, 10) + + Text(status) + .daxSubheadRegular() + .foregroundColor(Color(designSystemColor: .textPrimary)) + .frame(width: DesignConstants.internalZStackWidth - 2 * DesignConstants.horizontalPadding) + .multilineTextAlignment(.center) + .background(GeometryReader { geometry in + Color.clear.onAppear { + viewHeight = geometry.size.height + DesignConstants.verticalPadding + } + }) + } + .frame(width: DesignConstants.internalZStackWidth) + } + } + } +} + +// Preference key to store the height of the content +struct ViewHeightKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = max(value, nextValue()) + } +} + +struct PurchaseInProgressView_Previews: PreviewProvider { + static var previews: some View { + PurchaseInProgressView(status: "Completing Purchase... ") + .previewLayout(.sizeThatFits) + .padding() + } +} diff --git a/DuckDuckGo/PrivacyPro/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift similarity index 61% rename from DuckDuckGo/PrivacyPro/Views/SubscriptionFlowView.swift rename to DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index 4060d53e57..07d7571e0a 100644 --- a/DuckDuckGo/PrivacyPro/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -26,6 +26,19 @@ struct SubscriptionFlowView: View { @ObservedObject var viewModel: SubscriptionFlowViewModel + 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 "" + } + } + var body: some View { ZStack { AsyncHeadlessWebView(url: $viewModel.purchaseURL, @@ -34,13 +47,17 @@ struct SubscriptionFlowView: View { shouldReload: $viewModel.shouldReloadWebview).background() // Overlay that appears when transaction is in progress - if viewModel.transactionInProgress { - PurchaseInProgressView() + if viewModel.transactionStatus != .idle { + PurchaseInProgressView(status: getTransactionStatus()) + } + } + .onChange(of: viewModel.shouldReloadWebview) { shouldReload in + if shouldReload { + viewModel.shouldReloadWebview = false } } .onChange(of: viewModel.shouldReloadWebview) { shouldReload in if shouldReload { - print("WebView reload triggered") viewModel.shouldReloadWebview = false } } @@ -48,21 +65,21 @@ struct SubscriptionFlowView: View { Task { await viewModel.initializeViewData() } }) .navigationTitle(viewModel.viewTitle) - .navigationBarBackButtonHidden(viewModel.transactionInProgress) + .navigationBarBackButtonHidden(viewModel.transactionStatus != .idle) // Active subscription found Alert .alert(isPresented: $viewModel.hasActiveSubscription) { Alert( - title: Text("Subscription Found"), - message: Text("We found a subscription associated with this Apple ID."), - primaryButton: .cancel(Text("Cancel")) { - // TODO: Handle subscription Restore cancellation + title: Text(UserText.subscriptionFoundTitle), + message: Text(UserText.subscriptionFoundText), + primaryButton: .cancel(Text(UserText.subscriptionFoundCancel)) { }, - secondaryButton: .default(Text("Restore")) { + secondaryButton: .default(Text(UserText.subscriptionFoundCancel)) { viewModel.restoreAppstoreTransaction() } ) } + .navigationBarBackButtonHidden(viewModel.transactionStatus != .idle) } } #endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift new file mode 100644 index 0000000000..769041e705 --- /dev/null +++ b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift @@ -0,0 +1,99 @@ +// +// SubscriptionSettingsView.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 + +class SceneEnvironment: ObservableObject { + weak var windowScene: UIWindowScene? +} + +#if SUBSCRIPTION +@available(iOS 15.0, *) +struct SubscriptionSettingsView: View { + + @ObservedObject var viewModel: SubscriptionSettingsViewModel + @Environment(\.presentationMode) var presentationMode + @StateObject var sceneEnvironment = SceneEnvironment() + + var body: some View { + List { + Section(header: Text(viewModel.subscriptionDetails) + .lineLimit(nil) + .daxBodyRegular() + .fixedSize(horizontal: false, vertical: true)) { + EmptyView() + .frame(height: 0) + .hidden() + }.textCase(nil) + Section(header: Text(UserText.subscriptionManageDevices)) { + SettingsCustomCell(content: { + Text(UserText.subscriptionAddDevice) + .daxBodyRegular() + .foregroundColor(Color.init(designSystemColor: .accent)) + }, + action: {}) + + SettingsCustomCell(content: { + Text(UserText.subscriptionRemoveFromDevice) + .daxBodyRegular() + .foregroundColor(Color.init(designSystemColor: .accent))}, + action: { viewModel.shouldDisplayRemovalNotice.toggle() }, + isButton: true) + + } + Section(header: Text(UserText.subscriptionManagePlan)) { + SettingsCustomCell(content: { + Text(UserText.subscriptionChangePlan) + .daxBodyRegular() + }, + action: { Task { viewModel.manageSubscription() } }, + isButton: true) + } + Section(header: Text(UserText.subscriptionHelpAndSupport), + footer: Text(UserText.subscriptionFAQFooter)) { + NavigationLink(destination: Text(UserText.subscriptionFAQ)) { + SettingsCustomCell(content: { + Text(UserText.subscriptionFAQ) + .daxBodyRegular() + }, + action: {}, + disclosureIndicator: false) + } + } + } + .navigationTitle(UserText.settingsPProManageSubscription) + + // Remove subscription + .alert(isPresented: $viewModel.shouldDisplayRemovalNotice) { + Alert( + title: Text(UserText.subscriptionRemoveFromDeviceConfirmTitle), + message: Text(UserText.subscriptionRemoveFromDeviceConfirmText), + primaryButton: .cancel(Text(UserText.subscriptionRemoveCancel)) { + }, + secondaryButton: .destructive(Text(UserText.subscriptionRemove)) { + viewModel.removeSubscription() + presentationMode.wrappedValue.dismiss() + } + ) + } + } +} +#endif diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index cf49931d46..dc0705d9e9 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -961,25 +961,24 @@ But if you *do* want a peek under the hood, you can find more information about // Privacy Pro Section public static let settingsPProSection = NSLocalizedString("settings.ppro", value: "Privacy Pro", comment: "Product name for the subscription bundle") - public static let settingsPProSubscribe = NSLocalizedString("settings.ppro.subscribe", value: "Subscribe to Privacy Pro", comment: "Call to action title for Privacy Pro") - public static let settingsPProDescription = NSLocalizedString("settings.ppro.description", value:"More seamless privacy with three new protections, including:", comment: "Privacy pro description subtext") - public static let settingsPProFeatures = NSLocalizedString("settings.ppro.features", value: + 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") + public static let settingsPProFeatures = NSLocalizedString("settings.subscription.features", value: """ • VPN (Virtual Private Network) • Personal Information Removal • Identity Theft Restoration """, comment: "Privacy pro features list") - public static let settingsPProLearnMore = NSLocalizedString("settings.ppro.learn.more", value: "Learn More", comment: "Learn more button text for privacy pro") + public static let settingsPProLearnMore = NSLocalizedString("settings.subscription.learn.more", value: "Learn More", comment: "Learn more button text for privacy pro") - public static let settingsPProManageSubscription = NSLocalizedString("settings.ppro.manage", value: "Subscription Settings", comment: "Subscription Settings button text for privacy pro") - - public static let settingsPProVPNTitle = NSLocalizedString("settings.ppro.VPN.title", value: "VPN", comment: "VPN cell title for privacy pro") - public static let settingsPProDBPTitle = NSLocalizedString("settings.ppro.DBP.title", value: "Personal Information Removal", comment: "Data Broker protection cell title for privacy pro") - public static let settingsPProDBPSubTitle = NSLocalizedString("settings.ppro.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.ppro.ITR.title", value: "Identity Theft Restoration", comment: "Identity theft restoration cell title for privacy pro") - public static let settingsPProITRSubTitle = NSLocalizedString("settings.ppro.ITR.subtitle", value: "If your identity is stolen, we'll help restore it", comment: "Identity theft restoration cell subtitle for privacy pro") + public static let settingsPProManageSubscription = NSLocalizedString("settings.subscription.manage", value: "Subscription Settings", comment: "Subscription Settings button text for privacy pro") + public static let settingsPProVPNTitle = NSLocalizedString("settings.subscription.VPN.title", value: "VPN", comment: "VPN cell title for privacy pro") + public static let settingsPProDBPTitle = NSLocalizedString("settings.subscription.DBP.title", value: "Personal Information Removal", comment: "Data Broker protection cell title for privacy pro") + 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 @@ -1002,6 +1001,31 @@ 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 + + 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) + } + 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 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") + 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.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") } diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index b4aaf23976..34845d2775 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1837,43 +1837,43 @@ But if you *do* want a peek under the hood, you can find more information about /* Product name for the subscription bundle */ "settings.ppro" = "Privacy Pro"; +/* Settings screen cell for long press previews */ +"settings.previews" = "Long-Press Previews"; + +/* Settings title for the privacy section */ +"settings.privacy" = "Privacy"; + /* Data Broker protection cell subtitle for privacy pro */ -"settings.ppro.DBP.subtitle" = "Remove your info from sites that sell it"; +"settings.subscription.DBP.subtitle" = "Remove your info from sites that sell it"; /* Data Broker protection cell title for privacy pro */ -"settings.ppro.DBP.title" = "Personal Information Removal"; +"settings.subscription.DBP.title" = "Personal Information Removal"; /* Privacy pro description subtext */ -"settings.ppro.description" = "More seamless privacy with three new protections, including:"; +"settings.subscription.description" = "More seamless privacy with three new protections, including:"; /* Privacy pro features list */ -"settings.ppro.features" = " • VPN (Virtual Private Network) +"settings.subscription.features" = " • VPN (Virtual Private Network) • Personal Information Removal • Identity Theft Restoration"; /* Identity theft restoration cell subtitle for privacy pro */ -"settings.ppro.ITR.subtitle" = "If your identity is stolen, we'll help restore it"; +"settings.subscription.ITR.subtitle" = "If your identity is stolen, we'll help restore it"; /* Identity theft restoration cell title for privacy pro */ -"settings.ppro.ITR.title" = "Identity Theft Restoration"; +"settings.subscription.ITR.title" = "Identity Theft Restoration"; /* Learn more button text for privacy pro */ -"settings.ppro.learn.more" = "Learn More"; +"settings.subscription.learn.more" = "Learn More"; /* Subscription Settings button text for privacy pro */ -"settings.ppro.manage" = "Subscription Settings"; +"settings.subscription.manage" = "Subscription Settings"; /* Call to action title for Privacy Pro */ -"settings.ppro.subscribe" = "Subscribe to Privacy Pro"; +"settings.subscription.subscribe" = "Subscribe to Privacy Pro"; /* VPN cell title for privacy pro */ -"settings.ppro.VPN.title" = "VPN"; - -/* Settings screen cell for long press previews */ -"settings.previews" = "Long-Press Previews"; - -/* Settings title for the privacy section */ -"settings.privacy" = "Privacy"; +"settings.subscription.VPN.title" = "VPN"; /* Settings screen cell text for sync and backup */ "settings.sync" = "Sync & Backup"; @@ -1914,6 +1914,66 @@ 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?"; +/* Add to another device button */ +"subscription.add.device" = "Add to Another Device"; + +/* Change plan or billing title */ +"subscription.change.plan" = "Change Plan Or Billing"; + +/* FAQ Button */ +"subscription.faq" = "Privacy Pro FAQ"; + +/* FAQ Description */ +"subscription.faq.description" = "Visit our Privacy Pro help pages for answers to frequently asked questions"; + +/* Help and support Section header */ +"subscription.help" = "Help and support"; + +/* Header for the device management section */ +"subscription.manage.device" = "Manage Devices"; + +/* Manage Plan header */ +"subscription.manage.plan" = "Manage Plan"; + +/* Progress view title when completing the purchase */ +"subscription.progress.view.completing.purchase" = "Completing purchase..."; + +/* Progress view title when starting the purchase */ +"subscription.progress.view.purchasing.subscription" = "Purchase in progress..."; + +/* Progress view title when restoring past subscription purchase */ +"subscription.progress.view.restoring.subscription" = "Restoring subscription..."; + +/* Remove from this device button */ +"subscription.remove.from.device" = "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."; + +/* Remove from device confirmation dialog title */ +"subscription.remove.from.device.title" = "Remove From This Device?"; + +/* Remove subscription button text */ +"subscription.remove.subscription" = "Remove Subscription"; + +/* 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"; + +/* Restore action for the existing subscription dialog */ +"subscription.subscription.found.restore" = "Restore"; + +/* Message for the existing subscription dialog */ +"subscription.subscription.found.text" = "We found a subscription associated with this Apple ID."; + +/* Title for the existing subscription dialog */ +"subscription.subscription.found.tite" = "Subscription Found"; + /* Message confirming that recovery code was copied to clipboard */ "sync.code.copied" = "Recovery code copied to clipboard";