Skip to content

Commit

Permalink
13. Iterate on Subscription Changes (#2498)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1206362842825232/f

Description:
This includes several copy and small UI changes for the iOS Subscription flow based on user feedback.
Memory management updates to prevent a leak on the subscriber feature and thoroughly deallocating the webviews
  • Loading branch information
afterxleep authored Feb 27, 2024
1 parent d3597eb commit eb0e479
Show file tree
Hide file tree
Showing 21 changed files with 501 additions and 183 deletions.
14 changes: 14 additions & 0 deletions DuckDuckGo/SettingsSubscriptionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ struct SettingsSubscriptionView: View {
.foregroundColor(Color.init(designSystemColor: .accent))
}

private var iHaveASubscriptionView: some View {
Text(UserText.settingsPProIHaveASubscription)
.daxBodyRegular()
.foregroundColor(Color.init(designSystemColor: .accent))
}

private var manageSubscriptionView: some View {
Text(UserText.settingsPProManageSubscription)
.daxBodyRegular()
Expand All @@ -62,6 +68,14 @@ struct SettingsSubscriptionView: View {
.sheet(isPresented: $isShowingsubScriptionFlow) {
SubscriptionFlowView(viewModel: subscriptionFlowViewModel).interactiveDismissDisabled()
}

SettingsCustomCell(content: { iHaveASubscriptionView },
action: {
isShowingsubScriptionFlow = true
subscriptionFlowViewModel.activateSubscriptionOnLoad = true
},
isButton: true )

}
}

Expand Down
4 changes: 4 additions & 0 deletions DuckDuckGo/SettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -342,10 +342,14 @@ extension SettingsViewModel {
case .success(let response) where !response.isSubscriptionActive:
AccountManager().signOut()
setupSubscriptionPurchaseOptions()

case .success(let response):

// Cache Subscription state
self.state.subscription.hasActiveSubscription = true
Self.cachedHasActiveSubscription = self.state.subscription.hasActiveSubscription


// Check entitlements and update UI accordingly
let entitlements: [AccountManager.Entitlement] = [.identityTheftRestoration, .dataBrokerProtection, .networkProtection]
for entitlement in entitlements {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import Core
import Combine

final class AsyncHeadlessWebViewViewModel: ObservableObject {
let userScript: UserScriptMessaging?
weak var userScript: UserScriptMessaging?
let subFeature: Subfeature?
let settings: AsyncHeadlessWebViewSettings

Expand Down
31 changes: 29 additions & 2 deletions DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import UserScript
import BrowserServicesKit

struct HeadlessWebView: UIViewRepresentable {
let userScript: UserScriptMessaging?
weak var userScript: UserScriptMessaging?
let subFeature: Subfeature?
let settings: AsyncHeadlessWebViewSettings
var onScroll: ((CGPoint) -> Void)?
Expand Down Expand Up @@ -66,7 +66,8 @@ struct HeadlessWebView: UIViewRepresentable {
func updateUIView(_ uiView: WKWebView, context: Context) {}

func makeCoordinator() -> HeadlessWebViewCoordinator {
HeadlessWebViewCoordinator(self,
let currentViewController = UIViewController.getCurrentViewController()
return HeadlessWebViewCoordinator(self, presenter: currentViewController,
onScroll: onScroll,
onURLChange: onURLChange,
onCanGoBack: onCanGoBack,
Expand Down Expand Up @@ -98,5 +99,31 @@ struct HeadlessWebView: UIViewRepresentable {
}
return userContentController
}

static func dismantleUIView(_ uiView: WKWebView, coordinator: HeadlessWebViewCoordinator) {
uiView.stopLoading()
uiView.uiDelegate = nil
uiView.navigationDelegate = nil
uiView.scrollView.delegate = nil
uiView.configuration.userContentController = WKUserContentController()
uiView.configuration.userContentController.removeAllScriptMessageHandlers()
uiView.configuration.userContentController.removeAllContentRuleLists()
uiView.configuration.userContentController.removeAllUserScripts()
coordinator.cleanUp()

}

}

extension UIViewController {
static func getCurrentViewController(base: UIViewController? = UIApplication.shared.windows.first { $0.isKeyWindow }?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return getCurrentViewController(base: nav.visibleViewController)
} else if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
return getCurrentViewController(base: selected)
} else if let presented = base?.presentedViewController {
return getCurrentViewController(base: presented)
}
return base
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import Foundation
import WebKit

final class HeadlessWebViewCoordinator: NSObject {

var parent: HeadlessWebView
weak var presenter: UIViewController?

var onScroll: ((CGPoint) -> Void)?
var onURLChange: ((URL) -> Void)?
var onCanGoBack: ((Bool) -> Void)?
Expand All @@ -43,6 +46,7 @@ final class HeadlessWebViewCoordinator: NSObject {
private var webViewCanGoForwardObservation: NSKeyValueObservation?

init(_ parent: HeadlessWebView,
presenter: UIViewController?,
onScroll: ((CGPoint) -> Void)?,
onURLChange: ((URL) -> Void)?,
onCanGoBack: ((Bool) -> Void)?,
Expand All @@ -51,6 +55,7 @@ final class HeadlessWebViewCoordinator: NSObject {
allowedDomains: [String]? = nil,
settings: AsyncHeadlessWebViewSettings = AsyncHeadlessWebViewSettings()) {
self.parent = parent
self.presenter = presenter
self.onScroll = onScroll
self.onURLChange = onURLChange
self.onCanGoBack = onCanGoBack
Expand All @@ -62,23 +67,46 @@ final class HeadlessWebViewCoordinator: NSObject {
func setupWebViewObservation(_ webView: WKWebView) {
webViewURLObservation = webView.observe(\.url, options: [.new]) { [weak self] _, change in
if let newURL = change.newValue as? URL {
self?.onURLChange?(newURL)
self?.onCanGoBack?(webView.canGoBack)
DispatchQueue.main.async {
self?.onURLChange?(newURL)
self?.onCanGoBack?(webView.canGoBack)
}
}
}

webViewCanGoBackObservation = webView.observe(\.canGoBack, options: [.new]) { [weak self] _, change in
if let canGoBack = change.newValue {
self?.onCanGoBack?(canGoBack)
DispatchQueue.main.async {
self?.onCanGoBack?(canGoBack)
}
}
}

webViewCanGoForwardObservation = webView.observe(\.canGoForward, options: [.new]) { [weak self] _, change in
if let onCanGoForward = change.newValue {
self?.onCanGoForward?(onCanGoForward)
DispatchQueue.main.async {
self?.onCanGoForward?(onCanGoForward)
}
}
}
}

// Called from the webView dismantle
func cleanUp() {
webViewURLObservation?.invalidate()
webViewCanGoBackObservation?.invalidate()
webViewCanGoForwardObservation?.invalidate()

webViewURLObservation = nil
webViewCanGoBackObservation = nil
webViewCanGoForwardObservation = nil

onScroll = nil
onURLChange = nil
onCanGoBack = nil
onCanGoForward = nil
onContentType = nil
}

}

Expand Down Expand Up @@ -158,4 +186,29 @@ extension HeadlessWebViewCoordinator: WKNavigationDelegate {
// NOOP
}

// Javascript Confirm dialogs Delegate
func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
guard let presenter = presenter else {
completionHandler(false)
return
}

let alertController = UIAlertController(title: UserText.subscriptionConfirmTitle, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: UserText.actionOK, style: .default, handler: { _ in completionHandler(true) }))
alertController.addAction(UIAlertAction(title: UserText.actionCancel, style: .cancel, handler: { _ in completionHandler(false) }))
presenter.present(alertController, animated: true, completion: nil)
}

// Javascript Confirm alert dialogs
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
guard let presenter = presenter else {
completionHandler()
return
}

let alertController = UIAlertController(title: UserText.subscriptionAlertTitle, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: UserText.actionOK, style: .default, handler: { _ in completionHandler() }))
presenter.present(alertController, animated: true, completion: nil)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Privacy-Pro-96x96.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ final class IdentityTheftRestorationPagesFeature: Subfeature, ObservableObject {
}


var broker: UserScriptMessageBroker?
weak var broker: UserScriptMessageBroker?
var featureName: String = Constants.featureName

var messageOriginPolicy: MessageOriginPolicy = .only(rules: [
Expand All @@ -70,6 +70,10 @@ final class IdentityTheftRestorationPagesFeature: Subfeature, ObservableObject {
let authToken = AccountManager().authToken ?? ""
return Subscription(token: authToken)
}

deinit {
broker = nil
}

}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import UserScript
import Combine
import Subscription

enum SubscriptionTransactionStatus {
case idle, purchasing, restoring, polling
}

@available(iOS 15.0, *)
final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObject {

Expand Down Expand Up @@ -59,23 +63,19 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec
static let month = "monthly"
static let year = "yearly"
}

enum TransactionStatus {
case idle, purchasing, restoring, polling
}


struct FeatureSelection: Codable {
let feature: String
}

@Published var transactionStatus: TransactionStatus = .idle
@Published var transactionStatus: SubscriptionTransactionStatus = .idle
@Published var hasActiveSubscription = false
@Published var purchaseError: AppStorePurchaseFlow.Error?
@Published var activateSubscription: Bool = false
@Published var emailActivationComplete: Bool = false
@Published var selectedFeature: FeatureSelection?

var broker: UserScriptMessageBroker?
weak var broker: UserScriptMessageBroker?

var featureName = Constants.featureName

Expand Down Expand Up @@ -274,5 +274,16 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec
}

}

func cleanup() {
transactionStatus = .idle
hasActiveSubscription = false
purchaseError = nil
activateSubscription = false
emailActivationComplete = false
selectedFeature = nil
broker = nil
}

}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ final class SubscriptionEmailViewModel: ObservableObject {
let subFeature: SubscriptionPagesUseSubscriptionFeature

var emailURL = URL.activateSubscriptionViaEmail
var viewTitle = UserText.subscriptionRestoreEmail
var viewTitle = UserText.subscriptionActivateEmail
@Published var subscriptionEmail: String?
@Published var shouldReloadWebView = false
@Published var activateSubscription = false
@Published var managingSubscriptionEmail = false
@Published var webViewModel: AsyncHeadlessWebViewViewModel
var webViewModel: AsyncHeadlessWebViewViewModel

private static let allowedDomains = [
"duckduckgo.com",
Expand Down Expand Up @@ -92,6 +92,11 @@ final class SubscriptionEmailViewModel: ObservableObject {
func loadURL() {
webViewModel.navigationCoordinator.navigateTo(url: emailURL )
}

deinit {
cancellables.removeAll()

}

}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ final class SubscriptionExternalLinkViewModel: ObservableObject {

var url: URL
var allowedDomains: [String]?
var webViewModel: AsyncHeadlessWebViewViewModel

private var canGoBackCancellable: AnyCancellable?
private var cancellables = Set<AnyCancellable>()

@Published var webViewModel: AsyncHeadlessWebViewViewModel
@Published var canNavigateBack: Bool = false

private var cancellables = Set<AnyCancellable>()

init(url: URL, allowedDomains: [String]? = nil) {
let webViewSettings = AsyncHeadlessWebViewSettings(bounces: false,
allowedDomains: allowedDomains,
Expand Down Expand Up @@ -64,5 +64,9 @@ final class SubscriptionExternalLinkViewModel: ObservableObject {
await webViewModel.navigationCoordinator.goBack()
}

deinit {
cancellables.removeAll()
}

}
#endif
Loading

0 comments on commit eb0e479

Please sign in to comment.