Skip to content

Commit

Permalink
PIA-000: Handle Onboarding screens inside the Authenticated flow
Browse files Browse the repository at this point in the history
  • Loading branch information
kp-laura-sempere authored and kp-said-rehouni committed Feb 1, 2024
1 parent 5dfdcaf commit 1a40f29
Show file tree
Hide file tree
Showing 11 changed files with 69 additions and 42 deletions.
18 changes: 15 additions & 3 deletions PIA VPN-tvOS/Navigation/AppRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ protocol AppRouterType {
func navigate(to destination: any Destinations)
func pop()
func goBackToRoot()

func execute(action: AppRouter.Actions)
}


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

Expand Down Expand Up @@ -38,14 +41,24 @@ class AppRouter: ObservableObject, AppRouterType {
func goBackToRoot() {
path.removeLast(path.count)
}

/*
func execute(action: AppRouter.Actions) {
switch action {
case .pop:
pop()
case .goBackToRoot:
goBackToRoot()
case .navigate(let destination):
navigate(to: destination)
}
}
*/
}


extension AppRouter {

enum Actions: Equatable {

case pop(router: AppRouterType)
case goBackToRoot(router: AppRouterType)
case navigate(router: AppRouterType, destination: any Destinations)
Expand Down Expand Up @@ -74,5 +87,4 @@ extension AppRouter {
}

}

}
1 change: 0 additions & 1 deletion PIA VPN-tvOS/Navigation/Destinations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import Foundation

typealias Destinations = Hashable


enum DashboardDestinations: Destinations {
case home
}
Expand Down
3 changes: 0 additions & 3 deletions PIA VPN-tvOS/Navigation/Routes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ enum AuthenticationDestinations: Destinations {

enum OnboardingDestinations: Destinations {
case installVPNProfile
case dashboard
}

public extension View {
Expand All @@ -33,8 +32,6 @@ public extension View {
switch destination {
case .installVPNProfile:
VPNConfigurationInstallingFactory.makeVPNConfigurationInstallingView()
case .dashboard:
UserActivatedContainerFactory.makeUSerActivatedContainerView()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ class RootContainerFactory {
guard let defaultAccountProvider = Client.providers.accountProvider as? DefaultAccountProvider else {
fatalError("Incorrect account provider type")
}
return RootContainerViewModel(accountProvider: defaultAccountProvider,
return RootContainerViewModel(accountProvider: defaultAccountProvider,
vpnConfigurationAvailability: VPNConfigurationAvailability(),
bootstrap: BootstraperFactory.makeBootstrapper(),
userAuthenticationStatusMonitor: StateMonitorsFactory.makeUserAuthenticationStatusMonitor())
userAuthenticationStatusMonitor: StateMonitorsFactory.makeUserAuthenticationStatusMonitor(),
appRouter: AppRouterFactory.makeAppRouter())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,21 @@ class RootContainerViewModel: ObservableObject {
private let vpnConfigurationAvailability: VPNConfigurationAvailabilityType
private let bootstrap: BootstraperType
private let userAuthenticationStatusMonitor: UserAuthenticationStatusMonitorType
private let appRouter: AppRouterType
private var cancellables = Set<AnyCancellable>()

init(accountProvider: AccountProviderType, notificationCenter: NotificationCenterType = NotificationCenter.default, vpnConfigurationAvailability: VPNConfigurationAvailabilityType, bootstrap: BootstraperType, userAuthenticationStatusMonitor: UserAuthenticationStatusMonitorType) {
init(accountProvider: AccountProviderType, notificationCenter: NotificationCenterType = NotificationCenter.default, vpnConfigurationAvailability: VPNConfigurationAvailabilityType, bootstrap: BootstraperType, userAuthenticationStatusMonitor: UserAuthenticationStatusMonitorType, appRouter: AppRouterType) {

self.accountProvider = accountProvider
self.notificationCenter = notificationCenter
self.vpnConfigurationAvailability = vpnConfigurationAvailability
self.bootstrap = bootstrap
self.userAuthenticationStatusMonitor = userAuthenticationStatusMonitor
self.appRouter = appRouter
updateState()
subscribeToAccountUpdates()
}

deinit {
notificationCenter.removeObserver(self)
}

func phaseDidBecomeActive() {
bootstrap()
isBootstrapped = true
Expand All @@ -56,6 +54,7 @@ class RootContainerViewModel: ObservableObject {
state = .activated
// logged in, vpn profile not installed
case (true, false):
appRouter.navigate(to: OnboardingDestinations.installVPNProfile)
state = .activatedNotOnboarded
// not logged in, any
case (false, _):
Expand Down
6 changes: 2 additions & 4 deletions PIA VPN-tvOS/RootContainer/UI/RootContainerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,9 @@ struct RootContainerView: View {
LoginFactory.makeLoginView()
.withAuthenticationRoutes()
.withOnboardingRoutes()
case .activatedNotOnboarded:
VPNConfigurationInstallingFactory.makeVPNConfigurationInstallingView()
.withOnboardingRoutes()
case .activated:
case .activatedNotOnboarded, .activated:
UserActivatedContainerFactory.makeUSerActivatedContainerView()
.withOnboardingRoutes()
}
}.onChange(of: scenePhase) { _, newPhase in
if newPhase == .active {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ class VPNConfigurationInstallingFactory {
VPNConfigurationInstallingViewModel(installVPNConfiguration:
makeInstallVPNConfigurationUseCase(),
errorMapper: VPNConfigurationInstallingErrorMapper(),
appRouter: AppRouter.shared,
successDestination: OnboardingDestinations.dashboard)
appRouter: AppRouter.shared, onSuccessAction: .goBackToRoot)
}

private static func makeInstallVPNConfigurationUseCase() -> InstallVPNConfigurationUseCaseType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,17 @@ class VPNConfigurationInstallingViewModel: ObservableObject {
private let installVPNConfiguration: InstallVPNConfigurationUseCaseType
private let errorMapper: VPNConfigurationInstallingErrorMapper
private let appRouter: AppRouterType
private let onSuccessAction: AppRouter.Actions

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

private let successDestination: any Destinations

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

func install() {
Expand All @@ -38,7 +37,7 @@ class VPNConfigurationInstallingViewModel: ObservableObject {
try await installVPNConfiguration()
Task { @MainActor in
installingStatus = .succeeded
appRouter.navigate(to: successDestination)
appRouter.execute(action: onSuccessAction)
}
} catch {
errorMessage = errorMapper.map(error: error)
Expand Down
10 changes: 10 additions & 0 deletions PIA VPN-tvOSTests/Common/Mocks/AppRouterSpy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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) {
Expand Down Expand Up @@ -55,4 +56,13 @@ class AppRouterSpy: AppRouterType {
requests.append(.goBackToRoot)
didGetARequest?()
}

func execute(action: AppRouter.Actions) {
switch action {
case .pop: pop()
case .goBackToRoot: goBackToRoot()
case .navigate(let destination): navigate(to: destination)
}
}

}
23 changes: 18 additions & 5 deletions PIA VPN-tvOSTests/RootContainer/RootContainerViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@
import XCTest
import Combine
@testable import PIA_VPN_tvOS
import SwiftUI

final class RootContainerViewModelTests: XCTestCase {

final class Fixture {
let accountProvierMock = AccountProviderTypeMock()
let notificationCenterMock = NotificationCenterMock()
var vpnConfigurationAvailabilityMock = VPNConfigurationAvailabilityMock(value: false)
let appRouterSpy = AppRouterSpy()
let bootstrapMock = BootstraperMock()

func makeUserAuthenticationStatusMonitorMock(status: UserAuthenticationStatus) -> UserAuthenticationStatusMonitorMock {
return UserAuthenticationStatusMonitorMock(status: status)
}

}

var fixture: Fixture!
Expand All @@ -38,11 +39,12 @@ final class RootContainerViewModelTests: XCTestCase {
}

private func initializeSut(bootStrapped: Bool = true) {
sut = RootContainerViewModel(accountProvider: fixture.accountProvierMock,
sut = RootContainerViewModel(accountProvider: fixture.accountProvierMock,
notificationCenter: fixture.notificationCenterMock,
vpnConfigurationAvailability: fixture.vpnConfigurationAvailabilityMock,
bootstrap: fixture.bootstrapMock,
userAuthenticationStatusMonitor: fixture.makeUserAuthenticationStatusMonitorMock(status: .loggedOut))
userAuthenticationStatusMonitor: fixture.makeUserAuthenticationStatusMonitorMock(status: .loggedOut),
appRouter: fixture.appRouterSpy)
sut.isBootstrapped = bootStrapped
}

Expand All @@ -57,6 +59,9 @@ final class RootContainerViewModelTests: XCTestCase {

// THEN the state becomes 'notActivated'
XCTAssertEqual(sut.state, .notActivated)

// AND no navigation requests are sent to the router
XCTAssertEqual(fixture.appRouterSpy.requests, [])
}

func testState_WhenUserIsAuthenticatedAndVpnProfileNotInstalled() {
Expand All @@ -72,6 +77,9 @@ final class RootContainerViewModelTests: XCTestCase {

// THEN the state becomes 'activatedNotOnboarded'
XCTAssertEqual(sut.state, .activatedNotOnboarded)

// AND the router is called to navigate to the Onboarding Install VPN profile
XCTAssertEqual(fixture.appRouterSpy.requests, [AppRouterSpy.Request.navigate(OnboardingDestinations.installVPNProfile)])
}

func testState_WhenUserIsAuthenticatedAndVpnProfileInstalled() {
Expand All @@ -87,6 +95,9 @@ final class RootContainerViewModelTests: XCTestCase {

// THEN the state becomes 'activated'
XCTAssertEqual(sut.state, .activated)

// AND no navigation requests are sent to the router
XCTAssertEqual(fixture.appRouterSpy.requests, [])
}

func testBoostrapperIsCalled_WhenAppIsLaunched() {
Expand All @@ -112,7 +123,8 @@ final class RootContainerViewModelTests: XCTestCase {
notificationCenter: fixture.notificationCenterMock,
vpnConfigurationAvailability: fixture.vpnConfigurationAvailabilityMock,
bootstrap: fixture.bootstrapMock,
userAuthenticationStatusMonitor: userAuthenticationStatusMonitor)
userAuthenticationStatusMonitor: userAuthenticationStatusMonitor,
appRouter: fixture.appRouterSpy)

// AND the app is launched
sut.phaseDidBecomeActive()
Expand All @@ -138,7 +150,8 @@ final class RootContainerViewModelTests: XCTestCase {
notificationCenter: fixture.notificationCenterMock,
vpnConfigurationAvailability: fixture.vpnConfigurationAvailabilityMock,
bootstrap: fixture.bootstrapMock,
userAuthenticationStatusMonitor: userAuthenticationStatusMonitor)
userAuthenticationStatusMonitor: userAuthenticationStatusMonitor,
appRouter: fixture.appRouterSpy)

// AND the app is launched
sut.phaseDidBecomeActive()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,18 @@ final class VPNConfigurationInstallingViewModelTests: XCTestCase {
fixture = nil
cancellables = nil
}

func test_install_fails_when_installVPNConfiguration_fails() {
// GIVEN

func instantiateSut(with installConfigError: InstallVPNConfigurationError? = nil) {
sut = VPNConfigurationInstallingViewModel(
installVPNConfiguration: fixture.makeInstallVPNConfiguration(error: .userCanceled),
installVPNConfiguration: fixture.makeInstallVPNConfiguration(error: installConfigError),
errorMapper: fixture.errorMapper,
appRouter: fixture.appRouterSpy,
successDestination: OnboardingDestinations.dashboard)
onSuccessAction: AppRouter.Actions.goBackToRoot)
}

func test_install_fails_when_installVPNConfiguration_fails() {
// GIVEN
instantiateSut(with: .userCanceled)

let expectation = expectation(description: "Waiting for installing to finish with error message")
let expectedErrorMessage = "We need this permission for the application to function."
Expand Down Expand Up @@ -70,11 +74,7 @@ final class VPNConfigurationInstallingViewModelTests: XCTestCase {

func test_install_succeeds_when_installVPNConfiguration_succeeds() {
// GIVEN
sut = VPNConfigurationInstallingViewModel(
installVPNConfiguration: fixture.makeInstallVPNConfiguration(error: nil),
errorMapper: fixture.errorMapper,
appRouter: fixture.appRouterSpy,
successDestination: OnboardingDestinations.dashboard)
instantiateSut()

let expectation = expectation(description: "Waiting for installing to finish successfully")
var capturedInstallingStatuses = [VPNConfigurationInstallingStatus]()
Expand All @@ -89,10 +89,10 @@ final class VPNConfigurationInstallingViewModelTests: XCTestCase {
sut.install()

// THEN
wait(for: [expectation], timeout: 1)
wait(for: [expectation], timeout: 2)
XCTAssertEqual(capturedInstallingStatuses, [.isInstalling, .succeeded])
XCTAssertFalse(sut.shouldShowErrorMessage)
XCTAssertNil(sut.errorMessage)
XCTAssertEqual(fixture.appRouterSpy.requests, [.navigate(OnboardingDestinations.dashboard)])
XCTAssertEqual(fixture.appRouterSpy.requests, [.goBackToRoot])
}
}

0 comments on commit 1a40f29

Please sign in to comment.