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
@@ -0,0 +1,6 @@
1
+ export { RevnixPaywall, DEFAULT_PAYWALL_THEME, LIGHT_PAYWALL_THEME, } from "./RevnixPaywall.js";
2
+ export { RevnixBlockPaywall, } from "./RevnixBlockPaywall.js";
3
+ export { DESIGN_CANVAS, } from "../blocks/types.js";
4
+ export { backgroundBaseColor, backgroundLayers, coverPlacement, } from "../blocks/background.js";
5
+ export { detectLocale, linkLabels, localeChain, localizeBlockDoc, normalizeLocale, setLocale, } from "../blocks/locale.js";
6
+ export { minorUnitsForCurrency, resolveTags } from "../blocks/tags.js";
@@ -0,0 +1,4 @@
1
+ // L3: the single source of the SDK version inside src/. Must match
2
+ // package.json "version" — version.test.ts fails the build when they drift
3
+ // (they had: client.ts said 0.7.0 while the package shipped 0.9.0).
4
+ export const SDK_VERSION = "1.4.0";
package/dist/iap.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { RegisterPurchaseInput, Store } from "./types.js";
2
+ export interface IapPurchase {
3
+ productId: string;
4
+ transactionId?: string | null;
5
+ /** Unix ms. */
6
+ transactionDate?: number | null;
7
+ /** Android purchase token. react-native-iap v13+ also sets it on iOS,
8
+ * where it is the StoreKit 2 JWS — not the token Revnix needs, but the
9
+ * proof it wants (`signedTransactionInfo`), so the Apple branch picks it
10
+ * up from here when it has the JWS shape. */
11
+ purchaseToken?: string | null;
12
+ /** StoreKit 2 JWS (`VerificationResult.jwsRepresentation`), for IAP
13
+ * libraries that expose it under its own name. */
14
+ jwsRepresentation?: string | null;
15
+ originalTransactionIdentifierIOS?: string | null;
16
+ }
17
+ export declare function purchaseInputFromIap(purchase: IapPurchase, source: Store, extras?: {
18
+ expiresAt?: number;
19
+ rawPayload?: unknown;
20
+ }): RegisterPurchaseInput;
package/dist/iap.js ADDED
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.purchaseInputFromIap = purchaseInputFromIap;
4
+ // A JWS is three dot-separated base64url segments. Older react-native-iap
5
+ // versions put other receipt shapes in purchaseToken on iOS — shape-check so
6
+ // only a real JWS is sent as proof (the server rejects a malformed one).
7
+ const JWS_SHAPE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
8
+ function jwsFromIap(purchase) {
9
+ for (const candidate of [purchase.jwsRepresentation, purchase.purchaseToken]) {
10
+ if (candidate && JWS_SHAPE.test(candidate))
11
+ return candidate;
12
+ }
13
+ return undefined;
14
+ }
15
+ // REV-200 (CTO-R2 M25): base64url → string without atob (not available on
16
+ // every React Native runtime). The ids read from the payload are ASCII;
17
+ // multi-byte content is never consulted.
18
+ const B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
19
+ function base64UrlDecode(segment) {
20
+ const b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
21
+ let out = "";
22
+ let buffer = 0;
23
+ let bits = 0;
24
+ for (const ch of b64) {
25
+ const idx = B64_ALPHABET.indexOf(ch);
26
+ if (idx === -1)
27
+ continue; // padding / stray characters
28
+ buffer = (buffer << 6) | idx;
29
+ bits += 6;
30
+ if (bits >= 8) {
31
+ bits -= 8;
32
+ out += String.fromCharCode((buffer >> bits) & 0xff);
33
+ }
34
+ }
35
+ return out;
36
+ }
37
+ // The JWS payload's own ids — read WITHOUT verifying the signature, which is
38
+ // fine here: the server verifies the chain and only trusts the proof when the
39
+ // verified payload's ids match what we send. Reading them locally is exactly
40
+ // how we make them match.
41
+ function jwsPayload(jws) {
42
+ try {
43
+ const segment = jws.split(".")[1];
44
+ if (!segment)
45
+ return undefined;
46
+ const parsed = JSON.parse(base64UrlDecode(segment));
47
+ return typeof parsed === "object" && parsed !== null
48
+ ? parsed
49
+ : undefined;
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ }
55
+ function purchaseInputFromIap(purchase, source, extras) {
56
+ if (source === "apple") {
57
+ const transactionId = purchase.transactionId;
58
+ if (!transactionId) {
59
+ throw new Error("Revnix: Apple purchase is missing transactionId");
60
+ }
61
+ const jws = jwsFromIap(purchase);
62
+ const proof = jws ? jwsPayload(jws) : undefined;
63
+ return {
64
+ source,
65
+ // REV-200 (M25): token precedence. The old fallback (original id
66
+ // missing → the renewal's own transactionId) minted a purchaseKey the
67
+ // store webhook would never produce — a second purchase.completed and
68
+ // double-counted revenue — AND failed the server's proof check, which
69
+ // requires the verified JWS's originalTransactionId to EQUAL this
70
+ // token, silently downgrading a proof-carrying purchase to
71
+ // provisional. The JWS payload names the true original id, so when a
72
+ // JWS is present it wins; the IAP library's field is next; the bare
73
+ // transactionId remains only for the proof-less first purchase, where
74
+ // original === transaction anyway.
75
+ token: proof?.originalTransactionId ??
76
+ purchase.originalTransactionIdentifierIOS ??
77
+ transactionId,
78
+ productId: purchase.productId,
79
+ transactionId,
80
+ occurredAt: purchase.transactionDate ?? undefined,
81
+ expiresAt: extras?.expiresAt,
82
+ rawPayload: extras?.rawPayload,
83
+ // Without this the purchase lands PROVISIONAL (see
84
+ // RegisterPurchaseInput.signedTransactionInfo).
85
+ signedTransactionInfo: jws,
86
+ };
87
+ }
88
+ const token = purchase.purchaseToken;
89
+ if (!token) {
90
+ throw new Error("Revnix: Google purchase is missing purchaseToken");
91
+ }
92
+ return {
93
+ source,
94
+ token,
95
+ productId: purchase.productId,
96
+ // The Google webhook adapter (convex/lib/google.ts) reports the
97
+ // purchaseToken as the transactionId — send the same, never the Play
98
+ // orderId, or client-register and webhook won't share a purchaseKey.
99
+ transactionId: token,
100
+ occurredAt: purchase.transactionDate ?? undefined,
101
+ expiresAt: extras?.expiresAt,
102
+ rawPayload: extras?.rawPayload,
103
+ };
104
+ }
@@ -0,0 +1,69 @@
1
+ /** The six reserved keys. Lowercase throughout: a placement key must match
2
+ * `^[a-z0-9][a-z0-9._-]{0,63}$` server-side, which is why this is
3
+ * `deeplink_open` and not Superwall's `deepLink_open`. */
4
+ export declare const IMPLICIT_PLACEMENTS: readonly ["app_install", "app_launch", "session_start", "deeplink_open", "paywall_decline", "transaction_abandon"];
5
+ export type ImplicitPlacement = (typeof IMPLICIT_PLACEMENTS)[number];
6
+ /** Why a trigger came back with no paywall — as opposed to a placement that
7
+ * resolved fine and simply has none attached, which is `null`. */
8
+ export type ImplicitSkipReason = "not_configured" | "same_paywall";
9
+ /** The one moment the SDK cannot see unaided: the URL that opened the app is
10
+ * delivered to the host's own entry point, and an SDK trying to intercept it
11
+ * would be fighting the host's router. Call `handleDeepLink(url)`. */
12
+ export declare const HOST_CALL_PLACEMENT: ImplicitPlacement;
13
+ /** The placementKey `handleDeepLink` reports for a dashboard QR/link preview
14
+ * (`<scheme>://revnix-preview?revnix_preview=<token>`) — not one of the six
15
+ * above, and never sent to /v1/placements/triggered. */
16
+ export declare const PREVIEW_PLACEMENT_KEY = "revnix_preview";
17
+ /** How long the app must have been backgrounded for the return to count as a
18
+ * new session rather than an app switch. Matches Superwall's own definition
19
+ * so a team moving over gets the same numbers. */
20
+ export declare const DEFAULT_SESSION_TIMEOUT_MS: number;
21
+ /** How long after a FAILED /v1/config read the SDK waits before asking again.
22
+ * See RevnixClient.implicitConfigRetryAt. */
23
+ export declare const IMPLICIT_CONFIG_RETRY_MS = 60000;
24
+ /** Where the app is. Both transitions matter: a session is defined by how
25
+ * long the app was in the BACKGROUND, which cannot be known from foreground
26
+ * events alone — "time since the last return" would mint a session after 35
27
+ * minutes of continuous use plus a three-second app switch. */
28
+ export type RevnixAppState = "foreground" | "background";
29
+ /** The platform seam. The SDK ships a browser implementation (see
30
+ * `browserLifecycle`); React Native passes an adapter over `AppState`, which
31
+ * is a few lines and keeps `react-native` out of this package's dependencies:
32
+ *
33
+ * lifecycle: {
34
+ * onStateChange: (handler) => {
35
+ * const sub = AppState.addEventListener("change", (s) => {
36
+ * handler(s === "active" ? "foreground" : "background");
37
+ * });
38
+ * return () => sub.remove();
39
+ * },
40
+ * }
41
+ */
42
+ export interface RevnixLifecycle {
43
+ /** Subscribe to app state transitions. Returns an unsubscribe. Called at
44
+ * most once per client; the client calls the unsubscribe from dispose().
45
+ * Repeated reports of the same state are harmless. */
46
+ onStateChange(handler: (state: RevnixAppState) => void): () => void;
47
+ }
48
+ /** Foreground detection in a browser tab. `visibilitychange` rather than
49
+ * `focus`: switching windows fires focus without the page ever having been
50
+ * hidden, which would mint sessions for someone alt-tabbing. Returns null
51
+ * where there is no document (React Native, Node, SSR) — the host passes its
52
+ * own adapter there. */
53
+ export declare function browserLifecycle(): RevnixLifecycle | null;
54
+ /** What the host is handed when a moment resolved to a paywall. */
55
+ export interface ImplicitTrigger<TResolution> {
56
+ /** Which of the six fired, or `PREVIEW_PLACEMENT_KEY` for a dashboard preview. */
57
+ placement: ImplicitPlacement | typeof PREVIEW_PLACEMENT_KEY;
58
+ /** The resolution, exactly as `resolvePlacement` would have returned it —
59
+ * offering, paywall, experiment and targeting all included. */
60
+ resolution: TResolution;
61
+ }
62
+ /** The trigger response body. The resolve shape plus the two fields the
63
+ * trigger route adds. */
64
+ export interface ImplicitTriggerResponse {
65
+ skipReason: ImplicitSkipReason | null;
66
+ /** True when this call appended a NEW ledger event (false for a duplicate,
67
+ * and for the three moments that reuse an event reported elsewhere). */
68
+ recorded: boolean;
69
+ }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ // REV-272: implicit placements — the six moments the SDK reports on its own.
3
+ //
4
+ // A placement is normally a location the HOST resolves by name, so every new
5
+ // place a paywall could appear costs a code change and a release. Six moments
6
+ // are the same in every app and visible to the SDK without the host saying
7
+ // anything, so an operator can attach a paywall to them from the dashboard
8
+ // alone. This module owns the vocabulary and the platform-lifecycle seam;
9
+ // client.ts owns the firing.
10
+ //
11
+ // Mirrors convex/lib/implicitPlacements.ts in revnix-app — the backend is the
12
+ // source of truth for the key spellings, and a mismatch here is silent (the
13
+ // server 400s a key it does not know, and the moment is simply never
14
+ // configured), so the two lists are checked against each other in the tests.
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.IMPLICIT_CONFIG_RETRY_MS = exports.DEFAULT_SESSION_TIMEOUT_MS = exports.PREVIEW_PLACEMENT_KEY = exports.HOST_CALL_PLACEMENT = exports.IMPLICIT_PLACEMENTS = void 0;
17
+ exports.browserLifecycle = browserLifecycle;
18
+ /** The six reserved keys. Lowercase throughout: a placement key must match
19
+ * `^[a-z0-9][a-z0-9._-]{0,63}$` server-side, which is why this is
20
+ * `deeplink_open` and not Superwall's `deepLink_open`. */
21
+ exports.IMPLICIT_PLACEMENTS = [
22
+ "app_install",
23
+ "app_launch",
24
+ "session_start",
25
+ "deeplink_open",
26
+ "paywall_decline",
27
+ "transaction_abandon",
28
+ ];
29
+ /** The one moment the SDK cannot see unaided: the URL that opened the app is
30
+ * delivered to the host's own entry point, and an SDK trying to intercept it
31
+ * would be fighting the host's router. Call `handleDeepLink(url)`. */
32
+ exports.HOST_CALL_PLACEMENT = "deeplink_open";
33
+ /** The placementKey `handleDeepLink` reports for a dashboard QR/link preview
34
+ * (`<scheme>://revnix-preview?revnix_preview=<token>`) — not one of the six
35
+ * above, and never sent to /v1/placements/triggered. */
36
+ exports.PREVIEW_PLACEMENT_KEY = "revnix_preview";
37
+ /** How long the app must have been backgrounded for the return to count as a
38
+ * new session rather than an app switch. Matches Superwall's own definition
39
+ * so a team moving over gets the same numbers. */
40
+ exports.DEFAULT_SESSION_TIMEOUT_MS = 30 * 60000;
41
+ /** How long after a FAILED /v1/config read the SDK waits before asking again.
42
+ * See RevnixClient.implicitConfigRetryAt. */
43
+ exports.IMPLICIT_CONFIG_RETRY_MS = 60000;
44
+ /** Foreground detection in a browser tab. `visibilitychange` rather than
45
+ * `focus`: switching windows fires focus without the page ever having been
46
+ * hidden, which would mint sessions for someone alt-tabbing. Returns null
47
+ * where there is no document (React Native, Node, SSR) — the host passes its
48
+ * own adapter there. */
49
+ function browserLifecycle() {
50
+ const doc = globalThis.document;
51
+ if (!doc ||
52
+ typeof doc.addEventListener !== "function" ||
53
+ typeof doc.removeEventListener !== "function") {
54
+ return null;
55
+ }
56
+ const add = doc.addEventListener.bind(doc);
57
+ const remove = doc.removeEventListener.bind(doc);
58
+ return {
59
+ onStateChange(handler) {
60
+ const listener = () => {
61
+ handler(doc.visibilityState === "visible" ? "foreground" : "background");
62
+ };
63
+ add("visibilitychange", listener);
64
+ return () => remove("visibilitychange", listener);
65
+ },
66
+ };
67
+ }
@@ -0,0 +1,18 @@
1
+ import { RevnixClient } from "./client.js";
2
+ import { setLocale } from "./blocks/locale.js";
3
+ import type { RevnixConfig } from "./types.js";
4
+ export { RevnixClient } from "./client.js";
5
+ export { PurchaseBlockedError, RevnixAuthError, RevnixError, RevnixNetworkError, RevnixRateLimitError, RevnixTimeoutError, } from "./errors.js";
6
+ export { purchaseInputFromIap, type IapPurchase } from "./iap.js";
7
+ export { memoryStorage, generateAnonymousId } from "./storage.js";
8
+ export { SDK_VERSION } from "./version.js";
9
+ export { DEVICE_HEADER, encodeDeviceHeader } from "./device.js";
10
+ export { browserLifecycle, DEFAULT_SESSION_TIMEOUT_MS, HOST_CALL_PLACEMENT, IMPLICIT_PLACEMENTS, PREVIEW_PLACEMENT_KEY, type ImplicitPlacement, type ImplicitSkipReason, type ImplicitTrigger, type ImplicitTriggerResponse, type RevnixLifecycle, } from "./implicit.js";
11
+ export type { AdRevenue, AliasResult, CustomerEntitlements, DeviceFacts, Entitlement, EntitlementSource, Environment, IdentifyResult, IdentifyStrategy, IdentityInfo, ImplicitResolution, LastDeepLink, MmpAttribution, MmpProvider, PaywallConfig, PaywallInteraction, PaywallPurchaseOutcome, PlacementOffering, PlacementPackage, PlacementPaywall, PlacementProduct, PlacementResolution, RegisterPurchaseInput, RegisterPurchaseResult, RevnixAttribution, RevnixConfig, RevnixDiagnostic, RevnixStorage, Store, } from "./types.js";
12
+ /** Singleton facade — configure once at app start, then import `Revnix`
13
+ * anywhere. Prefer constructing RevnixClient directly in tests. */
14
+ export declare const Revnix: {
15
+ configure(config: RevnixConfig): RevnixClient;
16
+ setLocale: typeof setLocale;
17
+ readonly client: RevnixClient;
18
+ };
package/dist/index.js ADDED
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Revnix = exports.PREVIEW_PLACEMENT_KEY = exports.IMPLICIT_PLACEMENTS = exports.HOST_CALL_PLACEMENT = exports.DEFAULT_SESSION_TIMEOUT_MS = exports.browserLifecycle = exports.encodeDeviceHeader = exports.DEVICE_HEADER = exports.SDK_VERSION = exports.generateAnonymousId = exports.memoryStorage = exports.purchaseInputFromIap = exports.RevnixTimeoutError = exports.RevnixRateLimitError = exports.RevnixNetworkError = exports.RevnixError = exports.RevnixAuthError = exports.PurchaseBlockedError = exports.RevnixClient = void 0;
4
+ const client_js_1 = require("./client.js");
5
+ const locale_js_1 = require("./blocks/locale.js");
6
+ var client_js_2 = require("./client.js");
7
+ Object.defineProperty(exports, "RevnixClient", { enumerable: true, get: function () { return client_js_2.RevnixClient; } });
8
+ var errors_js_1 = require("./errors.js");
9
+ Object.defineProperty(exports, "PurchaseBlockedError", { enumerable: true, get: function () { return errors_js_1.PurchaseBlockedError; } });
10
+ Object.defineProperty(exports, "RevnixAuthError", { enumerable: true, get: function () { return errors_js_1.RevnixAuthError; } });
11
+ Object.defineProperty(exports, "RevnixError", { enumerable: true, get: function () { return errors_js_1.RevnixError; } });
12
+ Object.defineProperty(exports, "RevnixNetworkError", { enumerable: true, get: function () { return errors_js_1.RevnixNetworkError; } });
13
+ Object.defineProperty(exports, "RevnixRateLimitError", { enumerable: true, get: function () { return errors_js_1.RevnixRateLimitError; } });
14
+ Object.defineProperty(exports, "RevnixTimeoutError", { enumerable: true, get: function () { return errors_js_1.RevnixTimeoutError; } });
15
+ var iap_js_1 = require("./iap.js");
16
+ Object.defineProperty(exports, "purchaseInputFromIap", { enumerable: true, get: function () { return iap_js_1.purchaseInputFromIap; } });
17
+ var storage_js_1 = require("./storage.js");
18
+ Object.defineProperty(exports, "memoryStorage", { enumerable: true, get: function () { return storage_js_1.memoryStorage; } });
19
+ Object.defineProperty(exports, "generateAnonymousId", { enumerable: true, get: function () { return storage_js_1.generateAnonymousId; } });
20
+ var version_js_1 = require("./version.js");
21
+ Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return version_js_1.SDK_VERSION; } });
22
+ var device_js_1 = require("./device.js");
23
+ Object.defineProperty(exports, "DEVICE_HEADER", { enumerable: true, get: function () { return device_js_1.DEVICE_HEADER; } });
24
+ Object.defineProperty(exports, "encodeDeviceHeader", { enumerable: true, get: function () { return device_js_1.encodeDeviceHeader; } });
25
+ var implicit_js_1 = require("./implicit.js");
26
+ Object.defineProperty(exports, "browserLifecycle", { enumerable: true, get: function () { return implicit_js_1.browserLifecycle; } });
27
+ Object.defineProperty(exports, "DEFAULT_SESSION_TIMEOUT_MS", { enumerable: true, get: function () { return implicit_js_1.DEFAULT_SESSION_TIMEOUT_MS; } });
28
+ Object.defineProperty(exports, "HOST_CALL_PLACEMENT", { enumerable: true, get: function () { return implicit_js_1.HOST_CALL_PLACEMENT; } });
29
+ Object.defineProperty(exports, "IMPLICIT_PLACEMENTS", { enumerable: true, get: function () { return implicit_js_1.IMPLICIT_PLACEMENTS; } });
30
+ Object.defineProperty(exports, "PREVIEW_PLACEMENT_KEY", { enumerable: true, get: function () { return implicit_js_1.PREVIEW_PLACEMENT_KEY; } });
31
+ let instance = null;
32
+ /** Singleton facade — configure once at app start, then import `Revnix`
33
+ * anywhere. Prefer constructing RevnixClient directly in tests. */
34
+ exports.Revnix = {
35
+ configure(config) {
36
+ // REV-204 (L10): dev fast-refresh re-runs configure(); without teardown
37
+ // the old client kept its retry/prefetch background work running
38
+ // alongside the new one.
39
+ instance?.dispose();
40
+ instance = new client_js_1.RevnixClient(config);
41
+ return instance;
42
+ },
43
+ setLocale: locale_js_1.setLocale,
44
+ get client() {
45
+ if (!instance) {
46
+ throw new Error("Revnix: call Revnix.configure(...) before use");
47
+ }
48
+ return instance;
49
+ },
50
+ };
@@ -0,0 +1,14 @@
1
+ import type { RevnixStorage } from "./types.js";
2
+ /** Default storage: survives only the current JS runtime. Real apps must
3
+ * pass a persistent adapter (MMKV, AsyncStorage, localStorage) or every
4
+ * launch becomes a new anonymous customer. */
5
+ export declare function memoryStorage(): RevnixStorage;
6
+ export declare function resolveStorage(configured?: RevnixStorage): {
7
+ storage: RevnixStorage;
8
+ persistent: boolean;
9
+ };
10
+ export declare function generateAnonymousId(): string;
11
+ /** One id per paywall display (REV-094) — the backend dedupes retries on it
12
+ * while genuine repeat views (fresh ids) count separately. */
13
+ export declare function generateViewId(): string;
14
+ export declare function generateEventId(): string;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.memoryStorage = memoryStorage;
4
+ exports.resolveStorage = resolveStorage;
5
+ exports.generateAnonymousId = generateAnonymousId;
6
+ exports.generateViewId = generateViewId;
7
+ exports.generateEventId = generateEventId;
8
+ /** Default storage: survives only the current JS runtime. Real apps must
9
+ * pass a persistent adapter (MMKV, AsyncStorage, localStorage) or every
10
+ * launch becomes a new anonymous customer. */
11
+ function memoryStorage() {
12
+ const map = new Map();
13
+ return {
14
+ getItem: (key) => map.get(key) ?? null,
15
+ setItem: (key, value) => {
16
+ map.set(key, value);
17
+ },
18
+ };
19
+ }
20
+ // CTO-SCALE-REVIEW M5: what actually backs the client when `storage` is
21
+ // omitted. Memory-only used to be the silent default — every launch became a
22
+ // new anonymous customer + a new install report (analytics inflated by the
23
+ // DAU multiplier, purchases orphaned on the previous launch's id). Now:
24
+ // 1. explicit adapter → use it (unchanged);
25
+ // 2. web (localStorage usable) → use localStorage automatically;
26
+ // 3. truly nothing (React Native without an adapter) → memory, but WARN
27
+ // loudly, and the client skips install reporting (see client.ts) so a
28
+ // forgotten config line can't fabricate fleet-wide installs.
29
+ function resolveStorage(configured) {
30
+ if (configured)
31
+ return { storage: configured, persistent: true };
32
+ const ls = globalThis.localStorage;
33
+ if (ls) {
34
+ try {
35
+ const probe = "revnix.__probe";
36
+ ls.setItem(probe, "1");
37
+ ls.removeItem(probe);
38
+ return {
39
+ storage: {
40
+ getItem: (key) => ls.getItem(key),
41
+ setItem: (key, value) => {
42
+ ls.setItem(key, value);
43
+ },
44
+ },
45
+ persistent: true,
46
+ };
47
+ }
48
+ catch {
49
+ // Private mode / quota quirks — fall through to memory.
50
+ }
51
+ }
52
+ console.warn("[revnix] No `storage` adapter configured and no localStorage available; " +
53
+ "the customer id lives in memory only. Every launch will look like a NEW " +
54
+ "anonymous customer: prior purchases won't be attached and analytics " +
55
+ "would be inflated, so install reporting and experiment enrollment are " +
56
+ "disabled. Pass a persistent adapter (AsyncStorage/MMKV on React " +
57
+ "Native) to fix this.");
58
+ return { storage: memoryStorage(), persistent: false };
59
+ }
60
+ function randomHex(byteLength) {
61
+ const bytes = new Uint8Array(byteLength);
62
+ const cryptoObj = globalThis.crypto;
63
+ // L2: no Math.random fallback — a guessable customer id is readable by
64
+ // anyone holding the public key, so failing loudly beats failing silently.
65
+ // Every supported runtime (browsers, RN ≥0.72 via Hermes, Node ≥19) exposes
66
+ // getRandomValues; older Node needs `globalThis.crypto = require("crypto").webcrypto`.
67
+ if (!cryptoObj?.getRandomValues) {
68
+ throw new Error("[revnix] crypto.getRandomValues is unavailable in this runtime; " +
69
+ "cannot generate a secure customer id. Polyfill globalThis.crypto " +
70
+ "or pass an explicit `customerId` in configure().");
71
+ }
72
+ cryptoObj.getRandomValues(bytes);
73
+ return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
74
+ }
75
+ function generateAnonymousId() {
76
+ return `rvx_anon_${randomHex(16)}`;
77
+ }
78
+ /** One id per paywall display (REV-094) — the backend dedupes retries on it
79
+ * while genuine repeat views (fresh ids) count separately. */
80
+ function generateViewId() {
81
+ return `rvx_view_${randomHex(16)}`;
82
+ }
83
+ function generateEventId() {
84
+ return `rvx_evt_${randomHex(16)}`;
85
+ }