Files
keyboard/keyBoard/Class/Pay/StoreKit2Manager/Converts/TransactionConverter.swift
2025-12-16 13:10:50 +08:00

476 lines
17 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// TransactionConverter.swift
// StoreKit2Manager
//
// Created by xiaopin on 2025/12/6.
//
import Foundation
import StoreKit
/// Transaction
/// Transaction Dictionary/JSON
public struct TransactionConverter {
/// Transaction Dictionary JSON
/// - Parameter transaction: Transaction
/// - Returns: Dictionary
public static func toDictionary(_ transaction: Transaction) -> [String: Any] {
var dict: [String: Any] = [:]
//
dict["id"] = String(transaction.id)
dict["productID"] = transaction.productID
//
dict["productType"] = productTypeToString(transaction.productType)
//
if let price = transaction.price {
dict["price"] = Double(String(format: "%.2f", NSDecimalNumber(decimal: price).doubleValue)) ?? NSDecimalNumber(decimal: price).doubleValue
} else {
dict["price"] = 0.00
}
// iOS 16.0+
if #available(iOS 16.0, *) {
if let currency = transaction.currency {
//
dict["currency"] = String(describing: currency)
} else {
dict["currency"] = NSNull()
}
} else {
dict["currency"] = NSNull()
}
//
dict["ownershipType"] = ownershipTypeToString(transaction.ownershipType)
// ID
dict["originalID"] = String(transaction.originalID)
//
dict["originalPurchaseDate"] = dateToTimestamp(transaction.originalPurchaseDate)
dict["purchaseDate"] = dateToTimestamp(transaction.purchaseDate)
//
dict["purchasedQuantity"] = transaction.purchasedQuantity
// iOS 17.0+
if #available(iOS 17.0, *) {
dict["purchaseReason"] = transactionReasonToString(transaction.reason)
} else {
dict["purchaseReason"] = ""
}
// ID
if let subscriptionGroupID = transaction.subscriptionGroupID {
dict["subscriptionGroupID"] = String(describing: subscriptionGroupID)
} else {
dict["subscriptionGroupID"] = NSNull()
}
//
if let expirationDate = transaction.expirationDate {
dict["expirationDate"] = dateToTimestamp(expirationDate)
} else {
dict["expirationDate"] = NSNull()
}
//
dict["isUpgraded"] = transaction.isUpgraded
//
if let revocationDate = transaction.revocationDate {
dict["hasRevocation"] = true
dict["revocationDate"] = dateToTimestamp(revocationDate)
} else {
dict["hasRevocation"] = false
dict["revocationDate"] = NSNull()
}
//
if let revocationReason = transaction.revocationReason {
dict["revocationReason"] = revocationReasonToString(revocationReason)
} else {
dict["revocationReason"] = NSNull()
}
// iOS 16.0+
if #available(iOS 16.0, *) {
dict["environment"] = environmentToString(transaction.environment)
} else {
dict["environment"] = "unknown"
}
//
if let appAccountToken = transaction.appAccountToken {
dict["appAccountToken"] = appAccountToken.uuidString
} else {
dict["appAccountToken"] = ""
}
// IDiOS 18.4+
if #available(iOS 18.4, macOS 15.4, tvOS 18.4, watchOS 11.4, visionOS 2.4, *) {
dict["appBundleID"] = String(describing: transaction.appBundleID)
dict["appTransactionID"] = transaction.appTransactionID
} else {
dict["appTransactionID"] = NSNull()
dict["appBundleID"] = NSNull()
}
//
dict["signedDate"] = dateToTimestamp(transaction.signedDate)
// iOS 17.0+
if #available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, visionOS 1.0, *) {
let storefront = transaction.storefront
dict["storefrontId"] = storefront.id
dict["storefrontCountryCode"] = storefront.countryCode
if let currency = storefront.currency {
dict["storefrontCurrency"] = String(describing: currency)
} else {
dict["storefrontCurrency"] = ""
}
} else {
dict["storefrontId"] = ""
dict["storefrontCountryCode"] = ""
dict["storefrontCurrency"] = ""
}
// WebID
if let webOrderLineItemID = transaction.webOrderLineItemID {
dict["webOrderLineItemID"] = webOrderLineItemID
} else {
dict["webOrderLineItemID"] = ""
}
//
dict["deviceVerification"] = transaction.deviceVerification.base64EncodedString()
// Nonce
dict["deviceVerificationNonce"] = transaction.deviceVerificationNonce.uuidString
//
dict["offer"] = offerToDictionary(from: transaction)
// iOS 18.4+
// Transaction.AdvancedCommerceInfo API
//
return dict
}
/// Transaction Dictionary
/// - Parameter transactions: Transaction
/// - Returns: Dictionary
public static func toDictionaryArray(_ transactions: [Transaction]) -> [[String: Any]] {
return transactions.map { toDictionary($0) }
}
/// Transaction JSON
/// - Parameter transaction: Transaction
/// - Returns: JSON
public static func toJSONString(_ transaction: Transaction) -> String? {
let dict = toDictionary(transaction)
return dictionaryToJSONString(dict)
}
/// Transaction JSON
/// - Parameter transactions: Transaction
/// - Returns: JSON
public static func toJSONString(_ transactions: [Transaction]) -> String? {
let array = toDictionaryArray(transactions)
return arrayToJSONString(array)
}
// MARK: -
///
private static func dateToTimestamp(_ date: Date) -> Int64 {
return Int64(date.timeIntervalSince1970 * 1000)
}
///
private static func productTypeToString(_ type: Product.ProductType) -> String {
switch type {
case .consumable:
return "consumable"
case .nonConsumable:
return "nonConsumable"
case .autoRenewable:
return "autoRenewable"
case .nonRenewable:
return "nonRenewable"
default:
return "unknown"
}
}
///
private static func ownershipTypeToString(_ type: Transaction.OwnershipType) -> String {
switch type {
case .purchased:
return "purchased"
case .familyShared:
return "familyShared"
default:
return "unknown"
}
}
///
@available(iOS 16.0, *)
private static func environmentToString(_ environment: AppStore.Environment) -> String {
switch environment {
case .production:
return "production"
case .sandbox:
return "sandbox"
case .xcode:
return "xcode"
default:
return "unknown"
}
}
///
@available(iOS 17.0, *)
private static func transactionReasonToString(_ reason: Transaction.Reason) -> String {
switch reason {
case .purchase:
return "purchase"
case .renewal:
return "renewal"
default:
return "unknown"
}
}
///
private static func revocationReasonToString(_ reason: Transaction.RevocationReason) -> String {
return extractEnumValueName(from: reason)
}
///
/// - Parameter value:
/// - Returns:
private static func extractEnumValueName<T>(from value: T) -> String {
let valueString = String(describing: value)
// "Transaction.OfferType.introductory" -> "introductory"
if let lastDot = valueString.lastIndex(of: ".") {
return String(valueString[valueString.index(after: lastDot)...])
}
return valueString
}
/// iOS 15.0-17.1
@available(iOS 15.0, *)
private static func transactionOfferTypeDeprecatedToString(_ type: Transaction.OfferType) -> String {
switch type {
case .introductory:
return "introductory"
case .promotional:
return "promotional"
case .code:
return "code"
default:
return "unknown"
}
}
/// Product.SubscriptionOffer.PaymentMode
private static func paymentModeToString(_ mode: Product.SubscriptionOffer.PaymentMode) -> String {
switch mode {
case .freeTrial:
return "freeTrial"
case .payAsYouGo:
return "payAsYouGo"
case .payUpFront:
return "payUpFront"
default:
return "unknown"
}
}
// MARK: - Offer
/// Transaction.OfferiOS 17.2+
@available(iOS 17.2, *)
private static func transactionOfferTypeToString(_ type: Transaction.OfferType) -> String {
// 使 if-else switch
if type == .introductory {
return "introductory"
} else if type == .promotional {
return "promotional"
} else if type == .code {
return "code"
} else {
// iOS 18.0+ winBack
if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
if type == .winBack {
return "winBack"
}
}
return "unknown"
}
}
/// Transaction.Offer.PaymentMode
@available(iOS 17.2, *)
private static func transactionOfferPaymentModeToString(_ mode: Transaction.Offer.PaymentMode) -> String {
switch mode {
case .freeTrial:
return "freeTrial"
case .payAsYouGo:
return "payAsYouGo"
case .payUpFront:
return "payUpFront"
default:
return "unknown"
}
}
/// Transaction Dictionary
/// - Parameter transaction: Transaction
/// - Returns: NSNull
private static func offerToDictionary(from transaction: Transaction) -> Any {
// iOS 17.2+ 使 offer
if #available(iOS 17.2, macOS 14.2, tvOS 17.2, watchOS 10.2, *) {
return modernOfferToDictionary(from: transaction)
}
// iOS 15.0 - iOS 17.1 使
else if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) {
return deprecatedOfferToDictionary(from: transaction)
}
// iOS 15.0
else {
return NSNull()
}
}
/// 使 offer iOS 17.2+
@available(iOS 17.2, macOS 14.2, tvOS 17.2, watchOS 10.2, *)
private static func modernOfferToDictionary(from transaction: Transaction) -> Any {
guard let offer = transaction.offer else {
return NSNull()
}
var offerDict: [String: Any] = [:]
//
offerDict["type"] = transactionOfferTypeToString(offer.type)
// ID
if let offerID = offer.id {
offerDict["id"] = offerID
} else {
offerDict["id"] = NSNull()
}
// 使
if let paymentMode = offer.paymentMode {
offerDict["paymentMode"] = transactionOfferPaymentModeToString(paymentMode)
} else {
offerDict["paymentMode"] = NSNull()
}
// iOS 18.4+
offerDict["periodCount"] = 0
offerDict["periodUnit"] = ""
if #available(iOS 18.4, macOS 15.4, tvOS 18.4, watchOS 11.4, visionOS 2.4, *) {
if let period = offer.period {
offerDict["periodCount"] = period.value
offerDict["periodUnit"] = subscriptionPeriodUnitToString(period.unit)
}
}
return offerDict
}
/// 使iOS 15.0 - iOS 17.1
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
private static func deprecatedOfferToDictionary(from transaction: Transaction) -> Any {
guard let offerType = transaction.offerType else {
return NSNull()
}
var offerDict: [String: Any] = [:]
//
offerDict["type"] = transactionOfferTypeDeprecatedToString(offerType)
// ID
if let offerID = transaction.offerID {
offerDict["id"] = String(describing: offerID)
} else {
offerDict["id"] = NSNull()
}
//
if let paymentMode = transaction.offerPaymentModeStringRepresentation {
offerDict["paymentMode"] = paymentMode
} else {
offerDict["paymentMode"] = NSNull()
}
// iOS 15.0 - iOS 18.3 使iOS 18.4+
if #available(iOS 18.4, macOS 15.4, tvOS 18.4, watchOS 11.4, visionOS 2.4, *) {
// iOS 18.4+ offerPeriodStringRepresentation NSNull
offerDict["period"] = NSNull()
} else {
// iOS 15.0 - iOS 18.3 使
if let period = transaction.offerPeriodStringRepresentation {
offerDict["period"] = period
} else {
offerDict["period"] = NSNull()
}
}
return offerDict
}
/// Dictionary
private static func subscriptionPeriodToDictionary(_ period: Product.SubscriptionPeriod) -> [String: Any] {
var dict: [String: Any] = [:]
dict["value"] = period.value
dict["unit"] = subscriptionPeriodUnitToString(period.unit)
return dict
}
///
private static func subscriptionPeriodUnitToString(_ unit: Product.SubscriptionPeriod.Unit) -> String {
switch unit {
case .day:
return "day"
case .week:
return "week"
case .month:
return "month"
case .year:
return "year"
default:
return "unknown"
}
}
// Transaction.AdvancedCommerceProduct
// 使 jsonRepresentation
/// Dictionary JSON
private static func dictionaryToJSONString(_ dict: [String: Any]) -> String? {
guard let jsonData = try? JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted),
let jsonString = String(data: jsonData, encoding: .utf8) else {
return nil
}
return jsonString
}
/// Array JSON
private static func arrayToJSONString(_ array: [[String: Any]]) -> String? {
guard let jsonData = try? JSONSerialization.data(withJSONObject: array, options: .prettyPrinted),
let jsonString = String(data: jsonData, encoding: .utf8) else {
return nil
}
return jsonString
}
}