@pixels-online/pixels-client-js-sdk 1.0.2 → 1.2.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/README.md CHANGED
@@ -1 +1,338 @@
1
- # pixels-buildon-client-js-sdk
1
+ # Pixels BuildOn Client JS SDK
2
+
3
+ A powerful TypeScript SDK for integrating Pixels BuildOn offerwall functionality into web applications and games. This SDK provides real-time offer management, player progression tracking, and reward handling capabilities.
4
+
5
+ ## 🚀 Features
6
+
7
+ - **Real-time Offer Management** - Live updates via Server-Sent Events (SSE)
8
+ - **Player Progression Tracking** - Monitor player stats, achievements, and conditions
9
+ - **Reward System Integration** - Handle various reward types (coins, items, exp, etc.)
10
+ - **Event-Driven Architecture** - React to offer events and player actions
11
+ - **TypeScript Support** - Full type safety and IntelliSense support
12
+ - **Flexible Configuration** - Customizable asset resolution and hooks
13
+ - **Multi-Environment Support** - Local, Test, staging, and production environments
14
+ - **Auto-Reconnection** - Robust connection management with retry logic
15
+
16
+ ## 📦 Installation
17
+
18
+ ```bash
19
+ npm install @pixels-online/pixels-client-js-sdk
20
+ ```
21
+
22
+ ## 🔧 Quick Start
23
+
24
+ ### 1. Basic Setup
25
+
26
+ ```typescript
27
+ import { OfferwallClient } from '@pixels-online/pixels-client-js-sdk';
28
+
29
+ const client = new OfferwallClient({
30
+ env: 'test', // or 'live' for production
31
+ tokenProvider: async () => {
32
+ // Fetch JWT token from your server. We recommend using our server-side SDK on your server for JWT creation
33
+ const response = await fetch('/api/auth/pixels-token', {
34
+ method: 'POST',
35
+ headers: { 'Content-Type': 'application/json' },
36
+ });
37
+ const data = await response.json();
38
+ return data.token;
39
+ },
40
+ assetResolver: (_, id: string) => {
41
+ return {
42
+ name: exampleGameLib[id]?.name || 'Unknown2',
43
+ image: exampleGameLib[id]?.image,
44
+ };
45
+ },
46
+ fallbackRewardImage: 'https://example.com/default-reward.png',
47
+ hooks: {
48
+ onOfferSurfaced: (offer) => {
49
+ var random = Math.random() < 0.5;
50
+ return random; // 50% chance to show the offer
51
+ },
52
+ },
53
+ autoConnect: true,
54
+ });
55
+ ```
56
+
57
+ ### 2. Initialize and Connect
58
+
59
+ ```typescript
60
+ // Initialize the client
61
+ await client.initialize();
62
+
63
+ // Listen for offers
64
+ client.on('offersUpdated', (offers) => {
65
+ console.log('Available offers:', offers);
66
+ displayOffersInUI(offers);
67
+ });
68
+
69
+ // Listen for player updates
70
+ client.on('playerUpdated', (player) => {
71
+ console.log('Player data updated:', player);
72
+ updatePlayerStatsUI(player);
73
+ });
74
+ ```
75
+
76
+ ### 3. Claim Offers
77
+
78
+ ```typescript
79
+ // Claim an offer
80
+ try {
81
+ const result = await client.claimOffer(offerId);
82
+ console.log('Offer claimed successfully:', result);
83
+ } catch (error) {
84
+ console.error('Failed to claim offer:', error);
85
+ }
86
+ ```
87
+
88
+ ## 🎯 Configuration Options
89
+
90
+ ### OfferwallConfig
91
+
92
+ ```typescript
93
+ interface OfferwallConfig {
94
+ /** Environment: 'test' | 'live' | custom endpoint */
95
+ env: 'test' | 'live' | (string & {});
96
+
97
+ /** Auto-connect on initialization (default: false) */
98
+ autoConnect?: boolean;
99
+
100
+ /** Enable auto-reconnection (default: true) */
101
+ reconnect?: boolean;
102
+
103
+ /** Reconnection delay in ms (default: 1000) */
104
+ reconnectDelay?: number;
105
+
106
+ /** Max reconnection attempts (default: 5) */
107
+ maxReconnectAttempts?: number;
108
+
109
+ /** Enable debug logging (default: false) */
110
+ debug?: boolean;
111
+
112
+ /** Event hooks for custom logic */
113
+ hooks?: Partial<OfferwallHooks>;
114
+
115
+ /** Custom asset resolver for rewards */
116
+ assetResolver?: AssetResolver;
117
+
118
+ /** Fallback image for unknown rewards */
119
+ fallbackRewardImage: string;
120
+
121
+ /** JWT token provider function */
122
+ tokenProvider: TokenProvider;
123
+ }
124
+ ```
125
+
126
+ ## 🎨 Asset Resolution
127
+
128
+ Customize how rewards are displayed in your game:
129
+
130
+ ```typescript
131
+ const gameAssets = {
132
+ gems: { name: 'Gems', image: '/assets/gems.png' },
133
+ gold: { name: 'Gold Coins', image: '/assets/gold.png' },
134
+ sword_1: { name: 'Iron Sword', image: '/assets/sword_iron.png' },
135
+ };
136
+
137
+ const client = new OfferwallClient({
138
+ // ... other config
139
+ assetResolver: (reward, assetId) => {
140
+ const asset = gameAssets[assetId];
141
+ if (asset) {
142
+ return { name: asset.name, image: asset.image };
143
+ }
144
+ return { name: `Unknown Item (${assetId})`, image: null };
145
+ },
146
+ });
147
+ ```
148
+
149
+ ## 🎣 Event Hooks
150
+
151
+ Implement custom logic with event hooks:
152
+
153
+ ```typescript
154
+ const client = new OfferwallClient({
155
+ // ... other config
156
+ hooks: {
157
+ // Control which offers to show
158
+ onOfferSurfaced: (offer) => {
159
+ // Custom logic to determine if offer should be shown
160
+ return player.level >= offer.minLevel;
161
+ },
162
+
163
+ // Handle successful offer claims
164
+ onOfferClaimed: async (offer, rewards) => {
165
+ console.log('Offer completed!', { offer, rewards });
166
+ // Award rewards in your game
167
+ await showConfetti(rewards);
168
+ },
169
+
170
+ // Handle connection events
171
+ onConnect: () => {
172
+ console.log('Connected to Pixels BuildOn');
173
+ showConnectionStatus('connected');
174
+ },
175
+
176
+ onDisconnect: () => {
177
+ console.log('Disconnected from Pixels BuildOn');
178
+ showConnectionStatus('disconnected');
179
+ },
180
+ },
181
+ });
182
+ ```
183
+
184
+ ## 📡 Events
185
+
186
+ Listen to various events emitted by the client:
187
+
188
+ ```typescript
189
+ // Offer-related events
190
+ client.on('offersUpdated', (offers) => {
191
+ /* Handle offers update */
192
+ });
193
+ client.on('offerAdded', (offer) => {
194
+ /* Handle new offer */
195
+ });
196
+ client.on('offerRemoved', (offerId) => {
197
+ /* Handle offer removal */
198
+ });
199
+ client.on('offerUpdated', (offer) => {
200
+ /* Handle offer changes */
201
+ });
202
+
203
+ // Player-related events
204
+ client.on('playerUpdated', (player) => {
205
+ /* Handle player data changes */
206
+ });
207
+
208
+ // Connection events
209
+ client.on('connected', () => {
210
+ /* Handle connection */
211
+ });
212
+ client.on('disconnected', () => {
213
+ /* Handle disconnection */
214
+ });
215
+ client.on('reconnecting', (attempt) => {
216
+ /* Handle reconnection attempts */
217
+ });
218
+
219
+ // Error events
220
+ client.on('error', (error) => {
221
+ /* Handle errors */
222
+ });
223
+ ```
224
+
225
+ ## 🏗️ API Reference
226
+
227
+ ### OfferwallClient Methods
228
+
229
+ #### `initialize(): Promise<void>`
230
+
231
+ Initialize the client and establish connection.
232
+
233
+ #### `disconnect(): Promise<void>`
234
+
235
+ Disconnect from the service.
236
+
237
+ #### `getOffers(): IClientOffer[]`
238
+
239
+ Get all current offers.
240
+
241
+ #### `getOffer(offerId: string): IClientOffer | null`
242
+
243
+ Get a specific offer by ID.
244
+
245
+ #### `claimOffer(offerId: string): Promise<ClaimResult>`
246
+
247
+ Claim an offer and receive rewards.
248
+
249
+ #### `getPlayer(): IClientPlayerSnapshot | null`
250
+
251
+ Get current player data.
252
+
253
+ #### `getConnectionState(): ConnectionState`
254
+
255
+ Get current connection status.
256
+
257
+ ### Utility Functions
258
+
259
+ ```typescript
260
+ import { meetsConditions, AssetHelper } from '@pixels-online/pixels-client-js-sdk';
261
+
262
+ // Check if player meets offer conditions
263
+ const canClaim = meetsConditions(player, offer.surfacingConditions);
264
+
265
+ // Asset helper utilities
266
+ const assetHelper = new AssetHelper(assetResolver, fallbackImage);
267
+ const rewardAsset = assetHelper.resolveRewardAsset(reward, assetId);
268
+ ```
269
+
270
+ ## 🧪 Testing
271
+
272
+ The SDK includes comprehensive test coverage:
273
+
274
+ ```bash
275
+ # Run all tests
276
+ npm test
277
+
278
+ # Run unit tests
279
+ npm run test:unit
280
+
281
+ # Run integration tests
282
+ npm run test:integration
283
+
284
+ # Run e2e tests
285
+ npm run test:e2e
286
+
287
+ # Run with coverage
288
+ npm run test:coverage
289
+
290
+ # Watch mode
291
+ npm run test:watch
292
+ ```
293
+
294
+ ## 🌍 Environments
295
+
296
+ The SDK supports multiple environments:
297
+
298
+ - **`test`** - Sandbox environment for development
299
+ - **`live`** - Production environment
300
+ - **`staging`** - Staging environment
301
+ - **`preview`** - Preview environment
302
+ - **`dev`** - Development environment
303
+ - **Custom URL** - Provide your own endpoint
304
+
305
+ ## 📋 Requirements
306
+
307
+ - Node.js 16+
308
+ - TypeScript 4.5+ (if using TypeScript)
309
+ - Modern browser with EventSource support
310
+
311
+ ## 🤝 Contributing
312
+
313
+ 1. Fork the repository
314
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
315
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
316
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
317
+ 5. Open a Pull Request
318
+
319
+ ## 📄 License
320
+
321
+ This project is licensed under the AGPLv3 License - see the [LICENSE.md](LICENSE.md) file for details.
322
+
323
+ ## 🔗 Links
324
+
325
+ - [GitLab Repository](https://gitlab.com/pixels-online-oss/pixels-buildon-client-js-sdk)
326
+ - [Issue Tracker](https://gitlab.com/pixels-online-oss/pixels-buildon-client-js-sdk/issues)
327
+ - [Pixels BuildOn Documentation](https://docs.pixels.xyz)
328
+
329
+ ## 📞 Support
330
+
331
+ For support and questions:
332
+
333
+ - Create an issue on [GitLab](https://gitlab.com/pixels-online-oss/pixels-buildon-client-js-sdk/issues)
334
+ - Contact the Pixels team
335
+
336
+ ---
337
+
338
+ Made with ❤️ by the Pixels team
@@ -3,8 +3,10 @@ import { OfferStore } from './OfferStore';
3
3
  import { AssetHelper } from '../utils/assets';
4
4
  import { OfferwallConfig } from '../types';
5
5
  import { ConnectionState } from '../types/connection';
6
+ export declare const mapEnvToBuildOnApiUrl: (env: "test" | "live" | (string & {})) => "https://api.pixels.xyz" | "https://api.sandbox.pixels.xyz" | "https://api.staging.pixels.xyz" | "https://api.preview.pixels.xyz" | "https://api.dev.pixels.xyz";
6
7
  export declare class OfferwallClient {
7
8
  private config;
9
+ private endpoint;
8
10
  private eventEmitter;
9
11
  private sseConnection;
10
12
  private offerStore;
@@ -7,6 +7,7 @@ export declare class SSEConnection {
7
7
  private eventEmitter;
8
8
  private tokenManager;
9
9
  private eventSource;
10
+ private endpoint;
10
11
  private reconnectAttempts;
11
12
  private reconnectTimeout;
12
13
  private isConnecting;
package/dist/index.esm.js CHANGED
@@ -344,6 +344,7 @@ class SSEConnection {
344
344
  this.connectionState = ConnectionState.DISCONNECTED;
345
345
  this.serverSuggestedRetryTime = null;
346
346
  this.logger = createLogger(config, 'SSEConnection');
347
+ this.endpoint = mapEnvToBuildOnApiUrl(config.env);
347
348
  }
348
349
  /**
349
350
  * Get current connection state
@@ -364,7 +365,7 @@ class SSEConnection {
364
365
  previousState,
365
366
  error,
366
367
  attempt: this.reconnectAttempts,
367
- maxAttempts: this.config.maxReconnectAttempts
368
+ maxAttempts: this.config.maxReconnectAttempts,
368
369
  });
369
370
  }
370
371
  connect() {
@@ -384,13 +385,13 @@ class SSEConnection {
384
385
  this.logger.log('Connecting to SSE endpoint...');
385
386
  try {
386
387
  // Create SSE URL
387
- const url = new URL(this.config.endpoint + '/sse/connect');
388
+ const url = new URL(this.endpoint + '/v1/sse/connect');
388
389
  const token = await this.tokenManager.getTokenForConnection();
389
390
  // Use CustomEventSource with Authorization Bearer header
390
391
  this.eventSource = new CustomEventSource(url.toString(), {
391
392
  headers: {
392
- 'Authorization': `Bearer ${token}`,
393
- }
393
+ Authorization: `Bearer ${token}`,
394
+ },
394
395
  });
395
396
  // Listen for server-suggested retry time updates
396
397
  this.eventSource.onRetryUpdate = (retryTime) => {
@@ -409,11 +410,14 @@ class SSEConnection {
409
410
  this.logger.log('SSE connection error', error);
410
411
  this.isConnecting = false;
411
412
  // Check if it's an auth error
412
- if (error?.detail?.isAuthError || error?.message === 'jwt-expired' || error?.message === 'jwt-invalid') {
413
+ if (error?.detail?.isAuthError ||
414
+ error?.message === 'jwt-expired' ||
415
+ error?.message === 'jwt-invalid') {
413
416
  this.tokenManager.clearToken();
414
417
  this.setConnectionState(ConnectionState.DISCONNECTED);
415
418
  // Try to reconnect with fresh token if reconnect is enabled
416
- if (this.config.reconnect && this.reconnectAttempts < (this.config.maxReconnectAttempts || 5)) {
419
+ if (this.config.reconnect &&
420
+ this.reconnectAttempts < (this.config.maxReconnectAttempts || 5)) {
417
421
  this.logger.log('JWT invalid/expired, attempting reconnect with fresh token');
418
422
  this.handleReconnect();
419
423
  resolve(); // Resolve instead of reject to allow reconnection
@@ -424,21 +428,22 @@ class SSEConnection {
424
428
  return;
425
429
  }
426
430
  }
427
- const errorMsg = this.eventSource?.getReadyState() === ReadyState.CLOSED ?
428
- 'Connection closed' : 'Connection error';
431
+ const errorMsg = this.eventSource?.getReadyState() === ReadyState.CLOSED
432
+ ? 'Connection closed'
433
+ : 'Connection error';
429
434
  const connectionError = new Error(errorMsg);
430
435
  this.setConnectionState(ConnectionState.ERROR, connectionError);
431
436
  if (this.eventSource?.getReadyState() === ReadyState.CLOSED) {
432
437
  this.eventEmitter.emit(OfferEvent.CONNECTION_ERROR, {
433
438
  error: connectionError,
434
- timestamp: new Date()
439
+ timestamp: new Date(),
435
440
  });
436
441
  this.handleReconnect();
437
442
  }
438
443
  else {
439
444
  this.eventEmitter.emit(OfferEvent.CONNECTION_ERROR, {
440
445
  error: connectionError,
441
- timestamp: new Date()
446
+ timestamp: new Date(),
442
447
  });
443
448
  reject(connectionError);
444
449
  }
@@ -543,7 +548,7 @@ class SSEConnection {
543
548
  this.logger.log('Error parsing SSE message:', error);
544
549
  this.eventEmitter.emit(OfferEvent.ERROR, {
545
550
  error: error,
546
- context: 'sse_message_parse'
551
+ context: 'sse_message_parse',
547
552
  });
548
553
  }
549
554
  }
@@ -558,7 +563,7 @@ class SSEConnection {
558
563
  this.disconnect();
559
564
  this.eventEmitter.emit(OfferEvent.DISCONNECTED, {
560
565
  reason: 'max_reconnect_attempts',
561
- timestamp: new Date()
566
+ timestamp: new Date(),
562
567
  });
563
568
  return;
564
569
  }
@@ -587,7 +592,7 @@ class SSEConnection {
587
592
  this.setConnectionState(ConnectionState.DISCONNECTED);
588
593
  this.eventEmitter.emit(OfferEvent.DISCONNECTED, {
589
594
  reason: 'manual',
590
- timestamp: new Date()
595
+ timestamp: new Date(),
591
596
  });
592
597
  }
593
598
  this.isConnecting = false;
@@ -837,18 +842,35 @@ class AssetHelper {
837
842
  }
838
843
  }
839
844
 
845
+ const mapEnvToBuildOnApiUrl = (env) => {
846
+ switch (env) {
847
+ case 'live':
848
+ return 'https://api.pixels.xyz';
849
+ case 'test':
850
+ return 'https://api.sandbox.pixels.xyz';
851
+ case 'staging':
852
+ return 'https://api.staging.pixels.xyz';
853
+ case 'preview':
854
+ return 'https://api.preview.pixels.xyz';
855
+ case 'dev':
856
+ case 'development':
857
+ return 'https://api.dev.pixels.xyz';
858
+ default:
859
+ return 'https://api.sandbox.pixels.xyz';
860
+ }
861
+ };
840
862
  class OfferwallClient {
841
863
  constructor(config) {
842
864
  this.isInitializing = false;
843
865
  this.config = {
844
- endpoint: config.endpoint || 'https://api.buildon.pixels.xyz',
845
866
  autoConnect: config.autoConnect ?? false,
846
867
  reconnect: config.reconnect ?? true,
847
868
  reconnectDelay: config.reconnectDelay ?? 1000,
848
869
  maxReconnectAttempts: config.maxReconnectAttempts ?? 5,
849
870
  debug: config.debug ?? false,
850
- ...config
871
+ ...config,
851
872
  };
873
+ this.endpoint = mapEnvToBuildOnApiUrl(config.env);
852
874
  this.hooks = this.config.hooks || {};
853
875
  this.logger = createLogger(this.config, 'OfferwallClient');
854
876
  this.eventEmitter = new EventEmitter(this.config);
@@ -858,7 +880,7 @@ class OfferwallClient {
858
880
  this.sseConnection = new SSEConnection(this.config, this.eventEmitter, this.tokenManager);
859
881
  this.setupInternalListeners();
860
882
  if (this.config.autoConnect) {
861
- this.initialize().catch(err => {
883
+ this.initialize().catch((err) => {
862
884
  this.logger.error('Auto-initialization failed:', err);
863
885
  });
864
886
  }
@@ -910,7 +932,7 @@ class OfferwallClient {
910
932
  }
911
933
  const token = await this.tokenManager.getTokenForConnection();
912
934
  if (this.hooks.beforeConnect) {
913
- await this.hooks.beforeConnect({ jwt: token, endpoint: this.config.endpoint });
935
+ await this.hooks.beforeConnect({ jwt: token, endpoint: this.endpoint });
914
936
  }
915
937
  try {
916
938
  await this.sseConnection.connect();
@@ -959,7 +981,7 @@ class OfferwallClient {
959
981
  }
960
982
  try {
961
983
  const response = await this.claimOfferAPI(instanceId);
962
- const updatedOffer = { ...offer, status: 'claimed', };
984
+ const updatedOffer = { ...offer, status: 'claimed' };
963
985
  this.offerStore.upsertOffer(updatedOffer);
964
986
  this.eventEmitter.emit(OfferEvent.OFFER_CLAIMED, {
965
987
  instanceId,
@@ -1023,11 +1045,11 @@ class OfferwallClient {
1023
1045
  */
1024
1046
  async postWithAuth(endpoint, body, retry = false) {
1025
1047
  const token = await this.tokenManager.getTokenForConnection();
1026
- const response = await fetch(`${this.config.endpoint}${endpoint}`, {
1048
+ const response = await fetch(`${this.endpoint}${endpoint}`, {
1027
1049
  method: 'POST',
1028
1050
  headers: {
1029
- 'Authorization': `Bearer ${token}`,
1030
- 'Content-Type': 'application/json'
1051
+ Authorization: `Bearer ${token}`,
1052
+ 'Content-Type': 'application/json',
1031
1053
  },
1032
1054
  body: body ? JSON.stringify(body) : undefined,
1033
1055
  });
@@ -1045,7 +1067,10 @@ class OfferwallClient {
1045
1067
  return response.json();
1046
1068
  }
1047
1069
  async claimOfferAPI(instanceId) {
1048
- return this.postWithAuth('/client/reward/claim', { instanceId, kind: 'offer' });
1070
+ return this.postWithAuth('/v1/client/reward/claim', {
1071
+ instanceId,
1072
+ kind: 'offer',
1073
+ });
1049
1074
  }
1050
1075
  async refreshOffersAndSnapshot() {
1051
1076
  try {
@@ -1061,7 +1086,9 @@ class OfferwallClient {
1061
1086
  }
1062
1087
  }
1063
1088
  async getOffersAndSnapshot() {
1064
- const data = await this.postWithAuth('/client/player/campaigns', { viewingCampaigns: true });
1089
+ const data = await this.postWithAuth('/v1/client/player/campaigns', {
1090
+ viewingCampaigns: true,
1091
+ });
1065
1092
  if (!data.offers || !Array.isArray(data.offers)) {
1066
1093
  throw new Error('No offers returned from offers endpoint');
1067
1094
  }
@@ -1071,7 +1098,7 @@ class OfferwallClient {
1071
1098
  return data;
1072
1099
  }
1073
1100
  async getAuthLinkToken() {
1074
- const data = await this.postWithAuth('/auth/one_time_token/generate');
1101
+ const data = await this.postWithAuth('/v1/auth/one_time_token/generate');
1075
1102
  if (!data.token) {
1076
1103
  throw new Error('No token returned from auth link endpoint');
1077
1104
  }