From f448c149946cefee83e0e61f9c3c03b0d69a4362 Mon Sep 17 00:00:00 2001 From: Fringg Date: Fri, 24 Jul 2026 19:07:23 +0300 Subject: [PATCH] =?UTF-8?q?feat(sbp-recurring):=20=D0=A1=D0=91=D0=9F-?= =?UTF-8?q?=D0=BE=D1=84=D0=BE=D1=80=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=BA=D0=B8=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D1=84=D0=BE=D1=80=D0=BC=D0=B5=20=D0=BF=D0=BE=D0=BA=D1=83?= =?UTF-8?q?=D0=BF=D0=BA=D0=B8=20=D1=82=D0=B0=D1=80=D0=B8=D1=84=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вторая CTA «Оформить с автооплатой СБП» под кнопкой покупки с баланса (и в daily-ветке) — показывается по platega_recurrent_enabled из purchase-options. POST /platega-recurrent/purchase → редирект в банк (первое списание = подтверждение привязки), инвалидация подписочных и sbp-запросов, переход в /subscriptions. Локали en/ru/zh/fa. --- src/api/subscription.ts | 16 ++++++ .../purchase/TariffPurchaseForm.tsx | 53 +++++++++++++++++++ src/locales/en.json | 4 +- src/locales/fa.json | 4 +- src/locales/ru.json | 4 +- src/locales/zh.json | 4 +- src/pages/SubscriptionPurchase.tsx | 6 +++ src/types/index.ts | 3 ++ 8 files changed, 90 insertions(+), 4 deletions(-) diff --git a/src/api/subscription.ts b/src/api/subscription.ts index b15d44d..4572a0e 100644 --- a/src/api/subscription.ts +++ b/src/api/subscription.ts @@ -378,6 +378,22 @@ export const subscriptionApi = { return response.data; }, + /** + * Оформление подписки на тариф через СБП-автопродление: первое списание = + * подтверждение привязки в банке. Для нового тарифа бэкенд создаёт + * неактивную заготовку, которую активирует первый чардж. + */ + purchaseWithSbpRecurring: async ( + tariffId: number, + ): Promise<{ status: string; redirect_url: string | null; subscription_id: number }> => { + const response = await apiClient.post( + '/cabinet/subscription/platega-recurrent/purchase', + {}, + { params: { tariff_id: tariffId } }, + ); + return response.data; + }, + // ── Trial ─────────────────────────────────────────────────────────── getTrialInfo: async (): Promise => { diff --git a/src/components/subscription/purchase/TariffPurchaseForm.tsx b/src/components/subscription/purchase/TariffPurchaseForm.tsx index d67087a..d49a918 100644 --- a/src/components/subscription/purchase/TariffPurchaseForm.tsx +++ b/src/components/subscription/purchase/TariffPurchaseForm.tsx @@ -6,6 +6,8 @@ import { subscriptionApi } from '../../../api/subscription'; import { getErrorMessage, getInsufficientBalanceError } from '../../../utils/subscriptionHelpers'; import { useCurrency } from '../../../hooks/useCurrency'; import { usePromoDiscount } from '../../../hooks/usePromoDiscount'; +import { usePlatform } from '../../../platform'; +import { openPaymentUrl } from '../../../utils/openPaymentUrl'; import InsufficientBalancePrompt from '../../InsufficientBalancePrompt'; import type { Tariff, TariffPeriod } from '../../../types'; @@ -31,6 +33,8 @@ export interface TariffPurchaseFormProps { tariff: Tariff; subscriptionId: number | undefined; balanceKopeks: number | undefined; + /** СБП-оформление (Platega recurrent) доступно — показать вторую CTA. */ + sbpPurchaseEnabled?: boolean; onBack: () => void; } @@ -38,6 +42,7 @@ export function TariffPurchaseForm({ tariff, subscriptionId, balanceKopeks, + sbpPurchaseEnabled = false, onBack, }: TariffPurchaseFormProps) { const { t } = useTranslation(); @@ -45,6 +50,7 @@ export function TariffPurchaseForm({ const queryClient = useQueryClient(); const { formatAmount, currencySymbol } = useCurrency(); const { applyPromoDiscount } = usePromoDiscount(); + const { openLink, platform } = usePlatform(); const ref = useRef(null); const formatPrice = (kopeks: number) => @@ -93,6 +99,49 @@ export function TariffPurchaseForm({ }, }); + // СБП-оформление: первое списание = подтверждение привязки в банке; период + // на форме не участвует — списания идут по каденс-правилу тарифа. + const sbpPurchaseMutation = useMutation({ + mutationFn: () => subscriptionApi.purchaseWithSbpRecurring(tariff.id), + onSuccess: (data) => { + if (data.redirect_url) { + openPaymentUrl(data.redirect_url, platform, openLink); + } + queryClient.invalidateQueries({ queryKey: ['subscription'] }); + queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); + queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] }); + queryClient.invalidateQueries({ queryKey: ['sbp-recurring', data.subscription_id] }); + navigate('/subscriptions', { replace: true }); + }, + }); + + const sbpPurchaseButton = sbpPurchaseEnabled && ( + <> + +
+ {t('subscription.sbpRecurring.purchaseHint')} +
+ {sbpPurchaseMutation.isError && ( +
+ {getErrorMessage(sbpPurchaseMutation.error)} +
+ )} + + ); + // Smooth scroll the form into view when first mounted. useEffect(() => { if (ref.current) { @@ -190,6 +239,8 @@ export function TariffPurchaseForm({ )} + {sbpPurchaseButton} + {purchaseMutation.isError && !getInsufficientBalanceError(purchaseMutation.error) && (
@@ -613,6 +664,8 @@ export function TariffPurchaseForm({ t('subscription.purchase') )} + + {sbpPurchaseButton} ); })()} diff --git a/src/locales/en.json b/src/locales/en.json index 92b8a93..6824c06 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -697,7 +697,9 @@ "month": "monthly", "year": "yearly" }, - "autopayHint": "Your subscription will be charged automatically via SBP, no card required" + "autopayHint": "Your subscription will be charged automatically via SBP, no card required", + "purchaseButton": "⚡ Subscribe with SBP auto-payment", + "purchaseHint": "First charge confirms the binding in your bank app; renewals are charged automatically per the tariff cadence." }, "backToList": "Back to subscriptions", "confirmDelete": "Delete subscription?", diff --git a/src/locales/fa.json b/src/locales/fa.json index 33372f7..224e3ec 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -596,7 +596,9 @@ "month": "ماهانه", "year": "سالانه" }, - "autopayHint": "اشتراک شما به‌صورت خودکار از طریق SBP بدون نیاز به کارت ذخیره‌شده شارژ می‌شود" + "autopayHint": "اشتراک شما به‌صورت خودکار از طریق SBP بدون نیاز به کارت ذخیره‌شده شارژ می‌شود", + "purchaseButton": "⚡ اشتراک با پرداخت خودکار SBP", + "purchaseHint": "اولین برداشت، اتصال را در اپ بانک تأیید می‌کند؛ تمدیدها به‌صورت خودکار برداشت می‌شوند." }, "backToList": "بازگشت به فهرست اشتراک‌ها", "confirmDelete": "اشتراک حذف شود؟", diff --git a/src/locales/ru.json b/src/locales/ru.json index 038aaa1..1cfef4f 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -716,7 +716,9 @@ "month": "ежемесячно", "year": "ежегодно" }, - "autopayHint": "Подписка будет автоматически продлеваться через СБП без сохранённой карты" + "autopayHint": "Подписка будет автоматически продлеваться через СБП без сохранённой карты", + "purchaseButton": "⚡ Оформить с автооплатой СБП", + "purchaseHint": "Первое списание подтверждает привязку в банке; продления списываются автоматически по каденсу тарифа." }, "backToList": "К списку подписок", "confirmDelete": "Удалить подписку?", diff --git a/src/locales/zh.json b/src/locales/zh.json index c9cfb4a..7ebb9af 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -596,7 +596,9 @@ "month": "每月", "year": "每年" }, - "autopayHint": "您的订阅将通过 SBP 自动扣款,无需保存银行卡" + "autopayHint": "您的订阅将通过 SBP 自动扣款,无需保存银行卡", + "purchaseButton": "⚡ 通过SBP自动扣款订阅", + "purchaseHint": "首次扣款即在银行应用中确认绑定;续费将按资费周期自动扣款。" }, "backToList": "返回订阅列表", "confirmDelete": "删除订阅?", diff --git a/src/pages/SubscriptionPurchase.tsx b/src/pages/SubscriptionPurchase.tsx index d9dc868..1e3f8ce 100644 --- a/src/pages/SubscriptionPurchase.tsx +++ b/src/pages/SubscriptionPurchase.tsx @@ -286,6 +286,12 @@ export default function SubscriptionPurchase() { tariff={selectedTariff} subscriptionId={subscriptionId} balanceKopeks={purchaseOptions?.balance_kopeks} + sbpPurchaseEnabled={ + isTariffsMode && + purchaseOptions !== undefined && + 'platega_recurrent_enabled' in purchaseOptions && + purchaseOptions.platega_recurrent_enabled === true + } onBack={() => { setShowTariffPurchase(false); setSelectedTariff(null); diff --git a/src/types/index.ts b/src/types/index.ts index aa3c1be..4012737 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -370,6 +370,9 @@ export interface TariffsPurchaseOptions { has_subscription?: boolean; // Multi-tariff: all available tariffs already purchased all_tariffs_purchased?: boolean; + // СБП-оформление (Platega recurrent): показывать кнопку «Оформить с + // автооплатой СБП» рядом с покупкой с баланса + platega_recurrent_enabled?: boolean; } export interface ClassicPurchaseOptions {