@flagdown/react 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,42 +1,48 @@
1
1
  # @flagdown/react
2
2
 
3
- React bindings for [Flagdown](https://github.com/flagdown/flagdown) feature
4
- flags.
5
-
6
- ## Installation
7
-
8
- ```bash
9
- npm install @flagdown/react
10
- ```
11
-
12
- ## Current bootstrap API
13
-
14
- Wrap the part of the React tree that reads flags in `FlagdownProvider`, then
15
- read a flag with `useFlag`:
3
+ Read boolean feature flags from a Flagdown project in React 18+.
16
4
 
17
5
  ```tsx
18
- import { FlagdownProvider, useFlag } from "@flagdown/react";
6
+ import { FlagdownProvider, useFlag } from '@flagdown/react';
19
7
 
20
- function BetaFeature() {
21
- return useFlag("beta-feature") ? <p>Enabled</p> : null;
8
+ function Checkout() {
9
+ const enabled = useFlag('new-checkout');
10
+ return enabled ? <NewCheckout /> : <CurrentCheckout />;
22
11
  }
23
12
 
24
- export function App() {
25
- return (
26
- <FlagdownProvider>
27
- <BetaFeature />
28
- </FlagdownProvider>
29
- );
30
- }
13
+ <FlagdownProvider
14
+ apiUrl="https://your-flagdown-api.example.com"
15
+ clientToken="COPY_PUBLIC_CLIENT_TOKEN_FROM_DASHBOARD"
16
+ >
17
+ <Checkout />
18
+ </FlagdownProvider>
31
19
  ```
32
20
 
33
- This initial provider returns `false` for every flag. A future release will
34
- provide an adapter generated from Flagdown's OpenAPI contract.
21
+ Create a project and flag in the dashboard, then copy the generated integration
22
+ snippet. `apiUrl` is the Worker base URL without `/v1`. Install the SDK build that
23
+ includes the remote adapter; the previously published 0.0.2 build only provides
24
+ an injectable evaluator. This source must be released before registry installs
25
+ can use remote reads.
26
+
27
+ Flags refresh every 10 seconds, without reloading the page. Set
28
+ `refreshIntervalMs` to change this (minimum 1000ms). Requests time out after 10
29
+ seconds. Initial values, unknown flags and refresh failures evaluate `false`;
30
+ a successful later refresh restores values. An optional `onError(error)` callback
31
+ can report failures. Changing project configuration immediately discards the
32
+ previous project's values. Unmounting cancels in-flight work and future refreshes.
35
33
 
36
- For tests or custom adapters, supply a `FlagEvaluator` implementation:
34
+ Client tokens are public, project-scoped read configuration. They cannot manage
35
+ flags. Replacing a token in the dashboard invalidates the previous token; update
36
+ apps using it. Client-side flags are not an authorization boundary.
37
+
38
+ For tests or local values, the existing evaluator port remains available:
37
39
 
38
40
  ```tsx
39
- <FlagdownProvider evaluator={{ isEnabled: (key) => key === "beta-feature" }}>
40
- <BetaFeature />
41
+ <FlagdownProvider evaluator={{ isEnabled: key => key === 'new-checkout' }}>
42
+ <Checkout />
41
43
  </FlagdownProvider>
42
44
  ```
45
+
46
+ An explicit evaluator takes precedence over remote configuration. A provider
47
+ without either configuration evaluates every flag off. `useFlag` must be called
48
+ inside a provider.
package/dist/index.d.ts CHANGED
@@ -14,13 +14,15 @@ interface FlagEvaluator {
14
14
  declare const disabledFlagEvaluator: FlagEvaluator;
15
15
 
16
16
  interface FlagdownProviderProps extends PropsWithChildren {
17
- /**
18
- * Optional evaluation port. This enables a future Flagdown API adapter and
19
- * keeps consumers independent from the transport that supplies flag values.
20
- */
21
17
  evaluator?: FlagEvaluator;
18
+ apiUrl?: string;
19
+ /** Public, read-only project token copied from the dashboard. */
20
+ clientToken?: string;
21
+ /** Refresh interval; minimum 1 second, default 10 seconds. */
22
+ refreshIntervalMs?: number;
23
+ onError?: (error: Error) => void;
22
24
  }
23
- declare function FlagdownProvider({ children, evaluator, }: FlagdownProviderProps): react.JSX.Element;
25
+ declare function FlagdownProvider({ children, evaluator, apiUrl, clientToken, refreshIntervalMs, onError }: FlagdownProviderProps): react.JSX.Element;
24
26
 
25
27
  declare function useFlag(flagKey: string): boolean;
26
28
 
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ // src/FlagdownProvider.tsx
2
+ import { useEffect, useMemo, useState, useRef } from "react";
3
+
1
4
  // src/FlagdownContext.tsx
2
5
  import { createContext, useContext } from "react";
3
6
  var FlagdownContext = createContext(void 0);
@@ -14,13 +17,62 @@ var disabledFlagEvaluator = {
14
17
  isEnabled: () => false
15
18
  };
16
19
 
20
+ // src/remote.ts
21
+ async function fetchFlagState(apiUrl, clientToken, signal) {
22
+ const response = await fetch(apiUrl.replace(/\/$/, "") + "/v1/client/flags", {
23
+ headers: { "x-flagdown-client-token": clientToken },
24
+ cache: "no-store",
25
+ signal
26
+ });
27
+ if (!response.ok) throw new Error(`Flagdown request failed (${response.status})`);
28
+ const data = await response.json();
29
+ if (!data || typeof data !== "object" || !("flags" in data) || !data.flags || typeof data.flags !== "object" || Array.isArray(data.flags) || Object.values(data.flags).some((value) => typeof value !== "boolean")) {
30
+ throw new Error("Invalid Flagdown response");
31
+ }
32
+ return data.flags;
33
+ }
34
+
17
35
  // src/FlagdownProvider.tsx
18
36
  import { jsx } from "react/jsx-runtime";
19
- function FlagdownProvider({
20
- children,
21
- evaluator = disabledFlagEvaluator
22
- }) {
23
- return /* @__PURE__ */ jsx(FlagdownContext.Provider, { value: evaluator, children });
37
+ function FlagdownProvider({ children, evaluator, apiUrl, clientToken, refreshIntervalMs = 1e4, onError }) {
38
+ const errorHandler = useRef(onError);
39
+ useEffect(() => {
40
+ errorHandler.current = onError;
41
+ }, [onError]);
42
+ const [snapshot, setSnapshot] = useState();
43
+ useEffect(() => {
44
+ if (evaluator || !apiUrl || !clientToken) return;
45
+ let active = true;
46
+ let timer;
47
+ let controller;
48
+ async function refresh() {
49
+ controller = new AbortController();
50
+ const timeout = setTimeout(() => controller.abort(), 1e4);
51
+ try {
52
+ const flags = await fetchFlagState(apiUrl, clientToken, controller.signal);
53
+ if (active) setSnapshot({ apiUrl, clientToken, flags });
54
+ } catch (error) {
55
+ if (active) {
56
+ setSnapshot(void 0);
57
+ try {
58
+ errorHandler.current?.(error instanceof Error ? error : new Error("Flagdown request failed"));
59
+ } catch {
60
+ }
61
+ }
62
+ } finally {
63
+ clearTimeout(timeout);
64
+ if (active) timer = setTimeout(() => void refresh(), Number.isFinite(refreshIntervalMs) ? Math.max(1e3, refreshIntervalMs) : 1e4);
65
+ }
66
+ }
67
+ void refresh();
68
+ return () => {
69
+ active = false;
70
+ clearTimeout(timer);
71
+ controller?.abort();
72
+ };
73
+ }, [apiUrl, clientToken, evaluator, refreshIntervalMs]);
74
+ const value = useMemo(() => evaluator || (snapshot && snapshot.apiUrl === apiUrl && snapshot.clientToken === clientToken ? { isEnabled: (key) => Object.hasOwn(snapshot.flags, key) && snapshot.flags[key] === true } : disabledFlagEvaluator), [evaluator, snapshot, apiUrl, clientToken]);
75
+ return /* @__PURE__ */ jsx(FlagdownContext.Provider, { value, children });
24
76
  }
25
77
  export {
26
78
  FlagdownProvider,
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@flagdown/react",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "React SDK for evaluating Flagdown feature flags.",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/flagdown/flagdown.git",
9
+ "url": "git+https://github.com/KieranBond/flagdown.git",
10
10
  "directory": "packages/sdk"
11
11
  },
12
12
  "publishConfig": {
@@ -22,7 +22,11 @@
22
22
  "types": "./dist/index.d.ts"
23
23
  }
24
24
  },
25
- "files": ["dist", "README.md", "LICENSE"],
25
+ "files": [
26
+ "dist",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
26
30
  "scripts": {
27
31
  "build": "tsup src/index.ts --format esm --dts --clean",
28
32
  "test": "bun test",