diff --git a/purchasely/lib/purchasely_flutter.dart b/purchasely/lib/purchasely_flutter.dart index 8c7e0445..aa864f26 100644 --- a/purchasely/lib/purchasely_flutter.dart +++ b/purchasely/lib/purchasely_flutter.dart @@ -1327,7 +1327,13 @@ class PLYEventPropertyRedemptionPurchaseContext { /// /// Every field is optional: the SDK omits a key it has no value for. class PLYEventPropertyRedemption { - /// The redemption link token this event reports on. + /// The redemption link token this event reports on — the **raw bearer + /// credential**, not a redacted form. + /// + /// If you forward Purchasely events to a third-party analytics stack, exclude + /// this field. `DEEPLINK_OPENED` redacts the token to a 6-character prefix and + /// [PLYWebRedemptionResult.errorMessage] never contains it, but this field + /// does. String? token; PLYEventPropertyRedemptionReceipt? receipt; List? subscriptions; diff --git a/purchasely/lib/src/purchasely_builder.dart b/purchasely/lib/src/purchasely_builder.dart index 8e7697b8..cec3975d 100644 --- a/purchasely/lib/src/purchasely_builder.dart +++ b/purchasely/lib/src/purchasely_builder.dart @@ -132,6 +132,11 @@ class PurchaselyBuilder { /// **`override: true` splits the user history.** The backend keeps every /// event and every purchase under the previous id. Use it only when the app /// owns the anonymous identity, e.g. after a cross-device restore. + /// + /// **Compare an anonymous user id case-insensitively.** An id passed here is + /// stored uppercase on both platforms, but an id the SDK *generates* is not + /// consistent across them — measured on 6.1.0: uppercase on iOS, lowercase on + /// Android. Never `==` a stored id against [Purchasely.anonymousUserId]. PurchaselyBuilder anonymousUserId(String id, {bool override = false}) { _anonymousUserId = id; _anonymousUserIdOverride = override; diff --git a/sdk_public_doc.md b/sdk_public_doc.md index 1175d0a4..3d1eae77 100644 --- a/sdk_public_doc.md +++ b/sdk_public_doc.md @@ -190,6 +190,13 @@ holds no anonymous id yet. Pass `override: true` to replace an existing id: every purchase under the previous id. Use `true` only when your app owns the anonymous identity, for example after a cross-device restore. +> **Compare an anonymous user id case-insensitively.** An id you pass here is +> stored uppercase on both platforms. An id the SDK generates itself is *not +> consistent across platforms* — measured on 6.1.0: uppercase on iOS +> (`1933C4CC-…`), lowercase on Android (`ff394b92-…`). So never compare a stored +> id to `Purchasely.anonymousUserId` with `==`; use +> `a.toLowerCase() == b.toLowerCase()`. + ### API proxy (6.1.0) Route Purchasely API traffic through a proxy instead of `api.purchasely.io`, for @@ -315,11 +322,42 @@ Three behaviours to know: event drops the hint on both platforms, so the listener is the only place it appears. +#### A successful redemption also restores the web purchase's user attributes + +The built-in and custom user attributes attached to the web purchase are applied +**before** the entitlements refresh, so every later event and every audience +already sees them. Read them with the getters you already use +(`Purchasely.userAttribute`, `Purchasely.userAttributes`) or observe them through +`Purchasely.setUserAttributeListener`. + +`PLYEventPropertyRedemptionPurchaseContext.built_in_attributes` and +`.custom_attributes` report what the SDK actually applied, not the raw response. + +#### ⚠️ The redemption token IS exposed on the analytics events — do not forward it + +`PLYEventProperties.redemption.token` carries the **raw bearer token** on both +`REDEMPTION_CONSUMED` and `REDEMPTION_FAILED`. If you forward Purchasely events +to a third-party analytics stack, **exclude that field**, or the credential leaves +the device with your telemetry. + +The other channels do not carry it, verified against the 6.1.0 native sources: + +| Channel | Token | +|---|---| +| `PLYEventProperties.redemption.token` | **raw token** — exclude it from telemetry | +| `DEEPLINK_OPENED`'s `deeplink_identifier` | redacted to a 6-character prefix | +| `PLYWebRedemptionResult.errorMessage` | never contains it | +| SDK logs | never contains it | + The SDK also emits two analytics events for a redemption, `PLYEventName.REDEMPTION_CONSUMED` and `PLYEventName.REDEMPTION_FAILED`, with their payload on `PLYEventProperties.redemption`. Read them with `Purchasely.addEventListener`. +`REDEMPTION_CONSUMED` also fires when the user taps an **already redeemed** link: +a replay is a success. `properties.redemption?.purchase_context?.replay` tells a +first redemption from a repeat. + --- ## Displaying Presentations