From 0d2bd536c9005777dd72c0ad8194e26d09b8466a Mon Sep 17 00:00:00 2001 From: William Chong Date: Thu, 19 Mar 2026 17:35:53 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=B8=20Distinguish=20past=5Fdue=20from?= =?UTF-8?q?=20canceled=20for=20expired=20Plus=20users?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expired Plus users were all sent to the billing portal, but canceled users can't resubscribe there. Now canceled users go to /member for new checkout, while past_due users go to the portal and get an automatic payment retry on return. --- composables/use-likecoin-session-api.ts | 5 +++++ i18n/locales/en.json | 2 ++ i18n/locales/zh-Hant.json | 2 ++ pages/account/index.vue | 30 ++++++++++++++++++++++--- server/api/account/refresh.post.ts | 1 + server/api/login.post.ts | 1 + server/api/register.post.ts | 1 + shared/types/api.d.ts | 1 + types/nuxt-auth-utils.d.ts | 1 + 9 files changed, 41 insertions(+), 3 deletions(-) diff --git a/composables/use-likecoin-session-api.ts b/composables/use-likecoin-session-api.ts index 80b1b04c8..51218e91e 100644 --- a/composables/use-likecoin-session-api.ts +++ b/composables/use-likecoin-session-api.ts @@ -467,6 +467,10 @@ export function useLikeCoinSessionAPI() { }) } + function retryLikerPlusPayment() { + return fetch.value(`/plus/retry`, { method: 'POST' }) + } + return { createNFTBookPurchase, createNFTBookCartPurchase, @@ -483,5 +487,6 @@ export function useLikeCoinSessionAPI() { sendCollectorMessage, fetchClaimableFreeBooks, claimFreeBook, + retryLikerPlusPayment, } } diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 07b04cb59..ea5a5e432 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -105,8 +105,10 @@ "account_page_subscription": "Membership", "account_page_subscription_expired": "Expired", "account_page_subscription_free": "Free", + "account_page_subscription_past_due": "Payment Failed", "account_page_subscription_plus": "Plus", "account_page_title": "Account", + "account_page_update_payment": "Update Payment", "account_page_update_settings_error": "Unable to update account settings.", "account_page_upgrade_to_plus": "Upgrade to Plus", "account_page_view_about": "About 3ook.com", diff --git a/i18n/locales/zh-Hant.json b/i18n/locales/zh-Hant.json index 69bafc3f4..07d066f17 100644 --- a/i18n/locales/zh-Hant.json +++ b/i18n/locales/zh-Hant.json @@ -105,8 +105,10 @@ "account_page_subscription": "會員", "account_page_subscription_expired": "已過期", "account_page_subscription_free": "免費", + "account_page_subscription_past_due": "付款失敗", "account_page_subscription_plus": "Plus", "account_page_title": "帳戶", + "account_page_update_payment": "更新付款方式", "account_page_update_settings_error": "未能更新帳戶設定。", "account_page_upgrade_to_plus": "升級至 Plus", "account_page_view_about": "關於 3ook.com", diff --git a/pages/account/index.vue b/pages/account/index.vue index 3a2baf7c2..222e90d68 100644 --- a/pages/account/index.vue +++ b/pages/account/index.vue @@ -19,7 +19,7 @@ #right > { return `${config.public.publishBookEndpoint}?utm_source=3ookcom&utm_medium=referral&utm_campaign=3ookcom_account` }) +const isPaymentPastDue = computed(() => + !!user.value?.isExpiredLikerPlus && user.value?.likerPlusSubscriptionStatus === 'past_due', +) + const subscriptionStateLabel = computed(() => { if (!user.value) return undefined if (user.value.isLikerPlus) { @@ -624,11 +628,20 @@ const subscriptionStateLabel = computed(() => { return $t('account_page_subscription_plus') } if (user.value.isExpiredLikerPlus) { + if (isPaymentPastDue.value) { + return $t('account_page_subscription_past_due') + } return $t('account_page_subscription_expired') } return $t('account_page_subscription_free') }) +const likerPlusButtonLabel = computed(() => { + if (user.value?.isLikerPlus) return $t('account_page_manage_subscription') + if (isPaymentPastDue.value) return $t('account_page_update_payment') + return $t('account_page_renew_subscription') +}) + const likeWalletButtonTo = computed(() => { if (!user.value?.likeWallet) return undefined return `${config.public.likerLandSiteURL}/${locale.value}/${user.value.likeWallet}?tab=collected` @@ -665,11 +678,10 @@ const isOpeningBillingPortal = ref(false) async function handleLikerPlusButtonClick() { useLogEvent('account_liker_plus_button_click') - if (!user.value?.isLikerPlus && !user.value?.isExpiredLikerPlus) { + if (!user.value?.isLikerPlus && !isPaymentPastDue.value) { await navigateTo(localeRoute({ name: 'member' })) return } - if (isOpeningBillingPortal.value) return try { isOpeningBillingPortal.value = true @@ -680,6 +692,18 @@ async function handleLikerPlusButtonClick() { do { await sleep(3000) } while (!isWindowFocused.value) + + await accountStore.refreshSessionInfo() + + if (isPaymentPastDue.value) { + try { + await likeCoinSessionAPI.retryLikerPlusPayment() + } + catch (retryError) { + console.warn('Payment retry after portal return:', retryError) + } + } + isOpeningBillingPortal.value = false } catch (error) { diff --git a/server/api/account/refresh.post.ts b/server/api/account/refresh.post.ts index 6aa31427e..eb114a8bf 100644 --- a/server/api/account/refresh.post.ts +++ b/server/api/account/refresh.post.ts @@ -35,6 +35,7 @@ export default defineEventHandler(async (event) => { isLikerPlus: userInfoRes.isLikerPlus || false, isExpiredLikerPlus: userInfoRes.isExpiredLikerPlus || false, likerPlusPeriod: userInfoRes.likerPlusPeriod, + likerPlusSubscriptionStatus: userInfoRes.likerPlusSubscriptionStatus, }, }) diff --git a/server/api/login.post.ts b/server/api/login.post.ts index 0bde17cc9..969e2b197 100644 --- a/server/api/login.post.ts +++ b/server/api/login.post.ts @@ -73,6 +73,7 @@ export default defineEventHandler(async (event) => { isExpiredLikerPlus: userInfoRes.isExpiredLikerPlus || false, likerPlusPeriod: userInfoRes.likerPlusPeriod, ttsKey: randomBytes(16).toString('hex'), + likerPlusSubscriptionStatus: userInfoRes.likerPlusSubscriptionStatus, } await setUserSession(event, { user: userInfo }) diff --git a/server/api/register.post.ts b/server/api/register.post.ts index 4ab889d5d..f3a806702 100644 --- a/server/api/register.post.ts +++ b/server/api/register.post.ts @@ -72,6 +72,7 @@ export default defineEventHandler(async (event) => { isLikerPlus: userInfoRes.isLikerPlus || false, isExpiredLikerPlus: userInfoRes.isExpiredLikerPlus || false, ttsKey: randomBytes(16).toString('hex'), + likerPlusSubscriptionStatus: userInfoRes.likerPlusSubscriptionStatus, } await setUserSession(event, { user: userInfo }) diff --git a/shared/types/api.d.ts b/shared/types/api.d.ts index f1d111e7a..da64f1bf6 100644 --- a/shared/types/api.d.ts +++ b/shared/types/api.d.ts @@ -8,6 +8,7 @@ export interface LikerInfoResponseData { description: string isLikerPlus?: boolean isExpiredLikerPlus?: boolean + likerPlusSubscriptionStatus?: 'active' | 'past_due' | 'canceled' } export interface LikerProfileResponseData extends LikerInfoResponseData { diff --git a/types/nuxt-auth-utils.d.ts b/types/nuxt-auth-utils.d.ts index ff0cdb4b5..17b20980b 100644 --- a/types/nuxt-auth-utils.d.ts +++ b/types/nuxt-auth-utils.d.ts @@ -15,6 +15,7 @@ declare module '#auth-utils' { isExpiredLikerPlus?: boolean likerPlusPeriod?: LikerPlusStatus ttsKey?: string + likerPlusSubscriptionStatus?: 'active' | 'past_due' | 'canceled' } }