@neosianexus/super-tebex 2.0.2 → 3.0.3

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,471 +1,545 @@
1
- # @neosia-core/super-tebex
1
+ # @neosia/tebex-nextjs
2
2
 
3
- SDK Tebex permettant une intégration facile dans un environnement React/Next.js. Cette bibliothèque fournit des hooks React et des stores Zustand pour gérer facilement les catégories, paniers et la création de commandes Tebex.
3
+ Tebex Headless SDK optimized for Next.js App Router with TanStack Query and Zustand.
4
4
 
5
- ## 📦 Installation
5
+ ## Features
6
+
7
+ - **TanStack Query v5** - Automatic caching, retry, stale-while-revalidate
8
+ - **Zustand v5** - Persisted client state (basket, user)
9
+ - **TypeScript First** - Zero `any`, strict mode, exhaustive types
10
+ - **Provider Pattern** - Single provider, granular hooks
11
+ - **Error Codes** - i18n-friendly error handling with `TebexErrorCode`
12
+ - **Optimistic Updates** - Instant UI feedback on basket mutations
13
+ - **React Query DevTools** - Automatic in development mode
14
+
15
+ ## Installation
6
16
 
7
17
  ```bash
8
- npm install @neosia-core/super-tebex
9
- # ou
10
- yarn add @neosia-core/super-tebex
11
- # ou
12
- pnpm add @neosia-core/super-tebex
18
+ npm install @neosia/tebex-nextjs
19
+ # or
20
+ yarn add @neosia/tebex-nextjs
21
+ # or
22
+ pnpm add @neosia/tebex-nextjs
23
+ # or
24
+ bun add @neosia/tebex-nextjs
13
25
  ```
14
26
 
15
27
  ### Peer Dependencies
16
28
 
17
- Cette bibliothèque nécessite les dépendances suivantes :
18
-
19
- - `react` ^18.3.1
20
- - `react-dom` ^18.3.1
21
- - `zustand` ^5.0.6
22
- - `sonner` ^2.0.6
23
- - `tebex_headless` ^1.15.1
24
-
25
29
  ```bash
26
- npm install zustand sonner tebex_headless
30
+ npm install @tanstack/react-query zustand tebex_headless
27
31
  ```
28
32
 
29
- ## 🚀 Initialisation dans Next.js
30
-
31
- ### 1. Configuration initiale
32
-
33
- Créez un fichier de configuration (ex: `lib/tebex.ts`) :
34
-
35
- ```typescript
36
- import { initTebex, initShopUrls } from '@neosia-core/super-tebex';
33
+ | Dependency | Version |
34
+ |------------|---------|
35
+ | `react` | ^18.3.1 \|\| ^19.0.0 |
36
+ | `react-dom` | ^18.3.1 \|\| ^19.0.0 |
37
+ | `next` | ^14.0.0 \|\| ^15.0.0 (optional) |
38
+ | `@tanstack/react-query` | ^5.0.0 |
39
+ | `zustand` | ^5.0.0 |
40
+ | `tebex_headless` | ^1.15.0 |
37
41
 
38
- // Initialiser Tebex avec votre clé publique
39
- export function initializeTebex() {
40
- const publicKey = process.env.NEXT_PUBLIC_TEBEX_PUBLIC_KEY;
41
-
42
- if (!publicKey) {
43
- throw new Error('NEXT_PUBLIC_TEBEX_PUBLIC_KEY is not defined');
44
- }
42
+ ## Quick Start
45
43
 
46
- // Initialiser l'instance Tebex
47
- initTebex(publicKey);
44
+ ### 1. Setup Provider
48
45
 
49
- // Initialiser les URLs du shop (utilisées pour la création de panier)
50
- const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://votre-domaine.com';
51
-
52
- initShopUrls(baseUrl, {
53
- complete: '/shop/complete-purchase', // Optionnel, défaut: /shop/complete-purchase
54
- cancel: '/shop/cancel-purchase', // Optionnel, défaut: /shop/cancel-purchase
55
- });
56
- }
57
- ```
46
+ Wrap your app with `TebexProvider` in your root layout:
58
47
 
59
- ### 2. Initialisation dans `app/layout.tsx` (App Router)
60
-
61
- ```typescript
62
- import { useEffect } from 'react';
63
- import { Toaster } from 'sonner';
64
- import { initializeTebex } from '@/lib/tebex';
48
+ ```tsx
49
+ // app/layout.tsx
50
+ import { TebexProvider } from '@neosia/tebex-nextjs';
65
51
 
66
52
  export default function RootLayout({ children }: { children: React.ReactNode }) {
67
- useEffect(() => {
68
- initializeTebex();
69
- }, []);
70
-
71
53
  return (
72
- <html lang="fr">
54
+ <html lang="en">
73
55
  <body>
74
- {children}
75
- <Toaster position="top-right" />
56
+ <TebexProvider
57
+ config={{
58
+ publicKey: process.env.NEXT_PUBLIC_TEBEX_KEY!,
59
+ baseUrl: process.env.NEXT_PUBLIC_BASE_URL!,
60
+ urls: {
61
+ complete: '/shop/complete', // optional, default: /shop/complete
62
+ cancel: '/shop/cancel', // optional, default: /shop/cancel
63
+ },
64
+ onError: (error) => {
65
+ // Global error handler (optional)
66
+ console.error(`Tebex Error [${error.code}]:`, error.message);
67
+ },
68
+ }}
69
+ >
70
+ {children}
71
+ </TebexProvider>
76
72
  </body>
77
73
  </html>
78
74
  );
79
75
  }
80
76
  ```
81
77
 
82
- ### 3. Initialisation dans `pages/_app.tsx` (Pages Router)
83
-
84
- ```typescript
85
- import type { AppProps } from 'next/app';
86
- import { useEffect } from 'react';
87
- import { Toaster } from 'sonner';
88
- import { initializeTebex } from '@/lib/tebex';
78
+ ### 2. Use Hooks
89
79
 
90
- export default function App({ Component, pageProps }: AppProps) {
91
- useEffect(() => {
92
- initializeTebex();
93
- }, []);
94
-
95
- return (
96
- <>
97
- <Component {...pageProps} />
98
- <Toaster position="top-right" />
99
- </>
100
- );
101
- }
102
- ```
103
-
104
- ## 📚 Hooks disponibles
105
-
106
- ### `useBasket`
107
-
108
- Hook principal pour gérer le panier d'achat.
109
-
110
- ```typescript
111
- import { useBasket } from '@neosia-core/super-tebex';
80
+ ```tsx
81
+ 'use client';
112
82
 
113
- function BasketComponent() {
114
- // Le username peut venir du store global ou être passé directement
115
- const username = useShopUserStore(s => s.username);
116
- const { basket, loading, error, addPackageToBasket, removePackageFromBasket, refetch } = useBasket(username);
83
+ import { useCategories, useBasket, useUser } from '@neosia/tebex-nextjs';
117
84
 
118
- if (loading) {
119
- return <div>Chargement du panier...</div>;
120
- }
85
+ export function Shop() {
86
+ const { username, setUsername } = useUser();
87
+ const { categories, isLoading } = useCategories({ includePackages: true });
88
+ const { addPackage, itemCount, isAddingPackage } = useBasket();
121
89
 
122
- if (error) {
123
- return <div>Erreur: {error.message}</div>;
124
- }
125
-
126
- if (!basket) {
127
- return <div>Votre panier est vide</div>;
128
- }
90
+ if (isLoading) return <div>Loading...</div>;
129
91
 
130
92
  return (
131
93
  <div>
132
- <h2>Votre panier</h2>
133
- <ul>
134
- {basket.packages.map(pkg => (
135
- <li key={pkg.id}>
136
- {pkg.name} - Quantité: {pkg.in_basket.quantity}
137
- <button onClick={() => removePackageFromBasket(pkg.id)}>
138
- Supprimer
94
+ <p>Cart: {itemCount} items</p>
95
+ {categories?.map(category => (
96
+ <div key={category.id}>
97
+ <h2>{category.name}</h2>
98
+ {category.packages?.map(pkg => (
99
+ <button
100
+ key={pkg.id}
101
+ onClick={() => addPackage({ packageId: pkg.id })}
102
+ disabled={!username || isAddingPackage}
103
+ >
104
+ Add {pkg.name} - {pkg.price}
139
105
  </button>
140
- </li>
141
- ))}
142
- </ul>
143
- <button onClick={() => addPackageToBasket(123, 1)}>
144
- Ajouter un article
145
- </button>
106
+ ))}
107
+ </div>
108
+ ))}
146
109
  </div>
147
110
  );
148
111
  }
149
112
  ```
150
113
 
151
- #### API
152
-
153
- ```typescript
154
- interface UseBasketResult {
155
- basket: Basket | null; // Panier actuel
156
- loading: boolean; // État de chargement
157
- error: Error | null; // Erreur éventuelle
158
- addPackageToBasket: (
159
- packageId: number,
160
- quantity?: number,
161
- type?: PackageType,
162
- variableData?: Record<string, string>
163
- ) => Promise<void>;
164
- removePackageFromBasket: (packageId: number) => Promise<void>;
165
- updateManualBasket: (basket: Basket | null) => void;
166
- refetch: () => Promise<void>;
167
- }
114
+ ## Available Hooks
115
+
116
+ ### Data Fetching Hooks
117
+
118
+ | Hook | Description |
119
+ |------|-------------|
120
+ | `useCategories()` | Fetch all categories (with optional packages) |
121
+ | `useCategory(id)` | Fetch a single category by ID |
122
+ | `usePackages()` | Fetch all packages |
123
+ | `usePackage(id)` | Fetch a single package by ID |
124
+ | `useWebstore()` | Fetch webstore info (name, currency, domain) |
125
+
126
+ ### Basket Management
127
+
128
+ | Hook | Description |
129
+ |------|-------------|
130
+ | `useBasket()` | Full basket management with optimistic updates |
131
+ | `useCheckout()` | Launch Tebex.js checkout modal |
132
+ | `useCoupons()` | Apply/remove coupon codes |
133
+ | `useGiftCards()` | Apply/remove gift cards |
134
+ | `useCreatorCodes()` | Apply/remove creator codes |
135
+ | `useGiftPackage()` | Gift a package to another user |
136
+
137
+ ### User Management
138
+
139
+ | Hook | Description |
140
+ |------|-------------|
141
+ | `useUser()` | Username management (persisted in localStorage) |
142
+
143
+ ## Hook Examples
144
+
145
+ ### useBasket
146
+
147
+ ```tsx
148
+ const {
149
+ basket, // Basket | null
150
+ packages, // BasketPackage[]
151
+ basketIdent, // string | null
152
+
153
+ // Loading states
154
+ isLoading,
155
+ isFetching,
156
+ isAddingPackage,
157
+ isRemovingPackage,
158
+ isUpdatingQuantity,
159
+
160
+ // Error handling
161
+ error, // TebexError | null
162
+ errorCode, // TebexErrorCode | null
163
+
164
+ // Actions
165
+ addPackage, // (params: AddPackageParams) => Promise<void>
166
+ removePackage, // (packageId: number) => Promise<void>
167
+ updateQuantity, // (params: UpdateQuantityParams) => Promise<void>
168
+ clearBasket, // () => void
169
+ refetch, // () => Promise<...>
170
+
171
+ // Computed
172
+ itemCount, // number
173
+ total, // number
174
+ isEmpty, // boolean
175
+ } = useBasket();
176
+
177
+ // Add package with options
178
+ await addPackage({
179
+ packageId: 123,
180
+ quantity: 2,
181
+ type: 'single', // optional: 'single' | 'subscription'
182
+ variableData: { server: 'survival' }, // optional
183
+ });
168
184
  ```
169
185
 
170
- ### `useCategories`
171
-
172
- Hook pour récupérer et gérer les catégories de produits.
173
-
174
- ```typescript
175
- import { useCategories } from '@neosia-core/super-tebex';
176
-
177
- function CategoriesComponent() {
178
- const { categories, loading, error, getByName, refetch } = useCategories({
179
- includePackages: true,
180
- });
181
-
182
- if (loading) {
183
- return <div>Chargement des catégories...</div>;
184
- }
185
-
186
- if (error) {
187
- return <div>Erreur: {error.message}</div>;
188
- }
189
-
190
- return (
191
- <div>
192
- <h2>Catégories</h2>
193
- <button onClick={() => refetch()}>Actualiser</button>
194
- <ul>
195
- {categories?.map(category => (
196
- <li key={category.id}>
197
- <h3>{category.name}</h3>
198
- <p>{category.description}</p>
199
- </li>
200
- ))}
201
- </ul>
202
- </div>
203
- );
204
- }
186
+ ### useCategories
187
+
188
+ ```tsx
189
+ const {
190
+ categories, // Category[] | null
191
+ data, // same as categories
192
+ isLoading,
193
+ isFetching,
194
+ error,
195
+ errorCode,
196
+ refetch,
197
+
198
+ // Helpers
199
+ getByName, // (name: string) => Category | undefined
200
+ getById, // (id: number) => Category | undefined
201
+ } = useCategories({
202
+ includePackages: true, // default: true
203
+ enabled: true, // default: true
204
+ });
205
205
  ```
206
206
 
207
- #### API
208
-
209
- ```typescript
210
- interface UseCategoriesResult {
211
- categories: Category[] | null;
212
- loading: boolean;
213
- error: Error | null;
214
- getByName: (name: string) => Category | undefined;
215
- refetch: () => Promise<void>;
216
- }
207
+ ### useCheckout
208
+
209
+ ```tsx
210
+ const {
211
+ launch, // () => Promise<void>
212
+ isLaunching,
213
+ error,
214
+ errorCode,
215
+ canCheckout, // boolean - true if basket has items
216
+ checkoutUrl, // string | null - direct checkout URL
217
+ } = useCheckout({
218
+ onSuccess: () => console.log('Payment complete!'),
219
+ onError: (error) => console.error(error),
220
+ onClose: () => console.log('Checkout closed'),
221
+ });
222
+
223
+ // Requires Tebex.js script in your page
224
+ // <script src="https://js.tebex.io/v/1.js" />
217
225
  ```
218
226
 
219
- ### `useCreateBasket`
220
-
221
- Hook pour créer un nouveau panier (utilisé en interne par `useBasket`).
227
+ ### useUser
222
228
 
223
- ```typescript
224
- import { useCreateBasket } from '@neosia-core/super-tebex';
225
-
226
- function CreateBasketButton() {
227
- const username = useShopUserStore(s => s.username);
228
- const createBasket = useCreateBasket(username);
229
-
230
- const handleCreate = async () => {
231
- const basket = await createBasket();
232
- if (basket) {
233
- console.log('Panier créé:', basket.ident);
234
- }
235
- };
229
+ ```tsx
230
+ const {
231
+ username, // string | null
232
+ setUsername, // (username: string) => void
233
+ clearUsername, // () => void
234
+ isAuthenticated, // boolean
235
+ } = useUser();
236
+ ```
236
237
 
237
- return <button onClick={handleCreate}>Créer un panier</button>;
238
- }
238
+ ### useCoupons
239
+
240
+ ```tsx
241
+ const {
242
+ coupons, // Code[]
243
+ apply, // (code: string) => Promise<void>
244
+ remove, // (code: string) => Promise<void>
245
+ isApplying,
246
+ isRemoving,
247
+ error,
248
+ errorCode,
249
+ } = useCoupons();
239
250
  ```
240
251
 
241
- ## 🗄️ Stores Zustand
252
+ ## Error Handling
242
253
 
243
- ### `useShopUserStore`
254
+ All hooks expose `error` (TebexError) and `errorCode` (TebexErrorCode) for i18n-friendly error handling:
244
255
 
245
- Store pour gérer le nom d'utilisateur (persisté dans localStorage).
256
+ ```tsx
257
+ import { TebexErrorCode } from '@neosia/tebex-nextjs';
246
258
 
247
- ```typescript
248
- import { useShopUserStore } from '@neosia-core/super-tebex';
259
+ const { error, errorCode } = useBasket();
249
260
 
250
- function UserProfile() {
251
- const username = useShopUserStore(s => s.username);
252
- const setUsername = useShopUserStore(s => s.setUsername);
253
- const clearUsername = useShopUserStore(s => s.clearUsername);
261
+ // Use error codes for translations
262
+ const errorMessages: Record<TebexErrorCode, string> = {
263
+ [TebexErrorCode.NOT_AUTHENTICATED]: 'Please log in first',
264
+ [TebexErrorCode.BASKET_NOT_FOUND]: 'Your cart has expired',
265
+ [TebexErrorCode.PACKAGE_OUT_OF_STOCK]: 'Item is out of stock',
266
+ // ...
267
+ };
254
268
 
255
- return (
256
- <div>
257
- {username ? (
258
- <>
259
- <p>Connecté en tant que: {username}</p>
260
- <button onClick={clearUsername}>Déconnexion</button>
261
- </>
262
- ) : (
263
- <button onClick={() => setUsername('Player123')}>Se connecter</button>
264
- )}
265
- </div>
266
- );
269
+ if (errorCode) {
270
+ toast.error(errorMessages[errorCode] ?? 'An error occurred');
267
271
  }
268
272
  ```
269
273
 
270
- ### `useShopBasketStore`
271
-
272
- Store pour gérer l'identifiant du panier (persisté dans localStorage).
273
-
274
- ```typescript
275
- import { useShopBasketStore } from '@neosia-core/super-tebex';
274
+ ### Available Error Codes
275
+
276
+ | Category | Codes |
277
+ |----------|-------|
278
+ | Provider | `PROVIDER_NOT_FOUND`, `INVALID_CONFIG` |
279
+ | Auth | `NOT_AUTHENTICATED`, `INVALID_USERNAME` |
280
+ | Basket | `BASKET_NOT_FOUND`, `BASKET_CREATION_FAILED`, `BASKET_EXPIRED` |
281
+ | Package | `PACKAGE_NOT_FOUND`, `PACKAGE_OUT_OF_STOCK`, `PACKAGE_ALREADY_OWNED`, `INVALID_QUANTITY` |
282
+ | Category | `CATEGORY_NOT_FOUND` |
283
+ | Coupon | `COUPON_INVALID`, `COUPON_EXPIRED`, `COUPON_ALREADY_USED` |
284
+ | Gift Card | `GIFTCARD_INVALID`, `GIFTCARD_INSUFFICIENT_BALANCE` |
285
+ | Creator Code | `CREATOR_CODE_INVALID` |
286
+ | Checkout | `CHECKOUT_FAILED`, `CHECKOUT_CANCELLED` |
287
+ | Network | `NETWORK_ERROR`, `TIMEOUT`, `RATE_LIMITED` |
288
+ | Unknown | `UNKNOWN` |
289
+
290
+ ## TypeScript
291
+
292
+ All types are exported:
293
+
294
+ ```tsx
295
+ import type {
296
+ // Config
297
+ TebexConfig,
298
+ TebexUrls,
299
+
300
+ // Hook Returns
301
+ UseBasketReturn,
302
+ UseCategoriesReturn,
303
+ UseCheckoutReturn,
304
+ // ... all hook return types
305
+
306
+ // Tebex API types
307
+ Basket,
308
+ BasketPackage,
309
+ Category,
310
+ Package,
311
+ Webstore,
312
+
313
+ // Utilities
314
+ Result,
315
+ TebexError,
316
+ TebexErrorCode,
317
+ } from '@neosia/tebex-nextjs';
318
+
319
+ // Type guards
320
+ import { isTebexError, isSuccess, isError, isDefined } from '@neosia/tebex-nextjs';
321
+ ```
276
322
 
277
- function BasketStatus() {
278
- const basketIdent = useShopBasketStore(s => s.basketIdent);
279
- const clearBasketIdent = useShopBasketStore(s => s.clearBasketIdent);
323
+ ## Migration from v2
280
324
 
281
- return (
282
- <div>
283
- {basketIdent ? (
284
- <>
285
- <p>Panier actif: {basketIdent}</p>
286
- <button onClick={clearBasketIdent}>Vider le panier</button>
287
- </>
288
- ) : (
289
- <p>Aucun panier actif</p>
290
- )}
291
- </div>
292
- );
293
- }
294
- ```
325
+ ### Breaking Changes
295
326
 
296
- ### `useShopUiStore`
327
+ | v2 | v3 | Migration |
328
+ |----|-----|-----------|
329
+ | `initTebex(key)` | `<TebexProvider config={{...}}>` | Wrap app with Provider |
330
+ | `initShopUrls(url)` | `config.baseUrl` + `config.urls` | Pass in config |
331
+ | `useBasket(username)` | `useBasket()` + `useUser()` | User is separate |
332
+ | `error.message` (FR) | `error.code` | Translate codes yourself |
333
+ | `sonner` peer dep | Removed | Handle toasts yourself |
334
+ | `useShopUserStore` | `useUserStore` | Renamed |
335
+ | `useShopBasketStore` | `useBasketStore` | Renamed |
297
336
 
298
- Store pour gérer les états d'interface utilisateur (loading, etc.).
337
+ ### Migration Example
299
338
 
300
- ```typescript
301
- import { useShopUiStore } from '@neosia-core/super-tebex';
339
+ **Before (v2):**
302
340
 
303
- function LoadingIndicator() {
304
- const isGlobalLoading = useShopUiStore(s => s.isGlobalLoading);
305
- const isCreatingBasket = useShopUiStore(s => s.isCreatingBasket);
341
+ ```tsx
342
+ // lib/tebex.ts
343
+ initTebex(process.env.NEXT_PUBLIC_TEBEX_KEY);
344
+ initShopUrls('https://mysite.com');
306
345
 
307
- if (isGlobalLoading || isCreatingBasket) {
308
- return <div className="spinner">Chargement...</div>;
309
- }
346
+ // Component
347
+ const username = useShopUserStore(s => s.username);
348
+ const { basket, addPackageToBasket, error } = useBasket(username);
310
349
 
311
- return null;
312
- }
350
+ if (error) toast.error(error.message); // French message
313
351
  ```
314
352
 
315
- ## 💡 Exemples complets
353
+ **After (v3):**
354
+
355
+ ```tsx
356
+ // app/layout.tsx
357
+ <TebexProvider
358
+ config={{
359
+ publicKey: process.env.NEXT_PUBLIC_TEBEX_KEY!,
360
+ baseUrl: 'https://mysite.com',
361
+ onError: (err) => toast.error(t(`errors.${err.code}`)),
362
+ }}
363
+ >
364
+ {children}
365
+ </TebexProvider>
366
+
367
+ // Component
368
+ const { username } = useUser();
369
+ const { basket, addPackage, errorCode } = useBasket();
370
+
371
+ // Errors handled by onError callback or manually with errorCode
372
+ ```
316
373
 
317
- ### Exemple : Page de boutique complète
374
+ ## Complete Example
318
375
 
319
- ```typescript
376
+ ```tsx
320
377
  'use client';
321
378
 
322
- import { useCategories, useBasket, useShopUserStore } from '@neosia-core/super-tebex';
379
+ import { useCategories, useBasket, useUser, useCheckout } from '@neosia/tebex-nextjs';
380
+ import { useState } from 'react';
323
381
 
324
382
  export default function ShopPage() {
325
- const username = useShopUserStore(s => s.username);
326
- const { categories, loading: categoriesLoading } = useCategories({ includePackages: true });
327
- const { basket, loading: basketLoading, addPackageToBasket, removePackageFromBasket } = useBasket(username);
328
-
329
- if (categoriesLoading || basketLoading) {
330
- return <div>Chargement...</div>;
331
- }
332
-
333
- return (
334
- <div className="shop-container">
335
- <aside>
336
- <h2>Panier</h2>
337
- {basket ? (
338
- <ul>
339
- {basket.packages.map(pkg => (
340
- <li key={pkg.id}>
341
- {pkg.name} x{pkg.in_basket.quantity}
342
- <button onClick={() => removePackageFromBasket(pkg.id)}>Retirer</button>
343
- </li>
344
- ))}
345
- </ul>
346
- ) : (
347
- <p>Panier vide</p>
348
- )}
349
- </aside>
350
-
351
- <main>
352
- <h1>Boutique</h1>
353
- {categories?.map(category => (
354
- <section key={category.id}>
355
- <h2>{category.name}</h2>
356
- {category.packages?.map(pkg => (
357
- <div key={pkg.id} className="product-card">
358
- <h3>{pkg.name}</h3>
359
- <p>{pkg.description}</p>
360
- <p className="price">{pkg.price.display}</p>
361
- <button
362
- onClick={() => addPackageToBasket(pkg.id, 1)}
363
- disabled={!username}
364
- >
365
- Ajouter au panier
366
- </button>
367
- </div>
368
- ))}
369
- </section>
370
- ))}
371
- </main>
372
- </div>
373
- );
374
- }
375
- ```
383
+ const [input, setInput] = useState('');
376
384
 
377
- ### Exemple : Composant de connexion avec gestion d'utilisateur
385
+ // User
386
+ const { username, setUsername, clearUsername } = useUser();
378
387
 
379
- ```typescript
380
- 'use client';
381
-
382
- import { useState } from 'react';
383
- import { useShopUserStore, useBasket } from '@neosia-core/super-tebex';
388
+ // Categories
389
+ const { categories, isLoading: categoriesLoading } = useCategories({
390
+ includePackages: true,
391
+ });
384
392
 
385
- export default function LoginForm() {
386
- const [input, setInput] = useState('');
387
- const username = useShopUserStore(s => s.username);
388
- const setUsername = useShopUserStore(s => s.setUsername);
389
- const clearUsername = useShopUserStore(s => s.clearUsername);
390
- const { basket, refetch } = useBasket(username);
393
+ // Basket
394
+ const {
395
+ basket,
396
+ packages,
397
+ addPackage,
398
+ removePackage,
399
+ itemCount,
400
+ total,
401
+ isAddingPackage,
402
+ isEmpty,
403
+ } = useBasket();
404
+
405
+ // Checkout
406
+ const { launch, canCheckout, isLaunching } = useCheckout({
407
+ onSuccess: () => alert('Thank you for your purchase!'),
408
+ });
391
409
 
410
+ // Login handler
392
411
  const handleLogin = () => {
393
412
  if (input.trim()) {
394
413
  setUsername(input.trim());
395
- // Recharger le panier après connexion
396
- setTimeout(() => refetch(), 100);
414
+ setInput('');
397
415
  }
398
416
  };
399
417
 
400
- const handleLogout = () => {
401
- clearUsername();
402
- setInput('');
403
- };
404
-
405
- if (username) {
406
- return (
407
- <div>
408
- <p>Connecté: {username}</p>
409
- {basket && <p>Articles dans le panier: {basket.packages.length}</p>}
410
- <button onClick={handleLogout}>Déconnexion</button>
411
- </div>
412
- );
418
+ if (categoriesLoading) {
419
+ return <div>Loading store...</div>;
413
420
  }
414
421
 
415
422
  return (
416
- <div>
417
- <input
418
- type="text"
419
- value={input}
420
- onChange={(e) => setInput(e.target.value)}
421
- placeholder="Nom d'utilisateur"
422
- />
423
- <button onClick={handleLogin}>Se connecter</button>
423
+ <div className="container">
424
+ {/* Auth Section */}
425
+ <header>
426
+ {username ? (
427
+ <div>
428
+ <span>Welcome, {username}</span>
429
+ <button onClick={clearUsername}>Logout</button>
430
+ </div>
431
+ ) : (
432
+ <div>
433
+ <input
434
+ value={input}
435
+ onChange={(e) => setInput(e.target.value)}
436
+ placeholder="Enter username"
437
+ />
438
+ <button onClick={handleLogin}>Login</button>
439
+ </div>
440
+ )}
441
+ </header>
442
+
443
+ <main>
444
+ {/* Products */}
445
+ <section>
446
+ <h1>Products</h1>
447
+ {categories?.map(category => (
448
+ <div key={category.id}>
449
+ <h2>{category.name}</h2>
450
+ <div className="grid">
451
+ {category.packages?.map(pkg => (
452
+ <div key={pkg.id} className="card">
453
+ <h3>{pkg.name}</h3>
454
+ <p>{pkg.price}</p>
455
+ <button
456
+ onClick={() => addPackage({ packageId: pkg.id })}
457
+ disabled={!username || isAddingPackage}
458
+ >
459
+ {isAddingPackage ? 'Adding...' : 'Add to Cart'}
460
+ </button>
461
+ </div>
462
+ ))}
463
+ </div>
464
+ </div>
465
+ ))}
466
+ </section>
467
+
468
+ {/* Cart */}
469
+ <aside>
470
+ <h2>Cart ({itemCount})</h2>
471
+ {isEmpty ? (
472
+ <p>Your cart is empty</p>
473
+ ) : (
474
+ <>
475
+ <ul>
476
+ {packages.map(pkg => (
477
+ <li key={pkg.id}>
478
+ {pkg.name} x{pkg.in_basket.quantity}
479
+ <button onClick={() => removePackage(pkg.id)}>Remove</button>
480
+ </li>
481
+ ))}
482
+ </ul>
483
+ <p>Total: {basket?.base_price}</p>
484
+ <button
485
+ onClick={launch}
486
+ disabled={!canCheckout || isLaunching}
487
+ >
488
+ {isLaunching ? 'Loading...' : 'Checkout'}
489
+ </button>
490
+ </>
491
+ )}
492
+ </aside>
493
+ </main>
424
494
  </div>
425
495
  );
426
496
  }
427
497
  ```
428
498
 
429
- ## 🔔 Notifications (Toasts)
430
-
431
- La bibliothèque utilise `sonner` pour afficher des notifications. Assurez-vous d'avoir le composant `<Toaster />` dans votre application (voir section Initialisation).
499
+ ## Advanced Usage
432
500
 
433
- Les notifications sont automatiquement affichées pour :
434
- - Ajout/suppression d'articles au panier
435
- - Erreurs lors de la création du panier
436
- - Erreurs de connexion
501
+ ### Custom QueryClient
437
502
 
438
- ## 📝 Types TypeScript
503
+ ```tsx
504
+ import { QueryClient } from '@tanstack/react-query';
505
+ import { TebexProvider } from '@neosia/tebex-nextjs';
439
506
 
440
- Tous les types sont exportés depuis la bibliothèque :
507
+ const queryClient = new QueryClient({
508
+ defaultOptions: {
509
+ queries: {
510
+ staleTime: 30 * 1000, // 30 seconds
511
+ },
512
+ },
513
+ });
441
514
 
442
- ```typescript
443
- import type { Basket, Category, Package, PackageType } from '@neosia-core/super-tebex';
515
+ <TebexProvider config={config} queryClient={queryClient}>
516
+ {children}
517
+ </TebexProvider>
444
518
  ```
445
519
 
446
- ## 🐛 Gestion des erreurs
520
+ ### Direct Store Access
447
521
 
448
- Tous les hooks retournent un objet `error` que vous pouvez vérifier :
522
+ ```tsx
523
+ import { useBasketStore, useUserStore } from '@neosia/tebex-nextjs';
449
524
 
450
- ```typescript
451
- const { basket, error, loading } = useBasket(username);
452
-
453
- useEffect(() => {
454
- if (error) {
455
- console.error('Erreur panier:', error);
456
- // Gérer l'erreur (afficher un message, logger, etc.)
457
- }
458
- }, [error]);
525
+ // Access stores directly (outside of hooks)
526
+ const basketIdent = useBasketStore(state => state.basketIdent);
527
+ const username = useUserStore(state => state.username);
459
528
  ```
460
529
 
461
- ## 🔄 Persistance
530
+ ### Query Keys
462
531
 
463
- Les stores `useShopUserStore` et `useShopBasketStore` sont automatiquement persistés dans le `localStorage`, permettant de conserver l'état entre les sessions.
532
+ ```tsx
533
+ import { tebexKeys } from '@neosia/tebex-nextjs';
534
+ import { useQueryClient } from '@tanstack/react-query';
464
535
 
465
- ## 📚 API Reference
536
+ const queryClient = useQueryClient();
466
537
 
467
- Pour plus de détails sur les types et interfaces, consultez les définitions TypeScript dans `dist/index.d.ts`.
538
+ // Invalidate specific queries
539
+ queryClient.invalidateQueries({ queryKey: tebexKeys.categories() });
540
+ queryClient.invalidateQueries({ queryKey: tebexKeys.basket(basketIdent) });
541
+ ```
468
542
 
469
- ## 🤝 Contribution
543
+ ## License
470
544
 
471
- Les contributions sont les bienvenues ! N'hésitez pas à ouvrir une issue ou une pull request.
545
+ MIT