Skip to content

Commit 36c98f8

Browse files
KM-8127: Fix QR sign in tvos (#180)
* KM-8127:tvOS: Integrate GenerateQRRequest usecase on tvOS * KM-8127:tvOS: Integrate ValidateQRLogin usecase on tvOS
1 parent da9e7ed commit 36c98f8

10 files changed

Lines changed: 91 additions & 64 deletions

File tree

PIA VPN-tvOS/LoginQR/CompositionRoot/LoginQRFactory.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ class LoginQRFactory {
3434
LoginQRProvider(httpClient: URLSessionHTTPClient(),
3535
urlRequestMaker: LoginQRURLRequestMaker(),
3636
domainMapper: LoginQRCodeDomainMapper(),
37-
errorMapper: LoginQRErrorMapper())
37+
errorMapper: LoginQRErrorMapper(),
38+
generateQRLogin: AccountFactory.makeGenerateQRLoginUseCase(),
39+
accountProvider: Client.providers.accountProvider)
3840
}
3941

4042
private static func makeValidateLoginQRCodeUseCase() -> ValidateLoginQRCodeUseCaseType {

PIA VPN-tvOS/LoginQR/Data/LoginQRProvider.swift

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,19 @@ class LoginQRProvider: ValidateLoginQRCodeProviderType {
1717
private let errorMapper: LoginQRErrorMapper
1818
private var timer: Publishers.Autoconnect<Timer.TimerPublisher>?
1919
private var cancellable: Cancellable?
20+
private let generateQRLogin: GenerateQRLoginUseCaseType
21+
private let accountProvider: AccountProvider
2022

21-
init(httpClient: HTTPClientType, urlRequestMaker: LoginQRURLRequestMaker, domainMapper: LoginQRCodeDomainMapper, errorMapper: LoginQRErrorMapper) {
23+
init(httpClient: HTTPClientType, urlRequestMaker: LoginQRURLRequestMaker, domainMapper: LoginQRCodeDomainMapper, errorMapper: LoginQRErrorMapper, generateQRLogin: GenerateQRLoginUseCaseType, accountProvider: AccountProvider) {
2224
self.httpClient = httpClient
2325
self.urlRequestMaker = urlRequestMaker
2426
self.domainMapper = domainMapper
2527
self.errorMapper = errorMapper
28+
self.generateQRLogin = generateQRLogin
29+
self.accountProvider = accountProvider
2630
}
2731

28-
func validateLoginQRCodeToken(_ qrCodeToken: LoginQRCode) async throws -> UserToken {
32+
func validateLoginQRCodeToken(_ qrCodeToken: LoginQRCode) async throws -> String {
2933
return try await withCheckedThrowingContinuation { continuation in
3034
validateLoginQRCodeToken(qrCodeToken) { [weak self] result in
3135
guard let self = self else { return }
@@ -45,63 +49,61 @@ class LoginQRProvider: ValidateLoginQRCodeProviderType {
4549
timer = nil
4650
}
4751

48-
private func validateLoginQRCodeToken(_ qrCodeToken: LoginQRCode, completion: @escaping (Result<UserToken, ClientError>) -> Void) {
49-
let urlRequest = urlRequestMaker.makeValidateLoginQRURLRequest(loginQRToken: qrCodeToken.token)
50-
52+
private func validateLoginQRCodeToken(_ qrCodeToken: LoginQRCode, completion: @escaping (Result<String, ClientError>) -> Void) {
53+
var isValidating = false
54+
5155
timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
5256
cancellable = timer?.sink(receiveValue: { [weak self] _ in
5357
guard let self = self else { return }
54-
Task {
55-
do {
56-
let userTokenDTO = try await self.validateLoginQRCodeToken(urlRequest: urlRequest)
58+
if isValidating { return }
59+
isValidating = true
60+
61+
accountProvider.validateLoginQR(with: qrCodeToken.token) { [weak self] apiToken, error in
62+
guard let self = self else { return }
63+
64+
if let apiToken {
5765
self.stopTimer()
58-
59-
guard let userToken = self.domainMapper.map(dto: userTokenDTO) else {
60-
completion(.failure(ClientError.malformedResponseData))
61-
return
62-
}
63-
64-
completion(.success(userToken))
65-
} catch {
66+
completion(.success(apiToken))
67+
return
68+
} else {
6669
if qrCodeToken.expiresAt.timeIntervalSinceNow <= 0 {
70+
if self.timer != nil {
71+
completion(.failure(ClientError.expired))
72+
}
6773
self.stopTimer()
68-
completion(.failure(ClientError.expired))
6974
}
7075
}
76+
isValidating = false
7177
}
7278
})
7379
}
7480

75-
private func validateLoginQRCodeToken(urlRequest: URLRequest) async throws -> UserTokenDTO {
76-
do {
77-
let data = try await httpClient.makeRequest(request: urlRequest)
78-
guard let userTokenDTO = try? JSONDecoder().decode(UserTokenDTO.self, from: data) else {
79-
throw ClientError.malformedResponseData
80-
}
81-
82-
return userTokenDTO
83-
} catch {
84-
throw error
85-
}
86-
}
87-
8881
deinit {
8982
stopTimer()
9083
}
9184
}
9285

9386
extension LoginQRProvider: GenerateLoginQRCodeProviderType {
9487
func generateLoginQRCodeToken() async throws -> LoginQRCode {
95-
let urlRequest = urlRequestMaker.makeGenerateLoginQRURLRequest()
9688
do {
97-
let data = try await httpClient.makeRequest(request: urlRequest)
89+
let data = try await withCheckedThrowingContinuation { continuation in
90+
generateQRLogin { result in
91+
switch result {
92+
case let .success(data):
93+
continuation.resume(returning: data)
94+
case let .failure(error):
95+
continuation.resume(throwing: error)
96+
}
97+
}
98+
}
9899

99100
guard let loginQRTokenDTO = try? JSONDecoder().decode(LoginQRTokenDTO.self, from: data),
100101
let loginQRCode = self.domainMapper.map(dto: loginQRTokenDTO) else {
101102
throw ClientError.malformedResponseData
102103
}
103104

104105
return loginQRCode
106+
105107
} catch {
106108
throw error
107109
}

PIA VPN-tvOS/LoginQR/Domain/Interfaces/ValidateLoginQRCodeProviderType.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@
99
import Foundation
1010

1111
protocol ValidateLoginQRCodeProviderType {
12-
func validateLoginQRCodeToken(_ qrCodeToken: LoginQRCode) async throws -> UserToken
12+
func validateLoginQRCodeToken(_ qrCodeToken: LoginQRCode) async throws -> String
1313
}

PIA VPN-tvOS/LoginQR/Domain/Use cases/ValidateLoginQRCodeUseCase.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ class ValidateLoginQRCodeUseCase: ValidateLoginQRCodeUseCaseType {
2222
}
2323

2424
func callAsFunction(qrCodeToken: LoginQRCode) async throws {
25-
let userToken = try await validateLoginQRCodeProvider.validateLoginQRCodeToken(qrCodeToken)
25+
let apiToken = try await validateLoginQRCodeProvider.validateLoginQRCodeToken(qrCodeToken)
2626

2727
return try await withCheckedThrowingContinuation { continuation in
28-
accountProviderType.login(with: userToken.token) { _, error in
28+
accountProviderType.login(with: apiToken) { _, error in
2929
if let error = error {
3030
continuation.resume(throwing: error)
3131
return

PIA VPN-tvOSTests/Login/Helpers/AccountProviderMock.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ class AccountProviderMock: AccountProvider {
7070
func loginUsingMagicLink(withEmail email: String, _ callback: PIALibrary.SuccessLibraryCallback?) {}
7171
func listRenewablePlans(_ callback: PIALibrary.LibraryCallback<[PIALibrary.Plan]>?) {}
7272
func renew(with request: PIALibrary.RenewRequest, _ callback: PIALibrary.LibraryCallback<PIALibrary.UserAccount>?) {}
73+
74+
func validateLoginQR(with qrToken: String, _ callback: ((String?, (any Error)?) -> Void)?) {
75+
callback?(apiToken, errorResult)
76+
}
7377
}
7478

7579
extension AccountProviderMock: AccountProviderType {}

PIA VPN-tvOSTests/LoginQR/LoginQRProviderTests.swift

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,24 @@ final class LoginQRProviderTests: XCTestCase {
1717
let urlRequestMaker = LoginQRURLRequestMaker()
1818
let domainMapper = LoginQRCodeDomainMapper()
1919
let errorMapper = LoginQRErrorMapper()
20+
var generateQRLoginUseCaseMock: GenerateQRLoginUseCaseMock!
21+
var accountProviderMock: AccountProviderMock!
2022
}
2123

2224
var fixture: Fixture!
2325
var sut: LoginQRProvider!
2426

25-
func instantiateSut(result: Result<Data, ClientError>) {
27+
func instantiateSut(result: Result<Data, ClientError>, apiToken: String? = nil, accountProviderErrorResult: Error? = nil) {
2628
fixture.httpClientMock = HTTPClientMock(result: result)
27-
29+
fixture.generateQRLoginUseCaseMock = GenerateQRLoginUseCaseMock(result: result)
30+
fixture.accountProviderMock = AccountProviderMock(userResult: nil, errorResult: accountProviderErrorResult)
31+
fixture.accountProviderMock.apiToken = apiToken
2832
sut = LoginQRProvider(httpClient: fixture.httpClientMock,
2933
urlRequestMaker: fixture.urlRequestMaker,
3034
domainMapper: fixture.domainMapper,
31-
errorMapper: fixture.errorMapper)
35+
errorMapper: fixture.errorMapper,
36+
generateQRLogin: fixture.generateQRLoginUseCaseMock,
37+
accountProvider: fixture.accountProviderMock)
3238
}
3339

3440
override func setUp() {
@@ -128,21 +134,19 @@ final class LoginQRProviderTests: XCTestCase {
128134
}
129135
""".data(using: .utf8)!
130136

131-
instantiateSut(result: .success(data))
132-
var capturedUserToken: UserToken?
137+
instantiateSut(result: .success(data), apiToken: "apiToken")
138+
var capturedApiToken: String?
133139
let loginQRCode = LoginQRCode(token: "dasdqe", expiresAt: Date.makeISO8601Date(string: "2024-05-28T00:00:00Z")!)
134140

135141
// WHEN
136142
do {
137-
capturedUserToken = try await sut.validateLoginQRCodeToken(loginQRCode)
143+
capturedApiToken = try await sut.validateLoginQRCodeToken(loginQRCode)
138144
} catch {
139145
XCTFail("Expected success, got error \(error)")
140146
}
141147

142148
// THEN
143-
XCTAssertEqual(capturedUserToken?.token, "29fa8b5ff37b7928357")
144-
XCTAssertEqual(capturedUserToken?.expiresAt, Date.makeISO8601Date(string: "2024-05-28T00:00:00Z"))
145-
XCTAssertEqual(capturedUserToken?.userId, "781187")
149+
XCTAssertEqual(capturedApiToken, "apiToken")
146150
}
147151

148152
func test_validateLoginQRCodeToken_succeeds_when_httpclient_returns_an_invalid_json() async throws {
@@ -159,20 +163,20 @@ final class LoginQRProviderTests: XCTestCase {
159163
instantiateSut(result: .success(data))
160164

161165
let loginQRCode = LoginQRCode(token: "dasdqe", expiresAt: Date.makeISO8601Date(string: "2023-05-28T00:00:00Z")!)
162-
var capturedUserToken: UserToken?
166+
var capturedApiToken: String?
163167
var capturedError: Error?
164168

165169
// WHEN
166170
do {
167-
capturedUserToken = try await sut.validateLoginQRCodeToken(loginQRCode)
171+
capturedApiToken = try await sut.validateLoginQRCodeToken(loginQRCode)
168172
XCTFail("Expected error, got success")
169173
} catch {
170174
capturedError = error
171175
}
172176

173177
// THEN
174178
let error = try XCTUnwrap(capturedError as? LoginQRCodeError)
175-
XCTAssertNil(capturedUserToken)
179+
XCTAssertNil(capturedApiToken)
176180
XCTAssertEqual(error, LoginQRCodeError.expired)
177181
}
178182

@@ -181,20 +185,20 @@ final class LoginQRProviderTests: XCTestCase {
181185
instantiateSut(result: .failure(ClientError.malformedResponseData))
182186

183187
let loginQRCode = LoginQRCode(token: "dasdqe", expiresAt: Date.makeISO8601Date(string: "2023-05-28T00:00:00Z")!)
184-
var capturedUserToken: UserToken?
188+
var capturedApiToken: String?
185189
var capturedError: Error?
186190

187191
// WHEN
188192
do {
189-
capturedUserToken = try await sut.validateLoginQRCodeToken(loginQRCode)
193+
capturedApiToken = try await sut.validateLoginQRCodeToken(loginQRCode)
190194
XCTFail("Expected error, got success")
191195
} catch {
192196
capturedError = error
193197
}
194198

195199
// THEN
196200
let error = try XCTUnwrap(capturedError as? LoginQRCodeError)
197-
XCTAssertNil(capturedUserToken)
201+
XCTAssertNil(capturedApiToken)
198202
XCTAssertEqual(error, LoginQRCodeError.expired)
199203
}
200204
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
import Foundation
3+
import PIALibrary
4+
5+
class GenerateQRLoginUseCaseMock: GenerateQRLoginUseCaseType {
6+
private let result: Result<Data, ClientError>
7+
8+
init(result: Result<Data, ClientError>) {
9+
self.result = result
10+
}
11+
12+
public func callAsFunction(completion: @escaping Completion) {
13+
completion(result)
14+
}
15+
}

PIA VPN-tvOSTests/LoginQR/Mocks/ValidateLoginQRCodeProviderMock.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import Foundation
1010
@testable import PIA_VPN_tvOS
1111

1212
class ValidateLoginQRCodeProviderMock: ValidateLoginQRCodeProviderType {
13-
private let result: Result<UserToken, LoginQRCodeError>
13+
private let result: Result<String, LoginQRCodeError>
1414

15-
init(result: Result<UserToken, LoginQRCodeError>) {
15+
init(result: Result<String, LoginQRCodeError>) {
1616
self.result = result
1717
}
1818

19-
func validateLoginQRCodeToken(_ qrCodeToken: LoginQRCode) async throws -> UserToken {
19+
func validateLoginQRCodeToken(_ qrCodeToken: LoginQRCode) async throws -> String {
2020
switch result {
2121
case .success(let user):
2222
return user

PIA VPN-tvOSTests/LoginQR/ValidateLoginQRCodeUseCaseTests.swift

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ final class ValidateLoginQRCodeUseCaseTests: XCTestCase {
1919
var fixture: Fixture!
2020
var sut: ValidateLoginQRCodeUseCase!
2121

22-
func instantiateSut(userResult: PIALibrary.UserAccount?, errorResult: Error?, validateLoginQRCodeResult: Result<UserToken, LoginQRCodeError>) {
22+
func instantiateSut(userResult: PIALibrary.UserAccount?, errorResult: Error?, validateLoginQRCodeResult: Result<String, LoginQRCodeError>) {
2323
fixture.accountProviderMock = AccountProviderMock(userResult: userResult, errorResult: errorResult)
2424
fixture.validateLoginQRCodeProviderMock = ValidateLoginQRCodeProviderMock(result: validateLoginQRCodeResult)
2525

@@ -39,14 +39,12 @@ final class ValidateLoginQRCodeUseCaseTests: XCTestCase {
3939
func test_callAsFunction_succeeds_when_validateLoginQRCodeProvider_and_accountProvider_succeeds() async {
4040
// GIVEN
4141
let userAccount = PIALibrary.UserAccount(credentials: PIALibrary.Credentials(username: "", password: ""), info: nil)
42-
let userToken = UserToken(token: "token",
43-
expiresAt: Date.makeISO8601Date(string: "2024-03-15T16:43:24Z")!,
44-
userId: "userId")
42+
let apiToken = "api_token"
4543
let qrCodeToken = LoginQRCode(token: "token", expiresAt: Date.makeISO8601Date(string: "2024-03-15T16:43:24Z")!)
4644

4745
instantiateSut(userResult: userAccount,
4846
errorResult: nil,
49-
validateLoginQRCodeResult: .success(userToken))
47+
validateLoginQRCodeResult: .success(apiToken))
5048

5149
// WHEN
5250
do {
@@ -82,14 +80,12 @@ final class ValidateLoginQRCodeUseCaseTests: XCTestCase {
8280

8381
func test_callAsFunction_fails_when_accountProvider_returns_an_error() async throws {
8482
// GIVEN
85-
let userToken = UserToken(token: "token",
86-
expiresAt: Date.makeISO8601Date(string: "2024-03-15T16:43:24Z")!,
87-
userId: "userId")
83+
let apiToken = "api_token"
8884
let qrCodeToken = LoginQRCode(token: "token", expiresAt: Date.makeISO8601Date(string: "2024-03-15T16:43:24Z")!)
8985

9086
instantiateSut(userResult: nil,
9187
errorResult: LoginQRCodeError.generic,
92-
validateLoginQRCodeResult: .success(userToken))
88+
validateLoginQRCodeResult: .success(apiToken))
9389

9490
var capturedError: Error?
9591

PIA VPN.xcodeproj/project.pbxproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,7 @@
832832
E58A457E2BACD0F0002A0704 /* ExpiredAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E58A457D2BACD0F0002A0704 /* ExpiredAccountView.swift */; };
833833
E58A45812BB09F4F002A0704 /* ExpiredAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E58A45802BB09F4F002A0704 /* ExpiredAccountViewModel.swift */; };
834834
E58A45842BB09F7F002A0704 /* ExpiredAccountFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E58A45832BB09F7F002A0704 /* ExpiredAccountFactory.swift */; };
835+
E58CE0842D7B5D28006AEC08 /* GenerateQRLoginUseCaseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E58CE0832D7B5D28006AEC08 /* GenerateQRLoginUseCaseMock.swift */; };
835836
E59D1A152B7E2D4B00A2FBFB /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD606AB921C7A17900E0781D /* NetworkExtension.framework */; };
836837
E59D1A182B7E2D4B00A2FBFB /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E59D1A172B7E2D4B00A2FBFB /* PacketTunnelProvider.swift */; };
837838
E59D1A1D2B7E2D4B00A2FBFB /* PIA tvOS Tunnel.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E59D1A142B7E2D4B00A2FBFB /* PIA tvOS Tunnel.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@@ -1789,6 +1790,7 @@
17891790
E58A457D2BACD0F0002A0704 /* ExpiredAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpiredAccountView.swift; sourceTree = "<group>"; };
17901791
E58A45802BB09F4F002A0704 /* ExpiredAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpiredAccountViewModel.swift; sourceTree = "<group>"; };
17911792
E58A45832BB09F7F002A0704 /* ExpiredAccountFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpiredAccountFactory.swift; sourceTree = "<group>"; };
1793+
E58CE0832D7B5D28006AEC08 /* GenerateQRLoginUseCaseMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerateQRLoginUseCaseMock.swift; sourceTree = "<group>"; };
17921794
E59D1A142B7E2D4B00A2FBFB /* PIA tvOS Tunnel.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "PIA tvOS Tunnel.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
17931795
E59D1A172B7E2D4B00A2FBFB /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
17941796
E59D1A192B7E2D4B00A2FBFB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -3975,6 +3977,7 @@
39753977
E58A453C2BA49C0F002A0704 /* Mocks */ = {
39763978
isa = PBXGroup;
39773979
children = (
3980+
E58CE0832D7B5D28006AEC08 /* GenerateQRLoginUseCaseMock.swift */,
39783981
E58A453D2BA49C1D002A0704 /* GenerateLoginQRCodeUseCaseMock.swift */,
39793982
E58A453F2BA49C43002A0704 /* ValidateLoginQRCodeMock.swift */,
39803983
E58A454A2BA4BE5C002A0704 /* HTTPClientMock.swift */,
@@ -6107,6 +6110,7 @@
61076110
E58A45452BA4A11A002A0704 /* LoginQRCodeDomainMapperTests.swift in Sources */,
61086111
E52E69002B56ABE400471913 /* AppRouterSpy.swift in Sources */,
61096112
693474C62B6B8ABA0061F788 /* FavoriteRegionUseCaseMock.swift in Sources */,
6113+
E58CE0842D7B5D28006AEC08 /* GenerateQRLoginUseCaseMock.swift in Sources */,
61106114
E574D9E72B83F3D2000FADAF /* DipServerProviderMock.swift in Sources */,
61116115
E56E75F22BD2F67F0018E279 /* GetAvailableProductsUseCaseTests.swift in Sources */,
61126116
E56E76032BD402C80018E279 /* DecoratorProductsProviderTests.swift in Sources */,
@@ -7791,7 +7795,7 @@
77917795
repositoryURL = "https://github.com/pia-foss/mobile-ios-library.git";
77927796
requirement = {
77937797
kind = revision;
7794-
revision = 1a5202451b88b8a5f65338fd9336e1dd7818f7a6;
7798+
revision = 27e25fa11c59369a0c9284375d40746978a8caca;
77957799
};
77967800
};
77977801
/* End XCRemoteSwiftPackageReference section */

0 commit comments

Comments
 (0)