Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds pixels to VPN tips #3629

Merged
merged 2 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,23 @@ extension Pixel {

case networkProtectionMalformedErrorDetected

// MARK: - VPN Tips

case networkProtectionGeoswitchingTipShown
case networkProtectionGeoswitchingTipActioned
case networkProtectionGeoswitchingTipDismissed
case networkProtectionGeoswitchingTipIgnored

case networkProtectionSnoozeTipShown
case networkProtectionSnoozeTipActioned
case networkProtectionSnoozeTipDismissed
case networkProtectionSnoozeTipIgnored

case networkProtectionWidgetTipShown
case networkProtectionWidgetTipActioned
case networkProtectionWidgetTipDismissed
case networkProtectionWidgetTipIgnored
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New pixels


// MARK: remote messaging pixels

case remoteMessageShown
Expand Down Expand Up @@ -1317,6 +1334,23 @@ extension Pixel.Event {

case .networkProtectionMalformedErrorDetected: return "m_netp_vpn_malformed_error_detected"

// MARK: VPN tips

case .networkProtectionGeoswitchingTipShown: return "m_vpn_tip_geoswitching_shown"
case .networkProtectionGeoswitchingTipActioned: return "m_vpn_tip_geoswitching_actioned"
case .networkProtectionGeoswitchingTipDismissed: return "m_vpn_tip_geoswitching_dismissed"
case .networkProtectionGeoswitchingTipIgnored: return "m_vpn_tip_geoswitching_ignored"

case .networkProtectionSnoozeTipShown: return "m_vpn_tip_snooze_shown"
case .networkProtectionSnoozeTipActioned: return "m_vpn_tip_snooze_actioned"
case .networkProtectionSnoozeTipDismissed: return "m_vpn_tip_snooze_dismissed"
case .networkProtectionSnoozeTipIgnored: return "m_vpn_tip_snooze_ignored"

case .networkProtectionWidgetTipShown: return "m_vpn_tip_widget_shown"
case .networkProtectionWidgetTipActioned: return "m_vpn_tip_widget_actioned"
case .networkProtectionWidgetTipDismissed: return "m_vpn_tip_widget_dismissed"
case .networkProtectionWidgetTipIgnored: return "m_vpn_tip_widget_ignored"

// MARK: remote messaging pixels

case .remoteMessageShown: return "m_remote_message_shown"
Expand Down
15 changes: 9 additions & 6 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -365,12 +365,13 @@
6FEC0B852C999352006B4F6E /* FavoriteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FEC0B842C999352006B4F6E /* FavoriteItem.swift */; };
6FEC0B882C999961006B4F6E /* FavoritesListInteractingAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FEC0B872C999961006B4F6E /* FavoritesListInteractingAdapter.swift */; };
6FF915822B88E07A0042AC87 /* AdAttributionFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */; };
6FF9AD452CE766F700C5A406 /* NewTabPageControllerPixelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF9AD442CE766F700C5A406 /* NewTabPageControllerPixelTests.swift */; };
6FF9AD3F2CE63DD800C5A406 /* TabSwitcherOpenDailyPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF9AD3E2CE63DC200C5A406 /* TabSwitcherOpenDailyPixel.swift */; };
6FF9AD412CE6610F00C5A406 /* TabSwitcherDailyPixelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF9AD402CE6610600C5A406 /* TabSwitcherDailyPixelTests.swift */; };
6FF9AD452CE766F700C5A406 /* NewTabPageControllerPixelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF9AD442CE766F700C5A406 /* NewTabPageControllerPixelTests.swift */; };
7B1604E82CB685B400A44EC6 /* Logger+TipKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1604E72CB685B400A44EC6 /* Logger+TipKit.swift */; };
7B1604EC2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1604EB2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift */; };
7B1604EE2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1604ED2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift */; };
7B1C892C2CF714AA0008224E /* VPNTipsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1C892B2CF714AA0008224E /* VPNTipsModel.swift */; };
7B8E0EC62CC81B4900B2B722 /* TipKitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8E0EC52CC81B4800B2B722 /* TipKitController.swift */; };
7BC571202BDBB877003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; };
7BC571212BDBB977003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; };
Expand Down Expand Up @@ -470,7 +471,6 @@
853A717820F645FB00FE60BC /* PixelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853A717720F645FB00FE60BC /* PixelTests.swift */; };
853C5F6121C277C7001F7A05 /* global.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853C5F6021C277C7001F7A05 /* global.swift */; };
8540BBA22440857A00017FE4 /* FireproofingWorking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540BBA12440857A00017FE4 /* FireproofingWorking.swift */; };
8540BD5223D8C2220057FDD2 /* UserDefaultsFireproofingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540BD5123D8C2220057FDD2 /* UserDefaultsFireproofingTests.swift */; };
8540BD5423D8D5080057FDD2 /* FireproofingAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540BD5323D8D5080057FDD2 /* FireproofingAlert.swift */; };
8540BD5623D9E9C20057FDD2 /* FireproofingSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540BD5523D9E9C20057FDD2 /* FireproofingSettingsViewController.swift */; };
85449EF523FDA02800512AAF /* KeyboardSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85449EF423FDA02800512AAF /* KeyboardSettingsViewController.swift */; };
Expand Down Expand Up @@ -651,7 +651,7 @@
98424AAD2CED4FF10071C7DB /* WKWebViewConfigurationExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F198D7971E3A45D90088DA8A /* WKWebViewConfigurationExtensionTests.swift */; };
98424AAE2CED4FF10071C7DB /* WebCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850559D123CF710C0055C0D5 /* WebCacheManagerTests.swift */; };
98424AAF2CED4FF10071C7DB /* UserAgentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 834DF990248FDDF60075EA48 /* UserAgentTests.swift */; };
98424AB02CED4FF10071C7DB /* PreserveLoginsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540BD5123D8C2220057FDD2 /* PreserveLoginsTests.swift */; };
98424AB02CED4FF10071C7DB /* UserDefaultsFireproofingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540BD5123D8C2220057FDD2 /* UserDefaultsFireproofingTests.swift */; };
98424AB22CEDD6150071C7DB /* BrowserServicesKit in Frameworks */ = {isa = PBXBuildFile; productRef = 98424AB12CEDD6150071C7DB /* BrowserServicesKit */; };
98424AB42CEDD61C0071C7DB /* BrowserServicesKitTestsUtils in Frameworks */ = {isa = PBXBuildFile; productRef = 98424AB32CEDD61C0071C7DB /* BrowserServicesKitTestsUtils */; };
9847C00027A2DDBB00DB07AA /* AppPrivacyConfigurationDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9847BFFF27A2DDBB00DB07AA /* AppPrivacyConfigurationDataProvider.swift */; };
Expand Down Expand Up @@ -1698,12 +1698,13 @@
6FEC0B842C999352006B4F6E /* FavoriteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteItem.swift; sourceTree = "<group>"; };
6FEC0B872C999961006B4F6E /* FavoritesListInteractingAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesListInteractingAdapter.swift; sourceTree = "<group>"; };
6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAttributionFetcherTests.swift; sourceTree = "<group>"; };
6FF9AD442CE766F700C5A406 /* NewTabPageControllerPixelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageControllerPixelTests.swift; sourceTree = "<group>"; };
6FF9AD3E2CE63DC200C5A406 /* TabSwitcherOpenDailyPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSwitcherOpenDailyPixel.swift; sourceTree = "<group>"; };
6FF9AD402CE6610600C5A406 /* TabSwitcherDailyPixelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSwitcherDailyPixelTests.swift; sourceTree = "<group>"; };
6FF9AD442CE766F700C5A406 /* NewTabPageControllerPixelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageControllerPixelTests.swift; sourceTree = "<group>"; };
7B1604E72CB685B400A44EC6 /* Logger+TipKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+TipKit.swift"; sourceTree = "<group>"; };
7B1604EB2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TipKitController+ConvenienceInitializers.swift"; sourceTree = "<group>"; };
7B1604ED2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipKitDebugOptionsUIActionHandling.swift; sourceTree = "<group>"; };
7B1C892B2CF714AA0008224E /* VPNTipsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNTipsModel.swift; sourceTree = "<group>"; };
7B8E0EC52CC81B4800B2B722 /* TipKitController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipKitController.swift; sourceTree = "<group>"; };
7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNActivationDateStore.swift; sourceTree = "<group>"; };
7BDBAD0D2CBFB3F1000379B7 /* VPN.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = VPN.xcassets; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4785,7 +4786,7 @@
isa = PBXGroup;
children = (
834DF990248FDDF60075EA48 /* UserAgentTests.swift */,
8540BD5123D8C2220057FDD2 /* PreserveLoginsTests.swift */,
8540BD5123D8C2220057FDD2 /* UserDefaultsFireproofingTests.swift */,
850559D123CF710C0055C0D5 /* WebCacheManagerTests.swift */,
981C49AF2C8FA61D00DF11E8 /* DataStoreIdManagerTests.swift */,
F198D7971E3A45D90088DA8A /* WKWebViewConfigurationExtensionTests.swift */,
Expand Down Expand Up @@ -5729,6 +5730,7 @@
children = (
EE4FB1852A28CE7200E5CBA7 /* NetworkProtectionStatusView.swift */,
EE4FB1872A28D11900E5CBA7 /* NetworkProtectionStatusViewModel.swift */,
7B1C892B2CF714AA0008224E /* VPNTipsModel.swift */,
);
name = Status;
sourceTree = "<group>";
Expand Down Expand Up @@ -7966,6 +7968,7 @@
1E908BF129827C480008C8F3 /* AutoconsentUserScript.swift in Sources */,
1D200C972BA3157A00108701 /* SettingsNextStepsView.swift in Sources */,
4B0295192537BC6700E00CEF /* ConfigurationDebugViewController.swift in Sources */,
7B1C892C2CF714AA0008224E /* VPNTipsModel.swift in Sources */,
1E7A71192934EC6100B7EA19 /* OmniBarNotificationContainerView.swift in Sources */,
D60B1F272B9DDE5A00AE4760 /* SubscriptionGoogleView.swift in Sources */,
984D035C24AE15CD0066CFB8 /* TabSwitcherSettings.swift in Sources */,
Expand Down Expand Up @@ -8390,7 +8393,7 @@
98424AAD2CED4FF10071C7DB /* WKWebViewConfigurationExtensionTests.swift in Sources */,
98424AAE2CED4FF10071C7DB /* WebCacheManagerTests.swift in Sources */,
98424AAF2CED4FF10071C7DB /* UserAgentTests.swift in Sources */,
98424AB02CED4FF10071C7DB /* PreserveLoginsTests.swift in Sources */,
98424AB02CED4FF10071C7DB /* UserDefaultsFireproofingTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
110 changes: 64 additions & 46 deletions DuckDuckGo/NetworkProtectionStatusView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,48 +31,9 @@ struct NetworkProtectionStatusView: View {
@ObservedObject
public var statusModel: NetworkProtectionStatusViewModel

// MARK: - Tips

let geoswitchingTip: VPNGeoswitchingTip = {
let tip = VPNGeoswitchingTip()

if #available(iOS 17.0, *) {
if tip.shouldDisplay {
Task {
for await status in tip.statusUpdates {
if case .invalidated = status {
await VPNSnoozeTip.geolocationTipDismissedEvent.donate()
await VPNAddWidgetTip.geolocationTipDismissedEvent.donate()
}
}
}
}
}

return tip
}()

let snoozeTip: VPNSnoozeTip = {
let tip = VPNSnoozeTip()

if #available(iOS 17.0, *) {
if tip.shouldDisplay {
Task {
for await status in tip.statusUpdates {
if case .invalidated = status {
await VPNAddWidgetTip.snoozeTipDismissedEvent.donate()
}
}
}
}
}

return tip
}()

let widgetTip: VPNAddWidgetTip = {
VPNAddWidgetTip()
}()
var tipsModel: VPNTipsModel {
statusModel.tipsModel
}
Comment on lines +34 to +36
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Placing the tips in a model to separate view and other logic.


// MARK: - View

Expand Down Expand Up @@ -106,6 +67,16 @@ struct NetworkProtectionStatusView: View {
.sheet(isPresented: $statusModel.showAddWidgetEducationView) {
widgetEducationSheet()
}
.onAppear {
if #available(iOS 18.0, *) {
tipsModel.handleStatusViewAppear()
}
}
.onDisappear {
if #available(iOS 18.0, *) {
tipsModel.handleStatusViewDisappear()
}
}
}

@ViewBuilder
Expand Down Expand Up @@ -359,11 +330,26 @@ struct NetworkProtectionStatusView: View {
@ViewBuilder
private func geoswitchingTipView() -> some View {
if statusModel.canShowTips {

TipView(geoswitchingTip)
TipView(tipsModel.geoswitchingTip)
.removeGroupedListStyleInsets()
.tipCornerRadius(0)
.tipBackground(Color(designSystemColor: .surface))
.onAppear {
tipsModel.handleGeoswitchingTipShown()
}
.task {
var previousStatus = tipsModel.geoswitchingTip.status

for await status in tipsModel.geoswitchingTip.statusUpdates {
if case .invalidated(let reason) = status {
if case .available = previousStatus {
Comment on lines +343 to +345
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit nasty. I copied this logic from macOS.

Tips don't have a simple action hook for when they're dismissed through the "X" button, unless we reimplement them from scratch.

Since I didn't want to recreate the standard tip UI, I am observing the tip status and when I detect a change from available (should be shown to the user) to invalidated (either closed or actioned) I'm firing a call to handle the event. This is mostly used for pixel-firing within tipsModel.

tipsModel.handleGeoswitchingTipInvalidated(reason)
}
}

previousStatus = status
}
}
}
}

Expand All @@ -373,10 +359,26 @@ struct NetworkProtectionStatusView: View {
if statusModel.canShowTips,
statusModel.hasServerInfo {

TipView(snoozeTip, action: statusModel.snoozeActionHandler(action:))
TipView(tipsModel.snoozeTip, action: statusModel.snoozeActionHandler(action:))
.removeGroupedListStyleInsets()
.tipCornerRadius(0)
.tipBackground(Color(designSystemColor: .surface))
.onAppear {
tipsModel.handleSnoozeTipShown()
}
.task {
var previousStatus = tipsModel.snoozeTip.status

for await status in tipsModel.snoozeTip.statusUpdates {
if case .invalidated(let reason) = status {
if case .available = previousStatus {
tipsModel.handleSnoozeTipInvalidated(reason)
}
}

previousStatus = status
}
}
}
}

Expand All @@ -386,10 +388,26 @@ struct NetworkProtectionStatusView: View {
if statusModel.canShowTips,
!statusModel.isNetPEnabled && !statusModel.isSnoozing {

TipView(widgetTip, action: statusModel.widgetActionHandler(action:))
TipView(tipsModel.widgetTip, action: statusModel.widgetActionHandler(action:))
.removeGroupedListStyleInsets()
.tipCornerRadius(0)
.tipBackground(Color(designSystemColor: .surface))
.onAppear {
tipsModel.handleWidgetTipShown()
}
.task {
var previousStatus = tipsModel.widgetTip.status

for await status in tipsModel.widgetTip.statusUpdates {
if case .invalidated(let reason) = status {
if case .available = previousStatus {
tipsModel.handleWidgetTipInvalidated(reason)
}
}

previousStatus = status
}
}
}
}

Expand Down
24 changes: 16 additions & 8 deletions DuckDuckGo/NetworkProtectionStatusViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ final class NetworkProtectionStatusViewModel: ObservableObject {
@Published
var showAddWidgetEducationView: Bool = false

let tipsModel: VPNTipsModel

// MARK: Error

struct ErrorItem {
Expand All @@ -138,7 +140,7 @@ final class NetworkProtectionStatusViewModel: ObservableObject {
didSet {
if #available(iOS 17.0, *) {
if isNetPEnabled {
VPNGeoswitchingTip.donateVPNConnectedEvent()
VPNGeoswitchingTip.vpnEnabledOnce = true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a change to keep this as a flag, rather than an ever growing counter. We just need to know this happened once.

}

VPNSnoozeTip.vpnEnabled = isNetPEnabled
Expand Down Expand Up @@ -184,6 +186,7 @@ final class NetworkProtectionStatusViewModel: ObservableObject {
errorObserver: ConnectionErrorObserver = ConnectionErrorObserverThroughSession(),
locationListRepository: NetworkProtectionLocationListRepository,
usesUnifiedFeedbackForm: Bool) {

self.tunnelController = tunnelController
self.settings = settings
self.statusObserver = statusObserver
Expand All @@ -199,6 +202,11 @@ final class NetworkProtectionStatusViewModel: ObservableObject {

self.dnsSettings = settings.dnsSettings

self.tipsModel = VPNTipsModel(
isTipFeatureEnabled: featureFlagger.isFeatureOn(.networkProtectionUserTips),
statusObserver: statusObserver,
vpnSettings: settings)
Comment on lines +205 to +208
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spawning the tips model.


updateViewModel(withStatus: statusObserver.recentValue)

setUpIsConnectedStatePublishers()
Expand Down Expand Up @@ -477,8 +485,8 @@ final class NetworkProtectionStatusViewModel: ObservableObject {
return
}

if #available(iOS 17.0, *) {
VPNSnoozeTip().invalidate(reason: .actionPerformed)
if #available(iOS 18.0, *) {
tipsModel.handleUserSnoozedVPN()
}

let defaultDuration: TimeInterval = .minutes(20)
Expand Down Expand Up @@ -577,30 +585,30 @@ final class NetworkProtectionStatusViewModel: ObservableObject {

// MARK: - UI Events handling

@available(iOS 17.0, *)
@available(iOS 18.0, *)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may seem confusing, but the VPN tips feature was initially meant for iOS 17, but due to some unexpected crashes that seem to be on Apple, we've decided to move them to iOS 18.

This just needed to be updated.

func snoozeActionHandler(action: Tips.Action) {
if action.id == VPNSnoozeTip.ActionIdentifiers.learnMore.rawValue {
let url = URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/vpn/troubleshooting/")!
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}

@available(iOS 17.0, *)
@available(iOS 18.0, *)
@MainActor
func widgetActionHandler(action: Tips.Action) {
if action.id == VPNAddWidgetTip.ActionIdentifiers.addWidget.rawValue {
showAddWidgetEducationView = true

VPNAddWidgetTip().invalidate(reason: .actionPerformed)
tipsModel.handleUserOpenedWidgetLearnMore()
}
}

/// The user opened the VPN locations view
///
func handleUserOpenedVPNLocations() {
if #available(iOS 17.0, *) {
if #available(iOS 18.0, *) {
Task { @MainActor in
VPNGeoswitchingTip().invalidate(reason: .actionPerformed)
tipsModel.handleUserOpenedLocations()
}
}
}
Expand Down
Loading
Loading