/**
* ***********************************************************************
* SMINRANA CONFIDENTIAL
* __________________
*
* Copyright 2020 SMINRANA
* All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of SMINRANA and its suppliers,
* if any. The intellectual and technical concepts contained
* herein are proprietary to SMINRANA
* and its suppliers and may be covered by U.S. and Foreign Patents,
* patents in process, and are protected by trade secret or copyright law.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from SMINRANA.
* www.sminrana.com
*
*/
import SwiftUI
import StoreKit
import Combine
class AppStorageManager: NSObject, ObservableObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
@AppStorage("username") var username: String = ""
@AppStorage("password") var password: String = ""
override init() {
super.init()
SKPaymentQueue.default().add(self)
}
@Published var products = [SKProduct]()
func getProdcut(indetifiers: [String]) {
print("Start requesting products ...")
let request = SKProductsRequest(productIdentifiers: Set(indetifiers))
request.delegate = self
request.start()
}
// SKProductsRequestDelegate
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("Did receive response \(response.products)")
if !response.products.isEmpty {
for fetchedProduct in response.products {
DispatchQueue.main.async {
self.products.append(fetchedProduct)
}
}
}
for invalidIdentifier in response.invalidProductIdentifiers {
print("Invalid identifiers found: \(invalidIdentifier)")
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Request did fail: \(error)")
}
// Transaction
@Published var transactionState: SKPaymentTransactionState?
func purchaseProduct(product: SKProduct) {
if SKPaymentQueue.canMakePayments() {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
} else {
print("User can't make payment.")
}
}
func restorePurchase() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
struct PaymentReceiptResponseModel: Codable {
var status: Int
var email: String?
var password: String?
var message: String?
}
// SKPaymentTransactionObserver
// This gets called when transaction purchased by user
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
self.transactionState = .purchasing
case .purchased:
print("===============Purchased================")
UserDefaults.standard.setValue(true, forKey: transaction.payment.productIdentifier)
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
do {
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
let receiptString = receiptData.base64EncodedString(options: [])
// Send receiptString to server for further verification
}
catch { print("Couldn't read receipt data with error: " + error.localizedDescription) }
}
case .restored:
UserDefaults.standard.setValue(true, forKey: transaction.payment.productIdentifier)
queue.finishTransaction(transaction)
print("==================RESTORED State=============")
self.transactionState = .restored
case .failed, .deferred:
print("Payment Queue Error: \(String(describing: transaction.error))")
queue.finishTransaction(transaction)
self.transactionState = .failed
default:
print(">>>> something else")
queue.finishTransaction(transaction)
}
}
}
// This gets called when a transaction restored by user
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
print("===============Restored================")
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
do {
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
let receiptString = receiptData.base64EncodedString(options: [])
// Send receiptString to server for further verification
}
catch { print("Couldn't read receipt data with error: " + error.localizedDescription) }
}
}
}
App Store Server Notifications V2 allows Apple to notify your backend in real time about subscription events such as renewals, cancellations, refunds, and billing issues. In this guide, you’ll learn how to decode, verify, and process App Store Server Notifications V2 using PHP and Laravel, with practical examples and best practices for production use.
App offering subscription based products must use App Store Server Notification to verify the purchase, renew the subscription, cancel the subscription, and more.
What are App Store Server Notifications?
App Store Server Notifications are webhooks sent by Apple to your server whenever a significant event happens to an in-app purchase or auto-renewable subscription.
Common events include:
Subscription renewal
Cancellation or expiration
Refunds and revocations
Billing retry failures
Price increases
With Server Notifications V2, Apple sends a signed payload (JWT) that contains detailed transaction and renewal information, allowing your backend to stay in sync without relying solely on client-side validation.
Why Use Version 2 (vs V1)?
Apple introduced Server Notifications V2 to replace V1 with a more secure, structured, and extensible format.
Key advantages of V2:
Uses JWT (JSON Web Tokens) for payload security
Provides richer transaction and renewal data
Supports App Store Server API integration
Better future compatibility with Apple’s subscription system
If you’re building or maintaining a modern subscription-based app, V2 is the recommended and future-proof choice.
How Apple Server Notifications Work The notification flow looks like this:
A subscription event occurs on the App Store
Apple sends a POST request to your webhook endpoint
The request contains a signedPayload
Your server:
Decodes the JWT
Verifies the signature
Extracts transaction and renewal data
Your backend updates the user subscription status accordingly
This process ensures your server remains the source of truth for subscription state.
My Intentions
My main goal is to retrieve the originalTransactionId from the notification data. Then, when a subscription expires or Is Refunded, it will downgrade the user’s subscription status.
If you prefer full control, you can implement Server Notifications V2 without any third-party libraries.
High-level steps:
Read the raw POST body from Apple
Extract the signedPayload
Split the JWT into header, payload, and signature
Base64-decode the payload
Parse and process the JSON data
This approach is useful for:
Learning how V2 works internally
Minimal dependencies
Custom verification logic
However, you must be careful with signature verification and edge cases.
Implementation with JWT Library
Using a JWT library simplifies decoding and validation while reducing security risks.
Typical steps:
Install a trusted JWT library
Decode the signedPayload
Validate the JWT signature using Apple’s public key
Extract:
notificationType
subtype
transactionInfo
renewalInfo
This method is recommended for most production systems because it is safer, cleaner, and easier to maintain.
App Store Configuration
User purchases we verify Apple’s receipt and we save originalTransactionId in our database. So each user has originalTransactionId and we can find the user with this originalTransactionId. You can read more about this here Auto-renewable subscriptions with SwiftUI
Make sure you set your app store notification URL on the AppStoreConnect. Choose Version 2 Notifications.
Each data will look like this, call it signedPayload.
The signedPayload object is a JWS representation. To get the transaction and subscription renewal details from the notification payload, process the signedPayload as follows:
Parse signedPayload to identify the JWS header, payload, and signature representations.
The data object contains a signedTransactionInfo (JWSTransaction) and depending on the notification type, a signedRenewalInfo (JWSRenewalInfo). Parse and Base64 URL-decode these signed JWS representations to get transaction and subscription renewal details.
Each of the signed JWS representations, signedPayload, signedTransactionInfo, and signedRenewalInfo, have a JWS signature that you can validate on your server. Use the algorithm specified in the header’s alg parameter to validate the signature. For more information about validating signatures, see the JSON Web Signature (JWS) IETF RFC 7515 specification.
Hopefully, you are already getting this data. Now let’s get originalTransactionId from this. We will do this without any 3rd party library first to understand the process.
First thing, I will download the Apple root certificate and make it.PEM file from it.
For testing purposes, I’m loading apple_root.pem and my signedPayload from a file called notification.json (replace it with file_get_contents(‘php://input’);) and then decoding the signedPayload. signedPayload has three parts, separated by .(dot), line 13.
The first part is the header, the Second part is the body (payload), and the Third part is the signature. The header has an algorithm and x5c, x5c has three elements. Certificate, intermediate certificate, and root certificate. We can verify the certificate in two steps. Once the verification is completed, we know we have signedPayload from Apple.
Finally, decode the payload again and get the originalTransactionId from lines 56 to 65.
Server Side
For this article, our production server URL looks like inafiz.com/jwt.php. You can get whatever Apple sends you and write a log in your server if you are interested.
Having a shopping cart icon on the right side of the AppBar on Flutter it’s not that hard, you can use basically a stack widget and inside the stack get IconButton and Positioned widgets.
Here is the code and screenshot
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(appBarTitle),
actions: [
Stack(
children: [
IconButton(
onPressed: () {},
icon: const Icon(
Icons.shopping_cart_rounded,
size: 30,
),
),
Positioned(
top: 4,
right: 6,
child: Container(
height: 22,
width: 22,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.purple,
),
child: const Center(
child: Text(
"2",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
)),
),
),
],
),
],
)