Meet StoreKit 2

RSS for tag

Discuss the WWDC21 session Meet StoreKit 2.

Posts under wwdc21-10114 tag

50 Posts

Post

Replies

Boosts

Views

Activity

How to check in-app Purchase Date on Device with StoreKit 2
Hello, I have a markdown editor app, the model is in-app purchases, User download it for free,  and it have a 14-days trails for free (0 tier). a PRO 5$ unlock all the features. And it is not a subscribe model app. In 14-days trails, user could use all the features too, after the 14-days trails, if user do not buy the PRO, it return to the view mode. Example, user buy the 14-days trails in Oct 12, in Oct 15, user could use all features, how to get the purchase date, and validate the current date , if less than 14, user could still use all feature, and after 14 days, It return to view mode. I find some document that build a server connect to App Store server may validate the receipt, but I see the StoreKit 2 documents says it could validate in local, my small project maybe use local validate is more simple. How to check the user’s device date in or out of trails? Thanks for any advice.
0
0
533
Oct ’21
Storekit2 currentEntitlement function gives incorrect result in Sandbox testing after initial renewal?
I am using StoreKit2 to offer an auto-renewable subscription in an iOS app, and have observed inconsistent results from the currentEntitlement function during sandbox testing To illustrate the issue I have created a function which prints out a timestamped entitlement, as well as subscription status using the following code. @MainActor func printSubscriptionInfo() async { let productId = "com.mycompany.autorenewablesubscription.subscription.id" print() print(Date.now) // Current entitlement if let currentEntitlement = await Transaction.currentEntitlement(for: productId) { if case .verified(let transaction) = currentEntitlement { print("Purchase date: \(transaction.purchaseDate)") print("Expiry date: \(transaction.expirationDate!)") } else { print("Transaction not verified") } } else { print("No entitlement for \(productId)") } // Subscription information let product = (availableProducts.filter { $0.id == productId }).first! guard let statuses = try? await product.subscription?.status, let status = statuses.first else { print("Unable to get subscription status") return } print("Subscription status: \(status.stateDescription)") } The following shows the output of this function at various time points after a purchase, as well as the associated server notifications received. Purchase completed: at 12:23:20 Function output at 12:23:40: Purchase date: 12:23:09 Expiry date: 12:26:09 Subscription status: subscribed Server notifications: 12:23:13 - INTERACTIVE_RENEWAL 12:25:20 - INITIAL_BUY Function output at 12:26:18: No entitlement for com.mycompany.autorenewablesubscription.subscription.id Subscription status: subscribed Server notifications: 12:28:26 - INITIAL_BUY Function output at 12:31:03 Purchase date: 12:29:09 Expiry date: 12:32:09 Subscription status: subscribed The output of the function called after the first subscription renewal (ie at 12:26:09) shows no current entitlement, even though the subscription status is correct and there are no server notifications indicating expiry. After the next expiry period has passed, the function returns correct results once more. Has anyone else noticed this behaviour?
0
0
1.4k
Oct ’21
"“Test Apple Root CA - G3” certificate is not trusted" when verifying renewal info
Following the sample code from the session, I received errors when verifying the renewal info: [Default] [StoreKit] Failed to verify certificate chain due to client recoverable failure: Error Domain=NSOSStatusErrorDomain Code=-67843 "“Test Apple Root CA - G3” certificate is not trusted" UserInfo={NSLocalizedDescription=“Test Apple Root CA - G3” certificate is not trusted, NSUnderlyingError=0x2800835d0 {Error Domain=NSOSStatusErrorDomain Code=-67843 "Certificate 2 “Test Apple Root CA - G3” has errors: Root is not trusted;" UserInfo={NSLocalizedDescription=Certificate 2 “Test Apple Root CA - G3” has errors: Root is not trusted;}}} [Default] [StoreKit] Failed to verify signature for subscription status, will assume invalid: failedToVerifyCertificateChain Here's the code I'm using for getting subscription status @MainActor func subscriptionStatus() async -> (Product, Product.SubscriptionInfo.Status)? {   do {     // This app has only one subscription group so products in the subscriptions     // array all belong to the same group. The statuses returned by     // `product.subscription.status` apply to the entire subscription group.     guard let product = subscriptions.first,        let statuses = try await product.subscription?.status else {          return nil        }     var highestSubscriptionStatus: (product: Product, status: Product.SubscriptionInfo.Status)?     for status in statuses {       switch status.state {       case .expired, .revoked:         continue       default:         let renewalInfo = try checkVerified(status.renewalInfo)         guard let newSubscription = subscriptions.first(where: { $0.id == renewalInfo.currentProductID }) else {           continue         }         guard let currentProduct = highestSubscriptionStatus?.product else {           highestSubscriptionStatus = (newSubscription, status)           continue         }         let highestTier = tier(for: currentProduct.id)         let newTier = tier(for: renewalInfo.currentProductID)         if newTier > highestTier {           highestSubscriptionStatus = (newSubscription, status)         }       }     }     return highestSubscriptionStatus   } catch {     print("Could not update subscription status \(error)")     return nil   } } I'm running this on a real device without the storekit configuration file. Is this a known issue or am I missing something to get this working? Thanks
8
0
3.3k
Sep ’21
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
1
0
1.4k
Sep ’21
Storekit: get transaction history return 404
We try to get transaction history per this doc https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history However, we got 404, and response details are < HTTP/2 404 < server: daiquiri/3.0.0 < date: Mon, 19 Jul 2021 09:04:48 GMT < content-length: 0 < x-apple-jingle-correlation-key: TROLAF3A72KS6FCSQRCVCFFJKY < x-apple-request-uuid: 9c5cb017-60fe-952f-1452-84455114a956 < b3: 9c5cb01760fe952f145284455114a956-cdbaee3838492961 < x-b3-traceid: 9c5cb01760fe952f145284455114a956 < x-b3-spanid: cdbaee3838492961 < apple-seq: 0.0 < apple-tk: false < apple-originating-system: CommerceGateway < x-responding-instance: CommerceGateway:010196::: < apple-timing-app: 3 ms < strict-transport-security: max-age=31536000; includeSubDomains < x-daiquiri-instance: daiquiri:45824002:st44p00it-hyhk15104701:7987:21HOTFIX14 < Does it mean there is no related transaction history or something wrong with API usage? Thanks
3
0
2.6k
Aug ’21
How do I get all Consumable IAP Transactions with StoreKit 2
Hey all, I've been using StoreKit 2 lately and I'm so deeply impressed with how much easier it has become. For a better user experience regarding transaction history, I've built a view that would show every transaction made in the past with the option to request a refund natively. The only problem I'm facing at the moment is that I can't seem to get a list with all transactions, my app has Non-consumable, Consumable and Subscription IAP and I only get the Non-consumable + Subscriptions when I use the Transaction.all Does anyone have any idea how I can get the Consumable transactions as well? Current code @MainActor     func getPurchasedProducts() async {         //Iterate through all of the user's purchased products.         for await result in StoreKit.Transaction.all {             if case .verified(let transaction) = result {                 if !self.transactions.contains(transaction) {                     self.transactions.append(transaction)                     self.transactions = sortByDate(self.transactions)                 }             }         }     }
2
0
3.6k
Aug ’21
How to test Billing Retry subscriptions case in Sanbox or Xcode Environment?
We have a case to show users a message that their subscription was in Billing Retry. But we were unable to test it in Sandbox Environment. In the Sandbox environment, we were enabled interrupted purchase after initial purchase but that's not working as expected. In Xcode environment tried interrupted purchase and fail transactions with error. But Nothing works for renewal transactions. These all are working as expected only for the Initial purchase. Please provide a proper way to test the Billing Retry case.
1
0
1.3k
Jul ’21
Does currentEntitlement will contain a Billing Retry Subscription?
We have a case to show users a message that their subscription was in Billing Retry. Current Entitlements documentation says only active subscription transactions will be returned. Our doubt is whether the current entitlements will also provide billing retry subscriptions. If not what is the best way to get the billing retry subscriptions?
0
0
801
Jul ’21
Transaction.updates weird behaviors
I ran the SKDemoApp using Xcode 13.0 beta (13A5155e) and found that even if the transactions were finished in the detach task, all transactions were presented again in the next renewal. 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)                 print("tid: \(transaction.id!)")                 //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")             }         }     } } With above code snippet I printed the transaction id and it looked like this: tid: 0 tid: 0 tid: 1 tid: 0 tid: 1 tid: 2 tid: 0 tid: 1 tid: 2 tid: 3 tid: 0 tid: 1 tid: 2 tid: 3 tid: 4 tid: 0 tid: 1 tid: 2 tid: 3 tid: 4 tid: 5 Is this the expected behavior?
1
0
1.8k
Jul ’21
Transaction.all complains of "Bag value missing"
When querying Transaction.all, the output posts the following error: 2021-06-27 20:00:17.765248-0400 Test App[1203:12345] [Default] Error enumerating all transactions: Error Domain=AMSErrorDomain Code=204 "Bag Value Missing" UserInfo={NSLocalizedDescription=Bag Value Missing, NSLocalizedFailureReason=The bag does not contain in-app-history-max-age nor did anyone register a default value. <AMSBagFrozenDataSource: 0x107e5d370; profile: AMSFrozenBagValue; version: 1>} How might one provide an "in-app-history-max-age" or set a default?
1
0
1.4k
Jul ’21
How to check in-app Purchase Date on Device with StoreKit 2
Hello, I have a markdown editor app, the model is in-app purchases, User download it for free,  and it have a 14-days trails for free (0 tier). a PRO 5$ unlock all the features. And it is not a subscribe model app. In 14-days trails, user could use all the features too, after the 14-days trails, if user do not buy the PRO, it return to the view mode. Example, user buy the 14-days trails in Oct 12, in Oct 15, user could use all features, how to get the purchase date, and validate the current date , if less than 14, user could still use all feature, and after 14 days, It return to view mode. I find some document that build a server connect to App Store server may validate the receipt, but I see the StoreKit 2 documents says it could validate in local, my small project maybe use local validate is more simple. How to check the user’s device date in or out of trails? Thanks for any advice.
Replies
0
Boosts
0
Views
533
Activity
Oct ’21
Storekit2 currentEntitlement function gives incorrect result in Sandbox testing after initial renewal?
I am using StoreKit2 to offer an auto-renewable subscription in an iOS app, and have observed inconsistent results from the currentEntitlement function during sandbox testing To illustrate the issue I have created a function which prints out a timestamped entitlement, as well as subscription status using the following code. @MainActor func printSubscriptionInfo() async { let productId = "com.mycompany.autorenewablesubscription.subscription.id" print() print(Date.now) // Current entitlement if let currentEntitlement = await Transaction.currentEntitlement(for: productId) { if case .verified(let transaction) = currentEntitlement { print("Purchase date: \(transaction.purchaseDate)") print("Expiry date: \(transaction.expirationDate!)") } else { print("Transaction not verified") } } else { print("No entitlement for \(productId)") } // Subscription information let product = (availableProducts.filter { $0.id == productId }).first! guard let statuses = try? await product.subscription?.status, let status = statuses.first else { print("Unable to get subscription status") return } print("Subscription status: \(status.stateDescription)") } The following shows the output of this function at various time points after a purchase, as well as the associated server notifications received. Purchase completed: at 12:23:20 Function output at 12:23:40: Purchase date: 12:23:09 Expiry date: 12:26:09 Subscription status: subscribed Server notifications: 12:23:13 - INTERACTIVE_RENEWAL 12:25:20 - INITIAL_BUY Function output at 12:26:18: No entitlement for com.mycompany.autorenewablesubscription.subscription.id Subscription status: subscribed Server notifications: 12:28:26 - INITIAL_BUY Function output at 12:31:03 Purchase date: 12:29:09 Expiry date: 12:32:09 Subscription status: subscribed The output of the function called after the first subscription renewal (ie at 12:26:09) shows no current entitlement, even though the subscription status is correct and there are no server notifications indicating expiry. After the next expiry period has passed, the function returns correct results once more. Has anyone else noticed this behaviour?
Replies
0
Boosts
0
Views
1.4k
Activity
Oct ’21
"“Test Apple Root CA - G3” certificate is not trusted" when verifying renewal info
Following the sample code from the session, I received errors when verifying the renewal info: [Default] [StoreKit] Failed to verify certificate chain due to client recoverable failure: Error Domain=NSOSStatusErrorDomain Code=-67843 "“Test Apple Root CA - G3” certificate is not trusted" UserInfo={NSLocalizedDescription=“Test Apple Root CA - G3” certificate is not trusted, NSUnderlyingError=0x2800835d0 {Error Domain=NSOSStatusErrorDomain Code=-67843 "Certificate 2 “Test Apple Root CA - G3” has errors: Root is not trusted;" UserInfo={NSLocalizedDescription=Certificate 2 “Test Apple Root CA - G3” has errors: Root is not trusted;}}} [Default] [StoreKit] Failed to verify signature for subscription status, will assume invalid: failedToVerifyCertificateChain Here's the code I'm using for getting subscription status @MainActor func subscriptionStatus() async -> (Product, Product.SubscriptionInfo.Status)? {   do {     // This app has only one subscription group so products in the subscriptions     // array all belong to the same group. The statuses returned by     // `product.subscription.status` apply to the entire subscription group.     guard let product = subscriptions.first,        let statuses = try await product.subscription?.status else {          return nil        }     var highestSubscriptionStatus: (product: Product, status: Product.SubscriptionInfo.Status)?     for status in statuses {       switch status.state {       case .expired, .revoked:         continue       default:         let renewalInfo = try checkVerified(status.renewalInfo)         guard let newSubscription = subscriptions.first(where: { $0.id == renewalInfo.currentProductID }) else {           continue         }         guard let currentProduct = highestSubscriptionStatus?.product else {           highestSubscriptionStatus = (newSubscription, status)           continue         }         let highestTier = tier(for: currentProduct.id)         let newTier = tier(for: renewalInfo.currentProductID)         if newTier > highestTier {           highestSubscriptionStatus = (newSubscription, status)         }       }     }     return highestSubscriptionStatus   } catch {     print("Could not update subscription status \(error)")     return nil   } } I'm running this on a real device without the storekit configuration file. Is this a known issue or am I missing something to get this working? Thanks
Replies
8
Boosts
0
Views
3.3k
Activity
Sep ’21
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
1
Boosts
0
Views
1.4k
Activity
Sep ’21
Storekit: get transaction history return 404
We try to get transaction history per this doc https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history However, we got 404, and response details are < HTTP/2 404 < server: daiquiri/3.0.0 < date: Mon, 19 Jul 2021 09:04:48 GMT < content-length: 0 < x-apple-jingle-correlation-key: TROLAF3A72KS6FCSQRCVCFFJKY < x-apple-request-uuid: 9c5cb017-60fe-952f-1452-84455114a956 < b3: 9c5cb01760fe952f145284455114a956-cdbaee3838492961 < x-b3-traceid: 9c5cb01760fe952f145284455114a956 < x-b3-spanid: cdbaee3838492961 < apple-seq: 0.0 < apple-tk: false < apple-originating-system: CommerceGateway < x-responding-instance: CommerceGateway:010196::: < apple-timing-app: 3 ms < strict-transport-security: max-age=31536000; includeSubDomains < x-daiquiri-instance: daiquiri:45824002:st44p00it-hyhk15104701:7987:21HOTFIX14 < Does it mean there is no related transaction history or something wrong with API usage? Thanks
Replies
3
Boosts
0
Views
2.6k
Activity
Aug ’21
How do I get all Consumable IAP Transactions with StoreKit 2
Hey all, I've been using StoreKit 2 lately and I'm so deeply impressed with how much easier it has become. For a better user experience regarding transaction history, I've built a view that would show every transaction made in the past with the option to request a refund natively. The only problem I'm facing at the moment is that I can't seem to get a list with all transactions, my app has Non-consumable, Consumable and Subscription IAP and I only get the Non-consumable + Subscriptions when I use the Transaction.all Does anyone have any idea how I can get the Consumable transactions as well? Current code @MainActor     func getPurchasedProducts() async {         //Iterate through all of the user's purchased products.         for await result in StoreKit.Transaction.all {             if case .verified(let transaction) = result {                 if !self.transactions.contains(transaction) {                     self.transactions.append(transaction)                     self.transactions = sortByDate(self.transactions)                 }             }         }     }
Replies
2
Boosts
0
Views
3.6k
Activity
Aug ’21
How to test Billing Retry subscriptions case in Sanbox or Xcode Environment?
We have a case to show users a message that their subscription was in Billing Retry. But we were unable to test it in Sandbox Environment. In the Sandbox environment, we were enabled interrupted purchase after initial purchase but that's not working as expected. In Xcode environment tried interrupted purchase and fail transactions with error. But Nothing works for renewal transactions. These all are working as expected only for the Initial purchase. Please provide a proper way to test the Billing Retry case.
Replies
1
Boosts
0
Views
1.3k
Activity
Jul ’21
Does currentEntitlement will contain a Billing Retry Subscription?
We have a case to show users a message that their subscription was in Billing Retry. Current Entitlements documentation says only active subscription transactions will be returned. Our doubt is whether the current entitlements will also provide billing retry subscriptions. If not what is the best way to get the billing retry subscriptions?
Replies
0
Boosts
0
Views
801
Activity
Jul ’21
Transaction.updates weird behaviors
I ran the SKDemoApp using Xcode 13.0 beta (13A5155e) and found that even if the transactions were finished in the detach task, all transactions were presented again in the next renewal. 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)                 print("tid: \(transaction.id!)")                 //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")             }         }     } } With above code snippet I printed the transaction id and it looked like this: tid: 0 tid: 0 tid: 1 tid: 0 tid: 1 tid: 2 tid: 0 tid: 1 tid: 2 tid: 3 tid: 0 tid: 1 tid: 2 tid: 3 tid: 4 tid: 0 tid: 1 tid: 2 tid: 3 tid: 4 tid: 5 Is this the expected behavior?
Replies
1
Boosts
0
Views
1.8k
Activity
Jul ’21
Transaction.all complains of "Bag value missing"
When querying Transaction.all, the output posts the following error: 2021-06-27 20:00:17.765248-0400 Test App[1203:12345] [Default] Error enumerating all transactions: Error Domain=AMSErrorDomain Code=204 "Bag Value Missing" UserInfo={NSLocalizedDescription=Bag Value Missing, NSLocalizedFailureReason=The bag does not contain in-app-history-max-age nor did anyone register a default value. <AMSBagFrozenDataSource: 0x107e5d370; profile: AMSFrozenBagValue; version: 1>} How might one provide an "in-app-history-max-age" or set a default?
Replies
1
Boosts
0
Views
1.4k
Activity
Jul ’21