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),
+ );
},
};
}