Skip to content

Instantly share code, notes, and snippets.

@nezhyborets
Last active January 16, 2025 00:50
Show Gist options
  • Save nezhyborets/70f128de8a5aa157bd80ccb37ad9d822 to your computer and use it in GitHub Desktop.
Save nezhyborets/70f128de8a5aa157bd80ccb37ad9d822 to your computer and use it in GitHub Desktop.
enum ReceiptValidationError: Error {
case urlNotInitialized(string: String)
case urlResponseIsNil(data: Data?, error: Error?)
case wrongResponseJsonFormat(jsonObjectDescription: String)
case unacceptableVerificationStatus(Int)
case verificationStatusSuccessButNoReceiptFound(responseDescription: String)
}
struct ReceiptValidationAPI: ReceiptValidationAPIProtocol {
private let appSpecificSharedSecret: String
private let sandboxURLPath = "https://sandbox.itunes.apple.com/verifyReceipt"
private let productionURLPath = "https://buy.itunes.apple.com/verifyReceipt"
init(appSpecificSharedSecret: String) {
self.appSpecificSharedSecret = appSpecificSharedSecret
}
func verifyReceiptWithAppStoreServer(
receiptData: String,
shouldCallProductionURL: Bool,
completionBlock: @escaping @Sendable (Result<Receipt, Error>, _ isSandbox: Bool) -> Void
) {
let receiptVerificationPath = (shouldCallProductionURL ? productionURLPath : sandboxURLPath)
guard let receiptVerificationURL = URL(string: receiptVerificationPath) else {
completionBlock(.failure(ReceiptValidationError.urlNotInitialized(string: receiptVerificationPath)), false)
return
}
let dictParameters = ["receipt-data": receiptData,
"password": appSpecificSharedSecret] as [String: Any]
var verificationRequest = URLRequest(url: receiptVerificationURL)
verificationRequest.httpMethod = "POST"
verificationRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
do {
let httpBody = try JSONSerialization.data(withJSONObject: dictParameters, options: [])
verificationRequest.httpBody = httpBody
let session = URLSession.shared
session.dataTask(with: verificationRequest) { (data, response, error) in
if let error {
completionBlock(.failure(error), !shouldCallProductionURL)
return
}
guard let data = data else {
completionBlock(
.failure(ReceiptValidationError.urlResponseIsNil(data: data, error: error)),
!shouldCallProductionURL
)
return
}
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
guard let dictResponse = json as? [String: Any] else {
throw ReceiptValidationError.wrongResponseJsonFormat(
jsonObjectDescription: String(describing: json)
)
}
if let theStatus = dictResponse["status"] as? Int, theStatus != 0 {
guard theStatus == 21007 else {
throw ReceiptValidationError.unacceptableVerificationStatus(theStatus)
}
self.verifyReceiptWithAppStoreServer(
receiptData: receiptData,
shouldCallProductionURL: false,
completionBlock: { (result, isSandbox) in
completionBlock(result, isSandbox)
}
)
} else if let dictReceiptInfo = dictResponse["receipt"] as? [String: Any] {
let receipt = Receipt(
dictData: dictReceiptInfo,
latestReceiptInfo: dictResponse["latest_receipt_info"] as? [[String: Any]]
)
completionBlock(.success(receipt), !shouldCallProductionURL)
} else {
throw ReceiptValidationError.verificationStatusSuccessButNoReceiptFound(
responseDescription: String(describing: dictResponse)
)
}
} catch {
completionBlock(.failure(error), !shouldCallProductionURL)
}
}.resume()
} catch {
completionBlock(.failure(error), !shouldCallProductionURL)
}
}
}
struct Receipt {
let applicationVersion: String
let bundleID: String
let arrIAPReceipts: [IAPReceipt]
let originalApplicationVersion: String
let originalPurchaseDate: Date?
let receiptCreationDate: Date?
let receiptType: String
init(dictData: [String: Any], latestReceiptInfo: [[String: Any]]?) {
if let originalApplicationVersion = dictData["original_application_version"] as? String {
self.originalApplicationVersion = originalApplicationVersion
} else {
self.originalApplicationVersion = ""
}
if let bundleID = dictData["bundle_id"] as? String {
self.bundleID = bundleID
} else {
bundleID = ""
}
if let applicationVersion = dictData["application_version"] as? String {
self.applicationVersion = applicationVersion
} else {
applicationVersion = ""
}
if let receiptType = dictData["receipt_type"] as? String {
self.receiptType = receiptType
} else {
receiptType = ""
}
if
let strOriginalPurchaseTime = dictData["original_purchase_date_ms"] as? String,
let originalPurchaseTime = Double(strOriginalPurchaseTime) {
let theDate = Date(timeIntervalSince1970: TimeInterval(originalPurchaseTime / 1000.0))
self.originalPurchaseDate = theDate
} else {
originalPurchaseDate = nil
}
if let strReceiptCreationTime = dictData["receipt_creation_date_ms"] as? String, let receiptCreationTime = Double(strReceiptCreationTime) {
let theDate = Date(timeIntervalSince1970: TimeInterval(receiptCreationTime / 1000.0))
self.receiptCreationDate = theDate
} else {
receiptCreationDate = nil
}
if let arrInAppReceipts = latestReceiptInfo ?? (dictData["in_app"] as? [[String: Any]]) {
var arrIAPReceipts: [IAPReceipt] = []
for i in 0..<arrInAppReceipts.count {
let dictIAPReceipt = arrInAppReceipts[i]
let theIAPReceipt = IAPReceipt(dictData: dictIAPReceipt)
arrIAPReceipts.append(theIAPReceipt)
}
self.arrIAPReceipts = arrIAPReceipts
} else {
arrIAPReceipts = []
}
}
}
struct IAPReceipt {
let isTrialPeriod: Bool
let originalPurchaseDate: Date?
let originalTransactionID: String
let productID: String
let purchaseDate: Date?
let quantity: Int
let transactionID: String
let expireDate: Date? // Only used if user has subscribed
init(dictData: [String: Any]) {
if let isTrialPeriod = dictData["is_trial_period"] as? Bool {
self.isTrialPeriod = isTrialPeriod
} else {
isTrialPeriod = false
}
if let productID = dictData["product_id"] as? String {
self.productID = productID
} else {
productID = ""
}
if let originalTransactionID = dictData["original_transaction_id"] as? String {
self.originalTransactionID = originalTransactionID
} else {
originalTransactionID = ""
}
if let transactionID = dictData["transaction_id"] as? String {
self.transactionID = transactionID
} else {
transactionID = ""
}
if let quantity = dictData["quantity"] as? Int {
self.quantity = quantity
} else {
quantity = 0
}
if let strOriginalPurchaseTime = dictData["original_purchase_date_ms"] as? String, let originalPurchaseTime = Double(strOriginalPurchaseTime) {
let theDate = Date(timeIntervalSince1970: TimeInterval(originalPurchaseTime / 1000.0))
self.originalPurchaseDate = theDate
} else {
originalPurchaseDate = nil
}
if let strPurchaseTime = dictData["purchase_date_ms"] as? String, let purchaseTime = Double(strPurchaseTime) {
let theDate = Date(timeIntervalSince1970: TimeInterval(purchaseTime / 1000.0))
self.purchaseDate = theDate
} else {
purchaseDate = nil
}
if let strExpireTime = dictData["expires_date_ms"] as? String, let expireTime = Double(strExpireTime) {
let theDate = Date(timeIntervalSince1970: TimeInterval(expireTime / 1000.0))
self.expireDate = theDate
} else {
expireDate = nil
}
}
func getExpireDate(isNonRenewableSubscription: Bool, isSandbox: Bool) -> Date? {
if isNonRenewableSubscription, let originalPurchaseDate {
if isSandbox {
return Calendar.current.date(byAdding: .minute, value: 5, to: originalPurchaseDate)
} else {
return Calendar.current.date(byAdding: .day, value: 31, to: originalPurchaseDate)
}
} else if let expireDate {
return expireDate
}
return nil
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment