@flagdown/react 0.0.2 → 0.0.4

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,59 @@
1
1
  # @flagdown/react
2
2
 
3
- React bindings for [Flagdown](https://github.com/flagdown/flagdown) feature
4
- flags.
3
+ Use the `Feature` component to switch between new UI and a fallback in React 18+.
5
4
 
6
- ## Installation
7
-
8
- ```bash
9
- npm install @flagdown/react
5
+ ```sh
6
+ npm install @flagdown/react@0.0.4
10
7
  ```
11
8
 
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`:
16
-
17
9
  ```tsx
18
- import { FlagdownProvider, useFlag } from "@flagdown/react";
19
-
20
- function BetaFeature() {
21
- return useFlag("beta-feature") ? <p>Enabled</p> : null;
22
- }
10
+ import { Feature } from '@flagdown/react';
23
11
 
24
- export function App() {
12
+ export function Checkout() {
25
13
  return (
26
- <FlagdownProvider>
27
- <BetaFeature />
28
- </FlagdownProvider>
14
+ <Feature
15
+ flag="new-checkout"
16
+ fallback={<OldCheckout />}
17
+ >
18
+ <NewCheckout />
19
+ </Feature>
29
20
  );
30
21
  }
31
22
  ```
32
23
 
33
- This initial provider returns `false` for every flag. A future release will
34
- provide an adapter generated from Flagdown's OpenAPI contract.
24
+ `Feature` renders its children when the named flag is on. When it is off, unknown,
25
+ still loading, or unavailable, it renders `fallback`. Omit `fallback` to render
26
+ nothing in those cases. Only the selected branch mounts; `Feature` adds no DOM
27
+ wrapper.
35
28
 
36
- For tests or custom adapters, supply a `FlagEvaluator` implementation:
29
+ Configure the project once at your app root:
37
30
 
38
31
  ```tsx
39
- <FlagdownProvider evaluator={{ isEnabled: (key) => key === "beta-feature" }}>
40
- <BetaFeature />
32
+ import { FlagdownProvider } from '@flagdown/react';
33
+
34
+ <FlagdownProvider
35
+ apiUrl="https://api.flagdown.dev"
36
+ clientToken="COPY_PUBLIC_CLIENT_TOKEN_FROM_DASHBOARD"
37
+ >
38
+ <App />
41
39
  </FlagdownProvider>
42
40
  ```
41
+
42
+ Create a project and flag in the dashboard, then copy its integration snippet.
43
+ `apiUrl` is the API base URL without `/v1`. `Feature` must be inside the provider.
44
+
45
+ Flags refresh every 10 seconds, without reloading the page. Set
46
+ `refreshIntervalMs` to change this (minimum 1000ms). Requests time out after 10
47
+ seconds. Initial values, unknown flags and refresh failures evaluate off;
48
+ a successful later refresh restores values. An optional `onError(error)` callback
49
+ can report failures. Changing project configuration immediately discards the
50
+ previous project's values. Unmounting cancels in-flight work and future refreshes.
51
+
52
+ Client tokens are public, project-scoped read configuration. They cannot manage
53
+ flags. Replacing a token in the dashboard invalidates the previous token; update
54
+ apps using it. Client-side flags are not an authorization boundary.
55
+
56
+ `Feature` is the SDK's flag consumption API. The evaluation context and evaluator
57
+ are internal; the package exposes no flag hooks or direct evaluation functions.
58
+ Use `FlagdownProvider` to configure your project. A provider without project
59
+ configuration renders each feature's fallback.
package/dist/index.d.ts CHANGED
@@ -1,27 +1,22 @@
1
1
  import * as react from 'react';
2
- import { PropsWithChildren } from 'react';
2
+ import { PropsWithChildren, ReactNode } from 'react';
3
3
 
4
- /** A port for resolving a feature flag's enabled state. */
5
- interface FlagEvaluator {
6
- isEnabled(flagKey: string): boolean;
7
- }
8
- /**
9
- * Temporary adapter used until the SDK is connected to the Flagdown API.
10
- *
11
- * It deliberately keeps every flag disabled so applications can install the
12
- * provider and hook safely before remote evaluation is available.
13
- */
14
- declare const disabledFlagEvaluator: FlagEvaluator;
4
+ type FeatureProps = PropsWithChildren<{
5
+ /** Key of the flag controlling this feature. */
6
+ flag: string;
7
+ /** Rendered while the flag is off, unknown, loading, or unavailable. */
8
+ fallback?: ReactNode;
9
+ }>;
10
+ declare function Feature({ flag, fallback, children }: FeatureProps): react.JSX.Element;
15
11
 
16
12
  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
- evaluator?: FlagEvaluator;
13
+ apiUrl?: string;
14
+ /** Public, read-only project token copied from the dashboard. */
15
+ clientToken?: string;
16
+ /** Refresh interval; minimum 1 second, default 10 seconds. */
17
+ refreshIntervalMs?: number;
18
+ onError?: (error: Error) => void;
22
19
  }
23
- declare function FlagdownProvider({ children, evaluator, }: FlagdownProviderProps): react.JSX.Element;
24
-
25
- declare function useFlag(flagKey: string): boolean;
20
+ declare function FlagdownProvider({ children, apiUrl, clientToken, refreshIntervalMs, onError }: FlagdownProviderProps): react.JSX.Element;
26
21
 
27
- export { type FlagEvaluator, FlagdownProvider, type FlagdownProviderProps, disabledFlagEvaluator, useFlag };
22
+ export { Feature, type FeatureProps, FlagdownProvider, type FlagdownProviderProps };
package/dist/index.js CHANGED
@@ -1,29 +1,84 @@
1
+ // src/Feature.tsx
2
+ import { useContext } from "react";
3
+
1
4
  // src/FlagdownContext.tsx
2
- import { createContext, useContext } from "react";
5
+ import { createContext } from "react";
3
6
  var FlagdownContext = createContext(void 0);
4
- function useFlag(flagKey) {
7
+
8
+ // src/Feature.tsx
9
+ import { Fragment, jsx } from "react/jsx-runtime";
10
+ function Feature({ flag, fallback = null, children }) {
5
11
  const evaluator = useContext(FlagdownContext);
6
- if (!evaluator) {
7
- throw new Error("useFlag must be used within a FlagdownProvider");
8
- }
9
- return evaluator.isEnabled(flagKey);
12
+ if (!evaluator) throw new Error("Feature must be used within a FlagdownProvider");
13
+ return /* @__PURE__ */ jsx(Fragment, { children: evaluator.isEnabled(flag) ? children : fallback });
10
14
  }
11
15
 
16
+ // src/FlagdownProvider.tsx
17
+ import { useEffect, useMemo, useState, useRef } from "react";
18
+
12
19
  // src/flags.ts
13
20
  var disabledFlagEvaluator = {
14
21
  isEnabled: () => false
15
22
  };
16
23
 
24
+ // src/remote.ts
25
+ async function fetchFlagState(apiUrl, clientToken, signal) {
26
+ const response = await fetch(apiUrl.replace(/\/$/, "") + "/v1/client/flags", {
27
+ headers: { "x-flagdown-client-token": clientToken },
28
+ cache: "no-store",
29
+ signal
30
+ });
31
+ if (!response.ok) throw new Error(`Flagdown request failed (${response.status})`);
32
+ const data = await response.json();
33
+ 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")) {
34
+ throw new Error("Invalid Flagdown response");
35
+ }
36
+ return data.flags;
37
+ }
38
+
17
39
  // src/FlagdownProvider.tsx
18
- import { jsx } from "react/jsx-runtime";
19
- function FlagdownProvider({
20
- children,
21
- evaluator = disabledFlagEvaluator
22
- }) {
23
- return /* @__PURE__ */ jsx(FlagdownContext.Provider, { value: evaluator, children });
40
+ import { jsx as jsx2 } from "react/jsx-runtime";
41
+ function FlagdownProvider({ children, apiUrl, clientToken, refreshIntervalMs = 1e4, onError }) {
42
+ const errorHandler = useRef(onError);
43
+ useEffect(() => {
44
+ errorHandler.current = onError;
45
+ }, [onError]);
46
+ const [snapshot, setSnapshot] = useState();
47
+ useEffect(() => {
48
+ if (!apiUrl || !clientToken) return;
49
+ let active = true;
50
+ let timer;
51
+ let controller;
52
+ async function refresh() {
53
+ controller = new AbortController();
54
+ const timeout = setTimeout(() => controller.abort(), 1e4);
55
+ try {
56
+ const flags = await fetchFlagState(apiUrl, clientToken, controller.signal);
57
+ if (active) setSnapshot({ apiUrl, clientToken, flags });
58
+ } catch (error) {
59
+ if (active) {
60
+ setSnapshot(void 0);
61
+ try {
62
+ errorHandler.current?.(error instanceof Error ? error : new Error("Flagdown request failed"));
63
+ } catch {
64
+ }
65
+ }
66
+ } finally {
67
+ clearTimeout(timeout);
68
+ if (active) timer = setTimeout(() => void refresh(), Number.isFinite(refreshIntervalMs) ? Math.max(1e3, refreshIntervalMs) : 1e4);
69
+ }
70
+ }
71
+ void refresh();
72
+ return () => {
73
+ active = false;
74
+ clearTimeout(timer);
75
+ controller?.abort();
76
+ };
77
+ }, [apiUrl, clientToken, refreshIntervalMs]);
78
+ const value = useMemo(() => snapshot && snapshot.apiUrl === apiUrl && snapshot.clientToken === clientToken ? { isEnabled: (key) => Object.hasOwn(snapshot.flags, key) && snapshot.flags[key] === true } : disabledFlagEvaluator, [snapshot, apiUrl, clientToken]);
79
+ return /* @__PURE__ */ jsx2(FlagdownContext.Provider, { value, children });
24
80
  }
25
81
  export {
26
- FlagdownProvider,
27
- disabledFlagEvaluator,
28
- useFlag
82
+ Feature,
83
+ FlagdownProvider
29
84
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flagdown/react",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "React SDK for evaluating Flagdown feature flags.",
@@ -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",