react-native-revnix 1.4.0

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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +766 -0
  3. package/dist/blocks/background.d.ts +101 -0
  4. package/dist/blocks/background.js +165 -0
  5. package/dist/blocks/color.d.ts +88 -0
  6. package/dist/blocks/color.js +329 -0
  7. package/dist/blocks/locale.d.ts +42 -0
  8. package/dist/blocks/locale.js +263 -0
  9. package/dist/blocks/selection.d.ts +49 -0
  10. package/dist/blocks/selection.js +109 -0
  11. package/dist/blocks/style.d.ts +93 -0
  12. package/dist/blocks/style.js +365 -0
  13. package/dist/blocks/tags.d.ts +12 -0
  14. package/dist/blocks/tags.js +121 -0
  15. package/dist/blocks/types.d.ts +301 -0
  16. package/dist/blocks/types.js +44 -0
  17. package/dist/client.d.ts +402 -0
  18. package/dist/client.js +1653 -0
  19. package/dist/device.d.ts +29 -0
  20. package/dist/device.js +137 -0
  21. package/dist/errors.d.ts +39 -0
  22. package/dist/errors.js +68 -0
  23. package/dist/esm/blocks/background.js +160 -0
  24. package/dist/esm/blocks/color.js +318 -0
  25. package/dist/esm/blocks/locale.js +255 -0
  26. package/dist/esm/blocks/selection.js +100 -0
  27. package/dist/esm/blocks/style.js +356 -0
  28. package/dist/esm/blocks/tags.js +117 -0
  29. package/dist/esm/blocks/types.js +40 -0
  30. package/dist/esm/client.js +1649 -0
  31. package/dist/esm/device.js +130 -0
  32. package/dist/esm/errors.js +59 -0
  33. package/dist/esm/iap.js +101 -0
  34. package/dist/esm/implicit.js +63 -0
  35. package/dist/esm/index.js +29 -0
  36. package/dist/esm/package.json +1 -0
  37. package/dist/esm/storage.js +78 -0
  38. package/dist/esm/types.js +4 -0
  39. package/dist/esm/ui/RevnixBlockPaywall.js +706 -0
  40. package/dist/esm/ui/RevnixPaywall.js +890 -0
  41. package/dist/esm/ui/index.js +6 -0
  42. package/dist/esm/version.js +4 -0
  43. package/dist/iap.d.ts +20 -0
  44. package/dist/iap.js +104 -0
  45. package/dist/implicit.d.ts +69 -0
  46. package/dist/implicit.js +67 -0
  47. package/dist/index.d.ts +18 -0
  48. package/dist/index.js +50 -0
  49. package/dist/storage.d.ts +14 -0
  50. package/dist/storage.js +85 -0
  51. package/dist/types.d.ts +463 -0
  52. package/dist/types.js +5 -0
  53. package/dist/ui/RevnixBlockPaywall.d.ts +96 -0
  54. package/dist/ui/RevnixBlockPaywall.js +742 -0
  55. package/dist/ui/RevnixPaywall.d.ts +148 -0
  56. package/dist/ui/RevnixPaywall.js +927 -0
  57. package/dist/ui/index.d.ts +7 -0
  58. package/dist/ui/index.js +25 -0
  59. package/dist/version.d.ts +1 -0
  60. package/dist/version.js +7 -0
  61. package/package.json +79 -0
  62. package/src/blocks/background.ts +232 -0
  63. package/src/blocks/color.ts +352 -0
  64. package/src/blocks/locale.ts +271 -0
  65. package/src/blocks/paywall-background-wire.json +12 -0
  66. package/src/blocks/paywall-selection-wire.json +149 -0
  67. package/src/blocks/selection.ts +161 -0
  68. package/src/blocks/style.ts +411 -0
  69. package/src/blocks/tags.ts +125 -0
  70. package/src/blocks/types.ts +352 -0
  71. package/src/client.ts +1993 -0
  72. package/src/device.ts +146 -0
  73. package/src/errors.ts +69 -0
  74. package/src/iap.ts +137 -0
  75. package/src/implicit.ts +129 -0
  76. package/src/index.ts +82 -0
  77. package/src/storage.ts +92 -0
  78. package/src/types.ts +528 -0
  79. package/src/ui/RevnixBlockPaywall.tsx +1423 -0
  80. package/src/ui/RevnixPaywall.tsx +1542 -0
  81. package/src/ui/index.ts +43 -0
  82. package/src/version.ts +4 -0
  83. package/ui.d.ts +1 -0
  84. package/ui.js +5 -0
package/src/types.ts ADDED
@@ -0,0 +1,528 @@
1
+ // Response and input types for the Revnix /v1 API. These mirror the shapes
2
+ // returned by convex/http.ts in revnix-app — the backend is the source of
3
+ // truth; update here when the API surface changes.
4
+
5
+ import type {
6
+ ImplicitTrigger,
7
+ ImplicitTriggerResponse,
8
+ RevnixLifecycle,
9
+ } from "./implicit.js";
10
+
11
+ export type Environment = "sandbox" | "production";
12
+ export type Store = "apple" | "google";
13
+
14
+ export interface RevnixConfig {
15
+ /** API key issued from the Revnix dashboard — publishable (rvx_pk_live_… /
16
+ * rvx_pk_test_…, safe on-device) or secret (rvx_sk_…, server-side only;
17
+ * required for identify/alias). Legacy rvx_live_…/rvx_test_… keys have
18
+ * full access. The key fixes both the app and the environment server-side. */
19
+ apiKey: string;
20
+ /** Convex site URL of the Revnix deployment, e.g. https://xyz.convex.site */
21
+ baseUrl: string;
22
+ /** CTO-SCALE-REVIEW M9: retry delays for the read-your-writes poll after
23
+ * registerPurchase (entitlements re-fetched until the read's cursor
24
+ * reflects the purchase). Pass [] to disable. Default [250,500,1000,2000]. */
25
+ readYourWritesDelaysMs?: number[];
26
+ /** Persistent key-value storage for the customer id. Pass an adapter over
27
+ * MMKV/AsyncStorage in React Native; defaults to in-memory (ids reset on
28
+ * every app launch — fine for tests, wrong for production). */
29
+ storage?: RevnixStorage;
30
+ /** Start as this customer instead of generating/loading an anonymous id. */
31
+ customerId?: string;
32
+ /** Custom fetch implementation (tests, polyfills). Defaults to global fetch. */
33
+ fetch?: typeof fetch;
34
+ /** Report an install (POST /v1/installs) when a fresh anonymous id is
35
+ * generated — powers the dashboard's Installs / install-conversion charts.
36
+ * Fire-and-forget; never blocks startup. Defaults to true. */
37
+ reportInstalls?: boolean;
38
+ /** Optional device context attached to the install report. Superseded by
39
+ * `device` (REV-268), which feeds the install report too; kept so
40
+ * existing configurations keep working. */
41
+ installMetadata?: { platform?: string; appVersion?: string };
42
+ /** REV-268: facts about the device, sent with every placement resolve so
43
+ * targeting rules ("US storefront", "app version at least 3") can be
44
+ * evaluated on the request that serves the paywall, and stored on the
45
+ * customer as reserved `device.*` attributes visible in the dashboard.
46
+ *
47
+ * In a browser the SDK detects platform, OS version and locale itself;
48
+ * everything else — and everything, under React Native — comes from
49
+ * here (react-native-device-info has all of it). Fields you leave out are
50
+ * simply not reported. Pass `false` to send nothing at all. */
51
+ device?: DeviceFacts | false;
52
+ /** C7: per-request timeout in ms (AbortController-based). Defaults to 10000.
53
+ * A slow or dead backend then fails fast with RevnixTimeoutError instead of
54
+ * hanging the app until the OS TCP timeout (60–120s+). */
55
+ requestTimeoutMs?: number;
56
+ /** REV-157: placement keys to resolve-and-cache at construction (fire and
57
+ * forget, never blocks startup), so the session's first paywall can render
58
+ * from cache even when the device is offline by the time it is shown. */
59
+ prefetchPlacements?: string[];
60
+ /** REV-198 (M23): how old a cached entitlements snapshot may be and still
61
+ * grant access when served offline. Beyond this (or when the device clock
62
+ * sits BEFORE the snapshot was written — a rollback), cached entitlements
63
+ * are served inactive. Default 14 days. */
64
+ offlineMaxCacheAgeMs?: number;
65
+ /** REV-199 (M21): soft TTL on entitlement reads — a fresh network read
66
+ * younger than this answers getEntitlements()/isEntitled() from memory,
67
+ * so gating several screens costs one request, not one each. Pass 0 for
68
+ * the old always-fetch behavior. Default 30000. */
69
+ entitlementsTtlMs?: number;
70
+ /** REV-200 (M26): called whenever a background operation fails — purchase
71
+ * retries, placement prefetch, install/paywall reporting, storage
72
+ * read/writes. These were all silent (`catch(() => {})`), so a fleet
73
+ * failing BEFORE the server (DNS block, broken storage adapter) produced
74
+ * no signal anywhere. Wire this to your crash/analytics reporter. Must
75
+ * not throw; exceptions from it are swallowed. */
76
+ onDiagnostic?: (event: RevnixDiagnostic) => void;
77
+ /** REV-272: called when one of the six implicit moments resolved to a
78
+ * paywall — an app launch, a session start, a deep link, a dismissed
79
+ * paywall, an abandoned checkout, or the install itself. Present it however
80
+ * your app presents paywalls; the SDK deliberately does not present for
81
+ * you, because it does not own your navigation stack and a paywall pushed
82
+ * over a splash screen is worse than no paywall.
83
+ *
84
+ * Providing this handler is what TURNS IMPLICIT PLACEMENTS ON. Without it
85
+ * the SDK makes no extra requests at all — there would be nothing to do
86
+ * with the answer. With it, the SDK asks GET /v1/config once at
87
+ * construction and then fires only for the moments this app has actually
88
+ * configured in the dashboard, so an app that configured none still costs
89
+ * one cached response per launch. The one exception is `handleDeepLink`:
90
+ * it always reports the URL it is handed, handler or not, because the
91
+ * server files that link's attribution facts from the report itself.
92
+ *
93
+ * ⚠️ Pass `trigger.resolution.placementKey` to `logPaywallShown` for the
94
+ * display you present. That is what tells the SDK this display came FROM an
95
+ * implicit trigger, and it is the only thing that stops a `paywall_decline`
96
+ * paywall from firing `paywall_decline` again when the customer dismisses
97
+ * it — a loop with no way out but force-quitting. The server refuses to
98
+ * serve back the very same paywall as a backstop, but it cannot see a rule
99
+ * that points at a DIFFERENT paywall which points back.
100
+ *
101
+ * Never throws into your app: an exception from this handler is reported
102
+ * through `onDiagnostic` and swallowed. */
103
+ onImplicitPaywall?: (trigger: ImplicitTrigger<ImplicitResolution>) => void;
104
+ /** REV-272: explicit off switch for implicit placements, even when
105
+ * `onImplicitPaywall` is set. Defaults to whether that handler is present.
106
+ * Does not stop `handleDeepLink` from reporting the link it is handed —
107
+ * that report still lands the link's attribution facts on the customer,
108
+ * it just never presents a paywall for an ordinary deep link. A dashboard
109
+ * preview link is the exception: it always presents. */
110
+ implicitPlacements?: boolean;
111
+ /** REV-299: the link the customer clicked before they had the app.
112
+ * `POST /v1/installs` returns it whenever the install matched an ad click
113
+ * (Android: exact, from the Play Install Referrer; iOS: probabilistic IP
114
+ * match) — on Android that can be every referrer call. The SDK delivers it
115
+ * to this handler at most once per install. Route it yourself — you may
116
+ * also pass it to `handleDeepLink` for `deeplink_open` paywall rules.
117
+ * Never throws into your app: an exception from this handler is reported
118
+ * through `onDiagnostic` and swallowed. */
119
+ onDeferredDeepLink?: (
120
+ url: string,
121
+ match: "exact" | "probabilistic"
122
+ ) => void;
123
+ /** REV-272: how the SDK learns the app came to the foreground, which is what
124
+ * `session_start` is built on. Defaults to a `visibilitychange` listener in
125
+ * a browser and to nothing under React Native — pass an adapter over
126
+ * `AppState` there (see RevnixLifecycle). `false` disables foreground
127
+ * detection; launch-time moments still fire. */
128
+ lifecycle?: RevnixLifecycle | false;
129
+ /** REV-272: how long the app must have been backgrounded for the return to
130
+ * count as a new session rather than an app switch. Default 30 minutes. */
131
+ sessionTimeoutMs?: number;
132
+ /** AT11: called whenever the install-attribution verdict changes — a fresh
133
+ * Play referrer, an ASA token resolving, or a re-attribution genuinely
134
+ * changes the answer, so this can fire more than once across a session
135
+ * (Adjust's `attributionChanged` semantics). Fired after the cold-start
136
+ * install report resolves, and again after `handleInstallReferrer` or
137
+ * `handleAttributionToken` bring in a late signal. Call `getAttribution()`
138
+ * for the same answer on demand.
139
+ * Never throws into your app: an exception from this handler is reported
140
+ * through `onDiagnostic` and swallowed. */
141
+ onAttribution?: (attribution: RevnixAttribution) => void;
142
+ }
143
+
144
+ /** AT11: the install-attribution verdict — GET
145
+ * /v1/customers/{id}/attribution. `installMatch: "unknown"` (no install
146
+ * recorded yet, a normal cold-start race) is never sent to hosts as such;
147
+ * `getAttribution()` and `onAttribution` both surface it as `null` instead,
148
+ * so there is one "no attribution yet" representation, not two. */
149
+ export interface RevnixAttribution {
150
+ installMatch: "referrer" | "click" | "impression" | "organic";
151
+ /** Unix ms. */
152
+ attributedAt: number;
153
+ /** Unix ms — set when the customer was later re-attributed. */
154
+ reattributedAt?: number;
155
+ linkToken?: string;
156
+ referrerSource?: string;
157
+ matchSignals?: string[];
158
+ source?: string;
159
+ medium?: string;
160
+ campaign?: string;
161
+ term?: string;
162
+ content?: string;
163
+ }
164
+
165
+ /** REV-272: what an implicit trigger resolves to — an ordinary placement
166
+ * resolution plus why it carries no paywall, when it does not. */
167
+ export type ImplicitResolution = PlacementResolution & ImplicitTriggerResponse;
168
+
169
+ /** REV-268: the app-suppliable half of the device attribute contract. The
170
+ * SDK adds `sdkVersion`, `installedAt` and `firstOpen` itself. Every field
171
+ * is optional; the server normalizes case, locale separators and
172
+ * alpha-3 storefronts, so pass what the platform gives you. */
173
+ export interface DeviceFacts {
174
+ /** "ios", "android", "web", … — lowercase family name. */
175
+ platform?: string;
176
+ /** e.g. "18.1". */
177
+ osVersion?: string;
178
+ /** The host app's version string, e.g. "1.2.10". The server derives a
179
+ * zero-padded sortable form (`device.appVersionPadded`) from it. */
180
+ appVersion?: string;
181
+ /** "en-US" or "en_US" — either separator. */
182
+ locale?: string;
183
+ /** ISO 4217, e.g. "USD". */
184
+ currency?: string;
185
+ /** App Store / Play storefront country — alpha-2 ("US") or alpha-3 ("USA"). */
186
+ storefront?: string;
187
+ /** Hardware model identifier, e.g. "iPhone15,3". */
188
+ model?: string;
189
+ /** True in a sandbox / debug build. */
190
+ sandbox?: boolean;
191
+ }
192
+
193
+ /** REV-200 (M26): one background failure. */
194
+ export interface RevnixDiagnostic {
195
+ /** Which background operation failed, e.g. "retryPendingPurchases",
196
+ * "prefetchPlacement", "reportInstall", "logPaywallShown",
197
+ * "waitForEntitlements", "storageWrite", "storageRead". */
198
+ op: string;
199
+ error: unknown;
200
+ /** Unix ms. */
201
+ at: number;
202
+ }
203
+
204
+ export interface RevnixStorage {
205
+ getItem(key: string): string | null | Promise<string | null>;
206
+ setItem(key: string, value: string): void | Promise<void>;
207
+ }
208
+
209
+ // ——— POST /v1/purchases ———
210
+
211
+ export interface RegisterPurchaseInput {
212
+ source: Store;
213
+ /** Apple: originalTransactionId · Google: purchaseToken. */
214
+ token: string;
215
+ productId: string;
216
+ transactionId: string;
217
+ /** Unix ms. Defaults server-side to registration time. */
218
+ occurredAt?: number;
219
+ /** Unix ms subscription expiry, when the client knows it. */
220
+ expiresAt?: number;
221
+ /** Raw store payload to retain in the ledger for reprocessing. */
222
+ rawPayload?: unknown;
223
+ /** iOS only — the JWS StoreKit 2 hands you on every transaction
224
+ * (`VerificationResult.jwsRepresentation`). Send it and the purchase is
225
+ * proven: Revnix verifies it against Apple's certificate chain, checks it
226
+ * names this exact transaction, and records a full purchase.
227
+ *
228
+ * Without it the purchase is accepted but PROVISIONAL — a publishable key
229
+ * ships inside your app binary, so an unproven claim is only an assertion.
230
+ * A provisional purchase still grants the entitlement, for a short window,
231
+ * and is promoted automatically when Apple's or Google's own notification
232
+ * confirms the charge. Always send it if you can; apps that enable
233
+ * "require store-verified purchases" reject claims that lack it. */
234
+ signedTransactionInfo?: string;
235
+ /** REV-261: which paywall sold it. Pass the `viewId` `logPaywallShown`
236
+ * returned for the display the customer bought from and the server
237
+ * resolves it to that view's paywall and placement, so per-paywall
238
+ * analytics credit the purchase (and every renewal on it) exactly.
239
+ * Without it the server attributes the purchase to the customer's last
240
+ * paywall view inside the previous 24 hours. */
241
+ viewId?: string;
242
+ /** Explicit paywall / placement; each wins over the view's when sent. */
243
+ paywallId?: string;
244
+ placementKey?: string;
245
+ }
246
+
247
+ export interface RegisterPurchaseResult {
248
+ eventId: string;
249
+ /** Ledger position of the write — read-your-writes is assertable (ADR 0004). */
250
+ seq: number;
251
+ /** True when the store webhook already recorded this purchase. */
252
+ duplicate: boolean;
253
+ /** The id THIS caller should use going forward — its own canonical id.
254
+ * Differs from the id you sent only when identity resolution merged it
255
+ * away, so following it is always safe. */
256
+ customerId: string;
257
+ /** True when the store token belongs to a different customer than the
258
+ * caller: the purchase was recorded against them, not you. Safe for every
259
+ * caller — it reports the outcome without naming anyone. */
260
+ ownedByOtherCustomer?: boolean;
261
+ /** Which customer holds the token, when it is not you. Present only for
262
+ * secret-key callers — a publishable key learns the outcome above but
263
+ * never another customer's id. */
264
+ tokenOwner?: string;
265
+ /** True when the token was reassigned TO this caller. */
266
+ transferred: boolean;
267
+ /** Set when an identity side-effect (a transfer, or absorbing an imported
268
+ * history) was refused because the claim carried no proof. Send
269
+ * `signedTransactionInfo`, or register from your server with a secret
270
+ * key. */
271
+ refused?: "unverified-claim";
272
+ /** True when this transaction was already in the ledger — a StoreKit
273
+ * replay or restore, not a new sale. */
274
+ restored?: boolean;
275
+ /** True when the purchase was recorded without store proof: the entitlement
276
+ * is live but time-boxed until the store confirms the charge. Send
277
+ * `signedTransactionInfo` (iOS) to avoid it. */
278
+ provisional?: boolean;
279
+ }
280
+
281
+ // ——— GET /v1/customers/{id}/entitlements ———
282
+
283
+ export interface EntitlementSource {
284
+ kind: "subscription" | "grant";
285
+ key: string;
286
+ isActive: boolean;
287
+ expiresAt?: number;
288
+ }
289
+
290
+ export interface Entitlement {
291
+ entitlementId: string;
292
+ isActive: boolean;
293
+ expiresAt?: number;
294
+ sources: EntitlementSource[];
295
+ }
296
+
297
+ export interface CustomerEntitlements {
298
+ customerId: string;
299
+ /** Ledger cursor this read reflects. */
300
+ cursor: number;
301
+ entitlements: Entitlement[];
302
+ /** C7 (SDK-populated): true when this was served from the local cache
303
+ * because the live read failed (offline / incident) — a paying customer is
304
+ * shown as subscribed from cache rather than as unsubscribed. false on a
305
+ * fresh server read. */
306
+ stale?: boolean;
307
+ /** C7 (SDK-populated): unix ms these entitlements were fetched/cached. */
308
+ fetchedAt?: number;
309
+ }
310
+
311
+ // ——— POST /v1/identify · POST /v1/customers/{id}/alias ———
312
+
313
+ export type IdentifyStrategy = "merge" | "block";
314
+
315
+ export interface IdentifyResult {
316
+ canonicalId: string;
317
+ merged: boolean;
318
+ }
319
+
320
+ export interface AliasResult {
321
+ canonicalId: string;
322
+ aliased: boolean;
323
+ }
324
+
325
+ // ——— GET /v1/customers/{id}/identity ———
326
+
327
+ export interface IdentityInfo {
328
+ canonicalId: string;
329
+ isAlias: boolean;
330
+ anonymous: boolean;
331
+ linkedIds?: string[];
332
+ }
333
+
334
+ export interface LastDeepLink {
335
+ url: string;
336
+ receivedAt: number;
337
+ }
338
+
339
+ // ——— GET /v1/placements/{key}/offering ———
340
+
341
+ export interface PlacementPackage {
342
+ packageId: string;
343
+ productId: string;
344
+ metadata?: Record<string, unknown>;
345
+ product: PlacementProduct | null;
346
+ }
347
+
348
+ export interface PlacementProduct {
349
+ productId: string;
350
+ displayName: string;
351
+ entitlementIds: string[];
352
+ prices?: Array<{
353
+ store: Store | "stripe";
354
+ currency: string;
355
+ amountMinor: number;
356
+ }>;
357
+ metadata?: Record<string, unknown>;
358
+ }
359
+
360
+ export interface PlacementOffering {
361
+ offeringId: string;
362
+ displayName: string;
363
+ metadata?: Record<string, unknown>;
364
+ packages: PlacementPackage[];
365
+ }
366
+
367
+ // REV-028: remote paywall design attached to a placement. Render contract —
368
+ // the app draws this with its own components; prices still come from the
369
+ // store (StoreKit / Play Billing) so the display never disagrees with the
370
+ // charge.
371
+ export interface PaywallConfig {
372
+ /** Layout — the screen structure RevnixPaywall renders. The dashboard's
373
+ * template gallery is presets over these layouts; the first three are the
374
+ * original templates and render exactly as before. */
375
+ template:
376
+ | "focus"
377
+ | "feature-list"
378
+ | "minimal"
379
+ | "hero"
380
+ | "timeline"
381
+ | "plans"
382
+ | "feature-grid"
383
+ | "offer"
384
+ | "reveal";
385
+ /** Color scheme the paywall renders in. Absent (legacy config) = dark.
386
+ * The host app's explicit `theme` prop still overrides either scheme. */
387
+ mode?: "dark" | "light";
388
+ headline: string;
389
+ subheadline?: string;
390
+ features: Array<{ icon?: string; title: string; description?: string }>;
391
+ ctaLabel: string;
392
+ /** packageId of the visually highlighted package. */
393
+ highlightPackageId?: string;
394
+ /** Badge on the highlighted package, e.g. "SAVE 17%". */
395
+ badgeText?: string;
396
+ /** Accent hex like "#6478ff"; fall back to the app theme when absent. */
397
+ accent?: string;
398
+ /** Hero image URL rendered above the headline in place of the icon tile. */
399
+ heroImageUrl?: string;
400
+ /** Social proof, dashboard-configured. Any layout renders the pieces that
401
+ * are set: stars/quote card above the packages, `count` under the CTA. */
402
+ review?: {
403
+ /** 0–5; rendered as a star row. */
404
+ rating?: number;
405
+ quote?: string;
406
+ author?: string;
407
+ /** e.g. "Join 2M+ users" — small line under the CTA. */
408
+ count?: string;
409
+ };
410
+ /** Win-back/offer presentation: anchor price struck through on the
411
+ * highlighted package, urgency line above the CTA. Any layout. */
412
+ offer?: {
413
+ strikethroughPrice?: string;
414
+ urgencyText?: string;
415
+ };
416
+ /** Footer links, dashboard-configured. Absent (legacy config) = show all
417
+ * three. When a URL is set the SDK opens it directly; otherwise the host
418
+ * app's onTerms/onPrivacy handler runs. */
419
+ footer?: {
420
+ showRestore: boolean;
421
+ showTerms: boolean;
422
+ showPrivacy: boolean;
423
+ termsUrl?: string;
424
+ privacyUrl?: string;
425
+ };
426
+ /** A designed paywall: the block tree the dashboard's builder authored.
427
+ * When present RevnixPaywall renders THIS and the fields above act as the
428
+ * fallback for clients that predate block rendering — so an older app
429
+ * keeps showing a sane classic screen instead of nothing. Typed as the
430
+ * block document in "revnix-react/ui"; the core entrypoint keeps it opaque
431
+ * so non-UI consumers pull in no renderer types. */
432
+ blocks?: unknown;
433
+ }
434
+
435
+ export interface PlacementPaywall {
436
+ paywallId: string;
437
+ name: string;
438
+ config: PaywallConfig;
439
+ }
440
+
441
+ export interface PlacementResolution {
442
+ status: "ok";
443
+ placementKey: string;
444
+ revision: number;
445
+ placementMetadata?: Record<string, unknown>;
446
+ offering: PlacementOffering | null;
447
+ paywall: PlacementPaywall | null;
448
+ /** REV-219: the A/B test serving this resolution — set when the customer
449
+ * is enrolled in an experiment on this placement (the offering/paywall
450
+ * above are the assigned variant's). Null when no experiment applies.
451
+ * Assignment is sticky per customer, survives identity merges, and the
452
+ * enrollment is recorded as an `experiment.enrolled` ledger event. */
453
+ experiment: { key: string; variantId: string } | null;
454
+ /** REV-157 (SDK-populated): true when this was served from the local cache
455
+ * because the live read failed (offline / incident) — the paywall renders
456
+ * the last published resolution rather than not at all. false on a fresh
457
+ * server read. */
458
+ stale?: boolean;
459
+ /** REV-157 (SDK-populated): unix ms this resolution was fetched/cached. */
460
+ fetchedAt?: number;
461
+ /** Set only on a dashboard preview resolution, never on a real resolve. */
462
+ preview?: true;
463
+ }
464
+
465
+ /** REV-263: the six paywall interactions `logPaywallEvent` can report — what
466
+ * the customer did on a display, between the view that opened it and the
467
+ * close or purchase that ended it. The server turns each into the ledger
468
+ * type `paywall.<name>`. */
469
+ export type PaywallInteraction =
470
+ | "selected"
471
+ | "purchase_started"
472
+ | "purchase_abandoned"
473
+ | "purchase_failed"
474
+ | "restore"
475
+ | "error";
476
+
477
+ /** What the host tells RevnixPaywall about a purchase it performed (REV-263).
478
+ * Returning it from `onPurchase` is what lets the paywall report
479
+ * `purchase_abandoned` / `purchase_failed` itself — without it the SDK sees
480
+ * only that checkout STARTED, because the store call happens in the host.
481
+ *
482
+ * `completed` needs no report of its own: the purchase lands as
483
+ * `purchase.completed` through registerPurchase. */
484
+ export interface PaywallPurchaseOutcome {
485
+ status: "completed" | "cancelled" | "failed";
486
+ /** Store error code — a StoreKit `SKError` code, a Play
487
+ * `BillingResponseCode`, or whatever your layer calls it. */
488
+ code?: string;
489
+ message?: string;
490
+ }
491
+
492
+ /** Input for `logAdRevenue` — one ad impression's payout. */
493
+ export interface AdRevenue {
494
+ /** Major units, e.g. 0.0032. Must be > 0. */
495
+ revenue: number;
496
+ /** ISO 4217, e.g. "USD". */
497
+ currency: string;
498
+ /** Monetization network that paid, e.g. "admob". */
499
+ network?: string;
500
+ /** Mediation platform that reported it, e.g. "applovin_max". */
501
+ mediation?: string;
502
+ adUnit?: string;
503
+ placement?: string;
504
+ /** "banner" | "interstitial" | "rewarded" | … free text. */
505
+ format?: string;
506
+ /** Idempotency key for this impression; omit to let the server generate one. */
507
+ eventId?: string;
508
+ }
509
+
510
+ /** MMP that produced an attribution callback. */
511
+ export type MmpProvider =
512
+ | "adjust"
513
+ | "appsflyer"
514
+ | "singular"
515
+ | "branch"
516
+ | "kochava"
517
+ | "tenjin"
518
+ | "airbridge";
519
+
520
+ /** Input for `setAttribution` — the network/campaign an MMP's attribution
521
+ * callback (Adjust, AppsFlyer, …) resolved for this install. */
522
+ export interface MmpAttribution {
523
+ provider: MmpProvider;
524
+ network: string;
525
+ campaign?: string;
526
+ adGroup?: string;
527
+ creative?: string;
528
+ }