@hexdspace/react 0.0.11 → 0.0.13

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/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
2
  import { QueryClient, QueryKey } from '@tanstack/react-query';
3
- import { ResultOk, ResultError } from '@hexdspace/util';
4
- import React, { CSSProperties } from 'react';
3
+ import { ResultOk, ResultError, AsyncResult, Result } from '@hexdspace/util';
4
+ import * as react from 'react';
5
+ import react__default, { CSSProperties, Dispatch, ReactNode } from 'react';
6
+ import * as react_jsx_runtime from 'react/jsx-runtime';
5
7
 
6
8
  type NotificationVariant = 'success' | 'warning' | 'error' | 'info';
7
9
  declare const DEFAULT_NOTIFICATION_CHANNEL = "app.notifications";
@@ -118,7 +120,7 @@ type NotificationHostProps = {
118
120
  isDark?: () => boolean;
119
121
  theme?: ToastTheme;
120
122
  };
121
- declare const NotificationHost: React.FC<NotificationHostProps>;
123
+ declare const NotificationHost: react__default.FC<NotificationHostProps>;
122
124
 
123
125
  type InstructionContext = {
124
126
  queryClient: QueryClient;
@@ -163,4 +165,214 @@ type ResponsiveMutation<Args, Res> = {
163
165
 
164
166
  declare function useResponsiveMutation<Args, Res>(responsiveMutation: ResponsiveMutation<Args, Res>, queryClient: QueryClient): _tanstack_react_query.UseMutationResult<UIOk<Res>, UIFail, Args, OptimisticSnapshot | undefined>;
165
167
 
166
- export { type CacheInstruction, type CustomInstruction, DEFAULT_NOTIFICATION_CHANNEL, type Instruction, type InstructionContext, type Notification, type NotificationAction, NotificationHost, type NotificationInstruction, type NotificationVariant, NotifierController, type OptimisticSnapshot, type ResolvedToastTheme, type ResponsiveMutation, type ToastActionTheme, type ToastTheme, type ToastTransition, type ToastifyCSSVars, type UIFail, type UIOk, type UIResult, notifierController, resolveToastTheme, ui, useResponsiveMutation };
168
+ type FactoryBuilder<TDeps, TController> = (deps: TDeps) => TController;
169
+ type FactoryDefaults<TDeps> = (overrides: Partial<TDeps>) => TDeps;
170
+ declare function controllerFactory<TDeps, TController>(builder: FactoryBuilder<TDeps, TController>, defaults: FactoryDefaults<TDeps>): (overrides?: Partial<TDeps>) => TController;
171
+
172
+ interface GenericResponse {
173
+ message: string;
174
+ }
175
+
176
+ interface ErrorResponse {
177
+ error: string;
178
+ details?: unknown;
179
+ }
180
+
181
+ interface HttpResponse<T = unknown> {
182
+ data: T;
183
+ status: number;
184
+ headers: Record<string, string>;
185
+ }
186
+
187
+ declare class HttpError extends Error {
188
+ readonly status: number;
189
+ readonly response: ErrorResponse;
190
+ constructor(message: string, status: number, response: ErrorResponse);
191
+ }
192
+
193
+ type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
194
+ interface HttpClient {
195
+ get<T = unknown>(url: string, config?: RequestConfig): Promise<HttpResponse<T>>;
196
+ post<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<HttpResponse<T>>;
197
+ put<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<HttpResponse<T>>;
198
+ delete<T = unknown>(url: string, config?: RequestConfig): Promise<HttpResponse<T>>;
199
+ }
200
+ interface RequestConfig {
201
+ headers?: Record<string, string>;
202
+ timeout?: number;
203
+ }
204
+
205
+ declare class AutoRefreshDecorator implements HttpClient {
206
+ private readonly httpClient;
207
+ private readonly refresh;
208
+ private static inFlightRefresh;
209
+ constructor(httpClient: HttpClient, refresh: (client: HttpClient) => AsyncResult<HttpError, GenericResponse>);
210
+ get<T = unknown>(url: string, config?: RequestConfig): Promise<HttpResponse<T>>;
211
+ post<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<HttpResponse<T>>;
212
+ put<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<HttpResponse<T>>;
213
+ delete<T = unknown>(url: string, config?: RequestConfig): Promise<HttpResponse<T>>;
214
+ private tryRequest;
215
+ private needsRefresh;
216
+ private performRefresh;
217
+ }
218
+
219
+ declare const httpClient: AutoRefreshDecorator;
220
+
221
+ type MockHandler<T = unknown> = (payload: {
222
+ url: string;
223
+ data?: unknown;
224
+ config?: RequestConfig;
225
+ }) => Promise<HttpResponse<T>> | HttpResponse<T>;
226
+ declare class MockHttpClient implements HttpClient {
227
+ private handlers;
228
+ register<T>(method: HttpMethod, url: string, handler: MockHandler<T>): void;
229
+ respondWith<T>(method: HttpMethod, url: string, response: HttpResponse<T>): void;
230
+ respondWithError(method: HttpMethod, url: string, error: Error): void;
231
+ reset(): void;
232
+ get<T = unknown>(url: string, config?: RequestConfig): Promise<HttpResponse<T>>;
233
+ post<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<HttpResponse<T>>;
234
+ put<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<HttpResponse<T>>;
235
+ delete<T = unknown>(url: string, config?: RequestConfig): Promise<HttpResponse<T>>;
236
+ private invoke;
237
+ }
238
+
239
+ interface User {
240
+ id: string;
241
+ email: string;
242
+ }
243
+
244
+ interface LoginUser {
245
+ execute(email: string, password: string): AsyncResult<Error, User>;
246
+ }
247
+
248
+ interface LogoutUser {
249
+ execute(): AsyncResult<Error, null>;
250
+ }
251
+
252
+ interface RegisterUser {
253
+ execute(email: string, password: string): AsyncResult<Error, GenericResponse>;
254
+ }
255
+
256
+ interface GetAuthenticatedUser {
257
+ execute(): AsyncResult<Error, User | null>;
258
+ }
259
+
260
+ interface AuthProvider$1 {
261
+ getAuthenticatedUser(): AsyncResult<Error, User | null>;
262
+ login(email: string, password: string): AsyncResult<Error, User>;
263
+ register(email: string, password: string): AsyncResult<Error, GenericResponse>;
264
+ logout(): AsyncResult<Error, null>;
265
+ }
266
+
267
+ declare class AuthController {
268
+ private readonly loginUser;
269
+ private readonly logoutUser;
270
+ private readonly registerUser;
271
+ private readonly getAuthenticatedUser;
272
+ constructor(loginUser: LoginUser, logoutUser: LogoutUser, registerUser: RegisterUser, getAuthenticatedUser: GetAuthenticatedUser);
273
+ login(email: string, password: string): AsyncResult<Error, User>;
274
+ logout(): AsyncResult<Error, null>;
275
+ register(email: string, password: string): AsyncResult<Error, {
276
+ message: string;
277
+ }>;
278
+ getCurrentUser(): AsyncResult<Error, User | null>;
279
+ }
280
+ type AuthControllerDeps = {
281
+ httpClient: HttpClient;
282
+ authProvider: AuthProvider$1;
283
+ loginUser: LoginUser;
284
+ logoutUser: LogoutUser;
285
+ registerUser: RegisterUser;
286
+ getAuthenticatedUser: GetAuthenticatedUser;
287
+ };
288
+ declare const createAuthController: (overrides?: Partial<AuthControllerDeps>) => AuthController;
289
+ declare const authController: AuthController;
290
+
291
+ type AuthState = {
292
+ status: 'loading';
293
+ } | {
294
+ status: 'authenticated';
295
+ user: User;
296
+ } | {
297
+ status: 'unauthenticated';
298
+ message?: string;
299
+ } | {
300
+ status: 'error';
301
+ error: string;
302
+ };
303
+
304
+ declare function useAuth(): AuthState;
305
+
306
+ declare function useAuthActions(): {
307
+ login: (email: string, password: string) => void;
308
+ logout: () => void;
309
+ register: (email: string, password: string) => Promise<Result<Error, {
310
+ message: string;
311
+ }>>;
312
+ };
313
+
314
+ declare function useAuthedUser(): User;
315
+
316
+ declare function useAuthController(): AuthController;
317
+
318
+ type AuthAction = {
319
+ type: 'REQUEST';
320
+ } | {
321
+ type: 'COMPLETE';
322
+ user: User;
323
+ } | {
324
+ type: 'SUCCESS';
325
+ message: string;
326
+ } | {
327
+ type: 'FAILED';
328
+ error: string;
329
+ } | {
330
+ type: 'LOGOUT';
331
+ };
332
+
333
+ declare function useAuthDispatch(): Dispatch<AuthAction>;
334
+
335
+ declare const AuthStateCtx: react.Context<AuthState | null>;
336
+ declare const AuthDispatchCtx: react.Context<Dispatch<AuthAction> | null>;
337
+ declare function AuthProvider({ children }: {
338
+ readonly children: ReactNode;
339
+ }): react_jsx_runtime.JSX.Element;
340
+
341
+ declare const AuthControllerCtx: react.Context<AuthController | null>;
342
+ type AuthControllerProviderProps = {
343
+ readonly children: ReactNode;
344
+ readonly controller: AuthController;
345
+ };
346
+ declare function AuthControllerProvider({ children, controller }: AuthControllerProviderProps): react_jsx_runtime.JSX.Element;
347
+
348
+ interface InputProps {
349
+ id: string;
350
+ label: string;
351
+ type: string;
352
+ value: string;
353
+ onChange: (e: react__default.ChangeEvent<HTMLInputElement>) => void;
354
+ placeholder: string;
355
+ className?: string;
356
+ }
357
+ declare const AuthFormInputField: react__default.FC<InputProps>;
358
+
359
+ type AuthFixtures = {
360
+ meError?: HttpError;
361
+ loginError?: HttpError;
362
+ registerError?: HttpError;
363
+ logoutError?: Error;
364
+ currentUser?: User | null;
365
+ };
366
+ declare class MockAuthHttpClient extends MockHttpClient {
367
+ private fixtures;
368
+ private readonly delayMs;
369
+ constructor(initial?: AuthFixtures, delayMs?: number);
370
+ setCurrentUser(user: User | null): void;
371
+ setLoginError(error: HttpError | undefined): void;
372
+ setRegisterError(error: HttpError | undefined): void;
373
+ setMeError(error: HttpError | undefined): void;
374
+ setLogoutError(error: Error | undefined): void;
375
+ private registerAuthRoutes;
376
+ }
377
+
378
+ export { AuthController, AuthControllerCtx, type AuthControllerDeps, AuthControllerProvider, AuthDispatchCtx, AuthFormInputField, AuthProvider, type AuthState, AuthStateCtx, type CacheInstruction, type CustomInstruction, DEFAULT_NOTIFICATION_CHANNEL, type ErrorResponse, type GenericResponse, HttpError, type HttpResponse, type Instruction, type InstructionContext, MockAuthHttpClient, MockHttpClient, type Notification, type NotificationAction, NotificationHost, type NotificationInstruction, type NotificationVariant, NotifierController, type OptimisticSnapshot, type ResolvedToastTheme, type ResponsiveMutation, type ToastActionTheme, type ToastTheme, type ToastTransition, type ToastifyCSSVars, type UIFail, type UIOk, type UIResult, type User, authController, controllerFactory, createAuthController, httpClient as fetchHttpClient, notifierController, resolveToastTheme, ui, useAuth, useAuthActions, useAuthController, useAuthDispatch, useAuthedUser, useResponsiveMutation };
package/dist/index.js CHANGED
@@ -103,52 +103,11 @@ var notifierController = new NotifierController(sendNotification, subscribe);
103
103
 
104
104
  // src/feature/notifier/infra/web/react/NotificationHost.tsx
105
105
  import { useCallback, useEffect, useMemo as useMemo2 } from "react";
106
- import { toast as toast2, ToastContainer } from "react-toastify";
106
+ import { cssTransition, Slide, toast as toast2, ToastContainer } from "react-toastify";
107
107
 
108
108
  // src/feature/notifier/entity/notification.ts
109
109
  var DEFAULT_NOTIFICATION_CHANNEL = "app.notifications";
110
110
 
111
- // src/feature/notifier/infra/web/react/CustomToastTransition.tsx
112
- import { cssTransition } from "react-toastify";
113
- var CUSTOM_TRANSITION_STYLE_ID = "hexd-toast-transition-styles";
114
- var CUSTOM_TRANSITION_STYLES = `
115
- @keyframes slideIn {
116
- from { transform: translateY(-20px); opacity: 0; }
117
- to { transform: translateY(0); opacity: 1; }
118
- }
119
-
120
- @keyframes slideOut {
121
- from { transform: translateY(0); opacity: 1; }
122
- to { transform: translateY(-20px); opacity: 0; }
123
- }
124
-
125
- .slideIn {
126
- animation: slideIn 0.35s forwards;
127
- }
128
-
129
- .slideOut {
130
- animation: slideOut 0.25s forwards;
131
- }
132
- `;
133
- var ensureCustomToastTransitionStyles = () => {
134
- if (typeof document === "undefined") return;
135
- if (document.getElementById(CUSTOM_TRANSITION_STYLE_ID)) return;
136
- const style = document.createElement("style");
137
- style.id = CUSTOM_TRANSITION_STYLE_ID;
138
- style.textContent = CUSTOM_TRANSITION_STYLES;
139
- document.head.appendChild(style);
140
- };
141
- var SlideUp = cssTransition({
142
- enter: "slideIn",
143
- exit: "slideOut",
144
- collapseDuration: 300
145
- });
146
- var buildToastTransition = (transition) => {
147
- ensureCustomToastTransitionStyles();
148
- if (!transition) return SlideUp;
149
- return cssTransition(transition);
150
- };
151
-
152
111
  // src/feature/notifier/infra/web/react/ToastContent.tsx
153
112
  import { useMemo, useState } from "react";
154
113
  import { toast } from "react-toastify";
@@ -327,12 +286,7 @@ var DEFAULT_THEME_BASE = {
327
286
  width: "min(24rem, calc(100vw - 2rem))",
328
287
  contentPadding: "0.5rem 0.75rem 1rem 0.5rem",
329
288
  bodyColumnGap: "0.75rem",
330
- bodyRowGap: "0.25rem",
331
- transition: {
332
- enter: "slideIn",
333
- exit: "slideOut",
334
- collapseDuration: 300
335
- }
289
+ bodyRowGap: "0.25rem"
336
290
  };
337
291
  function resolveToastTheme(theme) {
338
292
  return {
@@ -350,7 +304,7 @@ function resolveToastTheme(theme) {
350
304
  contentPadding: theme?.contentPadding ?? DEFAULT_THEME_BASE.contentPadding,
351
305
  bodyColumnGap: theme?.bodyColumnGap ?? DEFAULT_THEME_BASE.bodyColumnGap,
352
306
  bodyRowGap: theme?.bodyRowGap ?? DEFAULT_THEME_BASE.bodyRowGap,
353
- transition: theme?.transition ?? DEFAULT_THEME_BASE.transition,
307
+ transition: theme?.transition,
354
308
  action: {
355
309
  ...DEFAULT_ACTION_THEME,
356
310
  ...theme?.action ?? {}
@@ -372,6 +326,10 @@ var NotificationHost = ({ channel = DEFAULT_NOTIFICATION_CHANNEL, isDark, theme
372
326
  }
373
327
  return isDark() ? "dark" : "light";
374
328
  }, [isDark]);
329
+ const transition = useMemo2(() => {
330
+ const config = resolvedTheme.transition;
331
+ return config ? cssTransition(config) : Slide;
332
+ }, [resolvedTheme.transition]);
375
333
  const style = useMemo2(() => ({
376
334
  "--toastify-color-light": resolvedTheme.lightBg,
377
335
  "--toastify-text-color-light": resolvedTheme.lightText,
@@ -386,12 +344,13 @@ var NotificationHost = ({ channel = DEFAULT_NOTIFICATION_CHANNEL, isDark, theme
386
344
  "--toastify-toast-width": resolvedTheme.width
387
345
  }), [resolvedTheme]);
388
346
  const renderToast = useCallback((notification) => {
347
+ const variant = notification.variant ?? "info";
389
348
  toast2(/* @__PURE__ */ jsx2(ToastContent, { notification, theme: resolvedTheme, mode }), {
390
349
  toastId: notification.id,
391
- icon: false
350
+ icon: false,
351
+ type: variant
392
352
  });
393
353
  }, [mode, resolvedTheme]);
394
- const transition = useMemo2(() => buildToastTransition(resolvedTheme.transition), [resolvedTheme.transition]);
395
354
  useEffect(() => {
396
355
  let unsub;
397
356
  let disposed = false;
@@ -467,12 +426,597 @@ var executeEffects = (effects, ctx) => {
467
426
  }
468
427
  });
469
428
  };
429
+
430
+ // src/util/controller-factory.ts
431
+ function controllerFactory(builder, defaults) {
432
+ return (overrides = {}) => {
433
+ const resolvedDeps = defaults(overrides);
434
+ return builder({ ...resolvedDeps, ...overrides });
435
+ };
436
+ }
437
+
438
+ // src/feature/http/entity/http-error.ts
439
+ var HttpError = class extends Error {
440
+ constructor(message, status, response) {
441
+ super(message);
442
+ this.status = status;
443
+ this.response = response;
444
+ this.name = "HttpError";
445
+ }
446
+ };
447
+
448
+ // src/feature/http/infra/fetch-http-client.ts
449
+ import { nok, ok } from "@hexdspace/util";
450
+
451
+ // src/feature/http/infra/auto-refresh-decorator.ts
452
+ var AutoRefreshDecorator = class _AutoRefreshDecorator {
453
+ constructor(httpClient2, refresh2) {
454
+ this.httpClient = httpClient2;
455
+ this.refresh = refresh2;
456
+ }
457
+ static inFlightRefresh = null;
458
+ async get(url, config) {
459
+ return this.tryRequest(() => this.httpClient.get(url, config));
460
+ }
461
+ async post(url, data, config) {
462
+ return this.tryRequest(() => this.httpClient.post(url, data, config));
463
+ }
464
+ async put(url, data, config) {
465
+ return this.tryRequest(() => this.httpClient.put(url, data, config));
466
+ }
467
+ async delete(url, config) {
468
+ return this.tryRequest(() => this.httpClient.delete(url, config));
469
+ }
470
+ async tryRequest(fn) {
471
+ try {
472
+ return await fn();
473
+ } catch (e) {
474
+ if (e instanceof HttpError && this.needsRefresh(e.status)) {
475
+ await this.performRefresh();
476
+ return fn();
477
+ }
478
+ throw e;
479
+ }
480
+ }
481
+ needsRefresh(status) {
482
+ return [401, 403, 419, 440].includes(status);
483
+ }
484
+ async performRefresh() {
485
+ if (_AutoRefreshDecorator.inFlightRefresh) {
486
+ return _AutoRefreshDecorator.inFlightRefresh;
487
+ }
488
+ _AutoRefreshDecorator.inFlightRefresh = (async () => {
489
+ const res = await this.refresh(this.httpClient);
490
+ if (!res.ok) {
491
+ throw new HttpError("Token refresh failed", res.error.status, res.error.response);
492
+ }
493
+ })().finally(() => {
494
+ _AutoRefreshDecorator.inFlightRefresh = null;
495
+ });
496
+ return _AutoRefreshDecorator.inFlightRefresh;
497
+ }
498
+ };
499
+
500
+ // src/feature/http/infra/fetch-http-client.ts
501
+ var FetchHttpClient = class {
502
+ baseURL;
503
+ constructor(baseURL = "/api") {
504
+ this.baseURL = baseURL;
505
+ }
506
+ async get(url, config) {
507
+ return this.doRequest("GET", url, void 0, config);
508
+ }
509
+ async post(url, data, config) {
510
+ return this.doRequest("POST", url, data, config);
511
+ }
512
+ async put(url, data, config) {
513
+ return this.doRequest("PUT", url, data, config);
514
+ }
515
+ async delete(url, config) {
516
+ return this.doRequest("DELETE", url, void 0, config);
517
+ }
518
+ async doRequest(method, url, body, config) {
519
+ const isFormData = body instanceof FormData;
520
+ const headers = { ...config?.headers };
521
+ let parsedBody;
522
+ if (body !== void 0) {
523
+ parsedBody = isFormData ? body : JSON.stringify(body);
524
+ }
525
+ if (!isFormData) {
526
+ headers["Content-Type"] ??= "application/json";
527
+ }
528
+ try {
529
+ const res = await fetch(this.resolve(url), {
530
+ method,
531
+ body: parsedBody,
532
+ headers: { ...headers, ...config?.headers },
533
+ credentials: "include",
534
+ signal: config?.signal
535
+ });
536
+ const data = await (res.headers.get("content-type")?.includes("application/json") ? res.json() : res.text());
537
+ if (!res.ok) throw new HttpError(res.statusText, res.status, data);
538
+ return {
539
+ data,
540
+ status: res.status,
541
+ headers: Object.fromEntries(res.headers.entries())
542
+ };
543
+ } catch (err) {
544
+ if (err instanceof HttpError) throw err;
545
+ const message = err instanceof Error ? err.message : "HTTP request failed";
546
+ throw new HttpError(message, 0, { error: JSON.stringify(err) });
547
+ }
548
+ }
549
+ resolve(url) {
550
+ return url.startsWith("http") ? url : `${this.baseURL}${url}`;
551
+ }
552
+ };
553
+ async function refresh(httpClient2) {
554
+ try {
555
+ const res = await httpClient2.get("/auth/refresh");
556
+ return ok(res.data);
557
+ } catch (err) {
558
+ if (err instanceof HttpError) {
559
+ return nok(err);
560
+ } else {
561
+ return nok(new HttpError("Unknown error during refresh", 0, { error: JSON.stringify(err) }));
562
+ }
563
+ }
564
+ }
565
+ var httpClient = new AutoRefreshDecorator(new FetchHttpClient(), refresh);
566
+
567
+ // src/feature/http/infra/mock-http-client.ts
568
+ function getKey(method, url) {
569
+ return `${method}:${url}`;
570
+ }
571
+ var MockHttpClient = class {
572
+ handlers = /* @__PURE__ */ new Map();
573
+ register(method, url, handler) {
574
+ this.handlers.set(getKey(method, url), handler);
575
+ }
576
+ respondWith(method, url, response) {
577
+ this.register(method, url, () => response);
578
+ }
579
+ respondWithError(method, url, error) {
580
+ this.register(method, url, () => Promise.reject(error));
581
+ }
582
+ reset() {
583
+ this.handlers.clear();
584
+ }
585
+ get(url, config) {
586
+ return this.invoke("GET", url, void 0, config);
587
+ }
588
+ post(url, data, config) {
589
+ return this.invoke("POST", url, data, config);
590
+ }
591
+ put(url, data, config) {
592
+ return this.invoke("PUT", url, data, config);
593
+ }
594
+ delete(url, config) {
595
+ return this.invoke("DELETE", url, void 0, config);
596
+ }
597
+ invoke(method, url, data, config) {
598
+ const handler = this.handlers.get(getKey(method, url));
599
+ if (!handler) {
600
+ return Promise.reject(new Error(`No mock handler registered for ${method} ${url}`));
601
+ }
602
+ const result = handler({ url, data, config });
603
+ return Promise.resolve(result);
604
+ }
605
+ };
606
+
607
+ // src/feature/auth/infra/http-cookie-auth-provider.ts
608
+ import { nok as nok2, ok as ok2 } from "@hexdspace/util";
609
+ var HttpCookieAuthProvider = class {
610
+ constructor(httpClient2) {
611
+ this.httpClient = httpClient2;
612
+ }
613
+ async getAuthenticatedUser() {
614
+ try {
615
+ const response = await this.httpClient.get("/auth/me");
616
+ return ok2(response.data.user);
617
+ } catch (err) {
618
+ if (isUnauthorizedError(err)) {
619
+ return ok2(null);
620
+ } else if (err instanceof Error) {
621
+ return nok2(err);
622
+ } else {
623
+ return nok2(new Error("Unknown error during authentication"));
624
+ }
625
+ }
626
+ }
627
+ async login(email, password) {
628
+ try {
629
+ const body = { email, password };
630
+ await this.httpClient.post("/auth/login", body);
631
+ const userResponse = await this.httpClient.get("/auth/me");
632
+ return ok2(userResponse.data.user);
633
+ } catch (err) {
634
+ if (err instanceof Error) {
635
+ return nok2(err);
636
+ } else {
637
+ return nok2(new Error("Unknown error during authentication"));
638
+ }
639
+ }
640
+ }
641
+ async register(email, password) {
642
+ try {
643
+ const res = await this.httpClient.post("/auth/register", { email, password });
644
+ return ok2(res.data);
645
+ } catch (err) {
646
+ if (err instanceof Error) {
647
+ return nok2(err);
648
+ } else {
649
+ return nok2(new Error("Unknown error during authentication"));
650
+ }
651
+ }
652
+ }
653
+ async logout() {
654
+ try {
655
+ await this.httpClient.post("/auth/logout");
656
+ return ok2(null);
657
+ } catch (err) {
658
+ if (err instanceof Error) {
659
+ return nok2(err);
660
+ } else {
661
+ return nok2(new Error("Unknown error during logout"));
662
+ }
663
+ }
664
+ }
665
+ };
666
+ function isUnauthorizedError(error) {
667
+ return error instanceof HttpError && error.status === 401;
668
+ }
669
+
670
+ // src/feature/auth/application/use-case/login-user-use-case.ts
671
+ var LoginUserUseCase = class {
672
+ constructor(authProvider) {
673
+ this.authProvider = authProvider;
674
+ }
675
+ execute(email, password) {
676
+ return this.authProvider.login(email, password);
677
+ }
678
+ };
679
+
680
+ // src/feature/auth/application/use-case/logout-user-use-case.ts
681
+ var LogoutUserUseCase = class {
682
+ constructor(authProvider) {
683
+ this.authProvider = authProvider;
684
+ }
685
+ execute() {
686
+ return this.authProvider.logout();
687
+ }
688
+ };
689
+
690
+ // src/feature/auth/application/use-case/register-user-use-case.ts
691
+ var RegisterUserUseCase = class {
692
+ constructor(authProvider) {
693
+ this.authProvider = authProvider;
694
+ }
695
+ execute(email, password) {
696
+ return this.authProvider.register(email, password);
697
+ }
698
+ };
699
+
700
+ // src/feature/auth/application/use-case/get-authenticated-user-use-case.ts
701
+ var GetAuthenticatedUserUseCase = class {
702
+ constructor(authProvider) {
703
+ this.authProvider = authProvider;
704
+ }
705
+ execute() {
706
+ return this.authProvider.getAuthenticatedUser();
707
+ }
708
+ };
709
+
710
+ // src/feature/auth/interface/controller/auth-controller.ts
711
+ var AuthController = class {
712
+ constructor(loginUser, logoutUser, registerUser, getAuthenticatedUser) {
713
+ this.loginUser = loginUser;
714
+ this.logoutUser = logoutUser;
715
+ this.registerUser = registerUser;
716
+ this.getAuthenticatedUser = getAuthenticatedUser;
717
+ }
718
+ async login(email, password) {
719
+ return this.loginUser.execute(email, password);
720
+ }
721
+ async logout() {
722
+ return this.logoutUser.execute();
723
+ }
724
+ async register(email, password) {
725
+ return this.registerUser.execute(email, password);
726
+ }
727
+ async getCurrentUser() {
728
+ return this.getAuthenticatedUser.execute();
729
+ }
730
+ };
731
+ var createAuthController = controllerFactory(
732
+ (deps) => new AuthController(
733
+ deps.loginUser,
734
+ deps.logoutUser,
735
+ deps.registerUser,
736
+ deps.getAuthenticatedUser
737
+ ),
738
+ (overrides) => {
739
+ const httpClient2 = overrides.httpClient ?? httpClient;
740
+ const authProvider = overrides.authProvider ?? new HttpCookieAuthProvider(httpClient2);
741
+ return {
742
+ httpClient: httpClient2,
743
+ authProvider,
744
+ loginUser: overrides.loginUser ?? new LoginUserUseCase(authProvider),
745
+ logoutUser: overrides.logoutUser ?? new LogoutUserUseCase(authProvider),
746
+ registerUser: overrides.registerUser ?? new RegisterUserUseCase(authProvider),
747
+ getAuthenticatedUser: overrides.getAuthenticatedUser ?? new GetAuthenticatedUserUseCase(authProvider)
748
+ };
749
+ }
750
+ );
751
+ var authController = createAuthController();
752
+
753
+ // src/feature/auth/interface/web/react/hook/useAuth.tsx
754
+ import { useContext as useContext2 } from "react";
755
+
756
+ // src/feature/auth/interface/web/react/AuthProvider.tsx
757
+ import { createContext as createContext2, useEffect as useEffect2, useReducer } from "react";
758
+
759
+ // src/feature/auth/interface/web/react/state/auth-reducer.ts
760
+ function authStateReducer(_state, action) {
761
+ switch (action.type) {
762
+ case "REQUEST":
763
+ return { status: "loading" };
764
+ case "SUCCESS":
765
+ return { status: "unauthenticated", message: action.message };
766
+ case "FAILED":
767
+ return { status: "error", error: action.error };
768
+ case "LOGOUT":
769
+ return { status: "unauthenticated" };
770
+ case "COMPLETE":
771
+ return { status: "authenticated", user: action.user };
772
+ default:
773
+ throw new Error(`Unknown state: ${JSON.stringify(action)}`);
774
+ }
775
+ }
776
+
777
+ // src/feature/auth/interface/web/react/hook/useAuthController.tsx
778
+ import { useContext } from "react";
779
+
780
+ // src/feature/auth/interface/web/react/AuthControllerProvider.tsx
781
+ import { createContext } from "react";
782
+ import { jsx as jsx3 } from "react/jsx-runtime";
783
+ var AuthControllerCtx = createContext(null);
784
+ function AuthControllerProvider({ children, controller }) {
785
+ return /* @__PURE__ */ jsx3(AuthControllerCtx.Provider, { value: controller, children });
786
+ }
787
+
788
+ // src/feature/auth/interface/web/react/hook/useAuthController.tsx
789
+ function useAuthController() {
790
+ const authController2 = useContext(AuthControllerCtx);
791
+ if (!authController2) {
792
+ throw new Error("useAuthController must be used inside <AuthControllerProvider>");
793
+ }
794
+ return authController2;
795
+ }
796
+
797
+ // src/feature/auth/interface/web/react/AuthProvider.tsx
798
+ import { jsx as jsx4 } from "react/jsx-runtime";
799
+ var AuthStateCtx = createContext2(null);
800
+ var AuthDispatchCtx = createContext2(null);
801
+ function AuthProvider({ children }) {
802
+ const [state, dispatch] = useReducer(authStateReducer, {
803
+ status: "loading"
804
+ });
805
+ const authController2 = useAuthController();
806
+ useEffect2(() => {
807
+ const fetchActiveUser = async () => {
808
+ const res = await authController2.getCurrentUser();
809
+ if (res.ok) {
810
+ if (res.value) {
811
+ dispatch({ type: "COMPLETE", user: res.value });
812
+ } else {
813
+ dispatch({ type: "LOGOUT" });
814
+ }
815
+ } else {
816
+ dispatch({ type: "FAILED", error: res.error.message });
817
+ }
818
+ };
819
+ void fetchActiveUser();
820
+ }, [dispatch, authController2]);
821
+ return /* @__PURE__ */ jsx4(AuthDispatchCtx.Provider, { value: dispatch, children: /* @__PURE__ */ jsx4(AuthStateCtx.Provider, { value: state, children }) });
822
+ }
823
+
824
+ // src/feature/auth/interface/web/react/hook/useAuth.tsx
825
+ function useAuth() {
826
+ const authState = useContext2(AuthStateCtx);
827
+ if (!authState) {
828
+ throw new Error("useAuth must be used inside <AuthProvider>");
829
+ }
830
+ return authState;
831
+ }
832
+
833
+ // src/feature/auth/interface/web/react/hook/useAuthActions.tsx
834
+ import { useCallback as useCallback2 } from "react";
835
+
836
+ // src/feature/auth/interface/web/react/hook/useAuthDispatch.tsx
837
+ import { useContext as useContext3 } from "react";
838
+ function useAuthDispatch() {
839
+ const dispatch = useContext3(AuthDispatchCtx);
840
+ if (!dispatch) {
841
+ throw new Error("useAuthDispatch must be used inside <AuthProvider>");
842
+ }
843
+ return dispatch;
844
+ }
845
+
846
+ // src/feature/auth/interface/web/react/hook/useAuthActions.tsx
847
+ function useAuthActions() {
848
+ const dispatch = useAuthDispatch();
849
+ const authController2 = useAuthController();
850
+ const login = useCallback2((email, password) => {
851
+ dispatch({ type: "REQUEST" });
852
+ authController2.login(email, password).then((res) => {
853
+ dispatchLoginResult(res, dispatch);
854
+ });
855
+ }, [dispatch, authController2]);
856
+ const logout = useCallback2(() => {
857
+ authController2.logout().then(() => dispatch({ type: "LOGOUT" }));
858
+ }, [dispatch, authController2]);
859
+ const register = useCallback2(async (email, password) => {
860
+ dispatch({ type: "REQUEST" });
861
+ const res = await authController2.register(email, password);
862
+ dispatchRegisterResult(res, dispatch);
863
+ return res;
864
+ }, [dispatch, authController2]);
865
+ return { login, logout, register };
866
+ }
867
+ function dispatchLoginResult(res, dispatch) {
868
+ if (res.ok) {
869
+ dispatch({ type: "COMPLETE", user: res.value });
870
+ } else {
871
+ dispatch({ type: "FAILED", error: res.error.message });
872
+ }
873
+ }
874
+ function dispatchRegisterResult(res, dispatch) {
875
+ if (res.ok) {
876
+ dispatch({ type: "SUCCESS", message: res.value.message });
877
+ } else {
878
+ dispatch({ type: "FAILED", error: res.error.message });
879
+ }
880
+ }
881
+
882
+ // src/feature/auth/interface/web/react/hook/useAuthedUser.tsx
883
+ function useAuthedUser() {
884
+ const auth = useAuth();
885
+ if (auth.status !== "authenticated") {
886
+ throw new Error("useAuthedUser called outside of an authenticated route");
887
+ }
888
+ return auth.user;
889
+ }
890
+
891
+ // src/feature/auth/interface/web/react/AuthFormInputField.tsx
892
+ import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
893
+ var inputLabelStyles = {
894
+ display: "block",
895
+ fontSize: "small",
896
+ fontWeight: "bolder",
897
+ marginBottom: "4px"
898
+ };
899
+ var inputFieldStyles = {
900
+ display: "block",
901
+ width: "100%",
902
+ padding: "0.5rem 0.75rem"
903
+ };
904
+ var AuthFormInputField = ({ id, label, type, value, onChange, placeholder, className }) => {
905
+ return /* @__PURE__ */ jsxs2("div", { children: [
906
+ /* @__PURE__ */ jsx5("label", { htmlFor: id, style: inputLabelStyles, children: label }),
907
+ /* @__PURE__ */ jsx5(
908
+ "input",
909
+ {
910
+ id,
911
+ name: id,
912
+ type,
913
+ required: true,
914
+ value,
915
+ onChange,
916
+ className,
917
+ placeholder,
918
+ style: inputFieldStyles
919
+ }
920
+ )
921
+ ] });
922
+ };
923
+
924
+ // src/feature/auth/infra/mock-auth-http-client.ts
925
+ function httpResponse(data, status = 200) {
926
+ return { data, status, headers: {} };
927
+ }
928
+ function unauthorized() {
929
+ return new HttpError("Unauthorized", 401, { error: "Unauthorized" });
930
+ }
931
+ var MockAuthHttpClient = class extends MockHttpClient {
932
+ fixtures;
933
+ delayMs;
934
+ constructor(initial, delayMs = 0) {
935
+ super();
936
+ this.fixtures = { currentUser: null, ...initial };
937
+ this.delayMs = delayMs;
938
+ this.registerAuthRoutes();
939
+ }
940
+ setCurrentUser(user) {
941
+ this.fixtures.currentUser = user;
942
+ }
943
+ setLoginError(error) {
944
+ this.fixtures.loginError = error;
945
+ }
946
+ setRegisterError(error) {
947
+ this.fixtures.registerError = error;
948
+ }
949
+ setMeError(error) {
950
+ this.fixtures.meError = error;
951
+ }
952
+ setLogoutError(error) {
953
+ this.fixtures.logoutError = error;
954
+ }
955
+ registerAuthRoutes() {
956
+ this.register("GET", "/auth/me", async () => {
957
+ await new Promise((resolve) => setTimeout(resolve, this.delayMs));
958
+ if (this.fixtures.meError) {
959
+ throw this.fixtures.meError;
960
+ }
961
+ if (this.fixtures.currentUser) {
962
+ return httpResponse({ user: this.fixtures.currentUser });
963
+ }
964
+ throw unauthorized();
965
+ });
966
+ this.register("POST", "/auth/login", async (payload) => {
967
+ await new Promise((resolve) => setTimeout(resolve, this.delayMs));
968
+ if (this.fixtures.loginError) {
969
+ throw this.fixtures.loginError;
970
+ }
971
+ const body = payload.data;
972
+ this.fixtures.currentUser ||= {
973
+ id: "mock-user",
974
+ email: body.email
975
+ };
976
+ return httpResponse({ message: "Logged in" });
977
+ });
978
+ this.register("POST", "/auth/register", async () => {
979
+ await new Promise((resolve) => setTimeout(resolve, this.delayMs));
980
+ if (this.fixtures.registerError) {
981
+ throw this.fixtures.registerError;
982
+ }
983
+ return httpResponse({ message: "Registered" });
984
+ });
985
+ this.register("POST", "/auth/logout", async () => {
986
+ await new Promise((resolve) => setTimeout(resolve, this.delayMs));
987
+ if (this.fixtures.logoutError) {
988
+ throw this.fixtures.logoutError;
989
+ }
990
+ this.fixtures.currentUser = null;
991
+ return httpResponse(null);
992
+ });
993
+ }
994
+ };
470
995
  export {
996
+ AuthController,
997
+ AuthControllerCtx,
998
+ AuthControllerProvider,
999
+ AuthDispatchCtx,
1000
+ AuthFormInputField,
1001
+ AuthProvider,
1002
+ AuthStateCtx,
471
1003
  DEFAULT_NOTIFICATION_CHANNEL,
1004
+ HttpError,
1005
+ MockAuthHttpClient,
1006
+ MockHttpClient,
472
1007
  NotificationHost,
473
1008
  NotifierController,
1009
+ authController,
1010
+ controllerFactory,
1011
+ createAuthController,
1012
+ httpClient as fetchHttpClient,
474
1013
  notifierController,
475
1014
  resolveToastTheme,
476
1015
  ui,
1016
+ useAuth,
1017
+ useAuthActions,
1018
+ useAuthController,
1019
+ useAuthDispatch,
1020
+ useAuthedUser,
477
1021
  useResponsiveMutation
478
1022
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexdspace/react",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -21,9 +21,10 @@
21
21
  "dependencies": {
22
22
  "@tanstack/react-query": "^5.90.11",
23
23
  "lucide-react": "^0.555.0",
24
+ "react-router-dom": "^7.10.1",
24
25
  "react-toastify": "^11.0.5",
25
26
  "uuid": "^13.0.0",
26
- "@hexdspace/util": "0.0.1"
27
+ "@hexdspace/util": "0.0.25"
27
28
  },
28
29
  "peerDependencies": {
29
30
  "@tanstack/react-query": "^5.90.11",
@@ -31,8 +32,8 @@
31
32
  },
32
33
  "devDependencies": {
33
34
  "@tanstack/react-query": "^5.90.11",
34
- "react": "^19.2.0",
35
- "@types/react": "^19.2.7"
35
+ "@types/react": "^19.2.7",
36
+ "react": "^19.2.0"
36
37
  },
37
38
  "scripts": {
38
39
  "build": "tsup src/index.ts --dts --format esm --clean",