Skip to content

Commit

Permalink
PIA-1219: Separate Onboarding and UserAuthenticated flows (#63)
Browse files Browse the repository at this point in the history
* PIA-000: Handle Onboarding screens inside the Authenticated flow

* PIA-1219: Separate Onboarding flow from UserAuthenticated flow

---------

Co-authored-by: Said Rehouni <[email protected]>
  • Loading branch information
kp-laura-sempere and kp-said-rehouni authored Feb 2, 2024
1 parent 5dfdcaf commit 5fdcfeb
Show file tree
Hide file tree
Showing 22 changed files with 88 additions and 103 deletions.
4 changes: 1 addition & 3 deletions PIA VPN-tvOS/Login/CompositionRoot/LoginFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ class LoginFactory {
checkLoginAvailability: CheckLoginAvailability(),
validateLoginCredentials: ValidateCredentialsFormat(),
errorHandler: makeLoginViewModelErrorHandler(),
appRouter: AppRouter.shared,
successDestination: OnboardingDestinations.installVPNProfile)

onSuccessAction: .navigate(router: AppRouter.shared, destination: OnboardingDestinations.installVPNProfile))
}

private static func makeLoginWithCredentialsUseCase() -> LoginWithCredentialsUseCaseType {
Expand Down
11 changes: 4 additions & 7 deletions PIA VPN-tvOS/Login/Presentation/LoginViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,18 @@ class LoginViewModel: ObservableObject {
private let checkLoginAvailability: CheckLoginAvailabilityType
private let validateLoginCredentials: ValidateCredentialsFormatType
private let errorHandler: LoginViewModelErrorHandlerType
private let appRouter: AppRouterType

private let successDestination: any Destinations
private let onSuccessAction: AppRouter.Actions

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

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

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

case .failure(let error):
Expand Down
8 changes: 3 additions & 5 deletions PIA VPN-tvOS/Navigation/AppRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ protocol AppRouterType {
func goBackToRoot()
}


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

Expand Down Expand Up @@ -38,19 +39,17 @@ class AppRouter: ObservableObject, AppRouterType {
func goBackToRoot() {
path.removeLast(path.count)
}

}


extension AppRouter {

enum Actions: Equatable {

case pop(router: AppRouterType)
case goBackToRoot(router: AppRouterType)
case navigate(router: AppRouterType, destination: any Destinations)

func execute() {
func callAsFunction() {
switch self {
case .pop(let router):
router.pop()
Expand All @@ -60,7 +59,7 @@ extension AppRouter {
router.navigate(to: destination)
}
}

static func == (lhs: AppRouter.Actions, rhs: AppRouter.Actions) -> Bool {
switch (lhs, rhs) {
case (.pop, .pop):
Expand All @@ -74,5 +73,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 @@ -43,7 +43,7 @@ class RegionsContainerViewModel: ObservableObject {

func navigate(to route: RegionSelectionSideMenuItems) {
if route == .search {
onSearchSelectedAction.execute()
onSearchSelectedAction()
} else {
selectedSideMenuItem = route
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@ class RegionsListViewModel: ObservableObject {

func didSelectRegionServer(_ server: ServerType) {
useCase.select(server: server)
onServerSelectedRouterAction.execute()
onServerSelectedRouterAction()
}
}
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,30 +20,29 @@ 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
updateState()
self.appRouter = appRouter

subscribeToAccountUpdates()
setup()
}

deinit {
notificationCenter.removeObserver(self)
}

func phaseDidBecomeActive() {
private func setup() {
bootstrap()
isBootstrapped = true
updateState()
}

private func updateState() {
@objc private func updateState() {
guard isBootstrapped else {
return
}
Expand All @@ -57,12 +56,16 @@ class RootContainerViewModel: ObservableObject {
// logged in, vpn profile not installed
case (true, false):
state = .activatedNotOnboarded
appRouter.navigate(to: OnboardingDestinations.installVPNProfile)
// not logged in, any
case (false, _):
state = .notActivated
}
}


deinit {
notificationCenter.removeObserver(self)
}
}

// Combine subscriptions
Expand All @@ -72,5 +75,10 @@ extension RootContainerViewModel {
userAuthenticationStatusMonitor.getStatus().sink { status in
self.updateState()
}.store(in: &cancellables)

notificationCenter.addObserver(self,
selector: #selector(updateState),
name: .DidInstallVPNProfile,
object: nil)
}
}
6 changes: 1 addition & 5 deletions PIA VPN-tvOS/RootContainer/UI/RootContainerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,12 @@ struct RootContainerView: View {
LoginFactory.makeLoginView()
.withAuthenticationRoutes()
.withOnboardingRoutes()
case .activatedNotOnboarded:
VPNConfigurationInstallingFactory.makeVPNConfigurationInstallingView()
.withOnboardingRoutes()
case .activated:
case .activatedNotOnboarded, .activated:
UserActivatedContainerFactory.makeUSerActivatedContainerView()
}
}.onChange(of: scenePhase) { _, newPhase in
if newPhase == .active {
NSLog(">>> Active")
viewModel.phaseDidBecomeActive()
} else if newPhase == .inactive {
NSLog(">>> Inactive")
} else if newPhase == .background {
Expand Down
4 changes: 4 additions & 0 deletions PIA VPN-tvOS/Shared/Utils/Foundation+Protocols.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ protocol NotificationCenterType {
}

extension NotificationCenter: NotificationCenterType {}

extension Notification.Name {
public static let DidInstallVPNProfile = Notification.Name("DidInstallVPNProfile")
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ class VPNConfigurationInstallingFactory {
private static func makeVPNConfigurationInstallingViewModel() -> VPNConfigurationInstallingViewModel {
VPNConfigurationInstallingViewModel(installVPNConfiguration:
makeInstallVPNConfigurationUseCase(),
errorMapper: VPNConfigurationInstallingErrorMapper(),
appRouter: AppRouter.shared,
successDestination: OnboardingDestinations.dashboard)
errorMapper: VPNConfigurationInstallingErrorMapper()) {
AppRouter.Actions.goBackToRoot(router: AppRouter.shared)()
NotificationCenter.default.post(name: .DidInstallVPNProfile, object: nil)
}
}

private static func makeInstallVPNConfigurationUseCase() -> InstallVPNConfigurationUseCaseType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ class InstallVpnConfigurationProvider: InstallVPNConfigurationUseCaseType {
return
}

continuation.resume()
vpnConfigurationAvailability.set(value: true)

continuation.resume()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,16 @@ import Foundation
class VPNConfigurationInstallingViewModel: ObservableObject {
private let installVPNConfiguration: InstallVPNConfigurationUseCaseType
private let errorMapper: VPNConfigurationInstallingErrorMapper
private let appRouter: AppRouterType
private let onSuccessAction: () -> Void

@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, onSuccessAction: @escaping () -> Void) {
self.installVPNConfiguration = installVPNConfiguration
self.errorMapper = errorMapper
self.appRouter = appRouter
self.successDestination = successDestination
self.onSuccessAction = onSuccessAction
}

func install() {
Expand All @@ -38,7 +35,7 @@ class VPNConfigurationInstallingViewModel: ObservableObject {
try await installVPNConfiguration()
Task { @MainActor in
installingStatus = .succeeded
appRouter.navigate(to: successDestination)
onSuccessAction()
}
} catch {
errorMessage = errorMapper.map(error: error)
Expand Down
1 change: 1 addition & 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
2 changes: 1 addition & 1 deletion PIA VPN-tvOSTests/Dashboard/DashboardViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import SwiftUI
class DashboardViewModelTests: XCTestCase {
class Fixture {
let accountProviderMock = AccountProviderTypeMock()
let appRouter = AppRouter.shared
let appRouter = AppRouter(with: NavigationPath())
}

var fixture: Fixture!
Expand Down
8 changes: 3 additions & 5 deletions PIA VPN-tvOSTests/Login/LoginIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ final class LoginIntegrationTests: XCTestCase {
checkLoginAvailability: CheckLoginAvailability(),
validateLoginCredentials: ValidateCredentialsFormat(),
errorHandler: LoginViewModelErrorHandler(errorMapper: LoginPresentableErrorMapper()),
appRouter: appRouter,
successDestination: OnboardingDestinations.installVPNProfile)
onSuccessAction: .navigate(router: appRouter, destination: OnboardingDestinations.installVPNProfile))

var cancellables = Set<AnyCancellable>()
let expectation = expectation(description: "Waiting for didLoginSuccessfully property to be updated")
Expand Down Expand Up @@ -104,9 +103,8 @@ final class LoginIntegrationTests: XCTestCase {
let sut = LoginViewModel(loginWithCredentialsUseCase: loginWithCredentialsUseCase,
checkLoginAvailability: CheckLoginAvailability(),
validateLoginCredentials: ValidateCredentialsFormat(),
errorHandler: LoginViewModelErrorHandler(errorMapper: LoginPresentableErrorMapper()),
appRouter: appRouter,
successDestination: OnboardingDestinations.installVPNProfile)
errorHandler: LoginViewModelErrorHandler(errorMapper: LoginPresentableErrorMapper()),
onSuccessAction: .navigate(router: appRouter, destination: OnboardingDestinations.installVPNProfile))

var cancellables = Set<AnyCancellable>()
let expectation = expectation(description: "Waiting for isAccountExpired property to be updated")
Expand Down
18 changes: 6 additions & 12 deletions PIA VPN-tvOSTests/Login/LoginViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ final class LoginViewModelTests: XCTestCase {
checkLoginAvailability: checkLoginAvailabilityMock,
validateLoginCredentials: ValidateCredentialsFormat(),
errorHandler: LoginViewModelErrorHandler(errorMapper: LoginPresentableErrorMapper()),
appRouter: appRouterSpy,
successDestination: OnboardingDestinations.installVPNProfile)
onSuccessAction: .navigate(router: appRouterSpy, destination: OnboardingDestinations.installVPNProfile))

var cancellables = Set<AnyCancellable>()
let expectation = expectation(description: "Waiting for shouldShowErrorMessage property to be updated")
Expand Down Expand Up @@ -72,8 +71,7 @@ final class LoginViewModelTests: XCTestCase {
checkLoginAvailability: checkLoginAvailabilityMock,
validateLoginCredentials: ValidateCredentialsFormat(),
errorHandler: LoginViewModelErrorHandler(errorMapper: LoginPresentableErrorMapper()),
appRouter: appRouterSpy,
successDestination: OnboardingDestinations.installVPNProfile)
onSuccessAction: .navigate(router: appRouterSpy, destination: OnboardingDestinations.installVPNProfile))

var cancellables = Set<AnyCancellable>()
let expectation = expectation(description: "Waiting for shouldShowErrorMessage property to be updated")
Expand Down Expand Up @@ -117,8 +115,7 @@ final class LoginViewModelTests: XCTestCase {
checkLoginAvailability: checkLoginAvailabilityMock,
validateLoginCredentials: ValidateCredentialsFormat(),
errorHandler: LoginViewModelErrorHandler(errorMapper: LoginPresentableErrorMapper()),
appRouter: appRouterSpy,
successDestination: OnboardingDestinations.installVPNProfile)
onSuccessAction: .navigate(router: appRouterSpy, destination: OnboardingDestinations.installVPNProfile))

var cancellables = Set<AnyCancellable>()
let expectation = expectation(description: "Waiting for shouldShowErrorMessage property to be updated")
Expand Down Expand Up @@ -163,8 +160,7 @@ final class LoginViewModelTests: XCTestCase {
checkLoginAvailability: checkLoginAvailabilityMock,
validateLoginCredentials: ValidateCredentialsFormat(),
errorHandler: LoginViewModelErrorHandler(errorMapper: LoginPresentableErrorMapper()),
appRouter: appRouterSpy,
successDestination: OnboardingDestinations.installVPNProfile)
onSuccessAction: .navigate(router: appRouterSpy, destination: OnboardingDestinations.installVPNProfile))

var cancellables = Set<AnyCancellable>()
let expectation = expectation(description: "Waiting for didLoginSuccessfully property to be updated")
Expand Down Expand Up @@ -205,8 +201,7 @@ final class LoginViewModelTests: XCTestCase {
checkLoginAvailability: checkLoginAvailabilityMock,
validateLoginCredentials: ValidateCredentialsFormat(),
errorHandler: LoginViewModelErrorHandler(errorMapper: LoginPresentableErrorMapper()),
appRouter: appRouterSpy,
successDestination: OnboardingDestinations.installVPNProfile)
onSuccessAction: .navigate(router: appRouterSpy, destination: OnboardingDestinations.installVPNProfile))

var cancellables = Set<AnyCancellable>()
let expectation = expectation(description: "Waiting for isAccountExpired property to be updated")
Expand Down Expand Up @@ -249,8 +244,7 @@ final class LoginViewModelTests: XCTestCase {
checkLoginAvailability: checkLoginAvailabilityMock,
validateLoginCredentials: ValidateCredentialsFormat(),
errorHandler: LoginViewModelErrorHandler(errorMapper: LoginPresentableErrorMapper()),
appRouter: appRouterSpy,
successDestination: OnboardingDestinations.installVPNProfile)
onSuccessAction: .navigate(router: appRouterSpy, destination: OnboardingDestinations.installVPNProfile))

var cancellables = Set<AnyCancellable>()
let expectation = expectation(description: "Waiting for shouldShowErrorMessage property to be updated")
Expand Down
Loading

0 comments on commit 5fdcfeb

Please sign in to comment.