中断购买在接受 T&C 后不呼叫代表

Interrupted Purchase not calling delegate after accepting T&C

在实际设备上测试中断的购买,下面的第 10 项不会出现在同一会话中。它只会在

时出现
  1. 应用重启
  2. 应用程序进入后台,然后返回前台 (对于#2,我认为这是因为当应用程序返回前台时,TransactionObserver 再次被调用?)

这个 SO 也谈到了类似的事情: Apple In App Purchase, Interrupted purchase in sandbox

https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/testing_in-app_purchases_with_sandbox/testing_an_interrupted_purchase

数周以来,我一直在努力寻找解决这种情况的方法。浏览许多 SO 以及来自苹果论坛的帖子。

参考线程:

  1. https://developer.apple.com/forums/thread/674081
  2. https://developer.apple.com/forums/thread/671492
  3. https://developer.apple.com/forums/thread/685938
  4. Apple In App Purchase, Interrupted purchase in sandbox

根据 Apple Docs,中断的交易应该在用户同意 T&C/continued 中断的购买后发送失败然后发​​送购买(但实际上它似乎没有发生 - 请参阅link编辑以上线程)

这就是我目前的解决方案。它并不完全理想,但在这种情况下它是最好的(我发现)。请参阅 link #3,其中用户观察到调用 restoreCompletedTransactions() 实际上能够让事务观察器处理(已完成的)中断事务。这样做,用户不必关闭(到后台)然后再次打开应用程序。 (请注意,我尝试了各种再次调用事务观察器的方法,但都没有帮助)

这是我的解决方案。

    private func purchaseFailed(_ transaction: SKPaymentTransaction) {
        var failureReason: String = ""
        var message: String = ""
        var code = Int()
        
        print("\(FormatDisplay.datems(Date())) [IAP] Transcation FAILED/CANCELLED")
        
        // 
        // https://adapty.io/blog/ios-in-app-purchases-part-5-list-of-skerror-codes-and-how-to-handle-them
        if let skError = transaction.error as? SKError {
            switch skError.code {  // https://developer.apple.com/reference/storekit/skerror.code
            case .unknown:
                // https://developer.apple.com/forums/thread/674081
                if let underlyingError = skError.userInfo["NSUnderlyingError"] as? NSError {
                   if underlyingError.code == 3038 {
                     print(">> General conditions have changed, don't display an error for the interrupted transaction")
                    failureReason = "ERROR: Unknown Error. Transaction Interrupted"
                    message = "Transaction Interrupted. Your Purchase is being processed. Please Check Back in 5mins."
                    code = underlyingError.code
                    
                   } else {
                    failureReason = "Unknown or unexpected error occurred"
                    message = "Oops, something unknown occurred or the transaction was interrupted. If the interrupted purchase was successful, please check back in 5mins."
                    code = skError.code.rawValue
                   }
                }
                break
            case .clientInvalid:
                failureReason = "ERROR: Invalid Client"
                message = "The purchase cannot be completed. Please, change your account or device."
                code = skError.code.rawValue
                break
            case .paymentCancelled:
                failureReason = "ERROR: User Cancelled Payment"
                message = ""
                code = skError.code.rawValue
                break
            case .paymentInvalid:
                failureReason = "ERROR: Invalid Payment"
                message = "Your purchase was declined. Please, check the payment details and make sure there are enough funds in your account."
                code = skError.code.rawValue
                break
            case .paymentNotAllowed:
                failureReason = "ERROR: Payment not allowed"
                message = "The purchase is not available for the selected payment method. Please, make sure your payment method allows you to make online purchases."
                code = skError.code.rawValue
                break
            case .storeProductNotAvailable:
                failureReason = "ERROR: Store product not available"
                message = "This product is not available in your region. Please, change the store and try again"
                code = skError.code.rawValue
                break
            case .cloudServicePermissionDenied:
                failureReason = "ERROR: Cloud service permission denied"
                message = "Your purchase was declined"
                code = skError.code.rawValue
                break
                
            case .cloudServiceNetworkConnectionFailed:
                failureReason = "ERROR: Cloud service network connection failed"
                message = "he purchase cannot be completed because your device is not connected to the Internet. Please, try again later with a stable internet connection"
                code = skError.code.rawValue
                break
            case .cloudServiceRevoked:
                failureReason = "ERROR: Cloud service revoked"
                message = "Sorry, an error has occurred."
                code = skError.code.rawValue
                break
            case .privacyAcknowledgementRequired:
                failureReason = "ERROR: Privacy Acknowledgement Required"
                message = "The purchase cannot be completed because you have not accepted the terms of use of the AppStore. Please, confirm your consent in the settings and then return to the purchase."
                code = skError.code.rawValue
                break
            case .unauthorizedRequestData:
                failureReason = "ERROR: Unauthorized Request Data"
                message = "An error has occurred. Please, try again later."
                code = skError.code.rawValue
                break
            case .invalidOfferIdentifier:
                failureReason = "ERROR: Invalid offer identifier"
                message = "The promotional offer is invalid or expired."
                code = skError.code.rawValue
                break
            case .invalidSignature:
                failureReason = "ERROR: Invalid Signature"
                message = "Sorry, an error has occurred when applying the promo code. Please, try again later."
                code = skError.code.rawValue
                break
            case .missingOfferParams:
                failureReason = "ERROR: Missing offer params"
                message = "Sorry, an error has occurred when applying the promo code. Please, try again later."
                code = skError.code.rawValue
                break
            case .invalidOfferPrice:
                failureReason = "ERROR: Invalid offer price"
                message = "Sorry, your purchase cannot be completed. Please, try again later."
                code = skError.code.rawValue
                break
            case .overlayCancelled:
                failureReason = "ERROR: overlay Cancelled"
                message = ""
                code = skError.code.rawValue
                break
            case .overlayInvalidConfiguration:
                failureReason = "ERROR: Overlay Invalid Configuration"
                message = ""
                code = skError.code.rawValue
                break
            case .overlayTimeout:
                failureReason = "ERROR: Overlay Timeout"
                message = ""
                code = skError.code.rawValue
                break
            case .ineligibleForOffer:
                failureReason = "ERROR: Ineligible Offer"
                message = "Sorry, your purchase cannot be completed. Please, try again later."
                code = skError.code.rawValue
                break
            case .unsupportedPlatform:
                failureReason = "ERROR: Unsupported Platform"
                message = "Sorry, unsupported Platform"
                code = skError.code.rawValue
                break
            case .overlayPresentedInBackgroundScene:
                failureReason = "ERROR: Overlay Presented In Background Scene"
                message = ""
                code = skError.code.rawValue
                break
            @unknown default:
                failureReason = "ERROR: Unknown Default"
                message = "Oops. Something Has Happened. Please try again later."
                code = skError.code.rawValue
                break
            }
            let title = "Error"
            let errorMsg = failureReason
            if message != "" {
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                    DisplayAlert.presentIapFailed(title: title, message: message, errMsg: errorMsg)
                    
                }
            }
            
            failureReason += " ErrorCode: \(code)"
        }
        
        print("\(FormatDisplay.datems(Date())) [IAP] \(failureReason)")
        print("\(FormatDisplay.datems(Date())) [IAP] -- \(transaction.error?.localizedDescription ?? "")")
        print("\(FormatDisplay.datems(Date())) [IAP] -- \(transaction.error.debugDescription)")
        print("\(FormatDisplay.datems(Date())) [IAP] -- Calling SKPaymentQ.FinishTransaction")
        
        SKPaymentQueue.default().finishTransaction(transaction)
        purchaseCompletionHandler?(true)
    }

这是 purchaseIapFailed() 的内容,一旦用户按下 OK 按钮就会调用 restoreCompletedPurchases()

static func presentIapFailed(title: String, message: String, errMsg: String) {
    let root = UIApplication.shared.keyWindow?.rootViewController
    let alertController = UIAlertController(title: title,
                                            message: message,
                                            preferredStyle: .alert)
    
    alertController.addAction(UIAlertAction(title: "OK", style: .cancel, handler: {( action: UIAlertAction ) in
        print("    displayAlert - presentIapFailed: \(errMsg) - OK Pressed")
        
        // Call RestorePurchases to force replaying the transaction list.
        Medals.store.restorePurchases()
    }))
    
    root?.present(alertController, animated: true, completion: nil)
}

交易“恢复”后,会继续触发后续代码进行购买,发送通知(购买completion/success),然后弹出提示信息告知用户购买成功,消耗品已到账。