#swift #in-app-purchase #storekit
#swift #покупка в приложении #storekit
Вопрос:
Мы сталкиваемся с серьезными проблемами с покупками в приложении в нашем приложении.
Мы предлагаем 3 IAP: автоматически возобновляемая подписка 3M, автоматически возобновляемая подписка 1Y, одноразовая покупка без использования (пожизненный доступ)
В нашем случае 70-80% транзакций завершаются неудачно, и мы в основном получаем код ошибки = 0 или код = 2 — Не удается подключиться к iTunes Store. Согласно документации SKError, это неизвестная ошибка (код 0) или отмененная транзакция (ошибка 2).
Иногда покупка не выполняется несколько раз для одного и того же пользователя, поэтому очень трудно поверить, что пользователь намеренно отменяет транзакцию для одного и того же продукта 3 или 4 раза подряд.
Это происходит независимо от версии iOS, модели устройства, версии нашего приложения. Ниже приведен наш код, используемый для получения продуктов и совершения транзакции.
Мы проверили несколько потоков с одной и той же проблемой, но не смогли найти никакого решения.
Мы не предлагаем никаких рекламных акций, идентификаторы продуктов действительны… Некоторые пользователи могут совершать покупки без каких-либо проблем.
Есть идеи?
import Foundation
import SwiftyStoreKit
import StoreKit
final class IAPService: NSObject {
static let shared = IAPService()
public var isSubscriptionAvailable = false
private var identifiers = ["product_x_id",
"product_y_id",
"product_z_id"]
var products: [SKProduct] = []
var purchaseProducts: [PurchaseProduct] = []
private var successBlock: ((String?,String?)->())? //product Id, receipt
private var errorBlock: ((String)->())?
private var productsRequest: SKProductsRequest?
private var productsRequestCompletionHandler: (()->())?
override init() {
super.init()
SKPaymentQueue.default().add(self)
self.loadProducts()
}
func loadProducts(completion: (()->())? = nil) {
productsRequest?.cancel()
productsRequestCompletionHandler = completion
productsRequest = SKProductsRequest(productIdentifiers: Set(identifiers))
productsRequest?.delegate = self
productsRequest?.start()
}
func purchaseProduct(identifier: String, onSuccess: ((String?,String?) -> ())?, onError: ((String?) -> ())?) {
guard products.count > 0 else {
loadProducts {
self.purchaseProduct(identifier: identifier, onSuccess: onSuccess, onError: onError)
}
return
}
guard let product = self.products.first(where: { (skProduct) -> Bool in
return skProduct.productIdentifier == identifier
}) else {
onError?("IAP error: cannot find product id")
return
}
clearHandlers()
self.successBlock = onSuccess
self.errorBlock = onError
print("Buying (product.productIdentifier)...")
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
public func restorePurchases(onSuccess: ((String?,String?) -> ())?, onError: ((String?) -> ())?) {
clearHandlers()
self.successBlock = onSuccess
self.errorBlock = onError
SKPaymentQueue.default().restoreCompletedTransactions()
}
public func fetchReceipt(productId: String) {
SwiftyStoreKit.fetchReceipt(forceRefresh: false) { (result) in
switch result {
case .success(let receiptData):
self.successBlock?(productId, receiptData.base64EncodedString(options: []))
self.clearHandlers()
break
case .error(let error):
print("Receipt verification failed: (error.localizedDescription)")
self.errorBlock?(error.localizedDescription)
self.clearHandlers()
break
}
}
}
private func clearHandlers() {
successBlock = nil
errorBlock = nil
productsRequestCompletionHandler = nil
productsRequest = nil
}
}
extension IAPService: SKProductsRequestDelegate {
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("Loaded list of products...")
let skProducts = response.products
skProducts.forEach { (skProduct) in
products.append(skProduct)
}
productsRequestCompletionHandler?()
clearHandlers()
}
public func request(_ request: SKRequest, didFailWithError error: Error) {
print("Failed to load list of products: (error.localizedDescription)")
productsRequestCompletionHandler?()
clearHandlers()
}
}
extension IAPService: 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...")
var failureReason: String = ""
if let skError = transaction.error as? SKError {
switch skError.code { // https://developer.apple.com/reference/storekit/skerror.code
case .unknown:
failureReason = "Unknown or unexpected error occurred"
break
case .paymentCancelled:
failureReason = "Payment cancelled by user"
break
case .clientInvalid:
failureReason = "Invalid Client"
break
case .paymentInvalid:
failureReason = "Invalid Payment"
break
case .paymentNotAllowed:
failureReason = "Payment not allowed"
break
case .cloudServiceNetworkConnectionFailed:
failureReason = "Cloud service network connection failed"
break
case .cloudServicePermissionDenied:
failureReason = "Cloud service permission denied"
break
case .storeProductNotAvailable:
failureReason = "Store product not available"
break
case .cloudServiceRevoked:
failureReason = "Cloud service revoked"
break
case .privacyAcknowledgementRequired:
failureReason = "Privacy Acknowledgement Required"
break
case .unauthorizedRequestData:
failureReason = "Unauthorized Request Data"
break
case .invalidOfferIdentifier:
failureReason = "Invalid offer identifier"
break
case .invalidSignature:
failureReason = "Invalid signature"
break
case .missingOfferParams:
failureReason = "Missing offer params"
break
case .invalidOfferPrice:
failureReason = "Invalid offer price"
break
}
failureReason = " code: (skError.code.rawValue)"
}
else if let isCancelledError = transaction.error?.isCancelledError, isCancelledError == true {
failureReason = "isCancelledError"
}
else {
failureReason = "(transaction.error.debugDescription)"
}
SKPaymentQueue.default().finishTransaction(transaction)
errorBlock?(failureReason)
self.clearHandlers()
}
private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else { return }
fetchReceipt(productId: identifier)
}
}
Комментарии:
1. Хм. Ну
code=2
, это отменено, как вы сказали, так что беспокоиться не о чем. Болееcode=0
неясно. Apple заявляет, что «Когда эта ошибка возникает в процессе производства, это может указывать на проблему с учетной записью iTunes пользователя».2. все ли эти записи об ошибках отправляются в базу данных, такую как Firebase? Любопытно, как известны 70-80% сбоев. Спасибо.
3. Не могли бы вы решить эту проблему @Magda? У нас точно такая же проблема, и нам нужна помощь. Некоторые из наших пользователей на производстве получают код ошибки = 0, и сумма, как вы описали, высока.
Ответ №1:
У меня была такая же проблема, когда я получаю либо код ошибки 0, либо код ошибки 2 (обычно код ошибки 2 срабатывает даже при успешной покупке), и в течение последних нескольких месяцев я переписывался по электронной почте с технической поддержкой разработчиков Apple.
Похоже, что это известная проблема, возникающая для определенных приложений Apple в настоящее время лучшим, что можно сделать в вашем случае, это обратиться в службу технической поддержки (TSI) для получения поддержки на уровне кода напрямую от Apple.
Кроме того, вы также можете использовать помощник обратной связи для создания отчета об ошибке.
К сожалению, это, похоже, вызвано чем-то на стороне Store Kit / App Store и требует, чтобы Apple изучала это в каждом конкретном случае, когда это происходит.