apple / app-store-server-library-java

MIT License
166 stars 38 forks source link

Getting null transaction id when using extractTransactionIdFromAppReceipt #123

Closed nayan-dhabarde closed 2 weeks ago

nayan-dhabarde commented 1 month ago

Hello team,

I have tested the code to extract transaction id from app receipt on sandbox, it was working fine.

When I deployed it on production it is failing to extractTransactionIdFromAppReceipt

// this is the server side code
val receiptUtil = ReceiptUtility();
val transactionId = receiptUtil.extractTransactionIdFromAppReceipt(appReceipt);

Server side build gradle


    implementation("com.apple.itunes.storekit:app-store-server-library:3.1.0")
    implementation("com.auth0:java-jwt:4.4.0")
    implementation("com.squareup.okhttp3:okhttp:4.9.0")
// this is the code in the app

let receiptURL = Bundle.main.appStoreReceiptURL

            if let url = receiptURL, let data = try? Data(contentsOf: url) {

                let base64Receipt = data.base64EncodedString(options: [])
                    do {
                        let result = try await repository.validateApplePurchase(userId: AppUserDefaults.shared.user?.id, purchaseToken: base64Receipt).data
                        if result == false {
                            fetchingProducts  = false
                            return nil
                        }
                    }
                    catch {
                        print("Couldn't read receipt data with error: " + error.localizedDescription)
                    }
            }
            // jUST SAMPLE code from SKDemo
            // Check whether the transaction is verified. If it isn't,
            // this function rethrows the verification error.
            let transaction = try checkVerified(verification)
alexanderjordanbaker commented 1 month ago

@nayan-dhabarde Not all app receipts have a transaction id within them, are you confident this one does?

nayan-dhabarde commented 1 month ago

@alexanderjordanbaker Wait, can you point me to some resources which can help me understand what kind of appReceipt won't have a transactionId.

  1. I was under the assumption that if a person made a transaction and generated the receipt using above code it will have the id. which I can send to my server and then use getTransactionInfo with this transaction id. This helps me verify the purchase and save it against my userId.

  2. I am doing this specially to bind my userId and the transaction on the server, I am getting the notification and i am able to decode it successfully, but the notification can only use userId in UUID format in appAccountToken whereas my userId is a Long right now.

  3. Why won't it have a transaction id?

alexanderjordanbaker commented 1 month ago

@nayan-dhabarde Transaction ids are for in-app purchases. This is an app receipt, if the user has no in-app purchases then there will be no transaction ids.

nayan-dhabarde commented 1 month ago

@alexanderjordanbaker I see, I am sending the receipt only after user has made the purchase to auto-renewable subscription. This is indeed an in-app purchase.

Here is my complete code, if it helps:

func purchase(_ product: Product) async throws -> Transaction? {
        guard let userId = AppUserDefaults.shared.user?.id else {
            throw NSError()
        }
        // Begin purchasing the `Product` the user selects.

        let options = Product.PurchaseOption.custom(key: "userId", value: "\(userId)")
        let result = try await product.purchase(options: [options])
        fetchingProducts  = true
        switch result {
        case .success(let verification):
            let receiptURL = Bundle.main.appStoreReceiptURL

            if let url = receiptURL, let data = try? Data(contentsOf: url) {

                let base64Receipt = data.base64EncodedString(options: [])
                    do {
                        let result = try await repository.validateApplePurchase(userId: AppUserDefaults.shared.user?.id, purchaseToken: base64Receipt).data
                        if result == false {
                            fetchingProducts  = false
                            return nil
                        }
                    }
                    catch {
                        print("Couldn't read receipt data with error: " + error.localizedDescription)
                    }
            }

            // Check whether the transaction is verified. If it isn't,
            // this function rethrows the verification error.
            let transaction = try checkVerified(verification)
            // The transaction is verified. Deliver content to the user.
            await updateCustomerProductStatus()

            // Always finish a transaction.
            await transaction.finish()

            fetchingProducts  = false
            return transaction
        case .userCancelled, .pending:
            fetchingProducts  = false
            return nil
        default:
            fetchingProducts  = false
            return nil
        }
    }
alexanderjordanbaker commented 1 month ago

@nayan-dhabarde This looks to be a StoreKit 2 purchase, is there any reason you aren't sending the transaction directly to your server and verifying it with a SignedDataVerifier? This would be much simpler and faster

That would be

case .success(let verification):
      let signedTransaction = verification.jwsRepresentation
alexanderjordanbaker commented 1 month ago

If you are using StoreKit 2 to perform the purchase, there should be no reason to use the deprecated app receipt and associated functions, ReceiptUtility is only around to support use cases that need to support clients using deprecated StoreKit 1, which doesn't appear to be the case here

nayan-dhabarde commented 1 month ago

@alexanderjordanbaker I see, thanks for clarifying that. I will give it a try

alexanderjordanbaker commented 2 weeks ago

Closing due to no comments, please create a new issue if any further questions are needed