@supashiphq/react-sdk 0.7.7
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 +724 -0
- package/dist/index.d.mts +260 -0
- package/dist/index.d.ts +260 -0
- package/dist/index.js +469 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +442 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +79 -0
package/README.md
ADDED
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
# Supaship React SDK
|
|
2
|
+
|
|
3
|
+
A React SDK for Supaship that provides hooks and components for feature flag management with full TypeScript type safety.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @supashiphq/react-sdk
|
|
9
|
+
# or
|
|
10
|
+
yarn add @supashiphq/react-sdk
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @supashiphq/react-sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { SupaProvider, useFeature, FeaturesWithFallbacks } from '@supashiphq/react-sdk'
|
|
19
|
+
|
|
20
|
+
// Define your features with type safety
|
|
21
|
+
const features = {
|
|
22
|
+
'new-header': false,
|
|
23
|
+
'theme-config': { mode: 'dark' as const, showLogo: true },
|
|
24
|
+
'beta-features': [] as string[],
|
|
25
|
+
} satisfies FeaturesWithFallbacks
|
|
26
|
+
|
|
27
|
+
function App() {
|
|
28
|
+
return (
|
|
29
|
+
<SupaProvider
|
|
30
|
+
config={{
|
|
31
|
+
apiKey: 'your-api-key',
|
|
32
|
+
environment: 'production',
|
|
33
|
+
features,
|
|
34
|
+
context: {
|
|
35
|
+
userID: '123',
|
|
36
|
+
email: 'user@example.com',
|
|
37
|
+
},
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<YourApp />
|
|
41
|
+
</SupaProvider>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function YourApp() {
|
|
46
|
+
// Hook returns { feature, isLoading, error, ... }
|
|
47
|
+
const { feature: newHeader, isLoading } = useFeature('new-header')
|
|
48
|
+
|
|
49
|
+
if (isLoading) return <div>Loading...</div>
|
|
50
|
+
|
|
51
|
+
return <div>{newHeader ? <NewHeader /> : <OldHeader />}</div>
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Type-Safe Feature Flags
|
|
56
|
+
|
|
57
|
+
For full TypeScript type safety, define your features and augment the `Features` interface:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
// lib/features.ts
|
|
61
|
+
import { FeaturesWithFallbacks, InferFeatures } from '@supashiphq/react-sdk'
|
|
62
|
+
|
|
63
|
+
export const FEATURE_FLAGS = {
|
|
64
|
+
'new-header': false,
|
|
65
|
+
'theme-config': {
|
|
66
|
+
mode: 'dark' as const,
|
|
67
|
+
primaryColor: '#007bff',
|
|
68
|
+
showLogo: true,
|
|
69
|
+
},
|
|
70
|
+
'beta-features': [] as string[],
|
|
71
|
+
'disabled-feature': null,
|
|
72
|
+
} satisfies FeaturesWithFallbacks
|
|
73
|
+
|
|
74
|
+
// Type augmentation for global type safety, it is required
|
|
75
|
+
declare module '@supashiphq/react-sdk' {
|
|
76
|
+
interface Features extends InferFeatures<typeof FEATURE_FLAGS> {}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Now `useFeature` and `useFeatures` will have full type safety:
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
function MyComponent() {
|
|
84
|
+
// TypeScript knows 'new-header' is valid and feature is boolean
|
|
85
|
+
const { feature } = useFeature('new-header')
|
|
86
|
+
|
|
87
|
+
// TypeScript knows 'theme-config' returns the exact object shape
|
|
88
|
+
const { feature: config } = useFeature('theme-config')
|
|
89
|
+
// config is { mode: 'dark' | 'light', primaryColor: string, showLogo: boolean }
|
|
90
|
+
|
|
91
|
+
// TypeScript will error on invalid feature names
|
|
92
|
+
const { feature: invalid } = useFeature('non-existent-feature') // ❌ Type error
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
[See detailed type-safe usage guide](./TYPE_SAFE_FEATURES.md)
|
|
97
|
+
|
|
98
|
+
## API Reference
|
|
99
|
+
|
|
100
|
+
### SupaProvider
|
|
101
|
+
|
|
102
|
+
The provider component that makes feature flags available to your React component tree.
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<SupaProvider config={config}>{children}</SupaProvider>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Props:**
|
|
109
|
+
|
|
110
|
+
| Prop | Type | Required | Description |
|
|
111
|
+
| ---------- | ------------------ | -------- | ---------------------------- |
|
|
112
|
+
| `config` | `SupaClientConfig` | Yes | Configuration for the client |
|
|
113
|
+
| `children` | `React.ReactNode` | Yes | Child components |
|
|
114
|
+
| `plugins` | `SupaPlugin[]` | No | Custom plugins |
|
|
115
|
+
| `toolbar` | `ToolbarConfig` | No | Development toolbar settings |
|
|
116
|
+
|
|
117
|
+
**Configuration Options:**
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { createFeatures } from '@supashiphq/react-sdk'
|
|
121
|
+
|
|
122
|
+
const config = {
|
|
123
|
+
apiKey: 'your-api-key',
|
|
124
|
+
environment: 'production',
|
|
125
|
+
features: createFeatures({
|
|
126
|
+
// Required: define all feature flags with fallback values
|
|
127
|
+
'my-feature': false,
|
|
128
|
+
config: { theme: 'light' },
|
|
129
|
+
}),
|
|
130
|
+
context: {
|
|
131
|
+
// Optional: targeting context
|
|
132
|
+
userID: 'user-123',
|
|
133
|
+
email: 'user@example.com',
|
|
134
|
+
plan: 'premium',
|
|
135
|
+
},
|
|
136
|
+
networkConfig: {
|
|
137
|
+
// Optional: network settings
|
|
138
|
+
featuresAPIUrl: 'https://api.supashiphq.com/features',
|
|
139
|
+
retry: {
|
|
140
|
+
enabled: true,
|
|
141
|
+
maxAttempts: 3,
|
|
142
|
+
backoff: 1000,
|
|
143
|
+
},
|
|
144
|
+
requestTimeoutMs: 5000,
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Supported Feature Value Types:**
|
|
150
|
+
|
|
151
|
+
| Type | Example | Description |
|
|
152
|
+
| --------- | ----------------------------------- | ------------------------- |
|
|
153
|
+
| `boolean` | `false` | Simple on/off flags |
|
|
154
|
+
| `object` | `{ theme: 'dark', showLogo: true }` | Configuration objects |
|
|
155
|
+
| `array` | `['feature-a', 'feature-b']` | Lists of values |
|
|
156
|
+
| `null` | `null` | Disabled/unavailable flag |
|
|
157
|
+
|
|
158
|
+
> **Note:** Strings and numbers are not supported as standalone feature values. Use objects instead: `{ value: 'string' }` or `{ value: 42 }`.
|
|
159
|
+
|
|
160
|
+
### useFeature Hook
|
|
161
|
+
|
|
162
|
+
Retrieves a single feature flag value with React state management and full TypeScript type safety.
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
const result = useFeature(featureName, options?)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Parameters:**
|
|
169
|
+
|
|
170
|
+
- `featureName: string` - The feature flag key
|
|
171
|
+
- `options?: object`
|
|
172
|
+
- `context?: Record<string, unknown>` - Context override for this request
|
|
173
|
+
- `shouldFetch?: boolean` - Whether to fetch the feature (default: true)
|
|
174
|
+
|
|
175
|
+
**Return Value:**
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
{
|
|
179
|
+
feature: T, // The feature value (typed based on your Features interface)
|
|
180
|
+
isLoading: boolean, // Loading state
|
|
181
|
+
isSuccess: boolean, // Success state
|
|
182
|
+
isError: boolean, // Error state
|
|
183
|
+
error: Error | null, // Error object if failed
|
|
184
|
+
status: 'idle' | 'loading' | 'success' | 'error',
|
|
185
|
+
refetch: () => void, // Function to manually refetch
|
|
186
|
+
// ... other query state properties
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Examples:**
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
function MyComponent() {
|
|
194
|
+
// Simple boolean feature
|
|
195
|
+
const { feature: isEnabled, isLoading } = useFeature('new-ui')
|
|
196
|
+
|
|
197
|
+
if (isLoading) return <Skeleton />
|
|
198
|
+
|
|
199
|
+
return <div>{isEnabled ? <NewUI /> : <OldUI />}</div>
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function ConfigComponent() {
|
|
203
|
+
// Object feature
|
|
204
|
+
const { feature: config } = useFeature('theme-config')
|
|
205
|
+
|
|
206
|
+
if (!config) return null
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div className={config.theme}>
|
|
210
|
+
{config.showLogo && <Logo />}
|
|
211
|
+
<div style={{ color: config.primaryColor }}>Content</div>
|
|
212
|
+
</div>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function ConditionalFetch() {
|
|
217
|
+
const { user, isLoading: userLoading } = useUser()
|
|
218
|
+
|
|
219
|
+
// Only fetch when user is loaded
|
|
220
|
+
const { feature } = useFeature('user-specific-feature', {
|
|
221
|
+
context: { userId: user?.id },
|
|
222
|
+
shouldFetch: !userLoading && !!user,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
return <div>{feature && <SpecialContent />}</div>
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### useFeatures Hook
|
|
230
|
+
|
|
231
|
+
Retrieves multiple feature flags in a single request with type safety.
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
const result = useFeatures(featureNames, options?)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Parameters:**
|
|
238
|
+
|
|
239
|
+
- `featureNames: readonly string[]` - Array of feature flag keys
|
|
240
|
+
- `options?: object`
|
|
241
|
+
- `context?: Record<string, unknown>` - Context override for this request
|
|
242
|
+
- `shouldFetch?: boolean` - Whether to fetch features (default: true)
|
|
243
|
+
|
|
244
|
+
**Return Value:**
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
{
|
|
248
|
+
features: { [key: string]: T }, // Object with feature values (typed based on keys)
|
|
249
|
+
isLoading: boolean,
|
|
250
|
+
isSuccess: boolean,
|
|
251
|
+
isError: boolean,
|
|
252
|
+
error: Error | null,
|
|
253
|
+
status: 'idle' | 'loading' | 'success' | 'error',
|
|
254
|
+
refetch: () => void,
|
|
255
|
+
// ... other query state properties
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Examples:**
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
function Dashboard() {
|
|
263
|
+
const { user } = useUser()
|
|
264
|
+
|
|
265
|
+
// Fetch multiple features at once (more efficient than multiple useFeature calls)
|
|
266
|
+
const { features, isLoading } = useFeatures(['new-dashboard', 'beta-mode', 'show-sidebar'], {
|
|
267
|
+
context: {
|
|
268
|
+
userId: user?.id,
|
|
269
|
+
plan: user?.plan,
|
|
270
|
+
},
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
if (isLoading) return <LoadingSpinner />
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<div className={features['new-dashboard'] ? 'new-layout' : 'old-layout'}>
|
|
277
|
+
{features['show-sidebar'] && <Sidebar />}
|
|
278
|
+
{features['beta-mode'] && <BetaBadge />}
|
|
279
|
+
<MainContent />
|
|
280
|
+
</div>
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function FeatureList() {
|
|
285
|
+
// TypeScript will infer the correct types for each feature
|
|
286
|
+
const { features } = useFeatures(['feature-a', 'feature-b', 'config-feature'])
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<div>
|
|
290
|
+
{features['feature-a'] && <FeatureA />}
|
|
291
|
+
{features['feature-b'] && <FeatureB />}
|
|
292
|
+
{features['config-feature'] && <ConfigDisplay config={features['config-feature']} />}
|
|
293
|
+
</div>
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### useFeatureContext Hook
|
|
299
|
+
|
|
300
|
+
Access and update the feature context within components.
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
const { context, updateContext } = useFeatureContext()
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Example:**
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
function UserProfileSettings() {
|
|
310
|
+
const { context, updateContext } = useFeatureContext()
|
|
311
|
+
const [user, setUser] = useState(null)
|
|
312
|
+
|
|
313
|
+
const handleUserUpdate = newUser => {
|
|
314
|
+
setUser(newUser)
|
|
315
|
+
|
|
316
|
+
// Update feature context when user changes
|
|
317
|
+
// This will trigger refetch of all features
|
|
318
|
+
updateContext({
|
|
319
|
+
userId: newUser.id,
|
|
320
|
+
plan: newUser.subscriptionPlan,
|
|
321
|
+
segment: newUser.segment,
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return <form onSubmit={handleUserUpdate}>{/* User profile form */}</form>
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### useClient Hook
|
|
330
|
+
|
|
331
|
+
Access the underlying SupaClient instance for advanced use cases.
|
|
332
|
+
|
|
333
|
+
```tsx
|
|
334
|
+
const client = useClient()
|
|
335
|
+
|
|
336
|
+
// Use client methods directly
|
|
337
|
+
const feature = await client.getFeature('my-feature', { context: { ... } })
|
|
338
|
+
const features = await client.getFeatures(['feature-1', 'feature-2'])
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Best Practices
|
|
342
|
+
|
|
343
|
+
### 1. Always Use `satisfies` for Feature Definitions
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
// ✅ Good - preserves literal types
|
|
347
|
+
const features = {
|
|
348
|
+
'dark-mode': false,
|
|
349
|
+
theme: { mode: 'light' as const, variant: 'compact' as const },
|
|
350
|
+
} satisfies FeaturesWithFallbacks
|
|
351
|
+
|
|
352
|
+
// ❌ Bad - loses literal types (don't use type annotation)
|
|
353
|
+
const features: FeaturesWithFallbacks = {
|
|
354
|
+
'dark-mode': false,
|
|
355
|
+
theme: { mode: 'light', variant: 'compact' }, // Types widened to string
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### 2. Centralize Feature Definitions
|
|
360
|
+
|
|
361
|
+
```tsx
|
|
362
|
+
// ✅ Good - centralized feature definitions
|
|
363
|
+
// lib/features.ts
|
|
364
|
+
export const FEATURE_FLAGS = {
|
|
365
|
+
'new-header': false,
|
|
366
|
+
theme: { mode: 'light' as const },
|
|
367
|
+
'beta-features': [] as string[],
|
|
368
|
+
} satisfies FeaturesWithFallbacks
|
|
369
|
+
|
|
370
|
+
// ❌ Bad - scattered feature definitions
|
|
371
|
+
const config1 = { features: { 'feature-1': false } satisfies FeaturesWithFallbacks }
|
|
372
|
+
const config2 = { features: { 'feature-2': true } satisfies FeaturesWithFallbacks }
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### 3. Use Type Augmentation for Type Safety
|
|
376
|
+
|
|
377
|
+
```tsx
|
|
378
|
+
// ✅ Good - type augmentation for global type safety
|
|
379
|
+
declare module '@supashiphq/react-sdk' {
|
|
380
|
+
interface Features extends InferFeatures<typeof FEATURE_FLAGS> {}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Now all useFeature calls are type-safe
|
|
384
|
+
const { feature } = useFeature('new-header') // ✅ TypeScript knows this is boolean
|
|
385
|
+
const { feature } = useFeature('invalid') // ❌ TypeScript error
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### 3. Use Context for User Targeting
|
|
389
|
+
|
|
390
|
+
```tsx
|
|
391
|
+
function App() {
|
|
392
|
+
const { user } = useAuth()
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
|
+
<SupaProvider
|
|
396
|
+
config={{
|
|
397
|
+
apiKey: 'your-api-key',
|
|
398
|
+
features: FEATURE_FLAGS,
|
|
399
|
+
context: {
|
|
400
|
+
userId: user?.id,
|
|
401
|
+
email: user?.email,
|
|
402
|
+
plan: user?.subscriptionPlan,
|
|
403
|
+
version: process.env.REACT_APP_VERSION,
|
|
404
|
+
},
|
|
405
|
+
}}
|
|
406
|
+
>
|
|
407
|
+
<YourApp />
|
|
408
|
+
</SupaProvider>
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### 4. Batch Feature Requests
|
|
414
|
+
|
|
415
|
+
```tsx
|
|
416
|
+
// ✅ Good - single API call
|
|
417
|
+
const { features } = useFeatures(['feature-1', 'feature-2', 'feature-3'])
|
|
418
|
+
|
|
419
|
+
// ❌ Less efficient - multiple API calls
|
|
420
|
+
const feature1 = useFeature('feature-1')
|
|
421
|
+
const feature2 = useFeature('feature-2')
|
|
422
|
+
const feature3 = useFeature('feature-3')
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### 5. Handle Loading States
|
|
426
|
+
|
|
427
|
+
```tsx
|
|
428
|
+
function MyComponent() {
|
|
429
|
+
const { user, isLoading: userLoading } = useUser()
|
|
430
|
+
|
|
431
|
+
const { features, isLoading: featuresLoading } = useFeatures(['user-specific-feature'], {
|
|
432
|
+
context: { userId: user?.id },
|
|
433
|
+
shouldFetch: !userLoading && !!user,
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
if (userLoading || featuresLoading) return <Skeleton />
|
|
437
|
+
|
|
438
|
+
return <div>{features['user-specific-feature'] && <SpecialContent />}</div>
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### 6. Update Context Reactively
|
|
443
|
+
|
|
444
|
+
```tsx
|
|
445
|
+
function UserDashboard() {
|
|
446
|
+
const { updateContext } = useFeatureContext()
|
|
447
|
+
const [currentPage, setCurrentPage] = useState('dashboard')
|
|
448
|
+
|
|
449
|
+
// Update context when navigation changes
|
|
450
|
+
useEffect(() => {
|
|
451
|
+
updateContext({ currentPage })
|
|
452
|
+
}, [currentPage, updateContext])
|
|
453
|
+
|
|
454
|
+
return (
|
|
455
|
+
<div>
|
|
456
|
+
<Navigation onPageChange={setCurrentPage} />
|
|
457
|
+
<PageContent page={currentPage} />
|
|
458
|
+
</div>
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
## Framework Integration
|
|
464
|
+
|
|
465
|
+
### Next.js App Router (Next.js 13+)
|
|
466
|
+
|
|
467
|
+
```tsx
|
|
468
|
+
// app/providers.tsx
|
|
469
|
+
'use client'
|
|
470
|
+
import { SupaProvider, FeaturesWithFallbacks } from '@supashiphq/react-sdk'
|
|
471
|
+
|
|
472
|
+
const FEATURE_FLAGS = {
|
|
473
|
+
'new-hero': false,
|
|
474
|
+
theme: { mode: 'light' as const },
|
|
475
|
+
} satisfies FeaturesWithFallbacks
|
|
476
|
+
|
|
477
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
478
|
+
return (
|
|
479
|
+
<SupaProvider
|
|
480
|
+
config={{
|
|
481
|
+
apiKey: process.env.NEXT_PUBLIC_SUPASHIP_API_KEY!,
|
|
482
|
+
environment: process.env.NODE_ENV!,
|
|
483
|
+
features: FEATURE_FLAGS,
|
|
484
|
+
}}
|
|
485
|
+
>
|
|
486
|
+
{children}
|
|
487
|
+
</SupaProvider>
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// app/layout.tsx
|
|
492
|
+
import { Providers } from './providers'
|
|
493
|
+
|
|
494
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
495
|
+
return (
|
|
496
|
+
<html lang="en">
|
|
497
|
+
<body>
|
|
498
|
+
<Providers>{children}</Providers>
|
|
499
|
+
</body>
|
|
500
|
+
</html>
|
|
501
|
+
)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// app/page.tsx
|
|
505
|
+
;('use client')
|
|
506
|
+
import { useFeature } from '@supashiphq/react-sdk'
|
|
507
|
+
|
|
508
|
+
export default function HomePage() {
|
|
509
|
+
const { feature: newHero } = useFeature('new-hero')
|
|
510
|
+
|
|
511
|
+
return <main>{newHero ? <NewHeroSection /> : <OldHeroSection />}</main>
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Next.js Pages Router (Next.js 12 and below)
|
|
516
|
+
|
|
517
|
+
```tsx
|
|
518
|
+
// pages/_app.tsx
|
|
519
|
+
import { SupaProvider, FeaturesWithFallbacks } from '@supashiphq/react-sdk'
|
|
520
|
+
import type { AppProps } from 'next/app'
|
|
521
|
+
|
|
522
|
+
const FEATURE_FLAGS = {
|
|
523
|
+
'new-homepage': false,
|
|
524
|
+
} satisfies FeaturesWithFallbacks
|
|
525
|
+
|
|
526
|
+
export default function App({ Component, pageProps }: AppProps) {
|
|
527
|
+
return (
|
|
528
|
+
<SupaProvider
|
|
529
|
+
config={{
|
|
530
|
+
apiKey: process.env.NEXT_PUBLIC_SUPASHIP_API_KEY!,
|
|
531
|
+
environment: process.env.NODE_ENV!,
|
|
532
|
+
features: FEATURE_FLAGS,
|
|
533
|
+
}}
|
|
534
|
+
>
|
|
535
|
+
<Component {...pageProps} />
|
|
536
|
+
</SupaProvider>
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### Vite / Create React App
|
|
542
|
+
|
|
543
|
+
```tsx
|
|
544
|
+
// src/main.tsx or src/index.tsx
|
|
545
|
+
import { SupaProvider, FeaturesWithFallbacks } from '@supashiphq/react-sdk'
|
|
546
|
+
|
|
547
|
+
const FEATURE_FLAGS = {
|
|
548
|
+
'new-ui': false,
|
|
549
|
+
theme: { mode: 'light' as const },
|
|
550
|
+
} satisfies FeaturesWithFallbacks
|
|
551
|
+
|
|
552
|
+
function App() {
|
|
553
|
+
return (
|
|
554
|
+
<SupaProvider
|
|
555
|
+
config={{
|
|
556
|
+
apiKey: import.meta.env.VITE_SUPASHIP_API_KEY, // Vite
|
|
557
|
+
// or
|
|
558
|
+
apiKey: process.env.REACT_APP_SUPASHIP_API_KEY, // CRA
|
|
559
|
+
environment: import.meta.env.MODE,
|
|
560
|
+
features: FEATURE_FLAGS,
|
|
561
|
+
}}
|
|
562
|
+
>
|
|
563
|
+
<YourApp />
|
|
564
|
+
</SupaProvider>
|
|
565
|
+
)
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
## Development Toolbar
|
|
570
|
+
|
|
571
|
+
The SDK includes a development toolbar for testing and debugging feature flags locally.
|
|
572
|
+
|
|
573
|
+
```tsx
|
|
574
|
+
<SupaProvider
|
|
575
|
+
config={{ ... }}
|
|
576
|
+
toolbar={{
|
|
577
|
+
enabled: 'auto', // 'auto' | 'always' | 'never'
|
|
578
|
+
position: 'bottom-right', // 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
|
|
579
|
+
}}
|
|
580
|
+
>
|
|
581
|
+
<YourApp />
|
|
582
|
+
</SupaProvider>
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
- `'auto'`: Shows toolbar in development environments only (default)
|
|
586
|
+
- `true`: Always shows toolbar
|
|
587
|
+
- `false`: Never shows toolbar
|
|
588
|
+
|
|
589
|
+
The toolbar allows you to:
|
|
590
|
+
|
|
591
|
+
- View all available feature flags
|
|
592
|
+
- Override feature values locally
|
|
593
|
+
- See feature value types and current values
|
|
594
|
+
- Clear local overrides
|
|
595
|
+
|
|
596
|
+
## Testing
|
|
597
|
+
|
|
598
|
+
### Mocking Feature Flags in Tests
|
|
599
|
+
|
|
600
|
+
```tsx
|
|
601
|
+
// test-utils/providers.tsx
|
|
602
|
+
import { SupaProvider, FeaturesWithFallbacks } from '@supashiphq/react-sdk'
|
|
603
|
+
|
|
604
|
+
export function TestProviders({ children, features = {} as FeaturesWithFallbacks }) {
|
|
605
|
+
return (
|
|
606
|
+
<SupaProvider
|
|
607
|
+
config={{
|
|
608
|
+
apiKey: 'test-key',
|
|
609
|
+
environment: 'test',
|
|
610
|
+
features,
|
|
611
|
+
context: {},
|
|
612
|
+
}}
|
|
613
|
+
>
|
|
614
|
+
{children}
|
|
615
|
+
</SupaProvider>
|
|
616
|
+
)
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### Example Test
|
|
621
|
+
|
|
622
|
+
```tsx
|
|
623
|
+
// MyComponent.test.tsx
|
|
624
|
+
import { render, screen } from '@testing-library/react'
|
|
625
|
+
import { TestProviders } from '../test-utils/providers'
|
|
626
|
+
import MyComponent from './MyComponent'
|
|
627
|
+
|
|
628
|
+
describe('MyComponent', () => {
|
|
629
|
+
it('shows new feature when enabled', () => {
|
|
630
|
+
render(
|
|
631
|
+
<TestProviders features={{ 'new-feature': true }}>
|
|
632
|
+
<MyComponent />
|
|
633
|
+
</TestProviders>
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
expect(screen.getByText('New Feature Content')).toBeInTheDocument()
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
it('shows old feature when disabled', () => {
|
|
640
|
+
render(
|
|
641
|
+
<TestProviders features={{ 'new-feature': false }}>
|
|
642
|
+
<MyComponent />
|
|
643
|
+
</TestProviders>
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
expect(screen.getByText('Old Feature Content')).toBeInTheDocument()
|
|
647
|
+
})
|
|
648
|
+
})
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
## Troubleshooting
|
|
652
|
+
|
|
653
|
+
### Common Issues
|
|
654
|
+
|
|
655
|
+
#### Type errors with FeaturesWithFallbacks
|
|
656
|
+
|
|
657
|
+
If you encounter type errors when defining features, ensure you're using the correct pattern:
|
|
658
|
+
|
|
659
|
+
**Solution:** Always use `satisfies FeaturesWithFallbacks` (not type annotation)
|
|
660
|
+
|
|
661
|
+
```tsx
|
|
662
|
+
// ✅ Good - preserves literal types
|
|
663
|
+
const features = {
|
|
664
|
+
'my-feature': false,
|
|
665
|
+
config: { theme: 'dark' as const },
|
|
666
|
+
} satisfies FeaturesWithFallbacks
|
|
667
|
+
|
|
668
|
+
// ❌ Bad - loses literal types
|
|
669
|
+
const features: FeaturesWithFallbacks = {
|
|
670
|
+
'my-feature': false,
|
|
671
|
+
config: { theme: 'dark' }, // Widened to string
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
#### Provider Not Found Error
|
|
676
|
+
|
|
677
|
+
```
|
|
678
|
+
Error: useFeature must be used within a SupaProvider
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
**Solution:** Ensure your component is wrapped in a `SupaProvider`:
|
|
682
|
+
|
|
683
|
+
```tsx
|
|
684
|
+
// ✅ Correct
|
|
685
|
+
function App() {
|
|
686
|
+
return (
|
|
687
|
+
<SupaProvider config={{ ... }}>
|
|
688
|
+
<MyComponent />
|
|
689
|
+
</SupaProvider>
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// ❌ Incorrect
|
|
694
|
+
function App() {
|
|
695
|
+
return <MyComponent /> // Missing provider
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
#### Features Not Loading
|
|
700
|
+
|
|
701
|
+
- **Check API key:** Verify your API key is correct
|
|
702
|
+
- **Check network:** Open browser dev tools and check network requests
|
|
703
|
+
- **Check features config:** Ensure features are defined in the config
|
|
704
|
+
|
|
705
|
+
#### Type Errors
|
|
706
|
+
|
|
707
|
+
```
|
|
708
|
+
Property 'my-feature' does not exist on type 'Features'
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
**Solution:** Add type augmentation:
|
|
712
|
+
|
|
713
|
+
```tsx
|
|
714
|
+
import { InferFeatures } from '@supashiphq/react-sdk'
|
|
715
|
+
import { FEATURE_FLAGS } from './features'
|
|
716
|
+
|
|
717
|
+
declare module '@supashiphq/react-sdk' {
|
|
718
|
+
interface Features extends InferFeatures<typeof FEATURE_FLAGS> {}
|
|
719
|
+
}
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
## License
|
|
723
|
+
|
|
724
|
+
This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
|