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

import Foundation
import StoreKit


enum AppleProductPaymentErrorMsg: Int64 {
  case missingUUID = -1
  case productNotFound = -2
  case userCancelled = -3
  case failedVerification = -4
  case unknown = -5
}

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)
        }
    }
}


struct IAPRestoreTransactions: Codable {
    var product_id: String?
    var transaction_id: String?
    var original_transaction_id: String?
    var app_account_token: String?
    var web_order_line_item_id: String?
}

class ApplePayment {
    var delegate: AppDelegate?
    
    private var waitingVerifyTransitions: [Transaction] = []
    
    func purchase(_ productId: String, uuidString: String) async -> AppleProductPaymentResult {
        do {
            guard let uuid = UUID(uuidString: uuidString) else {
                return paymentResult(
                    productId: productId,
                    success: false,
                    errorCode: AppleProductPaymentErrorMsg.missingUUID.rawValue
                )
            }
            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,
                        errorCode: AppleProductPaymentErrorMsg.failedVerification.rawValue
                    )
                }
            case .userCancelled:
                return paymentResult(
                    productId: productId,
                    success: false,
                    errorCode: AppleProductPaymentErrorMsg.userCancelled.rawValue
                )
            case .pending:
                return paymentResult(
                    productId: productId
                )
            @unknown default:
                return paymentResult(
                    productId: productId,
                    success: false,
                    errorCode: AppleProductPaymentErrorMsg.unknown.rawValue
                )
            }
        } 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 AppShared.shared.isLogin else {
            print("Apple payment verify skipped: missing login info")
            return false
        }

        guard let baseURL = URL(string: AppShared.shared.baseUrl),
              let url = URL(string: "/client/doublefeel/payment/order_verify/apple/", relativeTo: baseURL)?.absoluteURL else {
            print("Apple payment verify failed: invalid baseUrl \(AppShared.shared.baseUrl)")
            return false
        }

        var params: [String: String] = [
            "product_id": productId,
            "app_account_token": appAccountToken
        ]
        params["original_transaction_id"] = originalTransactionId
        params["transaction_id"] = transactionId

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.timeoutInterval = 30
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.setValue(AppShared.shared.token, forHTTPHeaderField: "access_token")
        request.setValue(AppShared.shared.agent.finalUA, forHTTPHeaderField: "User-Agent")

        do {
            request.httpBody = try JSONSerialization.data(withJSONObject: params)
            let (data, response) = try await URLSession.shared.data(for: request)
            guard let httpResponse = response as? HTTPURLResponse else {
                print("Apple payment verify failed: invalid response")
                return false
            }

            let bodyText = String(data: data, encoding: .utf8) ?? ""
            print("Apple payment verify response: status=\(httpResponse.statusCode), body=\(bodyText)")

            guard (200..<300).contains(httpResponse.statusCode) else {
                return false
            }
            return true
        } catch {
            print("Apple payment verify failed: \(error.localizedDescription)")
            return false
        }
    }

    private func verifyRestoreWithServer(transaction: IAPRestoreTransactions) async -> Bool {
        guard AppShared.shared.isLogin else {
            print("Apple payment restore verify skipped: missing login info")
            return false
        }

        guard let baseURL = URL(string: AppShared.shared.baseUrl),
              let url = URL(string: "/client/doublefeel/payment/order_restore/apple/", relativeTo: baseURL)?.absoluteURL else {
            print("Apple payment restore verify failed: invalid baseUrl \(AppShared.shared.baseUrl)")
            return false
        }

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.timeoutInterval = 30
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.setValue(AppShared.shared.token, forHTTPHeaderField: "access_token")
        request.setValue(AppShared.shared.agent.finalUA, forHTTPHeaderField: "User-Agent")

        do {
            request.httpBody = try JSONEncoder().encode(transaction)
            let (data, response) = try await URLSession.shared.data(for: request)
            guard let httpResponse = response as? HTTPURLResponse else {
                print("Apple payment restore verify failed: invalid response")
                return false
            }

            let bodyText = String(data: data, encoding: .utf8) ?? ""
            print("Apple payment restore verify response: status=\(httpResponse.statusCode), body=\(bodyText)")

            guard (200..<300).contains(httpResponse.statusCode) else {
                return false
            }
            return true
        } catch {
            print("Apple payment restore verify failed: \(error.localizedDescription)")
            return false
        }
    }

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

    private func paymentErrorMessage(_ error: Error) -> String {
        if let storeKitError = error as? StoreKitError {
            return storeKitError.localizedDescription
        }
        return error.localizedDescription
    }
    
    func restore() async -> Bool {
        var transactions = [Transaction]()
        for await result in Transaction.currentEntitlements {
            if case let .verified(trans) = result {
                
                if trans.appAccountToken != nil &&
                    trans.revocationDate == nil &&
                    trans.productType == .autoRenewable {
                    transactions.append(trans)
                }else {
                    await trans.finish()
                }
            }
        }
        
       guard let restoreTransaction = transactions.compactMap({ IAPRestoreTransactions(
        product_id: $0.productID,
        transaction_id: "\($0.id)",
        original_transaction_id: "\($0.originalID)",
        app_account_token: $0.appAccountToken?.uuidString.lowercased() ?? "",
        web_order_line_item_id: $0.webOrderLineItemID ?? "") }
       ).last else{
           return false
       }

        guard await verifyRestoreWithServer(transaction: restoreTransaction) else {
            return false
        }
        
        for tran in transactions {
            await tran.finish()
        }
    
        return true
    }
}

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