Skip to content

Commit

Permalink
PIA-1177: Integrate VPN profile installing in App flow
Browse files Browse the repository at this point in the history
  • Loading branch information
kp-said-rehouni committed Jan 16, 2024
1 parent bb71380 commit d0017f2
Show file tree
Hide file tree
Showing 16 changed files with 254 additions and 75 deletions.
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?()
}
}
Loading

0 comments on commit d0017f2

Please sign in to comment.