feat: redesign wallet login and favorites, fix desktop/mobile bugs

- Remove forced BNB chain switch on injected login (signature is chain-agnostic)
- Refine isMobileDevice so touch Macs stay on desktop flow
- Wire RainbowKit/WalletConnect as a real MetaMask/imToken QR fallback,
  gated on a valid VITE_WALLETCONNECT_PROJECT_ID
- Rebuild login modal: single desktop primary action, collapsible other
  methods, mobile open-app fallback feedback, brand icons
- Add My Favorites entry points (header, mobile menu, wallet dropdown)
- Favorites page: error retry, mobile filter drawer
- Auto sign-out and re-login prompt on favorites 401
- Full native translations for all wallet strings across 7 locales

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
TerryM
2026-06-02 03:43:13 +08:00
parent f935f122f9
commit 7abe4a868c
17 changed files with 715 additions and 155 deletions

View File

@@ -11,7 +11,12 @@ import {
import { useToast } from "../components/Toast";
import { useI18n } from "../i18n";
import { useWallet } from "../wallet/WalletProvider";
import { addFavorite, getFavoriteIds, removeFavorite } from "./api";
import {
addFavorite,
getFavoriteIds,
isFavoritesAuthError,
removeFavorite,
} from "./api";
type FavoriteStatus = "unknown" | "favorited" | "notFavorited";
@@ -30,7 +35,13 @@ export function FavoritesProvider({ children }: { children: ReactNode }) {
const { t } = useI18n();
const { showToast } = useToast();
const wallet = useWallet();
const { address, openLoginModal, status, token } = wallet;
const { address, logout, openLoginModal, status, token } = wallet;
const handleAuthError = useCallback(() => {
logout();
openLoginModal();
showToast(t("favoriteSessionExpired"));
}, [logout, openLoginModal, showToast, t]);
const [favoriteIds, setFavoriteIds] = useState<Set<string>>(() => new Set());
const [knownIds, setKnownIds] = useState<Set<string>>(() => new Set());
const [pendingIds, setPendingIds] = useState<Set<string>>(() => new Set());
@@ -113,6 +124,8 @@ export function FavoritesProvider({ children }: { children: ReactNode }) {
ids.forEach((id) => next.add(id));
return next;
});
} catch (error) {
if (isFavoritesAuthError(error)) handleAuthError();
} finally {
requestIds.forEach((id) => inFlightIdsRef.current.delete(id));
if (queuedIdsRef.current.size > 0 && batchTimerRef.current === null) {
@@ -122,7 +135,7 @@ export function FavoritesProvider({ children }: { children: ReactNode }) {
}, 0);
}
}
}, []);
}, [handleAuthError]);
const ensureFavoriteIds = useCallback(
async (resourceIds: string[]) => {
@@ -158,7 +171,8 @@ export function FavoritesProvider({ children }: { children: ReactNode }) {
);
} catch (error) {
markFavorite(resourceId, currentlyFavorite);
showToast(t("favoriteFailed"), "error");
if (isFavoritesAuthError(error)) handleAuthError();
else showToast(t("favoriteFailed"), "error");
throw error;
} finally {
setPendingIds((prev) => {
@@ -168,7 +182,7 @@ export function FavoritesProvider({ children }: { children: ReactNode }) {
});
}
},
[favoriteIds, markFavorite, showToast, t, token],
[favoriteIds, handleAuthError, markFavorite, showToast, t, token],
);
const toggleFavorite = useCallback(