@inwink-mcp/backoffice-extension-helpers 1.0.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.
Files changed (27) hide show
  1. package/README.md +96 -0
  2. package/dist/cli.js +500 -0
  3. package/package.json +36 -0
  4. package/templates/test-app/src/pages/HomePage.tsx.tpl +55 -0
  5. package/templates/vite-extension/AGENTS.md.tpl +132 -0
  6. package/templates/vite-extension/CLAUDE.md +5 -0
  7. package/templates/vite-extension/extension.manifest.json.tpl +18 -0
  8. package/templates/vite-extension/index.html.tpl +12 -0
  9. package/templates/vite-extension/package.json.tpl +29 -0
  10. package/templates/vite-extension/scripts/zip-build.mjs +36 -0
  11. package/templates/vite-extension/src/App.tsx.tpl +23 -0
  12. package/templates/vite-extension/src/api/endpoints.ts.tpl +5 -0
  13. package/templates/vite-extension/src/auth/AuthProvider.tsx +80 -0
  14. package/templates/vite-extension/src/auth/ProtectedRoute.tsx +12 -0
  15. package/templates/vite-extension/src/auth/config.ts +16 -0
  16. package/templates/vite-extension/src/hooks/useAuth.ts +12 -0
  17. package/templates/vite-extension/src/hooks/useInwinkApi.ts +101 -0
  18. package/templates/vite-extension/src/hooks/useInwinkCommunityApi.ts +7 -0
  19. package/templates/vite-extension/src/hooks/useInwinkCustomerApi.ts +7 -0
  20. package/templates/vite-extension/src/hooks/useInwinkEventApi.ts +7 -0
  21. package/templates/vite-extension/src/main.tsx +16 -0
  22. package/templates/vite-extension/src/pages/HomePage.tsx.tpl +26 -0
  23. package/templates/vite-extension/src/styles.css +32 -0
  24. package/templates/vite-extension/src/vite-env.d.ts +1 -0
  25. package/templates/vite-extension/tsconfig.json +20 -0
  26. package/templates/vite-extension/tsconfig.node.json +15 -0
  27. package/templates/vite-extension/vite.config.ts +30 -0
@@ -0,0 +1,132 @@
1
+ # __DISPLAY_NAME__ — Guide pour les agents IA
2
+
3
+ > Ce fichier documente l'architecture du projet et les contraintes à respecter pour tout agent IA (Claude Code, Copilot, etc.) qui contribue à cette extension inwink.
4
+
5
+ ## Objectif du projet
6
+
7
+ `__DISPLAY_NAME__` (`__NAME__`) est une **extension backoffice inwink** : __DESCRIPTION__
8
+
9
+ C'est une SPA React/TypeScript **100 % client-side** (zéro code serveur) qui s'authentifie via OIDC (Authorization Code + PKCE) et communique avec les API inwink en Bearer token.
10
+
11
+ ## Stack technique
12
+
13
+ - **React 19** + **TypeScript** (strict)
14
+ - **Vite** (build/bundling) — `base: './'` obligatoire
15
+ - **react-router-dom** avec **HashRouter** (routage dans le fragment `#`)
16
+ - **oidc-client-ts** pour l'authentification OIDC
17
+ - **react-icons** pour les icônes
18
+
19
+ ## Structure des dossiers
20
+
21
+ ```
22
+ __NAME__/
23
+ ├── extension.manifest.json # Identité + permissions de l'extension (obligatoire)
24
+ ├── index.html # Point d'entrée HTML
25
+ ├── vite.config.ts # Config Vite (base: './' + copie du manifest)
26
+ ├── scripts/zip-build.mjs # Packaging zip {name}-{version}.zip
27
+ └── src/
28
+ ├── main.tsx # Bootstrap React (HashRouter + AuthProvider)
29
+ ├── App.tsx # Routes
30
+ ├── styles.css # Styles
31
+ ├── pages/
32
+ │ └── HomePage.tsx # Page d'accueil (logique métier / UI)
33
+ ├── api/
34
+ │ └── endpoints.ts # URLs de base des API inwink (injectées au bootstrap)
35
+ ├── auth/
36
+ │ ├── config.ts # Configuration OIDC (UserManager)
37
+ │ ├── AuthProvider.tsx # Contexte d'auth (callback OIDC inline + renewToken)
38
+ │ └── ProtectedRoute.tsx
39
+ └── hooks/
40
+ ├── useAuth.ts # Hook d'accès au contexte auth
41
+ ├── useInwinkApi.ts # Hook générique d'appel API (Bearer + renouvellement 401)
42
+ ├── useInwinkEventApi.ts # Hook API Event
43
+ ├── useInwinkCommunityApi.ts # Hook API Community
44
+ └── useInwinkCustomerApi.ts # Hook API Customer
45
+ ```
46
+
47
+ ## Découverte des API inwink (obligatoire)
48
+
49
+ Avant d'écrire **tout** appel API, utilise les outils MCP inwink pour découvrir dynamiquement endpoints, méthodes, payloads et entités :
50
+
51
+ 1. `inwink_mcp_find_api_endpoint` avec une requête sémantique précise et le bon `targetApi` (`eventapi`, `communityapi` ou `customerapi`).
52
+ 2. `inwink_mcp_get_entity_description` pour comprendre les entités, champs, payloads, filtres et DTOs.
53
+ 3. Code ensuite l'appel via le hook API approprié, uniquement à partir du résultat de ces outils.
54
+
55
+ **Ne devine jamais une route ou un format de payload.** Le MCP est un outil de développement : l'extension n'appelle jamais le serveur MCP en runtime.
56
+
57
+ ## Appeler l'API inwink
58
+
59
+ **N'appelle jamais `fetch` directement.** Utilise les hooks dédiés (`useInwinkEventApi`, `useInwinkCommunityApi`, `useInwinkCustomerApi`). Ils encapsulent l'envoi du Bearer token, le renouvellement automatique sur 401 (`signinSilent` puis rejeu, sinon login interactif) et la gestion d'erreurs (`InwinkApiError`).
60
+
61
+ ```typescript
62
+ import { useInwinkEventApi } from './hooks/useInwinkEventApi';
63
+
64
+ function MonComposant() {
65
+ const eventApi = useInwinkEventApi();
66
+
67
+ async function charger() {
68
+ // GET — le chemin est relatif à la base de l'API Event (déjà configurée)
69
+ const data = await eventApi.get<MaReponse>('/v1/...');
70
+
71
+ // POST avec corps JSON
72
+ await eventApi.post('/v1/...', { champ: 'valeur' });
73
+ }
74
+ }
75
+ ```
76
+
77
+ Méthodes disponibles sur chaque hook : `request<T>(path, init?)`, `get<T>`, `post<T>(path, body?)`, `put<T>(path, body?)`, `del<T>`.
78
+
79
+ Pour appeler un **hôte externe déclaré** (hors API inwink), utilise le hook générique `useInwinkApi(baseUrl)`.
80
+
81
+ Bonnes pratiques :
82
+ - Toujours passer par un hook `useInwink*Api` — jamais de `fetch` direct.
83
+ - Le 401 est géré automatiquement (renouvellement puis reconnexion) ; ne pas le ré-implémenter.
84
+ - Gérer le 429 (rate limiting) avec un backoff exponentiel côté appelant si nécessaire.
85
+ - Ne déclarer dans `permissions.platform` que les scopes réellement utilisés.
86
+ - Déclarer les hôtes externes dans `permissions.externalHosts` (HTTPS uniquement).
87
+
88
+ ## Règles et contraintes
89
+
90
+ | Règle | Détail |
91
+ |-------|--------|
92
+ | Pas de code serveur | L'extension est 100 % client-side (SPA statique) |
93
+ | Pas d'`eval()` / `new Function()` | Interdit pour raisons de sécurité |
94
+ | Pas de tokens dans `localStorage` manuellement | Utiliser uniquement le `WebStorageStateStore` d'`oidc-client-ts` |
95
+ | Pas de tokens dans les URLs | Ne jamais passer de token en query string |
96
+ | Pas d'imports Node.js | `fs`, `path`, `http`, `child_process`, etc. interdits dans le code source |
97
+ | Pas de `fetch` direct | Toujours passer par `useInwinkEventApi` / `useInwinkCommunityApi` / `useInwinkCustomerApi` (ou `useInwinkApi` pour un hôte externe déclaré) |
98
+ | Icônes via `react-icons` | Ne pas embarquer de SVG inline ni d'images d'icônes |
99
+ | `HashRouter` obligatoire | Pour compatibilité avec l'hébergement versionné |
100
+ | `base: './'` obligatoire | Dans `vite.config.ts`, pour les chemins d'assets relatifs |
101
+ | `response_type: 'code'` | PKCE obligatoire, pas de flux implicit |
102
+ | HTTPS uniquement | Pour les `externalHosts` déclarés dans le manifest |
103
+ | `redirect_uri` = pathname courant | Toujours `${window.location.origin}${window.location.pathname}` |
104
+ | `post_logout_redirect_uri` | Ne pas l'envoyer par défaut ; uniquement si l'URL exacte est enregistrée côté IdP |
105
+ | Pas de `CallbackPage` séparée | Le callback est détecté inline dans `AuthProvider` via `?code=&state=` |
106
+ | `authority` en dur | `'https://auth.inwink.com/inwink'` — ne pas utiliser `import.meta.env` (non substitué par Vite) |
107
+
108
+ ## Commandes
109
+
110
+ ```powershell
111
+ npm install # Installer les dépendances
112
+ npm run dev # Serveur de développement Vite
113
+ npm run build # Vérification TypeScript + build dans dist/
114
+ npm run package # Build + zip {name}-{version}.zip
115
+ npm run preview # Prévisualiser le build
116
+ ```
117
+
118
+ Packaging et vérification via la CLI helper :
119
+
120
+ ```powershell
121
+ npx @inwink-mcp/backoffice-extension-helpers@latest package # build + zip + vérification de conformité
122
+ npx @inwink-mcp/backoffice-extension-helpers@latest verify # vérification de conformité seule
123
+ ```
124
+
125
+ ## Conventions de code
126
+
127
+ - TypeScript strict ; pas de `any` implicite.
128
+ - Composants fonctionnels React + hooks.
129
+ - L'auth passe **toujours** par `useAuth()` ; ne pas appeler `userManager` directement hors de `auth/`.
130
+ - Les appels API passent **toujours** par un hook `useInwink*Api` ; jamais de `fetch` direct dans les composants.
131
+ - Les icônes proviennent de **`react-icons`** (ex. `react-icons/fi`, `react-icons/md`) ; pas de SVG inline ni d'images d'icônes.
132
+ - Le squelette (auth, api, config, structure) est généré par la CLI helper — ne pas le régénérer à la main. Ajouter la logique métier dans `App.tsx` et de nouveaux composants/hooks.
@@ -0,0 +1,5 @@
1
+ # Instructions projet (Claude Code)
2
+
3
+ La documentation d'architecture, la stack, les règles et contraintes, et les conventions de ce projet vivent dans `AGENTS.md`. Claude Code ne lit pas `AGENTS.md` nativement, donc ce fichier l'importe ci-dessous.
4
+
5
+ @AGENTS.md
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "__NAME__",
3
+ "version": "0.1.0",
4
+ "displayName": "__DISPLAY_NAME__",
5
+ "description": "__DESCRIPTION__",
6
+ "author": "__AUTHOR__",
7
+ "permissions": {
8
+ "platform": [],
9
+ "externalHosts": []
10
+ },
11
+ "auth": {
12
+ "scopes": ["openid", "profile", "email"]
13
+ },
14
+ "entrypoint": "./dist/index.html",
15
+ "platform": {
16
+ "minVersion": "1.0.0"
17
+ }
18
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>__DISPLAY_NAME__</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "__NAME__",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc --noEmit && vite build",
9
+ "package": "npm run build && npm run zip",
10
+ "preview": "vite preview",
11
+ "zip": "node ./scripts/zip-build.mjs"
12
+ },
13
+ "dependencies": {
14
+ "oidc-client-ts": "^3.1.0",
15
+ "react": "^19.1.0",
16
+ "react-dom": "^19.1.0",
17
+ "react-icons": "^5.4.0",
18
+ "react-router-dom": "^7.5.1"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.14.1",
22
+ "adm-zip": "^0.5.16",
23
+ "@types/react": "^19.1.2",
24
+ "@types/react-dom": "^19.1.2",
25
+ "@vitejs/plugin-react": "^4.4.1",
26
+ "typescript": "^5.8.3",
27
+ "vite": "^6.3.5"
28
+ }
29
+ }
@@ -0,0 +1,36 @@
1
+ import AdmZip from 'adm-zip';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
7
+ const projectRoot = path.resolve(currentDir, '..');
8
+ const distDir = path.join(projectRoot, 'dist');
9
+ const manifestPath = path.join(projectRoot, 'extension.manifest.json');
10
+
11
+ if (!fs.existsSync(distDir)) {
12
+ throw new Error('dist directory not found. Run "npm run build" before "npm run zip".');
13
+ }
14
+
15
+ if (!fs.existsSync(manifestPath)) {
16
+ throw new Error('extension.manifest.json not found.');
17
+ }
18
+
19
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
20
+
21
+ if (typeof manifest.name !== 'string' || !manifest.name.trim()) {
22
+ throw new Error('extension.manifest.json must define a non-empty name.');
23
+ }
24
+
25
+ if (typeof manifest.version !== 'string' || !manifest.version.trim()) {
26
+ throw new Error('extension.manifest.json must define a non-empty version.');
27
+ }
28
+
29
+ const zipFileName = `${manifest.name}-${manifest.version}.zip`;
30
+ const zipFilePath = path.join(projectRoot, zipFileName);
31
+ const zip = new AdmZip();
32
+
33
+ zip.addLocalFolder(distDir);
34
+ zip.writeZip(zipFilePath);
35
+
36
+ process.stdout.write(`${zipFileName}\n`);
@@ -0,0 +1,23 @@
1
+ import { Navigate, Route, Routes } from 'react-router-dom';
2
+ import ProtectedRoute from './auth/ProtectedRoute';
3
+ import HomePage from './pages/HomePage';
4
+ // Pour appeler une API inwink, utilisez un hook dédié (jamais fetch directement) :
5
+ // import { useInwinkEventApi } from './hooks/useInwinkEventApi';
6
+ // const eventApi = useInwinkEventApi();
7
+ // const data = await eventApi.get('/v1/...');
8
+
9
+ export default function App() {
10
+ return (
11
+ <Routes>
12
+ <Route
13
+ path="/"
14
+ element={
15
+ <ProtectedRoute>
16
+ <HomePage />
17
+ </ProtectedRoute>
18
+ }
19
+ />
20
+ <Route path="*" element={<Navigate to="/" replace />} />
21
+ </Routes>
22
+ );
23
+ }
@@ -0,0 +1,5 @@
1
+ // URLs de base des API inwink, injectées au bootstrap de l'extension.
2
+ // Ne pas modifier à la main : régénérées par la CLI selon les flags --*-api-url.
3
+ export const EVENT_API_URL = '__EVENT_API_URL__';
4
+ export const COMMUNITY_API_URL = '__COMMUNITY_API_URL__';
5
+ export const CUSTOMER_API_URL = '__CUSTOMER_API_URL__';
@@ -0,0 +1,80 @@
1
+ // src/auth/AuthProvider.tsx
2
+ import { createContext, useEffect, useState, type PropsWithChildren } from 'react';
3
+ import type { User } from 'oidc-client-ts';
4
+ import { userManager } from './config';
5
+
6
+ type AuthContextValue = {
7
+ isAuthenticated: boolean;
8
+ isLoading: boolean;
9
+ login: () => Promise<void>;
10
+ logout: () => Promise<void>;
11
+ user: User | null;
12
+ getAccessToken: () => string | null;
13
+ /** Renouvelle le token silencieusement. Retourne le nouvel access_token, ou null si échec. */
14
+ renewToken: () => Promise<string | null>;
15
+ };
16
+
17
+ export const AuthContext = createContext<AuthContextValue | null>(null);
18
+
19
+ function isOidcCallback(): boolean {
20
+ const params = new URLSearchParams(window.location.search);
21
+ return params.has('code') && params.has('state');
22
+ }
23
+
24
+ export function AuthProvider({ children }: PropsWithChildren) {
25
+ const [user, setUser] = useState<User | null>(null);
26
+ const [isLoading, setIsLoading] = useState(true);
27
+
28
+ useEffect(() => {
29
+ let isMounted = true;
30
+
31
+ async function init() {
32
+ try {
33
+ if (isOidcCallback()) {
34
+ const callbackUser = await userManager.signinCallback(window.location.href);
35
+ window.history.replaceState({}, '', window.location.pathname);
36
+ if (isMounted) { setUser(callbackUser ?? null); setIsLoading(false); }
37
+ return;
38
+ }
39
+ const loadedUser = await userManager.getUser();
40
+ if (isMounted) { setUser(loadedUser); setIsLoading(false); }
41
+ } catch {
42
+ if (isMounted) { setUser(null); setIsLoading(false); }
43
+ }
44
+ }
45
+
46
+ void init();
47
+
48
+ const handleUserLoaded = (u: User) => { setUser(u); setIsLoading(false); };
49
+ const handleUserUnloaded = () => { setUser(null); setIsLoading(false); };
50
+ userManager.events.addUserLoaded(handleUserLoaded);
51
+ userManager.events.addUserUnloaded(handleUserUnloaded);
52
+ return () => {
53
+ isMounted = false;
54
+ userManager.events.removeUserLoaded(handleUserLoaded);
55
+ userManager.events.removeUserUnloaded(handleUserUnloaded);
56
+ };
57
+ }, []);
58
+
59
+ return (
60
+ <AuthContext.Provider value={{
61
+ isAuthenticated: Boolean(user && !user.expired),
62
+ isLoading,
63
+ login: async () => { setIsLoading(true); await userManager.signinRedirect(); },
64
+ logout: async () => { await userManager.removeUser(); setUser(null); },
65
+ user,
66
+ getAccessToken: () => user?.access_token ?? null,
67
+ renewToken: async () => {
68
+ try {
69
+ const renewed = await userManager.signinSilent();
70
+ setUser(renewed);
71
+ return renewed?.access_token ?? null;
72
+ } catch {
73
+ return null;
74
+ }
75
+ },
76
+ }}>
77
+ {children}
78
+ </AuthContext.Provider>
79
+ );
80
+ }
@@ -0,0 +1,12 @@
1
+ import type { PropsWithChildren } from 'react';
2
+ import { useAuth } from '../hooks/useAuth';
3
+
4
+ export default function ProtectedRoute({ children }: PropsWithChildren) {
5
+ const { isLoading } = useAuth();
6
+
7
+ if (isLoading) {
8
+ return <p>Chargement...</p>;
9
+ }
10
+
11
+ return <>{children}</>;
12
+ }
@@ -0,0 +1,16 @@
1
+ // src/auth/config.ts
2
+ import { UserManager, WebStorageStateStore } from 'oidc-client-ts';
3
+
4
+ const redirectUri = `${window.location.origin}${window.location.pathname}`;
5
+
6
+ export const userManager = new UserManager({
7
+ authority: 'https://auth.inwink.com/inwink',
8
+ client_id: 'inwink-extensions',
9
+ redirect_uri: redirectUri,
10
+ response_type: 'code',
11
+ scope: 'openid profile email',
12
+ loadUserInfo: true,
13
+ monitorSession: false,
14
+ automaticSilentRenew: false,
15
+ userStore: new WebStorageStateStore({ store: localStorage }),
16
+ });
@@ -0,0 +1,12 @@
1
+ import { useContext } from 'react';
2
+ import { AuthContext } from '../auth/AuthProvider';
3
+
4
+ export function useAuth() {
5
+ const context = useContext(AuthContext);
6
+
7
+ if (!context) {
8
+ throw new Error('useAuth must be used within AuthProvider.');
9
+ }
10
+
11
+ return context;
12
+ }
@@ -0,0 +1,101 @@
1
+ import { useAuth } from './useAuth';
2
+
3
+ /** Erreur levée lorsqu'une réponse API n'est pas OK (hors 401 géré par renouvellement). */
4
+ export class InwinkApiError extends Error {
5
+ constructor(
6
+ public readonly status: number,
7
+ public readonly body: unknown,
8
+ ) {
9
+ super(`Inwink API error ${status}`);
10
+ this.name = 'InwinkApiError';
11
+ }
12
+ }
13
+
14
+ export interface InwinkApi {
15
+ request: <T = unknown>(path: string, init?: RequestInit) => Promise<T>;
16
+ get: <T = unknown>(path: string, init?: RequestInit) => Promise<T>;
17
+ post: <T = unknown>(path: string, body?: unknown, init?: RequestInit) => Promise<T>;
18
+ put: <T = unknown>(path: string, body?: unknown, init?: RequestInit) => Promise<T>;
19
+ del: <T = unknown>(path: string, init?: RequestInit) => Promise<T>;
20
+ }
21
+
22
+ async function safeBody(response: Response): Promise<unknown> {
23
+ try {
24
+ return await response.json();
25
+ } catch {
26
+ return undefined;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Hook générique d'appel aux API inwink. Encapsule l'envoi du Bearer token,
32
+ * le renouvellement automatique sur 401 (signinSilent puis rejeu), et le
33
+ * fallback sur login interactif si le renouvellement échoue.
34
+ *
35
+ * Préférez les hooks dédiés (`useInwinkEventApi`, etc.) ; ce hook générique
36
+ * sert aux hôtes externes déclarés ou à des bases d'URL spécifiques.
37
+ */
38
+ export function useInwinkApi(baseUrl: string): InwinkApi {
39
+ const { getAccessToken, renewToken, login } = useAuth();
40
+
41
+ async function doFetch(path: string, token: string, init?: RequestInit): Promise<Response> {
42
+ return fetch(`${baseUrl}${path}`, {
43
+ ...init,
44
+ headers: {
45
+ Authorization: `Bearer ${token}`,
46
+ 'Content-Type': 'application/json',
47
+ ...(init?.headers as Record<string, string> | undefined),
48
+ },
49
+ });
50
+ }
51
+
52
+ async function request<T = unknown>(path: string, init?: RequestInit): Promise<T> {
53
+ let token = getAccessToken();
54
+ if (!token) {
55
+ await login();
56
+ throw new InwinkApiError(401, 'Non authentifié.');
57
+ }
58
+
59
+ let response = await doFetch(path, token, init);
60
+
61
+ if (response.status === 401) {
62
+ const renewed = await renewToken();
63
+ if (renewed) {
64
+ token = renewed;
65
+ response = await doFetch(path, token, init);
66
+ }
67
+ if (response.status === 401) {
68
+ await login();
69
+ throw new InwinkApiError(401, 'Session expirée, reconnexion requise.');
70
+ }
71
+ }
72
+
73
+ if (!response.ok) {
74
+ throw new InwinkApiError(response.status, await safeBody(response));
75
+ }
76
+
77
+ if (response.status === 204) {
78
+ return undefined as T;
79
+ }
80
+ return (await response.json()) as T;
81
+ }
82
+
83
+ const withBody =
84
+ (method: string) =>
85
+ <T = unknown>(path: string, body?: unknown, init?: RequestInit): Promise<T> =>
86
+ request<T>(path, {
87
+ ...init,
88
+ method,
89
+ body: body === undefined ? init?.body : JSON.stringify(body),
90
+ });
91
+
92
+ return {
93
+ request,
94
+ get: <T = unknown>(path: string, init?: RequestInit) =>
95
+ request<T>(path, { ...init, method: 'GET' }),
96
+ post: withBody('POST'),
97
+ put: withBody('PUT'),
98
+ del: <T = unknown>(path: string, init?: RequestInit) =>
99
+ request<T>(path, { ...init, method: 'DELETE' }),
100
+ };
101
+ }
@@ -0,0 +1,7 @@
1
+ import { COMMUNITY_API_URL } from '../api/endpoints';
2
+ import { useInwinkApi, type InwinkApi } from './useInwinkApi';
3
+
4
+ /** Accès à l'API Community inwink (Bearer token + renouvellement 401 gérés). */
5
+ export function useInwinkCommunityApi(): InwinkApi {
6
+ return useInwinkApi(COMMUNITY_API_URL);
7
+ }
@@ -0,0 +1,7 @@
1
+ import { CUSTOMER_API_URL } from '../api/endpoints';
2
+ import { useInwinkApi, type InwinkApi } from './useInwinkApi';
3
+
4
+ /** Accès à l'API Customer inwink (Bearer token + renouvellement 401 gérés). */
5
+ export function useInwinkCustomerApi(): InwinkApi {
6
+ return useInwinkApi(CUSTOMER_API_URL);
7
+ }
@@ -0,0 +1,7 @@
1
+ import { EVENT_API_URL } from '../api/endpoints';
2
+ import { useInwinkApi, type InwinkApi } from './useInwinkApi';
3
+
4
+ /** Accès à l'API Event inwink (Bearer token + renouvellement 401 gérés). */
5
+ export function useInwinkEventApi(): InwinkApi {
6
+ return useInwinkApi(EVENT_API_URL);
7
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import { HashRouter } from 'react-router-dom';
4
+ import App from './App';
5
+ import { AuthProvider } from './auth/AuthProvider';
6
+ import './styles.css';
7
+
8
+ ReactDOM.createRoot(document.getElementById('root')!).render(
9
+ <React.StrictMode>
10
+ <HashRouter>
11
+ <AuthProvider>
12
+ <App />
13
+ </AuthProvider>
14
+ </HashRouter>
15
+ </React.StrictMode>,
16
+ );
@@ -0,0 +1,26 @@
1
+ import { FiLogIn, FiLogOut } from 'react-icons/fi';
2
+ import { useAuth } from '../hooks/useAuth';
3
+
4
+ export default function HomePage() {
5
+ const { isAuthenticated, isLoading, login, logout, user } = useAuth();
6
+
7
+ return (
8
+ <main>
9
+ <h1>__DISPLAY_NAME__</h1>
10
+ {!isAuthenticated ? (
11
+ <button disabled={isLoading} onClick={() => void login()}>
12
+ <FiLogIn aria-hidden />
13
+ {isLoading ? 'Initialisation...' : 'Se connecter'}
14
+ </button>
15
+ ) : (
16
+ <>
17
+ <p>Connecté en tant que : {user?.profile.email ?? 'Inconnu'}</p>
18
+ <button onClick={() => void logout()}>
19
+ <FiLogOut aria-hidden />
20
+ Se déconnecter
21
+ </button>
22
+ </>
23
+ )}
24
+ </main>
25
+ );
26
+ }
@@ -0,0 +1,32 @@
1
+ :root {
2
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
3
+ line-height: 1.5;
4
+ color-scheme: light dark;
5
+ }
6
+
7
+ body {
8
+ margin: 0;
9
+ padding: 2rem;
10
+ }
11
+
12
+ main {
13
+ max-width: 720px;
14
+ margin: 0 auto;
15
+ }
16
+
17
+ button {
18
+ cursor: pointer;
19
+ display: inline-flex;
20
+ align-items: center;
21
+ gap: 0.5rem;
22
+ padding: 0.5rem 1rem;
23
+ border-radius: 6px;
24
+ border: 1px solid currentColor;
25
+ background: transparent;
26
+ font: inherit;
27
+ }
28
+
29
+ button:disabled {
30
+ cursor: default;
31
+ opacity: 0.6;
32
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "allowJs": false,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": true,
9
+ "allowSyntheticDefaultImports": true,
10
+ "strict": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "module": "ESNext",
13
+ "moduleResolution": "Bundler",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "noEmit": true,
17
+ "jsx": "react-jsx"
18
+ },
19
+ "include": ["src"]
20
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "strict": true,
5
+ "forceConsistentCasingInFileNames": true,
6
+ "target": "ES2022",
7
+ "lib": ["ES2022"],
8
+ "skipLibCheck": true,
9
+ "module": "ESNext",
10
+ "moduleResolution": "Bundler",
11
+ "allowSyntheticDefaultImports": true,
12
+ "types": ["node"]
13
+ },
14
+ "include": ["vite.config.ts"]
15
+ }
@@ -0,0 +1,30 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { defineConfig } from 'vite';
5
+ import react from '@vitejs/plugin-react';
6
+
7
+ const projectRoot = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ export default defineConfig({
10
+ base: './',
11
+ plugins: [
12
+ react(),
13
+ {
14
+ name: 'emit-extension-manifest',
15
+ generateBundle() {
16
+ const manifestPath = path.resolve(projectRoot, 'extension.manifest.json');
17
+ const source = fs.readFileSync(manifestPath, 'utf8');
18
+
19
+ this.emitFile({
20
+ fileName: 'extension.manifest.json',
21
+ source,
22
+ type: 'asset',
23
+ });
24
+ },
25
+ },
26
+ ],
27
+ build: {
28
+ outDir: 'dist',
29
+ },
30
+ });