diff --git a/PIA VPN-tvOS/Dashboard/CompositionRoot/DashboardFactory.swift b/PIA VPN-tvOS/Dashboard/CompositionRoot/DashboardFactory.swift index fb0c75e2..86aafc16 100644 --- a/PIA VPN-tvOS/Dashboard/CompositionRoot/DashboardFactory.swift +++ b/PIA VPN-tvOS/Dashboard/CompositionRoot/DashboardFactory.swift @@ -1,14 +1,23 @@ import Foundation +import PIALibrary class DashboardFactory { static func makeDashboardView() -> DashboardView { return DashboardView( + viewModel: makeDashboardViewModel(), connectionButton: makePIAConnectionButton() ) } + static func makeDashboardViewModel() -> DashboardViewModel { + guard let defaultAccountProvider = Client.providers.accountProvider as? DefaultAccountProvider else { + fatalError("Incorrect account provider type") + } + return DashboardViewModel(accountProvider: defaultAccountProvider) + } + static func makePIAConnectionButton(size: CGFloat = 160, lineWidth: CGFloat = 6) -> PIAConnectionButton { return PIAConnectionButton( size: size, diff --git a/PIA VPN-tvOS/Dashboard/Presentation/DashboardViewModel.swift b/PIA VPN-tvOS/Dashboard/Presentation/DashboardViewModel.swift new file mode 100644 index 00000000..7e82d389 --- /dev/null +++ b/PIA VPN-tvOS/Dashboard/Presentation/DashboardViewModel.swift @@ -0,0 +1,17 @@ + + +import Foundation + +class DashboardViewModel: ObservableObject { + + let accountProvider: AccountProviderType + + init(accountProvider: AccountProviderType) { + self.accountProvider = accountProvider + } + + func logOut() { + accountProvider.logout(nil) + } + +} diff --git a/PIA VPN-tvOS/Dashboard/UI/DashboardView.swift b/PIA VPN-tvOS/Dashboard/UI/DashboardView.swift index ba66827f..a6ed2b3f 100644 --- a/PIA VPN-tvOS/Dashboard/UI/DashboardView.swift +++ b/PIA VPN-tvOS/Dashboard/UI/DashboardView.swift @@ -5,6 +5,7 @@ struct DashboardView: View { let viewWidth = UIScreen.main.bounds.width let viewHeight = UIScreen.main.bounds.height + let viewModel: DashboardViewModel let connectionButton: PIAConnectionButton var body: some View { @@ -12,6 +13,14 @@ struct DashboardView: View { VStack(spacing: 20) { connectionButton .padding() + + Button { + viewModel.logOut() + } label: { + Text("LogOut") + } + .padding() + } .frame(maxWidth: (viewWidth/2)) .padding() @@ -25,5 +34,8 @@ struct DashboardView: View { } #Preview { - DashboardView(connectionButton: DashboardFactory.makePIAConnectionButton()) + DashboardView( + viewModel: DashboardFactory.makeDashboardViewModel(), + connectionButton: DashboardFactory.makePIAConnectionButton() + ) } diff --git a/PIA VPN-tvOS/Navigation/AppRouter.swift b/PIA VPN-tvOS/Navigation/AppRouter.swift new file mode 100644 index 00000000..ca9f4a82 --- /dev/null +++ b/PIA VPN-tvOS/Navigation/AppRouter.swift @@ -0,0 +1,36 @@ + +import Foundation + +import SwiftUI + +typealias Destinations = Hashable + +// AppRouter enables any component to navigate the user to any screen defined within Destinations +class AppRouter: ObservableObject { + + static let shared: AppRouter = AppRouter(with: NavigationPath()) + + @Published public var path: NavigationPath + + /// Returns the amount of stacked views. Useful during unit test validation + var stackCount: Int { + path.count + } + + init(with path: NavigationPath) { + self.path = path + } + + func navigate(to destination: any Destinations) { + path.append(destination) + } + + func pop() { + path.removeLast() + } + + func goBackToRoot() { + path.removeLast(path.count) + } + +} diff --git a/PIA VPN-tvOS/PIA_VPN_tvOSApp.swift b/PIA VPN-tvOS/PIA_VPN_tvOSApp.swift index 61a8b0ca..28d5324a 100644 --- a/PIA VPN-tvOS/PIA_VPN_tvOSApp.swift +++ b/PIA VPN-tvOS/PIA_VPN_tvOSApp.swift @@ -12,8 +12,7 @@ import SwiftUI struct PIA_VPN_tvOSApp: App { var body: some Scene { WindowGroup { - LoginFactory.makeLoginView() -// DashboardFactory.makeDashboardView() + RootContainerFactory.makeRootContainerView() } } } diff --git a/PIA VPN-tvOS/RootContainer/CompositionRoot/RootContainerFactory.swift b/PIA VPN-tvOS/RootContainer/CompositionRoot/RootContainerFactory.swift new file mode 100644 index 00000000..3d0ad3ed --- /dev/null +++ b/PIA VPN-tvOS/RootContainer/CompositionRoot/RootContainerFactory.swift @@ -0,0 +1,16 @@ + +import Foundation +import PIALibrary + +class RootContainerFactory { + static func makeRootContainerView() -> RootContainerView { + RootContainerView(viewModel: makeRootContainerViewModel()) + } + + private static func makeRootContainerViewModel() -> RootContainerViewModel { + guard let defaultAccountProvider = Client.providers.accountProvider as? DefaultAccountProvider else { + fatalError("Incorrect account provider type") + } + return RootContainerViewModel(accountProvider: defaultAccountProvider) + } +} diff --git a/PIA VPN-tvOS/RootContainer/Presentation/RootContainerViewModel.swift b/PIA VPN-tvOS/RootContainer/Presentation/RootContainerViewModel.swift new file mode 100644 index 00000000..050768a0 --- /dev/null +++ b/PIA VPN-tvOS/RootContainer/Presentation/RootContainerViewModel.swift @@ -0,0 +1,62 @@ + +import Foundation +import SwiftUI + +class RootContainerViewModel: ObservableObject { + enum State { + case splash + case notActivated + case activatedNotOnboarded + case activated + } + + @Published var state: State = .splash + + // TODO: Update this value from the Vpn OnBoarding installation profile screen + @AppStorage(.kOnboardingVpnProfileInstalled) var onBoardingVpnProfileInstalled = true + + let accountProvider: AccountProviderType + let notificationCenter: NotificationCenterType + + init(accountProvider: AccountProviderType, notificationCenter: NotificationCenterType = NotificationCenter.default) { + + self.accountProvider = accountProvider + self.notificationCenter = notificationCenter + updateState() + subscribeToAccountUpdates() + } + + deinit { + notificationCenter.removeObserver(self) + } + + private func updateState() { + switch (accountProvider.isLoggedIn, onBoardingVpnProfileInstalled) { + // logged in, vpn profile installed + case (true, true): + state = .activated + // logged in, vpn profile not installed + case (true, false): + state = .activatedNotOnboarded + // not logged in, any + case (false, _): + state = .notActivated + } + } + + private func subscribeToAccountUpdates() { + notificationCenter.addObserver(self, selector: #selector(handleAccountDidLogin), name: .PIAAccountDidLogin, object: nil) + + notificationCenter.addObserver(self, selector: #selector(handleAccountDidLogout), name: .PIAAccountDidLogout, object: nil) + } + + @objc func handleAccountDidLogin() { + updateState() + } + + @objc func handleAccountDidLogout() { + updateState() + } + + +} diff --git a/PIA VPN-tvOS/RootContainer/UI/RootContainerView.swift b/PIA VPN-tvOS/RootContainer/UI/RootContainerView.swift new file mode 100644 index 00000000..d3aa159a --- /dev/null +++ b/PIA VPN-tvOS/RootContainer/UI/RootContainerView.swift @@ -0,0 +1,28 @@ + +import Foundation +import SwiftUI + +struct RootContainerView: View { + @ObservedObject var viewModel: RootContainerViewModel + + var body: some View { + switch viewModel.state { + case .splash: + VStack { + // TODO: Add Splash screen here + } + case .notActivated: + LoginFactory.makeLoginView() + case .activatedNotOnboarded: + // TODO: Replace this view with the Onboarding Vpn Profile installation view + VStack { + Text("Show Onboarding vpn installation view") + } + case .activated: + DashboardFactory.makeDashboardView() + } + } + + +} + diff --git a/PIA VPN-tvOS/Shared/Utils/Foundation+Protocols.swift b/PIA VPN-tvOS/Shared/Utils/Foundation+Protocols.swift new file mode 100644 index 00000000..4eb18968 --- /dev/null +++ b/PIA VPN-tvOS/Shared/Utils/Foundation+Protocols.swift @@ -0,0 +1,20 @@ + +import Foundation + +/// Expose Core Foundation APIs via protocols to the PIA app + + +protocol NotificationCenterType { + func addObserver( + _ observer: Any, + selector aSelector: Selector, + name aName: NSNotification.Name?, + object anObject: Any? + ) + + func removeObserver(_ observer: Any) + + // Add methods here from NSNotificationCenter as needed +} + +extension NotificationCenter: NotificationCenterType {} diff --git a/PIA VPN-tvOS/Shared/Utils/PIA+String.swift b/PIA VPN-tvOS/Shared/Utils/PIA+String.swift new file mode 100644 index 00000000..5380ae5b --- /dev/null +++ b/PIA VPN-tvOS/Shared/Utils/PIA+String.swift @@ -0,0 +1,6 @@ + +import Foundation + +extension String { + static let kOnboardingVpnProfileInstalled = "kOnboardingVpnProfileInstalled" +} diff --git a/PIA VPN-tvOS/Shared/Utils/PIALibrary+Protocols.swift b/PIA VPN-tvOS/Shared/Utils/PIALibrary+Protocols.swift new file mode 100644 index 00000000..eb018ca1 --- /dev/null +++ b/PIA VPN-tvOS/Shared/Utils/PIALibrary+Protocols.swift @@ -0,0 +1,11 @@ + +import Foundation +import PIALibrary + + +protocol AccountProviderType { + var isLoggedIn: Bool { get } + func logout(_ callback: ((Error?) -> Void)?) +} + +extension DefaultAccountProvider: AccountProviderType { } diff --git a/PIA VPN-tvOSTests/Common/Mocks/AccountProviderTypeMock.swift b/PIA VPN-tvOSTests/Common/Mocks/AccountProviderTypeMock.swift new file mode 100644 index 00000000..51289927 --- /dev/null +++ b/PIA VPN-tvOSTests/Common/Mocks/AccountProviderTypeMock.swift @@ -0,0 +1,23 @@ +// +// AccountProviderTypeMock.swift +// PIA VPN-tvOSTests +// +// Created by Laura S on 12/20/23. +// Copyright © 2023 Private Internet Access Inc. All rights reserved. +// + +import Foundation +@testable import PIA_VPN_tvOS + +class AccountProviderTypeMock: AccountProviderType { + var isLoggedIn: Bool = false + + private(set) var logoutCalled = false + private(set) var logoutCalledAttempt = 0 + func logout(_ callback: ((Error?) -> Void)?) { + logoutCalled = true + logoutCalledAttempt += 1 + } + + +} diff --git a/PIA VPN-tvOSTests/Dashboard/Mocks/NotificationCenterMock.swift b/PIA VPN-tvOSTests/Dashboard/Mocks/NotificationCenterMock.swift new file mode 100644 index 00000000..0591f59f --- /dev/null +++ b/PIA VPN-tvOSTests/Dashboard/Mocks/NotificationCenterMock.swift @@ -0,0 +1,23 @@ + +import Foundation +@testable import PIA_VPN_tvOS + +class NotificationCenterMock: NotificationCenterType { + private(set) var addObserverCalled = false + private(set) var addObserverCalledAttempt = 0 + private(set) var addObserverCalledWithNotificationName: NSNotification.Name? = nil + + func addObserver(_ observer: Any, selector aSelector: Selector, name aName: NSNotification.Name?, object anObject: Any?) { + addObserverCalled = true + addObserverCalledAttempt += 1 + addObserverCalledWithNotificationName = aName + } + + private(set) var removeObserverCalled = false + private(set) var remoververCalledAttempt = 0 + func removeObserver(_ observer: Any) { + removeObserverCalled = true + remoververCalledAttempt += 1 + } + +} diff --git a/PIA VPN-tvOSTests/Dashboard/Mocks/VpnConnectionUseCaseTypeMock.swift b/PIA VPN-tvOSTests/Dashboard/Mocks/VpnConnectionUseCaseMock.swift similarity index 100% rename from PIA VPN-tvOSTests/Dashboard/Mocks/VpnConnectionUseCaseTypeMock.swift rename to PIA VPN-tvOSTests/Dashboard/Mocks/VpnConnectionUseCaseMock.swift diff --git a/PIA VPN-tvOSTests/RootContainer/RootContainerViewModelTests.swift b/PIA VPN-tvOSTests/RootContainer/RootContainerViewModelTests.swift new file mode 100644 index 00000000..02003b57 --- /dev/null +++ b/PIA VPN-tvOSTests/RootContainer/RootContainerViewModelTests.swift @@ -0,0 +1,79 @@ +// +// RootContainerViewModelTests.swift +// PIA VPN-tvOSTests +// +// Created by Laura S on 12/19/23. +// Copyright © 2023 Private Internet Access Inc. All rights reserved. +// + +import XCTest +@testable import PIA_VPN_tvOS + +final class RootContainerViewModelTests: XCTestCase { + + final class Fixture { + let accountProvierMock = AccountProviderTypeMock() + let notificationCenterMock = NotificationCenterMock() + } + + var fixture: Fixture! + var sut: RootContainerViewModel! + + override func setUp() { + fixture = Fixture() + } + + override func tearDown() { + fixture = nil + sut = nil + UserDefaults.standard.removeObject(forKey: .kOnboardingVpnProfileInstalled) + } + + private func initializeSut() { + sut = RootContainerViewModel(accountProvider: fixture.accountProvierMock, notificationCenter: fixture.notificationCenterMock) + } + + func testState_WhenUserIsNotAuthenticated() { + // GIVEN that the user is not logged in + fixture.accountProvierMock.isLoggedIn = false + + // WHEN the RootContainer is created + initializeSut() + + // THEN the state becomes 'notActivated' + XCTAssertEqual(sut.state, .notActivated) + } + + func testState_WhenUserIsAuthenticatedAndVpnProfileNotInstalled() { + // GIVEN that the user is logged in + fixture.accountProvierMock.isLoggedIn = true + // AND GIVEN that the Onboarding Vpn Profile is NOT installed + stubOnboardingVpnInstallation(finished: false) + + // WHEN the RootContainer is created + initializeSut() + + // THEN the state becomes 'activatedNotOnboarded' + XCTAssertEqual(sut.state, .activatedNotOnboarded) + } + + func testState_WhenUserIsAuthenticatedAndVpnProfileInstalled() { + // GIVEN that the user is logged in + fixture.accountProvierMock.isLoggedIn = true + // AND GIVEN that the Onboarding Vpn Profile is installed + stubOnboardingVpnInstallation(finished: true) + + // WHEN the RootContainer is created + initializeSut() + + // THEN the state becomes 'activated' + XCTAssertEqual(sut.state, .activated) + } +} + + +extension RootContainerViewModelTests { + private func stubOnboardingVpnInstallation(finished: Bool) { + UserDefaults.standard.setValue(finished, forKey: .kOnboardingVpnProfileInstalled) + } +} diff --git a/PIA VPN.xcodeproj/project.pbxproj b/PIA VPN.xcodeproj/project.pbxproj index d785aa1c..2fe3ea70 100644 --- a/PIA VPN.xcodeproj/project.pbxproj +++ b/PIA VPN.xcodeproj/project.pbxproj @@ -162,6 +162,8 @@ 35EDD65A2B035B51007B9ACB /* SideMenuScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35EDD6592B035B51007B9ACB /* SideMenuScreen.swift */; }; 35EDD65C2B047839007B9ACB /* QuickSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35EDD65B2B047839007B9ACB /* QuickSettingsTests.swift */; }; 35EDD65E2B048C68007B9ACB /* QuickSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35EDD65D2B048C68007B9ACB /* QuickSettingsScreen.swift */; }; + 6913179F2B32E4D4009B4E85 /* PIALibrary+Protocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6913179E2B32E4D4009B4E85 /* PIALibrary+Protocols.swift */; }; + 691317A12B32ED76009B4E85 /* AccountProviderTypeMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691317A02B32ED76009B4E85 /* AccountProviderTypeMock.swift */; }; 6924831A2AB045A5002A0407 /* PIAWidgetAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 692483192AB045A5002A0407 /* PIAWidgetAttributes.swift */; }; 6924831B2AB045A5002A0407 /* PIAWidgetAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 692483192AB045A5002A0407 /* PIAWidgetAttributes.swift */; }; 6924831C2AB045A5002A0407 /* PIAWidgetAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 692483192AB045A5002A0407 /* PIAWidgetAttributes.swift */; }; @@ -173,7 +175,9 @@ 694AC74F2B17ADF9007E7B56 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 291C6397183EBC210039EC03 /* Images.xcassets */; }; 695BF81D2AC30EFB00D1139C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0E7EC043209326E30029811E /* Localizable.strings */; }; 695BF81F2AC410E000D1139C /* (null) in Sources */ = {isa = PBXBuildFile; }; - 698C3B492B2B33650012D527 /* VpnConnectionUseCaseTypeMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698C3B482B2B33650012D527 /* VpnConnectionUseCaseTypeMock.swift */; }; + 696E8F0D2B31A8760080BB31 /* NotificationCenterMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 696E8F0C2B31A8760080BB31 /* NotificationCenterMock.swift */; }; + 696E8F102B31AC690080BB31 /* RootContainerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 696E8F0F2B31AC690080BB31 /* RootContainerViewModelTests.swift */; }; + 698C3B492B2B33650012D527 /* VpnConnectionUseCaseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698C3B482B2B33650012D527 /* VpnConnectionUseCaseMock.swift */; }; 698C3B4B2B2B34760012D527 /* PIAConnectionButtonViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698C3B4A2B2B34760012D527 /* PIAConnectionButtonViewModelTests.swift */; }; 698C3B4E2B2B3CBE0012D527 /* LoginIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AB28742B279BB600744E5F /* LoginIntegrationTests.swift */; }; 698F4F2B2AB8A2080010B2B0 /* PIAWidgetExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 8269A6D5251CB5E0000B4DBF /* PIAWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -182,6 +186,8 @@ 698F4F302ABA1DA10010B2B0 /* PIAConnectionLiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698F4F2F2ABA1DA10010B2B0 /* PIAConnectionLiveActivityManager.swift */; }; 698F4F312ABA1DA10010B2B0 /* PIAConnectionLiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698F4F2F2ABA1DA10010B2B0 /* PIAConnectionLiveActivityManager.swift */; }; 698F4F322ABA1DA10010B2B0 /* PIAConnectionLiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698F4F2F2ABA1DA10010B2B0 /* PIAConnectionLiveActivityManager.swift */; }; + 699311A02B31894D00D316C8 /* Foundation+Protocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6993119F2B31894D00D316C8 /* Foundation+Protocols.swift */; }; + 699311A22B318F6B00D316C8 /* DashboardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699311A12B318F6B00D316C8 /* DashboardViewModel.swift */; }; 699F23B22AFBA66000EBC5E6 /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699F23AC2AFBA66000EBC5E6 /* SettingsScreen.swift */; }; 699F23B32AFBA66000EBC5E6 /* HelpSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699F23AD2AFBA66000EBC5E6 /* HelpSettingsScreen.swift */; }; 699F23B42AFBA66000EBC5E6 /* AutomationSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699F23AE2AFBA66000EBC5E6 /* AutomationSettingsScreen.swift */; }; @@ -189,6 +195,11 @@ 699F23B62AFBA66000EBC5E6 /* GeneralSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699F23B02AFBA66000EBC5E6 /* GeneralSettingsScreen.swift */; }; 699F23B72AFBA66000EBC5E6 /* PrivacySettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699F23B12AFBA66000EBC5E6 /* PrivacySettingsScreen.swift */; }; 699F23B92AFBAC0B00EBC5E6 /* SettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699F23B82AFBAC0B00EBC5E6 /* SettingsTests.swift */; }; + 69A226B32B3074D30065EDDB /* AppRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69A226B22B3074D30065EDDB /* AppRouter.swift */; }; + 69A226B52B3075AB0065EDDB /* RootContainerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69A226B42B3075AB0065EDDB /* RootContainerViewModel.swift */; }; + 69A226B82B3079EA0065EDDB /* PIA+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69A226B72B3079EA0065EDDB /* PIA+String.swift */; }; + 69A226BE2B307D5F0065EDDB /* RootContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69A226BD2B307D5F0065EDDB /* RootContainerView.swift */; }; + 69A226C02B307DBF0065EDDB /* RootContainerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69A226BF2B307DBF0065EDDB /* RootContainerFactory.swift */; }; 69B70AB52ACBF51C0072A09D /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69B70AB42ACBF51C0072A09D /* LoginScreen.swift */; }; 69B70ABC2ACBF8300072A09D /* CredentialsUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EC2972D27D8B8580061C56A /* CredentialsUtil.swift */; }; 69B70ABE2ACC2CFE0072A09D /* AccessibilityId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69B70ABD2ACC2CFE0072A09D /* AccessibilityId.swift */; }; @@ -863,6 +874,8 @@ 35EDD6592B035B51007B9ACB /* SideMenuScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuScreen.swift; sourceTree = ""; }; 35EDD65B2B047839007B9ACB /* QuickSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickSettingsTests.swift; sourceTree = ""; }; 35EDD65D2B048C68007B9ACB /* QuickSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickSettingsScreen.swift; sourceTree = ""; }; + 6913179E2B32E4D4009B4E85 /* PIALibrary+Protocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PIALibrary+Protocols.swift"; sourceTree = ""; }; + 691317A02B32ED76009B4E85 /* AccountProviderTypeMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountProviderTypeMock.swift; sourceTree = ""; }; 692483192AB045A5002A0407 /* PIAWidgetAttributes.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = PIAWidgetAttributes.swift; sourceTree = ""; tabWidth = 4; }; 6924831D2AB04FFD002A0407 /* PIAWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PIAWidgetBundle.swift; sourceTree = ""; }; 6924831F2AB05F18002A0407 /* PIAConnectionView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = PIAConnectionView.swift; sourceTree = ""; tabWidth = 4; }; @@ -870,10 +883,14 @@ 692483252AB05F85002A0407 /* PIAConnectionActivityWidget.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = PIAConnectionActivityWidget.swift; sourceTree = ""; tabWidth = 4; }; 6947AADB2ACDC8AE001BCC66 /* PIA-VPN-e2e-simulator.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "PIA-VPN-e2e-simulator.xctestplan"; sourceTree = ""; }; 694AC74D2B17AB9C007E7B56 /* DashboardView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = ""; tabWidth = 4; }; - 698C3B482B2B33650012D527 /* VpnConnectionUseCaseTypeMock.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = VpnConnectionUseCaseTypeMock.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 = ""; }; + 698C3B482B2B33650012D527 /* VpnConnectionUseCaseMock.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = VpnConnectionUseCaseMock.swift; sourceTree = ""; tabWidth = 4; }; 698C3B4A2B2B34760012D527 /* PIAConnectionButtonViewModelTests.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = PIAConnectionButtonViewModelTests.swift; sourceTree = ""; tabWidth = 4; }; 698F4F2C2AB978BF0010B2B0 /* PIACircleImageView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = PIACircleImageView.swift; sourceTree = ""; tabWidth = 4; }; 698F4F2F2ABA1DA10010B2B0 /* PIAConnectionLiveActivityManager.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = PIAConnectionLiveActivityManager.swift; sourceTree = ""; tabWidth = 4; }; + 6993119F2B31894D00D316C8 /* Foundation+Protocols.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "Foundation+Protocols.swift"; sourceTree = ""; tabWidth = 4; }; + 699311A12B318F6B00D316C8 /* DashboardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardViewModel.swift; sourceTree = ""; }; 699F23AC2AFBA66000EBC5E6 /* SettingsScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = ""; }; 699F23AD2AFBA66000EBC5E6 /* HelpSettingsScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelpSettingsScreen.swift; sourceTree = ""; }; 699F23AE2AFBA66000EBC5E6 /* AutomationSettingsScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutomationSettingsScreen.swift; sourceTree = ""; }; @@ -881,6 +898,11 @@ 699F23B02AFBA66000EBC5E6 /* GeneralSettingsScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralSettingsScreen.swift; sourceTree = ""; }; 699F23B12AFBA66000EBC5E6 /* PrivacySettingsScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacySettingsScreen.swift; sourceTree = ""; }; 699F23B82AFBAC0B00EBC5E6 /* SettingsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTests.swift; sourceTree = ""; }; + 69A226B22B3074D30065EDDB /* AppRouter.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = AppRouter.swift; sourceTree = ""; tabWidth = 4; }; + 69A226B42B3075AB0065EDDB /* RootContainerViewModel.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = RootContainerViewModel.swift; sourceTree = ""; tabWidth = 4; }; + 69A226B72B3079EA0065EDDB /* PIA+String.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "PIA+String.swift"; sourceTree = ""; tabWidth = 4; }; + 69A226BD2B307D5F0065EDDB /* RootContainerView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = RootContainerView.swift; sourceTree = ""; tabWidth = 4; }; + 69A226BF2B307DBF0065EDDB /* RootContainerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootContainerFactory.swift; sourceTree = ""; }; 69B70AB02ACBF51C0072A09D /* PIA-VPN_E2E_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "PIA-VPN_E2E_Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 69B70AB42ACBF51C0072A09D /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreen.swift; sourceTree = ""; }; 69B70ABD2ACC2CFE0072A09D /* AccessibilityId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityId.swift; sourceTree = ""; }; @@ -1722,6 +1744,30 @@ path = UI; sourceTree = ""; }; + 696E8F0E2B31AC3C0080BB31 /* RootContainer */ = { + isa = PBXGroup; + children = ( + 696E8F0F2B31AC690080BB31 /* RootContainerViewModelTests.swift */, + ); + path = RootContainer; + sourceTree = ""; + }; + 696E8F132B31AF110080BB31 /* Common */ = { + isa = PBXGroup; + children = ( + 696E8F142B31AF200080BB31 /* Mocks */, + ); + path = Common; + sourceTree = ""; + }; + 696E8F142B31AF200080BB31 /* Mocks */ = { + isa = PBXGroup; + children = ( + 691317A02B32ED76009B4E85 /* AccountProviderTypeMock.swift */, + ); + path = Mocks; + sourceTree = ""; + }; 698C3B462B2B33260012D527 /* Dashboard */ = { isa = PBXGroup; children = ( @@ -1734,7 +1780,8 @@ 698C3B472B2B33440012D527 /* Mocks */ = { isa = PBXGroup; children = ( - 698C3B482B2B33650012D527 /* VpnConnectionUseCaseTypeMock.swift */, + 698C3B482B2B33650012D527 /* VpnConnectionUseCaseMock.swift */, + 696E8F0C2B31A8760080BB31 /* NotificationCenterMock.swift */, ); path = Mocks; sourceTree = ""; @@ -1753,6 +1800,58 @@ path = Settings; sourceTree = ""; }; + 69A226AF2B3074110065EDDB /* Navigation */ = { + isa = PBXGroup; + children = ( + 69A226B22B3074D30065EDDB /* AppRouter.swift */, + ); + path = Navigation; + sourceTree = ""; + }; + 69A226B62B3079D00065EDDB /* Utils */ = { + isa = PBXGroup; + children = ( + 69A226B72B3079EA0065EDDB /* PIA+String.swift */, + 6993119F2B31894D00D316C8 /* Foundation+Protocols.swift */, + 6913179E2B32E4D4009B4E85 /* PIALibrary+Protocols.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 69A226B92B307C8A0065EDDB /* RootContainer */ = { + isa = PBXGroup; + children = ( + 69A226BC2B307CD70065EDDB /* Presentation */, + 69A226BB2B307CCF0065EDDB /* UI */, + 69A226BA2B307CB40065EDDB /* CompositionRoot */, + ); + path = RootContainer; + sourceTree = ""; + }; + 69A226BA2B307CB40065EDDB /* CompositionRoot */ = { + isa = PBXGroup; + children = ( + 69A226BF2B307DBF0065EDDB /* RootContainerFactory.swift */, + ); + path = CompositionRoot; + sourceTree = ""; + }; + 69A226BB2B307CCF0065EDDB /* UI */ = { + isa = PBXGroup; + children = ( + 69A226BD2B307D5F0065EDDB /* RootContainerView.swift */, + ); + path = UI; + sourceTree = ""; + }; + 69A226BC2B307CD70065EDDB /* Presentation */ = { + isa = PBXGroup; + children = ( + 69A226B42B3075AB0065EDDB /* RootContainerViewModel.swift */, + ); + path = Presentation; + sourceTree = ""; + }; 69B70AB12ACBF51C0072A09D /* PIA-VPN_E2E_Tests */ = { isa = PBXGroup; children = ( @@ -1777,6 +1876,7 @@ 69F053442B1E054900AE0665 /* Shared */ = { isa = PBXGroup; children = ( + 69A226B62B3079D00065EDDB /* Utils */, 69F053452B1E055300AE0665 /* UI */, ); path = Shared; @@ -1804,6 +1904,7 @@ isa = PBXGroup; children = ( 69F1C2962B2B239000E924AE /* PIAConnectionButtonViewModel.swift */, + 699311A12B318F6B00D316C8 /* DashboardViewModel.swift */, ); path = Presentation; sourceTree = ""; @@ -2145,6 +2246,8 @@ E5C507782B0E144E00200A6A /* PIA VPN-tvOS */ = { isa = PBXGroup; children = ( + 69A226B92B307C8A0065EDDB /* RootContainer */, + 69A226AF2B3074110065EDDB /* Navigation */, E5C507A32B153A1800200A6A /* Login */, 69F053442B1E054900AE0665 /* Shared */, 694AC74B2B17AB73007E7B56 /* Dashboard */, @@ -2167,6 +2270,8 @@ E5C507892B0E145100200A6A /* PIA VPN-tvOSTests */ = { isa = PBXGroup; children = ( + 696E8F132B31AF110080BB31 /* Common */, + 696E8F0E2B31AC3C0080BB31 /* RootContainer */, 698C3B462B2B33260012D527 /* Dashboard */, E5C507A62B153E4800200A6A /* Login */, E5C5078A2B0E145100200A6A /* PIA_VPN_tvOSTests.swift */, @@ -3382,16 +3487,21 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6913179F2B32E4D4009B4E85 /* PIALibrary+Protocols.swift in Sources */, E5AB288C2B2A487A00744E5F /* LoginViewModelErrorHandler.swift in Sources */, E5AB28732B279B7000744E5F /* LoginProvider.swift in Sources */, E5C507A52B153A2F00200A6A /* LoginViewModel.swift in Sources */, E5C507B72B17E75B00200A6A /* CheckLoginAvailability.swift in Sources */, E5AB288E2B2A489500744E5F /* LoginStatus.swift in Sources */, E5AB286F2B27977000744E5F /* LoginFactory.swift in Sources */, + 699311A22B318F6B00D316C8 /* DashboardViewModel.swift in Sources */, + 69A226B82B3079EA0065EDDB /* PIA+String.swift in Sources */, 69F1C29C2B2B299300E924AE /* VpnConnectionUseCase.swift in Sources */, + 69A226B32B3074D30065EDDB /* AppRouter.swift in Sources */, E5AB288A2B29BDED00744E5F /* Foundation+PIA.swift in Sources */, E5C507A22B0FE40000200A6A /* LoginView.swift in Sources */, E5AB287D2B28E4E700744E5F /* UserAccountMapper.swift in Sources */, + 699311A02B31894D00D316C8 /* Foundation+Protocols.swift in Sources */, E5AB28792B28B4B300744E5F /* AccountInfo.swift in Sources */, E5C507B12B17E48900200A6A /* LoginWithCredentialsUseCase.swift in Sources */, E5C507B92B17E7A400200A6A /* ValidateCredentialsFormat.swift in Sources */, @@ -3400,15 +3510,18 @@ E5AB28712B279B4000744E5F /* LoginProviderType.swift in Sources */, E5AB287B2B28B4C400744E5F /* Credentials.swift in Sources */, E5C5077C2B0E144E00200A6A /* ContentView.swift in Sources */, + 69A226BE2B307D5F0065EDDB /* RootContainerView.swift in Sources */, 694AC74E2B17AB9C007E7B56 /* DashboardView.swift in Sources */, 69F1C2932B2B10D400E924AE /* DashboardFactory.swift in Sources */, E5C5077A2B0E144E00200A6A /* PIA_VPN_tvOSApp.swift in Sources */, 69F1C2952B2B216100E924AE /* PIA+Animation.swift in Sources */, + 69A226B52B3075AB0065EDDB /* RootContainerViewModel.swift in Sources */, E5C507C72B1FABFF00200A6A /* LoginDomainErrorMapperType.swift in Sources */, 69F1C2972B2B239000E924AE /* PIAConnectionButtonViewModel.swift in Sources */, E5C507B32B17E5AD00200A6A /* LoginError.swift in Sources */, E5C507CA2B1FAC3600200A6A /* LoginDomainErrorMapper.swift in Sources */, E5AB28772B28B49A00744E5F /* UserAccount.swift in Sources */, + 69A226C02B307DBF0065EDDB /* RootContainerFactory.swift in Sources */, 69F053472B1E056400AE0665 /* PIAColors+SwitUI.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3417,11 +3530,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 698C3B492B2B33650012D527 /* VpnConnectionUseCaseTypeMock.swift in Sources */, + 698C3B492B2B33650012D527 /* VpnConnectionUseCaseMock.swift in Sources */, E5C507CC2B1FACE000200A6A /* LoginWithCredentialsUseCaseTests.swift in Sources */, + 696E8F0D2B31A8760080BB31 /* NotificationCenterMock.swift in Sources */, E5C507A82B153E6B00200A6A /* LoginViewModelTests.swift in Sources */, E5AB286C2B2796E700744E5F /* LoginProviderMock.swift in Sources */, E5AB28872B2911C900744E5F /* AccountProviderMock.swift in Sources */, + 696E8F102B31AC690080BB31 /* RootContainerViewModelTests.swift in Sources */, 698C3B4E2B2B3CBE0012D527 /* LoginIntegrationTests.swift in Sources */, E5C5078B2B0E145100200A6A /* PIA_VPN_tvOSTests.swift in Sources */, 698C3B4B2B2B34760012D527 /* PIAConnectionButtonViewModelTests.swift in Sources */, @@ -3431,6 +3546,7 @@ E5C507C22B1F702700200A6A /* LoginWithCredentialsUseCaseMock.swift in Sources */, E5C507C42B1F72E700200A6A /* CheckLoginAvailabilityTests.swift in Sources */, E5C507C02B1F700C00200A6A /* CheckLoginAvailabilityMock.swift in Sources */, + 691317A12B32ED76009B4E85 /* AccountProviderTypeMock.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };