@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/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.