@geejay/use-feature-flags 1.0.13 → 1.0.19
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 +158 -20
- package/dist/index.cjs +131 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -2
- package/dist/index.js +126 -2
- package/dist/index.js.map +1 -0
- package/package.json +22 -15
- package/dist/store.d.ts +0 -17
- package/dist/store.js +0 -13
- package/dist/useFeatureFlags.d.ts +0 -8
- package/dist/useFeatureFlags.js +0 -104
package/README.md
CHANGED
|
@@ -1,41 +1,179 @@
|
|
|
1
|
-
# use-feature-flags
|
|
2
|
-
|
|
3
|
-
A lightweight React hook for fetching feature flags from Supabase. It ships with sensible defaults so you can drop it in and start using feature flags immediately.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
1
|
+
# @geejay/use-feature-flags
|
|
6
2
|
|
|
7
3
|
```
|
|
8
|
-
|
|
4
|
+
_____ ___ ____ ___ _ ____
|
|
5
|
+
|_ _/ _ \| __ )_ _| / \ / ___|
|
|
6
|
+
| || | | | _ \| | / _ \ \___ \
|
|
7
|
+
| || |_| | |_) | | / ___ \ ___) |
|
|
8
|
+
|_| \___/|____/___/_/ \_\____/
|
|
9
|
+
|
|
10
|
+
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
**TOBIAS** — the **T**otally **O**ptimized **B**ot for **I**ntelligent **A**pp **S**witching (Safe).
|
|
14
|
+
A tiny React hook that lets you manage feature flags **at runtime** across React, React Native, and React Native Web—no redeploys required.
|
|
12
15
|
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## ✨ What you get
|
|
19
|
+
|
|
20
|
+
* **Drop-in hook**: `useFeatureFlags()` returns `isActive(flag)` and `loading`.
|
|
21
|
+
* **Instant toggles**: Flip features on/off without reloading your app.
|
|
22
|
+
* **Multi-environment**: Scope flags to `dev`, `staging`, `prod`, previews, etc.
|
|
23
|
+
* **Multi-tenant**: Keep flags organized per customer or workspace.
|
|
24
|
+
* **Friendly UI**: Manage flags from a simple web dashboard.
|
|
25
|
+
* **TOBIAS Cleanup**: Our companion bot **scans for stale flags** and **opens PRs** to safely remove them, so your codebase stays clean without the busywork.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 🚀 Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @geejay/use-feature-flags
|
|
33
|
+
# or
|
|
34
|
+
yarn add @geejay/use-feature-flags
|
|
13
35
|
```
|
|
14
|
-
yarn add use-feature-flags
|
|
15
|
-
```
|
|
16
36
|
|
|
17
|
-
|
|
37
|
+
---
|
|
38
|
+
Here’s the updated **Quick Start** section with your note about the environment parameter baked in — so readers know it’s **usually not needed**.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## ⚡️ Quick start
|
|
43
|
+
|
|
44
|
+
> **One-time init:** Pass your API key on the **first** call anywhere in your app.
|
|
45
|
+
> **Thereafter:** Just call `useFeatureFlags()` with **no arguments** — the key is remembered.
|
|
46
|
+
|
|
47
|
+
The `environment` parameter is **optional** — in most cases you don’t need it.
|
|
48
|
+
If omitted, the app will **auto-detect** based on the domain:
|
|
49
|
+
|
|
50
|
+
* **Web**: Uses the current domain (e.g. `myapp.com`, `staging.myapp.com`)
|
|
51
|
+
* **Local dev**: Uses `"localhost"` when testing locally
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### Example: first call (e.g., in your root)
|
|
18
56
|
|
|
19
57
|
```tsx
|
|
20
|
-
|
|
58
|
+
// App.tsx
|
|
59
|
+
import { useFeatureFlags } from '@geejay/use-feature-flags';
|
|
21
60
|
|
|
22
|
-
export default function
|
|
23
|
-
|
|
61
|
+
export default function App() {
|
|
62
|
+
// ✅ Initialize once with your API key
|
|
63
|
+
// Environment is optional — auto-detects in most cases
|
|
64
|
+
const { loading } = useFeatureFlags("YOUR-API-KEY");
|
|
24
65
|
|
|
25
66
|
if (loading) return null;
|
|
67
|
+
return <MainRoutes />;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### Example: subsequent calls (no key needed)
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
// components/Dashboard.tsx
|
|
77
|
+
import { useFeatureFlags } from '@geejay/use-feature-flags';
|
|
78
|
+
|
|
79
|
+
export default function Dashboard() {
|
|
80
|
+
// ✅ No API key here — it’s already remembered
|
|
81
|
+
const { isActive, loading } = useFeatureFlags();
|
|
26
82
|
|
|
83
|
+
if (loading) return null;
|
|
27
84
|
return isActive('new-dashboard') ? <NewDashboard /> : <OldDashboard />;
|
|
28
85
|
}
|
|
29
86
|
```
|
|
30
87
|
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
### Another component, still no key
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// components/Header.tsx
|
|
94
|
+
import { useFeatureFlags } from '@geejay/use-feature-flags';
|
|
95
|
+
|
|
96
|
+
export function Header() {
|
|
97
|
+
const { isActive } = useFeatureFlags(); // ✅ key remembered globally
|
|
98
|
+
return isActive('show-announcement') ? <Banner /> : null;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
#### Notes
|
|
105
|
+
|
|
106
|
+
* Pass the API key **only once** — the hook remembers it for all future calls in the same session.
|
|
107
|
+
* Environment is **usually not required** — the hook will detect it automatically.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
**Tips**
|
|
112
|
+
|
|
113
|
+
* Call `useFeatureFlags("YOUR-API-KEY", "production")` if you want to force the app to use a specific custom environment. Maybe useful for mobile users??
|
|
114
|
+
* You can read multiple flags; `isActive` is stable and fast.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 🧰 Flag management
|
|
119
|
+
|
|
120
|
+
Use the hosted dashboard to:
|
|
121
|
+
|
|
122
|
+
* Create/edit/delete flags
|
|
123
|
+
* Group by environment and tenant
|
|
124
|
+
* Toggle in real time and see changes reflected instantly
|
|
125
|
+
|
|
126
|
+
Get started: **[https://featureflags-ai.netlify.app/](https://featureflags-ai.netlify.app/)**
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 🤖 TOBIAS: automatic flag cleanup PRs
|
|
131
|
+
|
|
132
|
+
Your team shouldn’t spend Fridays hunting down dead flags.
|
|
133
|
+
**TOBIAS** keeps your repo tidy by:
|
|
134
|
+
|
|
135
|
+
* scanning for **stale/retired flags**
|
|
136
|
+
* generating **safe diffs** (removing unused checks, dead branches, and config)
|
|
137
|
+
* opening **pull requests** with a friendly summary and checklist
|
|
138
|
+
|
|
139
|
+
Example PR title:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
🧹 TOBIAS: remove stale flags (billing_v2, heroA/B)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Example PR body snippet:
|
|
146
|
+
|
|
147
|
+
```md
|
|
148
|
+
This automated PR removes stale flags detected by TOBIAS.
|
|
149
|
+
|
|
150
|
+
- Removed: `billing_v2`, `heroA/B`
|
|
151
|
+
- Updated: related conditionals and config
|
|
152
|
+
- Notes: All changes are no-op at runtime
|
|
153
|
+
|
|
154
|
+
Review checklist:
|
|
155
|
+
- [ ] CI passes
|
|
156
|
+
- [ ] Screens relying on removed flags look correct
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 🧪 Local dev notes
|
|
162
|
+
|
|
163
|
+
* Works with React 18 (including Strict Mode).
|
|
164
|
+
* Designed for modern bundlers (Next.js, Vite, Expo/Metro).
|
|
165
|
+
* If you test the library locally via `yalc`, remember to remove the yalc reference before deploying your app.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 🤝 Contributing
|
|
170
|
+
|
|
171
|
+
Issues and PRs welcome! If you’ve got ideas for smarter cleanup rules or integrations, open a ticket—TOBIAS loves new tricks.
|
|
31
172
|
|
|
173
|
+
---
|
|
32
174
|
|
|
33
|
-
##
|
|
175
|
+
## 📄 License
|
|
34
176
|
|
|
35
|
-
|
|
36
|
-
- one-step install for your react/react-native/react-native-web apps
|
|
37
|
-
- toggles features on/off WITHOUT reload of your REACT app
|
|
38
|
-
- supports multiple environments/ subdomains from one UI
|
|
39
|
-
- supports multiple tenants
|
|
40
|
-
- bi-directional creation of flags and more!
|
|
177
|
+
MIT © GeeJay
|
|
41
178
|
|
|
179
|
+
---
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jotai = require('jotai');
|
|
5
|
+
var supabaseJs = require('@supabase/supabase-js');
|
|
6
|
+
|
|
7
|
+
// src/useFeatureFlags.ts
|
|
8
|
+
var featureFlagsAtom = jotai.atom({
|
|
9
|
+
flags: [],
|
|
10
|
+
loading: true
|
|
11
|
+
});
|
|
12
|
+
var apiKey = null;
|
|
13
|
+
function setApiKey(key) {
|
|
14
|
+
apiKey = key;
|
|
15
|
+
}
|
|
16
|
+
function getApiKey() {
|
|
17
|
+
return apiKey;
|
|
18
|
+
}
|
|
19
|
+
var DEFAULT_SUPABASE_URL = "https://khppgsehvvlukzfdqbuo.supabase.co";
|
|
20
|
+
var DEFAULT_SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtocHBnc2VodnZsdWt6ZmRxYnVvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTI2ODU1MzQsImV4cCI6MjA2ODI2MTUzNH0.8Z4VY4HFMm95UgO21c-DnDkbLPN_0mbDBZJPExaghDk";
|
|
21
|
+
var _supabase = null;
|
|
22
|
+
function getSupabase() {
|
|
23
|
+
if (_supabase) return _supabase;
|
|
24
|
+
_supabase = supabaseJs.createClient(DEFAULT_SUPABASE_URL, DEFAULT_SUPABASE_KEY, {
|
|
25
|
+
auth: {
|
|
26
|
+
persistSession: false,
|
|
27
|
+
autoRefreshToken: false,
|
|
28
|
+
storageKey: "feature-flags"
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return _supabase;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/useFeatureFlags.ts
|
|
35
|
+
var EDGE_FN_URL = `${DEFAULT_SUPABASE_URL}/functions/v1/get-feature-flags`;
|
|
36
|
+
var initialized = false;
|
|
37
|
+
function useFeatureFlags(passedKey, environment = window.location.hostname || "localhost") {
|
|
38
|
+
const sanitizedEnvironment = react.useMemo(
|
|
39
|
+
() => environment.replace(/:\d+$/, ""),
|
|
40
|
+
[environment]
|
|
41
|
+
);
|
|
42
|
+
const [state, setState] = jotai.useAtom(featureFlagsAtom);
|
|
43
|
+
const [envId, setEnvId] = react.useState(null);
|
|
44
|
+
const debounceTimeout = react.useRef(null);
|
|
45
|
+
const cleanupRef = react.useRef(null);
|
|
46
|
+
const apiKey2 = passedKey;
|
|
47
|
+
react.useEffect(() => {
|
|
48
|
+
console.log(
|
|
49
|
+
"[use-feature-flags] initializing",
|
|
50
|
+
`environment: ${sanitizedEnvironment}`,
|
|
51
|
+
`apiKey provided: ${Boolean(apiKey2)}`
|
|
52
|
+
);
|
|
53
|
+
}, [sanitizedEnvironment, apiKey2]);
|
|
54
|
+
const supabase = getSupabase();
|
|
55
|
+
const fetchFlags = async () => {
|
|
56
|
+
setState((prev) => ({ ...prev, loading: true }));
|
|
57
|
+
console.log("[use-feature-flags] fetching flags for", sanitizedEnvironment);
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(EDGE_FN_URL, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: {
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
...apiKey2 ? { "api-key": apiKey2 } : {}
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify({ environment: sanitizedEnvironment })
|
|
66
|
+
});
|
|
67
|
+
const json = await res.json();
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
console.warn("Edge function error:", (json == null ? void 0 : json.error) || res.statusText);
|
|
70
|
+
setState({ flags: [], loading: false });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const flags = json.flags || [];
|
|
74
|
+
setState({ flags, loading: false });
|
|
75
|
+
console.log("[use-feature-flags] fetched flags", flags);
|
|
76
|
+
if (flags.length > 0 && flags[0].environment_id) {
|
|
77
|
+
setEnvId(flags[0].environment_id);
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error("Error fetching flags:", err.message);
|
|
81
|
+
setState({ flags: [], loading: false });
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
react.useEffect(() => {
|
|
85
|
+
if (!apiKey2 || initialized) return;
|
|
86
|
+
initialized = true;
|
|
87
|
+
fetchFlags();
|
|
88
|
+
return () => {
|
|
89
|
+
if (debounceTimeout.current) clearTimeout(debounceTimeout.current);
|
|
90
|
+
if (cleanupRef.current) cleanupRef.current();
|
|
91
|
+
};
|
|
92
|
+
}, [sanitizedEnvironment, apiKey2]);
|
|
93
|
+
react.useEffect(() => {
|
|
94
|
+
if (!envId) return;
|
|
95
|
+
const channel = supabase.channel(`flags-${sanitizedEnvironment}`).on(
|
|
96
|
+
"postgres_changes",
|
|
97
|
+
{
|
|
98
|
+
event: "*",
|
|
99
|
+
schema: "public",
|
|
100
|
+
table: "feature_flags",
|
|
101
|
+
filter: `environment_id=eq.${envId}`
|
|
102
|
+
},
|
|
103
|
+
() => {
|
|
104
|
+
if (debounceTimeout.current) clearTimeout(debounceTimeout.current);
|
|
105
|
+
debounceTimeout.current = setTimeout(fetchFlags, 300);
|
|
106
|
+
}
|
|
107
|
+
).subscribe();
|
|
108
|
+
if (cleanupRef.current) cleanupRef.current();
|
|
109
|
+
cleanupRef.current = () => supabase.removeChannel(channel);
|
|
110
|
+
return () => {
|
|
111
|
+
if (debounceTimeout.current) clearTimeout(debounceTimeout.current);
|
|
112
|
+
supabase.removeChannel(channel);
|
|
113
|
+
};
|
|
114
|
+
}, [envId]);
|
|
115
|
+
return {
|
|
116
|
+
isActive: (key) => {
|
|
117
|
+
const active = state.flags.some((f) => f.key === key && f.enabled === true);
|
|
118
|
+
console.log("[use-feature-flags] isActive", key, active);
|
|
119
|
+
return active;
|
|
120
|
+
},
|
|
121
|
+
flags: state.flags,
|
|
122
|
+
loading: state.loading
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
exports.featureFlagsAtom = featureFlagsAtom;
|
|
127
|
+
exports.getApiKey = getApiKey;
|
|
128
|
+
exports.setApiKey = setApiKey;
|
|
129
|
+
exports.useFeatureFlags = useFeatureFlags;
|
|
130
|
+
//# sourceMappingURL=index.cjs.map
|
|
131
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store.ts","../src/supabaseClient.ts","../src/useFeatureFlags.ts"],"names":["atom","createClient","useMemo","useAtom","useState","useRef","apiKey","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["import { atom } from 'jotai';\n\nexport type FeatureFlag = {\n id: string;\n key: string;\n enabled: boolean;\n environment: string;\n};\n\nexport const featureFlagsAtom = atom<{ flags: FeatureFlag[]; loading: boolean }>({\n flags: [],\n loading: true,\n});\n\n// store.ts\nlet apiKey: string | null = null;\n\nexport function setApiKey(key: string) {\n apiKey = key;\n}\n\nexport function getApiKey() {\n return apiKey;\n}\n","import { createClient } from '@supabase/supabase-js';\n\nexport const DEFAULT_SUPABASE_URL = 'https://khppgsehvvlukzfdqbuo.supabase.co';\nexport const DEFAULT_SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtocHBnc2VodnZsdWt6ZmRxYnVvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTI2ODU1MzQsImV4cCI6MjA2ODI2MTUzNH0.8Z4VY4HFMm95UgO21c-DnDkbLPN_0mbDBZJPExaghDk';\n\nlet _supabase: ReturnType<typeof createClient> | null = null;\n\nexport function getSupabase() {\n if (_supabase) return _supabase;\n _supabase = createClient(DEFAULT_SUPABASE_URL, DEFAULT_SUPABASE_KEY, {\n auth: {\n persistSession: false,\n autoRefreshToken: false,\n storageKey: 'feature-flags',\n },\n });\n return _supabase;\n}\n","\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { FeatureFlag, featureFlagsAtom } from './store';\nimport { useAtom } from 'jotai';\nimport { getSupabase, DEFAULT_SUPABASE_URL } from './supabaseClient';\n\nconst EDGE_FN_URL = `${DEFAULT_SUPABASE_URL}/functions/v1/get-feature-flags`;\n\n\n\n\ntype FlagState = {\n flags: FeatureFlag[];\n loading: boolean;\n};\n\nlet initialized = false;\n\nexport function useFeatureFlags(\n passedKey?: string,\n environment = window.location.hostname || 'localhost'\n) {\n const sanitizedEnvironment = useMemo(\n () => environment.replace(/:\\d+$/, ''),\n [environment]\n );\n const [state, setState] = useAtom(featureFlagsAtom);\n const [envId, setEnvId] = useState<number | null>(null);\n const debounceTimeout = useRef<NodeJS.Timeout | null>(null);\n const cleanupRef = useRef<(() => void) | null>(null);\n\n const apiKey = passedKey;\n\n useEffect(() => {\n console.log(\n '[use-feature-flags] initializing',\n `environment: ${sanitizedEnvironment}`,\n `apiKey provided: ${Boolean(apiKey)}`\n );\n }, [sanitizedEnvironment, apiKey]);\n const supabase = getSupabase();\n\n const fetchFlags = async () => {\n setState((prev) => ({ ...prev, loading: true }));\n\n console.log('[use-feature-flags] fetching flags for', sanitizedEnvironment);\n\n try {\n const res = await fetch(EDGE_FN_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(apiKey ? { 'api-key': apiKey } : {}),\n },\n body: JSON.stringify({ environment: sanitizedEnvironment }),\n });\n\n const json = await res.json();\n if (!res.ok) {\n console.warn('Edge function error:', json?.error || res.statusText);\n setState({ flags: [], loading: false });\n return;\n }\n\n const flags = json.flags || [];\n setState({ flags, loading: false });\n console.log('[use-feature-flags] fetched flags', flags);\n\n // Store environment_id from first flag (assumes all have same env)\n if (flags.length > 0 && flags[0].environment_id) {\n setEnvId(flags[0].environment_id);\n }\n } catch (err: any) {\n console.error('Error fetching flags:', err.message);\n setState({ flags: [], loading: false });\n }\n };\n\n useEffect(() => {\n if (!apiKey || initialized) return;\n initialized = true;\n\n fetchFlags();\n\n return () => {\n if (debounceTimeout.current) clearTimeout(debounceTimeout.current);\n if (cleanupRef.current) cleanupRef.current();\n };\n }, [sanitizedEnvironment, apiKey]);\n\n // Subscribe to flag changes for the correct environment\n useEffect(() => {\n if (!envId) return;\n\n const channel = supabase\n .channel(`flags-${sanitizedEnvironment}`)\n .on(\n 'postgres_changes',\n {\n event: '*',\n schema: 'public',\n table: 'feature_flags',\n filter: `environment_id=eq.${envId}`,\n },\n () => {\n if (debounceTimeout.current) clearTimeout(debounceTimeout.current);\n debounceTimeout.current = setTimeout(fetchFlags, 300);\n }\n )\n .subscribe();\n\n if (cleanupRef.current) cleanupRef.current();\n cleanupRef.current = () => supabase.removeChannel(channel);\n\n return () => {\n if (debounceTimeout.current) clearTimeout(debounceTimeout.current);\n supabase.removeChannel(channel);\n };\n }, [envId]);\n\n return {\n isActive: (key: string) => {\n const active = state.flags.some((f) => f.key === key && f.enabled === true);\n console.log('[use-feature-flags] isActive', key, active);\n return active;\n },\n flags: state.flags,\n loading: state.loading,\n };\n}\n\n\n\n\n\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as jotai from 'jotai';
|
|
2
|
+
|
|
3
|
+
type FeatureFlag = {
|
|
4
|
+
id: string;
|
|
5
|
+
key: string;
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
environment: string;
|
|
8
|
+
};
|
|
9
|
+
declare const featureFlagsAtom: jotai.PrimitiveAtom<{
|
|
10
|
+
flags: FeatureFlag[];
|
|
11
|
+
loading: boolean;
|
|
12
|
+
}> & {
|
|
13
|
+
init: {
|
|
14
|
+
flags: FeatureFlag[];
|
|
15
|
+
loading: boolean;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
declare function setApiKey(key: string): void;
|
|
19
|
+
declare function getApiKey(): string | null;
|
|
20
|
+
|
|
21
|
+
declare function useFeatureFlags(passedKey?: string, environment?: string): {
|
|
22
|
+
isActive: (key: string) => boolean;
|
|
23
|
+
flags: FeatureFlag[];
|
|
24
|
+
loading: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export { type FeatureFlag, featureFlagsAtom, getApiKey, setApiKey, useFeatureFlags };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import * as jotai from 'jotai';
|
|
2
|
+
|
|
3
|
+
type FeatureFlag = {
|
|
4
|
+
id: string;
|
|
5
|
+
key: string;
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
environment: string;
|
|
8
|
+
};
|
|
9
|
+
declare const featureFlagsAtom: jotai.PrimitiveAtom<{
|
|
10
|
+
flags: FeatureFlag[];
|
|
11
|
+
loading: boolean;
|
|
12
|
+
}> & {
|
|
13
|
+
init: {
|
|
14
|
+
flags: FeatureFlag[];
|
|
15
|
+
loading: boolean;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
declare function setApiKey(key: string): void;
|
|
19
|
+
declare function getApiKey(): string | null;
|
|
20
|
+
|
|
21
|
+
declare function useFeatureFlags(passedKey?: string, environment?: string): {
|
|
22
|
+
isActive: (key: string) => boolean;
|
|
23
|
+
flags: FeatureFlag[];
|
|
24
|
+
loading: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export { type FeatureFlag, featureFlagsAtom, getApiKey, setApiKey, useFeatureFlags };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,126 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { useMemo, useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { atom, useAtom } from 'jotai';
|
|
3
|
+
import { createClient } from '@supabase/supabase-js';
|
|
4
|
+
|
|
5
|
+
// src/useFeatureFlags.ts
|
|
6
|
+
var featureFlagsAtom = atom({
|
|
7
|
+
flags: [],
|
|
8
|
+
loading: true
|
|
9
|
+
});
|
|
10
|
+
var apiKey = null;
|
|
11
|
+
function setApiKey(key) {
|
|
12
|
+
apiKey = key;
|
|
13
|
+
}
|
|
14
|
+
function getApiKey() {
|
|
15
|
+
return apiKey;
|
|
16
|
+
}
|
|
17
|
+
var DEFAULT_SUPABASE_URL = "https://khppgsehvvlukzfdqbuo.supabase.co";
|
|
18
|
+
var DEFAULT_SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtocHBnc2VodnZsdWt6ZmRxYnVvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTI2ODU1MzQsImV4cCI6MjA2ODI2MTUzNH0.8Z4VY4HFMm95UgO21c-DnDkbLPN_0mbDBZJPExaghDk";
|
|
19
|
+
var _supabase = null;
|
|
20
|
+
function getSupabase() {
|
|
21
|
+
if (_supabase) return _supabase;
|
|
22
|
+
_supabase = createClient(DEFAULT_SUPABASE_URL, DEFAULT_SUPABASE_KEY, {
|
|
23
|
+
auth: {
|
|
24
|
+
persistSession: false,
|
|
25
|
+
autoRefreshToken: false,
|
|
26
|
+
storageKey: "feature-flags"
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
return _supabase;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/useFeatureFlags.ts
|
|
33
|
+
var EDGE_FN_URL = `${DEFAULT_SUPABASE_URL}/functions/v1/get-feature-flags`;
|
|
34
|
+
var initialized = false;
|
|
35
|
+
function useFeatureFlags(passedKey, environment = window.location.hostname || "localhost") {
|
|
36
|
+
const sanitizedEnvironment = useMemo(
|
|
37
|
+
() => environment.replace(/:\d+$/, ""),
|
|
38
|
+
[environment]
|
|
39
|
+
);
|
|
40
|
+
const [state, setState] = useAtom(featureFlagsAtom);
|
|
41
|
+
const [envId, setEnvId] = useState(null);
|
|
42
|
+
const debounceTimeout = useRef(null);
|
|
43
|
+
const cleanupRef = useRef(null);
|
|
44
|
+
const apiKey2 = passedKey;
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
console.log(
|
|
47
|
+
"[use-feature-flags] initializing",
|
|
48
|
+
`environment: ${sanitizedEnvironment}`,
|
|
49
|
+
`apiKey provided: ${Boolean(apiKey2)}`
|
|
50
|
+
);
|
|
51
|
+
}, [sanitizedEnvironment, apiKey2]);
|
|
52
|
+
const supabase = getSupabase();
|
|
53
|
+
const fetchFlags = async () => {
|
|
54
|
+
setState((prev) => ({ ...prev, loading: true }));
|
|
55
|
+
console.log("[use-feature-flags] fetching flags for", sanitizedEnvironment);
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(EDGE_FN_URL, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
...apiKey2 ? { "api-key": apiKey2 } : {}
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify({ environment: sanitizedEnvironment })
|
|
64
|
+
});
|
|
65
|
+
const json = await res.json();
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
console.warn("Edge function error:", (json == null ? void 0 : json.error) || res.statusText);
|
|
68
|
+
setState({ flags: [], loading: false });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const flags = json.flags || [];
|
|
72
|
+
setState({ flags, loading: false });
|
|
73
|
+
console.log("[use-feature-flags] fetched flags", flags);
|
|
74
|
+
if (flags.length > 0 && flags[0].environment_id) {
|
|
75
|
+
setEnvId(flags[0].environment_id);
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error("Error fetching flags:", err.message);
|
|
79
|
+
setState({ flags: [], loading: false });
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!apiKey2 || initialized) return;
|
|
84
|
+
initialized = true;
|
|
85
|
+
fetchFlags();
|
|
86
|
+
return () => {
|
|
87
|
+
if (debounceTimeout.current) clearTimeout(debounceTimeout.current);
|
|
88
|
+
if (cleanupRef.current) cleanupRef.current();
|
|
89
|
+
};
|
|
90
|
+
}, [sanitizedEnvironment, apiKey2]);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!envId) return;
|
|
93
|
+
const channel = supabase.channel(`flags-${sanitizedEnvironment}`).on(
|
|
94
|
+
"postgres_changes",
|
|
95
|
+
{
|
|
96
|
+
event: "*",
|
|
97
|
+
schema: "public",
|
|
98
|
+
table: "feature_flags",
|
|
99
|
+
filter: `environment_id=eq.${envId}`
|
|
100
|
+
},
|
|
101
|
+
() => {
|
|
102
|
+
if (debounceTimeout.current) clearTimeout(debounceTimeout.current);
|
|
103
|
+
debounceTimeout.current = setTimeout(fetchFlags, 300);
|
|
104
|
+
}
|
|
105
|
+
).subscribe();
|
|
106
|
+
if (cleanupRef.current) cleanupRef.current();
|
|
107
|
+
cleanupRef.current = () => supabase.removeChannel(channel);
|
|
108
|
+
return () => {
|
|
109
|
+
if (debounceTimeout.current) clearTimeout(debounceTimeout.current);
|
|
110
|
+
supabase.removeChannel(channel);
|
|
111
|
+
};
|
|
112
|
+
}, [envId]);
|
|
113
|
+
return {
|
|
114
|
+
isActive: (key) => {
|
|
115
|
+
const active = state.flags.some((f) => f.key === key && f.enabled === true);
|
|
116
|
+
console.log("[use-feature-flags] isActive", key, active);
|
|
117
|
+
return active;
|
|
118
|
+
},
|
|
119
|
+
flags: state.flags,
|
|
120
|
+
loading: state.loading
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { featureFlagsAtom, getApiKey, setApiKey, useFeatureFlags };
|
|
125
|
+
//# sourceMappingURL=index.js.map
|
|
126
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store.ts","../src/supabaseClient.ts","../src/useFeatureFlags.ts"],"names":["apiKey"],"mappings":"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","file":"index.js","sourcesContent":["import { atom } from 'jotai';\n\nexport type FeatureFlag = {\n id: string;\n key: string;\n enabled: boolean;\n environment: string;\n};\n\nexport const featureFlagsAtom = atom<{ flags: FeatureFlag[]; loading: boolean }>({\n flags: [],\n loading: true,\n});\n\n// store.ts\nlet apiKey: string | null = null;\n\nexport function setApiKey(key: string) {\n apiKey = key;\n}\n\nexport function getApiKey() {\n return apiKey;\n}\n","import { createClient } from '@supabase/supabase-js';\n\nexport const DEFAULT_SUPABASE_URL = 'https://khppgsehvvlukzfdqbuo.supabase.co';\nexport const DEFAULT_SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtocHBnc2VodnZsdWt6ZmRxYnVvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTI2ODU1MzQsImV4cCI6MjA2ODI2MTUzNH0.8Z4VY4HFMm95UgO21c-DnDkbLPN_0mbDBZJPExaghDk';\n\nlet _supabase: ReturnType<typeof createClient> | null = null;\n\nexport function getSupabase() {\n if (_supabase) return _supabase;\n _supabase = createClient(DEFAULT_SUPABASE_URL, DEFAULT_SUPABASE_KEY, {\n auth: {\n persistSession: false,\n autoRefreshToken: false,\n storageKey: 'feature-flags',\n },\n });\n return _supabase;\n}\n","\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { FeatureFlag, featureFlagsAtom } from './store';\nimport { useAtom } from 'jotai';\nimport { getSupabase, DEFAULT_SUPABASE_URL } from './supabaseClient';\n\nconst EDGE_FN_URL = `${DEFAULT_SUPABASE_URL}/functions/v1/get-feature-flags`;\n\n\n\n\ntype FlagState = {\n flags: FeatureFlag[];\n loading: boolean;\n};\n\nlet initialized = false;\n\nexport function useFeatureFlags(\n passedKey?: string,\n environment = window.location.hostname || 'localhost'\n) {\n const sanitizedEnvironment = useMemo(\n () => environment.replace(/:\\d+$/, ''),\n [environment]\n );\n const [state, setState] = useAtom(featureFlagsAtom);\n const [envId, setEnvId] = useState<number | null>(null);\n const debounceTimeout = useRef<NodeJS.Timeout | null>(null);\n const cleanupRef = useRef<(() => void) | null>(null);\n\n const apiKey = passedKey;\n\n useEffect(() => {\n console.log(\n '[use-feature-flags] initializing',\n `environment: ${sanitizedEnvironment}`,\n `apiKey provided: ${Boolean(apiKey)}`\n );\n }, [sanitizedEnvironment, apiKey]);\n const supabase = getSupabase();\n\n const fetchFlags = async () => {\n setState((prev) => ({ ...prev, loading: true }));\n\n console.log('[use-feature-flags] fetching flags for', sanitizedEnvironment);\n\n try {\n const res = await fetch(EDGE_FN_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(apiKey ? { 'api-key': apiKey } : {}),\n },\n body: JSON.stringify({ environment: sanitizedEnvironment }),\n });\n\n const json = await res.json();\n if (!res.ok) {\n console.warn('Edge function error:', json?.error || res.statusText);\n setState({ flags: [], loading: false });\n return;\n }\n\n const flags = json.flags || [];\n setState({ flags, loading: false });\n console.log('[use-feature-flags] fetched flags', flags);\n\n // Store environment_id from first flag (assumes all have same env)\n if (flags.length > 0 && flags[0].environment_id) {\n setEnvId(flags[0].environment_id);\n }\n } catch (err: any) {\n console.error('Error fetching flags:', err.message);\n setState({ flags: [], loading: false });\n }\n };\n\n useEffect(() => {\n if (!apiKey || initialized) return;\n initialized = true;\n\n fetchFlags();\n\n return () => {\n if (debounceTimeout.current) clearTimeout(debounceTimeout.current);\n if (cleanupRef.current) cleanupRef.current();\n };\n }, [sanitizedEnvironment, apiKey]);\n\n // Subscribe to flag changes for the correct environment\n useEffect(() => {\n if (!envId) return;\n\n const channel = supabase\n .channel(`flags-${sanitizedEnvironment}`)\n .on(\n 'postgres_changes',\n {\n event: '*',\n schema: 'public',\n table: 'feature_flags',\n filter: `environment_id=eq.${envId}`,\n },\n () => {\n if (debounceTimeout.current) clearTimeout(debounceTimeout.current);\n debounceTimeout.current = setTimeout(fetchFlags, 300);\n }\n )\n .subscribe();\n\n if (cleanupRef.current) cleanupRef.current();\n cleanupRef.current = () => supabase.removeChannel(channel);\n\n return () => {\n if (debounceTimeout.current) clearTimeout(debounceTimeout.current);\n supabase.removeChannel(channel);\n };\n }, [envId]);\n\n return {\n isActive: (key: string) => {\n const active = state.flags.some((f) => f.key === key && f.enabled === true);\n console.log('[use-feature-flags] isActive', key, active);\n return active;\n },\n flags: state.flags,\n loading: state.loading,\n };\n}\n\n\n\n\n\n"]}
|
package/package.json
CHANGED
|
@@ -1,29 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geejay/use-feature-flags",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.19",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"import": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"require": "./dist/index.cjs"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"module": "dist/index.js",
|
|
6
13
|
"types": "dist/index.d.ts",
|
|
7
14
|
"files": [
|
|
8
15
|
"dist",
|
|
9
16
|
"README.md"
|
|
10
17
|
],
|
|
11
18
|
"scripts": {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"keywords": ["feature flags", "supabase", "react", "hooks"],
|
|
16
|
-
"repository": {
|
|
17
|
-
"type": "git",
|
|
18
|
-
"url": "https://github.com/your-org/use-feature-flags.git"
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"prepack": "npm run build",
|
|
21
|
+
"publish": "npm version patch && npm publish"
|
|
19
22
|
},
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
"dependencies": {
|
|
23
|
-
"@supabase/supabase-js": "^2.52.0",
|
|
24
|
-
"jotai": "^2.12.5"
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"react": ">=16.8"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@supabase/supabase-js": "^2.54.0",
|
|
31
|
+
"@types/react": "^18.3.23",
|
|
32
|
+
"jotai": "^2.13.0",
|
|
33
|
+
"tsup": "^8.5.0",
|
|
34
|
+
"typescript": "^5.9.2"
|
|
28
35
|
}
|
|
29
36
|
}
|
package/dist/store.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export type FeatureFlag = {
|
|
2
|
-
id: string;
|
|
3
|
-
key: string;
|
|
4
|
-
enabled: boolean;
|
|
5
|
-
environment: string;
|
|
6
|
-
};
|
|
7
|
-
export declare const featureFlagsAtom: import("jotai").PrimitiveAtom<{
|
|
8
|
-
flags: FeatureFlag[];
|
|
9
|
-
loading: boolean;
|
|
10
|
-
}> & {
|
|
11
|
-
init: {
|
|
12
|
-
flags: FeatureFlag[];
|
|
13
|
-
loading: boolean;
|
|
14
|
-
};
|
|
15
|
-
};
|
|
16
|
-
export declare function setApiKey(key: string): void;
|
|
17
|
-
export declare function getApiKey(): string;
|
package/dist/store.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { FeatureFlag } from './store';
|
|
2
|
-
export declare const DEFAULT_SUPABASE_URL = "https://khppgsehvvlukzfdqbuo.supabase.co";
|
|
3
|
-
export declare const DEFAULT_SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtocHBnc2VodnZsdWt6ZmRxYnVvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTI2ODU1MzQsImV4cCI6MjA2ODI2MTUzNH0.8Z4VY4HFMm95UgO21c-DnDkbLPN_0mbDBZJPExaghDk";
|
|
4
|
-
export declare function useFeatureFlags(passedKey?: string, environment?: string): {
|
|
5
|
-
isActive: (key: string) => boolean;
|
|
6
|
-
flags: FeatureFlag[];
|
|
7
|
-
loading: boolean;
|
|
8
|
-
};
|
package/dist/useFeatureFlags.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import { createClient } from '@supabase/supabase-js';
|
|
3
|
-
import { featureFlagsAtom } from './store';
|
|
4
|
-
import { useAtom } from 'jotai';
|
|
5
|
-
export const DEFAULT_SUPABASE_URL = 'https://khppgsehvvlukzfdqbuo.supabase.co';
|
|
6
|
-
export const DEFAULT_SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtocHBnc2VodnZsdWt6ZmRxYnVvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTI2ODU1MzQsImV4cCI6MjA2ODI2MTUzNH0.8Z4VY4HFMm95UgO21c-DnDkbLPN_0mbDBZJPExaghDk';
|
|
7
|
-
const EDGE_FN_URL = `${DEFAULT_SUPABASE_URL}/functions/v1/get-feature-flags`;
|
|
8
|
-
let initialized = false;
|
|
9
|
-
export function useFeatureFlags(passedKey, environment = window.location.hostname || 'localhost') {
|
|
10
|
-
const sanitizedEnvironment = useMemo(() => environment.replace(/:\d+$/, ''), [environment]);
|
|
11
|
-
const [state, setState] = useAtom(featureFlagsAtom);
|
|
12
|
-
const [envId, setEnvId] = useState(null);
|
|
13
|
-
const debounceTimeout = useRef(null);
|
|
14
|
-
const cleanupRef = useRef();
|
|
15
|
-
const apiKey = passedKey;
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
console.log('[use-feature-flags] initializing', `environment: ${sanitizedEnvironment}`, `apiKey provided: ${Boolean(apiKey)}`);
|
|
18
|
-
}, [sanitizedEnvironment, apiKey]);
|
|
19
|
-
const supabase = createClient(DEFAULT_SUPABASE_URL, DEFAULT_SUPABASE_KEY, {
|
|
20
|
-
global: {
|
|
21
|
-
headers: {
|
|
22
|
-
Authorization: `Bearer ${DEFAULT_SUPABASE_KEY}`, // 👈 dynamic access token
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
const fetchFlags = async () => {
|
|
27
|
-
setState((prev) => ({ ...prev, loading: true }));
|
|
28
|
-
console.log('[use-feature-flags] fetching flags for', sanitizedEnvironment);
|
|
29
|
-
try {
|
|
30
|
-
const res = await fetch(EDGE_FN_URL, {
|
|
31
|
-
method: 'POST',
|
|
32
|
-
headers: {
|
|
33
|
-
'Content-Type': 'application/json',
|
|
34
|
-
'api-key': apiKey,
|
|
35
|
-
},
|
|
36
|
-
body: JSON.stringify({ environment: sanitizedEnvironment }),
|
|
37
|
-
});
|
|
38
|
-
const json = await res.json();
|
|
39
|
-
if (!res.ok) {
|
|
40
|
-
console.warn('Edge function error:', (json === null || json === void 0 ? void 0 : json.error) || res.statusText);
|
|
41
|
-
setState({ flags: [], loading: false });
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
const flags = json.flags || [];
|
|
45
|
-
setState({ flags, loading: false });
|
|
46
|
-
console.log('[use-feature-flags] fetched flags', flags);
|
|
47
|
-
// Store environment_id from first flag (assumes all have same env)
|
|
48
|
-
if (flags.length > 0 && flags[0].environment_id) {
|
|
49
|
-
setEnvId(flags[0].environment_id);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
catch (err) {
|
|
53
|
-
console.error('Error fetching flags:', err.message);
|
|
54
|
-
setState({ flags: [], loading: false });
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
useEffect(() => {
|
|
58
|
-
if (!apiKey || initialized)
|
|
59
|
-
return;
|
|
60
|
-
initialized = true;
|
|
61
|
-
fetchFlags();
|
|
62
|
-
return () => {
|
|
63
|
-
if (debounceTimeout.current)
|
|
64
|
-
clearTimeout(debounceTimeout.current);
|
|
65
|
-
if (cleanupRef.current)
|
|
66
|
-
cleanupRef.current();
|
|
67
|
-
};
|
|
68
|
-
}, [sanitizedEnvironment, apiKey]);
|
|
69
|
-
// Subscribe to flag changes for the correct environment
|
|
70
|
-
useEffect(() => {
|
|
71
|
-
if (!envId)
|
|
72
|
-
return;
|
|
73
|
-
const channel = supabase
|
|
74
|
-
.channel(`flags-${sanitizedEnvironment}`)
|
|
75
|
-
.on('postgres_changes', {
|
|
76
|
-
event: '*',
|
|
77
|
-
schema: 'public',
|
|
78
|
-
table: 'feature_flags',
|
|
79
|
-
filter: `environment_id=eq.${envId}`,
|
|
80
|
-
}, () => {
|
|
81
|
-
if (debounceTimeout.current)
|
|
82
|
-
clearTimeout(debounceTimeout.current);
|
|
83
|
-
debounceTimeout.current = setTimeout(fetchFlags, 300);
|
|
84
|
-
})
|
|
85
|
-
.subscribe();
|
|
86
|
-
if (cleanupRef.current)
|
|
87
|
-
cleanupRef.current();
|
|
88
|
-
cleanupRef.current = () => supabase.removeChannel(channel);
|
|
89
|
-
return () => {
|
|
90
|
-
if (debounceTimeout.current)
|
|
91
|
-
clearTimeout(debounceTimeout.current);
|
|
92
|
-
supabase.removeChannel(channel);
|
|
93
|
-
};
|
|
94
|
-
}, [envId]);
|
|
95
|
-
return {
|
|
96
|
-
isActive: (key) => {
|
|
97
|
-
const active = state.flags.some((f) => f.key === key && f.enabled === true);
|
|
98
|
-
console.log('[use-feature-flags] isActive', key, active);
|
|
99
|
-
return active;
|
|
100
|
-
},
|
|
101
|
-
flags: state.flags,
|
|
102
|
-
loading: state.loading,
|
|
103
|
-
};
|
|
104
|
-
}
|