fix(payment): allow renewing active subscriptions
This commit is contained in:
@@ -11,13 +11,14 @@ import {
|
|||||||
usePaymentDispatch,
|
usePaymentDispatch,
|
||||||
usePaymentState,
|
usePaymentState,
|
||||||
} from "@/stores/payment/payment-context";
|
} from "@/stores/payment/payment-context";
|
||||||
import { AppEnvUtil } from "@/utils";
|
import { AppEnvUtil, Logger, Result } from "@/utils";
|
||||||
|
|
||||||
import { StripePaymentDialog } from "./stripe-payment-dialog";
|
import { StripePaymentDialog } from "./stripe-payment-dialog";
|
||||||
import { SubscriptionCtaButton } from "./subscription-cta-button";
|
import { SubscriptionCtaButton } from "./subscription-cta-button";
|
||||||
import styles from "./subscription-cta-button.module.css";
|
import styles from "./subscription-cta-button.module.css";
|
||||||
|
|
||||||
const EZPAY_DEVELOPMENT_REDIRECT_DELAY_MS = 5000;
|
const EZPAY_DEVELOPMENT_REDIRECT_DELAY_MS = 5000;
|
||||||
|
const log = new Logger("SubscriptionCheckoutButton");
|
||||||
|
|
||||||
export interface SubscriptionCheckoutButtonProps {
|
export interface SubscriptionCheckoutButtonProps {
|
||||||
/** 是否可用(未选套餐 / 未勾选协议 → false) */
|
/** 是否可用(未选套餐 / 未勾选协议 → false) */
|
||||||
@@ -107,9 +108,7 @@ export function SubscriptionCheckoutButton({
|
|||||||
payment.isCreatingOrder ||
|
payment.isCreatingOrder ||
|
||||||
payment.isPollingOrder ||
|
payment.isPollingOrder ||
|
||||||
payment.isCheckingVipStatus;
|
payment.isCheckingVipStatus;
|
||||||
const label = payment.isPaid
|
const label = payment.isPollingOrder
|
||||||
? "Activated"
|
|
||||||
: payment.isPollingOrder
|
|
||||||
? "Processing payment..."
|
? "Processing payment..."
|
||||||
: payment.isCreatingOrder
|
: payment.isCreatingOrder
|
||||||
? "Creating order..."
|
? "Creating order..."
|
||||||
@@ -246,23 +245,64 @@ async function redirectToEzpay({
|
|||||||
delayMs = 0,
|
delayMs = 0,
|
||||||
setTimeoutId,
|
setTimeoutId,
|
||||||
}: RedirectToEzpayInput): Promise<void> {
|
}: RedirectToEzpayInput): Promise<void> {
|
||||||
|
log.debug("[subscription-checkout] redirectToEzpay START", {
|
||||||
|
hasOrderId: Boolean(orderId),
|
||||||
|
orderId,
|
||||||
|
subscriptionType,
|
||||||
|
delayMs,
|
||||||
|
paymentUrl,
|
||||||
|
});
|
||||||
|
|
||||||
if (orderId) {
|
if (orderId) {
|
||||||
await PendingPaymentOrderStorage.setPendingOrder({
|
const saveResult = await PendingPaymentOrderStorage.setPendingOrder({
|
||||||
orderId,
|
orderId,
|
||||||
payChannel: "ezpay",
|
payChannel: "ezpay",
|
||||||
subscriptionType,
|
subscriptionType,
|
||||||
createdAt: Date.now(),
|
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) {
|
if (delayMs > 0) {
|
||||||
const timeoutId = window.setTimeout(() => {
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
log.debug("[subscription-checkout] redirectToEzpay DELAY elapsed", {
|
||||||
|
orderId,
|
||||||
|
subscriptionType,
|
||||||
|
paymentUrl,
|
||||||
|
});
|
||||||
window.location.href = paymentUrl;
|
window.location.href = paymentUrl;
|
||||||
}, delayMs);
|
}, delayMs);
|
||||||
setTimeoutId?.(timeoutId);
|
setTimeoutId?.(timeoutId);
|
||||||
|
log.debug("[subscription-checkout] redirectToEzpay DELAY scheduled", {
|
||||||
|
orderId,
|
||||||
|
subscriptionType,
|
||||||
|
delayMs,
|
||||||
|
timeoutId,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.debug("[subscription-checkout] redirectToEzpay NOW", {
|
||||||
|
orderId,
|
||||||
|
subscriptionType,
|
||||||
|
paymentUrl,
|
||||||
|
});
|
||||||
window.location.href = paymentUrl;
|
window.location.href = paymentUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import {
|
|||||||
SubscriptionBanner,
|
SubscriptionBanner,
|
||||||
SubscriptionBenefitsCard,
|
SubscriptionBenefitsCard,
|
||||||
SubscriptionCheckoutButton,
|
SubscriptionCheckoutButton,
|
||||||
SubscriptionCtaButton,
|
|
||||||
SubscriptionPaymentMethod,
|
SubscriptionPaymentMethod,
|
||||||
SubscriptionPlanCard,
|
SubscriptionPlanCard,
|
||||||
SubscriptionUserRow,
|
SubscriptionUserRow,
|
||||||
@@ -213,11 +212,6 @@ export function SubscriptionScreen({
|
|||||||
payment.isCheckingVipStatus;
|
payment.isCheckingVipStatus;
|
||||||
const canActivate =
|
const canActivate =
|
||||||
selectedPlan !== null && payment.agreed && !isPaymentBusy;
|
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 name = user.currentUser?.username ?? FALLBACK_USERNAME;
|
||||||
const avatarUrl = user.currentUser?.avatarUrl ?? null;
|
const avatarUrl = user.currentUser?.avatarUrl ?? null;
|
||||||
@@ -312,32 +306,24 @@ export function SubscriptionScreen({
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{!isVip ? (
|
<section className={styles.paymentMethodSlot}>
|
||||||
<section className={styles.paymentMethodSlot}>
|
<SubscriptionPaymentMethod
|
||||||
<SubscriptionPaymentMethod
|
value={payment.payChannel}
|
||||||
value={payment.payChannel}
|
disabled={isPaymentBusy}
|
||||||
disabled={isPaymentBusy}
|
onChange={(payChannel) =>
|
||||||
onChange={(payChannel) =>
|
paymentDispatch({
|
||||||
paymentDispatch({
|
type: "PaymentPayChannelChanged",
|
||||||
type: "PaymentPayChannelChanged",
|
payChannel,
|
||||||
payChannel,
|
})
|
||||||
})
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</section>
|
||||||
</section>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className={styles.ctaSlot}>
|
<div className={styles.ctaSlot}>
|
||||||
{isVip ? (
|
<SubscriptionCheckoutButton
|
||||||
<SubscriptionCtaButton type="button" disabled>
|
disabled={!canActivate}
|
||||||
VIP Activated
|
subscriptionType={subscriptionType}
|
||||||
</SubscriptionCtaButton>
|
/>
|
||||||
) : (
|
|
||||||
<SubscriptionCheckoutButton
|
|
||||||
disabled={!canActivate}
|
|
||||||
subscriptionType={subscriptionType}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.agreementSlot}>
|
<div className={styles.agreementSlot}>
|
||||||
|
|||||||
@@ -270,6 +270,37 @@ export const paymentMachine = setup({
|
|||||||
|
|
||||||
paid: {
|
paid: {
|
||||||
on: {
|
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: {
|
PaymentReset: {
|
||||||
target: "ready",
|
target: "ready",
|
||||||
actions: assign(resetOrderState),
|
actions: assign(resetOrderState),
|
||||||
|
|||||||
Reference in New Issue
Block a user