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 CHANGED
@@ -1 +1,294 @@
1
- # space-react-client
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
+ ![TypeScript](https://img.shields.io/badge/-TypeScript-3178C6?logo=typescript&logoColor=white&style=for-the-badge)
258
+ ![Rollup](https://img.shields.io/badge/-Rollup-EC4A3F?logo=rollup.js&logoColor=white&style=for-the-badge)
259
+ ![Vitest](https://img.shields.io/badge/-Vitest-6E9F18?logo=vitest&logoColor=white&style=for-the-badge)
260
+ ![Testing Library](https://img.shields.io/badge/-Testing%20Library-E33332?logo=testinglibrary&logoColor=white&style=for-the-badge)
261
+ ![Axios](https://img.shields.io/badge/-Axios-5A29E4?logo=axios&logoColor=white&style=for-the-badge)
262
+ ![Socket.io](https://img.shields.io/badge/-Socket.io-010101?logo=socketdotio&logoColor=white&style=for-the-badge)
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
- if (!isValidId) {
304
- setStatus('error');
305
- return;
306
- }
307
- if (tokenService.getPricingToken() === null) {
308
- setStatus('error');
309
- return;
310
- }
311
- setStatus('loading');
312
- setResult(null);
313
- const evaluationResult = tokenService.evaluateFeature(id);
314
- if (evaluationResult === null || evaluationResult === undefined) {
315
- setStatus('error');
316
- }
317
- else {
318
- setResult(evaluationResult);
319
- setStatus('success');
320
- }
321
- }, [id, isValidId]);
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
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "space-react-client",
3
3
  "type": "module",
4
- "version": "0.2.3",
4
+ "version": "0.2.5",
5
5
  "description": "",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",