Skip to content

Commit

Permalink
PIA-1058: App navigation setup (#51)
Browse files Browse the repository at this point in the history
* PIA-1058: App navigation setup

* PIA-1058: Move external library entities compliance out of presentation layer
  • Loading branch information
kp-laura-sempere authored Dec 20, 2023
1 parent b3d7dea commit 4618b55
Show file tree
Hide file tree
Showing 16 changed files with 464 additions and 7 deletions.
9 changes: 9 additions & 0 deletions PIA VPN-tvOS/Dashboard/CompositionRoot/DashboardFactory.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@

import Foundation
import PIALibrary

class DashboardFactory {

static func makeDashboardView() -> DashboardView {
return DashboardView(
viewModel: makeDashboardViewModel(),
connectionButton: makePIAConnectionButton()
)
}

static func makeDashboardViewModel() -> DashboardViewModel {
guard let defaultAccountProvider = Client.providers.accountProvider as? DefaultAccountProvider else {
fatalError("Incorrect account provider type")
}
return DashboardViewModel(accountProvider: defaultAccountProvider)
}

static func makePIAConnectionButton(size: CGFloat = 160, lineWidth: CGFloat = 6) -> PIAConnectionButton {
return PIAConnectionButton(
size: size,
Expand Down
17 changes: 17 additions & 0 deletions PIA VPN-tvOS/Dashboard/Presentation/DashboardViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@


import Foundation

class DashboardViewModel: ObservableObject {

let accountProvider: AccountProviderType

init(accountProvider: AccountProviderType) {
self.accountProvider = accountProvider
}

func logOut() {
accountProvider.logout(nil)
}

}
14 changes: 13 additions & 1 deletion PIA VPN-tvOS/Dashboard/UI/DashboardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@ struct DashboardView: View {
let viewWidth = UIScreen.main.bounds.width
let viewHeight = UIScreen.main.bounds.height

let viewModel: DashboardViewModel
let connectionButton: PIAConnectionButton

var body: some View {
VStack {
VStack(spacing: 20) {
connectionButton
.padding()

Button {
viewModel.logOut()
} label: {
Text("LogOut")
}
.padding()

}
.frame(maxWidth: (viewWidth/2))
.padding()
Expand All @@ -25,5 +34,8 @@ struct DashboardView: View {
}

#Preview {
DashboardView(connectionButton: DashboardFactory.makePIAConnectionButton())
DashboardView(
viewModel: DashboardFactory.makeDashboardViewModel(),
connectionButton: DashboardFactory.makePIAConnectionButton()
)
}
36 changes: 36 additions & 0 deletions PIA VPN-tvOS/Navigation/AppRouter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

import Foundation

import SwiftUI

typealias Destinations = Hashable

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

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

@Published public var path: NavigationPath

/// Returns the amount of stacked views. Useful during unit test validation
var stackCount: Int {
path.count
}

init(with path: NavigationPath) {
self.path = path
}

func navigate(to destination: any Destinations) {
path.append(destination)
}

func pop() {
path.removeLast()
}

func goBackToRoot() {
path.removeLast(path.count)
}

}
3 changes: 1 addition & 2 deletions PIA VPN-tvOS/PIA_VPN_tvOSApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import SwiftUI
struct PIA_VPN_tvOSApp: App {
var body: some Scene {
WindowGroup {
LoginFactory.makeLoginView()
// DashboardFactory.makeDashboardView()
RootContainerFactory.makeRootContainerView()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

import Foundation
import PIALibrary

class RootContainerFactory {
static func makeRootContainerView() -> RootContainerView {
RootContainerView(viewModel: makeRootContainerViewModel())
}

private static func makeRootContainerViewModel() -> RootContainerViewModel {
guard let defaultAccountProvider = Client.providers.accountProvider as? DefaultAccountProvider else {
fatalError("Incorrect account provider type")
}
return RootContainerViewModel(accountProvider: defaultAccountProvider)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@

import Foundation
import SwiftUI

class RootContainerViewModel: ObservableObject {
enum State {
case splash
case notActivated
case activatedNotOnboarded
case activated
}

@Published var state: State = .splash

// TODO: Update this value from the Vpn OnBoarding installation profile screen
@AppStorage(.kOnboardingVpnProfileInstalled) var onBoardingVpnProfileInstalled = true

let accountProvider: AccountProviderType
let notificationCenter: NotificationCenterType

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

self.accountProvider = accountProvider
self.notificationCenter = notificationCenter
updateState()
subscribeToAccountUpdates()
}

deinit {
notificationCenter.removeObserver(self)
}

private func updateState() {
switch (accountProvider.isLoggedIn, onBoardingVpnProfileInstalled) {
// logged in, vpn profile installed
case (true, true):
state = .activated
// logged in, vpn profile not installed
case (true, false):
state = .activatedNotOnboarded
// not logged in, any
case (false, _):
state = .notActivated
}
}

private func subscribeToAccountUpdates() {
notificationCenter.addObserver(self, selector: #selector(handleAccountDidLogin), name: .PIAAccountDidLogin, object: nil)

notificationCenter.addObserver(self, selector: #selector(handleAccountDidLogout), name: .PIAAccountDidLogout, object: nil)
}

@objc func handleAccountDidLogin() {
updateState()
}

@objc func handleAccountDidLogout() {
updateState()
}


}
28 changes: 28 additions & 0 deletions PIA VPN-tvOS/RootContainer/UI/RootContainerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

import Foundation
import SwiftUI

struct RootContainerView: View {
@ObservedObject var viewModel: RootContainerViewModel

var body: some View {
switch viewModel.state {
case .splash:
VStack {
// TODO: Add Splash screen here
}
case .notActivated:
LoginFactory.makeLoginView()
case .activatedNotOnboarded:
// TODO: Replace this view with the Onboarding Vpn Profile installation view
VStack {
Text("Show Onboarding vpn installation view")
}
case .activated:
DashboardFactory.makeDashboardView()
}
}


}

20 changes: 20 additions & 0 deletions PIA VPN-tvOS/Shared/Utils/Foundation+Protocols.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

import Foundation

/// Expose Core Foundation APIs via protocols to the PIA app


protocol NotificationCenterType {
func addObserver(
_ observer: Any,
selector aSelector: Selector,
name aName: NSNotification.Name?,
object anObject: Any?
)

func removeObserver(_ observer: Any)

// Add methods here from NSNotificationCenter as needed
}

extension NotificationCenter: NotificationCenterType {}
6 changes: 6 additions & 0 deletions PIA VPN-tvOS/Shared/Utils/PIA+String.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

import Foundation

extension String {
static let kOnboardingVpnProfileInstalled = "kOnboardingVpnProfileInstalled"
}
11 changes: 11 additions & 0 deletions PIA VPN-tvOS/Shared/Utils/PIALibrary+Protocols.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

import Foundation
import PIALibrary


protocol AccountProviderType {
var isLoggedIn: Bool { get }
func logout(_ callback: ((Error?) -> Void)?)
}

extension DefaultAccountProvider: AccountProviderType { }
23 changes: 23 additions & 0 deletions PIA VPN-tvOSTests/Common/Mocks/AccountProviderTypeMock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// AccountProviderTypeMock.swift
// PIA VPN-tvOSTests
//
// Created by Laura S on 12/20/23.
// Copyright © 2023 Private Internet Access Inc. All rights reserved.
//

import Foundation
@testable import PIA_VPN_tvOS

class AccountProviderTypeMock: AccountProviderType {
var isLoggedIn: Bool = false

private(set) var logoutCalled = false
private(set) var logoutCalledAttempt = 0
func logout(_ callback: ((Error?) -> Void)?) {
logoutCalled = true
logoutCalledAttempt += 1
}


}
23 changes: 23 additions & 0 deletions PIA VPN-tvOSTests/Dashboard/Mocks/NotificationCenterMock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

import Foundation
@testable import PIA_VPN_tvOS

class NotificationCenterMock: NotificationCenterType {
private(set) var addObserverCalled = false
private(set) var addObserverCalledAttempt = 0
private(set) var addObserverCalledWithNotificationName: NSNotification.Name? = nil

func addObserver(_ observer: Any, selector aSelector: Selector, name aName: NSNotification.Name?, object anObject: Any?) {
addObserverCalled = true
addObserverCalledAttempt += 1
addObserverCalledWithNotificationName = aName
}

private(set) var removeObserverCalled = false
private(set) var remoververCalledAttempt = 0
func removeObserver(_ observer: Any) {
removeObserverCalled = true
remoververCalledAttempt += 1
}

}
79 changes: 79 additions & 0 deletions PIA VPN-tvOSTests/RootContainer/RootContainerViewModelTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// RootContainerViewModelTests.swift
// PIA VPN-tvOSTests
//
// Created by Laura S on 12/19/23.
// Copyright © 2023 Private Internet Access Inc. All rights reserved.
//

import XCTest
@testable import PIA_VPN_tvOS

final class RootContainerViewModelTests: XCTestCase {

final class Fixture {
let accountProvierMock = AccountProviderTypeMock()
let notificationCenterMock = NotificationCenterMock()
}

var fixture: Fixture!
var sut: RootContainerViewModel!

override func setUp() {
fixture = Fixture()
}

override func tearDown() {
fixture = nil
sut = nil
UserDefaults.standard.removeObject(forKey: .kOnboardingVpnProfileInstalled)
}

private func initializeSut() {
sut = RootContainerViewModel(accountProvider: fixture.accountProvierMock, notificationCenter: fixture.notificationCenterMock)
}

func testState_WhenUserIsNotAuthenticated() {
// GIVEN that the user is not logged in
fixture.accountProvierMock.isLoggedIn = false

// WHEN the RootContainer is created
initializeSut()

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

func testState_WhenUserIsAuthenticatedAndVpnProfileNotInstalled() {
// GIVEN that the user is logged in
fixture.accountProvierMock.isLoggedIn = true
// AND GIVEN that the Onboarding Vpn Profile is NOT installed
stubOnboardingVpnInstallation(finished: false)

// WHEN the RootContainer is created
initializeSut()

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

func testState_WhenUserIsAuthenticatedAndVpnProfileInstalled() {
// GIVEN that the user is logged in
fixture.accountProvierMock.isLoggedIn = true
// AND GIVEN that the Onboarding Vpn Profile is installed
stubOnboardingVpnInstallation(finished: true)

// WHEN the RootContainer is created
initializeSut()

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


extension RootContainerViewModelTests {
private func stubOnboardingVpnInstallation(finished: Bool) {
UserDefaults.standard.setValue(finished, forKey: .kOnboardingVpnProfileInstalled)
}
}
Loading

0 comments on commit 4618b55

Please sign in to comment.