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

Change account expiry text to display 'less than a day' #5391

Merged
Show file tree
Hide file tree
Changes from all commits
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
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
Loading