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}