Skip to content
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

Refactor subscription configuration and dependency setup #720

Draft
wants to merge 31 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3aee24f
Provice AccountManager in SubscriptionManager
miasma13 Mar 12, 2024
277f483
Add SubscriptionConfiguration for providing currentPurchasePlatform
miasma13 Mar 12, 2024
bdf8df6
Provide SubscriptionServiceEnvironment in configuration
miasma13 Mar 12, 2024
a5b74d5
Update DefaultSubscriptionConfiguration init
miasma13 Mar 12, 2024
567aa9c
Define SubscriptionURLProvider for environment dependant URLs
miasma13 Mar 12, 2024
65bd0eb
Sketch of next providers
miasma13 Mar 12, 2024
93341b5
Remove migrated email related URLs
miasma13 Mar 12, 2024
5998c2b
Fetch app store products
miasma13 Mar 12, 2024
3e20ab4
Merge branch 'main' into michal/subs-config-refactor
miasma13 Mar 15, 2024
a5b1226
Remove #URL macro usage
miasma13 Mar 15, 2024
2b7c55a
Extract services and flows
miasma13 Mar 17, 2024
2988168
Tweaks
miasma13 Mar 17, 2024
5decc35
Remove the unnecessary subscriptionAppGroup parameter
miasma13 Mar 17, 2024
9dab4a2
Merge branch 'main' into michal/subs-config-refactor
miasma13 Mar 17, 2024
5d711bf
Fixes after merge
miasma13 Mar 18, 2024
55b6395
Fix removing of the very last URL param
miasma13 Mar 18, 2024
7886465
Add subscription URL environment logic
miasma13 Mar 18, 2024
7a33855
Streamline Subscription URL environment operations
miasma13 Mar 18, 2024
fc623e1
Remove relict of the past
miasma13 Mar 18, 2024
8edb7c5
Refactor Subscription Keychain storing
miasma13 Mar 18, 2024
69b5811
Move around implementation for access token storage
miasma13 Mar 18, 2024
b7f6f51
Fix keychain delete to taget specific item
miasma13 Mar 18, 2024
a3bab2b
Define property wrapper for the generic keychain accessors
miasma13 Mar 18, 2024
63d3b37
Support storing auth token
miasma13 Mar 18, 2024
ba4ce24
Clear all tokens in storage
miasma13 Mar 18, 2024
53c5811
Move auth and access token to their storage
miasma13 Mar 18, 2024
ec93876
Move isUserAuthenticated to SubscriptionManager
miasma13 Mar 19, 2024
3031cb0
Move signOut to SubscriptionManager
miasma13 Mar 19, 2024
a148282
Move to use accountStorage
miasma13 Mar 19, 2024
b76bfa1
Merge branch 'main' into michal/subs-config-refactor
miasma13 Mar 19, 2024
ae34e53
Fix post merge issues
miasma13 Mar 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/Common/Extensions/URLExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ extension URL {

var percentEncodedQueryItems = components.percentEncodedQueryItems ?? [URLQueryItem]()
percentEncodedQueryItems.removeAll { parametersToRemove.contains($0.name) }
components.percentEncodedQueryItems = percentEncodedQueryItems
components.percentEncodedQueryItems = !percentEncodedQueryItems.isEmpty ? percentEncodedQueryItems : nil

return components.url ?? self
}
Expand Down

This file was deleted.

25 changes: 0 additions & 25 deletions Sources/Subscription/AccountStorage/SubscriptionTokenStorage.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,36 @@ public final class AppStoreAccountManagementFlow {
case authenticatingWithTransactionFailed
}

private var tokenStorage: SubscriptionTokenStorage
private var accountStorage: SubscriptionAccountStorage
private var accountManager: AccountManaging
private var authService: AuthServiceProtocol

init(tokenStorage: SubscriptionTokenStorage, accountStorage: SubscriptionAccountStorage, accountManager: AccountManaging, authService: AuthServiceProtocol) {
self.tokenStorage = tokenStorage
self.accountStorage = accountStorage
self.accountManager = accountManager
self.authService = authService
}

@discardableResult
public static func refreshAuthTokenIfNeeded(subscriptionAppGroup: String) async -> Result<String, AppStoreAccountManagementFlow.Error> {
public func refreshAuthTokenIfNeeded() async -> Result<String, AppStoreAccountManagementFlow.Error> {
os_log(.info, log: .subscription, "[AppStoreAccountManagementFlow] refreshAuthTokenIfNeeded")
let accountManager = AccountManager(subscriptionAppGroup: subscriptionAppGroup)

var authToken = accountManager.authToken ?? ""
var authToken = tokenStorage.authToken ?? ""

// Check if auth token if still valid
if case let .failure(validateTokenError) = await AuthService.validateToken(accessToken: authToken) {
if case let .failure(validateTokenError) = await authService.validateToken(accessToken: authToken) {
os_log(.error, log: .subscription, "[AppStoreAccountManagementFlow] validateToken error: %{public}s", String(reflecting: validateTokenError))

// In case of invalid token attempt store based authentication to obtain a new one
guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { return .failure(.noPastTransaction) }

switch await AuthService.storeLogin(signature: lastTransactionJWSRepresentation) {
switch await authService.storeLogin(signature: lastTransactionJWSRepresentation) {
case .success(let response):
if response.externalID == accountManager.externalID {
if response.externalID == accountStorage.externalID {
authToken = response.authToken
accountManager.storeAuthToken(token: authToken)
tokenStorage.authToken = authToken
}
case .failure(let storeLoginError):
os_log(.error, log: .subscription, "[AppStoreAccountManagementFlow] storeLogin error: %{public}s", String(reflecting: storeLoginError))
Expand Down
42 changes: 30 additions & 12 deletions Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,21 @@ public final class AppStorePurchaseFlow {
case missingEntitlements
}

public static func subscriptionOptions() async -> Result<SubscriptionOptions, AppStorePurchaseFlow.Error> {
private var tokenStorage: SubscriptionTokenStorage
private var accountManager: AccountManaging
private var authService: AuthServiceProtocol
private var subscriptionService: SubscriptionServiceProtocol

private lazy var appStoreRestoreFlow = AppStoreRestoreFlow(tokenStorage: tokenStorage, accountManager: accountManager, authService: authService, subscriptionService: subscriptionService)

init(tokenStorage: SubscriptionTokenStorage, accountManager: AccountManaging, authService: AuthServiceProtocol, subscriptionService: SubscriptionServiceProtocol) {
self.tokenStorage = tokenStorage
self.accountManager = accountManager
self.authService = authService
self.subscriptionService = subscriptionService
}

public func subscriptionOptions() async -> Result<SubscriptionOptions, AppStorePurchaseFlow.Error> {
os_log(.info, log: .subscription, "[AppStorePurchaseFlow] subscriptionOptions")

let products = PurchaseManager.shared.availableProducts
Expand All @@ -59,17 +73,16 @@ public final class AppStorePurchaseFlow {
public typealias TransactionJWS = String

// swiftlint:disable cyclomatic_complexity
public static func purchaseSubscription(with subscriptionIdentifier: String, emailAccessToken: String?, subscriptionAppGroup: String) async -> Result<TransactionJWS, AppStorePurchaseFlow.Error> {
public func purchaseSubscription(with subscriptionIdentifier: String, emailAccessToken: String?) async -> Result<TransactionJWS, AppStorePurchaseFlow.Error> {
os_log(.info, log: .subscription, "[AppStorePurchaseFlow] purchaseSubscription")

let accountManager = AccountManager(subscriptionAppGroup: subscriptionAppGroup)
let externalID: String

// Clear the Subscription cache
SubscriptionService.signOut()

// Check for past transactions most recent
switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase(subscriptionAppGroup: subscriptionAppGroup) {
switch await appStoreRestoreFlow.restoreAccountFromPastPurchase() {
case .success:
os_log(.info, log: .subscription, "[AppStorePurchaseFlow] purchaseSubscription -> restoreAccountFromPastPurchase: activeSubscriptionAlreadyPresent")
return .failure(.activeSubscriptionAlreadyPresent)
Expand All @@ -78,17 +91,19 @@ public final class AppStorePurchaseFlow {
switch error {
case .subscriptionExpired(let expiredAccountDetails):
externalID = expiredAccountDetails.externalID
accountManager.storeAuthToken(token: expiredAccountDetails.authToken)
tokenStorage.authToken = expiredAccountDetails.authToken
tokenStorage.accessToken = expiredAccountDetails.accessToken
accountManager.storeAccount(token: expiredAccountDetails.accessToken, email: expiredAccountDetails.email, externalID: expiredAccountDetails.externalID)
default:
// No history, create new account
switch await AuthService.createAccount(emailAccessToken: emailAccessToken) {
switch await authService.createAccount(emailAccessToken: emailAccessToken) {
case .success(let response):
externalID = response.externalID

if case let .success(accessToken) = await accountManager.exchangeAuthTokenToAccessToken(response.authToken),
case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) {
accountManager.storeAuthToken(token: response.authToken)
tokenStorage.authToken = response.authToken
tokenStorage.accessToken = accessToken
accountManager.storeAccount(token: accessToken, email: accountDetails.email, externalID: accountDetails.externalID)
}
case .failure(let error):
Expand All @@ -104,7 +119,10 @@ public final class AppStorePurchaseFlow {
return .success(transactionJWS)
case .failure(let error):
os_log(.error, log: .subscription, "[AppStorePurchaseFlow] purchaseSubscription error: %{public}s", String(reflecting: error))
AccountManager(subscriptionAppGroup: subscriptionAppGroup).signOut(skipNotification: true)
//
// subscriptionManager.signOut(skipNotification: true)
tokenStorage.clear()
// Do the silent clean up
switch error {
case .purchaseCancelledByUser:
return .failure(.cancelledByUser)
Expand All @@ -116,17 +134,17 @@ public final class AppStorePurchaseFlow {

// swiftlint:enable cyclomatic_complexity
@discardableResult
public static func completeSubscriptionPurchase(with transactionJWS: TransactionJWS, subscriptionAppGroup: String) async -> Result<PurchaseUpdate, AppStorePurchaseFlow.Error> {
public func completeSubscriptionPurchase(with transactionJWS: TransactionJWS) async -> Result<PurchaseUpdate, AppStorePurchaseFlow.Error> {

// Clear subscription Cache
SubscriptionService.signOut()

os_log(.info, log: .subscription, "[AppStorePurchaseFlow] completeSubscriptionPurchase")

guard let accessToken = AccountManager(subscriptionAppGroup: subscriptionAppGroup).accessToken else { return .failure(.missingEntitlements) }
guard let accessToken = tokenStorage.accessToken else { return .failure(.missingEntitlements) }

let result = await callWithRetries(retry: 5, wait: 2.0) {
switch await SubscriptionService.confirmPurchase(accessToken: accessToken, signature: transactionJWS) {
switch await subscriptionService.confirmPurchase(accessToken: accessToken, signature: transactionJWS) {
case .success:
return true
case .failure:
Expand All @@ -137,7 +155,7 @@ public final class AppStorePurchaseFlow {
return result ? .success(PurchaseUpdate(type: "completed")) : .failure(.missingEntitlements)
}

private static func callWithRetries(retry retryCount: Int, wait waitTime: Double, conditionToCheck: () async -> Bool) async -> Bool {
private func callWithRetries(retry retryCount: Int, wait waitTime: Double, conditionToCheck: () async -> Bool) async -> Bool {
var count = 0
var successful = false

Expand Down