feat(subscription): split vip and topup flows
This commit is contained in:
@@ -9,33 +9,26 @@ import {
|
||||
usePaymentDispatch,
|
||||
usePaymentState,
|
||||
} from "@/stores/payment/payment-context";
|
||||
import { useUserDispatch, useUserState } from "@/stores/user/user-context";
|
||||
import { useUserDispatch } from "@/stores/user/user-context";
|
||||
import { ROUTES } from "@/router/routes";
|
||||
|
||||
import {
|
||||
SubscriptionBackLink,
|
||||
SubscriptionBanner,
|
||||
SubscriptionCheckoutButton,
|
||||
SubscriptionCoinsOfferSection,
|
||||
SubscriptionPaymentMethod,
|
||||
SubscriptionPaymentSuccessDialog,
|
||||
SubscriptionPlanCard,
|
||||
SubscriptionUserRow,
|
||||
SubscriptionVipOfferSection,
|
||||
} from "./components";
|
||||
import styles from "./components/subscription-screen.module.css";
|
||||
import {
|
||||
SUBSCRIPTION_BANNER_TITLES,
|
||||
isCreditPlan,
|
||||
isPlanForType,
|
||||
isVipPlan,
|
||||
toCoinsOfferPlanView,
|
||||
toPlanView,
|
||||
toVipOfferPlanView,
|
||||
type SubscriptionType,
|
||||
} from "./subscription-screen.helpers";
|
||||
|
||||
const FALLBACK_USERNAME = "User name";
|
||||
export type { SubscriptionType } from "./subscription-screen.helpers";
|
||||
|
||||
export interface SubscriptionScreenProps {
|
||||
@@ -50,7 +43,6 @@ export function SubscriptionScreen({
|
||||
returnTo = null,
|
||||
}: SubscriptionScreenProps) {
|
||||
const router = useRouter();
|
||||
const user = useUserState();
|
||||
const userDispatch = useUserDispatch();
|
||||
const payment = usePaymentState();
|
||||
const paymentDispatch = usePaymentDispatch();
|
||||
@@ -59,7 +51,7 @@ export function SubscriptionScreen({
|
||||
const successDialogShownOrderRef = useRef<string | null>(null);
|
||||
const [showPaymentSuccessDialog, setShowPaymentSuccessDialog] =
|
||||
useState(false);
|
||||
const isVipSubscription = subscriptionType === "vip";
|
||||
const canSubscribeVip = subscriptionType === "vip";
|
||||
|
||||
useEffect(() => {
|
||||
if (payment.status === "idle") {
|
||||
@@ -139,14 +131,6 @@ export function SubscriptionScreen({
|
||||
void PendingPaymentOrderStorage.clearPendingOrder();
|
||||
}, [payment.currentOrderId, payment.isPaid, payment.status]);
|
||||
|
||||
const plans = useMemo(
|
||||
() =>
|
||||
payment.plans
|
||||
.filter((plan) => isPlanForType(plan, subscriptionType))
|
||||
.slice(0, 3)
|
||||
.map((plan) => toPlanView(plan, subscriptionType)),
|
||||
[payment.plans, subscriptionType],
|
||||
);
|
||||
const vipOfferPlans = useMemo(
|
||||
() =>
|
||||
payment.plans
|
||||
@@ -161,28 +145,18 @@ export function SubscriptionScreen({
|
||||
);
|
||||
|
||||
const selectedPlan =
|
||||
(isVipSubscription
|
||||
(canSubscribeVip
|
||||
? [...vipOfferPlans, ...directCoinsPlans].find(
|
||||
(plan) => plan.id === payment.selectedPlanId,
|
||||
)
|
||||
: plans.find((plan) => plan.id === payment.selectedPlanId)) ?? null;
|
||||
: directCoinsPlans.find((plan) => plan.id === payment.selectedPlanId)) ??
|
||||
null;
|
||||
const isPaymentBusy =
|
||||
payment.isCreatingOrder ||
|
||||
payment.isPollingOrder;
|
||||
const canActivate =
|
||||
selectedPlan !== null && payment.agreed && !isPaymentBusy;
|
||||
|
||||
const name = user.currentUser?.username ?? FALLBACK_USERNAME;
|
||||
const avatarUrl = user.avatarUrl ?? user.currentUser?.avatarUrl ?? null;
|
||||
const plansStatusMessage =
|
||||
subscriptionType === "voice"
|
||||
? "Voice packages are unavailable."
|
||||
: "Membership plans are unavailable.";
|
||||
const plansAriaLabel =
|
||||
subscriptionType === "voice"
|
||||
? "Choose a voice package"
|
||||
: "Choose a subscription plan";
|
||||
|
||||
const finishPaymentSuccessClose = () => {
|
||||
setShowPaymentSuccessDialog(false);
|
||||
paymentDispatch({ type: "PaymentReset" });
|
||||
@@ -196,7 +170,7 @@ export function SubscriptionScreen({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isVipSubscription) {
|
||||
if (canSubscribeVip) {
|
||||
const firstVipPlanId = vipOfferPlans[0]?.id ?? "";
|
||||
const hasSelectedVipPlan = vipOfferPlans.some(
|
||||
(plan) => plan.id === payment.selectedPlanId,
|
||||
@@ -219,28 +193,21 @@ export function SubscriptionScreen({
|
||||
return;
|
||||
}
|
||||
|
||||
if (payment.isLoadingPlans || payment.plans.length === 0) return;
|
||||
if (payment.isLoadingPlans || directCoinsPlans.length === 0) return;
|
||||
if (selectedPlan !== null) return;
|
||||
|
||||
const nextPlan = payment.plans.find((plan) =>
|
||||
isPlanForType(plan, subscriptionType),
|
||||
);
|
||||
if (!nextPlan) return;
|
||||
|
||||
paymentDispatch({
|
||||
type: "PaymentPlanSelected",
|
||||
planId: nextPlan.planId,
|
||||
planId: directCoinsPlans[0]?.id ?? "",
|
||||
});
|
||||
}, [
|
||||
canSubscribeVip,
|
||||
directCoinsPlans,
|
||||
payment.isLoadingPlans,
|
||||
payment.plans,
|
||||
paymentDispatch,
|
||||
payment.selectedPlanId,
|
||||
payment.status,
|
||||
directCoinsPlans,
|
||||
isVipSubscription,
|
||||
selectedPlan,
|
||||
subscriptionType,
|
||||
vipOfferPlans,
|
||||
]);
|
||||
|
||||
@@ -251,8 +218,8 @@ export function SubscriptionScreen({
|
||||
<SubscriptionBackLink className={styles.backSlot} />
|
||||
</header>
|
||||
|
||||
{isVipSubscription ? (
|
||||
<div className={styles.offerStack}>
|
||||
<div className={styles.offerStack}>
|
||||
{canSubscribeVip ? (
|
||||
<SubscriptionVipOfferSection
|
||||
plans={vipOfferPlans}
|
||||
selectedPlanId={payment.selectedPlanId || selectedPlan?.id || null}
|
||||
@@ -263,55 +230,18 @@ export function SubscriptionScreen({
|
||||
})
|
||||
}
|
||||
/>
|
||||
<SubscriptionCoinsOfferSection
|
||||
plans={directCoinsPlans}
|
||||
selectedPlanId={payment.selectedPlanId || selectedPlan?.id || null}
|
||||
onSelectPlan={(planId) =>
|
||||
paymentDispatch({
|
||||
type: "PaymentPlanSelected",
|
||||
planId,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<section className={styles.userSlot}>
|
||||
<SubscriptionUserRow name={name} avatarUrl={avatarUrl} />
|
||||
</section>
|
||||
|
||||
<section className={styles.bannerSlot}>
|
||||
<SubscriptionBanner
|
||||
title={SUBSCRIPTION_BANNER_TITLES[subscriptionType]}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className={styles.plansRow} aria-label={plansAriaLabel}>
|
||||
{plans.length > 0 ? (
|
||||
plans.map((plan, index) => (
|
||||
<SubscriptionPlanCard
|
||||
key={plan.id}
|
||||
plan={plan}
|
||||
gradientIndex={index}
|
||||
selected={payment.selectedPlanId === plan.id}
|
||||
onClick={() =>
|
||||
paymentDispatch({
|
||||
type: "PaymentPlanSelected",
|
||||
planId: plan.id,
|
||||
})
|
||||
}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className={styles.plansStatus}>
|
||||
{payment.isLoadingPlans
|
||||
? "Loading membership plans..."
|
||||
: payment.errorMessage ?? plansStatusMessage}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
<SubscriptionCoinsOfferSection
|
||||
plans={directCoinsPlans}
|
||||
selectedPlanId={payment.selectedPlanId || selectedPlan?.id || null}
|
||||
onSelectPlan={(planId) =>
|
||||
paymentDispatch({
|
||||
type: "PaymentPlanSelected",
|
||||
planId,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section className={styles.paymentMethodSlot}>
|
||||
<SubscriptionPaymentMethod
|
||||
|
||||
Reference in New Issue
Block a user