ApplePayment.swift 8.26 KB
//
//  ApplePayment.swift
//  hippo
//
//  Created by shihao on 2025/6/27.
//

import Foundation
import StoreKit

enum StoreKitError: Error, LocalizedError {
    case productNotFound
    case failedVerification
    case unknown

    var errorDescription: String? {
        switch self {
        case .productNotFound:
            return String(describing: AppleProductPaymentErrorMsg.productNotFound)
        case .failedVerification:
            return String(describing: AppleProductPaymentErrorMsg.failedVerification)
        case .unknown:
            return String(describing: AppleProductPaymentErrorMsg.unknown)
        }
    }
}

class ApplePayment {
    static let shared = ApplePayment()
    var delegate: AppDelegate?
    
    func purchase(_ productId: String, uuidString: String) async -> AppleProductPaymentResult {
        do {
            guard let uuid = UUID(uuidString: uuidString) else {
                return paymentResult(
                    productId: productId,
                    success: false,
                    errorMessage: String(describing: AppleProductPaymentErrorMsg.missingUUID)
                )
            }
            let appleProduct = try await requestProducts(productId)
            let result = try await appleProduct.purchase(options: [
                Product.PurchaseOption.appAccountToken(uuid)
            ])

            switch result {
            case .success(let verification):
                let transaction = try checkVerified(verification)
                
                let success = await verifyWithServer(appAccountToken: transaction.appAccountToken?.uuidString ?? "", originalTransactionId: String(transaction.originalID), transactionId: String(transaction.id), productId: transaction.productID)
                // 更新客户产品状态
                await updateCustomerProductStatus()
                // 完成交易
                if success{
                    await transaction.finish()
                    return paymentResult(
                        productId: productId,
                        appAccountToken: transaction.appAccountToken?.uuidString,
                        originalTransactionId: String(transaction.originalID),
                        transactionId: String(transaction.id),
                        success: true
                    )
                }else{
                    return paymentResult(
                        productId: productId,
                        appAccountToken: transaction.appAccountToken?.uuidString,
                        originalTransactionId: String(transaction.originalID),
                        transactionId: String(transaction.id),
                        success: false,
                        errorMessage: String(describing: AppleProductPaymentErrorMsg.unknown)
                    )
                }
            case .userCancelled:
                return paymentResult(
                    productId: productId,
                    success: false,
                    errorMessage: String(describing: AppleProductPaymentErrorMsg.userCancelled)
                )
            case .pending:
                return paymentResult(
                    productId: productId,
                    success: false,
                    errorMessage: String(describing: AppleProductPaymentErrorMsg.unknown)
                )
            @unknown default:
                return paymentResult(
                    productId: productId,
                    success: false,
                    errorMessage: String(describing: AppleProductPaymentErrorMsg.unknown)
                )
            }
        } catch {
            return paymentResult(
                productId: productId,
                success: false,
                errorMessage: paymentErrorMessage(error)
            )
        }

    }

    //获取苹果商品
    func requestProducts(_ productAppleId: String) async throws
        -> Product
    {
        let storeProducts = try await Product.products(
            for: Set([productAppleId])
        )
        if let product = storeProducts.first {
            return product
        } else {
            throw StoreKitError.productNotFound
        }
    }
    
    func isFreeTrail(product: Product) async -> Bool{
        let isActive = await hasActiveEntitlement(productID: product.id)
        let eligible = await product.subscription?.isEligibleForIntroOffer ?? false
        
        return !isActive && eligible
    }
    
    func hasActiveEntitlement(productID: String) async -> Bool {
        for await result in Transaction.currentEntitlements {
            guard case .verified(let transaction) = result else {
                continue
            }
            if transaction.productID == productID,
               transaction.revocationDate == nil {
                return true
            }
        }
        return false
    }

    private func updateCustomerProductStatus() async {
        var activeSubscriptions: [String] = []

        for await result in Transaction.currentEntitlements {
            do {
                let transaction = try checkVerified(result)

                switch transaction.productType {
                case .autoRenewable:
                    if let expirationDate = transaction.expirationDate,
                        expirationDate > Date()
                    {
                        activeSubscriptions.append(transaction.productID)
                    }
                default:
                    break
                }
            } catch {
                print("验证交易失败: \(error)")
            }
        }

    }

    // MARK: - 验证交易
    private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
        switch result {
        case .unverified:
            throw StoreKitError.failedVerification
        case .verified(let safe):
            return safe
        }
    }
    
    private func verifyWithServer(appAccountToken: String, originalTransactionId: String?, transactionId: String?, productId: String) async -> Bool{
        guard let delegate else{
            return false
        }
        var params: [String: String] = [
            "productId": productId,
            "appAccountToken": appAccountToken
        ]
        params["originalTransactionId"] = originalTransactionId
        params["transactionId"] = transactionId
        
        return await withCheckedContinuation { continuous in
            delegate.invoke(method: .verifyPayment, arguments: params) { result in
                print("invoke verifyPayment result: \(String(describing: result))")
                continuous.resume(returning: true)
            }
        }
    }

    private func paymentResult(
        productId: String,
        appAccountToken: String? = nil,
        originalTransactionId: String? = nil,
        transactionId: String? = nil,
        success: Bool?,
        errorMessage: String? = nil
    ) -> AppleProductPaymentResult {
        AppleProductPaymentResult(
            productId: productId,
            appAccountToken: appAccountToken,
            originalTransactionId: originalTransactionId,
            transactionId: transactionId,
            success: success,
            errorMessage: errorMessage
        )
    }

    private func paymentErrorMessage(_ error: Error) -> String {
        if let storeKitError = error as? StoreKitError {
            return storeKitError.localizedDescription
        }
        return error.localizedDescription
    }
}

extension ApplePayment {
    // MARK: - 监听交易更新
    func listenForTransactions() -> Task<Void, Error> {
        return Task.detached {
            for await result in Transaction.updates {
                do {
                    let transaction = try await self.checkVerified(result)
                    let _ = await self.verifyWithServer(appAccountToken: transaction.appAccountToken?.uuidString ?? "", originalTransactionId: String(transaction.originalID), transactionId: String(transaction.id), productId: transaction.productID)
                    // 更新客户产品状态
                    await self.updateCustomerProductStatus()
                    await transaction.finish()
                    print("交易更新处理完成: \(transaction.debugDescription)")
                } catch {
                    print("交易更新处理失败: \(error)")
                }
            }
        }
    }
}