@variantlab/react-native 0.1.2 → 0.1.3
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/README.md +390 -48
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -23,96 +23,405 @@ npm install @variantlab/core@alpha @variantlab/react@alpha @variantlab/react-nat
|
|
|
23
23
|
- `react-native-safe-area-context` — safe area for debug overlay
|
|
24
24
|
- `react-native-svg` — QR code rendering
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Complete example
|
|
29
|
+
|
|
30
|
+
Here's a full working setup — from config to rendering variants:
|
|
31
|
+
|
|
32
|
+
### `experiments.json`
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"version": 1,
|
|
37
|
+
"experiments": [
|
|
38
|
+
{
|
|
39
|
+
"id": "card-layout",
|
|
40
|
+
"name": "Card layout experiment",
|
|
41
|
+
"type": "render",
|
|
42
|
+
"default": "standard",
|
|
43
|
+
"variants": [
|
|
44
|
+
{ "id": "standard" },
|
|
45
|
+
{ "id": "compact" },
|
|
46
|
+
{ "id": "pip-thumbnail" }
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "cta-copy",
|
|
51
|
+
"name": "CTA button text",
|
|
52
|
+
"type": "value",
|
|
53
|
+
"default": "buy-now",
|
|
54
|
+
"variants": [
|
|
55
|
+
{ "id": "buy-now", "value": "Buy now" },
|
|
56
|
+
{ "id": "get-started", "value": "Get started" },
|
|
57
|
+
{ "id": "try-free", "value": "Try it free" }
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "onboarding-flow",
|
|
62
|
+
"name": "Onboarding flow",
|
|
63
|
+
"type": "render",
|
|
64
|
+
"default": "classic",
|
|
65
|
+
"assignment": { "strategy": "sticky-hash" },
|
|
66
|
+
"variants": [
|
|
67
|
+
{ "id": "classic" },
|
|
68
|
+
{ "id": "quick-start" }
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
27
74
|
|
|
28
|
-
###
|
|
75
|
+
### `variantlab.ts` — engine setup
|
|
29
76
|
|
|
30
|
-
```
|
|
77
|
+
```ts
|
|
31
78
|
import { createEngine } from "@variantlab/core";
|
|
32
79
|
import { getAutoContext, createAsyncStorageAdapter } from "@variantlab/react-native";
|
|
33
80
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
34
81
|
import experiments from "./experiments.json";
|
|
35
82
|
|
|
36
|
-
const engine = createEngine(experiments, {
|
|
37
|
-
context:
|
|
83
|
+
export const engine = createEngine(experiments, {
|
|
84
|
+
context: {
|
|
85
|
+
...getAutoContext(), // auto-detects platform, screenSize, locale
|
|
86
|
+
userId: "user-123", // your authenticated user ID
|
|
87
|
+
},
|
|
38
88
|
storage: createAsyncStorageAdapter(AsyncStorage),
|
|
39
89
|
});
|
|
40
90
|
```
|
|
41
91
|
|
|
42
|
-
###
|
|
92
|
+
### `app/_layout.tsx` — wrap your app
|
|
43
93
|
|
|
44
94
|
```tsx
|
|
45
95
|
import { VariantLabProvider } from "@variantlab/react-native";
|
|
96
|
+
import { VariantDebugOverlay } from "@variantlab/react-native/debug";
|
|
97
|
+
import { engine } from "./variantlab";
|
|
46
98
|
|
|
47
|
-
export default function
|
|
99
|
+
export default function RootLayout() {
|
|
48
100
|
return (
|
|
49
101
|
<VariantLabProvider engine={engine}>
|
|
50
|
-
<
|
|
102
|
+
<Slot />
|
|
103
|
+
{__DEV__ && <VariantDebugOverlay />}
|
|
51
104
|
</VariantLabProvider>
|
|
52
105
|
);
|
|
53
106
|
}
|
|
54
107
|
```
|
|
55
108
|
|
|
56
|
-
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Hooks
|
|
112
|
+
|
|
113
|
+
### `useVariant(experimentId)` — get the active variant ID
|
|
114
|
+
|
|
115
|
+
Use this for **render experiments** where you switch between different components.
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
import { View } from "react-native";
|
|
119
|
+
import { useVariant } from "@variantlab/react-native";
|
|
120
|
+
|
|
121
|
+
function CardSection() {
|
|
122
|
+
const layout = useVariant("card-layout");
|
|
123
|
+
// Returns: "standard" | "compact" | "pip-thumbnail"
|
|
124
|
+
|
|
125
|
+
switch (layout) {
|
|
126
|
+
case "compact":
|
|
127
|
+
return <CompactCard />;
|
|
128
|
+
case "pip-thumbnail":
|
|
129
|
+
return <PipThumbnailCard />;
|
|
130
|
+
default:
|
|
131
|
+
return <StandardCard />;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### `useVariantValue<T>(experimentId)` — get the experiment value
|
|
137
|
+
|
|
138
|
+
Use this for **value experiments** where variants carry data (strings, numbers, objects).
|
|
57
139
|
|
|
58
140
|
```tsx
|
|
59
|
-
import {
|
|
141
|
+
import { Text, TouchableOpacity } from "react-native";
|
|
142
|
+
import { useVariantValue } from "@variantlab/react-native";
|
|
143
|
+
|
|
144
|
+
function CheckoutButton() {
|
|
145
|
+
const buttonText = useVariantValue<string>("cta-copy");
|
|
146
|
+
// Returns: "Buy now" | "Get started" | "Try it free"
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<TouchableOpacity style={styles.button}>
|
|
150
|
+
<Text>{buttonText}</Text>
|
|
151
|
+
</TouchableOpacity>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function PricingDisplay() {
|
|
156
|
+
const price = useVariantValue<number>("pricing-tier");
|
|
157
|
+
// Returns: 9.99 | 14.99 | 19.99
|
|
158
|
+
|
|
159
|
+
return <Text>${price}/month</Text>;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### `useExperiment(experimentId)` — get full experiment state
|
|
164
|
+
|
|
165
|
+
Returns the variant ID, the experiment config, and whether it's been manually overridden.
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
import { Text, View } from "react-native";
|
|
169
|
+
import { useExperiment } from "@variantlab/react-native";
|
|
170
|
+
|
|
171
|
+
function DebugBanner() {
|
|
172
|
+
const { variantId, experiment, isOverridden } = useExperiment("card-layout");
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<View>
|
|
176
|
+
<Text>Experiment: {experiment.name}</Text>
|
|
177
|
+
<Text>Active variant: {variantId}</Text>
|
|
178
|
+
{isOverridden && <Text style={{ color: "orange" }}>⚠ Manually overridden</Text>}
|
|
179
|
+
</View>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### `useSetVariant()` — override a variant (for testing/QA)
|
|
185
|
+
|
|
186
|
+
Returns a function to force-assign a variant. Useful for building your own debug UI or testing different variants during development.
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
import { Button, View } from "react-native";
|
|
190
|
+
import { useSetVariant, useVariant } from "@variantlab/react-native";
|
|
191
|
+
|
|
192
|
+
function VariantPicker() {
|
|
193
|
+
const setVariant = useSetVariant();
|
|
194
|
+
const current = useVariant("card-layout");
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<View>
|
|
198
|
+
<Text>Current: {current}</Text>
|
|
199
|
+
<Button title="Standard" onPress={() => setVariant("card-layout", "standard")} />
|
|
200
|
+
<Button title="Compact" onPress={() => setVariant("card-layout", "compact")} />
|
|
201
|
+
<Button title="PiP" onPress={() => setVariant("card-layout", "pip-thumbnail")} />
|
|
202
|
+
</View>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### `useVariantLabEngine()` — access the engine directly
|
|
208
|
+
|
|
209
|
+
Returns the engine instance for advanced operations like resetting all overrides or updating context.
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
import { Button } from "react-native";
|
|
213
|
+
import { useVariantLabEngine } from "@variantlab/react-native";
|
|
214
|
+
|
|
215
|
+
function ResetButton() {
|
|
216
|
+
const engine = useVariantLabEngine();
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<Button
|
|
220
|
+
title="Reset all experiments"
|
|
221
|
+
onPress={() => engine.resetAll()}
|
|
222
|
+
/>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function ContextUpdater() {
|
|
227
|
+
const engine = useVariantLabEngine();
|
|
228
|
+
|
|
229
|
+
const onLogin = (userId: string) => {
|
|
230
|
+
engine.updateContext({ userId });
|
|
231
|
+
};
|
|
60
232
|
|
|
61
|
-
function CardLayout() {
|
|
62
|
-
const variant = useVariant("card-layout");
|
|
63
233
|
// ...
|
|
64
234
|
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### `useRouteExperiments()` — get experiments targeting the current route
|
|
238
|
+
|
|
239
|
+
Returns only the experiments whose targeting rules match the current route (useful with Expo Router).
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
import { Text, FlatList } from "react-native";
|
|
243
|
+
import { useRouteExperiments } from "@variantlab/react-native";
|
|
244
|
+
|
|
245
|
+
function RouteExperimentsList() {
|
|
246
|
+
const experiments = useRouteExperiments();
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<FlatList
|
|
250
|
+
data={experiments}
|
|
251
|
+
renderItem={({ item }) => (
|
|
252
|
+
<Text>{item.name}: {item.variantId}</Text>
|
|
253
|
+
)}
|
|
254
|
+
/>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Components
|
|
262
|
+
|
|
263
|
+
### `<Variant>` — render-swap by variant ID
|
|
264
|
+
|
|
265
|
+
Renders the child matching the active variant. Cleaner than a switch statement when you have distinct JSX per variant.
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
import { Variant } from "@variantlab/react-native";
|
|
269
|
+
|
|
270
|
+
function OnboardingScreen() {
|
|
271
|
+
return (
|
|
272
|
+
<Variant experimentId="onboarding-flow" fallback={<ClassicOnboarding />}>
|
|
273
|
+
{{
|
|
274
|
+
classic: <ClassicOnboarding />,
|
|
275
|
+
"quick-start": <QuickStartOnboarding />,
|
|
276
|
+
}}
|
|
277
|
+
</Variant>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### `<VariantValue>` — render-prop for value experiments
|
|
283
|
+
|
|
284
|
+
Passes the experiment value to a render function.
|
|
285
|
+
|
|
286
|
+
```tsx
|
|
287
|
+
import { Text } from "react-native";
|
|
288
|
+
import { VariantValue } from "@variantlab/react-native";
|
|
289
|
+
|
|
290
|
+
function WelcomeBanner() {
|
|
291
|
+
return (
|
|
292
|
+
<VariantValue experimentId="welcome-message">
|
|
293
|
+
{(message) => <Text style={styles.banner}>{message}</Text>}
|
|
294
|
+
</VariantValue>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### `<VariantErrorBoundary>` — crash-safe experiments
|
|
300
|
+
|
|
301
|
+
Wraps an experiment in an error boundary. If a variant crashes repeatedly, the engine auto-rolls back to the default variant and renders the fallback.
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
import { Text } from "react-native";
|
|
305
|
+
import { VariantErrorBoundary } from "@variantlab/react-native";
|
|
65
306
|
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
307
|
+
function SafeCardSection() {
|
|
308
|
+
return (
|
|
309
|
+
<VariantErrorBoundary
|
|
310
|
+
experimentId="card-layout"
|
|
311
|
+
fallback={<Text>Something went wrong. Showing default layout.</Text>}
|
|
312
|
+
>
|
|
313
|
+
<CardSection />
|
|
314
|
+
</VariantErrorBoundary>
|
|
315
|
+
);
|
|
69
316
|
}
|
|
70
317
|
```
|
|
71
318
|
|
|
319
|
+
### `<VariantLabProvider>` — context provider
|
|
320
|
+
|
|
321
|
+
Wraps your app and provides the engine to all hooks and components. Must be at the top of your component tree.
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
import { VariantLabProvider } from "@variantlab/react-native";
|
|
325
|
+
import { engine } from "./variantlab";
|
|
326
|
+
|
|
327
|
+
export default function App() {
|
|
328
|
+
return (
|
|
329
|
+
<VariantLabProvider engine={engine}>
|
|
330
|
+
{/* All hooks and components work inside here */}
|
|
331
|
+
<Navigation />
|
|
332
|
+
</VariantLabProvider>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
72
339
|
## Auto-context detection
|
|
73
340
|
|
|
74
|
-
`getAutoContext()` automatically
|
|
341
|
+
`getAutoContext()` reads device info automatically so your targeting rules just work:
|
|
75
342
|
|
|
76
|
-
| Field | Source |
|
|
77
|
-
|
|
78
|
-
| `platform` | `Platform.OS`
|
|
79
|
-
| `screenSize` | `Dimensions.get("window").width`
|
|
80
|
-
| `locale` | `expo-localization` or `NativeModules` |
|
|
81
|
-
| `appVersion` | `expo-constants` or `DeviceInfo` |
|
|
343
|
+
| Field | Source | Example |
|
|
344
|
+
|-------|--------|---------|
|
|
345
|
+
| `platform` | `Platform.OS` | `"ios"`, `"android"`, `"web"` |
|
|
346
|
+
| `screenSize` | `Dimensions.get("window").width` | `"small"` (<375), `"medium"` (375-767), `"large"` (768+) |
|
|
347
|
+
| `locale` | `expo-localization` or `NativeModules` | `"en"`, `"bn"`, `"fr"` |
|
|
348
|
+
| `appVersion` | `expo-constants` or `DeviceInfo` | `"2.1.0"` |
|
|
82
349
|
|
|
83
350
|
```tsx
|
|
84
351
|
import { getAutoContext } from "@variantlab/react-native";
|
|
85
352
|
|
|
86
353
|
const context = getAutoContext();
|
|
87
|
-
// { platform: "ios", screenSize: "medium", locale: "en",
|
|
354
|
+
// { platform: "ios", screenSize: "medium", locale: "en", appVersion: "2.1.0" }
|
|
88
355
|
```
|
|
89
356
|
|
|
357
|
+
You can merge it with your own context:
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
const engine = createEngine(experiments, {
|
|
361
|
+
context: {
|
|
362
|
+
...getAutoContext(),
|
|
363
|
+
userId: "user-123",
|
|
364
|
+
attributes: { plan: "pro", country: "BD" },
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
90
371
|
## Storage adapters
|
|
91
372
|
|
|
92
|
-
|
|
373
|
+
Persist variant assignments across app restarts. Pick the one that fits your stack:
|
|
374
|
+
|
|
375
|
+
### AsyncStorage (most common)
|
|
93
376
|
|
|
94
377
|
```tsx
|
|
378
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
95
379
|
import { createAsyncStorageAdapter } from "@variantlab/react-native";
|
|
96
|
-
import { createMMKVStorageAdapter } from "@variantlab/react-native";
|
|
97
|
-
import { createSecureStoreAdapter } from "@variantlab/react-native";
|
|
98
|
-
import { createMemoryStorage } from "@variantlab/react-native";
|
|
99
380
|
|
|
100
|
-
// AsyncStorage — most common, works everywhere
|
|
101
381
|
const storage = createAsyncStorageAdapter(AsyncStorage);
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### MMKV (fastest — synchronous reads)
|
|
102
385
|
|
|
103
|
-
|
|
386
|
+
```tsx
|
|
387
|
+
import { MMKV } from "react-native-mmkv";
|
|
388
|
+
import { createMMKVStorageAdapter } from "@variantlab/react-native";
|
|
389
|
+
|
|
390
|
+
const mmkv = new MMKV();
|
|
104
391
|
const storage = createMMKVStorageAdapter(mmkv);
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### SecureStore (encrypted — for sensitive data)
|
|
395
|
+
|
|
396
|
+
```tsx
|
|
397
|
+
import * as SecureStore from "expo-secure-store";
|
|
398
|
+
import { createSecureStoreAdapter } from "@variantlab/react-native";
|
|
105
399
|
|
|
106
|
-
// SecureStore — encrypted, for sensitive experiment data
|
|
107
400
|
const storage = createSecureStoreAdapter(SecureStore);
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Memory (no persistence — for tests)
|
|
404
|
+
|
|
405
|
+
```tsx
|
|
406
|
+
import { createMemoryStorage } from "@variantlab/react-native";
|
|
108
407
|
|
|
109
|
-
// Memory — no persistence, resets on restart (good for tests)
|
|
110
408
|
const storage = createMemoryStorage();
|
|
111
409
|
```
|
|
112
410
|
|
|
411
|
+
Pass the storage to your engine:
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
const engine = createEngine(experiments, {
|
|
415
|
+
context: getAutoContext(),
|
|
416
|
+
storage, // variant assignments persist here
|
|
417
|
+
});
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
113
422
|
## Debug overlay
|
|
114
423
|
|
|
115
|
-
A
|
|
424
|
+
A floating button that opens a bottom-sheet for viewing and overriding experiments on device. Only use in development.
|
|
116
425
|
|
|
117
426
|
```tsx
|
|
118
427
|
import { VariantDebugOverlay } from "@variantlab/react-native/debug";
|
|
@@ -127,48 +436,81 @@ export default function App() {
|
|
|
127
436
|
}
|
|
128
437
|
```
|
|
129
438
|
|
|
130
|
-
|
|
131
|
-
- All active experiments
|
|
132
|
-
- Tap to
|
|
133
|
-
- Current targeting context
|
|
134
|
-
- Assignment source (default, hash, override, etc.)
|
|
439
|
+
What the overlay shows:
|
|
440
|
+
- All active experiments with their current variant
|
|
441
|
+
- Tap any experiment to switch variants
|
|
442
|
+
- Current targeting context (platform, screenSize, locale, etc.)
|
|
443
|
+
- Assignment source for each experiment (default, sticky-hash, override, etc.)
|
|
444
|
+
- Search/filter experiments
|
|
445
|
+
|
|
446
|
+
Customize the trigger position:
|
|
447
|
+
|
|
448
|
+
```tsx
|
|
449
|
+
<VariantDebugOverlay corner="bottom-left" />
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
135
453
|
|
|
136
454
|
## Deep link overrides
|
|
137
455
|
|
|
138
|
-
|
|
456
|
+
Let your QA team force variants by opening a URL:
|
|
139
457
|
|
|
140
458
|
```
|
|
141
|
-
myapp://variantlab?set=
|
|
459
|
+
myapp://variantlab?set=card-layout:compact
|
|
142
460
|
```
|
|
143
461
|
|
|
462
|
+
### Setup
|
|
463
|
+
|
|
144
464
|
```tsx
|
|
145
465
|
import { registerDeepLinkHandler } from "@variantlab/react-native";
|
|
466
|
+
import { engine } from "./variantlab";
|
|
146
467
|
|
|
147
|
-
//
|
|
468
|
+
// Call once during app initialization
|
|
148
469
|
registerDeepLinkHandler(engine);
|
|
149
470
|
```
|
|
150
471
|
|
|
472
|
+
Now opening `myapp://variantlab?set=card-layout:compact` will force the `card-layout` experiment to the `compact` variant.
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
151
476
|
## QR sharing
|
|
152
477
|
|
|
153
|
-
Share experiment state with teammates
|
|
478
|
+
Share your current experiment state with teammates — they scan the QR and get the exact same variants.
|
|
154
479
|
|
|
155
480
|
```tsx
|
|
156
481
|
import { buildQrUrl, parseQrUrl } from "@variantlab/react-native/qr";
|
|
482
|
+
import { encodeSharePayload, decodeSharePayload } from "@variantlab/react-native";
|
|
157
483
|
|
|
158
|
-
//
|
|
484
|
+
// Build a shareable URL from current assignments
|
|
485
|
+
const payload = encodeSharePayload({
|
|
486
|
+
v: 1,
|
|
487
|
+
u: "user-123",
|
|
488
|
+
a: { "card-layout": "compact", "cta-copy": "try-free" },
|
|
489
|
+
});
|
|
159
490
|
const url = buildQrUrl(payload);
|
|
491
|
+
// "variantlab://apply?p=..."
|
|
160
492
|
|
|
161
|
-
// Parse a
|
|
162
|
-
const result = parseQrUrl(
|
|
493
|
+
// Parse a received QR URL
|
|
494
|
+
const result = parseQrUrl(scannedUrl);
|
|
495
|
+
if (result.ok) {
|
|
496
|
+
// Apply the assignments to the engine
|
|
497
|
+
applyPayload(engine, result.payload);
|
|
498
|
+
}
|
|
163
499
|
```
|
|
164
500
|
|
|
165
|
-
|
|
501
|
+
---
|
|
166
502
|
|
|
167
|
-
|
|
503
|
+
## Codegen (type safety)
|
|
504
|
+
|
|
505
|
+
Generate TypeScript types so experiment IDs and variant IDs are checked at compile time:
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
npx @variantlab/cli@alpha generate
|
|
509
|
+
```
|
|
168
510
|
|
|
169
|
-
|
|
511
|
+
After codegen, `useVariant("card-layout")` returns `"standard" | "compact" | "pip-thumbnail"` as a literal type. Typos become compile errors.
|
|
170
512
|
|
|
171
|
-
|
|
513
|
+
---
|
|
172
514
|
|
|
173
515
|
## License
|
|
174
516
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@variantlab/react-native",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "React Native and Expo bindings for variantlab.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
"node": ">=18.17"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"@variantlab/core": "0.1.
|
|
75
|
-
"@variantlab/react": "0.1.
|
|
74
|
+
"@variantlab/core": "0.1.3",
|
|
75
|
+
"@variantlab/react": "0.1.3"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
78
|
"@react-native-async-storage/async-storage": "*",
|