dauth-context-react 6.3.0 → 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dauth-context-react",
3
- "version": "6.3.0",
3
+ "version": "6.5.0",
4
4
  "description": "React provider and hook for passwordless authentication via the Dauth service (BFF pattern with httpOnly cookies)",
5
5
  "license": "MIT",
6
6
  "author": "David T. Pizarro Frick",
@@ -299,6 +299,7 @@ export function DauthProfileModal({
299
299
  getPasskeyCredentials,
300
300
  registerPasskey,
301
301
  deletePasskeyCredential,
302
+ uploadAvatar,
302
303
  } = useDauth();
303
304
  const isDesktop = useMediaQuery('(min-width: 641px)');
304
305
  const phase = useModalAnimation(open);
@@ -570,20 +571,22 @@ export function DauthProfileModal({
570
571
  );
571
572
 
572
573
  const handleAvatarClick = useCallback(() => {
573
- if (onAvatarUpload) {
574
- avatarInputRef.current?.click();
575
- }
576
- }, [onAvatarUpload]);
574
+ avatarInputRef.current?.click();
575
+ }, []);
577
576
 
578
577
  const handleAvatarChange = useCallback(
579
578
  async (e: React.ChangeEvent<HTMLInputElement>) => {
580
579
  const file = e.target.files?.[0];
581
- if (!file || !onAvatarUpload) return;
580
+ if (!file) return;
582
581
  setUploadingAvatar(true);
583
582
  try {
584
- const url = await onAvatarUpload(file);
585
- if (url) {
586
- await updateUser({ avatar: url } as any);
583
+ if (onAvatarUpload) {
584
+ const url = await onAvatarUpload(file);
585
+ if (url) {
586
+ await updateUser({ avatar: url } as any);
587
+ }
588
+ } else {
589
+ await uploadAvatar(file);
587
590
  }
588
591
  } catch {
589
592
  // Error handled by onError callback
@@ -593,7 +596,7 @@ export function DauthProfileModal({
593
596
  avatarInputRef.current.value = '';
594
597
  }
595
598
  },
596
- [onAvatarUpload, updateUser]
599
+ [onAvatarUpload, updateUser, uploadAvatar]
597
600
  );
598
601
 
599
602
  const handleSignOut = useCallback(() => {
@@ -776,9 +779,7 @@ export function DauthProfileModal({
776
779
  <div
777
780
  style={{
778
781
  ...avatarCircle,
779
- cursor: onAvatarUpload
780
- ? 'pointer'
781
- : 'default',
782
+ cursor: 'pointer',
782
783
  position: 'relative' as const,
783
784
  }}
784
785
  onClick={handleAvatarClick}
@@ -798,22 +799,20 @@ export function DauthProfileModal({
798
799
  ) : (
799
800
  avatarInitial
800
801
  )}
801
- {onAvatarUpload && !uploadingAvatar && (
802
+ {!uploadingAvatar && (
802
803
  <div style={avatarOverlay}>
803
804
  <IconCamera />
804
805
  </div>
805
806
  )}
806
807
  </div>
807
808
  <div style={emailText}>{user.email}</div>
808
- {onAvatarUpload && (
809
- <input
810
- ref={avatarInputRef}
811
- type="file"
812
- accept="image/*"
813
- style={{ display: 'none' }}
814
- onChange={handleAvatarChange}
815
- />
816
- )}
809
+ <input
810
+ ref={avatarInputRef}
811
+ type="file"
812
+ accept="image/*"
813
+ style={{ display: 'none' }}
814
+ onChange={handleAvatarChange}
815
+ />
817
816
  </div>
818
817
 
819
818
  {/* Status */}
@@ -8,6 +8,7 @@ import {
8
8
  IPasskeyRegistrationStartResponse,
9
9
  IPasskeyRegistrationFinishResponse,
10
10
  IDeletePasskeyResponse,
11
+ IUploadAvatarResponse,
11
12
  } from './interfaces/dauth.api.responses';
12
13
 
13
14
  function getCsrfToken(): string {
@@ -158,3 +159,19 @@ export async function deletePasskeyCredentialAPI(
158
159
  const data = await response.json();
159
160
  return { response, data };
160
161
  }
162
+
163
+ export async function uploadAvatarAPI(
164
+ basePath: string,
165
+ file: File
166
+ ): Promise<IUploadAvatarResponse> {
167
+ const formData = new FormData();
168
+ formData.append('avatar', file);
169
+ const response = await fetch(`${basePath}/avatar`, {
170
+ method: 'POST',
171
+ headers: { 'X-CSRF-Token': getCsrfToken() },
172
+ credentials: 'include',
173
+ body: formData,
174
+ });
175
+ const data = await response.json();
176
+ return { response, data };
177
+ }
@@ -69,3 +69,12 @@ export interface IDeletePasskeyResponse {
69
69
  message: string;
70
70
  };
71
71
  }
72
+
73
+ export interface IUploadAvatarResponse {
74
+ response: Response;
75
+ data: {
76
+ status: string;
77
+ user: IDauthUser;
78
+ message: string;
79
+ };
80
+ }
package/src/index.tsx CHANGED
@@ -21,6 +21,7 @@ import type {
21
21
  IModalTheme,
22
22
  IPasskeyCredential,
23
23
  DauthProfileModalProps,
24
+ ThemeMode,
24
25
  } from './interfaces';
25
26
 
26
27
  export { DauthProfileModal } from './DauthProfileModal';
@@ -32,6 +33,7 @@ export type {
32
33
  IModalTheme,
33
34
  IPasskeyCredential,
34
35
  DauthProfileModalProps,
36
+ ThemeMode,
35
37
  };
36
38
 
37
39
  const defaultOnError = (error: Error) => console.error(error);
@@ -92,10 +94,12 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
92
94
  telPrefix,
93
95
  telSuffix,
94
96
  language,
97
+ theme,
95
98
  avatar,
96
99
  birthDate,
97
100
  country,
98
101
  metadata,
102
+ customFields,
99
103
  } = fields;
100
104
  const user = {
101
105
  name,
@@ -104,10 +108,12 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
104
108
  telPrefix,
105
109
  telSuffix,
106
110
  language,
111
+ theme,
107
112
  avatar,
108
113
  birthDate,
109
114
  country,
110
115
  metadata,
116
+ customFields,
111
117
  } as Partial<IDauthUser>;
112
118
  return action.updateUserAction(ctx, user);
113
119
  },
@@ -135,6 +141,11 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
135
141
  [ctx]
136
142
  );
137
143
 
144
+ const uploadAvatar = useCallback(
145
+ (file: File) => action.uploadAvatarAction(ctx, file),
146
+ [ctx]
147
+ );
148
+
138
149
  const memoProvider = useMemo(
139
150
  () => ({
140
151
  ...dauthState,
@@ -145,6 +156,7 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
145
156
  getPasskeyCredentials,
146
157
  registerPasskey,
147
158
  deletePasskeyCredential,
159
+ uploadAvatar,
148
160
  }),
149
161
  [
150
162
  dauthState,
@@ -155,6 +167,7 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
155
167
  getPasskeyCredentials,
156
168
  registerPasskey,
157
169
  deletePasskeyCredential,
170
+ uploadAvatar,
158
171
  ]
159
172
  );
160
173
 
@@ -6,6 +6,7 @@ const initialDauthState: IDauthState = {
6
6
  (typeof window !== 'undefined'
7
7
  ? window.document.documentElement.getAttribute('lang')
8
8
  : null) || 'es',
9
+ theme: 'system',
9
10
  } as IDauthUser,
10
11
  domain: {} as IDauthDomainState,
11
12
  isLoading: true,
@@ -17,6 +18,7 @@ const initialDauthState: IDauthState = {
17
18
  getPasskeyCredentials: () => Promise.resolve([]),
18
19
  registerPasskey: () => Promise.resolve(null),
19
20
  deletePasskeyCredential: () => Promise.resolve(false),
21
+ uploadAvatar: () => Promise.resolve(false),
20
22
  };
21
23
 
22
24
  export default initialDauthState;
package/src/interfaces.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export type ThemeMode = 'dark' | 'light' | 'system';
2
+
1
3
  export interface IDauthUser {
2
4
  _id: string;
3
5
  name: string;
@@ -6,6 +8,7 @@ export interface IDauthUser {
6
8
  email: string;
7
9
  isVerified: boolean;
8
10
  language: string;
11
+ theme: ThemeMode;
9
12
  avatar: {
10
13
  id: string;
11
14
  url: string;
@@ -89,14 +92,15 @@ export interface IDauthState {
89
92
  deletePasskeyCredential: (
90
93
  credentialId: string
91
94
  ) => Promise<boolean>;
95
+ uploadAvatar: (file: File) => Promise<boolean>;
92
96
  }
93
97
 
94
98
  export interface DauthProfileModalProps {
95
99
  open: boolean;
96
100
  onClose: () => void;
97
- /** Optional: provide a function to handle avatar upload.
101
+ /** Optional override for avatar upload.
98
102
  * Receives a File, should return the URL string.
99
- * If not provided, the avatar edit button is hidden. */
103
+ * If not provided, uses the built-in upload via the auth proxy. */
100
104
  onAvatarUpload?: (file: File) => Promise<string>;
101
105
  }
102
106
 
@@ -8,6 +8,7 @@ import {
8
8
  startPasskeyRegistrationAPI,
9
9
  finishPasskeyRegistrationAPI,
10
10
  deletePasskeyCredentialAPI,
11
+ uploadAvatarAPI,
11
12
  } from '../api/dauth.api';
12
13
  import type { IPasskeyCredential } from '../api/interfaces/dauth.api.responses';
13
14
  import { createPasskeyCredential } from '../webauthn';
@@ -239,6 +240,36 @@ export async function deletePasskeyCredentialAction(
239
240
  }
240
241
  }
241
242
 
243
+ export async function uploadAvatarAction(
244
+ ctx: ActionContext,
245
+ file: File
246
+ ): Promise<boolean> {
247
+ const { dispatch, authProxyPath, onError } = ctx;
248
+ try {
249
+ const result = await uploadAvatarAPI(authProxyPath, file);
250
+ if (result.response.status === 200) {
251
+ dispatch({
252
+ type: DauthTypes.UPDATE_USER,
253
+ payload: result.data.user,
254
+ });
255
+ return true;
256
+ }
257
+ onError(
258
+ new Error(
259
+ 'Avatar upload error: ' + result.data.message
260
+ )
261
+ );
262
+ return false;
263
+ } catch (error) {
264
+ onError(
265
+ error instanceof Error
266
+ ? error
267
+ : new Error('Avatar upload error')
268
+ );
269
+ return false;
270
+ }
271
+ }
272
+
242
273
  export const resetUser = (dispatch: React.Dispatch<any>) => {
243
274
  return dispatch({
244
275
  type: DauthTypes.LOGIN,