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 committed Jan 17, 2024
1 parent 11c9e89 commit bd71c08
Show file tree
Hide file tree
Showing 11 changed files with 79 additions and 30 deletions.
32 changes: 32 additions & 0 deletions PIA VPN-tvOS/Navigation/AppRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,36 @@ 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 {

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

@Published public var path: NavigationPath

enum Actions: Equatable {
case pop
case goBackToRoot
case navigate(destination: any Destinations)

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
}
}
}

/// Returns the amount of stacked views. Useful during unit test validation
var stackCount: Int {
path.count
Expand All @@ -40,4 +61,15 @@ class AppRouter: ObservableObject, AppRouterType {
path.removeLast(path.count)
}

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

}
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 @@ -12,6 +12,6 @@ class RootContainerFactory {
fatalError("Incorrect account provider type")
}
return RootContainerViewModel(accountProvider: defaultAccountProvider,
vpnConfigurationAvailability: VPNConfigurationAvailability())
vpnConfigurationAvailability: VPNConfigurationAvailability(), appRouter: AppRouterFactory.makeAppRouter())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ class RootContainerViewModel: ObservableObject {
private let accountProvider: AccountProviderType
private let notificationCenter: NotificationCenterType
private let vpnConfigurationAvailability: VPNConfigurationAvailabilityType
private let appRouter: AppRouterType

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

self.accountProvider = accountProvider
self.notificationCenter = notificationCenter
self.vpnConfigurationAvailability = vpnConfigurationAvailability
self.appRouter = appRouter
updateState()
subscribeToAccountUpdates()
}
Expand Down Expand Up @@ -52,6 +55,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 @@ -18,8 +18,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)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@

import XCTest
@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()
}

var fixture: Fixture!
Expand All @@ -33,7 +35,7 @@ final class RootContainerViewModelTests: XCTestCase {
private func initializeSut(bootStrapped: Bool = true) {
sut = RootContainerViewModel(accountProvider: fixture.accountProvierMock,
notificationCenter: fixture.notificationCenterMock,
vpnConfigurationAvailability: fixture.vpnConfigurationAvailabilityMock)
vpnConfigurationAvailability: fixture.vpnConfigurationAvailabilityMock, appRouter: fixture.appRouterSpy)
sut.isBootstrapped = bootStrapped
}

Expand All @@ -48,6 +50,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 @@ -63,6 +68,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 @@ -78,6 +86,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, [])
}
}

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 bd71c08

Please sign in to comment.