Skip to content

Commit

Permalink
Subscription ITP Fixes (#2480)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/1204099484721401/1206585463538617/f

Description:
Adds allowedDomains setting to Webviews to prevent the user from loading pages outside of the scope
Opens External Iris links in a separate Sheet
Adds privacy protection and content blocking rules to Webviews
Minor UI glitches and fixes
Enabled links from Subscription Welcome page to ITR and PIR
  • Loading branch information
afterxleep authored Feb 19, 2024
1 parent e12fca8 commit 0222e3e
Show file tree
Hide file tree
Showing 15 changed files with 359 additions and 38 deletions.
10 changes: 9 additions & 1 deletion DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,8 @@
D668D9292B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D9282B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift */; };
D668D92B2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D92A2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift */; };
D668D92D2B696945008E2FF2 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D92C2B696945008E2FF2 /* Subscription.swift */; };
D68A21442B7EC08500BB372E /* SubscriptionExternalLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68A21432B7EC08500BB372E /* SubscriptionExternalLinkView.swift */; };
D68A21462B7EC16200BB372E /* SubscriptionExternalLinkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68A21452B7EC16200BB372E /* SubscriptionExternalLinkViewModel.swift */; };
D68DF81C2B58302E0023DBEA /* SubscriptionRestoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */; };
D68DF81E2B5830380023DBEA /* SubscriptionRestoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */; };
D69DBB502B72B1D300156310 /* View+TopMostController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69DBB4F2B72B1D200156310 /* View+TopMostController.swift */; };
Expand Down Expand Up @@ -2436,6 +2438,8 @@
D668D9282B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityTheftRestorationPagesUserScript.swift; sourceTree = "<group>"; };
D668D92A2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityTheftRestorationPagesFeature.swift; sourceTree = "<group>"; };
D668D92C2B696945008E2FF2 /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = "<group>"; };
D68A21432B7EC08500BB372E /* SubscriptionExternalLinkView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionExternalLinkView.swift; sourceTree = "<group>"; };
D68A21452B7EC16200BB372E /* SubscriptionExternalLinkViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionExternalLinkViewModel.swift; sourceTree = "<group>"; };
D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRestoreView.swift; sourceTree = "<group>"; };
D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRestoreViewModel.swift; sourceTree = "<group>"; };
D69DBB4F2B72B1D200156310 /* View+TopMostController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+TopMostController.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4512,6 +4516,7 @@
D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */,
D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */,
D6BFCB602B7525160051FF81 /* SubscriptionPIRViewModel.swift */,
D68A21452B7EC16200BB372E /* SubscriptionExternalLinkViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
Expand All @@ -4533,6 +4538,7 @@
D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */,
D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */,
D668D9242B693778008E2FF2 /* SubscriptionITPView.swift */,
D68A21432B7EC08500BB372E /* SubscriptionExternalLinkView.swift */,
D6BFCB5E2B7524AA0051FF81 /* SubscriptionPIRView.swift */,
D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */,
D6D95CE02B6D52DA00960317 /* RootPresentationMode.swift */,
Expand All @@ -4545,9 +4551,9 @@
children = (
D668D92C2B696945008E2FF2 /* Subscription.swift */,
D664C7B32B289AA000CBFA76 /* SubscriptionPagesUserScript.swift */,
D664C7B52B289AA000CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift */,
D668D9282B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift */,
D668D92A2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift */,
D664C7B52B289AA000CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift */,
);
path = UserScripts;
sourceTree = "<group>";
Expand Down Expand Up @@ -6618,6 +6624,7 @@
31CB4251273AF50700FA0F3F /* SpeechRecognizerProtocol.swift in Sources */,
319A37172829C8AD0079FBCE /* UITableViewExtension.swift in Sources */,
85EE7F59224673C5000FE757 /* WebContainerNavigationController.swift in Sources */,
D68A21462B7EC16200BB372E /* SubscriptionExternalLinkViewModel.swift in Sources */,
F4C9FBF528340DDA002281CC /* AutofillInterfaceEmailTruncator.swift in Sources */,
1E016AB42949FEB500F21625 /* OmniBarNotificationViewModel.swift in Sources */,
6AC6DAB328804F97002723C0 /* BarsAnimator.swift in Sources */,
Expand Down Expand Up @@ -6645,6 +6652,7 @@
D6E83C602B22B3C9006C8AFB /* SettingsState.swift in Sources */,
D6E83C482B20C812006C8AFB /* SettingsHostingController.swift in Sources */,
F46FEC5727987A5F0061D9DF /* KeychainItemsDebugViewController.swift in Sources */,
D68A21442B7EC08500BB372E /* SubscriptionExternalLinkView.swift in Sources */,
BD862E0B2B30F9300073E2EE /* VPNFeedbackFormView.swift in Sources */,
02341FA62A4379CC008A1531 /* OnboardingStepViewModel.swift in Sources */,
850365F323DE087800D0F787 /* UIImageViewExtension.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
{
"identity" : "browserserviceskit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/DuckDuckGo/BrowserServicesKit",
"location" : "https://github.com/duckduckgo/BrowserServicesKit",
"state" : {
"revision" : "5ecf4fe56f334be6eaecb65f6d55632a6d53921c",
"version" : "109.0.0"
Expand Down
18 changes: 18 additions & 0 deletions DuckDuckGo/SettingsSubscriptionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,24 @@ struct SettingsSubscriptionView: View {
}
})

.onChange(of: viewModel.shouldNavigateToDBP, perform: { value in
if value {
// Allow the sheet to dismiss before presenting a new one
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
isShowingDBP = true
}
}
})

.onChange(of: viewModel.shouldNavigateToITP, perform: { value in
if value {
// Allow the sheet to dismiss before presenting a new one
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
isShowingITP = true
}
}
})

.onReceive(subscriptionFlowViewModel.$selectedFeature) { value in
guard let value else { return }
viewModel.onAppearNavigationTarget = value
Expand Down
1 change: 1 addition & 0 deletions DuckDuckGo/SettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ final class SettingsViewModel: ObservableObject {
enum SettingsSection: String {
case none, netP, dbp, itr
}

@Published var onAppearNavigationTarget: SettingsSection

// MARK: Bindings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,18 @@ import Core

struct AsyncHeadlessWebViewSettings {
let bounces: Bool
let javascriptEnabled: Bool
let allowedDomains: [String]?
let contentBlocking: Bool

init(bounces: Bool = false) {
init(bounces: Bool = true,
javascriptEnabled: Bool = true,
allowedDomains: [String]? = nil,
contentBlocking: Bool = true) {
self.bounces = bounces
self.javascriptEnabled = javascriptEnabled
self.allowedDomains = allowedDomains
self.contentBlocking = contentBlocking
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,13 @@ final class AsyncHeadlessWebViewViewModel: ObservableObject {
@Published var canGoBack: Bool = false
@Published var canGoForward: Bool = false
@Published var contentType: String = ""
@Published var allowedDomains: [String]?

var navigationCoordinator = HeadlessWebViewNavCoordinator(webView: nil)

init(userScript: UserScriptMessaging?, subFeature: Subfeature?, settings: AsyncHeadlessWebViewSettings) {
init(userScript: UserScriptMessaging? = nil,
subFeature: Subfeature? = nil,
settings: AsyncHeadlessWebViewSettings) {
self.userScript = userScript
self.subFeature = subFeature
self.settings = settings
Expand Down
29 changes: 25 additions & 4 deletions DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Foundation
import SwiftUI
import WebKit
import UserScript
import BrowserServicesKit

struct HeadlessWebView: UIViewRepresentable {
let userScript: UserScriptMessaging?
Expand All @@ -33,18 +34,24 @@ struct HeadlessWebView: UIViewRepresentable {
var onContentType: ((String) -> Void)?
var navigationCoordinator: HeadlessWebViewNavCoordinator


func makeUIView(context: Context) -> WKWebView {
let configuration = WKWebViewConfiguration()
configuration.userContentController = makeUserContentController()

let webView = WKWebView(frame: .zero, configuration: configuration)
let preferences = WKWebpagePreferences()
preferences.allowsContentJavaScript = settings.javascriptEnabled
preferences.preferredContentMode = .mobile
configuration.defaultWebpagePreferences = preferences

navigationCoordinator.webView = webView
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.uiDelegate = context.coordinator
webView.scrollView.delegate = context.coordinator
webView.scrollView.bounces = settings.bounces
webView.scrollView.contentInsetAdjustmentBehavior = .never
webView.navigationDelegate = context.coordinator
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
navigationCoordinator.webView = webView


#if DEBUG
if #available(iOS 16.4, *) {
Expand All @@ -64,16 +71,30 @@ struct HeadlessWebView: UIViewRepresentable {
onURLChange: onURLChange,
onCanGoBack: onCanGoBack,
onCanGoForward: onCanGoForward,
onContentType: onContentType)
onContentType: onContentType,
settings: settings
)
}

@MainActor
private func makeUserContentController() -> WKUserContentController {
let userContentController = WKUserContentController()

// Enable content blocking rules
if settings.contentBlocking {
let sourceProvider = DefaultScriptSourceProvider()
let contentBlockerUserScript = ContentBlockerRulesUserScript(configuration: sourceProvider.contentBlockerRulesConfig)
let contentScopeUserScript = ContentScopeUserScript(sourceProvider.privacyConfigurationManager,
properties: sourceProvider.contentScopeProperties)
userContentController.addUserScript(contentBlockerUserScript.makeWKUserScriptSync())
userContentController.addUserScript(contentScopeUserScript.makeWKUserScriptSync())
}

if let userScript, let subFeature {
userContentController.addUserScript(userScript.makeWKUserScriptSync())
userContentController.addHandler(userScript)
userScript.registerSubfeature(delegate: subFeature)

}
return userContentController
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ final class HeadlessWebViewCoordinator: NSObject {
var onCanGoBack: ((Bool) -> Void)?
var onCanGoForward: ((Bool) -> Void)?
var onContentType: ((String) -> Void)?
var settings: AsyncHeadlessWebViewSettings

var size: CGSize = .zero

private var lastURL: URL?

Expand All @@ -44,13 +47,16 @@ final class HeadlessWebViewCoordinator: NSObject {
onURLChange: ((URL) -> Void)?,
onCanGoBack: ((Bool) -> Void)?,
onCanGoForward: ((Bool) -> Void)?,
onContentType: ((String) -> Void)?) {
onContentType: ((String) -> Void)?,
allowedDomains: [String]? = nil,
settings: AsyncHeadlessWebViewSettings = AsyncHeadlessWebViewSettings()) {
self.parent = parent
self.onScroll = onScroll
self.onURLChange = onURLChange
self.onCanGoBack = onCanGoBack
self.onCanGoForward = onCanGoForward
self.onContentType = onContentType
self.settings = settings
}

func setupWebViewObservation(_ webView: WKWebView) {
Expand Down Expand Up @@ -107,32 +113,49 @@ extension HeadlessWebViewCoordinator: WKNavigationDelegate {
}

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript(Constants.contentTypeJS) { result, error in
guard error == nil, let contentType = result as? String else {
return
if settings.javascriptEnabled {
webView.evaluateJavaScript(Constants.contentTypeJS) { result, error in
guard error == nil, let contentType = result as? String else {
return
}
self.onContentType?(contentType)
}
self.onContentType?(contentType)
}
}

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard let url = navigationAction.request.url else {

decisionHandler(.allow)
return
guard let url = navigationAction.request.url, let scheme = url.scheme else {
decisionHandler(.cancel)
return
}

guard let scheme = url.scheme else {

// Handle custom schemes (e.g., tel:, facetime:, etc.)
if Constants.externalSchemes.contains(scheme), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
decisionHandler(.cancel)
return
}

// Publish the URL change
self.onURLChange?(url)
lastURL = url

if Constants.externalSchemes.contains(scheme) && UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
decisionHandler(.cancel)
} else {
decisionHandler(.allow)
// Validate the URL against allowed domains list, if present
if let allowedDomains = settings.allowedDomains, !allowedDomains.isEmpty {
let isURLAllowed = allowedDomains.contains { domain in
url.isPart(ofDomain: domain)
}

decisionHandler(isURLAllowed ? .allow : .cancel)
return
}

// Default policy: allow navigation
decisionHandler(.allow)
}

func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
// NOOP
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec
await withTransactionInProgress {
transactionStatus = .restoring
switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() {
case .success(let update):
case .success:
return true
case .failure:
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ final class SubscriptionEmailViewModel: ObservableObject {
@Published var managingSubscriptionEmail = false
@Published var webViewModel: AsyncHeadlessWebViewViewModel

private static let allowedDomains = [
"duckduckgo.com",
"microsoftonline.com",
"duosecurity.com",
]

private var cancellables = Set<AnyCancellable>()

init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(),
Expand All @@ -49,7 +55,9 @@ final class SubscriptionEmailViewModel: ObservableObject {
self.accountManager = accountManager
self.webViewModel = AsyncHeadlessWebViewViewModel(userScript: userScript,
subFeature: subFeature,
settings: AsyncHeadlessWebViewSettings(bounces: false))
settings: AsyncHeadlessWebViewSettings(bounces: false,
allowedDomains: Self.allowedDomains,
contentBlocking: false))
initializeView()
setupTransactionObservers()
}
Expand Down
Loading

0 comments on commit 0222e3e

Please sign in to comment.