@revturbine/sdk 0.11.5 → 0.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17171,11 +17171,14 @@ interface RevTurbineTreatmentInteractionInput {
17171
17171
  * `PlacementOutput.rule_id` — projected onto the `placement_interaction`
17172
17172
  * clickstream payload as `rule_handle` (BL-0182).
17173
17173
  *
17174
- * Unlike its neighbours this does NOT reach the wire
17175
- * `TreatmentInteractionInputSchema` / `placement_presentations`: that path is
17176
- * column-shaped and would need a datasource migration. The clickstream
17177
- * carries payload fields inside a JSON column, so the rule slice for
17178
- * interactions is readable the moment producers send it.
17174
+ * It reaches the wire `TreatmentInteractionInputSchema` too, as of BL-0200.
17175
+ * It deliberately did not under BL-0182: that path is column-shaped and the
17176
+ * rule would have needed a datasource migration, while the clickstream
17177
+ * carries payload fields inside a JSON column and was readable at once.
17178
+ * `placement_exposure_attribution.rule_handle` exists now (web ledger 015),
17179
+ * so the base exposure row records the rule the exposure was DECIDED by
17180
+ * rather than waiting for attribution to reconstruct one when a conversion
17181
+ * lands — which is what makes presentation-grain rule cuts servable.
17179
17182
  *
17180
17183
  * Omitted, never sent empty, when no decision is in scope — a bare
17181
17184
  * `trackTreatmentInteraction` caller supplies no rule, and absence is what
@@ -21147,6 +21150,12 @@ declare class RevTurbineCustomerSdk {
21147
21150
  * redundant segment re-evaluation.
21148
21151
  */
21149
21152
  private lastServerContextHash?;
21153
+ /**
21154
+ * The plan move an optimistic `convert()` made ahead of the server (BL-0004),
21155
+ * held only until a `client-context` read stops reporting the old plan. See
21156
+ * {@link dropStalePostConversionPlan}.
21157
+ */
21158
+ private pendingConversionPlan?;
21150
21159
  private usageBalances;
21151
21160
  private localDecisionsByPlacementId;
21152
21161
  private localPlacementsByLookupKey;
@@ -22291,12 +22300,16 @@ declare class RevTurbineCustomerSdk {
22291
22300
  /** Map the client-safe context response into a UserContext patch (plan 157). */
22292
22301
  private mapClientSafeContext;
22293
22302
  /**
22294
- * Apply a server-derived UserContext patch (plan 157) — mirrors setUserContext
22295
- * but accepts a partial. The patch's RevTurbine-authoritative fields (trial,
22296
- * billing signals) overlay the held context; undefined fields are skipped by
22297
- * the merge, so app-set values are never nulled.
22303
+ * Apply a UserContext patch — mirrors setUserContext but accepts a partial.
22304
+ * RevTurbine-authoritative fields (trial, billing signals, server plan)
22305
+ * overlay the held context; undefined fields are skipped by the merge, so
22306
+ * app-set values are never nulled.
22307
+ *
22308
+ * Two callers: the server client-context fetch (plan 157) and the optimistic
22309
+ * post-conversion plan move (BL-0004). Both notify subscribers, which is what
22310
+ * makes mounted slots and gates re-resolve.
22298
22311
  */
22299
- private applyServerContextPatch;
22312
+ private applyUserContextPatch;
22300
22313
  /**
22301
22314
  * Record that a placement was rendered to the user. Plan 43 TASK-9.
22302
22315
  *
@@ -22473,13 +22486,73 @@ declare class RevTurbineCustomerSdk {
22473
22486
  */
22474
22487
  snooze(outputId: string, seconds?: number): Promise<void>;
22475
22488
  /**
22476
- * Record a confirmed CTA completion and retire the decision's placement
22477
- * permanently — future calls to `getPlacementDecision` for it will not
22478
- * return it again.
22489
+ * Record a confirmed CTA completion, then reload the UserContext so every
22490
+ * mounted surface re-decides against the plan the user just converted to.
22491
+ *
22492
+ * Conversion is **not** a suppression: it writes no cooldown and no permanent
22493
+ * retirement (spec §"interaction" table; Kent's plan 254 D-9 ruling). What
22494
+ * makes a converted upgrade banner leave the screen is the user no longer
22495
+ * matching it — so a conversion that did not refresh the context left the
22496
+ * placement mounted until something else happened to re-resolve it. Kent,
22497
+ * 2026-09-25 (BL-0004): *"Converting should trigger a reloading of UserContext
22498
+ * with the new plan/billing state the user has converted to."*
22499
+ *
22500
+ * Ordering, and why it is this way round:
22501
+ *
22502
+ * 1. The interaction is recorded (analytics + attribution), as before.
22503
+ * 2. **Optimistically**, locally: when the converted output's CTA names a
22504
+ * target plan (`cta_path.plan_handle`), the held context moves to that plan
22505
+ * immediately. Billing truth arrives by Stripe webhook, so
22506
+ * `GET /api/sdk/client-context` can still report the OLD plan for seconds
22507
+ * after a successful checkout — waiting for it would leave the upgrade
22508
+ * prompt on screen exactly when the user has just paid.
22509
+ * 3. Subscribers are notified either way — a conversion whose CTA names no
22510
+ * plan still re-decides mounted slots and re-checks mounted gates, which is
22511
+ * what turns a plan change the host applied itself into a re-render.
22512
+ * 4. **Then** the server refresh (`fetchClientContext()`), which overlays the
22513
+ * RevTurbine-authoritative trial / billing-health / plan fields and
22514
+ * notifies again if it changed anything. It is best-effort and never
22515
+ * throws, so awaiting it cannot fail a conversion; with no client-session
22516
+ * minter configured it is a no-op and the optimistic state stands.
22517
+ *
22518
+ * Mounted `PlacementController`s re-decide and mounted `EntitlementGate`s
22519
+ * re-check through their `watchUserContext()` subscriptions — which the React
22520
+ * bindings attach for you. Re-resolution emits the normal lifecycle events
22521
+ * once per final decision; the transient states emit nothing.
22522
+ * `PlacementController.ctaComplete()` is unchanged and remains the
22523
+ * component-level path.
22479
22524
  *
22480
22525
  * @public
22481
22526
  */
22482
22527
  convert(outputId: string): Promise<void>;
22528
+ /**
22529
+ * The BL-0004 post-conversion context reload: optimistic local plan move,
22530
+ * then the server refresh. See {@link convert} for the ordering rationale.
22531
+ */
22532
+ private reloadUserContextAfterConversion;
22533
+ /**
22534
+ * Keep a stale server plan from undoing an optimistic conversion (BL-0004).
22535
+ *
22536
+ * Billing truth arrives by Stripe webhook, so a `client-context` read that
22537
+ * races the webhook reports the PRE-conversion plan — and the server plan
22538
+ * deliberately overlays the app-supplied one (plan 179 TASK-1), so applying
22539
+ * that read verbatim would put the upgrade prompt straight back on screen
22540
+ * seconds after the user paid.
22541
+ *
22542
+ * So while a conversion is pending, a server plan equal to the plan converted
22543
+ * AWAY from is dropped — only the plan fields, never the trial or billing-health
22544
+ * signals, which are authoritative regardless. Any other value (the new plan,
22545
+ * or a third one the user was moved to out-of-band) clears the pending state
22546
+ * and applies, so this can suppress at most the one value it was created for
22547
+ * and cannot outlive the lag it exists to cover.
22548
+ */
22549
+ private dropStalePostConversionPlan;
22550
+ /**
22551
+ * The display name the loaded Playbook gives a plan handle, else the handle.
22552
+ * Keeps an optimistic plan move from putting a raw handle in front of a user
22553
+ * when the Playbook already knows the plan's name.
22554
+ */
22555
+ private planNameForHandle;
22483
22556
  /**
22484
22557
  * Emit a canonical trigger event. There is no in-SDK consumer that matches
22485
22558
  * on trigger names — the shipped behavior is ingest to `events_clickstream`
@@ -22943,12 +23016,41 @@ declare class PlacementController {
22943
23016
  private _awaitingConfig;
22944
23017
  private _configRetries;
22945
23018
  private _playbookUnsubscribe;
23019
+ private _userContextUnsubscribe;
22946
23020
  constructor(sdk: RevTurbineCustomerSdk, options: PlacementControllerOptions);
22947
23021
  /**
22948
- * Stop listening for Playbook loads. Idempotent; call it when the owning
22949
- * component unmounts (`usePlacement` does).
23022
+ * Stop listening for Playbook loads and user-context changes. Idempotent; call
23023
+ * it when the owning component unmounts (`usePlacement` does).
22950
23024
  */
22951
23025
  dispose(): void;
23026
+ /**
23027
+ * Drop only the Playbook-load subscription — the within-load-cycle teardown
23028
+ * (BL-0177). A mounted controller keeps watching the user context (BL-0004).
23029
+ */
23030
+ private unsubscribePlaybook;
23031
+ /**
23032
+ * Re-decide whenever the user context changes (BL-0004).
23033
+ *
23034
+ * The counterpart to {@link EntitlementGate.watchUserContext}, and the reason
23035
+ * `sdk.convert()` now removes a converted placement in place: conversion writes
23036
+ * no suppression — eligibility is a function of the user's CURRENT plan and
23037
+ * targeting — so the only thing that can take a converted upgrade banner off
23038
+ * screen is a re-decide against the reloaded context. Without this the
23039
+ * placement stayed mounted until something else happened to re-resolve it, and
23040
+ * the host had to call `refresh()` itself or use the component's
23041
+ * `ctaComplete()` (the limitation plan 236 TASK-13 recorded).
23042
+ *
23043
+ * Only re-decides a controller that has already resolved something: before the
23044
+ * first `load()` there is nothing on screen to be stale, and firing then would
23045
+ * turn every startup `identify()` into a duplicate decision.
23046
+ *
23047
+ * Calls `load()` rather than `refresh()` on purpose — `refresh()` clears
23048
+ * impression/exposure tracking, so a context change would re-credit a
23049
+ * presentation the user never saw again.
23050
+ *
23051
+ * Returns an unsubscribe function; {@link dispose} also drops it.
23052
+ */
23053
+ watchUserContext(): () => void;
22952
23054
  /**
22953
23055
  * Is this decision a race against a Playbook load rather than a verdict?
22954
23056
  *
@@ -23166,12 +23268,18 @@ declare class EntitlementGate {
23166
23268
  private _awaitedResult;
23167
23269
  private _configRetries;
23168
23270
  private _playbookUnsubscribe;
23271
+ private _userContextUnsubscribe;
23169
23272
  constructor(sdk: RevTurbineCustomerSdk, options: EntitlementGateOptions);
23170
23273
  /**
23171
- * Stop listening for Playbook loads. Idempotent; call it when the owning
23172
- * component unmounts (`useEntitlement` does).
23274
+ * Stop listening for Playbook loads and user-context changes. Idempotent; call
23275
+ * it when the owning component unmounts (`useEntitlement` does).
23173
23276
  */
23174
23277
  dispose(): void;
23278
+ /**
23279
+ * Drop only the Playbook-load subscription — the within-check-cycle teardown
23280
+ * (BL-0179). A mounted gate keeps watching the user context (BL-0004).
23281
+ */
23282
+ private unsubscribePlaybook;
23175
23283
  /** Current state snapshot. */
23176
23284
  get state(): EntitlementGateState;
23177
23285
  /** Convenience: `true` when the entitlement is allowed. */
@@ -23271,8 +23379,13 @@ declare class EntitlementGate {
23271
23379
  * `check()` there is nothing on screen to be stale, and firing then would
23272
23380
  * turn every `identify()` at startup into a redundant evaluation.
23273
23381
  *
23382
+ * `sdk.convert()` reloads the UserContext (BL-0004), so this is also what
23383
+ * flips a mounted `useCan` gate to allowed after a conversion.
23384
+ *
23274
23385
  * Returns an unsubscribe function. Call it when the gate is discarded —
23275
23386
  * without that, a gate outlives its consumer and keeps re-checking.
23387
+ * Idempotent: a second call returns the first subscription rather than
23388
+ * orphaning it, and {@link dispose} drops it too.
23276
23389
  */
23277
23390
  watchUserContext(): () => void;
23278
23391
  private notify;