@cavos/kit 0.0.3 → 0.0.5

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.
@@ -1,4 +1,4 @@
1
- import { CavosAuth, HttpRecoveryClient, Cavos, generateRecoveryCode, CavosSolana } from '../chunk-BNGLH3Q3.mjs';
1
+ import { CavosAuth, PasskeySigner, HttpRecoveryClient, Cavos, PasskeyPrf, generateRecoveryCode, CavosSolana } from '../chunk-M5BGBODC.mjs';
2
2
  import { createContext, useState, useRef, useEffect, useCallback, useContext } from 'react';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
 
@@ -105,6 +105,17 @@ var EmailIcon = () => /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", v
105
105
  /* @__PURE__ */ jsx("rect", { x: "2", y: "4", width: "20", height: "16", rx: "2" }),
106
106
  /* @__PURE__ */ jsx("path", { d: "m22 7-8.97 5.7a1.94 1.94 0 01-2.06 0L2 7" })
107
107
  ] });
108
+ var FingerprintIcon = ({ size = 22, color = "currentColor" }) => /* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", children: [
109
+ /* @__PURE__ */ jsx("path", { d: "M2 12C2 6.5 6.5 2 12 2a10 10 0 0 1 8 4" }),
110
+ /* @__PURE__ */ jsx("path", { d: "M5 19.5C5.5 18 6 15 6 12c0-.7.12-1.37.34-2" }),
111
+ /* @__PURE__ */ jsx("path", { d: "M17.29 21.02c.12-.6.43-2.3.5-3.02" }),
112
+ /* @__PURE__ */ jsx("path", { d: "M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4" }),
113
+ /* @__PURE__ */ jsx("path", { d: "M8.65 22c.21-.66.45-1.32.57-2" }),
114
+ /* @__PURE__ */ jsx("path", { d: "M14 13.12c0 2.38 0 6.38-1 8.88" }),
115
+ /* @__PURE__ */ jsx("path", { d: "M2 16h.01" }),
116
+ /* @__PURE__ */ jsx("path", { d: "M21.8 16c.2-2 .131-5.354 0-6" }),
117
+ /* @__PURE__ */ jsx("path", { d: "M9 6.8a6 6 0 0 1 9 5.2c0 .47 0 1.17-.02 2" })
118
+ ] });
108
119
  var CavosLogo = ({ invert }) => /* @__PURE__ */ jsx("img", { src: `data:image/png;base64,${cavosLogoBase64}`, alt: "Cavos", style: { width: "auto", height: "16px", objectFit: "contain", opacity: 0.55, flexShrink: 0, filter: invert ? "invert(1)" : "none" } });
109
120
  function Spinner({ size = 16, color = "#888" }) {
110
121
  return /* @__PURE__ */ jsxs(
@@ -255,7 +266,11 @@ function CavosAuthModal({
255
266
  authError,
256
267
  clearAuthError,
257
268
  resendDeviceApproval,
258
- recover
269
+ recover,
270
+ passkeySupported,
271
+ enrollPasskeyDefault,
272
+ approveDeviceWithPasskey,
273
+ setupRecovery
259
274
  } = useCavos();
260
275
  const isMobile = useIsMobile();
261
276
  const isLight = theme !== "dark";
@@ -283,7 +298,12 @@ function CavosAuthModal({
283
298
  const [deviceResendBusy, setDeviceResendBusy] = useState(false);
284
299
  const [recoverCode, setRecoverCode] = useState("");
285
300
  const [recoverBusy, setRecoverBusy] = useState(false);
301
+ const [pkBusy, setPkBusy] = useState(false);
302
+ const [pkError, setPkError] = useState("");
303
+ const [savedRecoveryCode, setSavedRecoveryCode] = useState("");
304
+ const [copied, setCopied] = useState(false);
286
305
  const doneHandledRef = useRef(false);
306
+ const secureHandledRef = useRef(false);
287
307
  const closeTimerRef = useRef(null);
288
308
  const otpBoxRefs = useRef([null, null, null, null, null, null]);
289
309
  const setOtpDigit = (i, raw) => {
@@ -344,6 +364,7 @@ function CavosAuthModal({
344
364
  setOtpCode("");
345
365
  setError("");
346
366
  doneHandledRef.current = false;
367
+ secureHandledRef.current = false;
347
368
  }, 1600);
348
369
  }, [onClose]);
349
370
  useEffect(() => {
@@ -354,13 +375,26 @@ function CavosAuthModal({
354
375
  doneHandledRef.current = false;
355
376
  }
356
377
  if (isAuthenticated && address && walletStatus.isReady) {
357
- triggerDone(address);
378
+ if (walletStatus.isNewAccount && !secureHandledRef.current) {
379
+ if (screen !== "secure-account" && screen !== "recovery-code" && !doneHandledRef.current) {
380
+ setScreen("secure-account");
381
+ }
382
+ } else {
383
+ triggerDone(address);
384
+ }
358
385
  }
359
- if (walletStatus.awaitingApproval && screen !== "device-approval" && screen !== "recover") {
360
- setScreen("device-approval");
361
- doneHandledRef.current = false;
386
+ if (walletStatus.needsDeviceApproval && // A deploy in progress owns the screen; never fight the deploying branch
387
+ // above (both flags true would oscillate deploying ↔ approval → loop).
388
+ !walletStatus.isDeploying && screen !== "recover" && screen !== "device-approval" && screen !== "passkey-approval") {
389
+ if (walletStatus.hasPasskey && passkeySupported) {
390
+ setScreen("passkey-approval");
391
+ doneHandledRef.current = false;
392
+ } else if (walletStatus.awaitingApproval) {
393
+ setScreen("device-approval");
394
+ doneHandledRef.current = false;
395
+ }
362
396
  }
363
- }, [open, isAuthenticated, address, walletStatus.isReady, walletStatus.isDeploying, walletStatus.awaitingApproval, screen, triggerDone]);
397
+ }, [open, isAuthenticated, address, walletStatus.isReady, walletStatus.isDeploying, walletStatus.awaitingApproval, walletStatus.needsDeviceApproval, walletStatus.hasPasskey, walletStatus.isNewAccount, passkeySupported, screen, triggerDone]);
364
398
  useEffect(() => () => {
365
399
  if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
366
400
  }, []);
@@ -370,9 +404,61 @@ function CavosAuthModal({
370
404
  setEmail("");
371
405
  setOtpCode("");
372
406
  setError("");
407
+ setPkError("");
408
+ setSavedRecoveryCode("");
409
+ setCopied(false);
373
410
  doneHandledRef.current = false;
411
+ secureHandledRef.current = false;
374
412
  onClose();
375
413
  };
414
+ const finishSecureStep = () => {
415
+ secureHandledRef.current = true;
416
+ if (address) triggerDone(address);
417
+ };
418
+ const handleSetupPasskey = async () => {
419
+ setPkBusy(true);
420
+ setPkError("");
421
+ try {
422
+ await enrollPasskeyDefault();
423
+ finishSecureStep();
424
+ } catch (e) {
425
+ setPkError(e instanceof Error ? e.message : "We couldn't set up your passkey. Try again.");
426
+ } finally {
427
+ setPkBusy(false);
428
+ }
429
+ };
430
+ const handleSaveRecovery = async () => {
431
+ setPkBusy(true);
432
+ setPkError("");
433
+ try {
434
+ const code = await setupRecovery();
435
+ setSavedRecoveryCode(code);
436
+ setScreen("recovery-code");
437
+ } catch (e) {
438
+ setPkError(e instanceof Error ? e.message : "We couldn't create your recovery phrase. Try again.");
439
+ } finally {
440
+ setPkBusy(false);
441
+ }
442
+ };
443
+ const handleCopyRecovery = async () => {
444
+ try {
445
+ await navigator.clipboard.writeText(savedRecoveryCode);
446
+ setCopied(true);
447
+ setTimeout(() => setCopied(false), 2e3);
448
+ } catch {
449
+ }
450
+ };
451
+ const handlePasskeyApprove = async () => {
452
+ setPkBusy(true);
453
+ setPkError("");
454
+ try {
455
+ await approveDeviceWithPasskey();
456
+ setPkBusy(false);
457
+ } catch (e) {
458
+ setPkError(e instanceof Error ? e.message : "We couldn't verify your passkey. Try again or use email.");
459
+ setPkBusy(false);
460
+ }
461
+ };
376
462
  const handleOAuth = async (provider) => {
377
463
  setError("");
378
464
  setBusy(true);
@@ -463,6 +549,85 @@ function CavosAuthModal({
463
549
  setDeviceResendBusy(false);
464
550
  }
465
551
  };
552
+ if (screen === "secure-account") {
553
+ return /* @__PURE__ */ jsx("div", { style: overlay, "data-cavos-theme": theme, role: "dialog", "aria-modal": true, children: /* @__PURE__ */ jsxs("div", { style: { ...card, position: "relative" }, children: [
554
+ isMobile && /* @__PURE__ */ jsx("div", { style: handle }),
555
+ /* @__PURE__ */ jsxs("div", { style: { padding: isMobile ? "28px 24px 28px" : "44px 24px 28px", textAlign: "center" }, children: [
556
+ /* @__PURE__ */ jsx("div", { style: { width: 48, height: 48, borderRadius: "50%", margin: "0 auto 16px", background: isLight ? "rgba(0,0,0,0.04)" : "rgba(255,255,255,0.06)", border: `1px solid ${isLight ? "rgba(0,0,0,0.08)" : "rgba(255,255,255,0.1)"}`, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: textColor, strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", children: [
557
+ /* @__PURE__ */ jsx("path", { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" }),
558
+ /* @__PURE__ */ jsx("path", { d: "M9 12l2 2 4-4" })
559
+ ] }) }),
560
+ /* @__PURE__ */ jsx("h2", { style: { margin: "0 0 8px", fontSize: "17px", fontWeight: 600, color: textColor, letterSpacing: "-0.02em" }, children: "Keep your account safe" }),
561
+ /* @__PURE__ */ jsx("p", { style: { margin: "0 0 24px", fontSize: "13px", color: subTextColor, lineHeight: 1.55 }, children: "Set up a passkey so you can sign in on a new phone or computer in one tap. It takes a few seconds." }),
562
+ pkError && /* @__PURE__ */ jsx("div", { style: { background: errBg, border: `1px solid ${errBorder}`, borderRadius: "10px", padding: "9px 13px", fontSize: "13px", color: errColor, marginBottom: "14px", textAlign: "left" }, children: pkError }),
563
+ passkeySupported ? /* @__PURE__ */ jsxs(Fragment, { children: [
564
+ /* @__PURE__ */ jsxs("button", { className: "cavos-primary-btn", style: { width: "100%", padding: "13px", borderRadius: "12px", border: "none", background: primaryColor, color: "#fff", fontSize: "14px", fontWeight: 600, cursor: pkBusy ? "default" : "pointer", fontFamily: "inherit", display: "flex", alignItems: "center", justifyContent: "center", gap: "9px", opacity: pkBusy ? 0.65 : 1, transition: "opacity 0.15s, transform 0.1s" }, onClick: handleSetupPasskey, disabled: pkBusy, children: [
565
+ pkBusy ? /* @__PURE__ */ jsx(Spinner, { size: 16, color: "#fff" }) : /* @__PURE__ */ jsx(FingerprintIcon, { size: 18, color: "#fff" }),
566
+ pkBusy ? "Setting up\u2026" : "Set up a passkey"
567
+ ] }),
568
+ /* @__PURE__ */ jsx("p", { style: { margin: "10px 0 0", fontSize: "12px", color: subTextColor, lineHeight: 1.5 }, children: "Uses Face ID, Touch ID, or your device PIN." }),
569
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "12px", margin: "18px 0" }, children: [
570
+ /* @__PURE__ */ jsx("span", { className: "cavos-divider-line", style: { background: isLight ? "rgba(0,0,0,0.08)" : "rgba(255,255,255,0.1)" } }),
571
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "11px", color: subTextColor, textTransform: "uppercase", letterSpacing: "0.06em" }, children: "or" }),
572
+ /* @__PURE__ */ jsx("span", { className: "cavos-divider-line", style: { background: isLight ? "rgba(0,0,0,0.08)" : "rgba(255,255,255,0.1)" } })
573
+ ] }),
574
+ /* @__PURE__ */ jsx("button", { className: "cavos-submit-btn", style: { width: "100%", padding: "12px", borderRadius: "12px", border: inputBorder, background: "transparent", color: textColor, fontSize: "14px", fontWeight: 500, cursor: pkBusy ? "default" : "pointer", fontFamily: "inherit", transition: "background 0.15s", opacity: pkBusy ? 0.6 : 1 }, onClick: handleSaveRecovery, disabled: pkBusy, children: "Save a recovery phrase instead" })
575
+ ] }) : /* @__PURE__ */ jsxs("button", { className: "cavos-primary-btn", style: { width: "100%", padding: "13px", borderRadius: "12px", border: "none", background: primaryColor, color: "#fff", fontSize: "14px", fontWeight: 600, cursor: pkBusy ? "default" : "pointer", fontFamily: "inherit", display: "flex", alignItems: "center", justifyContent: "center", gap: "9px", opacity: pkBusy ? 0.65 : 1, transition: "opacity 0.15s" }, onClick: handleSaveRecovery, disabled: pkBusy, children: [
576
+ pkBusy && /* @__PURE__ */ jsx(Spinner, { size: 16, color: "#fff" }),
577
+ pkBusy ? "Setting up\u2026" : "Save a recovery phrase"
578
+ ] }),
579
+ /* @__PURE__ */ jsx("button", { className: "cavos-sub-btn", style: { background: "none", border: "none", cursor: pkBusy ? "default" : "pointer", fontSize: "13px", color: subTextColor, width: "100%", textAlign: "center", padding: "18px 0 0", fontFamily: "inherit", transition: "color 0.15s", opacity: pkBusy ? 0.6 : 1 }, onClick: finishSecureStep, disabled: pkBusy, children: "Skip for now" })
580
+ ] }),
581
+ /* @__PURE__ */ jsxs("div", { style: footer, children: [
582
+ /* @__PURE__ */ jsx(CavosLogo, { invert: !isLight }),
583
+ /* @__PURE__ */ jsx("span", { children: "Secured by Cavos" })
584
+ ] })
585
+ ] }) });
586
+ }
587
+ if (screen === "recovery-code") {
588
+ return /* @__PURE__ */ jsx("div", { style: overlay, "data-cavos-theme": theme, role: "dialog", "aria-modal": true, children: /* @__PURE__ */ jsxs("div", { style: { ...card, position: "relative" }, children: [
589
+ isMobile && /* @__PURE__ */ jsx("div", { style: handle }),
590
+ /* @__PURE__ */ jsxs("div", { style: { padding: isMobile ? "28px 24px 28px" : "44px 24px 28px", textAlign: "center" }, children: [
591
+ /* @__PURE__ */ jsx("h2", { style: { margin: "0 0 8px", fontSize: "17px", fontWeight: 600, color: textColor, letterSpacing: "-0.02em" }, children: "Save your recovery phrase" }),
592
+ /* @__PURE__ */ jsx("p", { style: { margin: "0 0 18px", fontSize: "13px", color: subTextColor, lineHeight: 1.55 }, children: "Write this down and keep it somewhere safe. It's the only way to get back in if you lose your devices, and we can't recover it for you." }),
593
+ /* @__PURE__ */ jsx("div", { style: { background: isLight ? "rgba(0,0,0,0.03)" : "rgba(255,255,255,0.05)", border: `1px solid ${isLight ? "rgba(0,0,0,0.08)" : "rgba(255,255,255,0.1)"}`, borderRadius: "12px", padding: "16px", marginBottom: "12px", fontSize: "14px", fontWeight: 500, color: textColor, wordBreak: "break-word", lineHeight: 1.6, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", textAlign: "center" }, children: savedRecoveryCode }),
594
+ /* @__PURE__ */ jsx("button", { className: "cavos-provider", style: { ...pBtn, marginBottom: "10px" }, onClick: handleCopyRecovery, children: /* @__PURE__ */ jsx("span", { children: copied ? "Copied \u2713" : "Copy to clipboard" }) }),
595
+ /* @__PURE__ */ jsx("button", { className: "cavos-primary-btn", style: { width: "100%", padding: "12px", borderRadius: "12px", border: "none", background: primaryColor, color: "#fff", fontSize: "14px", fontWeight: 500, cursor: "pointer", fontFamily: "inherit", transition: "opacity 0.15s" }, onClick: finishSecureStep, children: "I've saved it" })
596
+ ] }),
597
+ /* @__PURE__ */ jsxs("div", { style: footer, children: [
598
+ /* @__PURE__ */ jsx(CavosLogo, { invert: !isLight }),
599
+ /* @__PURE__ */ jsx("span", { children: "Secured by Cavos" })
600
+ ] })
601
+ ] }) });
602
+ }
603
+ if (screen === "passkey-approval") {
604
+ return /* @__PURE__ */ jsx("div", { style: overlay, "data-cavos-theme": theme, role: "dialog", "aria-modal": true, children: /* @__PURE__ */ jsxs("div", { style: { ...card, position: "relative" }, children: [
605
+ isMobile && /* @__PURE__ */ jsx("div", { style: handle }),
606
+ /* @__PURE__ */ jsx("button", { className: "cavos-close", style: close, onClick: handleClose, "aria-label": "Close", children: /* @__PURE__ */ jsx(CloseX, {}) }),
607
+ /* @__PURE__ */ jsxs("div", { style: { padding: isMobile ? "28px 24px 32px" : "48px 24px 32px", textAlign: "center" }, children: [
608
+ /* @__PURE__ */ jsx("div", { style: { width: 48, height: 48, borderRadius: "50%", margin: "0 auto 16px", background: isLight ? "rgba(0,0,0,0.04)" : "rgba(255,255,255,0.06)", border: `1px solid ${isLight ? "rgba(0,0,0,0.08)" : "rgba(255,255,255,0.1)"}`, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsx(FingerprintIcon, { size: 24, color: textColor }) }),
609
+ /* @__PURE__ */ jsx("h2", { style: { margin: "0 0 8px", fontSize: "17px", fontWeight: 600, color: textColor, letterSpacing: "-0.02em" }, children: "Verify it's you" }),
610
+ /* @__PURE__ */ jsx("p", { style: { margin: "0 0 20px", fontSize: "13px", color: subTextColor, lineHeight: 1.55 }, children: "Confirm with Face ID, Touch ID, or your device PIN to add this device to your account." }),
611
+ pkError && /* @__PURE__ */ jsx("div", { style: { background: errBg, border: `1px solid ${errBorder}`, borderRadius: "10px", padding: "9px 13px", fontSize: "13px", color: errColor, marginBottom: "14px" }, children: pkError }),
612
+ /* @__PURE__ */ jsxs("button", { className: "cavos-primary-btn", style: { width: "100%", padding: "12px", borderRadius: "12px", border: "none", background: primaryColor, color: "#fff", fontSize: "14px", fontWeight: 500, cursor: pkBusy ? "default" : "pointer", fontFamily: "inherit", display: "flex", alignItems: "center", justifyContent: "center", gap: "8px", opacity: pkBusy ? 0.65 : 1 }, onClick: handlePasskeyApprove, disabled: pkBusy, children: [
613
+ pkBusy && /* @__PURE__ */ jsx(Spinner, { size: 15, color: "#fff" }),
614
+ pkBusy ? "Verifying\u2026" : "Continue with passkey"
615
+ ] }),
616
+ walletStatus.awaitingApproval && /* @__PURE__ */ jsx("button", { className: "cavos-sub-btn", style: { background: "none", border: "none", cursor: "pointer", fontSize: "13px", color: subTextColor, width: "100%", textAlign: "center", padding: "16px 0 0", fontFamily: "inherit", transition: "color 0.15s" }, onClick: () => {
617
+ setScreen("device-approval");
618
+ setPkError("");
619
+ }, children: "Approve by email instead" }),
620
+ /* @__PURE__ */ jsx("button", { className: "cavos-sub-btn", style: { background: "none", border: "none", cursor: "pointer", fontSize: "13px", color: subTextColor, width: "100%", textAlign: "center", padding: `${walletStatus.awaitingApproval ? "10px" : "16px"} 0 0`, fontFamily: "inherit", transition: "color 0.15s" }, onClick: () => {
621
+ setScreen("recover");
622
+ setPkError("");
623
+ }, children: "Use a recovery phrase" })
624
+ ] }),
625
+ /* @__PURE__ */ jsxs("div", { style: footer, children: [
626
+ /* @__PURE__ */ jsx(CavosLogo, { invert: !isLight }),
627
+ /* @__PURE__ */ jsx("span", { children: "Secured by Cavos" })
628
+ ] })
629
+ ] }) });
630
+ }
466
631
  if (screen === "device-approval") {
467
632
  const expired = walletStatus.awaitingApproval && !walletStatus.pendingRequestId;
468
633
  return /* @__PURE__ */ jsx("div", { style: overlay, "data-cavos-theme": theme, role: "dialog", "aria-modal": true, children: /* @__PURE__ */ jsxs("div", { style: { ...card, position: "relative" }, children: [
@@ -522,7 +687,7 @@ function CavosAuthModal({
522
687
  }, children: /* @__PURE__ */ jsxs("div", { style: { ...card, position: "relative" }, children: [
523
688
  isMobile && /* @__PURE__ */ jsx("div", { style: handle }),
524
689
  /* @__PURE__ */ jsx("button", { className: "cavos-close", style: { ...close, left: "16px", right: "auto" }, onClick: () => {
525
- setScreen("device-approval");
690
+ setScreen(walletStatus.hasPasskey && passkeySupported ? "passkey-approval" : "device-approval");
526
691
  setError("");
527
692
  setRecoverCode("");
528
693
  }, "aria-label": "Back", children: /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "M19 12H5M12 5l-7 7 7 7" }) }) }),
@@ -761,23 +926,36 @@ var INITIAL_STATUS = {
761
926
  isReady: false,
762
927
  needsDeviceApproval: false,
763
928
  awaitingApproval: false,
764
- pendingRequestId: null
929
+ pendingRequestId: null,
930
+ hasPasskey: false,
931
+ isNewAccount: false
765
932
  };
766
933
  function CavosProvider({ config, modal, children }) {
767
934
  const [auth] = useState(
768
935
  () => new CavosAuth({ appId: config.appId, backendUrl: config.authBackendUrl })
769
936
  );
770
- const [cavos, setCavos] = useState(null);
937
+ const [wallet, setWallet] = useState(null);
771
938
  const [identity, setIdentity] = useState(null);
772
939
  const [walletStatus, setWalletStatus] = useState(INITIAL_STATUS);
773
940
  const [isLoading, setIsLoading] = useState(false);
774
941
  const [authError, setAuthError] = useState(null);
775
942
  const [modalOpen, setModalOpen] = useState(false);
943
+ const [passkeySupported, setPasskeySupported] = useState(false);
776
944
  const [branding, setBranding] = useState({});
945
+ useEffect(() => {
946
+ let cancelled = false;
947
+ PasskeySigner.isSupported().then((ok) => {
948
+ if (!cancelled) setPasskeySupported(ok);
949
+ }).catch(() => {
950
+ });
951
+ return () => {
952
+ cancelled = true;
953
+ };
954
+ }, []);
777
955
  const configRef = useRef(config);
778
956
  useEffect(() => {
779
957
  configRef.current = config;
780
- }, [config]);
958
+ });
781
959
  useEffect(() => {
782
960
  if (!config.appId || typeof window === "undefined") return;
783
961
  const base = config.authBackendUrl ?? "https://cavos.xyz";
@@ -791,41 +969,52 @@ function CavosProvider({ config, modal, children }) {
791
969
  const closeModal = useCallback(() => setModalOpen(false), []);
792
970
  const clearAuthError = useCallback(() => setAuthError(null), []);
793
971
  const resendDeviceApproval = useCallback(async () => {
794
- if (!identity || !cavos || cavos.chain !== "starknet" || !cavos.pendingRequestId) return;
795
- const backendUrl = configRef.current.authBackendUrl ?? "https://cavos.xyz";
796
- if (!configRef.current.appId) return;
797
- const recovery = new HttpRecoveryClient({ baseUrl: backendUrl, appId: configRef.current.appId });
972
+ const cfg = configRef.current;
973
+ if (!identity || !wallet || wallet.chain !== "starknet" || !wallet.pendingRequestId) return;
974
+ const backendUrl = cfg.authBackendUrl ?? "https://cavos.xyz";
975
+ if (!cfg.appId) return;
976
+ const recovery = new HttpRecoveryClient({ baseUrl: backendUrl, appId: cfg.appId });
798
977
  await recovery.requestDeviceAddition({
799
978
  userId: identity.userId,
800
- accountAddress: cavos.address,
801
- newSigner: cavos.publicKey,
979
+ accountAddress: wallet.address,
980
+ newSigner: wallet.publicKey,
802
981
  ...identity.email ? { email: identity.email } : {}
803
982
  });
804
- }, [identity, cavos]);
805
- const connect = useCallback(async (id) => {
806
- setWalletStatus({ ...INITIAL_STATUS, isDeploying: true });
807
- const c = await Cavos.connect({
808
- chain: configRef.current.chain ?? "starknet",
809
- network: configRef.current.network,
983
+ }, [identity, wallet]);
984
+ const connect = useCallback(async (id, opts) => {
985
+ const cfg = configRef.current;
986
+ if (!opts?.silent) setWalletStatus({ ...INITIAL_STATUS, isDeploying: true });
987
+ const w = await Cavos.connect({
988
+ chain: cfg.chain ?? "starknet",
989
+ network: cfg.network,
810
990
  identity: id,
811
- appSalt: configRef.current.appSalt,
812
- ...configRef.current.paymasterApiKey ? { paymasterApiKey: configRef.current.paymasterApiKey } : {},
813
- ...configRef.current.appId ? { appId: configRef.current.appId } : {},
814
- ...configRef.current.authBackendUrl ? { backendUrl: configRef.current.authBackendUrl } : {},
815
- ...configRef.current.rpcUrl ? { rpcUrl: configRef.current.rpcUrl } : {}
991
+ appSalt: cfg.appSalt,
992
+ ...cfg.paymasterApiKey ? { paymasterApiKey: cfg.paymasterApiKey } : {},
993
+ ...cfg.appId ? { appId: cfg.appId } : {},
994
+ ...cfg.authBackendUrl ? { backendUrl: cfg.authBackendUrl } : {},
995
+ ...cfg.rpcUrl ? { rpcUrl: cfg.rpcUrl } : {}
816
996
  });
817
- setCavos(c);
997
+ setWallet(w);
818
998
  setIdentity(id);
819
- const pendingRequestId = c.chain === "starknet" ? c.pendingRequestId : null;
999
+ const pendingRequestId = w.chain === "starknet" ? w.pendingRequestId : null;
1000
+ let hasPasskey = false;
1001
+ if (w.status === "needs-device-approval") {
1002
+ try {
1003
+ hasPasskey = await w.hasPasskey();
1004
+ } catch {
1005
+ }
1006
+ }
820
1007
  setWalletStatus({
821
1008
  isDeploying: false,
822
- isReady: c.status === "ready",
823
- needsDeviceApproval: c.status === "needs-device-approval",
824
- awaitingApproval: c.status === "needs-device-approval" && !!pendingRequestId,
825
- pendingRequestId
1009
+ isReady: w.status === "ready",
1010
+ needsDeviceApproval: w.status === "needs-device-approval",
1011
+ awaitingApproval: w.status === "needs-device-approval" && !!pendingRequestId,
1012
+ pendingRequestId,
1013
+ hasPasskey,
1014
+ isNewAccount: w.isNewAccount
826
1015
  });
827
- modal?.onSuccess?.(c.address);
828
- return c;
1016
+ modal?.onSuccess?.(w.address);
1017
+ return w;
829
1018
  }, [modal]);
830
1019
  useEffect(() => {
831
1020
  if (typeof window === "undefined") return;
@@ -876,76 +1065,145 @@ function CavosProvider({ config, modal, children }) {
876
1065
  const id = await auth.verifyOtp(email, code);
877
1066
  await connect(id);
878
1067
  }, [auth, connect]);
879
- const execute = useCallback(async (calls) => {
880
- if (!cavos) throw new Error("Not logged in");
881
- if (cavos.chain !== "starknet") {
1068
+ const execute = useCallback(async (calls, opts) => {
1069
+ if (!wallet) throw new Error("Not logged in");
1070
+ if (wallet.chain !== "starknet") {
882
1071
  throw new Error(
883
- "kit: useCavos().execute(calls) is Starknet-only. On Solana use the `wallet` handle: wallet.execute(amount, dest)."
1072
+ "kit: useCavos().execute(calls) is Starknet-only. On Solana/Stellar use the `wallet` handle: wallet.execute(amount, dest)."
884
1073
  );
885
1074
  }
886
- return cavos.execute(calls);
887
- }, [cavos]);
1075
+ return wallet.execute(calls, opts);
1076
+ }, [wallet]);
888
1077
  const addSigner = useCallback(
889
1078
  async (pubkey) => {
890
- if (!cavos) throw new Error("Not logged in");
891
- if (cavos.chain !== "starknet") {
892
- throw new Error("kit: addSigner via useCavos() is Starknet-only; use the `wallet` handle on Solana.");
1079
+ if (!wallet) throw new Error("Not logged in");
1080
+ if (wallet.chain !== "starknet") {
1081
+ throw new Error("kit: addSigner via useCavos() is Starknet-only; use the `wallet` handle on other chains.");
893
1082
  }
894
- return cavos.addSigner(pubkey);
1083
+ return wallet.addSigner(pubkey);
895
1084
  },
896
- [cavos]
1085
+ [wallet]
897
1086
  );
898
1087
  const enrollPasskey = useCallback(
899
1088
  async (passkey, params) => {
900
- if (!cavos) throw new Error("Not logged in");
901
- return cavos.enrollPasskey(passkey, params);
902
- },
903
- [cavos]
904
- );
905
- const approveThisDeviceWithPasskey = useCallback(
906
- async (passkey, submit) => {
907
- if (!cavos) throw new Error("Not logged in");
908
- if (cavos.chain === "starknet") {
909
- return cavos.approveThisDeviceWithPasskey({ passkey, ...submit ? { submit } : {} });
1089
+ if (!wallet) throw new Error("Not logged in");
1090
+ if (wallet.chain === "stellar") {
1091
+ throw new Error(
1092
+ "kit: on Stellar, use enrollPasskeyDefault() \u2014 the passkey factor is a WebAuthn PRF secret, not a signer object."
1093
+ );
910
1094
  }
911
- const transactionHash = await cavos.approveThisDeviceWithPasskey(passkey);
912
- return { transactionHash };
1095
+ return wallet.enrollPasskey(passkey, params);
913
1096
  },
914
- [cavos]
1097
+ [wallet]
915
1098
  );
1099
+ const rpName = branding.appName ?? modal?.appName ?? "Cavos";
1100
+ const enrollPasskeyDefault = useCallback(async () => {
1101
+ if (!wallet || !identity) throw new Error("Not logged in");
1102
+ if (wallet.status !== "ready") throw new Error("kit: no ready device to enroll a passkey on");
1103
+ if (wallet.chain === "stellar") {
1104
+ const prf = new PasskeyPrf({ rpName });
1105
+ const { secret } = await prf.enroll({
1106
+ userId: identity.userId,
1107
+ userName: identity.email ?? identity.userId,
1108
+ ...identity.email ? { displayName: identity.email } : {}
1109
+ });
1110
+ await wallet.enrollPasskey(secret ?? await prf.getSecret());
1111
+ return;
1112
+ }
1113
+ const passkey = new PasskeySigner({ rpName });
1114
+ await wallet.enrollPasskey(passkey, {
1115
+ userId: identity.userId,
1116
+ userName: identity.email ?? identity.userId,
1117
+ ...identity.email ? { displayName: identity.email } : {}
1118
+ });
1119
+ }, [wallet, identity, rpName]);
1120
+ const approveDeviceWithPasskey = useCallback(async () => {
1121
+ if (!wallet || !identity) throw new Error("Not logged in");
1122
+ if (wallet.status !== "needs-device-approval") {
1123
+ await connect(identity);
1124
+ return;
1125
+ }
1126
+ if (wallet.chain === "stellar") {
1127
+ const prf = new PasskeyPrf({ rpName });
1128
+ await wallet.approveThisDeviceWithPasskey(await prf.getSecret());
1129
+ } else if (wallet.chain === "starknet") {
1130
+ const passkey = new PasskeySigner({ rpName });
1131
+ await wallet.approveThisDeviceWithPasskey({ passkey });
1132
+ } else {
1133
+ const passkey = new PasskeySigner({ rpName });
1134
+ await wallet.approveThisDeviceWithPasskey(passkey);
1135
+ }
1136
+ setWalletStatus((s) => ({ ...s, isDeploying: true, needsDeviceApproval: false, awaitingApproval: false }));
1137
+ const deadline = Date.now() + 6e4;
1138
+ for (; ; ) {
1139
+ let ready = false;
1140
+ try {
1141
+ ready = await wallet.isReady();
1142
+ } catch {
1143
+ }
1144
+ if (ready) break;
1145
+ if (Date.now() > deadline) {
1146
+ setWalletStatus((s) => ({ ...s, isDeploying: false, needsDeviceApproval: true }));
1147
+ throw new Error(
1148
+ "Your device is being added, but it's taking longer than usual. Please try again in a moment."
1149
+ );
1150
+ }
1151
+ await new Promise((r) => setTimeout(r, 3e3));
1152
+ }
1153
+ await connect(identity, { silent: true });
1154
+ }, [wallet, identity, rpName, connect]);
916
1155
  const setupRecovery = useCallback(async () => {
917
- if (!cavos) throw new Error("Not logged in");
1156
+ if (!wallet) throw new Error("Not logged in");
918
1157
  const code = generateRecoveryCode();
919
- await cavos.setupRecovery(code);
1158
+ await wallet.setupRecovery(code);
920
1159
  return code;
921
- }, [cavos]);
1160
+ }, [wallet]);
922
1161
  const recover = useCallback(async (code) => {
923
1162
  if (!identity) throw new Error("Sign in first so we know which account to recover.");
1163
+ const cfg = configRef.current;
924
1164
  setAuthError(null);
925
1165
  setWalletStatus({ ...INITIAL_STATUS, isDeploying: true });
926
1166
  try {
927
- const chain = configRef.current.chain ?? "starknet";
928
- const c = chain === "solana" ? await CavosSolana.recover({
929
- code,
930
- identity,
931
- network: configRef.current.network === "mainnet" ? "solana-mainnet" : "solana-devnet",
932
- appSalt: configRef.current.appSalt,
933
- ...configRef.current.appId ? { appId: configRef.current.appId } : {},
934
- ...configRef.current.authBackendUrl ? { backendUrl: configRef.current.authBackendUrl } : {},
935
- ...configRef.current.rpcUrl ? { rpcUrl: configRef.current.rpcUrl } : {}
936
- }) : await Cavos.recover({
937
- code,
938
- identity,
939
- network: configRef.current.network,
940
- appSalt: configRef.current.appSalt,
941
- paymasterApiKey: configRef.current.paymasterApiKey ?? "",
942
- ...configRef.current.appId ? { appId: configRef.current.appId } : {},
943
- ...configRef.current.authBackendUrl ? { backendUrl: configRef.current.authBackendUrl } : {},
944
- ...configRef.current.rpcUrl ? { rpcUrl: configRef.current.rpcUrl } : {}
945
- });
946
- setCavos(c);
1167
+ const chain = cfg.chain ?? "starknet";
1168
+ let w;
1169
+ if (chain === "solana") {
1170
+ w = await CavosSolana.recover({
1171
+ code,
1172
+ identity,
1173
+ network: cfg.network === "mainnet" ? "solana-mainnet" : "solana-devnet",
1174
+ appSalt: cfg.appSalt,
1175
+ ...cfg.appId ? { appId: cfg.appId } : {},
1176
+ ...cfg.authBackendUrl ? { backendUrl: cfg.authBackendUrl } : {},
1177
+ ...cfg.rpcUrl ? { rpcUrl: cfg.rpcUrl } : {}
1178
+ });
1179
+ } else if (chain === "stellar") {
1180
+ const sw = await Cavos.connect({
1181
+ chain: "stellar",
1182
+ network: cfg.network,
1183
+ identity,
1184
+ appSalt: cfg.appSalt,
1185
+ ...cfg.appId ? { appId: cfg.appId } : {},
1186
+ ...cfg.authBackendUrl ? { backendUrl: cfg.authBackendUrl } : {}
1187
+ });
1188
+ if (sw.chain === "stellar" && sw.status === "needs-device-approval") {
1189
+ await sw.approveThisDeviceWithRecovery(code);
1190
+ }
1191
+ w = sw;
1192
+ } else {
1193
+ w = await Cavos.recover({
1194
+ code,
1195
+ identity,
1196
+ network: cfg.network,
1197
+ appSalt: cfg.appSalt,
1198
+ paymasterApiKey: cfg.paymasterApiKey ?? "",
1199
+ ...cfg.appId ? { appId: cfg.appId } : {},
1200
+ ...cfg.authBackendUrl ? { backendUrl: cfg.authBackendUrl } : {},
1201
+ ...cfg.rpcUrl ? { rpcUrl: cfg.rpcUrl } : {}
1202
+ });
1203
+ }
1204
+ setWallet(w);
947
1205
  setWalletStatus({ ...INITIAL_STATUS, isReady: true });
948
- modal?.onSuccess?.(c.address);
1206
+ modal?.onSuccess?.(w.address);
949
1207
  } catch (e) {
950
1208
  const msg = e instanceof Error ? e.message : "Recovery failed. Check your code and try again.";
951
1209
  setAuthError(msg);
@@ -955,9 +1213,10 @@ function CavosProvider({ config, modal, children }) {
955
1213
  }, [identity, modal]);
956
1214
  useEffect(() => {
957
1215
  if (!walletStatus.awaitingApproval || !walletStatus.pendingRequestId || !identity) return;
958
- if (!configRef.current.appId) return;
959
- const backendUrl = configRef.current.authBackendUrl ?? "https://cavos.xyz";
960
- const recovery = new HttpRecoveryClient({ baseUrl: backendUrl, appId: configRef.current.appId });
1216
+ const cfg = configRef.current;
1217
+ if (!cfg.appId) return;
1218
+ const backendUrl = cfg.authBackendUrl ?? "https://cavos.xyz";
1219
+ const recovery = new HttpRecoveryClient({ baseUrl: backendUrl, appId: cfg.appId });
961
1220
  const requestId = walletStatus.pendingRequestId;
962
1221
  let cancelled = false;
963
1222
  const tick = async () => {
@@ -981,7 +1240,7 @@ function CavosProvider({ config, modal, children }) {
981
1240
  };
982
1241
  }, [walletStatus.awaitingApproval, walletStatus.pendingRequestId, identity, connect]);
983
1242
  const logout = useCallback(() => {
984
- setCavos(null);
1243
+ setWallet(null);
985
1244
  setIdentity(null);
986
1245
  setWalletStatus(INITIAL_STATUS);
987
1246
  setAuthError(null);
@@ -989,11 +1248,11 @@ function CavosProvider({ config, modal, children }) {
989
1248
  const value = {
990
1249
  openModal,
991
1250
  closeModal,
992
- isAuthenticated: !!cavos,
1251
+ isAuthenticated: !!wallet,
993
1252
  user: identity ? { userId: identity.userId, email: identity.email, provider: identity.provider } : null,
994
1253
  chain: config.chain ?? "starknet",
995
- wallet: cavos,
996
- address: cavos?.address ?? null,
1254
+ wallet,
1255
+ address: wallet?.address ?? null,
997
1256
  walletStatus,
998
1257
  isLoading,
999
1258
  authError,
@@ -1006,7 +1265,9 @@ function CavosProvider({ config, modal, children }) {
1006
1265
  execute,
1007
1266
  addSigner,
1008
1267
  enrollPasskey,
1009
- approveThisDeviceWithPasskey,
1268
+ passkeySupported,
1269
+ enrollPasskeyDefault,
1270
+ approveDeviceWithPasskey,
1010
1271
  resendDeviceApproval,
1011
1272
  setupRecovery,
1012
1273
  recover,