fix(payment): allow renewing active subscriptions

This commit is contained in:
2026-06-22 18:48:21 +08:00
parent 2cfbaba058
commit 9a3c8f0d12
3 changed files with 92 additions and 35 deletions
@@ -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<void> {
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;
}
@@ -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,7 +306,6 @@ export function SubscriptionScreen({
)}
</section>
{!isVip ? (
<section className={styles.paymentMethodSlot}>
<SubscriptionPaymentMethod
value={payment.payChannel}
@@ -325,19 +318,12 @@ export function SubscriptionScreen({
}
/>
</section>
) : null}
<div className={styles.ctaSlot}>
{isVip ? (
<SubscriptionCtaButton type="button" disabled>
VIP Activated
</SubscriptionCtaButton>
) : (
<SubscriptionCheckoutButton
disabled={!canActivate}
subscriptionType={subscriptionType}
/>
)}
</div>
<div className={styles.agreementSlot}>
+31
View File
@@ -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),