From 9367e5665f2e2c684f4d189517a38ee93b6cb62a Mon Sep 17 00:00:00 2001 From: gyx Date: Mon, 27 Oct 2025 17:16:11 +1100 Subject: [PATCH 1/5] feature/sub-stripe --- .../components/BillingHistorySection.tsx | 6 +- .../billing/components/BillingSection.tsx | 54 ++++++++++++++- .../overview/components/ActivitySection.tsx | 23 ++++++- src/components/ui/StatusChip.tsx | 8 ++- src/features/subscription/useSubscription.ts | 6 +- src/types/subscription.d.ts | 4 +- src/utils/subscriptionUtils.ts | 69 +++++++++++++++++++ 7 files changed, 159 insertions(+), 11 deletions(-) create mode 100644 src/utils/subscriptionUtils.ts diff --git a/src/app/admin/billing/components/BillingHistorySection.tsx b/src/app/admin/billing/components/BillingHistorySection.tsx index 1af6d42..cb22e63 100644 --- a/src/app/admin/billing/components/BillingHistorySection.tsx +++ b/src/app/admin/billing/components/BillingHistorySection.tsx @@ -77,9 +77,9 @@ const BillingHistorySection = () => { timestamp: new Date(ref.date).getTime(), })); - const combined: BillingEntry[] = [...invoiceEntries, ...refundEntries].sort( - (a, b) => b.timestamp - a.timestamp, - ); + const combined: BillingEntry[] = [...invoiceEntries, ...refundEntries] + .filter(entry => ['paid', 'unpaid', 'refunded'].includes(entry.status)) + .sort((a, b) => b.timestamp - a.timestamp); return ( diff --git a/src/app/admin/billing/components/BillingSection.tsx b/src/app/admin/billing/components/BillingSection.tsx index 010f570..3e6149c 100644 --- a/src/app/admin/billing/components/BillingSection.tsx +++ b/src/app/admin/billing/components/BillingSection.tsx @@ -38,21 +38,53 @@ function getPrice(pricing: { rrule: string; price: number }[]) { function getButtonsByPlan( plan: Plan, currentPlanId: string, + pendingPlanId: string | undefined, isSubscribed: boolean, isCancelled: boolean, + isPendingCancellation: boolean, + isPendingDowngrade: boolean, ): PlanButton[] { const isCurrent = plan._id === currentPlanId; + const isPendingDowngradeToPlan = isPendingDowngrade && plan._id === pendingPlanId; + if (isCancelled) { if (plan.tier === 'FREE') return [{ label: 'Your current plan', variant: 'disabled' }]; return [{ label: `Go with ${plan.tier}`, variant: 'primary' }]; } + if (isSubscribed) { + // Current plan display if (isCurrent) { + if (isPendingCancellation) { + return [{ label: 'Cancels at period end', variant: 'disabled' }]; + } + if (isPendingDowngrade) { + // Current plan during pending downgrade - show cancel downgrade option + return [{ label: 'Cancel downgrade', variant: 'primary' }]; + } return [{ label: 'Cancel Subscription', variant: 'cancel' }]; } + + // Special handling for pending downgrade + if (isPendingDowngrade) { + if (isPendingDowngradeToPlan) { + // Target plan (Basic) - show downgrade info + return [{ label: 'Downgrades next cycle', variant: 'disabled' }]; + } else { + // Other plans during pending downgrade - show switch option + return [{ label: `Go with ${plan.tier}`, variant: 'primary' }]; + } + } + + // If pending cancellation, show "Go with" to allow reactivation + if (isPendingCancellation) { + return [{ label: `Go with ${plan.tier}`, variant: 'primary' }]; + } + return [{ label: `Switch to ${plan.tier}`, variant: 'primary' }]; } + return [{ label: 'Try for Free', variant: 'primary' }]; } @@ -69,8 +101,10 @@ export default function BillingSection() { const { create } = useCreateSubscription(); const { change } = useChangePlan(); const { downgrade } = useDowngradeToFree(); - const { subscription, isSubscribed, isCancelled, currentPlanId } = + const { subscription, isSubscribed, isCancelled, isPendingCancellation, isPendingDowngrade, currentPlanId } = useSubscription(); + + const pendingPlanId = subscription?.pendingPlanId?._id; const tierOrder = { FREE: 0, BASIC: 1, PRO: 2 }; const sortedPlans = [...plans].sort( @@ -111,19 +145,32 @@ export default function BillingSection() { if (label.startsWith('Go with')) { if (!subscription || subscription.status === 'cancelled') { await create(planId); + } else if (subscription.status === 'pending_cancellation' || subscription.status === 'pending_downgrade') { + // If pending cancellation or downgrade, use change to cancel the pending change and switch plan + await change(planId); + window.location.reload(); } else if (subscription.planId._id !== planId) { await change(planId); } } - if (label === 'Cancel Subscription') { + if (label === 'Cancel Subscription' || label === 'Cancel Instead') { setShowCancelModal(true); return; } + if (label === 'Cancel downgrade') { + // Cancel the pending downgrade by switching back to current plan + if (subscription?.planId._id) { + await change(subscription.planId._id); + window.location.reload(); + } + return; + } if (label.startsWith('Switch to')) { if (tier === 'FREE') await downgrade(); else await change(planId); window.location.reload(); } + // Do nothing for disabled states ('Cancels at period end', 'Downgrades next cycle', 'Your current plan') }; const handleConfirmCancel = async () => { @@ -182,8 +229,11 @@ export default function BillingSection() { buttons={getButtonsByPlan( plan, currentPlanId, + pendingPlanId, isSubscribed, isCancelled, + isPendingCancellation, + isPendingDowngrade, )} onButtonClick={label => void handleClick(label, plan.tier, plan._id) diff --git a/src/app/admin/overview/components/ActivitySection.tsx b/src/app/admin/overview/components/ActivitySection.tsx index 21e9b64..55a1e2e 100644 --- a/src/app/admin/overview/components/ActivitySection.tsx +++ b/src/app/admin/overview/components/ActivitySection.tsx @@ -12,6 +12,11 @@ import { useGetBookingsQuery } from '@/features/service/serviceBookingApi'; import { useSubscription } from '@/features/subscription/useSubscription'; import { useAppSelector } from '@/redux/hooks'; import { getPlanTier, isFreeOrBasicPlan } from '@/utils/planUtils'; +import { + getRemainingMinutes, + getTotalMinutes, + getUsagePercentage +} from '@/utils/subscriptionUtils'; function formatSubscriptionPeriod( start?: string | Date, @@ -160,6 +165,18 @@ export default function ActivitySection() { const planTier = getPlanTier(subscription); const shouldHideBookingFeatures = isFreeOrBasicPlan(planTier); + // Calculate minutes data for the progress circle + const remainingMinutes = getRemainingMinutes(subscription); + const totalMinutes = getTotalMinutes(subscription?.planId); + const usedMinutes = totalMinutes - remainingMinutes; + const usagePercentage = getUsagePercentage(subscription, subscription?.planId); + + // Determine display values and unit text + const isUnlimited = totalMinutes === Number.MAX_SAFE_INTEGER; + const displayValue = remainingMinutes; // Always show remaining minutes + const displayMaxValue = isUnlimited ? Math.max(remainingMinutes, 1000) : totalMinutes; + const unitText = isUnlimited ? '/Unlimited' : `/${totalMinutes}`; + const { data: bookings } = useGetBookingsQuery({ userId }, { skip: !userId }); const todayBookings = (bookings ?? []).filter(booking => { @@ -262,9 +279,9 @@ export default function ActivitySection() { diff --git a/src/components/ui/StatusChip.tsx b/src/components/ui/StatusChip.tsx index 37282f0..98b67f1 100644 --- a/src/components/ui/StatusChip.tsx +++ b/src/components/ui/StatusChip.tsx @@ -25,7 +25,13 @@ const statusStyles = { }; const StatusChip = ({ status }: Props) => { - const { bg, dot, text } = statusStyles[status]; + const statusConfig = statusStyles[status] || { + bg: '#f5f5f5', + dot: '#999999', + text: status || 'Unknown', + }; + + const { bg, dot, text } = statusConfig; return ( { refetch, } = useGetSubscriptionByUserQuery(userId!, { skip: !userId }); - const isSubscribed = subscription?.status === 'active'; + const isSubscribed = subscription?.status === 'active' || subscription?.status === 'pending_cancellation' || subscription?.status === 'pending_downgrade'; const isCancelled = subscription?.status === 'cancelled' || !subscription; const isFailed = subscription?.status === 'failed'; + const isPendingCancellation = subscription?.status === 'pending_cancellation'; + const isPendingDowngrade = subscription?.status === 'pending_downgrade'; const currentPlanId = subscription?.planId._id ?? ''; return { @@ -31,6 +33,8 @@ export const useSubscription = () => { isSubscribed, isCancelled, isFailed, + isPendingCancellation, + isPendingDowngrade, currentPlanId, isLoading, isError, diff --git a/src/types/subscription.d.ts b/src/types/subscription.d.ts index e19361f..938a63b 100644 --- a/src/types/subscription.d.ts +++ b/src/types/subscription.d.ts @@ -13,13 +13,15 @@ export interface Subscription { _id: string; userId: string; planId: Plan; + pendingPlanId?: Plan; subscriptionId: string; stripeCustomerId: string; chargeId: string; - status: 'active' | 'cancelled' | 'failed'; + status: 'active' | 'cancelled' | 'failed' | 'pending_cancellation' | 'pending_downgrade'; startAt: string; endAt: string; createdAt: string; + secondsLeft: number; } export interface RawInvoice { diff --git a/src/utils/subscriptionUtils.ts b/src/utils/subscriptionUtils.ts new file mode 100644 index 0000000..b25633c --- /dev/null +++ b/src/utils/subscriptionUtils.ts @@ -0,0 +1,69 @@ +/** + * Extract numeric minutes from callMinutes string + * Handles various formats like "100 Min/Month", "Unlimited", "100", etc. + */ +export function extractMinutesFromCallMinutes(callMinutes: string | number): number { + // If it's already a number, return it + if (typeof callMinutes === 'number') { + return callMinutes; + } + + // Handle string formats + const str = callMinutes.toString().toLowerCase(); + + // Check for unlimited + if (str.includes('unlimited') || str.includes('∞')) { + return Number.MAX_SAFE_INTEGER; // Use a very large number for unlimited + } + + // Extract number from string using regex + const match = str.match(/(\d+)/); + if (match) { + return parseInt(match[1], 10); + } + + // Default fallback + return 0; +} + +/** + * Convert seconds to minutes + */ +export function secondsToMinutes(seconds: number): number { + return Math.floor(seconds / 60); +} + +/** + * Get remaining minutes from subscription + */ +export function getRemainingMinutes(subscription: { secondsLeft: number } | null | undefined): number { + if (!subscription) return 0; + return secondsToMinutes(subscription.secondsLeft); +} + +/** + * Get total minutes from plan + */ +export function getTotalMinutes(plan: { features: { callMinutes: string | number } } | null | undefined): number { + if (!plan) return 0; + return extractMinutesFromCallMinutes(plan.features.callMinutes); +} + +/** + * Get usage percentage (used minutes / total minutes) + */ +export function getUsagePercentage( + subscription: { secondsLeft: number } | null | undefined, + plan: { features: { callMinutes: string | number } } | null | undefined +): number { + if (!subscription || !plan) return 0; + + const totalMinutes = getTotalMinutes(plan); + const remainingMinutes = getRemainingMinutes(subscription); + const usedMinutes = totalMinutes - remainingMinutes; + + if (totalMinutes === 0) return 0; + if (totalMinutes === Number.MAX_SAFE_INTEGER) return 0; // Unlimited plan + + return Math.max(0, Math.min(100, (usedMinutes / totalMinutes) * 100)); +} From d4e6de3ea29b7f884812f27ee365c7a49102b933 Mon Sep 17 00:00:00 2001 From: gyx Date: Tue, 28 Oct 2025 13:03:33 +1100 Subject: [PATCH 2/5] feature/minstatus --- .../admin/billing/components/BillingCard.tsx | 4 +- .../billing/components/BillingSection.tsx | 37 ++++++++- src/components/ui/CommonButton.tsx | 13 +++- src/components/ui/HalfCircleProgress.tsx | 2 +- src/components/ui/PaymentFailedModal.tsx | 75 +++++++++++++++++++ src/types/plan.types.ts | 2 +- 6 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 src/components/ui/PaymentFailedModal.tsx diff --git a/src/app/admin/billing/components/BillingCard.tsx b/src/app/admin/billing/components/BillingCard.tsx index 508d172..7221c56 100644 --- a/src/app/admin/billing/components/BillingCard.tsx +++ b/src/app/admin/billing/components/BillingCard.tsx @@ -226,7 +226,9 @@ export default function PricingCard({ ? 'green' : btn.variant === 'cancel' ? 'cancel' - : 'disabled' + : btn.variant === 'retry' + ? 'retry' + : 'disabled' } onClick={ btn.variant === 'disabled' diff --git a/src/app/admin/billing/components/BillingSection.tsx b/src/app/admin/billing/components/BillingSection.tsx index 3e6149c..443bcbb 100644 --- a/src/app/admin/billing/components/BillingSection.tsx +++ b/src/app/admin/billing/components/BillingSection.tsx @@ -7,11 +7,13 @@ import { useKeenSlider } from 'keen-slider/react'; import { useEffect, useState } from 'react'; import CancelConfirmModal from '@/components/ui/CancelConfirmModal'; +import PaymentFailedModal from '@/components/ui/PaymentFailedModal'; import { useGetPlansQuery } from '@/features/public/publicApiSlice'; import { useChangePlan, useCreateSubscription, useDowngradeToFree, + useRetryPayment, useSubscription, } from '@/features/subscription/useSubscription'; import type { Plan, PlanButton } from '@/types/plan.types'; @@ -43,6 +45,7 @@ function getButtonsByPlan( isCancelled: boolean, isPendingCancellation: boolean, isPendingDowngrade: boolean, + isFailed: boolean, ): PlanButton[] { const isCurrent = plan._id === currentPlanId; const isPendingDowngradeToPlan = isPendingDowngrade && plan._id === pendingPlanId; @@ -53,6 +56,15 @@ function getButtonsByPlan( return [{ label: `Go with ${plan.tier}`, variant: 'primary' }]; } + if (isFailed) { + // For failed subscriptions, show retry payment button for current plan + if (isCurrent) { + return [{ label: 'Retry Payment', variant: 'retry' }]; + } + // For other plans, disable them during payment failure + return [{ label: `Go with ${plan.tier}`, variant: 'disabled' }]; + } + if (isSubscribed) { // Current plan display if (isCurrent) { @@ -101,7 +113,8 @@ export default function BillingSection() { const { create } = useCreateSubscription(); const { change } = useChangePlan(); const { downgrade } = useDowngradeToFree(); - const { subscription, isSubscribed, isCancelled, isPendingCancellation, isPendingDowngrade, currentPlanId } = + const { retryPayment } = useRetryPayment(); + const { subscription, isSubscribed, isCancelled, isFailed, isPendingCancellation, isPendingDowngrade, currentPlanId } = useSubscription(); const pendingPlanId = subscription?.pendingPlanId?._id; @@ -136,6 +149,7 @@ export default function BillingSection() { }, [slider]); const [showCancelModal, setShowCancelModal] = useState(false); + const [showPaymentFailedModal, setShowPaymentFailedModal] = useState(false); const handleClick = async ( label: string, @@ -157,6 +171,10 @@ export default function BillingSection() { setShowCancelModal(true); return; } + if (label === 'Retry Payment') { + setShowPaymentFailedModal(true); + return; + } if (label === 'Cancel downgrade') { // Cancel the pending downgrade by switching back to current plan if (subscription?.planId._id) { @@ -183,6 +201,16 @@ export default function BillingSection() { } }; + const handleRetryPayment = async () => { + try { + await retryPayment(); + setShowPaymentFailedModal(false); + } catch { + // Handle error silently + } + }; + + return ( void handleClick(label, plan.tier, plan._id) @@ -254,6 +283,12 @@ export default function BillingSection() { onClose={() => setShowCancelModal(false)} onConfirm={handleConfirmCancel} /> + + setShowPaymentFailedModal(false)} + onRetryPayment={handleRetryPayment} + /> ); } diff --git a/src/components/ui/CommonButton.tsx b/src/components/ui/CommonButton.tsx index b8c02c5..cb1fd25 100644 --- a/src/components/ui/CommonButton.tsx +++ b/src/components/ui/CommonButton.tsx @@ -5,7 +5,7 @@ import { Button } from '@mui/material'; import { styled } from '@mui/material/styles'; import React from 'react'; -type ButtonVariant = 'black' | 'green' | 'disabled' | 'cancel'; +type ButtonVariant = 'black' | 'green' | 'disabled' | 'cancel' | 'retry'; interface CommonButtonProps extends Omit { children: React.ReactNode; @@ -50,6 +50,17 @@ const StyledButton = styled(Button, { }; } + if (buttonVariant === 'retry') { + return { + ...baseStyle, + backgroundColor: '#ff4444', + color: '#ffffff', + '&:hover': { + backgroundColor: '#cc3333', + }, + }; + } + const isBlack = buttonVariant === 'black'; return { diff --git a/src/components/ui/HalfCircleProgress.tsx b/src/components/ui/HalfCircleProgress.tsx index 63ab226..2b337de 100644 --- a/src/components/ui/HalfCircleProgress.tsx +++ b/src/components/ui/HalfCircleProgress.tsx @@ -63,7 +63,7 @@ export default function HalfCircleProgress({ marginLeft: '3px', }} > - {unitText} + {unitText} min diff --git a/src/components/ui/PaymentFailedModal.tsx b/src/components/ui/PaymentFailedModal.tsx new file mode 100644 index 0000000..18a0ae1 --- /dev/null +++ b/src/components/ui/PaymentFailedModal.tsx @@ -0,0 +1,75 @@ +// components/ui/PaymentFailedModal.tsx +'use client'; + +import CloseIcon from '@mui/icons-material/Close'; +import { Box, IconButton, Modal, Typography } from '@mui/material'; + +import CommonButton from '@/components/ui/CommonButton'; + +interface Props { + open: boolean; + onClose: () => void; + onRetryPayment: () => Promise; +} + +export default function PaymentFailedModal({ + open, + onClose, + onRetryPayment, +}: Props) { + return ( + + + + + + + + Payment Failed + + + Your subscription payment failed. Please update your payment method to continue using our service. + + + + + Close + + { + void onRetryPayment(); + }} + > + Update Payment Method + + + + + ); +} diff --git a/src/types/plan.types.ts b/src/types/plan.types.ts index 4daa458..ae742da 100644 --- a/src/types/plan.types.ts +++ b/src/types/plan.types.ts @@ -1,5 +1,5 @@ export type PlanTier = 'FREE' | 'BASIC' | 'PRO'; -export type ButtonVariant = 'primary' | 'secondary' | 'disabled' | 'cancel'; +export type ButtonVariant = 'primary' | 'secondary' | 'disabled' | 'cancel' | 'retry'; export interface PlanButton { label: string; From 7f0a73b478db901dfac00db2e52638dad88e88b3 Mon Sep 17 00:00:00 2001 From: "ewan.yxg@gmail.com" Date: Mon, 3 Nov 2025 09:26:30 +1100 Subject: [PATCH 3/5] feature/sub-stripe --- src/utils/subscriptionUtils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/utils/subscriptionUtils.ts b/src/utils/subscriptionUtils.ts index b25633c..131b490 100644 --- a/src/utils/subscriptionUtils.ts +++ b/src/utils/subscriptionUtils.ts @@ -67,3 +67,6 @@ export function getUsagePercentage( return Math.max(0, Math.min(100, (usedMinutes / totalMinutes) * 100)); } + + + From 31a0e757a211686a19a1e7f89af4ebf24ba9d760 Mon Sep 17 00:00:00 2001 From: Depeng Sun Date: Sat, 8 Nov 2025 19:04:14 +1030 Subject: [PATCH 4/5] Refactor code for improved readability and maintainability; remove unused husky prepare script from package.json, clean up imports and formatting in various components, and enhance subscription utility functions. --- package.json | 3 +- .../admin/billing/components/BillingCard.tsx | 1 - .../billing/components/BillingSection.tsx | 42 +++++++++++-------- .../overview/components/ActivitySection.tsx | 17 ++++---- src/app/auth/callback/AuthCallbackContent.tsx | 2 - src/components/ui/PaymentFailedModal.tsx | 3 +- src/components/ui/StatusChip.tsx | 2 +- src/features/overview/overviewApi.ts | 25 +---------- src/features/subscription/useSubscription.ts | 5 ++- .../twilioPhoneNumberApi.ts | 40 ++++++++++++++++++ src/redux/root-reducer.ts | 2 + src/redux/store.ts | 2 + src/types/plan.types.ts | 7 +++- src/types/subscription.d.ts | 9 +++- src/utils/subscriptionUtils.ts | 31 +++++++------- 15 files changed, 115 insertions(+), 76 deletions(-) create mode 100644 src/features/twilio-phone-number/twilioPhoneNumberApi.ts diff --git a/package.json b/package.json index 1bfccb3..e81df25 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,7 @@ "start": "next start", "lint": "next lint --fix", "type-check": "tsc --noEmit", - "test": "echo \"No tests yet\"", - "prepare": "husky install && (chmod -R +x .husky || echo 'Skipping chmod')" + "test": "echo \"No tests yet\"" }, "dependencies": { "@emotion/react": "^11.14.0", diff --git a/src/app/admin/billing/components/BillingCard.tsx b/src/app/admin/billing/components/BillingCard.tsx index 7221c56..e922c6d 100644 --- a/src/app/admin/billing/components/BillingCard.tsx +++ b/src/app/admin/billing/components/BillingCard.tsx @@ -1,7 +1,6 @@ 'use client'; import { styled } from '@mui/material/styles'; -import { padding } from '@mui/system'; import Image from 'next/image'; import CommonButton from '@/components/ui/CommonButton'; diff --git a/src/app/admin/billing/components/BillingSection.tsx b/src/app/admin/billing/components/BillingSection.tsx index 443bcbb..031e0a1 100644 --- a/src/app/admin/billing/components/BillingSection.tsx +++ b/src/app/admin/billing/components/BillingSection.tsx @@ -48,14 +48,15 @@ function getButtonsByPlan( isFailed: boolean, ): PlanButton[] { const isCurrent = plan._id === currentPlanId; - const isPendingDowngradeToPlan = isPendingDowngrade && plan._id === pendingPlanId; - + const isPendingDowngradeToPlan = + isPendingDowngrade && plan._id === pendingPlanId; + if (isCancelled) { if (plan.tier === 'FREE') return [{ label: 'Your current plan', variant: 'disabled' }]; return [{ label: `Go with ${plan.tier}`, variant: 'primary' }]; } - + if (isFailed) { // For failed subscriptions, show retry payment button for current plan if (isCurrent) { @@ -64,7 +65,7 @@ function getButtonsByPlan( // For other plans, disable them during payment failure return [{ label: `Go with ${plan.tier}`, variant: 'disabled' }]; } - + if (isSubscribed) { // Current plan display if (isCurrent) { @@ -77,7 +78,7 @@ function getButtonsByPlan( } return [{ label: 'Cancel Subscription', variant: 'cancel' }]; } - + // Special handling for pending downgrade if (isPendingDowngrade) { if (isPendingDowngradeToPlan) { @@ -88,15 +89,15 @@ function getButtonsByPlan( return [{ label: `Go with ${plan.tier}`, variant: 'primary' }]; } } - + // If pending cancellation, show "Go with" to allow reactivation if (isPendingCancellation) { return [{ label: `Go with ${plan.tier}`, variant: 'primary' }]; } - + return [{ label: `Switch to ${plan.tier}`, variant: 'primary' }]; } - + return [{ label: 'Try for Free', variant: 'primary' }]; } @@ -114,9 +115,16 @@ export default function BillingSection() { const { change } = useChangePlan(); const { downgrade } = useDowngradeToFree(); const { retryPayment } = useRetryPayment(); - const { subscription, isSubscribed, isCancelled, isFailed, isPendingCancellation, isPendingDowngrade, currentPlanId } = - useSubscription(); - + const { + subscription, + isSubscribed, + isCancelled, + isFailed, + isPendingCancellation, + isPendingDowngrade, + currentPlanId, + } = useSubscription(); + const pendingPlanId = subscription?.pendingPlanId?._id; const tierOrder = { FREE: 0, BASIC: 1, PRO: 2 }; @@ -127,7 +135,6 @@ export default function BillingSection() { { /* slide */ } - const [currentSlide, setCurrentSlide] = useState(0); const [sliderRef, slider] = useKeenSlider({ slides: { perView: 'auto', spacing: 0, origin: 'center' }, rubberband: false, @@ -136,9 +143,6 @@ export default function BillingSection() { '(min-width: 1000px)': { slides: { perView: 2, spacing: 0 } }, '(min-width: 1420px)': { slides: { perView: 2, spacing: 0 } }, }, - slideChanged(sliderInstance) { - setCurrentSlide(sliderInstance.track.details.rel); - }, }); useEffect(() => { @@ -159,7 +163,10 @@ export default function BillingSection() { if (label.startsWith('Go with')) { if (!subscription || subscription.status === 'cancelled') { await create(planId); - } else if (subscription.status === 'pending_cancellation' || subscription.status === 'pending_downgrade') { + } else if ( + subscription.status === 'pending_cancellation' || + subscription.status === 'pending_downgrade' + ) { // If pending cancellation or downgrade, use change to cancel the pending change and switch plan await change(planId); window.location.reload(); @@ -210,7 +217,6 @@ export default function BillingSection() { } }; - return ( setShowCancelModal(false)} onConfirm={handleConfirmCancel} /> - + setShowPaymentFailedModal(false)} diff --git a/src/app/admin/overview/components/ActivitySection.tsx b/src/app/admin/overview/components/ActivitySection.tsx index 55a1e2e..2e2f999 100644 --- a/src/app/admin/overview/components/ActivitySection.tsx +++ b/src/app/admin/overview/components/ActivitySection.tsx @@ -7,15 +7,14 @@ import Image from 'next/image'; import HalfCircleProgress from '@/components/ui/HalfCircleProgress'; import { useGetTodayMetricsQuery } from '@/features/callog/calllogApi'; -import { useGetTwilioPhoneNumberQuery } from '@/features/overview/overviewApi'; import { useGetBookingsQuery } from '@/features/service/serviceBookingApi'; import { useSubscription } from '@/features/subscription/useSubscription'; +import { useGetTwilioPhoneNumberQuery } from '@/features/twilio-phone-number/twilioPhoneNumberApi'; import { useAppSelector } from '@/redux/hooks'; import { getPlanTier, isFreeOrBasicPlan } from '@/utils/planUtils'; -import { - getRemainingMinutes, - getTotalMinutes, - getUsagePercentage +import { + getRemainingMinutes, + getTotalMinutes, } from '@/utils/subscriptionUtils'; function formatSubscriptionPeriod( @@ -168,13 +167,13 @@ export default function ActivitySection() { // Calculate minutes data for the progress circle const remainingMinutes = getRemainingMinutes(subscription); const totalMinutes = getTotalMinutes(subscription?.planId); - const usedMinutes = totalMinutes - remainingMinutes; - const usagePercentage = getUsagePercentage(subscription, subscription?.planId); - + // Determine display values and unit text const isUnlimited = totalMinutes === Number.MAX_SAFE_INTEGER; const displayValue = remainingMinutes; // Always show remaining minutes - const displayMaxValue = isUnlimited ? Math.max(remainingMinutes, 1000) : totalMinutes; + const displayMaxValue = isUnlimited + ? Math.max(remainingMinutes, 1000) + : totalMinutes; const unitText = isUnlimited ? '/Unlimited' : `/${totalMinutes}`; const { data: bookings } = useGetBookingsQuery({ userId }, { skip: !userId }); diff --git a/src/app/auth/callback/AuthCallbackContent.tsx b/src/app/auth/callback/AuthCallbackContent.tsx index 12afc63..51636c3 100644 --- a/src/app/auth/callback/AuthCallbackContent.tsx +++ b/src/app/auth/callback/AuthCallbackContent.tsx @@ -49,8 +49,6 @@ export default function AuthCallbackContent() { // Clear any persisted auth state to prevent old user ID from being used localStorage.removeItem('persist:root'); - console.log('[AuthCallback] Setting user with ID:', parsedUser._id); - dispatch( setCredentials({ csrfToken, diff --git a/src/components/ui/PaymentFailedModal.tsx b/src/components/ui/PaymentFailedModal.tsx index 18a0ae1..8189269 100644 --- a/src/components/ui/PaymentFailedModal.tsx +++ b/src/components/ui/PaymentFailedModal.tsx @@ -47,7 +47,8 @@ export default function PaymentFailedModal({ Payment Failed - Your subscription payment failed. Please update your payment method to continue using our service. + Your subscription payment failed. Please update your payment method to + continue using our service. diff --git a/src/components/ui/StatusChip.tsx b/src/components/ui/StatusChip.tsx index 98b67f1..4762e82 100644 --- a/src/components/ui/StatusChip.tsx +++ b/src/components/ui/StatusChip.tsx @@ -30,7 +30,7 @@ const StatusChip = ({ status }: Props) => { dot: '#999999', text: status || 'Unknown', }; - + const { bg, dot, text } = statusConfig; return ( diff --git a/src/features/overview/overviewApi.ts b/src/features/overview/overviewApi.ts index e313c71..08d26c0 100644 --- a/src/features/overview/overviewApi.ts +++ b/src/features/overview/overviewApi.ts @@ -19,18 +19,6 @@ export interface Service { updatedAt: string; } -interface TwilioPhoneNumberResponse { - twilioPhoneNumber: string; -} - -interface User { - _id: string; - firstName: string; - lastName: string; - email: string; - twilioPhoneNumber?: string; -} - export const overviewApi = createApi({ reducerPath: 'overviewApi', baseQuery: axiosBaseQuery(), @@ -44,18 +32,7 @@ export const overviewApi = createApi({ }), providesTags: ['Service'], }), - - getTwilioPhoneNumber: builder.query({ - query: userId => ({ - url: `/users/${userId}`, - method: 'GET', - }), - transformResponse: (response: User): TwilioPhoneNumberResponse => ({ - twilioPhoneNumber: response.twilioPhoneNumber ?? '', - }), - }), }), }); -export const { useGetRecentServicesQuery, useGetTwilioPhoneNumberQuery } = - overviewApi; +export const { useGetRecentServicesQuery } = overviewApi; diff --git a/src/features/subscription/useSubscription.ts b/src/features/subscription/useSubscription.ts index 2d2468a..e914fd4 100644 --- a/src/features/subscription/useSubscription.ts +++ b/src/features/subscription/useSubscription.ts @@ -21,7 +21,10 @@ export const useSubscription = () => { refetch, } = useGetSubscriptionByUserQuery(userId!, { skip: !userId }); - const isSubscribed = subscription?.status === 'active' || subscription?.status === 'pending_cancellation' || subscription?.status === 'pending_downgrade'; + const isSubscribed = + subscription?.status === 'active' || + subscription?.status === 'pending_cancellation' || + subscription?.status === 'pending_downgrade'; const isCancelled = subscription?.status === 'cancelled' || !subscription; const isFailed = subscription?.status === 'failed'; const isPendingCancellation = subscription?.status === 'pending_cancellation'; diff --git a/src/features/twilio-phone-number/twilioPhoneNumberApi.ts b/src/features/twilio-phone-number/twilioPhoneNumberApi.ts new file mode 100644 index 0000000..a4a718d --- /dev/null +++ b/src/features/twilio-phone-number/twilioPhoneNumberApi.ts @@ -0,0 +1,40 @@ +import { createApi } from '@reduxjs/toolkit/query/react'; + +import { axiosBaseQuery } from '@/lib/axiosBaseQuery'; + +export interface TwilioPhoneNumberResponse { + twilioPhoneNumber: string; +} + +interface User { + _id: string; + firstName: string; + lastName: string; + email: string; + twilioPhoneNumber?: string; +} + +export const twilioPhoneNumberApi = createApi({ + reducerPath: 'twilioPhoneNumberApi', + baseQuery: axiosBaseQuery(), + tagTypes: ['TwilioPhoneNumber'], + endpoints: builder => ({ + getTwilioPhoneNumber: builder.query({ + query: userId => ({ + url: `/users/${userId}`, + method: 'GET', + }), + transformResponse: (response: User): TwilioPhoneNumberResponse => ({ + twilioPhoneNumber: response.twilioPhoneNumber ?? '', + }), + providesTags: ['TwilioPhoneNumber'], + }), + }), +}); + +// Export hooks +export const { useGetTwilioPhoneNumberQuery } = twilioPhoneNumberApi; + +// Export raw endpoints +export const getTwilioPhoneNumber = + twilioPhoneNumberApi.endpoints.getTwilioPhoneNumber.initiate; diff --git a/src/redux/root-reducer.ts b/src/redux/root-reducer.ts index 214c408..6be7dcc 100644 --- a/src/redux/root-reducer.ts +++ b/src/redux/root-reducer.ts @@ -17,6 +17,7 @@ import { subscriptionApi } from '@/features/subscription/subscriptionApi'; import { testApi } from '@/features/test/testApiSlice'; import { transcriptApi } from '@/features/transcript/transcriptApi'; import { transcriptChunksApi } from '@/features/transcript-chunk/transcriptChunksApi'; +import { twilioPhoneNumberApi } from '@/features/twilio-phone-number/twilioPhoneNumberApi'; export const rootReducer = combineReducers({ auth: authReducer, @@ -35,6 +36,7 @@ export const rootReducer = combineReducers({ [serviceBookingApi.reducerPath]: serviceBookingApi.reducer, [serviceApi.reducerPath]: serviceApi.reducer, [serviceManagementApi.reducerPath]: serviceManagementApi.reducer, + [twilioPhoneNumberApi.reducerPath]: twilioPhoneNumberApi.reducer, }); export type RootState = ReturnType; diff --git a/src/redux/store.ts b/src/redux/store.ts index 1c14f03..b17cdca 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -26,6 +26,7 @@ import { subscriptionApi } from '@/features/subscription/subscriptionApi'; import { testApi } from '@/features/test/testApiSlice'; import { transcriptApi } from '@/features/transcript/transcriptApi'; import { transcriptChunksApi } from '@/features/transcript-chunk/transcriptChunksApi'; +import { twilioPhoneNumberApi } from '@/features/twilio-phone-number/twilioPhoneNumberApi'; import { rootReducer } from './root-reducer'; @@ -60,6 +61,7 @@ export const store = configureStore({ serviceBookingApi.middleware, serviceApi.middleware, serviceManagementApi.middleware, + twilioPhoneNumberApi.middleware, ), }); diff --git a/src/types/plan.types.ts b/src/types/plan.types.ts index ae742da..9786f15 100644 --- a/src/types/plan.types.ts +++ b/src/types/plan.types.ts @@ -1,5 +1,10 @@ export type PlanTier = 'FREE' | 'BASIC' | 'PRO'; -export type ButtonVariant = 'primary' | 'secondary' | 'disabled' | 'cancel' | 'retry'; +export type ButtonVariant = + | 'primary' + | 'secondary' + | 'disabled' + | 'cancel' + | 'retry'; export interface PlanButton { label: string; diff --git a/src/types/subscription.d.ts b/src/types/subscription.d.ts index 938a63b..b938cd0 100644 --- a/src/types/subscription.d.ts +++ b/src/types/subscription.d.ts @@ -17,11 +17,16 @@ export interface Subscription { subscriptionId: string; stripeCustomerId: string; chargeId: string; - status: 'active' | 'cancelled' | 'failed' | 'pending_cancellation' | 'pending_downgrade'; + status: + | 'active' + | 'cancelled' + | 'failed' + | 'pending_cancellation' + | 'pending_downgrade'; startAt: string; endAt: string; createdAt: string; - secondsLeft: number; + secondsLeft: number; } export interface RawInvoice { diff --git a/src/utils/subscriptionUtils.ts b/src/utils/subscriptionUtils.ts index 131b490..062e2d4 100644 --- a/src/utils/subscriptionUtils.ts +++ b/src/utils/subscriptionUtils.ts @@ -2,7 +2,9 @@ * Extract numeric minutes from callMinutes string * Handles various formats like "100 Min/Month", "Unlimited", "100", etc. */ -export function extractMinutesFromCallMinutes(callMinutes: string | number): number { +export function extractMinutesFromCallMinutes( + callMinutes: string | number, +): number { // If it's already a number, return it if (typeof callMinutes === 'number') { return callMinutes; @@ -10,18 +12,18 @@ export function extractMinutesFromCallMinutes(callMinutes: string | number): num // Handle string formats const str = callMinutes.toString().toLowerCase(); - + // Check for unlimited if (str.includes('unlimited') || str.includes('∞')) { return Number.MAX_SAFE_INTEGER; // Use a very large number for unlimited } - + // Extract number from string using regex - const match = str.match(/(\d+)/); + const match = /(\d+)/.exec(str); if (match) { return parseInt(match[1], 10); } - + // Default fallback return 0; } @@ -36,7 +38,9 @@ export function secondsToMinutes(seconds: number): number { /** * Get remaining minutes from subscription */ -export function getRemainingMinutes(subscription: { secondsLeft: number } | null | undefined): number { +export function getRemainingMinutes( + subscription: { secondsLeft: number } | null | undefined, +): number { if (!subscription) return 0; return secondsToMinutes(subscription.secondsLeft); } @@ -44,7 +48,9 @@ export function getRemainingMinutes(subscription: { secondsLeft: number } | null /** * Get total minutes from plan */ -export function getTotalMinutes(plan: { features: { callMinutes: string | number } } | null | undefined): number { +export function getTotalMinutes( + plan: { features: { callMinutes: string | number } } | null | undefined, +): number { if (!plan) return 0; return extractMinutesFromCallMinutes(plan.features.callMinutes); } @@ -54,19 +60,16 @@ export function getTotalMinutes(plan: { features: { callMinutes: string | number */ export function getUsagePercentage( subscription: { secondsLeft: number } | null | undefined, - plan: { features: { callMinutes: string | number } } | null | undefined + plan: { features: { callMinutes: string | number } } | null | undefined, ): number { if (!subscription || !plan) return 0; - + const totalMinutes = getTotalMinutes(plan); const remainingMinutes = getRemainingMinutes(subscription); const usedMinutes = totalMinutes - remainingMinutes; - + if (totalMinutes === 0) return 0; if (totalMinutes === Number.MAX_SAFE_INTEGER) return 0; // Unlimited plan - + return Math.max(0, Math.min(100, (usedMinutes / totalMinutes) * 100)); } - - - From 95e8208fd5e00fa128d97416f22bffeacf0c325e Mon Sep 17 00:00:00 2001 From: "ewan.yxg@gmail.com" Date: Mon, 10 Nov 2025 11:52:14 +1100 Subject: [PATCH 5/5] . --- src/utils/subscriptionUtils.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/utils/subscriptionUtils.ts b/src/utils/subscriptionUtils.ts index 062e2d4..8dade1c 100644 --- a/src/utils/subscriptionUtils.ts +++ b/src/utils/subscriptionUtils.ts @@ -73,3 +73,11 @@ export function getUsagePercentage( return Math.max(0, Math.min(100, (usedMinutes / totalMinutes) * 100)); } + + + + + + + +