Skip to content

Commit cf21e6a

Browse files
committed
feat(sdk): bubble up sign in error message
test: Add comprehensive test suite for SignatureScreen component test: Mock auth functionality in SignatureScreen tests feat: Add mocks for wallet hooks in SignatureScreen test
1 parent 33c23e7 commit cf21e6a

File tree

4 files changed

+148
-5
lines changed

4 files changed

+148
-5
lines changed

.changeset/moody-turtles-count.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Feature: Propagate failed sign in error message to the UI
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { userEvent } from "@testing-library/user-event";
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
4+
import { render, waitFor } from "../../../../../../test/src/react-render.js";
5+
import { TEST_CLIENT } from "../../../../../../test/src/test-clients.js";
6+
import { createWallet } from "../../../../../wallets/create-wallet.js";
7+
import type { ConnectLocale } from "../locale/types.js";
8+
import { SignatureScreen } from "./SignatureScreen.js";
9+
10+
const mockAuth = vi.hoisted(() => ({
11+
doLogin: vi.fn().mockResolvedValue(undefined),
12+
doLogout: vi.fn().mockResolvedValue(undefined),
13+
getLoginPayload: vi.fn().mockResolvedValue(undefined),
14+
isLoggedIn: vi.fn().mockResolvedValue(true),
15+
}));
16+
17+
const wallet = createWallet("io.metamask");
18+
19+
vi.mock("../../../../core/hooks/auth/useSiweAuth", () => ({
20+
useSiweAuth: () => mockAuth,
21+
}));
22+
23+
vi.mock("../../../../core/hooks/wallets/useActiveWallet", () => ({
24+
useActiveWallet: () => wallet,
25+
}));
26+
27+
vi.mock("../../../../core/hooks/wallets/useActiveAccount", () => ({
28+
useActiveAccount: () => vi.fn().mockReturnValue(TEST_ACCOUNT_A),
29+
}));
30+
31+
vi.mock("../../../../core/hooks/wallets/useAdminWallet", () => ({
32+
useAdminWallet: () => vi.fn().mockReturnValue(null),
33+
}));
34+
35+
const mockConnectLocale = {
36+
signatureScreen: {
37+
title: "Sign In",
38+
instructionScreen: {
39+
title: "Sign Message",
40+
instruction: "Please sign the message",
41+
signInButton: "Sign In",
42+
disconnectWallet: "Disconnect",
43+
},
44+
signingScreen: {
45+
title: "Signing",
46+
inProgress: "Signing in progress...",
47+
failedToSignIn: "Failed to sign in",
48+
prompt: "Please check your wallet",
49+
tryAgain: "Try Again",
50+
},
51+
},
52+
agreement: {
53+
prefix: "By connecting, you agree to our",
54+
termsOfService: "Terms of Service",
55+
and: "and",
56+
privacyPolicy: "Privacy Policy",
57+
},
58+
} as unknown as ConnectLocale;
59+
60+
describe("SignatureScreen", () => {
61+
beforeEach(() => {
62+
vi.clearAllMocks();
63+
mockAuth.doLogin.mockResolvedValue(undefined);
64+
});
65+
66+
it("renders initial state correctly", () => {
67+
const { getByTestId } = render(
68+
<SignatureScreen
69+
onDone={() => {}}
70+
modalSize="wide"
71+
connectLocale={mockConnectLocale}
72+
client={TEST_CLIENT}
73+
auth={mockAuth}
74+
/>,
75+
{ setConnectedWallet: true },
76+
);
77+
78+
expect(getByTestId("sign-in-button")).toBeInTheDocument();
79+
expect(getByTestId("disconnect-button")).toBeInTheDocument();
80+
});
81+
82+
it("handles signing flow", async () => {
83+
const onDoneMock = vi.fn();
84+
const { getByRole, getByText } = render(
85+
<SignatureScreen
86+
onDone={onDoneMock}
87+
modalSize="wide"
88+
connectLocale={mockConnectLocale}
89+
client={TEST_CLIENT}
90+
auth={mockAuth}
91+
/>,
92+
{ setConnectedWallet: true },
93+
);
94+
95+
const signInButton = getByRole("button", { name: "Sign In" });
96+
await userEvent.click(signInButton);
97+
98+
// Should show signing in progress
99+
await waitFor(() => {
100+
expect(getByText("Signing in progress...")).toBeInTheDocument();
101+
});
102+
});
103+
104+
it("handles error state", async () => {
105+
mockAuth.doLogin.mockRejectedValueOnce(new Error("Signing failed"));
106+
const { getByRole, getByText } = render(
107+
<SignatureScreen
108+
onDone={() => {}}
109+
modalSize="wide"
110+
connectLocale={mockConnectLocale}
111+
client={TEST_CLIENT}
112+
auth={mockAuth}
113+
/>,
114+
{ setConnectedWallet: true },
115+
);
116+
117+
const signInButton = getByRole("button", { name: "Sign In" });
118+
await userEvent.click(signInButton);
119+
120+
// Should show error state
121+
await waitFor(
122+
() => {
123+
expect(getByText("Signing failed")).toBeInTheDocument();
124+
expect(getByRole("button", { name: "Try Again" })).toBeInTheDocument();
125+
},
126+
{
127+
timeout: 2000,
128+
},
129+
);
130+
});
131+
});

packages/thirdweb/src/react/web/ui/ConnectWallet/screens/SignatureScreen.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,21 @@ export const SignatureScreen: React.FC<{
5151
const adminWallet = useAdminWallet();
5252
const activeAccount = useActiveAccount();
5353
const siweAuth = useSiweAuth(wallet, activeAccount, props.auth);
54-
const [status, setStatus] = useState<Status>("idle");
54+
const [error, setError] = useState<string | undefined>(undefined);
55+
const [status, setStatus] = useState<Status>(error ? "failed" : "idle");
5556
const { disconnect } = useDisconnect();
5657
const locale = connectLocale.signatureScreen;
5758

5859
const signIn = useCallback(async () => {
5960
try {
61+
setError(undefined);
6062
setStatus("signing");
6163
await siweAuth.doLogin();
6264
onDone?.();
6365
} catch (err) {
6466
await wait(1000);
67+
setError((err as Error).message);
6568
setStatus("failed");
66-
console.error("failed to log in", err);
6769
}
6870
}, [onDone, siweAuth]);
6971

@@ -78,6 +80,7 @@ export const SignatureScreen: React.FC<{
7880
) {
7981
return (
8082
<HeadlessSignIn
83+
error={error}
8184
signIn={signIn}
8285
status={status}
8386
connectLocale={connectLocale}
@@ -126,6 +129,7 @@ export const SignatureScreen: React.FC<{
126129
<Button
127130
fullWidth
128131
variant="accent"
132+
data-testid="sign-in-button"
129133
onClick={signIn}
130134
style={{
131135
alignItems: "center",
@@ -138,6 +142,7 @@ export const SignatureScreen: React.FC<{
138142
<Button
139143
fullWidth
140144
variant="secondary"
145+
data-testid="disconnect-button"
141146
onClick={() => {
142147
disconnect(wallet);
143148
}}
@@ -162,7 +167,7 @@ export const SignatureScreen: React.FC<{
162167
<Container flex="column" gap="md" animate="fadein" key={status}>
163168
<Text size="lg" center color="primaryText">
164169
{status === "failed"
165-
? locale.signingScreen.failedToSignIn
170+
? error || locale.signingScreen.failedToSignIn
166171
: locale.signingScreen.inProgress}
167172
</Text>
168173

@@ -224,12 +229,14 @@ export const SignatureScreen: React.FC<{
224229

225230
function HeadlessSignIn({
226231
signIn,
232+
error,
227233
status,
228234
connectLocale,
229235
wallet,
230236
}: {
231237
signIn: () => void;
232238
status: Status;
239+
error: string | undefined;
233240
connectLocale: ConnectLocale;
234241
wallet: Wallet;
235242
}) {
@@ -262,7 +269,7 @@ function HeadlessSignIn({
262269
<Container>
263270
<Spacer y="lg" />
264271
<Text size="lg" center color="danger">
265-
{locale.signingScreen.failedToSignIn}
272+
{error || locale.signingScreen.failedToSignIn}
266273
</Text>
267274

268275
<Spacer y="lg" />

packages/thirdweb/src/wallets/ecosystem/is-ecosystem-wallet.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function isEcosystemWallet(wallet: string): wallet is EcosystemWalletId;
1010
/**
1111
* Checks if the given wallet is an ecosystem wallet.
1212
*
13-
* @param {string} walletId - The wallet ID to check.
13+
* @param {Wallet | string} wallet - The wallet or wallet ID to check.
1414
* @returns {boolean} True if the wallet is an ecosystem wallet, false otherwise.
1515
* @internal
1616
*/

0 commit comments

Comments
 (0)