|
|
@@ -28,11 +28,11 @@ struct StoreKitCatalogSnapshot: Sendable {
|
|
|
private let entriesByProductID: [String: StoreKitCatalogEntry]
|
|
|
|
|
|
init() {
|
|
|
- let loadedEntries = Self.loadEntriesFromBundle() ?? Self.fallbackEntries()
|
|
|
- entries = loadedEntries
|
|
|
- orderedPlans = loadedEntries.map { PaywallPlan(productID: $0.productID) }
|
|
|
- allProductIDs = Set(loadedEntries.map(\.productID))
|
|
|
- entriesByProductID = Dictionary(uniqueKeysWithValues: loadedEntries.map { ($0.productID, $0) })
|
|
|
+ let configuredEntries = Self.entriesFromPaywallConfig()
|
|
|
+ entries = configuredEntries
|
|
|
+ orderedPlans = configuredEntries.map { PaywallPlan(productID: $0.productID) }
|
|
|
+ allProductIDs = Set(configuredEntries.map(\.productID))
|
|
|
+ entriesByProductID = Dictionary(uniqueKeysWithValues: configuredEntries.map { ($0.productID, $0) })
|
|
|
}
|
|
|
|
|
|
func contains(productID: String) -> Bool {
|
|
|
@@ -91,53 +91,7 @@ struct StoreKitCatalogSnapshot: Sendable {
|
|
|
orderedPlans.filter { kind(for: $0.productID) != .lifetime }
|
|
|
}
|
|
|
|
|
|
- private static func loadEntriesFromBundle() -> [StoreKitCatalogEntry]? {
|
|
|
- guard let url = Bundle.main.url(forResource: "Products", withExtension: "storekit"),
|
|
|
- let data = try? Data(contentsOf: url),
|
|
|
- let file = try? JSONDecoder().decode(StoreKitConfigurationFile.self, from: data) else {
|
|
|
- return nil
|
|
|
- }
|
|
|
-
|
|
|
- var entries: [StoreKitCatalogEntry] = []
|
|
|
-
|
|
|
- for product in file.products {
|
|
|
- entries.append(
|
|
|
- StoreKitCatalogEntry(
|
|
|
- productID: product.productID,
|
|
|
- displayName: product.preferredDisplayName,
|
|
|
- kind: kind(forProductID: product.productID, type: product.type),
|
|
|
- sortOrder: 1_000
|
|
|
- )
|
|
|
- )
|
|
|
- }
|
|
|
-
|
|
|
- for group in file.subscriptionGroups {
|
|
|
- for subscription in group.subscriptions {
|
|
|
- entries.append(
|
|
|
- StoreKitCatalogEntry(
|
|
|
- productID: subscription.productID,
|
|
|
- displayName: subscription.preferredDisplayName,
|
|
|
- kind: kind(
|
|
|
- forProductID: subscription.productID,
|
|
|
- subscriptionPeriod: subscription.recurringSubscriptionPeriod
|
|
|
- ),
|
|
|
- sortOrder: sortOrder(for: subscription.recurringSubscriptionPeriod)
|
|
|
- )
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- guard !entries.isEmpty else { return nil }
|
|
|
-
|
|
|
- return entries.sorted { lhs, rhs in
|
|
|
- if lhs.sortOrder == rhs.sortOrder {
|
|
|
- return lhs.productID < rhs.productID
|
|
|
- }
|
|
|
- return lhs.sortOrder < rhs.sortOrder
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private static func fallbackEntries() -> [StoreKitCatalogEntry] {
|
|
|
+ private static func entriesFromPaywallConfig() -> [StoreKitCatalogEntry] {
|
|
|
let config = PaywallConfig.loadBundled()
|
|
|
return [
|
|
|
StoreKitCatalogEntry(
|
|
|
@@ -160,75 +114,4 @@ struct StoreKitCatalogSnapshot: Sendable {
|
|
|
),
|
|
|
]
|
|
|
}
|
|
|
-
|
|
|
- private static func kind(
|
|
|
- forProductID productID: String,
|
|
|
- type: String? = nil,
|
|
|
- subscriptionPeriod: String? = nil
|
|
|
- ) -> PaywallPlanKind {
|
|
|
- if let suffixKind = PaywallPlanKind.allCases.first(where: { productID.hasSuffix(".\($0.rawValue)") }) {
|
|
|
- return suffixKind
|
|
|
- }
|
|
|
- if type == "NonConsumable" {
|
|
|
- return .lifetime
|
|
|
- }
|
|
|
- switch subscriptionPeriod {
|
|
|
- case "P1M": return .monthly
|
|
|
- case "P1Y": return .yearly
|
|
|
- default: return .monthly
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private static func sortOrder(for subscriptionPeriod: String?) -> Int {
|
|
|
- switch subscriptionPeriod {
|
|
|
- case "P1M": return 20
|
|
|
- case "P1Y": return 30
|
|
|
- default: return 40
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-private struct StoreKitConfigurationFile: Decodable {
|
|
|
- struct Product: Decodable {
|
|
|
- struct Localization: Decodable {
|
|
|
- let displayName: String
|
|
|
- let locale: String
|
|
|
- }
|
|
|
-
|
|
|
- let productID: String
|
|
|
- let type: String
|
|
|
- let localizations: [Localization]?
|
|
|
- }
|
|
|
-
|
|
|
- struct SubscriptionGroup: Decodable {
|
|
|
- struct Subscription: Decodable {
|
|
|
- struct Localization: Decodable {
|
|
|
- let displayName: String
|
|
|
- let locale: String
|
|
|
- }
|
|
|
-
|
|
|
- let productID: String
|
|
|
- let recurringSubscriptionPeriod: String?
|
|
|
- let localizations: [Localization]?
|
|
|
- }
|
|
|
-
|
|
|
- let subscriptions: [Subscription]
|
|
|
- }
|
|
|
-
|
|
|
- let products: [Product]
|
|
|
- let subscriptionGroups: [SubscriptionGroup]
|
|
|
-}
|
|
|
-
|
|
|
-private extension StoreKitConfigurationFile.Product {
|
|
|
- var preferredDisplayName: String? {
|
|
|
- localizations?.first(where: { $0.locale.hasPrefix("en") })?.displayName
|
|
|
- ?? localizations?.first?.displayName
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-private extension StoreKitConfigurationFile.SubscriptionGroup.Subscription {
|
|
|
- var preferredDisplayName: String? {
|
|
|
- localizations?.first(where: { $0.locale.hasPrefix("en") })?.displayName
|
|
|
- ?? localizations?.first?.displayName
|
|
|
- }
|
|
|
}
|