Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iOS Receipt Validation from Server Side #918

Closed
InfiniteDreamz opened this issue Jan 28, 2020 · 9 comments
Closed

iOS Receipt Validation from Server Side #918

InfiniteDreamz opened this issue Jan 28, 2020 · 9 comments
Labels
📱 iOS Related to iOS 🕵️‍♂️ need more investigation Need investigation on current issue 🚶🏻 stale Stale

Comments

@InfiniteDreamz
Copy link

InfiniteDreamz commented Jan 28, 2020

I am using my backend server to validate the iOS receipt. For that all I've done is passed the purchase.transactionReceipt to the application server in a post request. I want to know if purchase.transactionReceipt returns a base64 encoded receipt data or what? I'm getting error: 21002 with the current implementation

requestServerForReceiptVerification = (purchase: InAppPurchase | SubscriptionPurchase) => {
        const receipt = purchase.transactionReceipt;
        if (receipt) {
            // Hit Server API for Receipt Validation
            if (Platform.OS == 'ios') {
                let headers = {
                    'Content-Type': 'application/json'
                }
                Stores.UserStore.hitVerifyiOSReceiptAPI(receipt, headers, this.dropdown, (response: any) => {
                    finishTransaction(purchase).then(() => {
                        console.warn('Trasaction Finished');
                    }).catch((error) => {
                        console.warn(error.message);
                    })
                })
            } 
        }
    }
@hyochan hyochan added 📱 iOS Related to iOS 🕵️‍♂️ need more investigation Need investigation on current issue labels Feb 1, 2020
@InfiniteDreamz
Copy link
Author

@hyochan Can you help here please. The transaction receipt that I'm geting.. is it a base 64 encoded string? If not how can I convert it. I'm sending this to my BE server from where I'm hitting apple receipt validation API.

@leelandclay
Copy link

I'm looking into this same issue right now. On ios the transactionReceipt "looks" like a base64 string, but it's not. If I decode it on the device and output into the console, I get screens of garbage with occasional random words.

I then attempted to send it, as is up to my BE Server. There, I decoded it using dotnet core using this code:

string base64Decoded;
byte[] data = System.Convert.FromBase64String(receiptString);
base64Decoded = System.Text.Encoding.ASCII.GetString(data);
Console.WriteLine("ios receiptString:");
Console.WriteLine(base64Decoded);

The output for that is:
Screen Shot 2020-02-07 at 10 58 37 AM

My thought is that the receipt had each entry encoded...then the entire receipt encoded. I was hoping to find an answer here before I started testing that theory...but I'm going to go ahead and do that.

@leelandclay
Copy link

I've made a bit of progress. I was never able to get the validateReceiptIos function to return anything that could be decoded. My thought on that is there was a change in the way that Apple encodes it.

I was able to get usable information by passing the raw string up to my server and then posting it to the verifyReceipt endpoint. I used this as a guide: https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html

@InfiniteDreamz
Copy link
Author

InfiniteDreamz commented Feb 9, 2020

@leelandclay:
"I was able to get usable information by passing the raw string up to my server and then posting it to the verifyReceipt endpoint."
Did you just sent that transactionReceipt string to BE ?
I've went through the Apple Documentation but there the method of creating the receipt data is for the native iOS Application wherein we get appStoreReceiptURL.

@leelandclay
Copy link

Yes. Pass the object you got from the iap up to a server then send it to the website and you get back a JSON object.

@leelandclay
Copy link

OK...Here's where I'm at right now....

This is the code that I have within my app:

this.purchaseUpdateSubscription = purchaseUpdatedListener(
  (purchase: InAppPurchase | SubscriptionPurchase) => {
    console.log('purchaseUpdateSubscription called');
    if (purchase) {
      try {
        sendTransactionReceipt(this.state.profile.userId, purchase.transactionReceipt, this.state.token)
          .then(transactionReceipt => {
            finishTransaction(purchase, false)
              .then(finish => {
                this.setState({ transactionReceipt }, () => { this.sendingTransactionReceipt = false; });
              })
              .catch(err => {
                console.log('FinishTransaction ERROR: ' + err);
                this.sendingTransactionReceipt = false;
              });
          })
          .catch(() => {
            this.sendingTransactionReceipt = false;
          });
      } catch (ackErr) {
        console.warn('ackErr', ackErr);
      }
    }
  },
);

On the BE Server, I simply create the following object and POST it to the /verifyReceipt endpoint (using the server locations en the above documentation).

{
  "receipt-data": <purchase.transactionReceipt received from app>,
  "password": <secret password from within your apple account>,
  "exclude-old-transactions": true
}

I'm able to get responses back with valid information. Part of the response is a "latest_receipt" entry (used later). I store everything in my database as well as pass the information back down to the app so that it can call finishTransaction and store the valid receipt in state to be used.

I'm also working on a cron job that will execute once a day to grab all subscriptions that are showing as active and passed the expires date. That follows the same process as far as sending the information to the verifyReceipt. The "receipt-data" that I pass in is the "latest_receipt" that I stored from the previous verifyReceipt call.

Apparently, there's some weird way that the sandbox handles auto-renewing subscriptions, so I'm waiting to get a response on the Apple Developer Forums on what I should actually be looking for.

The situation that I'm seeing is that after purchasing a subscription, I get a response that shows auto renewing as true (pending_renewal_info[0].auto_renew_status from verifyReceipt shows as 1) and the expires_date entries (there's 3) show that it's in the future (5 minutes). If I wait until after the expires time and call verifyReceipt again, it will show the exact same values for the expires_date and auto renew.

My plan was to use the condition of auto renews === false and expires time < current time to determine whether to deactivate the subscription. Has anyone else seen this response from the verifyReceipt????

@bang9
Copy link
Contributor

bang9 commented Feb 18, 2020

check this one

const IOS_SHARED_PWD = "********";

async function validateIOS() {
    const latestPurchase = await getLatestPurchase();
    if (latestPurchase) {
        return false;
    }

    return RNIap.validateReceiptIos(
        {
            "receipt-data": latestPurchase.transactionReceipt,
            "password": IOS_SHARED_PWD,
            "exclude-old-transactions": false
        },
        false
    )
        .then(uncheckedValidation => {
            //check test receipt
            if (uncheckedValidation?.status === 21007) {
                return RNIap.validateReceiptIos(
                    {
                        "receipt-data": latestPurchase.transactionReceipt,
                        "password": IOS_SHARED_PWD,
                        "exclude-old-transactions": false
                    },
                    true
                );
            } else {
                return uncheckedValidation;
            }
        })
        .then(checkedValidation => {
            return isValidReceipt(checkedValidation);
        });
}

function getLatestPurchase() {
    return RNIap.getAvailablePurchases().then(purchases => {
        return purchases?.sort((a, b) => Number(b.transactionDate) - Number(a.transactionDate))?.[0] || null;
    });
}

function isValidReceipt(checkedValidation) {
    if (!checkedValidation) {
        return false;
    }

    // check is valid validation request
    if (checkedValidation.status === 21006 || checkedValidation.status === 21010) {
        return false;
    }

    const { latest_receipt_info: latestReceiptInfo } = checkedValidation;
    const latestReceipt = latestReceiptInfo
        ?.sort((a, b) => Number(b.purchase_date_ms) - Number(a.purchase_date_ms))
        ?.find(receipt => receipt.product_id === "some.product.id")?.[0];

    // no receipt
    if (!latestReceipt) {
        return false;
    }

    // refunded receipt
    if (latestReceipt.cancellation_date) {
        return false;
    }

    // expired receipt
    if (Number(latestReceipt.expires_date_ms) < Date.now()) {
        return false;
    }

    return checkedValidation.status === 0;
}

@stale
Copy link

stale bot commented May 18, 2020

Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as "For Discussion" or "Good first issue" and I will leave it open. Thank you for your contributions.

@stale stale bot added the 🚶🏻 stale Stale label May 18, 2020
@InfiniteDreamz
Copy link
Author

@leelandclay Thanks for putting up your solutions. This really helped a lot and we finally were able to validate our receipt. Thanks !!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
📱 iOS Related to iOS 🕵️‍♂️ need more investigation Need investigation on current issue 🚶🏻 stale Stale
Projects
None yet
Development

No branches or pull requests

4 participants