Fix Telegram bot binding fallback

This commit is contained in:
2569718930@qq.com
2026-06-09 12:57:37 +08:00
parent f320d07e03
commit d0c0bf7034
9 changed files with 189 additions and 18 deletions
+13 -5
View File
@@ -102,6 +102,7 @@ export function AccountCenter() {
lastPaymentStartedAt,
telegramBindOpening,
telegramBindUrl,
telegramBindCommand,
manualPayment,
manualTxHash,
txValidation,
@@ -175,6 +176,7 @@ export function AccountCenter() {
submitManualPaymentTx,
validateTxHash,
handleOverlayCheckout,
createTelegramBotBindCommand,
openTelegramBotBindLink,
} = useAccountPayment({
isEn,
@@ -497,9 +499,7 @@ export function AccountCenter() {
: monthlyReferralLimit * referralRewardPoints;
// ── Telegram bind command ──────────────────────────────
const bindCommand = userId
? `/bind ${userId}${email ? ` ${email}` : ""}`
: "/bind <supabase_user_id> <email>";
const bindCommand = telegramBindCommand || copy.telegramBindCommandPlaceholder;
// ── Copy handler ──────────────────────────────────────
const handleCopy = (text: string) => {
@@ -509,6 +509,13 @@ export function AccountCenter() {
});
};
const handleCopyTelegramBindCommand = async () => {
if (!isAuthenticated || telegramBindOpening) return;
const command = await createTelegramBotBindCommand();
if (!command) return;
handleCopy(command);
};
const applyReferralCode = useCallback(async () => {
const code = referralCodeInput.trim();
if (!code || referralApplying) return;
@@ -1040,8 +1047,9 @@ export function AccountCenter() {
: copy.telegramBotBindLink}
</button>
<button
onClick={() => handleCopy(bindCommand)}
className="rounded-xl border border-blue-700 bg-blue-600 p-4 text-white shadow-sm transition-all hover:bg-blue-700"
onClick={() => void handleCopyTelegramBindCommand()}
disabled={telegramBindOpening || !isAuthenticated}
className="rounded-xl border border-blue-700 bg-blue-600 p-4 text-white shadow-sm transition-all hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50"
title={copy.copyCommand}
aria-label={copy.copyCommand}
>
@@ -62,6 +62,10 @@ export function runTests() {
path.join(accountDir, "usePaymentFlow.ts"),
"utf8",
);
const useBillingSource = fs.readFileSync(
path.join(accountDir, "useBilling.ts"),
"utf8",
);
assert(
(accountCenterSource.includes('import { usePaymentState } from "./usePaymentState";') ||
hookSource.includes('import { usePaymentState } from "./usePaymentState";')) &&
@@ -193,6 +197,23 @@ export function runTests() {
accountCenterSource.includes('hasTelegramPanel ? "" : "sm:grid-cols-2"'),
"payment management must only split into internal columns when it is not already sharing the row with a Telegram panel",
);
assert(
!accountCenterSource.includes("`/bind ${userId}${email ? ` ${email}` : \"\"}`") &&
!accountCenterSource.includes("/bind <supabase_user_id> <email>"),
"Telegram fallback copy must not expose the legacy /bind supabase_user_id command",
);
assert(
accountCenterSource.includes("telegramBindCommand") &&
accountCenterSource.includes("createTelegramBotBindCommand") &&
accountCenterSource.includes("handleCopyTelegramBindCommand"),
"account Telegram fallback copy must generate a fresh one-time /start bind token before copying",
);
assert(
useBillingSource.includes("bot_command") &&
useBillingSource.includes("createTelegramBotBindCommand") &&
useBillingSource.includes("telegramBindCommand"),
"billing hook must persist the backend /start bind token command for Telegram fallback copy",
);
assert(
!appAnalyticsSource.includes('NEXT_PUBLIC_POLYWEATHER_APP_ANALYTICS === "true"') &&
!analyticsRouteSource.includes('NEXT_PUBLIC_POLYWEATHER_APP_ANALYTICS === "true"'),
+8 -2
View File
@@ -46,8 +46,14 @@ export function createAccountCopy(isEn: boolean): Record<string, string> {
? "Use one-click Telegram binding first to sync notifications and access. After binding, refresh this page and submit your Telegram group join request."
: "优先使用「一键绑定 Telegram Bot」同步通知与权限。绑定完成后刷新本页,再提交 Telegram 群组入群申请。",
telegramFallbackHint: isEn
? "Fallback copy method: only use this if one-click binding does not open Telegram correctly. Copy the command below and send it to @polyyuanbot. After binding, refresh this page to show the group entry."
: "兜底复制方式:仅在一键绑定无法正常打开 Telegram 时使用。请复制下方命令并发送给 @polyyuanbot。绑定完成后刷新本页,即可显示入群入口。",
? "Fallback copy method: click Copy to generate a 10-minute one-time /start bind command, send it to @polyyuanbot, then confirm binding in the Bot and refresh this page."
: "兜底复制方式:点击复制会生成 10 分钟有效的一次性 /start bind 命令。请把它发送给 @polyyuanbot,并在 Bot 内确认绑定后刷新本页。",
telegramBindCommandPlaceholder: isEn
? "Click Copy to generate a one-time /start bind command"
: "点击复制生成一次性 /start bind 命令",
telegramBindCommandCopied: isEn
? "One-time Telegram bind command generated and copied. Send it to @polyyuanbot, then confirm binding in the Bot."
: "一次性 Telegram 绑定命令已生成并复制。请发送给 @polyyuanbot,然后在 Bot 内确认绑定。",
paymentManualSupport: isEn
? "If payment succeeds but Pro is still not activated, email yhrsc30@gmail.com. This project is currently maintained by one developer, so manual recovery may be needed in edge cases."
: "如果付款成功后 Pro 仍未开通,请发邮件到 yhrsc30@gmail.com。当前项目由我一人维护,极少数边缘情况可能需要人工补开。给你带来的不便,敬请谅解!",
@@ -549,6 +549,7 @@ export function useAccountPayment(params: UseAccountPaymentParams) {
lastPaymentStartedAt,
telegramBindOpening,
telegramBindUrl: billing.telegramBindUrl,
telegramBindCommand: billing.telegramBindCommand,
manualPayment,
manualTxHash,
txValidation,
@@ -622,6 +623,7 @@ export function useAccountPayment(params: UseAccountPaymentParams) {
submitManualPaymentTx: paymentFlow.submitManualPaymentTx,
validateTxHash: paymentFlow.validateTxHash,
handleOverlayCheckout: paymentFlow.handleOverlayCheckout,
createTelegramBotBindCommand: billing.createTelegramBotBindCommand,
openTelegramBotBindLink: billing.openTelegramBotBindLink,
};
}
+56 -11
View File
@@ -23,6 +23,12 @@ import { isTelegramPrivateGroupPriceEligible } from "./telegram-pricing";
import { trackAppEvent } from "@/lib/app-analytics";
// ============================================================
type TelegramBotBindPayload = {
bot_command?: string;
bot_url?: string;
start_param?: string;
};
export interface UseBillingParams {
isEn: boolean;
copy: Record<string, string>;
@@ -96,6 +102,7 @@ export function useBilling(params: UseBillingParams) {
// ── Billing-specific state ────────────────────────────────
const [reconcileBusy, setReconcileBusy] = useState(false);
const [telegramBindUrl, setTelegramBindUrl] = useState("");
const [telegramBindCommand, setTelegramBindCommand] = useState("");
// ── Derived values ──────────────────────────────────────
const paymentReadyForRecovery = Boolean(paymentConfig?.enabled && paymentConfig?.configured);
@@ -273,23 +280,59 @@ export function useBilling(params: UseBillingParams) {
[buildAuthedHeaders, copy, loadPaymentSnapshot, refreshEntitlementAfterPayment, reconcileLatestPayment],
);
// ── openTelegramBotBindLink ──────────────────────────────
// ── Telegram bot bind helpers ─────────────────────────────
const requestTelegramBotBindPayload = useCallback(async () => {
const authHeaders = await buildAuthedHeaders(true, false);
const res = await fetch("/api/auth/telegram/bot-bind-link", {
method: "POST",
headers: authHeaders,
});
if (!res.ok) {
const raw = (await res.text()).slice(0, 300);
throw new Error(raw || copy.telegramBindFailed);
}
const data = (await res.json()) as TelegramBotBindPayload;
const botUrl = String(data.bot_url || "").trim();
const startParam = String(data.start_param || "").trim();
const botCommand = String(
data.bot_command || (startParam ? `/start ${startParam}` : ""),
).trim();
if (!botUrl && !botCommand) throw new Error(copy.telegramBindLinkMissing);
setTelegramBindUrl(botUrl);
setTelegramBindCommand(botCommand);
return { botCommand, botUrl };
}, [
buildAuthedHeaders,
copy.telegramBindFailed,
copy.telegramBindLinkMissing,
]);
const createTelegramBotBindCommand = async () => {
setTelegramBindOpening(true);
setPaymentError("");
setTelegramBindUrl("");
setTelegramBindCommand("");
try {
const { botCommand } = await requestTelegramBotBindPayload();
if (!botCommand) throw new Error(copy.telegramBindLinkMissing);
setPaymentInfo(copy.telegramBindCommandCopied);
return botCommand;
} catch (error) {
setPaymentError(normalizePaymentError(error).message);
return "";
} finally {
setTelegramBindOpening(false);
}
};
const openTelegramBotBindLink = async () => {
setTelegramBindOpening(true);
setPaymentError("");
setTelegramBindUrl("");
setTelegramBindCommand("");
const popup = window.open("about:blank", "_blank", "noopener,noreferrer");
try {
const authHeaders = await buildAuthedHeaders(true, false);
const res = await fetch("/api/auth/telegram/bot-bind-link", {
method: "POST", headers: authHeaders,
});
if (!res.ok) {
const raw = (await res.text()).slice(0, 300);
throw new Error(raw || copy.telegramBindFailed);
}
const data = (await res.json()) as { bot_url?: string };
const botUrl = String(data.bot_url || "").trim();
const { botUrl } = await requestTelegramBotBindPayload();
if (!botUrl) throw new Error(copy.telegramBindLinkMissing);
if (popup && !popup.closed) {
popup.location.href = botUrl;
@@ -423,9 +466,11 @@ export function useBilling(params: UseBillingParams) {
return {
reconcileBusy,
telegramBindUrl,
telegramBindCommand,
setTelegramBindUrl,
reconcileLatestPayment,
handleSubmit409,
createTelegramBotBindCommand,
openTelegramBotBindLink,
paymentReadyForRecovery,
hasRecentPaymentRecovery,