Skip to content

Commit

Permalink
Adds VPN Control Center widget and Siri Commands. (#3414)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1203108348835387/1208485618883319/f

DRK PR: duckduckgo/DesignResourcesKit#9

## Description

Adds a VPN control widget for the control center and lock screen.
Adds Siri commands to control the VPN with voice.
  • Loading branch information
diegoreymendez authored Dec 20, 2024
1 parent b5dac56 commit 805a41e
Show file tree
Hide file tree
Showing 62 changed files with 1,978 additions and 299 deletions.
52 changes: 50 additions & 2 deletions Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -483,8 +483,32 @@ extension Pixel {

case networkProtectionWidgetConnectAttempt
case networkProtectionWidgetConnectSuccess
case networkProtectionWidgetConnectCancelled
case networkProtectionWidgetConnectFailure
case networkProtectionWidgetDisconnectAttempt
case networkProtectionWidgetDisconnectSuccess
case networkProtectionWidgetDisconnectCancelled
case networkProtectionWidgetDisconnectFailure

case vpnControlCenterConnectAttempt
case vpnControlCenterConnectSuccess
case vpnControlCenterConnectCancelled
case vpnControlCenterConnectFailure

case vpnControlCenterDisconnectAttempt
case vpnControlCenterDisconnectSuccess
case vpnControlCenterDisconnectCancelled
case vpnControlCenterDisconnectFailure

case vpnShortcutConnectAttempt
case vpnShortcutConnectSuccess
case vpnShortcutConnectCancelled
case vpnShortcutConnectFailure

case vpnShortcutDisconnectAttempt
case vpnShortcutDisconnectSuccess
case vpnShortcutDisconnectCancelled
case vpnShortcutDisconnectFailure

case networkProtectionDNSUpdateCustom
case networkProtectionDNSUpdateDefault
Expand Down Expand Up @@ -1455,9 +1479,9 @@ extension Pixel.Event {
case .remoteMessagePrimaryActionClicked: return "m_remote_message_primary_action_clicked"
case .remoteMessageSecondaryActionClicked: return "m_remote_message_secondary_action_clicked"
case .remoteMessageSheet: return "m_remote_message_sheet"

// MARK: debug pixels

case .dbCrashDetected: return "m_d_crash"
case .dbCrashDetectedDaily: return "m_d_crash_daily"
case .crashReportCRCIDMissing: return "m_crashreporting_crcid-missing"
Expand Down Expand Up @@ -1761,8 +1785,32 @@ extension Pixel.Event {

case .networkProtectionWidgetConnectAttempt: return "m_netp_widget_connect_attempt"
case .networkProtectionWidgetConnectSuccess: return "m_netp_widget_connect_success"
case .networkProtectionWidgetConnectCancelled: return "m_netp_widget_connect_cancelled"
case .networkProtectionWidgetConnectFailure: return "m_netp_widget_connect_failure"
case .networkProtectionWidgetDisconnectAttempt: return "m_netp_widget_disconnect_attempt"
case .networkProtectionWidgetDisconnectSuccess: return "m_netp_widget_disconnect_success"
case .networkProtectionWidgetDisconnectCancelled: return "m_netp_widget_disconnect_cancelled"
case .networkProtectionWidgetDisconnectFailure: return "m_netp_widget_disconnect_failure"

case .vpnControlCenterConnectAttempt: return "m_vpn_control-center_connect_attempt"
case .vpnControlCenterConnectSuccess: return "m_vpn_control-center_connect_success"
case .vpnControlCenterConnectCancelled: return "m_vpn_control-center_connect_cancelled"
case .vpnControlCenterConnectFailure: return "m_vpn_control-center_connect_failure"

case .vpnControlCenterDisconnectAttempt: return "m_vpn_control-center_disconnect_attempt"
case .vpnControlCenterDisconnectSuccess: return "m_vpn_control-center_disconnect_success"
case .vpnControlCenterDisconnectCancelled: return "m_vpn_control-center_disconnect_cancelled"
case .vpnControlCenterDisconnectFailure: return "m_vpn_control-center_disconnect_failure"

case .vpnShortcutConnectAttempt: return "m_vpn_shortcut_connect_attempt"
case .vpnShortcutConnectSuccess: return "m_vpn_shortcut_connect_success"
case .vpnShortcutConnectCancelled: return "m_vpn_shortcut_connect_cancelled"
case .vpnShortcutConnectFailure: return "m_vpn_shortcut_connect_failure"

case .vpnShortcutDisconnectAttempt: return "m_vpn_shortcut_disconnect_attempt"
case .vpnShortcutDisconnectSuccess: return "m_vpn_shortcut_disconnect_success"
case .vpnShortcutDisconnectCancelled: return "m_vpn_shortuct_disconnect_cancelled"
case .vpnShortcutDisconnectFailure: return "m_vpn_shortcut_disconnect_failure"

// MARK: Secure Vault
case .secureVaultL1KeyMigration: return "m_secure-vault_keystore_event_l1-key-migration"
Expand Down
158 changes: 132 additions & 26 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/DesignResourcesKit",
"state" : {
"revision" : "ad133f76501edcb2bfa841e33aebc0da5f92bb5c",
"version" : "3.3.0"
"revision" : "a35414a0b07da7fb79255370edfe845b7a22558c",
"version" : "3.3.1"
}
},
{
Expand Down
2 changes: 2 additions & 0 deletions DuckDuckGo/DuckDuckGo.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.developer.siri</key>
<true/>
<key>com.apple.developer.web-browser</key>
<true/>
<key>com.apple.security.application-groups</key>
Expand Down
2 changes: 2 additions & 0 deletions DuckDuckGo/DuckDuckGoAlpha.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.developer.siri</key>
<true/>
<key>com.apple.developer.web-browser</key>
<true/>
<key>com.apple.security.application-groups</key>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Frame 624697 1.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "[email protected]",
"filename" : "Frame 624697.pdf",
"idiom" : "universal"
}
],
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "[email protected]",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
File renamed without changes
5 changes: 4 additions & 1 deletion DuckDuckGo/NetworkProtectionStatusView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ struct NetworkProtectionStatusView: View {
})
.applyInsetGroupedListStyle()
.sheet(isPresented: $statusModel.showAddWidgetEducationView) {
widgetEducationSheet()
if #available(iOS 17.0, *) {
widgetEducationSheet()
}
}
.onAppear {
if #available(iOS 18.0, *) {
Expand Down Expand Up @@ -413,6 +415,7 @@ struct NetworkProtectionStatusView: View {

// MARK: - Sheets

@available(iOS 17.0, *)
private func widgetEducationSheet() -> some View {
NavigationView {
WidgetEducationView()
Expand Down
2 changes: 1 addition & 1 deletion DuckDuckGo/NetworkProtectionStatusViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ final class NetworkProtectionStatusViewModel: ObservableObject {
await disableNetP()
}

WidgetCenter.shared.reloadTimelines(ofKind: "VPNStatusWidget")
VPNReloadStatusWidgets()
}

@MainActor
Expand Down
73 changes: 57 additions & 16 deletions DuckDuckGo/NetworkProtectionVPNSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,14 @@ struct NetworkProtectionVPNSettingsView: View {
var body: some View {
VStack {
List {
// Widget only available for iOS 17 and up
if #available(iOS 17.0, *) {
Section {
NavigationLink {
WidgetEducationView.vpn
} label: {
Text(UserText.vpnSettingsAddWidget).daxBodyRegular()
}
}
.listRowBackground(Color(designSystemColor: .surface))
}

switch viewModel.viewKind {
case .loading: EmptyView()
case .unauthorized: notificationsUnauthorizedView
case .authorized: notificationAuthorizedView
}

shortcutsView

toggleSection(
text: UserText.netPExcludeLocalNetworksSettingTitle,
headerText: UserText.netPExcludeLocalNetworksSettingHeader,
Expand Down Expand Up @@ -156,17 +146,68 @@ struct NetworkProtectionVPNSettingsView: View {
.listRowBackground(Color(designSystemColor: .surface))
}

@ViewBuilder
private var shortcutsView: some View {
// Widget only available for iOS 17 and up
if #available(iOS 17.0, *) {
Section {
NavigationLink {
WidgetEducationView.vpn
} label: {
Label {
Text(UserText.vpnSettingsAddWidget)
} icon: {
Image(.addWidgetColor24)
.frame(width: 24, height: 24)
}.daxBodyRegular()
}

#if ALPHA || DEBUG
if #available(iOS 18.0, *) {
NavigationLink {
ControlCenterWidgetEducationView(navBarTitle: "Add DuckDuckGo VPN Shortcut to Your Control Center",
widget: .vpnToggle)
} label: {
Label {
Text(UserText.vpnSettingsAddControlCenterWidget)
} icon: {
Image(.settingsColor24)
.frame(width: 24, height: 24)
}.daxBodyRegular()
}
}

NavigationLink {
SiriEducationView()
} label: {
Label {
Text(UserText.vpnSettingsControlWithSiri)
} icon: {
Image(.askSiriColor24)
.frame(width: 24, height: 24)
}.daxBodyRegular()
}
#endif
} header: {
Text(UserText.netPVPNShortcutsSectionHeader)
}
.listRowBackground(Color(designSystemColor: .surface))
}
}
}

@available(iOS 17.0, *)
private extension WidgetEducationView {

static var vpn: Self {
WidgetEducationView(
navBarTitle: UserText.vpnSettingsAddWidget,
navBarTitle: UserText.settingsAddVPNWidget,
thirdParagraphText: UserText.addVPNWidgetSettingsThirdParagraph,
widgetExampleImageConfig: .init(
image: Image("WidgetEducationVPNWidgetExample"),
thirdParagraphDetail: .image(
Image("WidgetEducationVPNWidgetExample"),
maxWidth: 164,
horizontalOffset: -7
horizontalOffset: -7,
dropsShadow: true
)
)
}
Expand Down
2 changes: 1 addition & 1 deletion DuckDuckGo/NetworkProtectionWidgetRefreshModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class NetworkProtectionWidgetRefreshModel {
}

public func refreshVPNWidget() {
WidgetCenter.shared.reloadTimelines(ofKind: "VPNStatusWidget")
VPNReloadStatusWidgets()
}

}
76 changes: 76 additions & 0 deletions DuckDuckGo/Siri/SiriBubbleView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//
// SiriBubbleView.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 DesignResourcesKit
import SwiftUICore

private struct SiriBubble: Shape {

static let tipHeight: CGFloat = 7

func path(in rect: CGRect) -> Path {
var path = Path()
let cornerRadius: CGFloat = 21
let tipHeight: CGFloat = 7
let tipWidth: CGFloat = 9

// Rounded rectangle portion
let roundedRect = CGRect(
x: rect.minX,
y: rect.minY,
width: rect.width,
height: rect.height - Self.tipHeight
)
path.addRoundedRect(in: roundedRect, cornerSize: CGSize(width: cornerRadius, height: cornerRadius))

// Triangle tip drawn out of bounds
let tipStartX = rect.maxX - (cornerRadius + 2 * tipWidth)
let tipBaseY = rect.maxY - Self.tipHeight

path.move(to: CGPoint(x: tipStartX, y: tipBaseY)) // Bottom-right corner of rounded rectangle
path.addLine(to: CGPoint(x: tipStartX + tipWidth, y: tipBaseY)) // Tip top-right
path.addLine(to: CGPoint(x: tipStartX, y: tipBaseY + tipHeight)) // Tip bottom-right (out of bounds)
path.closeSubpath()

return path
}
}

struct SiriBubbleView: View {

let text: String

init(_ text: String) {
self.text = text
}

var body: some View {
HStack(alignment: .center) {
Text(text)
.foregroundStyle(Color(designSystemColor: .textPrimary))
.multilineTextAlignment(.center)
}.padding(12)
.padding(.bottom, SiriBubble.tipHeight)
.frame(maxWidth: .infinity)
.background(SiriBubble()
.fill(Color(designSystemColor: .surface))
.shadow(color: .black.opacity(0.08), radius: 8, x: 0, y: 8)
.shadow(color: .black.opacity(0.1), radius: 3, x: 0, y: 2))
}
}
6 changes: 6 additions & 0 deletions DuckDuckGo/Siri/SiriEducation.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
6 changes: 6 additions & 0 deletions DuckDuckGo/Siri/SiriEducation.xcassets/Images/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Siri-Control-128.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.
Loading

0 comments on commit 805a41e

Please sign in to comment.