Files
keyboard/keyBoard/Class/Pay/StoreKit2Manager/StoreKitManager.swift
2025-12-16 15:47:12 +08:00

552 lines
19 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

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

//
// 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] = []
/// ID
private var recentJWSPayloads: [String: String] = [:]
// 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
}
/// JWS StoreKitService
/// - Parameter productId: ID
/// - Returns:
@MainActor
func consumeRecentPayload(for productId: String) -> String? {
return recentJWSPayloads.removeValue(forKey: productId)
}
///
/// - 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)
}
@MainActor
func service(_ service: StoreKitService, didCompletePurchaseFor productId: String, payload: String) {
recentJWSPayloads[productId] = payload
}
}