Skip to content
This repository was archived by the owner on Oct 16, 2025. It is now read-only.

Commit a2b45aa

Browse files
authored
Normalize in-app request type handling (#8)
## Summary - Accept both `inapp` and `in-app` while defaulting to the preferred form - Update module filtering, docs, sample, and add regression coverage ## Testing - `swift test` *(fails: sandbox denied access to SwiftPM cache directories)*
1 parent e5d6824 commit a2b45aa

File tree

5 files changed

+95
-14
lines changed

5 files changed

+95
-14
lines changed

Example/OpenIapExample/Screens/PurchaseFlowScreen.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ struct PurchaseFlowScreen: View {
224224
private func loadProducts() {
225225
Task {
226226
do {
227-
try await iapStore.fetchProducts(skus: productIds, type: .inapp)
227+
try await iapStore.fetchProducts(skus: productIds, type: .inApp)
228228
await MainActor.run {
229229
if iapStore.products.isEmpty {
230230
errorMessage = "No products found. Please check your App Store Connect configuration."

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ class StoreViewModel: ObservableObject {
139139
// Fetch products
140140
try await iapStore.fetchProducts(
141141
skus: ["product1", "product2"],
142-
type: .inapp
142+
type: .inApp
143143
)
144144
}
145145
}
@@ -312,7 +312,7 @@ struct OpenIapProduct {
312312
let id: String
313313
let title: String
314314
let description: String
315-
let type: String // "inapp" or "subs"
315+
let type: String // "in-app" (preferred) or legacy "inapp" (deprecated, removal in 1.2.0) or "subs"
316316
let displayPrice: String
317317
let currency: String
318318
let price: Double?

Sources/Models/OpenIapProductRequest.swift

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,106 @@
11
import Foundation
22

33
/// Request product type for filtering when fetching products
4-
/// Maps to literal strings: "inapp", "subs", "all"
4+
/// Maps to literal strings: "in-app" (preferred), legacy "inapp" (deprecated, removal scheduled for 1.2.0), "subs", "all"
55
public enum OpenIapRequestProductType: String, Codable, Sendable {
6+
internal static let legacyInAppRawValue = "inapp"
7+
internal static let modernInAppRawValue = "in-app"
8+
9+
@available(*, deprecated, message: "'inapp' is deprecated and will be removed in 1.2.0. Use .inApp instead.")
610
case inapp = "inapp"
11+
case inApp = "in-app"
712
case subs = "subs"
813
case all = "all"
14+
15+
internal var normalizedRawValue: String {
16+
if rawValue == Self.legacyInAppRawValue {
17+
return Self.modernInAppRawValue
18+
}
19+
return rawValue
20+
}
921
}
1022

1123
/// Product request parameters following OpenIAP specification
1224
public struct OpenIapProductRequest: Codable, Equatable, Sendable {
1325
/// Product SKUs to fetch
1426
public let skus: [String]
1527

16-
/// Product type filter: "inapp" (default), "subs", or "all"
28+
/// Product type filter: "in-app" (default), "subs", or "all". The legacy value "inapp" is still accepted but will be removed in 1.2.0.
1729
public let type: String
18-
19-
public init(skus: [String], type: String = "inapp") {
30+
31+
private enum CodingKeys: String, CodingKey {
32+
case skus
33+
case type
34+
}
35+
36+
/// Create request specifying raw type string. Passing `nil` or an empty string defaults to "in-app".
37+
public init(skus: [String], type: String? = nil) {
2038
self.skus = skus
21-
self.type = type
39+
self.type = OpenIapProductRequest.normalizeType(type)
2240
}
2341

2442
/// Convenience initializer with RequestProductType enum
25-
public init(skus: [String], type: OpenIapRequestProductType = .inapp) {
43+
public init(skus: [String], type: OpenIapRequestProductType = .inApp) {
2644
self.skus = skus
27-
self.type = type.rawValue
45+
self.type = type.normalizedRawValue
46+
}
47+
48+
public init(from decoder: Decoder) throws {
49+
let container = try decoder.container(keyedBy: CodingKeys.self)
50+
let skus = try container.decode([String].self, forKey: .skus)
51+
let rawType = try container.decodeIfPresent(String.self, forKey: .type)
52+
self.init(skus: skus, type: rawType ?? OpenIapProductRequest.defaultTypeValue)
53+
}
54+
55+
public func encode(to encoder: Encoder) throws {
56+
var container = encoder.container(keyedBy: CodingKeys.self)
57+
try container.encode(skus, forKey: .skus)
58+
try container.encode(type, forKey: .type)
2859
}
2960

3061
/// Get the type as RequestProductType enum
3162
public var requestType: OpenIapRequestProductType {
32-
return OpenIapRequestProductType(rawValue: type) ?? .inapp
63+
if let parsedType = OpenIapRequestProductType(rawValue: type) {
64+
if parsedType.rawValue == OpenIapRequestProductType.legacyInAppRawValue {
65+
return .inApp
66+
}
67+
return parsedType
68+
}
69+
70+
let normalized = OpenIapProductRequest.normalizeType(type)
71+
return OpenIapRequestProductType(rawValue: normalized) ?? .inApp
72+
}
73+
74+
private static let defaultTypeValue = OpenIapRequestProductType.modernInAppRawValue
75+
76+
private static func normalizeType(_ rawType: String?) -> String {
77+
guard let rawType else {
78+
return defaultTypeValue
79+
}
80+
81+
let trimmed = rawType.trimmingCharacters(in: .whitespacesAndNewlines)
82+
guard !trimmed.isEmpty else {
83+
return defaultTypeValue
84+
}
85+
86+
if let productType = OpenIapRequestProductType(rawValue: trimmed) {
87+
return productType.normalizedRawValue
88+
}
89+
90+
let lowered = trimmed.lowercased()
91+
switch lowered {
92+
case OpenIapRequestProductType.legacyInAppRawValue, OpenIapRequestProductType.modernInAppRawValue:
93+
return OpenIapRequestProductType.modernInAppRawValue
94+
case OpenIapRequestProductType.subs.rawValue:
95+
return OpenIapRequestProductType.subs.rawValue
96+
case OpenIapRequestProductType.all.rawValue:
97+
return OpenIapRequestProductType.all.rawValue
98+
default:
99+
return defaultTypeValue
100+
}
33101
}
34102
}
35103

36104
// Backward compatibility aliases
37105
public typealias RequestProductType = OpenIapRequestProductType
38106
public typealias ProductRequest = OpenIapProductRequest
39-

Sources/OpenIapModule.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,8 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol {
171171

172172
// Filter by type using enum
173173
switch params.requestType {
174-
case .inapp:
175-
OpenIapLog.debug("🔷 [OpenIapModule] Filtering for inapp products")
174+
case .inapp, .inApp:
175+
OpenIapLog.debug("🔷 [OpenIapModule] Filtering for in-app products")
176176
openIapProducts = openIapProducts.filter { product in
177177
let isInApp = product.productType == .inapp
178178
OpenIapLog.debug("🔷 [OpenIapModule] Product \(product.id): productType=\(product.productType), isInApp=\(isInApp)")

Tests/OpenIapTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,18 @@ final class OpenIapTests: XCTestCase {
189189
XCTAssertEqual(receipt.inAppPurchases.count, 1)
190190
XCTAssertEqual(receipt.inAppPurchases.first?.id, "trans1")
191191
}
192+
193+
func testProductRequestTypeNormalization() {
194+
let legacyRequest = OpenIapProductRequest(skus: ["sku1"], type: "inapp")
195+
XCTAssertEqual(legacyRequest.type, "in-app")
196+
XCTAssertEqual(legacyRequest.requestType, .inApp)
197+
198+
let modernRequest = OpenIapProductRequest(skus: ["sku1"], type: "in-app")
199+
XCTAssertEqual(modernRequest.type, "in-app")
200+
XCTAssertEqual(modernRequest.requestType, .inApp)
201+
202+
let enumRequest = OpenIapProductRequest(skus: ["sku1"], type: .inApp)
203+
XCTAssertEqual(enumRequest.type, "in-app")
204+
XCTAssertEqual(enumRequest.requestType, .inApp)
205+
}
192206
}

0 commit comments

Comments
 (0)