Testing declined purchase

I'm trying to test a declined "Ask to buy" transaction and it runs but I'm getting a result I didn't expect: after the decline, the transaction's status shows as .pending, not .failed, which is what my mental model of the transaction cycle expects.

Here's the test code:

    func testAskToBuyThenDecline() throws {
        let session = try SKTestSession(configurationFileNamed: "Configuration")
        // We clear out all the old transactions before doing this transaction
        session.clearTransactions()
        // Make the test run without human intervention
        session.disableDialogs = true
        // Set AskToBuy
        session.askToBuyEnabled = true
        // Make the purchase
        XCTAssertNoThrow(try session.buyProduct(productIdentifier: "com.borderstamp.IAP0002"), "Couldn't buy product IAP0002")
        // A new transaction should be created with the purchase status == .deferred. The transaction will remain in the deferred state until it gets approved or rejected.
        XCTAssertTrue(session.allTransactions().count == 1, "Expected transaction count of 1, got \(session.allTransactions().count)")
        XCTAssertTrue(session.allTransactions()[0].state == .deferred, "Deferred purchase should be .deferred. Instead, we got a status of \(session.allTransactions()[0].state)")
        // Now we decline that transaction
        XCTAssertNoThrow(try session.declineAskToBuyTransaction(identifier: session.allTransactions()[0].identifier), "Failed to approve AskToBuy transaction")
        XCTAssertTrue(session.allTransactions()[0].state == .failed, "Declined purchase should fail. Instead, we got a status of \(session.allTransactions()[0].state.rawValue)")
         // Why is transaction.state set to .pending instead of .failed?

My mental model looks like PurchaseFlow1, where there are two separate transactions that occur. The result of my unit test, though, seems to imply PurchaseFlow2. Are either of these models correct?

PurchaseFlow1

PurchaseFlow2

In the SKDemo app (sample code project associated with WWDC21 session 10114: Meet StoreKit 2, the code to handle deferred transactions is

    func listenForTransactions() -> Task.Handle<Void, Error> {
        return detach {
            //Iterate through any transactions which didn't come from a direct call to `purchase()`.
            for await result in Transaction.updates {
                do {
                    let transaction = try self.checkVerified(result)

                    //Deliver content to the user.
                    await self.updatePurchasedIdentifiers(transaction)

                    //Always finish a transaction.
                    await transaction.finish()
                } catch {
                    //StoreKit has a receipt it can read but it failed verification. Don't deliver content to the user.
                    print("Transaction failed verification")
                }
            }
        }
    }

It seems to me the catch section should try to handle declined transactions but we don't get a transaction to handle unless we pass checkVerified. But we failed to pass checkVerified, which is how we ended up in the catch section.

I'm obviously thinking about this wrongly. Can anyone help me understand how it all actually does work?

Thanks

Replies

Thank you for the question. For child accounts where parental permission is required for in-app purchases, the purchase(options:) method will always result in .pending. There is no way to track the progress of the child's request in any environment, but if the purchase succeeds a transaction will be emitted from Transaction.updates. If the request for parental permission expires or is declined, there will be no notification of failure, which is why the SKTestTransaction's state property will never change to .failed. You should allow the user to initiate another purchase after purchase(options:) returns .pending, in the case a request for parental permission expires or is declined.

  • Thanks for clearing that up. It still feels odd to me that, if a parent declines a child's transaction, it remains as .pending rather than being set to .declined but it's good to know that everything is working as expected, and it's just my mental model that's wrong.

Add a Comment