react-native-revnix 1.5.0 → 1.7.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 (49) hide show
  1. package/README.md +16 -0
  2. package/android/build.gradle +3 -0
  3. package/android/consumer-rules.pro +1 -0
  4. package/android/src/main/kotlin/com/revnix/reactnative/RevnixStoreModule.kt +43 -0
  5. package/dist/blocks/locale.js +7 -0
  6. package/dist/blocks/selection.d.ts +7 -1
  7. package/dist/blocks/selection.js +9 -0
  8. package/dist/blocks/tags.d.ts +3 -0
  9. package/dist/blocks/tags.js +33 -0
  10. package/dist/blocks/types.d.ts +45 -3
  11. package/dist/blocks/types.js +1 -0
  12. package/dist/client.d.ts +50 -11
  13. package/dist/client.js +408 -119
  14. package/dist/esm/blocks/locale.js +7 -0
  15. package/dist/esm/blocks/selection.js +8 -0
  16. package/dist/esm/blocks/tags.js +31 -0
  17. package/dist/esm/blocks/types.js +1 -0
  18. package/dist/esm/client.js +407 -119
  19. package/dist/esm/store/index.js +7 -0
  20. package/dist/esm/ui/RevnixBlockPaywall.js +103 -22
  21. package/dist/esm/ui/RevnixPaywall.js +7 -0
  22. package/dist/esm/ui/index.js +1 -1
  23. package/dist/esm/version.js +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/store/index.d.ts +8 -0
  26. package/dist/store/index.js +10 -1
  27. package/dist/types.d.ts +32 -0
  28. package/dist/ui/RevnixBlockPaywall.js +100 -19
  29. package/dist/ui/RevnixPaywall.d.ts +6 -2
  30. package/dist/ui/RevnixPaywall.js +8 -0
  31. package/dist/ui/index.d.ts +1 -1
  32. package/dist/ui/index.js +2 -1
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/ios/RevnixStore.m +4 -0
  36. package/ios/RevnixStore.swift +29 -0
  37. package/package.json +1 -1
  38. package/src/blocks/locale.ts +7 -0
  39. package/src/blocks/selection.ts +13 -0
  40. package/src/blocks/tags.ts +38 -0
  41. package/src/blocks/types.ts +51 -2
  42. package/src/client.ts +478 -154
  43. package/src/index.ts +1 -0
  44. package/src/store/index.ts +15 -0
  45. package/src/types.ts +30 -0
  46. package/src/ui/RevnixBlockPaywall.tsx +186 -20
  47. package/src/ui/RevnixPaywall.tsx +15 -1
  48. package/src/ui/index.ts +1 -0
  49. package/src/version.ts +1 -1
package/README.md CHANGED
@@ -145,6 +145,22 @@ Learn more in [A/B tests](https://www.revnix.io/docs/ab-tests).
145
145
  - **MMP forwarding** to AppsFlyer and Adjust, **ad revenue** logging and **uninstall measurement**. See [MMP attribution](https://www.revnix.io/docs/mmp-attribution), [Ad revenue](https://www.revnix.io/docs/ad-revenue) and [Uninstall measurement](https://www.revnix.io/docs/uninstall-measurement).
146
146
  - **Fraud prevention**: anonymous-IP and click-injection checks keep paid channels honest. See [Fraud prevention](https://www.revnix.io/docs/fraud-prevention).
147
147
 
148
+ ### Device integrity
149
+
150
+ Pass `nativeDeviceIntegrity` so install reports carry Apple App Attest or Google Play Integrity evidence. With "Require device integrity" on in the dashboard, installs without it lose their attribution.
151
+
152
+ ```ts
153
+ import { nativeDeviceIntegrity } from "react-native-revnix/store";
154
+
155
+ Revnix.configure({
156
+ apiKey: "rvx_pk_live_…",
157
+ baseUrl: "https://<your-deployment>.convex.site",
158
+ deviceIntegrity: nativeDeviceIntegrity,
159
+ });
160
+ ```
161
+
162
+ It runs once per customer, waits at most 10 seconds and never blocks the report. Needs App Attest on iOS or Google Play services on Android; simulators and emulators send no evidence.
163
+
148
164
  ## Real-time analytics for your React Native app
149
165
 
150
166
  ![Revnix overview: active subscriptions, revenue and MRR over 90 days](https://www.revnix.io/sdk/react-native/analytics.png)
@@ -11,6 +11,7 @@ android {
11
11
 
12
12
  defaultConfig {
13
13
  minSdkVersion safeExtGet("minSdkVersion", 24)
14
+ consumerProguardFiles "consumer-rules.pro"
14
15
  }
15
16
 
16
17
  compileOptions {
@@ -31,4 +32,6 @@ repositories {
31
32
  dependencies {
32
33
  implementation "com.facebook.react:react-native:+"
33
34
  implementation "com.android.billingclient:billing:8.3.0"
35
+ implementation "com.google.android.play:integrity:1.6.0"
36
+ compileOnly "com.google.android.gms:play-services-ads-identifier:18.2.0"
34
37
  }
@@ -0,0 +1 @@
1
+ -dontwarn com.google.android.gms.ads.identifier.**
@@ -1,5 +1,6 @@
1
1
  package com.revnix.reactnative
2
2
 
3
+ import android.util.Base64
3
4
  import android.util.Log
4
5
  import com.android.billingclient.api.AcknowledgePurchaseParams
5
6
  import com.android.billingclient.api.BillingClient
@@ -21,6 +22,10 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule
21
22
  import com.facebook.react.bridge.ReactMethod
22
23
  import com.facebook.react.bridge.WritableMap
23
24
  import com.facebook.react.modules.core.DeviceEventManagerModule
25
+ import com.google.android.gms.ads.identifier.AdvertisingIdClient
26
+ import com.google.android.play.core.integrity.IntegrityManagerFactory
27
+ import com.google.android.play.core.integrity.IntegrityTokenRequest
28
+ import java.security.MessageDigest
24
29
  import java.util.concurrent.ConcurrentHashMap
25
30
  import java.util.concurrent.atomic.AtomicReference
26
31
 
@@ -115,6 +120,44 @@ class RevnixStoreModule(context: ReactApplicationContext) :
115
120
  }
116
121
  }
117
122
 
123
+ @ReactMethod
124
+ fun deviceIntegrity(customerId: String, promise: Promise) {
125
+ val nonce = Base64.encodeToString(
126
+ MessageDigest.getInstance("SHA-256").digest(customerId.toByteArray(Charsets.UTF_8)),
127
+ Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP,
128
+ )
129
+ try {
130
+ IntegrityManagerFactory.create(reactApplicationContext.applicationContext)
131
+ .requestIntegrityToken(IntegrityTokenRequest.builder().setNonce(nonce).build())
132
+ .addOnSuccessListener { response ->
133
+ promise.resolve(
134
+ Arguments.createMap().apply {
135
+ putString("platform", "android")
136
+ putString("token", response.token())
137
+ },
138
+ )
139
+ }
140
+ .addOnFailureListener { promise.resolve(null) }
141
+ .addOnCanceledListener { promise.resolve(null) }
142
+ } catch (err: Exception) {
143
+ promise.resolve(null)
144
+ }
145
+ }
146
+
147
+ @ReactMethod
148
+ fun advertisingId(promise: Promise) {
149
+ Thread {
150
+ try {
151
+ val info = AdvertisingIdClient.getAdvertisingIdInfo(reactApplicationContext.applicationContext)
152
+ promise.resolve(
153
+ info.id.takeUnless { info.isLimitAdTrackingEnabled || it == "00000000-0000-0000-0000-000000000000" },
154
+ )
155
+ } catch (err: Throwable) {
156
+ promise.reject("advertising_id_unavailable", err.message, err)
157
+ }
158
+ }.start()
159
+ }
160
+
118
161
  override fun onPurchasesUpdated(result: BillingResult, purchases: List<Purchase>?) {
119
162
  if (result.responseCode != BillingResponseCode.OK) {
120
163
  settle(pending.getAndSet(null) ?: return, result)
@@ -123,7 +123,14 @@ function localizeBlock(block, lookup) {
123
123
  case "text":
124
124
  return { ...block, text: at("text", block.text) };
125
125
  case "button":
126
+ case "toggle":
126
127
  return { ...block, label: at("label", block.label) };
128
+ case "timer":
129
+ return {
130
+ ...block,
131
+ format: opt("format", block.format),
132
+ expiredText: opt("expiredText", block.expiredText),
133
+ };
127
134
  case "list":
128
135
  return {
129
136
  ...block,
@@ -1,4 +1,4 @@
1
- import type { BlockStyle, PaywallBlock, PaywallBlockDoc } from "./types.js";
1
+ import type { BlockStyle, PaywallBlock, PaywallBlockDoc, ToggleBlock } from "./types.js";
2
2
  import type { RevnixPaywallPackage } from "../ui/RevnixPaywall.js";
3
3
  /** Where a block sits: inside the selected plan card, inside another plan
4
4
  * card, or outside any plan card (undefined). */
@@ -9,6 +9,12 @@ export type SelectionContext = "selected" | "unselected" | undefined;
9
9
  export declare function selectedPackageId(packages: ReadonlyArray<{
10
10
  packageId: string;
11
11
  }>, ...candidates: Array<string | undefined>): string | undefined;
12
+ export declare function toggleSelection(block: ToggleBlock, packages: ReadonlyArray<{
13
+ packageId: string;
14
+ }>, selectedId: string | undefined): {
15
+ on: boolean;
16
+ target: string;
17
+ } | null;
12
18
  /** The context a package-bearing card opens for itself and its subtree. */
13
19
  export declare function packageContext(pkg: {
14
20
  packageId: string;
@@ -9,6 +9,7 @@
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.CANVAS_MAX_WIDTH = void 0;
11
11
  exports.selectedPackageId = selectedPackageId;
12
+ exports.toggleSelection = toggleSelection;
12
13
  exports.packageContext = packageContext;
13
14
  exports.effectiveStyle = effectiveStyle;
14
15
  exports.isVisible = isVisible;
@@ -26,6 +27,14 @@ function selectedPackageId(packages, ...candidates) {
26
27
  }
27
28
  return packages[0]?.packageId;
28
29
  }
30
+ function toggleSelection(block, packages, selectedId) {
31
+ const on = packages[block.onPackageIndex];
32
+ const off = packages[block.offPackageIndex];
33
+ if (on === undefined || off === undefined)
34
+ return null;
35
+ const isOn = on.packageId === selectedId;
36
+ return { on: isOn, target: (isOn ? off : on).packageId };
37
+ }
29
38
  /** The context a package-bearing card opens for itself and its subtree. */
30
39
  function packageContext(pkg, selectedId) {
31
40
  if (pkg === undefined)
@@ -1,4 +1,5 @@
1
1
  import type { RevnixPaywallPackage } from "../ui/RevnixPaywall.js";
2
+ import type { TimerBlock } from "./types.js";
2
3
  /** Minor units per major unit. Not every currency is a hundredth: JPY and KRW
3
4
  * have no minor unit at all, so dividing by 100 would understate a price by
4
5
  * 100×. Intl knows each currency's exponent — ask it rather than assume.
@@ -10,3 +11,5 @@ export declare function minorUnitsForCurrency(currency: string): number;
10
11
  /** Fills a copy template from one package. `all` is the rest of the offering,
11
12
  * which {save_percent} needs to have something to compare against. */
12
13
  export declare function resolveTags(text: string, pkg: RevnixPaywallPackage | undefined, all?: RevnixPaywallPackage[]): string;
14
+ export declare function formatCountdown(format: string, remaining: number): string;
15
+ export declare function timerText(block: TimerBlock, shownAt: number, now: number): string | null;
@@ -13,6 +13,8 @@
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.minorUnitsForCurrency = minorUnitsForCurrency;
15
15
  exports.resolveTags = resolveTags;
16
+ exports.formatCountdown = formatCountdown;
17
+ exports.timerText = timerText;
16
18
  /** Renewal cycles, in months. Lifetime and one-time products have no cycle. */
17
19
  const MONTHS = {
18
20
  weekly: 1 / 4.345,
@@ -119,3 +121,34 @@ function resolveTags(text, pkg, all = []) {
119
121
  }
120
122
  });
121
123
  }
124
+ function formatCountdown(format, remaining) {
125
+ const pad = (n) => String(n).padStart(2, "0");
126
+ let span = Infinity;
127
+ const part = (token, unit) => {
128
+ const value = Math.floor((remaining % span) / unit);
129
+ if (format.includes(token))
130
+ span = unit;
131
+ return value;
132
+ };
133
+ const days = part("{d}", 86400);
134
+ const hours = part("{hh}", 3600);
135
+ const minutes = part("{mm}", 60);
136
+ const seconds = part("{ss}", 1);
137
+ return format
138
+ .replace(/\{d\}/g, String(days))
139
+ .replace(/\{hh\}/g, pad(hours))
140
+ .replace(/\{mm\}/g, pad(minutes))
141
+ .replace(/\{ss\}/g, pad(seconds));
142
+ }
143
+ function timerText(block, shownAt, now) {
144
+ const deadline = block.endsAt !== undefined
145
+ ? Date.parse(block.endsAt)
146
+ : shownAt + (block.duration ?? 0) * 1000;
147
+ const remaining = Number.isNaN(deadline)
148
+ ? 0
149
+ : Math.max(0, Math.floor((deadline - now) / 1000));
150
+ if (remaining > 0) {
151
+ return formatCountdown(block.format ?? "{hh}:{mm}:{ss}", remaining);
152
+ }
153
+ return block.expiredText ?? null;
154
+ }
@@ -229,13 +229,55 @@ export interface SpacerBlock {
229
229
  selectedStyle?: BlockStyle;
230
230
  visibility?: BlockVisibility;
231
231
  }
232
+ /** Countdown text. `endsAt` is one deadline for everyone; `duration` counts
233
+ * from the moment this paywall view opened. At zero it draws `expiredText`,
234
+ * or nothing when that is absent. Tokens in `format`: {d} {hh} {mm} {ss}. */
235
+ export interface TimerBlock {
236
+ id: string;
237
+ type: "timer";
238
+ endsAt?: string;
239
+ duration?: number;
240
+ format?: string;
241
+ expiredText?: string;
242
+ style?: BlockStyle;
243
+ selectedStyle?: BlockStyle;
244
+ visibility?: BlockVisibility;
245
+ }
246
+ /** A muted looping clip. React Native has no built-in player, so this SDK
247
+ * draws `poster` in its place, sized like an image block. */
248
+ export interface VideoBlock {
249
+ id: string;
250
+ type: "video";
251
+ url: string;
252
+ poster?: string;
253
+ fit?: "cover" | "contain";
254
+ style?: BlockStyle;
255
+ selectedStyle?: BlockStyle;
256
+ visibility?: BlockVisibility;
257
+ }
258
+ /** The free-trial switch: ON while package `onPackageIndex` is selected, and
259
+ * tapping it selects the other one. Hidden when the offering lacks either
260
+ * index. */
261
+ export interface ToggleBlock {
262
+ id: string;
263
+ type: "toggle";
264
+ label: string;
265
+ onPackageIndex: number;
266
+ offPackageIndex: number;
267
+ style?: BlockStyle;
268
+ selectedStyle?: BlockStyle;
269
+ visibility?: BlockVisibility;
270
+ }
232
271
  /** The one container block. `layout` picks how children are placed: column /
233
272
  * row are flex lines, `stack` layers them (children position with
234
- * style.inset or the edge offsets), and `grid` is an N-column grid. */
273
+ * style.inset or the edge offsets), `grid` is an N-column grid, and
274
+ * `carousel` pages through its children one swipe at a time. */
235
275
  export interface CardBlock {
236
276
  id: string;
237
277
  type: "card";
238
- layout?: "column" | "row" | "stack" | "grid";
278
+ layout?: "column" | "row" | "stack" | "grid" | "carousel";
279
+ /** carousel only: seconds per page before it moves on by itself. */
280
+ autoAdvance?: number;
239
281
  /** Renders this container once per package in the attached offering. Its
240
282
  * subtree reads the package through tag variables. */
241
283
  repeat?: "packages";
@@ -254,7 +296,7 @@ export interface CardBlock {
254
296
  selectedStyle?: BlockStyle;
255
297
  visibility?: BlockVisibility;
256
298
  }
257
- export type PaywallBlock = TextBlock | ImageBlock | ListBlock | ProductsBlock | ButtonBlock | LinksBlock | LineBlock | SpacerBlock | CardBlock;
299
+ export type PaywallBlock = TextBlock | ImageBlock | ListBlock | ProductsBlock | ButtonBlock | LinksBlock | LineBlock | SpacerBlock | TimerBlock | VideoBlock | ToggleBlock | CardBlock;
258
300
  /** The published document: screen palette plus the block tree. */
259
301
  export interface PaywallBlockDoc {
260
302
  version: number;
@@ -38,6 +38,7 @@ function hasCloseAction(blocks) {
38
38
  : b.type === "card"
39
39
  ? b.repeat === undefined &&
40
40
  b.packageIndex === undefined &&
41
+ b.layout !== "carousel" &&
41
42
  hasCloseAction(b.children)
42
43
  : (b.type === "text" || b.type === "image" || b.type === "button") &&
43
44
  b.action === "close");
package/dist/client.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { AdRevenue, AliasResult, CustomerEntitlements, IdentifyResult, IdentifyStrategy, IdentityInfo, LastDeepLink, MmpAttribution, PaywallInteraction, PlacementResolution, RegisterPurchaseInput, RegisterPurchaseResult, RevnixAttribution, RevnixConfig } from "./types.js";
2
+ export declare function paywallImageUrls(value: unknown): string[];
2
3
  export declare class RevnixClient {
3
4
  private readonly apiKey;
4
5
  private readonly baseUrl;
@@ -25,6 +26,8 @@ export declare class RevnixClient {
25
26
  private readonly onImplicitPaywall?;
26
27
  private readonly onDeferredDeepLink?;
27
28
  private readonly onAttribution?;
29
+ private readonly deviceIntegrity?;
30
+ private integrityRequest;
28
31
  private deferredDeepLinkClaimed;
29
32
  private landingUrl;
30
33
  private lastDeepLinkWrite;
@@ -56,6 +59,9 @@ export declare class RevnixClient {
56
59
  * /v1/installs uses, and independent of the device header (which an app
57
60
  * can turn off entirely). */
58
61
  private freshInstall;
62
+ private eventFlush;
63
+ private eventTimer;
64
+ private eventRetryAt;
59
65
  constructor(config: RevnixConfig);
60
66
  /** The id every call is made as: the stored identified/canonical id, or a
61
67
  * generated-and-persisted anonymous id (rvx_anon_…) on first launch. */
@@ -105,7 +111,8 @@ export declare class RevnixClient {
105
111
  private postPurchase;
106
112
  /** C7: retry purchases that failed to register and were persisted locally.
107
113
  * Runs automatically at construction; call again when connectivity returns.
108
- * Idempotent and safe to call repeatedly. */
114
+ * Idempotent and safe to call repeatedly. Purchases queued more than 30
115
+ * days ago are dropped with a diagnostic. */
109
116
  retryPendingPurchases(): Promise<void>;
110
117
  private freshEntitlements;
111
118
  private inflightEntitlements;
@@ -150,12 +157,22 @@ export declare class RevnixClient {
150
157
  * RevnixAuthError locally, before any network call, like identify(). */
151
158
  getIdentity(): Promise<IdentityInfo>;
152
159
  /** Resolve a named in-app location to the currently published offering —
153
- * the one-line paywall call. REV-157: reads the network first and caches
154
- * the result; if that read FAILS (offline / incident) it falls back to the
155
- * last cached resolution with `stale: true`, so the paywall renders the
156
- * last published version rather than not at all. Throws only when there
157
- * is nothing cached. */
160
+ * the one-line paywall call. Served instantly from a cache under 24 h old
161
+ * that was fetched for this customer since the last `setAttributes` or
162
+ * purchase, refreshed in the background. Otherwise reads the network and
163
+ * caches the result; if that read FAILS (offline / incident) it falls back
164
+ * to the last cached resolution with `stale: true`, so the paywall renders
165
+ * the last published version rather than not at all. Throws only when
166
+ * there is nothing cached. A deliberate rejection seen by the background
167
+ * refresh drops the cache, so the next resolve throws it. */
158
168
  resolvePlacement(placementKey: string): Promise<PlacementResolution>;
169
+ /** Fetch these placements now so the next `resolvePlacement` answers from
170
+ * the cache. Never throws; failures report to `onDiagnostic`. Paywall
171
+ * images are warmed by `prefetchPaywallImages` from
172
+ * `react-native-revnix/ui`. */
173
+ preload(placementKeys: string[]): Promise<void>;
174
+ private refreshPlacement;
175
+ private fetchPlacement;
159
176
  /** REV-157: the last resolution cached for `placementKey`, with NO network
160
177
  * call — for an instant, offline-safe paywall at launch. Always
161
178
  * `stale: true`; follow with resolvePlacement() to revalidate. Null if
@@ -207,7 +224,8 @@ export declare class RevnixClient {
207
224
  * - `restore` — restore purchases was tapped
208
225
  * - `error` — the paywall itself failed (config, products, render)
209
226
  *
210
- * Fire-and-forget like the other beacons: never blocks, never throws.
227
+ * Queued and sent in batches (see `flushEvents`): resolves once queued,
228
+ * never throws.
211
229
  *
212
230
  * `viewId` is the id `logPaywallShown` returned for THIS display. Passing
213
231
  * it is what threads the whole life of one impression together and puts
@@ -233,15 +251,28 @@ export declare class RevnixClient {
233
251
  message?: string;
234
252
  /** Idempotency key for this occurrence; defaults to `viewId`. */
235
253
  eventId?: string;
236
- }): void;
254
+ }): Promise<void>;
237
255
  /** Reports one ad impression's payout (POST /v1/ad-revenue).
238
256
  * Fire-and-forget like the other beacons: never blocks, never throws. */
239
257
  logAdRevenue(input: AdRevenue): void;
240
258
  /** Reports one custom in-app event (POST /v1/events), not for purchases.
241
- * Fire-and-forget like the other beacons: never blocks, never throws. */
259
+ * Queued and sent in batches (see `flushEvents`): resolves once queued,
260
+ * never throws. */
242
261
  track(event: string, properties?: Record<string, string | number | boolean>, options?: {
243
262
  eventId?: string;
244
- }): void;
263
+ }): Promise<void>;
264
+ /** Send every queued `track` and paywall interaction event now. Events are
265
+ * otherwise sent in batches: at 20 queued, 10 s after the first, when the
266
+ * app goes to the background, and on the next launch. Never throws; a
267
+ * transient failure keeps the events and holds further sends for 30 s. */
268
+ flushEvents(): Promise<void>;
269
+ private readEvents;
270
+ private mutateEvents;
271
+ private enqueueEvent;
272
+ private armEventTimer;
273
+ private startEventFlush;
274
+ private runEventFlush;
275
+ private removeEvents;
245
276
  /** PT11: forward an MMP's attribution callback (Adjust, AppsFlyer, …) so
246
277
  * Revnix credits revenue to the right network/campaign. */
247
278
  setAttribution(attribution: MmpAttribution): void;
@@ -250,6 +281,7 @@ export declare class RevnixClient {
250
281
  * report the token dead. iOS/Android only (no-op on web),
251
282
  * fire-and-forget. */
252
283
  setPushToken(token: string): void;
284
+ private reportAdvertisingId;
253
285
  /** REV-272: the two moments that happen ON a paywall, with the loop guard
254
286
  * applied. `fromPaywallId` travels so the server can refuse to hand back
255
287
  * the very paywall being dismissed — the backstop for a host that reports
@@ -361,6 +393,7 @@ export declare class RevnixClient {
361
393
  * cold start to ONE paywall. Never throws past its caller's catch. */
362
394
  private triggerImplicit;
363
395
  private consumePreviousSessionMs;
396
+ private subscribeLifecycle;
364
397
  /** REV-272: the launch-time moments, plus the foreground listener that
365
398
  * `session_start` is built on.
366
399
  *
@@ -386,6 +419,7 @@ export declare class RevnixClient {
386
419
  /** Fire-and-forget install report (REV-093). Never blocks startup and
387
420
  * never throws — the backend absorbs retries idempotently per id. */
388
421
  private reportInstall;
422
+ private integrityFor;
389
423
  /** REV-268: assemble and encode the device facts once. Runtime-detected
390
424
  * facts first, the app's `device` over them, then the three the SDK owns.
391
425
  * `installedAt` is the first launch this storage ever saw — written on
@@ -405,7 +439,12 @@ export declare class RevnixClient {
405
439
  private readPlacementMap;
406
440
  private readPlacementCache;
407
441
  private writePlacementCache;
408
- private queueLock;
442
+ private deletePlacementCache;
443
+ private mutatePlacements;
444
+ private placementsInvalidatedAt;
445
+ private invalidatePlacements;
446
+ private storageLock;
447
+ private withStorageLock;
409
448
  private mutateQueue;
410
449
  private readPurchaseQueue;
411
450
  private writePurchaseQueue;