Skip to content

Commit a0bd994

Browse files
Merge pull request #962 from nasalehj/fix/issue-815-use-auth-wallet-state
fix(auth): derive client state from wallet connection
2 parents f19230c + 1bcf6b5 commit a0bd994

3 files changed

Lines changed: 98 additions & 99 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { renderHook, waitFor } from "@testing-library/react";
2+
import { useWalletStore } from "@/store/walletStore";
3+
import { useAuth } from "../useAuth";
4+
5+
jest.mock("@/store/walletStore", () => ({
6+
useWalletStore: jest.fn(),
7+
}));
8+
9+
const mockUseWalletStore = useWalletStore as jest.MockedFunction<
10+
typeof useWalletStore
11+
>;
12+
13+
const connectedAddress = "0x1234567890123456789012345678901234567890";
14+
15+
const setWalletState = (
16+
overrides: Partial<ReturnType<typeof useWalletStore>> = {},
17+
) => {
18+
mockUseWalletStore.mockReturnValue({
19+
address: null,
20+
isConnected: false,
21+
...overrides,
22+
} as ReturnType<typeof useWalletStore>);
23+
};
24+
25+
describe("useAuth", () => {
26+
beforeEach(() => {
27+
jest.clearAllMocks();
28+
document.cookie = "";
29+
setWalletState();
30+
});
31+
32+
it("reports an unauthenticated state with no connected wallet and no cookie", async () => {
33+
const { result } = renderHook(() => useAuth());
34+
35+
await waitFor(() => expect(result.current.isLoading).toBe(false));
36+
37+
expect(result.current.isAuthenticated).toBe(false);
38+
expect(result.current.userAddress).toBeNull();
39+
});
40+
41+
it("does not authenticate from cookie presence alone", async () => {
42+
document.cookie = "auth-token=garbage-token";
43+
const { result } = renderHook(() => useAuth());
44+
45+
await waitFor(() => expect(result.current.isLoading).toBe(false));
46+
47+
expect(result.current.isAuthenticated).toBe(false);
48+
expect(result.current.userAddress).toBeNull();
49+
});
50+
51+
it("does not authenticate from an expired or invalid cookie payload", async () => {
52+
document.cookie = "auth-token=eyJhbGciOiJIUzI1NiJ9.invalid-payload";
53+
const { result } = renderHook(() => useAuth());
54+
55+
await waitFor(() => expect(result.current.isLoading).toBe(false));
56+
57+
expect(result.current.isAuthenticated).toBe(false);
58+
expect(result.current.userAddress).toBeNull();
59+
});
60+
61+
it("authenticates with the connected wallet address", async () => {
62+
setWalletState({ address: connectedAddress, isConnected: true });
63+
const { result } = renderHook(() => useAuth());
64+
65+
await waitFor(() => expect(result.current.isLoading).toBe(false));
66+
67+
expect(result.current.isAuthenticated).toBe(true);
68+
expect(result.current.userAddress).toBe(connectedAddress);
69+
expect(result.current.userAddress).not.toBe("0x...");
70+
});
71+
72+
it("does not authenticate when the wallet has an address but is disconnected", async () => {
73+
setWalletState({ address: connectedAddress, isConnected: false });
74+
const { result } = renderHook(() => useAuth());
75+
76+
await waitFor(() => expect(result.current.isLoading).toBe(false));
77+
78+
expect(result.current.isAuthenticated).toBe(false);
79+
expect(result.current.userAddress).toBeNull();
80+
});
81+
});

src/hooks/useAuth.ts

Lines changed: 16 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
'use client';
1+
"use client";
22

3-
import { useState, useEffect } from 'react';
3+
import { useEffect, useState } from "react";
4+
import { useWalletStore } from "@/store/walletStore";
45

5-
const SESSION_DURATION_MS = 30 * 60 * 1000; // 30 minutes
6-
const WARN_BEFORE_MS = 5 * 60 * 1000; // warn 5 minutes before expiry
6+
const WARN_BEFORE_MS = 5 * 60 * 1000;
77

88
interface AuthState {
99
isAuthenticated: boolean;
@@ -13,56 +13,20 @@ interface AuthState {
1313
}
1414

1515
export function useAuth() {
16-
const [authState, setAuthState] = useState<AuthState>({
17-
isAuthenticated: false,
18-
isLoading: true,
19-
userAddress: null,
20-
sessionExpiresAt: null,
21-
});
16+
const { address, isConnected } = useWalletStore();
17+
const [isHydrated, setIsHydrated] = useState(false);
2218

2319
useEffect(() => {
24-
const checkAuth = async () => {
25-
try {
26-
const tokenMatch = document.cookie.match(/auth-token=([^;]+)/);
27-
const token = tokenMatch ? tokenMatch[1] : null;
28-
let isValid = false;
29-
let expiresAt = null;
30-
let address = null;
31-
32-
if (token) {
33-
try {
34-
const payloadBase64 = token.split('.')[1];
35-
if (payloadBase64) {
36-
const payload = JSON.parse(atob(payloadBase64));
37-
if (payload.exp && payload.exp * 1000 > Date.now()) {
38-
isValid = true;
39-
expiresAt = payload.exp * 1000;
40-
address = payload.address || '0x...';
41-
}
42-
}
43-
} catch (e) {
44-
// Invalid token format
45-
}
46-
}
47-
48-
setAuthState({
49-
isAuthenticated: isValid,
50-
isLoading: false,
51-
userAddress: address,
52-
sessionExpiresAt: expiresAt,
53-
});
54-
} catch {
55-
setAuthState({
56-
isAuthenticated: false,
57-
isLoading: false,
58-
userAddress: null,
59-
sessionExpiresAt: null,
60-
});
61-
}
62-
};
63-
64-
checkAuth();
20+
setIsHydrated(true);
6521
}, []);
6622

67-
return { ...authState, WARN_BEFORE_MS };
23+
const hasVerifiedWallet = isHydrated && isConnected && Boolean(address);
24+
25+
return {
26+
isAuthenticated: hasVerifiedWallet,
27+
isLoading: !isHydrated,
28+
userAddress: hasVerifiedWallet ? address : null,
29+
sessionExpiresAt: null,
30+
WARN_BEFORE_MS,
31+
} satisfies AuthState & { WARN_BEFORE_MS: number };
6832
}

useAuth.ts

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1 @@
1-
'use client';
2-
3-
import { useState, useEffect } from 'react';
4-
import { useWalletConnector } from './useWalletConnector';
5-
6-
interface AuthState {
7-
isAuthenticated: boolean;
8-
isLoading: boolean;
9-
userAddress: string | null;
10-
}
11-
12-
export function useAuth() {
13-
const { connectWallet } = useWalletConnector();
14-
const [authState, setAuthState] = useState<AuthState>({
15-
isAuthenticated: false,
16-
isLoading: true,
17-
userAddress: null,
18-
});
19-
20-
useEffect(() => {
21-
// Check for existing session/token on mount
22-
const checkAuth = async () => {
23-
try {
24-
// In a real Web3 app, we'd check if the wallet is still connected
25-
// and if a valid session token exists in cookies
26-
const hasToken = document.cookie.includes('auth-token=');
27-
28-
// Mocking check - in production, validate JWT or wallet state here
29-
setAuthState({
30-
isAuthenticated: hasToken,
31-
isLoading: false,
32-
userAddress: hasToken ? '0x...' : null, // Get from wallet provider
33-
});
34-
} catch (error) {
35-
setAuthState({
36-
isAuthenticated: false,
37-
isLoading: false,
38-
userAddress: null,
39-
});
40-
}
41-
};
42-
43-
checkAuth();
44-
}, []);
45-
46-
return authState;
47-
}
1+
export { useAuth } from "./src/hooks/useAuth";

0 commit comments

Comments
 (0)