We have some users who have upgraded to iOS 26 beta3. Currently, we observe that when these users make in-app purchases, our code calls [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; method, and we clearly receive the successful removal callback in the delegate method - (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray<SKPaymentTransaction *> *)transactions. However, when users click on products with the same productId again, the method - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions still returns information about previously removed transactions, preventing users from making further in-app purchases.
StoreKit
RSS for tagSupport in-app purchases and interactions with the App Store using StoreKit.
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
While testing an inapp purchase, if you purchase the same product twice from an account that does not have testflight permissions for the project and only has sandbox permissions, the second payment will receive the same transactionId as the first.
Why is that so? I know transactionID is unique.
We are considering a price change for the auto-renewing subscriptions we currently offer in a Production environment and have made system modifications to our servers.
We would like to implement a price change for purchases made through our SANDBOX Apple account in order to test if our system is capable of handling the price change.
I set both production and sandbox App Store Notification server.
In sandbox, my server can receive all kinds of app store notification, including subscription and non-consumable in-app-purchase.
But in production, my server only receive subscription notification.
I can see some non-consumable in-app-purchase done in log, but the server didn't receive expected notification.
Anyone know why?
I really need the notification cause I need to know who made refund.
My question is simple, I do not have much experience in writing swift code, I am only doing it to create a small executable that I can call from my python application which completes Subcription Management.
I was hoping someone with more experience could point out my flaws along with giving me tips on how to verify that the check is working for my applicaiton. Any inight is appreciated, thank you.
import Foundation
import StoreKit
class SubscriptionValidator {
static func getReceiptURL() -> URL? {
guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL else {
print("No receipt found.")
return nil
}
return appStoreReceiptURL
}
static func validateReceipt() -> Bool {
guard let receiptURL = getReceiptURL(),
let receiptData = try? Data(contentsOf: receiptURL) else {
print("Could not read receipt.")
return false
}
let receiptString = receiptData.base64EncodedString()
let validationResult = sendReceiptToApple(receiptString: receiptString)
return validationResult
}
static func sendReceiptToApple(receiptString: String) -> Bool {
let isSandbox = Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"
let urlString = isSandbox ? "https://sandbox.itunes.apple.com/verifyReceipt" : "https://buy.itunes.apple.com/verifyReceipt"
let url = URL(string: urlString)!
let requestData: [String: Any] = [
"receipt-data": receiptString,
"password": "0b7f88907b77443997838c72be52f5fc"
]
guard let requestBody = try? JSONSerialization.data(withJSONObject: requestData) else {
print("Error creating request body.")
return false
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = requestBody
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let semaphore = DispatchSemaphore(value: 0)
var isValid = false
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil,
let jsonResponse = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let status = jsonResponse["status"] as? Int else {
print("Receipt validation failed.")
semaphore.signal()
return
}
if status == 0, let receipt = jsonResponse["receipt"] as? [String: Any],
let inApp = receipt["in_app"] as? [[String: Any]] {
for purchase in inApp {
if let expiresDateMS = purchase["expires_date_ms"] as? String,
let expiresDate = Double(expiresDateMS) {
let expiryDate = Date(timeIntervalSince1970: expiresDate / 1000.0)
if expiryDate > Date() {
isValid = true
}
}
}
}
semaphore.signal()
}
task.resume()
semaphore.wait()
return isValid
}
}
For testing purposes, I log out of media and purchases to use sandbox ID's to test in app purchases. Once I log out of media and purchases in device settings, in app purchases is still using my real apple ID, I'm not being prompted anymore to log into a sandbox ID for purchases.
Topic:
App & System Services
SubTopic:
StoreKit
Our app offers auto-renewable subscriptions using StoreKit Original API for In-App Purchase and App Store Server Notifications V1.
Starting around 2025-03-15, we found some cases where original_transaction_id which was associated to web_order_line_item_id of already purchased subscription had changed in the receipt information of verifyReceipt response or App Store Server Notifications V1.
The detailed steps are:
Around February 2025, re-purchase from the app the same subscription product which was canceled and expired some time ago, using StoreKit Original API for In-App Purchase
Receive the following 2 notifications from App Store Server Notifications V1 almost at the same time
INITIAL_BUY
DID_CHANGE_RENEWAL_STATUS
In both notifications, latest_receipt_info contains the receipt for the re-purchased subscription period with new original_transaction_id and web_order_line_item_id
pending_renewal_info contains both new original_transaction_id and original one which was generated at first purchase, and original one has "is_in_billing_retry_period": "1"
Starting around 2025-03-15, the following happens
When we receive another "DID_CHANGE_RENEWAL_STATUS" notification from App Store Server Notifications V1, original_transaction_id which is associated to web_order_line_item_id of the re-purchased subscription period has changed back to the original one (the one which was generated at first purchase) in latest_receipt_info.
When we call verifyReceipt with the receipt obtained from appStoreReceiptURL, the response does not seem to contain new original_transaction_id which was generated at re-purchase
We have some questions regarding this original_transaction_id behavior.
When a user re-purchase the same subscription product which was canceled and expired some time ago, it seems that new original_transaction_id is generated. Is this an expected behavior?
If yes, it seems that, at some point, original_transaction_id which is associated to web_order_line_item_id of the re-purchased subscription changed back to the original original_transaction_id which had been generated at first purchase. Is this an expected behavior?
What triggers the original_transaction_id change to the original one? Is it related to some user actions or subscription status change?
Topic:
App & System Services
SubTopic:
StoreKit
Tags:
StoreKit
App Store Server Notifications
App Store Receipts
I am currently developing an auto-renewal subscription in-app purchase for my app.
Currently, the subscription items have already been approved in appStoreConnect, and the .store file is synced with appStoreConnect, so the subscription items are displayed well and the test is also progressing well.
However, when I build without using the .store file to perform sandbox testing, the subscription items do not appear and response.invalidProductIdentifiers appears.
Is there anything I need to do additionally so that the subscription items can appear in response.products?
ps. The bank account item in the contract is in 'processing' status, and the paid app contract status is 'waiting for user information'.
While reviewing the Apple Documentation, I came across a potential issue in one of the examples that I believe is worth addressing.
The example appears to compare strings instead of integers, which could lead to unexpected behavior in production environments. Specifically, in the line where originalMajorVersion (a string) is compared with newBusinessModelMajorVersion (also a string) using <:
if originalMajorVersion < newBusinessModelMajorVersion
This comparison performs a lexicographical check rather than evaluating the numerical values of the strings. As a result, strings like "10" would incorrectly be considered less than "2", which is not the desired behaviour when comparing version numbers.
I have reported this via the Feedback assistant (FB16432337) but at the time of posting this there has been no reply at all (23 days)
Supporting business model changes by using the app transaction
do {
// Get the appTransaction.
let shared = try await AppTransaction.shared
if case .verified(let appTransaction) = shared {
// Hard-code the major version number in which the app's business model changed.
let newBusinessModelMajorVersion = "2"
// Get the major version number of the version the customer originally purchased.
let versionComponents = appTransaction.originalAppVersion.split(separator: ".")
let originalMajorVersion = versionComponents[0]
if originalMajorVersion < newBusinessModelMajorVersion {
// This customer purchased the app before the business model changed.
// Deliver content that they're entitled to based on their app purchase.
}
else {
// This customer purchased the app after the business model changed.
}
}
}
catch {
// Handle errors.
}
Topic:
App & System Services
SubTopic:
StoreKit
My Mac app fails to open for some users with the error:
"ABC.app does not support the latest receipt validation requirements."
I assume this is due to the update of the App Store receipt signing intermediate certificate with one that uses the SHA-256 algorithm.
I cannot reproduce this myself and I have trouble figuring out how to address this issue.
Below is the code that decrypts the receipt and verifies its signature.
How does this code need to be updated to support the new signing certificate?
Thanks a lot in advance!
inline static void CheckBundleSignature(void)
{
NSURL *bundleURL = [[NSBundle mainBundle] bundleURL];
SecStaticCodeRef staticCode = NULL;
OSStatus status = SecStaticCodeCreateWithPath((__bridge CFURLRef)bundleURL, kSecCSDefaultFlags, &staticCode);
if (status != errSecSuccess) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to validate bundle signature: Create a static code", nil];
}
NSString *requirementText = @"anchor apple generic";
SecRequirementRef requirement = NULL;
status = SecRequirementCreateWithString((__bridge CFStringRef)requirementText, kSecCSDefaultFlags, &requirement);
if (status != errSecSuccess) {
if (staticCode) CFRelease(staticCode);
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to validate bundle signature: Create a requirement", nil];
}
status = SecStaticCodeCheckValidity(staticCode, kSecCSDefaultFlags, requirement);
if (status != errSecSuccess) {
if (staticCode) CFRelease(staticCode);
if (requirement) CFRelease(requirement);
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to validate bundle signature: Check the static code validity", nil];
}
if (staticCode) CFRelease(staticCode);
if (requirement) CFRelease(requirement);
}
static NSData *DecodeReceiptData(NSData *receiptData)
{
CMSDecoderRef decoder = NULL;
SecPolicyRef policyRef = NULL;
SecTrustRef trustRef = NULL;
@try {
OSStatus status = CMSDecoderCreate(&decoder);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to decode receipt data: Create a decoder", nil];
}
status = CMSDecoderUpdateMessage(decoder, receiptData.bytes, receiptData.length);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to decode receipt data: Update message", nil];
}
status = CMSDecoderFinalizeMessage(decoder);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to decode receipt data: Finalize message", nil];
}
NSData *ret = nil;
CFDataRef dataRef = NULL;
status = CMSDecoderCopyContent(decoder, &dataRef);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to decode receipt data: Get decrypted content", nil];
}
ret = [NSData dataWithData:(__bridge NSData *)dataRef];
CFRelease(dataRef);
size_t numSigners;
status = CMSDecoderGetNumSigners(decoder, &numSigners);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to check receipt signature: Get singer count", nil];
}
if (numSigners == 0) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to check receipt signature: No signer found", nil];
}
policyRef = SecPolicyCreateBasicX509();
CMSSignerStatus signerStatus;
OSStatus certVerifyResult;
status = CMSDecoderCopySignerStatus(decoder, 0, policyRef, TRUE, &signerStatus, &trustRef, &certVerifyResult);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to check receipt signature: Get signer status", nil];
}
if (signerStatus != kCMSSignerValid) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to check receipt signature: No valid signer", nil];
}
return ret;
} @catch (NSException *e) {
@throw e;
} @finally {
if (policyRef) CFRelease(policyRef);
if (trustRef) CFRelease(trustRef);
if (decoder) CFRelease(decoder);
}
}
We sell magazines through a third party app platform called Pocketmags and our website. The magazines have Print, Digital and Combo options available for purchase. Also, this third party app provides the platform to read the digital version of the magazines.
Once a user buys a subscription on the website, he/she receives the email with the login information on PocketMags, where he registers his login details and can start reading his subscription. With us, the customer shares his billing/shipping address along with their email id while they make payment.
Now we have our own app where through which we want to sell these magazines and have to integrate In-App purchase for selling these digital subscriptions. How do we send the 3rd party subscription access details on email to the user if they do in app purchase?
I confirm that the configured callback address is accurate and can be called using Postman.
The server receives notification messages from IAP
Topic:
App & System Services
SubTopic:
StoreKit
Tags:
App Store Server Notifications
App Store Server API
Strange issue with currency display in subscription products
Hi everyone,
I'm facing a strange issue in my app where I use a subscription-based in-app purchase model.
The products I created in App Store Connect are all in "Approved" status.
I've tested with both RevenueCat and StoreKit, but the result is the same.
Here are the products being loaded:
Product loaded: weekly_product_id
Display name: Weekly Pro
Description: Weekly Pro Subscription
Price: ₺229,99
Product loaded: annual_product_id
Display name: Annual Pro
Description: Annual Pro Subscription
Price: ₺1.799,99
Even though I can see the correct prices and currency (Turkish Lira) in the Xcode debug console, on my real device the currency appears as Philippine Peso, as shown in the attached screenshot.
Interestingly, in the iOS simulator, it's displayed in USD.
I've double-checked and my device's region settings are set to Turkey.
Any ideas on what could be causing this? And more importantly, how can I fix it?
Thanks in advance!
Can you implement storekit2 for in app purchases for a flutter project?
Topic:
App & System Services
SubTopic:
StoreKit
A customer of mine signed up for a free trial. I got a apple server notification with notification type DID_RENEW. What does that mean? Does that mean that they will be charged the subscription price now?
We are using consumable in-app purchases. Starting from May 27th, we began receiving refund callbacks with the notificationType set to ONE_TIME_CHARGE immediately after users successfully completed a payment.
{
"notificationType": "ONE_TIME_CHARGE",
"signedPayload": "..."
}
During this period, we did not make any changes to our App release or server-side purchase handling logic.
Could this issue result in actual refunds being processed? What steps should we take to resolve this issue?
We also noticed in your changelog that a new notification type ONE_TIME_CHARGE has been introduced.
Can we safely ignore callbacks with the ONE_TIME_CHARGE notification type without affecting refund processing or user experience?
Topic:
App & System Services
SubTopic:
StoreKit
代码块
让购买结果=尝试等待购买(产品,选项:[选项])
//处理支付结果
开关购买结果{
案例让.success(验证结果):
如果案例让.verified(交易)=验证结果{
await transaction.finish()case .userCancelled:
自我.取消回调?()
案例.pending:
/// 交易可能在未来成功,通过Transaction.updates进行通知。
打印(“苹果支付中待定”)
默认:
打破
}
} 抓住 {
自我失败回电话?(”产品购买失败:\(错误)")
打印(“产品购买失败:\(错误)”)
}
凭证相关信息如下:
transactionid:1230000065994257
appAccountToken:D613C126-4142-4BFF-9960-00AE3F5A6F83
"jwsInfo": ["header": "eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWZUbGZkMGZOdkZXdnpDMVlJQU5zWGpBS0JnZ3Foa2pPUFFRREF6QjFNVVF3UWdZRFZRUURERHRCY0hCc1pTQlhiM0pzWkhkcFpHVWdSR1YyWld4dmNHVnlJRkpsYkdGMGFXOXVjeUJEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURUxNQWtHQTFVRUN3d0NSell4RXpBUkJnTlZCQW9NQ2tGd2NHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJek1Ea3hNakU1TlRFMU0xb1hEVEkxTVRBeE1URTVOVEUxTWxvd2daSXhRREErQmdOVkJBTU1OMUJ5YjJRZ1JVTkRJRTFoWXlCQmNIQWdVM1J2Y21VZ1lXNWtJR2xVZFc1bGN5QlRkRzl5WlNCU1pXTmxhWEIwSUZOcFoyNXBibWN4TERBcUJnTlZCQXNNSTBGd2NHeGxJRmR2Y214a2QybGtaU0JFWlhabGJHOXdaWElnVW1Wc1lYUnBiMjV6TVJNd0VRWURWUVFLREFwQmNIQnNaU0JKYm1NdU1Rc3dDUVlEVlFRR0V3SlZVekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCRUZFWWUvSnFUcXlRdi9kdFhrYXVESENTY1YxMjlGWVJWLzB4aUIyNG5DUWt6UWYzYXNISk9OUjVyMFJBMGFMdko0MzJoeTFTWk1vdXZ5ZnBtMjZqWFNqZ2dJSU1JSUNCREFNQmdOVkhSTUJBZjhFQWpBQU1COEdBMVVkSXdRWU1CYUFGRDh2bENOUjAxREptaWc5N2JCODVjK2xrR0taTUhBR0NDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnZiUzkzZDJSeVp6WXVaR1Z5TURFR0NDc0dBUVVGQnpBQmhpVm9kSFJ3T2k4dmIyTnpjQzVoY0hCc1pTNWpiMjB2YjJOemNEQXpMWGQzWkhKbk5qQXlNSUlCSGdZRFZSMGdCSUlCRlRDQ0FSRXdnZ0VOQmdvcWhraUc5Mk5rQlFZQk1JSCtNSUhEQmdnckJnRUZCUWNDQWpDQnRneUJzMUpsYkdsaGJtTmxJRzl1SUhSb2FYTWdZMlZ5ZEdsbWFXTmhkR1VnWW5rZ1lXNTVJSEJoY25SNUlHRnpjM1Z0WlhNZ1lXTmpaWEIwWVc1alpTQnZaaUIwYUdVZ2RHaGxiaUJoY0hCc2FXTmhZbXhsSUhOMFlXNWtZWEprSUhSbGNtMXpJR0Z1WkNCamIyNWthWFJwYjI1eklHOW1JSFZ6WlN3Z1kyVnlkR2xtYVdOaGRHVWdjRzlzYVdONUlHRnVaQ0JqWlhKMGFXWnBZMkYwYVc5dUlIQnlZV04wYVdObElITjBZWFJsYldWdWRITXVNRFlHQ0NzR0FRVUZCd0lCRmlwb2RIUndPaTh2ZDNkM0xtRndjR3hsTG1OdmJTOWpaWEowYVdacFkyRjBaV0YxZEdodmNtbDBlUzh3SFFZRFZSME9CQllFRkFNczhQanM2VmhXR1FsekUyWk9FK0dYNE9vL01BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3Tm9BREJsQWpFQTh5Uk5kc2twNTA2REZkUExnaExMSndBdjVKOGhCR0xhSThERXhkY1BYK2FCS2pqTzhlVW85S3BmcGNOWVVZNVlBakFQWG1NWEVaTCtRMDJhZHJtbXNoTnh6M05uS20rb3VRd1U3dkJUbjBMdmxNN3ZwczJZc2xWVGFtUllMNGFTczVrPSIsIk1JSURGakNDQXB5Z0F3SUJBZ0lVSXNHaFJ3cDBjMm52VTRZU3ljYWZQVGp6Yk5jd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NakV3TXpFM01qQXpOekV3V2hjTk16WXdNekU1TURBd01EQXdXakIxTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRFpYSjBhV1pwWTJGMGFXOXVJRUYxZEdodmNtbDBlVEVMTUFrR0ExVUVDd3dDUnpZeEV6QVJCZ05WQkFvTUNrRndjR3hsSUVsdVl5NHhDekFKQmdOVkJBWVRBbFZUTUhZd0VBWUhLb1pJemowQ0FRWUZLNEVFQUNJRFlnQUVic1FLQzk0UHJsV21aWG5YZ3R4emRWSkw4VDBTR1luZ0RSR3BuZ24zTjZQVDhKTUViN0ZEaTRiQm1QaENuWjMvc3E2UEYvY0djS1hXc0w1dk90ZVJoeUo0NXgzQVNQN2NPQithYW85MGZjcHhTdi9FWkZibmlBYk5nWkdoSWhwSW80SDZNSUgzTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFBd0h3WURWUjBqQkJnd0ZvQVV1N0Rlb1ZnemlKcWtpcG5ldnIzcnI5ckxKS3N3UmdZSUt3WUJCUVVIQVFFRU9qQTRNRFlHQ0NzR0FRVUZCekFCaGlwb2RIUndPaTh2YjJOemNDNWhjSEJzWlM1amIyMHZiMk56Y0RBekxXRndjR3hsY205dmRHTmhaek13TndZRFZSMGZCREF3TGpBc29DcWdLSVltYUhSMGNEb3ZMMk55YkM1aGNIQnNaUzVqYjIwdllYQndiR1Z5YjI5MFkyRm5NeTVqY213d0hRWURWUjBPQkJZRUZEOHZsQ05SMDFESm1pZzk3YkI4NWMrbGtHS1pNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVFCZ29xaGtpRzkyTmtCZ0lCQkFJRkFEQUtCZ2dxaGtqT1BRUURBd05vQURCbEFqQkFYaFNxNUl5S29nTUNQdHc0OTBCYUI2NzdDYUVHSlh1ZlFCL0VxWkdkNkNTamlDdE9udU1UYlhWWG14eGN4ZmtDTVFEVFNQeGFyWlh2TnJreFUzVGtVTUkzM3l6dkZWVlJUNHd4V0pDOTk0T3NkY1o0K1JHTnNZRHlSNWdtZHIwbkRHZz0iLCJNSUlDUXpDQ0FjbWdBd0lCQWdJSUxjWDhpTkxGUzVVd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NVFF3TkRNd01UZ3hPVEEyV2hjTk16a3dORE13TVRneE9UQTJXakJuTVJzd0dRWURWUVFEREJKQmNIQnNaU0JTYjI5MElFTkJJQzBnUnpNeEpqQWtCZ05WQkFzTUhVRndjR3hsSUVObGNuUnBabWxqWVhScGIyNGdRWFYwYUc5eWFYUjVNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVFzd0NRWURWUVFHRXdKVlV6QjJNQkFHQnlxR1NNNDlBZ0VHQlN1QkJBQWlBMklBQkpqcEx6MUFjcVR0a3lKeWdSTWMzUkNWOGNXalRuSGNGQmJaRHVXbUJTcDNaSHRmVGpqVHV4eEV0WC8xSDdZeVlsM0o2WVJiVHpCUEVWb0EvVmhZREtYMUR5eE5CMGNUZGRxWGw1ZHZNVnp0SzUxN0lEdll1VlRaWHBta09sRUtNYU5DTUVBd0hRWURWUjBPQkJZRUZMdXczcUZZTTRpYXBJcVozcjY5NjYvYXl5U3JNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEZ1lEVlIwUEFRSC9CQVFEQWdFR01Bb0dDQ3FHU000OUJBTURBMmdBTUdVQ01RQ0Q2Y0hFRmw0YVhUUVkyZTN2OUd3T0FFWkx1Tit5UmhIRkQvM21lb3locG12T3dnUFVuUFdUeG5TNGF0K3FJeFVDTUcxbWloREsxQTNVVDgyTlF6NjBpbU9sTTI3amJkb1h0MlFmeUZNbStZaGlkRGtMRjF2TFVhZ002QmdENTZLeUtBPT0iXX0", "payload": "eyJ0cmFuc2FjdGlvbklkIjoiMTIzMDAwMDA2NTk5NDI1NyIsIm9yaWdpbmFsVHJhbnNhY3Rpb25JZCI6IjEyMzAwMDAwNjU5OTQyNTciLCJidW5kbGVJZCI6ImNvbS5taWd1LmNsb3VkYXZwIiwicHJvZHVjdElkIjoibWlndS52aXNpb24uTW92aWUuOCIsInB1cmNoYXNlRGF0ZSI6MTc0NDgwNzYzMjAwMCwib3JpZ2luYWxQdXJjaGFzZURhdGUiOjE3NDQ4MDc2MzIwMDAsInF1YW50aXR5IjoxLCJ0eXBlIjoiTm9uLVJlbmV3aW5nIFN1YnNjcmlwdGlvbiIsImRldmljZVZlcmlmaWNhdGlvbiI6IjdIdUtPRUhRdVd2L0hKZjdLdlBzQnJOWUNoc2V3c3k3enpPZ2k1YjE3UW8wVnd2clhhQ3B5TTNmZTN3cFBqRUwiLCJkZXZpY2VWZXJpZmljYXRpb25Ob25jZSI6ImQ3YzgwOWI2LTFjNDMtNDIwOC1iZWVmLWVhZDUwYzY1ZGIwZCIsImFwcEFjY291bnRUb2tlbiI6ImQ2MTNjMTI2LTQxNDItNGJmZi05OTYwLTAwYWUzZjVhNmY4MyIsImluQXBwT3duZXJzaGlwVHlwZSI6IlBVUkNIQVNFRCIsInNpZ25lZERhdGUiOjE3NDU4MjM1MTU5OTUsImVudmlyb25tZW50IjoiUHJvZHVjdGlvbiIsInRyYW5zYWN0aW9uUmVhc29uIjoiUFVSQ0hBU0UiLCJzdG9yZWZyb250IjoiQ0hOIiwic3RvcmVmcm9udElkIjoiMTQzNDY1IiwicHJpY2UiOjgwMDAsImN1cnJlbmN5IjoiQ05ZIiwiYXBwVHJhbnNhY3Rpb25JZCI6IjcwNDQxMzM2NTEzNjUyNzAzMyJ9", "signature": "SXieZGabBt6xHoSaBsZ1k4AexqkNYzwZel0BEhGqc3mxrd4kzOR5wERRATXySqbqfT3WJzkDAsr9jmCdoz_7-g"], "status": "normal", "transactionId": "1230000065994257"]","Band_Phone_Num":"18653588566","Platform":"124","Oper_Time":"1745823519","verification_time":"1745823519115"},"ISP":"移动","OETM":"1745823519116","CLIENTID":"","CPURATE":"0.257","AMBERUDID":"1f72113ecc704ce4a4cc135e8af71ee6","ANAME":"","MEMRATE":"0.02346919","CITY":"北京","PROMOTION":"\\","CLIENTIP":"192.168.31.74","CLIENTIPV6":"fe80::4e3:40a8:51c3:dbf5","DB":"Apple","APN":"com.migu.cloudavp","ETM":"2025-04-28 14:58:39 116"}
请帮我查一下 是这个订单没关闭成功吗?为什么出现购买新的产品 返回的永远是这个支付凭证。
Topic:
App & System Services
SubTopic:
StoreKit
Hello, I’m trying to change the business model of my app to in-app subscriptions. My goal is to ensure that previous users who paid for the app have access to all premium content seamlessly, without even noticing any changes.
I’ve tried using RevenueCat for this, but I’m not entirely sure it’s working as expected. I would like to use RevenueCat to manage subscriptions, so I’m attempting a hybrid model. On the first launch of the updated app, the plan is to validate the app receipts, extract the originalAppVersion, and store it in a variable. If the original version is lower than the latest paid version, the isPremium variable is set to true, and this status propagates throughout the app. For users with versions equal to or higher than the latest paid version, RevenueCat will handle the subscription status—checking if a subscription is active and determining whether to display the paywall for premium features.
In a sandbox environment, it seems to work fine, but I’ve often encountered situations where the receipt doesn’t exist. I haven’t found a way to test this behavior properly in production. For example, I uploaded the app to TestFlight, but it doesn’t validate the actual transaction for a previously purchased version of the app. Correct me if I’m wrong, but it seems TestFlight doesn’t confirm whether I installed or purchased a paid version of the app.
I need to be 100% sure that users who previously paid for the app won’t face any issues with this migration. Is there any method to verify this behavior in a production-like scenario that I might not be aware of?
I’m sharing the code here to see if you can confirm that it will work as intended or suggest any necessary adjustments.
func fetchAppReceipt(completion: @escaping (Bool) -> Void) {
// Check if the receipt URL exists
guard let receiptURL = Bundle.main.appStoreReceiptURL else {
print("Receipt URL not found.")
requestReceiptRefresh(completion: completion)
return
}
// Check if the receipt file exists at the given path
if !FileManager.default.fileExists(atPath: receiptURL.path) {
print("The receipt does not exist at the specified location. Attempting to fetch a new receipt...")
requestReceiptRefresh(completion: completion)
return
}
do {
// Read the receipt data from the file
let receiptData = try Data(contentsOf: receiptURL)
let receiptString = receiptData.base64EncodedString()
print("Receipt found and encoded in base64: \(receiptString.prefix(50))...")
completion(true)
} catch {
// Handle errors while reading the receipt
print("Error reading the receipt: \(error.localizedDescription). Attempting to fetch a new receipt...")
requestReceiptRefresh(completion: completion)
}
}
func validateAppReceipt(completion: @escaping (Bool) -> Void) {
print("Starting receipt validation...")
guard let receiptURL = Bundle.main.appStoreReceiptURL else {
print("Receipt not found on the device.")
requestReceiptRefresh(completion: completion)
completion(false)
return
}
print("Receipt found at URL: \(receiptURL.absoluteString)")
do {
let receiptData = try Data(contentsOf: receiptURL, options: .alwaysMapped)
print(receiptData)
let receiptString = receiptData.base64EncodedString(options: [])
print("Receipt encoded in base64: \(receiptString.prefix(50))...")
let request = [
"receipt-data": receiptString,
"password": "c8bc9070bf174a8a8df108ef6b8d2ae3" // Shared Secret
]
print("Request prepared for Apple's validation server.")
guard let url = URL(string: "https://buy.itunes.apple.com/verifyReceipt") else {
print("Error: Invalid URL for Apple's validation server.")
completion(false)
return
}
print("Validation URL: \(url.absoluteString)")
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: request)
URLSession.shared.dataTask(with: urlRequest) { data, response, error in
if let error = error {
print("Error sending the request: \(error.localizedDescription)")
completion(false)
return
}
guard let data = data else {
print("No response received from Apple's server.")
completion(false)
return
}
print("Response received from Apple's server.")
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
print("Response JSON: \(json)")
// Verify original_application_version
if let receipt = json["receipt"] as? [String: Any],
let appVersion = receipt["original_application_version"] as? String {
print("Original application version found: \(appVersion)")
// Save the version in @AppStorage
savedOriginalVersion = appVersion
print("Original version saved in AppStorage: \(appVersion)")
if let appVersionNumber = Double(appVersion), appVersionNumber < 1.62 {
print("Original version is less than 1.62. User considered premium.")
isFirstLaunch = true
completion(true)
} else {
print("Original version is not less than 1.62. User is not premium.")
completion(false)
}
} else {
print("Could not find the original application version in the receipt.")
completion(false)
}
} else {
print("Error parsing the response JSON.")
completion(false)
}
} catch {
print("Error processing the JSON response: \(error.localizedDescription)")
completion(false)
}
}.resume()
} catch {
print("Error reading the receipt: \(error.localizedDescription)")
requestReceiptRefresh(completion: completion)
completion(false)
}
}
Some of these functions might seem redundant, but they are intended to double-check and ensure that the user is not a previous user. Is there any way to be certain that this will work when the app is downloaded from the App Store?
Thanks in advance!
Looking for assistance in managing subscription upgrades for TestFlight users.
I have a few monthly subscriptions, 30days, each with a different set of available features, 1 with all, and 1 with fewer. (All are in proper order and grouped in App Store connection subscriptions)
subscribing seems to be working fine, and purchasing an upgrade is going ok.
what is not: reflecting the upgraded plan in app (currently reflects it will start in 30days when current subscription expires)
I’m lead to believe this will be resolved with a live app in App Store, that will then handle prorating, terminate the old plan and immediately start the new one.
looking for help getting TestFlight to show immediate upgrades.
在沙盒环境下或者TestFlight 测试消费型项目会提示此项目将免费恢复