diff --git a/CTRTests/Interface/Holder/Dashboard/HolderDashboardViewModelTests+RemoteConfigUpdates.swift b/CTRTests/Interface/Holder/Dashboard/HolderDashboardViewModelTests+RemoteConfigUpdates.swift index 043e017c9..865369fe7 100644 --- a/CTRTests/Interface/Holder/Dashboard/HolderDashboardViewModelTests+RemoteConfigUpdates.swift +++ b/CTRTests/Interface/Holder/Dashboard/HolderDashboardViewModelTests+RemoteConfigUpdates.swift @@ -28,7 +28,7 @@ extension HolderDashboardViewModelTests { sut = vendSut(dashboardRegionToggleValue: .domestic, activeDisclosurePolicies: [.policy3G]) // Act - sendUpdate?((RemoteConfiguration.default, Data(), URLResponse())) + sendUpdate?((RemoteConfiguration.default, Data(), URLResponse(), "hash")) // Assert diff --git a/Packages/Managers/Sources/Managers/RemoteConfiguration/DisclosurePolicyManager.swift b/Packages/Managers/Sources/Managers/RemoteConfiguration/DisclosurePolicyManager.swift index 874ceed11..c785ea6fa 100644 --- a/Packages/Managers/Sources/Managers/RemoteConfiguration/DisclosurePolicyManager.swift +++ b/Packages/Managers/Sources/Managers/RemoteConfiguration/DisclosurePolicyManager.swift @@ -45,7 +45,7 @@ public class DisclosurePolicyManager: DisclosurePolicyManaging { private func configureRemoteConfigManager() { - remoteConfigManagerObserverToken = remoteConfigManager.observatoryForUpdates.append { [weak self] _, _, _ in + remoteConfigManagerObserverToken = remoteConfigManager.observatoryForUpdates.append { [weak self] _, _, _, _ in self?.detectPolicyChange() } } diff --git a/Packages/Managers/Sources/Managers/RemoteConfiguration/RemoteConfigManager.swift b/Packages/Managers/Sources/Managers/RemoteConfiguration/RemoteConfigManager.swift index 320d983d8..7501de448 100644 --- a/Packages/Managers/Sources/Managers/RemoteConfiguration/RemoteConfigManager.swift +++ b/Packages/Managers/Sources/Managers/RemoteConfiguration/RemoteConfigManager.swift @@ -31,7 +31,8 @@ public protocol RemoteConfigManaging: AnyObject { /// The remote configuration manager public class RemoteConfigManager: RemoteConfigManaging { - public typealias ConfigNotification = (RemoteConfiguration, Data, URLResponse) + /// String is the config hash: + public typealias ConfigNotification = (RemoteConfiguration, Data, URLResponse, String) // MARK: - Vars @@ -200,7 +201,7 @@ public class RemoteConfigManager: RemoteConfigManaging { storedConfiguration = remoteConfiguration // Some observers want to know whenever the config is reloaded (regardless if data changed since last time): - self.notifyReloadObservers(Result.success((remoteConfiguration: remoteConfiguration, data: data, response: urlResponse))) + self.notifyReloadObservers(Result.success((remoteConfiguration: remoteConfiguration, data: data, response: urlResponse, hash: newHash ?? ""))) // Is the newly fetched config hash the same as the existing one? // Use the hash, as not all of the config values are mapping in the remoteconfig object. @@ -208,7 +209,7 @@ public class RemoteConfigManager: RemoteConfigManaging { completion(.success((false, remoteConfiguration))) } else { // Inform the observers that only wish to know when config has changed: - notifyUpdateObservers((remoteConfiguration: remoteConfiguration, data: data, response: urlResponse)) + notifyUpdateObservers((remoteConfiguration: remoteConfiguration, data: data, response: urlResponse, hash: newHash ?? "")) completion(.success((true, remoteConfiguration))) } } diff --git a/Packages/Managers/Sources/Managers/RemoteConfiguration/VerificationPolicyEnabler.swift b/Packages/Managers/Sources/Managers/RemoteConfiguration/VerificationPolicyEnabler.swift index e12560911..dc580c98e 100644 --- a/Packages/Managers/Sources/Managers/RemoteConfiguration/VerificationPolicyEnabler.swift +++ b/Packages/Managers/Sources/Managers/RemoteConfiguration/VerificationPolicyEnabler.swift @@ -55,7 +55,7 @@ public final class VerificationPolicyEnabler: VerificationPolicyEnableable { private func configureRemoteConfigManager() { - remoteConfigManagerObserverToken = remoteConfigManager.observatoryForUpdates.append { [weak self] remoteConfiguration, _, _ in + remoteConfigManagerObserverToken = remoteConfigManager.observatoryForUpdates.append { [weak self] remoteConfiguration, _, _, _ in guard let policies = remoteConfiguration.verificationPolicies else { // No feature flag available, enable default policy self?.enable(verificationPolicies: []) diff --git a/Packages/Managers/Tests/ManagersTests/RemoteConfigManagerTests.swift b/Packages/Managers/Tests/ManagersTests/RemoteConfigManagerTests.swift index ce440c8c9..385480b09 100644 --- a/Packages/Managers/Tests/ManagersTests/RemoteConfigManagerTests.swift +++ b/Packages/Managers/Tests/ManagersTests/RemoteConfigManagerTests.swift @@ -250,7 +250,7 @@ class RemoteConfigManagerTests: XCTestCase { _ = sut.observatoryForReloads.append { result in reloadObserverReceivedConfiguration = result.successValue?.0 } - _ = sut.observatoryForUpdates.append { remoteConfiguration, _, _ in + _ = sut.observatoryForUpdates.append { remoteConfiguration, _, _, _ in updateObserverReceivedConfiguration = remoteConfiguration } @@ -297,7 +297,7 @@ class RemoteConfigManagerTests: XCTestCase { _ = sut.observatoryForReloads.append { result in reloadObserverReceivedConfiguration = result.successValue?.0 } - _ = sut.observatoryForUpdates.append { remoteConfiguration, _, _ in + _ = sut.observatoryForUpdates.append { remoteConfiguration, _, _, _ in updateObserverReceivedConfiguration = remoteConfiguration } @@ -389,7 +389,7 @@ class RemoteConfigManagerTests: XCTestCase { _ = sut.observatoryForReloads.append { result in reloadObserverReceivedConfiguration = result.successValue?.0 } - _ = sut.observatoryForUpdates.append { remoteConfiguration, _, _ in + _ = sut.observatoryForUpdates.append { remoteConfiguration, _, _, _ in updateObserverReceivedConfiguration = remoteConfiguration } @@ -427,7 +427,7 @@ class RemoteConfigManagerTests: XCTestCase { _ = sut.observatoryForReloads.append { result in reloadObserverReceivedConfiguration = result.successValue?.0 } - _ = sut.observatoryForUpdates.append { remoteConfiguration, _, _ in + _ = sut.observatoryForUpdates.append { remoteConfiguration, _, _, _ in updateObserverReceivedConfiguration = remoteConfiguration } diff --git a/Sources/CTR/Interface/AppCoordinator/LaunchStateManager.swift b/Sources/CTR/Interface/AppCoordinator/LaunchStateManager.swift index cd43eba89..7f7e5c577 100644 --- a/Sources/CTR/Interface/AppCoordinator/LaunchStateManager.swift +++ b/Sources/CTR/Interface/AppCoordinator/LaunchStateManager.swift @@ -136,7 +136,7 @@ final class LaunchStateManager: LaunchStateManaging { // Attach behaviours that we want the RemoteConfigManager to perform // each time it refreshes the config in future: - remoteConfigManagerUpdateObserverToken = Current.remoteConfigManager.observatoryForUpdates.append { _, rawData, _ in + remoteConfigManagerUpdateObserverToken = Current.remoteConfigManager.observatoryForUpdates.append { _, rawData, _, _ in // Update the remote config for the crypto library Current.cryptoLibUtility.store(rawData, for: .remoteConfiguration) @@ -169,7 +169,7 @@ final class LaunchStateManager: LaunchStateManaging { // We are within the TTL. Nothing to do. break } - case .success(let (_, _, urlResponse)): + case .success(let (_, _, urlResponse, _)): // Mark remote config loaded Current.cryptoLibUtility.checkFile(.remoteConfiguration) diff --git a/Sources/CTR/Interface/Holder/Dashboard/HolderDashboardViewModel.swift b/Sources/CTR/Interface/Holder/Dashboard/HolderDashboardViewModel.swift index 52edf7165..6dff115be 100644 --- a/Sources/CTR/Interface/Holder/Dashboard/HolderDashboardViewModel.swift +++ b/Sources/CTR/Interface/Holder/Dashboard/HolderDashboardViewModel.swift @@ -93,17 +93,23 @@ final class HolderDashboardViewModel: HolderDashboardViewModelType { /// that allows us to use a `didSet{}` to /// get a callback if any of them are mutated. struct State: Equatable { + enum StrippenRefresherFailMissingCredentialsError: Error { // swiftlint:disable:this type_name + case noInternet + case otherFailureFirstOccurence, otherFailureSubsequentOccurence + } + var qrCards: [QRCard] var expiredGreenCards: [ExpiredQR] var blockedEventItems: [RemovedEventItem] var mismatchedIdentityItems: [RemovedEventItem] var isRefreshingStrippen: Bool + var lastKnownConfigHash: String? // Related to strippen refreshing. // When there's an error with the refreshing process, // we show an error message on each QR card that lacks credentials. // This does not discriminate between domestic/EU. - var errorForQRCardsMissingCredentials: String? + var errorForQRCardsMissingCredentials: StrippenRefresherFailMissingCredentialsError? var deviceHasClockDeviation: Bool = false @@ -211,7 +217,6 @@ final class HolderDashboardViewModel: HolderDashboardViewModelType { // Observation tokens: private var remoteConfigUpdateObserverToken: Observatory.ObserverToken? private var clockDeviationObserverToken: Observatory.ObserverToken? - private var remoteConfigUpdatesConfigurationWarningToken: Observatory.ObserverToken? private var disclosurePolicyUpdateObserverToken: Observatory.ObserverToken? private var configurationAlmostOutOfDateObserverToken: Observatory.ObserverToken? @@ -271,7 +276,6 @@ final class HolderDashboardViewModel: HolderDashboardViewModelType { setupFuzzyMatchingRemovedEventsDatasource() setupStrippenRefresher() setupNotificationListeners() - setupConfigNotificationManager() setupRecommendedVersion() recalculateActiveDisclosurePolicyMode() recalculateDisclosureBannerState() @@ -289,8 +293,10 @@ final class HolderDashboardViewModel: HolderDashboardViewModelType { } // If the config ever changes, reload dependencies: - remoteConfigUpdateObserverToken = Current.remoteConfigManager.observatoryForUpdates.append { [weak self] _, _, _ in + remoteConfigUpdateObserverToken = Current.remoteConfigManager.observatoryForUpdates.append { [weak self] _, _, _, hash in self?.strippenRefresher.load() + self?.setupRecommendedVersion() // Config changed, check recommended version. + self?.state.lastKnownConfigHash = hash } disclosurePolicyUpdateObserverToken = Current.disclosurePolicyManager.observatory.append { [weak self] in @@ -317,7 +323,6 @@ final class HolderDashboardViewModel: HolderDashboardViewModelType { clockDeviationObserverToken.map(Current.clockDeviationManager.observatory.remove) disclosurePolicyUpdateObserverToken.map(Current.disclosurePolicyManager.observatory.remove) remoteConfigUpdateObserverToken.map(Current.remoteConfigManager.observatoryForUpdates.remove) - remoteConfigUpdatesConfigurationWarningToken.map(Current.remoteConfigManager.observatoryForReloads.remove) } // MARK: - Setup @@ -370,14 +375,6 @@ final class HolderDashboardViewModel: HolderDashboardViewModelType { strippenRefresher.load() } - func setupConfigNotificationManager() { - - remoteConfigUpdatesConfigurationWarningToken = Current.remoteConfigManager.observatoryForReloads.append { [weak self] result in - guard let self, case .success = result else { return } - self.setupRecommendedVersion() - } - } - // MARK: - View Lifecycle callbacks: func viewWillAppear() { @@ -461,7 +458,7 @@ final class HolderDashboardViewModel: HolderDashboardViewModelType { case (.noInternet, .expired, true): logDebug("StrippenRefresh: Need refreshing now, but no internet. Showing in UI.") - state.errorForQRCardsMissingCredentials = L.holderDashboardStrippenExpiredErrorfooterNointernet() + state.errorForQRCardsMissingCredentials = .noInternet case (.noInternet, .expiring, true): // Do nothing @@ -478,8 +475,8 @@ final class HolderDashboardViewModel: HolderDashboardViewModelType { checkForMismatchedIdentityError(error: error) state.errorForQRCardsMissingCredentials = refresherState.errorOccurenceCount > 1 - ? L.holderDashboardStrippenExpiredErrorfooterServerHelpdesk(Current.contactInformationProvider.phoneNumberLink) - : L.holderDashboardStrippenExpiredErrorfooterServerTryagain(AppAction.tryAgain) + ? .otherFailureSubsequentOccurence + : .otherFailureFirstOccurence case let (.failed(error), .expiring, _): // In this case we just swallow the server errors. diff --git a/Sources/CTR/Interface/Holder/Dashboard/Logic/HolderDashboardViewModel+Model.swift b/Sources/CTR/Interface/Holder/Dashboard/Logic/HolderDashboardViewModel+Model.swift index 484906abc..91241c617 100644 --- a/Sources/CTR/Interface/Holder/Dashboard/Logic/HolderDashboardViewModel+Model.swift +++ b/Sources/CTR/Interface/Holder/Dashboard/Logic/HolderDashboardViewModel+Model.swift @@ -130,7 +130,15 @@ extension HolderDashboardViewModel { let firstOrigin = firstGreenCard.origins.first else { return .greatestFiniteMagnitude } - return firstOrigin.customSortIndex + // DCCs and CTBs should be grouped when the GreenCards are sorted: + let regionModifier: Double = { + switch self.region { + case .europeanUnion: return 1 + case .netherlands: return 0 + } + }() + + return firstOrigin.customSortIndex + regionModifier } var origins: [GreenCard.Origin] { diff --git a/Sources/CTR/Interface/Holder/Dashboard/Logic/HolderDashboardViewModel+makeVCCards.swift b/Sources/CTR/Interface/Holder/Dashboard/Logic/HolderDashboardViewModel+makeVCCards.swift index 53d553d89..0da046b7b 100644 --- a/Sources/CTR/Interface/Holder/Dashboard/Logic/HolderDashboardViewModel+makeVCCards.swift +++ b/Sources/CTR/Interface/Holder/Dashboard/Logic/HolderDashboardViewModel+makeVCCards.swift @@ -4,6 +4,7 @@ * * SPDX-License-Identifier: EUPL-1.2 */ +// swiftlint:disable file_length import Foundation import Shared @@ -572,7 +573,16 @@ extension HolderDashboardViewModel.QRCard { /// Returns `HolderDashboardViewController.Card.Error`, if appropriate, which configures the display of an error on the QRCardView. private func qrCardError(state: HolderDashboardViewModel.State, actionHandler: HolderDashboardCardUserActionHandling) -> HolderDashboardViewController.Card.Error? { guard let error = state.errorForQRCardsMissingCredentials, shouldShowErrorBeneathCard else { return nil } - return HolderDashboardViewController.Card.Error(message: error, didTapURL: { [weak actionHandler] url in + + let errorMessage: String = { + switch error { + case .noInternet: return L.holderDashboardStrippenExpiredErrorfooterNointernet() + case .otherFailureFirstOccurence: return L.holderDashboardStrippenExpiredErrorfooterServerTryagain(AppAction.tryAgain) + case .otherFailureSubsequentOccurence: return L.holderDashboardStrippenExpiredErrorfooterServerHelpdesk(Current.contactInformationProvider.phoneNumberLink) + } + }() + + return HolderDashboardViewController.Card.Error(message: errorMessage, didTapURL: { [weak actionHandler] url in if url.absoluteString == AppAction.tryAgain { actionHandler?.didTapRetryLoadQRCards() } else {