From b1ec383e15ccc35d61f976ec6b660145e2a27ebc Mon Sep 17 00:00:00 2001 From: Leonardo Bilck Date: Sun, 30 Aug 2026 19:15:20 -0300 Subject: [PATCH] Fix onDismiss purchased result sending a description instead of a product id `PaywallResult.purchased` carries a `StoreProduct`, not a `String`, but the binding was named `productId` and the value was placed straight into the `onDismiss` payload. JSON serialization then received a non-JSON leaf and fell back to the object's debug description, so Unity was handed "productId": "" instead of the identifier. Any consumer that matches that id against a store catalogue - revenue reporting, receipt lookup, entitlement mapping - finds nothing and silently drops the purchase. It fails quietly: the purchase itself succeeds and entitlements activate normally, so only downstream reporting is affected, which makes it easy to miss. Found on a real sandbox subscription: the id never resolved against the Unity IAP catalogue, so the client-side revenue fan-out was skipped entirely. Reads `product.productIdentifier`, matching what `serializeStoreProduct` and the product-serialization paths in this same file already do. Co-Authored-By: Claude Opus 5 (1M context) --- Plugins/iOS/SuperwallUnityBridge.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Plugins/iOS/SuperwallUnityBridge.swift b/Plugins/iOS/SuperwallUnityBridge.swift index 03b892a..136225a 100644 --- a/Plugins/iOS/SuperwallUnityBridge.swift +++ b/Plugins/iOS/SuperwallUnityBridge.swift @@ -883,8 +883,13 @@ public func _SuperwallBridge_RegisterPlacement( "paywallInfo": serializePaywallInfo(info) ] switch result { - case .purchased(let productId): - data["result"] = ["type": "purchased", "productId": productId] + // `PaywallResult.purchased` carries a `StoreProduct`, not a `String` — the old binding name + // was misleading. Passing the object straight into the payload meant JSON serialization + // received a non-JSON leaf and fell back to its debug description, so Unity was handed + // "" as `productId`. Any consumer matching that against a store + // catalogue (revenue reporting, receipt lookup) finds nothing and silently drops the purchase. + case .purchased(let product): + data["result"] = ["type": "purchased", "productId": product.productIdentifier] case .declined: data["result"] = ["type": "declined"] case .restored: