@variantlab/react-native 0.1.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.
@@ -0,0 +1,322 @@
1
+ export { UseExperimentResult, Variant, VariantErrorBoundary, VariantErrorBoundaryProps, VariantLabContext, VariantLabProvider, VariantLabProviderProps, VariantProps, VariantValue, VariantValueProps, useExperiment, useRouteExperiments, useSetVariant, useVariant, useVariantLabEngine, useVariantValue } from '@variantlab/react';
2
+ import { VariantContext, VariantEngine } from '@variantlab/core';
3
+ import { V as ValidationResult, S as SharePayload, a as ValidationFailure } from './types-CnSyez2D.cjs';
4
+ export { b as ShareVersion } from './types-CnSyez2D.cjs';
5
+
6
+ /**
7
+ * `getAutoContext()` — best-effort detection of the runtime context
8
+ * for variantlab targeting on React Native.
9
+ *
10
+ * The goal is to fill in `platform`, `screenSize`, and `locale` from
11
+ * native modules so that targeting like `{ platform: ["ios"] }` and
12
+ * `{ screenSize: ["small"] }` Just Works without the user having to
13
+ * import any RN modules themselves. `appVersion` is filled in when
14
+ * `expo-constants` is installed (it almost always is on Expo apps).
15
+ *
16
+ * Implementation rules:
17
+ *
18
+ * - Every native module is imported through a guarded `try / require`
19
+ * so a missing optional peer never crashes. Failing to read any
20
+ * individual field returns `undefined` for that field — never throws.
21
+ * - The function is **synchronous** because variantlab's resolution
22
+ * hot path is synchronous; we cannot wait on a Promise here.
23
+ * - Locale detection prefers `expo-localization` (the only RN-supported
24
+ * way that works in production). Falls back to `NativeModules` for
25
+ * bare RN apps and finally to `undefined`.
26
+ *
27
+ * The screen-size buckets follow the same thresholds as the rest of
28
+ * variantlab (see `targeting-dsl.md`):
29
+ *
30
+ * - small → < 360 pt wide (iPhone SE, older Androids)
31
+ * - medium → 360–767 pt wide (most phones)
32
+ * - large → ≥ 768 pt wide (tablets, foldables in unfolded mode)
33
+ */
34
+
35
+ interface AutoContextOptions {
36
+ /** Inject `expo-constants` for `appVersion`. Optional. */
37
+ readonly constants?: {
38
+ expoConfig?: {
39
+ version?: string;
40
+ } | null;
41
+ } | null;
42
+ /** Inject `expo-localization` for `locale`. Optional. */
43
+ readonly localization?: {
44
+ getLocales: () => Array<{
45
+ languageTag: string;
46
+ }>;
47
+ } | null;
48
+ }
49
+ declare function getAutoContext(options?: AutoContextOptions): VariantContext;
50
+ type ScreenSizeBucket = NonNullable<VariantContext["screenSize"]>;
51
+ declare function bucketScreenWidth(width: number): ScreenSizeBucket;
52
+
53
+ /**
54
+ * Encode and decode share payloads for deep links and QR codes.
55
+ *
56
+ * The wire format is documented in `docs/features/qr-sharing.md`:
57
+ *
58
+ * 1. Serialise the payload as compact JSON.
59
+ * 2. Optionally gzip-like compress (deferred — we ship plain base64
60
+ * first; the wire format is forward-compatible because the prefix
61
+ * byte tells the decoder which mode was used).
62
+ * 3. Base64url-encode the bytes (RFC 4648 §5: `+` → `-`, `/` → `_`,
63
+ * no `=` padding).
64
+ *
65
+ * We implement base64url by hand because we cannot rely on
66
+ * `globalThis.btoa` / `Buffer` being present on every RN runtime,
67
+ * and pulling in a base64 polyfill would burst the bundle budget.
68
+ * The encoder/decoder operates on UTF-8 byte arrays via `TextEncoder`
69
+ * / `TextDecoder`, both of which ship in Hermes.
70
+ *
71
+ * The format prefix byte is `0` for "raw JSON" and reserved for future
72
+ * compression schemes (`1` = deflate-raw, `2` = brotli, etc.). On
73
+ * decode we tolerate a missing prefix to keep older clients working.
74
+ */
75
+
76
+ /**
77
+ * Encode a payload to its on-the-wire base64url string. Throws on
78
+ * the rare case where validation fails on a payload constructed by
79
+ * the caller themselves — that's a developer error worth surfacing.
80
+ */
81
+ declare function encodeSharePayload(payload: SharePayload): string;
82
+ /**
83
+ * Decode a base64url string back into a validated payload. Returns
84
+ * a `ValidationResult` so callers can branch on `ok` rather than
85
+ * wrap the call in try/catch.
86
+ */
87
+ declare function decodeSharePayload(encoded: string, now?: number): ValidationResult;
88
+ declare function bytesToBase64Url(bytes: Uint8Array): string;
89
+ declare function base64UrlToBytes(input: string): Uint8Array;
90
+
91
+ /**
92
+ * Deep link handler for variantlab share payloads.
93
+ *
94
+ * Wires React Native's `Linking` module to the engine: when the OS
95
+ * delivers a URL with our share scheme, we decode the `?p=...` query
96
+ * parameter, validate it, and apply the overrides.
97
+ *
98
+ * Design notes:
99
+ *
100
+ * - The handler accepts the engine *and* the `Linking` module from
101
+ * the caller. In a real app the caller passes `Linking` from
102
+ * `react-native`; in tests the caller passes a mock that records
103
+ * `addEventListener` and `getInitialURL` calls. This keeps the
104
+ * module free of any real RN imports for the test runner.
105
+ * - URL parsing is hand-rolled because RN's `URL` polyfill is
106
+ * unreliable across versions and we only need to extract one
107
+ * query param.
108
+ * - Each apply emits via `engine.setVariant(id, variantId, "deeplink")`
109
+ * so the source field on `variantChanged` events lets debug overlays
110
+ * and telemetry distinguish deep-link writes from manual ones.
111
+ * - We rate-limit deep-link applies to one per second by default
112
+ * (`qr-sharing.md` §Rate limiting). Callers can override the
113
+ * interval but cannot disable it entirely.
114
+ */
115
+
116
+ /** The minimal Linking surface we depend on. */
117
+ interface LinkingLike {
118
+ addEventListener(type: "url", handler: (event: {
119
+ url: string;
120
+ }) => void): {
121
+ remove: () => void;
122
+ };
123
+ getInitialURL(): Promise<string | null>;
124
+ }
125
+ interface RegisterDeepLinkOptions {
126
+ /** App URL scheme, e.g. `"drishtikon"`. Required to filter foreign URLs. */
127
+ readonly scheme?: string;
128
+ /** Optional host portion, e.g. `"variantlab"`. Defaults to any host. */
129
+ readonly host?: string;
130
+ /** Query parameter holding the encoded payload. Default: `"p"`. */
131
+ readonly param?: string;
132
+ /** Called after each successful apply with the decoded payload. */
133
+ readonly onApply?: (payload: SharePayload) => void;
134
+ /** Called whenever a candidate link fails validation. */
135
+ readonly onError?: (reason: ValidationFailure | "wrong-scheme" | "no-payload") => void;
136
+ /** Minimum ms between successful applies. Default: 1000. */
137
+ readonly minIntervalMs?: number;
138
+ }
139
+ /**
140
+ * Register the deep-link handler. Returns an unsubscribe function
141
+ * that removes the listener and stops handling pending initial URLs.
142
+ *
143
+ * The handler also fires once for the URL that launched the app
144
+ * (via `getInitialURL`) so cold-launches via QR scan still apply
145
+ * the override on first render.
146
+ */
147
+ declare function registerDeepLinkHandler(engine: VariantEngine, linking: LinkingLike, options?: RegisterDeepLinkOptions): () => void;
148
+ /**
149
+ * Apply a previously-decoded payload to the engine. Exposed publicly
150
+ * so callers that build their own URL-handling pipeline (Next.js
151
+ * middleware, web router) can reuse the same logic.
152
+ */
153
+ declare function applyPayload(engine: VariantEngine, payload: SharePayload): void;
154
+
155
+ /**
156
+ * Validate a candidate share payload before applying it.
157
+ *
158
+ * Share payloads come from untrusted input — a deep link, a QR code,
159
+ * a clipboard paste — so we treat them as adversarial. The validator
160
+ * walks the structure with `Object.create(null)` semantics in mind:
161
+ *
162
+ * - Reject prototype-pollution keys (`__proto__`, `constructor`,
163
+ * `prototype`) anywhere in the tree.
164
+ * - Enforce strict id regexes on every override key/value so a
165
+ * malformed payload cannot smuggle non-printable bytes into a
166
+ * `setVariant` call.
167
+ * - Cap the override count and total decoded size to bound memory.
168
+ * - Honor an optional `expires` field so QR codes shared earlier
169
+ * in the day cannot be replayed forever.
170
+ *
171
+ * The validator returns a `ValidationResult` discriminated union
172
+ * instead of throwing because callers (deep-link handler, QR scanner)
173
+ * almost always want to silently surface a failure to the UI rather
174
+ * than treat it as a runtime error.
175
+ */
176
+
177
+ declare function validatePayload(input: unknown, now?: number): ValidationResult;
178
+
179
+ /**
180
+ * The pluggable storage interface used by every adapter in this package.
181
+ *
182
+ * Mirrors the surface in `API.md` §`Storage interface` so that callers
183
+ * can persist engine overrides to whichever native KV store fits their
184
+ * app — AsyncStorage by default, MMKV when bundle/perf matter, or
185
+ * SecureStore when the override list itself is sensitive.
186
+ *
187
+ * Methods may be sync or async to match the underlying backing store.
188
+ * `@variantlab/core` does not yet wire this into `EngineOptions`; for
189
+ * now adapters expose them as standalone factories so that callers can
190
+ * implement persistence at the app boundary (e.g. by serialising
191
+ * `engine.getHistory()` after every `variantChanged` event).
192
+ */
193
+ interface Storage {
194
+ getItem(key: string): string | null | Promise<string | null>;
195
+ setItem(key: string, value: string): void | Promise<void>;
196
+ removeItem(key: string): void | Promise<void>;
197
+ /** Optional bulk-key listing for debug overlay export and tests. */
198
+ keys?(): string[] | Promise<string[]>;
199
+ /** Optional clear-all helper used by `engine.resetAll()` integrations. */
200
+ clear?(): void | Promise<void>;
201
+ }
202
+ /** Prefix every variantlab key with this so storage stays sandboxed. */
203
+ declare const STORAGE_KEY_PREFIX = "variantlab:";
204
+ /** Build a fully-qualified storage key from a logical name. */
205
+ declare function buildKey(name: string): string;
206
+
207
+ /**
208
+ * `Storage` adapter backed by `@react-native-async-storage/async-storage`.
209
+ *
210
+ * AsyncStorage is the de-facto standard KV store on React Native and
211
+ * the only one we recommend by default — it's bundled with every
212
+ * Expo project, ships in the New Architecture, and works on iOS,
213
+ * Android, web, and tvOS. It is, however, async on every method, so
214
+ * the returned `Storage` is a fully async implementation.
215
+ *
216
+ * The adapter is constructed lazily: callers pass in their AsyncStorage
217
+ * module rather than us importing it. This keeps `@variantlab/react-native`
218
+ * itself free of any RN package imports at module load — the user's
219
+ * bundler resolves the peer, we never see it. The factory accepts the
220
+ * module directly so the surface is dependency-injected and trivially
221
+ * testable.
222
+ */
223
+
224
+ /** Subset of `AsyncStorageStatic` we actually use. */
225
+ interface AsyncStorageLike {
226
+ getItem(key: string): Promise<string | null>;
227
+ setItem(key: string, value: string): Promise<void>;
228
+ removeItem(key: string): Promise<void>;
229
+ getAllKeys(): Promise<readonly string[]>;
230
+ clear?(): Promise<void>;
231
+ }
232
+ declare function createAsyncStorageAdapter(asyncStorage: AsyncStorageLike): Storage;
233
+
234
+ /**
235
+ * In-memory `Storage` implementation. Useful for tests, SSR fallbacks,
236
+ * and any code path where the host platform's native KV is unavailable.
237
+ *
238
+ * The implementation is intentionally trivial: a `Map` keyed on the
239
+ * fully-qualified key, no namespacing logic, no async wrappers. Real
240
+ * adapters wrap a native module and may be async; this one is sync to
241
+ * keep tests deterministic.
242
+ */
243
+
244
+ declare function createMemoryStorage(): Storage;
245
+
246
+ /**
247
+ * `Storage` adapter backed by `react-native-mmkv`.
248
+ *
249
+ * MMKV is a fully synchronous KV store implemented in C++ on top of
250
+ * mmap. It's the fastest option on RN by a wide margin and is
251
+ * particularly nice for variantlab because variant resolution is
252
+ * synchronous on the hot path — using MMKV means storage round trips
253
+ * never block on a JS-bridge call.
254
+ *
255
+ * Like `async-storage`, the underlying instance is dependency-injected
256
+ * rather than imported here. The caller constructs an `MMKV` instance
257
+ * (typically with their own `id` namespace and optional encryption key)
258
+ * and hands it in. We only require a tiny subset of the surface so
259
+ * users can also pass any compatible mock in tests.
260
+ */
261
+
262
+ /** Subset of the real MMKV instance we depend on. */
263
+ interface MMKVLike {
264
+ set(key: string, value: string): void;
265
+ getString(key: string): string | undefined;
266
+ contains(key: string): boolean;
267
+ delete(key: string): void;
268
+ getAllKeys(): string[];
269
+ clearAll?(): void;
270
+ }
271
+ declare function createMMKVStorageAdapter(mmkv: MMKVLike): Storage;
272
+
273
+ /**
274
+ * `Storage` adapter backed by `expo-secure-store`.
275
+ *
276
+ * SecureStore writes through to Keychain (iOS) and the EncryptedSharedPreferences
277
+ * /Keystore pair (Android). It's appropriate when the override payload
278
+ * is itself sensitive — e.g. an experiment that toggles an internal
279
+ * staging endpoint, an enterprise feature flag, or a beta-channel key.
280
+ *
281
+ * SecureStore has two notable constraints:
282
+ *
283
+ * 1. **No bulk key listing.** The native API does not expose a
284
+ * "list every key" operation, so `keys()` is intentionally absent.
285
+ * Callers that need bulk operations should layer an index in a
286
+ * separate AsyncStorage entry, or compose with `createMemoryStorage`.
287
+ * 2. **Slow on Android.** Each read/write hits the native module.
288
+ * Don't use this on hot paths where latency matters.
289
+ *
290
+ * As with the other adapters, the upstream module is injected so this
291
+ * file imports nothing from `expo-secure-store` itself.
292
+ */
293
+
294
+ /** Subset of the real `expo-secure-store` namespace we use. */
295
+ interface SecureStoreLike {
296
+ getItemAsync(key: string): Promise<string | null>;
297
+ setItemAsync(key: string, value: string): Promise<void>;
298
+ deleteItemAsync(key: string): Promise<void>;
299
+ }
300
+ declare function createSecureStoreAdapter(secureStore: SecureStoreLike): Storage;
301
+
302
+ /**
303
+ * `@variantlab/react-native` — public barrel.
304
+ *
305
+ * This entrypoint deliberately **re-exports everything** from
306
+ * `@variantlab/react` so a React Native app can
307
+ *
308
+ * import { VariantLabProvider, useVariant } from "@variantlab/react-native";
309
+ *
310
+ * without the user having to remember that the hook layer lives in
311
+ * the web-React package. The hooks and components are 100% compatible
312
+ * with React Native — they rely only on React's own APIs.
313
+ *
314
+ * RN-specific surface sits alongside: storage adapters, auto-context
315
+ * detection, and the deep-link handler. The debug overlay and the
316
+ * QR helpers are intentionally split into their own sub-entrypoints
317
+ * (`/debug`, `/qr`) so tree-shaking works at the package level:
318
+ * production bundles that never import `/debug` do not pay for it.
319
+ */
320
+ declare const VERSION = "0.0.0";
321
+
322
+ export { type AsyncStorageLike, type AutoContextOptions, type LinkingLike, type MMKVLike, type RegisterDeepLinkOptions, STORAGE_KEY_PREFIX, type ScreenSizeBucket, type SecureStoreLike, SharePayload, type Storage, VERSION, ValidationFailure, ValidationResult, applyPayload, base64UrlToBytes, bucketScreenWidth, buildKey, bytesToBase64Url, createAsyncStorageAdapter, createMMKVStorageAdapter, createMemoryStorage, createSecureStoreAdapter, decodeSharePayload, encodeSharePayload, getAutoContext, registerDeepLinkHandler, validatePayload };
@@ -0,0 +1,322 @@
1
+ export { UseExperimentResult, Variant, VariantErrorBoundary, VariantErrorBoundaryProps, VariantLabContext, VariantLabProvider, VariantLabProviderProps, VariantProps, VariantValue, VariantValueProps, useExperiment, useRouteExperiments, useSetVariant, useVariant, useVariantLabEngine, useVariantValue } from '@variantlab/react';
2
+ import { VariantContext, VariantEngine } from '@variantlab/core';
3
+ import { V as ValidationResult, S as SharePayload, a as ValidationFailure } from './types-CnSyez2D.js';
4
+ export { b as ShareVersion } from './types-CnSyez2D.js';
5
+
6
+ /**
7
+ * `getAutoContext()` — best-effort detection of the runtime context
8
+ * for variantlab targeting on React Native.
9
+ *
10
+ * The goal is to fill in `platform`, `screenSize`, and `locale` from
11
+ * native modules so that targeting like `{ platform: ["ios"] }` and
12
+ * `{ screenSize: ["small"] }` Just Works without the user having to
13
+ * import any RN modules themselves. `appVersion` is filled in when
14
+ * `expo-constants` is installed (it almost always is on Expo apps).
15
+ *
16
+ * Implementation rules:
17
+ *
18
+ * - Every native module is imported through a guarded `try / require`
19
+ * so a missing optional peer never crashes. Failing to read any
20
+ * individual field returns `undefined` for that field — never throws.
21
+ * - The function is **synchronous** because variantlab's resolution
22
+ * hot path is synchronous; we cannot wait on a Promise here.
23
+ * - Locale detection prefers `expo-localization` (the only RN-supported
24
+ * way that works in production). Falls back to `NativeModules` for
25
+ * bare RN apps and finally to `undefined`.
26
+ *
27
+ * The screen-size buckets follow the same thresholds as the rest of
28
+ * variantlab (see `targeting-dsl.md`):
29
+ *
30
+ * - small → < 360 pt wide (iPhone SE, older Androids)
31
+ * - medium → 360–767 pt wide (most phones)
32
+ * - large → ≥ 768 pt wide (tablets, foldables in unfolded mode)
33
+ */
34
+
35
+ interface AutoContextOptions {
36
+ /** Inject `expo-constants` for `appVersion`. Optional. */
37
+ readonly constants?: {
38
+ expoConfig?: {
39
+ version?: string;
40
+ } | null;
41
+ } | null;
42
+ /** Inject `expo-localization` for `locale`. Optional. */
43
+ readonly localization?: {
44
+ getLocales: () => Array<{
45
+ languageTag: string;
46
+ }>;
47
+ } | null;
48
+ }
49
+ declare function getAutoContext(options?: AutoContextOptions): VariantContext;
50
+ type ScreenSizeBucket = NonNullable<VariantContext["screenSize"]>;
51
+ declare function bucketScreenWidth(width: number): ScreenSizeBucket;
52
+
53
+ /**
54
+ * Encode and decode share payloads for deep links and QR codes.
55
+ *
56
+ * The wire format is documented in `docs/features/qr-sharing.md`:
57
+ *
58
+ * 1. Serialise the payload as compact JSON.
59
+ * 2. Optionally gzip-like compress (deferred — we ship plain base64
60
+ * first; the wire format is forward-compatible because the prefix
61
+ * byte tells the decoder which mode was used).
62
+ * 3. Base64url-encode the bytes (RFC 4648 §5: `+` → `-`, `/` → `_`,
63
+ * no `=` padding).
64
+ *
65
+ * We implement base64url by hand because we cannot rely on
66
+ * `globalThis.btoa` / `Buffer` being present on every RN runtime,
67
+ * and pulling in a base64 polyfill would burst the bundle budget.
68
+ * The encoder/decoder operates on UTF-8 byte arrays via `TextEncoder`
69
+ * / `TextDecoder`, both of which ship in Hermes.
70
+ *
71
+ * The format prefix byte is `0` for "raw JSON" and reserved for future
72
+ * compression schemes (`1` = deflate-raw, `2` = brotli, etc.). On
73
+ * decode we tolerate a missing prefix to keep older clients working.
74
+ */
75
+
76
+ /**
77
+ * Encode a payload to its on-the-wire base64url string. Throws on
78
+ * the rare case where validation fails on a payload constructed by
79
+ * the caller themselves — that's a developer error worth surfacing.
80
+ */
81
+ declare function encodeSharePayload(payload: SharePayload): string;
82
+ /**
83
+ * Decode a base64url string back into a validated payload. Returns
84
+ * a `ValidationResult` so callers can branch on `ok` rather than
85
+ * wrap the call in try/catch.
86
+ */
87
+ declare function decodeSharePayload(encoded: string, now?: number): ValidationResult;
88
+ declare function bytesToBase64Url(bytes: Uint8Array): string;
89
+ declare function base64UrlToBytes(input: string): Uint8Array;
90
+
91
+ /**
92
+ * Deep link handler for variantlab share payloads.
93
+ *
94
+ * Wires React Native's `Linking` module to the engine: when the OS
95
+ * delivers a URL with our share scheme, we decode the `?p=...` query
96
+ * parameter, validate it, and apply the overrides.
97
+ *
98
+ * Design notes:
99
+ *
100
+ * - The handler accepts the engine *and* the `Linking` module from
101
+ * the caller. In a real app the caller passes `Linking` from
102
+ * `react-native`; in tests the caller passes a mock that records
103
+ * `addEventListener` and `getInitialURL` calls. This keeps the
104
+ * module free of any real RN imports for the test runner.
105
+ * - URL parsing is hand-rolled because RN's `URL` polyfill is
106
+ * unreliable across versions and we only need to extract one
107
+ * query param.
108
+ * - Each apply emits via `engine.setVariant(id, variantId, "deeplink")`
109
+ * so the source field on `variantChanged` events lets debug overlays
110
+ * and telemetry distinguish deep-link writes from manual ones.
111
+ * - We rate-limit deep-link applies to one per second by default
112
+ * (`qr-sharing.md` §Rate limiting). Callers can override the
113
+ * interval but cannot disable it entirely.
114
+ */
115
+
116
+ /** The minimal Linking surface we depend on. */
117
+ interface LinkingLike {
118
+ addEventListener(type: "url", handler: (event: {
119
+ url: string;
120
+ }) => void): {
121
+ remove: () => void;
122
+ };
123
+ getInitialURL(): Promise<string | null>;
124
+ }
125
+ interface RegisterDeepLinkOptions {
126
+ /** App URL scheme, e.g. `"drishtikon"`. Required to filter foreign URLs. */
127
+ readonly scheme?: string;
128
+ /** Optional host portion, e.g. `"variantlab"`. Defaults to any host. */
129
+ readonly host?: string;
130
+ /** Query parameter holding the encoded payload. Default: `"p"`. */
131
+ readonly param?: string;
132
+ /** Called after each successful apply with the decoded payload. */
133
+ readonly onApply?: (payload: SharePayload) => void;
134
+ /** Called whenever a candidate link fails validation. */
135
+ readonly onError?: (reason: ValidationFailure | "wrong-scheme" | "no-payload") => void;
136
+ /** Minimum ms between successful applies. Default: 1000. */
137
+ readonly minIntervalMs?: number;
138
+ }
139
+ /**
140
+ * Register the deep-link handler. Returns an unsubscribe function
141
+ * that removes the listener and stops handling pending initial URLs.
142
+ *
143
+ * The handler also fires once for the URL that launched the app
144
+ * (via `getInitialURL`) so cold-launches via QR scan still apply
145
+ * the override on first render.
146
+ */
147
+ declare function registerDeepLinkHandler(engine: VariantEngine, linking: LinkingLike, options?: RegisterDeepLinkOptions): () => void;
148
+ /**
149
+ * Apply a previously-decoded payload to the engine. Exposed publicly
150
+ * so callers that build their own URL-handling pipeline (Next.js
151
+ * middleware, web router) can reuse the same logic.
152
+ */
153
+ declare function applyPayload(engine: VariantEngine, payload: SharePayload): void;
154
+
155
+ /**
156
+ * Validate a candidate share payload before applying it.
157
+ *
158
+ * Share payloads come from untrusted input — a deep link, a QR code,
159
+ * a clipboard paste — so we treat them as adversarial. The validator
160
+ * walks the structure with `Object.create(null)` semantics in mind:
161
+ *
162
+ * - Reject prototype-pollution keys (`__proto__`, `constructor`,
163
+ * `prototype`) anywhere in the tree.
164
+ * - Enforce strict id regexes on every override key/value so a
165
+ * malformed payload cannot smuggle non-printable bytes into a
166
+ * `setVariant` call.
167
+ * - Cap the override count and total decoded size to bound memory.
168
+ * - Honor an optional `expires` field so QR codes shared earlier
169
+ * in the day cannot be replayed forever.
170
+ *
171
+ * The validator returns a `ValidationResult` discriminated union
172
+ * instead of throwing because callers (deep-link handler, QR scanner)
173
+ * almost always want to silently surface a failure to the UI rather
174
+ * than treat it as a runtime error.
175
+ */
176
+
177
+ declare function validatePayload(input: unknown, now?: number): ValidationResult;
178
+
179
+ /**
180
+ * The pluggable storage interface used by every adapter in this package.
181
+ *
182
+ * Mirrors the surface in `API.md` §`Storage interface` so that callers
183
+ * can persist engine overrides to whichever native KV store fits their
184
+ * app — AsyncStorage by default, MMKV when bundle/perf matter, or
185
+ * SecureStore when the override list itself is sensitive.
186
+ *
187
+ * Methods may be sync or async to match the underlying backing store.
188
+ * `@variantlab/core` does not yet wire this into `EngineOptions`; for
189
+ * now adapters expose them as standalone factories so that callers can
190
+ * implement persistence at the app boundary (e.g. by serialising
191
+ * `engine.getHistory()` after every `variantChanged` event).
192
+ */
193
+ interface Storage {
194
+ getItem(key: string): string | null | Promise<string | null>;
195
+ setItem(key: string, value: string): void | Promise<void>;
196
+ removeItem(key: string): void | Promise<void>;
197
+ /** Optional bulk-key listing for debug overlay export and tests. */
198
+ keys?(): string[] | Promise<string[]>;
199
+ /** Optional clear-all helper used by `engine.resetAll()` integrations. */
200
+ clear?(): void | Promise<void>;
201
+ }
202
+ /** Prefix every variantlab key with this so storage stays sandboxed. */
203
+ declare const STORAGE_KEY_PREFIX = "variantlab:";
204
+ /** Build a fully-qualified storage key from a logical name. */
205
+ declare function buildKey(name: string): string;
206
+
207
+ /**
208
+ * `Storage` adapter backed by `@react-native-async-storage/async-storage`.
209
+ *
210
+ * AsyncStorage is the de-facto standard KV store on React Native and
211
+ * the only one we recommend by default — it's bundled with every
212
+ * Expo project, ships in the New Architecture, and works on iOS,
213
+ * Android, web, and tvOS. It is, however, async on every method, so
214
+ * the returned `Storage` is a fully async implementation.
215
+ *
216
+ * The adapter is constructed lazily: callers pass in their AsyncStorage
217
+ * module rather than us importing it. This keeps `@variantlab/react-native`
218
+ * itself free of any RN package imports at module load — the user's
219
+ * bundler resolves the peer, we never see it. The factory accepts the
220
+ * module directly so the surface is dependency-injected and trivially
221
+ * testable.
222
+ */
223
+
224
+ /** Subset of `AsyncStorageStatic` we actually use. */
225
+ interface AsyncStorageLike {
226
+ getItem(key: string): Promise<string | null>;
227
+ setItem(key: string, value: string): Promise<void>;
228
+ removeItem(key: string): Promise<void>;
229
+ getAllKeys(): Promise<readonly string[]>;
230
+ clear?(): Promise<void>;
231
+ }
232
+ declare function createAsyncStorageAdapter(asyncStorage: AsyncStorageLike): Storage;
233
+
234
+ /**
235
+ * In-memory `Storage` implementation. Useful for tests, SSR fallbacks,
236
+ * and any code path where the host platform's native KV is unavailable.
237
+ *
238
+ * The implementation is intentionally trivial: a `Map` keyed on the
239
+ * fully-qualified key, no namespacing logic, no async wrappers. Real
240
+ * adapters wrap a native module and may be async; this one is sync to
241
+ * keep tests deterministic.
242
+ */
243
+
244
+ declare function createMemoryStorage(): Storage;
245
+
246
+ /**
247
+ * `Storage` adapter backed by `react-native-mmkv`.
248
+ *
249
+ * MMKV is a fully synchronous KV store implemented in C++ on top of
250
+ * mmap. It's the fastest option on RN by a wide margin and is
251
+ * particularly nice for variantlab because variant resolution is
252
+ * synchronous on the hot path — using MMKV means storage round trips
253
+ * never block on a JS-bridge call.
254
+ *
255
+ * Like `async-storage`, the underlying instance is dependency-injected
256
+ * rather than imported here. The caller constructs an `MMKV` instance
257
+ * (typically with their own `id` namespace and optional encryption key)
258
+ * and hands it in. We only require a tiny subset of the surface so
259
+ * users can also pass any compatible mock in tests.
260
+ */
261
+
262
+ /** Subset of the real MMKV instance we depend on. */
263
+ interface MMKVLike {
264
+ set(key: string, value: string): void;
265
+ getString(key: string): string | undefined;
266
+ contains(key: string): boolean;
267
+ delete(key: string): void;
268
+ getAllKeys(): string[];
269
+ clearAll?(): void;
270
+ }
271
+ declare function createMMKVStorageAdapter(mmkv: MMKVLike): Storage;
272
+
273
+ /**
274
+ * `Storage` adapter backed by `expo-secure-store`.
275
+ *
276
+ * SecureStore writes through to Keychain (iOS) and the EncryptedSharedPreferences
277
+ * /Keystore pair (Android). It's appropriate when the override payload
278
+ * is itself sensitive — e.g. an experiment that toggles an internal
279
+ * staging endpoint, an enterprise feature flag, or a beta-channel key.
280
+ *
281
+ * SecureStore has two notable constraints:
282
+ *
283
+ * 1. **No bulk key listing.** The native API does not expose a
284
+ * "list every key" operation, so `keys()` is intentionally absent.
285
+ * Callers that need bulk operations should layer an index in a
286
+ * separate AsyncStorage entry, or compose with `createMemoryStorage`.
287
+ * 2. **Slow on Android.** Each read/write hits the native module.
288
+ * Don't use this on hot paths where latency matters.
289
+ *
290
+ * As with the other adapters, the upstream module is injected so this
291
+ * file imports nothing from `expo-secure-store` itself.
292
+ */
293
+
294
+ /** Subset of the real `expo-secure-store` namespace we use. */
295
+ interface SecureStoreLike {
296
+ getItemAsync(key: string): Promise<string | null>;
297
+ setItemAsync(key: string, value: string): Promise<void>;
298
+ deleteItemAsync(key: string): Promise<void>;
299
+ }
300
+ declare function createSecureStoreAdapter(secureStore: SecureStoreLike): Storage;
301
+
302
+ /**
303
+ * `@variantlab/react-native` — public barrel.
304
+ *
305
+ * This entrypoint deliberately **re-exports everything** from
306
+ * `@variantlab/react` so a React Native app can
307
+ *
308
+ * import { VariantLabProvider, useVariant } from "@variantlab/react-native";
309
+ *
310
+ * without the user having to remember that the hook layer lives in
311
+ * the web-React package. The hooks and components are 100% compatible
312
+ * with React Native — they rely only on React's own APIs.
313
+ *
314
+ * RN-specific surface sits alongside: storage adapters, auto-context
315
+ * detection, and the deep-link handler. The debug overlay and the
316
+ * QR helpers are intentionally split into their own sub-entrypoints
317
+ * (`/debug`, `/qr`) so tree-shaking works at the package level:
318
+ * production bundles that never import `/debug` do not pay for it.
319
+ */
320
+ declare const VERSION = "0.0.0";
321
+
322
+ export { type AsyncStorageLike, type AutoContextOptions, type LinkingLike, type MMKVLike, type RegisterDeepLinkOptions, STORAGE_KEY_PREFIX, type ScreenSizeBucket, type SecureStoreLike, SharePayload, type Storage, VERSION, ValidationFailure, ValidationResult, applyPayload, base64UrlToBytes, bucketScreenWidth, buildKey, bytesToBase64Url, createAsyncStorageAdapter, createMMKVStorageAdapter, createMemoryStorage, createSecureStoreAdapter, decodeSharePayload, encodeSharePayload, getAutoContext, registerDeepLinkHandler, validatePayload };