diff --git a/src/api/gift.ts b/src/api/gift.ts index 3a607d9..fa136ec 100644 --- a/src/api/gift.ts +++ b/src/api/gift.ts @@ -74,6 +74,7 @@ export interface GiftPurchaseStatus { status: GiftPurchaseStatusValue; is_gift: boolean; is_code_only: boolean; + is_claimable: boolean; purchase_token: string | null; recipient_contact_value: string | null; gift_message: string | null; diff --git a/src/locales/en.json b/src/locales/en.json index 0d3b4c8..a6b4092 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -4850,7 +4850,9 @@ "activateTelegram": "Activate in Telegram", "activateWeb": "Activate by email", "emailLabel": "Your email", - "claimNow": "Get my gift" + "claimNow": "Get my gift", + "failedTitle": "Gift unavailable", + "failedDesc": "The payment for this gift did not go through, so it cannot be activated." }, "giftLink": { "shareText": "I have a gift for you! Activate it here:", diff --git a/src/locales/fa.json b/src/locales/fa.json index 8596469..e457de3 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -4510,7 +4510,9 @@ "activateTelegram": "فعال‌سازی در تلگرام", "activateWeb": "فعال‌سازی با ایمیل", "emailLabel": "ایمیل شما", - "claimNow": "دریافت هدیه" + "claimNow": "دریافت هدیه", + "failedTitle": "هدیه در دسترس نیست", + "failedDesc": "پرداخت این هدیه انجام نشد، بنابراین قابل فعال‌سازی نیست." }, "giftLink": { "shareText": "یک هدیه برای شما دارم! اینجا فعال کنید:", diff --git a/src/locales/ru.json b/src/locales/ru.json index 217c878..586eeb8 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -5405,7 +5405,9 @@ "activateTelegram": "Активировать в Telegram", "activateWeb": "Активировать по почте", "emailLabel": "Ваш email", - "claimNow": "Получить подарок" + "claimNow": "Получить подарок", + "failedTitle": "Подарок недоступен", + "failedDesc": "Оплата подарка не прошла, поэтому активировать его нельзя." }, "giftLink": { "shareText": "Дарю вам подписку! Активируйте здесь:", diff --git a/src/locales/zh.json b/src/locales/zh.json index a06eeef..da7f73f 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -4509,7 +4509,9 @@ "activateTelegram": "在 Telegram 中激活", "activateWeb": "通过邮箱激活", "emailLabel": "您的邮箱", - "claimNow": "领取礼物" + "claimNow": "领取礼物", + "failedTitle": "礼物不可用", + "failedDesc": "此礼物的付款未成功,无法激活。" }, "giftLink": { "shareText": "我送您一份订阅!在此激活:", diff --git a/src/pages/GiftClaim.tsx b/src/pages/GiftClaim.tsx index 11c4dbd..902ee95 100644 --- a/src/pages/GiftClaim.tsx +++ b/src/pages/GiftClaim.tsx @@ -142,6 +142,25 @@ export default function GiftClaim() { ); } + // Payment failed/expired → tell the recipient instead of spinning forever + if (gift.status === 'failed' || gift.status === 'expired') { + return ( + +
+

+ {t('landing.giftClaim.failedTitle', 'Gift unavailable')} +

+

+ {t( + 'landing.giftClaim.failedDesc', + 'The payment for this gift did not go through, so it cannot be activated.', + )} +

+
+
+ ); + } + // Successful web claim → show connection link if (result) { return ( diff --git a/src/pages/GiftResult.tsx b/src/pages/GiftResult.tsx index 2fea2d8..b083f1a 100644 --- a/src/pages/GiftResult.tsx +++ b/src/pages/GiftResult.tsx @@ -476,8 +476,9 @@ export default function GiftResult() { const s = d?.status; if (s === 'delivered' || s === 'failed' || s === 'pending_activation' || s === 'expired') return false; - // Code-only gifts stay in 'paid' status — stop polling - if (s === 'paid' && d?.is_code_only) return false; + // Claimable gifts (code-only AND directed) stay in 'paid' until claimed — + // stop polling; the buyer shares the link. + if (s === 'paid' && d?.is_claimable) return false; // Check poll timeout if (Date.now() - pollStart.current > MAX_POLL_MS) { @@ -511,8 +512,8 @@ export default function GiftResult() { ); } - const isCodeOnlyPaid = - status?.status === 'paid' && status?.is_code_only && status?.purchase_token != null; + const isClaimablePaid = + status?.status === 'paid' && status?.is_claimable && status?.purchase_token != null; const isDelivered = status?.status === 'delivered'; const isPendingActivation = status?.status === 'pending_activation'; const isFailed = status?.status === 'failed' || status?.status === 'expired'; @@ -531,7 +532,7 @@ export default function GiftResult() { > {isError ? ( - ) : isCodeOnlyPaid ? ( + ) : isClaimablePaid ? ( { const data = query.state.data; const currentStatus = data?.status; - // A gift that reached PAID is claimable and terminal for the BUYER (it - // stays PAID until the recipient claims) — stop polling and show the - // share link instead of spinning forever. - if (currentStatus === 'paid' && data?.is_gift && data?.is_claimable) return false; + // A gift that reached PAID is terminal for the BUYER (it stays PAID until + // the recipient claims) — stop polling and show the share link instead of + // spinning. A paid gift is always claimable, so don't gate on is_claimable. + if (currentStatus === 'paid' && data?.is_gift) return false; if (currentStatus === 'pending' || currentStatus === 'paid') { if (Date.now() - pollStart.current > MAX_POLL_MS) { setPollTimedOut(true); @@ -766,10 +766,7 @@ export default function PurchaseSuccess() { // Deferred gift the buyer just paid for → show the transferable claim link to // forward (it stays PAID until the recipient claims it). - const isBuyerGiftLink = - purchaseStatus?.status === 'paid' && - !!purchaseStatus?.is_gift && - !!purchaseStatus?.is_claimable; + const isBuyerGiftLink = purchaseStatus?.status === 'paid' && !!purchaseStatus?.is_gift; // 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.