Skip to content

Commit

Permalink
PIA-1824: Recover purchases sign in tvOS (#147)
Browse files Browse the repository at this point in the history
* Integrate Sign up tvOS

* PIA-1824: Add recover purchases Sign in on tvOS
  • Loading branch information
kp-said-rehouni authored May 22, 2024
1 parent 6108ab3 commit 395dd1b
Show file tree
Hide file tree
Showing 18 changed files with 625 additions and 76 deletions.
2 changes: 1 addition & 1 deletion PIA VPN-tvOS/Login/CompositionRoot/LoginFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class LoginFactory {
errorMapper: LoginDomainErrorMapper())
}

private static func makeLoginProvider() -> LoginProviderType {
static func makeLoginProvider() -> LoginProviderType {
LoginProvider(accountProvider: Client.providers.accountProvider,
userAccountMapper: UserAccountMapper())
}
Expand Down
50 changes: 30 additions & 20 deletions PIA VPN-tvOS/Login/Data/LoginProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,36 @@ class LoginProvider: LoginProviderType {
let request = LoginRequest(credentials: pialibraryCredentials)

accountProvider.login(with: request) { [weak self] userAccount, error in
guard let self = self else { return }

if let error = error {
completion(.failure(error))
return
}

guard let userAccount = userAccount else {
completion(.failure(ClientError.unexpectedReply))
return
}

let user = userAccountMapper.map(userAccount: userAccount)

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

completion(.failure(ClientError.expired))
self?.handleLoginResult(userAccount: userAccount, error: error, completion: completion)
}
}

func login(with receipt: Data, completion: @escaping (Result<UserAccount, Error>) -> Void) {
let request = LoginReceiptRequest(receipt: receipt)

accountProvider.login(with: request) { [weak self] userAccount, error in
self?.handleLoginResult(userAccount: userAccount, error: error, completion: completion)
}
}

private func handleLoginResult(userAccount: PIALibrary.UserAccount?, error: Error?, completion: @escaping (Result<UserAccount, Error>) -> Void) {
if let error = error {
completion(.failure(error))
return
}

guard let userAccount = userAccount else {
completion(.failure(ClientError.unexpectedReply))
return
}

let user = userAccountMapper.map(userAccount: userAccount)

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

completion(.failure(ClientError.expired))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ import Foundation

protocol LoginProviderType {
func login(with credentials: Credentials, completion: @escaping (Result<UserAccount, Error>) -> Void)
func login(with receipt: Data, completion: @escaping (Result<UserAccount, Error>) -> Void)
}
19 changes: 15 additions & 4 deletions PIA VPN-tvOS/LoginQR/CompositionRoot/LoginQRFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ class LoginQRFactory {
}

private static func makeLoginQRViewModel() -> LoginQRViewModel {
LoginQRViewModel(generateLoginQRCode: generateLoginQRCodeUseCase(),
validateLoginQRCode: validateLoginQRCodeUseCase(),
LoginQRViewModel(generateLoginQRCode: makeGenerateLoginQRCodeUseCase(),
validateLoginQRCode: makeValidateLoginQRCodeUseCase(),
loginWithReceipt: makeloginWithReceiptUseCase(),
onSuccessAction: {
AppRouter.navigateToConnectionstatsDestinationAction()
}, onNavigateAction: {
Expand All @@ -25,7 +26,7 @@ class LoginQRFactory {
})
}

private static func generateLoginQRCodeUseCase() -> GenerateLoginQRCodeUseCaseType {
private static func makeGenerateLoginQRCodeUseCase() -> GenerateLoginQRCodeUseCaseType {
GenerateLoginQRCodeUseCase(generateLoginQRCodeProvider: makeLoginQRProvider())
}

Expand All @@ -36,12 +37,22 @@ class LoginQRFactory {
errorMapper: LoginQRErrorMapper())
}

private static func validateLoginQRCodeUseCase() -> ValidateLoginQRCodeUseCaseType {
private static func makeValidateLoginQRCodeUseCase() -> ValidateLoginQRCodeUseCaseType {
guard let defaultAccountProvider = Client.providers.accountProvider as? DefaultAccountProvider else {
fatalError("Incorrect account provider type")
}

return ValidateLoginQRCodeUseCase(accountProviderType: defaultAccountProvider,
validateLoginQRCodeProvider: makeLoginQRProvider())
}

private static func makeloginWithReceiptUseCase() -> LoginWithReceiptUseCaseType {
LoginWithReceiptUseCase(paymentProvider: makePaymentProvider(),
loginProvider: LoginFactory.makeLoginProvider(),
errorMapper: LoginDomainErrorMapper())
}

private static func makePaymentProvider() -> PaymentProviderType {
PaymentProvider(store: Client.store)
}
}
34 changes: 34 additions & 0 deletions PIA VPN-tvOS/LoginQR/Data/PaymentProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// PaymentProvider.swift
// PIA VPN-tvOS
//
// Created by Said Rehouni on 20/5/24.
// Copyright © 2024 Private Internet Access Inc. All rights reserved.
//

import Foundation
import PIALibrary

class PaymentProvider: PaymentProviderType {
private let store: InAppProvider

init(store: InAppProvider) {
self.store = store
}

func refreshPaymentReceipt(_ completion: @escaping (Result<Data, Error>) -> Void) {
store.refreshPaymentReceipt { [weak self] error in
if let error {
completion(.failure(error))
return
}

guard let receipt = self?.store.paymentReceipt else {
completion(.failure(ClientError.unexpectedReply))
return
}

completion(.success(receipt))
}
}
}
13 changes: 13 additions & 0 deletions PIA VPN-tvOS/LoginQR/Domain/Interfaces/PaymentProviderType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// PaymentProviderType.swift
// PIA VPN-tvOS
//
// Created by Said Rehouni on 21/5/24.
// Copyright © 2024 Private Internet Access Inc. All rights reserved.
//

import Foundation

protocol PaymentProviderType {
func refreshPaymentReceipt(_ completion: @escaping (Result<Data, Error>) -> Void)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// LoginWithReceiptUseCase.swift
// PIA VPN-tvOS
//
// Created by Said Rehouni on 20/5/24.
// Copyright © 2024 Private Internet Access Inc. All rights reserved.
//

import Foundation

protocol LoginWithReceiptUseCaseType {
func callAsFunction() async throws -> UserAccount
}

class LoginWithReceiptUseCase: LoginWithReceiptUseCaseType {
private let paymentProvider: PaymentProviderType
private let loginProvider: LoginProviderType
private let errorMapper: LoginDomainErrorMapperType

init(paymentProvider: PaymentProviderType, loginProvider: LoginProviderType, errorMapper: LoginDomainErrorMapperType) {
self.paymentProvider = paymentProvider
self.loginProvider = loginProvider
self.errorMapper = errorMapper
}

func callAsFunction() async throws -> UserAccount {
return try await withCheckedThrowingContinuation { continuation in
paymentProvider.refreshPaymentReceipt { [weak self] result in
guard let self else { return }

switch result {
case .success(let receipt):
login(with: receipt, continuation: continuation)

case .failure(let error):
continuation.resume(throwing: errorMapper.map(error: error))
}
}
}
}

private func login(with receipt: Data, continuation: CheckedContinuation<UserAccount, any Error>) {
loginProvider.login(with: Data()) { [weak self] result in
guard let self else { return }

switch result {
case .success(let userAccount):
continuation.resume(returning: userAccount)
case .failure(let error):
continuation.resume(throwing: errorMapper.map(error: error))
}
}
}
}
21 changes: 20 additions & 1 deletion PIA VPN-tvOS/LoginQR/Presentation/LoginQRViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ class LoginQRViewModel: ObservableObject {

private let generateLoginQRCode: GenerateLoginQRCodeUseCaseType
private let validateLoginQRCode: ValidateLoginQRCodeUseCaseType
private let loginWithReceipt: LoginWithReceiptUseCaseType
private let onSuccessAction: () -> Void
private let onNavigateAction: () -> Void

init(generateLoginQRCode: GenerateLoginQRCodeUseCaseType, validateLoginQRCode: ValidateLoginQRCodeUseCaseType, onSuccessAction: @escaping () -> Void, onNavigateAction: @escaping () -> Void) {
init(generateLoginQRCode: GenerateLoginQRCodeUseCaseType, validateLoginQRCode: ValidateLoginQRCodeUseCaseType, loginWithReceipt: LoginWithReceiptUseCaseType, onSuccessAction: @escaping () -> Void, onNavigateAction: @escaping () -> Void) {
self.generateLoginQRCode = generateLoginQRCode
self.validateLoginQRCode = validateLoginQRCode
self.loginWithReceipt = loginWithReceipt
self.onSuccessAction = onSuccessAction
self.onNavigateAction = onNavigateAction
}
Expand Down Expand Up @@ -61,6 +63,23 @@ class LoginQRViewModel: ObservableObject {
}
}

func recoverPurchases() {
state = .loading
Task {
do {
let userAccount = try await loginWithReceipt()
Task { @MainActor in
onSuccessAction()
}
} catch {
Task { @MainActor in
state = .validating
shouldShowErrorMessage = true
}
}
}
}

private func map(loginQRToken: LoginQRTokenDTO) -> LoginQRCode? {
let dateString = loginQRToken.expiresAt
let dateFormatter = ISO8601DateFormatter()
Expand Down
6 changes: 4 additions & 2 deletions PIA VPN-tvOS/LoginQR/UI/LoginQRContainerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ struct LoginQRContainerView: View {
}
}
} else {
LoginQRView(expiresAt: $viewModel.expiresAt, qrCodeURL: viewModel.qrCodeURL) {
LoginQRView(expiresAt: $viewModel.expiresAt, qrCodeURL: viewModel.qrCodeURL, loginAction: {
viewModel.navigateToRoute()
}
}, restorePurchasesAction: {
viewModel.recoverPurchases()
})
}
}.onAppear {
viewModel.generateQRCode()
Expand Down
6 changes: 6 additions & 0 deletions PIA VPN-tvOS/LoginQR/UI/LoginQRView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ struct LoginQRView: View {
@Binding var expiresAt: String
var qrCodeURL: URL?
let loginAction: () -> Void
let restorePurchasesAction: () -> Void

var body: some View {
HStack {
Expand Down Expand Up @@ -51,6 +52,11 @@ struct LoginQRView: View {
action: { loginAction() }
)
.frame(width: 480, height: 66)
ActionButton(
title: L10n.Localizable.Tvos.Login.Qr.Button.restore,
action: { restorePurchasesAction() }
)
.frame(width: 480, height: 66)
}

}
Expand Down
5 changes: 4 additions & 1 deletion PIA VPN-tvOSTests/Login/Helpers/AccountProviderMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ class AccountProviderMock: AccountProvider {
callback?(appStoreInformationResult, errorResult)
}

func login(with receiptRequest: PIALibrary.LoginReceiptRequest, _ callback: PIALibrary.LibraryCallback<PIALibrary.UserAccount>?) {}
func login(with receiptRequest: PIALibrary.LoginReceiptRequest, _ callback: PIALibrary.LibraryCallback<PIALibrary.UserAccount>?) {
callback?(userResult, errorResult)
}

func refreshAccountInfo(_ callback: PIALibrary.LibraryCallback<PIALibrary.AccountInfo>?) {}
func accountInformation(_ callback: ((PIALibrary.AccountInfo?, Error?) -> Void)?) {}
func update(with request: PIALibrary.UpdateAccountRequest, resetPassword reset: Bool, andPassword password: String, _ callback: PIALibrary.LibraryCallback<PIALibrary.AccountInfo>?) {}
Expand Down
4 changes: 4 additions & 0 deletions PIA VPN-tvOSTests/Login/Helpers/LoginProviderMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ class LoginProviderMock: LoginProviderType {
func login(with credentials: Credentials, completion: @escaping (Result<UserAccount, Error>) -> Void) {
completion(result)
}

func login(with receipt: Data, completion: @escaping (Result<UserAccount, any Error>) -> Void) {
completion(result)
}
}
Loading

0 comments on commit 395dd1b

Please sign in to comment.