RevenueCat / purchases-ios

In-app purchases and subscriptions made easy. Support for iOS, watchOS, tvOS, macOS, and visionOS.
https://www.revenuecat.com/
MIT License
2.27k stars 303 forks source link

Issue with receipt. Receipt is not valid #2251

Closed jesus-mg-ios closed 1 year ago

jesus-mg-ios commented 1 year ago

Describe the bug A clear and concise description of what the bug is. The more detail you can provide the faster our team will be able to triage and resolve the issue. Do not remove any of the steps from the template below. If a step is not applicable to your issue, please leave that step empty.

  1. Environment
    1. Platform: iOS & MacOS
    2. SDK version: 4.16.0
    3. StoreKit 2 (disabled with useStoreKit2IfEnabled(false)) (Y/N): default
    4. OS version: MacOS 12.x, 13.x, iOS 15.x, 16.x
    5. Xcode version: 14.1.0
    6. How widespread is the issue. Percentage of devices affected.
  2. Debug logs that reproduce the issue
  3. Steps to reproduce, with a description of expected vs. actual behavior
  4. Other information (e.g. stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, etc.)

Additional context The Apple stuff experimented this error each time that starts to reviewing.

This error was experimented also on: https://github.com/RevenueCat/purchases-ios/issues/2074#issuecomment-1330389918,

2022-11-29 10:30:55.031363+0100 app[15527:1106073] [Purchases] - DEBUG: ℹ️ PaymentQueue updatedTransaction: app_product 2000000139898222 2000000042575785 1
2022-11-29 10:30:55.032634+0100 app[15527:1106073] [Purchases] - DEBUG: ℹ️ Force refreshing the receipt to get latest transactions from Apple.
2022-11-29 10:30:55.032724+0100 app[15527:1106073] [Purchases] - DEBUG: ℹ️ PaymentQueue updatedTransaction: app_product 2000000139941350 2000000042575785 1
2022-11-29 10:30:55.033742+0100 app[15527:1106073] [Purchases] - DEBUG: ℹ️ Force refreshing the receipt to get latest transactions from Apple.
2022-11-29 10:30:55.033817+0100 app[15527:1106073] [Purchases] - DEBUG: ℹ️ PaymentQueue updatedTransaction: app_product 2000000139998667 2000000042575785 1
2022-11-29 10:30:55.034896+0100 app[15527:1106073] [Purchases] - DEBUG: ℹ️ Force refreshing the receipt to get latest transactions from Apple.
2022-11-29 10:30:55.035001+0100 app[15527:1106073] [Purchases] - DEBUG: ℹ️ PaymentQueue updatedTransaction: app_product 2000000140025065 2000000042575785 1
2022-11-29 10:30:55.036151+0100 app[15527:1106073] [Purchases] - DEBUG: ℹ️ Force refreshing the receipt to get latest transactions from Apple.
2022-11-29 10:30:55.036248+0100 app[15527:1106073] [Purchases] - DEBUG: ℹ️ PaymentQueue updatedTransaction: app_product 2000000140060987 2000000042575785 1
2022-11-29 10:30:55.037464+0100 app[15527:1106073] [Purchases] - DEBUG: ℹ️ Force refreshing the receipt to get latest transactions from Apple.

--------
DEBUG: ℹ️ PaymentTransactionObserver (0x00000002815b2cc0) removedTransaction: app_product 2000000204486462 2000000042575785 1
2022-11-29 17:58:42.671777+0100 app[20021:1250441] [Purchases] - DEBUG: ℹ️ PaymentTransactionObserver (0x00000002815b2cc0) removedTransaction: app_product 2000000204486461 2000000042575785 1
2022-11-29 17:58:42.677649+0100 app[20021:1250441] [Purchases] - DEBUG: ℹ️ PaymentTransactionObserver (0x00000002815b2cc0) removedTransaction: mi app_product 2000000204486463 2000000042575785 1
2022-11-29 17:58:42.679432+0100 app[20021:1250441] [Purchases] - DEBUG: ℹ️ PaymentTransactionObserver (0x00000002815b2cc0) removedTransaction: app_product 2000000204486449 2000000042575785 1
2022-11-29 17:58:43.297245+0100 app[20021:1250441] [Purchases] - DEBUG: ℹ️ PaymentTransactionObserver (0x00000002815b2cc0) removedTransaction: app_product 2000000204486464 2000000042575785 1
ERROR: 😿‼️ The receipt is not valid. The purchased product was missing in the receipt. This is typically due to a bug in StoreKit.

This error is happening on MacOs and iOS on TestFlight:

ERROR: 😿‼️ The receipt is not valid. The purchased product was missing in the receipt. This is typically due to a bug in StoreKit.

And the reviewer rejects the binary each time that starts to make a purchase and the purchase cannot be done because of it.

RCGitBot commented 1 year ago

👀 SDKONCALL-213 We've just linked this issue to our internal tracker and notified the team. Thank you for reporting, we're checking this out!

fandongtongxue commented 1 year ago

I also meet this problem, here is the log 2023-01-30 09:42:32.668928+0800 ScreenRecordCase[14120:1333903] [Purchases] - ERROR: 💰 Product purchase for 'me.fandong.ScreenRecordCase.Pro' failed with error: Error Domain=RevenueCat.ErrorCode Code=8 "The receipt is not valid. The purchased product was missing in the receipt. This is typically due to a bug in StoreKit." UserInfo={rc_backend_error_code=7712, NSLocalizedDescription=The receipt is not valid. The purchased product was missing in the receipt. This is typically due to a bug in StoreKit., readable_error_code=INVALID_RECEIPT, NSUnderlyingError=0x282f842a0 {Error Domain=RevenueCat.BackendErrorCode Code=7712 "The purchased product was missing in the receipt. This is typically due to a bug in StoreKit." UserInfo={NSLocalizedDescription=The purchased product was missing in the receipt. This is typically due to a bug in StoreKit., rc_backend_error_code=7712}}, rc_response_status_code=422, source_function=convertUnsuccessfulResponseToError(), source_file=RevenueCat/HTTPClient.swift:379}

OrbitalTech commented 1 year ago

i am facing the same bug, i have checked all:

  1. Entitlements
  2. offering
  3. product
  4. App-Specific Shared Secret is uploaded
aboedo commented 1 year ago

@fandongtongxue @OrbitalTech @jesus-mg-ios we're aware of this issue happening on occasion, and have a radar with apple through which we're trying to figure out potential solutions.

The underlying problem, as stated by the error message, is that after the purchase is made, it doesn't show up on the receipt after we send it for verification with Apple.

If any of you can reproduce this consistently, please let me know, as it would be very helpful in getting this solved.

If you can't, do you know whether there are any patterns in terms of the users experiencing this? i.e.: is this happening to users who are re-subscribing to an expired subscription? Any commonalities in terms of OS version / device?

fandongtongxue commented 1 year ago

@aboedo i can reproduce this here is my video record contact me

https://user-images.githubusercontent.com/21986591/215754337-221697b0-d364-4a04-95a4-08b96da0e51e.mp4

aboedo commented 1 year ago

@fandongtongxue How are you configuring the SDK? Are you using StoreKit 2? Does this reproduce if you use SK1?

fandongtongxue commented 1 year ago

@aboedo

here is my appdelegate file

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        /* Enable debug logs before calling `configure`. */
        Purchases.logLevel = .debug

        /*
         Initialize the RevenueCat Purchases SDK.

            - `appUserID` is nil by default, so an anonymous ID will be generated automatically by the Purchases SDK.
                Read more about Identifying Users here: https://docs.revenuecat.com/docs/user-ids

            - `observerMode` is false by default, so Purchases will automatically handle finishing transactions.
                Read more about Observer Mode here: https://docs.revenuecat.com/docs/observer-mode
         */

        Purchases.configure(
            with: Configuration.Builder(withAPIKey: Constants.apiKey)
                .with(usesStoreKit2IfAvailable: true)
                .build()
        )

        /// - Set the delegate to this instance of AppDelegate. Scroll down to see this implementation.
        Purchases.shared.delegate = self
        return true
    }

/*
 Example implementation of PurchasesDelegate.
 */
extension AppDelegate: PurchasesDelegate {

    /// -  Whenever the `shared` instance of Purchases updates the PurchaserInfo cache, this method will be called.
    func purchases(_ purchases: Purchases, receivedUpdated customerInfo: CustomerInfo) {
        /// - If necessary, refresh app UI from updated PurchaserInfo
    }

}

now it is my product file

//
//  UpgradeViewController.swift
//  ScreenRecordCase
//
//  Created by Mac on 2021/5/22.
//

import UIKit
import StoreKit
import SafariServices
import FDUIKit
import FDNetwork
import RevenueCat

class UpgradeViewController: BaseViewController {

    var offering: Offering?

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        title = NSLocalizedString("Unlock all feature", comment: "")
        navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelBtnAction))
        navigationItem.rightBarButtonItem = UIBarButtonItem(customView: restoreBtn)

        view.addSubview(segmentControl)
        segmentControl.snp.makeConstraints { (make) in
            make.left.equalToSuperview().offset(15)
            make.right.equalToSuperview().offset(-15)
            make.top.equalToSuperview().offset(CGFloat.largeNavigationTitleHeight + 15)
        }
        view.addSubview(bgView)
        bgView.snp.makeConstraints { (make) in
            make.left.equalToSuperview().offset(15)
            make.right.equalToSuperview().offset(-15)
            make.height.equalTo(180)
            make.top.equalTo(self.segmentControl.snp.bottom).offset(15)
        }
        bgView.addSubview(funcLabel)
        funcLabel.snp.makeConstraints { (make) in
            make.top.equalToSuperview().offset(15)
            make.left.equalToSuperview().offset(15)
        }
        bgView.addSubview(musicLabel)
        musicLabel.snp.makeConstraints { (make) in
            make.top.equalTo(self.funcLabel.snp.bottom).offset(10)
            make.left.equalToSuperview().offset(15)
        }
        bgView.addSubview(watermarkLabel)
        watermarkLabel.snp.makeConstraints { (make) in
            make.top.equalTo(self.musicLabel.snp.bottom).offset(10)
            make.left.equalToSuperview().offset(15)
        }
        bgView.addSubview(modelLabel)
        modelLabel.snp.makeConstraints { (make) in
            make.top.equalTo(self.watermarkLabel.snp.bottom).offset(10)
            make.left.equalToSuperview().offset(15)
        }
        bgView.addSubview(futureLabel)
        futureLabel.snp.makeConstraints { (make) in
            make.top.equalTo(self.modelLabel.snp.bottom).offset(10)
            make.left.equalToSuperview().offset(15)
        }

        view.addSubview(collectionView)
        collectionView.snp.makeConstraints { make in
            make.left.right.equalToSuperview()
            make.top.equalTo(bgView.snp.bottom)
            make.height.equalTo(60 * 1 + 15 * 2)
        }

        view.addSubview(toolBar)
        if UIDevice.current.userInterfaceIdiom == .pad {
            toolBar.snp.makeConstraints { make in
                make.left.right.equalToSuperview()
                make.height.equalTo(44)
                make.bottom.equalToSuperview()
            }
        }else{
            toolBar.snp.makeConstraints { make in
                make.left.right.equalToSuperview()
                make.height.equalTo(CGFloat.tabBarHeight - .safeAreaBottomHeight)
                make.bottom.equalToSuperview().offset(-CGFloat.safeAreaBottomHeight)
            }
        }
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        requestProductData()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        UMAnalyticsSwift.beginLogPageView(pageName: title!)
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        UMAnalyticsSwift.endLogPageView(pageName: title!)
    }

    func requestProductData(isCurrent: Bool = true){
        /// - Load offerings when the paywall is displayed
        Purchases.shared.getOfferings { (offerings, error) in

            /// - If we have an error fetching offerings here, we'll print it out. You'll want to handle this case by either retrying, or letting your users know offerings weren't able to be fetched.
            if let error = error {
                print(error.localizedDescription)
            }
            if isCurrent {
                self.offering = offerings?.all["Ultra"]
            }else{
                self.offering = offerings?.all["Pro"]
            }

            self.collectionView.reloadData()
        }
    }

    @objc func cancelBtnAction(){
        dismiss(animated: true, completion: nil)
    }

    @objc func restoreBtnAction(sender: UIButton){
        Purchases.shared.getCustomerInfo { customerInfo, error in
            if self.segmentControl.selectedSegmentIndex == 0 {
                if customerInfo?.entitlements[Constants.ultra_entitlementID]?.isActive ?? false {
                    self.view.makeToast(NSLocalizedString("Already buy", comment: ""))
                    return
                }
            }else {
                if customerInfo?.entitlements[Constants.pro_entitlementID]?.isActive ?? false {
                    self.view.makeToast(NSLocalizedString("Already buy", comment: ""))
                    return
                }
            }
            self.view.isUserInteractionEnabled = false
            sender.isUserInteractionEnabled = false
            sender.setTitle("", for: .normal)
            self.indicatorView.color = .systemBlue
            sender.addSubview(self.indicatorView)
            self.indicatorView.snp.makeConstraints { make in
                make.center.equalToSuperview()
                make.size.equalTo(CGSize(width: 30, height: 30))
            }
            self.indicatorView.startAnimating()
            Purchases.shared.restorePurchases { (purchaserInfo, error) in
                if let error = error {
                    self.present(UIAlertController.errorAlert(message: error.localizedDescription), animated: true, completion: nil)
                }else {
                    if self.segmentControl.selectedSegmentIndex == 0 {
                        if purchaserInfo?.entitlements[Constants.ultra_entitlementID]?.isActive ?? false {
                            self.view.makeToast(NSLocalizedString("Already buy", comment: ""))
                        }else{
                            self.view.makeToast(NSLocalizedString("Not purchase", comment: ""))
                        }
                    }else {
                        if purchaserInfo?.entitlements[Constants.pro_entitlementID]?.isActive ?? false {
                            self.view.makeToast(NSLocalizedString("Already buy", comment: ""))
                        }else{
                            self.view.makeToast(NSLocalizedString("Not purchase", comment: ""))
                        }
                    }
                }
                self.asyncRestoreBtnAction()
            }
        }
    }

    @objc func asyncRestoreBtnAction(){
        view.isUserInteractionEnabled = true
        indicatorView.stopAnimating()
        indicatorView.removeFromSuperview()
        restoreBtn.setTitle(NSLocalizedString("Restore", comment: ""), for: .normal)
        restoreBtn.isUserInteractionEnabled = true
    }

    @objc func asyncBuyBtnAction(){
        indicatorView.stopAnimating()
        indicatorView.removeFromSuperview()
        view.isUserInteractionEnabled = true
    }

    @objc func segmentControlAction(sender: UISegmentedControl){
        requestProductData(isCurrent: segmentControl.selectedSegmentIndex == 0)
        modelLabel.isHidden = segmentControl.selectedSegmentIndex != 0
        futureLabel.isHidden = segmentControl.selectedSegmentIndex != 0
    }

    lazy var segmentControl: UISegmentedControl = {
        let pro_credit = UserDefaults.standard.integer(forKey: .pro_creditKey)
        let ultra_credit = UserDefaults.standard.integer(forKey: .ultra_creditKey)
        let segmentControl = UISegmentedControl(items: [NSLocalizedString("Ultra", comment: "")+"(\(ultra_credit))", NSLocalizedString("Pro", comment: "")+"(\(pro_credit))"])
        segmentControl.addTarget(self, action: #selector(segmentControlAction(sender:)), for: .valueChanged)
        segmentControl.selectedSegmentIndex = 0
        return segmentControl
    }()

    lazy var bgView: UIView = {
        let bgView = UIView(frame: .zero)
        bgView.layer.cornerRadius = 10
        bgView.clipsToBounds = true
        bgView.layer.borderColor = UIColor.systemGray4.cgColor
        bgView.layer.borderWidth = 1
        return bgView
    } ()

    lazy var banner: UIImageView = {
        let banner = UIImageView(frame: .zero)
        banner.backgroundColor = .systemGroupedBackground
        banner.layer.cornerRadius = 10
        banner.clipsToBounds = true
        banner.contentMode = .scaleAspectFill
        banner.image = UIImage(named: "music")
        return banner
    }()

    lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.register(ProductCell.classForCoder(), forCellWithReuseIdentifier: NSStringFromClass(ProductCell.classForCoder()))
        return collectionView
    }()

    lazy var funcLabel: UILabel = {
        let funcLabel = UILabel(frame: .zero)
        funcLabel.text = NSLocalizedString("Function", comment: "")
        funcLabel.font = .systemFont(ofSize: 17, weight: .medium)
        return funcLabel
    }()

    lazy var musicLabel: UILabel = {
        let musicLabel = UILabel(frame: .zero)
        musicLabel.text = NSLocalizedString("① DIY background music", comment: "")
        musicLabel.font = .systemFont(ofSize: 17, weight: .medium)
        return musicLabel
    }()

    lazy var watermarkLabel: UILabel = {
        let watermarkLabel = UILabel(frame: .zero)
        watermarkLabel.text = NSLocalizedString("② Delete app watermark", comment: "")
        watermarkLabel.font = .systemFont(ofSize: 17, weight: .medium)
        return watermarkLabel
    }()

    lazy var modelLabel: UILabel = {
        let modelLabel = UILabel(frame: .zero)
        modelLabel.text = NSLocalizedString("③ Select device model", comment: "")
        modelLabel.font = .systemFont(ofSize: 17, weight: .medium)
        return modelLabel
    }()

    lazy var futureLabel: UILabel = {
        let futureLabel = UILabel(frame: .zero)
        futureLabel.text = NSLocalizedString("④ Future functions", comment: "")
        futureLabel.font = .systemFont(ofSize: 17, weight: .medium)
        return futureLabel
    }()

    lazy var restoreBtn: UIButton = {
        let restoreBtn = UIButton(frame: .zero)
        restoreBtn.setTitle(NSLocalizedString("Restore", comment: ""), for: .normal)
        restoreBtn.setTitleColor(.systemBlue, for: .normal)
        restoreBtn.titleLabel?.font = .systemFont(ofSize: 17)
        restoreBtn.layer.cornerRadius = 10
        restoreBtn.clipsToBounds = true
        restoreBtn.addTarget(self, action: #selector(restoreBtnAction), for: .touchUpInside)
        return restoreBtn
    }()

    lazy var indicatorView: UIActivityIndicatorView = {
        let indicatorView = UIActivityIndicatorView()
        return indicatorView
    }()

    lazy var toolBar: UpgradeToolBar = {
        let toolBar = UpgradeToolBar(frame: .zero)
        toolBar.itemDelegate = self
        return toolBar
    }()

}

extension UpgradeViewController: UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout{
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        1
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        offering?.availablePackages.count ?? 0
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(ProductCell.classForCoder()), for: indexPath) as! ProductCell
        if let package = self.offering?.availablePackages[indexPath.row] {
            cell.titleLabel.text = package.storeProduct.localizedTitle + " " + package.localizedPriceString

//            if let intro = package.storeProduct.introductoryDiscount {
//                let packageTermsLabelText = intro.price == 0
//                ? "\(intro.subscriptionPeriod.periodTitle()) free trial"
//                : "\(package.localizedIntroductoryPriceString!) for \(intro.subscriptionPeriod.periodTitle())"
//
//                cell.packageTermsLabel.text = packageTermsLabelText
//            } else {
//                cell.packageTermsLabel.text = "Unlocks Premium"
//            }
        }
        return cell
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        CGSize(width: floor(view.bounds.size.width - 30), height: 60)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        15
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        15
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15)
    }

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        Purchases.shared.getCustomerInfo { customerInfo, error in
            if self.segmentControl.selectedSegmentIndex == 0 {
                if customerInfo?.entitlements[Constants.ultra_entitlementID]?.isActive ?? false {
                    self.view.makeToast(NSLocalizedString("Already buy", comment: ""))
                    return
                }
            }else {
                if customerInfo?.entitlements[Constants.pro_entitlementID]?.isActive ?? false {
                    self.view.makeToast(NSLocalizedString("Already buy", comment: ""))
                    return
                }
            }

            self.view.makeToastActivity(.center)
            self.view.isUserInteractionEnabled = false
            /// - Find the package being selected, and purchase it
            if let package = self.offering?.availablePackages[indexPath.row] {
                Purchases.shared.purchase(package: package) { (transaction, purchaserInfo, error, userCancelled) in
                    if userCancelled {
                        self.view.makeToast(NSLocalizedString("UserCancel", comment: ""))
                        self.view.hideToastActivity()
                        self.view.isUserInteractionEnabled = true
                        return
                    }
                    if let error = error {
                        self.present(UIAlertController.errorAlert(message: error.localizedDescription), animated: true, completion: nil)
                    } else {
                        /// - If the entitlement is active after the purchase completed, dismiss the paywall
                        if self.segmentControl.selectedSegmentIndex == 0 {
                            if purchaserInfo?.entitlements[Constants.ultra_entitlementID]?.isActive == true {
                                self.view.makeToast(NSLocalizedString("buyed", comment: ""))
                            }
                        }else {
                            if purchaserInfo?.entitlements[Constants.pro_entitlementID]?.isActive == true {
                                self.view.makeToast(NSLocalizedString("buyed", comment: ""))
                            }
                        }
                    }
                    self.view.hideToastActivity()
                    self.view.isUserInteractionEnabled = true
                }
            }
        }
    }

}

extension UpgradeViewController: UpgradeToolBarDelegate{
    func toolBar(toolBar: UpgradeToolBar, didClickItem: UpgradeToolBarItem) {
        var url = ""
        if didClickItem == .privacy {
            url = .serverURL+"privacy.html"
        }else{
            url = .serverURL+"service.html"
        }
        let config = SFSafariViewController.Configuration()
        config.entersReaderIfAvailable = true
        let vc = SFSafariViewController(url: URL(string: url)!, configuration: config)
        present(vc, animated: true, completion: nil)
    }
}

extension UIAlertController {
    class func errorAlert(message: String) -> UIAlertController {
        let errorAlert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
        errorAlert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
        return errorAlert
    }
}

about storekit2 i follow this doc and set version 2

WX20230201-093803@2x

the truth is i use correct for appstore, when in review it is not correct

the appstore link is https://apps.apple.com/cn/app/%E5%BD%95%E5%B1%8F%E5%B8%A6%E5%A3%B3/id1577909658

aboedo commented 1 year ago

@fandongtongxue could you try: using StoreKit 1 by doing:

Purchases.configure(
            with: Configuration.Builder(withAPIKey: Constants.apiKey)
                .with(usesStoreKit2IfAvailable: false)
                .build()
        )

And seeing if it reproduces?

fandongtongxue commented 1 year ago

@aboedo i tryed this, and it worked, should i alway use storekit 1?

aboedo commented 1 year ago

@fandongtongxue I'm glad it worked. Stick to StoreKit 1 for now, we'll work on getting this fixed for SK2.

aboedo commented 1 year ago

cc @NachoSoto

jesus-mg-ios commented 1 year ago

@aboedo is this meaning that sk2 is enabled by default? I thought that it was sk1

aboedo commented 1 year ago

@jesus-mg-ios recent versions of the SDK have rolled back the default to SK1, since we started noticing issues like this one happening more often when using SK2, sadly.

piersebdon commented 1 year ago

It would have been good if this was more widely reported to users - really disappointed that I have to check a GitHub issue to see this information in regards to not using SK2

aboedo commented 1 year ago

@piersebdon you're right. It's been a bit of a struggle on our side since SK2 does handle many things better, but it seems more prone to this sort of unpredictable behavior in practice.

In production, both Sk1 and Sk2 work reasonably well (better than in sandbox at least), but settling on a recommendation has been a bit difficult to be honest. We're working on improving our own data w/regards to real life performance of both, which will allow us to provide a more informed recommendation, and when we have that we'll communicate it out.

I apologize for the inconvenience.

kushsolitary commented 1 year ago

Hello, I am facing a similar issue with both versions 1 and 2. I can see that the purchase is made successfully (because it says you're already on the subscription when I try to purchase it again), but it fails in the completion block.

2023-02-08 01:36:54.861463+0530 Supersonic[24738:4439469] [Purchases] - INFO: ℹ️ Receipt parsed successfully
2023-02-08 01:36:54.862478+0530 Supersonic[24738:4439469] [Purchases] - INFO: ℹ️ Parsing receipt
2023-02-08 01:36:54.878269+0530 Supersonic[24738:4439469] [Purchases] - INFO: ℹ️ Receipt parsed successfully
2023-02-08 01:36:54.878638+0530 Supersonic[24738:4439491] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Started
2023-02-08 01:36:54.878709+0530 Supersonic[24738:4439491] [Purchases] - INFO: ℹ️ Parsing receipt
2023-02-08 01:36:54.894701+0530 Supersonic[24738:4439491] [Purchases] - INFO: ℹ️ Receipt parsed successfully
2023-02-08 01:36:54.896886+0530 Supersonic[24738:4439491] [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request POST receipts
2023-02-08 01:36:54.897912+0530 Supersonic[24738:4439491] [Purchases] - DEBUG: ℹ️ API request started: POST /v1/receipts
2023-02-08 01:36:55.251157+0530 Supersonic[24738:4439273] [Purchases] - ERROR: 😿‼️ The receipt is not valid.
2023-02-08 01:36:55.251365+0530 Supersonic[24738:4439273] [Purchases] - DEBUG: ℹ️ API request failed: POST /v1/receipts: The receipt is not valid.
2023-02-08 01:36:55.251498+0530 Supersonic[24738:4439273] [Purchases] - DEBUG: ℹ️ PostReceiptDataOperation: Finished
2023-02-08 01:36:55.251550+0530 Supersonic[24738:4439063] [Purchases] - ERROR: 😿‼️ The receipt is not valid.
2023-02-08 01:36:55.251905+0530 Supersonic[24738:4439273] [Purchases] - DEBUG: ℹ️ Serial request done: POST receipts, 0 requests left in the queue
2023-02-08 01:36:55.252739+0530 Supersonic[24738:4439566] [Purchases] - ERROR: 😿‼️ The receipt is not valid.

This only happens in TestFlight and Sandbox versions. The App Store version seems to be working fine.

tawhidkuet04 commented 1 year ago

Facing the same issue today both versions SK1 and SK2. It is happening in the sandbox mode only.

NachoSoto commented 1 year ago

Thanks for letting us know. The team is currently looking into this, it appears to be an issue on Apple's end.

joshuah459 commented 1 year ago

My team is also facing this issue. Its been intermittent for the last few days, and now seems to not be working at all.

corysullivan commented 1 year ago

I see the same issue. I feel like I have tried everything. I reviewed all the tutorials several times to ensure I got everything. I reverted to storeKit v1 and although this fixed my issues for the simulator I still have "The receipt is not valid." error in the sandbox environment.

kushsolitary commented 1 year ago

The receipt verification service is down https://developer.apple.com/system-status/

Looks like it's related to this news: https://developer.apple.com/news/?id=ytb7qj0x

tawhidkuet04 commented 1 year ago

The receipt verification service is up now and it's working fine now.

aboedo commented 1 year ago

@tawhidkuet04 thanks for the update! We're still seeing some apps having issues so it looks like it'll take a bit to propagate to everyone, but it's good to hear it's working for you.

jesus-mg-ios commented 1 year ago

@aboedo this affected to production environment (no sandbox)? I mean, when the receipt verification service went down

aboedo commented 1 year ago

@jesus-mg-ios the receipt verification service outage affected Sandbox only (and StoreKit Test / StoreKit Configuration files) as far as we could tell, production wasn't affected.

danielpunkass commented 1 year ago

I came upon this issue because I'm investigating the root cause of a behavior that my pre-publication subscriptions seem to routinely error out with "ERROR: 😿‼️ The receipt is not valid. The purchased product was missing in the receipt. This is typically due to a bug in StoreKit." It seems like a close match to this report. I'm only testing in sandbox/TestFlight for now. I think my version of the Purchases SDK is defaulting to enabledForCompatibleDevices ... I'll try hardcoding the initialization to disable StoreKit2 and see if that helps. Unfortunately it seems to take a while, maybe a few hours? After making a sandbox purchase, before I notice the failure.

danielpunkass commented 1 year ago

Oh, looking closer I guess the enabledOnlyForOptimizations is effectively the same as specifying false for the initialization value for usesStoreKit2IfAvailable. So maybe the issue I'm seeing is not the same as this?

mj-schmitz commented 1 year ago

@aboedo Still having this issue, even when setting purchases like so:

Purchases.configure({ apiKey: API_KEY, appUserID: null, observerMode: false, useAmazon: false, usesStoreKit2IfAvailable: false }); I'm using expo dev client to test in sandbox mode.

dezinezync commented 1 year ago

I've run into this issue in the past as well. A few things that fixed it for me:

  1. Using usesStoreKit2IfAvailable: false, already outlined above by several people
  2. Removing the StoreKit configuration (debug builds) from the RC app's configuration page
  3. Ensuring the DerivedData (and subsequently the .app/.ipa binary) is on the host OS' disk. If this folder is on any other disk, Sandboxing prevents the OS from writing the receipt to the app's contents folder.

Using StoreKit2 style web hooks under AppStoreConnect has no effect on any of this. RC continues to correctly parse the webhook and process it.

In my case, my app was being built for iOS-Universal and Mac Catalyst.

I hope this helps someone.

nickkohrn commented 1 year ago

I encountered this today. Unfortunately, Apple rejected my app for being unable to complete a purchase. After some testing, changing

.with(usesStoreKit2IfAvailable: true)

to

.with(usesStoreKit2IfAvailable: false)

seems to have resolved the errors during my testing.

I am going to resubmit my app shortly, and I will report back.

DigitalSolomon commented 1 year ago

I've been getting similar errors from the Flutter SDK on iOS while testing purchases. I'm on RevenueCat Flutter SDK 4.11.1.

Purchase Error: PlatformException(8, The receipt is not valid. The purchased product was missing in the receipt. This is typically due to a bug in StoreKit., {code: 8, underlyingErrorMessage: The purchased product was missing in the receipt. This is typically due to a bug in StoreKit., userCancelled: false, message: The receipt is not valid. The purchased product was missing in the receipt. This is typically due to a bug in StoreKit., readableErrorCode: INVALID_RECEIPT, readable_error_code: INVALID_RECEIPT

hansemannn commented 1 year ago

Any updates or common recommendations on this issue? It seems like it may only affect Sandbox, but as Apple tests with that as well, it's a blocker for any GO-live IIRC.

aboedo commented 1 year ago

The issue seems to be in StoreKit itself, and all we've heard back in our radar was that "The underlying data stores are eventually consistent", which is not a very satisfying response to be honest.

I think this is less likely to be triggered when using StoreKit 1, so I'd recommend configuring the SDK with StoreKit 1 for now (it's the default in all the latest SDKs), while we continue to work with Apple to get it fixed.

hansemannn commented 1 year ago

We actually did that (via init config), but it still happened, even on Testflight builds. Not sure how to proceed into review in this state right now.

aboedo commented 1 year ago

@hansemannn is this happening to you consistently? If so, would you mind sharing debug logs and sysdiag? That might help in getting this fixed. Is the message you're getting the same? "The purchased product was missing in the receipt. "?

hansemannn commented 1 year ago

Happy to share it with you in private. You can reach me via Twitter, many thanks!

aboedo commented 1 year ago

Will do!

aryunaabhay commented 1 year ago

I've been getting similar errors from the Flutter SDK on iOS while testing purchases. I'm on RevenueCat Flutter SDK 4.11.1.

Purchase Error: PlatformException(8, The receipt is not valid. The purchased product was missing in the receipt. This is typically due to a bug in StoreKit., {code: 8, underlyingErrorMessage: The purchased product was missing in the receipt. This is typically due to a bug in StoreKit., userCancelled: false, message: The receipt is not valid. The purchased product was missing in the receipt. This is typically due to a bug in StoreKit., readableErrorCode: INVALID_RECEIPT, readable_error_code: INVALID_RECEIPT

same thing happening to me, was about to update the plugin to the newest version but when testing a purchase is giving me this sam error. whats the highest and stable version for the flutter plugin that we should use ? the 4.11.1 does not seem to be working. cc @aboedo

sykbn1 commented 1 year ago

We're getting the same error on our Expo-React Native app. I checked and the default version is currently StoreKit 1 so I don't really know how my team can do at this point. Any news?

davidcv5 commented 1 year ago

In case it helps to troubleshoot. I am getting the same error for my Monthly Product. However, the Yearly one works fine.

kajensen commented 1 year ago

There should really be a disclaimer on using .with(usesStoreKit2IfAvailable: true)

It's clearly buggy per this thread!

aboedo commented 1 year ago

@kajensen we did deprecate the parameter (see the code in the latest version of the SDK), however, we're not aware of any specific bugs regarding StoreKit 2.

There are multiple issues can result in a similar message, so it's not necessarily related to StoreKit 2 usage. I went through some of the receipts that were resulting in these error messages with @hansemannn and IIRC we found that the issues were coming from the receipt itself, which seemed to have been malformed, and wouldn't be verifiable from StoreKit 1. Using a different sandbox account worked for that case.

I'm happy to help figure out each one of these, but I'm going to need more info:

Before doing any of that, I'd advise trying a brand new sandbox account though - there were issues with Apple's sandbox not too long ago, which caused devices to generate malformed receipts in iOS. The issue was coming from iOS itself, and receipts for those simply don't work.

klivin commented 1 year ago

I can reproduce this via the following steps.

  1. On a sandbox account start a subscription.
  2. Cancel the subscription
  3. Wait for the subscription period to lapse (helpful to use weekly as this lapses in a few minutes)
  4. Try to repurchase the subscription

receive the error: The purchased product was missing in the receipt. This is typically due to a bug in StoreKit.

Interestingly enough, I can successfully purchase non-purchased subscriptions, which is why I think step 2** is the key to this bug. I can also repurchase subscriptions that expire naturally (sandbox does this after a dayish for most durations)

** update, the subscription started working again after the "natural" expiration period lapses. I am implying that the natural expiration is some default limit on the subscriptions that sandbox accounts impose. So I found this error to be apparent during the period after cancelling and next expiration cycle and the "natural" sandbox expiration time.

chadpod commented 1 year ago

Are there any updates on this issue? Dealing with a lot of blowback from customers making repeated one time in-app purchases, but not ever getting what they paid for. Super frustrating.

NachoSoto commented 1 year ago

I just went through the repro steps @klivin mentioned and I couldn't reproduce this. @chadpod do you have debug logs after reproducing this? On debug, when we post the receipt, we print the receipt JSON. Seeing that would be helpful to diagnose the problem.

Thanks.

chadpod commented 1 year ago

Our QA person that was able to reproduce it (only on an iPhone XR test device for some reason) is on holiday and I am unable to reproduce. I'll try to get you some logs in the next couple of days.

chadpod commented 1 year ago

@NachoSoto I have a debug log of it failing. Do you want me to post it here or send it to you directly somehow (I scrubbed bundle id and a couple of things, but it still has RevCat user id, endpoints, etc)?

NachoSoto commented 1 year ago

If you can email me that would be great: nacho@revenuecat.com

kleon1024 commented 1 year ago

Hi, I'm facing with the same issue testing on Flutter TestFlight too. Is there any updates?

NachoSoto commented 1 year ago

@kleon1024 what’s the full error that you’re getting?