@@ -34,6 +34,7 @@ export type ResponseAllInfo = { } payment_plan: { uid: string + is_recurring: boolean plan: { uid: string price: string @@ -76,6 +77,7 @@ const initialState: UserState & ResponseAllInfo = { account_type: 'regular', payment_plan: { uid: '', + is_recurring: false, plan: { uid: '', price: '', @@ -3,3 +3,4 @@ export const RESEND_INVATION_PASSWORD = 'resend-invation-password' export const ERROR_REPORT = 'error-report' export const BUSINESS_ERROR_REPORT = 'business-error-report' export const LOW_BALANCE_OFFER = 'low-balance-offer' +export const SUBSCRIPTION_CHANGE_NOTIFICATION = 'subscription-change-notification' @@ -0,0 +1,44 @@ +.container { + width: 100%; + max-width: 920px; + padding: 32px 0 12px 0px; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.text_heading { + font-weight: 800; + font-size: 56px; + margin-bottom: 16px; +} + +.text { + color: #ffffff; + font-size: 18px; + font-weight: 500; + margin: 0 0 24px; +} + +.highlight { + color: #7F7DF3; +} + +.buttons { + display: flex; + justify-content: flex-end; + flex-direction: row; + align-items: flex-end; + margin-top: 12px; + gap: 8px; +} + +.buttonSecondary { + background-color: #2A2B30; + color: #ffffff; + border: none; + border-radius: 12px; + padding: 15px 25px; + cursor: pointer; + text-transform: none; +} @@ -0,0 +1,50 @@ +import React from 'react' + +import { getModalById, PlateTemplate, SUBSCRIPTION_CHANGE_NOTIFICATION } from '#/features/modals' +import { c } from '#/shared' +import { CommonButton } from '#/shared/ui/button' + +import styles from './subscription-change-notification-plate.module.scss' + +const formatTokens = (num: number) => num.toLocaleString('ru-RU') + +export const SubscriptionChangeNotificationPlate = () => { + const modal = getModalById(SUBSCRIPTION_CHANGE_NOTIFICATION) + + const fromTokens = modal.getStoreProperty('fromTokens') ?? 0 + const toTokens = modal.getStoreProperty('toTokens') ?? 0 + + const handleClose = () => { + modal.setState(false) + } + + const handleConfirm = () => { + modal.getStoreProperty<() => void>('onConfirm')?.() + modal.setState(false) + } + + return ( + +
e.stopPropagation()}> +

Смена подписки

+ +

+ Внимание! Вы собираетесь перейти с подписки{' '} + {formatTokens(fromTokens)} токенов на подписку{' '} + {formatTokens(toTokens)} токенов. +
+ Уведомляем Вас, что при смене подписки сгорают все неиспользованные в прежней подписке токены. Продолжить? +

+ +
+ + Подтвердить смену тарифа + + + Отмена + +
+
+
+ ) +} @@ -0,0 +1,5 @@ +import { SubscriptionChangeNotificationPlate } from './subscription-change-notification-plate' + +export const SubscriptionChangeNotificationTrigger = () => { + return +} @@ -0,0 +1,2 @@ +export { SubscriptionChangeNotificationPlate } from './ui/subscription-change-notification-plate' +export { SubscriptionChangeNotificationTrigger } from './ui/subscription-change-notification-trigger' @@ -13,7 +13,8 @@ import { FlagProvider } from '@unleash/proxy-client-react' import { store } from '#/app/store/store' import { Error } from '#/shared' -import { LowBalanceOfferTrigger, LowBalanceOfferOnStart } from '#/features/low-balance-offer' +import { LowBalanceOfferTrigger } from '#/features/low-balance-offer' +import { SubscriptionChangeNotificationTrigger } from '#/features/subscription-change-notification' import { TourManager, getTourSteps, TourCard, setTourCompleted } from '#/features/nextstep-tour' import { pingFangFont } from '#/shared/lib/constants/font/font' import { getDeviceType } from '#/shared/lib/helpers' @@ -92,6 +93,7 @@ function AppContent({ Component, pageProps }: { Component: NextPageWithLayout; p {Component.getLayout ? Component.getLayout() : } + @@ -3,7 +3,10 @@ import { Box } from '@mui/material' import Router from 'next/router' import { useSession } from 'next-auth/react' +import { useAppSelector } from '#/app/store/store' +import { balanceSelector } from '#/shared/lib/selectors' import LightningSvg from '#/assets/svg/lightning.svg?react' +import { getModalById, SUBSCRIPTION_CHANGE_NOTIFICATION } from '#/features/modals' import { accountApi } from '#/shared/api/account-endpoints' import { commaSeparated } from '#/shared/lib/helpers' import { IFeatures, IOffer, formatFeatureValue } from '#/views/subscription' @@ -14,10 +17,13 @@ interface IOfferProps extends IOffer { device: 'desktop' | 'mobile' isCurrentSubscription?: boolean index?: number + currentPlanTokens?: number } -const Offer: React.FC = ({ uid, tokens_per_plan, price, grouped_features, isCurrentSubscription, index }) => { +const Offer: React.FC = ({ uid, tokens_per_plan, price, grouped_features, isCurrentSubscription, index, currentPlanTokens = 0 }) => { const { data } = useSession() + const isRecurring = useAppSelector((state) => state.user.payment_plan?.is_recurring) ?? false + const balance = useAppSelector(balanceSelector) const unitMap: Record = { Изображения: 'изображений', @@ -51,11 +57,61 @@ const Offer: React.FC = ({ uid, tokens_per_plan, price, grouped_fea const formattedFeatures = formatFeatures(grouped_features) - const pay = async (uid: string) => { - const urlForPay = await accountApi.payProduct(data!.access, uid) + const modal = getModalById(SUBSCRIPTION_CHANGE_NOTIFICATION) + + const pay = async (planUid: string) => { + const urlForPay = await accountApi.payProduct(data!.access, planUid) urlForPay ? await Router.push(urlForPay) : null } + const isFreePlan = currentPlanTokens <= 10 + + const toTokens = Number(tokens_per_plan) + const isUpgrade = currentPlanTokens < toTokens + const isDowngrade = currentPlanTokens > toTokens + const balanceExceedsLimit = balance > currentPlanTokens + + const handlePayClick = () => { + + // Бесплатный план: не показываем модалку + if (isFreePlan) { + pay(uid) + return + } + + // Ежемесячный план: показываем модалку с уведомлением о том, что неиспользованные токены сгорят + if (!isRecurring) { + modal.setState(true, { + fromTokens: currentPlanTokens, + toTokens, + onConfirm: () => pay(uid), + }) + return + } + + // Апгрейд: модалка только если баланс превышает лимит текущего плана (токены сгорят) + if (isUpgrade && balanceExceedsLimit) { + modal.setState(true, { + fromTokens: currentPlanTokens, + toTokens, + onConfirm: () => pay(uid), + }) + return + } + + // Даунгрейд: модалка всегда (неиспользованные токены сгорят) + if (isDowngrade) { + modal.setState(true, { + fromTokens: currentPlanTokens, + toTokens, + onConfirm: () => pay(uid), + }) + return + } + + pay(uid) + } + return ( @@ -81,7 +137,7 @@ const Offer: React.FC = ({ uid, tokens_per_plan, price, grouped_fea className={`${styles.tarif_btn} ${isCurrentSubscription ? styles.tarif_btn_current : ''}`} onClick={(e) => { e.stopPropagation() - pay(uid) + handlePayClick() }} > Оплатить @@ -8,12 +8,14 @@ import styles from './subscription.module.scss' interface PlansSectionProps { offers: IOffer[] | null - currentOfferUID:string + currentOfferUID: string + currentPlanTokens: number } export const PlansSection = ({ offers, - currentOfferUID + currentOfferUID, + currentPlanTokens, }: PlansSectionProps) => { return ( @@ -27,6 +29,7 @@ export const PlansSection = ({ isCurrentSubscription={false} index={idx} key={offer.uid} + currentPlanTokens={currentPlanTokens} /> ) })} @@ -65,7 +65,7 @@ const SubscriptionPage: NextPageWithLayout = () => { - +