From 97959b013241597e77ed3223fb5aa2d1de8be2d0 Mon Sep 17 00:00:00 2001 From: Fringg Date: Sat, 7 Mar 2026 06:00:17 +0300 Subject: [PATCH] fix: prevent buyer from activating gift pending subscription When a gift purchase results in pending_activation (recipient has active subscription), the buyer was shown an Activate button and could replace the recipient's subscription without consent. - Add GiftPendingActivationState component showing "Gift sent" message - Use ?activate=1 URL hint from recipient email to distinguish viewer - Buyer sees success message, recipient sees activate button - Add giftPendingActivationDesc translations (ru, en, zh, fa) --- src/locales/en.json | 1 + src/locales/fa.json | 1 + src/locales/ru.json | 1 + src/locales/zh.json | 1 + src/pages/PurchaseSuccess.tsx | 85 ++++++++++++++++++++++++++++++++++- 5 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/locales/en.json b/src/locales/en.json index 042121d..1972b8f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -3998,6 +3998,7 @@ "credentialsSentToEmail": "Login credentials sent to your email", "giftSentSuccess": "Gift sent!", "giftSentDesc": "Recipient will be notified by email", + "giftPendingActivationDesc": "The recipient already has an active subscription. They will receive a link to activate the gift.", "autoLoginFailed": "Auto-login failed", "autoLoginProcessing": "Signing in...", "periodLabels": { diff --git a/src/locales/fa.json b/src/locales/fa.json index 3e84979..08fe356 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -3549,6 +3549,7 @@ "credentialsSentToEmail": "اطلاعات ورود به ایمیل شما ارسال شد", "giftSentSuccess": "هدیه ارسال شد!", "giftSentDesc": "گیرنده از طریق ایمیل مطلع خواهد شد", + "giftPendingActivationDesc": "گیرنده در حال حاضر اشتراک فعال دارد. لینک فعال‌سازی هدیه برای او ارسال خواهد شد.", "autoLoginFailed": "ورود خودکار ناموفق بود", "autoLoginProcessing": "در حال ورود...", "periodLabels": { diff --git a/src/locales/ru.json b/src/locales/ru.json index 10dcf2d..83f3e65 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -4557,6 +4557,7 @@ "credentialsSentToEmail": "Данные для входа отправлены на email", "giftSentSuccess": "Подарок отправлен!", "giftSentDesc": "Получатель получит уведомление на email", + "giftPendingActivationDesc": "У получателя уже есть активная подписка. Ему будет отправлена ссылка для активации подарка.", "autoLoginFailed": "Не удалось выполнить автоматический вход", "autoLoginProcessing": "Выполняется вход...", "periodLabels": { diff --git a/src/locales/zh.json b/src/locales/zh.json index 2ae4911..cf376ec 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -3548,6 +3548,7 @@ "credentialsSentToEmail": "登录信息已发送到您的邮箱", "giftSentSuccess": "礼物已发送!", "giftSentDesc": "收件人将通过邮件收到通知", + "giftPendingActivationDesc": "收件人已有活跃订阅。他们将收到激活礼物的链接。", "autoLoginFailed": "自动登录失败", "autoLoginProcessing": "正在登录...", "periodLabels": { diff --git a/src/pages/PurchaseSuccess.tsx b/src/pages/PurchaseSuccess.tsx index aacb47c..a4e9575 100644 --- a/src/pages/PurchaseSuccess.tsx +++ b/src/pages/PurchaseSuccess.tsx @@ -1,5 +1,5 @@ import { useState, useCallback, useRef, useEffect } from 'react'; -import { useParams, useNavigate } from 'react-router'; +import { useParams, useNavigate, useSearchParams } from 'react-router'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { motion } from 'framer-motion'; @@ -502,6 +502,76 @@ function PendingActivationState({ ); } +function GiftPendingActivationState({ + tariffName, + periodDays, + recipientContactValue, + giftMessage, +}: { + tariffName: string | null; + periodDays: number | null; + recipientContactValue: string | null; + giftMessage: string | null; +}) { + const { t } = useTranslation(); + + return ( + + {/* Animated checkmark */} + + + + + + +
+

{t('landing.giftSentSuccess')}

+ {tariffName && periodDays !== null && ( +

+ {tariffName} — {periodDays} {t('landing.daysAccess')} +

+ )} + {recipientContactValue && ( +

+ {t('landing.giftSentTo', { contact: recipientContactValue })} +

+ )} +

{t('landing.giftPendingActivationDesc')}

+ {giftMessage && ( +

+ {t('landing.giftMessage')}: {giftMessage} +

+ )} +
+
+ ); +} + function FailedState() { const { t } = useTranslation(); @@ -583,6 +653,8 @@ function PollTimedOutState({ onRetry }: { onRetry: () => void }) { export default function PurchaseSuccess() { const { t } = useTranslation(); const { token } = useParams<{ token: string }>(); + const [searchParams] = useSearchParams(); + const isActivateHint = searchParams.get('activate') === '1'; const pollStart = useRef(Date.now()); const [pollTimedOut, setPollTimedOut] = useState(false); const [isActivating, setIsActivating] = useState(false); @@ -650,6 +722,10 @@ export default function PurchaseSuccess() { const isPendingActivation = purchaseStatus?.status === 'pending_activation'; const isFailed = purchaseStatus?.status === 'failed' || purchaseStatus?.status === 'expired'; + // Gift pending activation → buyer sees "gift sent" message, not the activate button. + // Recipient arrives via email link with ?activate=1 and sees the activate button instead. + const isGiftPendingActivation = isPendingActivation && purchaseStatus?.is_gift && !isActivateHint; + // Email self-purchase delivered → show cabinet credentials const isEmailSelfPurchase = isSuccess && @@ -681,6 +757,13 @@ export default function PurchaseSuccess() { isGift={purchaseStatus.is_gift} giftMessage={purchaseStatus.gift_message} /> + ) : isGiftPendingActivation ? ( + ) : isPendingActivation ? (