Skip to content

Commit

Permalink
Daniel/subscriptions/8.itp (#2427)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/72649045549333/1205043809052693/f

Description:
Implements ITP for subscriptions
Implements navigating back in WebViews
Shows/Hide navigation bar on WebViews depending on scroll position
Subscription WebViews are now shown in a sheet for better navigation/usability
Includes several improvements to the subscription flow for memory management and etc.
Allows navigating to tel: FaceTime: and etc schemas
Properly manages to navigate to new windows
  • Loading branch information
afterxleep authored Feb 9, 2024
1 parent d7f7111 commit 18663c3
Show file tree
Hide file tree
Showing 34 changed files with 1,366 additions and 246 deletions.
66 changes: 61 additions & 5 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions DuckDuckGo/MainViewController+Segues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,6 @@ extension MainViewController {
let navController = UINavigationController(rootViewController: settingsController)
navController.applyTheme(ThemeManager.shared.currentTheme)
settingsController.modalPresentationStyle = .automatic

settingsController.isModalInPresentation = true

present(navController, animated: true) {
completion?(settingsViewModel)
Expand Down
39 changes: 31 additions & 8 deletions DuckDuckGo/SettingsSubscriptionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import UIKit
struct SettingsSubscriptionView: View {

@EnvironmentObject var viewModel: SettingsViewModel
@StateObject var subscriptionFlowViewModel = SubscriptionFlowViewModel()
@State var isShowingsubScriptionFlow = false
@State var isShowingDBP = false
@State var isShowingITP = false

private var subscriptionDescriptionView: some View {
VStack(alignment: .leading) {
Expand All @@ -51,11 +55,11 @@ struct SettingsSubscriptionView: View {
private var purchaseSubscriptionView: some View {
return Group {
SettingsCustomCell(content: { subscriptionDescriptionView })
let viewModel = SubscriptionFlowViewModel(onFeatureSelected: { value in
self.viewModel.onAppearNavigationTarget = value
})
NavigationLink(destination: SubscriptionFlowView(viewModel: viewModel)) {
SettingsCustomCell(content: { learnMoreView })
SettingsCustomCell(content: { learnMoreView },
action: { isShowingsubScriptionFlow = true },
isButton: true )
.sheet(isPresented: $isShowingsubScriptionFlow) {
SubscriptionFlowView(viewModel: subscriptionFlowViewModel).interactiveDismissDisabled()
}
}
}
Expand All @@ -68,17 +72,24 @@ struct SettingsSubscriptionView: View {
disclosureIndicator: true,
isButton: true)

/*
NavigationLink(destination: Text("Data Broker Protection"), isActive: $viewModel.shouldNavigateToDBP) {
SettingsCellView(label: UserText.settingsPProDBPTitle, subtitle: UserText.settingsPProDBPSubTitle)
}
*/

NavigationLink(destination: Text("Identity Theft Restoration"), isActive: $viewModel.shouldNavigateToITP) {
SettingsCellView(label: UserText.settingsPProITRTitle, subtitle: UserText.settingsPProITRSubTitle)
SettingsCellView(label: UserText.settingsPProITRTitle,
subtitle: UserText.settingsPProITRSubTitle,
action: { isShowingITP.toggle() }, isButton: true)
.sheet(isPresented: $isShowingITP) {
SubscriptionITPView()
}


NavigationLink(destination: SubscriptionSettingsView(viewModel: SubscriptionSettingsViewModel())) {
NavigationLink(destination: SubscriptionSettingsView()) {
SettingsCustomCell(content: { manageSubscriptionView })
}

}
}

Expand All @@ -91,6 +102,18 @@ struct SettingsSubscriptionView: View {
} else {
purchaseSubscriptionView
}

}
// Refresh subscription when dismissing the Subscription Flow
.onChange(of: isShowingsubScriptionFlow, perform: { value in
if !value {
Task { viewModel.onAppear() }
}
})

.onReceive(subscriptionFlowViewModel.$selectedFeature) { value in
guard let value else { return }
viewModel.onAppearNavigationTarget = value
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions DuckDuckGo/SettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ final class SettingsViewModel: ObservableObject {

// Used to automatically navigate on Appear to a specific section
enum SettingsSection: String {
case none, netP, dbp, itp
case none, netP, dbp, itr
}
@Published var onAppearNavigationTarget: SettingsSection

Expand Down Expand Up @@ -422,13 +422,13 @@ extension SettingsViewModel {
private func navigateOnAppear() {
// We need a short delay to let the SwifttUI view lifecycle complete
// Otherwise the transition can be inconsistent
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
switch self.onAppearNavigationTarget {
case .netP:
self.presentLegacyView(.netP)
case .dbp:
self.shouldNavigateToDBP = true
case .itp:
case .itr:
self.shouldNavigateToITP = true
default:
break
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// AsyncHeadlessWebView.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 Foundation
import WebKit
import UserScript
import SwiftUI
import DesignResourcesKit
import Core

struct AsyncHeadlessWebViewSettings {
let bounces: Bool

init(bounces: Bool = false) {
self.bounces = bounces
}
}

struct AsyncHeadlessWebView: View {
@StateObject var viewModel: AsyncHeadlessWebViewViewModel

var body: some View {
GeometryReader { geometry in
HeadlessWebView(
userScript: viewModel.userScript,
subFeature: viewModel.subFeature,
settings: viewModel.settings,
onScroll: { newPosition in
viewModel.updateScrollPosition(newPosition)
},
onURLChange: { newURL in
viewModel.url = newURL
},
onCanGoBack: { value in
viewModel.canGoBack = value
},
onCanGoForward: { value in
viewModel.canGoForward = value
},
onContentType: { value in
viewModel.contentType = value
},
navigationCoordinator: viewModel.navigationCoordinator
)
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// AsyncHeadlessWebViewModel.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 UserScript
import Core
import Combine

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

private var initialScrollPositionSubject = PassthroughSubject<CGPoint, Never>()
private var subsequentScrollPositionSubject = PassthroughSubject<CGPoint, Never>()
private var cancellables = Set<AnyCancellable>()
private var isFirstUpdate = true
private var initialDelay = 1

@Published var scrollPosition: CGPoint = .zero
@Published var url: URL?
@Published var canGoBack: Bool = false
@Published var canGoForward: Bool = false
@Published var contentType: String = ""

var navigationCoordinator = HeadlessWebViewNavCoordinator(webView: nil)

init(userScript: UserScriptMessaging?, subFeature: Subfeature?, settings: AsyncHeadlessWebViewSettings) {
self.userScript = userScript
self.subFeature = subFeature
self.settings = settings

// Delayed publishing first update for scrollPosition
// To avoid publishing events on view updates
initialScrollPositionSubject
.delay(for: .seconds(initialDelay), scheduler: RunLoop.main)
.merge(with: subsequentScrollPositionSubject)
.assign(to: &$scrollPosition)
}

func updateScrollPosition(_ newPosition: CGPoint) {
if isFirstUpdate {
initialScrollPositionSubject.send(newPosition)
isFirstUpdate = false
} else {
subsequentScrollPositionSubject.send(newPosition)
}
}


}
81 changes: 81 additions & 0 deletions DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// HeadlessWebView.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 WebKit
import UserScript

struct HeadlessWebView: UIViewRepresentable {
let userScript: UserScriptMessaging?
let subFeature: Subfeature?
let settings: AsyncHeadlessWebViewSettings
var onScroll: ((CGPoint) -> Void)?
var onURLChange: ((URL) -> Void)?
var onCanGoBack: ((Bool) -> Void)?
var onCanGoForward: ((Bool) -> Void)?
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)

navigationCoordinator.webView = webView
webView.uiDelegate = context.coordinator
webView.scrollView.delegate = context.coordinator
webView.scrollView.bounces = settings.bounces
webView.navigationDelegate = context.coordinator

#if DEBUG
if #available(iOS 16.4, *) {
webView.isInspectable = true
}
#endif

context.coordinator.setupWebViewObservation(webView)
return webView
}

func updateUIView(_ uiView: WKWebView, context: Context) {}

func makeCoordinator() -> HeadlessWebViewCoordinator {
HeadlessWebViewCoordinator(self,
onScroll: onScroll,
onURLChange: onURLChange,
onCanGoBack: onCanGoBack,
onCanGoForward: onCanGoForward,
onContentType: onContentType)
}

@MainActor
private func makeUserContentController() -> WKUserContentController {
let userContentController = WKUserContentController()
if let userScript, let subFeature {
userContentController.addUserScript(userScript.makeWKUserScriptSync())
userContentController.addHandler(userScript)
userScript.registerSubfeature(delegate: subFeature)
}
return userContentController
}

}
Loading

0 comments on commit 18663c3

Please sign in to comment.