-
Notifications
You must be signed in to change notification settings - Fork 316
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
[SK2] Enable Observer Mode for StoreKit 2 #3580
Changes from 10 commits
f1e49d1
e988db6
762df7d
0b62e26
d2dba88
7a274b6
ebb32b6
87a431a
8c05b52
2a9de8a
1b7a1fe
e52b1d1
48a84d9
f0e4b22
373f081
c800831
39a5a22
5d91282
7686b63
baf90c6
3754740
9e58ecf
31d1d5c
e6008c3
7b34817
f7d0a1f
3fc2dac
b00ef6a
eb83202
8e68c20
97bd115
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -1101,6 +1101,18 @@ public extension Purchases { | |||||||||||
|
||||||||||||
#endif | ||||||||||||
|
||||||||||||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||||||||||||
func handleObserverModeTransaction( | ||||||||||||
purchaseResult: StoreKit.Product.PurchaseResult | ||||||||||||
) async throws -> StoreTransaction? { | ||||||||||||
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. Note that this needs to call 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. I know this is Observer Mode but does it matter what they have set for the StoreKit version? Like... should this also require 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. Good catch. Indeed that's a requirement. |
||||||||||||
guard self.systemInfo.observerMode else { | ||||||||||||
throw NewErrorUtils.observerModeNotEnabledError() | ||||||||||||
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. Throw early, throw often. 😄 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. I would prevent creating yet another error for this. That's yet another error that users need to handle not just on this API, but in all of our APIs, and maybe localize to different languages, etc. |
||||||||||||
} | ||||||||||||
let (_, transaction) = try await self.purchasesOrchestrator.storeKit2TransactionListener.handle( | ||||||||||||
purchaseResult: purchaseResult, fromTransactionUpdate: true) | ||||||||||||
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. I wish we linted this stuff 😇
Suggested change
|
||||||||||||
return transaction | ||||||||||||
} | ||||||||||||
|
||||||||||||
} | ||||||||||||
|
||||||||||||
// swiftlint:enable missing_docs | ||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -12,6 +12,7 @@ | |||||
// Created by Nacho Soto on 9/20/22. | ||||||
|
||||||
import Foundation | ||||||
import StoreKit | ||||||
|
||||||
// swiftlint:disable file_length | ||||||
|
||||||
|
@@ -1008,6 +1009,35 @@ public protocol PurchasesSwiftType: AnyObject { | |||||
|
||||||
#endif | ||||||
|
||||||
/** | ||||||
* Use this method only if you already have your own IAP implementation using StoreKit 2 and want to use | ||||||
* RevenueCat's backend. If you are using StoreKit 1 for your implementation, you do not need this method. | ||||||
* | ||||||
* You only need to use this method with *new* purchases. Subscription updates are observed automatically. | ||||||
* | ||||||
* #### Example: | ||||||
* | ||||||
* ```swift | ||||||
* let result = try await product.purchase(options: options) | ||||||
* Purchases.shared.handleObserverModeTransaction(purchaseResult: result) | ||||||
* ``` | ||||||
* | ||||||
* - Note: You need to finish the transaction yourself after calling this method. | ||||||
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. Maybe turn this into a |
||||||
* | ||||||
* - Parameter purchaseResult: The ``StoreKit.Product.PurchaseResult`` of the product that was just purchased. | ||||||
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. I don't think you can reference types outside the module with "``", does this not give a warning when building? 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. I haven't noticed any warning, but replaced with single ticks since it cannot link anywhere ayway. 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. |
||||||
* | ||||||
* - Throws: An error of type ``ErrorCode`` is thrown if a failure occurs while handling the purchase. | ||||||
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 isn't guaranteed because Swift doesn't have typed throws yet. You need to do this: do {
try purchasesOrchestratorMethod()
} catch {
throw ErrorUtils.purchasesError(withUntypedError: error).asPublicError
} |
||||||
* | ||||||
* - Returns: A ``StoreTransaction`` if there was a transacton found and handled for the provided product ID. | ||||||
* | ||||||
* - Important: This should only be used if you have enabled observer mode during SDK configuration using | ||||||
* ``Configuration/Builder/with(observerMode:)`` | ||||||
*/ | ||||||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||||||
func handleObserverModeTransaction( | ||||||
purchaseResult: StoreKit.Product.PurchaseResult | ||||||
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. I think the swifty way would be
Suggested change
|
||||||
) async throws -> StoreTransaction? | ||||||
|
||||||
} | ||||||
|
||||||
// MARK: - | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,10 +37,11 @@ protocol StoreKit2TransactionListenerType: Sendable { | |
|
||
func set(delegate: StoreKit2TransactionListenerDelegate) async | ||
|
||
/// - Returns: `nil` `CustomerInfo` if purchases were not synced | ||
/// - Throws: Error if purchase was not completed successfully | ||
/// - Throws: ``ErrorCode`` if purchase was not completed successfully | ||
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. Technically these throw |
||
/// - Parameter fromTransactionUpdate: `true` only for transactions detected outside of a manual purchase flow. | ||
func handle( | ||
purchaseResult: StoreKit.Product.PurchaseResult | ||
purchaseResult: StoreKit.Product.PurchaseResult, | ||
fromTransactionUpdate: Bool | ||
) async throws -> StoreKit2TransactionListener.ResultData | ||
|
||
} | ||
|
@@ -113,15 +114,14 @@ actor StoreKit2TransactionListener: StoreKit2TransactionListenerType { | |
self.taskHandle = nil | ||
} | ||
|
||
/// - Returns: `nil` `CustomerInfo` if purchases were not synced | ||
/// - Throws: Error if purchase was not completed successfully | ||
func handle( | ||
purchaseResult: StoreKit.Product.PurchaseResult | ||
purchaseResult: StoreKit.Product.PurchaseResult, | ||
fromTransactionUpdate: Bool = false | ||
) async throws -> ResultData { | ||
switch purchaseResult { | ||
case let .success(verificationResult): | ||
let transaction = try await self.handle(transactionResult: verificationResult, | ||
fromTransactionUpdate: false) | ||
fromTransactionUpdate: fromTransactionUpdate) | ||
return (false, transaction) | ||
case .pending: | ||
throw ErrorUtils.paymentDeferredError() | ||
|
@@ -135,11 +135,6 @@ actor StoreKit2TransactionListener: StoreKit2TransactionListenerType { | |
} | ||
} | ||
|
||
} | ||
|
||
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) | ||
private extension StoreKit2TransactionListener { | ||
|
||
/// - Throws: ``ErrorCode`` if the transaction fails to verify. | ||
/// - Parameter fromTransactionUpdate: `true` only for transactions detected outside of a manual purchase flow. | ||
func handle( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -263,6 +263,11 @@ private func checkAsyncMethods(purchases: Purchases) async { | |
let _: CustomerInfo = try await purchases.restorePurchases() | ||
let _: CustomerInfo = try await purchases.syncPurchases() | ||
|
||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { | ||
let result = try await StoreKit.Product.products(for: [""]).first!.purchase() | ||
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. I like that you did it this way to ensure that whatever we pass to the next method is the result of this. |
||
let _: StoreTransaction? = try await purchases.handleObserverModeTransaction(purchaseResult: result) | ||
} | ||
|
||
for try await _: CustomerInfo in purchases.customerInfoStream {} | ||
|
||
#if os(iOS) || VISION_OS | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,7 +12,7 @@ | |
// Created by Nacho Soto on 12/15/22. | ||
|
||
import Foundation | ||
|
||
import Nimble | ||
@testable import RevenueCat | ||
import StoreKit | ||
import StoreKitTest | ||
|
@@ -45,13 +45,13 @@ class StoreKit2ObserverModeIntegrationTests: StoreKit1ObserverModeIntegrationTes | |
} | ||
|
||
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) | ||
func testPurchaseInDevicePostsReceipt() async throws { | ||
func testObservingTransactionUnlocksEntitlement() async throws { | ||
let result = try await self.manager.purchaseProductFromStoreKit2() | ||
let transaction = try XCTUnwrap(result.verificationResult?.underlyingTransaction) | ||
|
||
try self.testSession.disableAutoRenewForTransaction(identifier: UInt(transaction.id)) | ||
|
||
XCTExpectFailure("This test currently does not pass (see FB12231111)") | ||
_ = try await Purchases.shared.handleObserverModeTransaction(purchaseResult: result) | ||
|
||
try await asyncWait( | ||
description: "Entitlement didn't become active", | ||
|
@@ -81,6 +81,33 @@ class StoreKit2ObserverModeIntegrationTests: StoreKit1ObserverModeIntegrationTes | |
|
||
} | ||
|
||
class StoreKit2ObserverModeDisabledIntegrationTests: StoreKit1ObserverModeIntegrationTests { | ||
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 going to inherit and run every single test in the super class. 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. Ugh. This can be tested as unit in 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.
Yeah I think it would be useful, but also mock all the dependencies. So I think we can extract the 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. But we can put this in the regular SK2 test class, right? 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. true! I moved it to |
||
|
||
override class var storeKitVersion: StoreKitVersion { .storeKit2 } | ||
|
||
override class var observerMode: Bool { false } | ||
|
||
override func setUp() async throws { | ||
try await super.setUp() | ||
|
||
try AvailabilityChecks.iOS15APIAvailableOrSkipTest() | ||
} | ||
|
||
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) | ||
func testObservingTransactionThrowsIfObserverModeNotEnabled() async throws { | ||
NachoSoto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let result = try await self.manager.purchaseProductFromStoreKit2() | ||
let transaction = try XCTUnwrap(result.verificationResult?.underlyingTransaction) | ||
|
||
do { | ||
_ = try await Purchases.shared.handleObserverModeTransaction(purchaseResult: result) | ||
fail("Expected error") | ||
} catch { | ||
expect(error).to(matchError(ErrorCode.observerModeNotEnabledError)) | ||
} | ||
} | ||
|
||
} | ||
|
||
class StoreKit1ObserverModeIntegrationTests: BaseStoreKitObserverModeIntegrationTests { | ||
|
||
override class var storeKitVersion: StoreKitVersion { .storeKit1 } | ||
|
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.
Nit: the
error
label isn't required since it doesn't add anything to the type: