1
This commit is contained in:
2
Podfile
2
Podfile
@@ -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
|
||||
|
||||
@@ -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 */;
|
||||
}
|
||||
|
||||
15
keyBoard.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
15
keyBoard.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"] = ""
|
||||
}
|
||||
|
||||
// 应用交易ID(iOS 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"] = ""
|
||||
}
|
||||
|
||||
// Web订单行项目ID(如果有)
|
||||
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.Offer,iOS 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
|
||||
}
|
||||
}
|
||||
|
||||
1300
keyBoard/Class/Pay/StoreKit2Manager/Internal/StoreKitService.swift
Normal file
1300
keyBoard/Class/Pay/StoreKit2Manager/Internal/StoreKitService.swift
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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])
|
||||
}
|
||||
|
||||
1332
keyBoard/Class/Pay/StoreKit2Manager/Locals/SubscriptionLocale.swift
Normal file
1332
keyBoard/Class/Pay/StoreKit2Manager/Locals/SubscriptionLocale.swift
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 "发生了未预期的错误"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]) {
|
||||
// 默认实现为空,子类可以选择性实现
|
||||
}
|
||||
}
|
||||
|
||||
536
keyBoard/Class/Pay/StoreKit2Manager/StoreKitManager.swift
Normal file
536
keyBoard/Class/Pay/StoreKit2Manager/StoreKitManager.swift
Normal 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 包含 willAutoRenew(是否自动续订)、expirationDate(过期日期)、renewalDate(续订日期)等信息
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user