Closed jesus-mg-ios closed 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!
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}
i am facing the same bug, i have checked all:
@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?
@aboedo i can reproduce this here is my video record contact me
@fandongtongxue How are you configuring the SDK? Are you using StoreKit 2? Does this reproduce if you use SK1?
@aboedo
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
}
}
//
// 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
}
}
the appstore link is https://apps.apple.com/cn/app/%E5%BD%95%E5%B1%8F%E5%B8%A6%E5%A3%B3/id1577909658
@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?
@aboedo i tryed this, and it worked, should i alway use storekit 1?
@fandongtongxue I'm glad it worked. Stick to StoreKit 1 for now, we'll work on getting this fixed for SK2.
cc @NachoSoto
@aboedo is this meaning that sk2 is enabled by default? I thought that it was sk1
@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.
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
@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.
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.
Facing the same issue today both versions SK1 and SK2. It is happening in the sandbox mode only.
Thanks for letting us know. The team is currently looking into this, it appears to be an issue on Apple's end.
My team is also facing this issue. Its been intermittent for the last few days, and now seems to not be working at all.
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.
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
The receipt verification service is up now and it's working fine now.
@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.
@aboedo this affected to production environment (no sandbox)? I mean, when the receipt verification service went down
@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.
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.
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?
@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.
I've run into this issue in the past as well. A few things that fixed it for me:
usesStoreKit2IfAvailable: false
, already outlined above by several people.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.
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.
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
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.
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.
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.
@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. "?
Happy to share it with you in private. You can reach me via Twitter, many thanks!
Will do!
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
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?
In case it helps to troubleshoot. I am getting the same error for my Monthly Product. However, the Yearly one works fine.
There should really be a disclaimer on using .with(usesStoreKit2IfAvailable: true)
It's clearly buggy per this thread!
@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.
I can reproduce this via the following steps.
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.
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.
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.
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.
@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)?
If you can email me that would be great: nacho@revenuecat.com
Hi, I'm facing with the same issue testing on Flutter TestFlight too. Is there any updates?
@kleon1024 what’s the full error that you’re getting?
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.
useStoreKit2IfEnabled(false)
) (Y/N): defaultAdditional 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,
This error is happening on MacOs and iOS on TestFlight:
And the reviewer rejects the binary each time that starts to make a purchase and the purchase cannot be done because of it.