Skip to content

Commit

Permalink
PIA-1847: Migrate subscriptions API to native
Browse files Browse the repository at this point in the history
  • Loading branch information
kp-laura-sempere committed Jun 19, 2024
1 parent 5c784fe commit 09a5299
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 121 deletions.
17 changes: 16 additions & 1 deletion Sources/PIALibrary/Account/CompositionRoot/AccountFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,23 @@ public class AccountFactory {
PaymentUseCase(networkClient: NetworkRequestFactory.maketNetworkRequestClient(), paymentInformationDataConverter: makePaymentInformationDataConverter())
}

static func makeSubscriptionsUseCase() -> SubscriptionsUseCaseType {
SubscriptionsUseCase(networkClient: NetworkRequestFactory.maketNetworkRequestClient(), refreshAuthTokensChecker: makeRefreshAuthTokensChecker())
}

static func makeDefaultAccountProvider(with webServices: WebServices? = nil) -> DefaultAccountProvider {
DefaultAccountProvider(webServices: webServices, logoutUseCase: makeLogoutUseCase(), loginUseCase: makeLoginUseCase(), signupUseCase: makeSignupUseCase(), apiTokenProvider: makeAPITokenProvider(), vpnTokenProvider: makeVpnTokenProvider(), accountDetailsUseCase: makeAccountDetailsUseCase(), updateAccountUseCase: makeUpdateAccountUseCase(), paymentUseCase: makePaymentUseCase())
DefaultAccountProvider(
webServices: webServices,
logoutUseCase: makeLogoutUseCase(),
loginUseCase: makeLoginUseCase(),
signupUseCase: makeSignupUseCase(),
apiTokenProvider: makeAPITokenProvider(),
vpnTokenProvider: makeVpnTokenProvider(),
accountDetailsUseCase: makeAccountDetailsUseCase(),
updateAccountUseCase: makeUpdateAccountUseCase(),
paymentUseCase: makePaymentUseCase(),
subscriptionsUseCase: makeSubscriptionsUseCase()
)

}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@


import Foundation
import NWHttpConnection

struct SubscriptionsRequestConfiguration: NetworkRequestConfigurationType {

let networkRequestModule: NetworkRequestModule = .account
let path: RequestAPI.Path = .iosSubscriptions
let httpMethod: NWHttpConnection.NWConnectionHTTPMethod = .get
let contentType: NetworkRequestContentType = .json
let inlcudeAuthHeaders: Bool = false
var urlQueryParameters: [String : String]? = nil
let responseDataType: NWDataResponseType = .jsonData

var body: Data? = nil
var otherHeaders: [String : String]? = nil

let timeout: TimeInterval = 10
let requestQueue: DispatchQueue? = DispatchQueue(label: "subscriptions_request.queue")
}

59 changes: 36 additions & 23 deletions Sources/PIALibrary/Account/DefaultAccountProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ open class DefaultAccountProvider: AccountProvider, ConfigurationAccess, Databas
private let accountDetailsUseCase: AccountDetailsUseCaseType
private let updateAccountUseCase: UpdateAccountUseCaseType
private let paymentUseCase: PaymentUseCaseType
private let subscriptionsUseCase: SubscriptionsUseCaseType


init(webServices: WebServices? = nil, logoutUseCase: LogoutUseCaseType, loginUseCase: LoginUseCaseType, signupUseCase: SignupUseCaseType, apiTokenProvider: APITokenProviderType, vpnTokenProvider: VpnTokenProviderType, accountDetailsUseCase: AccountDetailsUseCaseType, updateAccountUseCase: UpdateAccountUseCaseType, paymentUseCase: PaymentUseCaseType) {
init(webServices: WebServices? = nil, logoutUseCase: LogoutUseCaseType, loginUseCase: LoginUseCaseType, signupUseCase: SignupUseCaseType, apiTokenProvider: APITokenProviderType, vpnTokenProvider: VpnTokenProviderType, accountDetailsUseCase: AccountDetailsUseCaseType, updateAccountUseCase: UpdateAccountUseCaseType, paymentUseCase: PaymentUseCaseType, subscriptionsUseCase: SubscriptionsUseCaseType) {
self.logoutUseCase = logoutUseCase
self.loginUseCase = loginUseCase
self.signupUseCase = signupUseCase
Expand All @@ -50,6 +51,7 @@ open class DefaultAccountProvider: AccountProvider, ConfigurationAccess, Databas
self.accountDetailsUseCase = accountDetailsUseCase
self.updateAccountUseCase = updateAccountUseCase
self.paymentUseCase = paymentUseCase
self.subscriptionsUseCase = subscriptionsUseCase
if let webServices = webServices {
customWebServices = webServices
} else {
Expand Down Expand Up @@ -446,22 +448,26 @@ open class DefaultAccountProvider: AccountProvider, ConfigurationAccess, Databas
public func subscriptionInformation(_ callback: LibraryCallback<AppStoreInformation>?) {
log.debug("Fetching available product keys...")

let receipt = accessedStore.paymentReceipt

webServices.subscriptionInformation(with: receipt, { appStoreInformation, error in

guard error == nil else {
callback?(nil, error)
return
subscriptionsUseCase.callAsFunction(receiptBase64: nil) { result in
switch result {
case .failure(let error):
log.debug("SubscriptionsUseCase executed with error: \(error)")
DispatchQueue.main.async {
callback?(nil, error.asClientError())
}
case .success(let appStoreInformation):
DispatchQueue.main.async {
if let info = appStoreInformation {
callback?(info, nil)
} else {
log.debug("SubscriptionUseCase executed without error but unable to decode app store information")
callback?(nil, ClientError.malformedResponseData)
}
}
}

if let appStoreInformation = appStoreInformation {
callback?(appStoreInformation, nil)
} else {
callback?(nil, ClientError.malformedResponseData)
}

})
}

}

public func listPlanProducts(_ callback: (([Plan : InAppProduct]?, Error?) -> Void)?) {
Expand Down Expand Up @@ -623,15 +629,22 @@ open class DefaultAccountProvider: AccountProvider, ConfigurationAccess, Databas
}

paymentUseCase.processPayment(with: user.credentials, request: payment) { (error) in
NSLog(">>> >>> DefaultAccountProvider: process payment error: \(error)")
if let _ = error {
callback?(nil, error)
return
}
if let transaction = request.transaction {
self.accessedStore.finishTransaction(transaction, success: true)

log.debug("Payment processed with error: \(error)")

DispatchQueue.main.async {
if let error {
callback?(nil, error)
return
}

if let transaction = request.transaction {
self.accessedStore.finishTransaction(transaction, success: true)
}

Macros.postNotification(.PIAAccountDidRenew)
}
Macros.postNotification(.PIAAccountDidRenew)


self.accountDetailsUseCase() { result in
switch result {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@

import Foundation
import SwiftyBeaver

private let log = SwiftyBeaver.self

protocol PaymentUseCaseType {
typealias Completion = ((NetworkRequestError?) -> Void)
Expand Down Expand Up @@ -28,11 +31,13 @@ class PaymentUseCase: PaymentUseCaseType {

networkClient.executeRequest(with: configuration) { [weak self] error, dataResponse in
guard let self else { return }
NSLog(">>> >>> Execute payment request error: \(error) -- dataResponse: \(dataResponse)")

log.debug("PaymentUseCase: request executed with status code: \(dataResponse?.statusCode)")

if let error {
log.debug("PaymentUseCase: request executed with error: \(error)")
self.handleErrorResponse(error, completion: completion)
} else {
NSLog(">>> >>> Execute payment response: \(dataResponse)")
completion(nil)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@

import Foundation

protocol SubscriptionsUseCaseType {
typealias Completion = ((Result<AppStoreInformation?, NetworkRequestError>) -> Void)
func callAsFunction(receiptBase64: String?, completion: @escaping Completion)
}
class SubscriptionsUseCase: SubscriptionsUseCaseType {
let networkClient: NetworkRequestClientType
let refreshAuthTokensChecker: RefreshAuthTokensCheckerType

init(networkClient: NetworkRequestClientType, refreshAuthTokensChecker: RefreshAuthTokensCheckerType) {
self.networkClient = networkClient
self.refreshAuthTokensChecker = refreshAuthTokensChecker
}

func callAsFunction(receiptBase64: String?, completion: @escaping Completion) {

// The auth token is not required in the Subscriptions request
// That's why refreshing the tokens if needed can be executed in parallel
refreshAuthTokensChecker.refreshIfNeeded { _ in }

executeRequest(with: receiptBase64, completion: completion)

}


}

private extension SubscriptionsUseCase {

func executeRequest(with receiptBase64: String?, completion: @escaping Completion) {
var configuration = SubscriptionsRequestConfiguration()

var queryParams: [String: String] = [
"type": "subscription"
]

if let receiptBase64 {
queryParams["receipt"] = receiptBase64
}

configuration.urlQueryParameters = queryParams

networkClient.executeRequest(with: configuration) { error, dataResponse in

if let error {
completion(.failure(error))
} else {
if let dataContent = dataResponse?.data {
if let appStoreInfo = try? JSONDecoder().decode(AppStoreInformation.self, from: dataContent) {
completion(.success(appStoreInfo))
} else {
completion(.failure(.unableToDecodeData))
}
} else {
completion(.failure(.noDataContent))
}
}
}

}

}
7 changes: 6 additions & 1 deletion Sources/PIALibrary/WebServices/AppStoreInformation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@

import Foundation

public struct AppStoreInformation {
public struct AppStoreInformation: Codable {

public let products: [Product]

public let eligibleForTrial: Bool

enum CodingKeys: String, CodingKey {
case products = "available_products"
case eligibleForTrial = "eligible_for_trial"
}
}

55 changes: 1 addition & 54 deletions Sources/PIALibrary/WebServices/PIAWebServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -363,27 +363,6 @@ class PIAWebServices: WebServices, ConfigurationAccess {
return ""
}

// func processPayment(credentials: Credentials, request: Payment, _ callback: SuccessLibraryCallback?) {
// var marketingJSON = ""
// if let json = request.marketing as? JSON {
// marketingJSON = stringify(json: json)
// }
//
// var debugJSON = ""
// if let json = request.debug as? JSON {
// debugJSON = stringify(json: json)
// }
//
// let info = IOSPaymentInformation(store: Self.store, receipt: request.receipt.base64EncodedString(), marketing: marketingJSON, debug: debugJSON)
//
// self.accountAPI.payment(username: credentials.username, password: credentials.password, information: info) { (errors) in
// if !errors.isEmpty {
// callback?(ClientError.badReceipt)
// return
// }
// callback?(nil)
// }
// }
#endif

func downloadServers(_ callback: ((ServersBundle?, Error?) -> Void)?) {
Expand Down Expand Up @@ -423,39 +402,7 @@ class PIAWebServices: WebServices, ConfigurationAccess {
}
}

// MARK: Store
func subscriptionInformation(with receipt: Data?, _ callback: LibraryCallback<AppStoreInformation>?) {
self.accountAPI.subscriptions(receipt: nil) { (response, errors) in
if !errors.isEmpty {
callback?(nil, errors.last?.code == 400 ? ClientError.badReceipt : ClientError.invalidParameter)
return
}

if let response = response {

var products = [Product]()
for prod in response.availableProducts {
let product = Product(identifier: prod.id,
plan: Plan(rawValue: prod.plan) ?? .other,
price: prod.price,
legacy: prod.legacy)
products.append(product)
}

let eligibleForTrial = response.eligibleForTrial

let info = AppStoreInformation(products: products,
eligibleForTrial: eligibleForTrial)
Client.configuration.eligibleForTrial = info.eligibleForTrial

callback?(info, nil)

} else {
callback?(nil, ClientError.malformedResponseData)
return
}
}
}

}

typealias HandlerType<T> = (T?, Int?, Error?) -> Void
Expand Down
9 changes: 8 additions & 1 deletion Sources/PIALibrary/WebServices/Product.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

import Foundation

public struct Product {
public struct Product: Codable {

public let identifier: String

Expand All @@ -32,4 +32,11 @@ public struct Product {

public let legacy: Bool

enum CodingKeys: String, CodingKey {
case identifier = "id"
case plan = "plan"
case price = "price"
case legacy = "legacy"
}

}
6 changes: 0 additions & 6 deletions Sources/PIALibrary/WebServices/WebServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,8 @@ protocol WebServices: class {

#if os(iOS) || os(tvOS)
func signup(with request: Signup, _ callback: LibraryCallback<Credentials>?)

// func processPayment(credentials: Credentials, request: Payment, _ callback: SuccessLibraryCallback?)
#endif

// MARK: Store

func subscriptionInformation(with receipt: Data?, _ callback: LibraryCallback<AppStoreInformation>?)

// MARK: Ephemeral

func downloadServers(_ callback: LibraryCallback<ServersBundle>?)
Expand Down
33 changes: 0 additions & 33 deletions Tests/PIALibraryTests/ProductTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ class ProductTests: XCTestCase {
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}

func testPlans() {
__testRetrieveSubscriptionPlans(webServices: PIAWebServices())
}

func testMockProductIdentifiers() {
let expUpdate = expectation(description: "productIdentifiers")
Expand Down Expand Up @@ -131,36 +127,7 @@ class ProductTests: XCTestCase {

waitForExpectations(timeout: 5.0, handler: nil)


}

private func __testRetrieveSubscriptionPlans(webServices: PIAWebServices) {
let exp = expectation(description: "subscription.plans")

webServices.subscriptionInformation(with: nil) { subscriptionInfo, error in

if let _ = error {
print("Request error: \(error!)")
XCTAssert(false)
exp.fulfill()
return
}

if let subscriptionInfo = subscriptionInfo,
subscriptionInfo.products.count > 0 {
XCTAssertEqual(subscriptionInfo.products.count, self.subscriptionProductIds.count)
XCTAssertEqual(subscriptionInfo.products.first!.identifier, self.subscriptionProductIds.first!)
XCTAssertEqual(subscriptionInfo.products.last!.identifier, self.subscriptionProductIds.last!)
exp.fulfill()
} else {
XCTAssert(error as? ClientError != ClientError.malformedResponseData, "malformedResponseData")
XCTAssert(false)
exp.fulfill()
}

}
waitForExpectations(timeout: 5.0, handler: nil)

}

}

0 comments on commit 09a5299

Please sign in to comment.