space-react-client 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Alejandro García Fernández
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # space-react-client
@@ -0,0 +1,136 @@
1
+ import React, { JSX } from 'react';
2
+
3
+ declare class TokenService$1 {
4
+ private tokenPayload;
5
+ /**
6
+ * Retrieves the stored pricing token's payload.
7
+ * @returns The stored pricing token payload.
8
+ */
9
+ getPricingToken(): Record<string, any> | null;
10
+ /**
11
+ * Retrieves an attribute from the stored pricing token payload and returns it.
12
+ * @param key - A key of the stored pricing token whose is going to be retrieved.
13
+ * @return The value of the key in the stored pricing token payload.
14
+ */
15
+ getFromToken(key: string): any;
16
+ /**
17
+ * Updates the stored pricing token with the payload of a new one.
18
+ * @param token - Pricing token string
19
+ */
20
+ updatePricingToken(token: string): void;
21
+ evaluateFeature(featureId: string): boolean | null;
22
+ private _validToken;
23
+ }
24
+
25
+ /**
26
+ * SpaceClient handles API and WebSocket communication with SPACE.
27
+ * It allows event subscription and provides feature evaluation methods.
28
+ */
29
+ declare class SpaceClient$1 {
30
+ private readonly httpUrl;
31
+ private readonly wsUrl;
32
+ private readonly socketClient;
33
+ /**
34
+ * The WebSocket namespace specifically for pricing-related events.
35
+ */
36
+ private readonly pricingSocketNamespace;
37
+ private readonly apiKey;
38
+ private readonly axios;
39
+ private readonly emitter;
40
+ readonly tokenService: TokenService$1;
41
+ private userId;
42
+ constructor(config: SpaceConfiguration);
43
+ /**
44
+ * Connects to the SPACE WebSocket and handles incoming events.
45
+ */
46
+ private connectWebSocket;
47
+ /**
48
+ * Listen to SPACE and connection events.
49
+ * @param event The event key to listen for.
50
+ * @param callback The callback function to execute when the event is emitted.
51
+ * @example
52
+ * ```typescript
53
+ * spaceClient.on('pricing_created', (data) => {
54
+ * console.log('Pricing created:', data);
55
+ * });
56
+ * ```
57
+ * @throws Will throw an error if the event is not recognized.
58
+ * @throws Will throw an error if the callback is not a function.
59
+ */
60
+ on(event: SpaceEvents, callback: (data: any) => void): void;
61
+ /**
62
+ * Sets the user ID for the client and loads the pricing token for that user.
63
+ * @param userId The user ID to set for the client.
64
+ * @returns A promise that resolves when the user ID is set and the pricing token is generated.
65
+ * @throws Will throw an error if the user ID is not a non-empty string.
66
+ * @example
67
+ * ```typescript
68
+ * await spaceClient.setUserId('user123');
69
+ * ```
70
+ */
71
+ setUserId(userId: string): Promise<void>;
72
+ /**
73
+ * Performs a request to SPACE to retrieve a new pricing token for the user with the given userId.
74
+ * @param userId The user ID for which to evaluate the feature.
75
+ * @returns A promise that resolves to the pricing token.
76
+ * @throws Will throw an error if the request fails.
77
+ */
78
+ generateUserPricingToken(): Promise<string>;
79
+ }
80
+
81
+ interface EventMessage {
82
+ code: string;
83
+ details: {
84
+ serviceName: string;
85
+ pricingVersion?: string;
86
+ };
87
+ }
88
+
89
+ type SpaceEvents =
90
+ | 'synchronized'
91
+ | 'pricing_created'
92
+ | 'pricing_archived'
93
+ | 'pricing_actived'
94
+ | 'service_disabled'
95
+ | 'error';
96
+
97
+ interface SpaceConfiguration {
98
+ url: string;
99
+ apiKey: string;
100
+ allowConnectionWithSpace: boolean
101
+ }
102
+
103
+ interface SpaceClientContext{
104
+ client?: SpaceClient;
105
+ tokenService: TokenService;
106
+ }
107
+
108
+ /**
109
+ * SpaceProvider initializes and provides the SpaceClient instance to children.
110
+ */
111
+ declare const SpaceProvider: ({ config, loader, children, }: {
112
+ config: SpaceConfiguration;
113
+ loader?: React.ReactNode;
114
+ children: React.ReactNode;
115
+ }) => JSX.Element;
116
+
117
+ /**
118
+ * Custom hook to access the SpaceClient instance from context.
119
+ * Throws an error if used outside of SpaceProvider.
120
+ */
121
+ declare function useSpaceClient(): any;
122
+
123
+ /**
124
+ * Custom hook to access the service that manages the pricing token.
125
+ * Throws an error if used outside of SpaceProvider.
126
+ */
127
+ declare function usePricingToken(): TokenService;
128
+
129
+ interface FeatureProps {
130
+ id: string;
131
+ children: React.ReactNode;
132
+ }
133
+ declare const Feature: ({ id, children }: FeatureProps) => JSX.Element;
134
+
135
+ export { Feature, SpaceClient$1 as SpaceClient, SpaceProvider, TokenService$1 as TokenService, usePricingToken, useSpaceClient };
136
+ export type { EventMessage, SpaceClientContext, SpaceConfiguration, SpaceEvents };
package/dist/index.js ADDED
@@ -0,0 +1,317 @@
1
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
+ import React, { createContext, useMemo, useContext, useState, useEffect } from 'react';
3
+ import { TinyEmitter } from 'tiny-emitter';
4
+ import axios from 'axios';
5
+ import { io } from 'socket.io-client';
6
+
7
+ function parseJwt(token) {
8
+ return JSON.parse(atob(token.split('.')[1]));
9
+ }
10
+ /**
11
+ * Checks if the current pricing token is expired based on the 'exp' claim.
12
+ * @returns True if the token is expired or not set, false otherwise.
13
+ */
14
+ function isTokenExpired(tokenPayload) {
15
+ if (typeof tokenPayload.exp !== 'number') {
16
+ return true;
17
+ }
18
+ // 'exp' is in seconds since epoch
19
+ const now = Math.floor(Date.now() / 1000);
20
+ return now >= tokenPayload.exp;
21
+ }
22
+ class TokenService {
23
+ constructor() {
24
+ this.tokenPayload = null;
25
+ }
26
+ /**
27
+ * Retrieves the stored pricing token's payload.
28
+ * @returns The stored pricing token payload.
29
+ */
30
+ getPricingToken() {
31
+ if (!this._validToken()) {
32
+ return null;
33
+ }
34
+ return this.tokenPayload;
35
+ }
36
+ /**
37
+ * Retrieves an attribute from the stored pricing token payload and returns it.
38
+ * @param key - A key of the stored pricing token whose is going to be retrieved.
39
+ * @return The value of the key in the stored pricing token payload.
40
+ */
41
+ getFromToken(key) {
42
+ if (!this._validToken()) {
43
+ return null;
44
+ }
45
+ return this.tokenPayload[key];
46
+ }
47
+ /**
48
+ * Updates the stored pricing token with the payload of a new one.
49
+ * @param token - Pricing token string
50
+ */
51
+ updatePricingToken(token) {
52
+ const parsedToken = parseJwt(token);
53
+ this.tokenPayload = parsedToken;
54
+ }
55
+ evaluateFeature(featureId) {
56
+ if (!this._validToken()) {
57
+ return false;
58
+ }
59
+ // Check if the feature exists in the token payload
60
+ if (this.tokenPayload?.features?.[featureId]) {
61
+ return this.tokenPayload.features[featureId].eval;
62
+ }
63
+ else {
64
+ console.warn(`Feature '${featureId}' not found in token payload.`);
65
+ return null;
66
+ }
67
+ }
68
+ _validToken() {
69
+ if (!this.tokenPayload) {
70
+ console.warn('Token payload is not set. Please call updateLocalPricingToken first.');
71
+ return false;
72
+ }
73
+ if (isTokenExpired(this.tokenPayload)) {
74
+ console.warn('Pricing token is expired. Please update it the pricing token.');
75
+ return false;
76
+ }
77
+ return true;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * SpaceClient handles API and WebSocket communication with SPACE.
83
+ * It allows event subscription and provides feature evaluation methods.
84
+ */
85
+ class SpaceClient {
86
+ constructor(config) {
87
+ this.userId = null;
88
+ this.httpUrl = config.url.endsWith('/')
89
+ ? config.url.slice(0, -1) + '/api/v1'
90
+ : config.url + '/api/v1';
91
+ this.wsUrl = config.url.replace(/^http/, 'ws') + '/events/pricings';
92
+ this.socketClient = io(this.wsUrl, {
93
+ path: '/events',
94
+ });
95
+ this.pricingSocketNamespace = this.socketClient.io.socket('/pricings');
96
+ this.apiKey = config.apiKey;
97
+ this.emitter = new TinyEmitter();
98
+ this.tokenService = new TokenService();
99
+ this.axios = axios.create({
100
+ baseURL: this.httpUrl,
101
+ headers: {
102
+ 'Content-Type': 'application/json',
103
+ 'x-api-key': `${this.apiKey}`,
104
+ },
105
+ });
106
+ this.connectWebSocket();
107
+ }
108
+ /**
109
+ * Connects to the SPACE WebSocket and handles incoming events.
110
+ */
111
+ connectWebSocket() {
112
+ this.pricingSocketNamespace.on('connect', () => {
113
+ console.log('Connected to SPACE');
114
+ this.emitter.emit('synchronized', 'WebSocket connection established');
115
+ });
116
+ this.pricingSocketNamespace.on('message', data => {
117
+ const event = data.code.toLowerCase();
118
+ this.emitter.emit(event, data.details);
119
+ });
120
+ this.pricingSocketNamespace.on('connect_error', error => {
121
+ this.emitter.emit('error', error);
122
+ });
123
+ }
124
+ /**
125
+ * Listen to SPACE and connection events.
126
+ * @param event The event key to listen for.
127
+ * @param callback The callback function to execute when the event is emitted.
128
+ * @example
129
+ * ```typescript
130
+ * spaceClient.on('pricing_created', (data) => {
131
+ * console.log('Pricing created:', data);
132
+ * });
133
+ * ```
134
+ * @throws Will throw an error if the event is not recognized.
135
+ * @throws Will throw an error if the callback is not a function.
136
+ */
137
+ on(event, callback) {
138
+ if (typeof callback !== 'function') {
139
+ throw new Error(`Callback for event '${event}' must be a function.`);
140
+ }
141
+ if ([
142
+ 'synchronized',
143
+ 'pricing_created',
144
+ 'pricing_archived',
145
+ 'pricing_actived',
146
+ 'service_disabled',
147
+ 'error',
148
+ ].indexOf(event) === -1) {
149
+ throw new Error(`Event '${event}' is not recognized.`);
150
+ }
151
+ this.emitter.on(event, callback);
152
+ }
153
+ /**
154
+ * Sets the user ID for the client and loads the pricing token for that user.
155
+ * @param userId The user ID to set for the client.
156
+ * @returns A promise that resolves when the user ID is set and the pricing token is generated.
157
+ * @throws Will throw an error if the user ID is not a non-empty string.
158
+ * @example
159
+ * ```typescript
160
+ * await spaceClient.setUserId('user123');
161
+ * ```
162
+ */
163
+ async setUserId(userId) {
164
+ if (typeof userId !== 'string' || userId.trim() === '') {
165
+ throw new Error('User ID must be a non-empty string.');
166
+ }
167
+ this.userId = userId;
168
+ this.tokenService.updatePricingToken(await this.generateUserPricingToken());
169
+ }
170
+ /**
171
+ * Performs a request to SPACE to retrieve a new pricing token for the user with the given userId.
172
+ * @param userId The user ID for which to evaluate the feature.
173
+ * @returns A promise that resolves to the pricing token.
174
+ * @throws Will throw an error if the request fails.
175
+ */
176
+ async generateUserPricingToken() {
177
+ if (!this.userId) {
178
+ throw new Error('User ID is not set. Please set the user ID with `setUserId(userId)` before trying to generate a pricing token.');
179
+ }
180
+ return this.axios
181
+ .post(`/features/${this.userId}`)
182
+ .then((response) => {
183
+ return response.data.pricingToken;
184
+ })
185
+ .catch((error) => {
186
+ console.error(`Error generating pricing token for user ${this.userId}:`, error);
187
+ throw error;
188
+ });
189
+ }
190
+ }
191
+
192
+ // Context provides the SpaceClient instance
193
+ const SpaceContext = createContext(undefined);
194
+ /**
195
+ * SpaceProvider initializes and provides the SpaceClient instance to children.
196
+ */
197
+ const SpaceProvider = ({ config, loader, children, }) => {
198
+ // Memorize the client to avoid unnecessary re-instantiation
199
+ const context = useMemo(() => {
200
+ const client = config.allowConnectionWithSpace ? new SpaceClient(config) : undefined;
201
+ let tokenService;
202
+ if (!client) {
203
+ tokenService = new TokenService();
204
+ }
205
+ else {
206
+ tokenService = client.tokenService;
207
+ }
208
+ return {
209
+ client: client,
210
+ tokenService: tokenService,
211
+ };
212
+ }, [config.url, config.apiKey]);
213
+ const [connected, setConnected] = React.useState(false);
214
+ React.useEffect(() => {
215
+ if (!context.client) {
216
+ setConnected(true); // No connection needed if client is undefined
217
+ return;
218
+ }
219
+ const handleSync = () => setConnected(true);
220
+ context.client.on('synchronized', handleSync);
221
+ }, [context.client]);
222
+ if (!connected)
223
+ return (jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', width: '100%' }, children: [loader ?? (jsx("div", { style: {
224
+ border: '4px solid #e0e0e0',
225
+ borderTop: '4px solid #1976d2',
226
+ borderRadius: '50%',
227
+ width: 48,
228
+ height: 48,
229
+ animation: 'spin 1s linear infinite',
230
+ } })), jsx("style", { children: `
231
+ @keyframes spin {
232
+ 0% { transform: rotate(0deg); }
233
+ 100% { transform: rotate(360deg); }
234
+ }
235
+ ` })] }));
236
+ return jsx(SpaceContext.Provider, { value: context, children: children });
237
+ };
238
+
239
+ /**
240
+ * Custom hook to access the SpaceClient instance from context.
241
+ * Throws an error if used outside of SpaceProvider.
242
+ */
243
+ function useSpaceClient() {
244
+ const spaceContext = useContext(SpaceContext);
245
+ if (!spaceContext) {
246
+ throw new Error('useSpaceClient must be used within a SpaceProvider');
247
+ }
248
+ if (!spaceContext.client) {
249
+ throw new Error(`SpaceClient is not initialized, so it cannot be instanciated.
250
+ If you want to allow direct connection with the SPACE instance,
251
+ ensure that you configuration has allowConnectionWithSpace = true,
252
+ and url and apiKey provided.`);
253
+ }
254
+ return spaceContext.client;
255
+ }
256
+
257
+ /**
258
+ * Custom hook to access the service that manages the pricing token.
259
+ * Throws an error if used outside of SpaceProvider.
260
+ */
261
+ function usePricingToken() {
262
+ const spaceContext = useContext(SpaceContext);
263
+ if (!spaceContext) {
264
+ throw new Error('usePricingToken must be used within a SpaceProvider');
265
+ }
266
+ return spaceContext.tokenService;
267
+ }
268
+
269
+ // Helper to get a child by type name
270
+ function getChildByType(children, typeName) {
271
+ const arr = React.Children.toArray(children);
272
+ return arr.find(child => child.type && child.type.name === typeName) || null;
273
+ }
274
+ const Feature = ({ id, children }) => {
275
+ const tokenService = usePricingToken();
276
+ const [status, setStatus] = useState('loading');
277
+ const [result, setResult] = useState(null);
278
+ // Validate id
279
+ const isValidId = useMemo(() => id.includes('-'), [id]);
280
+ useEffect(() => {
281
+ if (!isValidId) {
282
+ console.error(`Invalid feature ID: ‘${id}’. A valid feature ID must contain a hyphen (’-’) and follow the format: ‘{serviceName in lowercase}-{featureName as defined in the pricing}’.`);
283
+ setStatus('error');
284
+ return;
285
+ }
286
+ if (tokenService.getPricingToken() === null) {
287
+ console.error(`Pricing token is either not set or expired. Please ensure the token is initialized and not expired before using the Feature component.`);
288
+ setStatus('error');
289
+ return;
290
+ }
291
+ setStatus('loading');
292
+ setResult(null);
293
+ const evaluationResult = tokenService.evaluateFeature(id);
294
+ if (evaluationResult === null || evaluationResult === undefined) {
295
+ setStatus('error');
296
+ }
297
+ else {
298
+ setResult(evaluationResult);
299
+ setStatus('success');
300
+ }
301
+ }, [id, isValidId]);
302
+ if (status === 'loading') {
303
+ return getChildByType(children, 'Loading') || jsx(Fragment, {});
304
+ }
305
+ if (status === 'error') {
306
+ return getChildByType(children, 'ErrorFallback') || jsx(Fragment, {});
307
+ }
308
+ if (status === 'success' && result === true) {
309
+ return getChildByType(children, 'On') || jsx(Fragment, {});
310
+ }
311
+ if (status === 'success' && result === false) {
312
+ return getChildByType(children, 'Default') || jsx(Fragment, {});
313
+ }
314
+ return jsx(Fragment, {});
315
+ };
316
+
317
+ export { Feature, SpaceProvider, usePricingToken, useSpaceClient };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "space-react-client",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "rollup -c",
20
+ "dev": "rollup -c --watch",
21
+ "lint": "eslint src --ext .ts,.tsx",
22
+ "lint:fix": "eslint src --ext .ts,.tsx --fix",
23
+ "format": "prettier --write .",
24
+ "test": "vitest run"
25
+ },
26
+ "keywords": [],
27
+ "author": "",
28
+ "license": "ISC",
29
+ "packageManager": "pnpm@10.10.0",
30
+ "peerDependencies": {
31
+ "react": "^19.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "@rollup/plugin-alias": "^5.1.1",
35
+ "@rollup/plugin-typescript": "^12.1.2",
36
+ "@testing-library/react": "^16.3.0",
37
+ "@types/react": "^19.1.6",
38
+ "@vitejs/plugin-react": "^4.5.0",
39
+ "eslint": "^9.28.0",
40
+ "eslint-config-prettier": "^10.1.5",
41
+ "eslint-plugin-import": "^2.31.0",
42
+ "eslint-plugin-react": "^7.37.5",
43
+ "eslint-plugin-react-hooks": "^5.2.0",
44
+ "jiti": "^2.4.2",
45
+ "jsdom": "^26.1.0",
46
+ "prettier": "^3.5.3",
47
+ "rollup": "^4.41.1",
48
+ "rollup-plugin-dts": "^6.2.1",
49
+ "tslib": "^2.8.1",
50
+ "typescript": "^5.8.3",
51
+ "typescript-eslint": "^8.33.0",
52
+ "vitest": "^3.2.0"
53
+ },
54
+ "dependencies": {
55
+ "axios": "^1.9.0",
56
+ "socket.io-client": "^4.8.1",
57
+ "tiny-emitter": "^2.1.0"
58
+ }
59
+ }