@neosianexus/super-tebex 2.0.2 → 3.0.2
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 +440 -366
- package/dist/index.cjs +1355 -1
- package/dist/index.d.ts +701 -96
- package/dist/index.js +1355 -1
- package/package.json +64 -38
package/README.md
CHANGED
|
@@ -1,471 +1,545 @@
|
|
|
1
|
-
# @neosia
|
|
1
|
+
# @neosia/tebex-nextjs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Tebex Headless SDK optimized for Next.js App Router with TanStack Query and Zustand.
|
|
4
4
|
|
|
5
|
-
##
|
|
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
|
|
9
|
-
#
|
|
10
|
-
yarn add @neosia
|
|
11
|
-
#
|
|
12
|
-
pnpm add @neosia
|
|
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
|
|
30
|
+
npm install @tanstack/react-query zustand tebex_headless
|
|
27
31
|
```
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
initTebex(publicKey);
|
|
44
|
+
### 1. Setup Provider
|
|
48
45
|
|
|
49
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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="
|
|
54
|
+
<html lang="en">
|
|
73
55
|
<body>
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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 (
|
|
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
|
-
<
|
|
133
|
-
|
|
134
|
-
{
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
<button
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
###
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
error
|
|
214
|
-
|
|
215
|
-
|
|
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/checkout.js" />
|
|
217
225
|
```
|
|
218
226
|
|
|
219
|
-
###
|
|
220
|
-
|
|
221
|
-
Hook pour créer un nouveau panier (utilisé en interne par `useBasket`).
|
|
227
|
+
### useUser
|
|
222
228
|
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
252
|
+
## Error Handling
|
|
242
253
|
|
|
243
|
-
|
|
254
|
+
All hooks expose `error` (TebexError) and `errorCode` (TebexErrorCode) for i18n-friendly error handling:
|
|
244
255
|
|
|
245
|
-
|
|
256
|
+
```tsx
|
|
257
|
+
import { TebexErrorCode } from '@neosia/tebex-nextjs';
|
|
246
258
|
|
|
247
|
-
|
|
248
|
-
import { useShopUserStore } from '@neosia-core/super-tebex';
|
|
259
|
+
const { error, errorCode } = useBasket();
|
|
249
260
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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
|
-
###
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
278
|
-
const basketIdent = useShopBasketStore(s => s.basketIdent);
|
|
279
|
-
const clearBasketIdent = useShopBasketStore(s => s.clearBasketIdent);
|
|
323
|
+
## Migration from v2
|
|
280
324
|
|
|
281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
337
|
+
### Migration Example
|
|
299
338
|
|
|
300
|
-
|
|
301
|
-
import { useShopUiStore } from '@neosia-core/super-tebex';
|
|
339
|
+
**Before (v2):**
|
|
302
340
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
341
|
+
```tsx
|
|
342
|
+
// lib/tebex.ts
|
|
343
|
+
initTebex(process.env.NEXT_PUBLIC_TEBEX_KEY);
|
|
344
|
+
initShopUrls('https://mysite.com');
|
|
306
345
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
346
|
+
// Component
|
|
347
|
+
const username = useShopUserStore(s => s.username);
|
|
348
|
+
const { basket, addPackageToBasket, error } = useBasket(username);
|
|
310
349
|
|
|
311
|
-
|
|
312
|
-
}
|
|
350
|
+
if (error) toast.error(error.message); // French message
|
|
313
351
|
```
|
|
314
352
|
|
|
315
|
-
|
|
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
|
-
|
|
374
|
+
## Complete Example
|
|
318
375
|
|
|
319
|
-
```
|
|
376
|
+
```tsx
|
|
320
377
|
'use client';
|
|
321
378
|
|
|
322
|
-
import { useCategories, useBasket,
|
|
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
|
|
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
|
-
|
|
385
|
+
// User
|
|
386
|
+
const { username, setUsername, clearUsername } = useUser();
|
|
378
387
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
import { useShopUserStore, useBasket } from '@neosia-core/super-tebex';
|
|
388
|
+
// Categories
|
|
389
|
+
const { categories, isLoading: categoriesLoading } = useCategories({
|
|
390
|
+
includePackages: true,
|
|
391
|
+
});
|
|
384
392
|
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
396
|
-
setTimeout(() => refetch(), 100);
|
|
414
|
+
setInput('');
|
|
397
415
|
}
|
|
398
416
|
};
|
|
399
417
|
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
503
|
+
```tsx
|
|
504
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
505
|
+
import { TebexProvider } from '@neosia/tebex-nextjs';
|
|
439
506
|
|
|
440
|
-
|
|
507
|
+
const queryClient = new QueryClient({
|
|
508
|
+
defaultOptions: {
|
|
509
|
+
queries: {
|
|
510
|
+
staleTime: 30 * 1000, // 30 seconds
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
});
|
|
441
514
|
|
|
442
|
-
|
|
443
|
-
|
|
515
|
+
<TebexProvider config={config} queryClient={queryClient}>
|
|
516
|
+
{children}
|
|
517
|
+
</TebexProvider>
|
|
444
518
|
```
|
|
445
519
|
|
|
446
|
-
|
|
520
|
+
### Direct Store Access
|
|
447
521
|
|
|
448
|
-
|
|
522
|
+
```tsx
|
|
523
|
+
import { useBasketStore, useUserStore } from '@neosia/tebex-nextjs';
|
|
449
524
|
|
|
450
|
-
|
|
451
|
-
const
|
|
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
|
-
|
|
530
|
+
### Query Keys
|
|
462
531
|
|
|
463
|
-
|
|
532
|
+
```tsx
|
|
533
|
+
import { tebexKeys } from '@neosia/tebex-nextjs';
|
|
534
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
464
535
|
|
|
465
|
-
|
|
536
|
+
const queryClient = useQueryClient();
|
|
466
537
|
|
|
467
|
-
|
|
538
|
+
// Invalidate specific queries
|
|
539
|
+
queryClient.invalidateQueries({ queryKey: tebexKeys.categories() });
|
|
540
|
+
queryClient.invalidateQueries({ queryKey: tebexKeys.basket(basketIdent) });
|
|
541
|
+
```
|
|
468
542
|
|
|
469
|
-
##
|
|
543
|
+
## License
|
|
470
544
|
|
|
471
|
-
|
|
545
|
+
MIT
|