From 15dba690d3c7cd50adf33fadcd9e665665255592 Mon Sep 17 00:00:00 2001 From: eric Date: Fri, 13 Mar 2026 05:51:29 -0500 Subject: [PATCH] 's' --- app/[locale]/join/page.tsx | 7 +++--- app/[locale]/join/paid/page.tsx | 18 ++++++++++++-- app/components/HeroSection.tsx | 18 +++++++++++++- app/components/JoinModal.tsx | 3 +++ app/components/Navbar.tsx | 37 ++++++++++++++++++++++++----- app/lib/payment/usePayStatusPoll.ts | 17 +++++++++---- app/lib/pocketbase/auth.ts | 18 +++++++++++--- app/lib/pocketbase/index.ts | 1 + config/services.config.ts | 6 ++--- 9 files changed, 103 insertions(+), 22 deletions(-) diff --git a/app/[locale]/join/page.tsx b/app/[locale]/join/page.tsx index 5373586..7c4534d 100644 --- a/app/[locale]/join/page.tsx +++ b/app/[locale]/join/page.tsx @@ -84,12 +84,13 @@ export default function JoinPage() { checkExisting(); }, []); - // 轮询到支付成功时跳转到 /join/paid,避免在 /join 重复显示成功页 - usePayStatusPoll(() => { + // 轮询到支付成功时跳转到 /join/paid,携带 order_id 供 complete-order 使用 + usePayStatusPoll((orderId) => { if (typeof window === "undefined") return; const path = window.location.pathname; const paidPath = path.replace(/\/join\/?$/, "") + "/join/paid"; - window.location.replace(paidPath); + const url = orderId ? `${paidPath}?order_id=${encodeURIComponent(orderId)}` : paidPath; + window.location.replace(url); }); useEffect(() => { diff --git a/app/[locale]/join/paid/page.tsx b/app/[locale]/join/paid/page.tsx index adefe37..acca7ad 100644 --- a/app/[locale]/join/paid/page.tsx +++ b/app/[locale]/join/paid/page.tsx @@ -53,18 +53,32 @@ export default function JoinPaidPage() { pbSaveAuth(d.token, d.record); } window.dispatchEvent(new CustomEvent("auth:updated")); - setTimeout(() => window.dispatchEvent(new CustomEvent("auth:updated")), 500); + window.dispatchEvent(new CustomEvent("vip:updated")); + setTimeout(() => { + window.dispatchEvent(new CustomEvent("auth:updated")); + window.dispatchEvent(new CustomEvent("vip:updated")); + }, 400); if (d.is_new) { await fetch("/api/meetup/set-needs-password-change", { method: "POST", credentials: "include" }); if (!cancelled) { setNeedPasswordChange(true); setShowPasswordModal(true); } + } else { + const needRes = await fetch("/api/meetup/need-password-change", { credentials: "include" }); + const needData = await needRes.json().catch(() => ({})); + if (!cancelled && needData?.need === true) { + setNeedPasswordChange(true); + setShowPasswordModal(true); + } } return { ok: true }; }; const run = async () => { + const urlOrderId = typeof window !== "undefined" + ? new URLSearchParams(window.location.search).get("order_id") || "" + : ""; const cookie = document.cookie.split(";").find((c) => c.trim().startsWith("join_pay_order=")); const rawValue = cookie?.split("=")[1]?.trim(); const storageValue = (() => { @@ -75,7 +89,7 @@ export default function JoinPaidPage() { } })(); const decoded = rawValue ? decodeURIComponent(rawValue) : storageValue; - const orderId = decoded.split("|")[0]?.trim() || ""; + const orderId = urlOrderId || decoded.split("|")[0]?.trim() || ""; if (!orderId) { clearPendingOrder(); window.location.replace(`/${window.location.pathname.split("/")[1] || "zh"}/join`); diff --git a/app/components/HeroSection.tsx b/app/components/HeroSection.tsx index edb1675..7f7cca4 100644 --- a/app/components/HeroSection.tsx +++ b/app/components/HeroSection.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback, useEffect } from "react"; +import { useState, useCallback, useEffect, useRef } from "react"; import { Link, useLocale } from "@/i18n/navigation"; import { useTranslation } from "@/i18n/navigation"; import JoinModal from "./JoinModal"; @@ -85,6 +85,9 @@ export default function HeroSection() { return () => window.removeEventListener("auth:updated", onAuth); }, []); + const checkUserCacheRef = useRef<{ email: string; data: { exists: boolean; vip: boolean }; ts: number } | null>(null); + const CACHE_TTL_MS = 60_000; + const handleCheckAndProceed = useCallback( async (em: string) => { if (checking) return; @@ -96,6 +99,15 @@ export default function HeroSection() { setError(null); setChecking(true); try { + const cached = checkUserCacheRef.current; + if (cached && cached.email === trimmed && Date.now() - cached.ts < CACHE_TTL_MS) { + const { exists, vip } = cached.data; + if (exists) { + setJoinModalVipOnly(!!vip); + setJoinModalOpen(true); + return; + } + } const checkRes = await fetch("/api/meetup/check-user", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -107,6 +119,7 @@ export default function HeroSection() { throw new Error(checkData?.error || checkData?.detail || (locale === "zh" ? "操作失败" : "Operation failed")); } if (checkData.exists) { + checkUserCacheRef.current = { email: trimmed, data: { exists: true, vip: !!checkData.vip }, ts: Date.now() }; if (checkData.vip) { setJoinModalOpen(true); setJoinModalVipOnly(true); @@ -136,6 +149,9 @@ export default function HeroSection() { (locale === "zh" ? "创建用户失败" : "Failed to create user") ); } + if (ensureData.is_new) { + await fetch("/api/meetup/set-needs-password-change", { method: "POST", credentials: "include" }); + } user_id = String(ensureData.user_id).trim(); if (ensureData?.token && ensureData?.record) { await fetch("/api/auth/sync-session", { diff --git a/app/components/JoinModal.tsx b/app/components/JoinModal.tsx index 1520a38..1f495dd 100644 --- a/app/components/JoinModal.tsx +++ b/app/components/JoinModal.tsx @@ -54,6 +54,9 @@ export default function JoinModal({ isOpen, onClose, initialEmail = "", vipOnly if (!res.ok || !data?.ok) { throw new Error(data?.error || (locale === "zh" ? "操作失败" : "Operation failed")); } + if (data.is_new) { + await fetch("/api/meetup/set-needs-password-change", { method: "POST", credentials: "include" }); + } const { pbSaveAuth } = await import("@/app/lib/pocketbase"); pbSaveAuth(data.token, data.record); await fetch("/api/auth/sync-session", { diff --git a/app/components/Navbar.tsx b/app/components/Navbar.tsx index cdd6c8f..bbf28af 100644 --- a/app/components/Navbar.tsx +++ b/app/components/Navbar.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from "react"; import { Link, useLocale, usePathname, useRouter, useTranslation } from "@/i18n/navigation"; -import { getStoredUserEmail, pbLogout } from "@/app/lib/pocketbase"; +import { getStoredUserEmail, getStoredUser, getStoredToken, pbLogout } from "@/app/lib/pocketbase"; import { getNavLinks } from "@/config"; import AuthModal from "./AuthModal"; import ThemeToggle from "./ThemeToggle"; @@ -25,8 +25,8 @@ export default function NavbarComponent() { const fetchAuth = useCallback(async () => { try { - const res = await fetch("/api/auth/me", { credentials: "include", cache: "no-store" }); - const data = await res.json(); + let res = await fetch("/api/auth/me", { credentials: "include", cache: "no-store" }); + let data = await res.json(); if (data?.user && data?.token) { const { pbSaveAuth } = await import("@/app/lib/pocketbase"); pbSaveAuth(data.token, { id: data.user.id, email: data.user.email }); @@ -34,6 +34,25 @@ export default function NavbarComponent() { setUserVip(!!data.user.vip); return; } + const token = getStoredToken(); + const record = getStoredUser(); + if (token && record?.id && record?.email) { + const syncRes = await fetch("/api/auth/sync-session", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ token, record }), + credentials: "include", + }); + if (syncRes.ok) { + res = await fetch("/api/auth/me", { credentials: "include", cache: "no-store" }); + data = await res.json(); + if (data?.user && data?.token) { + setUserEmail(data.user.email); + setUserVip(!!data.user.vip); + return; + } + } + } } catch { /* ignore */ } @@ -47,15 +66,21 @@ export default function NavbarComponent() { useEffect(() => { const onAuthUpdated = () => fetchAuth(); + const onVipUpdated = () => fetchAuth(); window.addEventListener("auth:updated", onAuthUpdated); - return () => window.removeEventListener("auth:updated", onAuthUpdated); + window.addEventListener("vip:updated", onVipUpdated); + return () => { + window.removeEventListener("auth:updated", onAuthUpdated); + window.removeEventListener("vip:updated", onVipUpdated); + }; }, [fetchAuth]); const handleLogout = useCallback(async () => { await pbLogout(); setUserEmail(null); setUserVip(false); - }, []); + router.replace("/"); + }, [router]); const isActive = useCallback((href: string) => { if (href.startsWith("/#")) return false; @@ -124,7 +149,7 @@ export default function NavbarComponent() { {userEmail} - { setUserEmail(null); setUserVip(false); }} /> + ) : null}