@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 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
- npm install use-feature-flags
4
+ _____ ___ ____ ___ _ ____
5
+ |_ _/ _ \| __ )_ _| / \ / ___|
6
+ | || | | | _ \| | / _ \ \___ \
7
+ | || |_| | |_) | | / ___ \ ___) |
8
+ |_| \___/|____/___/_/ \_\____/
9
+
10
+
9
11
  ```
10
12
 
11
- or
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
- ## Usage
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
- import { useFeatureFlags } from 'use-feature-flags';
58
+ // App.tsx
59
+ import { useFeatureFlags } from '@geejay/use-feature-flags';
21
60
 
22
- export default function MyComponent() {
23
- const { isActive, loading } = useFeatureFlags("YOUR-API-KEY"); //API param only needed on the first useFeatureFlags call
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
- ## Flag Management
175
+ ## 📄 License
34
176
 
35
- visit https://featureflags-ai.netlify.app/ to get an API key and manage your feature flags from the web.
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"]}
@@ -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
- export * from './useFeatureFlags';
2
- export * from './store';
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
- export * from './useFeatureFlags';
2
- export * from './store';
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.13",
4
- "description": "React hook for feature flag management using Supabase",
5
- "main": "dist/index.js",
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
- "build": "rm -rf dist && tsc -p tsconfig.build.json"
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
- "author": "GeeJay",
21
- "license": "MIT",
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,13 +0,0 @@
1
- import { atom } from 'jotai';
2
- export const featureFlagsAtom = atom({
3
- flags: [],
4
- loading: true,
5
- });
6
- // store.ts
7
- let apiKey = null;
8
- export function setApiKey(key) {
9
- apiKey = key;
10
- }
11
- export function getApiKey() {
12
- return apiKey;
13
- }
@@ -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
- };
@@ -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
- }