mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
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:
@@ -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": {
|
||||
|
||||
@@ -3549,6 +3549,7 @@
|
||||
"credentialsSentToEmail": "اطلاعات ورود به ایمیل شما ارسال شد",
|
||||
"giftSentSuccess": "هدیه ارسال شد!",
|
||||
"giftSentDesc": "گیرنده از طریق ایمیل مطلع خواهد شد",
|
||||
"giftPendingActivationDesc": "گیرنده در حال حاضر اشتراک فعال دارد. لینک فعالسازی هدیه برای او ارسال خواهد شد.",
|
||||
"autoLoginFailed": "ورود خودکار ناموفق بود",
|
||||
"autoLoginProcessing": "در حال ورود...",
|
||||
"periodLabels": {
|
||||
|
||||
@@ -4557,6 +4557,7 @@
|
||||
"credentialsSentToEmail": "Данные для входа отправлены на email",
|
||||
"giftSentSuccess": "Подарок отправлен!",
|
||||
"giftSentDesc": "Получатель получит уведомление на email",
|
||||
"giftPendingActivationDesc": "У получателя уже есть активная подписка. Ему будет отправлена ссылка для активации подарка.",
|
||||
"autoLoginFailed": "Не удалось выполнить автоматический вход",
|
||||
"autoLoginProcessing": "Выполняется вход...",
|
||||
"periodLabels": {
|
||||
|
||||
@@ -3548,6 +3548,7 @@
|
||||
"credentialsSentToEmail": "登录信息已发送到您的邮箱",
|
||||
"giftSentSuccess": "礼物已发送!",
|
||||
"giftSentDesc": "收件人将通过邮件收到通知",
|
||||
"giftPendingActivationDesc": "收件人已有活跃订阅。他们将收到激活礼物的链接。",
|
||||
"autoLoginFailed": "自动登录失败",
|
||||
"autoLoginProcessing": "正在登录...",
|
||||
"periodLabels": {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user