@@ -11,8 +11,9 @@ import MullvadSettings
11
11
import UserNotifications
12
12
13
13
final class AccountExpirySystemNotificationProvider : NotificationProvider , SystemNotificationProvider {
14
- private var accountExpiry : Date ?
14
+ private var accountExpiry = AccountExpiry ( )
15
15
private var tunnelObserver : TunnelBlockObserver ?
16
+ private var accountHasRecentlyExpired = false
16
17
17
18
init ( tunnelManager: TunnelManager ) {
18
19
super. init ( )
@@ -21,8 +22,16 @@ final class AccountExpirySystemNotificationProvider: NotificationProvider, Syste
21
22
didLoadConfiguration: { [ weak self] tunnelManager in
22
23
self ? . invalidate ( deviceState: tunnelManager. deviceState)
23
24
} ,
24
- didUpdateDeviceState: { [ weak self] _, deviceState, _ in
25
- self ? . invalidate ( deviceState: deviceState)
25
+ didUpdateDeviceState: { [ weak self] tunnelManager, deviceState, previousDeviceState in
26
+ guard let self else { return }
27
+
28
+ checkAccountExpiry (
29
+ tunnelStatus: tunnelManager. tunnelStatus,
30
+ deviceState: deviceState,
31
+ previousDeviceState: previousDeviceState
32
+ )
33
+
34
+ invalidate ( deviceState: tunnelManager. deviceState)
26
35
}
27
36
)
28
37
@@ -38,21 +47,21 @@ final class AccountExpirySystemNotificationProvider: NotificationProvider, Syste
38
47
// MARK: - SystemNotificationProvider
39
48
40
49
var notificationRequest : UNNotificationRequest ? {
41
- guard let trigger else { return nil }
50
+ let trigger = accountHasRecentlyExpired ? triggerExpiry : triggerCloseToExpiry
51
+
52
+ guard let trigger, let durationText = formattedRemainingDuration else {
53
+ return nil
54
+ }
42
55
43
56
let content = UNMutableNotificationContent ( )
44
57
content. title = NSLocalizedString (
45
58
" ACCOUNT_EXPIRY_SYSTEM_NOTIFICATION_TITLE " ,
46
59
tableName: " AccountExpiry " ,
47
60
value: " Account credit expires soon " ,
48
- comment: " Title for system account expiry notification, fired 3 days prior to account expiry. "
49
- )
50
- content. body = NSLocalizedString (
51
- " ACCOUNT_EXPIRY_SYSTEM_NOTIFICATION_BODY " ,
52
- tableName: " AccountExpiry " ,
53
- value: " Account credit expires in 3 days. Buy more credit. " ,
54
- comment: " Message for system account expiry notification, fired 3 days prior to account expiry. "
61
+ comment: " Title for system account expiry notification, fired X days prior to account expiry. "
55
62
)
63
+
64
+ content. body = durationText
56
65
content. sound = UNNotificationSound . default
57
66
58
67
return UNNotificationRequest (
@@ -74,33 +83,105 @@ final class AccountExpirySystemNotificationProvider: NotificationProvider, Syste
74
83
75
84
// MARK: - Private
76
85
77
- private var trigger : UNNotificationTrigger ? {
78
- guard let accountExpiry else { return nil }
86
+ private var triggerCloseToExpiry : UNNotificationTrigger ? {
87
+ guard let triggerDate = accountExpiry. nextTriggerDate ( for : . system ) else { return nil }
79
88
80
- guard let triggerDate = Calendar . current. date (
81
- byAdding: . day,
82
- value: - NotificationConfiguration. closeToExpiryTriggerInterval,
83
- to: accountExpiry
84
- ) else { return nil }
89
+ let dateComponents = Calendar . current. dateComponents (
90
+ [ . second, . minute, . hour, . day, . month, . year] ,
91
+ from: triggerDate
92
+ )
85
93
86
- // Do not produce notification if less than 3 days left till expiry
87
- guard triggerDate > Date ( ) else { return nil }
94
+ return UNCalendarNotificationTrigger ( dateMatching : dateComponents , repeats : false )
95
+ }
88
96
89
- // Create date components for calendar trigger
97
+ private var triggerExpiry : UNNotificationTrigger {
90
98
let dateComponents = Calendar . current. dateComponents (
91
99
[ . second, . minute, . hour, . day, . month, . year] ,
92
- from: triggerDate
100
+ from: Date ( ) . addingTimeInterval ( 1 ) // Give some leeway.
93
101
)
94
102
95
103
return UNCalendarNotificationTrigger ( dateMatching: dateComponents, repeats: false )
96
104
}
97
105
98
106
private var shouldRemovePendingOrDeliveredRequests : Bool {
99
- accountExpiry == nil
107
+ return accountExpiry. expiryDate == nil
108
+ }
109
+
110
+ private func checkAccountExpiry(
111
+ tunnelStatus: TunnelStatus ,
112
+ deviceState: DeviceState ,
113
+ previousDeviceState: DeviceState
114
+ ) {
115
+ var blockedStateByExpiredAccount = false
116
+ if case . accountExpired = tunnelStatus. observedState. blockedState? . reason {
117
+ blockedStateByExpiredAccount = true
118
+ }
119
+
120
+ let accountHasExpired = deviceState. accountData? . isExpired == true
121
+ let accountHasRecentlyExpired = deviceState. accountData? . isExpired != previousDeviceState. accountData? . isExpired
122
+
123
+ self . accountHasRecentlyExpired = blockedStateByExpiredAccount && accountHasExpired && accountHasRecentlyExpired
100
124
}
101
125
102
126
private func invalidate( deviceState: DeviceState ) {
103
- accountExpiry = deviceState. accountData? . expiry
127
+ accountExpiry. expiryDate = deviceState. accountData? . expiry
104
128
invalidate ( )
105
129
}
106
130
}
131
+
132
+ extension AccountExpirySystemNotificationProvider {
133
+ private var formattedRemainingDuration : String ? {
134
+ if accountHasRecentlyExpired {
135
+ return expiredText
136
+ }
137
+
138
+ switch accountExpiry. daysRemaining ( for: . system) ? . day {
139
+ case . none:
140
+ return nil
141
+ case 1 :
142
+ return singleDayText
143
+ default :
144
+ return multipleDaysText
145
+ }
146
+ }
147
+
148
+ private var expiredText : String {
149
+ NSLocalizedString (
150
+ " ACCOUNT_EXPIRY_SYSTEM_NOTIFICATION_BODY " ,
151
+ tableName: " AccountExpiry " ,
152
+ value: """
153
+ Blocking internet: Your time on this account has expired. To continue using the internet, \
154
+ please add more time or disconnect the VPN.
155
+ """ ,
156
+ comment: " Message for in-app notification, displayed on account expiry while connected to VPN. "
157
+ )
158
+ }
159
+
160
+ private var singleDayText : String {
161
+ NSLocalizedString (
162
+ " ACCOUNT_EXPIRY_SYSTEM_NOTIFICATION_BODY " ,
163
+ tableName: " AccountExpiry " ,
164
+ value: " You have one day left on this account. Please add more time to continue using the VPN. " ,
165
+ comment: " Message for in-app notification, displayed within the last 1 day until account expiry. "
166
+ )
167
+ }
168
+
169
+ private var multipleDaysText : String ? {
170
+ guard
171
+ let expiryDate = accountExpiry. expiryDate,
172
+ let nextTriggerDate = accountExpiry. nextTriggerDate ( for: . system) ,
173
+ let duration = CustomDateComponentsFormatting . localizedString (
174
+ from: nextTriggerDate,
175
+ to: expiryDate,
176
+ unitsStyle: . full
177
+ )
178
+ else { return nil }
179
+
180
+ return String ( format: NSLocalizedString (
181
+ " ACCOUNT_EXPIRY_SYSTEM_NOTIFICATION_BODY " ,
182
+ tableName: " AccountExpiry " ,
183
+ value: " You have %@ left on this account. " ,
184
+ comment: " Message for in-app notification, displayed within the last X days until account expiry. "
185
+ ) , duration. lowercased ( ) )
186
+ }
187
+ }
0 commit comments