mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat(sbp-recurring): СБП-оформление подписки на форме покупки тарифа
Вторая CTA «Оформить с автооплатой СБП» под кнопкой покупки с баланса (и в daily-ветке) — показывается по platega_recurrent_enabled из purchase-options. POST /platega-recurrent/purchase → редирект в банк (первое списание = подтверждение привязки), инвалидация подписочных и sbp-запросов, переход в /subscriptions. Локали en/ru/zh/fa.
This commit is contained in:
@@ -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<TrialInfo> => {
|
||||
|
||||
@@ -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<HTMLDivElement>(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 && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => sbpPurchaseMutation.mutate()}
|
||||
disabled={sbpPurchaseMutation.isPending || purchaseMutation.isPending}
|
||||
className="mt-2 w-full rounded-xl border border-accent-500/40 bg-accent-500/10 py-3 text-sm font-medium text-accent-400 transition-colors hover:bg-accent-500/20 disabled:opacity-50"
|
||||
>
|
||||
{sbpPurchaseMutation.isPending ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
{t('common.loading')}
|
||||
</span>
|
||||
) : (
|
||||
t('subscription.sbpRecurring.purchaseButton')
|
||||
)}
|
||||
</button>
|
||||
<div className="mt-1.5 text-center text-[11px] text-dark-500">
|
||||
{t('subscription.sbpRecurring.purchaseHint')}
|
||||
</div>
|
||||
{sbpPurchaseMutation.isError && (
|
||||
<div className="mt-2 text-center text-sm text-error-400">
|
||||
{getErrorMessage(sbpPurchaseMutation.error)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
// Smooth scroll the form into view when first mounted.
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
@@ -190,6 +239,8 @@ export function TariffPurchaseForm({
|
||||
)}
|
||||
</button>
|
||||
|
||||
{sbpPurchaseButton}
|
||||
|
||||
{purchaseMutation.isError &&
|
||||
!getInsufficientBalanceError(purchaseMutation.error) && (
|
||||
<div className="mt-3 text-center text-sm text-error-400">
|
||||
@@ -613,6 +664,8 @@ export function TariffPurchaseForm({
|
||||
t('subscription.purchase')
|
||||
)}
|
||||
</button>
|
||||
|
||||
{sbpPurchaseButton}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -596,7 +596,9 @@
|
||||
"month": "ماهانه",
|
||||
"year": "سالانه"
|
||||
},
|
||||
"autopayHint": "اشتراک شما بهصورت خودکار از طریق SBP بدون نیاز به کارت ذخیرهشده شارژ میشود"
|
||||
"autopayHint": "اشتراک شما بهصورت خودکار از طریق SBP بدون نیاز به کارت ذخیرهشده شارژ میشود",
|
||||
"purchaseButton": "⚡ اشتراک با پرداخت خودکار SBP",
|
||||
"purchaseHint": "اولین برداشت، اتصال را در اپ بانک تأیید میکند؛ تمدیدها بهصورت خودکار برداشت میشوند."
|
||||
},
|
||||
"backToList": "بازگشت به فهرست اشتراکها",
|
||||
"confirmDelete": "اشتراک حذف شود؟",
|
||||
|
||||
@@ -716,7 +716,9 @@
|
||||
"month": "ежемесячно",
|
||||
"year": "ежегодно"
|
||||
},
|
||||
"autopayHint": "Подписка будет автоматически продлеваться через СБП без сохранённой карты"
|
||||
"autopayHint": "Подписка будет автоматически продлеваться через СБП без сохранённой карты",
|
||||
"purchaseButton": "⚡ Оформить с автооплатой СБП",
|
||||
"purchaseHint": "Первое списание подтверждает привязку в банке; продления списываются автоматически по каденсу тарифа."
|
||||
},
|
||||
"backToList": "К списку подписок",
|
||||
"confirmDelete": "Удалить подписку?",
|
||||
|
||||
@@ -596,7 +596,9 @@
|
||||
"month": "每月",
|
||||
"year": "每年"
|
||||
},
|
||||
"autopayHint": "您的订阅将通过 SBP 自动扣款,无需保存银行卡"
|
||||
"autopayHint": "您的订阅将通过 SBP 自动扣款,无需保存银行卡",
|
||||
"purchaseButton": "⚡ 通过SBP自动扣款订阅",
|
||||
"purchaseHint": "首次扣款即在银行应用中确认绑定;续费将按资费周期自动扣款。"
|
||||
},
|
||||
"backToList": "返回订阅列表",
|
||||
"confirmDelete": "删除订阅?",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user