mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 10:27:49 +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",
|
"credentialsSentToEmail": "Login credentials sent to your email",
|
||||||
"giftSentSuccess": "Gift sent!",
|
"giftSentSuccess": "Gift sent!",
|
||||||
"giftSentDesc": "Recipient will be notified by email",
|
"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",
|
"autoLoginFailed": "Auto-login failed",
|
||||||
"autoLoginProcessing": "Signing in...",
|
"autoLoginProcessing": "Signing in...",
|
||||||
"periodLabels": {
|
"periodLabels": {
|
||||||
|
|||||||
@@ -3549,6 +3549,7 @@
|
|||||||
"credentialsSentToEmail": "اطلاعات ورود به ایمیل شما ارسال شد",
|
"credentialsSentToEmail": "اطلاعات ورود به ایمیل شما ارسال شد",
|
||||||
"giftSentSuccess": "هدیه ارسال شد!",
|
"giftSentSuccess": "هدیه ارسال شد!",
|
||||||
"giftSentDesc": "گیرنده از طریق ایمیل مطلع خواهد شد",
|
"giftSentDesc": "گیرنده از طریق ایمیل مطلع خواهد شد",
|
||||||
|
"giftPendingActivationDesc": "گیرنده در حال حاضر اشتراک فعال دارد. لینک فعالسازی هدیه برای او ارسال خواهد شد.",
|
||||||
"autoLoginFailed": "ورود خودکار ناموفق بود",
|
"autoLoginFailed": "ورود خودکار ناموفق بود",
|
||||||
"autoLoginProcessing": "در حال ورود...",
|
"autoLoginProcessing": "در حال ورود...",
|
||||||
"periodLabels": {
|
"periodLabels": {
|
||||||
|
|||||||
@@ -4557,6 +4557,7 @@
|
|||||||
"credentialsSentToEmail": "Данные для входа отправлены на email",
|
"credentialsSentToEmail": "Данные для входа отправлены на email",
|
||||||
"giftSentSuccess": "Подарок отправлен!",
|
"giftSentSuccess": "Подарок отправлен!",
|
||||||
"giftSentDesc": "Получатель получит уведомление на email",
|
"giftSentDesc": "Получатель получит уведомление на email",
|
||||||
|
"giftPendingActivationDesc": "У получателя уже есть активная подписка. Ему будет отправлена ссылка для активации подарка.",
|
||||||
"autoLoginFailed": "Не удалось выполнить автоматический вход",
|
"autoLoginFailed": "Не удалось выполнить автоматический вход",
|
||||||
"autoLoginProcessing": "Выполняется вход...",
|
"autoLoginProcessing": "Выполняется вход...",
|
||||||
"periodLabels": {
|
"periodLabels": {
|
||||||
|
|||||||
@@ -3548,6 +3548,7 @@
|
|||||||
"credentialsSentToEmail": "登录信息已发送到您的邮箱",
|
"credentialsSentToEmail": "登录信息已发送到您的邮箱",
|
||||||
"giftSentSuccess": "礼物已发送!",
|
"giftSentSuccess": "礼物已发送!",
|
||||||
"giftSentDesc": "收件人将通过邮件收到通知",
|
"giftSentDesc": "收件人将通过邮件收到通知",
|
||||||
|
"giftPendingActivationDesc": "收件人已有活跃订阅。他们将收到激活礼物的链接。",
|
||||||
"autoLoginFailed": "自动登录失败",
|
"autoLoginFailed": "自动登录失败",
|
||||||
"autoLoginProcessing": "正在登录...",
|
"autoLoginProcessing": "正在登录...",
|
||||||
"periodLabels": {
|
"periodLabels": {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
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 { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { motion } from 'framer-motion';
|
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() {
|
function FailedState() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -583,6 +653,8 @@ function PollTimedOutState({ onRetry }: { onRetry: () => void }) {
|
|||||||
export default function PurchaseSuccess() {
|
export default function PurchaseSuccess() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { token } = useParams<{ token: string }>();
|
const { token } = useParams<{ token: string }>();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const isActivateHint = searchParams.get('activate') === '1';
|
||||||
const pollStart = useRef(Date.now());
|
const pollStart = useRef(Date.now());
|
||||||
const [pollTimedOut, setPollTimedOut] = useState(false);
|
const [pollTimedOut, setPollTimedOut] = useState(false);
|
||||||
const [isActivating, setIsActivating] = useState(false);
|
const [isActivating, setIsActivating] = useState(false);
|
||||||
@@ -650,6 +722,10 @@ export default function PurchaseSuccess() {
|
|||||||
const isPendingActivation = purchaseStatus?.status === 'pending_activation';
|
const isPendingActivation = purchaseStatus?.status === 'pending_activation';
|
||||||
const isFailed = purchaseStatus?.status === 'failed' || purchaseStatus?.status === 'expired';
|
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
|
// Email self-purchase delivered → show cabinet credentials
|
||||||
const isEmailSelfPurchase =
|
const isEmailSelfPurchase =
|
||||||
isSuccess &&
|
isSuccess &&
|
||||||
@@ -681,6 +757,13 @@ export default function PurchaseSuccess() {
|
|||||||
isGift={purchaseStatus.is_gift}
|
isGift={purchaseStatus.is_gift}
|
||||||
giftMessage={purchaseStatus.gift_message}
|
giftMessage={purchaseStatus.gift_message}
|
||||||
/>
|
/>
|
||||||
|
) : isGiftPendingActivation ? (
|
||||||
|
<GiftPendingActivationState
|
||||||
|
tariffName={purchaseStatus.tariff_name}
|
||||||
|
periodDays={purchaseStatus.period_days}
|
||||||
|
recipientContactValue={purchaseStatus.recipient_contact_value}
|
||||||
|
giftMessage={purchaseStatus.gift_message}
|
||||||
|
/>
|
||||||
) : isPendingActivation ? (
|
) : isPendingActivation ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<PendingActivationState
|
<PendingActivationState
|
||||||
|
|||||||
Reference in New Issue
Block a user