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

Commit 6b7edc2

Browse files
committed
fix: improve promotional offer error handling and logging
1 parent d0bffd4 commit 6b7edc2

File tree

3 files changed

+60
-20
lines changed

3 files changed

+60
-20
lines changed

Sources/Helpers/StoreKitTypesBridge.swift

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,15 +167,22 @@ enum StoreKitTypesBridge {
167167
return nil
168168
}
169169

170-
static func purchaseOptions(from props: RequestPurchaseIosProps) -> Set<StoreKit.Product.PurchaseOption> {
170+
static func purchaseOptions(from props: RequestPurchaseIosProps) throws -> Set<StoreKit.Product.PurchaseOption> {
171171
var options: Set<StoreKit.Product.PurchaseOption> = []
172172
if let quantity = props.quantity, quantity > 1 {
173173
options.insert(.quantity(quantity))
174174
}
175175
if let token = props.appAccountToken, let uuid = UUID(uuidString: token) {
176176
options.insert(.appAccountToken(uuid))
177177
}
178-
if let offerInput = props.withOffer, let option = promotionalOffer(from: offerInput) {
178+
if let offerInput = props.withOffer {
179+
guard let option = promotionalOffer(from: offerInput) else {
180+
throw PurchaseError.make(
181+
code: .developerError,
182+
productId: props.sku,
183+
message: "Invalid promotional offer: nonce must be valid UUID and signature must be base64 encoded"
184+
)
185+
}
179186
options.insert(option)
180187
}
181188
return options
@@ -278,11 +285,19 @@ private extension StoreKitTypesBridge {
278285
}
279286

280287
static func promotionalOffer(from offer: DiscountOfferInputIOS) -> StoreKit.Product.PurchaseOption? {
281-
guard let nonce = UUID(uuidString: offer.nonce),
282-
let signature = Data(base64Encoded: offer.signature) else {
288+
guard let nonce = UUID(uuidString: offer.nonce) else {
289+
OpenIapLog.error("❌ Invalid nonce format: \(offer.nonce)")
290+
return nil
291+
}
292+
293+
guard let signature = Data(base64Encoded: offer.signature) else {
294+
OpenIapLog.error("❌ Invalid signature format (must be base64): \(offer.signature)")
283295
return nil
284296
}
297+
285298
let timestamp = Int(offer.timestamp)
299+
OpenIapLog.debug("✅ Creating promotional offer - ID: \(offer.identifier), KeyID: \(offer.keyIdentifier), Timestamp: \(timestamp)")
300+
286301
return .promotionalOffer(
287302
offerID: offer.identifier,
288303
keyID: offer.keyIdentifier,

Sources/OpenIapModule.swift

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -170,26 +170,51 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol {
170170
let iosProps = try resolveIosPurchaseProps(from: params)
171171
let sku = iosProps.sku
172172
let product = try await storeProduct(for: sku)
173-
let options = StoreKitTypesBridge.purchaseOptions(from: iosProps)
173+
let options = try StoreKitTypesBridge.purchaseOptions(from: iosProps)
174174

175175
let result: StoreKit.Product.PurchaseResult
176-
#if canImport(UIKit)
177-
if #available(iOS 17.0, *) {
178-
let scene: UIWindowScene? = await MainActor.run {
179-
UIApplication.shared.connectedScenes.first as? UIWindowScene
180-
}
181-
guard let scene else {
182-
let error = makePurchaseError(code: .purchaseError, message: "Could not find window scene")
183-
emitPurchaseError(error)
184-
throw error
176+
do {
177+
#if canImport(UIKit)
178+
if #available(iOS 17.0, *) {
179+
let scene: UIWindowScene? = await MainActor.run {
180+
UIApplication.shared.connectedScenes.first as? UIWindowScene
181+
}
182+
guard let scene else {
183+
let error = makePurchaseError(code: .purchaseError, message: "Could not find window scene")
184+
emitPurchaseError(error)
185+
throw error
186+
}
187+
result = try await product.purchase(confirmIn: scene, options: options)
188+
} else {
189+
result = try await product.purchase(options: options)
185190
}
186-
result = try await product.purchase(confirmIn: scene, options: options)
187-
} else {
191+
#else
188192
result = try await product.purchase(options: options)
193+
#endif
194+
} catch {
195+
// Enhanced error handling for promotional offers
196+
if iosProps.withOffer != nil {
197+
OpenIapLog.error("Purchase with promotional offer failed: \(error.localizedDescription)")
198+
let enhancedMessage = """
199+
Promotional offer purchase failed: \(error.localizedDescription)
200+
201+
Common causes:
202+
1. Invalid signature - verify server generates correct signature
203+
2. Sandbox testing - ensure current subscription has expired before testing offers
204+
3. Offer eligibility - user may not be eligible for this promotional offer
205+
4. Key mismatch - verify using correct In-App Purchase key for environment
206+
"""
207+
let purchaseError = makePurchaseError(
208+
code: .purchaseError,
209+
productId: sku,
210+
message: enhancedMessage
211+
)
212+
emitPurchaseError(purchaseError)
213+
throw purchaseError
214+
}
215+
// Re-throw original error for non-promotional purchases
216+
throw error
189217
}
190-
#else
191-
result = try await product.purchase(options: options)
192-
#endif
193218

194219
switch result {
195220
case .success(let verification):

openiap-versions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"apple": "1.2.11",
2+
"apple": "1.2.13",
33
"gql": "1.0.12"
44
}

0 commit comments

Comments
 (0)