feat(subscription): separate selection from payment and add issue feedback
Docker Image / Build and Push Docker Image (push) Successful in 2m2s
Docker Image / Build and Push Docker Image (push) Successful in 2m2s
(cherry picked from commit fe9d31146b)
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
"use client";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { BackButton } from "@/app/_components";
|
||||
import { Checkbox, MobileShell } from "@/app/_components/core";
|
||||
import { MobileShell } from "@/app/_components/core";
|
||||
import { PaymentMethodSelector } from "@/app/_components/payment/payment-method-selector";
|
||||
import { usePaymentMethodSelection } from "@/app/_hooks/use-payment-method-selection";
|
||||
import { usePaymentPlanAnalytics } from "@/app/_hooks/use-payment-plan-analytics";
|
||||
import { AppConstants } from "@/core/app_constants";
|
||||
import type { PayChannel } from "@/data/schemas/payment";
|
||||
import type { SubscriptionReturnTo } from "@/lib/navigation/subscription_exit";
|
||||
import { DEFAULT_CHARACTER_SLUG } from "@/data/constants/character";
|
||||
@@ -22,14 +21,18 @@ import { useUserState } from "@/stores/user/user-context";
|
||||
import {
|
||||
SubscriptionCheckoutButton,
|
||||
SubscriptionCoinsOfferSection,
|
||||
SubscriptionPaymentIssueDialog,
|
||||
SubscriptionPaymentSuccessDialog,
|
||||
SubscriptionRenewalConfirmationDialog,
|
||||
SubscriptionVipOfferSection,
|
||||
} from "./components";
|
||||
import styles from "./components/subscription-screen.module.css";
|
||||
import {
|
||||
canCheckoutSubscriptionPlan,
|
||||
findSelectedSubscriptionPlan,
|
||||
getFirstRechargeOfferView,
|
||||
getDefaultSubscriptionPlanId,
|
||||
requiresVipPlanConfirmation,
|
||||
toCoinsOfferPlanViews,
|
||||
toVipOfferPlanViews,
|
||||
type SubscriptionType,
|
||||
@@ -63,6 +66,14 @@ export function SubscriptionScreen({
|
||||
resumeOrderId = null,
|
||||
chatActionId = null,
|
||||
}: SubscriptionScreenProps) {
|
||||
const [confirmedVipPlanIds, setConfirmedVipPlanIds] = useState<
|
||||
ReadonlySet<string>
|
||||
>(() => new Set());
|
||||
const [pendingVipPlanId, setPendingVipPlanId] = useState<string | null>(null);
|
||||
const [showPaymentIssueDialog, setShowPaymentIssueDialog] = useState(false);
|
||||
const [paymentIssueNotice, setPaymentIssueNotice] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const userState = useUserState();
|
||||
const hasHydrated = useHasHydrated();
|
||||
const countryCode = userState.currentUser?.countryCode;
|
||||
@@ -139,15 +150,48 @@ export function SubscriptionScreen({
|
||||
const isPaymentBusy =
|
||||
payment.isCreatingOrder ||
|
||||
payment.isPollingOrder;
|
||||
const selectedPlanIsVip = vipOfferPlans.some(
|
||||
(plan) => plan.id === payment.selectedPlanId,
|
||||
);
|
||||
const canActivate =
|
||||
selectedPlan !== null && payment.agreed && !isPaymentBusy;
|
||||
selectedPlan !== null &&
|
||||
canCheckoutSubscriptionPlan({
|
||||
selectedPlanId: payment.selectedPlanId,
|
||||
vipPlans: vipOfferPlans,
|
||||
confirmedVipPlanIds,
|
||||
}) &&
|
||||
!isPaymentBusy;
|
||||
const pendingVipPlan =
|
||||
vipOfferPlans.find((plan) => plan.id === pendingVipPlanId) ?? null;
|
||||
|
||||
const handleSelectPlan = (planId: string) => {
|
||||
const plan = payment.plans.find((item) => item.planId === planId);
|
||||
if (plan) behaviorAnalytics.planClick(plan, analyticsContext);
|
||||
if (
|
||||
requiresVipPlanConfirmation({
|
||||
planId,
|
||||
vipPlans: vipOfferPlans,
|
||||
confirmedVipPlanIds,
|
||||
})
|
||||
) {
|
||||
setPendingVipPlanId(planId);
|
||||
return;
|
||||
}
|
||||
paymentDispatch({ type: "PaymentPlanSelected", planId });
|
||||
};
|
||||
|
||||
const handleConfirmVipPlan = () => {
|
||||
if (!pendingVipPlanId) return;
|
||||
const planId = pendingVipPlanId;
|
||||
setConfirmedVipPlanIds((current) => {
|
||||
const next = new Set(current);
|
||||
next.add(planId);
|
||||
return next;
|
||||
});
|
||||
paymentDispatch({ type: "PaymentPlanSelected", planId });
|
||||
setPendingVipPlanId(null);
|
||||
};
|
||||
|
||||
const handlePaymentMethodChange = (payChannel: PayChannel) => {
|
||||
paymentDispatch({
|
||||
type: "PaymentPayChannelChanged",
|
||||
@@ -200,8 +244,24 @@ export function SubscriptionScreen({
|
||||
variant="soft"
|
||||
analyticsKey="subscription.back"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.paymentIssueButton}
|
||||
onClick={() => {
|
||||
setPaymentIssueNotice(null);
|
||||
setShowPaymentIssueDialog(true);
|
||||
}}
|
||||
>
|
||||
Payment issue?
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{paymentIssueNotice ? (
|
||||
<p className={styles.paymentIssueNotice} role="status">
|
||||
{paymentIssueNotice}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{firstRechargeOffer ? (
|
||||
<section
|
||||
className={styles.firstRechargeBanner}
|
||||
@@ -280,39 +340,32 @@ export function SubscriptionScreen({
|
||||
/>
|
||||
</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.membershipAgreementUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.agreementLink}
|
||||
>
|
||||
VIP Membership Benefits Agreement
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a
|
||||
href={AppConstants.autoRenewalAgreementUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.agreementLink}
|
||||
>
|
||||
Automatic renewal Agreement
|
||||
</a>
|
||||
</span>
|
||||
<SubscriptionRenewalConfirmationDialog
|
||||
open={pendingVipPlan !== null}
|
||||
plan={pendingVipPlan}
|
||||
onCancel={() => setPendingVipPlanId(null)}
|
||||
onConfirm={handleConfirmVipPlan}
|
||||
/>
|
||||
|
||||
{showPaymentIssueDialog ? (
|
||||
<SubscriptionPaymentIssueDialog
|
||||
open
|
||||
subscriptionType={
|
||||
selectedPlan
|
||||
? selectedPlanIsVip
|
||||
? "vip"
|
||||
: "topup"
|
||||
: subscriptionType
|
||||
}
|
||||
planId={selectedPlan?.id ?? null}
|
||||
orderId={payment.currentOrderId}
|
||||
payChannel={payment.payChannel}
|
||||
countryCode={countryCode}
|
||||
characterId={sourceCharacterSlug}
|
||||
onClose={() => setShowPaymentIssueDialog(false)}
|
||||
onSubmitted={setPaymentIssueNotice}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<SubscriptionPaymentSuccessDialog
|
||||
open={showPaymentSuccessDialog}
|
||||
|
||||
Reference in New Issue
Block a user