@startsimpli/api 0.5.0 → 0.5.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startsimpli/api",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Type-safe Django REST API client for StartSimpli apps",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -49,4 +49,6 @@ export const ENDPOINTS = {
49
49
  FUNNEL_RUN_BY_ID: (runId: string) => `api/v1/funnel-runs/${runId}`,
50
50
  FUNNEL_RUN_CANCEL: (runId: string) => `api/v1/funnel-runs/${runId}/cancel`,
51
51
  FUNNEL_RUNS_GLOBAL: 'api/v1/funnel-runs',
52
+ // Companies / Feature flags
53
+ FEATURE_FLAGS: 'api/v1/companies/feature-flags',
52
54
  } as const;
package/src/index.ts CHANGED
@@ -19,6 +19,10 @@ export type { Message, MessageStatus as MessageApiStatus, MessageRecipient, Mess
19
19
  export { FunnelsApi, isFunnelRunConflict, isFunnelValidationError } from './lib/funnels-api';
20
20
  export type { FunnelPreviewResult, FunnelRunFilters, FunnelTemplate } from './lib/funnels-api';
21
21
 
22
+ // Feature flags
23
+ export { FeatureFlagsApi, FeatureFlagProvider, useFeatureFlags } from './lib/feature-flags';
24
+ export type { FeatureFlags, FeatureFlagsResponse, FeatureFlagContextValue, FeatureFlagProviderProps } from './lib/feature-flags';
25
+
22
26
  // Fetch wrapper
23
27
  export { FetchWrapper } from './lib/fetch-wrapper';
24
28
  export type { FetchWrapperConfig } from './lib/fetch-wrapper';
@@ -121,6 +125,7 @@ import { EntitiesApi } from './lib/entities-api';
121
125
  import { WorkflowsApi } from './lib/workflows-api';
122
126
  import { MessagesApi } from './lib/messages-api';
123
127
  import { FunnelsApi } from './lib/funnels-api';
128
+ import { FeatureFlagsApi } from './lib/feature-flags';
124
129
 
125
130
  import type { ApiClientConfig } from './lib/api-client';
126
131
 
@@ -139,5 +144,6 @@ export function createStartSimpliApi(config: ApiClientConfig = {}) {
139
144
  workflows: new WorkflowsApi(client),
140
145
  messages: new MessagesApi(client),
141
146
  funnels: new FunnelsApi(client),
147
+ featureFlags: new FeatureFlagsApi(client),
142
148
  };
143
149
  }
@@ -0,0 +1,124 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Feature flags API client and React primitives.
5
+ *
6
+ * Flags are stored per-company in the Django backend (Company.settings.feature_flags).
7
+ * Default behavior is fail-closed: if a flag is not set, the feature is OFF.
8
+ * A company must explicitly enable features via Django admin or API.
9
+ *
10
+ * Convention:
11
+ * section_<name> – controls entire sidebar section visibility
12
+ * feature_<name> – controls individual feature availability
13
+ */
14
+
15
+ import { createElement, createContext, useContext, useState, useEffect, useCallback } from 'react';
16
+ import type { ReactNode } from 'react';
17
+ import { ENDPOINTS } from '../constants/endpoints';
18
+ import type { ApiClient } from './api-client';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Types
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export type FeatureFlags = Record<string, boolean>;
25
+
26
+ export interface FeatureFlagsResponse {
27
+ flags: FeatureFlags;
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // API class (used by createStartSimpliApi)
32
+ // ---------------------------------------------------------------------------
33
+
34
+ export class FeatureFlagsApi {
35
+ constructor(private client: ApiClient) {}
36
+
37
+ /** Fetch all flags for the current user's company. */
38
+ async getFlags(): Promise<FeatureFlags> {
39
+ const res = await this.client.get<FeatureFlagsResponse>(ENDPOINTS.FEATURE_FLAGS);
40
+ return res.flags;
41
+ }
42
+
43
+ /** Merge-update flags (admin only). Omitted flags are unchanged. */
44
+ async updateFlags(flags: Partial<FeatureFlags>): Promise<FeatureFlags> {
45
+ const res = await this.client.patch<FeatureFlagsResponse>(ENDPOINTS.FEATURE_FLAGS, { flags });
46
+ return res.flags;
47
+ }
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // React context + provider + hook
52
+ // ---------------------------------------------------------------------------
53
+
54
+ export interface FeatureFlagContextValue {
55
+ /** Raw flag map. Missing keys are treated as `false` (fail-closed). */
56
+ flags: FeatureFlags;
57
+ /** True while the initial fetch is in-flight. */
58
+ isLoading: boolean;
59
+ /** Check a single flag. Returns `false` when flag is absent (fail-closed). */
60
+ isEnabled: (flag: string) => boolean;
61
+ /** Convenience: check `section_<name>` flag. */
62
+ isSectionEnabled: (sectionTitle: string) => boolean;
63
+ }
64
+
65
+ const FeatureFlagContext = createContext<FeatureFlagContextValue>({
66
+ flags: {},
67
+ isLoading: true,
68
+ isEnabled: () => false,
69
+ isSectionEnabled: () => false,
70
+ });
71
+
72
+ export interface FeatureFlagProviderProps {
73
+ children: ReactNode;
74
+ /** Injected fetcher so the provider is decoupled from a specific ApiClient instance. */
75
+ fetchFlags: () => Promise<FeatureFlags>;
76
+ }
77
+
78
+ /**
79
+ * Wrap your authenticated layout with this provider.
80
+ * It fetches flags once on mount and makes them available via `useFeatureFlags()`.
81
+ */
82
+ export function FeatureFlagProvider({ children, fetchFlags }: FeatureFlagProviderProps) {
83
+ // null = not yet loaded (fail-open while loading or on error)
84
+ // Record = API responded with flags (fail-closed for missing keys)
85
+ const [flags, setFlags] = useState<FeatureFlags | null>(null);
86
+ const [isLoading, setIsLoading] = useState(true);
87
+
88
+ useEffect(() => {
89
+ let cancelled = false;
90
+ fetchFlags()
91
+ .then((f) => { if (!cancelled) setFlags(f); })
92
+ .catch(() => { if (!cancelled) setFlags(null); }) // network error = fail-open
93
+ .finally(() => { if (!cancelled) setIsLoading(false); });
94
+ return () => { cancelled = true; };
95
+ }, [fetchFlags]);
96
+
97
+ const isEnabled = useCallback(
98
+ (flag: string) => {
99
+ if (flags === null) return true; // API unreachable — fail-open
100
+ return flags[flag] ?? false; // API responded — fail-closed
101
+ },
102
+ [flags],
103
+ );
104
+
105
+ const isSectionEnabled = useCallback(
106
+ (sectionTitle: string) => {
107
+ if (flags === null) return true; // API unreachable — fail-open
108
+ const key = `section_${sectionTitle.toLowerCase()}`;
109
+ return flags[key] ?? false; // API responded — fail-closed
110
+ },
111
+ [flags],
112
+ );
113
+
114
+ return createElement(
115
+ FeatureFlagContext.Provider,
116
+ { value: { flags: flags ?? {}, isLoading, isEnabled, isSectionEnabled } },
117
+ children,
118
+ );
119
+ }
120
+
121
+ /** Access feature flags anywhere below FeatureFlagProvider. */
122
+ export function useFeatureFlags(): FeatureFlagContextValue {
123
+ return useContext(FeatureFlagContext);
124
+ }