Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to this package will be documented in this file.
## [Unreleased]

### Fixes
* Both native bridges serialized only 4 of the 13 fields of an `Entitlement` (`id`, a hardcoded `type`, `isActive`, `productIds`), so `LatestProductId`, `Store`, `StartsAt`, `RenewedAt`, `ExpiresAt`, `IsLifetime`, `WillRenew`, `State` and `OfferType` were always null on the C# side even though `DeserializeEntitlement` reads them. A consumer could not tell a trial from a paid subscription, nor read an expiry or renewal state, from the entitlement. Every field is now emitted on iOS and Android with one shared vocabulary (enum case names matching the C# enums, dates as epoch milliseconds).
* `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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import com.superwall.sdk.identity.setUserAttributes
import com.superwall.sdk.logger.LogLevel
import com.superwall.sdk.logger.LogScope
import com.superwall.sdk.models.entitlements.Entitlement
import com.superwall.sdk.models.product.Store
import com.superwall.sdk.store.abstractions.product.receipt.LatestPeriodType
import com.superwall.sdk.store.abstractions.product.receipt.LatestSubscriptionState
import com.superwall.sdk.models.entitlements.SubscriptionStatus
import com.superwall.sdk.models.triggers.Experiment
import com.superwall.sdk.models.customer.CustomerInfo
Expand Down Expand Up @@ -692,11 +695,61 @@ class SuperwallUnityBridge {
})
}

// Mirrors every field of the C# `Entitlement` model (see Runtime/Models/Entitlement.cs), which
// `BridgeCallbackHandler.DeserializeEntitlement` already reads. Enum values are emitted under the
// C# member names (parsed case-insensitively), so the wire shape matches the iOS bridge and a
// consumer sees one vocabulary. Dates are epoch milliseconds. Optional fields are omitted rather
// than sent as null so the C# nullable stays null.
private fun serializeEntitlement(e: Entitlement) = JSONObject().apply {
put("id", e.id)
put("type", "serviceLevel")
put("type", serializeEntitlementType(e.type))
put("isActive", e.isActive)
put("productIds", JSONArray(e.productIds.toList()))
e.latestProductId?.let { put("latestProductId", it) }
e.store?.let { put("store", serializeStore(it)) }
e.startsAt?.let { put("startsAt", it.time) }
e.renewedAt?.let { put("renewedAt", it.time) }
e.expiresAt?.let { put("expiresAt", it.time) }
e.isLifetime?.let { put("isLifetime", it) }
e.willRenew?.let { put("willRenew", it) }
serializeSubscriptionState(e.state)?.let { put("state", it) }
serializeOfferType(e.offerType)?.let { put("offerType", it) }
}

private fun serializeEntitlementType(type: Entitlement.Type) = when (type) {
Entitlement.Type.SERVICE_LEVEL -> "serviceLevel"
}

private fun serializeStore(store: Store) = when (store) {
Store.PLAY_STORE -> "playStore"
Store.APP_STORE -> "appStore"
Store.STRIPE -> "stripe"
Store.PADDLE -> "paddle"
Store.SUPERWALL -> "superwall"
Store.OTHER -> "other"
}

// `UNKNOWN` has no C# counterpart and is dropped, leaving `Entitlement.State` null.
private fun serializeSubscriptionState(state: LatestSubscriptionState?) = when (state) {
LatestSubscriptionState.GRACE_PERIOD -> "inGracePeriod"
LatestSubscriptionState.SUBSCRIBED -> "subscribed"
LatestSubscriptionState.EXPIRED -> "expired"
LatestSubscriptionState.BILLING_RETRY -> "inBillingRetryPeriod"
LatestSubscriptionState.REVOKED -> "revoked"
LatestSubscriptionState.UNKNOWN, null -> null
}

// The C# `LatestSubscriptionOfferType` has Trial, Code, Promotional and Winback. The remaining
// Android values are sent through under their own names; the C# parser leaves `OfferType` null
// for them rather than mislabelling a paid period as an offer.
private fun serializeOfferType(type: LatestPeriodType?) = when (type) {
LatestPeriodType.TRIAL -> "trial"
LatestPeriodType.CODE -> "code"
LatestPeriodType.PROMOTIONAL -> "promotional"
LatestPeriodType.WINBACK -> "winback"
LatestPeriodType.SUBSCRIPTION -> "subscription"
LatestPeriodType.REVOKED -> "revoked"
null -> null
}

private fun serializeEntitlementSet(set: Set<Entitlement>) = JSONArray().apply {
Expand Down
42 changes: 40 additions & 2 deletions Plugins/iOS/SuperwallUnityBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,51 @@ private func serializeExperiment(_ experiment: Experiment?) -> [String: Any]? {
]
}

private func epochMilliseconds(_ date: Date) -> Int64 {
return Int64((date.timeIntervalSince1970 * 1000).rounded())
}

// Mirrors every field of the C# `Entitlement` model (see Runtime/Models/Entitlement.cs), which
// `BridgeCallbackHandler.DeserializeEntitlement` already reads. Enum values are emitted as their
// case names, which the C# side parses case-insensitively (`playStore` -> `ProductStore.PlayStore`,
// `inGracePeriod` -> `LatestSubscriptionState.InGracePeriod`, `trial` ->
// `LatestSubscriptionOfferType.Trial`). Dates are epoch milliseconds. Optional fields are omitted
// rather than sent as null so the C# nullable stays null.
private func serializeEntitlement(_ entitlement: Entitlement) -> [String: Any] {
return [
var dict: [String: Any] = [
"id": entitlement.id,
"type": "serviceLevel",
"type": String(describing: entitlement.type),
"isActive": entitlement.isActive,
"productIds": Array(entitlement.productIds)
]
if let latestProductId = entitlement.latestProductId {
dict["latestProductId"] = latestProductId
}
if let store = entitlement.store {
dict["store"] = String(describing: store)
}
if let startsAt = entitlement.startsAt {
dict["startsAt"] = epochMilliseconds(startsAt)
}
if let renewedAt = entitlement.renewedAt {
dict["renewedAt"] = epochMilliseconds(renewedAt)
}
if let expiresAt = entitlement.expiresAt {
dict["expiresAt"] = epochMilliseconds(expiresAt)
}
if let isLifetime = entitlement.isLifetime {
dict["isLifetime"] = isLifetime
}
if let willRenew = entitlement.willRenew {
dict["willRenew"] = willRenew
}
if let state = entitlement.state {
dict["state"] = state.rawValue
}
if let offerType = entitlement.offerType {
dict["offerType"] = offerType.rawValue
}
return dict
}

private func serializeEntitlements(_ entitlements: Set<Entitlement>) -> [[String: Any]] {
Expand Down
Loading