Skip to content

Commit a09adf9

Browse files
committed
Merge branch 'notification-onboarding-shown-whenever-app-is-opened-ios-1515'
2 parents 70dc620 + 56ca4d5 commit a09adf9

File tree

12 files changed

+118
-80
lines changed

12 files changed

+118
-80
lines changed

ios/MullvadSettings/AppPreferences.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,34 +26,34 @@ enum AppStorageKey: String {
2626
case isAgreedToTermsOfService
2727
case lastSeenChangeLogVersion
2828
case lastVersionCheck
29-
case isNotificationPermissionNeeded
29+
case isNotificationPermissionAsked
3030
case notificationSettings
3131
}
3232

3333
public final class AppPreferences: AppPreferencesDataSource {
3434
public init() {}
3535

36-
@AppStorage(key: AppStorageKey.hasDoneFirstTimeLaunch.rawValue, container: .standard)
36+
@PrimitiveStorage(key: AppStorageKey.hasDoneFirstTimeLaunch.rawValue, container: .standard)
3737
public var hasDoneFirstTimeLaunch: Bool = false
3838

39-
@AppStorage(key: AppStorageKey.hasDoneFirstTimeLogin.rawValue, container: .standard)
39+
@PrimitiveStorage(key: AppStorageKey.hasDoneFirstTimeLogin.rawValue, container: .standard)
4040
public var hasDoneFirstTimeLogin: Bool = false
4141

42-
@AppStorage(key: AppStorageKey.isShownOnboarding.rawValue, container: .standard)
42+
@PrimitiveStorage(key: AppStorageKey.isShownOnboarding.rawValue, container: .standard)
4343
public var isShownOnboarding = true
4444

45-
@AppStorage(key: AppStorageKey.isAgreedToTermsOfService.rawValue, container: .standard)
45+
@PrimitiveStorage(key: AppStorageKey.isAgreedToTermsOfService.rawValue, container: .standard)
4646
public var isAgreedToTermsOfService = false
4747

48-
@AppStorage(key: AppStorageKey.lastSeenChangeLogVersion.rawValue, container: .standard)
48+
@PrimitiveStorage(key: AppStorageKey.lastSeenChangeLogVersion.rawValue, container: .standard)
4949
public var lastSeenChangeLogVersion = ""
5050

51-
@AppStorage(key: AppStorageKey.lastVersionCheck.rawValue, container: .standard)
51+
@CompositeStorage(key: AppStorageKey.lastVersionCheck.rawValue, container: .standard)
5252
public var lastVersionCheck = VersionCheck(version: "", date: .distantPast)
5353

54-
@AppStorage(key: AppStorageKey.isNotificationPermissionNeeded.rawValue, container: .standard)
54+
@PrimitiveStorage(key: AppStorageKey.isNotificationPermissionAsked.rawValue, container: .standard)
5555
public var isNotificationPermissionAsked = false
5656

57-
@AppStorage(key: AppStorageKey.notificationSettings.rawValue, container: .standard)
57+
@CompositeStorage(key: AppStorageKey.notificationSettings.rawValue, container: .standard)
5858
public var notificationSettings = NotificationSettings()
5959
}

ios/MullvadSettings/AppStorage.swift

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,58 @@
77
//
88

99
import Foundation
10+
import MullvadLogging
1011

1112
@propertyWrapper
12-
public struct AppStorage<Value: Codable> {
13+
public struct PrimitiveStorage<Value: UserDefaultsPrimitive> {
1314
let key: String
1415
let defaultValue: Value
1516
let container: UserDefaults
1617

1718
public var wrappedValue: Value {
1819
get {
19-
guard
20-
let data = container.data(forKey: key),
21-
let value = try? JSONDecoder().decode(Value.self, from: data)
20+
container.value(forKey: key) as? Value ?? defaultValue
21+
}
22+
set {
23+
if let anyOptional = newValue as? AnyOptional,
24+
anyOptional.isNil
25+
{
26+
container.removeObject(forKey: key)
27+
} else {
28+
container.set(newValue, forKey: key)
29+
}
30+
}
31+
}
32+
33+
public init(wrappedValue: Value, key: String, container: UserDefaults) {
34+
self.defaultValue = wrappedValue
35+
self.container = container
36+
self.key = key
37+
}
38+
}
39+
40+
@propertyWrapper
41+
public struct CompositeStorage<Value: Codable> {
42+
let logger = Logger(label: "CompositeStorage")
43+
private let key: String
44+
private let defaultValue: Value
45+
private let container: UserDefaults
46+
47+
public var wrappedValue: Value {
48+
get {
49+
guard let data = container.data(forKey: key),
50+
let decoded = try? JSONDecoder().decode(Value.self, from: data)
2251
else {
2352
return container.value(forKey: key) as? Value ?? defaultValue
2453
}
25-
return value
54+
return decoded
2655
}
2756
set {
28-
if let data = try? JSONEncoder().encode(newValue) {
57+
do {
58+
let data = try JSONEncoder().encode(newValue)
2959
container.set(data, forKey: key)
30-
} else {
31-
container.removeObject(forKey: key)
60+
} catch {
61+
logger.error("Failed to encode \(Value.self)")
3262
}
3363
}
3464
}
@@ -47,3 +77,12 @@ protocol AnyOptional {
4777
extension Optional: AnyOptional {
4878
var isNil: Bool { self == nil }
4979
}
80+
81+
public protocol UserDefaultsPrimitive {}
82+
extension String: UserDefaultsPrimitive {}
83+
extension Int: UserDefaultsPrimitive {}
84+
extension Bool: UserDefaultsPrimitive {}
85+
extension Double: UserDefaultsPrimitive {}
86+
extension Data: UserDefaultsPrimitive {}
87+
extension Array: UserDefaultsPrimitive where Element: UserDefaultsPrimitive {}
88+
extension Dictionary: UserDefaultsPrimitive where Key == String, Value: UserDefaultsPrimitive {}

ios/MullvadVPN.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,7 @@
996996
F09D04BD2AEBB7C5003D4F89 /* OutgoingConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */; };
997997
F09D04C02AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */; };
998998
F09D04C12AF39EA2003D4F89 /* OutgoingConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */; };
999+
F0A005572F3DFDDC00B09EFC /* NotificationPromptPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A005562F3DFDCF00B09EFC /* NotificationPromptPage.swift */; };
9991000
F0A086902C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A0868F2C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift */; };
10001001
F0A7EBB22CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A7EBB12CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift */; };
10011002
F0A7EBB62CF092CC005BB671 /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; };
@@ -2469,6 +2470,7 @@
24692470
F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionService.swift; sourceTree = "<group>"; };
24702471
F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionServiceTests.swift; sourceTree = "<group>"; };
24712472
F09F45242E4A35D600FF5C82 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = "<group>"; };
2473+
F0A005562F3DFDCF00B09EFC /* NotificationPromptPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPromptPage.swift; sourceTree = "<group>"; };
24722474
F0A0868F2C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsStrategyTests.swift; sourceTree = "<group>"; };
24732475
F0A163882C47B46300592300 /* SingleHopEphemeralPeerExchangerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleHopEphemeralPeerExchangerTests.swift; sourceTree = "<group>"; };
24742476
F0A66C992E4A3607006F190A /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@@ -4576,6 +4578,7 @@
45764578
A998DA802BD147AD001D61A2 /* ListCustomListsPage.swift */,
45774579
852969342B4E9270007EAD4C /* LoginPage.swift */,
45784580
7A8A19232CF4C9B8000BCB5B /* MultihopPage.swift */,
4581+
F0A005562F3DFDCF00B09EFC /* NotificationPromptPage.swift */,
45794582
85139B2C2B84B4A700734217 /* OutOfTimePage.swift */,
45804583
852969322B4E9232007EAD4C /* Page.swift */,
45814584
7A4ED7B52ECCC09C00E0DC51 /* PaymentPage.swift */,
@@ -6971,6 +6974,7 @@
69716974
85EC620C2B838D10005AFFB5 /* MullvadAPIWrapper.swift in Sources */,
69726975
A9DF789D2B7D1E8B0094E4AD /* LoggedInWithTimeUITestCase.swift in Sources */,
69736976
85607C892D131CD500037E34 /* TestRouterAPIClient.swift in Sources */,
6977+
F0A005572F3DFDDC00B09EFC /* NotificationPromptPage.swift in Sources */,
69746978
7A4ED7B62ECCC09F00E0DC51 /* PaymentPage.swift in Sources */,
69756979
85D2B0B12B6BD32400DF9DA7 /* BaseUITestCase.swift in Sources */,
69766980
F09084682C6E88ED001CD36E /* DaitaPromptAlert.swift in Sources */,

ios/MullvadVPN/Classes/AccessbilityIdentifier.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ public enum AccessibilityIdentifier: Equatable {
176176
case multihopView
177177
case daitaView
178178
case notificationSettingsView
179+
case notificationPromptView
180+
case notificationPromptSkipButton
181+
case notificationPromptEnableButton
179182

180183
// Other UI elements
181184
case accessMethodEnableSwitch

ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo
7474
accessMethodRepository: AccessMethodRepositoryProtocol,
7575
ipOverrideRepository: IPOverrideRepository,
7676
relaySelectorWrapper: RelaySelectorWrapper
77-
7877
) {
7978
self.tunnelManager = tunnelManager
8079
self.storePaymentManager = storePaymentManager
@@ -89,13 +88,6 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo
8988
self.relaySelectorWrapper = relaySelectorWrapper
9089

9190
super.init()
92-
93-
Task {
94-
let isAllowed = await UNUserNotificationCenter.isAllowed
95-
let isNotificationPermissionAsked = self.appPreferences.isNotificationPermissionAsked
96-
self.appPreferences.isNotificationPermissionAsked = !isAllowed && isNotificationPermissionAsked
97-
}
98-
9991
navigationContainer.delegate = self
10092

10193
router = ApplicationRouter(self)
@@ -107,8 +99,15 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo
10799

108100
func start() {
109101
navigationContainer.notificationController = notificationController
110-
111-
continueFlow(animated: false)
102+
if !appPreferences.isNotificationPermissionAsked {
103+
Task {
104+
let isAllowed = await UNUserNotificationCenter.isAllowed
105+
appPreferences.isNotificationPermissionAsked = isAllowed
106+
continueFlow(animated: false)
107+
}
108+
} else {
109+
continueFlow(animated: false)
110+
}
112111
}
113112

114113
// MARK: - ApplicationRouterDelegate

ios/MullvadVPN/Coordinators/NotificationPromptCoordinator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ final class NotificationPromptCoordinator: Coordinator, Presentable {
3030
didConclude?(self)
3131
})
3232
let viewController = UIHostingController(rootView: view)
33+
viewController.view.setAccessibilityIdentifier(.notificationPromptView)
3334
navigationController.isNavigationBarHidden = true
3435
navigationController.pushViewController(viewController, animated: animated)
3536
}

ios/MullvadVPN/View controllers/Notification/NotificationPromptView.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ struct NotificationPromptView<ViewModel>: View where ViewModel: NotificationProm
4747
case .emptyView:
4848
Spacer()
4949

50-
case .action(let text, let style, let action):
50+
case .action(let text, let style, let accessibilityIdentifier, let action):
5151
MainButton(text: text, style: style, action: action)
52+
.accessibilityIdentifier(accessibilityIdentifier)
5253
}
5354
}
5455
}

ios/MullvadVPN/View controllers/Notification/NotificationPromptViewModel.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ enum NotificationPromptViewRowType: Identifiable {
2525
case action(
2626
text: LocalizedStringKey,
2727
style: MainButtonStyle.Style,
28+
accessibilityIdentifier: AccessibilityIdentifier,
2829
action: () -> Void)
2930
case emptyView
3031

@@ -55,6 +56,7 @@ final class NotificationPromptViewModel: NotificationPromptViewModelProtocol {
5556
.action(
5657
text: "Enable notifications",
5758
style: .success,
59+
accessibilityIdentifier: .notificationPromptEnableButton,
5860
action: { [weak self] in
5961
guard let self else { return }
6062
if isNotificationsDisabled {
@@ -72,6 +74,7 @@ final class NotificationPromptViewModel: NotificationPromptViewModelProtocol {
7274
.action(
7375
text: "Skip",
7476
style: .default,
77+
accessibilityIdentifier: .notificationPromptSkipButton,
7578
action: { [weak self] in
7679
self?.isSkipped = true
7780
}),

ios/MullvadVPNUITests/AccountTests.swift

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,7 @@ class AccountTests: LoggedOutUITestCase {
6060

6161
func testDeleteAccount() throws {
6262
let accountNumber = createTemporaryAccountWithoutTime()
63-
64-
LoginPage(app)
65-
.tapAccountNumberTextField()
66-
.enterText(accountNumber)
67-
.tapAccountNumberSubmitButton()
63+
login(accountNumber: accountNumber)
6864

6965
OutOfTimePage(app)
7066

@@ -96,10 +92,7 @@ class AccountTests: LoggedOutUITestCase {
9692
self.mullvadAPIWrapper.deleteAccount(temporaryAccountNumber)
9793
}
9894

99-
LoginPage(app)
100-
.tapAccountNumberTextField()
101-
.enterText(temporaryAccountNumber)
102-
.tapAccountNumberSubmitButton()
95+
login(accountNumber: temporaryAccountNumber)
10396

10497
OutOfTimePage(app)
10598

@@ -160,22 +153,7 @@ class AccountTests: LoggedOutUITestCase {
160153
self.deleteTemporaryAccountWithTime(accountNumber: hasTimeAccountNumber)
161154
}
162155

163-
var successIconShown = false
164-
var retryCount = 0
165-
let maxRetryCount = 3
166-
167-
let loginPage = LoginPage(app)
168-
.tapAccountNumberTextField()
169-
.enterText(hasTimeAccountNumber)
170-
171-
repeat {
172-
successIconShown =
173-
loginPage
174-
.tapAccountNumberSubmitButton()
175-
.getSuccessIconShown()
176-
177-
retryCount += 1
178-
} while successIconShown == false && retryCount < maxRetryCount
156+
login(accountNumber: hasTimeAccountNumber)
179157

180158
HeaderBar(app)
181159
.verifyDeviceLabelShown()

ios/MullvadVPNUITests/Base/BaseUITestCase.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,10 +314,19 @@ class BaseUITestCase: XCTestCase {
314314
retryCount += 1
315315
} while successIconShown == false && retryCount < maxRetryCount
316316

317+
skipNotificationPromptIfShown()
318+
317319
HeaderBar(app)
318320
.verifyDeviceLabelShown()
319321
}
320322

323+
func skipNotificationPromptIfShown() {
324+
if app.otherElements[.notificationPromptView].existsAfterWait() {
325+
NotificationPromptPage(app)
326+
.tapSkipButton()
327+
}
328+
}
329+
321330
func logoutIfLoggedIn() {
322331
if isLoggedIn() {
323332
// First dismiss settings modal if presented

0 commit comments

Comments
 (0)