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)
This commit is contained in:
Fringg
2026-03-07 06:00:17 +03:00
parent bc45294487
commit 97959b0132
5 changed files with 88 additions and 1 deletions

View File

@@ -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": {

View File

@@ -3549,6 +3549,7 @@
"credentialsSentToEmail": "اطلاعات ورود به ایمیل شما ارسال شد",
"giftSentSuccess": "هدیه ارسال شد!",
"giftSentDesc": "گیرنده از طریق ایمیل مطلع خواهد شد",
"giftPendingActivationDesc": "گیرنده در حال حاضر اشتراک فعال دارد. لینک فعال‌سازی هدیه برای او ارسال خواهد شد.",
"autoLoginFailed": "ورود خودکار ناموفق بود",
"autoLoginProcessing": "در حال ورود...",
"periodLabels": {

View File

@@ -4557,6 +4557,7 @@
"credentialsSentToEmail": "Данные для входа отправлены на email",
"giftSentSuccess": "Подарок отправлен!",
"giftSentDesc": "Получатель получит уведомление на email",
"giftPendingActivationDesc": "У получателя уже есть активная подписка. Ему будет отправлена ссылка для активации подарка.",
"autoLoginFailed": "Не удалось выполнить автоматический вход",
"autoLoginProcessing": "Выполняется вход...",
"periodLabels": {

View File

@@ -3548,6 +3548,7 @@
"credentialsSentToEmail": "登录信息已发送到您的邮箱",
"giftSentSuccess": "礼物已发送!",
"giftSentDesc": "收件人将通过邮件收到通知",
"giftPendingActivationDesc": "收件人已有活跃订阅。他们将收到激活礼物的链接。",
"autoLoginFailed": "自动登录失败",
"autoLoginProcessing": "正在登录...",
"periodLabels": {

View File

@@ -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 (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="flex flex-col items-center gap-6 text-center"
>
{/* Animated checkmark */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 200, damping: 15, delay: 0.1 }}
className="flex h-20 w-20 items-center justify-center rounded-full bg-success-500/10"
>
<motion.svg
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 0.4, delay: 0.3 }}
className="h-10 w-10 text-success-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2.5}
>
<motion.path
strokeLinecap="round"
strokeLinejoin="round"
d="M5 13l4 4L19 7"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.4, delay: 0.3 }}
/>
</motion.svg>
</motion.div>
<div>
<h1 className="text-xl font-bold text-dark-50">{t('landing.giftSentSuccess')}</h1>
{tariffName && periodDays !== null && (
<p className="mt-1 text-sm text-dark-300">
{tariffName} {periodDays} {t('landing.daysAccess')}
</p>
)}
{recipientContactValue && (
<p className="mt-2 text-sm text-dark-400">
{t('landing.giftSentTo', { contact: recipientContactValue })}
</p>
)}
<p className="mt-2 text-sm text-dark-400">{t('landing.giftPendingActivationDesc')}</p>
{giftMessage && (
<p className="mt-2 text-sm italic text-dark-400">
{t('landing.giftMessage')}: {giftMessage}
</p>
)}
</div>
</motion.div>
);
}
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 ? (
<GiftPendingActivationState
tariffName={purchaseStatus.tariff_name}
periodDays={purchaseStatus.period_days}
recipientContactValue={purchaseStatus.recipient_contact_value}
giftMessage={purchaseStatus.gift_message}
/>
) : isPendingActivation ? (
<div className="space-y-4">
<PendingActivationState