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

Fixes handling of deferred payments (ask to buy) #296

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions Purchases/Public/RCPurchases.m
Original file line number Diff line number Diff line change
Expand Up @@ -1018,10 +1018,7 @@ - (void)storeKitWrapper:(RCStoreKitWrapper *)storeKitWrapper
break;
}
case SKPaymentTransactionStateFailed: {
RCPurchaseCompletedBlock completion = nil;
@synchronized (self) {
completion = self.purchaseCompleteCallbacks[transaction.payment.productIdentifier];
}
_Nullable RCPurchaseCompletedBlock completion = [self getAndRemovePurchaseCompletedBlockFor:transaction];

CALL_IF_SET_ON_MAIN_THREAD(
completion,
Expand All @@ -1033,18 +1030,36 @@ - (void)storeKitWrapper:(RCStoreKitWrapper *)storeKitWrapper
if (self.finishTransactions) {
[self.storeKitWrapper finishTransaction:transaction];
}

@synchronized (self) {
self.purchaseCompleteCallbacks[transaction.payment.productIdentifier] = nil;
}
break;
}
case SKPaymentTransactionStateDeferred:
case SKPaymentTransactionStateDeferred: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

_Nullable RCPurchaseCompletedBlock completion = [self getAndRemovePurchaseCompletedBlockFor:transaction];

NSError *pendingError = [RCPurchasesErrorUtils paymentDeferredError];
CALL_IF_SET_ON_MAIN_THREAD(completion,
transaction,
nil,
pendingError,
transaction.error.code == SKErrorPaymentCancelled);
break;
}
case SKPaymentTransactionStatePurchasing:
break;
}
}

- (nullable RCPurchaseCompletedBlock)getAndRemovePurchaseCompletedBlockFor:(SKPaymentTransaction *)transaction
{
RCPurchaseCompletedBlock completion = nil;
if (transaction.payment.productIdentifier) {
@synchronized (self) {
completion = self.purchaseCompleteCallbacks[transaction.payment.productIdentifier];
self.purchaseCompleteCallbacks[transaction.payment.productIdentifier] = nil;
}
}
return completion;
}

- (void)storeKitWrapper:(RCStoreKitWrapper *)storeKitWrapper
removedTransaction:(SKPaymentTransaction *)transaction
{}
Expand Down
9 changes: 8 additions & 1 deletion Purchases/Public/RCPurchasesErrorUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ NS_SWIFT_NAME(Purchases.ErrorUtils)
*/
+ (NSError *)backendErrorWithBackendCode:(nullable NSNumber *)backendCode backendMessage:(nullable NSString *)backendMessage;


/**
* Maps an RCBackendError code to a [RCPurchasesErrorCode] code. Constructs an NSError with the mapped code and adds a
* [RCUnderlyingErrorKey] in the [NSError.userInfo] dictionary. The backend error code will be mapped using
Expand Down Expand Up @@ -79,6 +78,14 @@ NS_SWIFT_NAME(Purchases.ErrorUtils)
*/
+ (NSError *)missingAppUserIDError;

/**
* Constructs an NSError with the [RCPaymentPendingError] code.
*
* @note This error is used during an “ask to buy” flow for a payment. The completion block of the purchasing function
* will get this error to indicate the guardian has to complete the purchase.
*/
+ (NSError *)paymentDeferredError;

/**
* Maps an SKErrorCode code to a RCPurchasesErrorCode code. Constructs an NSError with the mapped code and adds a
* [RCUnderlyingErrorKey] in the NSError.userInfo dictionary. The SKErrorCode code will be mapped using
Expand Down
6 changes: 5 additions & 1 deletion Purchases/Public/RCPurchasesErrorUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,6 @@ + (NSError *)errorWithCode:(RCPurchasesErrorCode)code
return [self errorWithCode:code message:message underlyingError:nil];
}


+ (NSError *)errorWithCode:(RCPurchasesErrorCode)code
underlyingError:(nullable NSError *)underlyingError {
return [self errorWithCode:code message:nil underlyingError:underlyingError extraUserInfo:nil];
Expand Down Expand Up @@ -300,6 +299,11 @@ + (NSError *)missingAppUserIDError {
return [self errorWithCode:RCInvalidAppUserIdError];
}

+ (NSError *)paymentDeferredError {
return [self errorWithCode:RCPaymentPendingError
message:@"The payment is deferred."];
}

+ (NSError *)purchasesErrorWithSKError:(NSError *)skError {

RCPurchasesErrorCode errorCode = RCPurchasesErrorCodeFromSKError(skError);
Expand Down
21 changes: 21 additions & 0 deletions PurchasesTests/Purchasing/PurchasesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2046,6 +2046,27 @@ class PurchasesTests: XCTestCase {
expect(RCSystemInfo.serverHostURL()) == defaultHostURL
}

func testNotifiesIfTransactionIsDeferredFromStoreKit() {
setupPurchases()
let product = MockSKProduct(mockProductIdentifier: "com.product.id1")
var receivedError: NSError?
self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in
receivedError = error as NSError?
}

let transaction = MockTransaction()
transaction.mockPayment = self.storeKitWrapper.payment!

transaction.mockState = SKPaymentTransactionState.deferred
self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction)

expect(self.backend.postReceiptDataCalled).to(beFalse())
expect(self.storeKitWrapper.finishCalled).to(beFalse())
expect(receivedError).toEventuallyNot(beNil())
expect(receivedError?.domain).toEventually(equal(Purchases.ErrorDomain))
expect(receivedError?.code).toEventually(equal(Purchases.ErrorCode.paymentPendingError.rawValue))
}

private func verifyUpdatedCaches(newAppUserID: String) {
expect(self.backend.getSubscriberCallCount).toEventually(equal(2))
expect(self.deviceCache.cachedPurchaserInfo.count).toEventually(equal(2))
Expand Down