Skip to content

Commit

Permalink
VPN snooze mode (#3184)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/72649045549333/1207974416599035/f
Tech Design URL:
CC:

Description:

This PR adds VPN snooze mode.
  • Loading branch information
samsymons authored Aug 12, 2024
1 parent 7cc31e9 commit 73162b4
Show file tree
Hide file tree
Showing 36 changed files with 861 additions and 47 deletions.
1 change: 1 addition & 0 deletions Core/NetworkProtectionNotificationIdentifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ public enum NetworkProtectionNotificationIdentifier: String {
case superseded = "network-protection.notification.superseded"
case test = "network-protection.notification.test"
case entitlement = "network-protection.notification.entitlement"
case snoozeEnded = "network-protection.notification.snooze-ended"
}
8 changes: 8 additions & 0 deletions Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,10 @@ extension Pixel {
case networkProtectionGeoswitchingSetCustom
case networkProtectionGeoswitchingNoLocations

case networkProtectionSnoozeEnabledFromStatusMenu
case networkProtectionSnoozeDisabledFromStatusMenu
case networkProtectionSnoozeDisabledFromLiveActivity

case networkProtectionFailureRecoveryStarted
case networkProtectionFailureRecoveryFailed
case networkProtectionFailureRecoveryCompletedHealthy
Expand Down Expand Up @@ -1150,6 +1154,10 @@ extension Pixel.Event {
case .networkProtectionGeoswitchingSetCustom: return "m_netp_ev_geoswitching_set_custom"
case .networkProtectionGeoswitchingNoLocations: return "m_netp_ev_geoswitching_no_locations"

case .networkProtectionSnoozeEnabledFromStatusMenu: return "m_netp_snooze_enabled_status_menu"
case .networkProtectionSnoozeDisabledFromStatusMenu: return "m_netp_snooze_disabled_status_menu"
case .networkProtectionSnoozeDisabledFromLiveActivity: return "m_netp_snooze_disabled_live_activity"

case .networkProtectionClientFailedToFetchServerStatus: return "m_netp_server_migration_failed_to_fetch_status"
case .networkProtectionClientFailedToParseServerStatusResponse: return "m_netp_server_migration_failed_to_parse_response"

Expand Down
28 changes: 27 additions & 1 deletion DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@
4B0295192537BC6700E00CEF /* ConfigurationDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0295182537BC6700E00CEF /* ConfigurationDebugViewController.swift */; };
4B0F3F502B9BFF2100392892 /* NetworkProtectionFAQView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F3F4F2B9BFF2100392892 /* NetworkProtectionFAQView.swift */; };
4B274F602AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B274F5F2AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift */; };
4B2C79612C5B27AC00A240CC /* VPNSnoozeActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD96E082C4DCDD2003BC32C /* VPNSnoozeActivityAttributes.swift */; };
4B37E0502B928CA6009E81CA /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B37E04F2B928CA6009E81CA /* vpn-light-mode.json */; };
4B412ACC2BBB3D0900A39F5E /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B412ACB2BBB3D0900A39F5E /* LazyView.swift */; };
4B45D85C2BE0B115006061B5 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 4B45D85B2BE0B115006061B5 /* Subscription */; };
Expand All @@ -229,6 +230,11 @@
4BCBE45E2BA7E81F00FC75A1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4BCBE45D2BA7E81F00FC75A1 /* PrivacyInfo.xcprivacy */; };
4BCBE4602BA7E87100FC75A1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4BCBE45F2BA7E87100FC75A1 /* PrivacyInfo.xcprivacy */; };
4BCD14672B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCD14662B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift */; };
4BD96E062C4DBC93003BC32C /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02025663298818B100E694E7 /* NetworkExtension.framework */; };
4BD96E0B2C4DCE55003BC32C /* VPNSnoozeLiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD96E0A2C4DCE55003BC32C /* VPNSnoozeLiveActivityManager.swift */; };
4BD96E0E2C4DCFD7003BC32C /* VPNSnoozeLiveActivityWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD96E0C2C4DCEAA003BC32C /* VPNSnoozeLiveActivityWidget.swift */; };
4BD96E0F2C4DCFEB003BC32C /* VPNSnoozeActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD96E082C4DCDD2003BC32C /* VPNSnoozeActivityAttributes.swift */; };
4BD96E102C4DF329003BC32C /* VPNSnoozeLiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD96E0A2C4DCE55003BC32C /* VPNSnoozeLiveActivityManager.swift */; };
4BE2756827304F57006B20B0 /* URLRequestExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE27566272F878F006B20B0 /* URLRequestExtension.swift */; };
4BE67B012B96B741007335F7 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 4BE67B002B96B741007335F7 /* Common */; };
4BE67B032B96B864007335F7 /* ContentBlocking in Frameworks */ = {isa = PBXBuildFile; productRef = 4BE67B022B96B864007335F7 /* ContentBlocking */; };
Expand Down Expand Up @@ -1439,6 +1445,9 @@
4BCBE45D2BA7E81F00FC75A1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
4BCBE45F2BA7E87100FC75A1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
4BCD14662B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionTermsAndConditionsStore.swift; sourceTree = "<group>"; };
4BD96E082C4DCDD2003BC32C /* VPNSnoozeActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSnoozeActivityAttributes.swift; sourceTree = "<group>"; };
4BD96E0A2C4DCE55003BC32C /* VPNSnoozeLiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSnoozeLiveActivityManager.swift; sourceTree = "<group>"; };
4BD96E0C2C4DCEAA003BC32C /* VPNSnoozeLiveActivityWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSnoozeLiveActivityWidget.swift; sourceTree = "<group>"; };
4BE27566272F878F006B20B0 /* URLRequestExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = URLRequestExtension.swift; path = ../DuckDuckGo/URLRequestExtension.swift; sourceTree = "<group>"; };
4BF3E4AE2C06A85200ED5D57 /* VPNRedditSessionWorkaround.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNRedditSessionWorkaround.swift; sourceTree = "<group>"; };
560E990E2BEE2CB800507CE0 /* SyncErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncErrorMessage.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2923,6 +2932,7 @@
buildActionMask = 2147483647;
files = (
8512EA5124ED30D20073EE19 /* SwiftUI.framework in Frameworks */,
4BD96E062C4DBC93003BC32C /* NetworkExtension.framework in Frameworks */,
85DF714624F7FE6100C89288 /* Core.framework in Frameworks */,
8512EA4F24ED30D20073EE19 /* WidgetKit.framework in Frameworks */,
4BBBBA872B02E85400D965DA /* DesignResourcesKit in Frameworks */,
Expand Down Expand Up @@ -3554,6 +3564,16 @@
name = VPN;
sourceTree = "<group>";
};
4BD96E072C4DCCD1003BC32C /* LiveActivity */ = {
isa = PBXGroup;
children = (
4BD96E082C4DCDD2003BC32C /* VPNSnoozeActivityAttributes.swift */,
4BD96E0A2C4DCE55003BC32C /* VPNSnoozeLiveActivityManager.swift */,
4BD96E0C2C4DCEAA003BC32C /* VPNSnoozeLiveActivityWidget.swift */,
);
name = LiveActivity;
sourceTree = "<group>";
};
566B736E2BECD3DC00FF1959 /* Utilities */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -5200,6 +5220,7 @@
EECD94B22A28B8580085C66E /* NetworkProtection */ = {
isa = PBXGroup;
children = (
4BD96E072C4DCCD1003BC32C /* LiveActivity */,
4B37E04E2B928C91009E81CA /* Resources */,
EE01EB412AFC1DE10096AAC9 /* PreferredLocation */,
EE9D68CF2AE00CE000B55EF4 /* VPNSettings */,
Expand Down Expand Up @@ -7022,6 +7043,7 @@
98D98A7425ED88D100D8E3DF /* BrowsingMenuEntryViewCell.swift in Sources */,
F1564F032B7B915F00D454A6 /* AppDelegate+SKAD4.swift in Sources */,
98F3A1D8217B37010011A0D4 /* Theme.swift in Sources */,
4B2C79612C5B27AC00A240CC /* VPNSnoozeActivityAttributes.swift in Sources */,
CB9B873C278C8FEA001F4906 /* WidgetEducationView.swift in Sources */,
85F200002215C17B006BB258 /* FindInPage.swift in Sources */,
F1386BA41E6846C40062FC3C /* TabDelegate.swift in Sources */,
Expand Down Expand Up @@ -7189,6 +7211,7 @@
D62EC3C22C248AF800FC9D04 /* DuckNavigationHandling.swift in Sources */,
9FB027142C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift in Sources */,
D664C7B62B289AA200CBFA76 /* SubscriptionFlowViewModel.swift in Sources */,
4BD96E0B2C4DCE55003BC32C /* VPNSnoozeLiveActivityManager.swift in Sources */,
1EFDCBC127D2393C00916BC5 /* DownloadsDeleteHelper.swift in Sources */,
C1836CE12C359EC90016D057 /* AutofillBreakageReportCellContentView.swift in Sources */,
6F9FFE282C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift in Sources */,
Expand Down Expand Up @@ -7658,14 +7681,17 @@
buildActionMask = 2147483647;
files = (
853273AE24FEF49600E3C778 /* ColorExtension.swift in Sources */,
4BD96E0F2C4DCFEB003BC32C /* VPNSnoozeActivityAttributes.swift in Sources */,
373608932ABB432600629E7F /* FavoritesDisplayMode+UserDefaults.swift in Sources */,
4BD96E102C4DF329003BC32C /* VPNSnoozeLiveActivityManager.swift in Sources */,
853273B324FF114700E3C778 /* DeepLinks.swift in Sources */,
853273B424FFB36100E3C778 /* UIColorExtension.swift in Sources */,
853273AB24FEF27500E3C778 /* WidgetViews.swift in Sources */,
4B5C462B2AF2BDC4002A4432 /* VPNIntents.swift in Sources */,
4BB7CBB02AF59C310014A35F /* VPNWidget.swift in Sources */,
8512EA5424ED30D20073EE19 /* Widgets.swift in Sources */,
85DB12EB2A1FE2A4000A4A72 /* LockScreenWidgets.swift in Sources */,
4BD96E0E2C4DCFD7003BC32C /* VPNSnoozeLiveActivityWidget.swift in Sources */,
8544C37C250B827300A0FE73 /* UserText.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -10481,7 +10507,7 @@
repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit";
requirement = {
kind = exactVersion;
version = 182.0.0;
version = 183.0.0;
};
};
9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/DuckDuckGo/BrowserServicesKit",
"state" : {
"revision" : "d67979814bdd3c4c43a38e6694c56f1fdb5969ac",
"version" : "182.0.0"
"revision" : "c3ae1865ba36ebbcb5451a836424213ea875d135",
"version" : "183.0.0"
}
},
{
Expand Down
4 changes: 4 additions & 0 deletions DuckDuckGo/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,10 @@ import WebKit
await stopAndRemoveVPNIfNotAuthenticated()
await refreshShortcuts()
await vpnWorkaround.installRedditSessionWorkaround()

if #available(iOS 17.0, *) {
await VPNSnoozeLiveActivityManager().endSnoozeActivityIfNecessary()
}
}

AppDependencyProvider.shared.subscriptionManager.refreshCachedSubscriptionAndEntitlements { isSubscriptionActive in
Expand Down
2 changes: 2 additions & 0 deletions DuckDuckGo/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@
<true/>
<key>SUBSCRIPTION_APP_GROUP</key>
<string>$(AppIdentifierPrefix)$(SUBSCRIPTION_APP_GROUP)</string>
<key>NSSupportsLiveActivities</key>
<true/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>UISupportedInterfaceOrientations~ipad</key>
Expand Down
31 changes: 24 additions & 7 deletions DuckDuckGo/LottieView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,24 @@ struct LottieView: UIViewRepresentable {
case withIntro(LoopWithIntroTiming)
}

let lottieFile: String
let delay: TimeInterval
var isAnimating: Binding<Bool>
private let loopMode: LoopMode

let animationName: String
let animation: LottieAnimation?
let animationView = LottieAnimationView()

init(lottieFile: String, delay: TimeInterval = 0, loopMode: LoopMode = .mode(.playOnce), isAnimating: Binding<Bool> = .constant(true)) {
self.lottieFile = lottieFile
self.animationName = lottieFile
self.animation = LottieAnimation.named(lottieFile)
self.delay = delay
self.isAnimating = isAnimating
self.loopMode = loopMode
}

func makeUIView(context: Context) -> some LottieAnimationView {
animationView.animation = LottieAnimation.named(lottieFile)
animationView.animation = animation
animationView.contentMode = .scaleAspectFit
animationView.clipsToBounds = false

Expand All @@ -68,10 +70,25 @@ struct LottieView: UIViewRepresentable {
return
}

guard isAnimating.wrappedValue, !uiView.isAnimationPlaying else { return }

if uiView.loopMode == .playOnce && uiView.currentProgress == 1 { return }

// If the view is not animating and the progress is 0, apply an animation-specific hack.
// The VPN startup animations have an issue with the initial frame that is introduced when backgrounding and foregrounding the app.
// The issue can be reproduced using the official Lottie SwiftUI wrapped, so instead it is being worked around by resetting the animation
// when appropriate.
if !isAnimating.wrappedValue, uiView.currentProgress == 0 {
if uiView.currentFrame == 0, self.animationName.hasPrefix("vpn-") {
uiView.animation = nil
uiView.animation = self.animation
}
}

guard isAnimating.wrappedValue, !uiView.isAnimationPlaying else {
return
}

if uiView.loopMode == .playOnce && uiView.currentProgress == 1 {
return
}

DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
switch loopMode {
case .mode:
Expand Down
1 change: 1 addition & 0 deletions DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ private class DefaultTunnelSessionProvider: TunnelSessionProvider {
extension ConnectionStatusObserverThroughSession {
convenience init() {
self.init(tunnelSessionProvider: DefaultTunnelSessionProvider(),
platformSnoozeTimingStore: NetworkProtectionSnoozeTimingStore(userDefaults: .networkProtectionGroupDefaults),
platformNotificationCenter: .default,
platformDidWakeNotification: UIApplication.didBecomeActiveNotification)
}
Expand Down
10 changes: 10 additions & 0 deletions DuckDuckGo/NetworkProtectionDebugUtilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ final class NetworkProtectionDebugUtilities {
}
try? await activeSession.sendProviderMessage(message)
}

// MARK: - Snooze

func startSnooze(duration: TimeInterval) async {
guard let activeSession = await AppDependencyProvider.shared.networkProtectionTunnelController.activeSession() else {
return
}

try? await activeSession.sendProviderMessage(.startSnooze(duration))
}
}

private extension NetworkProtectionSimulationOption {
Expand Down
7 changes: 7 additions & 0 deletions DuckDuckGo/NetworkProtectionDebugViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ final class NetworkProtectionDebugViewController: UITableViewController {
case shutDown
case showEntitlementMessaging
case resetEntitlementMessaging
case startSnooze
}

enum NetworkPathRows: Int, CaseIterable {
Expand Down Expand Up @@ -372,6 +373,8 @@ final class NetworkProtectionDebugViewController: UITableViewController {
cell.textLabel?.text = "Show Entitlement Messaging"
case .resetEntitlementMessaging:
cell.textLabel?.text = "Reset Entitlement Messaging"
case .startSnooze:
cell.textLabel?.text = "Snooze For 30 Seconds"
case .none:
break
}
Expand All @@ -391,6 +394,10 @@ final class NetworkProtectionDebugViewController: UITableViewController {
UserDefaults.networkProtectionGroupDefaults.enableEntitlementMessaging()
case .resetEntitlementMessaging:
UserDefaults.networkProtectionGroupDefaults.resetEntitlementMessaging()
case .startSnooze:
Task {
await NetworkProtectionDebugUtilities().startSnooze(duration: .seconds(30))
}
case .none:
break
}
Expand Down
Loading

0 comments on commit 73162b4

Please sign in to comment.