@multiplatform.one/web3 8.2.1 → 8.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/cjs/connectkit/siwe/SIWEContext.native.js.map +1 -1
  2. package/dist/cjs/connectkit/siwe/SIWEProvider.cjs +19 -16
  3. package/dist/cjs/connectkit/siwe/SIWEProvider.native.js +26 -16
  4. package/dist/cjs/connectkit/siwe/SIWEProvider.native.js.map +1 -1
  5. package/dist/cjs/keycloak/KeycloakWalletLogin.cjs +67 -7
  6. package/dist/cjs/keycloak/useWalletLogin.cjs +3 -2
  7. package/dist/esm/connectkit/siwe/SIWEContext.mjs.map +1 -1
  8. package/dist/esm/connectkit/siwe/SIWEContext.native.js.map +1 -1
  9. package/dist/esm/connectkit/siwe/SIWEProvider.mjs +19 -16
  10. package/dist/esm/connectkit/siwe/SIWEProvider.mjs.map +1 -1
  11. package/dist/esm/connectkit/siwe/SIWEProvider.native.js +26 -16
  12. package/dist/esm/connectkit/siwe/SIWEProvider.native.js.map +1 -1
  13. package/dist/esm/keycloak/KeycloakWalletLogin.mjs +66 -7
  14. package/dist/esm/keycloak/KeycloakWalletLogin.mjs.map +1 -1
  15. package/dist/esm/keycloak/useWalletLogin.mjs +3 -2
  16. package/dist/esm/keycloak/useWalletLogin.mjs.map +1 -1
  17. package/dist/jsx/connectkit/siwe/SIWEContext.mjs.map +1 -1
  18. package/dist/jsx/connectkit/siwe/SIWEContext.native.js.map +1 -1
  19. package/dist/jsx/connectkit/siwe/SIWEProvider.mjs +19 -16
  20. package/dist/jsx/connectkit/siwe/SIWEProvider.mjs.map +1 -1
  21. package/dist/jsx/connectkit/siwe/SIWEProvider.native.js +26 -16
  22. package/dist/jsx/connectkit/siwe/SIWEProvider.native.js.map +1 -1
  23. package/dist/jsx/keycloak/KeycloakWalletLogin.mjs +66 -7
  24. package/dist/jsx/keycloak/KeycloakWalletLogin.mjs.map +1 -1
  25. package/dist/jsx/keycloak/useWalletLogin.mjs +3 -2
  26. package/dist/jsx/keycloak/useWalletLogin.mjs.map +1 -1
  27. package/dist/keycloak/login.iife.js +362 -362
  28. package/package.json +7 -7
  29. package/src/connectkit/siwe/SIWEContext.tsx +2 -0
  30. package/src/connectkit/siwe/SIWEProvider.tsx +23 -10
  31. package/src/keycloak/KeycloakWalletLogin.spec.tsx +110 -0
  32. package/src/keycloak/KeycloakWalletLogin.stories.tsx +93 -0
  33. package/src/keycloak/KeycloakWalletLogin.theme.spec.tsx +118 -0
  34. package/src/keycloak/KeycloakWalletLogin.tsx +70 -7
  35. package/src/keycloak/useWalletLogin.ts +5 -1
  36. package/types/connectkit/siwe/SIWEContext.d.ts +2 -0
  37. package/types/connectkit/siwe/SIWEContext.d.ts.map +1 -1
  38. package/types/connectkit/siwe/SIWEProvider.d.ts +0 -6
  39. package/types/connectkit/siwe/SIWEProvider.d.ts.map +1 -1
  40. package/types/keycloak/KeycloakWalletLogin.d.ts +6 -1
  41. package/types/keycloak/KeycloakWalletLogin.d.ts.map +1 -1
  42. package/types/keycloak/useWalletLogin.d.ts +2 -0
  43. package/types/keycloak/useWalletLogin.d.ts.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplatform.one/web3",
3
- "version": "8.2.1",
3
+ "version": "8.3.0",
4
4
  "description": "Web3 wallet integration for multiplatform.one ecosystem with ConnectKit and wagmi",
5
5
  "keywords": [
6
6
  "connectkit",
@@ -120,10 +120,10 @@
120
120
  "@aave/account": "^0.2.1",
121
121
  "@coinbase/wallet-sdk": "^4.4.0",
122
122
  "@metamask/connect-evm": "^2.1.1",
123
- "@multiplatform.one/connectkit": "^8.2.1",
124
- "@multiplatform.one/gpg": "^8.2.1",
125
- "@multiplatform.one/platform": "^8.2.1",
126
- "@multiplatform.one/theme": "^8.2.1",
123
+ "@multiplatform.one/connectkit": "^8.3.0",
124
+ "@multiplatform.one/gpg": "^8.3.0",
125
+ "@multiplatform.one/platform": "^8.3.0",
126
+ "@multiplatform.one/theme": "^8.3.0",
127
127
  "@noble/ciphers": "^1.3.0",
128
128
  "@noble/curves": "^1.9.7",
129
129
  "@phosphor-icons/react": "^2.1.10",
@@ -147,8 +147,8 @@
147
147
  "wagmi": "^3.7.1"
148
148
  },
149
149
  "devDependencies": {
150
- "@multiplatform.one/config": "8.2.1",
151
- "@multiplatform.one/test-utils": "8.2.1",
150
+ "@multiplatform.one/config": "8.3.0",
151
+ "@multiplatform.one/test-utils": "8.3.0",
152
152
  "@tamagui/build": "2.7.6",
153
153
  "@tanstack/react-query": "^5.104.3",
154
154
  "@testing-library/jest-dom": "^7.0.1",
@@ -61,6 +61,8 @@ export type SIWEContextValue = Required<Omit<SIWEConfig, "onSign" | "subscribeNo
61
61
  nonce: ReturnType<typeof useQuery<string | null>>;
62
62
  session: ReturnType<typeof useQuery<SIWESession | null>>;
63
63
  status: StatusState;
64
+ /** What ended the last sign-in, while status is ERROR or REJECTED. */
65
+ error: Error | null;
64
66
  signIn: () => Promise<SIWESession | false>;
65
67
  resetStatus: () => void;
66
68
  };
@@ -26,6 +26,20 @@ const b64 = (s: string) => window.btoa(unescape(encodeURIComponent(s)));
26
26
  * connection store holds: `<SignInProvider {...kc.signInConfig(onSign)}>`,
27
27
  * or Web3Provider's `signIn` prop.
28
28
  */
29
+ // A transport wraps the wallet's own refusal (viem's HttpRequestError around a
30
+ // 4001), so the code can sit anywhere down the cause chain.
31
+ function isUserRejection(error: unknown): boolean {
32
+ for (
33
+ let cause = error as { code?: unknown; cause?: unknown } | undefined, depth = 0;
34
+ cause && depth < 8;
35
+ cause = cause.cause as typeof cause, depth++
36
+ ) {
37
+ if (cause.code === -32000 || cause.code === 4001 || cause.code === "ACTION_REJECTED")
38
+ return true;
39
+ }
40
+ return false;
41
+ }
42
+
29
43
  export const SIWEProvider = ({
30
44
  children,
31
45
  enabled = true,
@@ -39,7 +53,11 @@ export const SIWEProvider = ({
39
53
  ...siweConfig
40
54
  }: Props) => {
41
55
  const [status, setStatus] = useState<StatusState>(StatusState.READY);
42
- const resetStatus = () => setStatus(StatusState.READY);
56
+ const [error, setError] = useState<Error | null>(null);
57
+ const resetStatus = () => {
58
+ setStatus(StatusState.READY);
59
+ setError(null);
60
+ };
43
61
 
44
62
  // Only allow for mounting SIWEProvider once, so we avoid weird global state
45
63
  // collisions.
@@ -111,15 +129,8 @@ export const SIWEProvider = ({
111
129
 
112
130
  const onError = (error: any) => {
113
131
  console.error("signIn error", error, error.message);
114
- switch (error.code) {
115
- case -32000: // user rejected, as some wallets report it
116
- case 4001: // MetaMask: user rejected
117
- case "ACTION_REJECTED": // MetaMask: user rejected
118
- setStatus(StatusState.REJECTED);
119
- break;
120
- default:
121
- setStatus(StatusState.ERROR);
122
- }
132
+ setError(error instanceof Error ? error : new Error(String(error)));
133
+ setStatus(isUserRejection(error) ? StatusState.REJECTED : StatusState.ERROR);
123
134
  };
124
135
 
125
136
  const signIn = async () => {
@@ -135,6 +146,7 @@ export const SIWEProvider = ({
135
146
  if (!chainId) throw new Error("No chainId found");
136
147
 
137
148
  setStatus(StatusState.LOADING);
149
+ setError(null);
138
150
 
139
151
  // Fetch this sign-in's own nonce. A server that consumes its nonce on
140
152
  // verify refuses the one cached at mount the second time, so a cached
@@ -249,6 +261,7 @@ export const SIWEProvider = ({
249
261
  signIn,
250
262
  signOut: signOutAndRefetch,
251
263
  status,
264
+ error,
252
265
  resetStatus,
253
266
  }}
254
267
  >
@@ -423,3 +423,113 @@ describe("KeycloakWalletLogin after the page reloads during the wallet trip", ()
423
423
  expect(screen.queryByText("Mock Connector")).toBeNull();
424
424
  });
425
425
  });
426
+
427
+ describe("KeycloakWalletLogin when a sign-in fails (FZ-1825)", () => {
428
+ function failingFetch(answer: { nonce?: () => Response; sign?: Error }) {
429
+ vi.stubGlobal(
430
+ "fetch",
431
+ vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
432
+ if (String(input).endsWith("/nonce"))
433
+ return answer.nonce?.() ?? new Response("nonce123abc");
434
+ const body = String(
435
+ init?.body ?? (input instanceof Request ? await input.clone().text() : ""),
436
+ );
437
+ if (answer.sign && /"method":"(eth_sign|personal_sign)"/.test(body)) throw answer.sign;
438
+ return new Response("not found", { status: 404 });
439
+ }),
440
+ );
441
+ }
442
+
443
+ async function connectedLogin() {
444
+ const rendered = renderLogin({ wagmiConfig: makeConfig(memoryStorage(), true) });
445
+ await waitFor(() => expect(rendered.config.state.status).toBe("connected"));
446
+ return rendered;
447
+ }
448
+
449
+ it("keeps an empty live region under the wallet button before anything fails", async () => {
450
+ await connectedLogin();
451
+ const region = document.getElementById("mpo-wallet-login-error");
452
+ expect(region?.getAttribute("aria-live")).toBe("polite");
453
+ expect(region?.textContent).toBe("");
454
+ });
455
+
456
+ it("a rejected signature says so under the button, which stays usable", async () => {
457
+ failingFetch({
458
+ sign: Object.assign(new Error("User rejected the request."), { code: 4001 }),
459
+ });
460
+ const { walletButton } = await connectedLogin();
461
+ const region = document.getElementById("mpo-wallet-login-error")!;
462
+
463
+ fireEvent.click(walletButton);
464
+ await waitFor(() => expect(region.textContent).toBe("You declined the signature. Try again."));
465
+ expect(document.getElementById("mpo-wallet-login-error")).toBe(region);
466
+ expect(walletButton.disabled).toBe(false);
467
+ expect(walletButton.textContent).toBe("Sign in with Wallet");
468
+
469
+ failingFetch({});
470
+ fireEvent.click(walletButton);
471
+ await waitFor(() => expect(region.textContent).toBe(""));
472
+ });
473
+
474
+ // The sign-in joins the page's mount-time nonce fetch, which react-query
475
+ // retries three times with backoff before it fails (about seven seconds).
476
+ it.each([
477
+ ["a 503 nonce", () => new Response("down", { status: 503 })],
478
+ ["an empty siww nonce", () => new Response("")],
479
+ [
480
+ "an unreachable server",
481
+ () => {
482
+ throw new TypeError("Failed to fetch");
483
+ },
484
+ ],
485
+ ] as const)(
486
+ "%s says the sign-in server couldn't be reached, in plain words",
487
+ { timeout: 20_000 },
488
+ async (_case, nonce) => {
489
+ const logged = vi.spyOn(console, "error").mockImplementation(() => {});
490
+ failingFetch({ nonce });
491
+ const { walletButton } = await connectedLogin();
492
+
493
+ fireEvent.click(walletButton);
494
+ const region = document.getElementById("mpo-wallet-login-error")!;
495
+ await waitFor(
496
+ () => expect(region.textContent).toBe("Couldn't reach the sign-in server. Try again."),
497
+ { timeout: 15_000 },
498
+ );
499
+ expect(region.textContent).not.toMatch(/keycloak|nonce|\d{3}/i);
500
+ expect(walletButton.disabled).toBe(false);
501
+ expect(logged.mock.calls.some(([label]) => label === "signIn error")).toBe(true);
502
+ },
503
+ );
504
+
505
+ it("any other failure says the sign-in failed, without the server's words", async () => {
506
+ failingFetch({
507
+ sign: Object.assign(new Error("Internal JSON-RPC error."), { code: -32603 }),
508
+ });
509
+ const { walletButton } = await connectedLogin();
510
+
511
+ fireEvent.click(walletButton);
512
+ const region = document.getElementById("mpo-wallet-login-error")!;
513
+ await waitFor(() => expect(region.textContent).toBe("Couldn't sign in. Try again."));
514
+ });
515
+
516
+ it("paints the failure line in the field error ink, or the page's own class", async () => {
517
+ failingFetch({
518
+ sign: Object.assign(new Error("User rejected the request."), { code: 4001 }),
519
+ });
520
+ const { walletButton } = await connectedLogin();
521
+ fireEvent.click(walletButton);
522
+ const region = document.getElementById("mpo-wallet-login-error")!;
523
+ await waitFor(() => expect(region.textContent).not.toBe(""));
524
+ // jsdom drops light-dark() colours; the Storybook shots on FZ-1825 measure the ink.
525
+ expect(region.style.colorScheme).toBe("light dark");
526
+ cleanup();
527
+
528
+ renderLogin({ wagmiConfig: makeConfig(memoryStorage(), true), errorClassName: "kc-error" });
529
+ const themed = document.getElementById("mpo-wallet-login-error")!;
530
+ expect(themed.className).toBe("");
531
+ fireEvent.click(document.getElementById("mpo-wallet-login") as HTMLButtonElement);
532
+ await waitFor(() => expect(themed.className).toBe("kc-error"));
533
+ expect(themed.getAttribute("style")).toBeNull();
534
+ });
535
+ });
@@ -0,0 +1,93 @@
1
+ import type { ReactElement } from "react";
2
+ import { userEvent, within } from "storybook/test";
3
+ import { createConfig, createStorage, http } from "wagmi";
4
+ import { mainnet } from "wagmi/chains";
5
+ import { mock } from "wagmi/connectors";
6
+ import { KeycloakWalletLogin } from "./KeycloakWalletLogin";
7
+
8
+ const KEYCLOAK = "https://keycloak-wallet-login.mock.test";
9
+ const ACCOUNT = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
10
+
11
+ function memoryStorage() {
12
+ const map = new Map<string, string>();
13
+ return {
14
+ getItem: (key: string) => map.get(key) ?? null,
15
+ setItem: (key: string, value: string) => void map.set(key, value),
16
+ removeItem: (key: string) => void map.delete(key),
17
+ };
18
+ }
19
+
20
+ function connectedWallet() {
21
+ return createConfig({
22
+ chains: [mainnet],
23
+ connectors: [
24
+ mock({ accounts: [ACCOUNT], features: { reconnect: true, defaultConnected: true } }),
25
+ ],
26
+ transports: { [mainnet.id]: http() },
27
+ storage: createStorage({ storage: memoryStorage() }),
28
+ });
29
+ }
30
+
31
+ function answerKeycloak(nonce: () => Response) {
32
+ return () => {
33
+ const realFetch = window.fetch;
34
+ window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
35
+ const url =
36
+ typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
37
+ if (url.endsWith("/nonce")) return nonce();
38
+ const body = String(init?.body ?? "");
39
+ if (/"method":"(eth_sign|personal_sign)"/.test(body)) {
40
+ throw Object.assign(new Error("User rejected the request."), { code: 4001 });
41
+ }
42
+ if (!url.startsWith(KEYCLOAK)) return realFetch(input, init);
43
+ return new Response("not found", { status: 404 });
44
+ };
45
+ return () => {
46
+ window.fetch = realFetch;
47
+ };
48
+ };
49
+ }
50
+
51
+ async function pressWallet({ canvasElement }: { canvasElement: HTMLElement }) {
52
+ const canvas = within(canvasElement);
53
+ await userEvent.click(await canvas.findByRole("button", { name: /sign in with wallet/i }));
54
+ await canvas.findByText(/try again/i, {}, { timeout: 20_000 });
55
+ }
56
+
57
+ const meta = {
58
+ title: "Web3/KeycloakWalletLogin",
59
+ component: KeycloakWalletLogin,
60
+ tags: ["!test"],
61
+ parameters: { layout: "centered" },
62
+ decorators: [
63
+ (Story: () => ReactElement) => (
64
+ <div style={{ width: 320 }}>
65
+ <Story />
66
+ </div>
67
+ ),
68
+ ],
69
+ };
70
+ export default meta;
71
+
72
+ export const Idle = {
73
+ beforeEach: answerKeycloak(() => new Response("nonce123abc")),
74
+ render: () => (
75
+ <KeycloakWalletLogin baseUrl={KEYCLOAK} realm="main" wagmiConfig={connectedWallet()} />
76
+ ),
77
+ };
78
+
79
+ export const Declined = {
80
+ beforeEach: answerKeycloak(() => new Response("nonce123abc")),
81
+ render: () => (
82
+ <KeycloakWalletLogin baseUrl={KEYCLOAK} realm="main" wagmiConfig={connectedWallet()} />
83
+ ),
84
+ play: pressWallet,
85
+ };
86
+
87
+ export const ServerUnreachable = {
88
+ beforeEach: answerKeycloak(() => new Response("down", { status: 503 })),
89
+ render: () => (
90
+ <KeycloakWalletLogin baseUrl={KEYCLOAK} realm="main" wagmiConfig={connectedWallet()} />
91
+ ),
92
+ play: pressWallet,
93
+ };
@@ -0,0 +1,118 @@
1
+ // @vitest-environment happy-dom
2
+ import { cleanup, render } from "@testing-library/react";
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { createConfig, createStorage, http } from "wagmi";
5
+ import { mainnet } from "wagmi/chains";
6
+ import { mock } from "wagmi/connectors";
7
+ import { KeycloakWalletLogin, type KeycloakWalletLoginProps } from "./KeycloakWalletLogin";
8
+
9
+ const ACCOUNT = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
10
+
11
+ type ColorScheme = "light" | "dark";
12
+
13
+ function memoryStorage() {
14
+ const map = new Map<string, string>();
15
+ return {
16
+ getItem: (key: string) => map.get(key) ?? null,
17
+ setItem: (key: string, value: string) => void map.set(key, value),
18
+ removeItem: (key: string) => void map.delete(key),
19
+ clear: () => map.clear(),
20
+ };
21
+ }
22
+
23
+ function setColorScheme(scheme: ColorScheme) {
24
+ const happy = (window as unknown as { happyDOM: { settings: { device: object } } }).happyDOM;
25
+ Object.assign(happy.settings.device, { prefersColorScheme: scheme });
26
+ }
27
+
28
+ function renderLogin(props: KeycloakWalletLoginProps = {}) {
29
+ const config = createConfig({
30
+ chains: [mainnet],
31
+ connectors: [mock({ accounts: [ACCOUNT] })],
32
+ transports: { [mainnet.id]: http() },
33
+ storage: createStorage({ storage: memoryStorage() }),
34
+ });
35
+ render(
36
+ <KeycloakWalletLogin
37
+ baseUrl="https://kc.example.com"
38
+ realm="main"
39
+ resource="wallet"
40
+ wagmiConfig={config}
41
+ {...props}
42
+ />,
43
+ );
44
+ return {
45
+ wallet: document.getElementById("mpo-wallet-login") as HTMLButtonElement,
46
+ gpg: document.getElementById("mpo-gpg-login") as HTMLButtonElement,
47
+ };
48
+ }
49
+
50
+ /**
51
+ * happy-dom resolves a custom property only on the element that declares it,
52
+ * so a `var()` in the button's style is looked up the ancestor chain here, the
53
+ * way a browser's cascade inherits it.
54
+ */
55
+ function computed(element: HTMLElement, property: string) {
56
+ const declared = element.style.getPropertyValue(property);
57
+ const name = /^var\((--[\w-]+)\)$/.exec(declared)?.[1];
58
+ if (!name) return window.getComputedStyle(element).getPropertyValue(property);
59
+ for (let node: HTMLElement | null = element; node; node = node.parentElement) {
60
+ const value = window.getComputedStyle(node).getPropertyValue(name).trim();
61
+ if (value) return value;
62
+ }
63
+ return "";
64
+ }
65
+
66
+ function painted(button: HTMLElement) {
67
+ return { background: computed(button, "background"), color: computed(button, "color") };
68
+ }
69
+
70
+ /** ConnectKit writes each colour token in display-p3 where the engine supports it. */
71
+ function p3(hex: string) {
72
+ const [r, g, b] = [1, 3, 5].map((at) => Number.parseInt(hex.slice(at, at + 2), 16) / 255);
73
+ return `color(display-p3 ${r} ${g} ${b})`;
74
+ }
75
+
76
+ beforeEach(() => {
77
+ vi.stubGlobal("localStorage", memoryStorage());
78
+ vi.stubGlobal(
79
+ "fetch",
80
+ vi.fn(async () => new Response("not found", { status: 404 })),
81
+ );
82
+ });
83
+
84
+ afterEach(() => {
85
+ cleanup();
86
+ vi.unstubAllGlobals();
87
+ setColorScheme("light");
88
+ });
89
+
90
+ describe("KeycloakWalletLogin buttons follow the ConnectKit theme", () => {
91
+ it("paint the dark primary-button surface and ink in a dark scheme", () => {
92
+ setColorScheme("dark");
93
+ const { wallet, gpg } = renderLogin();
94
+
95
+ for (const button of [wallet, gpg]) {
96
+ expect(painted(button)).toEqual({ background: p3("#383838"), color: p3("#ffffff") });
97
+ }
98
+ });
99
+
100
+ it("paint the light primary-button surface and ink in a light scheme", () => {
101
+ setColorScheme("light");
102
+ const { wallet, gpg } = renderLogin();
103
+
104
+ for (const button of [wallet, gpg]) {
105
+ expect(painted(button)).toEqual({ background: p3("#F6F7F9"), color: p3("#373737") });
106
+ }
107
+ });
108
+
109
+ it("a buttonClassName keeps the host's look and takes no theme paint", () => {
110
+ setColorScheme("dark");
111
+ const { wallet, gpg } = renderLogin({ buttonClassName: "kc-button" });
112
+
113
+ for (const button of [wallet, gpg]) {
114
+ expect(button.getAttribute("style")).toBeNull();
115
+ expect(button.closest("[data-mpo-wallet-theme]")).toBeNull();
116
+ }
117
+ });
118
+ });
@@ -1,6 +1,7 @@
1
+ import { ResetContainer, useContext as useConnectKit } from "@multiplatform.one/connectkit";
1
2
  import { GpgProvider } from "@multiplatform.one/gpg";
2
3
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3
- import { type CSSProperties, useMemo } from "react";
4
+ import { type CSSProperties, type ReactNode, useMemo } from "react";
4
5
  import type { Chain } from "viem";
5
6
  import { type Config, WagmiProvider, createConfig } from "wagmi";
6
7
  import { arbitrum, mainnet, optimism, polygon } from "wagmi/chains";
@@ -13,7 +14,7 @@ import type { MobileConnectOptions } from "../connectkit/openConnector";
13
14
  import { ConnectKitProvider } from "../connectkit/web3Provider";
14
15
  import type { BrowserFlowOptions } from "./browserFlow";
15
16
  import { deriveKeycloakFromLocation, keycloakWallet } from "./keycloakWallet";
16
- import type { KeycloakWalletResource } from "./types";
17
+ import { KeycloakWalletError, type KeycloakWalletResource } from "./types";
17
18
  import { useWalletLogin } from "./useWalletLogin";
18
19
 
19
20
  export interface KeycloakWalletLoginProps {
@@ -43,6 +44,8 @@ export interface KeycloakWalletLoginProps {
43
44
  className?: string;
44
45
  /** Replaces the built-in button look, e.g. the theme's `kcButtonClass`. */
45
46
  buttonClassName?: string;
47
+ /** Replaces the built-in failure line look, e.g. the theme's `kcInputErrorMessageClass`. */
48
+ errorClassName?: string;
46
49
  /** The wagmi config to sign with; built from the app props and `chains` when absent. */
47
50
  wagmiConfig?: Config;
48
51
  }
@@ -87,12 +90,42 @@ const buttonStyle: CSSProperties = {
87
90
  marginTop: 8,
88
91
  fontSize: 14,
89
92
  cursor: "pointer",
90
- border: "1px solid #c4c4c4",
93
+ border: "none",
91
94
  borderRadius: 4,
92
- background: "#fff",
95
+ background: "var(--ck-primary-button-background)",
96
+ color: "var(--ck-primary-button-color)",
93
97
  };
94
98
 
99
+ const themeScopeStyle: CSSProperties = { display: "block" };
100
+
95
101
  const noticeStyle: CSSProperties = { margin: "8px 0 0", fontSize: 13 };
102
+ const hiddenNoticeStyle: CSSProperties = { margin: 0 };
103
+ // MPO's field error ink ($red10 light and dark, form-colors.md) for a page that brings no class of its own.
104
+ const errorStyle: CSSProperties = {
105
+ ...noticeStyle,
106
+ colorScheme: "light dark",
107
+ color: "light-dark(#dc3e42, #ec5d5e)",
108
+ };
109
+
110
+ const declinedMessage = "You declined the signature. Try again.";
111
+ const unreachableMessage = "Couldn't reach the sign-in server. Try again.";
112
+ const failedMessage = "Couldn't sign in. Try again.";
113
+
114
+ /** The words a failed wallet sign-in shows; the raw reason stays on the error and in the console. */
115
+ export function walletLoginFailureMessage(
116
+ status: StatusState,
117
+ error: Error | null,
118
+ ): string | undefined {
119
+ if (status === StatusState.REJECTED) return declinedMessage;
120
+ if (status !== StatusState.ERROR) return undefined;
121
+ if (error instanceof TypeError) return unreachableMessage;
122
+ if (error instanceof KeycloakWalletError && (error.status < 300 || error.status >= 500)) {
123
+ return unreachableMessage;
124
+ }
125
+ return failedMessage;
126
+ }
127
+
128
+ const themedNoticeStyle: CSSProperties = { ...noticeStyle, color: "var(--ck-body-color)" };
96
129
 
97
130
  /**
98
131
  * Wallet and GPG sign-in for a Keycloak login page, on the browser flow: the
@@ -174,12 +207,14 @@ function LoginButtons({
174
207
  gpgLabel = "Sign in with GPG",
175
208
  className,
176
209
  buttonClassName,
210
+ errorClassName,
177
211
  }: KeycloakWalletLoginProps) {
178
- const { login, status, dropped } = useWalletLogin();
212
+ const { login, status, dropped, error } = useWalletLogin();
179
213
  const { openGpg } = useWeb3Modal();
180
214
  const style = buttonClassName ? undefined : buttonStyle;
181
215
  const busy = status === StatusState.LOADING || status === StatusState.SUCCESS;
182
- return (
216
+ const failure = dropped || busy ? undefined : walletLoginFailureMessage(status, error);
217
+ const buttons = (
183
218
  <div className={className ?? "mpo-keycloak-wallet-login"}>
184
219
  <button
185
220
  type="button"
@@ -195,8 +230,20 @@ function LoginButtons({
195
230
  <WalletIcon />
196
231
  <span>{busy ? "Signing…" : walletLabel}</span>
197
232
  </button>
233
+ <p
234
+ id="mpo-wallet-login-error"
235
+ aria-live="polite"
236
+ className={failure ? errorClassName : undefined}
237
+ style={!failure ? hiddenNoticeStyle : errorClassName ? undefined : errorStyle}
238
+ >
239
+ {failure}
240
+ </p>
198
241
  {dropped && !busy && (
199
- <p id="mpo-wallet-login-dropped" role="status" style={noticeStyle}>
242
+ <p
243
+ id="mpo-wallet-login-dropped"
244
+ role="status"
245
+ style={style ? themedNoticeStyle : noticeStyle}
246
+ >
200
247
  The wallet connection was lost. Tap {walletLabel} to connect again.
201
248
  </p>
202
249
  )}
@@ -217,6 +264,22 @@ function LoginButtons({
217
264
  )}
218
265
  </div>
219
266
  );
267
+ return buttonClassName ? buttons : <ThemeScope>{buttons}</ThemeScope>;
268
+ }
269
+
270
+ function ThemeScope({ children }: { children: ReactNode }) {
271
+ const { theme, mode, customTheme } = useConnectKit();
272
+ return (
273
+ <ResetContainer
274
+ data-mpo-wallet-theme=""
275
+ $useTheme={theme}
276
+ $useMode={mode}
277
+ $customTheme={customTheme}
278
+ style={themeScopeStyle}
279
+ >
280
+ {children}
281
+ </ResetContainer>
282
+ );
220
283
  }
221
284
 
222
285
  function WalletIcon() {
@@ -29,6 +29,8 @@ export interface UseWalletLoginResult {
29
29
  * connects afresh and clears it.
30
30
  */
31
31
  dropped: boolean;
32
+ /** What ended the last signature, while status is ERROR or REJECTED. */
33
+ error: Error | null;
32
34
  }
33
35
 
34
36
  function readIntent(key: string, ttl: number): boolean {
@@ -98,7 +100,8 @@ export function useWalletLogin(opts: UseWalletLoginOptions = {}): UseWalletLogin
98
100
  setDropped(false);
99
101
  // A failed signature leaves SIWE on ERROR, which the sign-in step would
100
102
  // show the next attempt as "Unknown Error".
101
- if (!signIn && siwe?.status === StatusState.ERROR) siwe.resetStatus();
103
+ if (!signIn && (siwe?.status === StatusState.ERROR || siwe?.status === StatusState.REJECTED))
104
+ siwe.resetStatus();
102
105
  if (isConnected) {
103
106
  await sign();
104
107
  return;
@@ -129,5 +132,6 @@ export function useWalletLogin(opts: UseWalletLoginOptions = {}): UseWalletLogin
129
132
  isConnected,
130
133
  address,
131
134
  dropped,
135
+ error: signIn ? null : (siwe?.error ?? null),
132
136
  };
133
137
  }
@@ -42,6 +42,8 @@ export type SIWEContextValue = Required<Omit<SIWEConfig, "onSign" | "subscribeNo
42
42
  nonce: ReturnType<typeof useQuery<string | null>>;
43
43
  session: ReturnType<typeof useQuery<SIWESession | null>>;
44
44
  status: StatusState;
45
+ /** What ended the last sign-in, while status is ERROR or REJECTED. */
46
+ error: Error | null;
45
47
  signIn: () => Promise<SIWESession | false>;
46
48
  resetStatus: () => void;
47
49
  };
@@ -1 +1 @@
1
- {"version":3,"file":"SIWEContext.d.ts","sourceRoot":"","sources":["../../../src/connectkit/siwe/SIWEContext.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAE/B,eAAO,MAAM,WAAW;;;;;;CAMd,CAAC;AACX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC;AAEzE,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IAKvB,QAAQ,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAChC,aAAa,EAAE,CAAC,IAAI,EAAE;QACpB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;KACjB,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;IAC/B,aAAa,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAClF,UAAU,EAAE,MAAM,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC9C,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAGhC,OAAO,CAAC,EAAE,OAAO,CAAC;IAIlB,oBAAoB,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACtC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAKjC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;KACjB,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC;IAI/C,mBAAmB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;CAC5D,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,GAAG,qBAAqB,CAAC,CAAC,GACzF,IAAI,CAAC,UAAU,EAAE,QAAQ,GAAG,qBAAqB,CAAC,GAAG;IACnD,KAAK,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;IAClD,OAAO,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,CAAC;IACzD,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,MAAM,OAAO,CAAC,WAAW,GAAG,KAAK,CAAC,CAAC;IAC3C,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB,CAAC;AAEJ,eAAO,MAAM,WAAW,kDAA+C,CAAC"}
1
+ {"version":3,"file":"SIWEContext.d.ts","sourceRoot":"","sources":["../../../src/connectkit/siwe/SIWEContext.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAE/B,eAAO,MAAM,WAAW;;;;;;CAMd,CAAC;AACX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC;AAEzE,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IAKvB,QAAQ,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAChC,aAAa,EAAE,CAAC,IAAI,EAAE;QACpB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;KACjB,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;IAC/B,aAAa,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAClF,UAAU,EAAE,MAAM,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC9C,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAGhC,OAAO,CAAC,EAAE,OAAO,CAAC;IAIlB,oBAAoB,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACtC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAKjC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;KACjB,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC;IAI/C,mBAAmB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;CAC5D,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,GAAG,qBAAqB,CAAC,CAAC,GACzF,IAAI,CAAC,UAAU,EAAE,QAAQ,GAAG,qBAAqB,CAAC,GAAG;IACnD,KAAK,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;IAClD,OAAO,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,CAAC;IACzD,MAAM,EAAE,WAAW,CAAC;IACpB,sEAAsE;IACtE,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,OAAO,CAAC,WAAW,GAAG,KAAK,CAAC,CAAC;IAC3C,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB,CAAC;AAEJ,eAAO,MAAM,WAAW,kDAA+C,CAAC"}
@@ -7,12 +7,6 @@ type Props = SIWEConfig & {
7
7
  };
8
8
  export declare const SIWE_NONCE_QUERY_KEY = "ckSiweNonce";
9
9
  export declare const SIWE_SESSION_QUERY_KEY = "ckSiweSession";
10
- /**
11
- * Sign-in over wagmi's account, mounted outside ConnectKitProvider.
12
- * @deprecated Goes in a later major. `SignInProvider` signs with whichever family the
13
- * connection store holds: `<SignInProvider {...kc.signInConfig(onSign)}>`,
14
- * or Web3Provider's `signIn` prop.
15
- */
16
10
  export declare const SIWEProvider: ({ children, enabled, nonceRefetchInterval, sessionRefetchInterval, signOutOnDisconnect, signOutOnAccountChange, signOutOnNetworkChange, onSignIn, onSignOut, ...siweConfig }: Props) => import("react/jsx-runtime").JSX.Element;
17
11
  export {};
18
12
  //# sourceMappingURL=SIWEProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SIWEProvider.d.ts","sourceRoot":"","sources":["../../../src/connectkit/siwe/SIWEProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAmC,MAAM,OAAO,CAAC;AAMnE,OAAO,EAAe,UAAU,EAAe,WAAW,EAAE,MAAM,eAAe,CAAC;AAIlF,KAAK,KAAK,GAAG,UAAU,GAAG;IACxB,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,WAAW,KAAK,IAAI,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB,CAAC;AAEF,eAAO,MAAM,oBAAoB,gBAAgB,CAAC;AAClD,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;AAKtD;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAI,8KAW1B,KAAK,4CA0NP,CAAC"}
1
+ {"version":3,"file":"SIWEProvider.d.ts","sourceRoot":"","sources":["../../../src/connectkit/siwe/SIWEProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAmC,MAAM,OAAO,CAAC;AAMnE,OAAO,EAAe,UAAU,EAAe,WAAW,EAAE,MAAM,eAAe,CAAC;AAIlF,KAAK,KAAK,GAAG,UAAU,GAAG;IACxB,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,WAAW,KAAK,IAAI,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB,CAAC;AAEF,eAAO,MAAM,oBAAoB,gBAAgB,CAAC;AAClD,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;AAyBtD,eAAO,MAAM,YAAY,GAAI,8KAW1B,KAAK,4CAyNP,CAAC"}
@@ -1,8 +1,9 @@
1
1
  import type { Chain } from "viem";
2
2
  import { type Config } from "wagmi";
3
+ import { StatusState } from "../connectkit/siwe/SIWEContext";
3
4
  import type { MobileConnectOptions } from "../connectkit/openConnector";
4
5
  import type { BrowserFlowOptions } from "./browserFlow";
5
- import type { KeycloakWalletResource } from "./types";
6
+ import { type KeycloakWalletResource } from "./types";
6
7
  export interface KeycloakWalletLoginProps {
7
8
  /** Defaults to the Keycloak base of the page it runs on. */
8
9
  baseUrl?: string;
@@ -30,11 +31,15 @@ export interface KeycloakWalletLoginProps {
30
31
  className?: string;
31
32
  /** Replaces the built-in button look, e.g. the theme's `kcButtonClass`. */
32
33
  buttonClassName?: string;
34
+ /** Replaces the built-in failure line look, e.g. the theme's `kcInputErrorMessageClass`. */
35
+ errorClassName?: string;
33
36
  /** The wagmi config to sign with; built from the app props and `chains` when absent. */
34
37
  wagmiConfig?: Config;
35
38
  }
36
39
  /** The wagmi config `<KeycloakWalletLogin>` builds when it is given none. */
37
40
  export declare function createKeycloakWalletConfig({ appName, appDescription, appUrl, appIcon, chains, }?: Pick<KeycloakWalletLoginProps, "appName" | "appDescription" | "appUrl" | "appIcon" | "chains">): Config;
41
+ /** The words a failed wallet sign-in shows; the raw reason stays on the error and in the console. */
42
+ export declare function walletLoginFailureMessage(status: StatusState, error: Error | null): string | undefined;
38
43
  /**
39
44
  * Wallet and GPG sign-in for a Keycloak login page, on the browser flow: the
40
45
  * signed credential posts to the page's authenticator. Carries its own wagmi,