decane-connect-kit 2.0.3 → 2.2.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
@@ -1868,7 +1868,12 @@ function startBitcoinDiscovery(registry) {
1868
1868
  }
1869
1869
 
1870
1870
  // src/factory.ts
1871
- function createDecane(config = {}) {
1871
+ function createDecane(config) {
1872
+ if (!config?.appId) {
1873
+ throw new Error(
1874
+ "decane-connect-kit: appId is required. Pass your dashboard project ID as config.appId."
1875
+ );
1876
+ }
1872
1877
  const emitter = createEmitter();
1873
1878
  const registry = createRegistry(emitter);
1874
1879
  const session = createSessionManager();
@@ -1914,9 +1919,10 @@ var ERROR_MAP = {
1914
1919
  ORIGIN_NOT_ALLOWED: OriginNotAllowedError
1915
1920
  };
1916
1921
  var ApiClient = class {
1917
- constructor(baseUrl, apiKey) {
1922
+ constructor(baseUrl, apiKey, appId) {
1918
1923
  this.baseUrl = baseUrl;
1919
1924
  this.apiKey = apiKey;
1925
+ this.appId = appId;
1920
1926
  }
1921
1927
  async _request(path, init) {
1922
1928
  let res;
@@ -1926,6 +1932,7 @@ var ApiClient = class {
1926
1932
  headers: {
1927
1933
  "Content-Type": "application/json",
1928
1934
  "X-API-Key": this.apiKey,
1935
+ "X-App-Id": this.appId,
1929
1936
  ...init?.headers
1930
1937
  }
1931
1938
  });
@@ -1992,6 +1999,35 @@ var ApiClient = class {
1992
1999
  body: JSON.stringify({ response, origin })
1993
2000
  });
1994
2001
  }
2002
+ /**
2003
+ * Exchange a token from the developer's own auth system for a Decane
2004
+ * session. `providerId` is only needed when the project has more than one
2005
+ * provider configured.
2006
+ */
2007
+ async authExternal(token, providerId) {
2008
+ return this._request("/auth/external", {
2009
+ method: "POST",
2010
+ body: JSON.stringify(providerId ? { token, providerId } : { token })
2011
+ });
2012
+ }
2013
+ /**
2014
+ * Returns null when this user has no server-registered passkey, which is the
2015
+ * expected answer for wallets created before passkey sign-in existed or for
2016
+ * PIN-only users — the caller falls back to the normal sign-in flow.
2017
+ */
2018
+ async getWebAuthnLoginOptions(userId, rpId) {
2019
+ return this._request("/webauthn/login-options", {
2020
+ method: "POST",
2021
+ body: JSON.stringify({ userId, rpId }),
2022
+ allow404: true
2023
+ });
2024
+ }
2025
+ async postWebAuthnLogin(userId, response, origin) {
2026
+ return this._request("/webauthn/login", {
2027
+ method: "POST",
2028
+ body: JSON.stringify({ userId, response, origin })
2029
+ });
2030
+ }
1995
2031
  async revoke(jwt) {
1996
2032
  await this._request("/auth/revoke", {
1997
2033
  method: "POST",
@@ -2164,6 +2200,12 @@ var TeeClient = class {
2164
2200
  this.teeUrl = teeUrl;
2165
2201
  this.handle = null;
2166
2202
  this.attested = false;
2203
+ /** Mirrors what the signer confirmed for this session, not what we asked for. */
2204
+ this.assertionPerSignature = false;
2205
+ }
2206
+ /** Whether this session actually got per-signature assertions. */
2207
+ requiresAssertionPerSignature() {
2208
+ return this.assertionPerSignature;
2167
2209
  }
2168
2210
  async ensureAttested() {
2169
2211
  if (this.attested) return;
@@ -2179,16 +2221,20 @@ var TeeClient = class {
2179
2221
  * couldn't derive them (older signer build, or a transient share-fetch
2180
2222
  * failure); callers fall back to the backend's addresses in that case.
2181
2223
  */
2182
- async openSession(deviceShare, jwt) {
2224
+ async openSession(deviceShare, jwt, requireAssertionPerSignature = false) {
2183
2225
  await this.ensureAttested();
2184
2226
  const res = await fetch(`${this.teeUrl}/signer/session`, {
2185
2227
  method: "POST",
2186
2228
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2187
- body: JSON.stringify({ deviceShare: bytesToBase64(deviceShare) })
2229
+ body: JSON.stringify({
2230
+ deviceShare: bytesToBase64(deviceShare),
2231
+ requireAssertionPerSignature
2232
+ })
2188
2233
  });
2189
2234
  if (!res.ok) throw new Error("session open failed");
2190
2235
  const body = await res.json();
2191
2236
  this.handle = body.handle;
2237
+ this.assertionPerSignature = body.requireAssertionPerSignature === true;
2192
2238
  return body.addresses ?? null;
2193
2239
  }
2194
2240
  /**
@@ -2279,14 +2325,33 @@ var TeeClient = class {
2279
2325
  hasSession() {
2280
2326
  return this.handle !== null;
2281
2327
  }
2282
- async sign(req) {
2328
+ /**
2329
+ * `jwt` is only needed when this session requires per-signature assertions —
2330
+ * it authenticates the challenge request. Passed per call rather than cached
2331
+ * on the instance deliberately: the backend's JWTs are shorter-lived than the
2332
+ * TEE session TTL, so a stored copy would go stale mid-session.
2333
+ */
2334
+ async sign(req, jwt) {
2283
2335
  if (!this.handle) throw new Error("no session");
2336
+ let gate = null;
2337
+ if (this.assertionPerSignature) {
2338
+ if (!jwt) throw new Error("sign requires a jwt when per-signature assertion is enabled");
2339
+ gate = await this.completeAssertion(jwt);
2340
+ }
2284
2341
  const res = await fetch(`${this.teeUrl}/signer/sign`, {
2285
2342
  method: "POST",
2286
2343
  headers: { "Content-Type": "application/json" },
2287
- body: JSON.stringify({ handle: this.handle, ...req })
2344
+ body: JSON.stringify({
2345
+ handle: this.handle,
2346
+ ...req,
2347
+ ...gate ? { ...gate, origin: window.location.origin } : {}
2348
+ })
2288
2349
  });
2289
2350
  if (res.status === 401) {
2351
+ const body = await res.clone().json().catch(() => ({}));
2352
+ if (body?.error === "assertion_required") {
2353
+ throw new ExportNotAuthorizedError();
2354
+ }
2290
2355
  this.handle = null;
2291
2356
  throw new SessionExpiredError();
2292
2357
  }
@@ -2309,10 +2374,11 @@ var TeeClient = class {
2309
2374
  // src/social/session.ts
2310
2375
  init_errors();
2311
2376
  var SessionManager = class {
2312
- constructor(events, durationMs, teeUrl) {
2377
+ constructor(events, durationMs, teeUrl, requireAssertionPerSignature = false) {
2313
2378
  this.events = events;
2314
2379
  this.durationMs = durationMs;
2315
2380
  this.teeUrl = teeUrl;
2381
+ this.requireAssertionPerSignature = requireAssertionPerSignature;
2316
2382
  this.tee = null;
2317
2383
  this.evmAddress = null;
2318
2384
  this.solanaAddress = null;
@@ -2332,7 +2398,18 @@ var SessionManager = class {
2332
2398
  */
2333
2399
  async start(deviceShare, jwt, fallback) {
2334
2400
  this.tee = new TeeClient(this.teeUrl);
2335
- const verified = await this.tee.openSession(deviceShare, jwt);
2401
+ const verified = await this.tee.openSession(
2402
+ deviceShare,
2403
+ jwt,
2404
+ this.requireAssertionPerSignature
2405
+ );
2406
+ if (this.requireAssertionPerSignature && !this.tee.requiresAssertionPerSignature()) {
2407
+ void this.tee.closeSession();
2408
+ this.tee = null;
2409
+ throw new Error(
2410
+ "requireAssertionPerSignature was requested but this signer does not support it \u2014 refusing to open a session with weaker authority than configured."
2411
+ );
2412
+ }
2336
2413
  const addresses = verified ?? fallback;
2337
2414
  if (!addresses) {
2338
2415
  this.tee = null;
@@ -2450,7 +2527,13 @@ function validatePin(pin) {
2450
2527
 
2451
2528
  // src/social/crypto/passkey.ts
2452
2529
  init_errors();
2453
- import { startRegistration } from "@simplewebauthn/browser";
2530
+ import { startRegistration, startAuthentication as startAuthentication2 } from "@simplewebauthn/browser";
2531
+ function b64u(u) {
2532
+ return btoa(String.fromCharCode(...u)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
2533
+ }
2534
+ function fromB64u(s) {
2535
+ return Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
2536
+ }
2454
2537
  function buf3(u) {
2455
2538
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2456
2539
  }
@@ -2477,6 +2560,29 @@ async function registerPasskey(opts) {
2477
2560
  );
2478
2561
  return { credentialId, prfOutput, response };
2479
2562
  }
2563
+ async function authenticateWithPasskey(opts) {
2564
+ const optionsJSON = {
2565
+ ...opts.optionsJSON,
2566
+ extensions: {
2567
+ ...opts.optionsJSON.extensions,
2568
+ prf: { eval: { first: b64u(opts.prfSalt) } }
2569
+ }
2570
+ };
2571
+ let response;
2572
+ try {
2573
+ response = await startAuthentication2({ optionsJSON });
2574
+ } catch (e) {
2575
+ if (e.name === "NotAllowedError") throw new UserCancelledError();
2576
+ throw e;
2577
+ }
2578
+ const ext = response.clientExtensionResults;
2579
+ const prfResults = ext?.["prf"]?.results;
2580
+ let prfOutput;
2581
+ if (prfResults?.first) {
2582
+ prfOutput = typeof prfResults.first === "string" ? fromB64u(prfResults.first) : new Uint8Array(prfResults.first);
2583
+ }
2584
+ return { response, prfOutput };
2585
+ }
2480
2586
  async function getPrfOutput(opts) {
2481
2587
  const challenge = crypto.getRandomValues(new Uint8Array(32));
2482
2588
  let cred;
@@ -2532,15 +2638,15 @@ async function verifyEmailCode(api, email, code) {
2532
2638
 
2533
2639
  // src/social/signers/evm.ts
2534
2640
  import { keccak256, Transaction, resolveProperties } from "ethers";
2535
- async function signEvmTransaction(tee, chain, tx) {
2641
+ async function signEvmTransaction(tee, chain, tx, jwt) {
2536
2642
  const resolved = await resolveProperties(tx);
2537
2643
  const unsigned = Transaction.from(resolved).unsignedSerialized;
2538
2644
  const payload = unsigned.startsWith("0x") ? unsigned.slice(2) : unsigned;
2539
2645
  const txHash = keccak256("0x" + payload);
2540
- return tee.sign({ chain, kind: "transaction", payload, txHash });
2646
+ return tee.sign({ chain, kind: "transaction", payload, txHash }, jwt);
2541
2647
  }
2542
- async function signEvmMessage(tee, chain, message) {
2543
- return tee.sign({ chain, kind: "message", payload: message });
2648
+ async function signEvmMessage(tee, chain, message, jwt) {
2649
+ return tee.sign({ chain, kind: "message", payload: message }, jwt);
2544
2650
  }
2545
2651
 
2546
2652
  // src/social/signers/solana.ts
@@ -2553,13 +2659,13 @@ function b64(bytes) {
2553
2659
  function unb64(s) {
2554
2660
  return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
2555
2661
  }
2556
- async function signSolanaTransaction(tee, chain, tx) {
2662
+ async function signSolanaTransaction(tee, chain, tx, jwt) {
2557
2663
  const bytes = tx instanceof VTx ? tx.serialize() : tx.serialize({ requireAllSignatures: false });
2558
- const signed = await tee.sign({ chain, kind: "transaction", payload: b64(bytes) });
2664
+ const signed = await tee.sign({ chain, kind: "transaction", payload: b64(bytes) }, jwt);
2559
2665
  return unb64(signed);
2560
2666
  }
2561
- async function signSolanaMessage(tee, chain, message) {
2562
- const sig = await tee.sign({ chain, kind: "message", payload: b64(message) });
2667
+ async function signSolanaMessage(tee, chain, message, jwt) {
2668
+ const sig = await tee.sign({ chain, kind: "message", payload: b64(message) }, jwt);
2563
2669
  return unb64(sig);
2564
2670
  }
2565
2671
 
@@ -2819,6 +2925,7 @@ function clearPersisted(key) {
2819
2925
  }
2820
2926
  }
2821
2927
  async function createDecaneConnect(config) {
2928
+ if (!config.appId) throw new Error("decane-connect-kit: appId is required");
2822
2929
  if (!config.apiKey) throw new Error("decane-connect-kit: apiKey is required");
2823
2930
  if (!config.chains?.length)
2824
2931
  throw new Error("decane-connect-kit: at least one chain is required");
@@ -2829,9 +2936,14 @@ async function createDecaneConnect(config) {
2829
2936
  const PERSIST_KEY = makePersistKey(config.apiKey);
2830
2937
  const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
2831
2938
  const events = new EventEmitter();
2832
- const api = new ApiClient(baseUrl, config.apiKey);
2939
+ const api = new ApiClient(baseUrl, config.apiKey, config.appId);
2833
2940
  const storage = new Storage();
2834
- const session = new SessionManager(events, durationMs, teeUrl);
2941
+ const session = new SessionManager(
2942
+ events,
2943
+ durationMs,
2944
+ teeUrl,
2945
+ config.requireAssertionPerSignature === true
2946
+ );
2835
2947
  let auth = null;
2836
2948
  let persisted = loadPersisted(PERSIST_KEY);
2837
2949
  async function pinSetup() {
@@ -3079,6 +3191,45 @@ async function createDecaneConnect(config) {
3079
3191
  }
3080
3192
  return addresses;
3081
3193
  }
3194
+ async function passkeyLoginCandidate() {
3195
+ if (!persisted?.userId || !isPasskeySupported()) return null;
3196
+ const stored = await storage.getShare(persisted.userId);
3197
+ if (!stored || stored.unlockMethod !== "passkey") return null;
3198
+ if (!stored.credentialId || !stored.prfSalt) return null;
3199
+ return stored;
3200
+ }
3201
+ async function passkeyLogin(stored) {
3202
+ const userId = stored.userId;
3203
+ const options = await api.getWebAuthnLoginOptions(userId, rpId);
3204
+ if (!options) {
3205
+ throw new NewDeviceError();
3206
+ }
3207
+ const { response, prfOutput } = await authenticateWithPasskey({
3208
+ optionsJSON: options,
3209
+ prfSalt: stored.prfSalt
3210
+ });
3211
+ if (!prfOutput) throw new ShareCorruptedError();
3212
+ const result = await api.postWebAuthnLogin(userId, response, window.location.origin);
3213
+ let deviceShare;
3214
+ try {
3215
+ deviceShare = await aesGcmDecrypt(
3216
+ await importAesKey(prfOutput),
3217
+ stored.wrappedDeviceShare,
3218
+ stored.iv
3219
+ );
3220
+ } catch {
3221
+ throw new ShareCorruptedError();
3222
+ }
3223
+ const rawAddresses = await api.getAddresses(result.jwt);
3224
+ const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana } : null;
3225
+ const addresses = await session.start(deviceShare, result.jwt, recordedAddresses);
3226
+ zeroize(deviceShare);
3227
+ auth = { jwt: result.jwt, userId, addresses };
3228
+ persisted = { userId, addresses };
3229
+ savePersisted(PERSIST_KEY, persisted);
3230
+ events.emit("connected");
3231
+ return { addresses, isNewUser: false };
3232
+ }
3082
3233
  async function handleAuthResult(result) {
3083
3234
  let addresses;
3084
3235
  if (!result.hasShare) {
@@ -3097,6 +3248,15 @@ async function createDecaneConnect(config) {
3097
3248
  if (auth && session.isActive()) {
3098
3249
  return { addresses: auth.addresses, isNewUser: false };
3099
3250
  }
3251
+ const candidate = await passkeyLoginCandidate();
3252
+ if (candidate) {
3253
+ try {
3254
+ return await passkeyLogin(candidate);
3255
+ } catch (e) {
3256
+ if (e instanceof UserCancelledError) throw e;
3257
+ console.warn("decane-connect-kit: passkey sign-in unavailable, falling back to standard sign-in.", e);
3258
+ }
3259
+ }
3100
3260
  const method = config.authMethods[0];
3101
3261
  if (!method) throw new CallSequenceError("No authMethods configured");
3102
3262
  if (method === "google") return client.connectWithGoogle();
@@ -3129,6 +3289,11 @@ async function createDecaneConnect(config) {
3129
3289
  return new Promise(() => {
3130
3290
  });
3131
3291
  },
3292
+ async connectWithToken(token, opts) {
3293
+ if (!token) throw new CallSequenceError("connectWithToken requires a token");
3294
+ const result = await api.authExternal(token, opts?.providerId);
3295
+ return handleAuthResult(result);
3296
+ },
3132
3297
  async connectWithEmail(email) {
3133
3298
  await startEmailAuth(api, email);
3134
3299
  return { codeRequired: true };
@@ -3137,7 +3302,22 @@ async function createDecaneConnect(config) {
3137
3302
  const result = await verifyEmailCode(api, email, code);
3138
3303
  return handleAuthResult(result);
3139
3304
  },
3305
+ async canSignInWithPasskey() {
3306
+ return await passkeyLoginCandidate() !== null;
3307
+ },
3308
+ async signInWithPasskey() {
3309
+ const candidate = await passkeyLoginCandidate();
3310
+ if (!candidate) throw new NewDeviceError();
3311
+ return passkeyLogin(candidate);
3312
+ },
3140
3313
  async unlock() {
3314
+ if (!auth) {
3315
+ const candidate = await passkeyLoginCandidate();
3316
+ if (candidate) {
3317
+ await passkeyLogin(candidate);
3318
+ return;
3319
+ }
3320
+ }
3141
3321
  if (!auth) {
3142
3322
  throw persisted ? new ReconnectRequiredError() : new CallSequenceError("Not connected \u2014 call connect() first");
3143
3323
  }
@@ -3162,11 +3342,11 @@ async function createDecaneConnect(config) {
3162
3342
  async signMessage(req) {
3163
3343
  const tee = session.requireActive();
3164
3344
  if (req.chain.startsWith("evm:")) {
3165
- return signEvmMessage(tee, req.chain, req.message);
3345
+ return signEvmMessage(tee, req.chain, req.message, auth?.jwt);
3166
3346
  }
3167
3347
  if (req.chain.startsWith("solana:")) {
3168
3348
  const msg = typeof req.message === "string" ? new TextEncoder().encode(req.message) : req.message;
3169
- const sig = await signSolanaMessage(tee, req.chain, msg);
3349
+ const sig = await signSolanaMessage(tee, req.chain, msg, auth?.jwt);
3170
3350
  return bufferToB64u(sig);
3171
3351
  }
3172
3352
  throw new CallSequenceError(`Unsupported chain: ${req.chain}`);
@@ -3214,7 +3394,7 @@ async function createDecaneConnect(config) {
3214
3394
  maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? 0n
3215
3395
  } : { gasPrice: req.gasPrice ?? feeData.gasPrice ?? void 0 }
3216
3396
  };
3217
- const signedTx = await signEvmTransaction(tee, req.chain, tx);
3397
+ const signedTx = await signEvmTransaction(tee, req.chain, tx, auth?.jwt);
3218
3398
  const txResponse = await provider.broadcastTransaction(signedTx);
3219
3399
  return txResponse.hash;
3220
3400
  },
@@ -3227,7 +3407,7 @@ async function createDecaneConnect(config) {
3227
3407
  tx = Transaction2.from(serializedTx);
3228
3408
  }
3229
3409
  const solChain = config.chains.find((c) => c.startsWith("solana:")) ?? "solana:mainnet";
3230
- return signSolanaTransaction(tee, solChain, tx);
3410
+ return signSolanaTransaction(tee, solChain, tx, auth?.jwt);
3231
3411
  },
3232
3412
  async disconnect() {
3233
3413
  session.end();
@@ -3340,8 +3520,8 @@ async function createDecaneConnect(config) {
3340
3520
  function bufferToB64u(buf6) {
3341
3521
  return btoa(String.fromCharCode(...buf6)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
3342
3522
  }
3343
- function b64uToBuffer(b64u) {
3344
- const b644 = b64u.replace(/-/g, "+").replace(/_/g, "/");
3523
+ function b64uToBuffer(b64u2) {
3524
+ const b644 = b64u2.replace(/-/g, "+").replace(/_/g, "/");
3345
3525
  return Uint8Array.from(atob(b644), (c) => c.charCodeAt(0));
3346
3526
  }
3347
3527
  function toHex(bytes) {
@@ -3728,11 +3908,16 @@ function WalletSelector({
3728
3908
  socialAuthMethods = ["google", "email"],
3729
3909
  onGoogleSignIn,
3730
3910
  onEmailStart,
3731
- onEmailVerify
3911
+ onEmailVerify,
3912
+ customAuthLabel,
3913
+ onCustomAuthSignIn,
3914
+ canUsePasskey = false,
3915
+ passkeyAddress = null,
3916
+ onPasskeySignIn
3732
3917
  }) {
3733
3918
  const showSocial = hasSocialMode(mode);
3734
3919
  const showWallets = mode !== "social";
3735
- const [view, setView] = useState(showWallets ? "wallets" : "social");
3920
+ const [view, setView] = useState(showSocial ? "social" : "wallets");
3736
3921
  const [search, setSearch] = useState("");
3737
3922
  const [filter, setFilter] = useState("all");
3738
3923
  const [pendingWallet, setPendingWallet] = useState(null);
@@ -3741,6 +3926,8 @@ function WalletSelector({
3741
3926
  const [codeVal, setCodeVal] = useState("");
3742
3927
  const [socialLoad, setSocialLoad] = useState(false);
3743
3928
  const [socialErr, setSocialErr] = useState("");
3929
+ const [showOtherMethods, setShowOtherMethods] = useState(false);
3930
+ const passkeyFirst = canUsePasskey && !!onPasskeySignIn && !showOtherMethods;
3744
3931
  useEffect(() => {
3745
3932
  if (!open) return;
3746
3933
  injectFont();
@@ -3756,17 +3943,36 @@ function WalletSelector({
3756
3943
  setSearch("");
3757
3944
  setFilter("all");
3758
3945
  setPendingWallet(null);
3759
- setView(showWallets ? "wallets" : "social");
3946
+ setView(showSocial ? "social" : "wallets");
3760
3947
  setEmailStep("input");
3761
3948
  setEmailVal("");
3762
3949
  setCodeVal("");
3763
3950
  setSocialErr("");
3951
+ setShowOtherMethods(false);
3764
3952
  return;
3765
3953
  }
3766
3954
  return () => {
3767
3955
  document.getElementById("decane-tokens")?.remove();
3768
3956
  };
3769
3957
  }, [open, showWallets]);
3958
+ async function handlePasskeySignIn() {
3959
+ if (!onPasskeySignIn) return;
3960
+ setSocialLoad(true);
3961
+ setSocialErr("");
3962
+ try {
3963
+ await onPasskeySignIn();
3964
+ } catch (e) {
3965
+ const err = e;
3966
+ if (err?.name !== "UserCancelledError") {
3967
+ setSocialErr(err?.message ?? "Passkey sign-in failed");
3968
+ setShowOtherMethods(true);
3969
+ }
3970
+ setSocialLoad(false);
3971
+ }
3972
+ }
3973
+ async function handlePasskeyClick() {
3974
+ void handlePasskeySignIn();
3975
+ }
3770
3976
  async function handleGoogleClick() {
3771
3977
  if (!onGoogleSignIn) return;
3772
3978
  setSocialLoad(true);
@@ -3778,6 +3984,17 @@ function WalletSelector({
3778
3984
  setSocialLoad(false);
3779
3985
  }
3780
3986
  }
3987
+ async function handleCustomAuthClick() {
3988
+ if (!onCustomAuthSignIn) return;
3989
+ setSocialLoad(true);
3990
+ setSocialErr("");
3991
+ try {
3992
+ await onCustomAuthSignIn();
3993
+ } catch (e) {
3994
+ setSocialErr(e.message);
3995
+ setSocialLoad(false);
3996
+ }
3997
+ }
3781
3998
  async function handleEmailStart(e) {
3782
3999
  e.preventDefault();
3783
4000
  if (!onEmailStart) return;
@@ -4052,7 +4269,7 @@ function WalletSelector({
4052
4269
  }
4053
4270
  },
4054
4271
  /* @__PURE__ */ React2.createElement("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none" }, /* @__PURE__ */ React2.createElement("path", { d: "M1.5 1.5L10.5 10.5M10.5 1.5L1.5 10.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }))
4055
- )), showWallets && showSocial && /* @__PURE__ */ React2.createElement("div", { style: { display: "flex", gap: 4, marginBottom: 14, background: "var(--dc-surface)", borderRadius: 10, padding: 3 } }, ["wallets", "social"].map((v) => /* @__PURE__ */ React2.createElement(
4272
+ )), showWallets && showSocial && /* @__PURE__ */ React2.createElement("div", { style: { display: "flex", gap: 4, marginBottom: 14, background: "var(--dc-surface)", borderRadius: 10, padding: 3 } }, ["social", "wallets"].map((v) => /* @__PURE__ */ React2.createElement(
4056
4273
  "button",
4057
4274
  {
4058
4275
  key: v,
@@ -4111,7 +4328,53 @@ function WalletSelector({
4111
4328
  e.currentTarget.style.borderColor = "var(--dc-border)";
4112
4329
  }
4113
4330
  }
4114
- ))), view === "social" && /* @__PURE__ */ React2.createElement("div", { style: { padding: "4px 20px 24px", display: "flex", flexDirection: "column", gap: 10 } }, socialErr && /* @__PURE__ */ React2.createElement("div", { style: { background: "rgba(204,51,0,0.1)", border: "1px solid var(--dc-danger)", borderRadius: 8, padding: "9px 12px", fontSize: 12, color: "var(--dc-danger)" } }, socialErr), socialAuthMethods.includes("google") && /* @__PURE__ */ React2.createElement(
4331
+ ))), view === "social" && /* @__PURE__ */ React2.createElement("div", { style: { padding: "4px 20px 24px", display: "flex", flexDirection: "column", gap: 10 } }, socialErr && /* @__PURE__ */ React2.createElement("div", { style: { background: "rgba(204,51,0,0.1)", border: "1px solid var(--dc-danger)", borderRadius: 8, padding: "9px 12px", fontSize: 12, color: "var(--dc-danger)" } }, socialErr), passkeyFirst && /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(
4332
+ "button",
4333
+ {
4334
+ onClick: handlePasskeyClick,
4335
+ disabled: socialLoad,
4336
+ style: {
4337
+ display: "flex",
4338
+ alignItems: "center",
4339
+ justifyContent: "center",
4340
+ gap: 10,
4341
+ width: "100%",
4342
+ padding: "13px 16px",
4343
+ background: "var(--dc-accent)",
4344
+ border: "none",
4345
+ borderRadius: 10,
4346
+ cursor: socialLoad ? "not-allowed" : "pointer",
4347
+ color: "var(--dc-accent-text)",
4348
+ fontSize: 14,
4349
+ fontWeight: 700,
4350
+ fontFamily: "inherit",
4351
+ opacity: socialLoad ? 0.6 : 1
4352
+ }
4353
+ },
4354
+ /* @__PURE__ */ React2.createElement("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }, /* @__PURE__ */ React2.createElement("circle", { cx: "12", cy: "8", r: "4" }), /* @__PURE__ */ React2.createElement("path", { d: "M6 21v-1a4 4 0 0 1 4-4h1" }), /* @__PURE__ */ React2.createElement("path", { d: "M17 15v6M14.5 18h5" })),
4355
+ socialLoad ? "Verifying\u2026" : "Continue with passkey"
4356
+ ), passkeyAddress && /* @__PURE__ */ React2.createElement("div", { style: { textAlign: "center", fontSize: 12, color: "var(--dc-text-muted)" } }, "Resuming ", passkeyAddress.slice(0, 6), "\u2026", passkeyAddress.slice(-4)), /* @__PURE__ */ React2.createElement(
4357
+ "button",
4358
+ {
4359
+ onClick: () => {
4360
+ setShowOtherMethods(true);
4361
+ setSocialErr("");
4362
+ },
4363
+ disabled: socialLoad,
4364
+ style: {
4365
+ background: "none",
4366
+ border: "none",
4367
+ padding: "6px 0",
4368
+ color: "var(--dc-text-muted)",
4369
+ fontSize: 12.5,
4370
+ cursor: socialLoad ? "not-allowed" : "pointer",
4371
+ fontFamily: "inherit",
4372
+ textDecoration: "underline",
4373
+ textUnderlineOffset: 3
4374
+ }
4375
+ },
4376
+ "Use a different account"
4377
+ )), !passkeyFirst && socialAuthMethods.includes("google") && /* @__PURE__ */ React2.createElement(
4115
4378
  "button",
4116
4379
  {
4117
4380
  onClick: handleGoogleClick,
@@ -4143,7 +4406,7 @@ function WalletSelector({
4143
4406
  },
4144
4407
  /* @__PURE__ */ React2.createElement("svg", { width: "18", height: "18", viewBox: "0 0 18 18", fill: "none" }, /* @__PURE__ */ React2.createElement("path", { d: "M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.875 2.684-6.615z", fill: "#4285F4" }), /* @__PURE__ */ React2.createElement("path", { d: "M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z", fill: "#34A853" }), /* @__PURE__ */ React2.createElement("path", { d: "M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z", fill: "#FBBC05" }), /* @__PURE__ */ React2.createElement("path", { d: "M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 6.29C4.672 4.163 6.656 3.58 9 3.58z", fill: "#EA4335" })),
4145
4408
  socialLoad ? "Redirecting\u2026" : "Continue with Google"
4146
- ), socialAuthMethods.includes("google") && socialAuthMethods.includes("email") && /* @__PURE__ */ React2.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React2.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } }), /* @__PURE__ */ React2.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", whiteSpace: "nowrap" } }, "or continue with email"), /* @__PURE__ */ React2.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } })), socialAuthMethods.includes("email") && (emailStep === "input" ? /* @__PURE__ */ React2.createElement("form", { onSubmit: handleEmailStart, style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ React2.createElement(
4409
+ ), !passkeyFirst && socialAuthMethods.includes("google") && socialAuthMethods.includes("email") && /* @__PURE__ */ React2.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React2.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } }), /* @__PURE__ */ React2.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", whiteSpace: "nowrap" } }, "or continue with email"), /* @__PURE__ */ React2.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } })), !passkeyFirst && socialAuthMethods.includes("email") && (emailStep === "input" ? /* @__PURE__ */ React2.createElement("form", { onSubmit: handleEmailStart, style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ React2.createElement(
4147
4410
  "input",
4148
4411
  {
4149
4412
  type: "email",
@@ -4262,7 +4525,38 @@ function WalletSelector({
4262
4525
  }
4263
4526
  },
4264
4527
  socialLoad ? "Verifying\u2026" : "Verify"
4265
- ))))), view === "wallets" && /* @__PURE__ */ React2.createElement("div", { style: { overflowY: "auto", flex: 1, padding: "0 12px 20px" } }, filtered.length > 0 ? /* @__PURE__ */ React2.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 2 } }, filtered.map((w) => /* @__PURE__ */ React2.createElement(WalletRow, { key: w.id, wallet: w, onClick: () => handleWalletClick(w) }))) : /* @__PURE__ */ React2.createElement("div", { style: { textAlign: "center", padding: "32px 16px", color: "var(--dc-text-muted)", fontSize: 13 } }, q ? `No wallets matching "${q}"` : "No wallets detected")), view === "wallets" && /* @__PURE__ */ React2.createElement("div", { style: { padding: "10px 20px 14px", borderTop: "1px solid var(--dc-border)", textAlign: "center", fontSize: 11, color: "var(--dc-text-muted)" } }, "Don't see your wallet? Make sure it's installed and unlocked"))
4528
+ )))), !passkeyFirst && customAuthLabel && onCustomAuthSignIn && /* @__PURE__ */ React2.createElement(React2.Fragment, null, (socialAuthMethods.includes("google") || socialAuthMethods.includes("email")) && /* @__PURE__ */ React2.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React2.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } }), /* @__PURE__ */ React2.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", whiteSpace: "nowrap" } }, "or"), /* @__PURE__ */ React2.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } })), /* @__PURE__ */ React2.createElement(
4529
+ "button",
4530
+ {
4531
+ onClick: handleCustomAuthClick,
4532
+ disabled: socialLoad,
4533
+ style: {
4534
+ display: "flex",
4535
+ alignItems: "center",
4536
+ justifyContent: "center",
4537
+ gap: 10,
4538
+ width: "100%",
4539
+ padding: "11px 16px",
4540
+ background: "var(--dc-surface)",
4541
+ border: "1px solid var(--dc-border)",
4542
+ borderRadius: 10,
4543
+ cursor: socialLoad ? "not-allowed" : "pointer",
4544
+ fontSize: 14,
4545
+ fontWeight: 600,
4546
+ color: "var(--dc-text)",
4547
+ fontFamily: "inherit",
4548
+ transition: "border-color 0.12s, background 0.12s",
4549
+ opacity: socialLoad ? 0.6 : 1
4550
+ },
4551
+ onMouseEnter: (e) => {
4552
+ if (!socialLoad) e.currentTarget.style.borderColor = "var(--dc-border-accent)";
4553
+ },
4554
+ onMouseLeave: (e) => {
4555
+ e.currentTarget.style.borderColor = "var(--dc-border)";
4556
+ }
4557
+ },
4558
+ socialLoad ? "Connecting\u2026" : customAuthLabel
4559
+ ))), view === "wallets" && /* @__PURE__ */ React2.createElement("div", { style: { overflowY: "auto", flex: 1, padding: "0 12px 20px" } }, filtered.length > 0 ? /* @__PURE__ */ React2.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 2 } }, filtered.map((w) => /* @__PURE__ */ React2.createElement(WalletRow, { key: w.id, wallet: w, onClick: () => handleWalletClick(w) }))) : /* @__PURE__ */ React2.createElement("div", { style: { textAlign: "center", padding: "32px 16px", color: "var(--dc-text-muted)", fontSize: 13 } }, q ? `No wallets matching "${q}"` : "No wallets detected")), view === "wallets" && /* @__PURE__ */ React2.createElement("div", { style: { padding: "10px 20px 14px", borderTop: "1px solid var(--dc-border)", textAlign: "center", fontSize: 11, color: "var(--dc-text-muted)" } }, "Don't see your wallet? Make sure it's installed and unlocked"))
4266
4560
  )
4267
4561
  )
4268
4562
  );
@@ -4384,13 +4678,16 @@ function SignInView({
4384
4678
  onEmailStart,
4385
4679
  onEmailVerify,
4386
4680
  authMethods,
4387
- resumeHint
4681
+ resumeHint,
4682
+ customAuthLabel,
4683
+ onCustomAuthSignIn
4388
4684
  }) {
4389
4685
  const [googleLoading, setGoogleLoading] = useState2(false);
4390
4686
  const [email, setEmail] = useState2("");
4391
4687
  const [code, setCode] = useState2("");
4392
4688
  const [emailSent, setEmailSent] = useState2(false);
4393
4689
  const [emailLoading, setEmailLoading] = useState2(false);
4690
+ const [customLoading, setCustomLoading] = useState2(false);
4394
4691
  const [error, setError] = useState2("");
4395
4692
  async function handleGoogle() {
4396
4693
  setError("");
@@ -4427,6 +4724,18 @@ function SignInView({
4427
4724
  setEmailLoading(false);
4428
4725
  }
4429
4726
  }
4727
+ async function handleCustomAuth() {
4728
+ if (!onCustomAuthSignIn) return;
4729
+ setError("");
4730
+ setCustomLoading(true);
4731
+ try {
4732
+ await onCustomAuthSignIn();
4733
+ } catch (e) {
4734
+ setError(e.message);
4735
+ } finally {
4736
+ setCustomLoading(false);
4737
+ }
4738
+ }
4430
4739
  const inputStyle = {
4431
4740
  width: "100%",
4432
4741
  background: "var(--dc-surface)",
@@ -4499,7 +4808,27 @@ function SignInView({
4499
4808
  fontWeight: 700,
4500
4809
  cursor: "pointer",
4501
4810
  fontFamily: "inherit"
4502
- } }, emailLoading ? "\u2026" : "Verify"))), error && /* @__PURE__ */ React3.createElement("div", { style: { marginTop: 10, fontSize: 12, color: "var(--dc-danger, #ff5533)" } }, error));
4811
+ } }, emailLoading ? "\u2026" : "Verify"))), customAuthLabel && onCustomAuthSignIn && /* @__PURE__ */ React3.createElement(React3.Fragment, null, (authMethods.includes("google") || authMethods.includes("email")) && /* @__PURE__ */ React3.createElement("div", { style: { textAlign: "center", color: "var(--dc-text-muted)", fontSize: 12, margin: "10px 0" } }, "\u2014 or \u2014"), /* @__PURE__ */ React3.createElement(
4812
+ "button",
4813
+ {
4814
+ onClick: handleCustomAuth,
4815
+ disabled: customLoading,
4816
+ style: {
4817
+ width: "100%",
4818
+ padding: "10px 0",
4819
+ background: "var(--dc-surface)",
4820
+ color: "var(--dc-text)",
4821
+ border: "1px solid var(--dc-border)",
4822
+ borderRadius: 10,
4823
+ fontSize: 13,
4824
+ fontWeight: 600,
4825
+ cursor: customLoading ? "not-allowed" : "pointer",
4826
+ opacity: customLoading ? 0.7 : 1,
4827
+ fontFamily: "inherit"
4828
+ }
4829
+ },
4830
+ customLoading ? "Connecting\u2026" : customAuthLabel
4831
+ )), error && /* @__PURE__ */ React3.createElement("div", { style: { marginTop: 10, fontSize: 12, color: "var(--dc-danger, #ff5533)" } }, error));
4503
4832
  }
4504
4833
  function RecoverySection({
4505
4834
  addresses,
@@ -4616,6 +4945,8 @@ function SocialWalletModal({
4616
4945
  onEmailStart,
4617
4946
  onEmailVerify,
4618
4947
  authMethods = ["google", "email"],
4948
+ customAuthLabel,
4949
+ onCustomAuthSignIn,
4619
4950
  onRotateShares,
4620
4951
  onExportPortableBackup,
4621
4952
  rpcUrls,
@@ -4771,7 +5102,9 @@ function SocialWalletModal({
4771
5102
  onEmailStart,
4772
5103
  onEmailVerify,
4773
5104
  authMethods,
4774
- resumeHint: socialAuth.needsReconnect ? "your wallet" : void 0
5105
+ resumeHint: socialAuth.needsReconnect ? "your wallet" : void 0,
5106
+ customAuthLabel,
5107
+ onCustomAuthSignIn
4775
5108
  }
4776
5109
  ) : /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement("div", { style: {
4777
5110
  margin: "0 16px 14px",
@@ -4864,7 +5197,8 @@ var DEFAULT_SOCIAL = {
4864
5197
  isNewUser: false,
4865
5198
  isUnlocked: false,
4866
5199
  sessionExpiresAt: null,
4867
- needsReconnect: false
5200
+ needsReconnect: false,
5201
+ canUsePasskey: false
4868
5202
  };
4869
5203
  var DecaneContext = createContext(null);
4870
5204
  function hasSocial(mode) {
@@ -4879,6 +5213,16 @@ function deriveNetworks(mode, explicit) {
4879
5213
  }
4880
5214
  function DecaneKit({ config, children }) {
4881
5215
  const mode = config?.mode ?? "wallets";
5216
+ if (!config?.appId) {
5217
+ throw new Error(
5218
+ "DecaneKit: config.appId is required. Pass your dashboard project ID."
5219
+ );
5220
+ }
5221
+ if (hasSocial(mode) && !config.social?.apiKey) {
5222
+ throw new Error(
5223
+ `DecaneKit: config.social.apiKey is required for social sign-in (mode "${mode}"). Create an API key for this project in the dashboard.`
5224
+ );
5225
+ }
4882
5226
  const walletRef = useRef(null);
4883
5227
  if (!walletRef.current) walletRef.current = createDecane(config);
4884
5228
  const socialRef = useRef(null);
@@ -4909,6 +5253,7 @@ function DecaneKit({ config, children }) {
4909
5253
  socialInitialized.current = true;
4910
5254
  const { apiKey, authMethods, promptPin, apiBaseUrl, chains, rpcUrls } = config.social;
4911
5255
  createDecaneConnect({
5256
+ appId: config.appId,
4912
5257
  apiKey,
4913
5258
  apiBaseUrl,
4914
5259
  authMethods: authMethods ?? ["google", "email"],
@@ -4926,6 +5271,8 @@ function DecaneKit({ config, children }) {
4926
5271
  if (saved) {
4927
5272
  setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false, needsReconnect: true }));
4928
5273
  }
5274
+ sdk.canSignInWithPasskey().then((can) => setSocialAuth((p) => ({ ...p, canUsePasskey: can }))).catch(() => {
5275
+ });
4929
5276
  const params = new URLSearchParams(window.location.search);
4930
5277
  if (params.has("decane_jwt") || params.has("decane_error")) {
4931
5278
  sdk.connectWithGoogle().then(applyResult).catch(console.error);
@@ -4939,9 +5286,20 @@ function DecaneKit({ config, children }) {
4939
5286
  isNewUser: result.isNewUser,
4940
5287
  isUnlocked: sdk?.isUnlocked() ?? false,
4941
5288
  sessionExpiresAt: sdk?.sessionExpiresAt() ?? null,
4942
- needsReconnect: false
5289
+ needsReconnect: false,
5290
+ canUsePasskey: false
5291
+ });
5292
+ sdk?.canSignInWithPasskey().then((can) => setSocialAuth((p) => ({ ...p, canUsePasskey: can }))).catch(() => {
4943
5293
  });
4944
5294
  }
5295
+ const signInWithPasskey = useCallback3(async () => {
5296
+ const sdk = socialRef.current;
5297
+ if (!sdk) throw new Error("Social sign-in not configured");
5298
+ const result = await sdk.signInWithPasskey();
5299
+ applyResult(result);
5300
+ setSelectorOpen(false);
5301
+ return result;
5302
+ }, []);
4945
5303
  const connectWithGoogle = useCallback3(async () => {
4946
5304
  const sdk = socialRef.current;
4947
5305
  if (!sdk) throw new Error("Social sign-in not configured");
@@ -4959,6 +5317,19 @@ function DecaneKit({ config, children }) {
4959
5317
  applyResult(result);
4960
5318
  setSelectorOpen(false);
4961
5319
  }, []);
5320
+ const connectWithToken = useCallback3(async (token, opts) => {
5321
+ const sdk = socialRef.current;
5322
+ if (!sdk) throw new Error("Social sign-in not configured");
5323
+ const result = await sdk.connectWithToken(token, opts);
5324
+ applyResult(result);
5325
+ setSelectorOpen(false);
5326
+ }, []);
5327
+ const customAuthConfig = config?.social?.customAuth;
5328
+ const connectWithCustomAuth = useCallback3(async () => {
5329
+ if (!customAuthConfig) throw new Error("Custom auth not configured");
5330
+ const token = await customAuthConfig.getToken();
5331
+ await connectWithToken(token, customAuthConfig.providerId ? { providerId: customAuthConfig.providerId } : void 0);
5332
+ }, [customAuthConfig, connectWithToken]);
4962
5333
  const socialUnlock = useCallback3(async () => {
4963
5334
  const sdk = socialRef.current;
4964
5335
  if (!sdk) return;
@@ -5035,9 +5406,13 @@ function DecaneKit({ config, children }) {
5035
5406
  setTheme,
5036
5407
  mode,
5037
5408
  socialAuth,
5409
+ signInWithPasskey,
5038
5410
  connectWithGoogle,
5039
5411
  connectWithEmail,
5040
5412
  verifyEmailCode: verifyEmailCode2,
5413
+ connectWithToken,
5414
+ customAuthLabel: customAuthConfig?.label,
5415
+ connectWithCustomAuth: customAuthConfig ? connectWithCustomAuth : void 0,
5041
5416
  socialUnlock,
5042
5417
  socialDisconnect,
5043
5418
  socialSignMessage,
@@ -5067,7 +5442,12 @@ function DecaneKit({ config, children }) {
5067
5442
  socialAuthMethods: config?.social?.authMethods ?? ["google", "email"],
5068
5443
  onGoogleSignIn: connectWithGoogle,
5069
5444
  onEmailStart: connectWithEmail,
5070
- onEmailVerify: verifyEmailCode2
5445
+ onEmailVerify: verifyEmailCode2,
5446
+ canUsePasskey: socialAuth.canUsePasskey,
5447
+ passkeyAddress: socialAuth.addresses?.evm ?? socialAuth.addresses?.solana ?? null,
5448
+ onPasskeySignIn: signInWithPasskey,
5449
+ customAuthLabel: customAuthConfig?.label,
5450
+ onCustomAuthSignIn: customAuthConfig ? connectWithCustomAuth : void 0
5071
5451
  }
5072
5452
  ),
5073
5453
  /* @__PURE__ */ React4.createElement(
@@ -5079,6 +5459,8 @@ function DecaneKit({ config, children }) {
5079
5459
  socialAuth,
5080
5460
  onUnlock: socialUnlock,
5081
5461
  onDisconnect: socialDisconnect,
5462
+ customAuthLabel: customAuthConfig?.label,
5463
+ onCustomAuthSignIn: customAuthConfig ? connectWithCustomAuth : void 0,
5082
5464
  onGoogleSignIn: connectWithGoogle,
5083
5465
  onEmailStart: connectWithEmail,
5084
5466
  onEmailVerify: verifyEmailCode2,
@@ -5109,11 +5491,16 @@ function useSocialAuth() {
5109
5491
  connectWithGoogle,
5110
5492
  connectWithEmail,
5111
5493
  verifyEmailCode: verifyEmailCode2,
5494
+ connectWithToken,
5495
+ customAuthLabel,
5496
+ connectWithCustomAuth,
5112
5497
  socialUnlock,
5113
5498
  socialDisconnect
5114
5499
  } = useDecaneContext();
5115
5500
  const [googleLoading, setGoogleLoading] = useState4(false);
5116
5501
  const [emailLoading, setEmailLoading] = useState4(false);
5502
+ const [tokenLoading, setTokenLoading] = useState4(false);
5503
+ const [customLoading, setCustomLoading] = useState4(false);
5117
5504
  const [error, setError] = useState4(null);
5118
5505
  const signInWithGoogle = useCallback4(async () => {
5119
5506
  setGoogleLoading(true);
@@ -5150,6 +5537,31 @@ function useSocialAuth() {
5150
5537
  setEmailLoading(false);
5151
5538
  }
5152
5539
  }, [verifyEmailCode2]);
5540
+ const signInWithToken = useCallback4(async (token, opts) => {
5541
+ setTokenLoading(true);
5542
+ setError(null);
5543
+ try {
5544
+ await connectWithToken(token, opts);
5545
+ } catch (e) {
5546
+ setError(e.message);
5547
+ throw e;
5548
+ } finally {
5549
+ setTokenLoading(false);
5550
+ }
5551
+ }, [connectWithToken]);
5552
+ const signInWithCustomAuth = useCallback4(async () => {
5553
+ if (!connectWithCustomAuth) throw new Error("Custom auth not configured");
5554
+ setCustomLoading(true);
5555
+ setError(null);
5556
+ try {
5557
+ await connectWithCustomAuth();
5558
+ } catch (e) {
5559
+ setError(e.message);
5560
+ throw e;
5561
+ } finally {
5562
+ setCustomLoading(false);
5563
+ }
5564
+ }, [connectWithCustomAuth]);
5153
5565
  const unlock = useCallback4(async () => {
5154
5566
  setError(null);
5155
5567
  try {
@@ -5181,11 +5593,16 @@ function useSocialAuth() {
5181
5593
  needsReconnect: socialAuth.needsReconnect,
5182
5594
  googleLoading,
5183
5595
  emailLoading,
5596
+ tokenLoading,
5597
+ customLoading,
5598
+ customAuthLabel,
5184
5599
  error,
5185
5600
  clearError: () => setError(null),
5186
5601
  signInWithGoogle,
5187
5602
  sendEmailCode,
5188
5603
  confirmEmailCode,
5604
+ signInWithToken,
5605
+ signInWithCustomAuth: connectWithCustomAuth ? signInWithCustomAuth : void 0,
5189
5606
  unlock,
5190
5607
  disconnect
5191
5608
  };