1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
//
// AccountExpiryInAppNotificationProvider.swift
// MullvadVPN
//
// Created by pronebird on 12/12/2022.
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//
import Foundation
import MullvadSettings
import MullvadTypes
final class AccountExpiryInAppNotificationProvider: NotificationProvider, InAppNotificationProvider,
@unchecked Sendable
{
private var accountExpiry = AccountExpiry()
private var tunnelObserver: TunnelBlockObserver?
private var timer: DispatchSourceTimer?
init(tunnelManager: TunnelManager) {
super.init()
let tunnelObserver = TunnelBlockObserver(
didLoadConfiguration: { [weak self] tunnelManager in
self?.invalidate(deviceState: tunnelManager.deviceState)
},
didUpdateTunnelStatus: { [weak self] tunnelManager, _ in
self?.invalidate(deviceState: tunnelManager.deviceState)
},
didUpdateDeviceState: { [weak self] _, deviceState, _ in
self?.invalidate(deviceState: deviceState)
}
)
self.tunnelObserver = tunnelObserver
tunnelManager.addObserver(tunnelObserver)
}
override var identifier: NotificationProviderIdentifier {
.accountExpiryInAppNotification
}
override var priority: NotificationPriority {
.high
}
// MARK: - InAppNotificationProvider
var notificationDescriptor: InAppNotificationDescriptor? {
guard let durationText = remainingDaysText else {
return nil
}
return InAppNotificationDescriptor(
identifier: identifier,
style: .warning,
title: durationText,
body: NSAttributedString(
string: NSLocalizedString(
"You can add more time via the account view or website to continue using the VPN.",
comment: ""
))
)
}
// MARK: - Private
private func invalidate(deviceState: DeviceState) {
accountExpiry.expiryDate = deviceState.accountData?.expiry
updateTimer()
invalidate()
}
private func updateTimer() {
timer?.cancel()
guard let triggerDate = accountExpiry.nextTriggerDate(for: .inApp) else {
return
}
let now = Date()
let fireDate = max(now, triggerDate)
let timer = DispatchSource.makeTimerSource(queue: .main)
timer.setEventHandler { [weak self] in
self?.timerDidFire()
}
timer.schedule(
wallDeadline: .now() + fireDate.timeIntervalSince(now),
repeating: .seconds(NotificationConfiguration.closeToExpiryInAppNotificationRefreshInterval)
)
timer.activate()
self.timer = timer
}
private func timerDidFire() {
let shouldCancelTimer = accountExpiry.expiryDate.map { $0 <= Date() } ?? true
if shouldCancelTimer {
timer?.cancel()
}
invalidate()
}
}
extension AccountExpiryInAppNotificationProvider {
private var remainingDaysText: String? {
guard
let expiryDate = accountExpiry.expiryDate,
let nextTriggerDate = accountExpiry.nextTriggerDate(for: .inApp),
let duration = CustomDateComponentsFormatting.localizedString(
from: nextTriggerDate,
to: expiryDate,
unitsStyle: .full
)
else { return nil }
return String(format: NSLocalizedString("%@ left on this account", comment: ""), duration).uppercased()
}
}
|