@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.
- package/LICENSE +21 -0
- package/README.md +23 -0
- package/dist/debug.cjs +940 -0
- package/dist/debug.cjs.map +1 -0
- package/dist/debug.d.cts +206 -0
- package/dist/debug.d.ts +206 -0
- package/dist/debug.js +926 -0
- package/dist/debug.js.map +1 -0
- package/dist/index.cjs +548 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +322 -0
- package/dist/index.d.ts +322 -0
- package/dist/index.js +487 -0
- package/dist/index.js.map +1 -0
- package/dist/qr.cjs +252 -0
- package/dist/qr.cjs.map +1 -0
- package/dist/qr.d.cts +55 -0
- package/dist/qr.d.ts +55 -0
- package/dist/qr.js +247 -0
- package/dist/qr.js.map +1 -0
- package/dist/types-CnSyez2D.d.cts +36 -0
- package/dist/types-CnSyez2D.d.ts +36 -0
- package/package.json +124 -0
package/dist/index.d.cts
ADDED
|
@@ -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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|