Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PIA-1058: App navigation setup #51

Merged
merged 2 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading