dauth-context-react 6.1.0 → 6.3.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/dist/index.js CHANGED
@@ -40,7 +40,10 @@ var initialDauthState = {
40
40
  logout: () => {
41
41
  },
42
42
  updateUser: () => Promise.resolve(false),
43
- deleteAccount: () => Promise.resolve(false)
43
+ deleteAccount: () => Promise.resolve(false),
44
+ getPasskeyCredentials: () => Promise.resolve([]),
45
+ registerPasskey: () => Promise.resolve(null),
46
+ deletePasskeyCredential: () => Promise.resolve(false)
44
47
  };
45
48
  var initialDauthState_default = initialDauthState;
46
49
 
@@ -147,6 +150,134 @@ async function deleteAccountAPI(basePath) {
147
150
  const data = await response.json();
148
151
  return { response, data };
149
152
  }
153
+ async function getPasskeyCredentialsAPI(basePath) {
154
+ const response = await fetch(
155
+ `${basePath}/passkey/credentials`,
156
+ {
157
+ method: "GET",
158
+ headers: { "X-CSRF-Token": getCsrfToken() },
159
+ credentials: "include"
160
+ }
161
+ );
162
+ const data = await response.json();
163
+ return { response, data };
164
+ }
165
+ async function startPasskeyRegistrationAPI(basePath) {
166
+ const response = await fetch(
167
+ `${basePath}/passkey/register/start`,
168
+ {
169
+ method: "POST",
170
+ headers: {
171
+ "Content-Type": "application/json",
172
+ "X-CSRF-Token": getCsrfToken()
173
+ },
174
+ credentials: "include"
175
+ }
176
+ );
177
+ const data = await response.json();
178
+ return { response, data };
179
+ }
180
+ async function finishPasskeyRegistrationAPI(basePath, body) {
181
+ const response = await fetch(
182
+ `${basePath}/passkey/register/finish`,
183
+ {
184
+ method: "POST",
185
+ headers: {
186
+ "Content-Type": "application/json",
187
+ "X-CSRF-Token": getCsrfToken()
188
+ },
189
+ credentials: "include",
190
+ body: JSON.stringify(body)
191
+ }
192
+ );
193
+ const data = await response.json();
194
+ return { response, data };
195
+ }
196
+ async function deletePasskeyCredentialAPI(basePath, credentialId) {
197
+ const response = await fetch(
198
+ `${basePath}/passkey/credentials/${credentialId}`,
199
+ {
200
+ method: "DELETE",
201
+ headers: { "X-CSRF-Token": getCsrfToken() },
202
+ credentials: "include"
203
+ }
204
+ );
205
+ const data = await response.json();
206
+ return { response, data };
207
+ }
208
+
209
+ // src/webauthn.ts
210
+ function base64urlToBuffer(base64url) {
211
+ const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
212
+ const pad = base64.length % 4;
213
+ const padded = pad ? base64 + "=".repeat(4 - pad) : base64;
214
+ const binary = atob(padded);
215
+ const bytes = new Uint8Array(binary.length);
216
+ for (let i = 0; i < binary.length; i++) {
217
+ bytes[i] = binary.charCodeAt(i);
218
+ }
219
+ return bytes.buffer;
220
+ }
221
+ function bufferToBase64url(buffer) {
222
+ const bytes = new Uint8Array(buffer);
223
+ let binary = "";
224
+ for (let i = 0; i < bytes.length; i++) {
225
+ binary += String.fromCharCode(bytes[i]);
226
+ }
227
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
228
+ }
229
+ async function createPasskeyCredential(options) {
230
+ const publicKey = {
231
+ ...options,
232
+ challenge: base64urlToBuffer(options.challenge),
233
+ user: {
234
+ ...options.user,
235
+ id: base64urlToBuffer(options.user.id)
236
+ },
237
+ excludeCredentials: (options.excludeCredentials ?? []).map(
238
+ (c) => ({
239
+ ...c,
240
+ id: base64urlToBuffer(c.id)
241
+ })
242
+ )
243
+ };
244
+ const credential = await navigator.credentials.create({
245
+ publicKey
246
+ });
247
+ if (!credential) {
248
+ throw new Error("Passkey registration was cancelled");
249
+ }
250
+ const attestation = credential.response;
251
+ return {
252
+ id: credential.id,
253
+ rawId: bufferToBase64url(credential.rawId),
254
+ type: credential.type,
255
+ response: {
256
+ clientDataJSON: bufferToBase64url(
257
+ attestation.clientDataJSON
258
+ ),
259
+ attestationObject: bufferToBase64url(
260
+ attestation.attestationObject
261
+ ),
262
+ ...attestation.getTransports ? { transports: attestation.getTransports() } : {},
263
+ ...attestation.getPublicKeyAlgorithm ? {
264
+ publicKeyAlgorithm: attestation.getPublicKeyAlgorithm()
265
+ } : {},
266
+ ...attestation.getPublicKey ? {
267
+ publicKey: bufferToBase64url(
268
+ attestation.getPublicKey()
269
+ )
270
+ } : {},
271
+ ...attestation.getAuthenticatorData ? {
272
+ authenticatorData: bufferToBase64url(
273
+ attestation.getAuthenticatorData()
274
+ )
275
+ } : {}
276
+ },
277
+ clientExtensionResults: credential.getClientExtensionResults(),
278
+ authenticatorAttachment: credential.authenticatorAttachment ?? void 0
279
+ };
280
+ }
150
281
 
151
282
  // src/reducer/dauth.actions.ts
152
283
  async function exchangeCodeAction(ctx, code) {
@@ -270,6 +401,65 @@ async function deleteAccountAction(ctx) {
270
401
  return false;
271
402
  }
272
403
  }
404
+ async function getPasskeyCredentialsAction(ctx) {
405
+ const { authProxyPath, onError } = ctx;
406
+ try {
407
+ const result = await getPasskeyCredentialsAPI(authProxyPath);
408
+ if (result.response.status === 200) {
409
+ return result.data.credentials ?? [];
410
+ }
411
+ return [];
412
+ } catch (error) {
413
+ onError(
414
+ error instanceof Error ? error : new Error("Get passkey credentials error")
415
+ );
416
+ return [];
417
+ }
418
+ }
419
+ async function registerPasskeyAction(ctx, name) {
420
+ const { authProxyPath, onError } = ctx;
421
+ try {
422
+ const startResult = await startPasskeyRegistrationAPI(authProxyPath);
423
+ if (startResult.response.status !== 200) {
424
+ onError(new Error("Failed to start passkey registration"));
425
+ return null;
426
+ }
427
+ const credential = await createPasskeyCredential(
428
+ startResult.data
429
+ );
430
+ const finishResult = await finishPasskeyRegistrationAPI(
431
+ authProxyPath,
432
+ { credential, name }
433
+ );
434
+ if (finishResult.response.status === 200 || finishResult.response.status === 201) {
435
+ return finishResult.data.credential;
436
+ }
437
+ onError(
438
+ new Error("Failed to finish passkey registration")
439
+ );
440
+ return null;
441
+ } catch (error) {
442
+ onError(
443
+ error instanceof Error ? error : new Error("Passkey registration error")
444
+ );
445
+ return null;
446
+ }
447
+ }
448
+ async function deletePasskeyCredentialAction(ctx, credentialId) {
449
+ const { authProxyPath, onError } = ctx;
450
+ try {
451
+ const result = await deletePasskeyCredentialAPI(
452
+ authProxyPath,
453
+ credentialId
454
+ );
455
+ return result.response.status === 200;
456
+ } catch (error) {
457
+ onError(
458
+ error instanceof Error ? error : new Error("Delete passkey error")
459
+ );
460
+ return false;
461
+ }
462
+ }
273
463
  var resetUser = (dispatch) => {
274
464
  return dispatch({
275
465
  type: LOGIN,
@@ -361,6 +551,107 @@ function IconBack() {
361
551
  }
362
552
  );
363
553
  }
554
+ function IconFingerprint() {
555
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
556
+ "svg",
557
+ {
558
+ width: "16",
559
+ height: "16",
560
+ viewBox: "0 0 24 24",
561
+ fill: "none",
562
+ stroke: "currentColor",
563
+ strokeWidth: "2",
564
+ strokeLinecap: "round",
565
+ strokeLinejoin: "round",
566
+ children: [
567
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4" }),
568
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M14 13.12c0 2.38 0 6.38-1 8.88" }),
569
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M17.29 21.02c.12-.6.43-2.3.5-3.02" }),
570
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M2 12a10 10 0 0 1 18-6" }),
571
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M2 16h.01" }),
572
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21.8 16c.2-2 .131-5.354 0-6" }),
573
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5 19.5C5.5 18 6 15 6 12a6 6 0 0 1 .34-2" }),
574
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M8.65 22c.21-.66.45-1.32.57-2" }),
575
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M9 6.8a6 6 0 0 1 9 5.2v2" })
576
+ ]
577
+ }
578
+ );
579
+ }
580
+ function IconShield() {
581
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
582
+ "svg",
583
+ {
584
+ width: "20",
585
+ height: "20",
586
+ viewBox: "0 0 24 24",
587
+ fill: "none",
588
+ stroke: "currentColor",
589
+ strokeWidth: "2",
590
+ strokeLinecap: "round",
591
+ strokeLinejoin: "round",
592
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" })
593
+ }
594
+ );
595
+ }
596
+ function IconTrash() {
597
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
598
+ "svg",
599
+ {
600
+ width: "14",
601
+ height: "14",
602
+ viewBox: "0 0 24 24",
603
+ fill: "none",
604
+ stroke: "currentColor",
605
+ strokeWidth: "2",
606
+ strokeLinecap: "round",
607
+ strokeLinejoin: "round",
608
+ children: [
609
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M3 6h18" }),
610
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
611
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" })
612
+ ]
613
+ }
614
+ );
615
+ }
616
+ function IconLogOut() {
617
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
618
+ "svg",
619
+ {
620
+ width: "16",
621
+ height: "16",
622
+ viewBox: "0 0 24 24",
623
+ fill: "none",
624
+ stroke: "currentColor",
625
+ strokeWidth: "2",
626
+ strokeLinecap: "round",
627
+ strokeLinejoin: "round",
628
+ children: [
629
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }),
630
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "16 17 21 12 16 7" }),
631
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "21", y1: "12", x2: "9", y2: "12" })
632
+ ]
633
+ }
634
+ );
635
+ }
636
+ function IconCamera() {
637
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
638
+ "svg",
639
+ {
640
+ width: "14",
641
+ height: "14",
642
+ viewBox: "0 0 24 24",
643
+ fill: "none",
644
+ stroke: "currentColor",
645
+ strokeWidth: "2",
646
+ strokeLinecap: "round",
647
+ strokeLinejoin: "round",
648
+ children: [
649
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z" }),
650
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "13", r: "3" })
651
+ ]
652
+ }
653
+ );
654
+ }
364
655
  function Spinner() {
365
656
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: spinnerStyle, "aria-hidden": "true" });
366
657
  }
@@ -388,7 +679,10 @@ function useModalAnimation(open) {
388
679
  }
389
680
  if (phase === "entered" || phase === "entering") {
390
681
  setPhase("exiting");
391
- const timer = setTimeout(() => setPhase("exited"), MOBILE_TRANSITION_MS);
682
+ const timer = setTimeout(
683
+ () => setPhase("exited"),
684
+ MOBILE_TRANSITION_MS
685
+ );
392
686
  return () => clearTimeout(timer);
393
687
  }
394
688
  return void 0;
@@ -456,27 +750,64 @@ function useScrollLock(active) {
456
750
  };
457
751
  }, [active]);
458
752
  }
459
- function DauthProfileModal({ open, onClose }) {
460
- const { user, domain, updateUser, deleteAccount } = useDauth();
753
+ function DauthProfileModal({
754
+ open,
755
+ onClose,
756
+ onAvatarUpload
757
+ }) {
758
+ const {
759
+ user,
760
+ domain,
761
+ updateUser,
762
+ deleteAccount,
763
+ logout,
764
+ getPasskeyCredentials,
765
+ registerPasskey,
766
+ deletePasskeyCredential
767
+ } = useDauth();
461
768
  const isDesktop = useMediaQuery("(min-width: 641px)");
462
769
  const phase = useModalAnimation(open);
463
770
  const modalRef = (0, import_react.useRef)(null);
771
+ const avatarInputRef = (0, import_react.useRef)(null);
772
+ const showSecurity = domain.authMethods?.passkey === true;
773
+ const [activeTab, setActiveTab] = (0, import_react.useState)("profile");
464
774
  const [name, setName] = (0, import_react.useState)("");
465
775
  const [lastname, setLastname] = (0, import_react.useState)("");
466
776
  const [nickname, setNickname] = (0, import_react.useState)("");
467
777
  const [country, setCountry] = (0, import_react.useState)("");
778
+ const [telPrefix, setTelPrefix] = (0, import_react.useState)("");
779
+ const [telSuffix, setTelSuffix] = (0, import_react.useState)("");
780
+ const [birthDate, setBirthDate] = (0, import_react.useState)("");
781
+ const [customFieldValues, setCustomFieldValues] = (0, import_react.useState)({});
468
782
  const [populated, setPopulated] = (0, import_react.useState)(false);
469
783
  const [saving, setSaving] = (0, import_react.useState)(false);
470
784
  const [status, setStatus] = (0, import_react.useState)(null);
471
785
  const [showDelete, setShowDelete] = (0, import_react.useState)(false);
472
786
  const [deleteText, setDeleteText] = (0, import_react.useState)("");
473
787
  const [deleting, setDeleting] = (0, import_react.useState)(false);
788
+ const [credentials, setCredentials] = (0, import_react.useState)([]);
789
+ const [loadingCreds, setLoadingCreds] = (0, import_react.useState)(false);
790
+ const [showRegister, setShowRegister] = (0, import_react.useState)(false);
791
+ const [passkeyName, setPasskeyName] = (0, import_react.useState)("");
792
+ const [registering, setRegistering] = (0, import_react.useState)(false);
793
+ const [passkeyStatus, setPasskeyStatus] = (0, import_react.useState)(null);
794
+ const [uploadingAvatar, setUploadingAvatar] = (0, import_react.useState)(false);
474
795
  (0, import_react.useEffect)(() => {
475
796
  if (open && user?._id && !populated) {
476
797
  setName(user.name || "");
477
798
  setLastname(user.lastname || "");
478
799
  setNickname(user.nickname || "");
479
800
  setCountry(user.country || "");
801
+ setTelPrefix(user.telPrefix || "");
802
+ setTelSuffix(user.telSuffix || "");
803
+ setBirthDate(
804
+ user.birthDate ? new Date(user.birthDate).toISOString().split("T")[0] : ""
805
+ );
806
+ const cf = {};
807
+ for (const f of domain.customFields ?? []) {
808
+ cf[f.key] = user.customFields?.[f.key] ?? "";
809
+ }
810
+ setCustomFieldValues(cf);
480
811
  setPopulated(true);
481
812
  }
482
813
  if (!open) {
@@ -484,13 +815,36 @@ function DauthProfileModal({ open, onClose }) {
484
815
  setStatus(null);
485
816
  setShowDelete(false);
486
817
  setDeleteText("");
818
+ setActiveTab("profile");
819
+ setPasskeyStatus(null);
820
+ setShowRegister(false);
821
+ setPasskeyName("");
487
822
  }
488
823
  }, [open, user, populated]);
824
+ (0, import_react.useEffect)(() => {
825
+ if (activeTab !== "security" || !showSecurity) return;
826
+ setLoadingCreds(true);
827
+ getPasskeyCredentials().then((creds) => {
828
+ setCredentials(creds);
829
+ setLoadingCreds(false);
830
+ });
831
+ }, [activeTab, showSecurity, getPasskeyCredentials]);
489
832
  (0, import_react.useEffect)(() => {
490
833
  if (status?.type !== "success") return;
491
- const timer = setTimeout(() => setStatus(null), SUCCESS_TIMEOUT_MS);
834
+ const timer = setTimeout(
835
+ () => setStatus(null),
836
+ SUCCESS_TIMEOUT_MS
837
+ );
492
838
  return () => clearTimeout(timer);
493
839
  }, [status]);
840
+ (0, import_react.useEffect)(() => {
841
+ if (passkeyStatus?.type !== "success") return;
842
+ const timer = setTimeout(
843
+ () => setPasskeyStatus(null),
844
+ SUCCESS_TIMEOUT_MS
845
+ );
846
+ return () => clearTimeout(timer);
847
+ }, [passkeyStatus]);
494
848
  useFocusTrap(modalRef, phase === "entered", onClose);
495
849
  useScrollLock(phase !== "exited");
496
850
  const hasField = (0, import_react.useCallback)(
@@ -503,8 +857,23 @@ function DauthProfileModal({ open, onClose }) {
503
857
  );
504
858
  const hasChanges = (0, import_react.useMemo)(() => {
505
859
  if (!user?._id) return false;
506
- return name !== (user.name || "") || lastname !== (user.lastname || "") || nickname !== (user.nickname || "") || country !== (user.country || "");
507
- }, [name, lastname, nickname, country, user]);
860
+ const origBirthDate = user.birthDate ? new Date(user.birthDate).toISOString().split("T")[0] : "";
861
+ const cfChanged = (domain.customFields ?? []).some(
862
+ (f) => (customFieldValues[f.key] ?? "") !== (user.customFields?.[f.key] ?? "")
863
+ );
864
+ return name !== (user.name || "") || lastname !== (user.lastname || "") || nickname !== (user.nickname || "") || country !== (user.country || "") || telPrefix !== (user.telPrefix || "") || telSuffix !== (user.telSuffix || "") || birthDate !== origBirthDate || cfChanged;
865
+ }, [
866
+ name,
867
+ lastname,
868
+ nickname,
869
+ country,
870
+ telPrefix,
871
+ telSuffix,
872
+ birthDate,
873
+ customFieldValues,
874
+ user,
875
+ domain.customFields
876
+ ]);
508
877
  const canSave = name.trim().length > 0 && hasChanges && !saving;
509
878
  const handleSave = (0, import_react.useCallback)(async () => {
510
879
  setSaving(true);
@@ -513,6 +882,14 @@ function DauthProfileModal({ open, onClose }) {
513
882
  if (hasField("lastname")) fields.lastname = lastname;
514
883
  if (hasField("nickname")) fields.nickname = nickname;
515
884
  if (hasField("country")) fields.country = country;
885
+ if (hasField("tel_prefix"))
886
+ fields.telPrefix = telPrefix;
887
+ if (hasField("tel_suffix"))
888
+ fields.telSuffix = telSuffix;
889
+ if (hasField("birth_date") && birthDate)
890
+ fields.birthDate = birthDate;
891
+ if ((domain.customFields ?? []).length > 0)
892
+ fields.customFields = customFieldValues;
516
893
  const ok = await updateUser(fields);
517
894
  setSaving(false);
518
895
  if (ok) {
@@ -526,7 +903,19 @@ function DauthProfileModal({ open, onClose }) {
526
903
  message: "Something went wrong. Please try again."
527
904
  });
528
905
  }
529
- }, [name, lastname, nickname, country, hasField, updateUser]);
906
+ }, [
907
+ name,
908
+ lastname,
909
+ nickname,
910
+ country,
911
+ telPrefix,
912
+ telSuffix,
913
+ birthDate,
914
+ customFieldValues,
915
+ hasField,
916
+ updateUser,
917
+ domain.customFields
918
+ ]);
530
919
  const handleDelete = (0, import_react.useCallback)(async () => {
531
920
  setDeleting(true);
532
921
  const ok = await deleteAccount();
@@ -542,16 +931,87 @@ function DauthProfileModal({ open, onClose }) {
542
931
  setDeleteText("");
543
932
  }
544
933
  }, [deleteAccount, onClose]);
934
+ const handleLanguage = (0, import_react.useCallback)(
935
+ async (lang) => {
936
+ await updateUser({ language: lang });
937
+ },
938
+ [updateUser]
939
+ );
940
+ const handleRegisterPasskey = (0, import_react.useCallback)(async () => {
941
+ setRegistering(true);
942
+ setPasskeyStatus(null);
943
+ const cred = await registerPasskey(
944
+ passkeyName || void 0
945
+ );
946
+ setRegistering(false);
947
+ if (cred) {
948
+ setCredentials((prev) => [...prev, cred]);
949
+ setPasskeyName("");
950
+ setShowRegister(false);
951
+ setPasskeyStatus({
952
+ type: "success",
953
+ message: "Passkey registered successfully"
954
+ });
955
+ } else {
956
+ setPasskeyStatus({
957
+ type: "error",
958
+ message: "Failed to register passkey"
959
+ });
960
+ }
961
+ }, [passkeyName, registerPasskey]);
962
+ const handleDeletePasskey = (0, import_react.useCallback)(
963
+ async (credentialId) => {
964
+ const ok = await deletePasskeyCredential(credentialId);
965
+ if (ok) {
966
+ setCredentials(
967
+ (prev) => prev.filter((c) => c._id !== credentialId)
968
+ );
969
+ }
970
+ },
971
+ [deletePasskeyCredential]
972
+ );
973
+ const handleAvatarClick = (0, import_react.useCallback)(() => {
974
+ if (onAvatarUpload) {
975
+ avatarInputRef.current?.click();
976
+ }
977
+ }, [onAvatarUpload]);
978
+ const handleAvatarChange = (0, import_react.useCallback)(
979
+ async (e) => {
980
+ const file = e.target.files?.[0];
981
+ if (!file || !onAvatarUpload) return;
982
+ setUploadingAvatar(true);
983
+ try {
984
+ const url = await onAvatarUpload(file);
985
+ if (url) {
986
+ await updateUser({ avatar: url });
987
+ }
988
+ } catch {
989
+ }
990
+ setUploadingAvatar(false);
991
+ if (avatarInputRef.current) {
992
+ avatarInputRef.current.value = "";
993
+ }
994
+ },
995
+ [onAvatarUpload, updateUser]
996
+ );
997
+ const handleSignOut = (0, import_react.useCallback)(() => {
998
+ logout();
999
+ onClose();
1000
+ }, [logout, onClose]);
545
1001
  const themeVars = (0, import_react.useMemo)(() => {
546
1002
  const t = domain.modalTheme;
547
1003
  if (!t) return {};
548
1004
  const vars = {};
549
1005
  if (t.accent) vars["--dauth-accent"] = t.accent;
550
- if (t.accentHover) vars["--dauth-accent-hover"] = t.accentHover;
1006
+ if (t.accentHover)
1007
+ vars["--dauth-accent-hover"] = t.accentHover;
551
1008
  if (t.surface) vars["--dauth-surface"] = t.surface;
552
- if (t.surfaceHover) vars["--dauth-surface-hover"] = t.surfaceHover;
553
- if (t.textPrimary) vars["--dauth-text-primary"] = t.textPrimary;
554
- if (t.textSecondary) vars["--dauth-text-secondary"] = t.textSecondary;
1009
+ if (t.surfaceHover)
1010
+ vars["--dauth-surface-hover"] = t.surfaceHover;
1011
+ if (t.textPrimary)
1012
+ vars["--dauth-text-primary"] = t.textPrimary;
1013
+ if (t.textSecondary)
1014
+ vars["--dauth-text-secondary"] = t.textSecondary;
555
1015
  if (t.border) vars["--dauth-border"] = t.border;
556
1016
  return vars;
557
1017
  }, [domain.modalTheme]);
@@ -602,6 +1062,11 @@ function DauthProfileModal({ open, onClose }) {
602
1062
  transition: `transform ${dur}ms ${easing}`
603
1063
  };
604
1064
  const avatarInitial = (user.name || user.email || "?").charAt(0).toUpperCase();
1065
+ const tabs = [
1066
+ { key: "profile", label: "Profile" },
1067
+ ...showSecurity ? [{ key: "security", label: "Security" }] : [],
1068
+ { key: "account", label: "Account" }
1069
+ ];
605
1070
  return (0, import_react_dom.createPortal)(
606
1071
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
607
1072
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -645,191 +1110,663 @@ function DauthProfileModal({ open, onClose }) {
645
1110
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { id: "dauth-profile-title", style: titleStyle, children: "Your Profile" }),
646
1111
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { width: 36 } })
647
1112
  ] }),
1113
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: tabBar, role: "tablist", children: tabs.map((t) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1114
+ "button",
1115
+ {
1116
+ role: "tab",
1117
+ type: "button",
1118
+ "aria-selected": activeTab === t.key,
1119
+ style: {
1120
+ ...tabBtn,
1121
+ color: activeTab === t.key ? "var(--dauth-accent, #6366f1)" : "var(--dauth-text-secondary, #a1a1aa)",
1122
+ borderBottomColor: activeTab === t.key ? "var(--dauth-accent, #6366f1)" : "transparent"
1123
+ },
1124
+ onClick: () => setActiveTab(t.key),
1125
+ children: t.label
1126
+ },
1127
+ t.key
1128
+ )) }),
648
1129
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: bodyStyle, children: [
649
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: avatarSection, children: [
650
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: avatarCircle, children: user.avatar?.url ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
651
- "img",
652
- {
653
- src: user.avatar.url,
654
- alt: "",
655
- style: {
656
- width: "100%",
657
- height: "100%",
658
- objectFit: "cover"
659
- }
660
- }
661
- ) : avatarInitial }),
662
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: emailText, children: user.email })
663
- ] }),
664
- status && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
665
- "div",
666
- {
667
- role: "status",
668
- "aria-live": "polite",
669
- style: statusMsg(status.type),
670
- children: status.message
671
- }
672
- ),
673
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
674
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: fieldGroup, children: [
675
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "dauth-name", style: label, children: "Name *" }),
676
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
677
- "input",
1130
+ activeTab === "profile" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1131
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: avatarSection, children: [
1132
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1133
+ "div",
678
1134
  {
679
- id: "dauth-name",
680
- type: "text",
681
- value: name,
682
- onChange: (e) => setName(e.target.value),
683
- placeholder: "Your name",
684
- disabled: saving,
685
- style: input,
686
- onFocus: inputFocusHandler,
687
- onBlur: inputBlurHandler
1135
+ style: {
1136
+ ...avatarCircle,
1137
+ cursor: onAvatarUpload ? "pointer" : "default",
1138
+ position: "relative"
1139
+ },
1140
+ onClick: handleAvatarClick,
1141
+ children: [
1142
+ uploadingAvatar ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {}) : user.avatar?.url ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1143
+ "img",
1144
+ {
1145
+ src: user.avatar.url,
1146
+ alt: "",
1147
+ style: {
1148
+ width: "100%",
1149
+ height: "100%",
1150
+ objectFit: "cover"
1151
+ }
1152
+ }
1153
+ ) : avatarInitial,
1154
+ onAvatarUpload && !uploadingAvatar && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: avatarOverlay, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconCamera, {}) })
1155
+ ]
688
1156
  }
689
- )
690
- ] }),
691
- hasField("lastname") && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: fieldGroup, children: [
692
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { htmlFor: "dauth-lastname", style: label, children: [
693
- "Last name",
694
- isRequired("lastname") ? " *" : ""
695
- ] }),
696
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1157
+ ),
1158
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: emailText, children: user.email }),
1159
+ onAvatarUpload && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
697
1160
  "input",
698
1161
  {
699
- id: "dauth-lastname",
700
- type: "text",
701
- value: lastname,
702
- onChange: (e) => setLastname(e.target.value),
703
- placeholder: "Your last name",
704
- disabled: saving,
705
- style: input,
706
- onFocus: inputFocusHandler,
707
- onBlur: inputBlurHandler
1162
+ ref: avatarInputRef,
1163
+ type: "file",
1164
+ accept: "image/*",
1165
+ style: { display: "none" },
1166
+ onChange: handleAvatarChange
708
1167
  }
709
1168
  )
710
1169
  ] }),
711
- hasField("nickname") && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: fieldGroup, children: [
712
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { htmlFor: "dauth-nickname", style: label, children: [
713
- "Nickname",
714
- isRequired("nickname") ? " *" : ""
1170
+ status && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1171
+ "div",
1172
+ {
1173
+ role: "status",
1174
+ "aria-live": "polite",
1175
+ style: statusMsg(status.type),
1176
+ children: status.message
1177
+ }
1178
+ ),
1179
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1180
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: fieldGroup, children: [
1181
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1182
+ "label",
1183
+ {
1184
+ htmlFor: "dauth-name",
1185
+ style: label,
1186
+ children: "Name *"
1187
+ }
1188
+ ),
1189
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1190
+ "input",
1191
+ {
1192
+ id: "dauth-name",
1193
+ type: "text",
1194
+ value: name,
1195
+ onChange: (e) => setName(e.target.value),
1196
+ placeholder: "Your name",
1197
+ disabled: saving,
1198
+ style: input,
1199
+ onFocus: inputFocusHandler,
1200
+ onBlur: inputBlurHandler
1201
+ }
1202
+ )
715
1203
  ] }),
716
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
717
- "input",
718
- {
719
- id: "dauth-nickname",
720
- type: "text",
721
- value: nickname,
722
- onChange: (e) => setNickname(e.target.value),
723
- placeholder: "Choose a nickname",
724
- disabled: saving,
725
- style: input,
726
- onFocus: inputFocusHandler,
727
- onBlur: inputBlurHandler
728
- }
729
- )
730
- ] }),
731
- hasField("country") && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: fieldGroup, children: [
732
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { htmlFor: "dauth-country", style: label, children: [
733
- "Country",
734
- isRequired("country") ? " *" : ""
1204
+ hasField("lastname") && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: fieldGroup, children: [
1205
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1206
+ "label",
1207
+ {
1208
+ htmlFor: "dauth-lastname",
1209
+ style: label,
1210
+ children: [
1211
+ "Last name",
1212
+ isRequired("lastname") ? " *" : ""
1213
+ ]
1214
+ }
1215
+ ),
1216
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1217
+ "input",
1218
+ {
1219
+ id: "dauth-lastname",
1220
+ type: "text",
1221
+ value: lastname,
1222
+ onChange: (e) => setLastname(e.target.value),
1223
+ placeholder: "Your last name",
1224
+ disabled: saving,
1225
+ style: input,
1226
+ onFocus: inputFocusHandler,
1227
+ onBlur: inputBlurHandler
1228
+ }
1229
+ )
735
1230
  ] }),
736
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
737
- "input",
1231
+ hasField("nickname") && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: fieldGroup, children: [
1232
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1233
+ "label",
1234
+ {
1235
+ htmlFor: "dauth-nickname",
1236
+ style: label,
1237
+ children: [
1238
+ "Nickname",
1239
+ isRequired("nickname") ? " *" : ""
1240
+ ]
1241
+ }
1242
+ ),
1243
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1244
+ "input",
1245
+ {
1246
+ id: "dauth-nickname",
1247
+ type: "text",
1248
+ value: nickname,
1249
+ onChange: (e) => setNickname(e.target.value),
1250
+ placeholder: "Choose a nickname",
1251
+ disabled: saving,
1252
+ style: input,
1253
+ onFocus: inputFocusHandler,
1254
+ onBlur: inputBlurHandler
1255
+ }
1256
+ )
1257
+ ] }),
1258
+ hasField("country") && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: fieldGroup, children: [
1259
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1260
+ "label",
1261
+ {
1262
+ htmlFor: "dauth-country",
1263
+ style: label,
1264
+ children: [
1265
+ "Country",
1266
+ isRequired("country") ? " *" : ""
1267
+ ]
1268
+ }
1269
+ ),
1270
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1271
+ "input",
1272
+ {
1273
+ id: "dauth-country",
1274
+ type: "text",
1275
+ value: country,
1276
+ onChange: (e) => setCountry(e.target.value),
1277
+ placeholder: "Your country",
1278
+ disabled: saving,
1279
+ style: input,
1280
+ onFocus: inputFocusHandler,
1281
+ onBlur: inputBlurHandler
1282
+ }
1283
+ )
1284
+ ] }),
1285
+ (hasField("tel_prefix") || hasField("tel_suffix")) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: fieldGroup, children: [
1286
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: label, children: [
1287
+ "Phone",
1288
+ isRequired("tel_prefix") || isRequired("tel_suffix") ? " *" : ""
1289
+ ] }),
1290
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1291
+ "div",
1292
+ {
1293
+ style: {
1294
+ display: "flex",
1295
+ gap: 8
1296
+ },
1297
+ children: [
1298
+ hasField("tel_prefix") && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1299
+ "input",
1300
+ {
1301
+ id: "dauth-tel-prefix",
1302
+ type: "text",
1303
+ value: telPrefix,
1304
+ onChange: (e) => setTelPrefix(e.target.value),
1305
+ placeholder: "+34",
1306
+ disabled: saving,
1307
+ style: {
1308
+ ...input,
1309
+ width: 80,
1310
+ flexShrink: 0
1311
+ },
1312
+ onFocus: inputFocusHandler,
1313
+ onBlur: inputBlurHandler,
1314
+ "aria-label": "Phone prefix"
1315
+ }
1316
+ ),
1317
+ hasField("tel_suffix") && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1318
+ "input",
1319
+ {
1320
+ id: "dauth-tel-suffix",
1321
+ type: "tel",
1322
+ value: telSuffix,
1323
+ onChange: (e) => setTelSuffix(e.target.value),
1324
+ placeholder: "612 345 678",
1325
+ disabled: saving,
1326
+ style: {
1327
+ ...input,
1328
+ flex: 1
1329
+ },
1330
+ onFocus: inputFocusHandler,
1331
+ onBlur: inputBlurHandler,
1332
+ "aria-label": "Phone number"
1333
+ }
1334
+ )
1335
+ ]
1336
+ }
1337
+ )
1338
+ ] }),
1339
+ hasField("birth_date") && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: fieldGroup, children: [
1340
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1341
+ "label",
1342
+ {
1343
+ htmlFor: "dauth-birthdate",
1344
+ style: label,
1345
+ children: [
1346
+ "Birth date",
1347
+ isRequired("birth_date") ? " *" : ""
1348
+ ]
1349
+ }
1350
+ ),
1351
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1352
+ "input",
1353
+ {
1354
+ id: "dauth-birthdate",
1355
+ type: "date",
1356
+ value: birthDate,
1357
+ onChange: (e) => setBirthDate(e.target.value),
1358
+ disabled: saving,
1359
+ style: input,
1360
+ onFocus: inputFocusHandler,
1361
+ onBlur: inputBlurHandler
1362
+ }
1363
+ )
1364
+ ] }),
1365
+ (domain.customFields ?? []).length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1366
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("hr", { style: separator }),
1367
+ domain.customFields.map((cf) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1368
+ "div",
1369
+ {
1370
+ style: fieldGroup,
1371
+ children: [
1372
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1373
+ "label",
1374
+ {
1375
+ htmlFor: `dauth-cf-${cf.key}`,
1376
+ style: label,
1377
+ children: [
1378
+ cf.label,
1379
+ cf.required ? " *" : ""
1380
+ ]
1381
+ }
1382
+ ),
1383
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1384
+ "input",
1385
+ {
1386
+ id: `dauth-cf-${cf.key}`,
1387
+ type: "text",
1388
+ value: customFieldValues[cf.key] ?? "",
1389
+ onChange: (e) => setCustomFieldValues(
1390
+ (prev) => ({
1391
+ ...prev,
1392
+ [cf.key]: e.target.value
1393
+ })
1394
+ ),
1395
+ disabled: saving,
1396
+ style: input,
1397
+ onFocus: inputFocusHandler,
1398
+ onBlur: inputBlurHandler
1399
+ }
1400
+ )
1401
+ ]
1402
+ },
1403
+ cf.key
1404
+ ))
1405
+ ] })
1406
+ ] }),
1407
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("hr", { style: separator }),
1408
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: fieldGroup, children: [
1409
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: label, children: "Language" }),
1410
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", gap: 8 }, children: ["es", "en"].map((lang) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1411
+ "button",
738
1412
  {
739
- id: "dauth-country",
740
- type: "text",
741
- value: country,
742
- onChange: (e) => setCountry(e.target.value),
743
- placeholder: "Your country",
744
- disabled: saving,
745
- style: input,
746
- onFocus: inputFocusHandler,
747
- onBlur: inputBlurHandler
748
- }
749
- )
1413
+ type: "button",
1414
+ style: {
1415
+ ...langBtn,
1416
+ backgroundColor: user.language === lang ? "var(--dauth-accent, #6366f1)" : "var(--dauth-surface-secondary, rgba(255, 255, 255, 0.04))",
1417
+ color: user.language === lang ? "#ffffff" : "var(--dauth-text-secondary, #a1a1aa)"
1418
+ },
1419
+ onClick: () => handleLanguage(lang),
1420
+ children: lang === "es" ? "Espa\xF1ol" : "English"
1421
+ },
1422
+ lang
1423
+ )) })
750
1424
  ] })
751
1425
  ] }),
752
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("hr", { style: separator }),
753
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
754
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: dangerTitle, children: "Delete account" }),
755
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: dangerDesc, children: "Permanently delete your account and all associated data." }),
756
- !showDelete ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
757
- "button",
1426
+ activeTab === "security" && showSecurity && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1427
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1428
+ "div",
758
1429
  {
759
- type: "button",
760
- style: deleteBtn,
761
- onClick: () => setShowDelete(true),
762
- onMouseEnter: (e) => e.currentTarget.style.backgroundColor = "rgba(239, 68, 68, 0.2)",
763
- onMouseLeave: (e) => e.currentTarget.style.backgroundColor = "var(--dauth-error-bg, rgba(239, 68, 68, 0.1))",
764
- children: "Delete account"
1430
+ style: {
1431
+ display: "flex",
1432
+ alignItems: "center",
1433
+ justifyContent: "space-between",
1434
+ marginBottom: 16
1435
+ },
1436
+ children: [
1437
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1438
+ "div",
1439
+ {
1440
+ style: {
1441
+ ...label,
1442
+ marginBottom: 0,
1443
+ fontWeight: 600
1444
+ },
1445
+ children: "Passkeys"
1446
+ }
1447
+ ),
1448
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1449
+ "button",
1450
+ {
1451
+ type: "button",
1452
+ style: outlineBtn,
1453
+ onClick: () => setShowRegister(!showRegister),
1454
+ onMouseEnter: (e) => e.currentTarget.style.backgroundColor = "var(--dauth-surface-hover, #232340)",
1455
+ onMouseLeave: (e) => e.currentTarget.style.backgroundColor = "transparent",
1456
+ children: "+ Add passkey"
1457
+ }
1458
+ )
1459
+ ]
765
1460
  }
766
- ) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: deletePanel, children: [
767
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: deletePanelText, children: [
768
- "This action is permanent and cannot be undone. Type",
769
- " ",
770
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: CONFIRM_WORD }),
771
- " to confirm."
1461
+ ),
1462
+ showRegister && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: registerPanel, children: [
1463
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: fieldGroup, children: [
1464
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1465
+ "label",
1466
+ {
1467
+ htmlFor: "dauth-passkey-name",
1468
+ style: label,
1469
+ children: "Passkey name (optional)"
1470
+ }
1471
+ ),
1472
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1473
+ "input",
1474
+ {
1475
+ id: "dauth-passkey-name",
1476
+ type: "text",
1477
+ value: passkeyName,
1478
+ onChange: (e) => setPasskeyName(e.target.value),
1479
+ placeholder: "e.g. MacBook Touch ID",
1480
+ disabled: registering,
1481
+ style: input,
1482
+ onFocus: inputFocusHandler,
1483
+ onBlur: inputBlurHandler
1484
+ }
1485
+ )
772
1486
  ] }),
773
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
774
- "input",
775
- {
776
- type: "text",
777
- value: deleteText,
778
- onChange: (e) => setDeleteText(e.target.value),
779
- placeholder: `Type ${CONFIRM_WORD}`,
780
- style: input,
781
- onFocus: inputFocusHandler,
782
- onBlur: inputBlurHandler,
783
- disabled: deleting
784
- }
785
- ),
786
1487
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
787
1488
  "div",
788
1489
  {
789
1490
  style: {
790
1491
  display: "flex",
791
- gap: 8,
792
- marginTop: 12
1492
+ gap: 8
793
1493
  },
794
1494
  children: [
795
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1495
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
796
1496
  "button",
797
1497
  {
798
1498
  type: "button",
799
- style: cancelBtn,
800
- onClick: () => {
801
- setShowDelete(false);
802
- setDeleteText("");
1499
+ style: {
1500
+ ...smallAccentBtn,
1501
+ opacity: registering ? 0.6 : 1
803
1502
  },
804
- onMouseEnter: (e) => e.currentTarget.style.backgroundColor = "var(--dauth-surface-hover, #232340)",
805
- onMouseLeave: (e) => e.currentTarget.style.backgroundColor = "transparent",
806
- children: "Cancel"
1503
+ disabled: registering,
1504
+ onClick: handleRegisterPasskey,
1505
+ children: [
1506
+ registering ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconFingerprint, {}),
1507
+ registering ? "Registering..." : "Register"
1508
+ ]
807
1509
  }
808
1510
  ),
809
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1511
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
810
1512
  "button",
811
1513
  {
812
1514
  type: "button",
813
- style: {
814
- ...deleteConfirmBtn,
815
- opacity: deleteText !== CONFIRM_WORD || deleting ? 0.5 : 1,
816
- cursor: deleteText !== CONFIRM_WORD || deleting ? "not-allowed" : "pointer"
817
- },
818
- disabled: deleteText !== CONFIRM_WORD || deleting,
819
- onClick: handleDelete,
820
- children: [
821
- deleting && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {}),
822
- "Delete my account"
823
- ]
1515
+ style: cancelBtn,
1516
+ onClick: () => setShowRegister(false),
1517
+ onMouseEnter: (e) => e.currentTarget.style.backgroundColor = "var(--dauth-surface-hover, #232340)",
1518
+ onMouseLeave: (e) => e.currentTarget.style.backgroundColor = "transparent",
1519
+ children: "Cancel"
824
1520
  }
825
1521
  )
826
1522
  ]
827
1523
  }
828
1524
  )
829
- ] })
1525
+ ] }),
1526
+ passkeyStatus && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1527
+ "div",
1528
+ {
1529
+ role: "status",
1530
+ "aria-live": "polite",
1531
+ style: {
1532
+ ...statusMsg(passkeyStatus.type),
1533
+ marginTop: 12
1534
+ },
1535
+ children: passkeyStatus.message
1536
+ }
1537
+ ),
1538
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { marginTop: 12 }, children: loadingCreds ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1539
+ "div",
1540
+ {
1541
+ style: {
1542
+ textAlign: "center",
1543
+ padding: 24
1544
+ },
1545
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})
1546
+ }
1547
+ ) : credentials.length > 0 ? credentials.map((cred) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1548
+ "div",
1549
+ {
1550
+ style: credentialRow,
1551
+ children: [
1552
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1553
+ "div",
1554
+ {
1555
+ style: {
1556
+ display: "flex",
1557
+ alignItems: "center",
1558
+ gap: 12,
1559
+ flex: 1,
1560
+ minWidth: 0
1561
+ },
1562
+ children: [
1563
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1564
+ "span",
1565
+ {
1566
+ style: {
1567
+ color: "var(--dauth-accent, #6366f1)",
1568
+ flexShrink: 0
1569
+ },
1570
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconFingerprint, {})
1571
+ }
1572
+ ),
1573
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1574
+ "div",
1575
+ {
1576
+ style: {
1577
+ minWidth: 0,
1578
+ flex: 1
1579
+ },
1580
+ children: [
1581
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1582
+ "div",
1583
+ {
1584
+ style: {
1585
+ fontSize: "var(--dauth-font-size-sm, 0.875rem)",
1586
+ fontWeight: 500,
1587
+ color: "var(--dauth-text-primary, #e4e4e7)",
1588
+ overflow: "hidden",
1589
+ textOverflow: "ellipsis",
1590
+ whiteSpace: "nowrap"
1591
+ },
1592
+ children: cred.name || "Passkey"
1593
+ }
1594
+ ),
1595
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1596
+ "div",
1597
+ {
1598
+ style: {
1599
+ fontSize: "var(--dauth-font-size-xs, 0.75rem)",
1600
+ color: "var(--dauth-text-muted, #71717a)"
1601
+ },
1602
+ children: [
1603
+ cred.deviceType === "multiDevice" ? "Synced" : "Device-bound",
1604
+ cred.createdAt && ` \xB7 Created ${new Date(cred.createdAt).toLocaleDateString()}`
1605
+ ]
1606
+ }
1607
+ )
1608
+ ]
1609
+ }
1610
+ )
1611
+ ]
1612
+ }
1613
+ ),
1614
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1615
+ "button",
1616
+ {
1617
+ type: "button",
1618
+ onClick: () => handleDeletePasskey(cred._id),
1619
+ style: trashBtn,
1620
+ onMouseEnter: (e) => e.currentTarget.style.color = "var(--dauth-error, #ef4444)",
1621
+ onMouseLeave: (e) => e.currentTarget.style.color = "var(--dauth-text-muted, #71717a)",
1622
+ "aria-label": `Delete passkey ${cred.name || ""}`,
1623
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconTrash, {})
1624
+ }
1625
+ )
1626
+ ]
1627
+ },
1628
+ cred._id
1629
+ )) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: emptyState, children: [
1630
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1631
+ "span",
1632
+ {
1633
+ style: {
1634
+ color: "var(--dauth-accent, #6366f1)"
1635
+ },
1636
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconShield, {})
1637
+ }
1638
+ ),
1639
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1640
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1641
+ "div",
1642
+ {
1643
+ style: {
1644
+ fontSize: "var(--dauth-font-size-sm, 0.875rem)",
1645
+ fontWeight: 500,
1646
+ color: "var(--dauth-text-primary, #e4e4e7)"
1647
+ },
1648
+ children: "No passkeys registered"
1649
+ }
1650
+ ),
1651
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1652
+ "div",
1653
+ {
1654
+ style: {
1655
+ fontSize: "var(--dauth-font-size-xs, 0.75rem)",
1656
+ color: "var(--dauth-text-secondary, #a1a1aa)"
1657
+ },
1658
+ children: "Add a passkey for faster, more secure sign-in."
1659
+ }
1660
+ )
1661
+ ] })
1662
+ ] }) })
1663
+ ] }),
1664
+ activeTab === "account" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1665
+ status && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1666
+ "div",
1667
+ {
1668
+ role: "status",
1669
+ "aria-live": "polite",
1670
+ style: statusMsg(status.type),
1671
+ children: status.message
1672
+ }
1673
+ ),
1674
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1675
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: dangerTitle, children: "Delete account" }),
1676
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: dangerDesc, children: "Permanently delete your account and all associated data." }),
1677
+ !showDelete ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1678
+ "button",
1679
+ {
1680
+ type: "button",
1681
+ style: deleteBtn,
1682
+ onClick: () => setShowDelete(true),
1683
+ onMouseEnter: (e) => e.currentTarget.style.backgroundColor = "rgba(239, 68, 68, 0.2)",
1684
+ onMouseLeave: (e) => e.currentTarget.style.backgroundColor = "var(--dauth-error-bg, rgba(239, 68, 68, 0.1))",
1685
+ children: "Delete account"
1686
+ }
1687
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: deletePanel, children: [
1688
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: deletePanelText, children: [
1689
+ "This action is permanent and cannot be undone. Type",
1690
+ " ",
1691
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: CONFIRM_WORD }),
1692
+ " to confirm."
1693
+ ] }),
1694
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1695
+ "input",
1696
+ {
1697
+ type: "text",
1698
+ value: deleteText,
1699
+ onChange: (e) => setDeleteText(e.target.value),
1700
+ placeholder: `Type ${CONFIRM_WORD}`,
1701
+ style: input,
1702
+ onFocus: inputFocusHandler,
1703
+ onBlur: inputBlurHandler,
1704
+ disabled: deleting
1705
+ }
1706
+ ),
1707
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1708
+ "div",
1709
+ {
1710
+ style: {
1711
+ display: "flex",
1712
+ gap: 8,
1713
+ marginTop: 12
1714
+ },
1715
+ children: [
1716
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1717
+ "button",
1718
+ {
1719
+ type: "button",
1720
+ style: cancelBtn,
1721
+ onClick: () => {
1722
+ setShowDelete(false);
1723
+ setDeleteText("");
1724
+ },
1725
+ onMouseEnter: (e) => e.currentTarget.style.backgroundColor = "var(--dauth-surface-hover, #232340)",
1726
+ onMouseLeave: (e) => e.currentTarget.style.backgroundColor = "transparent",
1727
+ children: "Cancel"
1728
+ }
1729
+ ),
1730
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1731
+ "button",
1732
+ {
1733
+ type: "button",
1734
+ style: {
1735
+ ...deleteConfirmBtn,
1736
+ opacity: deleteText !== CONFIRM_WORD || deleting ? 0.5 : 1,
1737
+ cursor: deleteText !== CONFIRM_WORD || deleting ? "not-allowed" : "pointer"
1738
+ },
1739
+ disabled: deleteText !== CONFIRM_WORD || deleting,
1740
+ onClick: handleDelete,
1741
+ children: [
1742
+ deleting && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {}),
1743
+ "Delete my account"
1744
+ ]
1745
+ }
1746
+ )
1747
+ ]
1748
+ }
1749
+ )
1750
+ ] })
1751
+ ] }),
1752
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("hr", { style: separator }),
1753
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1754
+ "button",
1755
+ {
1756
+ type: "button",
1757
+ style: signOutBtn,
1758
+ onClick: handleSignOut,
1759
+ onMouseEnter: (e) => e.currentTarget.style.backgroundColor = "rgba(239, 68, 68, 0.2)",
1760
+ onMouseLeave: (e) => e.currentTarget.style.backgroundColor = "var(--dauth-error-bg, rgba(239, 68, 68, 0.1))",
1761
+ children: [
1762
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconLogOut, {}),
1763
+ "Sign out"
1764
+ ]
1765
+ }
1766
+ )
830
1767
  ] })
831
1768
  ] }),
832
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: footerStyle(isDesktop), children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1769
+ activeTab === "profile" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: footerStyle(isDesktop), children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
833
1770
  "button",
834
1771
  {
835
1772
  type: "button",
@@ -867,8 +1804,7 @@ var headerStyle = (isDesktop) => ({
867
1804
  display: "flex",
868
1805
  alignItems: "center",
869
1806
  justifyContent: "space-between",
870
- padding: "16px 24px",
871
- borderBottom: "1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))",
1807
+ padding: "16px 24px 0",
872
1808
  flexShrink: 0,
873
1809
  ...!isDesktop ? {
874
1810
  position: "sticky",
@@ -899,6 +1835,25 @@ var closeBtn = {
899
1835
  transition: "background-color 150ms, color 150ms",
900
1836
  padding: 0
901
1837
  };
1838
+ var tabBar = {
1839
+ display: "flex",
1840
+ padding: "0 24px",
1841
+ borderBottom: "1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))",
1842
+ flexShrink: 0
1843
+ };
1844
+ var tabBtn = {
1845
+ flex: 1,
1846
+ padding: "12px 4px",
1847
+ fontSize: "var(--dauth-font-size-sm, 0.875rem)",
1848
+ fontWeight: 500,
1849
+ border: "none",
1850
+ borderBottom: "2px solid transparent",
1851
+ backgroundColor: "transparent",
1852
+ cursor: "pointer",
1853
+ transition: "color 150ms, border-color 150ms",
1854
+ fontFamily: "inherit",
1855
+ textAlign: "center"
1856
+ };
902
1857
  var bodyStyle = {
903
1858
  flex: 1,
904
1859
  overflowY: "auto",
@@ -925,6 +1880,18 @@ var avatarCircle = {
925
1880
  fontSize: "1.5rem",
926
1881
  fontWeight: 600
927
1882
  };
1883
+ var avatarOverlay = {
1884
+ position: "absolute",
1885
+ inset: 0,
1886
+ backgroundColor: "rgba(0, 0, 0, 0.4)",
1887
+ display: "flex",
1888
+ alignItems: "center",
1889
+ justifyContent: "center",
1890
+ borderRadius: "50%",
1891
+ opacity: 0.7,
1892
+ transition: "opacity 150ms",
1893
+ color: "#ffffff"
1894
+ };
928
1895
  var emailText = {
929
1896
  fontSize: "var(--dauth-font-size-sm, 0.875rem)",
930
1897
  color: "var(--dauth-text-secondary, #a1a1aa)"
@@ -939,7 +1906,9 @@ var statusMsg = (type) => ({
939
1906
  textAlign: "center",
940
1907
  lineHeight: 1.5
941
1908
  });
942
- var fieldGroup = { marginBottom: 16 };
1909
+ var fieldGroup = {
1910
+ marginBottom: 16
1911
+ };
943
1912
  var label = {
944
1913
  display: "block",
945
1914
  fontSize: "var(--dauth-font-size-sm, 0.875rem)",
@@ -969,12 +1938,88 @@ var inputBlurHandler = (e) => {
969
1938
  e.currentTarget.style.borderColor = "var(--dauth-border, rgba(255, 255, 255, 0.08))";
970
1939
  e.currentTarget.style.boxShadow = "none";
971
1940
  };
1941
+ var langBtn = {
1942
+ padding: "8px 16px",
1943
+ fontSize: "var(--dauth-font-size-sm, 0.875rem)",
1944
+ fontWeight: 500,
1945
+ border: "none",
1946
+ borderRadius: "var(--dauth-radius-sm, 8px)",
1947
+ cursor: "pointer",
1948
+ transition: "background-color 150ms, color 150ms",
1949
+ fontFamily: "inherit"
1950
+ };
972
1951
  var separator = {
973
1952
  height: 1,
974
1953
  backgroundColor: "var(--dauth-border, rgba(255, 255, 255, 0.08))",
975
1954
  margin: "24px 0",
976
1955
  border: "none"
977
1956
  };
1957
+ var outlineBtn = {
1958
+ padding: "6px 12px",
1959
+ fontSize: "var(--dauth-font-size-xs, 0.75rem)",
1960
+ fontWeight: 500,
1961
+ color: "var(--dauth-text-secondary, #a1a1aa)",
1962
+ backgroundColor: "transparent",
1963
+ border: "1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))",
1964
+ borderRadius: "var(--dauth-radius-sm, 8px)",
1965
+ cursor: "pointer",
1966
+ transition: "background-color 150ms",
1967
+ fontFamily: "inherit"
1968
+ };
1969
+ var registerPanel = {
1970
+ padding: 16,
1971
+ borderRadius: "var(--dauth-radius-sm, 8px)",
1972
+ border: "1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))",
1973
+ backgroundColor: "var(--dauth-surface-secondary, rgba(255, 255, 255, 0.04))",
1974
+ marginBottom: 12
1975
+ };
1976
+ var smallAccentBtn = {
1977
+ padding: "8px 16px",
1978
+ fontSize: "var(--dauth-font-size-sm, 0.875rem)",
1979
+ fontWeight: 500,
1980
+ color: "#ffffff",
1981
+ backgroundColor: "var(--dauth-accent, #6366f1)",
1982
+ border: "none",
1983
+ borderRadius: "var(--dauth-radius-sm, 8px)",
1984
+ cursor: "pointer",
1985
+ transition: "opacity 150ms",
1986
+ fontFamily: "inherit",
1987
+ display: "flex",
1988
+ alignItems: "center",
1989
+ gap: 6
1990
+ };
1991
+ var credentialRow = {
1992
+ display: "flex",
1993
+ alignItems: "center",
1994
+ justifyContent: "space-between",
1995
+ padding: 12,
1996
+ borderRadius: "var(--dauth-radius-sm, 8px)",
1997
+ backgroundColor: "var(--dauth-surface-secondary, rgba(255, 255, 255, 0.04))",
1998
+ marginBottom: 8
1999
+ };
2000
+ var trashBtn = {
2001
+ display: "flex",
2002
+ alignItems: "center",
2003
+ justifyContent: "center",
2004
+ width: 28,
2005
+ height: 28,
2006
+ border: "none",
2007
+ backgroundColor: "transparent",
2008
+ color: "var(--dauth-text-muted, #71717a)",
2009
+ cursor: "pointer",
2010
+ transition: "color 150ms",
2011
+ padding: 0,
2012
+ borderRadius: "var(--dauth-radius-sm, 8px)",
2013
+ flexShrink: 0
2014
+ };
2015
+ var emptyState = {
2016
+ display: "flex",
2017
+ alignItems: "center",
2018
+ gap: 12,
2019
+ padding: 16,
2020
+ borderRadius: "var(--dauth-radius-sm, 8px)",
2021
+ backgroundColor: "var(--dauth-surface-secondary, rgba(255, 255, 255, 0.04))"
2022
+ };
978
2023
  var dangerTitle = {
979
2024
  fontSize: "var(--dauth-font-size-sm, 0.875rem)",
980
2025
  fontWeight: 600,
@@ -1013,7 +2058,6 @@ var deletePanelText = {
1013
2058
  lineHeight: 1.5
1014
2059
  };
1015
2060
  var cancelBtn = {
1016
- flex: 1,
1017
2061
  padding: "8px 16px",
1018
2062
  fontSize: "var(--dauth-font-size-sm, 0.875rem)",
1019
2063
  fontWeight: 500,
@@ -1042,6 +2086,23 @@ var deleteConfirmBtn = {
1042
2086
  justifyContent: "center",
1043
2087
  gap: 8
1044
2088
  };
2089
+ var signOutBtn = {
2090
+ width: "100%",
2091
+ padding: "12px 24px",
2092
+ fontSize: "var(--dauth-font-size-base, 1rem)",
2093
+ fontWeight: 500,
2094
+ color: "var(--dauth-error, #ef4444)",
2095
+ backgroundColor: "var(--dauth-error-bg, rgba(239, 68, 68, 0.1))",
2096
+ border: "1px solid rgba(239, 68, 68, 0.2)",
2097
+ borderRadius: "var(--dauth-radius-sm, 8px)",
2098
+ cursor: "pointer",
2099
+ transition: "background-color 150ms",
2100
+ fontFamily: "inherit",
2101
+ display: "flex",
2102
+ alignItems: "center",
2103
+ justifyContent: "center",
2104
+ gap: 8
2105
+ };
1045
2106
  var footerStyle = (isDesktop) => ({
1046
2107
  padding: "16px 24px",
1047
2108
  borderTop: "1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))",
@@ -1153,15 +2214,39 @@ var DauthProvider = (props) => {
1153
2214
  () => deleteAccountAction(ctx),
1154
2215
  [ctx]
1155
2216
  );
2217
+ const getPasskeyCredentials = (0, import_react2.useCallback)(
2218
+ () => getPasskeyCredentialsAction(ctx),
2219
+ [ctx]
2220
+ );
2221
+ const registerPasskey = (0, import_react2.useCallback)(
2222
+ (name) => registerPasskeyAction(ctx, name),
2223
+ [ctx]
2224
+ );
2225
+ const deletePasskeyCredential = (0, import_react2.useCallback)(
2226
+ (credentialId) => deletePasskeyCredentialAction(ctx, credentialId),
2227
+ [ctx]
2228
+ );
1156
2229
  const memoProvider = (0, import_react2.useMemo)(
1157
2230
  () => ({
1158
2231
  ...dauthState,
1159
2232
  loginWithRedirect,
1160
2233
  logout,
1161
2234
  updateUser,
1162
- deleteAccount
2235
+ deleteAccount,
2236
+ getPasskeyCredentials,
2237
+ registerPasskey,
2238
+ deletePasskeyCredential
1163
2239
  }),
1164
- [dauthState, loginWithRedirect, logout, updateUser, deleteAccount]
2240
+ [
2241
+ dauthState,
2242
+ loginWithRedirect,
2243
+ logout,
2244
+ updateUser,
2245
+ deleteAccount,
2246
+ getPasskeyCredentials,
2247
+ registerPasskey,
2248
+ deletePasskeyCredential
2249
+ ]
1165
2250
  );
1166
2251
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(DauthContext.Provider, { value: memoProvider, children });
1167
2252
  };