decane-connect-kit 2.19.0 → 2.21.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.cjs CHANGED
@@ -2028,6 +2028,41 @@ var EventEmitter = class {
2028
2028
  }
2029
2029
  };
2030
2030
 
2031
+ // src/social/auth/redirectResult.ts
2032
+ function readRedirectOutcome(search) {
2033
+ const params = new URLSearchParams(search);
2034
+ const error = params.get("decane_error");
2035
+ if (error) return { kind: "error", error };
2036
+ const jwt = params.get("decane_jwt");
2037
+ if (!jwt) return { kind: "none" };
2038
+ const name = params.get("decane_name");
2039
+ const email = params.get("decane_email");
2040
+ const picture = params.get("decane_picture");
2041
+ const username = params.get("decane_username");
2042
+ const providerSubject = params.get("decane_provider_subject");
2043
+ const profile = {
2044
+ ...name ? { name } : {},
2045
+ ...email ? { email } : {},
2046
+ ...picture ? { picture } : {},
2047
+ ...username ? { username } : {}
2048
+ };
2049
+ return {
2050
+ kind: "result",
2051
+ result: {
2052
+ jwt,
2053
+ userId: params.get("decane_user_id") ?? "",
2054
+ isNewUser: params.get("decane_is_new_user") === "true",
2055
+ hasShare: params.get("decane_has_share") === "true",
2056
+ // Omitted entirely when the provider released nothing, so callers can
2057
+ // test `result.profile` rather than an object that is always present but
2058
+ // sometimes empty. X never sends an email, so this is the normal case
2059
+ // there rather than an edge one.
2060
+ ...Object.keys(profile).length ? { profile } : {},
2061
+ ...providerSubject ? { providerSubject } : {}
2062
+ }
2063
+ };
2064
+ }
2065
+
2031
2066
  // src/social/api.ts
2032
2067
  init_errors();
2033
2068
  var ERROR_MAP = {
@@ -2079,6 +2114,14 @@ var ApiClient = class {
2079
2114
  async getGoogleInitUrl() {
2080
2115
  return this._request("/auth/google/init", { method: "GET" });
2081
2116
  }
2117
+ /**
2118
+ * The X consent URL. Same contract as Google's: the backend holds the client
2119
+ * secret and the PKCE verifier, and answers 503 X_NOT_CONFIGURED when the
2120
+ * deployment has no X credentials.
2121
+ */
2122
+ async getXInitUrl() {
2123
+ return this._request("/auth/x/init", { method: "GET" });
2124
+ }
2082
2125
  /**
2083
2126
  * Exchange a Google ID token the app already holds for a Decane session.
2084
2127
  * Resolves to the SAME identity as the redirect flow, so one Google account
@@ -4378,6 +4421,21 @@ async function createDecaneConnect(config) {
4378
4421
  ...stored.profile ? { profile: stored.profile } : {}
4379
4422
  };
4380
4423
  }
4424
+ async function connectViaRedirect(label, getInitUrl) {
4425
+ const outcome = readRedirectOutcome(window.location.search);
4426
+ if (outcome.kind === "error") {
4427
+ window.history.replaceState({}, "", window.location.pathname);
4428
+ throw new CallSequenceError(`${label} sign-in failed: ${outcome.error}`);
4429
+ }
4430
+ if (outcome.kind === "result") {
4431
+ window.history.replaceState({}, "", window.location.pathname);
4432
+ return handleAuthResult(outcome.result);
4433
+ }
4434
+ const { url } = await getInitUrl();
4435
+ window.location.href = url;
4436
+ return new Promise(() => {
4437
+ });
4438
+ }
4381
4439
  async function handleAuthResult(result) {
4382
4440
  let addresses;
4383
4441
  if (!result.hasShare) {
@@ -4396,7 +4454,8 @@ async function createDecaneConnect(config) {
4396
4454
  return {
4397
4455
  addresses,
4398
4456
  isNewUser: result.isNewUser,
4399
- ...result.profile ? { profile: result.profile } : {}
4457
+ ...result.profile ? { profile: result.profile } : {},
4458
+ ...result.providerSubject ? { providerSubject: result.providerSubject } : {}
4400
4459
  };
4401
4460
  }
4402
4461
  const client = {
@@ -4424,39 +4483,33 @@ async function createDecaneConnect(config) {
4424
4483
  throw new CallSequenceError(`Unknown auth method: ${method}`);
4425
4484
  },
4426
4485
  async connectWithGoogle() {
4427
- const params = new URLSearchParams(window.location.search);
4428
- const decaneJwt = params.get("decane_jwt");
4429
- const decaneError = params.get("decane_error");
4430
- if (decaneError) {
4431
- window.history.replaceState({}, "", window.location.pathname);
4432
- throw new CallSequenceError(`Google sign-in failed: ${decaneError}`);
4433
- }
4434
- if (decaneJwt) {
4435
- const name = params.get("decane_name");
4436
- const email = params.get("decane_email");
4437
- const picture = params.get("decane_picture");
4438
- const profile = {
4439
- ...name ? { name } : {},
4440
- ...email ? { email } : {},
4441
- ...picture ? { picture } : {}
4442
- };
4443
- const result = {
4444
- jwt: decaneJwt,
4445
- userId: params.get("decane_user_id") ?? "",
4446
- isNewUser: params.get("decane_is_new_user") === "true",
4447
- hasShare: params.get("decane_has_share") === "true",
4448
- // Omitted entirely when Google released nothing, so callers can test
4449
- // `result.profile` rather than an object that is always present but
4450
- // sometimes empty.
4451
- ...Object.keys(profile).length ? { profile } : {}
4452
- };
4453
- window.history.replaceState({}, "", window.location.pathname);
4454
- return handleAuthResult(result);
4486
+ return connectViaRedirect("Google", () => api.getGoogleInitUrl());
4487
+ },
4488
+ /**
4489
+ * Finish a redirect sign-in that is already sitting in the URL, whichever
4490
+ * provider started it.
4491
+ *
4492
+ * Google and X bounce back with the same decane_* params, so the page that
4493
+ * receives them cannot tell which one ran — and should not have to. Calling
4494
+ * connectWithGoogle() here would work by accident and then report an X
4495
+ * failure as "Google sign-in failed".
4496
+ *
4497
+ * Throws if there is nothing to finish: unlike the connectWith* methods,
4498
+ * this never starts a flow.
4499
+ */
4500
+ async completeRedirectSignIn() {
4501
+ const outcome = readRedirectOutcome(window.location.search);
4502
+ if (outcome.kind === "none") {
4503
+ throw new CallSequenceError("No sign-in result in the URL to complete");
4455
4504
  }
4456
- const { url } = await api.getGoogleInitUrl();
4457
- window.location.href = url;
4458
- return new Promise(() => {
4459
- });
4505
+ window.history.replaceState({}, "", window.location.pathname);
4506
+ if (outcome.kind === "error") {
4507
+ throw new CallSequenceError(`Sign-in failed: ${outcome.error}`);
4508
+ }
4509
+ return handleAuthResult(outcome.result);
4510
+ },
4511
+ async connectWithX() {
4512
+ return connectViaRedirect("X", () => api.getXInitUrl());
4460
4513
  },
4461
4514
  async connectWithGoogleToken(idToken) {
4462
4515
  if (!idToken) throw new CallSequenceError("connectWithGoogleToken requires an ID token");
@@ -5280,6 +5333,20 @@ function ChainIconStack({ support, size = 16 }) {
5280
5333
  /* @__PURE__ */ import_react.default.createElement(ChainCircleIcon, { chain, size })
5281
5334
  )));
5282
5335
  }
5336
+ function XLogo({ size = 14 }) {
5337
+ return /* @__PURE__ */ import_react.default.createElement(
5338
+ "svg",
5339
+ {
5340
+ width: size,
5341
+ height: size,
5342
+ viewBox: "0 0 24 24",
5343
+ fill: "currentColor",
5344
+ "aria-hidden": "true",
5345
+ focusable: "false"
5346
+ },
5347
+ /* @__PURE__ */ import_react.default.createElement("path", { d: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" })
5348
+ );
5349
+ }
5283
5350
 
5284
5351
  // src/ui/WalletSelector.tsx
5285
5352
  var FONT_URL = "https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap";
@@ -5421,6 +5488,7 @@ function WalletSelector({
5421
5488
  socialAuthMethods = ["google", "email"],
5422
5489
  onGoogleSignIn,
5423
5490
  onKingsChatSignIn,
5491
+ onXSignIn,
5424
5492
  onEmailStart,
5425
5493
  onEmailVerify,
5426
5494
  onPhoneStart,
@@ -5515,6 +5583,17 @@ function WalletSelector({
5515
5583
  setSocialLoad(false);
5516
5584
  }
5517
5585
  }
5586
+ async function handleXClick() {
5587
+ if (!onXSignIn) return;
5588
+ setSocialLoad(true);
5589
+ setSocialErr("");
5590
+ try {
5591
+ await onXSignIn();
5592
+ } catch (e) {
5593
+ setSocialErr(e.message);
5594
+ setSocialLoad(false);
5595
+ }
5596
+ }
5518
5597
  async function handleCustomAuthClick() {
5519
5598
  if (!onCustomAuthSignIn) return;
5520
5599
  setSocialLoad(true);
@@ -5996,6 +6075,38 @@ function WalletSelector({
5996
6075
  },
5997
6076
  /* @__PURE__ */ import_react2.default.createElement("svg", { width: "18", height: "18", viewBox: "0 0 18 18", fill: "none", "aria-hidden": "true" }, /* @__PURE__ */ import_react2.default.createElement("circle", { cx: "9", cy: "9", r: "9", fill: "var(--dc-accent)" }), /* @__PURE__ */ import_react2.default.createElement("path", { d: "M5.5 4.5h1.9v3.6l3.1-3.6h2.3l-3.3 3.8 3.5 5.2h-2.3l-2.5-3.8-.8.9v2.9H5.5V4.5z", fill: "var(--dc-accent-text)" })),
5998
6077
  socialLoad ? "Waiting for KingsChat\u2026" : "Continue with KingsChat"
6078
+ ), !passkeyFirst && socialAuthMethods.includes("x") && onXSignIn && /* @__PURE__ */ import_react2.default.createElement(
6079
+ "button",
6080
+ {
6081
+ onClick: handleXClick,
6082
+ disabled: socialLoad,
6083
+ style: {
6084
+ display: "flex",
6085
+ alignItems: "center",
6086
+ justifyContent: "center",
6087
+ gap: 10,
6088
+ width: "100%",
6089
+ padding: "11px 16px",
6090
+ background: "var(--dc-surface)",
6091
+ border: "1px solid var(--dc-border)",
6092
+ borderRadius: 10,
6093
+ cursor: socialLoad ? "not-allowed" : "pointer",
6094
+ fontSize: 14,
6095
+ fontWeight: 600,
6096
+ color: "var(--dc-text)",
6097
+ fontFamily: "inherit",
6098
+ transition: "border-color 0.12s, background 0.12s",
6099
+ opacity: socialLoad ? 0.6 : 1
6100
+ },
6101
+ onMouseEnter: (e) => {
6102
+ if (!socialLoad) e.currentTarget.style.borderColor = "var(--dc-border-accent)";
6103
+ },
6104
+ onMouseLeave: (e) => {
6105
+ e.currentTarget.style.borderColor = "var(--dc-border)";
6106
+ }
6107
+ },
6108
+ /* @__PURE__ */ import_react2.default.createElement(XLogo, { size: 16 }),
6109
+ socialLoad ? "Redirecting\u2026" : "Continue with X"
5999
6110
  ), !passkeyFirst && socialAuthMethods.includes("google") && socialAuthMethods.includes("email") && /* @__PURE__ */ import_react2.default.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ import_react2.default.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } }), /* @__PURE__ */ import_react2.default.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", whiteSpace: "nowrap" } }, "or continue with email"), /* @__PURE__ */ import_react2.default.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } })), !passkeyFirst && socialAuthMethods.includes("email") && (emailStep === "input" ? /* @__PURE__ */ import_react2.default.createElement("form", { onSubmit: handleEmailStart, style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ import_react2.default.createElement(
6000
6111
  "input",
6001
6112
  {
@@ -6363,6 +6474,7 @@ function AddressRow({
6363
6474
  function SignInView({
6364
6475
  onGoogleSignIn,
6365
6476
  onKingsChatSignIn,
6477
+ onXSignIn,
6366
6478
  onEmailStart,
6367
6479
  onEmailVerify,
6368
6480
  onPhoneStart,
@@ -6377,6 +6489,7 @@ function SignInView({
6377
6489
  }) {
6378
6490
  const [googleLoading, setGoogleLoading] = (0, import_react3.useState)(false);
6379
6491
  const [kingsChatLoading, setKingsChatLoading] = (0, import_react3.useState)(false);
6492
+ const [xLoading, setXLoading] = (0, import_react3.useState)(false);
6380
6493
  const [email, setEmail] = (0, import_react3.useState)("");
6381
6494
  const [code, setCode] = (0, import_react3.useState)("");
6382
6495
  const [emailSent, setEmailSent] = (0, import_react3.useState)(false);
@@ -6427,6 +6540,17 @@ function SignInView({
6427
6540
  setKingsChatLoading(false);
6428
6541
  }
6429
6542
  }
6543
+ async function handleX() {
6544
+ if (!onXSignIn) return;
6545
+ setError("");
6546
+ setXLoading(true);
6547
+ try {
6548
+ await onXSignIn();
6549
+ } catch (e) {
6550
+ setError(e.message);
6551
+ setXLoading(false);
6552
+ }
6553
+ }
6430
6554
  async function handleEmailStart(e) {
6431
6555
  e.preventDefault();
6432
6556
  setError("");
@@ -6585,6 +6709,32 @@ function SignInView({
6585
6709
  }
6586
6710
  },
6587
6711
  kingsChatLoading ? "Waiting for KingsChat\u2026" : "Continue with KingsChat"
6712
+ ), !passkeyFirst && authMethods.includes("x") && onXSignIn && /* @__PURE__ */ import_react3.default.createElement(
6713
+ "button",
6714
+ {
6715
+ onClick: handleX,
6716
+ disabled: xLoading,
6717
+ style: {
6718
+ width: "100%",
6719
+ padding: "10px 0",
6720
+ marginBottom: 10,
6721
+ background: "var(--dc-surface)",
6722
+ color: "var(--dc-text)",
6723
+ border: "1px solid var(--dc-border)",
6724
+ borderRadius: 10,
6725
+ fontSize: 13,
6726
+ fontWeight: 700,
6727
+ cursor: xLoading ? "not-allowed" : "pointer",
6728
+ opacity: xLoading ? 0.7 : 1,
6729
+ fontFamily: "inherit",
6730
+ display: "flex",
6731
+ alignItems: "center",
6732
+ justifyContent: "center",
6733
+ gap: 8
6734
+ }
6735
+ },
6736
+ /* @__PURE__ */ import_react3.default.createElement(XLogo, null),
6737
+ xLoading ? "Redirecting\u2026" : "Continue with X"
6588
6738
  ), !passkeyFirst && authMethods.includes("email") && /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, authMethods.includes("google") && /* @__PURE__ */ import_react3.default.createElement("div", { style: { textAlign: "center", color: "var(--dc-text-muted)", fontSize: 12, margin: "10px 0" } }, "\u2014 or \u2014"), !emailSent ? /* @__PURE__ */ import_react3.default.createElement("form", { onSubmit: handleEmailStart, style: { display: "flex", gap: 8 } }, /* @__PURE__ */ import_react3.default.createElement(
6589
6739
  "input",
6590
6740
  {
@@ -7118,6 +7268,7 @@ var DEFAULT_SOCIAL = {
7118
7268
  addresses: null,
7119
7269
  isNewUser: false,
7120
7270
  profile: null,
7271
+ providerSubject: null,
7121
7272
  isUnlocked: false,
7122
7273
  sessionExpiresAt: null,
7123
7274
  needsReconnect: false,
@@ -7240,7 +7391,7 @@ function DecaneKit({ config, children }) {
7240
7391
  });
7241
7392
  const params = new URLSearchParams(window.location.search);
7242
7393
  if (params.has("decane_jwt") || params.has("decane_error")) {
7243
- sdk.connectWithGoogle().then(applyResult).catch((e) => {
7394
+ sdk.completeRedirectSignIn().then(applyResult).catch((e) => {
7244
7395
  setSocialAuth((p) => p.phase ? { ...p, phase: null } : p);
7245
7396
  console.error(e);
7246
7397
  });
@@ -7253,6 +7404,7 @@ function DecaneKit({ config, children }) {
7253
7404
  addresses: result.addresses,
7254
7405
  isNewUser: result.isNewUser,
7255
7406
  profile: result.profile ?? null,
7407
+ providerSubject: result.providerSubject ?? null,
7256
7408
  isUnlocked: sdk?.isUnlocked() ?? false,
7257
7409
  sessionExpiresAt: sdk?.sessionExpiresAt() ?? null,
7258
7410
  needsReconnect: false,
@@ -7281,6 +7433,11 @@ function DecaneKit({ config, children }) {
7281
7433
  if (!sdk) throw new Error("Social sign-in not configured");
7282
7434
  await sdk.connectWithGoogle();
7283
7435
  }, []);
7436
+ const connectWithX = (0, import_react5.useCallback)(async () => {
7437
+ const sdk = socialRef.current;
7438
+ if (!sdk) throw new Error("Social sign-in not configured");
7439
+ await sdk.connectWithX();
7440
+ }, []);
7284
7441
  const connectWithKingsChat = (0, import_react5.useCallback)(async () => {
7285
7442
  const sdk = socialRef.current;
7286
7443
  if (!sdk) throw new Error("Social sign-in not configured");
@@ -7501,6 +7658,7 @@ function DecaneKit({ config, children }) {
7501
7658
  signInWithPasskey,
7502
7659
  connectWithGoogle,
7503
7660
  connectWithKingsChat,
7661
+ connectWithX,
7504
7662
  connectWithEmail,
7505
7663
  verifyEmailCode: verifyEmailCode2,
7506
7664
  connectWithPhone,
@@ -7552,6 +7710,7 @@ function DecaneKit({ config, children }) {
7552
7710
  socialAuthMethods: config?.social?.authMethods ?? ["google", "email"],
7553
7711
  onGoogleSignIn: connectWithGoogle,
7554
7712
  onKingsChatSignIn: connectWithKingsChat,
7713
+ onXSignIn: connectWithX,
7555
7714
  onEmailStart: connectWithEmail,
7556
7715
  onEmailVerify: verifyEmailCode2,
7557
7716
  onPhoneStart: connectWithPhone,
@@ -7576,6 +7735,7 @@ function DecaneKit({ config, children }) {
7576
7735
  onCustomAuthSignIn: customAuthConfig ? connectWithCustomAuth : void 0,
7577
7736
  onGoogleSignIn: connectWithGoogle,
7578
7737
  onKingsChatSignIn: connectWithKingsChat,
7738
+ onXSignIn: connectWithX,
7579
7739
  onEmailStart: connectWithEmail,
7580
7740
  onEmailVerify: verifyEmailCode2,
7581
7741
  onPhoneStart: connectWithPhone,
@@ -7610,6 +7770,7 @@ function useSocialAuth() {
7610
7770
  socialAuth,
7611
7771
  connectWithGoogle,
7612
7772
  connectWithKingsChat,
7773
+ connectWithX,
7613
7774
  connectWithEmail,
7614
7775
  verifyEmailCode: verifyEmailCode2,
7615
7776
  connectWithPhone,
@@ -7623,6 +7784,7 @@ function useSocialAuth() {
7623
7784
  } = useDecaneContext();
7624
7785
  const [googleLoading, setGoogleLoading] = (0, import_react6.useState)(false);
7625
7786
  const [kingschatLoading, setKingschatLoading] = (0, import_react6.useState)(false);
7787
+ const [xLoading, setXLoading] = (0, import_react6.useState)(false);
7626
7788
  const [emailLoading, setEmailLoading] = (0, import_react6.useState)(false);
7627
7789
  const [phoneLoading, setPhoneLoading] = (0, import_react6.useState)(false);
7628
7790
  const [tokenLoading, setTokenLoading] = (0, import_react6.useState)(false);
@@ -7638,6 +7800,16 @@ function useSocialAuth() {
7638
7800
  setGoogleLoading(false);
7639
7801
  }
7640
7802
  }, [connectWithGoogle]);
7803
+ const signInWithX = (0, import_react6.useCallback)(async () => {
7804
+ setXLoading(true);
7805
+ setError(null);
7806
+ try {
7807
+ await connectWithX();
7808
+ } catch (e) {
7809
+ setError(e.message);
7810
+ setXLoading(false);
7811
+ }
7812
+ }, [connectWithX]);
7641
7813
  const signInWithKingsChat = (0, import_react6.useCallback)(async () => {
7642
7814
  setKingschatLoading(true);
7643
7815
  setError(null);
@@ -7765,6 +7937,9 @@ function useSocialAuth() {
7765
7937
  // user first signed in, so it carries the same name and email a Google
7766
7938
  // sign-in does rather than coming back anonymous.
7767
7939
  profile: socialAuth.profile,
7940
+ // The provider's stable id, where it gives one. Null otherwise. Safe to
7941
+ // match an account on, unlike the handle in `profile.username`.
7942
+ providerSubject: socialAuth.providerSubject,
7768
7943
  // Long-running work in progress, for driving your own status UI.
7769
7944
  phase: socialAuth.phase,
7770
7945
  isCreatingWallet: socialAuth.phase === "creating",
@@ -7775,6 +7950,7 @@ function useSocialAuth() {
7775
7950
  needsReconnect: socialAuth.needsReconnect,
7776
7951
  googleLoading,
7777
7952
  kingschatLoading,
7953
+ xLoading,
7778
7954
  emailLoading,
7779
7955
  phoneLoading,
7780
7956
  tokenLoading,
@@ -7784,6 +7960,7 @@ function useSocialAuth() {
7784
7960
  clearError: () => setError(null),
7785
7961
  signInWithGoogle,
7786
7962
  signInWithKingsChat,
7963
+ signInWithX,
7787
7964
  sendEmailCode,
7788
7965
  confirmEmailCode,
7789
7966
  sendPhoneCode,