-
Notifications
You must be signed in to change notification settings - Fork 80
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
|
||
|
||
import Foundation | ||
import SwiftUI | ||
import PIALibrary | ||
|
||
class DashboardViewModel: ObservableObject { | ||
|
||
let accountProvider: AccountProvider | ||
|
||
init(accountProvider: AccountProvider) { | ||
self.accountProvider = accountProvider | ||
} | ||
|
||
func logOut() { | ||
accountProvider.logout { error in | ||
if let err = error { | ||
NSLog("DashboardViewModel: logout error: \(err)") | ||
} | ||
} | ||
} | ||
|
||
} |
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) | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
|
||
import Foundation | ||
import PIALibrary | ||
|
||
class RootContainerFactory { | ||
static func makeRootContainerView() -> RootContainerView { | ||
RootContainerView(viewModel: makeRootContainerViewModel()) | ||
} | ||
|
||
private static func makeRootContainerViewModel() -> RootContainerViewModel { | ||
return RootContainerViewModel(accountProvider: Client.providers.accountProvider) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
|
||
import Foundation | ||
import Combine | ||
import SwiftUI | ||
import PIALibrary | ||
|
||
|
||
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: AccountProvider | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd suggest the same as the comment above. To have something that hides all the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I was thinking the same. I think we can create a |
||
let notificationCenter: NotificationCenterType | ||
|
||
init(accountProvider: AccountProvider, 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() | ||
} | ||
|
||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
|
||
import Foundation | ||
import SwiftUI | ||
|
||
struct RootContainerView: View { | ||
@ObservedObject var viewModel: RootContainerViewModel | ||
|
||
var body: some View { | ||
switch viewModel.state { | ||
case .splash: | ||
VStack { | ||
Text("Splash Screen") | ||
} | ||
case .notActivated: | ||
LoginFactory.makeLoginView() | ||
case .activatedNotOnboarded: | ||
VStack { | ||
Text("Show Onboarding vpn installation view") | ||
} | ||
case .activated: | ||
DashboardFactory.makeDashboardView() | ||
} | ||
} | ||
|
||
|
||
} | ||
|
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 {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
|
||
import Foundation | ||
|
||
extension String { | ||
static let kOnboardingVpnProfileInstalled = "kOnboardingVpnProfileInstalled" | ||
} |
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 | ||
} | ||
|
||
} |
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 = AccountProviderMock(userResult: nil, errorResult: nil) | ||
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) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think since this is presentation, we could avoid leaking framework details like SwiftUI. I'd also try to avoid importing PIALibrary on presentation and domain layers. It's true that we will be removing PIALibrary but the
AccountProvider
protocol exposes many different methods that this ViewModel does not care about. I'd suggest to have something in between which will only expose the methods the ViewModel will be using.What do you think?