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