From c7acddd9e5b6908089a8f48eedcf1a2c521f0217 Mon Sep 17 00:00:00 2001 From: Leonardo Bilck Date: Thu, 3 Sep 2026 22:17:34 -0300 Subject: [PATCH 1/2] Fix SuperwallEventInfo.EventType never matching the enum on either platform `serializeEventInfo` on iOS sent `String(describing: info.event)`. On SuperwallKit versions where `SuperwallEvent` is not CustomStringConvertible that is Swift's default description, which for a case with associated values is the whole payload: transactionComplete(transaction: Optional(SuperwallKit.StoreTransaction), ...) Android sends `SuperwallEvent.rawName`, which is snake_case (`transaction_complete`, `freeTrial_start`). Neither shape matches the PascalCase `EventType` members, `Enum.TryParse` failed, and `EventType` stayed at its default (`FirstSeen`) with no log line. Every `switch` on it in consumer code was dead, on both platforms. iOS now keeps only the leading identifier of the description, which yields the case name on older SuperwallKit and the snake_case placement name on newer ones. The C# side normalises both by retrying the parse with underscores stripped, and logs a warning when a name still does not match instead of silently defaulting. Found on device: `begin_checkout`-style handlers keyed on `EventType.TransactionStart` never fired on iOS, and no transaction id was ever captured from `TransactionComplete`. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 5 +++++ Plugins/iOS/SuperwallUnityBridge.swift | 14 ++++++++++++-- Runtime/Internal/BridgeCallbackHandler.cs | 17 +++++++++++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffc0bc6..1932750 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog All notable changes to this package will be documented in this file. +## [Unreleased] + +### Fixes +* `SuperwallEventInfo.EventType` was never populated: iOS serialized `String(describing: event)`, which on SuperwallKit versions without a `CustomStringConvertible` conformance dumps the case together with its associated values (`transactionComplete(transaction: ..., product: ...)`), and Android sends the snake_case `rawName` (`transaction_complete`). Neither matched the `EventType` enum, so every delegate event arrived as the default `FirstSeen` and any `switch` on it was dead. iOS now emits only the case name, and the C# parser accepts both camelCase and snake_case, logging a warning instead of failing silently when a name is still unknown. + ## [0.2.6] ### Dependencies diff --git a/Plugins/iOS/SuperwallUnityBridge.swift b/Plugins/iOS/SuperwallUnityBridge.swift index 03b892a..4ccb8ae 100644 --- a/Plugins/iOS/SuperwallUnityBridge.swift +++ b/Plugins/iOS/SuperwallUnityBridge.swift @@ -292,8 +292,18 @@ private func serializeEventInfo(_ info: SuperwallEventInfo) -> [String: Any] { var dict: [String: Any] = [ "params": info.params ] - // Use the string representation of the event - dict["eventType"] = String(describing: info.event) + // `String(describing:)` is not a stable name for this enum. Recent SuperwallKit releases make + // `SuperwallEvent` CustomStringConvertible and describe it as its snake_case placement name + // ("transaction_complete"), but older 4.x releases fall back to Swift's default description, + // which for a case with associated values is the whole payload: + // + // transactionComplete(transaction: Optional(SuperwallKit.StoreTransaction), product: ..., ...) + // + // That never matched the C# `EventType` enum, so every delegate event arrived typed as the + // default (`FirstSeen`). Keep only the leading identifier: both shapes then parse on the C# side, + // which also normalises Android's snake_case `rawName`. + let described = String(describing: info.event) + dict["eventType"] = String(described.prefix(while: { $0 != "(" })) return dict } diff --git a/Runtime/Internal/BridgeCallbackHandler.cs b/Runtime/Internal/BridgeCallbackHandler.cs index 33d1a0c..d511b00 100644 --- a/Runtime/Internal/BridgeCallbackHandler.cs +++ b/Runtime/Internal/BridgeCallbackHandler.cs @@ -254,6 +254,18 @@ private static long GetLong(Dictionary data, string key, long de return null; } + /// + /// The two native bridges do not agree on the shape of an event name: iOS sends the Swift case + /// name (camelCase) and Android sends SuperwallEvent.rawName, which is snake_case + /// (transaction_complete, freeTrial_start). Stripping underscores makes both match + /// the PascalCase members under a case-insensitive parse. + /// + internal static EventType? ParseEventType(string value) + { + if (string.IsNullOrEmpty(value)) return null; + return ParseEnum(value) ?? ParseEnum(value.Replace("_", string.Empty)); + } + private static SubscriptionStatus DeserializeSubscriptionStatus(Dictionary data) { if (data == null) return SubscriptionStatus.CreateUnknown(); @@ -540,10 +552,11 @@ private static SuperwallEventInfo DeserializeSuperwallEventInfo(Dictionary(eventTypeStr); + var parsed = ParseEventType(eventTypeStr); if (parsed.HasValue) info.EventType = parsed.Value; + else + Debug.LogWarning($"[Superwall] Unrecognised eventType '{eventTypeStr}'; leaving EventType at its default. Handlers switching on it will not run for this event."); } var paramsDict = GetDict(data, "params"); From d4b3f049f0b2c423a4d3397388d9020cecdef7ec Mon Sep 17 00:00:00 2001 From: Leonardo Bilck Date: Fri, 4 Sep 2026 00:24:53 -0300 Subject: [PATCH 2/2] Add the EventType members both native SDKs emit but C# lacked Compared every name the two bridges can send - Android's `SuperwallEvent.rawName` values and iOS's `SuperwallEventObjc.description` strings - against the C# `EventType` enum. Twelve native events had no member (`paywall_page_view` fires on every paywall page and was warning several times per session), and two placement names are spelled differently from the member chosen for them (`deepLink_open` vs `DeepLink`, `paywallWebviewLoad_processTerminated` vs `PaywallWebviewProcessTerminated`), so they never matched either. New members are appended, never reordered. The two spelling mismatches are mapped explicitly in `ParseEventType` rather than by renaming the members, which would break consumers switching on them. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 2 +- Runtime/Internal/BridgeCallbackHandler.cs | 19 +++++++++++++++---- Runtime/Models/Enums.cs | 17 ++++++++++++++++- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1932750..9637fbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this package will be documented in this file. ## [Unreleased] ### Fixes -* `SuperwallEventInfo.EventType` was never populated: iOS serialized `String(describing: event)`, which on SuperwallKit versions without a `CustomStringConvertible` conformance dumps the case together with its associated values (`transactionComplete(transaction: ..., product: ...)`), and Android sends the snake_case `rawName` (`transaction_complete`). Neither matched the `EventType` enum, so every delegate event arrived as the default `FirstSeen` and any `switch` on it was dead. iOS now emits only the case name, and the C# parser accepts both camelCase and snake_case, logging a warning instead of failing silently when a name is still unknown. +* `SuperwallEventInfo.EventType` was never populated: iOS serialized `String(describing: event)`, which on SuperwallKit versions without a `CustomStringConvertible` conformance dumps the case together with its associated values (`transactionComplete(transaction: ..., product: ...)`), and Android sends the snake_case `rawName` (`transaction_complete`). Neither matched the `EventType` enum, so every delegate event arrived as the default `FirstSeen` and any `switch` on it was dead. iOS now emits only the case name, and the C# parser accepts both camelCase and snake_case, logging a warning instead of failing silently when a name is still unknown. `EventType` gains the members both native SDKs already emit but C# lacked (`PaywallPageView`, `TestModeModalOpen`/`Close`, `CelExpressionResult`, `ErrorThrown`, `ReviewGranted`/`Denied`, `AttributionMatch`, `StripeCheckoutStart`/`Submit`/`Complete`/`Fail`), and the two placement names whose member is spelled differently (`deepLink_open` → `DeepLink`, `paywallWebviewLoad_processTerminated` → `PaywallWebviewProcessTerminated`) are mapped explicitly. ## [0.2.6] diff --git a/Runtime/Internal/BridgeCallbackHandler.cs b/Runtime/Internal/BridgeCallbackHandler.cs index d511b00..ed30db4 100644 --- a/Runtime/Internal/BridgeCallbackHandler.cs +++ b/Runtime/Internal/BridgeCallbackHandler.cs @@ -256,14 +256,25 @@ private static long GetLong(Dictionary data, string key, long de /// /// The two native bridges do not agree on the shape of an event name: iOS sends the Swift case - /// name (camelCase) and Android sends SuperwallEvent.rawName, which is snake_case - /// (transaction_complete, freeTrial_start). Stripping underscores makes both match - /// the PascalCase members under a case-insensitive parse. + /// name (camelCase) or, on newer SuperwallKit, the snake_case placement name; Android sends + /// SuperwallEvent.rawName, which is snake_case (transaction_complete, + /// freeTrial_start). Stripping underscores makes all of them match the PascalCase + /// members under a case-insensitive parse. Two placement names differ from + /// the member that was chosen for them and are mapped explicitly. /// internal static EventType? ParseEventType(string value) { if (string.IsNullOrEmpty(value)) return null; - return ParseEnum(value) ?? ParseEnum(value.Replace("_", string.Empty)); + + var parsed = ParseEnum(value) ?? ParseEnum(value.Replace("_", string.Empty)); + if (parsed.HasValue) return parsed; + + switch (value.Replace("_", string.Empty).ToLowerInvariant()) + { + case "deeplinkopen": return EventType.DeepLink; + case "paywallwebviewloadprocessterminated": return EventType.PaywallWebviewProcessTerminated; + default: return null; + } } private static SubscriptionStatus DeserializeSubscriptionStatus(Dictionary data) diff --git a/Runtime/Models/Enums.cs b/Runtime/Models/Enums.cs index 0d6a5d6..3d165a8 100644 --- a/Runtime/Models/Enums.cs +++ b/Runtime/Models/Enums.cs @@ -277,7 +277,22 @@ public enum EventType ReviewRequested, PermissionRequested, PermissionGranted, - PermissionDenied + PermissionDenied, + // Names both native SDKs emit (SuperwallKit 4.16 / superwall-android 2.7) that had no member here, + // so they were reported with a warning and left at the default. Appended, never reordered: the + // enum is serialized by name, not by value, but consumers may still persist the ordinal. + PaywallPageView, + TestModeModalOpen, + TestModeModalClose, + CelExpressionResult, + ErrorThrown, + ReviewGranted, + ReviewDenied, + AttributionMatch, + StripeCheckoutStart, + StripeCheckoutSubmit, + StripeCheckoutComplete, + StripeCheckoutFail } [Serializable]