From 9a3c8f0d123d432e953177b4c9d0d5db713be8f2 Mon Sep 17 00:00:00 2001 From: chenhang Date: Mon, 22 Jun 2026 18:48:21 +0800 Subject: [PATCH 1/2] fix(payment): allow renewing active subscriptions --- .../subscription-checkout-button.tsx | 50 +++++++++++++++++-- src/app/subscription/subscription-screen.tsx | 46 ++++++----------- src/stores/payment/payment-machine.ts | 31 ++++++++++++ 3 files changed, 92 insertions(+), 35 deletions(-) diff --git a/src/app/subscription/components/subscription-checkout-button.tsx b/src/app/subscription/components/subscription-checkout-button.tsx index 0de76b96..e520b6c8 100644 --- a/src/app/subscription/components/subscription-checkout-button.tsx +++ b/src/app/subscription/components/subscription-checkout-button.tsx @@ -11,13 +11,14 @@ import { usePaymentDispatch, usePaymentState, } from "@/stores/payment/payment-context"; -import { AppEnvUtil } from "@/utils"; +import { AppEnvUtil, Logger, Result } from "@/utils"; import { StripePaymentDialog } from "./stripe-payment-dialog"; import { SubscriptionCtaButton } from "./subscription-cta-button"; import styles from "./subscription-cta-button.module.css"; const EZPAY_DEVELOPMENT_REDIRECT_DELAY_MS = 5000; +const log = new Logger("SubscriptionCheckoutButton"); export interface SubscriptionCheckoutButtonProps { /** 是否可用(未选套餐 / 未勾选协议 → false) */ @@ -107,9 +108,7 @@ export function SubscriptionCheckoutButton({ payment.isCreatingOrder || payment.isPollingOrder || payment.isCheckingVipStatus; - const label = payment.isPaid - ? "Activated" - : payment.isPollingOrder + const label = payment.isPollingOrder ? "Processing payment..." : payment.isCreatingOrder ? "Creating order..." @@ -246,23 +245,64 @@ async function redirectToEzpay({ delayMs = 0, setTimeoutId, }: RedirectToEzpayInput): Promise { + log.debug("[subscription-checkout] redirectToEzpay START", { + hasOrderId: Boolean(orderId), + orderId, + subscriptionType, + delayMs, + paymentUrl, + }); + if (orderId) { - await PendingPaymentOrderStorage.setPendingOrder({ + const saveResult = await PendingPaymentOrderStorage.setPendingOrder({ orderId, payChannel: "ezpay", subscriptionType, createdAt: Date.now(), }); + if (Result.isOk(saveResult)) { + log.debug("[subscription-checkout] pending ezpay order saved", { + orderId, + subscriptionType, + }); + } else { + log.error("[subscription-checkout] pending ezpay order save failed", { + orderId, + subscriptionType, + error: saveResult.error, + }); + } + } else { + log.warn("[subscription-checkout] skip pending ezpay order save: missing orderId", { + subscriptionType, + paymentUrl, + }); } if (delayMs > 0) { const timeoutId = window.setTimeout(() => { + log.debug("[subscription-checkout] redirectToEzpay DELAY elapsed", { + orderId, + subscriptionType, + paymentUrl, + }); window.location.href = paymentUrl; }, delayMs); setTimeoutId?.(timeoutId); + log.debug("[subscription-checkout] redirectToEzpay DELAY scheduled", { + orderId, + subscriptionType, + delayMs, + timeoutId, + }); return; } + log.debug("[subscription-checkout] redirectToEzpay NOW", { + orderId, + subscriptionType, + paymentUrl, + }); window.location.href = paymentUrl; } diff --git a/src/app/subscription/subscription-screen.tsx b/src/app/subscription/subscription-screen.tsx index e5db9782..67bdf790 100644 --- a/src/app/subscription/subscription-screen.tsx +++ b/src/app/subscription/subscription-screen.tsx @@ -32,7 +32,6 @@ import { SubscriptionBanner, SubscriptionBenefitsCard, SubscriptionCheckoutButton, - SubscriptionCtaButton, SubscriptionPaymentMethod, SubscriptionPlanCard, SubscriptionUserRow, @@ -213,11 +212,6 @@ export function SubscriptionScreen({ payment.isCheckingVipStatus; const canActivate = selectedPlan !== null && payment.agreed && !isPaymentBusy; - const isVip = - subscriptionType === "vip" && - (user.currentUser?.isVip === true || - payment.vipStatus?.isVip === true || - payment.isPaid); const name = user.currentUser?.username ?? FALLBACK_USERNAME; const avatarUrl = user.currentUser?.avatarUrl ?? null; @@ -312,32 +306,24 @@ export function SubscriptionScreen({ )} - {!isVip ? ( -
- - paymentDispatch({ - type: "PaymentPayChannelChanged", - payChannel, - }) - } - /> -
- ) : null} +
+ + paymentDispatch({ + type: "PaymentPayChannelChanged", + payChannel, + }) + } + /> +
- {isVip ? ( - - VIP Activated - - ) : ( - - )} +
diff --git a/src/stores/payment/payment-machine.ts b/src/stores/payment/payment-machine.ts index 3c6352fb..f7718502 100644 --- a/src/stores/payment/payment-machine.ts +++ b/src/stores/payment/payment-machine.ts @@ -270,6 +270,37 @@ export const paymentMachine = setup({ paid: { on: { + PaymentPlanSelected: { + target: "ready", + actions: assign(({ event }) => ({ + ...resetOrderState(), + selectedPlanId: event.planId, + autoRenew: defaultAutoRenewForPlan(event.planId), + })), + }, + PaymentPayChannelChanged: { + target: "ready", + actions: assign(({ event }) => ({ + ...resetOrderState(), + payChannel: event.payChannel, + errorMessage: null, + })), + }, + PaymentCreateOrderSubmitted: [ + { + guard: ({ context }) => + context.agreed && context.selectedPlanId.length > 0, + target: "creatingOrder", + actions: assign(resetOrderState), + }, + { + target: "ready", + actions: assign({ + errorMessage: + "Choose a plan and accept the agreement before continuing.", + }), + }, + ], PaymentReset: { target: "ready", actions: assign(resetOrderState), From 41b9a49c74fb50e9b97fa7aaded6cb1fd1d514d2 Mon Sep 17 00:00:00 2001 From: chenhang Date: Mon, 22 Jun 2026 18:48:36 +0800 Subject: [PATCH 2/2] copy(chat): update input placeholder --- src/app/chat/components/chat-input-text-field.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/chat/components/chat-input-text-field.tsx b/src/app/chat/components/chat-input-text-field.tsx index 946fa518..9525a19b 100644 --- a/src/app/chat/components/chat-input-text-field.tsx +++ b/src/app/chat/components/chat-input-text-field.tsx @@ -44,7 +44,7 @@ export const ChatInputTextField = forwardRef< onChange, onSubmit, onFocusChange, - placeholder = "Say something…", + placeholder = "|Say anything that’s on your mind ^U^…", disabled = false, autoFocus = false, },