diff --git a/PIA VPN-tvOS/Dashboard/CompositionRoot/DashboardFactory.swift b/PIA VPN-tvOS/Dashboard/CompositionRoot/DashboardFactory.swift index 2482b55b..db277dea 100644 --- a/PIA VPN-tvOS/Dashboard/CompositionRoot/DashboardFactory.swift +++ b/PIA VPN-tvOS/Dashboard/CompositionRoot/DashboardFactory.swift @@ -37,7 +37,7 @@ extension DashboardFactory { } private static func makeSelectedServerUserCase() -> SelectedServerUseCaseType { - return SelectedServerUseCase(serverProvider: makeServerProvider()) + return SelectedServerUseCase(serverProvider: makeServerProvider(), clientPreferences: Client.preferences) } private static func makeSelectedServerViewModel() -> SelectedServerViewModel { diff --git a/PIA VPN-tvOS/Dashboard/Presentation/QuickConnectViewModel.swift b/PIA VPN-tvOS/Dashboard/Presentation/QuickConnectViewModel.swift index 07c9c013..14bc2eb1 100644 --- a/PIA VPN-tvOS/Dashboard/Presentation/QuickConnectViewModel.swift +++ b/PIA VPN-tvOS/Dashboard/Presentation/QuickConnectViewModel.swift @@ -13,12 +13,10 @@ class QuickConnectViewModel: ObservableObject { selectedServerUseCase: SelectedServerUseCaseType) { self.connectUseCase = connectUseCase self.selectedServerUseCase = selectedServerUseCase - - updateStatus() } func updateStatus() { - servers = selectedServerUseCase.getHistoricalServers() + servers = selectedServerUseCase.getHistoricalServers().reversed() } } diff --git a/PIA VPN-tvOS/Dashboard/UI/QuickConnectView.swift b/PIA VPN-tvOS/Dashboard/UI/QuickConnectView.swift index c3ee3c9f..2ce0b5cf 100644 --- a/PIA VPN-tvOS/Dashboard/UI/QuickConnectView.swift +++ b/PIA VPN-tvOS/Dashboard/UI/QuickConnectView.swift @@ -12,6 +12,8 @@ struct QuickConnectView: View { DashboardFactory.makeQuickConnectButton(for: item, delegate: viewModel) } } + }.onAppear { + viewModel.updateStatus() } } diff --git a/PIA VPN-tvOS/Dashboard/UseCases/SelectedServerUseCase.swift b/PIA VPN-tvOS/Dashboard/UseCases/SelectedServerUseCase.swift index 341a9bc4..2dc2a3c8 100644 --- a/PIA VPN-tvOS/Dashboard/UseCases/SelectedServerUseCase.swift +++ b/PIA VPN-tvOS/Dashboard/UseCases/SelectedServerUseCase.swift @@ -9,23 +9,20 @@ protocol SelectedServerUseCaseType { class SelectedServerUseCase: SelectedServerUseCaseType { - let serverProvider: ServerProviderType + private let serverProvider: ServerProviderType + private let clientPreferences: ClientPreferencesType - init(serverProvider: ServerProviderType) { + init(serverProvider: ServerProviderType, clientPreferences: ClientPreferencesType) { self.serverProvider = serverProvider + self.clientPreferences = clientPreferences } func getSelectedServer() -> ServerType { - // TODO: get real selected server - return automaticServer() + return clientPreferences.selectedServer + } func getHistoricalServers() -> [ServerType] { - // TODO: Remove this `guard` statement when we get - // the real historical servers - guard !serverProvider.historicalServers.isEmpty else { - return Self.generateDemoServers() - } return serverProvider.historicalServers } diff --git a/PIA VPN-tvOS/Navigation/AppRouter.swift b/PIA VPN-tvOS/Navigation/AppRouter.swift index 598ec57c..9b063572 100644 --- a/PIA VPN-tvOS/Navigation/AppRouter.swift +++ b/PIA VPN-tvOS/Navigation/AppRouter.swift @@ -6,7 +6,6 @@ import SwiftUI protocol AppRouterType { var stackCount: Int { get } - func navigate(to destination: any Destinations) func pop() func goBackToRoot() @@ -41,3 +40,39 @@ class AppRouter: ObservableObject, AppRouterType { } } + + +extension AppRouter { + + enum Actions: Equatable { + + case pop(router: AppRouterType) + case goBackToRoot(router: AppRouterType) + case navigate(router: AppRouterType, destination: any Destinations) + + func execute() { + switch self { + case .pop(let router): + router.pop() + case .goBackToRoot(let router): + router.goBackToRoot() + case .navigate(let router, let destination): + router.navigate(to: destination) + } + } + + static func == (lhs: AppRouter.Actions, rhs: AppRouter.Actions) -> Bool { + switch (lhs, rhs) { + case (.pop, .pop): + return true + case (.goBackToRoot, .goBackToRoot): return true + case (.navigate(_, destination: let lhsDestination), .navigate(_, destination: let rhsDestination)): + return lhsDestination.hashValue == rhsDestination.hashValue + default: + return false + } + } + + } + +} diff --git a/PIA VPN-tvOS/RegionsSelection/CompositionRoot/RegionsSelectionFactory.swift b/PIA VPN-tvOS/RegionsSelection/CompositionRoot/RegionsSelectionFactory.swift index 51a73cd2..b101f953 100644 --- a/PIA VPN-tvOS/RegionsSelection/CompositionRoot/RegionsSelectionFactory.swift +++ b/PIA VPN-tvOS/RegionsSelection/CompositionRoot/RegionsSelectionFactory.swift @@ -7,10 +7,11 @@ // import Foundation +import PIALibrary class RegionsSelectionFactory { static func makeRegionsListViewModel() -> RegionsListViewModel { - return RegionsListViewModel(useCase: makeRegionsListUseCase()) + return RegionsListViewModel(useCase: makeRegionsListUseCase(), onServerSelectedRouterAction: .pop(router: AppRouterFactory.makeAppRouter())) } static func makeRegionsListView() -> RegionsListView { @@ -18,6 +19,6 @@ class RegionsSelectionFactory { } static func makeRegionsListUseCase() -> RegionsListUseCaseType { - return RegionsListUseCase(serverProvider: DashboardFactory.makeServerProvider()) + return RegionsListUseCase(serverProvider: DashboardFactory.makeServerProvider(), clientPreferences: Client.preferences) } } diff --git a/PIA VPN-tvOS/RegionsSelection/Presentation/RegionsListViewModel.swift b/PIA VPN-tvOS/RegionsSelection/Presentation/RegionsListViewModel.swift index 7ca02359..65a964ad 100644 --- a/PIA VPN-tvOS/RegionsSelection/Presentation/RegionsListViewModel.swift +++ b/PIA VPN-tvOS/RegionsSelection/Presentation/RegionsListViewModel.swift @@ -12,10 +12,12 @@ import PIALibrary class RegionsListViewModel: ObservableObject { private let useCase: RegionsListUseCaseType + private let onServerSelectedRouterAction: AppRouter.Actions @Published var servers: [ServerType] = [] - init(useCase: RegionsListUseCaseType) { + init(useCase: RegionsListUseCaseType, onServerSelectedRouterAction: AppRouter.Actions) { self.useCase = useCase + self.onServerSelectedRouterAction = onServerSelectedRouterAction refreshRegionsList() } @@ -25,6 +27,7 @@ class RegionsListViewModel: ObservableObject { func didSelectRegionServer(_ server: ServerType) { - // TODO: Implement me: connect to the selected server + useCase.select(server: server) + onServerSelectedRouterAction.execute() } } diff --git a/PIA VPN-tvOS/RegionsSelection/UseCases/RegionsListUseCase.swift b/PIA VPN-tvOS/RegionsSelection/UseCases/RegionsListUseCase.swift index 39626ccd..ee31bc23 100644 --- a/PIA VPN-tvOS/RegionsSelection/UseCases/RegionsListUseCase.swift +++ b/PIA VPN-tvOS/RegionsSelection/UseCases/RegionsListUseCase.swift @@ -10,17 +10,26 @@ import Foundation protocol RegionsListUseCaseType { func getCurrentServers() -> [ServerType] + func select(server: ServerType) } class RegionsListUseCase: RegionsListUseCaseType { - + private let serverProvider: ServerProviderType + private var clientPreferences: ClientPreferencesType - init(serverProvider: ServerProviderType) { + init(serverProvider: ServerProviderType, clientPreferences: ClientPreferencesType) { self.serverProvider = serverProvider + self.clientPreferences = clientPreferences } func getCurrentServers() -> [ServerType] { return serverProvider.currentServers } + + func select(server: ServerType) { + // This triggers a connection + clientPreferences.selectedServer = server + } + } diff --git a/PIA VPN-tvOS/Shared/Utils/PIALibrary+Protocols/ClientPreferences+Protocols.swift b/PIA VPN-tvOS/Shared/Utils/PIALibrary+Protocols/ClientPreferences+Protocols.swift new file mode 100644 index 00000000..f8d87e97 --- /dev/null +++ b/PIA VPN-tvOS/Shared/Utils/PIALibrary+Protocols/ClientPreferences+Protocols.swift @@ -0,0 +1,30 @@ +// +// ClientPreferences+Protocols.swift +// PIA VPN-tvOS +// +// Created by Laura S on 1/18/24. +// Copyright © 2024 Private Internet Access Inc. All rights reserved. +// + +import Foundation +import PIALibrary + +protocol ClientPreferencesType { + var selectedServer: ServerType { get set } +} + +extension Client.Preferences: ClientPreferencesType { + var selectedServer: ServerType { + get { + return displayedServer + } + set { + guard let newServer = newValue as? Server else { return } + displayedServer = newServer + // TODO: Verify whether this is necessary + let pendingPreferences = Client.preferences.editable() + pendingPreferences.commit() + } + } + +} diff --git a/PIA VPN-tvOSTests/Dashboard/QuickConnectViewModelTests.swift b/PIA VPN-tvOSTests/Dashboard/QuickConnectViewModelTests.swift index b6ae228a..79382111 100644 --- a/PIA VPN-tvOSTests/Dashboard/QuickConnectViewModelTests.swift +++ b/PIA VPN-tvOSTests/Dashboard/QuickConnectViewModelTests.swift @@ -30,6 +30,7 @@ final class QuickConnectViewModelTests: XCTestCase { // WHEN showing the Quick Connect section initilizeSut() + sut.updateStatus() // THEN there are 2 Quick Connect buttons displayed XCTAssertEqual(sut.servers.count, 2) diff --git a/PIA VPN-tvOSTests/RegionsList/Mocks/RegionsListUseCaseMock.swift b/PIA VPN-tvOSTests/RegionsList/Mocks/RegionsListUseCaseMock.swift new file mode 100644 index 00000000..506ca4d6 --- /dev/null +++ b/PIA VPN-tvOSTests/RegionsList/Mocks/RegionsListUseCaseMock.swift @@ -0,0 +1,32 @@ +// +// RegionsListUseTypeMock.swift +// PIA VPN-tvOSTests +// +// Created by Laura S on 1/18/24. +// Copyright © 2024 Private Internet Access Inc. All rights reserved. +// + +import Foundation +@testable import PIA_VPN_tvOS + +class RegionsListUseCaseMock: RegionsListUseCaseType { + var getCurrentServersCalled = false + var getCurrentServersCalledAttempt = 0 + var getCurrentServersResult: [ServerType] = [] + func getCurrentServers() -> [ServerType] { + getCurrentServersCalled = true + getCurrentServersCalledAttempt += 1 + return getCurrentServersResult + } + + var selectServerCalled = false + var selectServerCalledAttempt = 0 + var selectServerCalledWithArgument: ServerType? + func select(server: ServerType) { + selectServerCalled = true + selectServerCalledAttempt += 1 + selectServerCalledWithArgument = server + } + + +} diff --git a/PIA VPN-tvOSTests/RegionsList/RegionsListViewModelTests.swift b/PIA VPN-tvOSTests/RegionsList/RegionsListViewModelTests.swift new file mode 100644 index 00000000..2e8181ae --- /dev/null +++ b/PIA VPN-tvOSTests/RegionsList/RegionsListViewModelTests.swift @@ -0,0 +1,64 @@ +// +// RegionsListViewModelTests.swift +// PIA VPN-tvOSTests +// +// Created by Laura S on 1/18/24. +// Copyright © 2024 Private Internet Access Inc. All rights reserved. +// + +import Foundation +import XCTest +@testable import PIA_VPN_tvOS + +class RegionsListViewModelTests: XCTestCase { + class Fixture { + let regionsListUseCaseMock = RegionsListUseCaseMock() + let appRouterSpy = AppRouterSpy() + } + + var fixture: Fixture! + var sut: RegionsListViewModel! + + func instantiateSut(with routerAction: AppRouter.Actions) { + sut = RegionsListViewModel(useCase: fixture.regionsListUseCaseMock, onServerSelectedRouterAction: routerAction) + } + + override func setUp() { + fixture = Fixture() + } + + override func tearDown() { + fixture = nil + } + + + func test_regionServer_didSelect() { + // GIVEN that the Regions list is created + instantiateSut(with: .pop(router: fixture.appRouterSpy)) + + // THEN the useCase is called once to fetch the current servers + XCTAssertTrue(fixture.regionsListUseCaseMock.getCurrentServersCalled) + XCTAssertEqual(fixture.regionsListUseCaseMock.getCurrentServersCalledAttempt, 1) + + // AND the useCase is NOT called to select any server + XCTAssertFalse(fixture.regionsListUseCaseMock.selectServerCalled) + + // AND the AppRouter does not contain any request + XCTAssertEqual(fixture.appRouterSpy.requests, []) + + let selectedServer = ServerMock(name: "server-name", identifier: "server-id", regionIdentifier: "region-id", country: "ES", geo: false) + + // WHEN a server is selected + sut.didSelectRegionServer(selectedServer) + + // THEN the RegionsListUseCase is called once to select the expected server + XCTAssertTrue(fixture.regionsListUseCaseMock.selectServerCalled) + XCTAssertEqual(fixture.regionsListUseCaseMock.selectServerCalledAttempt, 1) + XCTAssertEqual(fixture.regionsListUseCaseMock.selectServerCalledWithArgument!.identifier, selectedServer.identifier) + + // AND the AppRouter is called to pop the current view + XCTAssertEqual(fixture.appRouterSpy.requests, [.pop]) + + } + +} diff --git a/PIA VPN.xcodeproj/project.pbxproj b/PIA VPN.xcodeproj/project.pbxproj index 136e9e9b..11f29659 100644 --- a/PIA VPN.xcodeproj/project.pbxproj +++ b/PIA VPN.xcodeproj/project.pbxproj @@ -196,6 +196,7 @@ 695BF81F2AC410E000D1139C /* (null) in Sources */ = {isa = PBXBuildFile; }; 696E8F0D2B31A8760080BB31 /* NotificationCenterMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 696E8F0C2B31A8760080BB31 /* NotificationCenterMock.swift */; }; 696E8F102B31AC690080BB31 /* RootContainerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 696E8F0F2B31AC690080BB31 /* RootContainerViewModelTests.swift */; }; + 69793DA12B59B96B000CB845 /* RegionsListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69793DA02B59B96B000CB845 /* RegionsListViewModelTests.swift */; }; 697A5F452B514B5700661977 /* AppRouterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 697A5F442B514B5700661977 /* AppRouterFactory.swift */; }; 697A5F4D2B514D9600661977 /* UserActivatedContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 697A5F4C2B514D9600661977 /* UserActivatedContainerView.swift */; }; 697A5F512B514DC500661977 /* UserActivatedContainerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 697A5F502B514DC500661977 /* UserActivatedContainerFactory.swift */; }; @@ -229,6 +230,7 @@ 69B70ABF2ACC2CFE0072A09D /* AccessibilityId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69B70ABD2ACC2CFE0072A09D /* AccessibilityId.swift */; }; 69B70AC02ACC2CFE0072A09D /* AccessibilityId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69B70ABD2ACC2CFE0072A09D /* AccessibilityId.swift */; }; 69C587FD2AD00C6300B95EF9 /* PIAExampleWithAuthenticatedAppTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69C587FC2AD00C6300B95EF9 /* PIAExampleWithAuthenticatedAppTest.swift */; }; + 69CA26B22B59668700E78894 /* RegionsListUseCaseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69CA26B12B59668700E78894 /* RegionsListUseCaseMock.swift */; }; 69E61DE82B55643500085648 /* RegionsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E61DE72B55643500085648 /* RegionsListView.swift */; }; 69E61DEA2B55644900085648 /* RegionsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E61DE92B55644900085648 /* RegionsListViewModel.swift */; }; 69E61DEC2B5564C600085648 /* RegionsSelectionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E61DEB2B5564C600085648 /* RegionsSelectionFactory.swift */; }; @@ -241,6 +243,9 @@ 69F1C2972B2B239000E924AE /* PIAConnectionButtonViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F1C2962B2B239000E924AE /* PIAConnectionButtonViewModel.swift */; }; 69F1C2992B2B28DA00E924AE /* PIAConnectionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F1C2982B2B28DA00E924AE /* PIAConnectionButton.swift */; }; 69F1C29C2B2B299300E924AE /* VpnConnectionUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F1C29B2B2B299300E924AE /* VpnConnectionUseCase.swift */; }; + 69F675AA2B592ED6000E31D5 /* ClientPreferences+Protocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F675A92B592ED6000E31D5 /* ClientPreferences+Protocols.swift */; }; + 69F675AB2B592FA0000E31D5 /* Server+Automatic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EFDC1CB1FE35C9A007C0B9B /* Server+Automatic.swift */; }; + 69F675AD2B593437000E31D5 /* ServerProvider+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB966731FDF0D6E0086ABC2 /* ServerProvider+UI.swift */; }; 69FF0B042B3AD1D40074AA04 /* SelectedServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69FF0B032B3AD1D40074AA04 /* SelectedServerView.swift */; }; 69FF0B062B3AD2860074AA04 /* QuickConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69FF0B052B3AD2860074AA04 /* QuickConnectView.swift */; }; 69FF0B082B3AD3E60074AA04 /* SelectedServerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69FF0B072B3AD3E60074AA04 /* SelectedServerViewModel.swift */; }; @@ -975,6 +980,7 @@ 694AC74D2B17AB9C007E7B56 /* DashboardView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = ""; tabWidth = 4; }; 696E8F0C2B31A8760080BB31 /* NotificationCenterMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenterMock.swift; sourceTree = ""; }; 696E8F0F2B31AC690080BB31 /* RootContainerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootContainerViewModelTests.swift; sourceTree = ""; }; + 69793DA02B59B96B000CB845 /* RegionsListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionsListViewModelTests.swift; sourceTree = ""; }; 697A5F442B514B5700661977 /* AppRouterFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouterFactory.swift; sourceTree = ""; }; 697A5F4C2B514D9600661977 /* UserActivatedContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivatedContainerView.swift; sourceTree = ""; }; 697A5F502B514DC500661977 /* UserActivatedContainerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivatedContainerFactory.swift; sourceTree = ""; }; @@ -1001,6 +1007,7 @@ 69B70AB42ACBF51C0072A09D /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreen.swift; sourceTree = ""; }; 69B70ABD2ACC2CFE0072A09D /* AccessibilityId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityId.swift; sourceTree = ""; }; 69C587FC2AD00C6300B95EF9 /* PIAExampleWithAuthenticatedAppTest.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = PIAExampleWithAuthenticatedAppTest.swift; sourceTree = ""; tabWidth = 2; }; + 69CA26B12B59668700E78894 /* RegionsListUseCaseMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionsListUseCaseMock.swift; sourceTree = ""; }; 69E61DE72B55643500085648 /* RegionsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionsListView.swift; sourceTree = ""; }; 69E61DE92B55644900085648 /* RegionsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionsListViewModel.swift; sourceTree = ""; }; 69E61DEB2B5564C600085648 /* RegionsSelectionFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionsSelectionFactory.swift; sourceTree = ""; }; @@ -1013,6 +1020,7 @@ 69F1C2962B2B239000E924AE /* PIAConnectionButtonViewModel.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = PIAConnectionButtonViewModel.swift; sourceTree = ""; tabWidth = 4; }; 69F1C2982B2B28DA00E924AE /* PIAConnectionButton.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = PIAConnectionButton.swift; sourceTree = ""; tabWidth = 4; }; 69F1C29B2B2B299300E924AE /* VpnConnectionUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VpnConnectionUseCase.swift; sourceTree = ""; }; + 69F675A92B592ED6000E31D5 /* ClientPreferences+Protocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ClientPreferences+Protocols.swift"; sourceTree = ""; }; 69FF0B032B3AD1D40074AA04 /* SelectedServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedServerView.swift; sourceTree = ""; }; 69FF0B052B3AD2860074AA04 /* QuickConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectView.swift; sourceTree = ""; }; 69FF0B072B3AD3E60074AA04 /* SelectedServerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedServerViewModel.swift; sourceTree = ""; }; @@ -1923,6 +1931,23 @@ path = Mocks; sourceTree = ""; }; + 69793D9E2B59B94A000CB845 /* RegionsList */ = { + isa = PBXGroup; + children = ( + 69793D9F2B59B955000CB845 /* Mocks */, + 69793DA02B59B96B000CB845 /* RegionsListViewModelTests.swift */, + ); + path = RegionsList; + sourceTree = ""; + }; + 69793D9F2B59B955000CB845 /* Mocks */ = { + isa = PBXGroup; + children = ( + 69CA26B12B59668700E78894 /* RegionsListUseCaseMock.swift */, + ); + path = Mocks; + sourceTree = ""; + }; 697A5F432B514B3D00661977 /* CompositionRoot */ = { isa = PBXGroup; children = ( @@ -2005,6 +2030,7 @@ 69A226B62B3079D00065EDDB /* Utils */ = { isa = PBXGroup; children = ( + 69F675A82B592EAF000E31D5 /* PIALibrary+Protocols */, 69A226B72B3079EA0065EDDB /* PIA+String.swift */, 6993119F2B31894D00D316C8 /* Foundation+Protocols.swift */, 6913179E2B32E4D4009B4E85 /* PIALibrary+Protocols.swift */, @@ -2160,6 +2186,14 @@ path = UseCases; sourceTree = ""; }; + 69F675A82B592EAF000E31D5 /* PIALibrary+Protocols */ = { + isa = PBXGroup; + children = ( + 69F675A92B592ED6000E31D5 /* ClientPreferences+Protocols.swift */, + ); + path = "PIALibrary+Protocols"; + sourceTree = ""; + }; 82183D8425014F940033023F /* Menu */ = { isa = PBXGroup; children = ( @@ -2611,6 +2645,7 @@ E5C507892B0E145100200A6A /* PIA VPN-tvOSTests */ = { isa = PBXGroup; children = ( + 69793D9E2B59B94A000CB845 /* RegionsList */, E5AB28AF2B34E06000744E5F /* VPNConfigurationInstalling */, 696E8F132B31AF110080BB31 /* Common */, 696E8F0E2B31AC3C0080BB31 /* RootContainer */, @@ -3877,6 +3912,7 @@ 6913179F2B32E4D4009B4E85 /* PIALibrary+Protocols.swift in Sources */, 69FF0B082B3AD3E60074AA04 /* SelectedServerViewModel.swift in Sources */, 69FF0B0B2B3AD4C70074AA04 /* SwiftGen+Strings.swift in Sources */, + 69F675AA2B592ED6000E31D5 /* ClientPreferences+Protocols.swift in Sources */, 69FF0B062B3AD2860074AA04 /* QuickConnectView.swift in Sources */, E5AB28B42B35AA8C00744E5F /* VPNConfigurationInstallingFactory.swift in Sources */, E5AB28A52B3224C300744E5F /* InstallVPNConfigurationUseCase.swift in Sources */, @@ -3885,6 +3921,7 @@ E5AB28732B279B7000744E5F /* LoginProvider.swift in Sources */, E5C507A52B153A2F00200A6A /* LoginViewModel.swift in Sources */, 693CA5AE2B3ED75500D38378 /* Flags.swift in Sources */, + 69F675AB2B592FA0000E31D5 /* Server+Automatic.swift in Sources */, E5C507B72B17E75B00200A6A /* CheckLoginAvailability.swift in Sources */, E5AB288E2B2A489500744E5F /* LoginStatus.swift in Sources */, 69FF0B132B3AF2050074AA04 /* QuickConnectButtonViewModel.swift in Sources */, @@ -3899,6 +3936,7 @@ E5C507A22B0FE40000200A6A /* LoginView.swift in Sources */, 693CA5AD2B3ED74100D38378 /* AppConfiguration.swift in Sources */, 69E61DE82B55643500085648 /* RegionsListView.swift in Sources */, + 69F675AD2B593437000E31D5 /* ServerProvider+UI.swift in Sources */, E5AB287D2B28E4E700744E5F /* UserAccountMapper.swift in Sources */, 697A5F512B514DC500661977 /* UserActivatedContainerFactory.swift in Sources */, 69FF0B0A2B3AD3F60074AA04 /* QuickConnectViewModel.swift in Sources */, @@ -3956,6 +3994,7 @@ E5AB28E22B4C107F00744E5F /* InstallVPNConfigurationUseCaseMock.swift in Sources */, E5AB28D02B4B39F000744E5F /* InstallVpnConfigurationProviderTests.swift in Sources */, 698C3B492B2B33650012D527 /* VpnConnectionUseCaseMock.swift in Sources */, + 69CA26B22B59668700E78894 /* RegionsListUseCaseMock.swift in Sources */, E5C507CC2B1FACE000200A6A /* LoginWithCredentialsUseCaseTests.swift in Sources */, 696E8F0D2B31A8760080BB31 /* NotificationCenterMock.swift in Sources */, E5C507A82B153E6B00200A6A /* LoginViewModelTests.swift in Sources */, @@ -3980,6 +4019,7 @@ E52E69002B56ABE400471913 /* AppRouterSpy.swift in Sources */, E5AB28E12B4C107F00744E5F /* VpnConfigurationProviderTypeMock.swift in Sources */, E5C507C42B1F72E700200A6A /* CheckLoginAvailabilityTests.swift in Sources */, + 69793DA12B59B96B000CB845 /* RegionsListViewModelTests.swift in Sources */, E5C507C02B1F700C00200A6A /* CheckLoginAvailabilityMock.swift in Sources */, 691317A12B32ED76009B4E85 /* AccountProviderTypeMock.swift in Sources */, );