@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 +1 -1
- package/src/constants/endpoints.ts +2 -0
- package/src/index.ts +6 -0
- package/src/lib/feature-flags.ts +124 -0
package/package.json
CHANGED
|
@@ -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
|
+
}
|