Skip to content

Commit

Permalink
Change account expiry text to display 'less than a day'
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Petersson authored and buggmagnet committed Nov 10, 2023
1 parent 1654b73 commit 5b24541
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 44 deletions.
12 changes: 10 additions & 2 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,8 @@
7A3FD1B82AD54AE60042BEA6 /* TimeServerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB9A2A98F58600F578F2 /* TimeServerProxy.swift */; };
7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */; };
7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */; };
7A6F2FA52AFA3CB2006D0856 /* AccountExpiryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */; };
7A6F2FA72AFBB9AE006D0856 /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */; };
7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */; };
7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */; };
7A83C3FF2A55B72E00DFB83A /* MullvadVPNApp.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = 7A83C3FE2A55B72E00DFB83A /* MullvadVPNApp.xctestplan */; };
Expand Down Expand Up @@ -508,6 +510,7 @@
A97F1F472A1F4E1A00ECEFDE /* MullvadTransport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A97F1F412A1F4E1A00ECEFDE /* MullvadTransport.framework */; };
A97F1F482A1F4E1A00ECEFDE /* MullvadTransport.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A97F1F412A1F4E1A00ECEFDE /* MullvadTransport.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
A97FF5502A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */; };
A988A3E22AFE54AC0008D2C7 /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */; };
A988DF212ADD293D00D807EF /* RESTTransportStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A1DE782AD5708E0073F689 /* RESTTransportStrategy.swift */; };
A988DF242ADD307200D807EF /* libRelaySelector.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5898D29829017DAC00EB5EBA /* libRelaySelector.a */; };
A988DF262ADE86ED00D807EF /* WireGuardObfuscationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988DF252ADE86ED00D807EF /* WireGuardObfuscationSettings.swift */; };
Expand All @@ -533,7 +536,6 @@
A9A5F9F12ACB05160083449F /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; };
A9A5F9F22ACB05160083449F /* NotificationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C8191729FAA2C400DEB1B4 /* NotificationConfiguration.swift */; };
A9A5F9F32ACB05160083449F /* AccountExpirySystemNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B75402668FD7700DEF7E9 /* AccountExpirySystemNotificationProvider.swift */; };
A9A5F9F42ACB05160083449F /* AccountExpiryInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58607A4C2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift */; };
A9A5F9F52ACB05160083449F /* RegisteredDeviceInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */; };
A9A5F9F62ACB05160083449F /* TunnelStatusNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */; };
A9A5F9F72ACB05160083449F /* NotificationProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E232943520C00D5980C /* NotificationProviderProtocol.swift */; };
Expand Down Expand Up @@ -1533,6 +1535,8 @@
7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = "<group>"; };
7A42DECC2A09064C00B209BE /* SelectableSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableSettingsCell.swift; sourceTree = "<group>"; };
7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorTimings.swift; sourceTree = "<group>"; };
7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryTests.swift; sourceTree = "<group>"; };
7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiry.swift; sourceTree = "<group>"; };
7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstTimeLaunch.swift; sourceTree = "<group>"; };
7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootConfiguration.swift; sourceTree = "<group>"; };
7A83C3FE2A55B72E00DFB83A /* MullvadVPNApp.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = MullvadVPNApp.xctestplan; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2396,6 +2400,7 @@
isa = PBXGroup;
children = (
58C8191729FAA2C400DEB1B4 /* NotificationConfiguration.swift */,
7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */,
587B75402668FD7700DEF7E9 /* AccountExpirySystemNotificationProvider.swift */,
58607A4C2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift */,
F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */,
Expand Down Expand Up @@ -2472,6 +2477,7 @@
isa = PBXGroup;
children = (
A900E9BF2ACC661900C95F67 /* AccessTokenManager+Stubs.swift */,
7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */,
A900E9B72ACC5C2B00C95F67 /* AccountsProxy+Stubs.swift */,
A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */,
A900E9BD2ACC654100C95F67 /* APIProxy+Stubs.swift */,
Expand Down Expand Up @@ -4097,7 +4103,6 @@
A9A5F9F12ACB05160083449F /* String+Split.swift in Sources */,
A9A5F9F22ACB05160083449F /* NotificationConfiguration.swift in Sources */,
A9A5F9F32ACB05160083449F /* AccountExpirySystemNotificationProvider.swift in Sources */,
A9A5F9F42ACB05160083449F /* AccountExpiryInAppNotificationProvider.swift in Sources */,
A9A5F9F52ACB05160083449F /* RegisteredDeviceInAppNotificationProvider.swift in Sources */,
A9A5F9F62ACB05160083449F /* TunnelStatusNotificationProvider.swift in Sources */,
A9A5F9F72ACB05160083449F /* NotificationProviderProtocol.swift in Sources */,
Expand All @@ -4121,6 +4126,7 @@
A9E0317A2ACB0AE70095D843 /* UIApplication+Stubs.swift in Sources */,
A9A5FA062ACB05160083449F /* SimulatorTunnelProviderManager.swift in Sources */,
A9A5FA072ACB05160083449F /* SimulatorVPNConnection.swift in Sources */,
7A6F2FA52AFA3CB2006D0856 /* AccountExpiryTests.swift in Sources */,
A9A5FA082ACB05160083449F /* StorePaymentBlockObserver.swift in Sources */,
A9E0317C2ACBFC7E0095D843 /* TunnelStore+Stubs.swift in Sources */,
A9A5FA092ACB05160083449F /* SendStoreReceiptOperation.swift in Sources */,
Expand Down Expand Up @@ -4151,6 +4157,7 @@
A9A5FA212ACB05160083449F /* TunnelManagerErrors.swift in Sources */,
A9C342C32ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift in Sources */,
A9A5FA222ACB05160083449F /* TunnelObserver.swift in Sources */,
A988A3E22AFE54AC0008D2C7 /* AccountExpiry.swift in Sources */,
A9E0317F2ACC331C0095D843 /* TunnelStatusBlockObserver.swift in Sources */,
A9A5FA232ACB05160083449F /* TunnelState.swift in Sources */,
A9A5FA242ACB05160083449F /* TunnelStore.swift in Sources */,
Expand Down Expand Up @@ -4427,6 +4434,7 @@
F0C6FA812A66E23300F521F0 /* DeleteAccountOperation.swift in Sources */,
F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift in Sources */,
587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */,
7A6F2FA72AFBB9AE006D0856 /* AccountExpiry.swift in Sources */,
5819C2172729595500D6EC38 /* SettingsAddDNSEntryCell.swift in Sources */,
7A1A26452A29CEF700B978AA /* RelayFilterViewController.swift in Sources */,
5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */,
Expand Down
24 changes: 20 additions & 4 deletions ios/MullvadVPN/Classes/CustomDateComponentsFormatting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,39 @@ extension CustomDateComponentsFormatting {
///
/// The behaviour of that method differs from `DateComponentsFormatter`:
///
/// 1. Intervals of two years or more are formatted in years quantity.
/// 2. Otherwise intervals matching none of the above are formatted in days quantity.
/// 1. Intervals of less than a day return a custom string.
/// 2. Intervals of two years or more are formatted in years quantity.
/// 3. Otherwise intervals matching none of the above are formatted in days quantity.
///
static func localizedString(
from start: Date,
to end: Date,
calendar: Calendar = Calendar.current,
unitsStyle: DateComponentsFormatter.UnitsStyle
) -> String? {
let years = calendar.dateComponents([.year], from: start, to: max(start, end)).year ?? 0
let dateComponents = calendar.dateComponents([.year, .day], from: start, to: max(start, end))

guard !isLessThanOneDay(dateComponents: dateComponents) else {
return NSLocalizedString(
"CUSTOM_DATE_COMPONENTS_FORMATTING_LESS_THAN_ONE_DAY",
value: "Less than a day",
comment: ""
)
}

let formatter = DateComponentsFormatter()
formatter.calendar = calendar
formatter.unitsStyle = unitsStyle
formatter.maximumUnitCount = 1
formatter.allowedUnits = years >= 2 ? .year : .day
formatter.allowedUnits = (dateComponents.year ?? 0) >= 2 ? .year : .day

return formatter.string(from: start, to: max(start, end))
}

private static func isLessThanOneDay(dateComponents: DateComponents) -> Bool {
let year = dateComponents.year ?? 0
let day = dateComponents.day ?? 0

return (year == 0) && (day == 0)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// AccountExpiry.swift
// MullvadVPN
//
// Created by Jon Petersson on 2023-11-08.
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
//

import Foundation

struct AccountExpiry {
var expiryDate: Date?

var triggerDate: Date? {
guard let expiryDate else { return nil }

return Calendar.current.date(
byAdding: .day,
value: -NotificationConfiguration.closeToExpiryTriggerInterval,
to: expiryDate
)
}

var formattedDuration: String? {
let now = Date()

guard
let expiryDate,
let triggerDate,
let duration = CustomDateComponentsFormatting.localizedString(
from: now,
to: expiryDate,
unitsStyle: .full
),
now >= triggerDate,
now < expiryDate
else {
return nil
}

return duration
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import MullvadSettings
import MullvadTypes

final class AccountExpiryInAppNotificationProvider: NotificationProvider, InAppNotificationProvider {
private var accountExpiry: Date?
private var accountExpiry = AccountExpiry()
private var tunnelObserver: TunnelBlockObserver?
private var timer: DispatchSourceTimer?

Expand All @@ -38,31 +38,10 @@ final class AccountExpiryInAppNotificationProvider: NotificationProvider, InAppN
// MARK: - InAppNotificationProvider

var notificationDescriptor: InAppNotificationDescriptor? {
let now = Date()
guard let accountExpiry, let triggerDate, now >= triggerDate,
now < accountExpiry
else {
guard let duration = accountExpiry.formattedDuration else {
return nil
}

let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
formatter.allowedUnits = [.minute, .hour, .day]
formatter.maximumUnitCount = 1

let duration: String?
if accountExpiry.timeIntervalSince(now) < .minutes(1) {
duration = NSLocalizedString(
"ACCOUNT_EXPIRY_INAPP_NOTIFICATION_LESS_THAN_ONE_MINUTE",
value: "Less than a minute",
comment: ""
)
} else {
duration = formatter.string(from: now, to: accountExpiry)
}

guard let duration else { return nil }

return InAppNotificationDescriptor(
identifier: identifier,
style: .warning,
Expand All @@ -83,30 +62,20 @@ final class AccountExpiryInAppNotificationProvider: NotificationProvider, InAppN

// MARK: - Private

private var triggerDate: Date? {
guard let accountExpiry else { return nil }

return Calendar.current.date(
byAdding: .day,
value: -NotificationConfiguration.closeToExpiryTriggerInterval,
to: accountExpiry
)
}

private func invalidate(deviceState: DeviceState) {
updateExpiry(deviceState: deviceState)
updateTimer()
invalidate()
}

private func updateExpiry(deviceState: DeviceState) {
accountExpiry = deviceState.accountData?.expiry
accountExpiry.expiryDate = deviceState.accountData?.expiry
}

private func updateTimer() {
timer?.cancel()

guard let triggerDate else {
guard let triggerDate = accountExpiry.triggerDate else {
return
}

Expand All @@ -127,7 +96,7 @@ final class AccountExpiryInAppNotificationProvider: NotificationProvider, InAppN
}

private func timerDidFire() {
let shouldCancelTimer = accountExpiry.map { $0 <= Date() } ?? true
let shouldCancelTimer = accountExpiry.expiryDate.map { $0 <= Date() } ?? true

if shouldCancelTimer {
timer?.cancel()
Expand Down
48 changes: 48 additions & 0 deletions ios/MullvadVPNTests/AccountExpiryTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// AccountExpiryTests.swift
// MullvadVPNTests
//
// Created by Jon Petersson on 2023-11-07.
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
//

import XCTest

class AccountExpiryTests: XCTestCase {
func testNoDateReturnsNoDuration() {
let accountExpiry = AccountExpiry()
XCTAssertNil(accountExpiry.formattedDuration)
}

func testDateNowReturnsNoDuration() {
let accountExpiry = AccountExpiry(expiryDate: Date())
XCTAssertNil(accountExpiry.formattedDuration)
}

func testDateInPastReturnsNoDuration() {
let accountExpiry = AccountExpiry(expiryDate: Date().addingTimeInterval(-10))
XCTAssertNil(accountExpiry.formattedDuration)
}

func testDateWithinTriggerIntervalReturnsDuration() {
let date = Calendar.current.date(
byAdding: .day,
value: NotificationConfiguration.closeToExpiryTriggerInterval - 1,
to: Date()
)

let accountExpiry = AccountExpiry(expiryDate: date)
XCTAssertNotNil(accountExpiry.formattedDuration)
}

func testDateNotWithinTriggerIntervalReturnsNoDuration() {
let date = Calendar.current.date(
byAdding: .day,
value: NotificationConfiguration.closeToExpiryTriggerInterval + 1,
to: Date()
)

let accountExpiry = AccountExpiry(expiryDate: date)
XCTAssertNil(accountExpiry.formattedDuration)
}
}
4 changes: 2 additions & 2 deletions ios/MullvadVPNTests/CustomDateComponentsFormattingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class CustomDateComponentsFormattingTests: XCTestCase {
unitsStyle: .full
)

XCTAssertEqual(result, "0 days")
XCTAssertEqual(result, "Less than a day")
}

func testLessThanTwoYearsFormatting() throws {
Expand Down Expand Up @@ -71,7 +71,7 @@ class CustomDateComponentsFormattingTests: XCTestCase {
unitsStyle: .full
)

XCTAssertEqual(result, "0 days")
XCTAssertEqual(result, "Less than a day")
}

private func makeDateRange(addingComponents dateComponents: DateComponents) -> (Date, Date) {
Expand Down

0 comments on commit 5b24541

Please sign in to comment.