Skip to content

Commit

Permalink
PIA-1274: Add expired account view for tvOS (#125)
Browse files Browse the repository at this point in the history
* PIA-1274: Add expired account view for tvOS

* PIA-1274: Moved expiring state handling to RootContainerViewModel
  • Loading branch information
kp-said-rehouni authored Mar 25, 2024
1 parent d2902c4 commit 35a656b
Show file tree
Hide file tree
Showing 17 changed files with 378 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// ExpiredAccountFactory.swift
// PIA VPN-tvOS
//
// Created by Said Rehouni on 20/3/24.
// Copyright © 2024 Private Internet Access Inc. All rights reserved.
//

import Foundation

class ExpiredAccountFactory {
static func makeExpiredAccountView() -> ExpiredAccountView {
ExpiredAccountView(viewModel: makeExpiredAccountViewModel())
}

static private func makeExpiredAccountViewModel() -> ExpiredAccountViewModel {
let qrTitle = [
L10n.Localizable.Tvos.Signin.Expired.Qr.title1,
L10n.Localizable.Tvos.Signin.Expired.Qr.title2
]

let buttonsTitle = [
L10n.Localizable.Tvos.Signin.Expired.Button.renewed,
L10n.Localizable.Tvos.Signin.Expired.Button.signout
]


let separation = L10n.Localizable.Settings.Dedicatedip.Status.expired.lowercased()
let titleSeparated = L10n.Localizable.Tvos.Signin.Expired.title.replacingOccurrences(of: separation, with: "")

return ExpiredAccountViewModel(title1: titleSeparated,
title2: separation,
subtitle: L10n.Localizable.Tvos.Signin.Expired.subtitle,
qrTitle: qrTitle,
qrCodeURL: URL(string: "https://apps.apple.com/us/app/vpn-by-private-internet-access/id955626407"),
logOutUseCase: SettingsFactory.makeLogOutUseCase())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// ExpiredAccountViewModel.swift
// PIA VPN-tvOS
//
// Created by Said Rehouni on 20/3/24.
// Copyright © 2024 Private Internet Access Inc. All rights reserved.
//

import Foundation

class ExpiredAccountViewModel {
let title1: String
let title2: String?
let subtitle: String
let qrTitle: [String]
let qrCodeURL: URL?

@Published var isLoading: Bool = false
private let logOutUseCase: LogOutUseCaseType

init(title1: String, title2: String?, subtitle: String, qrTitle: [String], qrCodeURL: URL?, logOutUseCase: LogOutUseCaseType) {
self.title1 = title1
self.title2 = title2
self.subtitle = subtitle
self.qrTitle = qrTitle
self.qrCodeURL = qrCodeURL
self.logOutUseCase = logOutUseCase
}

func logout() {
Task {
do {
setLoading(to: true)
try await logOutUseCase.logOut()
setLoading(to: false)
} catch {
setLoading(to: false)
}
}
}

private func setLoading(to loading: Bool) {
DispatchQueue.main.async { [weak self] in
self?.isLoading = loading
}
}
}
75 changes: 75 additions & 0 deletions PIA VPN-tvOS/ExpiredAccount/UI/ExpiredAccountView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// ExpiredAccountView.swift
// PIA VPN-tvOS
//
// Created by Said Rehouni on 20/3/24.
// Copyright © 2024 Private Internet Access Inc. All rights reserved.
//

import SwiftUI

struct ExpiredAccountView: View {
var viewModel: ExpiredAccountViewModel

var body: some View {
if viewModel.isLoading {
LoginLoadingView()
} else {
HStack {
VStack(alignment: .leading, spacing: 50) {
VStack(alignment: .leading, spacing: 35) {
HStack(spacing: 0) {
Text(viewModel.title1)
.font(.system(size: 57))
.foregroundColor(.piaOnBackground)
.bold()

if let title2 = viewModel.title2 {
Text(title2)
.font(.system(size: 57))
.foregroundColor(.piaError)
.bold()
.fixedSize(horizontal: false, vertical: true)
}
}

Text(viewModel.subtitle)
.font(.system(size: 31))
.foregroundColor(.piaOnSurfaceContainerSecondary)
.fixedSize(horizontal: false, vertical: true)
}

HStack(spacing: 30) {
if let qrCodeURL = viewModel.qrCodeURL {
QRImageView(qrImageURL: qrCodeURL)
}

VStack(alignment: .leading, spacing: 35) {
ForEach(viewModel.qrTitle, id: \.self) { title in
Text(title)
.font(.system(size: 29))
.foregroundColor(.piaOnSurfaceContainerSecondary)
.fixedSize(horizontal: false, vertical: true)
}
}
}.padding(EdgeInsets(top: 0, leading: 0, bottom: 15, trailing: 0))

VStack {
ActionButton(
title: L10n.Localizable.Tvos.Signin.Expired.Button.signout,
action: { viewModel.logout() }
)
.frame(width: 510, height: 66)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(EdgeInsets(top: 0, leading: 30, bottom: 0, trailing: 0))

Image.signup_setup_screen
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.trailing)
}
}
}
}

9 changes: 8 additions & 1 deletion PIA VPN-tvOS/Login/Data/LoginProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ class LoginProvider: LoginProviderType {
return
}

completion(.success(userAccountMapper.map(userAccount: userAccount)))
let user = userAccountMapper.map(userAccount: userAccount)

guard userAccount.info?.isExpired == true else {
completion(.success(user))
return
}

completion(.failure(ClientError.expired))
}
}
}
4 changes: 4 additions & 0 deletions PIA VPN-tvOS/Navigation/AppRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ extension AppRouter {
navigateRouterToDestinationAction(AuthenticationDestinations.signup)
}

static var navigateToExpiredDestinationAction: AppRouter.Actions {
navigateRouterToDestinationAction(AuthenticationDestinations.expired)
}

static var navigateToConnectionstatsDestinationAction: AppRouter.Actions {
navigateRouterToDestinationAction(OnboardingDestinations.connectionstats)
}
Expand Down
3 changes: 3 additions & 0 deletions PIA VPN-tvOS/Navigation/Routes+Onboarding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ enum AuthenticationDestinations: Destinations {
case loginCredentials
case signup
case loginQRCode
case expired
}

enum OnboardingDestinations: Destinations {
Expand All @@ -30,6 +31,8 @@ public extension View {
SignUpFactory.makeSignupView()
case .loginQRCode:
LoginQRFactory.makeLoginQRView()
case .expired:
ExpiredAccountFactory.makeExpiredAccountView()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class RootContainerViewModel: ObservableObject {
case notActivated
case activatedNotOnboarded
case activated
case expired
}

@Published var state: State = .splash
Expand Down Expand Up @@ -44,14 +45,28 @@ class RootContainerViewModel: ObservableObject {
private func setup() {
bootstrap()
isBootstrapped = true
updateState(isLoggedIn: accountProvider.isLoggedIn)
updateState(isLoggedIn: accountProvider.isLoggedIn, isExpired: accountProvider.isExpired)
}

private func updateState(isLoggedIn: Bool) {
private func handleExpiredState(isLoggedIn: Bool) {
if isLoggedIn {
appRouter.goBackToRoot()
state = .expired
} else {
state = .notActivated
}
}

private func updateState(isLoggedIn: Bool, isExpired: Bool) {
guard isBootstrapped else {
return
}

guard !isExpired else {
handleExpiredState(isLoggedIn: isLoggedIn)
return
}

let onBoardingVpnProfileInstalled = vpnConfigurationAvailability.get()
let shouldShowconnectionStatsPermisson = connectionStatsPermissonType.get() == nil
switch (isLoggedIn, onBoardingVpnProfileInstalled) {
Expand Down Expand Up @@ -100,8 +115,8 @@ extension RootContainerViewModel {

extension RootContainerViewModel {
private func subscribeToAccountUpdates() {
userAuthenticationStatusMonitor.getStatus().sink { status in
self.updateState(isLoggedIn: status == .loggedIn)
userAuthenticationStatusMonitor.getStatus().sink { [self] status in
self.updateState(isLoggedIn: status == .loggedIn, isExpired: accountProvider.isExpired)
}.store(in: &cancellables)

notificationCenter.addObserver(self,
Expand All @@ -111,6 +126,6 @@ extension RootContainerViewModel {
}

@objc private func didInstallVPNProfile() {
updateState(isLoggedIn: accountProvider.isLoggedIn)
updateState(isLoggedIn: accountProvider.isLoggedIn, isExpired: accountProvider.isExpired)
}
}
2 changes: 2 additions & 0 deletions PIA VPN-tvOS/RootContainer/UI/RootContainerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ struct RootContainerView: View {
case .activatedNotOnboarded, .activated:
UserActivatedContainerFactory.makeUSerActivatedContainerView()
.withOnboardingRoutes()
case .expired:
ExpiredAccountFactory.makeExpiredAccountView()
}
}.onChange(of: scenePhase) { _, newPhase in
switch newPhase {
Expand Down
6 changes: 6 additions & 0 deletions PIA VPN-tvOS/Settings/UseCases/LogOutUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ class LogOutUseCase: LogOutUseCaseType {

private func uninstallVpnConfiguration() async {
return await withCheckedContinuation { continuation in
guard self.vpnConfigurationAvailability.get() else {
self.connectionStatsPermisson.set(value: nil)
continuation.resume()
return
}

vpnConfigurationProvider.uninstall { _ in
self.vpnConfigurationAvailability.set(value: false)
self.connectionStatsPermisson.set(value: nil)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ import PIALibrary

protocol AccountProviderType {
var isLoggedIn: Bool { get }
var isExpired: Bool { get }
var publicUsername: String? { get }
var currentUser: PIALibrary.UserAccount? { get set }
func logout(_ callback: ((Error?) -> Void)?)
func login(with linkToken: String, _ callback: ((PIALibrary.UserAccount?, Error?) -> Void)?)
}

extension DefaultAccountProvider: AccountProviderType { }
extension DefaultAccountProvider: AccountProviderType {
var isExpired: Bool {
currentUser?.info?.isExpired ?? false
}
}

protocol ServerType {
var id: ObjectIdentifier { get }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class AccountProviderTypeMock: AccountProviderType {
var currentUser: PIALibrary.UserAccount? = nil

var isLoggedIn: Bool = false
var isExpired: Bool = false

private(set) var logoutCalled = false
private(set) var logoutCalledAttempt = 0
Expand Down
1 change: 1 addition & 0 deletions PIA VPN-tvOSTests/Login/Helpers/AccountProviderMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class AccountProviderMock: AccountProvider {

private let userResult: PIALibrary.UserAccount?
private let errorResult: Error?
var isExpired: Bool = false

init(userResult: PIALibrary.UserAccount?, errorResult: Error?) {
self.userResult = userResult
Expand Down
28 changes: 28 additions & 0 deletions PIA VPN-tvOSTests/Login/Helpers/Stubs+PIALibrary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,39 @@ extension PIALibrary.UserAccount {
return PIALibrary.UserAccount(credentials: credentials,
info: AccountInfo.makeStub())
}

static func makeExpiredStub() -> PIALibrary.UserAccount {
let credentials = PIALibrary.Credentials(username: "username",
password: "password")
return PIALibrary.UserAccount(credentials: credentials,
info: AccountInfo.makeExpiredStub())
}
}

extension PIALibrary.AccountInfo {
static func makeStub() -> PIALibrary.AccountInfo {

let account = AccountInformation(active: true,
canInvite: true,
canceled: true,
daysRemaining: 0,
email: "email",
expirationTime: Int32(Date(timeIntervalSinceNow: 800).timeIntervalSince1970),
expireAlert: false,
expired: false,
needsPayment: true,
plan: "monthly",
productId: "productId",
recurring: true,
renewUrl: "renewUrl",
renewable: true,
username: "username")

return PIALibrary.AccountInfo(accountInformation: account)
}

static func makeExpiredStub() -> PIALibrary.AccountInfo {

let account = AccountInformation(active: true,
canInvite: true,
canceled: true,
Expand Down
Loading

0 comments on commit 35a656b

Please sign in to comment.