Proper way to check Auto Renewable subscription cancelation (From Apple support), turn off Auto Renewing and change subscription scheme
Asked Answered
D

0

0

In my auto renewable subscription, I am always checking expires_date of latest_receipt_info from my validation receipt. If the expires_date is greater than my current time then I am giving my users all the premium facilities of my application, if not then I bring him to the purchase view controller.

Now my concern is:

  1. What if user cancel his Auto Renewable subscription from apple support?
  2. What if he turns off the auto renewing subscription of my application?
  3. What If user upgrade from one subscription to another subscription (Either upgrade or downgrade)

In the above three cases should I do some additional checking besides comparing expires_date with current date?

My receipt validation:

func receiptValidation(completion: @escaping(_ isPurchaseSchemeActive: Bool, _ error: Error?) -> ()) {
    let receiptFileURL = Bundle.main.appStoreReceiptURL
    guard let receiptData = try? Data(contentsOf: receiptFileURL!) else {
        //This is the First launch app VC pointer call
        completion(false, nil)
        return
    }
    let recieptString = receiptData.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
    let jsonDict: [String: AnyObject] = ["receipt-data" : recieptString as AnyObject, "password" : AppSpecificSharedSecret as AnyObject]
    
    do {
        let requestData = try JSONSerialization.data(withJSONObject: jsonDict, options: JSONSerialization.WritingOptions.prettyPrinted)
        let storeURL = URL(string: self.verifyReceiptURL)!
        var storeRequest = URLRequest(url: storeURL)
        storeRequest.httpMethod = "POST"
        storeRequest.httpBody = requestData
        let session = URLSession(configuration: URLSessionConfiguration.default)
        let task = session.dataTask(with: storeRequest, completionHandler: { [weak self] (data, response, error) in
            do {
                if let jsonResponse = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary {
                    //print("json response \(jsonResponse)")
                    if let expiresDate = self?.getPurchaseAndExpirationDateFromResponse(jsonResponse) {
                        //print("expiresDate \(expiresDate)")
                        let purchaseStatus = self?.isSubscriptionActive(expireDate: expiresDate)
                        if let purchaseStatus = purchaseStatus {
                            completion(purchaseStatus, nil)
                        }
                    }
                }
            } catch let parseError {
                completion(false, parseError)
            }
        })
        task.resume()
    } catch let parseError {
        completion(false, parseError)
    }
}

My JSON response:

json response {
    environment = Sandbox;
    "latest_receipt" = "***";
    "latest_receipt_info" =     (
                {
            "expires_date" = "2022-02-15 07:33:27 Etc/GMT";
            "expires_date_ms" = 1644910407000;
            "expires_date_pst" = "2022-02-14 23:33:27 America/Los_Angeles";
            "in_app_ownership_type" = PURCHASED;
            "is_in_intro_offer_period" = false;
            "is_trial_period" = false;
            "original_purchase_date" = "2022-02-15 06:29:29 Etc/GMT";
            "original_purchase_date_ms" = 1644906569000;
            "original_purchase_date_pst" = "2022-02-14 22:29:29 America/Los_Angeles";
            "original_transaction_id" = 1000000968987899;
            "product_id" = "com.test66.InAppPurchasePT.AutoRenewableGroup.ARenewable6";
            "purchase_date" = "2022-02-15 06:33:27 Etc/GMT";
            "purchase_date_ms" = 1644906807000;
            "purchase_date_pst" = "2022-02-14 22:33:27 America/Los_Angeles";
            quantity = 1;
            "subscription_group_identifier" = 20915143;
            "transaction_id" = 1000000968989231;
            "web_order_line_item_id" = 1000000072680194;
        },
                {
            "expires_date" = "2022-02-15 06:33:27 Etc/GMT";
            "expires_date_ms" = 1644906807000;
            "expires_date_pst" = "2022-02-14 22:33:27 America/Los_Angeles";
            "in_app_ownership_type" = PURCHASED;
            "is_in_intro_offer_period" = false;
            "is_trial_period" = true;
            "original_purchase_date" = "2022-02-15 06:29:29 Etc/GMT";
            "original_purchase_date_ms" = 1644906569000;
            "original_purchase_date_pst" = "2022-02-14 22:29:29 America/Los_Angeles";
            "original_transaction_id" = 1000000968987899;
            "product_id" = "com.test66.InAppPurchasePT.AutoRenewableGroup.ARenewable6";
            "purchase_date" = "2022-02-15 06:29:27 Etc/GMT";
            "purchase_date_ms" = 1644906567000;
            "purchase_date_pst" = "2022-02-14 22:29:27 America/Los_Angeles";
            quantity = 1;
            "subscription_group_identifier" = 20915143;
            "transaction_id" = 1000000968987899;
            "web_order_line_item_id" = 1000000072680193;
        }
    );
    "pending_renewal_info" =     (
                {
            "auto_renew_product_id" = "com.test66.InAppPurchasePT.AutoRenewableGroup.ARenewable6";
            "auto_renew_status" = 1;
            "original_transaction_id" = 1000000968987899;
            "product_id" = "com.test66.InAppPurchasePT.AutoRenewableGroup.ARenewable6";
        }
    );
    receipt =     {
        "adam_id" = 0;
        "app_item_id" = 0;
        "application_version" = 1;
        "bundle_id" = "com.test66.InAppPurchasePT";
        "download_id" = 0;
        "in_app" =         (
                        {
                "expires_date" = "2022-02-15 07:33:27 Etc/GMT";
                "expires_date_ms" = 1644910407000;
                "expires_date_pst" = "2022-02-14 23:33:27 America/Los_Angeles";
                "in_app_ownership_type" = PURCHASED;
                "is_in_intro_offer_period" = false;
                "is_trial_period" = false;
                "original_purchase_date" = "2022-02-15 06:29:29 Etc/GMT";
                "original_purchase_date_ms" = 1644906569000;
                "original_purchase_date_pst" = "2022-02-14 22:29:29 America/Los_Angeles";
                "original_transaction_id" = 1000000968987899;
                "product_id" = "com.test66.InAppPurchasePT.AutoRenewableGroup.ARenewable6";
                "purchase_date" = "2022-02-15 06:33:27 Etc/GMT";
                "purchase_date_ms" = 1644906807000;
                "purchase_date_pst" = "2022-02-14 22:33:27 America/Los_Angeles";
                quantity = 1;
                "transaction_id" = 1000000968989231;
                "web_order_line_item_id" = 1000000072680194;
            },
                        {
                "expires_date" = "2022-02-15 06:33:27 Etc/GMT";
                "expires_date_ms" = 1644906807000;
                "expires_date_pst" = "2022-02-14 22:33:27 America/Los_Angeles";
                "in_app_ownership_type" = PURCHASED;
                "is_in_intro_offer_period" = false;
                "is_trial_period" = true;
                "original_purchase_date" = "2022-02-15 06:29:29 Etc/GMT";
                "original_purchase_date_ms" = 1644906569000;
                "original_purchase_date_pst" = "2022-02-14 22:29:29 America/Los_Angeles";
                "original_transaction_id" = 1000000968987899;
                "product_id" = "com.test66.InAppPurchasePT.AutoRenewableGroup.ARenewable6";
                "purchase_date" = "2022-02-15 06:29:27 Etc/GMT";
                "purchase_date_ms" = 1644906567000;
                "purchase_date_pst" = "2022-02-14 22:29:27 America/Los_Angeles";
                quantity = 1;
                "transaction_id" = 1000000968987899;
                "web_order_line_item_id" = 1000000072680193;
            }
        );
        "original_application_version" = "1.0";
        "original_purchase_date" = "2013-08-01 07:00:00 Etc/GMT";
        "original_purchase_date_ms" = 1375340400000;
        "original_purchase_date_pst" = "2013-08-01 00:00:00 America/Los_Angeles";
        "receipt_creation_date" = "2022-02-15 06:33:20 Etc/GMT";
        "receipt_creation_date_ms" = 1644906800000;
        "receipt_creation_date_pst" = "2022-02-14 22:33:20 America/Los_Angeles";
        "receipt_type" = ProductionSandbox;
        "request_date" = "2022-02-15 06:33:21 Etc/GMT";
        "request_date_ms" = 1644906801424;
        "request_date_pst" = "2022-02-14 22:33:21 America/Los_Angeles";
        "version_external_identifier" = 0;
    };
    status = 0;
}
Drayton answered 15/2, 2022 at 5:29 Comment(12)
For case 1, I believe the only option is to have a server listening for updates from Apple. For case 2, the user's subscription simply won't renew at the end of the current period. For option 3, if they upgrade then they are refunded the pro-rata amount of their current subscription and the new level comes into effect immediately (You will see this as a new purchase in the receipt). For downgrade the new level comes into effect at the end of the current subscription period (again you will see that in the receipt at that time)Giovannigip
@Giovannigip Thanks a lot for your comment. So according to my implementation case 2 & case 3 will be satisfied I guess and for case 3 I need to implement server listening configuration. One more thing I verify the receipt verification from my app by 64base encoding. Do you think It will be rejected by Apple?Drayton
Why would it be rejected? The risk you have of verifying the receipt in your app is that it is vulnerable to spoofing compared to verifying on a server. You can also look at using StoreKit2 which has a much nicer API.Giovannigip
Ok, I got it :). I will definitely check Storekit2. In the case of cancellation (case 1) what attribute should I check? I have updated my post with the json response. I saw in the below post that I need to check cancellation_date_ms but it's not available in my json response. Will it appear If somebody cancel in production? How am I checking this case If I haven't set up server side configuration? Thanks! #5120677Drayton
You can call the validation endpoint from your app and you will get cancellation_date_ms if the purchase has been cancelled, but you are vulnerable to main-in-the-middle attacksGiovannigip
I added my receipt verification code in the post. In this call I am getting the JSON response. Did you mean this call by validation end point? Thanks!Drayton
Yes, since you are calling Apple's endpoint from your code and this endpoint is well-known as is the response it provides, it is possible for an attacker to place themselves between your app and the receipt validation endpoint and return a spoofed answer. This is why Apple recommends the use of a server to validate the receipt. StoreKit2 removes the need for you validate receipts yourself.Giovannigip
Yes, I got it but why there's no cancellation_date_ms in the response? Am I missing something?Drayton
Because the subscription isn't cancelled. There can only be a cancellation date if the subscription is cancelled.Giovannigip
Ok. Can you tell, will the cancellation_date_ms be in the latest_receipt_info if user cancel from apple support? Then I will write the code to handle it as well in my code, although I haven't configured the server listening configuration. Thanks!Drayton
@Giovannigip Can you please take have a look to this similar question? Thanks! #71139414Drayton
@Giovannigip I made the whole logic based on your reply. Can you please take have a look on this? #71174196Drayton

© 2022 - 2025 — McMap. All rights reserved.