terry-wallet-login #15
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
title: "Desktop Favorites Header Blank Page — Quick Fix"
|
||||||
|
type: quick-fix
|
||||||
|
date: 2026-06-04
|
||||||
|
---
|
||||||
|
|
||||||
|
# Desktop Favorites Header Blank Page — Quick Fix
|
||||||
|
|
||||||
|
## Bug
|
||||||
|
|
||||||
|
Clicking the desktop header “我的收藏 / My Favorites” button could leave the page visually blank in the local browser. The provided screenshot showed DevTools Elements with an empty `<body>` and no React `#root` node after navigating to `/cn/favorites`.
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
|
||||||
|
This was not a z-index overlay issue. The screenshot showed that React had not mounted at all because the current document had no `#root` element. In local Vite/HMR/browser state, client-side React Router navigation could land in a stale or broken document state. The favorites route itself was valid and returned the correct Vite HTML when requested directly.
|
||||||
|
|
||||||
|
There was also a possible same-page navigation edge case: clicking “我的收藏” while already on the favorites route would not necessarily trigger route scroll reset.
|
||||||
|
|
||||||
|
## Fix
|
||||||
|
|
||||||
|
The desktop header favorites button now uses React Router's `reloadDocument` so clicking it performs a full document navigation. This forces the browser/Vite dev server to return a fresh `index.html` with `<div id="root"></div>` instead of relying on a potentially stale client-side navigation state.
|
||||||
|
|
||||||
|
The route scroll reset was also made more robust by disabling browser scroll restoration and running the route scroll reset in `useLayoutEffect`, so a restored scroll position cannot leave the favorites page sitting in blank lower space before paint.
|
||||||
|
|
||||||
|
### Files Modified
|
||||||
|
|
||||||
|
- `src/layouts/PublicLayout.tsx` — added the desktop header favorites button and made it use `reloadDocument` plus top scroll reset.
|
||||||
|
- `src/components/ScrollToTop.tsx` — switched route scroll reset to `useLayoutEffect` and set `history.scrollRestoration = "manual"` while the app is mounted.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Ran `npx tsc --noEmit`.
|
||||||
|
- Ran `npm run format:check`.
|
||||||
|
- Used browser native to open `http://192.168.1.187:5173/cn/browse`, confirm the header “我的收藏” button is present, navigate to favorites, and inspect the resulting page.
|
||||||
|
- Verified with browser native eval that `http://192.168.1.187:5173/favorites` has `document.getElementById("root") === true`, title `My Favorites | ARK Library`, and `scrollY === 0`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
No deploy was performed.
|
||||||
@@ -61,9 +61,7 @@ Create a local `.env` only when needed. Do not commit secrets. See `.env.example
|
|||||||
|
|
||||||
## Wallet login notes
|
## Wallet login notes
|
||||||
|
|
||||||
Wallet login is used to verify address ownership for user favorites. The primary stable paths are injected wallets (`window.ethereum`) and TokenPocket QR callback login. MetaMask/imToken QR login is a RainbowKit/Reown fallback and may be unstable on some China networks.
|
Wallet login is used to attach a wallet address to user favorites. The frontend connects an injected wallet (`window.ethereum`), sends the selected address to `POST /api/auth/wallet/login`, and stores the returned wallet JWT in `localStorage` as a simple MVP session mechanism. This keeps the implementation small, but any future XSS vulnerability could expose a wallet session. A more secure future iteration should move wallet sessions to backend-set `httpOnly` cookies or shorten the token lifetime with refresh-token support.
|
||||||
|
|
||||||
The current frontend stores the wallet JWT in `localStorage` as a simple MVP session mechanism. This keeps the implementation small, but any future XSS vulnerability could expose a 30-day wallet session. A more secure future iteration should move wallet sessions to backend-set `httpOnly` cookies or shorten the token lifetime with refresh-token support.
|
|
||||||
|
|
||||||
## Project layout
|
## Project layout
|
||||||
|
|
||||||
|
|||||||
313
docs/backend-wallet-favorites-production-fixes.md
Normal file
313
docs/backend-wallet-favorites-production-fixes.md
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
# Backend fixes required for Wallet Login + Favorites production readiness
|
||||||
|
|
||||||
|
Date: 2026-06-04
|
||||||
|
Environment tested: `https://arkie-library-stag.com/apnew/api`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Frontend has been updated to the new backend contract:
|
||||||
|
|
||||||
|
- Wallet login: `POST /api/auth/wallet/login` with `{ address }`
|
||||||
|
- Wallet session check: `GET /api/auth/wallet/me` with `Authorization: Bearer <token>`
|
||||||
|
- Favorites list/status: `GET /api/favorites` and `GET /api/favorites?ids=...`
|
||||||
|
- Favorite mutation: `POST /api/posts/{id}/favorite` with `{ add: true|false }`
|
||||||
|
|
||||||
|
Staging confirms the new login endpoint works, but favorite mutation currently accepts an invalid Bearer token. This must be fixed before production trust.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority 0 — Fix favorite mutation authentication
|
||||||
|
|
||||||
|
### Current staging behavior
|
||||||
|
|
||||||
|
The following request currently returns `200 OK` even with an invalid token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i -X POST \
|
||||||
|
"https://arkie-library-stag.com/apnew/api/posts/8f4a571c-3477-4b05-91be-d85907048de5/favorite" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer invalid-token" \
|
||||||
|
--data '{"add":true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Observed response:
|
||||||
|
|
||||||
|
```http
|
||||||
|
HTTP 200
|
||||||
|
{"ok":true}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required behavior
|
||||||
|
|
||||||
|
`POST /api/posts/{id}/favorite` must require a valid wallet JWT.
|
||||||
|
|
||||||
|
Invalid, missing, expired, malformed, or unverifiable tokens must return:
|
||||||
|
|
||||||
|
```http
|
||||||
|
HTTP 401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
Recommended response body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "unauthorized"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Acceptance tests
|
||||||
|
|
||||||
|
#### Missing token
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i -X POST \
|
||||||
|
"https://arkie-library-stag.com/apnew/api/posts/{postId}/favorite" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
--data '{"add":true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
```http
|
||||||
|
HTTP 401
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Invalid token
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i -X POST \
|
||||||
|
"https://arkie-library-stag.com/apnew/api/posts/{postId}/favorite" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer invalid-token" \
|
||||||
|
--data '{"add":true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
```http
|
||||||
|
HTTP 401
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Valid token
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOKEN=$(curl -s -X POST \
|
||||||
|
"https://arkie-library-stag.com/apnew/api/auth/wallet/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
--data '{"address":"0x0000000000000000000000000000000000000001"}' \
|
||||||
|
| jq -r .token)
|
||||||
|
|
||||||
|
curl -i -X POST \
|
||||||
|
"https://arkie-library-stag.com/apnew/api/posts/{postId}/favorite" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
--data '{"add":true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
```http
|
||||||
|
HTTP 200
|
||||||
|
```
|
||||||
|
|
||||||
|
Response should include at least:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"favorited": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then cancel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i -X POST \
|
||||||
|
"https://arkie-library-stag.com/apnew/api/posts/{postId}/favorite" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
--data '{"add":false}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"favorited": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority 1 — Confirm wallet login security model
|
||||||
|
|
||||||
|
### Current contract
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/auth/wallet/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"address": "0x..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "<jwt>",
|
||||||
|
"wallet": "0x..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is what the frontend now uses.
|
||||||
|
|
||||||
|
### Production risk
|
||||||
|
|
||||||
|
This flow does not prove wallet ownership. Any client can submit any wallet address and receive a token for that address.
|
||||||
|
|
||||||
|
If wallet identity is only used for low-risk favorites, this may be acceptable as an MVP. If wallet identity will be used for user identity, permissions, membership, rewards, asset ownership, admin behavior, or anything security-sensitive, backend should require signature verification.
|
||||||
|
|
||||||
|
### Recommended secure production flow
|
||||||
|
|
||||||
|
If stronger security is required, backend should use nonce + signature:
|
||||||
|
|
||||||
|
1. `POST /api/auth/wallet/nonce` with `{ address }`
|
||||||
|
2. Backend returns a one-time message / nonce.
|
||||||
|
3. Frontend asks wallet to sign the message.
|
||||||
|
4. `POST /api/auth/wallet/verify` with `{ address, message, signature }`
|
||||||
|
5. Backend verifies recovered address equals requested address.
|
||||||
|
6. Backend issues JWT.
|
||||||
|
|
||||||
|
If backend decides to keep the simplified `{ address }` login, please explicitly confirm that this is an accepted production risk.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority 2 — Normalize favorites response contract
|
||||||
|
|
||||||
|
Frontend currently supports the staging response shape, but the response should be made explicit.
|
||||||
|
|
||||||
|
### Favorites list
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/favorites?lang=&limit=&page=&sort=&category=&q=
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
Current staging response observed:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "...",
|
||||||
|
"postType": "image",
|
||||||
|
"categoryId": 11,
|
||||||
|
"categorySlug": "official-assets",
|
||||||
|
"language": "zh",
|
||||||
|
"title": "..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Recommended production response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [],
|
||||||
|
"page": 1,
|
||||||
|
"limit": 24,
|
||||||
|
"total": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`page`, `limit`, and `total` are needed for correct pagination.
|
||||||
|
|
||||||
|
### Favorite status by ids
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/favorites?ids=id1,id2,id3
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
Current staging response observed:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This works, but for frontend performance and clarity, recommended response is:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ids": ["id1", "id3"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Meaning: only IDs that are already favorited by the current wallet user.
|
||||||
|
|
||||||
|
Frontend currently accepts both:
|
||||||
|
|
||||||
|
- `{ ids: string[] }`
|
||||||
|
- `{ items: Resource[] }`
|
||||||
|
|
||||||
|
But backend should document and standardize one shape.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority 3 — Required status codes
|
||||||
|
|
||||||
|
Please standardize these responses:
|
||||||
|
|
||||||
|
| Case | Expected status |
|
||||||
|
| --- | --- |
|
||||||
|
| Missing Bearer token on protected endpoint | `401` |
|
||||||
|
| Invalid/expired Bearer token | `401` |
|
||||||
|
| Valid token but post ID does not exist | `404` |
|
||||||
|
| Invalid JSON body | `400` |
|
||||||
|
| Invalid `add` value | `400` |
|
||||||
|
| Successful favorite add/remove | `200` |
|
||||||
|
|
||||||
|
Protected endpoints:
|
||||||
|
|
||||||
|
- `GET /api/auth/wallet/me`
|
||||||
|
- `GET /api/favorites`
|
||||||
|
- `GET /api/favorites?ids=...`
|
||||||
|
- `POST /api/posts/{id}/favorite`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend compatibility notes
|
||||||
|
|
||||||
|
The frontend currently calls these staging paths through the same-origin prefix:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
/apnew/api/auth/wallet/login
|
||||||
|
/apnew/api/auth/wallet/me
|
||||||
|
/apnew/api/favorites
|
||||||
|
/apnew/api/favorites?ids=...
|
||||||
|
/apnew/api/posts/{id}/favorite
|
||||||
|
```
|
||||||
|
|
||||||
|
In frontend source this is written as `/api/...`; staging build uses `VITE_API_PREFIX=/apnew`.
|
||||||
|
|
||||||
|
Please keep backend routes under `/api/...` behind the proxy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final production checklist
|
||||||
|
|
||||||
|
Backend should confirm all of the following before production release:
|
||||||
|
|
||||||
|
- [ ] `POST /api/posts/{id}/favorite` rejects missing token with `401`.
|
||||||
|
- [ ] `POST /api/posts/{id}/favorite` rejects invalid token with `401`.
|
||||||
|
- [ ] `POST /api/posts/{id}/favorite` only changes favorites for the wallet from the validated JWT.
|
||||||
|
- [ ] `GET /api/favorites` requires a valid Bearer token.
|
||||||
|
- [ ] `GET /api/favorites?ids=...` requires a valid Bearer token, unless explicitly declared public/legacy.
|
||||||
|
- [ ] `GET /api/auth/wallet/me` validates token and returns the wallet address from the token.
|
||||||
|
- [ ] Backend explicitly confirms whether simplified `{ address }` login is acceptable for production, or switches to nonce/signature verification.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useLayoutEffect, useRef } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,6 +23,15 @@ export function ScrollToTop() {
|
|||||||
const prevPathname = useRef(pathname);
|
const prevPathname = useRef(pathname);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!("scrollRestoration" in window.history)) return;
|
||||||
|
const previous = window.history.scrollRestoration;
|
||||||
|
window.history.scrollRestoration = "manual";
|
||||||
|
return () => {
|
||||||
|
window.history.scrollRestoration = previous;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
const pathnameChanged = prevPathname.current !== pathname;
|
const pathnameChanged = prevPathname.current !== pathname;
|
||||||
prevPathname.current = pathname;
|
prevPathname.current = pathname;
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import { apiBase, type Resource } from "../api";
|
import { apiBase, itemsOrEmpty, type Resource } from "../api";
|
||||||
|
|
||||||
export type FavoriteSort = "favorited_at" | "published_at" | "hot";
|
export type FavoriteSort = "favorited_at" | "published_at" | "hot";
|
||||||
|
|
||||||
export type FavoriteItem = {
|
|
||||||
favoritedAt: string;
|
|
||||||
resource: Resource;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FavoriteListResponse = {
|
export type FavoriteListResponse = {
|
||||||
items: FavoriteItem[];
|
items: Resource[];
|
||||||
page: number;
|
page?: number;
|
||||||
limit: number;
|
limit?: number;
|
||||||
total: number;
|
total?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FavoriteIdsResponse = {
|
export type FavoriteIdsResponse = {
|
||||||
@@ -20,16 +15,24 @@ export type FavoriteIdsResponse = {
|
|||||||
|
|
||||||
export type FavoriteMutationResponse = {
|
export type FavoriteMutationResponse = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
resourceId: string;
|
changed?: boolean;
|
||||||
favorited: boolean;
|
resourceId?: string;
|
||||||
|
favorited?: boolean;
|
||||||
favoritedAt?: string;
|
favoritedAt?: string;
|
||||||
favoriteCount: number;
|
favoriteCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function authHeaders(token: string): HeadersInit {
|
function authHeaders(token: string): HeadersInit {
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function authJSONHeaders(token: string): HeadersInit {
|
||||||
|
return {
|
||||||
|
...authHeaders(token),
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** HTTP error that preserves the status code so callers can react to 401s. */
|
/** HTTP error that preserves the status code so callers can react to 401s. */
|
||||||
export class FavoriteHttpError extends Error {
|
export class FavoriteHttpError extends Error {
|
||||||
readonly status: number;
|
readonly status: number;
|
||||||
@@ -68,7 +71,7 @@ export async function listFavorites(
|
|||||||
sp.set(key, String(value));
|
sp.set(key, String(value));
|
||||||
});
|
});
|
||||||
const suffix = sp.toString() ? `?${sp}` : "";
|
const suffix = sp.toString() ? `?${sp}` : "";
|
||||||
const res = await fetch(`${apiBase}/api/me/favorites${suffix}`, {
|
const res = await fetch(`${apiBase}/api/favorites${suffix}`, {
|
||||||
headers: authHeaders(token),
|
headers: authHeaders(token),
|
||||||
});
|
});
|
||||||
return parseJSON<FavoriteListResponse>(res);
|
return parseJSON<FavoriteListResponse>(res);
|
||||||
@@ -81,22 +84,23 @@ export async function getFavoriteIds(
|
|||||||
if (resourceIds.length === 0) return [];
|
if (resourceIds.length === 0) return [];
|
||||||
const uniqueIds = [...new Set(resourceIds)].slice(0, 100);
|
const uniqueIds = [...new Set(resourceIds)].slice(0, 100);
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${apiBase}/api/me/favorites/ids?resourceIds=${encodeURIComponent(
|
`${apiBase}/api/favorites?ids=${encodeURIComponent(uniqueIds.join(","))}`,
|
||||||
uniqueIds.join(","),
|
|
||||||
)}`,
|
|
||||||
{ headers: authHeaders(token) },
|
{ headers: authHeaders(token) },
|
||||||
);
|
);
|
||||||
const data = await parseJSON<FavoriteIdsResponse>(res);
|
const data = await parseJSON<FavoriteIdsResponse | FavoriteListResponse>(res);
|
||||||
return data.ids;
|
if ("ids" in data && Array.isArray(data.ids)) return data.ids;
|
||||||
|
if ("items" in data) return itemsOrEmpty(data.items).map((item) => item.id);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addFavorite(
|
export async function addFavorite(
|
||||||
token: string,
|
token: string,
|
||||||
resourceId: string,
|
resourceId: string,
|
||||||
): Promise<FavoriteMutationResponse> {
|
): Promise<FavoriteMutationResponse> {
|
||||||
const res = await fetch(`${apiBase}/api/me/favorites/${resourceId}`, {
|
const res = await fetch(`${apiBase}/api/posts/${resourceId}/favorite`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: authHeaders(token),
|
headers: authJSONHeaders(token),
|
||||||
|
body: JSON.stringify({ add: true }),
|
||||||
});
|
});
|
||||||
return parseJSON<FavoriteMutationResponse>(res);
|
return parseJSON<FavoriteMutationResponse>(res);
|
||||||
}
|
}
|
||||||
@@ -105,9 +109,10 @@ export async function removeFavorite(
|
|||||||
token: string,
|
token: string,
|
||||||
resourceId: string,
|
resourceId: string,
|
||||||
): Promise<FavoriteMutationResponse> {
|
): Promise<FavoriteMutationResponse> {
|
||||||
const res = await fetch(`${apiBase}/api/me/favorites/${resourceId}`, {
|
const res = await fetch(`${apiBase}/api/posts/${resourceId}/favorite`, {
|
||||||
method: "DELETE",
|
method: "POST",
|
||||||
headers: authHeaders(token),
|
headers: authJSONHeaders(token),
|
||||||
|
body: JSON.stringify({ add: false }),
|
||||||
});
|
});
|
||||||
return parseJSON<FavoriteMutationResponse>(res);
|
return parseJSON<FavoriteMutationResponse>(res);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,20 +50,12 @@ header button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Desktop header nav: thin scrollbar when links overflow (still 單列) */
|
|
||||||
.header-nav-scroll {
|
.header-nav-scroll {
|
||||||
scrollbar-width: thin;
|
-ms-overflow-style: none;
|
||||||
scrollbar-color: rgba(238, 183, 38, 0.45) transparent;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
.header-nav-scroll::-webkit-scrollbar {
|
.header-nav-scroll::-webkit-scrollbar {
|
||||||
height: 4px;
|
display: none;
|
||||||
}
|
|
||||||
.header-nav-scroll::-webkit-scrollbar-thumb {
|
|
||||||
background-color: rgba(238, 183, 38, 0.45);
|
|
||||||
border-radius: 9999px;
|
|
||||||
}
|
|
||||||
.header-nav-scroll::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gold-underline {
|
.gold-underline {
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { ChevronDown, Menu, Search as SearchIcon, X } from "lucide-react";
|
import {
|
||||||
|
ChevronDown,
|
||||||
|
Heart,
|
||||||
|
Menu,
|
||||||
|
Search as SearchIcon,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
import { AnimatePresence, m } from "framer-motion";
|
import { AnimatePresence, m } from "framer-motion";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Link, useLocation, useNavigate, useOutlet } from "react-router-dom";
|
import { Link, useLocation, useNavigate, useOutlet } from "react-router-dom";
|
||||||
@@ -690,6 +696,20 @@ export function PublicLayout() {
|
|||||||
ariaLabel={t("langLabel")}
|
ariaLabel={t("langLabel")}
|
||||||
className="hidden h-10 w-36 md:block lg:w-40"
|
className="hidden h-10 w-36 md:block lg:w-40"
|
||||||
/>
|
/>
|
||||||
|
<Link
|
||||||
|
to={lp("/favorites")}
|
||||||
|
reloadDocument
|
||||||
|
onClick={() => window.scrollTo({ top: 0, left: 0 })}
|
||||||
|
className={`hidden h-10 shrink-0 items-center justify-center gap-2 rounded-full border px-3 text-sm font-bold outline-none transition focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg md:inline-flex ${
|
||||||
|
na("favorites")
|
||||||
|
? "border-ark-gold bg-ark-gold text-black"
|
||||||
|
: "border-ark-line bg-[#1a1b20] text-neutral-200 hover:border-ark-gold/50 hover:text-ark-gold"
|
||||||
|
}`}
|
||||||
|
aria-current={na("favorites") ? "page" : undefined}
|
||||||
|
>
|
||||||
|
<Heart className="h-[18px] w-[18px]" strokeWidth={2.2} />
|
||||||
|
<span className="hidden xl:inline">{t("favorites")}</span>
|
||||||
|
</Link>
|
||||||
<div className="hidden md:block">
|
<div className="hidden md:block">
|
||||||
<WalletButton />
|
<WalletButton />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -163,13 +163,12 @@ export default function Favorites() {
|
|||||||
page,
|
page,
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
includeUnavailable: true,
|
includeUnavailable: true,
|
||||||
lang: langQuery(lang),
|
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
const resources = itemsOrEmpty(data.items).map((item) => item.resource);
|
const resources = itemsOrEmpty(data.items);
|
||||||
setItems(resources);
|
setItems(resources);
|
||||||
setTotal(data.total);
|
setTotal(data.total ?? resources.length);
|
||||||
resources.forEach((resource) => markFavorite(resource.id, true));
|
resources.forEach((resource) => markFavorite(resource.id, true));
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -187,7 +186,7 @@ export default function Favorites() {
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [category, lang, markFavorite, page, query, reloadKey, sort, t, wallet]);
|
}, [category, markFavorite, page, query, reloadKey, sort, t, wallet]);
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||||
const hasFilters = Boolean(category || query || sort !== "favorited_at");
|
const hasFilters = Boolean(category || query || sort !== "favorited_at");
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
getInjectedWallet,
|
getInjectedWallet,
|
||||||
type WalletKind,
|
type WalletKind,
|
||||||
} from "./injected";
|
} from "./injected";
|
||||||
import { localWalletToken, useWallet } from "./WalletProvider";
|
import { useWallet } from "./WalletProvider";
|
||||||
|
|
||||||
const AUTO_LOGIN_PARAMS = ["autoLogin", "autologin"];
|
const AUTO_LOGIN_PARAMS = ["autoLogin", "autologin"];
|
||||||
const ETHEREUM_WAIT_MS = 8000;
|
const ETHEREUM_WAIT_MS = 8000;
|
||||||
@@ -52,7 +52,7 @@ function waitForInjected(kind: WalletKind): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AutoInjectedLogin() {
|
export function AutoInjectedLogin() {
|
||||||
const { completeLogin, status } = useWallet();
|
const { loginAddress, status } = useWallet();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
@@ -69,7 +69,7 @@ export function AutoInjectedLogin() {
|
|||||||
try {
|
try {
|
||||||
const address = await connectInjectedWallet(kind);
|
const address = await connectInjectedWallet(kind);
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
completeLogin(localWalletToken(address), address);
|
await loginAddress(address);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[wallet-autologin] failed", err);
|
console.warn("[wallet-autologin] failed", err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
getInjectedWallet,
|
getInjectedWallet,
|
||||||
type WalletKind,
|
type WalletKind,
|
||||||
} from "./injected";
|
} from "./injected";
|
||||||
import { localWalletToken, useWallet } from "./WalletProvider";
|
import { useWallet } from "./WalletProvider";
|
||||||
import { WalletBrandIcon } from "./WalletBrandIcon";
|
import { WalletBrandIcon } from "./WalletBrandIcon";
|
||||||
|
|
||||||
const AUTO_LOGIN_PARAM = "autologin";
|
const AUTO_LOGIN_PARAM = "autologin";
|
||||||
@@ -51,7 +51,7 @@ function isMobileDevice(): boolean {
|
|||||||
|
|
||||||
export function WalletLoginModal() {
|
export function WalletLoginModal() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { closeLoginModal, completeLogin, loginModalOpen } = useWallet();
|
const { closeLoginModal, loginAddress, loginModalOpen } = useWallet();
|
||||||
const [selected, setSelected] = useState<WalletKind | null>(null);
|
const [selected, setSelected] = useState<WalletKind | null>(null);
|
||||||
const [mobileDevice, setMobileDevice] = useState(() => isMobileDevice());
|
const [mobileDevice, setMobileDevice] = useState(() => isMobileDevice());
|
||||||
const [state, setState] = useState<LoginState>("idle");
|
const [state, setState] = useState<LoginState>("idle");
|
||||||
@@ -101,7 +101,7 @@ export function WalletLoginModal() {
|
|||||||
try {
|
try {
|
||||||
const address = await connectInjectedWallet(kind);
|
const address = await connectInjectedWallet(kind);
|
||||||
if (mobileDevice) {
|
if (mobileDevice) {
|
||||||
completeLogin(localWalletToken(address), address);
|
await loginAddress(address);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setPendingLogin({ kind, address });
|
setPendingLogin({ kind, address });
|
||||||
@@ -112,9 +112,16 @@ export function WalletLoginModal() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmPendingLogin = () => {
|
const confirmPendingLogin = async () => {
|
||||||
if (!pendingLogin) return;
|
if (!pendingLogin) return;
|
||||||
completeLogin(localWalletToken(pendingLogin.address), pendingLogin.address);
|
setState("connecting");
|
||||||
|
setError("");
|
||||||
|
try {
|
||||||
|
await loginAddress(pendingLogin.address);
|
||||||
|
} catch (err) {
|
||||||
|
setState("idle");
|
||||||
|
setError(walletErrorMessage(err, t));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelPendingLogin = () => {
|
const cancelPendingLogin = () => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { useToast } from "../components/Toast";
|
import { useToast } from "../components/Toast";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../i18n";
|
||||||
import { fetchWalletMe } from "./api";
|
import { fetchWalletMe, loginWithWallet } from "./api";
|
||||||
import { signInWithInjectedWallet, type WalletKind } from "./injected";
|
import { signInWithInjectedWallet, type WalletKind } from "./injected";
|
||||||
import { clearWalletToken, readWalletToken, writeWalletToken } from "./token";
|
import { clearWalletToken, readWalletToken, writeWalletToken } from "./token";
|
||||||
|
|
||||||
@@ -22,18 +22,6 @@ function walletErrorMessage(error: unknown, t: Translate): string {
|
|||||||
return t(error.message) || t("walletLoginFailed");
|
return t(error.message) || t("walletLoginFailed");
|
||||||
}
|
}
|
||||||
|
|
||||||
const localWalletTokenPrefix = "local-wallet:";
|
|
||||||
|
|
||||||
export function localWalletToken(wallet: string): string {
|
|
||||||
return `${localWalletTokenPrefix}${wallet}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function walletFromLocalToken(token: string): string | null {
|
|
||||||
return token.startsWith(localWalletTokenPrefix)
|
|
||||||
? token.slice(localWalletTokenPrefix.length)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
type WalletContextValue = {
|
type WalletContextValue = {
|
||||||
address: string | null;
|
address: string | null;
|
||||||
token: string | null;
|
token: string | null;
|
||||||
@@ -43,6 +31,7 @@ type WalletContextValue = {
|
|||||||
closeLoginModal: () => void;
|
closeLoginModal: () => void;
|
||||||
signInInjected: (kind?: WalletKind) => Promise<void>;
|
signInInjected: (kind?: WalletKind) => Promise<void>;
|
||||||
completeLogin: (token: string, wallet: string) => void;
|
completeLogin: (token: string, wallet: string) => void;
|
||||||
|
loginAddress: (address: string) => Promise<void>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,13 +60,6 @@ export function WalletProvider({ children }: { children: ReactNode }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const localWallet = walletFromLocalToken(token);
|
|
||||||
if (localWallet) {
|
|
||||||
setAddress(localWallet);
|
|
||||||
setStatus("loggedIn");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatus("loading");
|
setStatus("loading");
|
||||||
fetchWalletMe(token)
|
fetchWalletMe(token)
|
||||||
.then((me) => {
|
.then((me) => {
|
||||||
@@ -110,6 +92,14 @@ export function WalletProvider({ children }: { children: ReactNode }) {
|
|||||||
[showToast, t],
|
[showToast, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const loginAddress = useCallback(
|
||||||
|
async (walletAddress: string) => {
|
||||||
|
const res = await loginWithWallet(walletAddress);
|
||||||
|
completeLogin(res.token, res.wallet);
|
||||||
|
},
|
||||||
|
[completeLogin],
|
||||||
|
);
|
||||||
|
|
||||||
const signInInjected = useCallback(
|
const signInInjected = useCallback(
|
||||||
async (kind?: WalletKind) => {
|
async (kind?: WalletKind) => {
|
||||||
try {
|
try {
|
||||||
@@ -141,12 +131,14 @@ export function WalletProvider({ children }: { children: ReactNode }) {
|
|||||||
closeLoginModal: () => setLoginModalOpen(false),
|
closeLoginModal: () => setLoginModalOpen(false),
|
||||||
signInInjected,
|
signInInjected,
|
||||||
completeLogin,
|
completeLogin,
|
||||||
|
loginAddress,
|
||||||
logout,
|
logout,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
address,
|
address,
|
||||||
completeLogin,
|
completeLogin,
|
||||||
loginModalOpen,
|
loginModalOpen,
|
||||||
|
loginAddress,
|
||||||
logout,
|
logout,
|
||||||
signInInjected,
|
signInInjected,
|
||||||
status,
|
status,
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { apiBase, getJSONAuth, postJSON } from "../api";
|
import { apiBase, getJSONAuth, postJSON } from "../api";
|
||||||
|
|
||||||
export type WalletNonceResponse = {
|
export type WalletLoginResponse = {
|
||||||
nonce: string;
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WalletVerifyResponse = {
|
|
||||||
token: string;
|
token: string;
|
||||||
wallet: string;
|
wallet: string;
|
||||||
};
|
};
|
||||||
@@ -36,18 +31,8 @@ export type TokenPocketLoginResult =
|
|||||||
signature: string;
|
signature: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function requestWalletNonce(
|
export function loginWithWallet(address: string): Promise<WalletLoginResponse> {
|
||||||
address: string,
|
return postJSON<WalletLoginResponse>("/api/auth/wallet/login", { address });
|
||||||
): Promise<WalletNonceResponse> {
|
|
||||||
return postJSON<WalletNonceResponse>("/api/auth/wallet/nonce", { address });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function verifyWalletSignature(params: {
|
|
||||||
address: string;
|
|
||||||
message: string;
|
|
||||||
signature: string;
|
|
||||||
}): Promise<WalletVerifyResponse> {
|
|
||||||
return postJSON<WalletVerifyResponse>("/api/auth/wallet/verify", params);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchWalletMe(token: string): Promise<WalletMeResponse> {
|
export function fetchWalletMe(token: string): Promise<WalletMeResponse> {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { requestWalletNonce, verifyWalletSignature } from "./api";
|
import { loginWithWallet } from "./api";
|
||||||
|
|
||||||
export type WalletKind = "tokenPocket" | "metaMask" | "imToken";
|
export type WalletKind = "tokenPocket" | "metaMask" | "imToken";
|
||||||
|
|
||||||
@@ -26,12 +26,6 @@ function isAddress(value: unknown): value is string {
|
|||||||
return typeof value === "string" && /^0x[a-fA-F0-9]{40}$/.test(value);
|
return typeof value === "string" && /^0x[a-fA-F0-9]{40}$/.test(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function utf8ToHex(value: string): string {
|
|
||||||
return `0x${Array.from(new TextEncoder().encode(value), (byte) =>
|
|
||||||
byte.toString(16).padStart(2, "0"),
|
|
||||||
).join("")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function errorText(error: unknown): string {
|
function errorText(error: unknown): string {
|
||||||
if (!error || typeof error !== "object") return String(error ?? "");
|
if (!error || typeof error !== "object") return String(error ?? "");
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
@@ -57,13 +51,6 @@ function normalizeWalletError(error: unknown): Error {
|
|||||||
return new Error(message || "Wallet login failed");
|
return new Error(message || "Wallet login failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldRetryPersonalSign(error: unknown): boolean {
|
|
||||||
const text = errorText(error);
|
|
||||||
return /wallet must has at least one account|wallet must has one account|must have at least one account|invalid params|invalid account|account not found/i.test(
|
|
||||||
text,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureBnbChain(ethereum: EthereumProvider): Promise<void> {
|
async function ensureBnbChain(ethereum: EthereumProvider): Promise<void> {
|
||||||
const chainId = await ethereum
|
const chainId = await ethereum
|
||||||
.request<string>({ method: "eth_chainId" })
|
.request<string>({ method: "eth_chainId" })
|
||||||
@@ -106,31 +93,6 @@ async function requestInjectedAddress(
|
|||||||
return requestedAddress;
|
return requestedAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function personalSign(params: {
|
|
||||||
ethereum: EthereumProvider;
|
|
||||||
message: string;
|
|
||||||
address: string;
|
|
||||||
}): Promise<string> {
|
|
||||||
const { ethereum, message, address } = params;
|
|
||||||
const hexMessage = utf8ToHex(message);
|
|
||||||
try {
|
|
||||||
return await ethereum.request<string>({
|
|
||||||
method: "personal_sign",
|
|
||||||
params: [hexMessage, address],
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (!shouldRetryPersonalSign(error)) throw error;
|
|
||||||
return ethereum
|
|
||||||
.request<string>({
|
|
||||||
method: "personal_sign",
|
|
||||||
params: [address, hexMessage],
|
|
||||||
})
|
|
||||||
.catch((retryError: unknown): never => {
|
|
||||||
throw normalizeWalletError(retryError);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getInjectedEthereum(): EthereumProvider | null {
|
export function getInjectedEthereum(): EthereumProvider | null {
|
||||||
if (typeof window === "undefined") return null;
|
if (typeof window === "undefined") return null;
|
||||||
const maybeWindow = window as typeof window & { ethereum?: EthereumProvider };
|
const maybeWindow = window as typeof window & { ethereum?: EthereumProvider };
|
||||||
@@ -214,35 +176,9 @@ export async function signInWithInjectedWallet(kind?: WalletKind): Promise<{
|
|||||||
token: string;
|
token: string;
|
||||||
wallet: string;
|
wallet: string;
|
||||||
}> {
|
}> {
|
||||||
console.info("[wallet-login] start injected", { kind });
|
const address = await connectInjectedWallet(kind);
|
||||||
logWalletProviders();
|
console.info("[wallet-login] requesting backend login for", address);
|
||||||
const ethereum = getInjectedWallet(kind);
|
const result = await loginWithWallet(address);
|
||||||
if (!ethereum) {
|
console.info("[wallet-login] logged in, wallet =", result.wallet);
|
||||||
console.warn("[wallet-login] no injected provider found");
|
|
||||||
throw new Error("No injected wallet found");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.info("[wallet-login] requesting BNB wallet account…");
|
|
||||||
const address = await requestInjectedAddress(ethereum);
|
|
||||||
console.info("[wallet-login] account", address);
|
|
||||||
|
|
||||||
console.info("[wallet-login] ensuring BNB Chain (0x38)…");
|
|
||||||
await ensureBnbChain(ethereum);
|
|
||||||
|
|
||||||
console.info("[wallet-login] requesting nonce for", address);
|
|
||||||
const nonce = await requestWalletNonce(address);
|
|
||||||
console.info("[wallet-login] got nonce, requesting personal_sign…");
|
|
||||||
const signature = await personalSign({
|
|
||||||
ethereum,
|
|
||||||
message: nonce.message,
|
|
||||||
address,
|
|
||||||
});
|
|
||||||
console.info("[wallet-login] signed, verifying with backend…");
|
|
||||||
const result = await verifyWalletSignature({
|
|
||||||
address,
|
|
||||||
message: nonce.message,
|
|
||||||
signature,
|
|
||||||
});
|
|
||||||
console.info("[wallet-login] verified, wallet =", result.wallet);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
getInjectedWallet,
|
getInjectedWallet,
|
||||||
type WalletKind,
|
type WalletKind,
|
||||||
} from "./injected";
|
} from "./injected";
|
||||||
import { localWalletToken, useWallet } from "./WalletProvider";
|
import { useWallet } from "./WalletProvider";
|
||||||
|
|
||||||
export type WalletConnectLoginState = "idle" | "connecting" | "signing";
|
export type WalletConnectLoginState = "idle" | "connecting" | "signing";
|
||||||
export type WalletConnectLoginMode = "deeplink" | "qr";
|
export type WalletConnectLoginMode = "deeplink" | "qr";
|
||||||
@@ -96,7 +96,7 @@ function connectorMatchesWallet(
|
|||||||
|
|
||||||
export function useWalletConnectLogin() {
|
export function useWalletConnectLogin() {
|
||||||
const available = hasWalletConnectProjectId();
|
const available = hasWalletConnectProjectId();
|
||||||
const { address: localAddress, completeLogin } = useWallet();
|
const { address: localAddress, loginAddress } = useWallet();
|
||||||
const { address: wagmiAddress, isConnected: wagmiConnected } = useAccount();
|
const { address: wagmiAddress, isConnected: wagmiConnected } = useAccount();
|
||||||
const { connectAsync, connectors } = useConnect();
|
const { connectAsync, connectors } = useConnect();
|
||||||
const { disconnectAsync } = useDisconnect();
|
const { disconnectAsync } = useDisconnect();
|
||||||
@@ -140,12 +140,17 @@ export function useWalletConnectLogin() {
|
|||||||
chain: "BNB Chain",
|
chain: "BNB Chain",
|
||||||
chainId: bsc.id,
|
chainId: bsc.id,
|
||||||
});
|
});
|
||||||
completeLogin(localWalletToken(wagmiAddress), wagmiAddress);
|
void loginAddress(wagmiAddress)
|
||||||
console.info("[wallet-login] local wallet session completed", {
|
.then(() => {
|
||||||
address: wagmiAddress,
|
console.info("[wallet-login] wallet session completed", {
|
||||||
});
|
address: wagmiAddress,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
setError(err instanceof Error ? err.message : "Wallet login failed");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [completeLogin, localAddress, wagmiAddress, wagmiConnected]);
|
}, [localAddress, loginAddress, wagmiAddress, wagmiConnected]);
|
||||||
|
|
||||||
const start = useCallback(
|
const start = useCallback(
|
||||||
async (
|
async (
|
||||||
@@ -173,7 +178,7 @@ export function useWalletConnectLogin() {
|
|||||||
chain: "BNB Chain",
|
chain: "BNB Chain",
|
||||||
chainId: bsc.id,
|
chainId: bsc.id,
|
||||||
});
|
});
|
||||||
completeLogin(localWalletToken(injectedAddress), injectedAddress);
|
await loginAddress(injectedAddress);
|
||||||
setState("idle");
|
setState("idle");
|
||||||
return;
|
return;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -256,10 +261,17 @@ export function useWalletConnectLogin() {
|
|||||||
chain: "BNB Chain",
|
chain: "BNB Chain",
|
||||||
chainId: bsc.id,
|
chainId: bsc.id,
|
||||||
});
|
});
|
||||||
completeLogin(localWalletToken(address), address);
|
void loginAddress(address)
|
||||||
console.info("[wallet-login] local wallet session completed", {
|
.then(() => {
|
||||||
address,
|
console.info("[wallet-login] wallet session completed", {
|
||||||
});
|
address,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
setError(
|
||||||
|
err instanceof Error ? err.message : "Wallet login failed",
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const pollId = window.setInterval(() => {
|
const pollId = window.setInterval(() => {
|
||||||
void connector
|
void connector
|
||||||
@@ -293,7 +305,7 @@ export function useWalletConnectLogin() {
|
|||||||
cleanupPollingRef.current = null;
|
cleanupPollingRef.current = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[available, completeLogin, connectAsync, connectors, disconnectAsync],
|
[available, connectAsync, connectors, disconnectAsync, loginAddress],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user