Meet StoreKit 2

RSS for tag

Discuss the WWDC21 session Meet StoreKit 2.

Posts under wwdc21-10114 tag

45 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
540
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.5k
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.4k
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 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
540
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.5k
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.4k
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