fix: inline Stars confirmation and unified payment type display

- Replace confirm dialog with inline UI panel to avoid browser popup
  blockers — pay button click is a direct user gesture
- Pre-open browser window synchronously on pay click for fallback
- Unify payment type display: show same styled panel whether one or
  both methods are enabled (single method = full-width button)
- Remove getCostSubtitle text, add payStars translation key (ru, en,
  zh, fa)
This commit is contained in:
c0mrade
2026-02-04 16:53:58 +03:00
parent e6f8ae6ab0
commit 8068f84724
5 changed files with 92 additions and 75 deletions

View File

@@ -714,8 +714,8 @@
"starsPaymentFailed": "Payment failed. Please try again.", "starsPaymentFailed": "Payment failed. Please try again.",
"starsPaymentRedirected": "Telegram will open for payment. Refresh the page after payment.", "starsPaymentRedirected": "Telegram will open for payment. Refresh the page after payment.",
"starsNotAvailable": "Star payments are currently unavailable. Contact support.", "starsNotAvailable": "Star payments are currently unavailable. Contact support.",
"confirmStarsTitle": "Star payment", "confirmStarsPayment": "You will be redirected to Telegram to pay for one wheel spin.",
"confirmStarsPayment": "You will be redirected to Telegram to pay for one wheel spin. Continue?", "payStars": "Pay {{count}} ⭐",
"youWon": "You won", "youWon": "You won",
"noPrize": "No luck this time...", "noPrize": "No luck this time...",
"noHistory": "No spin history yet", "noHistory": "No spin history yet",

View File

@@ -600,8 +600,8 @@
"starsPaymentFailed": "پرداخت ناموفق. دوباره تلاش کنید.", "starsPaymentFailed": "پرداخت ناموفق. دوباره تلاش کنید.",
"starsPaymentRedirected": "تلگرام برای پرداخت باز می‌شود. پس از پرداخت صفحه را بازخوانی کنید.", "starsPaymentRedirected": "تلگرام برای پرداخت باز می‌شود. پس از پرداخت صفحه را بازخوانی کنید.",
"starsNotAvailable": "پرداخت با ستاره در حال حاضر در دسترس نیست. با پشتیبانی تماس بگیرید.", "starsNotAvailable": "پرداخت با ستاره در حال حاضر در دسترس نیست. با پشتیبانی تماس بگیرید.",
"confirmStarsTitle": "پرداخت با ستاره", "confirmStarsPayment": "شما به تلگرام برای پرداخت یک چرخش منتقل خواهید شد.",
"confirmStarsPayment": "شما به تلگرام برای پرداخت یک چرخش منتقل خواهید شد. ادامه می‌دهید؟", "payStars": "پرداخت {{count}} ⭐",
"noHistory": "هنوز تاریخچه‌ای نیست", "noHistory": "هنوز تاریخچه‌ای نیست",
"banner": { "banner": {
"title": "شانس خود را امتحان کنید!", "title": "شانس خود را امتحان کنید!",

View File

@@ -742,8 +742,8 @@
"starsPaymentFailed": "Ошибка оплаты. Попробуйте еще раз.", "starsPaymentFailed": "Ошибка оплаты. Попробуйте еще раз.",
"starsPaymentRedirected": "Откроется Telegram для оплаты. После оплаты обновите страницу.", "starsPaymentRedirected": "Откроется Telegram для оплаты. После оплаты обновите страницу.",
"starsNotAvailable": "Оплата звёздами сейчас недоступна. Обратитесь в поддержку.", "starsNotAvailable": "Оплата звёздами сейчас недоступна. Обратитесь в поддержку.",
"confirmStarsTitle": "Оплата звёздами", "confirmStarsPayment": "Вы будете перенаправлены в Telegram для оплаты одного вращения колеса.",
"confirmStarsPayment": "Вы будете перенаправлены в Telegram для оплаты одного вращения колеса. Продолжить?", "payStars": "Оплатить {{count}} ⭐",
"youWon": "Вы выиграли", "youWon": "Вы выиграли",
"noPrize": "В этот раз не повезло...", "noPrize": "В этот раз не повезло...",
"noHistory": "История вращений пуста", "noHistory": "История вращений пуста",

View File

@@ -604,8 +604,8 @@
"starsPaymentFailed": "支付失败。请重试。", "starsPaymentFailed": "支付失败。请重试。",
"starsPaymentRedirected": "将打开Telegram进行支付。支付后请刷新页面。", "starsPaymentRedirected": "将打开Telegram进行支付。支付后请刷新页面。",
"starsNotAvailable": "星星支付暂不可用。请联系客服。", "starsNotAvailable": "星星支付暂不可用。请联系客服。",
"confirmStarsTitle": "星星支付", "confirmStarsPayment": "您将被重定向到Telegram进行一次转盘支付",
"confirmStarsPayment": "您将被重定向到Telegram进行一次转盘支付。继续吗", "payStars": "支付 {{count}} ⭐",
"noHistory": "暂无抽奖记录", "noHistory": "暂无抽奖记录",
"banner": { "banner": {
"title": "试试运气!", "title": "试试运气!",

View File

@@ -7,7 +7,6 @@ import WheelLegend from '../components/wheel/WheelLegend';
import InsufficientBalancePrompt from '../components/InsufficientBalancePrompt'; import InsufficientBalancePrompt from '../components/InsufficientBalancePrompt';
import { usePlatform, useHaptic } from '@/platform'; import { usePlatform, useHaptic } from '@/platform';
import { useNotify } from '@/platform/hooks/useNotify'; import { useNotify } from '@/platform/hooks/useNotify';
import { useNativeDialog } from '@/platform/hooks/useNativeDialog';
import { Card } from '@/components/data-display/Card/Card'; import { Card } from '@/components/data-display/Card/Card';
import { Button } from '@/components/primitives/Button/Button'; import { Button } from '@/components/primitives/Button/Button';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
@@ -64,7 +63,6 @@ export default function Wheel() {
const { openInvoice, capabilities } = usePlatform(); const { openInvoice, capabilities } = usePlatform();
const haptic = useHaptic(); const haptic = useHaptic();
const notify = useNotify(); const notify = useNotify();
const dialog = useNativeDialog();
const [isSpinning, setIsSpinning] = useState(false); const [isSpinning, setIsSpinning] = useState(false);
const [targetRotation, setTargetRotation] = useState<number | null>(null); const [targetRotation, setTargetRotation] = useState<number | null>(null);
@@ -74,6 +72,7 @@ export default function Wheel() {
); );
const [isPayingStars, setIsPayingStars] = useState(false); const [isPayingStars, setIsPayingStars] = useState(false);
const [historyExpanded, setHistoryExpanded] = useState(false); const [historyExpanded, setHistoryExpanded] = useState(false);
const [showStarsConfirm, setShowStarsConfirm] = useState(false);
const paymentTypeInitialized = useRef(false); const paymentTypeInitialized = useRef(false);
const { const {
@@ -171,6 +170,7 @@ export default function Wheel() {
const pendingStarsResultRef = useRef<SpinResult | null>(null); const pendingStarsResultRef = useRef<SpinResult | null>(null);
const isStarsSpinRef = useRef(false); const isStarsSpinRef = useRef(false);
const pollingAbortRef = useRef<AbortController | null>(null); const pollingAbortRef = useRef<AbortController | null>(null);
const preOpenedWindowRef = useRef<Window | null>(null);
// Cleanup polling on unmount // Cleanup polling on unmount
useEffect(() => { useEffect(() => {
@@ -272,9 +272,12 @@ export default function Wheel() {
setIsPayingStars(false); setIsPayingStars(false);
} }
} else { } else {
// Fallback: open invoice URL in browser (unsupported platform) // Fallback: redirect pre-opened window to invoice URL
setIsPayingStars(false); setIsPayingStars(false);
window.open(data.invoice_url, '_blank', 'noopener,noreferrer'); if (preOpenedWindowRef.current) {
preOpenedWindowRef.current.location.href = data.invoice_url;
preOpenedWindowRef.current = null;
}
setSpinResult({ setSpinResult({
success: true, success: true,
prize_id: null, prize_id: null,
@@ -292,6 +295,10 @@ export default function Wheel() {
}, },
onError: () => { onError: () => {
setIsPayingStars(false); setIsPayingStars(false);
if (preOpenedWindowRef.current) {
preOpenedWindowRef.current.close();
preOpenedWindowRef.current = null;
}
setSpinResult({ setSpinResult({
success: false, success: false,
prize_id: null, prize_id: null,
@@ -308,13 +315,13 @@ export default function Wheel() {
}, },
}); });
const handleDirectStarsPay = async () => { const handleDirectStarsPay = () => {
const confirmed = await dialog.confirm( setShowStarsConfirm(false);
t('wheel.confirmStarsPayment'),
t('wheel.confirmStarsTitle'),
);
if (!confirmed) return;
setIsPayingStars(true); setIsPayingStars(true);
// In browser: pre-open window synchronously (direct user gesture) to avoid popup blocker
if (!capabilities.hasInvoice) {
preOpenedWindowRef.current = window.open('about:blank', '_blank') || null;
}
starsInvoiceMutation.mutate(); starsInvoiceMutation.mutate();
}; };
@@ -359,7 +366,7 @@ export default function Wheel() {
notify.warning(t('wheel.starsNotAvailable')); notify.warning(t('wheel.starsNotAvailable'));
return; return;
} }
handleDirectStarsPay(); setShowStarsConfirm(true);
} else { } else {
handleSpin(); handleSpin();
} }
@@ -460,20 +467,6 @@ export default function Wheel() {
isPayingStars || isPayingStars ||
(config.daily_limit > 0 && config.user_spins_today >= config.daily_limit); (config.daily_limit > 0 && config.user_spins_today >= config.daily_limit);
// Build cost subtitle for the spin button
const getCostSubtitle = () => {
if (bothMethodsAvailable) return null; // toggle handles it
if (paymentType === 'telegram_stars' && starsEnabled) {
return `${config.spin_cost_stars}`;
}
if (paymentType === 'subscription_days' && daysEnabled) {
return t('wheel.days', { count: config.spin_cost_days ?? 0 });
}
return null;
};
const costSubtitle = getCostSubtitle();
return ( return (
<div className="animate-fade-in space-y-6 pb-8"> <div className="animate-fade-in space-y-6 pb-8">
{/* Simple Header */} {/* Simple Header */}
@@ -504,53 +497,77 @@ export default function Wheel() {
{/* Spin Controls */} {/* Spin Controls */}
<div className="mt-8 space-y-4"> <div className="mt-8 space-y-4">
{/* Payment type toggle - only if both methods available */} {/* Payment type selector */}
{bothMethodsAvailable && ( {(starsEnabled || daysEnabled) && (
<div className="rounded-xl border border-dark-700/30 bg-dark-800/30 p-1"> <div className="rounded-xl border border-dark-700/30 bg-dark-800/30 p-1">
<div className="grid grid-cols-2 gap-1"> <div
<button className={`grid gap-1 ${bothMethodsAvailable ? 'grid-cols-2' : 'grid-cols-1'}`}
onClick={() => setPaymentType('telegram_stars')} >
disabled={isSpinning} {starsEnabled && (
className={`flex items-center justify-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium transition-all ${ <button
paymentType === 'telegram_stars' onClick={() => setPaymentType('telegram_stars')}
? 'bg-accent-500/15 text-accent-400' disabled={isSpinning}
: 'text-dark-400 hover:text-dark-200' className={`flex items-center justify-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium transition-all ${
}`} paymentType === 'telegram_stars'
> ? 'bg-accent-500/15 text-accent-400'
<StarIcon /> : 'text-dark-400 hover:text-dark-200'
{`${config.spin_cost_stars}`} }`}
</button> >
<button <StarIcon />
onClick={() => setPaymentType('subscription_days')} {`${config.spin_cost_stars}`}
disabled={isSpinning} </button>
className={`flex items-center justify-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium transition-all ${ )}
paymentType === 'subscription_days' {daysEnabled && (
? 'bg-accent-500/15 text-accent-400' <button
: 'text-dark-400 hover:text-dark-200' onClick={() => setPaymentType('subscription_days')}
}`} disabled={isSpinning}
> className={`flex items-center justify-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium transition-all ${
<CalendarIcon /> paymentType === 'subscription_days'
{t('wheel.days', { count: config.spin_cost_days ?? 0 })} ? 'bg-accent-500/15 text-accent-400'
</button> : 'text-dark-400 hover:text-dark-200'
}`}
>
<CalendarIcon />
{t('wheel.days', { count: config.spin_cost_days ?? 0 })}
</button>
)}
</div> </div>
</div> </div>
)} )}
{/* Single Spin Button */} {/* Stars confirmation panel */}
<Button {showStarsConfirm && !isSpinning && !isPayingStars ? (
variant="primary" <div className="space-y-3 rounded-xl border border-accent-500/30 bg-accent-500/5 p-4">
size="lg" <p className="text-center text-sm text-dark-300">
fullWidth {t('wheel.confirmStarsPayment')}
onClick={handleUnifiedSpin} </p>
disabled={spinDisabled} <div className="grid grid-cols-2 gap-2">
loading={isSpinning || isPayingStars} <button
> onClick={() => setShowStarsConfirm(false)}
{isSpinning ? t('wheel.spinning') : t('wheel.spin')} className="rounded-lg border border-dark-700 bg-dark-800 px-4 py-2.5 text-sm font-medium text-dark-300 transition-colors hover:bg-dark-700"
</Button> >
{t('common.cancel')}
{/* Cost subtitle when no toggle */} </button>
{costSubtitle && !bothMethodsAvailable && ( <button
<p className="text-center text-sm text-dark-400">{costSubtitle}</p> onClick={handleDirectStarsPay}
className="rounded-lg bg-accent-500 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-accent-600"
>
{t('wheel.payStars', { count: config.spin_cost_stars ?? 0 })}
</button>
</div>
</div>
) : (
/* Single Spin Button */
<Button
variant="primary"
size="lg"
fullWidth
onClick={handleUnifiedSpin}
disabled={spinDisabled}
loading={isSpinning || isPayingStars}
>
{isSpinning ? t('wheel.spinning') : t('wheel.spin')}
</Button>
)} )}
{/* Cannot spin hint */} {/* Cannot spin hint */}