space-react-client 0.2.3 → 0.2.5
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 +294 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +53 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1 +1,294 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🚀 SPACE Client SDK for React
|
|
2
|
+
|
|
3
|
+
The **SPACE React SDK** provides fully-typed React/TypeScript **components, hooks, and utilities** to seamlessly integrate your web apps with SPACE.
|
|
4
|
+
|
|
5
|
+
With this SDK you can:
|
|
6
|
+
|
|
7
|
+
- ⚡ **Connect** a React application to a **SPACE** instance (HTTP + WebSocket).
|
|
8
|
+
- 🔑 Generate and manage **Pricing Tokens** directly in the client.
|
|
9
|
+
- 🧩 **Activate/deactivate UI components** declaratively with the `<Feature>` component.
|
|
10
|
+
- 🔔 **Subscribe to SPACE pricing events** to keep your UI in sync.
|
|
11
|
+
|
|
12
|
+
> [!WARNING]
|
|
13
|
+
> This SDK is intended for research and experimentation. For production usage, see **[License & Disclaimer](#-license--disclaimer)**.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## ⚠️ Important Note
|
|
18
|
+
|
|
19
|
+
- **Feature evaluation is always based on the Pricing Token.**
|
|
20
|
+
`space-react-client` does not call SPACE directly to check features; instead, it uses the Pricing Token stored in the browser’s `localStorage`.
|
|
21
|
+
(See [SPACE communication protocol](../../introduction.mdx) for more details on how pricing tokens work.)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 📦 Installation
|
|
26
|
+
|
|
27
|
+
Install with your package manager of choice:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install space-react-client
|
|
31
|
+
# or
|
|
32
|
+
yarn add space-react-client
|
|
33
|
+
# or
|
|
34
|
+
pnpm add space-react-client
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Peer dependencies:**
|
|
38
|
+
|
|
39
|
+
- `react >= 18`
|
|
40
|
+
- `react-dom >= 18`
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## ⚡ Quick Start
|
|
45
|
+
|
|
46
|
+
The minimal setup to connect to SPACE, load a user’s Pricing Token, and render UI conditionally with `<Feature>`.
|
|
47
|
+
|
|
48
|
+
### 1. Wrap your app with `SpaceProvider`
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import React from 'react';
|
|
52
|
+
import { createRoot } from 'react-dom/client';
|
|
53
|
+
import { SpaceProvider } from 'space-react-client';
|
|
54
|
+
import App from './App';
|
|
55
|
+
|
|
56
|
+
const config = {
|
|
57
|
+
url: 'http://localhost:5403', // Your SPACE instance URL
|
|
58
|
+
apiKey: 'YOUR_API_KEY', // API key issued by SPACE
|
|
59
|
+
allowConnectionWithSpace: true,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
createRoot(document.getElementById('root')!)
|
|
63
|
+
.render(
|
|
64
|
+
<SpaceProvider config={config}>
|
|
65
|
+
<App />
|
|
66
|
+
</SpaceProvider>
|
|
67
|
+
);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
> [!WARNING]
|
|
71
|
+
> Setting `allowConnectionWithSpace: false` disables all connections to SPACE.
|
|
72
|
+
> This means you can still evaluate features from a token, but **event listeners (e.g., pricing_created, pricing_archived, etc.) as well as methods like `setUserId` and `generateUserPricingToken` will not work.**
|
|
73
|
+
|
|
74
|
+
### 2. Identify the user and load a Pricing Token
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { useEffect } from 'react';
|
|
78
|
+
import { useSpaceClient } from 'space-react-client';
|
|
79
|
+
|
|
80
|
+
export function TokenBootstrapper() {
|
|
81
|
+
const spaceClient = useSpaceClient();
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
spaceClient.setUserId('user-123')
|
|
85
|
+
.then(() => console.log("User's pricing token set"))
|
|
86
|
+
.catch(console.error);
|
|
87
|
+
|
|
88
|
+
// Listen for SPACE sync events
|
|
89
|
+
const onSync = () => console.log('Connected & synchronized with SPACE');
|
|
90
|
+
spaceClient.on('synchronized', onSync);
|
|
91
|
+
|
|
92
|
+
return () => spaceClient.off('synchronized', onSync);
|
|
93
|
+
}, [spaceClient]);
|
|
94
|
+
|
|
95
|
+
return <YourComponent />;
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 3. Gate UI with `<Feature>`
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { Feature, On, Default, Loading, ErrorFallback } from 'space-react-client';
|
|
103
|
+
|
|
104
|
+
export function MeetingButton() {
|
|
105
|
+
return (
|
|
106
|
+
<Feature id="zoom-meetings">
|
|
107
|
+
<On>
|
|
108
|
+
{/* Rendered when feature is enabled */}
|
|
109
|
+
<button>Start meeting</button>
|
|
110
|
+
</On>
|
|
111
|
+
<Default>
|
|
112
|
+
{/* Rendered when feature is disabled */}
|
|
113
|
+
<button disabled>Upgrade to enable meetings</button>
|
|
114
|
+
</Default>
|
|
115
|
+
<Loading>
|
|
116
|
+
{/* Rendered while evaluating */}
|
|
117
|
+
<span>Checking your plan…</span>
|
|
118
|
+
</Loading>
|
|
119
|
+
<ErrorFallback>
|
|
120
|
+
{/* Rendered on error */}
|
|
121
|
+
<span>Could not verify your feature access.</span>
|
|
122
|
+
</ErrorFallback>
|
|
123
|
+
</Feature>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### 🔄 Alternative: Token-only mode (no live connection)
|
|
131
|
+
|
|
132
|
+
Set `allowConnectionWithSpace: false` to disable the WebSocket client.
|
|
133
|
+
You can then inject a Pricing Token from your backend:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import { useEffect } from 'react';
|
|
137
|
+
import { usePricingToken } from 'space-react-client';
|
|
138
|
+
|
|
139
|
+
export function InjectTokenFromServer() {
|
|
140
|
+
const tokenService = usePricingToken();
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
fetch('/api/my-pricing-token')
|
|
144
|
+
.then(res => res.text()) // token as string
|
|
145
|
+
.then(token => tokenService.updatePricingToken(token))
|
|
146
|
+
.catch(console.error);
|
|
147
|
+
}, [tokenService]);
|
|
148
|
+
|
|
149
|
+
return <YourComponent />;
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 📚 API Reference
|
|
156
|
+
|
|
157
|
+
### Providers
|
|
158
|
+
|
|
159
|
+
- **`SpaceProvider({ config, children })`**
|
|
160
|
+
Initializes the client and provides context.
|
|
161
|
+
|
|
162
|
+
- **Props:**
|
|
163
|
+
- `config: SpaceConfiguration`
|
|
164
|
+
- `url: string` — SPACE instance URL
|
|
165
|
+
- `apiKey: string` — Authentication key emitted by SPACE
|
|
166
|
+
- `allowConnectionWithSpace: boolean` — If `false`, event listeners take no effect (default: `true`)
|
|
167
|
+
- `children: React.ReactNode`
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### Hooks
|
|
172
|
+
|
|
173
|
+
- **`useSpaceClient(): SpaceClient`**
|
|
174
|
+
Access the connected SPACE client. Throws if not available.
|
|
175
|
+
|
|
176
|
+
- **`usePricingToken(): TokenService`**
|
|
177
|
+
Manage Pricing Tokens in context. Available even if live connection is disabled.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
### UI Components
|
|
182
|
+
|
|
183
|
+
- **`<Feature id="feature-id">…</Feature>`**
|
|
184
|
+
Declarative feature gating.
|
|
185
|
+
|
|
186
|
+
- **Subcomponents:**
|
|
187
|
+
- `<On>` — Rendered when feature evaluates to `true`.
|
|
188
|
+
- `<Default>` — Rendered when feature evaluates to `false`.
|
|
189
|
+
- `<Loading>` — Rendered while evaluating.
|
|
190
|
+
- `<ErrorFallback>` — Rendered on errors (invalid id, expired token, etc).
|
|
191
|
+
|
|
192
|
+
> [!WARNING]
|
|
193
|
+
> The `feature-id` is a string in the format `saasName-featureName`, always in lowercase.
|
|
194
|
+
> For example, to reference the **pets feature from PetClinic**, the resulting feature-id would be: `petclinic-pets`.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
### Client API
|
|
199
|
+
|
|
200
|
+
`SpaceClient` is instantiated by `SpaceProvider`.
|
|
201
|
+
|
|
202
|
+
**Methods:**
|
|
203
|
+
|
|
204
|
+
- `on(event, callback)` — Listen to SPACE events.
|
|
205
|
+
- `off(event?, callback?)` — Remove listeners. **If no args, removes all listeners**.
|
|
206
|
+
- `setUserId(userId)` — Set user for evaluations, **generates a pricing token, and stores it**.
|
|
207
|
+
- `generateUserPricingToken()` — Generate and **return** a fresh Pricing Token for the user configured with `setUserId`. It **does not store** the token.
|
|
208
|
+
|
|
209
|
+
**✅ Supported Events**
|
|
210
|
+
|
|
211
|
+
- **`synchronized`** — Client is connected and synced with SPACE.
|
|
212
|
+
- **`pricing_created`** — A new pricing was added.
|
|
213
|
+
- **`pricing_activated`** — A pricing moved from archived → active.
|
|
214
|
+
- **`pricing_archived`** — A pricing moved from active → archived.
|
|
215
|
+
- **`service_disabled`** — A service was disabled.
|
|
216
|
+
- **`error`** — Connection or processing error.
|
|
217
|
+
|
|
218
|
+
All events (except `synchronized` and `error`) include the following object:
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
{
|
|
222
|
+
serviceName: string; // REQUIRED: The name of the service that triggered the event
|
|
223
|
+
pricingVersion?: string; // OPTIONAL: The version of the pricing involved
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
### Token Service
|
|
230
|
+
|
|
231
|
+
**Methods:**
|
|
232
|
+
|
|
233
|
+
- `updatePricingToken(token)` — Validates & stores a pricing token.
|
|
234
|
+
- `getPricingToken()` — Return parsed token payload.
|
|
235
|
+
- `evaluateFeature(featureId)` — Returns `true | false | null`.
|
|
236
|
+
|
|
237
|
+
Token expectations:
|
|
238
|
+
- `exp: number` — UNIX expiration.
|
|
239
|
+
- `features: Record<string, { eval: boolean; limit?: number | null; used?: number | null }>`
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## 🛡️ Security Considerations
|
|
244
|
+
|
|
245
|
+
- **Do not expose SPACE API keys in production.**
|
|
246
|
+
Instead, issue Pricing Tokens from your backend and deliver them to the client.
|
|
247
|
+
- Tokens are validated client-side with the `exp` claim. Rotate or shorten TTLs as needed.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## ⚙️ Development & Tooling
|
|
252
|
+
|
|
253
|
+
The project uses the following main tools and technologies:
|
|
254
|
+
|
|
255
|
+
<div style="display: 'flex', gap: '0.5rem', flexWrap: 'wrap', justifyContent: 'center', marginTop: '1rem'">
|
|
256
|
+
|
|
257
|
+

|
|
258
|
+

|
|
259
|
+

|
|
260
|
+

|
|
261
|
+

|
|
262
|
+

|
|
263
|
+
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## 📄 License & Disclaimer
|
|
269
|
+
|
|
270
|
+
This project is licensed under the **MIT License**. See [LICENSE](https://github.com/Alex-GF/space-react-client/blob/main/LICENSE).
|
|
271
|
+
|
|
272
|
+
> [!WARNING]
|
|
273
|
+
> This SDK is part of **ongoing research** in pricing-driven devops.
|
|
274
|
+
> It is still in an early stage and not intended for production use.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## ❓ FAQ
|
|
279
|
+
|
|
280
|
+
**Q:** Do I need SPACE running to use this?
|
|
281
|
+
**A:** Yes, for live connectivity. In token-only mode, you just need a Pricing Token from your backend.
|
|
282
|
+
|
|
283
|
+
**Q:** Why does `useSpaceClient` throw?
|
|
284
|
+
**A:** Likely because you’re outside `SpaceProvider`, or `allowConnectionWithSpace` is `false`.
|
|
285
|
+
|
|
286
|
+
**Q:** What’s the format for feature IDs?
|
|
287
|
+
**A:** A **feature id** must:
|
|
288
|
+
|
|
289
|
+
- Always include a dash (`-`).
|
|
290
|
+
- Match exactly the keys present in the Pricing Token payload.
|
|
291
|
+
|
|
292
|
+
The format is built internally as: `saasName-featureName`, all in lowercase.
|
|
293
|
+
|
|
294
|
+
For example, if you want to instantiate the feature `pets` from the SaaS **PetClinic**, the feature id would be: `petclinic-pets`.
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import React, { JSX } from 'react';
|
|
|
2
2
|
|
|
3
3
|
declare class TokenService$1 {
|
|
4
4
|
private tokenPayload;
|
|
5
|
+
private listeners;
|
|
5
6
|
/**
|
|
6
7
|
* Retrieves the stored pricing token's payload.
|
|
7
8
|
* @returns The stored pricing token payload.
|
|
@@ -20,6 +21,12 @@ declare class TokenService$1 {
|
|
|
20
21
|
updatePricingToken(token: string): void;
|
|
21
22
|
evaluateFeature(featureId: string): boolean | null;
|
|
22
23
|
private _validToken;
|
|
24
|
+
/**
|
|
25
|
+
* Subscribe to pricing token updates. Returns an unsubscribe function.
|
|
26
|
+
*/
|
|
27
|
+
subscribe(listener: () => void): () => void;
|
|
28
|
+
/** Notify all listeners that the token has changed. */
|
|
29
|
+
private _notify;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
/**
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ function isTokenExpired(tokenPayload) {
|
|
|
22
22
|
class TokenService {
|
|
23
23
|
constructor() {
|
|
24
24
|
this.tokenPayload = null;
|
|
25
|
+
this.listeners = new Set();
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
28
|
* Retrieves the stored pricing token's payload.
|
|
@@ -51,6 +52,7 @@ class TokenService {
|
|
|
51
52
|
updatePricingToken(token) {
|
|
52
53
|
const parsedToken = parseJwt(token);
|
|
53
54
|
this.tokenPayload = parsedToken;
|
|
55
|
+
this._notify();
|
|
54
56
|
}
|
|
55
57
|
evaluateFeature(featureId) {
|
|
56
58
|
if (!this._validToken()) {
|
|
@@ -76,6 +78,26 @@ class TokenService {
|
|
|
76
78
|
}
|
|
77
79
|
return true;
|
|
78
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Subscribe to pricing token updates. Returns an unsubscribe function.
|
|
83
|
+
*/
|
|
84
|
+
subscribe(listener) {
|
|
85
|
+
this.listeners.add(listener);
|
|
86
|
+
return () => {
|
|
87
|
+
this.listeners.delete(listener);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** Notify all listeners that the token has changed. */
|
|
91
|
+
_notify() {
|
|
92
|
+
this.listeners.forEach((l) => {
|
|
93
|
+
try {
|
|
94
|
+
l();
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
console.error(e);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
79
101
|
}
|
|
80
102
|
|
|
81
103
|
/**
|
|
@@ -234,7 +256,7 @@ const SpaceProvider = ({ config, children, }) => {
|
|
|
234
256
|
client: client,
|
|
235
257
|
tokenService: tokenService,
|
|
236
258
|
};
|
|
237
|
-
}, [config.url, config.apiKey]);
|
|
259
|
+
}, [config.url, config.apiKey, config.allowConnectionWithSpace]);
|
|
238
260
|
useEffect(() => {
|
|
239
261
|
return () => {
|
|
240
262
|
if (context.client && typeof context.client.disconnectWebSocket === 'function') {
|
|
@@ -300,25 +322,36 @@ const Feature = ({ id, children }) => {
|
|
|
300
322
|
// Validate id
|
|
301
323
|
const isValidId = useMemo(() => id.includes('-'), [id]);
|
|
302
324
|
useEffect(() => {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
325
|
+
const evaluate = () => {
|
|
326
|
+
if (!isValidId) {
|
|
327
|
+
setStatus('error');
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (tokenService.getPricingToken() === null) {
|
|
331
|
+
setStatus('error');
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
setStatus('loading');
|
|
335
|
+
setResult(null);
|
|
336
|
+
const evaluationResult = tokenService.evaluateFeature(id);
|
|
337
|
+
if (evaluationResult === null || evaluationResult === undefined) {
|
|
338
|
+
setStatus('error');
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
setResult(evaluationResult);
|
|
342
|
+
setStatus('success');
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
// Initial evaluation
|
|
346
|
+
evaluate();
|
|
347
|
+
// Subscribe to token changes to re-evaluate
|
|
348
|
+
const unsubscribe = tokenService.subscribe(() => {
|
|
349
|
+
evaluate();
|
|
350
|
+
});
|
|
351
|
+
return () => {
|
|
352
|
+
unsubscribe();
|
|
353
|
+
};
|
|
354
|
+
}, [id, isValidId, tokenService]);
|
|
322
355
|
if (status === 'loading') {
|
|
323
356
|
return jsx(Fragment, { children: getChildrenOfType(children, Loading) });
|
|
324
357
|
}
|