diff --git a/src/components/PromoOffersSection.tsx b/src/components/PromoOffersSection.tsx index f47622f..ebba2ef 100644 --- a/src/components/PromoOffersSection.tsx +++ b/src/components/PromoOffersSection.tsx @@ -1,11 +1,10 @@ -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { createPortal } from 'react-dom'; import { promoApi, PromoOffer } from '../api/promo'; import { ClockIcon, CheckIcon } from './icons'; -import { useTelegramSDK } from '../hooks/useTelegramSDK'; +import { useDestructiveConfirm } from '@/platform/hooks/useNativeDialog'; // Helper functions const formatTimeLeft = ( @@ -81,140 +80,6 @@ const XCircleIcon = () => ( ); -const WarningIcon = () => ( - - - -); - -// ------- Confirmation Modal ------- - -interface DeactivateConfirmModalProps { - isOpen: boolean; - discountPercent: number; - isDeactivating: boolean; - onConfirm: () => void; - onCancel: () => void; -} - -function DeactivateConfirmModal({ - isOpen, - discountPercent, - isDeactivating, - onConfirm, - onCancel, -}: DeactivateConfirmModalProps) { - const { t } = useTranslation(); - - // Escape key to close - useEffect(() => { - if (!isOpen) return; - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape' && !isDeactivating) { - e.preventDefault(); - onCancel(); - } - }; - - document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('keydown', handleKeyDown); - }, [isOpen, isDeactivating, onCancel]); - - // Scroll lock - useEffect(() => { - if (!isOpen) return; - - document.body.style.overflow = 'hidden'; - return () => { - document.body.style.overflow = ''; - }; - }, [isOpen]); - - if (!isOpen) return null; - - const modalContent = ( -
- {/* Backdrop */} -
- - {/* Modal */} -
e.stopPropagation()} - > - {/* Warning header */} -
-
- -
-

- {t('promo.deactivate.confirmTitle')} -

-

- {t('promo.deactivate.confirmDescription', { percent: discountPercent })} -

-
- - {/* Warning text */} -
-
-

{t('promo.deactivate.warning')}

-
-
- - {/* Action buttons */} -
- - -
-
-
- ); - - if (typeof document !== 'undefined') { - return createPortal(modalContent, document.body); - } - return modalContent; -} - // ------- Main Component ------- interface PromoOffersSectionProps { @@ -225,11 +90,10 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio const { t } = useTranslation(); const navigate = useNavigate(); const queryClient = useQueryClient(); - const { isTelegramWebApp } = useTelegramSDK(); + const destructiveConfirm = useDestructiveConfirm(); const [claimingId, setClaimingId] = useState(null); const [successMessage, setSuccessMessage] = useState(null); const [errorMessage, setErrorMessage] = useState(null); - const [showConfirmModal, setShowConfirmModal] = useState(false); // Fetch available offers const { data: offers = [], isLoading: offersLoading } = useQuery({ @@ -272,14 +136,14 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio queryClient.invalidateQueries({ queryKey: ['promo-offers'] }); queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); - setShowConfirmModal(false); + setSuccessMessage(t('promo.deactivate.success')); setTimeout(() => setSuccessMessage(null), 5000); }, onError: (error: unknown) => { const axiosErr = error as { response?: { data?: { detail?: string } } }; setErrorMessage(axiosErr.response?.data?.detail || t('promo.deactivate.error')); - setShowConfirmModal(false); + setTimeout(() => setErrorMessage(null), 5000); }, }); @@ -295,42 +159,20 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio navigate('/subscription', { state: { scrollToExtend: true } }); }; - const handleDeactivateClick = () => { + const handleDeactivateClick = async () => { setErrorMessage(null); setSuccessMessage(null); - // Use Telegram native popup in Mini App - if (isTelegramWebApp && window.Telegram?.WebApp?.showPopup) { - window.Telegram.WebApp.showPopup( - { - title: t('promo.deactivate.confirmTitle'), - message: t('promo.deactivate.confirmDescription', { - percent: activeDiscount?.discount_percent || 0, - }), - buttons: [ - { id: 'cancel', type: 'cancel' }, - { id: 'confirm', type: 'destructive', text: t('promo.deactivate.confirm') }, - ], - }, - (button_id: string) => { - if (button_id === 'confirm') { - deactivateMutation.mutate(); - } - }, - ); - } else { - // Fallback to modal for web - setShowConfirmModal(true); - } - }; + const confirmed = await destructiveConfirm( + t('promo.deactivate.confirmDescription', { + percent: activeDiscount?.discount_percent || 0, + }), + t('promo.deactivate.confirm'), + t('promo.deactivate.confirmTitle'), + ); - const handleConfirmDeactivate = () => { - deactivateMutation.mutate(); - }; - - const handleCloseConfirmModal = () => { - if (!deactivateMutation.isPending) { - setShowConfirmModal(false); + if (confirmed) { + deactivateMutation.mutate(); } }; @@ -485,17 +327,6 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio
)} - - {/* Deactivation Confirmation Modal */} - {activeDiscount && ( - - )} ); } diff --git a/src/platform/adapters/TelegramAdapter.ts b/src/platform/adapters/TelegramAdapter.ts index 9dc3e7e..c6f36ba 100644 --- a/src/platform/adapters/TelegramAdapter.ts +++ b/src/platform/adapters/TelegramAdapter.ts @@ -183,58 +183,69 @@ function createHapticController(): HapticController { function createDialogController(): DialogController { const tg = getTelegram(); const inTelegram = isInTelegramWebApp(); + let popupOpen = false; + + function showPopupSafe( + params: Parameters>[0], + mapResult: (buttonId: string) => T, + fallback: () => T, + ): Promise { + if (!inTelegram || !tg?.showPopup) { + return Promise.resolve(fallback()); + } + + if (popupOpen) { + return Promise.resolve(mapResult('')); + } + + return new Promise((resolve) => { + popupOpen = true; + tg.showPopup(params, (buttonId) => { + popupOpen = false; + resolve(mapResult(buttonId)); + }); + }); + } return { alert(message: string, _title?: string): Promise { - return new Promise((resolve) => { - if (inTelegram && tg?.showPopup) { - tg.showPopup({ message, buttons: [{ type: 'ok' }] }, () => resolve()); - } else { + return showPopupSafe( + { message, buttons: [{ type: 'ok' }] }, + () => undefined, + () => { window.alert(message); - resolve(); - } - }); + }, + ); }, confirm(message: string, _title?: string): Promise { - return new Promise((resolve) => { - if (inTelegram && tg?.showPopup) { - tg.showPopup( - { - message, - buttons: [ - { id: 'ok', type: 'ok' }, - { id: 'cancel', type: 'cancel' }, - ], - }, - (buttonId) => resolve(buttonId === 'ok'), - ); - } else { - resolve(window.confirm(message)); - } - }); + return showPopupSafe( + { + message, + buttons: [ + { id: 'ok', type: 'ok' }, + { id: 'cancel', type: 'cancel' }, + ], + }, + (buttonId) => buttonId === 'ok', + () => window.confirm(message), + ); }, popup(options: PopupOptions): Promise { - return new Promise((resolve) => { - if (inTelegram && tg?.showPopup) { - tg.showPopup( - { - title: options.title, - message: options.message, - buttons: options.buttons?.map((btn) => ({ - id: btn.id, - type: btn.type, - text: btn.text, - })), - }, - (buttonId) => resolve(buttonId ?? null), - ); - } else { - const confirmed = window.confirm(options.message); - resolve(confirmed ? 'ok' : null); - } - }); + return showPopupSafe( + { + title: options.title, + message: options.message, + buttons: options.buttons?.map((btn) => ({ + id: btn.id, + type: btn.type, + text: btn.text, + })), + }, + (buttonId) => buttonId || null, + () => (window.confirm(options.message) ? 'ok' : null), + ); }, }; }