This commit is contained in:
2025-12-16 13:10:50 +08:00
parent 444877fb73
commit fd0ddfd45a
17 changed files with 4751 additions and 3 deletions

View File

@@ -1,6 +1,6 @@
# Uncomment the next line to define a global platform for your project
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '13.0'
platform :ios, '15.0'
target 'keyBoard' do
# Comment the next line if you don't want to use dynamic frameworks

View File

@@ -31,6 +31,21 @@
043FBCD22EAF97630036AFE1 /* KBPermissionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04C6EAE12EAF940F0089C901 /* KBPermissionViewController.m */; };
0450AA742EF013D000B6AF06 /* KBEmojiCollectionCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 0450AA732EF013D000B6AF06 /* KBEmojiCollectionCell.m */; };
0450AAE22EF03D5100B6AF06 /* KBPerson.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450AAE12EF03D5100B6AF06 /* KBPerson.swift */; };
0450AB682EF0443E00B6AF06 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 0450AB672EF0443E00B6AF06 /* OrderedCollections */; };
0450AC0A2EF11E4400B6AF06 /* StoreKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450AC082EF11E4400B6AF06 /* StoreKitManager.swift */; };
0450AC0B2EF11E4400B6AF06 /* StoreKitStateConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450ABF82EF11E4400B6AF06 /* StoreKitStateConverter.swift */; };
0450AC0C2EF11E4400B6AF06 /* SubscriptionLocale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450ABFF2EF11E4400B6AF06 /* SubscriptionLocale.swift */; };
0450AC0D2EF11E4400B6AF06 /* StoreKitConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450ABF72EF11E4400B6AF06 /* StoreKitConverter.swift */; };
0450AC0E2EF11E4400B6AF06 /* StoreKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450AC022EF11E4400B6AF06 /* StoreKitError.swift */; };
0450AC0F2EF11E4400B6AF06 /* StoreKitConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450AC012EF11E4400B6AF06 /* StoreKitConfig.swift */; };
0450AC102EF11E4400B6AF06 /* StoreKitDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450AC062EF11E4400B6AF06 /* StoreKitDelegate.swift */; };
0450AC112EF11E4400B6AF06 /* StoreKitService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450ABFC2EF11E4400B6AF06 /* StoreKitService.swift */; };
0450AC122EF11E4400B6AF06 /* TransactionConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450ABFA2EF11E4400B6AF06 /* TransactionConverter.swift */; };
0450AC132EF11E4400B6AF06 /* SubscriptionConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450ABF92EF11E4400B6AF06 /* SubscriptionConverter.swift */; };
0450AC142EF11E4400B6AF06 /* ProductConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450ABF62EF11E4400B6AF06 /* ProductConverter.swift */; };
0450AC152EF11E4400B6AF06 /* TransactionHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450AC042EF11E4400B6AF06 /* TransactionHistory.swift */; };
0450AC162EF11E4400B6AF06 /* StoreKitServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450ABFD2EF11E4400B6AF06 /* StoreKitServiceDelegate.swift */; };
0450AC172EF11E4400B6AF06 /* StoreKitState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450AC032EF11E4400B6AF06 /* StoreKitState.swift */; };
0459D1B42EBA284C00F2D189 /* KBSkinCenterVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */; };
0459D1B72EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; };
0459D1B82EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; };
@@ -268,6 +283,20 @@
0450AA732EF013D000B6AF06 /* KBEmojiCollectionCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBEmojiCollectionCell.m; sourceTree = "<group>"; };
0450AAE02EF03D5100B6AF06 /* keyBoard-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "keyBoard-Bridging-Header.h"; sourceTree = "<group>"; };
0450AAE12EF03D5100B6AF06 /* KBPerson.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KBPerson.swift; sourceTree = "<group>"; };
0450ABF62EF11E4400B6AF06 /* ProductConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductConverter.swift; sourceTree = "<group>"; };
0450ABF72EF11E4400B6AF06 /* StoreKitConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitConverter.swift; sourceTree = "<group>"; };
0450ABF82EF11E4400B6AF06 /* StoreKitStateConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitStateConverter.swift; sourceTree = "<group>"; };
0450ABF92EF11E4400B6AF06 /* SubscriptionConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionConverter.swift; sourceTree = "<group>"; };
0450ABFA2EF11E4400B6AF06 /* TransactionConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionConverter.swift; sourceTree = "<group>"; };
0450ABFC2EF11E4400B6AF06 /* StoreKitService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitService.swift; sourceTree = "<group>"; };
0450ABFD2EF11E4400B6AF06 /* StoreKitServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitServiceDelegate.swift; sourceTree = "<group>"; };
0450ABFF2EF11E4400B6AF06 /* SubscriptionLocale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionLocale.swift; sourceTree = "<group>"; };
0450AC012EF11E4400B6AF06 /* StoreKitConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitConfig.swift; sourceTree = "<group>"; };
0450AC022EF11E4400B6AF06 /* StoreKitError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitError.swift; sourceTree = "<group>"; };
0450AC032EF11E4400B6AF06 /* StoreKitState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitState.swift; sourceTree = "<group>"; };
0450AC042EF11E4400B6AF06 /* TransactionHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionHistory.swift; sourceTree = "<group>"; };
0450AC062EF11E4400B6AF06 /* StoreKitDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitDelegate.swift; sourceTree = "<group>"; };
0450AC082EF11E4400B6AF06 /* StoreKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitManager.swift; sourceTree = "<group>"; };
0459D1B22EBA284C00F2D189 /* KBSkinCenterVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinCenterVC.h; sourceTree = "<group>"; };
0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinCenterVC.m; sourceTree = "<group>"; };
0459D1B52EBA287900F2D189 /* KBSkinManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinManager.h; sourceTree = "<group>"; };
@@ -599,6 +628,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
0450AB682EF0443E00B6AF06 /* OrderedCollections in Frameworks */,
ECC9EE02174D86E8D792472F /* Pods_keyBoard.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -630,6 +660,7 @@
04122F612EC5F3DF00EF7AB3 /* Pay */ = {
isa = PBXGroup;
children = (
0450AC092EF11E4400B6AF06 /* StoreKit2Manager */,
04122F812EC5FC6F00EF7AB3 /* VC */,
04122F7D2EC5FC5500EF7AB3 /* V */,
04122F642EC5F40600EF7AB3 /* M */,
@@ -714,6 +745,67 @@
path = WMDragView;
sourceTree = "<group>";
};
0450ABFB2EF11E4400B6AF06 /* Converts */ = {
isa = PBXGroup;
children = (
0450ABF62EF11E4400B6AF06 /* ProductConverter.swift */,
0450ABF72EF11E4400B6AF06 /* StoreKitConverter.swift */,
0450ABF82EF11E4400B6AF06 /* StoreKitStateConverter.swift */,
0450ABF92EF11E4400B6AF06 /* SubscriptionConverter.swift */,
0450ABFA2EF11E4400B6AF06 /* TransactionConverter.swift */,
);
path = Converts;
sourceTree = "<group>";
};
0450ABFE2EF11E4400B6AF06 /* Internal */ = {
isa = PBXGroup;
children = (
0450ABFC2EF11E4400B6AF06 /* StoreKitService.swift */,
0450ABFD2EF11E4400B6AF06 /* StoreKitServiceDelegate.swift */,
);
path = Internal;
sourceTree = "<group>";
};
0450AC002EF11E4400B6AF06 /* Locals */ = {
isa = PBXGroup;
children = (
0450ABFF2EF11E4400B6AF06 /* SubscriptionLocale.swift */,
);
path = Locals;
sourceTree = "<group>";
};
0450AC052EF11E4400B6AF06 /* Models */ = {
isa = PBXGroup;
children = (
0450AC012EF11E4400B6AF06 /* StoreKitConfig.swift */,
0450AC022EF11E4400B6AF06 /* StoreKitError.swift */,
0450AC032EF11E4400B6AF06 /* StoreKitState.swift */,
0450AC042EF11E4400B6AF06 /* TransactionHistory.swift */,
);
path = Models;
sourceTree = "<group>";
};
0450AC072EF11E4400B6AF06 /* Protocols */ = {
isa = PBXGroup;
children = (
0450AC062EF11E4400B6AF06 /* StoreKitDelegate.swift */,
);
path = Protocols;
sourceTree = "<group>";
};
0450AC092EF11E4400B6AF06 /* StoreKit2Manager */ = {
isa = PBXGroup;
children = (
0450ABFB2EF11E4400B6AF06 /* Converts */,
0450ABFE2EF11E4400B6AF06 /* Internal */,
0450AC002EF11E4400B6AF06 /* Locals */,
0450AC052EF11E4400B6AF06 /* Models */,
0450AC072EF11E4400B6AF06 /* Protocols */,
0450AC082EF11E4400B6AF06 /* StoreKitManager.swift */,
);
path = StoreKit2Manager;
sourceTree = "<group>";
};
0477BD942EBAFF4E0055D639 /* Utils */ = {
isa = PBXGroup;
children = (
@@ -1622,6 +1714,9 @@
);
mainGroup = 727EC74A2EAF848B00B36487;
minimizedProjectReferenceProxies = 1;
packageReferences = (
0450AB662EF043C000B6AF06 /* XCRemoteSwiftPackageReference "swift-collections" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 727EC7542EAF848B00B36487 /* Products */;
projectDirPath = "";
@@ -1808,6 +1903,20 @@
04122F912EC73AF700EF7AB3 /* KBVipPay.m in Sources */,
0477BE002EBC6A330055D639 /* HomeRankVC.m in Sources */,
047C650D2EBC8A840035E841 /* KBPanModalView.m in Sources */,
0450AC0A2EF11E4400B6AF06 /* StoreKitManager.swift in Sources */,
0450AC0B2EF11E4400B6AF06 /* StoreKitStateConverter.swift in Sources */,
0450AC0C2EF11E4400B6AF06 /* SubscriptionLocale.swift in Sources */,
0450AC0D2EF11E4400B6AF06 /* StoreKitConverter.swift in Sources */,
0450AC0E2EF11E4400B6AF06 /* StoreKitError.swift in Sources */,
0450AC0F2EF11E4400B6AF06 /* StoreKitConfig.swift in Sources */,
0450AC102EF11E4400B6AF06 /* StoreKitDelegate.swift in Sources */,
0450AC112EF11E4400B6AF06 /* StoreKitService.swift in Sources */,
0450AC122EF11E4400B6AF06 /* TransactionConverter.swift in Sources */,
0450AC132EF11E4400B6AF06 /* SubscriptionConverter.swift in Sources */,
0450AC142EF11E4400B6AF06 /* ProductConverter.swift in Sources */,
0450AC152EF11E4400B6AF06 /* TransactionHistory.swift in Sources */,
0450AC162EF11E4400B6AF06 /* StoreKitServiceDelegate.swift in Sources */,
0450AC172EF11E4400B6AF06 /* StoreKitState.swift in Sources */,
043FBCD22EAF97630036AFE1 /* KBPermissionViewController.m in Sources */,
049FB20E2EC1CD2800FAB05D /* KBAlert.m in Sources */,
04A9FE162EB873C80020DB6D /* UIViewController+Extension.m in Sources */,
@@ -2074,7 +2183,7 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "keyBoard/Class/Pay/M/keyBoard-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 6.0;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
@@ -2121,7 +2230,7 @@
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "keyBoard/Class/Pay/M/keyBoard-Bridging-Header.h";
SWIFT_VERSION = 6.0;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
@@ -2275,6 +2384,25 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
0450AB662EF043C000B6AF06 /* XCRemoteSwiftPackageReference "swift-collections" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-collections.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.3.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
0450AB672EF0443E00B6AF06 /* OrderedCollections */ = {
isa = XCSwiftPackageProductDependency;
package = 0450AB662EF043C000B6AF06 /* XCRemoteSwiftPackageReference "swift-collections" */;
productName = OrderedCollections;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 727EC74B2EAF848B00B36487 /* Project object */;
}

View File

@@ -0,0 +1,15 @@
{
"originHash" : "51f90653b2c9f9f7064c0d52159b40bf7d222e5f314be23e62fe28520fec03db",
"pins" : [
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e",
"version" : "1.3.0"
}
}
],
"version" : 3
}

View File

@@ -0,0 +1,116 @@
//
// ProductConverter.swift
// StoreKit2Manager
//
// Created by xiaopin on 2025/12/6.
//
import Foundation
import StoreKit
/// Product
/// Product Dictionary/JSON
public struct ProductConverter {
/// Product Dictionary JSON
/// - Parameter product: Product
/// - Returns: Dictionary
public static func toDictionary(_ product: Product) -> [String: Any] {
var dict: [String: Any] = [:]
//
dict["id"] = product.id
dict["displayName"] = product.displayName
dict["description"] = product.description
//
let priceDecimal = product.price
let priceDouble = NSDecimalNumber(decimal: priceDecimal).doubleValue
dict["price"] = Double(String(format: "%.2f", priceDouble)) ?? priceDouble
dict["displayPrice"] = product.displayPrice
//
dict["type"] = productTypeToString(product.type)
//
dict["isFamilyShareable"] = product.isFamilyShareable
// JSON
// jsonRepresentation Data Base64 便 JSON
let jsonData = product.jsonRepresentation
if let jsonString = String(data: jsonData, encoding: .utf8) {
dict["jsonRepresentation"] = jsonString
} else {
dict["jsonRepresentation"] = ""
}
//
if let subscription = product.subscription {
dict["subscription"] = SubscriptionConverter.subscriptionInfoToDictionary(subscription, product: product)
} else {
dict["subscription"] = NSNull()
}
return dict
}
/// Product Dictionary
/// - Parameter products: Product
/// - Returns: Dictionary
public static func toDictionaryArray(_ products: [Product]) -> [[String: Any]] {
return products.map { toDictionary($0) }
}
/// Product JSON
/// - Parameter product: Product
/// - Returns: JSON
public static func toJSONString(_ product: Product) -> String? {
let dict = toDictionary(product)
return dictionaryToJSONString(dict)
}
/// Product JSON
/// - Parameter products: Product
/// - Returns: JSON
public static func toJSONString(_ products: [Product]) -> String? {
let array = toDictionaryArray(products)
return arrayToJSONString(array)
}
// MARK: -
///
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"
}
}
/// Dictionary JSON
internal 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
internal 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
}
}

View File

@@ -0,0 +1,98 @@
//
// StoreKitConverter.swift
// StoreKit2Manager
//
// Created by xiaopin on 2025/12/6.
//
import Foundation
import StoreKit
/// StoreKit
/// 便
public struct StoreKitConverter {
// MARK: - Product
/// Product Dictionary
public static func productToDictionary(_ product: Product) -> [String: Any] {
return ProductConverter.toDictionary(product)
}
/// Product Dictionary
public static func productsToDictionaryArray(_ products: [Product]) -> [[String: Any]] {
return ProductConverter.toDictionaryArray(products)
}
/// Product JSON
public static func productToJSONString(_ product: Product) -> String? {
return ProductConverter.toJSONString(product)
}
/// Product JSON
public static func productsToJSONString(_ products: [Product]) -> String? {
return ProductConverter.toJSONString(products)
}
// MARK: - Transaction
/// Transaction Dictionary
public static func transactionToDictionary(_ transaction: Transaction) -> [String: Any] {
return TransactionConverter.toDictionary(transaction)
}
/// Transaction Dictionary
public static func transactionsToDictionaryArray(_ transactions: [Transaction]) -> [[String: Any]] {
return TransactionConverter.toDictionaryArray(transactions)
}
/// Transaction JSON
public static func transactionToJSONString(_ transaction: Transaction) -> String? {
return TransactionConverter.toJSONString(transaction)
}
/// Transaction JSON
public static func transactionsToJSONString(_ transactions: [Transaction]) -> String? {
return TransactionConverter.toJSONString(transactions)
}
// MARK: - StoreKitState
/// StoreKitState Dictionary
public static func stateToDictionary(_ state: StoreKitState) -> [String: Any] {
return StoreKitStateConverter.toDictionary(state)
}
/// StoreKitState JSON
public static func stateToJSONString(_ state: StoreKitState) -> String? {
return StoreKitStateConverter.toJSONString(state)
}
// MARK: - RenewalInfo
/// RenewalInfo Dictionary
public static func renewalInfoToDictionary(_ renewalInfo: Product.SubscriptionInfo.RenewalInfo) -> [String: Any] {
return SubscriptionConverter.renewalInfoToDictionary(renewalInfo)
}
/// RenewalInfo JSON
public static func renewalInfoToJSONString(_ renewalInfo: Product.SubscriptionInfo.RenewalInfo) -> String? {
return SubscriptionConverter.renewalInfoToJSONString(renewalInfo)
}
// MARK: - RenewalState
/// RenewalState
public static func renewalStateToString(_ state: Product.SubscriptionInfo.RenewalState) -> String {
return SubscriptionConverter.renewalStateToString(state)
}
// MARK: - SubscriptionInfo
/// SubscriptionInfo Dictionary
public static func subscriptionInfoToDictionary(_ subscription: Product.SubscriptionInfo, product: Product? = nil) -> [String: Any] {
return SubscriptionConverter.subscriptionInfoToDictionary(subscription, product: product)
}
}

View File

@@ -0,0 +1,131 @@
//
// StoreKitStateConverter.swift
// StoreKit2Manager
//
// Created by xiaopin on 2025/12/6.
//
import Foundation
import StoreKit
/// StoreKitState
/// StoreKitState Dictionary/JSON
public struct StoreKitStateConverter {
/// StoreKitState Dictionary JSON
/// - Parameter state: StoreKitState
/// - Returns: Dictionary
public static func toDictionary(_ state: StoreKitState) -> [String: Any] {
var dict: [String: Any] = [:]
switch state {
case .idle:
dict["type"] = "idle"
case .loadingProducts:
dict["type"] = "loadingProducts"
// case .productsLoaded(let products):
// dict["type"] = "productsLoaded"
// dict["products"] = ProductConverter.toDictionaryArray(products)
case .loadingPurchases:
dict["type"] = "loadingPurchases"
case .purchasesLoaded:
dict["type"] = "purchasesLoaded"
case .purchasing(let productId):
dict["type"] = "purchasing"
dict["productId"] = productId
case .purchaseSuccess(let productId):
dict["type"] = "purchaseSuccess"
dict["productId"] = productId
case .purchasePending(let productId):
dict["type"] = "purchasePending"
dict["productId"] = productId
case .purchaseCancelled(let productId):
dict["type"] = "purchaseCancelled"
dict["productId"] = productId
case .purchaseFailed(let productId, let error):
dict["type"] = "purchaseFailed"
dict["productId"] = productId
dict["error"] = String(describing: error)
// case .subscriptionStatusChanged(let renewalState):
// dict["type"] = "subscriptionStatusChanged"
// dict["renewalState"] = renewalStateToString(renewalState)
case .restoringPurchases:
dict["type"] = "restoringPurchases"
case .restorePurchasesSuccess:
dict["type"] = "restorePurchasesSuccess"
case .restorePurchasesFailed(let error):
dict["type"] = "restorePurchasesFailed"
dict["error"] = String(describing: error)
case .purchaseRefunded(let productId):
dict["type"] = "purchaseRefunded"
dict["productId"] = productId
case .purchaseRevoked(let productId):
dict["type"] = "purchaseRevoked"
dict["productId"] = productId
case .subscriptionCancelled(let productId, let isFreeTrialCancelled):
dict["type"] = "subscriptionCancelled"
dict["productId"] = productId
dict["isFreeTrialCancelled"] = isFreeTrialCancelled
case .error(let error):
dict["type"] = "error"
dict["error"] = String(describing: error)
}
return dict
}
/// StoreKitState JSON
/// - Parameter state: StoreKitState
/// - Returns: JSON
public static func toJSONString(_ state: StoreKitState) -> String? {
let dict = toDictionary(state)
return dictionaryToJSONString(dict)
}
// MARK: -
///
private static func renewalStateToString(_ state: Product.SubscriptionInfo.RenewalState) -> String {
switch state {
case .subscribed:
return "subscribed"
case .expired:
return "expired"
case .inBillingRetryPeriod:
return "inBillingRetryPeriod"
case .inGracePeriod:
return "inGracePeriod"
case .revoked:
return "revoked"
default:
return "unknown"
}
}
/// 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
}
}

View File

@@ -0,0 +1,237 @@
//
// SubscriptionConverter.swift
// StoreKit2Manager
//
// Created by xiaopin on 2025/12/6.
//
import Foundation
import StoreKit
///
/// Dictionary/JSON
public struct SubscriptionConverter {
// MARK: - SubscriptionInfo
/// SubscriptionInfo Dictionary
/// - Parameters:
/// - subscription: SubscriptionInfo
/// - product: Product
/// - Returns: Dictionary
public static func subscriptionInfoToDictionary(_ subscription: Product.SubscriptionInfo, product: Product? = nil) -> [String: Any] {
var dict: [String: Any] = [:]
// ID
dict["subscriptionGroupID"] = subscription.subscriptionGroupID
//
dict["subscriptionPeriodCount"] = subscription.subscriptionPeriod.value
dict["subscriptionPeriodUnit"] = subscriptionPeriodUnitToString(subscription.subscriptionPeriod.unit)
//
if let introOffer = subscription.introductoryOffer {
dict["introductoryOffer"] = subscriptionOfferToDictionary(introOffer)
} else {
dict["introductoryOffer"] = NSNull()
}
//
dict["promotionalOffers"] = subscription.promotionalOffers.map { subscriptionOfferToDictionary($0) }
// iOS 18.0+
if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
dict["winBackOffers"] = subscription.winBackOffers.map { subscriptionOfferToDictionary($0) }
} else {
dict["winBackOffers"] = []
}
return dict
}
// MARK: - RenewalInfo
/// RenewalInfo Dictionary
/// - Parameter renewalInfo: RenewalInfo
/// - Returns: Dictionary
public static func renewalInfoToDictionary(_ renewalInfo: Product.SubscriptionInfo.RenewalInfo) -> [String: Any] {
var dict: [String: Any] = [:]
//
dict["willAutoRenew"] = renewalInfo.willAutoRenew
//
if let renewalDate = renewalInfo.renewalDate {
dict["renewalDate"] = dateToTimestamp(renewalDate)
} else {
dict["renewalDate"] = NSNull()
}
//
if let expirationReason = renewalInfo.expirationReason {
dict["expirationReason"] = expirationReasonToString(expirationReason)
} else {
dict["expirationReason"] = NSNull()
}
// expirationDate RenewalInfo Transaction
return dict
}
/// RenewalInfo JSON
/// - Parameter renewalInfo: RenewalInfo
/// - Returns: JSON
public static func renewalInfoToJSONString(_ renewalInfo: Product.SubscriptionInfo.RenewalInfo) -> String? {
let dict = renewalInfoToDictionary(renewalInfo)
return dictionaryToJSONString(dict)
}
// MARK: - RenewalState
/// RenewalState
/// - Parameter state: RenewalState
/// - Returns:
public static func renewalStateToString(_ state: Product.SubscriptionInfo.RenewalState) -> String {
switch state {
case .subscribed:
return "subscribed"
case .expired:
return "expired"
case .inBillingRetryPeriod:
return "inBillingRetryPeriod"
case .inGracePeriod:
return "inGracePeriod"
case .revoked:
return "revoked"
default:
return "unknown"
}
}
// MARK: - SubscriptionPeriod
/// SubscriptionPeriod Dictionary
/// - Parameter period: SubscriptionPeriod
/// - Returns: Dictionary
public static func subscriptionPeriodToDictionary(_ period: Product.SubscriptionPeriod) -> [String: Any] {
var dict: [String: Any] = [:]
dict["value"] = period.value
dict["unit"] = subscriptionPeriodUnitToString(period.unit)
return dict
}
// MARK: - SubscriptionOffer
/// SubscriptionOffer Dictionary
/// - Parameter offer: SubscriptionOffer
/// - Returns: Dictionary
private static func subscriptionOfferToDictionary(_ offer: Product.SubscriptionOffer) -> [String: Any] {
var dict: [String: Any] = [:]
// ID nil
if let offerID = offer.id {
dict["id"] = offerID
} else {
dict["id"] = NSNull()
}
//
dict["type"] = subscriptionOfferTypeToString(offer.type)
//
dict["displayPrice"] = String(describing: offer.displayPrice)
dict["price"] = Double(String(format: "%.2f", NSDecimalNumber(decimal: offer.price).doubleValue)) ?? NSDecimalNumber(decimal: offer.price).doubleValue
//
dict["paymentMode"] = paymentModeToString(offer.paymentMode)
//
dict["periodCount"] = offer.period.value
dict["periodUnit"] = subscriptionPeriodUnitToString(offer.period.unit)
//
dict["offerPeriodCount"] = offer.periodCount
return dict
}
// MARK: -
///
private static func dateToTimestamp(_ date: Date) -> Int64 {
return Int64(date.timeIntervalSince1970 * 1000)
}
///
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"
@unknown default:
return "unknown"
}
}
///
private static func subscriptionOfferTypeToString(_ type: Product.SubscriptionOffer.OfferType) -> String {
switch type {
case .introductory:
return "introductory"
case .promotional:
return "promotional"
default:
if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
if type == .winBack {
return "winBack"
}
}
return "unknown"
}
}
///
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"
}
}
///
private static func expirationReasonToString(_ reason: Product.SubscriptionInfo.RenewalInfo.ExpirationReason) -> String {
// ExpirationReason iOS
// 使 String(describing:)
let reasonString = String(describing: reason)
//
if let lastDot = reasonString.lastIndex(of: ".") {
let value = String(reasonString[reasonString.index(after: lastDot)...])
return value
}
return reasonString
}
/// 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
}
}

View File

@@ -0,0 +1,475 @@
//
// 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
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
//
// StoreKitServiceDelegate.swift
// StoreKit2Manager
//
// Created by xiaopin on 2025/12/6.
//
import Foundation
import StoreKit
/// StoreKitService
/// 线
@MainActor
internal protocol StoreKitServiceDelegate: AnyObject {
///
func service(_ service: StoreKitService, didUpdateState state: StoreKitState)
///
func service(_ service: StoreKitService, didLoadProducts products: [Product])
///
func service(_ service: StoreKitService, didUpdatePurchasedTransactions efficient: [Transaction], latests: [Transaction])
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
//
// StoreKitConfig.swift
// StoreKit2Manager
//
// Created by xiaopin on 2025/12/6.
//
import Foundation
/// StoreKit
public struct StoreKitConfig {
/// ID
public let productIds: [String]
/// ID
public let lifetimeIds: [String]
///
/// nil
public let nonRenewableExpirationDays: Int?
///
public let autoSortProducts: Bool
///
public let showLog: Bool
///
/// - Parameters:
/// - productIds: ID
/// - lifetimeIds: ID
/// - nonRenewableExpirationDays: 365
/// - autoSortProducts: true
/// - showLog: false
public init(
productIds: [String],
lifetimeIds: [String],
nonRenewableExpirationDays: Int? = 365,
autoSortProducts: Bool = true,
showLog: Bool = false
) {
self.productIds = productIds
self.lifetimeIds = lifetimeIds
self.nonRenewableExpirationDays = nonRenewableExpirationDays
self.autoSortProducts = autoSortProducts
self.showLog = showLog
}
}

View File

@@ -0,0 +1,85 @@
//
// StoreKitError.swift
// StoreKit2Manager
//
// Created by xiaopin on 2025/12/6.
//
import Foundation
/// StoreKit
public enum StoreKitError: Error, LocalizedError {
///
case productNotFound(String)
///
case purchaseFailed(Error)
///
case verificationFailed
///
case configurationMissing
///
case serviceNotStarted
///
case purchaseInProgress
///
case cancelSubscriptionFailed(Error)
///
case restorePurchasesFailed(Error)
///
case unknownError
public var errorDescription: String? {
switch self {
case .productNotFound(let id):
return "产品未找到: \(id)"
case .purchaseFailed(let error):
return "购买失败: \(error.localizedDescription)"
case .verificationFailed:
return "交易验证失败,可能是设备已越狱或交易数据被篡改"
case .configurationMissing:
return "配置缺失,请先调用 configure 方法进行配置"
case .serviceNotStarted:
return "服务未启动,请先调用 configure 方法"
case .purchaseInProgress:
return "购买正在进行中,请等待当前购买完成"
case .cancelSubscriptionFailed(let error):
return "取消订阅失败: \(error.localizedDescription)"
case .restorePurchasesFailed(let error):
return "恢复购买失败: \(error.localizedDescription)"
case .unknownError:
return "未知错误"
}
}
public var failureReason: String? {
switch self {
case .productNotFound(let id):
return "请检查产品ID是否正确并确保在 App Store Connect 中已配置该产品"
case .purchaseFailed(let error):
return error.localizedDescription
case .verificationFailed:
return "交易数据无法通过 Apple 的验证,这可能是由于设备已越狱或交易数据被篡改"
case .configurationMissing:
return "在调用其他方法之前,必须先调用 configure(with:delegate:) 或 configure(with:onStateChanged:) 方法"
case .serviceNotStarted:
return "StoreKit2Manager 尚未初始化,请先调用 configure 方法"
case .purchaseInProgress:
return "当前有购买正在进行,请等待完成后再试"
case .cancelSubscriptionFailed(let error):
return (error as? LocalizedError)?.failureReason ?? error.localizedDescription
case .restorePurchasesFailed(let error):
return (error as? LocalizedError)?.failureReason ?? error.localizedDescription
case .unknownError:
return "发生了未预期的错误"
}
}
}

View File

@@ -0,0 +1,95 @@
//
// StoreKitState.swift
// StoreKit2Manager
//
// Created by xiaopin on 2025/12/6.
//
import Foundation
import StoreKit
/// StoreKit
public enum StoreKitState: Equatable {
///
case idle
///
case loadingProducts
///
case loadingPurchases
///
case purchasesLoaded
///
case purchasing(String) // ID
///
case purchaseSuccess(String) // ID
///
case purchasePending(String) // ID
///
case purchaseCancelled(String) // ID
///
case purchaseFailed(String, Error) // ID,
///
case restoringPurchases
///
case restorePurchasesSuccess
///
case restorePurchasesFailed(Error)
/// 退
case purchaseRefunded(String) // ID
///
case purchaseRevoked(String) // ID
///
/// - Parameters:
/// - productId: ID
/// - isFreeTrialCancelled: true false
case subscriptionCancelled(String, isFreeTrialCancelled: Bool)
///
case error(Error)
public static func == (lhs: StoreKitState, rhs: StoreKitState) -> Bool {
switch (lhs, rhs) {
case (.idle, .idle),
(.loadingProducts, .loadingProducts),
(.loadingPurchases, .loadingPurchases),
(.purchasesLoaded, .purchasesLoaded):
return true
case (.purchasing(let lhsId), .purchasing(let rhsId)),
(.purchaseSuccess(let lhsId), .purchaseSuccess(let rhsId)),
(.purchasePending(let lhsId), .purchasePending(let rhsId)),
(.purchaseCancelled(let lhsId), .purchaseCancelled(let rhsId)):
return lhsId == rhsId
case (.purchaseFailed(let lhsId, _), .purchaseFailed(let rhsId, _)):
return lhsId == rhsId
case (.restoringPurchases, .restoringPurchases),
(.restorePurchasesSuccess, .restorePurchasesSuccess):
return true
case (.restorePurchasesFailed(let lhsError), .restorePurchasesFailed(let rhsError)):
return lhsError.localizedDescription == rhsError.localizedDescription
case (.purchaseRefunded(let lhsId), .purchaseRefunded(let rhsId)),
(.purchaseRevoked(let lhsId), .purchaseRevoked(let rhsId)):
return lhsId == rhsId
case (.subscriptionCancelled(let lhsId, let lhsIsFreeTrial), .subscriptionCancelled(let rhsId, let rhsIsFreeTrial)):
return lhsId == rhsId && lhsIsFreeTrial == rhsIsFreeTrial
case (.error(let lhsError), .error(let rhsError)):
return lhsError.localizedDescription == rhsError.localizedDescription
default:
return false
}
}
}

View File

@@ -0,0 +1,80 @@
//
// TransactionHistory.swift
// StoreKit2Manager
//
// Created by xiaopin on 2025/12/6.
//
import Foundation
import StoreKit
///
public struct TransactionHistory {
/// ID
public let productId: String
///
public let product: Product?
///
public let transaction: StoreKit.Transaction
///
public let purchaseDate: Date
///
public let expirationDate: Date?
/// 退
public let isRefunded: Bool
///
public let isRevoked: Bool
///
public let ownershipType: StoreKit.Transaction.OwnershipType
/// ID
public let transactionId: UInt64
public init(
productId: String,
product: Product?,
transaction: StoreKit.Transaction,
purchaseDate: Date,
expirationDate: Date? = nil,
isRefunded: Bool = false,
isRevoked: Bool = false,
ownershipType: StoreKit.Transaction.OwnershipType,
transactionId: UInt64
) {
self.productId = productId
self.product = product
self.transaction = transaction
self.purchaseDate = purchaseDate
self.expirationDate = expirationDate
self.isRefunded = isRefunded
self.isRevoked = isRevoked
self.ownershipType = ownershipType
self.transactionId = transactionId
}
}
// MARK: - Transaction
extension TransactionHistory {
/// Transaction
public static func from(_ transaction: StoreKit.Transaction, product: Product? = nil) -> TransactionHistory {
return TransactionHistory(
productId: transaction.productID,
product: product,
transaction: transaction,
purchaseDate: transaction.purchaseDate,
expirationDate: transaction.expirationDate,
isRefunded: transaction.revocationDate != nil,
isRevoked: transaction.revocationDate != nil,
ownershipType: transaction.ownershipType,
transactionId: transaction.id
)
}
}

View File

@@ -0,0 +1,48 @@
//
// StoreKitDelegate.swift
// StoreKit2Manager
//
// Created by xiaopin on 2025/12/6.
//
import Foundation
import StoreKit
/// StoreKit
/// 线
public protocol StoreKitDelegate: AnyObject {
///
/// - Parameters:
/// - manager: StoreKit2Manager
/// - state:
func storeKit(_ manager: StoreKit2Manager, didUpdateState state: StoreKitState)
///
/// - Parameters:
/// - manager: StoreKit2Manager
/// - products:
func storeKit(_ manager: StoreKit2Manager, didLoadProducts products: [Product])
///
/// - Parameters:
/// - manager: StoreKit2Manager
/// - efficient:
/// - latests:
func storeKit(_ manager: StoreKit2Manager, didUpdatePurchasedTransactions efficient: [Transaction], latests: [Transaction])
}
// MARK: -
extension StoreKitDelegate {
public func storeKit(_ manager: StoreKit2Manager, didUpdateState state: StoreKitState) {
//
}
public func storeKit(_ manager: StoreKit2Manager, didLoadProducts products: [Product]) {
//
}
public func storeKit(_ manager: StoreKit2Manager, didUpdatePurchasedTransactions efficient: [Transaction], latests: [Transaction]) {
//
}
}

View File

@@ -0,0 +1,536 @@
//
// StoreKit2Manager.swift
// StoreKit2Manager
//
// Created by xiaopin on 2025/12/6.
//
import Foundation
import StoreKit
///
public enum SubscriptionPeriodType: String {
case week = "week"
case month = "month"
case year = "year"
case lifetime = "lifetime"
}
///
public enum SubscriptionButtonType: String {
case standard = "standard" //
case freeTrial = "freeTrial" //
case payUpFront = "payUpFront" //
case payAsYouGo = "payAsYouGo" //
case lifetime = "lifetime" //
}
///
public typealias SubscriptionInfo = StoreKit.Product.SubscriptionInfo
///
public typealias SubscriptionStatus = StoreKit.Product.SubscriptionInfo.Status
///
public typealias SubscriptionPeriod = StoreKit.Product.SubscriptionPeriod
///
public typealias RenewalInfo = StoreKit.Product.SubscriptionInfo.RenewalInfo
///
public typealias RenewalState = StoreKit.Product.SubscriptionInfo.RenewalState
///
public typealias Transaction = StoreKit.Transaction
/// StoreKit
///
public class StoreKit2Manager {
///
public static let shared = StoreKit2Manager()
// MARK: -
private var config: StoreKitConfig?
private weak var delegate: StoreKitDelegate?
private var service: StoreKitService?
// MARK: -
///
public var onStateChanged: ((StoreKitState) -> Void)?
///
public var onProductsLoaded: (([Product]) -> Void)?
/// :
public var onPurchasedTransactionsUpdated: (([Transaction],[Transaction]) -> Void)?
// MARK: -
///
public private(set) var currentState: StoreKitState = .idle
///
public private(set) var allProducts: [Product] = []
///
public private(set) var purchasedTransactions: [Transaction] = []
///
public private(set) var latestTransactions: [Transaction] = []
// MARK: -
///
public var nonConsumables: [Product] {
allProducts.filter { $0.type == .nonConsumable }
}
///
public var consumables: [Product] {
allProducts.filter { $0.type == .consumable }
}
///
public var nonRenewables: [Product] {
allProducts.filter { $0.type == .nonRenewable }
}
///
public var autoRenewables: [Product] {
allProducts.filter { $0.type == .autoRenewable }
}
// MARK: -
private init() {}
// MARK: -
/// 使
/// - Parameters:
/// - config:
/// - delegate:
public func configure(with config: StoreKitConfig, delegate: StoreKitDelegate) {
self.config = config
self.delegate = delegate
self.service = StoreKitService(config: config, delegate: self)
service?.start()
}
/// 使
/// - Parameter config:
public func configure(with config: StoreKitConfig) {
self.config = config
self.service = StoreKitService(config: config, delegate: self)
service?.start()
}
// MARK: -
///
/// - Note: App Store
/// - Returns: nil
public func refreshProducts() async {
await service?.loadProducts()
}
///
/// - Returns:
public func getAllProducts() async -> [Product] {
if(allProducts.isEmpty){
if let products = await service?.loadProducts() {
allProducts = products
}
}
return allProducts
}
///
/// - Returns:
public func getNonConsumablesProducts() async -> [Product] {
return nonConsumables
}
///
/// - Returns:
public func getConsumablesProducts() async -> [Product] {
return consumables
}
///
/// - Returns:
public func getNonRenewablesProducts() async -> [Product] {
return nonRenewables
}
///
/// - Returns: /
public func getAutoRenewablesProducts() async -> [Product] {
return autoRenewables
}
///
/// - Parameter productId: ID
/// - Returns: nil
public func product(for productId: String) -> Product? {
return allProducts.first(where: { $0.id == productId })
}
/// VIP
/// - Parameter productId: ID
/// - Parameter periodType:
/// - Parameter languageCode:
/// - Parameter isShort:
/// - Returns:
public func productForVipTitle(for productId: String, periodType: SubscriptionPeriodType , languageCode: String, isShort: Bool = false) -> String {
guard let product = product(for: productId) else {
return ""
}
return SubscriptionLocale.subscriptionTitle(
periodType: periodType,
languageCode: languageCode,
isShort: isShort
)
}
/// VIP
/// - Parameter productId: ID
/// - Returns:
public func productForVipSubtitle(for productId: String, periodType: SubscriptionPeriodType , languageCode: String) async -> String {
guard let product = product(for: productId) else {
return ""
}
//
if let subscription = product.subscription {
// ,
let isEligible = await subscription.isEligibleForIntroOffer
if isEligible {
//
if subscription.introductoryOffer != nil {
return await SubscriptionLocale.introductoryOfferSubtitle(
product: product,
languageCode: languageCode
)
}
} else {
// 1
if !subscription.promotionalOffers.isEmpty {
return await SubscriptionLocale.promotionalOfferSubtitle(
product: product,
languageCode: languageCode
)
}
}
}
//
if config?.lifetimeIds.contains(productId) == true {
return SubscriptionLocale.defaultSubtitle(
product: product,
periodType: SubscriptionPeriodType.lifetime,
languageCode: languageCode
)
}
//
return SubscriptionLocale.defaultSubtitle(
product: product,
periodType: periodType,
languageCode: languageCode
)
}
///
/// - Parameter productId: ID
/// - Returns:
public func productForVipButtonText(for productId: String, languageCode: String) async -> String {
guard let product = product(for: productId) else {
return ""
}
//
var buttonType: SubscriptionButtonType = .standard
//
if let subscription = product.subscription {
//,
let isEligible = await subscription.isEligibleForIntroOffer
if isEligible {
//
if let introOffer = subscription.introductoryOffer {
switch introOffer.paymentMode {
case .freeTrial:
buttonType = .freeTrial
case .payUpFront:
buttonType = .payUpFront
case .payAsYouGo:
buttonType = .payAsYouGo
default:
buttonType = .standard
}
}
}else{
// 1
if let promotionalOffer = subscription.promotionalOffers.first {
switch promotionalOffer.paymentMode {
case .freeTrial:
buttonType = .freeTrial
case .payUpFront:
buttonType = .payUpFront
case .payAsYouGo:
buttonType = .payAsYouGo
default:
buttonType = .standard
}
}
}
}
//
if config?.lifetimeIds.contains(productId) == true {
buttonType = .lifetime
}
return SubscriptionLocale.subscriptionButtonText(
type: buttonType,
languageCode: languageCode
)
}
// MARK: -
/// ID
/// - Parameter productId: ID
/// - Throws: StoreKitError.productNotFound
public func purchase(productId: String) async throws {
guard let product = allProducts.first(where: { $0.id == productId }) else {
throw StoreKitError.productNotFound(productId)
}
try await service?.purchase(product)
}
///
/// - Parameter product:
/// - Throws: StoreKitError.purchaseInProgress
public func purchase(_ product: Product) async throws {
guard let service = service else {
throw StoreKitError.serviceNotStarted
}
try await service.purchase(product)
}
///
/// - Throws: StoreKitError.restorePurchasesFailed
public func restorePurchases() async throws {
try await service?.restorePurchases()
}
///
public func refreshPurchases() async {
await service?.loadPurchasedTransactions()
}
// MARK: -
///
/// - Parameter productId: ID
/// - Returns: true
public func isPurchased(productId: String) -> Bool {
return latestTransactions.contains(where: { $0.productID == productId })
}
///
/// - Parameter productId: ID
/// - Returns: true false
/// - Note:
public func isFamilyShared(productId: String) -> Bool {
guard let transaction = latestTransactions.first(where: { $0.productID == productId }) else {
return false
}
return transaction.ownershipType == .familyShared
}
///
/// - Parameter productId: ID
/// - Returns: true false
/// - Note:
public func isEligibleForIntroOffer(productId: String) async -> Bool {
guard let product = allProducts.first(where: { $0.id == productId }) else {
return false
}
guard let subscription = product.subscription else {
return false
}
return await subscription.isEligibleForIntroOffer
}
// MARK: -
///
/// - Returns: 退
public func getValidPurchasedTransactions() async -> [Transaction] {
return purchasedTransactions
}
///
/// - Returns:
public func getLatestTransactions() async -> [Transaction] {
return latestTransactions
}
///
/// - Parameter productId: ID
/// - Returns:
public func getTransactionHistory(for productId: String? = nil) async -> [TransactionHistory] {
await service?.getTransactionHistory(for: productId) ?? []
}
///
/// - Parameter productId: ID
/// - Returns:
public func getConsumablePurchaseHistory(for productId: String) async -> [TransactionHistory] {
await service?.getConsumablePurchaseHistory(for: productId) ?? []
}
// MARK: -
///
/// - Note:
/// -
/// -
/// -
/// - /
@MainActor
public func checkSubscriptionStatus() async {
await service?.checkSubscriptionStatusManually()
}
///
/// - Parameter productId: ID
/// - Returns: nil
public func getSubscriptionInfo(for productId: String) async -> SubscriptionInfo? {
guard let product = allProducts.first(where: { $0.id == productId }) else {
return nil
}
return product.subscription
}
///
/// - Parameter productId: ID
/// - Returns: nil
/// - Note: RenewalInfo willAutoRenewexpirationDaterenewalDate
public func getRenewalInfo(for productId: String) async -> RenewalInfo? {
guard let product = allProducts.first(where: { $0.id == productId }),
let subscription = product.subscription else {
return nil
}
do {
let statuses = try await subscription.status
if let status = statuses.first,
case .verified(let renewalInfo) = status.renewalInfo {
return renewalInfo
}
} catch {
print("获取续订信息失败: \(error)")
return nil
}
return nil
}
/// 使 URL
@MainActor
public func openSubscriptionManagement() {
service?.openSubscriptionManagement()
}
/// iOS 15.0+ / macOS 12.0+
/// - Returns:
@MainActor
public func showManageSubscriptionsSheet() async -> Bool {
await service?.showManageSubscriptionsSheet() ?? false
}
/// iOS 16.0+
/// - Throws: StoreKitError
/// - Note: Transaction.updates
@MainActor
public func presentOfferCodeRedeemSheet() async -> Bool {
guard let service = service else {
return false
}
if #available(iOS 16.0, visionOS 1.0, *){
do {
try await service.presentOfferCodeRedeemSheet()
return true
} catch {
return false
}
} else {
return false
}
}
// MARK: -
///
/// - Note: iOS 15.0+ iOS 16.0+
/// - iOS 15.0: 使 SKStoreReviewController.requestReview() (StoreKit 1)
/// - iOS 16.0+: 使 AppStore.requestReview(in:) (StoreKit 2)
/// - Important: 使
/// 3
@MainActor
public func requestReview() {
service?.requestReview()
}
///
public func stop() {
service?.stop()
service = nil
config = nil
delegate = nil
currentState = .idle
allProducts = []
}
}
// MARK: - StoreKitServiceDelegate
extension StoreKit2Manager: StoreKitServiceDelegate {
@MainActor
func service(_ service: StoreKitService, didUpdateState state: StoreKitState) {
currentState = state
//
delegate?.storeKit(self, didUpdateState: state)
//
onStateChanged?(state)
}
@MainActor
func service(_ service: StoreKitService, didLoadProducts products: [Product]) {
allProducts = products
//
delegate?.storeKit(self, didLoadProducts: products)
//
onProductsLoaded?(products)
}
@MainActor
func service(_ service: StoreKitService, didUpdatePurchasedTransactions efficient: [Transaction], latests: [Transaction]) {
purchasedTransactions = efficient
//
delegate?.storeKit(self, didUpdatePurchasedTransactions: efficient, latests: latests)
//
onPurchasedTransactionsUpdated?(efficient, latests)
}
}