Skip to content

Commit

Permalink
PIA-1177: Integrate VPN profile installing in App flow (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
kp-said-rehouni authored Jan 16, 2024
1 parent bb71380 commit 63525f2
Show file tree
Hide file tree
Showing 19 changed files with 260 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class DashboardFactory {
guard let defaultAccountProvider = Client.providers.accountProvider as? DefaultAccountProvider else {
fatalError("Incorrect account provider type")
}
return DashboardViewModel(accountProvider: defaultAccountProvider, appRouter: AppRouterFactory.makeAppRouter())
return DashboardViewModel(accountProvider: defaultAccountProvider, appRouter: AppRouterFactory.makeAppRouter(), navigationDestination: RegionsDestinations.serversList)
}

static func makePIAConnectionButton(size: CGFloat = 160, lineWidth: CGFloat = 6) -> PIAConnectionButton {
Expand Down
7 changes: 4 additions & 3 deletions PIA VPN-tvOS/Dashboard/Presentation/DashboardViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ class DashboardViewModel: ObservableObject {

private let accountProvider: AccountProviderType
private let appRouter: AppRouter
private let navigationDestination: any Destinations


init(accountProvider: AccountProviderType, appRouter: AppRouter) {
init(accountProvider: AccountProviderType, appRouter: AppRouter, navigationDestination: any Destinations) {
self.accountProvider = accountProvider
self.appRouter = appRouter
self.navigationDestination = navigationDestination
}

func regionSelectionSectionWasTapped() {
appRouter.navigate(to: RegionsDestinations.serversList)
appRouter.navigate(to: navigationDestination)
}


Expand Down
4 changes: 3 additions & 1 deletion PIA VPN-tvOS/Login/CompositionRoot/LoginFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ class LoginFactory {
LoginViewModel(loginWithCredentialsUseCase: makeLoginWithCredentialsUseCase(),
checkLoginAvailability: CheckLoginAvailability(),
validateLoginCredentials: ValidateCredentialsFormat(),
errorHandler: makeLoginViewModelErrorHandler())
errorHandler: makeLoginViewModelErrorHandler(),
appRouter: AppRouter.shared,
successDestination: OnboardingDestinations.installVPNProfile)

}

Expand Down
10 changes: 7 additions & 3 deletions PIA VPN-tvOS/Login/Presentation/LoginViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,21 @@ class LoginViewModel: ObservableObject {
private let checkLoginAvailability: CheckLoginAvailabilityType
private let validateLoginCredentials: ValidateCredentialsFormatType
private let errorHandler: LoginViewModelErrorHandlerType
private let appRouter: AppRouterType

private let successDestination: any Destinations

@Published var isAccountExpired = false
@Published var didLoginSuccessfully = false
@Published var shouldShowErrorMessage = false
@Published var loginStatus: LoginStatus = .none

init(loginWithCredentialsUseCase: LoginWithCredentialsUseCaseType, checkLoginAvailability: CheckLoginAvailabilityType, validateLoginCredentials: ValidateCredentialsFormatType, errorHandler: LoginViewModelErrorHandlerType) {
init(loginWithCredentialsUseCase: LoginWithCredentialsUseCaseType, checkLoginAvailability: CheckLoginAvailabilityType, validateLoginCredentials: ValidateCredentialsFormatType, errorHandler: LoginViewModelErrorHandlerType, appRouter: AppRouterType, successDestination: any Destinations) {
self.loginWithCredentialsUseCase = loginWithCredentialsUseCase
self.checkLoginAvailability = checkLoginAvailability
self.validateLoginCredentials = validateLoginCredentials
self.errorHandler = errorHandler
self.appRouter = appRouter
self.successDestination = successDestination
}

func login(username: String, password: String) {
Expand All @@ -50,7 +54,7 @@ class LoginViewModel: ObservableObject {
case .success(let userAccount):
Task { @MainActor in
self.loginStatus = .succeeded(userAccount: userAccount)
self.didLoginSuccessfully = true
self.appRouter.navigate(to: self.successDestination)
}

case .failure(let error):
Expand Down
10 changes: 9 additions & 1 deletion PIA VPN-tvOS/Navigation/AppRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@ import Foundation
import SwiftUI


protocol AppRouterType {
var stackCount: Int { get }

func navigate(to destination: any Destinations)
func pop()
func goBackToRoot()
}

// AppRouter enables any component to navigate the user to any screen defined within Destinations
class AppRouter: ObservableObject {
class AppRouter: ObservableObject, AppRouterType {

static let shared: AppRouter = AppRouter(with: NavigationPath())

Expand Down
41 changes: 41 additions & 0 deletions PIA VPN-tvOS/Navigation/Routes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// Routes.swift
// PIA VPN-tvOS
//
// Created by Said Rehouni on 12/1/24.
// Copyright © 2024 Private Internet Access Inc. All rights reserved.
//

import Foundation
import SwiftUI

enum AuthenticationDestinations: Destinations {
case loginCredentials
}

enum OnboardingDestinations: Destinations {
case installVPNProfile
case dashboard
}

public extension View {
func withAuthenticationRoutes() -> some View {
self.navigationDestination(for: AuthenticationDestinations.self) { destination in
switch destination {
case .loginCredentials:
LoginFactory.makeLoginView()
}
}
}

func withOnboardingRoutes() -> some View {
self.navigationDestination(for: OnboardingDestinations.self) { destination in
switch destination {
case .installVPNProfile:
VPNConfigurationInstallingFactory.makeVPNConfigurationInstallingView()
case .dashboard:
UserActivatedContainerFactory.makeUSerActivatedContainerView()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import PIALibrary

class RootContainerFactory {
static func makeRootContainerView() -> RootContainerView {
RootContainerView(viewModel: makeRootContainerViewModel())
RootContainerView(viewModel: makeRootContainerViewModel(), appRouter: AppRouter.shared)
}

private static func makeRootContainerViewModel() -> RootContainerViewModel {
guard let defaultAccountProvider = Client.providers.accountProvider as? DefaultAccountProvider else {
fatalError("Incorrect account provider type")
}
return RootContainerViewModel(accountProvider: defaultAccountProvider)
return RootContainerViewModel(accountProvider: defaultAccountProvider,
vpnConfigurationAvailability: VPNConfigurationAvailability())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,15 @@ class RootContainerViewModel: ObservableObject {
@Published var state: State = .splash
@Published internal var isBootstrapped: Bool = false

// TODO: Update this value from the Vpn OnBoarding installation profile screen
@AppStorage(.kOnboardingVpnProfileInstalled) var onBoardingVpnProfileInstalled = false
private let accountProvider: AccountProviderType
private let notificationCenter: NotificationCenterType
private let vpnConfigurationAvailability: VPNConfigurationAvailabilityType

let accountProvider: AccountProviderType
let notificationCenter: NotificationCenterType

init(accountProvider: AccountProviderType, notificationCenter: NotificationCenterType = NotificationCenter.default) {
init(accountProvider: AccountProviderType, notificationCenter: NotificationCenterType = NotificationCenter.default, vpnConfigurationAvailability: VPNConfigurationAvailabilityType) {

self.accountProvider = accountProvider
self.notificationCenter = notificationCenter
self.vpnConfigurationAvailability = vpnConfigurationAvailability
updateState()
subscribeToAccountUpdates()
}
Expand All @@ -44,6 +43,9 @@ class RootContainerViewModel: ObservableObject {
guard isBootstrapped else {
return
}

let onBoardingVpnProfileInstalled = vpnConfigurationAvailability.get()

switch (accountProvider.isLoggedIn, onBoardingVpnProfileInstalled) {
// logged in, vpn profile installed
case (true, true):
Expand Down
38 changes: 24 additions & 14 deletions PIA VPN-tvOS/RootContainer/UI/RootContainerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,32 @@ struct RootContainerView: View {
@ObservedObject var viewModel: RootContainerViewModel
@Environment(\.scenePhase) var scenePhase

@ObservedObject private var appRouter: AppRouter

init(viewModel: RootContainerViewModel, appRouter: AppRouter) {
self.viewModel = viewModel
self.appRouter = appRouter
}

var body: some View {
VStack {
switch viewModel.state {
case .splash:
VStack {
// TODO: Add Splash screen here
NavigationStack(path: $appRouter.path) {
// Add a root view here.
switch viewModel.state {
case .splash:
VStack {
// TODO: Add Splash screen here
}
case .notActivated:
LoginFactory.makeLoginView()
.withAuthenticationRoutes()
.withOnboardingRoutes()
case .activatedNotOnboarded:
VPNConfigurationInstallingFactory.makeVPNConfigurationInstallingView()
.withOnboardingRoutes()
case .activated:
UserActivatedContainerFactory.makeUSerActivatedContainerView()
}
case .notActivated:
LoginFactory.makeLoginView()
case .activatedNotOnboarded:
VPNConfigurationInstallingFactory.makeVPNConfigurationInstallingView()
case .activated:
UserActivatedContainerFactory.makeUSerActivatedContainerView()
}
}.onChange(of: scenePhase) { newPhase in

}.onChange(of: scenePhase) { _, newPhase in
if newPhase == .active {
NSLog(">>> Active")
viewModel.phaseDidBecomeActive()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,17 @@ struct UserActivatedContainerView: View {
@ObservedObject var router: AppRouter

var body: some View {
NavigationStack(path: $router.path) {
DashboardFactory.makeDashboardView()
.navigationDestination(for: RegionsDestinations.self) { destination in
switch destination {
case .serversList:
RegionsSelectionFactory.makeRegionsListView()
case .selectServer(let selectedServer):
VStack {
Text("Selected server: \(selectedServer.name)")
}
DashboardFactory.makeDashboardView()
.navigationDestination(for: RegionsDestinations.self) { destination in
switch destination {
case .serversList:
RegionsSelectionFactory.makeRegionsListView()
case .selectServer(let selectedServer):
VStack {
Text("Selected server: \(selectedServer.name)")
}
}

}

}
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ class VPNConfigurationInstallingFactory {
private static func makeVPNConfigurationInstallingViewModel() -> VPNConfigurationInstallingViewModel {
VPNConfigurationInstallingViewModel(installVPNConfiguration:
makeInstallVPNConfigurationUseCase(),
errorMapper: VPNConfigurationInstallingErrorMapper())
errorMapper: VPNConfigurationInstallingErrorMapper(),
appRouter: AppRouter.shared,
successDestination: OnboardingDestinations.dashboard)
}

private static func makeInstallVPNConfigurationUseCase() -> InstallVPNConfigurationUseCaseType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ import Foundation
class VPNConfigurationInstallingViewModel: ObservableObject {
private let installVPNConfiguration: InstallVPNConfigurationUseCaseType
private let errorMapper: VPNConfigurationInstallingErrorMapper
private let appRouter: AppRouterType

@Published var shouldShowErrorMessage = false
@Published var didInstallVPNProfile = false
@Published var installingStatus: VPNConfigurationInstallingStatus = .none
var errorMessage: String?

init(installVPNConfiguration: InstallVPNConfigurationUseCaseType, errorMapper: VPNConfigurationInstallingErrorMapper) {
private let successDestination: any Destinations

init(installVPNConfiguration: InstallVPNConfigurationUseCaseType, errorMapper: VPNConfigurationInstallingErrorMapper, appRouter: AppRouterType, successDestination: any Destinations) {
self.installVPNConfiguration = installVPNConfiguration
self.errorMapper = errorMapper
self.appRouter = appRouter
self.successDestination = successDestination
}

func install() {
Expand All @@ -32,8 +37,8 @@ class VPNConfigurationInstallingViewModel: ObservableObject {
do {
try await installVPNConfiguration()
Task { @MainActor in
didInstallVPNProfile = true
installingStatus = .succeeded
appRouter.navigate(to: successDestination)
}
} catch {
errorMessage = errorMapper.map(error: error)
Expand Down
58 changes: 58 additions & 0 deletions PIA VPN-tvOSTests/Common/Mocks/AppRouterSpy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// AppRouterSpy.swift
// PIA VPN-tvOSTests
//
// Created by Said Rehouni on 15/1/24.
// Copyright © 2024 Private Internet Access Inc. All rights reserved.
//

import Foundation
@testable import PIA_VPN_tvOS

class AppRouterSpy: AppRouterType {
enum Request: Equatable {
static func == (lhs: AppRouterSpy.Request, rhs: AppRouterSpy.Request) -> Bool {
switch (lhs, rhs) {
case let (.navigate(lhsRoute), .navigate(rhsRoute)):

if let lhsOnboarding = lhsRoute as? OnboardingDestinations, let rhsOnboarding = rhsRoute as? OnboardingDestinations {
return lhsOnboarding == rhsOnboarding
}

if let lhsAuth = lhsRoute as? AuthenticationDestinations, let rhsAuth = rhsRoute as? AuthenticationDestinations {
return lhsAuth == rhsAuth
}

return false
case (.pop, .pop), (.goBackToRoot, .goBackToRoot):
return true
default:
return false
}
}

case navigate(any Destinations)
case pop
case goBackToRoot
}

var stackCount: Int = 0
var requests = [AppRouterSpy.Request]()

var didGetARequest: (() -> Void)?

func navigate(to destination: any Destinations) {
requests.append(.navigate(destination))
didGetARequest?()
}

func pop() {
requests.append(.pop)
didGetARequest?()
}

func goBackToRoot() {
requests.append(.goBackToRoot)
didGetARequest?()
}
}
2 changes: 1 addition & 1 deletion PIA VPN-tvOSTests/Dashboard/DashboardViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class DashboardViewModelTests: XCTestCase {
}

private func initializeSut() {
sut = DashboardViewModel(accountProvider: fixture.accountProviderMock, appRouter: fixture.appRouter)
sut = DashboardViewModel(accountProvider: fixture.accountProviderMock, appRouter: fixture.appRouter, navigationDestination: RegionsDestinations.serversList)
}

func test_navigateToRegionsList() {
Expand Down
Loading

0 comments on commit 63525f2

Please sign in to comment.