fix(payment): allow renewing active subscriptions
This commit is contained in:
@@ -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,32 +306,24 @@ export function SubscriptionScreen({
|
||||
)}
|
||||
</section>
|
||||
|
||||
{!isVip ? (
|
||||
<section className={styles.paymentMethodSlot}>
|
||||
<SubscriptionPaymentMethod
|
||||
value={payment.payChannel}
|
||||
disabled={isPaymentBusy}
|
||||
onChange={(payChannel) =>
|
||||
paymentDispatch({
|
||||
type: "PaymentPayChannelChanged",
|
||||
payChannel,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
) : null}
|
||||
<section className={styles.paymentMethodSlot}>
|
||||
<SubscriptionPaymentMethod
|
||||
value={payment.payChannel}
|
||||
disabled={isPaymentBusy}
|
||||
onChange={(payChannel) =>
|
||||
paymentDispatch({
|
||||
type: "PaymentPayChannelChanged",
|
||||
payChannel,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div className={styles.ctaSlot}>
|
||||
{isVip ? (
|
||||
<SubscriptionCtaButton type="button" disabled>
|
||||
VIP Activated
|
||||
</SubscriptionCtaButton>
|
||||
) : (
|
||||
<SubscriptionCheckoutButton
|
||||
disabled={!canActivate}
|
||||
subscriptionType={subscriptionType}
|
||||
/>
|
||||
)}
|
||||
<SubscriptionCheckoutButton
|
||||
disabled={!canActivate}
|
||||
subscriptionType={subscriptionType}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.agreementSlot}>
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user