Skip to content

Commit

Permalink
Fixes handling of deferred payments (ask to buy) (#296)
Browse files Browse the repository at this point in the history
* adds handling of SKPaymentTransactionStateDeferred

* adds test

* Moves error to RCPurchasesErrorUtils

* extracts the completion block logic into a function and protects against transaction.payment.productIdentifier
  • Loading branch information
vegaro authored Aug 1, 2020
1 parent 4c89d35 commit de0af8e
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 11 deletions.
33 changes: 24 additions & 9 deletions Purchases/Public/RCPurchases.m
Original file line number Diff line number Diff line change
Expand Up @@ -1031,10 +1031,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 @@ -1046,18 +1043,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: {
_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 @@ -2070,6 +2070,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

0 comments on commit de0af8e

Please sign in to comment.