feat(subscription): load vip and credit plans from api
This commit is contained in:
@@ -17,7 +17,6 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
import { Checkbox, MobileShell } from "@/app/_components/core";
|
||||
import { AppConstants } from "@/core/app_constants";
|
||||
import { VIP_BENEFITS } from "@/data/constants/vip-benefits";
|
||||
import { PendingPaymentOrderStorage } from "@/data/storage";
|
||||
import {
|
||||
usePaymentDispatch,
|
||||
@@ -29,18 +28,23 @@ import { ROUTES } from "@/router/routes";
|
||||
import {
|
||||
SubscriptionBackLink,
|
||||
SubscriptionBanner,
|
||||
SubscriptionBenefitsCard,
|
||||
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";
|
||||
|
||||
@@ -71,6 +75,7 @@ export function SubscriptionScreen({
|
||||
useState(false);
|
||||
const [pendingChatReturnAfterVipSync, setPendingChatReturnAfterVipSync] =
|
||||
useState(false);
|
||||
const isVipSubscription = subscriptionType === "vip";
|
||||
|
||||
useEffect(() => {
|
||||
if (payment.status === "idle") {
|
||||
@@ -187,9 +192,25 @@ export function SubscriptionScreen({
|
||||
.map((plan) => toPlanView(plan, subscriptionType)),
|
||||
[payment.plans, subscriptionType],
|
||||
);
|
||||
const vipOfferPlans = useMemo(
|
||||
() =>
|
||||
payment.plans
|
||||
.filter(isVipPlan)
|
||||
.slice(0, 3)
|
||||
.map(toVipOfferPlanView),
|
||||
[payment.plans],
|
||||
);
|
||||
const directCoinsPlans = useMemo(
|
||||
() => payment.plans.filter(isCreditPlan).map(toCoinsOfferPlanView),
|
||||
[payment.plans],
|
||||
);
|
||||
|
||||
const selectedPlan =
|
||||
plans.find((plan) => plan.id === payment.selectedPlanId) ?? null;
|
||||
(isVipSubscription
|
||||
? [...vipOfferPlans, ...directCoinsPlans].find(
|
||||
(plan) => plan.id === payment.selectedPlanId,
|
||||
)
|
||||
: plans.find((plan) => plan.id === payment.selectedPlanId)) ?? null;
|
||||
const isPaymentBusy =
|
||||
payment.isCreatingOrder ||
|
||||
payment.isPollingOrder ||
|
||||
@@ -199,9 +220,6 @@ export function SubscriptionScreen({
|
||||
|
||||
const name = user.currentUser?.username ?? FALLBACK_USERNAME;
|
||||
const avatarUrl = user.avatarUrl ?? user.currentUser?.avatarUrl ?? null;
|
||||
const autoRenewCaption = payment.autoRenew
|
||||
? "It will automatically renew upon expiration, and can be canceled at any time"
|
||||
: "This plan is a one-time purchase and will not automatically renew";
|
||||
const plansStatusMessage =
|
||||
subscriptionType === "voice"
|
||||
? "Voice packages are unavailable."
|
||||
@@ -260,6 +278,29 @@ export function SubscriptionScreen({
|
||||
}, [pendingChatReturnAfterVipSync, subscriptionType, user.currentUser?.isVip]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isVipSubscription) {
|
||||
const firstVipPlanId = vipOfferPlans[0]?.id ?? "";
|
||||
const hasSelectedVipPlan = vipOfferPlans.some(
|
||||
(plan) => plan.id === payment.selectedPlanId,
|
||||
);
|
||||
const hasSelectedCoinsPlan = directCoinsPlans.some(
|
||||
(plan) => plan.id === payment.selectedPlanId,
|
||||
);
|
||||
const canSelectPlan =
|
||||
firstVipPlanId.length > 0 &&
|
||||
(payment.status === "ready" ||
|
||||
payment.status === "paid" ||
|
||||
payment.status === "failed");
|
||||
|
||||
if (!canSelectPlan || hasSelectedVipPlan || hasSelectedCoinsPlan) return;
|
||||
|
||||
paymentDispatch({
|
||||
type: "PaymentPlanSelected",
|
||||
planId: firstVipPlanId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (payment.isLoadingPlans || payment.plans.length === 0) return;
|
||||
if (selectedPlan !== null) return;
|
||||
|
||||
@@ -276,8 +317,13 @@ export function SubscriptionScreen({
|
||||
payment.isLoadingPlans,
|
||||
payment.plans,
|
||||
paymentDispatch,
|
||||
payment.selectedPlanId,
|
||||
payment.status,
|
||||
directCoinsPlans,
|
||||
isVipSubscription,
|
||||
selectedPlan,
|
||||
subscriptionType,
|
||||
vipOfferPlans,
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -287,45 +333,69 @@ export function SubscriptionScreen({
|
||||
<SubscriptionBackLink className={styles.backSlot} />
|
||||
</header>
|
||||
|
||||
<section className={styles.userSlot}>
|
||||
<SubscriptionUserRow name={name} avatarUrl={avatarUrl} />
|
||||
</section>
|
||||
{isVipSubscription ? (
|
||||
<>
|
||||
<SubscriptionVipOfferSection
|
||||
plans={vipOfferPlans}
|
||||
selectedPlanId={payment.selectedPlanId || selectedPlan?.id || null}
|
||||
onSelectPlan={(planId) =>
|
||||
paymentDispatch({
|
||||
type: "PaymentPlanSelected",
|
||||
planId,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<SubscriptionCoinsOfferSection
|
||||
plans={directCoinsPlans}
|
||||
selectedPlanId={payment.selectedPlanId || selectedPlan?.id || null}
|
||||
onSelectPlan={(planId) =>
|
||||
paymentDispatch({
|
||||
type: "PaymentPlanSelected",
|
||||
planId,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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,
|
||||
})
|
||||
}
|
||||
<section className={styles.bannerSlot}>
|
||||
<SubscriptionBanner
|
||||
title={SUBSCRIPTION_BANNER_TITLES[subscriptionType]}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className={styles.plansStatus}>
|
||||
{payment.isLoadingPlans
|
||||
? "Loading membership plans..."
|
||||
: payment.errorMessage ?? plansStatusMessage}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<p className={styles.autoRenewCaption}>{autoRenewCaption}</p>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* <p className={styles.autoRenewCaption}>{autoRenewCaption}</p>
|
||||
|
||||
<section className={styles.benefitsSlot}>
|
||||
{subscriptionType === "voice" ? (
|
||||
@@ -336,7 +406,7 @@ export function SubscriptionScreen({
|
||||
items={VIP_BENEFITS}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
</section> */}
|
||||
|
||||
<section className={styles.paymentMethodSlot}>
|
||||
<SubscriptionPaymentMethod
|
||||
|
||||
Reference in New Issue
Block a user