316 lines
9.3 KiB
TypeScript
316 lines
9.3 KiB
TypeScript
"use client";
|
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
import { BackButton } from "@/app/_components";
|
|
import { Checkbox, MobileShell } from "@/app/_components/core";
|
|
import { AppConstants } from "@/core/app_constants";
|
|
import { PendingPaymentOrderStorage } from "@/data/storage";
|
|
import {
|
|
usePaymentDispatch,
|
|
usePaymentState,
|
|
} from "@/stores/payment/payment-context";
|
|
import { useUserDispatch } from "@/stores/user/user-context";
|
|
import { ROUTES } from "@/router/routes";
|
|
|
|
import {
|
|
SubscriptionCheckoutButton,
|
|
SubscriptionCoinsOfferSection,
|
|
SubscriptionPaymentMethod,
|
|
SubscriptionPaymentSuccessDialog,
|
|
SubscriptionVipOfferSection,
|
|
} from "./components";
|
|
import styles from "./components/subscription-screen.module.css";
|
|
import {
|
|
isCreditPlan,
|
|
isVipPlan,
|
|
toCoinsOfferPlanView,
|
|
toVipOfferPlanView,
|
|
type SubscriptionType,
|
|
} from "./subscription-screen.helpers";
|
|
|
|
export type { SubscriptionType } from "./subscription-screen.helpers";
|
|
|
|
export interface SubscriptionScreenProps {
|
|
subscriptionType?: SubscriptionType;
|
|
shouldResumePendingOrder?: boolean;
|
|
returnTo?: "chat" | null;
|
|
}
|
|
|
|
export function SubscriptionScreen({
|
|
subscriptionType = "vip",
|
|
shouldResumePendingOrder = false,
|
|
returnTo = null,
|
|
}: SubscriptionScreenProps) {
|
|
const router = useRouter();
|
|
const userDispatch = useUserDispatch();
|
|
const payment = usePaymentState();
|
|
const paymentDispatch = usePaymentDispatch();
|
|
const refreshedPaidOrderRef = useRef<string | null>(null);
|
|
const resumedPendingOrderRef = useRef<string | null>(null);
|
|
const successDialogShownOrderRef = useRef<string | null>(null);
|
|
const [showPaymentSuccessDialog, setShowPaymentSuccessDialog] =
|
|
useState(false);
|
|
const canSubscribeVip = subscriptionType === "vip";
|
|
|
|
useEffect(() => {
|
|
if (payment.status === "idle") {
|
|
paymentDispatch({ type: "PaymentInit" });
|
|
}
|
|
}, [payment.status, paymentDispatch]);
|
|
|
|
useEffect(() => {
|
|
if (!payment.isPaid || !payment.currentOrderId) return;
|
|
if (refreshedPaidOrderRef.current === payment.currentOrderId) return;
|
|
refreshedPaidOrderRef.current = payment.currentOrderId;
|
|
userDispatch({ type: "UserFetch" });
|
|
}, [payment.currentOrderId, payment.isPaid, userDispatch]);
|
|
|
|
useEffect(() => {
|
|
if (!payment.isPaid || !payment.currentOrderId) return;
|
|
if (successDialogShownOrderRef.current === payment.currentOrderId) return;
|
|
|
|
successDialogShownOrderRef.current = payment.currentOrderId;
|
|
setShowPaymentSuccessDialog(true);
|
|
}, [payment.currentOrderId, payment.isPaid]);
|
|
|
|
useEffect(() => {
|
|
const canInspectPendingOrder =
|
|
payment.status === "ready" ||
|
|
(!shouldResumePendingOrder &&
|
|
(payment.isPollingOrder || payment.isPaid || payment.status === "failed"));
|
|
if (!canInspectPendingOrder) return;
|
|
|
|
let cancelled = false;
|
|
|
|
const handlePendingOrder = async () => {
|
|
const result = await PendingPaymentOrderStorage.getPendingOrderForType(
|
|
subscriptionType,
|
|
);
|
|
if (cancelled || !result.success || result.data === null) return;
|
|
|
|
if (!shouldResumePendingOrder) {
|
|
await PendingPaymentOrderStorage.clearPendingOrder();
|
|
if (
|
|
payment.currentOrderId === result.data.orderId &&
|
|
(payment.isPollingOrder || payment.isPaid || payment.status === "failed")
|
|
) {
|
|
paymentDispatch({ type: "PaymentReset" });
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (payment.currentOrderId === result.data.orderId) return;
|
|
if (resumedPendingOrderRef.current === result.data.orderId) return;
|
|
|
|
resumedPendingOrderRef.current = result.data.orderId;
|
|
paymentDispatch({
|
|
type: "PaymentReturned",
|
|
orderId: result.data.orderId,
|
|
createdAt: result.data.createdAt,
|
|
});
|
|
};
|
|
|
|
void handlePendingOrder();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [
|
|
payment.currentOrderId,
|
|
payment.isPaid,
|
|
payment.isPollingOrder,
|
|
payment.status,
|
|
paymentDispatch,
|
|
shouldResumePendingOrder,
|
|
subscriptionType,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (!payment.currentOrderId) return;
|
|
if (!payment.isPaid && payment.status !== "failed") return;
|
|
|
|
void PendingPaymentOrderStorage.clearPendingOrder();
|
|
}, [payment.currentOrderId, payment.isPaid, payment.status]);
|
|
|
|
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 =
|
|
(canSubscribeVip
|
|
? [...vipOfferPlans, ...directCoinsPlans].find(
|
|
(plan) => plan.id === payment.selectedPlanId,
|
|
)
|
|
: directCoinsPlans.find((plan) => plan.id === payment.selectedPlanId)) ??
|
|
null;
|
|
const isPaymentBusy =
|
|
payment.isCreatingOrder ||
|
|
payment.isPollingOrder;
|
|
const canActivate =
|
|
selectedPlan !== null && payment.agreed && !isPaymentBusy;
|
|
const backHref = returnTo === "chat" ? ROUTES.chat : ROUTES.sidebar;
|
|
|
|
const finishPaymentSuccessClose = () => {
|
|
setShowPaymentSuccessDialog(false);
|
|
paymentDispatch({ type: "PaymentReset" });
|
|
if (returnTo === "chat") {
|
|
router.replace(ROUTES.chat);
|
|
}
|
|
};
|
|
|
|
const handlePaymentSuccessClose = () => {
|
|
finishPaymentSuccessClose();
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (canSubscribeVip) {
|
|
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 || directCoinsPlans.length === 0) return;
|
|
if (selectedPlan !== null) return;
|
|
|
|
paymentDispatch({
|
|
type: "PaymentPlanSelected",
|
|
planId: directCoinsPlans[0]?.id ?? "",
|
|
});
|
|
}, [
|
|
canSubscribeVip,
|
|
directCoinsPlans,
|
|
payment.isLoadingPlans,
|
|
paymentDispatch,
|
|
payment.selectedPlanId,
|
|
payment.status,
|
|
selectedPlan,
|
|
vipOfferPlans,
|
|
]);
|
|
|
|
return (
|
|
<MobileShell>
|
|
<div className={styles.shell}>
|
|
<header className={styles.header}>
|
|
<BackButton
|
|
className={styles.backSlot}
|
|
href={backHref}
|
|
variant="soft"
|
|
/>
|
|
</header>
|
|
|
|
<div className={styles.offerStack}>
|
|
{canSubscribeVip ? (
|
|
<SubscriptionVipOfferSection
|
|
plans={vipOfferPlans}
|
|
selectedPlanId={payment.selectedPlanId || selectedPlan?.id || null}
|
|
onSelectPlan={(planId) =>
|
|
paymentDispatch({
|
|
type: "PaymentPlanSelected",
|
|
planId,
|
|
})
|
|
}
|
|
/>
|
|
) : null}
|
|
<SubscriptionCoinsOfferSection
|
|
plans={directCoinsPlans}
|
|
selectedPlanId={payment.selectedPlanId || selectedPlan?.id || null}
|
|
onSelectPlan={(planId) =>
|
|
paymentDispatch({
|
|
type: "PaymentPlanSelected",
|
|
planId,
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<section className={styles.paymentMethodSlot}>
|
|
<SubscriptionPaymentMethod
|
|
value={payment.payChannel}
|
|
disabled={isPaymentBusy}
|
|
onChange={(payChannel) =>
|
|
paymentDispatch({
|
|
type: "PaymentPayChannelChanged",
|
|
payChannel,
|
|
})
|
|
}
|
|
/>
|
|
</section>
|
|
|
|
<div className={styles.ctaSlot}>
|
|
<SubscriptionCheckoutButton
|
|
disabled={!canActivate}
|
|
subscriptionType={subscriptionType}
|
|
returnTo={returnTo}
|
|
/>
|
|
</div>
|
|
|
|
<div className={styles.agreementSlot}>
|
|
<Checkbox
|
|
checked={payment.agreed}
|
|
onChange={(agreed) =>
|
|
paymentDispatch({
|
|
type: "PaymentAgreementChanged",
|
|
agreed,
|
|
})
|
|
}
|
|
label={
|
|
<span className={styles.agreementLabel}>
|
|
I agree to the{" "}
|
|
<a
|
|
href={AppConstants.privacyPolicyUrl}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className={styles.agreementLink}
|
|
>
|
|
VIP Membership Benefits Agreement
|
|
</a>{" "}
|
|
and{" "}
|
|
<a
|
|
href={AppConstants.termsOfServiceUrl}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className={styles.agreementLink}
|
|
>
|
|
Automatic renewal Agreement
|
|
</a>
|
|
</span>
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<SubscriptionPaymentSuccessDialog
|
|
open={showPaymentSuccessDialog}
|
|
subscriptionType={subscriptionType}
|
|
onClose={handlePaymentSuccessClose}
|
|
/>
|
|
</div>
|
|
</MobileShell>
|
|
);
|
|
}
|