Difficulties when work with Apple payment
In-App Purchase
I/ Overview
Offer extra content and features — including digital goods, subscriptions, and premium content — directly within your app through in‑app purchases on all Apple platforms. You can even promote and offer in-app purchases directly on the App Store.
You need to create an App ID. This will link together your app to your in-app purchaseable products. Login to the Apple Developer Center
, then select Certificates, IDs & Profiles
.
II/ Creating In-App Purchase Products
There are four types of in-app purchases and you can offer multiple types within your app.
1. Consumable
Provide different types of consumables, such as lives or gems used to further progress in a game, boosts in a dating app to increase profile visibility, or digital tips for creators within a social media app. Consumable in‑app purchases are depleted as they’re used and can be purchased again. They’re frequently offered in apps and games that use the freemium business model.
2. Non-consumable
Provide non-consumable, premium features that are purchased once and don’t expire. Examples include additional filters in a photo app, extra brushes in an illustration app, or cosmetic items in a game. Non-consumable in-app purchases can offer Family Sharing.
3. Auto‑renewable subscriptions
Provide ongoing access to content, services, or premium features in your app. People are charged on a recurring basis until they decide to cancel. Common use cases include access to media or libraries of content (such as video, music, or articles), software as a service (such as cloud storage, productivity, or graphics and design), education, and more. Auto-renewable subscriptions can offer Family Sharing.
4. Non-renewing subscriptions
Provide access to services or content for a limited duration, such as a season pass to in-game content. This type of subscription doesn’t renew automatically, so people need to purchase a new subscription once it concludes if they want to retain access.
In this article, I will talk about Auto‑renewable subscriptions
.
III/ Implement Auto‑renewable subscriptions
to iOS project.
1. Project Configuration
Select Project
under Targets
in Xcode. Select the General
tab, switch your Team to your correct team, and enter the bundle ID you used earlier.
Next select the Capabilities tab. Scroll down to In-App Purchase and toggle the switch to ON.
Note: If IAP does not show up in the list, make sure that, in the Accounts section of Xcode preferences, you are logged in with the Apple ID you used to create the app ID.
2. Implement code
Using StoreKit
import StoreKit
Request all product for buy
private let productIdentifiers: Set<ProductIdentifier>
private var purchasedProductIdentifiers: Set<ProductIdentifier> = []
private var productsRequest: SKProductsRequest?
private var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("Loaded list of products...")
let products = response.products
productsRequestCompletionHandler?(true, products)
clearRequestAndHandler()
for p in products {
print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)")
}
}
public func request(_ request: SKRequest, didFailWithError error: Error) {
print("Failed to load list of products.")
print("Error: \(error.localizedDescription)")
productsRequestCompletionHandler?(false, nil)
clearRequestAndHandler()
}
private func clearRequestAndHandler() {
productsRequest = nil
productsRequestCompletionHandler = nil
}
}
This extension is used to get a list of products, their titles, descriptions and prices from Apple’s servers by implementing the two methods required by the SKProductsRequestDelegate protocol.
Making Purchases
public func buyProduct(_ product: SKProduct) {
print("Buying \(product.productIdentifier)...")
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
Response delegate
// MARK: - SKPaymentTransactionObserver
extension IAPHelper: SKPaymentTransactionObserver {
public func paymentQueue(_ queue: SKPaymentQueue,
updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
complete(transaction: transaction)
break
case .failed:
fail(transaction: transaction)
break
case .restored:
restore(transaction: transaction)
break
case .deferred:
break
case .purchasing:
break
}
}
}
private func complete(transaction: SKPaymentTransaction) {
print("complete...")
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restore... \(productIdentifier)")
deliverPurchaseNotificationFor(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func fail(transaction: SKPaymentTransaction) {
print("fail...")
if let transactionError = transaction.error as NSError?,
let localizedDescription = transaction.error?.localizedDescription,
transactionError.code != SKError.paymentCancelled.rawValue {
print("Transaction Error: \(localizedDescription)")
}
SKPaymentQueue.default().finishTransaction(transaction)
}
private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else { return }
purchasedProductIdentifiers.insert(identifier)
UserDefaults.standard.set(true, forKey: identifier)
NotificationCenter.default.post(name: .IAPHelperPurchaseNotification, object: identifier)
}
}
Verify With server
func verifyReceipt(completion: ((Bool) -> Void)? = nil) {
guard !isVerifying else { return }
guard let _ = UserSessionManager.shared.getUserId(),
let receiptData = getLocalReceipt(),
let udid = UIDevice.current.identifierForVendor?.uuidString else { completion?(false); return }
self.isVerifying = true
let request = VerifyRecepitRequest(receipt_data: receiptData, udid: udid)
APIService.verifyReceipt(request)
.subscribe(onNext: {[weak self] in completion?($0.isSuccess) ; self?.isVerifying = false },
onError: {[weak self] err in
print("verifyReceipt \(err.localizedDescription)")
completion?(false)
self?.isVerifying = false
DispatchQueue.main.async {
let isToLogin = err.codeData == 403
self?.showAlert(message: isToLogin ? R.string.localizable.login_again_title() : err.messageData,
buttonTitle: isToLogin ? R.string.localizable.to_login_title() : "OK",
completion: {
if isToLogin {
UserSessionManager.shared.logoutHandler()
}
})
self?.removeAll()
}
}).disposed(by: self.bag)
}
Request Local Reciept
private func getLocalReceipt() -> String? {
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
do {
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
return receiptData.base64EncodedString()
} catch { print("Couldn't read receipt data with error: " + error.localizedDescription)
return nil
}
} else {
return nil
}
}
3. Making a Sandbox Purchase (Testing)
Build and run the app — but to test out purchases, you’ll have to run it on a device. The sandbox tester created earlier can be used to perform the purchase without getting charged.
Go to your iPhone and make sure you’re logged out of your normal App Store account. To do this, go to the Settings
app and tap iTunes & App Store
.
IV/ Common cases
1. Purchase again with other account
Subscription package will follow google account and productId so when buying again will not pay anymore -> return status as purchased (try to check again)
-> check with the server with Purchase.purchaseToken
, the server checks that already in the db matches the userid, it will notify the purchase failed
2. I’m making a purchase but I’m losing my life while I’m checking out store (the payment won’t be completed, so I’ll receive the error event onPurchasesUpdated
)
3. I’m buying, while verifying the server is down
In this case, the purchase status will be saved if the purchase on google is successful, when restarting the app next time will check that flag and query history and then verify with the server.