Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.

Commit 87792c9

Browse files
graemesamsymons
andauthored
Get iOS VPN widget ready to ship (#2830)
Co-authored-by: Sam Symons <[email protected]>
1 parent 6dae6de commit 87792c9

29 files changed

+295
-135
lines changed

Core/PixelEvent.swift

+11-1
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,11 @@ extension Pixel {
385385
case networkProtectionFailureRecoveryCompletedHealthy
386386
case networkProtectionFailureRecoveryCompletedUnhealthy
387387

388+
case networkProtectionWidgetConnectAttempt
389+
case networkProtectionWidgetConnectSuccess
390+
case networkProtectionWidgetDisconnectAttempt
391+
case networkProtectionWidgetDisconnectSuccess
392+
388393
// MARK: remote messaging pixels
389394

390395
case remoteMessageShown
@@ -1370,7 +1375,12 @@ extension Pixel.Event {
13701375
case .networkProtectionFailureRecoveryFailed: return "m_netp_ev_failure_recovery_failed"
13711376
case .networkProtectionFailureRecoveryCompletedHealthy: return "m_netp_ev_failure_recovery_completed_server_healthy"
13721377
case .networkProtectionFailureRecoveryCompletedUnhealthy: return "m_netp_ev_failure_recovery_completed_server_unhealthy"
1373-
1378+
1379+
case .networkProtectionWidgetConnectAttempt: return "m_netp_widget_connect_attempt"
1380+
case .networkProtectionWidgetConnectSuccess: return "m_netp_widget_connect_success"
1381+
case .networkProtectionWidgetDisconnectAttempt: return "m_netp_widget_disconnect_attempt"
1382+
case .networkProtectionWidgetDisconnectSuccess: return "m_netp_widget_disconnect_success"
1383+
13741384
// MARK: Secure Vault
13751385
case .secureVaultL1KeyMigration: return "m_secure-vault_keystore_event_l1-key-migration"
13761386
case .secureVaultL2KeyMigration: return "m_secure-vault_keystore_event_l2-key-migration"

Core/UserDefaults+NetworkProtection.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public extension UserDefaults {
3333

3434
public enum NetworkProtectionUserDefaultKeys {
3535

36-
public static let lastSelectedServer = "com.duckduckgo.network-protection.last-selected-server"
36+
public static let lastSelectedServerCity = "com.duckduckgo.network-protection.last-selected-server-city"
3737

3838
}
3939

DuckDuckGo/AppDelegate.swift

+14-15
Original file line numberDiff line numberDiff line change
@@ -870,10 +870,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
870870

871871
#if NETWORK_PROTECTION
872872
if shortcutItem.type == ShortcutKey.openVPNSettings {
873-
let visibility = DefaultNetworkProtectionVisibility()
874-
if visibility.shouldShowVPNShortcut() {
875-
presentNetworkProtectionStatusSettingsModal()
876-
}
873+
presentNetworkProtectionStatusSettingsModal()
877874
}
878875
#endif
879876

@@ -982,13 +979,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
982979

983980
#if NETWORK_PROTECTION
984981
if NetworkProtectionNotificationIdentifier(rawValue: identifier) != nil {
985-
Task {
986-
let accountManager = AccountManager()
987-
if case .success(let hasEntitlements) = await accountManager.hasEntitlement(for: .networkProtection),
988-
hasEntitlements {
989-
presentNetworkProtectionStatusSettingsModal()
990-
}
991-
}
982+
presentNetworkProtectionStatusSettingsModal()
992983
}
993984

994985
if vpnFeatureVisibility.shouldKeepVPNAccessViaWaitlist(), identifier == VPNWaitlist.notificationIdentifier {
@@ -1009,9 +1000,17 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
10091000
}
10101001

10111002
func presentNetworkProtectionStatusSettingsModal() {
1012-
if #available(iOS 15, *) {
1013-
let networkProtectionRoot = NetworkProtectionRootViewController()
1014-
presentSettings(with: networkProtectionRoot)
1003+
Task {
1004+
let accountManager = AccountManager()
1005+
if case .success(let hasEntitlements) = await accountManager.hasEntitlement(for: .networkProtection),
1006+
hasEntitlements {
1007+
if #available(iOS 15, *) {
1008+
let networkProtectionRoot = NetworkProtectionRootViewController()
1009+
presentSettings(with: networkProtectionRoot)
1010+
}
1011+
} else {
1012+
(window?.rootViewController as? MainViewController)?.segueToPrivacyPro()
1013+
}
10151014
}
10161015
}
10171016
#endif
@@ -1039,7 +1038,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
10391038
rootViewController.segueToSettings()
10401039
let navigationController = rootViewController.presentedViewController as? UINavigationController
10411040
navigationController?.popToRootViewController(animated: false)
1042-
navigationController?.pushViewController(viewController, animated: true)
1041+
navigationController?.pushViewController(viewController, animated: false)
10431042
}
10441043
}
10451044
}

DuckDuckGo/HomeMessage.xcassets/WidgetEducation/WidgetEducationHomeScreen.imageset/Contents.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"images" : [
33
{
4-
"filename" : "iphone.png",
4+
"filename" : "instructions@2x.png",
55
"idiom" : "universal"
66
}
77
],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "[email protected]",
5+
"idiom" : "universal"
6+
},
7+
{
8+
"appearances" : [
9+
{
10+
"appearance" : "luminosity",
11+
"value" : "dark"
12+
}
13+
],
14+
"filename" : "vpn-darkmode_2x.png",
15+
"idiom" : "universal"
16+
}
17+
],
18+
"info" : {
19+
"author" : "xcode",
20+
"version" : 1
21+
}
22+
}

DuckDuckGo/MainViewController+Segues.swift

+15-8
Original file line numberDiff line numberDiff line change
@@ -256,14 +256,21 @@ extension MainViewController {
256256

257257
Pixel.fire(pixel: .settingsPresented,
258258
withAdditionalParameters: PixelExperiment.parameters)
259-
let settingsController = SettingsHostingController(viewModel: settingsViewModel, viewProvider: legacyViewProvider)
260-
261-
// We are still presenting legacy views, so use a Navcontroller
262-
let navController = UINavigationController(rootViewController: settingsController)
263-
settingsController.modalPresentationStyle = UIModalPresentationStyle.automatic
264-
265-
present(navController, animated: true) {
266-
completion?(settingsViewModel)
259+
260+
if let navigationController = self.presentedViewController as? UINavigationController,
261+
let settingsHostingController = navigationController.viewControllers.first as? SettingsHostingController {
262+
navigationController.popToRootViewController(animated: false)
263+
completion?(settingsHostingController.viewModel)
264+
} else {
265+
let settingsController = SettingsHostingController(viewModel: settingsViewModel, viewProvider: legacyViewProvider)
266+
267+
// We are still presenting legacy views, so use a Navcontroller
268+
let navController = UINavigationController(rootViewController: settingsController)
269+
settingsController.modalPresentationStyle = UIModalPresentationStyle.automatic
270+
271+
present(navController, animated: true) {
272+
completion?(settingsViewModel)
273+
}
267274
}
268275
}
269276

DuckDuckGo/NetworkProtectionVPNSettingsView.swift

+23
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ struct NetworkProtectionVPNSettingsView: View {
2929
var body: some View {
3030
VStack {
3131
List {
32+
// Widget only available for iOS 17 and up
33+
if #available(iOS 17.0, *) {
34+
NavigationLink {
35+
WidgetEducationView.vpn
36+
} label: {
37+
Text(UserText.vpnSettingsAddWidget).daxBodyRegular()
38+
}
39+
}
40+
3241
switch viewModel.viewKind {
3342
case .loading: EmptyView()
3443
case .unauthorized: notificationsUnauthorizedView
@@ -127,4 +136,18 @@ struct NetworkProtectionVPNSettingsView: View {
127136

128137
}
129138

139+
private extension WidgetEducationView {
140+
static var vpn: Self {
141+
WidgetEducationView(
142+
navBarTitle: UserText.vpnSettingsAddWidget,
143+
thirdParagraphText: UserText.addVPNWidgetSettingsThirdParagraph,
144+
widgetExampleImageConfig: .init(
145+
image: Image("WidgetEducationVPNWidgetExample"),
146+
maxWidth: 164,
147+
horizontalOffset: -7
148+
)
149+
)
150+
}
151+
}
152+
130153
#endif

DuckDuckGo/SettingsRootView.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ struct SettingsRootView: View {
8282
})
8383

8484
.onReceive(viewModel.$deepLinkTarget.removeDuplicates(), perform: { link in
85-
guard let link, link != self.deepLinkTarget else {
85+
guard let link else {
8686
return
8787
}
8888

DuckDuckGo/UserText.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ public struct UserText {
338338
public static let addWidget = NSLocalizedString("addWidget.button", value: "Add Widget", comment: "")
339339
public static let addWidgetTitle = NSLocalizedString("addWidget.title", value: "One tap to your favorite sites.", comment: "")
340340
public static let addWidgetDescription = NSLocalizedString("addWidget.description", value: "Get quick access to private search and the sites you love.", comment: "")
341-
public static let addWidgetSettingsFirstParagraph = NSLocalizedString("addWidget.settings.firstParagraph", value: "Long-press on the home screen to enter jiggle mode.", comment: "")
341+
public static let addWidgetSettingsFirstParagraph = NSLocalizedString("addWidget.settings.firstParagraph", value: "Long-press on the Home Screen to enter jiggle mode.", comment: "")
342342
public static let addWidgetSettingsSecondParagraph = NSLocalizedString("addWidget.settings.secondParagraph.%@", value: "Tap the plus %@ button.", comment: "Replacement string is a plus button icon.")
343343
public static let addWidgetSettingsThirdParagraph = NSLocalizedString("addWidget.settings.title", value: "Find and select DuckDuckGo. Then choose a widget.", comment: "")
344344

@@ -572,6 +572,11 @@ public struct UserText {
572572
static let vpnAccessRevokedAlertActionSubscribe = NSLocalizedString("vpn.access-revoked.alert.action.subscribe", value: "Subscribe", comment: "Primary action for the alert when the subscription expires")
573573
static let vpnAccessRevokedAlertActionCancel = NSLocalizedString("vpn.access-revoked.alert.action.cancel", value: "Dismiss", comment: "Cancel action for the alert when the subscription expires")
574574

575+
// MARK: VPN Widget
576+
577+
public static let vpnSettingsAddWidget = NSLocalizedString("vpn.settings.add.widget", value: "Add VPN Widget to Home Screen", comment: "VPN settings screen cell text for adding the VPN widget to the home screen")
578+
public static let addVPNWidgetSettingsThirdParagraph = NSLocalizedString("vpn.addWidget.settings.title", value: "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget.", comment: "Title for the VPN widget onboarding screen")
579+
575580
// MARK: Notifications
576581

577582
public static let macWaitlistAvailableNotificationTitle = NSLocalizedString("mac-waitlist.available.notification.title", value: "DuckDuckGo for Mac is ready!", comment: "Title for the macOS waitlist notification")

DuckDuckGo/VPNIntents.swift

+9-4
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,24 @@
1717
// limitations under the License.
1818
//
1919

20-
#if ALPHA
21-
2220
import AppIntents
2321
import NetworkExtension
2422
import WidgetKit
23+
import Core
2524

2625
@available(iOS 17.0, *)
2726
struct DisableVPNIntent: AppIntent {
2827

2928
static let title: LocalizedStringResource = "Disable VPN"
3029
static let description: LocalizedStringResource = "Disables the DuckDuckGo VPN"
3130
static let openAppWhenRun: Bool = false
31+
static let isDiscoverable: Bool = false
3232

3333
@MainActor
3434
func perform() async throws -> some IntentResult {
3535
do {
36+
DailyPixel.fire(pixel: .networkProtectionWidgetDisconnectAttempt)
37+
3638
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
3739
guard let manager = managers.first else {
3840
return .result()
@@ -49,6 +51,7 @@ struct DisableVPNIntent: AppIntent {
4951
try? await Task.sleep(interval: .seconds(0.5))
5052

5153
if manager.connection.status == .disconnected {
54+
DailyPixel.fire(pixel: .networkProtectionWidgetDisconnectSuccess)
5255
return .result()
5356
}
5457

@@ -69,10 +72,13 @@ struct EnableVPNIntent: AppIntent {
6972
static let title: LocalizedStringResource = "Enable VPN"
7073
static let description: LocalizedStringResource = "Enables the DuckDuckGo VPN"
7174
static let openAppWhenRun: Bool = false
75+
static let isDiscoverable: Bool = false
7276

7377
@MainActor
7478
func perform() async throws -> some IntentResult {
7579
do {
80+
DailyPixel.fire(pixel: .networkProtectionWidgetConnectAttempt)
81+
7682
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
7783
guard let manager = managers.first else {
7884
return .result()
@@ -89,6 +95,7 @@ struct EnableVPNIntent: AppIntent {
8995
try? await Task.sleep(interval: .seconds(0.5))
9096

9197
if manager.connection.status == .connected {
98+
DailyPixel.fire(pixel: .networkProtectionWidgetConnectSuccess)
9299
return .result()
93100
}
94101

@@ -102,5 +109,3 @@ struct EnableVPNIntent: AppIntent {
102109
}
103110

104111
}
105-
106-
#endif

DuckDuckGo/WidgetEducationView.swift

+41-12
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,33 @@ extension Font {
2727
}
2828
}
2929

30+
struct WidgetEducationImageConfig {
31+
let image: Image
32+
let maxWidth: CGFloat
33+
let horizontalOffset: CGFloat
34+
35+
init(image: Image, maxWidth: CGFloat, horizontalOffset: CGFloat = 0) {
36+
self.image = image
37+
self.maxWidth = maxWidth
38+
self.horizontalOffset = horizontalOffset
39+
}
40+
}
41+
3042
struct WidgetEducationView: View {
31-
43+
typealias ImageConfig = WidgetEducationImageConfig
44+
45+
let navBarTitle: String
46+
let thirdParagraphText: String
47+
let widgetExampleImageConfig: ImageConfig
48+
49+
init(navBarTitle: String = UserText.settingsAddWidget,
50+
thirdParagraphText: String = UserText.addWidgetSettingsThirdParagraph,
51+
widgetExampleImageConfig: ImageConfig = .init(image: .widgetExample, maxWidth: Const.Size.imageWidth)) {
52+
self.navBarTitle = navBarTitle
53+
self.thirdParagraphText = thirdParagraphText
54+
self.widgetExampleImageConfig = widgetExampleImageConfig
55+
}
56+
3257
var body: some View {
3358
ZStack {
3459
Color.background
@@ -39,15 +64,15 @@ struct WidgetEducationView: View {
3964
text: Text(UserText.addWidgetSettingsFirstParagraph))
4065
NumberedParagraph(number: 2,
4166
text: secondParagraphText,
42-
image: Image.homeScreen)
67+
imageConfig: ImageConfig(image: Image.homeScreen, maxWidth: Const.Size.imageWidth))
4368
NumberedParagraph(number: 3,
44-
text: Text(UserText.addWidgetSettingsThirdParagraph),
45-
image: Image.widgetExample)
69+
text: Text(thirdParagraphText),
70+
imageConfig: widgetExampleImageConfig)
4671
}
4772
.padding(.horizontal)
4873
.padding(.top, Const.Padding.top)
4974
}
50-
}.navigationBarTitle(UserText.settingsAddWidget, displayMode: .inline)
75+
}.navigationBarTitle(navBarTitle, displayMode: .inline)
5176
.onForwardNavigationAppear {
5277
Pixel.fire(pixel: .settingsNextStepsAddWidget,
5378
withAdditionalParameters: PixelExperiment.parameters)
@@ -63,8 +88,8 @@ struct WidgetEducationView: View {
6388
private struct NumberedParagraph: View {
6489
var number: Int
6590
var text: Text
66-
var image: Image?
67-
91+
var imageConfig: WidgetEducationImageConfig?
92+
6893
var body: some View {
6994
HStack(alignment: .firstTextBaseline, spacing: Const.Spacing.numberAndText) {
7095
NumberedCircle(number: number)
@@ -73,10 +98,14 @@ private struct NumberedParagraph: View {
7398
.font(Font(uiFont: Const.Font.text))
7499
.lineSpacing(Const.Spacing.line)
75100
.foregroundColor(Color.font)
76-
image?
77-
.resizable()
78-
.scaledToFit()
79-
.frame(maxWidth: Const.Size.imageWidth)
101+
if let imageConfig {
102+
imageConfig
103+
.image
104+
.resizable()
105+
.scaledToFit()
106+
.frame(maxWidth: imageConfig.maxWidth)
107+
.offset(x: imageConfig.horizontalOffset)
108+
}
80109
}
81110
}
82111
}
@@ -101,7 +130,7 @@ private struct NumberedCircle: View {
101130
private extension Color {
102131
static let background = Color(designSystemColor: .background)
103132
static let font = Color("WidgetEducationFontColor")
104-
static let circle = Color(UIColor.cornflowerBlue)
133+
static let circle = Color(designSystemColor: .accent)
105134
static let numbers = Color.white
106135
}
107136

0 commit comments

Comments
 (0)