Files
keyboard/keyBoard/Class/Pay/StoreKit2Manager/StoreKitManager.swift

552 lines
19 KiB
Swift
Raw Normal View History

2025-12-16 13:10:50 +08:00
//
// 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] = []
2025-12-16 15:47:12 +08:00
/// ID
private var recentJWSPayloads: [String: String] = [:]
2025-12-16 13:10:50 +08:00
// 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
}
2025-12-16 15:47:12 +08:00
/// JWS StoreKitService
/// - Parameter productId: ID
/// - Returns:
@MainActor
func consumeRecentPayload(for productId: String) -> String? {
return recentJWSPayloads.removeValue(forKey: productId)
}
2025-12-16 13:10:50 +08:00
///
/// - 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)
}
2025-12-16 15:47:12 +08:00
@MainActor
func service(_ service: StoreKitService, didCompletePurchaseFor productId: String, payload: String) {
recentJWSPayloads[productId] = payload
}
2025-12-16 13:10:50 +08:00
}