-
Notifications
You must be signed in to change notification settings - Fork 331
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
Call AppTransaction.refresh() in certain scenarios if AppTransaction.shared is invalid #4099
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
…shared is invalid: - After a purchase - When calling restorePurchaes This will always show an authentication prompt
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -223,14 +223,14 @@ final class PurchasesOrchestrator { | |
} | ||
|
||
func restorePurchases(completion: (@Sendable (Result<CustomerInfo, PurchasesError>) -> Void)?) { | ||
self.syncPurchases(receiptRefreshPolicy: .always, | ||
self.syncPurchases(receiptRefreshAllowed: true, | ||
isRestore: true, | ||
initiationSource: .restore, | ||
completion: completion) | ||
} | ||
|
||
func syncPurchases(completion: (@Sendable (Result<CustomerInfo, PurchasesError>) -> Void)? = nil) { | ||
self.syncPurchases(receiptRefreshPolicy: .never, | ||
self.syncPurchases(receiptRefreshAllowed: false, | ||
isRestore: allowSharingAppStoreAccount, | ||
initiationSource: .restore, | ||
completion: completion) | ||
|
@@ -1022,7 +1022,7 @@ private extension PurchasesOrchestrator { | |
} | ||
} | ||
|
||
func syncPurchases(receiptRefreshPolicy: ReceiptRefreshPolicy, | ||
func syncPurchases(receiptRefreshAllowed: Bool, | ||
isRestore: Bool, | ||
initiationSource: ProductRequestData.InitiationSource, | ||
completion: (@Sendable (Result<CustomerInfo, PurchasesError>) -> Void)?) { | ||
|
@@ -1034,11 +1034,12 @@ private extension PurchasesOrchestrator { | |
|
||
if self.systemInfo.storeKitVersion.isStoreKit2EnabledAndAvailable, | ||
#available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) { | ||
self.syncPurchasesSK2(isRestore: isRestore, | ||
self.syncPurchasesSK2(refreshPolicy: receiptRefreshAllowed ? .onlyIfEmpty : .never, | ||
isRestore: isRestore, | ||
initiationSource: initiationSource, | ||
completion: completion) | ||
} else { | ||
self.syncPurchasesSK1(receiptRefreshPolicy: receiptRefreshPolicy, | ||
self.syncPurchasesSK1(receiptRefreshPolicy: receiptRefreshAllowed ? .always : .never, | ||
isRestore: isRestore, | ||
initiationSource: initiationSource, | ||
completion: completion) | ||
|
@@ -1114,7 +1115,8 @@ private extension PurchasesOrchestrator { | |
|
||
// swiftlint:disable function_body_length | ||
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) | ||
private func syncPurchasesSK2(isRestore: Bool, | ||
private func syncPurchasesSK2(refreshPolicy: AppTransactionRefreshPolicy, | ||
isRestore: Bool, | ||
initiationSource: ProductRequestData.InitiationSource, | ||
completion: (@Sendable (Result<CustomerInfo, PurchasesError>) -> Void)?) { | ||
let currentAppUserID = self.appUserID | ||
|
@@ -1123,7 +1125,6 @@ private extension PurchasesOrchestrator { | |
self.attribution.unsyncedAdServicesToken { adServicesToken in | ||
_ = Task<Void, Never> { | ||
let transaction = await self.transactionFetcher.firstVerifiedTransaction | ||
let appTransactionJWS = await self.transactionFetcher.appTransactionJWS | ||
|
||
guard let transaction = transaction, let jwsRepresentation = transaction.jwsRepresentation else { | ||
// No transactions are present. If we have the originalPurchaseDate and originalApplicationVersion | ||
|
@@ -1139,6 +1140,9 @@ private extension PurchasesOrchestrator { | |
return | ||
} | ||
|
||
let appTransactionJWS = await self.transactionFetcher.appTransactionJWS( | ||
refreshPolicy: refreshPolicy) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved fetching the This way, if we don't need to post the receipt, we wouldn't be fetching the AppTransaction unnecessarily, potentially showing the authentication prompt. |
||
|
||
self.backend.post(receipt: .empty, | ||
productData: nil, | ||
transactionData: .init(appUserID: currentAppUserID, | ||
|
@@ -1157,6 +1161,7 @@ private extension PurchasesOrchestrator { | |
} | ||
|
||
let receipt = await self.encodedReceipt(transaction: transaction, jwsRepresentation: jwsRepresentation) | ||
let appTransactionJWS = await self.transactionFetcher.appTransactionJWS(refreshPolicy: refreshPolicy) | ||
|
||
self.createProductRequestData(with: transaction.productIdentifier) { productRequestData in | ||
let transactionData: PurchasedTransactionData = .init( | ||
|
@@ -1511,11 +1516,12 @@ extension PurchasesOrchestrator { | |
.get() | ||
} | ||
|
||
func syncPurchases(receiptRefreshPolicy: ReceiptRefreshPolicy, | ||
// Only used internally in tests | ||
func syncPurchases(receiptRefreshAllowed: Bool, | ||
isRestore: Bool, | ||
initiationSource: ProductRequestData.InitiationSource) async throws -> CustomerInfo { | ||
return try await Async.call { completion in | ||
self.syncPurchases(receiptRefreshPolicy: receiptRefreshPolicy, | ||
self.syncPurchases(receiptRefreshAllowed: receiptRefreshAllowed, | ||
isRestore: isRestore, | ||
initiationSource: initiationSource, | ||
completion: completion) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,13 +18,15 @@ import StoreKit | |
internal struct SK2AppTransaction { | ||
|
||
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) | ||
init(appTransaction: AppTransaction) { | ||
init(appTransaction: AppTransaction, jwsRepresentation: String) { | ||
self.bundleId = appTransaction.bundleID | ||
self.originalApplicationVersion = appTransaction.originalAppVersion | ||
self.originalPurchaseDate = appTransaction.originalPurchaseDate | ||
self.environment = .init(environment: appTransaction.environment) | ||
self.jwsRepresentation = jwsRepresentation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is similar to what we already so with the |
||
} | ||
|
||
let jwsRepresentation: String | ||
let bundleId: String | ||
let originalApplicationVersion: String? | ||
let originalPurchaseDate: Date? | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,16 @@ | |
import Foundation | ||
import StoreKit | ||
|
||
/// Determines the behavior when fetching the AppTransaction | ||
enum AppTransactionRefreshPolicy { | ||
|
||
// Calls refresh() only if AppTransaction.shared returns empty | ||
case onlyIfEmpty | ||
// Never calls refresh() | ||
case never | ||
|
||
} | ||
|
||
protocol StoreKit2TransactionFetcherType: Sendable { | ||
|
||
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) | ||
|
@@ -31,9 +41,9 @@ protocol StoreKit2TransactionFetcherType: Sendable { | |
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) | ||
var firstVerifiedTransaction: StoreTransaction? { get async } | ||
|
||
var appTransactionJWS: String? { get async } | ||
func appTransactionJWS(refreshPolicy: AppTransactionRefreshPolicy) async -> String? | ||
|
||
func appTransactionJWS(_ completionHandler: @escaping (String?) -> Void) | ||
func appTransactionJWS(refreshPolicy: AppTransactionRefreshPolicy, _ completionHandler: @escaping (String?) -> Void) | ||
|
||
} | ||
|
||
|
@@ -85,7 +95,7 @@ final class StoreKit2TransactionFetcher: StoreKit2TransactionFetcherType { | |
func fetchReceipt(containing transaction: StoreTransactionType) async -> StoreKit2Receipt { | ||
async let transactions = verifiedTransactions(containing: transaction) | ||
async let subscriptionStatuses = subscriptionStatusBySubscriptionGroupId | ||
async let appTransaction = appTransaction | ||
async let appTransaction = appTransaction(refreshPolicy: .onlyIfEmpty) | ||
|
||
return await .init( | ||
environment: .xcode, | ||
|
@@ -110,13 +120,11 @@ final class StoreKit2TransactionFetcher: StoreKit2TransactionFetcherType { | |
/// | ||
/// - Returns: A `String` containing the JWS representation of the app transaction, | ||
/// or `nil` if the feature is unavailable on the current platform version. | ||
var appTransactionJWS: String? { | ||
get async { | ||
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) { | ||
return try? await AppTransaction.shared.jwsRepresentation | ||
} else { | ||
return nil | ||
} | ||
func appTransactionJWS(refreshPolicy: AppTransactionRefreshPolicy) async -> String? { | ||
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) { | ||
return await appTransaction(refreshPolicy: refreshPolicy)?.jwsRepresentation | ||
} else { | ||
return nil | ||
} | ||
} | ||
|
||
|
@@ -130,10 +138,10 @@ final class StoreKit2TransactionFetcher: StoreKit2TransactionFetcherType { | |
/// if the feature is unavailable on the current platform version. | ||
/// - Parameter result: A `String?` containing the JWS representation of the app transaction, | ||
/// or `nil` if unavailable. | ||
func appTransactionJWS(_ completion: @escaping (String?) -> Void) { | ||
func appTransactionJWS(refreshPolicy: AppTransactionRefreshPolicy, _ completion: @escaping (String?) -> Void) { | ||
Async.call(with: completion) { | ||
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) { | ||
return try? await AppTransaction.shared.jwsRepresentation | ||
return await self.appTransaction(refreshPolicy: refreshPolicy)?.jwsRepresentation | ||
} else { | ||
return nil | ||
} | ||
|
@@ -181,7 +189,8 @@ extension StoreKit.VerificationResult where SignedType == StoreKit.AppTransactio | |
|
||
var verifiedAppTransaction: SK2AppTransaction? { | ||
switch self { | ||
case let .verified(transaction): return .init(appTransaction: transaction) | ||
case let .verified(transaction): | ||
return .init(appTransaction: transaction, jwsRepresentation: self.jwsRepresentation) | ||
case let .unverified(transaction, error): | ||
Logger.warn( | ||
Strings.storeKit.sk2_unverified_transaction(identifier: transaction.bundleID, error) | ||
|
@@ -279,22 +288,42 @@ extension StoreKit2TransactionFetcher { | |
} | ||
} | ||
|
||
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) | ||
private var appTransaction: SK2AppTransaction? { | ||
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) | ||
private var refreshedAppTransaction: SK2AppTransaction? { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The unified diff of this is a bit weird.
and |
||
get async { | ||
do { | ||
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) { | ||
let transaction = try await StoreKit.AppTransaction.shared | ||
return transaction.verifiedAppTransaction | ||
} else { | ||
Logger.warn(Strings.storeKit.sk2_app_transaction_unavailable) | ||
return nil | ||
} | ||
let transaction = try await StoreKit.AppTransaction.refresh() | ||
return transaction.verifiedAppTransaction | ||
} catch { | ||
Logger.warn(Strings.storeKit.sk2_error_fetching_app_transaction(error)) | ||
return nil | ||
} | ||
} | ||
} | ||
|
||
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) | ||
func appTransaction(refreshPolicy: AppTransactionRefreshPolicy) async -> SK2AppTransaction? { | ||
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) { | ||
do { | ||
let transaction = try await StoreKit.AppTransaction.shared | ||
if let transaction = transaction.verifiedAppTransaction { | ||
return transaction | ||
} else { | ||
return await refreshedAppTransaction | ||
} | ||
} catch { | ||
switch refreshPolicy { | ||
case .onlyIfEmpty: | ||
return await refreshedAppTransaction | ||
case .never: | ||
Logger.warn(Strings.storeKit.sk2_error_fetching_app_transaction(error)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would this qualify for a a |
||
return nil | ||
} | ||
} | ||
} else { | ||
Logger.warn(Strings.storeKit.sk2_app_transaction_unavailable) | ||
return nil | ||
} | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since
ReceiptRefreshPolicy
is very tied to SK1, I changed the parameter to a more SK-agnostic one and decide in the method body what policy to use for each SK version.