mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +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;
|
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 ───────────────────────────────────────────────────────────
|
// ── Trial ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
getTrialInfo: async (): Promise<TrialInfo> => {
|
getTrialInfo: async (): Promise<TrialInfo> => {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { subscriptionApi } from '../../../api/subscription';
|
|||||||
import { getErrorMessage, getInsufficientBalanceError } from '../../../utils/subscriptionHelpers';
|
import { getErrorMessage, getInsufficientBalanceError } from '../../../utils/subscriptionHelpers';
|
||||||
import { useCurrency } from '../../../hooks/useCurrency';
|
import { useCurrency } from '../../../hooks/useCurrency';
|
||||||
import { usePromoDiscount } from '../../../hooks/usePromoDiscount';
|
import { usePromoDiscount } from '../../../hooks/usePromoDiscount';
|
||||||
|
import { usePlatform } from '../../../platform';
|
||||||
|
import { openPaymentUrl } from '../../../utils/openPaymentUrl';
|
||||||
import InsufficientBalancePrompt from '../../InsufficientBalancePrompt';
|
import InsufficientBalancePrompt from '../../InsufficientBalancePrompt';
|
||||||
import type { Tariff, TariffPeriod } from '../../../types';
|
import type { Tariff, TariffPeriod } from '../../../types';
|
||||||
|
|
||||||
@@ -31,6 +33,8 @@ export interface TariffPurchaseFormProps {
|
|||||||
tariff: Tariff;
|
tariff: Tariff;
|
||||||
subscriptionId: number | undefined;
|
subscriptionId: number | undefined;
|
||||||
balanceKopeks: number | undefined;
|
balanceKopeks: number | undefined;
|
||||||
|
/** СБП-оформление (Platega recurrent) доступно — показать вторую CTA. */
|
||||||
|
sbpPurchaseEnabled?: boolean;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +42,7 @@ export function TariffPurchaseForm({
|
|||||||
tariff,
|
tariff,
|
||||||
subscriptionId,
|
subscriptionId,
|
||||||
balanceKopeks,
|
balanceKopeks,
|
||||||
|
sbpPurchaseEnabled = false,
|
||||||
onBack,
|
onBack,
|
||||||
}: TariffPurchaseFormProps) {
|
}: TariffPurchaseFormProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -45,6 +50,7 @@ export function TariffPurchaseForm({
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { formatAmount, currencySymbol } = useCurrency();
|
const { formatAmount, currencySymbol } = useCurrency();
|
||||||
const { applyPromoDiscount } = usePromoDiscount();
|
const { applyPromoDiscount } = usePromoDiscount();
|
||||||
|
const { openLink, platform } = usePlatform();
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const formatPrice = (kopeks: number) =>
|
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.
|
// Smooth scroll the form into view when first mounted.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
@@ -190,6 +239,8 @@ export function TariffPurchaseForm({
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{sbpPurchaseButton}
|
||||||
|
|
||||||
{purchaseMutation.isError &&
|
{purchaseMutation.isError &&
|
||||||
!getInsufficientBalanceError(purchaseMutation.error) && (
|
!getInsufficientBalanceError(purchaseMutation.error) && (
|
||||||
<div className="mt-3 text-center text-sm text-error-400">
|
<div className="mt-3 text-center text-sm text-error-400">
|
||||||
@@ -613,6 +664,8 @@ export function TariffPurchaseForm({
|
|||||||
t('subscription.purchase')
|
t('subscription.purchase')
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{sbpPurchaseButton}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|||||||
@@ -697,7 +697,9 @@
|
|||||||
"month": "monthly",
|
"month": "monthly",
|
||||||
"year": "yearly"
|
"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",
|
"backToList": "Back to subscriptions",
|
||||||
"confirmDelete": "Delete subscription?",
|
"confirmDelete": "Delete subscription?",
|
||||||
|
|||||||
@@ -596,7 +596,9 @@
|
|||||||
"month": "ماهانه",
|
"month": "ماهانه",
|
||||||
"year": "سالانه"
|
"year": "سالانه"
|
||||||
},
|
},
|
||||||
"autopayHint": "اشتراک شما بهصورت خودکار از طریق SBP بدون نیاز به کارت ذخیرهشده شارژ میشود"
|
"autopayHint": "اشتراک شما بهصورت خودکار از طریق SBP بدون نیاز به کارت ذخیرهشده شارژ میشود",
|
||||||
|
"purchaseButton": "⚡ اشتراک با پرداخت خودکار SBP",
|
||||||
|
"purchaseHint": "اولین برداشت، اتصال را در اپ بانک تأیید میکند؛ تمدیدها بهصورت خودکار برداشت میشوند."
|
||||||
},
|
},
|
||||||
"backToList": "بازگشت به فهرست اشتراکها",
|
"backToList": "بازگشت به فهرست اشتراکها",
|
||||||
"confirmDelete": "اشتراک حذف شود؟",
|
"confirmDelete": "اشتراک حذف شود؟",
|
||||||
|
|||||||
@@ -716,7 +716,9 @@
|
|||||||
"month": "ежемесячно",
|
"month": "ежемесячно",
|
||||||
"year": "ежегодно"
|
"year": "ежегодно"
|
||||||
},
|
},
|
||||||
"autopayHint": "Подписка будет автоматически продлеваться через СБП без сохранённой карты"
|
"autopayHint": "Подписка будет автоматически продлеваться через СБП без сохранённой карты",
|
||||||
|
"purchaseButton": "⚡ Оформить с автооплатой СБП",
|
||||||
|
"purchaseHint": "Первое списание подтверждает привязку в банке; продления списываются автоматически по каденсу тарифа."
|
||||||
},
|
},
|
||||||
"backToList": "К списку подписок",
|
"backToList": "К списку подписок",
|
||||||
"confirmDelete": "Удалить подписку?",
|
"confirmDelete": "Удалить подписку?",
|
||||||
|
|||||||
@@ -596,7 +596,9 @@
|
|||||||
"month": "每月",
|
"month": "每月",
|
||||||
"year": "每年"
|
"year": "每年"
|
||||||
},
|
},
|
||||||
"autopayHint": "您的订阅将通过 SBP 自动扣款,无需保存银行卡"
|
"autopayHint": "您的订阅将通过 SBP 自动扣款,无需保存银行卡",
|
||||||
|
"purchaseButton": "⚡ 通过SBP自动扣款订阅",
|
||||||
|
"purchaseHint": "首次扣款即在银行应用中确认绑定;续费将按资费周期自动扣款。"
|
||||||
},
|
},
|
||||||
"backToList": "返回订阅列表",
|
"backToList": "返回订阅列表",
|
||||||
"confirmDelete": "删除订阅?",
|
"confirmDelete": "删除订阅?",
|
||||||
|
|||||||
@@ -286,6 +286,12 @@ export default function SubscriptionPurchase() {
|
|||||||
tariff={selectedTariff}
|
tariff={selectedTariff}
|
||||||
subscriptionId={subscriptionId}
|
subscriptionId={subscriptionId}
|
||||||
balanceKopeks={purchaseOptions?.balance_kopeks}
|
balanceKopeks={purchaseOptions?.balance_kopeks}
|
||||||
|
sbpPurchaseEnabled={
|
||||||
|
isTariffsMode &&
|
||||||
|
purchaseOptions !== undefined &&
|
||||||
|
'platega_recurrent_enabled' in purchaseOptions &&
|
||||||
|
purchaseOptions.platega_recurrent_enabled === true
|
||||||
|
}
|
||||||
onBack={() => {
|
onBack={() => {
|
||||||
setShowTariffPurchase(false);
|
setShowTariffPurchase(false);
|
||||||
setSelectedTariff(null);
|
setSelectedTariff(null);
|
||||||
|
|||||||
@@ -370,6 +370,9 @@ export interface TariffsPurchaseOptions {
|
|||||||
has_subscription?: boolean;
|
has_subscription?: boolean;
|
||||||
// Multi-tariff: all available tariffs already purchased
|
// Multi-tariff: all available tariffs already purchased
|
||||||
all_tariffs_purchased?: boolean;
|
all_tariffs_purchased?: boolean;
|
||||||
|
// СБП-оформление (Platega recurrent): показывать кнопку «Оформить с
|
||||||
|
// автооплатой СБП» рядом с покупкой с баланса
|
||||||
|
platega_recurrent_enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClassicPurchaseOptions {
|
export interface ClassicPurchaseOptions {
|
||||||
|
|||||||
Reference in New Issue
Block a user