From 58e93cd2b72979ec95dd43ba7d6670d879e2f07d Mon Sep 17 00:00:00 2001 From: Fringg Date: Fri, 6 Mar 2026 23:53:31 +0300 Subject: [PATCH] feat: add payment sub-option selection on quick purchase page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show sub-options (e.g. СБП, Карта) as selectable buttons when a payment method has multiple enabled variants. Send suffixed payment_method ID to backend for correct provider routing. Split LandingPaymentMethod into public and admin types. --- src/api/landings.ts | 24 +++- .../admin/SortableSelectedMethodCard.tsx | 4 +- src/pages/QuickPurchase.tsx | 121 +++++++++++++----- 3 files changed, 113 insertions(+), 36 deletions(-) diff --git a/src/api/landings.ts b/src/api/landings.ts index 27fd3ae..a9739bf 100644 --- a/src/api/landings.ts +++ b/src/api/landings.ts @@ -27,7 +27,27 @@ export interface LandingTariff { periods: LandingTariffPeriod[]; } +export interface LandingPaymentMethodSubOption { + id: string; + name: string; +} + +/** Payment method as returned by the public landing config API */ export interface LandingPaymentMethod { + method_id: string; + display_name: string; + description: string | null; + icon_url: string | null; + sort_order: number; + min_amount_kopeks: number | null; + max_amount_kopeks: number | null; + currency: string | null; + return_url: string | null; + sub_options: LandingPaymentMethodSubOption[] | null; +} + +/** Payment method as stored/returned by the admin landing API (sub_options is a dict) */ +export interface AdminLandingPaymentMethod { method_id: string; display_name: string; description: string | null; @@ -158,7 +178,7 @@ export interface LandingDetail { footer_text: LocaleDict | null; allowed_tariff_ids: number[]; allowed_periods: Record; - payment_methods: LandingPaymentMethod[]; + payment_methods: AdminLandingPaymentMethod[]; gift_enabled: boolean; custom_css: string | null; meta_title: LocaleDict | null; @@ -177,7 +197,7 @@ export interface LandingCreateRequest { footer_text?: LocaleDict; allowed_tariff_ids?: number[]; allowed_periods?: Record; - payment_methods?: LandingPaymentMethod[]; + payment_methods?: AdminLandingPaymentMethod[]; gift_enabled?: boolean; custom_css?: string; meta_title?: LocaleDict; diff --git a/src/components/admin/SortableSelectedMethodCard.tsx b/src/components/admin/SortableSelectedMethodCard.tsx index e86ed6d..e8063d8 100644 --- a/src/components/admin/SortableSelectedMethodCard.tsx +++ b/src/components/admin/SortableSelectedMethodCard.tsx @@ -4,10 +4,10 @@ import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { cn } from '../../lib/utils'; import { GripIcon, TrashIcon } from '../icons/LandingIcons'; -import type { LandingPaymentMethod, EditableMethodField } from '../../api/landings'; +import type { AdminLandingPaymentMethod, EditableMethodField } from '../../api/landings'; import type { PaymentMethodSubOptionInfo } from '../../types'; -export type MethodWithId = LandingPaymentMethod & { _id: string }; +export type MethodWithId = AdminLandingPaymentMethod & { _id: string }; const ChevronDownIcon = ({ open }: { open: boolean }) => ( void; + onSelectSubOption: (subOptionId: string) => void; }) { + const hasSubOptions = method.sub_options && method.sub_options.length > 1; + return ( -
- + + + {/* Sub-options */} + {isSelected && hasSubOptions && ( +
+
+ {method.sub_options!.map((opt) => ( + + ))} +
)} - - {/* Text */} -
-

{method.display_name}

- {method.description && ( -

{method.description}

- )} -
- - {/* Radio */} -
- {isSelected &&
} -
- +
); } @@ -547,6 +580,7 @@ export default function QuickPurchase() { const [giftRecipient, setGiftRecipient] = useState(''); const [giftMessage, setGiftMessage] = useState(''); const [selectedMethod, setSelectedMethod] = useState(null); + const [selectedSubOption, setSelectedSubOption] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [submitError, setSubmitError] = useState(null); const redirectTimeoutRef = useRef>(undefined); @@ -595,7 +629,11 @@ export default function QuickPurchase() { } if (config.payment_methods.length > 0 && selectedMethod === null) { - setSelectedMethod(config.payment_methods[0].method_id); + const firstMethod = config.payment_methods[0]; + setSelectedMethod(firstMethod.method_id); + if (firstMethod.sub_options && firstMethod.sub_options.length > 1) { + setSelectedSubOption(firstMethod.sub_options[0].id); + } } }, [config, allPeriods, visibleTariffs, selectedTariffId, selectedPeriodDays, selectedMethod]); @@ -709,12 +747,19 @@ export default function QuickPurchase() { setIsSubmitting(true); setSubmitError(null); + // Build the payment_method string: append sub-option suffix if selected + // e.g. "platega" + "2" → "platega_2", "yookassa" + "sbp" → "yookassa_sbp" + let paymentMethod = selectedMethod!; + if (selectedSubOption) { + paymentMethod = `${paymentMethod}_${selectedSubOption}`; + } + const data: PurchaseRequest = { tariff_id: selectedTariffId!, period_days: selectedPeriodDays!, contact_type: detectContactType(contactValue), contact_value: contactValue.trim(), - payment_method: selectedMethod!, + payment_method: paymentMethod, is_gift: isGift, }; @@ -849,7 +894,19 @@ export default function QuickPurchase() { key={method.method_id} method={method} isSelected={method.method_id === selectedMethod} - onSelect={() => setSelectedMethod(method.method_id)} + selectedSubOption={ + method.method_id === selectedMethod ? selectedSubOption : null + } + onSelect={() => { + setSelectedMethod(method.method_id); + // Auto-select first sub-option when switching methods + if (method.sub_options && method.sub_options.length > 1) { + setSelectedSubOption(method.sub_options[0].id); + } else { + setSelectedSubOption(null); + } + }} + onSelectSubOption={setSelectedSubOption} /> ))}