@kelviq/react-sdk 0.0.1-beta
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 +377 -0
- package/dist/components/ShowWhenBooleanEntitled.d.ts +3 -0
- package/dist/components/ShowWhenConfigEntitled.d.ts +14 -0
- package/dist/components/ShowWhenEntitledInternal.d.ts +12 -0
- package/dist/components/ShowWhenMeteredEntitled.d.ts +13 -0
- package/dist/contexts/KelviqContext.d.ts +17 -0
- package/dist/contexts/KelviqProvider.d.ts +18 -0
- package/dist/hooks/index.d.ts +5 -0
- package/dist/hooks/useAllEntitlements.d.ts +7 -0
- package/dist/hooks/useBooleanEntitlement.d.ts +8 -0
- package/dist/hooks/useConfigEntitlement.d.ts +8 -0
- package/dist/hooks/useKelviq.d.ts +7 -0
- package/dist/hooks/useMeteredEntitlement.d.ts +8 -0
- package/dist/index.d.ts +11 -0
- package/dist/kelviq-react-sdk.js +461 -0
- package/dist/kelviq-react-sdk.umd.cjs +1 -0
- package/dist/services/makeRequest.service.d.ts +44 -0
- package/dist/types/api.types.d.ts +39 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/sdk.types.d.ts +68 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/transformations.d.ts +10 -0
- package/package.json +71 -0
package/README.md
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
|
|
2
|
+
The `@kelviq/react-sdk` provides a seamless way to integrate Kelviq entitlement management into your React applications.
|
|
3
|
+
|
|
4
|
+
## Overview
|
|
5
|
+
|
|
6
|
+
- A **React Context Provider** (`KelviqProvider`) to initialize and configure the SDK.
|
|
7
|
+
- **Hooks** to easily access entitlement status and data within your components.
|
|
8
|
+
- **Conditional rendering components** to show or hide UI elements based on feature access.
|
|
9
|
+
- Automatic and manual **data fetching** and caching mechanisms.
|
|
10
|
+
|
|
11
|
+
The SDK fetches all entitlements for a given organization and customer, caches them, and makes them available throughout your application via React Context and custom hooks.
|
|
12
|
+
|
|
13
|
+
## **2. Installation**
|
|
14
|
+
|
|
15
|
+
Install the SDK using npm or yarn:
|
|
16
|
+
|
|
17
|
+
``` bash
|
|
18
|
+
npm install @kelviq/react-sdk
|
|
19
|
+
# or
|
|
20
|
+
yarn add @kelviq/react-sdk
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
You will also need `react` as a peer dependency.
|
|
25
|
+
|
|
26
|
+
## **3. Setup & Configuration**
|
|
27
|
+
|
|
28
|
+
### `KelviqProvider`
|
|
29
|
+
|
|
30
|
+
The core of the SDK is the `KelviqProvider`. You need to wrap your application (or the relevant part of it) with this provider. It initializes the SDK and makes entitlement data available to its children components via context.
|
|
31
|
+
|
|
32
|
+
``` tsx
|
|
33
|
+
// In your App.tsx or main application entry point
|
|
34
|
+
import React from 'react';
|
|
35
|
+
import { KelviqProvider, PDPaywall } from '@kelviq/react-sdk';
|
|
36
|
+
|
|
37
|
+
const App = () => {
|
|
38
|
+
return (
|
|
39
|
+
<KelviqProvider
|
|
40
|
+
customerId="your-customer-id"
|
|
41
|
+
accessToken="your-access-token"
|
|
42
|
+
>
|
|
43
|
+
<MyAppContent />
|
|
44
|
+
</KelviqProvider>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const MyAppContent = () => {
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<PDPaywall offeringId="your-offering-id">
|
|
52
|
+
<div>My Application</div>
|
|
53
|
+
</PDPaywall>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default App;
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Required Props for `KelviqProvider`
|
|
63
|
+
|
|
64
|
+
- `customerId: string`
|
|
65
|
+
- The ID of the customer for whom entitlements are being fetched. This will be sent as a query parameter (`customer_id`).
|
|
66
|
+
- `accessToken: string | null`
|
|
67
|
+
- Your static API token or Bearer token for authenticating requests to the Kelviq API. If provided, it will be included in the `Authorization: Bearer <accessToken>` header.
|
|
68
|
+
|
|
69
|
+
### Optional Props for `KelviqProvider`
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
- `apiUrl: string`
|
|
73
|
+
- The base URL for your Kelviq API. For example, `https://edge.api.kelviq.com/api/v1`.
|
|
74
|
+
- `entitlementsPath: string`
|
|
75
|
+
- The specific path for the endpoint that returns all entitlements for a customer. This path will be appended to `apiUrl`. For example, `entitlement-check` or `user-entitlements`.
|
|
76
|
+
- `config?: KelviqApiBehaviorOptions`
|
|
77
|
+
- An optional object to configure more advanced behaviors of the SDK.
|
|
78
|
+
|
|
79
|
+
### **Configuration Object (**`config: KelviqApiBehaviorOptions`**)**
|
|
80
|
+
|
|
81
|
+
This optional object can contain the following properties:
|
|
82
|
+
|
|
83
|
+
- `onError?: (error: Error) => void`
|
|
84
|
+
- A callback function that will be invoked if an error occurs during the API request for entitlements.
|
|
85
|
+
- `maxRetries?: number`
|
|
86
|
+
- The maximum number of times to retry a failed API request. Defaults to the value set in your `apiRequest` service (typically 3).
|
|
87
|
+
- `timeout?: number`
|
|
88
|
+
- Timeout for API requests in milliseconds. Defaults to the value set in your `apiRequest` service (typically 5000ms).
|
|
89
|
+
- `backoffBaseDelay?: number`
|
|
90
|
+
- Base delay (in milliseconds) for the exponential backoff strategy used in retries. Defaults to the value set in your `apiRequest` service (typically 1000ms).
|
|
91
|
+
- `fetchEntitlementsOnMount?: boolean`
|
|
92
|
+
- Defaults to `true`. If true, entitlements are fetched automatically when the `KelviqProvider` mounts.
|
|
93
|
+
- Set to `false` if you want to control the initial fetch manually using the `refreshAllEntitlements` function from the `useKelviq` hook.
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
## **4. Fetching Entitlements**
|
|
97
|
+
|
|
98
|
+
### **Automatic Fetching on Mount**
|
|
99
|
+
|
|
100
|
+
By default (`fetchEntitlementsOnMount: true` in the `config` prop, or if `config` or this specific option is omitted), the SDK will attempt to fetch all entitlements for the specified `customerId` as soon as the `KelviqProvider` is mounted.
|
|
101
|
+
|
|
102
|
+
The `isLoading` state from `useKelviq()` will be `true` during this initial fetch.
|
|
103
|
+
|
|
104
|
+
### **Manual Fetching**
|
|
105
|
+
|
|
106
|
+
If `fetchEntitlementsOnMount` is set to `false`, or if you need to re-fetch entitlements at any point (e.g., after a user action that might change their entitlements), you can use the `refreshAllEntitlements` function.
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
``` tsx
|
|
110
|
+
import { useKelviq } from '@kelviq/react-sdk';
|
|
111
|
+
|
|
112
|
+
function MyComponent() {
|
|
113
|
+
const { refreshAllEntitlements, isLoading, error } = useKelviq();
|
|
114
|
+
|
|
115
|
+
const handleRefresh = async () => {
|
|
116
|
+
try {
|
|
117
|
+
await refreshAllEntitlements();
|
|
118
|
+
console.log("Entitlements refreshed!");
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.error("Failed to refresh entitlements:", e);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div>
|
|
126
|
+
<button onClick={handleRefresh} disabled={isLoading}>
|
|
127
|
+
{isLoading ? 'Refreshing...' : 'Refresh Entitlements'}
|
|
128
|
+
</button>
|
|
129
|
+
{error && <p style={{ color: 'red' }}>Error: {error.message}</p>}
|
|
130
|
+
{/* ... rest of your component ... */}
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## **5. Accessing Entitlement Data (Hooks)**
|
|
138
|
+
|
|
139
|
+
The SDK provides several hooks to access entitlement data and SDK state.
|
|
140
|
+
|
|
141
|
+
### `useKelviq()`
|
|
142
|
+
|
|
143
|
+
This is the primary hook to access the SDK's context.
|
|
144
|
+
|
|
145
|
+
- **Returns:** `KelviqContextValue` object containing:
|
|
146
|
+
- `allEntitlements: AsyncState<EntitlementMap | null>`: The state object for all fetched entitlements.
|
|
147
|
+
- `data`: An `EntitlementMap` (a `Record<string, AnyEntitlement>`) where keys are `featureKey`s, or `null` if not loaded or error.
|
|
148
|
+
- `isLoading`: Boolean indicating if the `allEntitlements` data is currently being fetched/refreshed.
|
|
149
|
+
- `error`: An `Error` object if the last fetch failed, otherwise `null`.
|
|
150
|
+
- `refreshAllEntitlements: () => Promise<void>`: Function to manually trigger a re-fetch of all entitlements.
|
|
151
|
+
- `getEntitlement: <T extends AnyEntitlement>(featureKey: string, type: T['type']) => AsyncState<T | null>`: Function to get a specific entitlement's state from the cache.
|
|
152
|
+
- `hasAccess: (featureKey: string) => boolean | undefined`: A utility function to quickly check access for a feature. Returns `true` if access is granted, `false` if denied or feature not found, and `undefined` if data is loading or an error occurred.
|
|
153
|
+
- `isLoading: boolean`: A global loading state indicating if the SDK is performing its initial configured fetch or a refresh operation.
|
|
154
|
+
- `error: Error | null`: A global error state reflecting any error from the last fetch operation initiated by the provider.
|
|
155
|
+
- `customerId: string`: The customer ID passed to the provider.
|
|
156
|
+
- `apiUrl: string`: The base API URL passed to the provider.
|
|
157
|
+
- `entitlementsPath: string`: The entitlements path passed to the provider.
|
|
158
|
+
|
|
159
|
+
### `useAllEntitlements()`
|
|
160
|
+
|
|
161
|
+
A convenience hook that directly returns the `allEntitlements` state object.
|
|
162
|
+
|
|
163
|
+
- **Returns:** `AsyncState<EntitlementMap | null>`
|
|
164
|
+
|
|
165
|
+
``` tsx
|
|
166
|
+
import { useAllEntitlements } from '@kelviq/react-sdk';
|
|
167
|
+
|
|
168
|
+
function EntitlementsList() {
|
|
169
|
+
const { data: entitlementsMap, isLoading, error } = useAllEntitlements();
|
|
170
|
+
|
|
171
|
+
if (isLoading) return <p>Loading all entitlements...</p>;
|
|
172
|
+
if (error) return <p>Error loading entitlements: {error.message}</p>;
|
|
173
|
+
if (!entitlementsMap) return <p>No entitlements data.</p>;
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<ul>
|
|
177
|
+
{Object.values(entitlementsMap).map(ent => (
|
|
178
|
+
<li key={ent.featureKey}>
|
|
179
|
+
{ent.featureKey}: {ent.hasAccess ? 'Enabled' : 'Disabled'} ({ent.type})
|
|
180
|
+
</li>
|
|
181
|
+
))}
|
|
182
|
+
</ul>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `useBooleanEntitlement(featureKey: string)`
|
|
189
|
+
|
|
190
|
+
Fetches the state for a specific boolean entitlement from the cache.
|
|
191
|
+
|
|
192
|
+
- **Arguments:**
|
|
193
|
+
- `featureKey: string`: The key of the boolean feature.
|
|
194
|
+
- **Returns:** `AsyncState<BooleanEntitlement | null>`
|
|
195
|
+
|
|
196
|
+
### `useConfigEntitlement(featureKey: string)`
|
|
197
|
+
|
|
198
|
+
Fetches the state for a specific configuration entitlement from the cache.
|
|
199
|
+
|
|
200
|
+
- **Arguments:**
|
|
201
|
+
- `featureKey: string`: The key of the configuration feature.
|
|
202
|
+
- **Returns:** `AsyncState<ConfigEntitlement | null>` (where `ConfigEntitlement` has a `configuration: number | null` property).
|
|
203
|
+
|
|
204
|
+
### `useMeteredEntitlement(featureKey: string)`
|
|
205
|
+
|
|
206
|
+
Fetches the state for a specific metered entitlement from the cache.
|
|
207
|
+
|
|
208
|
+
- **Arguments:**
|
|
209
|
+
- `featureKey: string`: The key of the metered feature.
|
|
210
|
+
- **Returns:** `AsyncState<MeteredEntitlement | null>`
|
|
211
|
+
|
|
212
|
+
**Example using a specific entitlement hook:**
|
|
213
|
+
|
|
214
|
+
``` tsx
|
|
215
|
+
import { useBooleanEntitlement } from '@kelviq/react-sdk';
|
|
216
|
+
|
|
217
|
+
function FeatureSpecificComponent({ featureKey }: { featureKey: string }) {
|
|
218
|
+
const { data: entitlement, isLoading, error } = useBooleanEntitlement(featureKey);
|
|
219
|
+
|
|
220
|
+
if (isLoading) return <p>Loading feature {featureKey}...</p>;
|
|
221
|
+
if (error) return <p>Error loading feature {featureKey}: {error.message}</p>;
|
|
222
|
+
|
|
223
|
+
if (entitlement && entitlement.hasAccess) {
|
|
224
|
+
return <p>You have access to {featureKey}!</p>;
|
|
225
|
+
} else {
|
|
226
|
+
return <p>You do not have access to {featureKey}.</p>;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## **6. Conditional Rendering Components**
|
|
233
|
+
|
|
234
|
+
These components provide a declarative way to render UI based on entitlement status. They internally use the respective hooks.
|
|
235
|
+
|
|
236
|
+
**Common Props:**
|
|
237
|
+
|
|
238
|
+
- `featureKey: string`: The unique key of the feature to check.
|
|
239
|
+
- `children: ReactNode | ((data: E) => ReactNode)`:
|
|
240
|
+
- If a `ReactNode`, it's rendered when the user is entitled and conditions are met.
|
|
241
|
+
- If a function (render prop), it's called with the entitlement data (`BooleanEntitlement`, `ConfigEntitlement`, or `MeteredEntitlement`) and its return value is rendered.
|
|
242
|
+
- `fallback?: ReactNode`: Content to render if the user is not entitled, or if data is loading (and no `loadingComponent` is provided), or if an error occurs. Defaults to `null`.
|
|
243
|
+
- `loadingComponent?: ReactNode`: Specific content to render while the entitlement data is loading. Overrides `fallback` during loading.
|
|
244
|
+
|
|
245
|
+
### `ShowWhenBooleanEntitled`
|
|
246
|
+
|
|
247
|
+
Renders children if the boolean feature is enabled (`hasAccess: true`).
|
|
248
|
+
|
|
249
|
+
``` tsx
|
|
250
|
+
import { ShowWhenBooleanEntitled } from '@kelviq/react-sdk';
|
|
251
|
+
|
|
252
|
+
<ShowWhenBooleanEntitled
|
|
253
|
+
featureKey="enable-dark-mode"
|
|
254
|
+
loadingComponent={<p>Checking theme settings...</p>}
|
|
255
|
+
fallback={<p>Dark mode is not available.</p>}
|
|
256
|
+
>
|
|
257
|
+
<button>Toggle Dark Mode</button>
|
|
258
|
+
</ShowWhenBooleanEntitled>
|
|
259
|
+
|
|
260
|
+
<ShowWhenBooleanEntitled featureKey="show-advanced-settings">
|
|
261
|
+
{(entitlementData) => (
|
|
262
|
+
<div>Advanced settings for {entitlementData.featureKey} are visible!</div>
|
|
263
|
+
)}
|
|
264
|
+
</ShowWhenBooleanEntitled>
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### `ShowWhenConfigEntitled`
|
|
269
|
+
|
|
270
|
+
Renders children if the config feature is enabled and provides the numeric configuration value to the children render prop.
|
|
271
|
+
|
|
272
|
+
- **Children Prop:** `(config: number) => ReactNode` (Required to be a function for this component).
|
|
273
|
+
|
|
274
|
+
``` tsx
|
|
275
|
+
import { ShowWhenConfigEntitled } from '@kelviq/react-sdk';
|
|
276
|
+
|
|
277
|
+
<ShowWhenConfigEntitled
|
|
278
|
+
featureKey="max-items-per-page"
|
|
279
|
+
loadingComponent={<p>Loading display settings...</p>}
|
|
280
|
+
fallback={<p>Default item limit applies.</p>}
|
|
281
|
+
>
|
|
282
|
+
{(maxItems) => <p>You can display up to {maxItems} items per page.</p>}
|
|
283
|
+
</ShowWhenConfigEntitled>
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### `ShowWhenMeteredEntitled`
|
|
288
|
+
|
|
289
|
+
Renders children if the metered feature is enabled and, by default, if `remaining` usage is greater than 0 or unlimited (`limit` is `null`).
|
|
290
|
+
|
|
291
|
+
- **Optional Prop:**
|
|
292
|
+
- `condition?: (data: MeteredEntitlement) => boolean`: A custom function to further determine if children should render based on the metered entitlement data.
|
|
293
|
+
- **Children Prop:** Can be `ReactNode` or `(data: MeteredEntitlement) => ReactNode`.
|
|
294
|
+
|
|
295
|
+
``` tsx
|
|
296
|
+
import { ShowWhenMeteredEntitled } from '@kelviq/react-sdk';
|
|
297
|
+
|
|
298
|
+
<ShowWhenMeteredEntitled
|
|
299
|
+
featureKey="api-calls-quota"
|
|
300
|
+
loadingComponent={<p>Loading API quota...</p>}
|
|
301
|
+
fallback={<p>API call limit reached or feature not available.</p>}
|
|
302
|
+
>
|
|
303
|
+
{(meterData) => (
|
|
304
|
+
<div>
|
|
305
|
+
<p>API Calls Used: {meterData.used} / {meterData.limit === null ? 'Unlimited' : meterData.limit}</p>
|
|
306
|
+
<p>Remaining: {meterData.remaining === null ? 'Unlimited' : meterData.remaining}</p>
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
</ShowWhenMeteredEntitled>
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## **7. Key Types**
|
|
314
|
+
|
|
315
|
+
The SDK exports several types for better integration with TypeScript. Some key ones include:
|
|
316
|
+
|
|
317
|
+
- `KelviqProviderOptions`: Configuration for the provider.
|
|
318
|
+
- `KelviqContextValue`: The shape of the object returned by `useKelviq()`.
|
|
319
|
+
- `AsyncState<T>`: Generic type for asynchronous data states.
|
|
320
|
+
- `EntitlementMap`: The structure of the cached entitlements (`Record<string, AnyEntitlement>`).
|
|
321
|
+
- `Entitlement`, `BooleanEntitlement`, `ConfigEntitlement`, `MeteredEntitlement`: Processed entitlement types.
|
|
322
|
+
- `ApiRequestConfig`, `ApiResponse`: Interfaces related to the underlying `apiRequest` service.
|
|
323
|
+
- `RawEntitlementsApiResponse` and related `Raw...` types: Represent the structure of the direct API response.
|
|
324
|
+
|
|
325
|
+
You can import these types from `@kelviq/react-sdk/types` (assuming your `src/types/index.ts` re-exports them) or directly from the main SDK entry if configured.
|
|
326
|
+
|
|
327
|
+
## **8. Error Handling**
|
|
328
|
+
|
|
329
|
+
- **Provider Level:** The `onError` callback in `KelviqProviderOptions` can be used to globally handle errors that occur during the entitlement fetching process initiated by the provider.
|
|
330
|
+
- **Hook Level:** Each hook (`useAllEntitlements`, `useBooleanEntitlement`, etc.) returns an `AsyncState` object which includes an `error: Error | null` property. You can check this to handle errors specific to that data slice.
|
|
331
|
+
- **Context Level:** `useKelviq()` also returns a global `error: Error | null` state, reflecting errors from the provider's most recent fetch operation.
|
|
332
|
+
|
|
333
|
+
## **9. Example Usage**
|
|
334
|
+
|
|
335
|
+
A comprehensive example is provided within the comments of the SDK code, demonstrating provider setup and component usage. The core setup involves:
|
|
336
|
+
|
|
337
|
+
1. Wrapping your application with `KelviqProvider` and providing the required props (`customerId`, `accessToken`)
|
|
338
|
+
2. Using hooks like `useBooleanEntitlement` or conditional components like `ShowWhenBooleanEntitled` in your components to control UI and behavior based on entitlements.
|
|
339
|
+
|
|
340
|
+
## **10. API Request Service**
|
|
341
|
+
|
|
342
|
+
The SDK relies on an `apiRequest` function for making HTTP requests. The SDK's `KelviqProvider` passes configuration like `timeout`, `maxRetries`, `backoffBaseDelay`, and `accessToken` to this function.
|
|
343
|
+
|
|
344
|
+
- **Your Responsibility:** You need to ensure that your project includes an implementation of this `apiRequest` service (e.g., in `src/services/makeRequest.service.ts`) that matches the `ApiRequestConfig` and `ApiResponse` interfaces used by the SDK. This service should handle actual HTTP communication, including appending query parameters passed via `ApiRequestConfig.queryParams` and using the `accessToken` to set the `Authorization` header.
|
|
345
|
+
- The placeholder `apiRequest` function included in the SDK's source code is for demonstration and type-checking purposes only and will not make real API calls.
|
|
346
|
+
|
|
347
|
+
## **11. Project Structure (for contributors/reference)**
|
|
348
|
+
|
|
349
|
+
The SDK source code (`src/`) is organized as follows:
|
|
350
|
+
|
|
351
|
+
- `components/`: Contains conditional rendering React components.
|
|
352
|
+
- `ShowWhenBooleanEntitled.tsx`
|
|
353
|
+
- `ShowWhenConfigEntitled.tsx`
|
|
354
|
+
- `ShowWhenMeteredEntitled.tsx`
|
|
355
|
+
- `ShowWhenEntitledInternal.tsx` (Internal helper, not typically exported)
|
|
356
|
+
- `index.ts` (Exports public components)
|
|
357
|
+
- `context/`: Defines the React Context.
|
|
358
|
+
- `KelviqContext.ts`
|
|
359
|
+
- `hooks/`: Contains all custom React Hooks.
|
|
360
|
+
- `useKelviq.ts`
|
|
361
|
+
- `useAllEntitlements.ts`
|
|
362
|
+
- `useBooleanEntitlement.ts`
|
|
363
|
+
- `useConfigEntitlement.ts`
|
|
364
|
+
- `useMeteredEntitlement.ts`
|
|
365
|
+
- `index.ts` (Exports all hooks)
|
|
366
|
+
- `provider/`: Contains the main `KelviqProvider`.
|
|
367
|
+
- `KelviqProvider.tsx`
|
|
368
|
+
- `types/`: Contains all TypeScript definitions.
|
|
369
|
+
- `api.types.ts` (Raw types from API)
|
|
370
|
+
- `sdk.types.ts` (Processed types for SDK and public use)
|
|
371
|
+
- `index.ts` (Re-exports all types)
|
|
372
|
+
- `utils/`: Contains utility functions.
|
|
373
|
+
- `transformations.ts` (For `transformApiEntitlements`)
|
|
374
|
+
- `index.ts` (Exports utilities)
|
|
375
|
+
- `index.ts`: The main entry point of the SDK, re-exporting all public APIs (provider, hooks, components, types).
|
|
376
|
+
|
|
377
|
+
This documentation should provide a good starting point for developers using your `@kelviq/react-sdk`. Remember to replace placeholder URLs and token examples with relevant information for your users.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { ConfigEntitlement } from '../types/sdk.types';
|
|
3
|
+
import { ShowWhenEntitledProps } from './ShowWhenEntitledInternal';
|
|
4
|
+
/**
|
|
5
|
+
* Conditionally renders children based on a configuration entitlement.
|
|
6
|
+
* The children prop is a render function that receives the numeric configuration value.
|
|
7
|
+
* @param featureKey The key of the configuration feature.
|
|
8
|
+
* @param children A function that receives the numeric configuration and returns a ReactNode.
|
|
9
|
+
* @param fallback Content to render if not entitled, loading (and no loadingComponent), error, or config is not a number.
|
|
10
|
+
* @param loadingComponent Specific content to render while the entitlement is loading.
|
|
11
|
+
*/
|
|
12
|
+
export declare const ShowWhenConfigEntitled: React.FC<Omit<ShowWhenEntitledProps<ConfigEntitlement>, 'condition' | 'type' | 'children'> & {
|
|
13
|
+
children: (config: number) => ReactNode;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { AnyEntitlement } from '../types/sdk.types';
|
|
3
|
+
export interface ShowWhenEntitledProps<E extends AnyEntitlement> {
|
|
4
|
+
featureKey: string;
|
|
5
|
+
children: ReactNode | ((data: E) => ReactNode);
|
|
6
|
+
fallback?: ReactNode;
|
|
7
|
+
loadingComponent?: ReactNode;
|
|
8
|
+
condition?: (data: E) => boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const ShowWhenEntitledInternal: <E extends AnyEntitlement>({ featureKey, type, children, fallback, loadingComponent, condition, }: ShowWhenEntitledProps<E> & {
|
|
11
|
+
type: E["type"];
|
|
12
|
+
}) => JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { MeteredEntitlement } from '../types/sdk.types';
|
|
2
|
+
import { ShowWhenEntitledProps } from './ShowWhenEntitledInternal';
|
|
3
|
+
/**
|
|
4
|
+
* Conditionally renders children based on a metered entitlement.
|
|
5
|
+
* By default, children are rendered if hasAccess is true and remaining usage is greater than 0 or unlimited.
|
|
6
|
+
* A custom `condition` function can be provided.
|
|
7
|
+
* @param featureKey The key of the metered feature.
|
|
8
|
+
* @param children Content to render if entitled and condition is met. Can be a ReactNode or a render function receiving the entitlement data.
|
|
9
|
+
* @param fallback Content to render if not entitled, loading (and no loadingComponent), error, or condition not met.
|
|
10
|
+
* @param loadingComponent Specific content to render while the entitlement is loading.
|
|
11
|
+
* @param condition Optional function to further refine when children are rendered based on metered data.
|
|
12
|
+
*/
|
|
13
|
+
export declare const ShowWhenMeteredEntitled: React.FC<Omit<ShowWhenEntitledProps<MeteredEntitlement>, 'type'>>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EntitlementMap, AnyEntitlement } from '../types/sdk.types';
|
|
2
|
+
import { AsyncState } from '../types';
|
|
3
|
+
/** Defines the shape of the context value provided by KelviqProvider. */
|
|
4
|
+
export interface KelviqContextValue {
|
|
5
|
+
allEntitlements: AsyncState<EntitlementMap | null>;
|
|
6
|
+
refreshAllEntitlements: () => Promise<void>;
|
|
7
|
+
getEntitlement: <T extends AnyEntitlement>(featureKey: string, type: T['type']) => AsyncState<T | null>;
|
|
8
|
+
hasAccess: (featureKey: string) => boolean | undefined;
|
|
9
|
+
isLoading: boolean;
|
|
10
|
+
error: Error | null;
|
|
11
|
+
customerId: string;
|
|
12
|
+
environment: string;
|
|
13
|
+
apiUrl: string;
|
|
14
|
+
entitlementsPath: string;
|
|
15
|
+
}
|
|
16
|
+
export declare const defaultKelviqContextValue: KelviqContextValue;
|
|
17
|
+
export declare const KelviqContext: import('react').Context<KelviqContextValue>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { default as React, ReactNode } from 'react';
|
|
2
|
+
import { KelviqApiBehaviorOptions } from '../types/sdk.types';
|
|
3
|
+
interface KelviqProviderProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
/** Base URL for the Kelviq API (e.g., https://edge.stagingapi.kelviq.com/api/v1). Required. */
|
|
6
|
+
apiUrl?: string;
|
|
7
|
+
/** Path for fetching all entitlements (e.g., "/entitlements"). Required. */
|
|
8
|
+
entitlementsPath?: string;
|
|
9
|
+
/** The customer ID. Required. */
|
|
10
|
+
customerId: string;
|
|
11
|
+
environment?: 'production' | 'sandbox';
|
|
12
|
+
/** Optional: The static authentication token string for API requests. */
|
|
13
|
+
accessToken: string | null;
|
|
14
|
+
/** Optional: Further configuration options for API behavior. */
|
|
15
|
+
config?: KelviqApiBehaviorOptions;
|
|
16
|
+
}
|
|
17
|
+
export declare const KelviqProvider: React.FC<KelviqProviderProps>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { AsyncState } from '../types';
|
|
2
|
+
import { EntitlementMap } from '../types/sdk.types';
|
|
3
|
+
/**
|
|
4
|
+
* Hook to get the state of all fetched and processed entitlements.
|
|
5
|
+
* @returns AsyncState containing the map of all entitlements (EntitlementMap) or null.
|
|
6
|
+
*/
|
|
7
|
+
export declare const useAllEntitlements: () => AsyncState<EntitlementMap | null>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AsyncState } from '../types';
|
|
2
|
+
import { BooleanEntitlement } from '../types/sdk.types';
|
|
3
|
+
/**
|
|
4
|
+
* Hook to get a specific boolean entitlement's state.
|
|
5
|
+
* @param featureKey The key of the boolean feature.
|
|
6
|
+
* @returns AsyncState containing the BooleanEntitlement or null if not found/error.
|
|
7
|
+
*/
|
|
8
|
+
export declare const useBooleanEntitlement: (featureKey: string) => AsyncState<BooleanEntitlement | null>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AsyncState } from '../types';
|
|
2
|
+
import { ConfigEntitlement } from '../types/sdk.types';
|
|
3
|
+
/**
|
|
4
|
+
* Hook to get a specific configuration entitlement's state.
|
|
5
|
+
* @param featureKey The key of the configuration feature.
|
|
6
|
+
* @returns AsyncState containing the ConfigEntitlement (with numeric configuration) or null.
|
|
7
|
+
*/
|
|
8
|
+
export declare const useConfigEntitlement: (featureKey: string) => AsyncState<ConfigEntitlement | null>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { KelviqContextValue } from '../contexts/KelviqContext';
|
|
2
|
+
/**
|
|
3
|
+
* Custom hook to access the Kelviq context.
|
|
4
|
+
* Provides access to entitlement data, loading states, and refresh functions.
|
|
5
|
+
* Throws an error if used outside of a KelviqProvider.
|
|
6
|
+
*/
|
|
7
|
+
export declare const useKelviq: () => KelviqContextValue;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AsyncState } from '../types';
|
|
2
|
+
import { MeteredEntitlement } from '../types/sdk.types';
|
|
3
|
+
/**
|
|
4
|
+
* Hook to get a specific metered entitlement's state.
|
|
5
|
+
* @param featureKey The key of the metered feature.
|
|
6
|
+
* @returns AsyncState containing the MeteredEntitlement or null.
|
|
7
|
+
*/
|
|
8
|
+
export declare const useMeteredEntitlement: (featureKey: string) => AsyncState<MeteredEntitlement | null>;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './contexts/KelviqProvider';
|
|
2
|
+
export type * from './types';
|
|
3
|
+
export * from './components/ShowWhenConfigEntitled';
|
|
4
|
+
export * from './components/ShowWhenMeteredEntitled';
|
|
5
|
+
export * from './components/ShowWhenBooleanEntitled';
|
|
6
|
+
export * from './contexts/KelviqContext';
|
|
7
|
+
export * from './hooks/useKelviq';
|
|
8
|
+
export * from './hooks/useAllEntitlements';
|
|
9
|
+
export * from './hooks/useBooleanEntitlement';
|
|
10
|
+
export * from './hooks/useConfigEntitlement';
|
|
11
|
+
export * from './hooks/useMeteredEntitlement';
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { jsx as q, Fragment as b } from "react/jsx-runtime";
|
|
2
|
+
import { createContext as C, useState as U, useCallback as R, useEffect as _, useMemo as F, useContext as O } from "react";
|
|
3
|
+
const B = "GET", j = 5e3, N = 3, z = 1e3;
|
|
4
|
+
function G(e) {
|
|
5
|
+
const {
|
|
6
|
+
method: i = B,
|
|
7
|
+
headers: t = {},
|
|
8
|
+
// Renamed to avoid conflict with internal 'headers' variable
|
|
9
|
+
body: n,
|
|
10
|
+
timeout: v = j,
|
|
11
|
+
maxRetries: o = N,
|
|
12
|
+
backoffBaseDelay: y = z,
|
|
13
|
+
queryParams: p,
|
|
14
|
+
accessToken: w
|
|
15
|
+
// Use the direct accessToken
|
|
16
|
+
} = e;
|
|
17
|
+
let { url: f } = e, c = 0;
|
|
18
|
+
return new Promise((T, m) => {
|
|
19
|
+
if (p) {
|
|
20
|
+
const s = new URLSearchParams();
|
|
21
|
+
for (const h in p)
|
|
22
|
+
p[h] !== void 0 && s.append(h, String(p[h]));
|
|
23
|
+
s.toString() && (f += (f.includes("?") ? "&" : "?") + s.toString());
|
|
24
|
+
}
|
|
25
|
+
function a() {
|
|
26
|
+
const s = new XMLHttpRequest();
|
|
27
|
+
s.open(i, f, !0), s.timeout = v;
|
|
28
|
+
const h = {
|
|
29
|
+
Accept: "application/json",
|
|
30
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
31
|
+
...t
|
|
32
|
+
// Apply custom headers, allowing them to override defaults
|
|
33
|
+
};
|
|
34
|
+
i !== "GET" && n && typeof n == "object" && !(n instanceof FormData) && (h["Content-Type"] || (h["Content-Type"] = "application/json")), w && (h.Authorization = `Bearer ${w}`);
|
|
35
|
+
for (const [l, E] of Object.entries(h))
|
|
36
|
+
s.setRequestHeader(l, E);
|
|
37
|
+
s.onload = function() {
|
|
38
|
+
const { status: l, statusText: E, responseText: A } = s;
|
|
39
|
+
if (l >= 200 && l < 300)
|
|
40
|
+
try {
|
|
41
|
+
const S = A ? JSON.parse(A) : {};
|
|
42
|
+
T({ status: l, statusText: E, data: S });
|
|
43
|
+
} catch (S) {
|
|
44
|
+
const $ = S instanceof Error ? S : new Error(String(S));
|
|
45
|
+
console.error(
|
|
46
|
+
`Kelviq SDK (apiRequest): Invalid JSON response for URL ${f}. Error: ${$.message}. Response: ${A}`
|
|
47
|
+
), m(
|
|
48
|
+
new Error(
|
|
49
|
+
`Invalid JSON response: ${$.message}`
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
else
|
|
54
|
+
L(
|
|
55
|
+
`Request to ${f} failed with status ${l} ${E}. Response: ${A}`
|
|
56
|
+
);
|
|
57
|
+
}, s.onerror = () => L(
|
|
58
|
+
`Network error for URL ${f}. The request could not be completed.`
|
|
59
|
+
), s.ontimeout = () => L(`Request to ${f} timed out.`);
|
|
60
|
+
function L(l) {
|
|
61
|
+
if (c < o) {
|
|
62
|
+
c++;
|
|
63
|
+
const E = Math.min(
|
|
64
|
+
y * Math.pow(2, c - 1),
|
|
65
|
+
3e4
|
|
66
|
+
// Cap retry delay at 30 seconds
|
|
67
|
+
);
|
|
68
|
+
console.warn(
|
|
69
|
+
`Kelviq SDK (apiRequest): Retrying request to ${f}. Attempt ${c}/${o}. Error: ${l}. Retrying in ${E}ms.`
|
|
70
|
+
), setTimeout(a, E);
|
|
71
|
+
} else
|
|
72
|
+
m(
|
|
73
|
+
new Error(
|
|
74
|
+
`${l} (Max retries ${o} reached)`
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
let l = n;
|
|
80
|
+
n && typeof n == "object" && !(n instanceof FormData) && h["Content-Type"] === "application/json" && (l = JSON.stringify(n)), s.send(l);
|
|
81
|
+
} catch (l) {
|
|
82
|
+
const E = l instanceof Error ? l : new Error(String(l));
|
|
83
|
+
console.error(
|
|
84
|
+
`Kelviq SDK (apiRequest): Error sending request to ${f}.`,
|
|
85
|
+
E
|
|
86
|
+
), m(new Error(`Failed to send request: ${E.message}`));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
a();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function H(e) {
|
|
93
|
+
const i = {};
|
|
94
|
+
return !e || !Array.isArray(e.entitlements) ? (console.warn(
|
|
95
|
+
"Kelviq SDK: Invalid or empty entitlements array in API response.",
|
|
96
|
+
e
|
|
97
|
+
), i) : (e.entitlements.forEach((t) => {
|
|
98
|
+
if (!t || typeof t.featureId != "string" || typeof t.featureType != "string") {
|
|
99
|
+
console.warn(
|
|
100
|
+
"Kelviq SDK: Skipping invalid raw entitlement object:",
|
|
101
|
+
t
|
|
102
|
+
);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
let n = null;
|
|
106
|
+
const v = {
|
|
107
|
+
// Omit type as it's added in switch
|
|
108
|
+
featureKey: t.featureId,
|
|
109
|
+
hasAccess: t.hasAccess
|
|
110
|
+
};
|
|
111
|
+
switch (t.featureType) {
|
|
112
|
+
case "BOOLEAN":
|
|
113
|
+
n = {
|
|
114
|
+
...v,
|
|
115
|
+
type: "boolean"
|
|
116
|
+
};
|
|
117
|
+
break;
|
|
118
|
+
case "CUSTOMIZABLE": {
|
|
119
|
+
const o = t;
|
|
120
|
+
n = {
|
|
121
|
+
...v,
|
|
122
|
+
type: "customizable",
|
|
123
|
+
configuration: typeof o.value == "number" ? o.value : null
|
|
124
|
+
};
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case "METER": {
|
|
128
|
+
const o = t, y = typeof o.usageLimit == "number" ? o.usageLimit : null, p = typeof o.currentUsage == "number" ? o.currentUsage : 0;
|
|
129
|
+
n = {
|
|
130
|
+
...v,
|
|
131
|
+
type: "metered",
|
|
132
|
+
limit: y,
|
|
133
|
+
used: p,
|
|
134
|
+
remaining: y !== null ? y - p : null,
|
|
135
|
+
// If limit is null (unlimited), remaining is also null
|
|
136
|
+
resetAt: o.resetAt,
|
|
137
|
+
hardLimit: o.hardLimit
|
|
138
|
+
};
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
default: {
|
|
142
|
+
const o = t;
|
|
143
|
+
console.warn(
|
|
144
|
+
`Kelviq SDK: Encountered unknown or unhandled entitlement featureType: '${String(
|
|
145
|
+
o.featureType
|
|
146
|
+
)}' for feature '${String(
|
|
147
|
+
o.featureId
|
|
148
|
+
)}'. This entitlement will be ignored.`
|
|
149
|
+
);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
n && (i[n.featureKey] = n);
|
|
154
|
+
}), i);
|
|
155
|
+
}
|
|
156
|
+
const W = () => ({
|
|
157
|
+
data: null,
|
|
158
|
+
isLoading: !1,
|
|
159
|
+
error: new Error(
|
|
160
|
+
"KelviqContext not fully initialized or entitlements not fetched."
|
|
161
|
+
)
|
|
162
|
+
}), I = {
|
|
163
|
+
allEntitlements: {
|
|
164
|
+
data: null,
|
|
165
|
+
isLoading: !0,
|
|
166
|
+
error: null
|
|
167
|
+
},
|
|
168
|
+
refreshAllEntitlements: () => (console.warn(
|
|
169
|
+
"refreshAllEntitlements called on default KelviqContext. Ensure KelviqProvider is properly set up."
|
|
170
|
+
), Promise.resolve()),
|
|
171
|
+
getEntitlement: W,
|
|
172
|
+
hasAccess: () => {
|
|
173
|
+
},
|
|
174
|
+
isLoading: !0,
|
|
175
|
+
error: null,
|
|
176
|
+
apiUrl: "",
|
|
177
|
+
environment: "production",
|
|
178
|
+
entitlementsPath: "",
|
|
179
|
+
customerId: ""
|
|
180
|
+
}, M = C(
|
|
181
|
+
I
|
|
182
|
+
), X = "https://edge.api.kelviq.com/api/v1/", J = "https://edge.sandboxapi.kelviq.com/api/v1/", V = "entitlements/", Q = ({
|
|
183
|
+
children: e,
|
|
184
|
+
apiUrl: i,
|
|
185
|
+
// Direct prop
|
|
186
|
+
entitlementsPath: t = V,
|
|
187
|
+
// Direct prop
|
|
188
|
+
customerId: n,
|
|
189
|
+
// Direct prop
|
|
190
|
+
environment: v = "production",
|
|
191
|
+
accessToken: o,
|
|
192
|
+
// Direct prop
|
|
193
|
+
config: y = {}
|
|
194
|
+
// Optional config object with defaults
|
|
195
|
+
}) => {
|
|
196
|
+
const {
|
|
197
|
+
onError: p,
|
|
198
|
+
maxRetries: w,
|
|
199
|
+
timeout: f,
|
|
200
|
+
backoffBaseDelay: c,
|
|
201
|
+
fetchEntitlementsOnMount: T = !0
|
|
202
|
+
// Default within config
|
|
203
|
+
} = y, m = i || (v === "sandbox" ? J : X), [a, s] = U({
|
|
204
|
+
data: null,
|
|
205
|
+
isLoading: !!m && T,
|
|
206
|
+
// Only loading if apiBaseUrl is present and fetchOnMount is true
|
|
207
|
+
error: null
|
|
208
|
+
}), [h, L] = U(
|
|
209
|
+
!!m && T
|
|
210
|
+
), [l, E] = U(null), A = R(
|
|
211
|
+
(g, d, r) => {
|
|
212
|
+
const u = new Error(
|
|
213
|
+
`Kelviq SDK (${d}): ${g}`
|
|
214
|
+
);
|
|
215
|
+
console.error(u.message, r || ""), E((P) => P || u), p && p(u);
|
|
216
|
+
},
|
|
217
|
+
[p]
|
|
218
|
+
), S = R(() => {
|
|
219
|
+
if (!m) {
|
|
220
|
+
const r = "API URL not configured. Cannot fetch entitlements.";
|
|
221
|
+
return s({
|
|
222
|
+
data: null,
|
|
223
|
+
isLoading: !1,
|
|
224
|
+
error: new Error(r)
|
|
225
|
+
}), A(r, "fetchAllEntitlements"), L(!1), Promise.reject(new Error(r));
|
|
226
|
+
}
|
|
227
|
+
if (!n) {
|
|
228
|
+
const r = "CustomerId must be provided as props to KelviqProvider.";
|
|
229
|
+
return s(() => ({
|
|
230
|
+
data: null,
|
|
231
|
+
isLoading: !1,
|
|
232
|
+
error: new Error(r)
|
|
233
|
+
})), A(r, "fetchAllEntitlements"), L(!1), Promise.reject(new Error(r));
|
|
234
|
+
}
|
|
235
|
+
s((r) => ({
|
|
236
|
+
...r,
|
|
237
|
+
isLoading: !0,
|
|
238
|
+
error: null
|
|
239
|
+
})), L(!0), E(null);
|
|
240
|
+
const g = `${m.replace(/\/$/, "")}/${t.replace(/^\//, "")}`;
|
|
241
|
+
return G({
|
|
242
|
+
url: g,
|
|
243
|
+
method: "GET",
|
|
244
|
+
timeout: f,
|
|
245
|
+
maxRetries: w,
|
|
246
|
+
backoffBaseDelay: c,
|
|
247
|
+
// Pass this to apiRequest
|
|
248
|
+
accessToken: o,
|
|
249
|
+
queryParams: {
|
|
250
|
+
customer_id: n
|
|
251
|
+
}
|
|
252
|
+
}).then((r) => {
|
|
253
|
+
if (r && r.data && Array.isArray(r.data.entitlements)) {
|
|
254
|
+
const u = H(
|
|
255
|
+
r.data
|
|
256
|
+
);
|
|
257
|
+
s({
|
|
258
|
+
data: u,
|
|
259
|
+
isLoading: !1,
|
|
260
|
+
error: null
|
|
261
|
+
});
|
|
262
|
+
} else {
|
|
263
|
+
const u = new Error(
|
|
264
|
+
"Received empty, malformed, or invalid data structure from entitlements API."
|
|
265
|
+
);
|
|
266
|
+
throw s({
|
|
267
|
+
data: null,
|
|
268
|
+
isLoading: !1,
|
|
269
|
+
error: u
|
|
270
|
+
}), A(
|
|
271
|
+
u.message,
|
|
272
|
+
"fetchAllEntitlements",
|
|
273
|
+
r
|
|
274
|
+
), u;
|
|
275
|
+
}
|
|
276
|
+
}).catch((r) => {
|
|
277
|
+
const u = r instanceof Error ? r : new Error(String(r));
|
|
278
|
+
return s((P) => ({
|
|
279
|
+
...P,
|
|
280
|
+
isLoading: !1,
|
|
281
|
+
error: u
|
|
282
|
+
})), A(u.message, "fetchAllEntitlements", r), Promise.reject(u);
|
|
283
|
+
}).finally(() => {
|
|
284
|
+
L(!1);
|
|
285
|
+
});
|
|
286
|
+
}, [
|
|
287
|
+
m,
|
|
288
|
+
n,
|
|
289
|
+
t,
|
|
290
|
+
f,
|
|
291
|
+
w,
|
|
292
|
+
c,
|
|
293
|
+
o,
|
|
294
|
+
A
|
|
295
|
+
]);
|
|
296
|
+
_(() => {
|
|
297
|
+
let g = !0;
|
|
298
|
+
if (!m) {
|
|
299
|
+
if (g) {
|
|
300
|
+
const d = new Error(
|
|
301
|
+
"KelviqProvider: `apiBaseUrl` must be provided in config."
|
|
302
|
+
);
|
|
303
|
+
E(d), L(!1), s((r) => ({
|
|
304
|
+
...r,
|
|
305
|
+
isLoading: !1,
|
|
306
|
+
error: d
|
|
307
|
+
})), y.onError && y.onError(d);
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
return T ? S().catch((d) => {
|
|
312
|
+
g && h && L(!1), console.error(
|
|
313
|
+
"Kelviq SDK: Initial entitlement fetch failed.",
|
|
314
|
+
d
|
|
315
|
+
);
|
|
316
|
+
}) : g && (s((d) => ({
|
|
317
|
+
...d,
|
|
318
|
+
isLoading: !1,
|
|
319
|
+
data: null,
|
|
320
|
+
error: null
|
|
321
|
+
})), L(!1)), () => {
|
|
322
|
+
g = !1;
|
|
323
|
+
};
|
|
324
|
+
}, [m, T, y.onError]);
|
|
325
|
+
const $ = R(
|
|
326
|
+
(g, d) => {
|
|
327
|
+
if (a.isLoading)
|
|
328
|
+
return { data: null, isLoading: !0, error: null };
|
|
329
|
+
if (a.error && !a.data)
|
|
330
|
+
return {
|
|
331
|
+
data: null,
|
|
332
|
+
isLoading: !1,
|
|
333
|
+
error: a.error
|
|
334
|
+
};
|
|
335
|
+
if (a.data) {
|
|
336
|
+
const u = a.data[g];
|
|
337
|
+
return u ? u.type === d ? {
|
|
338
|
+
data: u,
|
|
339
|
+
isLoading: !1,
|
|
340
|
+
error: null
|
|
341
|
+
} : {
|
|
342
|
+
data: null,
|
|
343
|
+
isLoading: !1,
|
|
344
|
+
error: new Error(
|
|
345
|
+
`Entitlement type mismatch for ${g}. Expected ${d}, got ${u.type}`
|
|
346
|
+
)
|
|
347
|
+
} : { data: null, isLoading: !1, error: null };
|
|
348
|
+
}
|
|
349
|
+
const r = m ? `Entitlements not yet fetched for ${g}. Call refreshAllEntitlements or ensure fetchEntitlementsOnMount is true.` : "API URL not configured. Check KelviqProvider configuration.";
|
|
350
|
+
return {
|
|
351
|
+
data: null,
|
|
352
|
+
isLoading: !1,
|
|
353
|
+
error: new Error(r)
|
|
354
|
+
};
|
|
355
|
+
},
|
|
356
|
+
[a, m]
|
|
357
|
+
), D = R(
|
|
358
|
+
(g) => {
|
|
359
|
+
if (a.isLoading && !a.data || a.error && !a.data || !a.data) return;
|
|
360
|
+
const d = a.data[g];
|
|
361
|
+
return d ? d.hasAccess : !1;
|
|
362
|
+
},
|
|
363
|
+
[a]
|
|
364
|
+
), x = F(
|
|
365
|
+
() => ({
|
|
366
|
+
allEntitlements: a,
|
|
367
|
+
refreshAllEntitlements: S,
|
|
368
|
+
getEntitlement: $,
|
|
369
|
+
hasAccess: D,
|
|
370
|
+
isLoading: h,
|
|
371
|
+
error: l,
|
|
372
|
+
environment: v,
|
|
373
|
+
apiUrl: m,
|
|
374
|
+
entitlementsPath: t,
|
|
375
|
+
customerId: n
|
|
376
|
+
}),
|
|
377
|
+
[
|
|
378
|
+
a,
|
|
379
|
+
S,
|
|
380
|
+
$,
|
|
381
|
+
D,
|
|
382
|
+
h,
|
|
383
|
+
l,
|
|
384
|
+
v,
|
|
385
|
+
m,
|
|
386
|
+
t,
|
|
387
|
+
n
|
|
388
|
+
]
|
|
389
|
+
);
|
|
390
|
+
return /* @__PURE__ */ q(M.Provider, { value: x, children: e });
|
|
391
|
+
}, K = () => {
|
|
392
|
+
const e = O(M);
|
|
393
|
+
if (e === void 0 || e === I)
|
|
394
|
+
throw new Error(
|
|
395
|
+
"useKelviq must be used within an initialized KelviqProvider."
|
|
396
|
+
);
|
|
397
|
+
return e;
|
|
398
|
+
}, k = ({
|
|
399
|
+
featureKey: e,
|
|
400
|
+
type: i,
|
|
401
|
+
children: t,
|
|
402
|
+
fallback: n = null,
|
|
403
|
+
loadingComponent: v,
|
|
404
|
+
condition: o
|
|
405
|
+
}) => {
|
|
406
|
+
const {
|
|
407
|
+
getEntitlement: y,
|
|
408
|
+
isLoading: p,
|
|
409
|
+
error: w,
|
|
410
|
+
allEntitlements: f
|
|
411
|
+
} = K(), c = y(e, i);
|
|
412
|
+
return c.isLoading || p && !c.data && !c.error && !f.data && !f.error ? /* @__PURE__ */ q(b, { children: v !== void 0 ? v : n }) : c.error || w && !c.data ? /* @__PURE__ */ q(b, { children: n }) : !c.data || !c.data.hasAccess || o && !o(c.data) ? /* @__PURE__ */ q(b, { children: n }) : typeof t == "function" ? /* @__PURE__ */ q(b, { children: t(c.data) }) : /* @__PURE__ */ q(b, { children: t });
|
|
413
|
+
}, ee = ({ children: e, ...i }) => /* @__PURE__ */ q(
|
|
414
|
+
k,
|
|
415
|
+
{
|
|
416
|
+
...i,
|
|
417
|
+
type: "customizable",
|
|
418
|
+
children: (t) => t.hasAccess && t.configuration !== null && typeof t.configuration == "number" ? e(t.configuration) : /* @__PURE__ */ q(b, { children: i.fallback || null })
|
|
419
|
+
}
|
|
420
|
+
), te = (e) => {
|
|
421
|
+
const i = (t) => t.hasAccess && (t.remaining === null || t.remaining > 0);
|
|
422
|
+
return /* @__PURE__ */ q(
|
|
423
|
+
k,
|
|
424
|
+
{
|
|
425
|
+
...e,
|
|
426
|
+
type: "metered",
|
|
427
|
+
condition: e.condition || i
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
}, re = (e) => /* @__PURE__ */ q(
|
|
431
|
+
k,
|
|
432
|
+
{
|
|
433
|
+
...e,
|
|
434
|
+
type: "boolean"
|
|
435
|
+
}
|
|
436
|
+
), ne = () => {
|
|
437
|
+
const { allEntitlements: e } = K();
|
|
438
|
+
return e;
|
|
439
|
+
}, oe = (e) => {
|
|
440
|
+
const { getEntitlement: i } = K();
|
|
441
|
+
return i(e, "boolean");
|
|
442
|
+
}, ie = (e) => {
|
|
443
|
+
const { getEntitlement: i } = K();
|
|
444
|
+
return i(e, "customizable");
|
|
445
|
+
}, se = (e) => {
|
|
446
|
+
const { getEntitlement: i } = K();
|
|
447
|
+
return i(e, "metered");
|
|
448
|
+
};
|
|
449
|
+
export {
|
|
450
|
+
M as KelviqContext,
|
|
451
|
+
Q as KelviqProvider,
|
|
452
|
+
re as ShowWhenBooleanEntitled,
|
|
453
|
+
ee as ShowWhenConfigEntitled,
|
|
454
|
+
te as ShowWhenMeteredEntitled,
|
|
455
|
+
I as defaultKelviqContextValue,
|
|
456
|
+
ne as useAllEntitlements,
|
|
457
|
+
oe as useBooleanEntitlement,
|
|
458
|
+
ie as useConfigEntitlement,
|
|
459
|
+
K as useKelviq,
|
|
460
|
+
se as useMeteredEntitlement
|
|
461
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(c,l){typeof exports=="object"&&typeof module<"u"?l(exports,require("react/jsx-runtime"),require("react")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react"],l):(c=typeof globalThis<"u"?globalThis:c||self,l(c.KelviqReactSDK={},c.jsxRuntime,c.React))})(this,function(c,l,q){"use strict";const P="GET";function I(e){const{method:i=P,headers:t={},body:r,timeout:y=5e3,maxRetries:o=3,backoffBaseDelay:L=1e3,queryParams:v,accessToken:K}=e;let{url:E}=e,f=0;return new Promise((b,g)=>{if(v){const s=new URLSearchParams;for(const A in v)v[A]!==void 0&&s.append(A,String(v[A]));s.toString()&&(E+=(E.includes("?")?"&":"?")+s.toString())}function a(){const s=new XMLHttpRequest;s.open(i,E,!0),s.timeout=y;const A={Accept:"application/json","X-Requested-With":"XMLHttpRequest",...t};i!=="GET"&&r&&typeof r=="object"&&!(r instanceof FormData)&&(A["Content-Type"]||(A["Content-Type"]="application/json")),K&&(A.Authorization=`Bearer ${K}`);for(const[u,h]of Object.entries(A))s.setRequestHeader(u,h);s.onload=function(){const{status:u,statusText:h,responseText:T}=s;if(u>=200&&u<300)try{const w=T?JSON.parse(T):{};b({status:u,statusText:h,data:w})}catch(w){const C=w instanceof Error?w:new Error(String(w));console.error(`Kelviq SDK (apiRequest): Invalid JSON response for URL ${E}. Error: ${C.message}. Response: ${T}`),g(new Error(`Invalid JSON response: ${C.message}`))}else S(`Request to ${E} failed with status ${u} ${h}. Response: ${T}`)},s.onerror=()=>S(`Network error for URL ${E}. The request could not be completed.`),s.ontimeout=()=>S(`Request to ${E} timed out.`);function S(u){if(f<o){f++;const h=Math.min(L*Math.pow(2,f-1),3e4);console.warn(`Kelviq SDK (apiRequest): Retrying request to ${E}. Attempt ${f}/${o}. Error: ${u}. Retrying in ${h}ms.`),setTimeout(a,h)}else g(new Error(`${u} (Max retries ${o} reached)`))}try{let u=r;r&&typeof r=="object"&&!(r instanceof FormData)&&A["Content-Type"]==="application/json"&&(u=JSON.stringify(r)),s.send(u)}catch(u){const h=u instanceof Error?u:new Error(String(u));console.error(`Kelviq SDK (apiRequest): Error sending request to ${E}.`,h),g(new Error(`Failed to send request: ${h.message}`))}}a()})}function _(e){const i={};return!e||!Array.isArray(e.entitlements)?(console.warn("Kelviq SDK: Invalid or empty entitlements array in API response.",e),i):(e.entitlements.forEach(t=>{if(!t||typeof t.featureId!="string"||typeof t.featureType!="string"){console.warn("Kelviq SDK: Skipping invalid raw entitlement object:",t);return}let r=null;const y={featureKey:t.featureId,hasAccess:t.hasAccess};switch(t.featureType){case"BOOLEAN":r={...y,type:"boolean"};break;case"CUSTOMIZABLE":{const o=t;r={...y,type:"customizable",configuration:typeof o.value=="number"?o.value:null};break}case"METER":{const o=t,L=typeof o.usageLimit=="number"?o.usageLimit:null,v=typeof o.currentUsage=="number"?o.currentUsage:0;r={...y,type:"metered",limit:L,used:v,remaining:L!==null?L-v:null,resetAt:o.resetAt,hardLimit:o.hardLimit};break}default:{const o=t;console.warn(`Kelviq SDK: Encountered unknown or unhandled entitlement featureType: '${String(o.featureType)}' for feature '${String(o.featureId)}'. This entitlement will be ignored.`);break}}r&&(i[r.featureKey]=r)}),i)}const D={allEntitlements:{data:null,isLoading:!0,error:null},refreshAllEntitlements:()=>(console.warn("refreshAllEntitlements called on default KelviqContext. Ensure KelviqProvider is properly set up."),Promise.resolve()),getEntitlement:()=>({data:null,isLoading:!1,error:new Error("KelviqContext not fully initialized or entitlements not fetched.")}),hasAccess:()=>{},isLoading:!0,error:null,apiUrl:"",environment:"production",entitlementsPath:"",customerId:""},U=q.createContext(D),R="https://edge.api.kelviq.com/api/v1/",O="https://edge.sandboxapi.kelviq.com/api/v1/",B="entitlements/",x=({children:e,apiUrl:i,entitlementsPath:t=B,customerId:r,environment:y="production",accessToken:o,config:L={}})=>{const{onError:v,maxRetries:K,timeout:E,backoffBaseDelay:f,fetchEntitlementsOnMount:b=!0}=L,g=i||(y==="sandbox"?O:R),[a,s]=q.useState({data:null,isLoading:!!g&&b,error:null}),[A,S]=q.useState(!!g&&b),[u,h]=q.useState(null),T=q.useCallback((p,m,n)=>{const d=new Error(`Kelviq SDK (${m}): ${p}`);console.error(d.message,n||""),h(k=>k||d),v&&v(d)},[v]),w=q.useCallback(()=>{if(!g){const n="API URL not configured. Cannot fetch entitlements.";return s({data:null,isLoading:!1,error:new Error(n)}),T(n,"fetchAllEntitlements"),S(!1),Promise.reject(new Error(n))}if(!r){const n="CustomerId must be provided as props to KelviqProvider.";return s(()=>({data:null,isLoading:!1,error:new Error(n)})),T(n,"fetchAllEntitlements"),S(!1),Promise.reject(new Error(n))}s(n=>({...n,isLoading:!0,error:null})),S(!0),h(null);const p=`${g.replace(/\/$/,"")}/${t.replace(/^\//,"")}`;return I({url:p,method:"GET",timeout:E,maxRetries:K,backoffBaseDelay:f,accessToken:o,queryParams:{customer_id:r}}).then(n=>{if(n&&n.data&&Array.isArray(n.data.entitlements)){const d=_(n.data);s({data:d,isLoading:!1,error:null})}else{const d=new Error("Received empty, malformed, or invalid data structure from entitlements API.");throw s({data:null,isLoading:!1,error:d}),T(d.message,"fetchAllEntitlements",n),d}}).catch(n=>{const d=n instanceof Error?n:new Error(String(n));return s(k=>({...k,isLoading:!1,error:d})),T(d.message,"fetchAllEntitlements",n),Promise.reject(d)}).finally(()=>{S(!1)})},[g,r,t,E,K,f,o,T]);q.useEffect(()=>{let p=!0;if(!g){if(p){const m=new Error("KelviqProvider: `apiBaseUrl` must be provided in config.");h(m),S(!1),s(n=>({...n,isLoading:!1,error:m})),L.onError&&L.onError(m)}return}return b?w().catch(m=>{p&&A&&S(!1),console.error("Kelviq SDK: Initial entitlement fetch failed.",m)}):p&&(s(m=>({...m,isLoading:!1,data:null,error:null})),S(!1)),()=>{p=!1}},[g,b,L.onError]);const C=q.useCallback((p,m)=>{if(a.isLoading)return{data:null,isLoading:!0,error:null};if(a.error&&!a.data)return{data:null,isLoading:!1,error:a.error};if(a.data){const d=a.data[p];return d?d.type===m?{data:d,isLoading:!1,error:null}:{data:null,isLoading:!1,error:new Error(`Entitlement type mismatch for ${p}. Expected ${m}, got ${d.type}`)}:{data:null,isLoading:!1,error:null}}const n=g?`Entitlements not yet fetched for ${p}. Call refreshAllEntitlements or ensure fetchEntitlementsOnMount is true.`:"API URL not configured. Check KelviqProvider configuration.";return{data:null,isLoading:!1,error:new Error(n)}},[a,g]),F=q.useCallback(p=>{if(a.isLoading&&!a.data||a.error&&!a.data||!a.data)return;const m=a.data[p];return m?m.hasAccess:!1},[a]),J=q.useMemo(()=>({allEntitlements:a,refreshAllEntitlements:w,getEntitlement:C,hasAccess:F,isLoading:A,error:u,environment:y,apiUrl:g,entitlementsPath:t,customerId:r}),[a,w,C,F,A,u,y,g,t,r]);return l.jsx(U.Provider,{value:J,children:e})},$=()=>{const e=q.useContext(U);if(e===void 0||e===D)throw new Error("useKelviq must be used within an initialized KelviqProvider.");return e},M=({featureKey:e,type:i,children:t,fallback:r=null,loadingComponent:y,condition:o})=>{const{getEntitlement:L,isLoading:v,error:K,allEntitlements:E}=$(),f=L(e,i);return f.isLoading||v&&!f.data&&!f.error&&!E.data&&!E.error?l.jsx(l.Fragment,{children:y!==void 0?y:r}):f.error||K&&!f.data?l.jsx(l.Fragment,{children:r}):!f.data||!f.data.hasAccess||o&&!o(f.data)?l.jsx(l.Fragment,{children:r}):typeof t=="function"?l.jsx(l.Fragment,{children:t(f.data)}):l.jsx(l.Fragment,{children:t})},N=({children:e,...i})=>l.jsx(M,{...i,type:"customizable",children:t=>t.hasAccess&&t.configuration!==null&&typeof t.configuration=="number"?e(t.configuration):l.jsx(l.Fragment,{children:i.fallback||null})}),j=e=>{const i=t=>t.hasAccess&&(t.remaining===null||t.remaining>0);return l.jsx(M,{...e,type:"metered",condition:e.condition||i})},W=e=>l.jsx(M,{...e,type:"boolean"}),G=()=>{const{allEntitlements:e}=$();return e},z=e=>{const{getEntitlement:i}=$();return i(e,"boolean")},H=e=>{const{getEntitlement:i}=$();return i(e,"customizable")},X=e=>{const{getEntitlement:i}=$();return i(e,"metered")};c.KelviqContext=U,c.KelviqProvider=x,c.ShowWhenBooleanEntitled=W,c.ShowWhenConfigEntitled=N,c.ShowWhenMeteredEntitled=j,c.defaultKelviqContextValue=D,c.useAllEntitlements=G,c.useBooleanEntitlement=z,c.useConfigEntitlement=H,c.useKelviq=$,c.useMeteredEntitlement=X,Object.defineProperty(c,Symbol.toStringTag,{value:"Module"})});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for an API request.
|
|
3
|
+
* @template ReqBody The type of the request body.
|
|
4
|
+
*/
|
|
5
|
+
export interface ApiRequestConfig<ReqBody = any> {
|
|
6
|
+
/** The URL for the request. */
|
|
7
|
+
url: string;
|
|
8
|
+
/** HTTP method (GET, POST, PUT, DELETE, etc.). Defaults to GET. */
|
|
9
|
+
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD';
|
|
10
|
+
/** Custom headers for the request. */
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
/** Data to be sent as the request body. */
|
|
13
|
+
body?: ReqBody;
|
|
14
|
+
/** Timeout for the request in milliseconds. Defaults to 5000ms. */
|
|
15
|
+
timeout?: number;
|
|
16
|
+
/** Maximum number of retry attempts for failed requests. Defaults to 3. */
|
|
17
|
+
maxRetries?: number;
|
|
18
|
+
/** Base delay (in milliseconds) for exponential backoff between retries. Defaults to 1000ms. */
|
|
19
|
+
backoffBaseDelay?: number;
|
|
20
|
+
/** Optional: The static authentication token string. */
|
|
21
|
+
accessToken?: string | null;
|
|
22
|
+
/** Optional: Query parameters to be appended to the URL. */
|
|
23
|
+
queryParams?: Record<string, string | number | boolean | undefined>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Represents the structure of a successful API response.
|
|
27
|
+
* @template ResData The type of the data in the response body.
|
|
28
|
+
*/
|
|
29
|
+
export interface ApiResponse<ResData> {
|
|
30
|
+
/** HTTP status code of the response. */
|
|
31
|
+
status: number;
|
|
32
|
+
/** HTTP status text of the response. */
|
|
33
|
+
statusText: string;
|
|
34
|
+
/** Parsed data from the response body. */
|
|
35
|
+
data: ResData;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Makes an HTTP request using XMLHttpRequest with support for retries and exponential backoff.
|
|
39
|
+
* @template ResData The expected type of the data in the API response.
|
|
40
|
+
* @template ReqBody The type of the request body.
|
|
41
|
+
* @param config Configuration object for the API request.
|
|
42
|
+
* @returns A Promise that resolves to an ApiResponse<ResData> or rejects with an Error.
|
|
43
|
+
*/
|
|
44
|
+
export declare function apiRequest<ResData = any, ReqBody = any>(config: ApiRequestConfig<ReqBody>): Promise<ApiResponse<ResData>>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Base interface for raw entitlement objects coming from the API. */
|
|
2
|
+
interface RawEntitlement {
|
|
3
|
+
/** The unique identifier for the feature from the backend. */
|
|
4
|
+
featureId: string;
|
|
5
|
+
/** The type of the feature, used to determine how to process it. */
|
|
6
|
+
featureType: 'BOOLEAN' | 'CUSTOMIZABLE' | 'METER';
|
|
7
|
+
/** Indicates if the user is entitled to this feature. */
|
|
8
|
+
hasAccess: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface RawBooleanEntitlementData extends RawEntitlement {
|
|
11
|
+
featureType: 'BOOLEAN';
|
|
12
|
+
}
|
|
13
|
+
export interface RawConfigEntitlementData extends RawEntitlement {
|
|
14
|
+
featureType: 'CUSTOMIZABLE';
|
|
15
|
+
/** The numeric configuration value associated with the feature, if applicable. */
|
|
16
|
+
value?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface RawMeteredEntitlementData extends RawEntitlement {
|
|
19
|
+
featureType: 'METER';
|
|
20
|
+
/** ISO 8601 string indicating when the usage counter resets, if applicable. */
|
|
21
|
+
resetAt?: string | null;
|
|
22
|
+
/** If true, the limit is a hard cap. If false, it might be a soft limit. */
|
|
23
|
+
hardLimit?: boolean;
|
|
24
|
+
/** The maximum usage allowed. Can be null for unlimited. */
|
|
25
|
+
usageLimit: number | null;
|
|
26
|
+
/** The current usage count. */
|
|
27
|
+
currentUsage: number;
|
|
28
|
+
}
|
|
29
|
+
/** Union type for any raw entitlement object received from the API. */
|
|
30
|
+
export type AnyRawEntitlementData = RawBooleanEntitlementData | RawConfigEntitlementData | RawMeteredEntitlementData;
|
|
31
|
+
/**
|
|
32
|
+
* Represents the expected raw API response structure when fetching all entitlements.
|
|
33
|
+
* Example: { "customer_id": "128", "entitlements": [...] }
|
|
34
|
+
*/
|
|
35
|
+
export interface RawEntitlementsApiResponse {
|
|
36
|
+
customer_id: string;
|
|
37
|
+
entitlements: AnyRawEntitlementData[];
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents the state of an asynchronous data fetching operation.
|
|
3
|
+
* @template T The type of the data being fetched.
|
|
4
|
+
*/
|
|
5
|
+
export interface AsyncState<T> {
|
|
6
|
+
data: T | null;
|
|
7
|
+
isLoading: boolean;
|
|
8
|
+
error: Error | null;
|
|
9
|
+
}
|
|
10
|
+
/** Base interface for all processed entitlements within the SDK. */
|
|
11
|
+
export interface Entitlement {
|
|
12
|
+
/** The unique key identifying the feature (mapped from `feature`). */
|
|
13
|
+
featureKey: string;
|
|
14
|
+
/** True if the user has access to this feature, false otherwise (mapped from `enabled`). */
|
|
15
|
+
hasAccess: boolean;
|
|
16
|
+
/** The standardized type of the entitlement for SDK logic. */
|
|
17
|
+
type: 'boolean' | 'customizable' | 'metered';
|
|
18
|
+
}
|
|
19
|
+
/** Represents a processed boolean entitlement. */
|
|
20
|
+
export interface BooleanEntitlement extends Entitlement {
|
|
21
|
+
type: 'boolean';
|
|
22
|
+
}
|
|
23
|
+
/** Represents a processed configuration entitlement. */
|
|
24
|
+
export interface ConfigEntitlement extends Entitlement {
|
|
25
|
+
type: 'customizable';
|
|
26
|
+
/** The numeric configuration value. Null if not applicable or not provided. */
|
|
27
|
+
configuration: number | null;
|
|
28
|
+
}
|
|
29
|
+
/** Represents a processed metered entitlement. */
|
|
30
|
+
export interface MeteredEntitlement extends Entitlement {
|
|
31
|
+
type: 'metered';
|
|
32
|
+
/** The usage limit for the feature. Null if unlimited. */
|
|
33
|
+
limit: number | null;
|
|
34
|
+
/** The current usage count. */
|
|
35
|
+
used: number;
|
|
36
|
+
/** The remaining usage (limit - used). Null if limit is null. */
|
|
37
|
+
remaining: number | null;
|
|
38
|
+
/** ISO 8601 string for when the meter resets. */
|
|
39
|
+
resetAt?: string | null;
|
|
40
|
+
/** Indicates if the limit is a hard cap. */
|
|
41
|
+
hardLimit?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/** Union type for any processed entitlement object used within the SDK. */
|
|
44
|
+
export type AnyEntitlement = BooleanEntitlement | ConfigEntitlement | MeteredEntitlement;
|
|
45
|
+
/**
|
|
46
|
+
* Type for the cached and processed entitlements data stored by the provider.
|
|
47
|
+
* It's a map where keys are featureKeys and values are the processed entitlement objects,
|
|
48
|
+
* allowing for efficient O(1) lookups.
|
|
49
|
+
*/
|
|
50
|
+
export type EntitlementMap = Record<string, AnyEntitlement>;
|
|
51
|
+
/** Optional behavioral configuration options for the KelviqProvider. */
|
|
52
|
+
export interface KelviqApiBehaviorOptions {
|
|
53
|
+
/** Optional callback for handling errors that occur within the SDK during entitlement fetching. */
|
|
54
|
+
onError?: (error: Error) => void;
|
|
55
|
+
/** Optional: Maximum number of retries for API requests. Defaults to 3. */
|
|
56
|
+
maxRetries?: number;
|
|
57
|
+
/** Optional: Timeout for API requests in milliseconds. Defaults to 5000. */
|
|
58
|
+
timeout?: number;
|
|
59
|
+
/**
|
|
60
|
+
* If true (default), fetches all entitlements when the provider mounts.
|
|
61
|
+
* Set to false to manually control the initial fetch using `refreshAllEntitlements`.
|
|
62
|
+
*/
|
|
63
|
+
fetchEntitlementsOnMount?: boolean;
|
|
64
|
+
/** Optional: Base delay (in milliseconds) for exponential backoff strategy in retries.
|
|
65
|
+
* If not provided, the `apiRequest` service's default will be used.
|
|
66
|
+
*/
|
|
67
|
+
backoffBaseDelay?: number;
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './transformations';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { RawEntitlementsApiResponse } from '../types/api.types';
|
|
2
|
+
import { EntitlementMap } from '../types/sdk.types';
|
|
3
|
+
/**
|
|
4
|
+
* Transforms the raw API response for entitlements into a more usable
|
|
5
|
+
* and structured format (EntitlementMap) for the SDK.
|
|
6
|
+
* This function maps API field names to SDK-standard field names and calculates derived values.
|
|
7
|
+
* @param apiResponse The raw response from the entitlements API.
|
|
8
|
+
* @returns An EntitlementMap: a map of feature keys to their processed entitlement objects.
|
|
9
|
+
*/
|
|
10
|
+
export declare function transformApiEntitlements(apiResponse: RawEntitlementsApiResponse): EntitlementMap;
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kelviq/react-sdk",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.0.1-beta",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/kelviq-react-sdk.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"package.json",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "vite",
|
|
15
|
+
"build": "tsc -b ./tsconfig.lib.json && vite build",
|
|
16
|
+
"lint": "eslint .",
|
|
17
|
+
"preview": "vite preview",
|
|
18
|
+
"prepublishOnly": "npm run build",
|
|
19
|
+
"test": "vitest",
|
|
20
|
+
"test:ui": "vitest --ui",
|
|
21
|
+
"coverage": "vitest run --coverage",
|
|
22
|
+
"precommit": "lint-staged",
|
|
23
|
+
"commitlint": "commitlint --edit",
|
|
24
|
+
"commit": "git-cz",
|
|
25
|
+
"prepare": "husky install"
|
|
26
|
+
},
|
|
27
|
+
"lint-staged": {
|
|
28
|
+
"{src,test}/**/*.{ts,tsx}": [
|
|
29
|
+
"prettier --write",
|
|
30
|
+
"eslint --fix"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"config": {
|
|
34
|
+
"commitizen": {
|
|
35
|
+
"path": "cz-conventional-changelog"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"react": "^19.0.0",
|
|
40
|
+
"react-dom": "^19.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@commitlint/cli": "^19.8.1",
|
|
44
|
+
"@commitlint/config-conventional": "^19.8.1",
|
|
45
|
+
"@eslint/js": "^9.15.0",
|
|
46
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
47
|
+
"@testing-library/react": "^16.3.0",
|
|
48
|
+
"@types/node": "^22.10.2",
|
|
49
|
+
"@types/react": "^18.3.12",
|
|
50
|
+
"@types/react-dom": "^18.3.1",
|
|
51
|
+
"@vitejs/plugin-react-swc": "^3.5.0",
|
|
52
|
+
"@vitest/ui": "^3.1.3",
|
|
53
|
+
"commitizen": "^4.3.1",
|
|
54
|
+
"cz-conventional-changelog": "^3.3.0",
|
|
55
|
+
"eslint": "^9.15.0",
|
|
56
|
+
"eslint-plugin-react-hooks": "^5.0.0",
|
|
57
|
+
"eslint-plugin-react-refresh": "^0.4.14",
|
|
58
|
+
"globals": "^15.12.0",
|
|
59
|
+
"husky": "^9.1.7",
|
|
60
|
+
"jsdom": "^26.1.0",
|
|
61
|
+
"lint-staged": "^16.0.0",
|
|
62
|
+
"prettier": "^3.5.3",
|
|
63
|
+
"react": "^19.0.0",
|
|
64
|
+
"react-dom": "^19.0.0",
|
|
65
|
+
"typescript": "~5.6.2",
|
|
66
|
+
"typescript-eslint": "^8.15.0",
|
|
67
|
+
"vite": "^6.0.1",
|
|
68
|
+
"vite-plugin-dts": "^4.3.0",
|
|
69
|
+
"vitest": "^3.1.3"
|
|
70
|
+
}
|
|
71
|
+
}
|