Repository: fleek-platform/dashboard
Pull request #45: feat: 🎸 Adds credits to billing flows
Author: login: angarita-dev
Created: 2025-05-06T03:08:19Z | Merged: 2025-05-07T16:12:28Z

## Why?

- Enables payment of pro plan with crypto
  - If the user has credits it tries creating a subscription to the pro plan, which should deduct from credits.
  > Note that currently the pro plan has a free trial, which is going to be removed.
- Displays credits in billing information
- Allows toping credits

## How?


## Tickets?

- [Ticket 1](the-ticket-url-here)
- [Ticket 2](the-ticket-url-here)
- [Ticket 3](the-ticket-url-here)

## Contribution checklist?

- [x] The commit messages are detailed
- [x] The `build` command runs locally
- [ ] Assets or static content are linked and stored in the project
- [ ] You've reviewed spelling using a grammar checker
- [ ] For documentation, guides or references, you've tested the commands and steps
- [ ] You've done enough research before writing

## Security checklist?

- [ ] Sensitive data has been identified and is being protected properly
- [ ] Injection has been prevented (parameterized queries, no eval or system calls)
- [ ] The Components are escaping output (to prevent XSS)

## References?

Optionally, provide references such as links

## Preview?


https://github.com/user-attachments/assets/3c27b643-e2eb-4d2e-8078-07728fd25886




Diff:
diff --git a/.changeset/sharp-walls-hide.md b/.changeset/sharp-walls-hide.md
new file mode 100644
index 00000000..3cea6d26
--- /dev/null
+++ b/.changeset/sharp-walls-hide.md
@@ -0,0 +1,5 @@
+---
+"@fleek-platform/dashboard": patch
+---
+
+Adds credit support
diff --git a/src/components/Billing/HorizontalPlanCard.tsx b/src/components/Billing/HorizontalPlanCard.tsx
index 4f7e268e..2a370bb1 100644
--- a/src/components/Billing/HorizontalPlanCard.tsx
+++ b/src/components/Billing/HorizontalPlanCard.tsx
@@ -11,7 +11,7 @@ export const HorizontalPlanCard: React.FC<HorizontalPlanCardProps> = ({
   ...headerProps
 }) => {
   return (
-    <Box variant="container">
+    <Box variant="container" className="h-full justify-between">
       <Header {...headerProps} />
       {children}
     </Box>
diff --git a/src/components/LegacyPlanUpgradeModal/LegacyPlanUpgradeModal.tsx b/src/components/LegacyPlanUpgradeModal/LegacyPlanUpgradeModal.tsx
index 59e0727c..29043ae7 100644
--- a/src/components/LegacyPlanUpgradeModal/LegacyPlanUpgradeModal.tsx
+++ b/src/components/LegacyPlanUpgradeModal/LegacyPlanUpgradeModal.tsx
@@ -29,10 +29,19 @@ export const LegacyPlanUpgradeModal = () => {
   const checkout = useFleekCheckout();
   const toast = useToast();
   const [isLoading, setLoading] = useState(false);
-  const { billingPlanType, loading: billingPlanTypeLoading } = useBillingContext();
+  const {
+    billingPlanType,
+    loading: billingPlanTypeLoading,
+    subscription,
+    team,
+  } = useBillingContext();
 
   useEffect(() => {
-    if (billingPlanTypeLoading || billingPlanType !== 'none' && billingPlanType !== 'free') return;
+    if (
+      billingPlanTypeLoading ||
+      (billingPlanType !== 'none' && billingPlanType !== 'free')
+    )
+      return;
     const shown = localStorage.getItem(shownKey);
     if (shown) return;
     setIsOpen(true);
@@ -53,7 +62,13 @@ export const LegacyPlanUpgradeModal = () => {
     setLoading(true);
     try {
       const response = await checkout.mutateAsync();
-      window.location.href = response.url;
+
+      if (response.type === 'CHECKOUT') {
+        window.location.href = response.content.url;
+      }
+
+      await Promise.all([subscription.refetch(), team.refetch()]);
+      toast.success({ message: 'Subscribed successfully!' });
     } catch (error) {
       toast.error({ error, log: 'Error upgrading plan. Please try again' });
     }
diff --git a/src/components/ftw/RootLayout/RootLayout.tsx b/src/components/ftw/RootLayout/RootLayout.tsx
index 5d683f2c..54cdbad7 100644
--- a/src/components/ftw/RootLayout/RootLayout.tsx
+++ b/src/components/ftw/RootLayout/RootLayout.tsx
@@ -32,6 +32,8 @@ import { LayoutHead } from '../../LayoutHead/LayoutHead';
 import { AccountDropdown } from '../AccountDropdown/AccountDropdown';
 import { Announcement } from '../Announcement/Announcement';
 import { BreadcrumbItem, Breadcrumbs } from '../Breadcrumbs/Breadcrumbs';
+import { useCredits, useCreditsCheckout } from '@/hooks/useCredits';
+import { usePermissions } from '@/hooks/usePermissions';
 
 export type NavigationItem = {
   icon: IconName;
@@ -174,6 +176,38 @@ const VersionTagsWrapper: React.FC = () => {
   return <VersionTags />;
 };
 
+const Credits = () => {
+  const { credits, isCreditsLoading } = useCredits();
+  const { handleAddCredits, isCreatingCheckout } = useCreditsCheckout();
+
+  return (
+    <Box className="flex-row justify-between items-center border border-neutral-6 bg-neutral-2 rounded-t-lg p-3">
+      <Text
+        className="text-neutral-12"
+        variant="tertiary"
+        size="sm"
+        weight={500}
+      >
+        {isCreditsLoading ? (
+          <Skeleton variant="text" />
+        ) : (
+          credits.positiveFormatted
+        )}
+      </Text>
+      <Button
+        className="py-0 h-auto"
+        variant="outline"
+        intent="ghost"
+        onClick={handleAddCredits as () => void}
+      >
+        <Text className="text-neutral-11" variant="tertiary" size="sm">
+          {isCreatingCheckout ? <Icon name="spinner" /> : 'Buy credits'}
+        </Text>
+      </Button>
+    </Box>
+  );
+};
+
 type SidebarProps = {
   slotSidebar: React.ReactNode;
   navigation: NavigationItem[];
@@ -185,6 +219,10 @@ const Sidebar: React.FC<SidebarProps> = ({
   navigation,
   isNavigationLoading,
 }) => {
+  const hasBillingPermission = usePermissions({
+    action: [constants.PERMISSION.BILLING.MANAGE],
+  });
+
   return (
     <Box
       className="w-[15.938rem] pt-4 pb-2.5 px-3 gap-2 justify-between shrink-0 h-full"
@@ -235,16 +273,27 @@ const Sidebar: React.FC<SidebarProps> = ({
       <Box className="gap-2.5">
         <VersionTagsWrapper />
         <Announcement />
-        <Box className="border border-neutral-6 py-2 pt-2.5 rounded-lg">
-          <FeedbackModalLink />
-          <ExternalLinkWrapper href={constants.EXTERNAL_LINK.FLEEK_DOCS}>
-            Documentation
-          </ExternalLinkWrapper>
-          <ExternalLinkWrapper href={constants.EXTERNAL_LINK.FLEEK_SUPPORT}>
-            Support
-          </ExternalLinkWrapper>
-          <Box className="h-[1px] my-2.5 mx-3 bg-neutral-6" />
-          <AccountDropdown />
+
+        <Box className="gap-0">
+          {hasBillingPermission && <Credits />}
+          <Box
+            className={cn(
+              'border-neutral-6 py-2 pt-2.5 rounded-b-lg',
+              hasBillingPermission
+                ? 'border-b border-x rounded-b-lg'
+                : 'border rounded-lg',
+            )}
+          >
+            <FeedbackModalLink />
+            <ExternalLinkWrapper href={constants.EXTERNAL_LINK.FLEEK_DOCS}>
+              Documentation
+            </ExternalLinkWrapper>
+            <ExternalLinkWrapper href={constants.EXTERNAL_LINK.FLEEK_SUPPORT}>
+              Support
+            </ExternalLinkWrapper>
+            <Box className="h-[1px] my-2.5 mx-3 bg-neutral-6" />
+            <AccountDropdown />
+          </Box>
         </Box>
       </Box>
     </Box>
diff --git a/src/fragments/Projects/Settings/Sections/Billing/BillingPlan/BillingPlan.tsx b/src/fragments/Projects/Settings/Sections/Billing/BillingPlan/BillingPlan.tsx
index de2961e6..431618a6 100644
--- a/src/fragments/Projects/Settings/Sections/Billing/BillingPlan/BillingPlan.tsx
+++ b/src/fragments/Projects/Settings/Sections/Billing/BillingPlan/BillingPlan.tsx
@@ -13,7 +13,6 @@ import { constants } from '@/constants';
 import { useCancelMockedMutation } from '@/hooks/useCancelSubscription';
 import { useFleekCheckout } from '@/hooks/useFleekCheckout';
 import { usePermissions } from '@/hooks/usePermissions';
-import { useRouter } from '@/hooks/useRouter';
 import { useToast } from '@/hooks/useToast';
 import { useBillingContext } from '@/providers/BillingProvider';
 import type { BillingPlanType } from '@/types/Billing';
@@ -23,11 +22,12 @@ import { dateFormat } from '@/utils/dateFormats';
 
 import { CancelPlanModal } from './CancelPlanModal';
 import { getDefined } from '@/defined';
+import { useCreditsCheckout } from '@/hooks/useCredits';
 
 export const BillingPlan: React.FC<LoadingProps> = ({ isLoading }) => {
   const toast = useToast();
-  const router = useRouter();
-  const { subscription, billingPlanType, paymentMethod } = useBillingContext();
+  const { subscription, billingPlanType, paymentMethod, team } =
+    useBillingContext();
 
   const [isCancelModalOpen, setIsCancelModalOpen] = useState(false);
 
@@ -53,7 +53,14 @@ export const BillingPlan: React.FC<LoadingProps> = ({ isLoading }) => {
       // TODO: This fails for some reason
       // router.replace(response.url);
 
-      window.location.href = response.url;
+      if (response.type === 'CHECKOUT') {
+        window.location.href = response.content.url;
+
+        return;
+      }
+
+      await Promise.all([subscription.refetch(), team.refetch()]);
+      toast.success({ message: 'Subscribed successfully!' });
     } catch (error) {
       toast.error({ error, log: 'Error upgrading plan. Please try again' });
     }
@@ -71,17 +78,6 @@ export const BillingPlan: React.FC<LoadingProps> = ({ isLoading }) => {
     }
   };
 
-  const endPlanDate = useMemo(() => {
-    if (subscription.data?.endDate) {
-      return dateFormat({
-        dateISO: subscription.data.endDate,
-        format: DateTime.DATE_FULL,
-      });
-    }
-
-    return '';
-  }, [subscription.data?.endDate]);
-
   const endPeriodDate = useMemo(() => {
     if (subscription.data?.periodEndDate) {
       return dateFormat({
@@ -93,57 +89,19 @@ export const BillingPlan: React.FC<LoadingProps> = ({ isLoading }) => {
     return '';
   }, [subscription.data?.periodEndDate]);
 
-  const shouldShowCancellationBanner = useMemo(() => {
-    if (subscription.data?.endDate) {
-      const targetTime = DateTime.fromISO(subscription.data.endDate);
-      const currentTime = DateTime.now();
-      const diff = targetTime.diff(currentTime);
-
-      return Math.floor(diff.as('months')) <= 1;
-    }
-
-    return null;
-  }, [subscription.data?.endDate]);
-
-  const trialEndDate = useMemo(() => {
-    if (subscription.data?.trialEndDate) {
-      return dateFormat({
-        dateISO: subscription.data.trialEndDate,
-        format: DateTime.DATE_FULL,
-      });
-    }
-
-    return '';
-  }, [subscription.data?.trialEndDate]);
-
   const { title, description, price } = getPlanData(
     billingPlanType,
     endPeriodDate,
   );
 
   return (
-    <>
+    <Box className="w-full">
       <CancelPlanModal
         isOpen={isCancelModalOpen}
         onOpenChange={setIsCancelModalOpen}
         onCancelPlan={handleCancelPlan}
         dueDate={endPeriodDate}
       />
-      {shouldShowCancellationBanner && !isLoading && (
-        <AlertBox size="sm" className="font-medium">
-          Your Pro Plan is expiring. You will be converted to a Free plan on{' '}
-          {endPlanDate}.
-        </AlertBox>
-      )}
-
-      {billingPlanType === 'trial' && !isLoading && (
-        <AlertBox size="sm" className="font-medium">
-          Your trial period expires on {trialEndDate}.{' '}
-          {paymentMethod.data?.id
-            ? 'You will be billed after that date.'
-            : 'Be sure to add your billing info before your trial ends.'}
-        </AlertBox>
-      )}
       <Billing.HorizontalPlanCard
         isLoading={isLoading}
         title={title}
@@ -172,7 +130,7 @@ export const BillingPlan: React.FC<LoadingProps> = ({ isLoading }) => {
           </Box>
         </SettingsBox.ActionRow>
       </Billing.HorizontalPlanCard>
-    </>
+    </Box>
   );
 };
 
@@ -192,6 +150,7 @@ const ButtonsContainer: React.FC<ButtonsContainerProps> = ({
   onCancelPlan,
 }) => {
   const [isLoading, setIsLoading] = useState(false);
+  const { handleAddCredits, isCreatingCheckout } = useCreditsCheckout();
   const hasManageBillingPermission = usePermissions({
     action: [constants.PERMISSION.BILLING.MANAGE],
   });
@@ -244,6 +203,13 @@ const ButtonsContainer: React.FC<ButtonsContainerProps> = ({
           {billingPlanType === 'trial' ? 'Add billing info' : 'Upgrade to Pro'}
         </Button>
       </PermissionsTooltip>
+      <Button
+        size="sm"
+        onClick={handleAddCredits as () => void}
+        loading={isCreatingCheckout}
+      >
+        Add credits
+      </Button>
     </>
   );
 };
@@ -283,3 +249,60 @@ const getPlanData = (plan: BillingPlanType, endPlanDate?: string): PlanData => {
     },
   }[plan];
 };
+
+export const Banners = ({ isLoading }: LoadingProps) => {
+  const { subscription, billingPlanType, paymentMethod } = useBillingContext();
+  const shouldShowCancellationBanner = useMemo(() => {
+    if (subscription.data?.endDate) {
+      const targetTime = DateTime.fromISO(subscription.data.endDate);
+      const currentTime = DateTime.now();
+      const diff = targetTime.diff(currentTime);
+
+      return Math.floor(diff.as('months')) <= 1;
+    }
+
+    return null;
+  }, [subscription.data?.endDate]);
+
+  const trialEndDate = useMemo(() => {
+    if (subscription.data?.trialEndDate) {
+      return dateFormat({
+        dateISO: subscription.data.trialEndDate,
+        format: DateTime.DATE_FULL,
+      });
+    }
+
+    return '';
+  }, [subscription.data?.trialEndDate]);
+
+  const endPlanDate = useMemo(() => {
+    if (subscription.data?.endDate) {
+      return dateFormat({
+        dateISO: subscription.data.endDate,
+        format: DateTime.DATE_FULL,
+      });
+    }
+
+    return '';
+  }, [subscription.data?.endDate]);
+
+  return (
+    <>
+      {shouldShowCancellationBanner && !isLoading && (
+        <AlertBox size="sm" className="font-medium">
+          Your Pro Plan is expiring. You will be converted to a Free plan on{' '}
+          {endPlanDate}.
+        </AlertBox>
+      )}
+
+      {billingPlanType === 'trial' && !isLoading && (
+        <AlertBox size="sm" className="font-medium">
+          Your trial period expires on {trialEndDate}.{' '}
+          {paymentMethod.data?.id
+            ? 'You will be billed after that date.'
+            : 'Be sure to add your billing info before your trial ends.'}
+        </AlertBox>
+      )}
+    </>
+  );
+};
diff --git a/src/fragments/Projects/Settings/Sections/Billing/Credits/index.tsx b/src/fragments/Projects/Settings/Sections/Billing/Credits/index.tsx
new file mode 100644
index 00000000..7870c5b5
--- /dev/null
+++ b/src/fragments/Projects/Settings/Sections/Billing/Credits/index.tsx
@@ -0,0 +1,84 @@
+import { useCredits } from '@/hooks/useCredits';
+import { Box, Button, Icon, Text } from '@/ui';
+
+export const Credits = () => {
+  const { credits, isCreditsLoading, isCreditsError, refetchCredits } =
+    useCredits();
+
+  const handleRefetchCredits = () => {
+    refetchCredits();
+  };
+
+  if (isCreditsLoading) {
+    return (
+      <Box
+        variant="container"
+        className="flex justify-center items-center px-7 min-w-max"
+      >
+        <Text
+          variant="secondary"
+          size="2xl"
+          className="text-neutral-12"
+          weight={700}
+        >
+          <Icon name="spinner" />
+        </Text>
+        <Text variant="secondary" size="sm" className="text-neutral-11">
+          Fetching Credits
+        </Text>
+      </Box>
+    );
+  }
+
+  if (isCreditsError || !credits) {
+    return (
+      <Box
+        variant="container"
+        className="flex justify-center items-center px-7 min-w-max"
+      >
+        <Text
+          variant="secondary"
+          size="2xl"
+          className="text-danger-10"
+          weight={700}
+        >
+          <Icon name="alert-circled" />
+        </Text>
+
+        <Text
+          variant="secondary"
+          className="text-balance max-w-480 text-center"
+        >
+          Unexpected error fetching credits
+        </Text>
+
+        <Button
+          intent="neutral"
+          onClick={handleRefetchCredits}
+          disabled={isCreditsLoading}
+        >
+          Try again
+        </Button>
+      </Box>
+    );
+  }
+
+  return (
+    <Box
+      variant="container"
+      className="flex justify-center items-center px-7 min-w-max"
+    >
+      <Text
+        variant="secondary"
+        size="3xl"
+        className="text-neutral-12"
+        weight={700}
+      >
+        {credits.formatted}
+      </Text>
+      <Text variant="secondary" size="sm" className="text-neutral-11">
+        Credit balance
+      </Text>
+    </Box>
+  );
+};
diff --git a/src/fragments/Projects/Settings/Sections/Billing/Invoices/InvoiceListItem.tsx b/src/fragments/Projects/Settings/Sections/Billing/Invoices/InvoiceListItem.tsx
index c827964e..ad4ddbdf 100644
--- a/src/fragments/Projects/Settings/Sections/Billing/Invoices/InvoiceListItem.tsx
+++ b/src/fragments/Projects/Settings/Sections/Billing/Invoices/InvoiceListItem.tsx
@@ -3,6 +3,7 @@ import type { LoadingProps } from '@/types/Props';
 import { dateFormat } from '@/utils/dateFormats';
 
 import useInvoicesHelpers from './useInvoicesHelpers';
+import { formatCurrency } from '@/utils/formatCurrency';
 
 type InvoiceListItemProps = LoadingProps<{
   value: number;
@@ -29,7 +30,7 @@ const InvoiceListItem: React.FC<InvoiceListItemProps> = ({
   }
 
   const subtitle = `${formatDate(dateFrom)} - ${formatDate(dateTo)}`;
-  const formattedValue = formatMoney(value);
+  const formattedValue = formatCurrency(value);
 
   const handleDownload = () => {
     if (invoiceDocumentUrl) {
@@ -82,11 +83,6 @@ const InvoiceListItem: React.FC<InvoiceListItemProps> = ({
   );
 };
 
-const formatMoney = new Intl.NumberFormat('en-US', {
-  style: 'currency',
-  currency: 'USD',
-}).format;
-
 const formatDate = (date: string) =>
   dateFormat({ dateISO: date, stringFormat: 'MMM dd, yyyy' });
 
diff --git a/src/fragments/Projects/Settings/Settings.ts b/src/fragments/Projects/Settings/Settings.ts
index 63742492..5663acb7 100644
--- a/src/fragments/Projects/Settings/Settings.ts
+++ b/src/fragments/Projects/Settings/Settings.ts
@@ -3,8 +3,12 @@ import { AllowAccessFromOfacCountriesSwitch } from '@/fragments/Settings/AllowAc
 import { ExtendedLayout } from './ExtendedLayout';
 import { AddApplicationCredentials } from './Sections/ApplicationCredentials/AddApplicationCredentials';
 import { ManageApplicationCredentials } from './Sections/ApplicationCredentials/ManageApplicationCredentials';
-import { BillingPlan } from './Sections/Billing/BillingPlan/BillingPlan';
+import {
+  BillingPlan,
+  Banners,
+} from './Sections/Billing/BillingPlan/BillingPlan';
 import { CurrentUsage } from './Sections/Billing/CurrentUsage/CurrentUsage';
+import { Credits } from './Sections/Billing/Credits';
 import { Invoices } from './Sections/Billing/Invoices/Invoices';
 import { PaymentMethod } from './Sections/Billing/PaymentMethod/PaymentMethod';
 import { DeleteProject } from './Sections/DeleteProject/DeleteProject';
@@ -61,7 +65,9 @@ export const Settings = {
     },
 
     Billing: {
+      Credits,
       CurrentUsage,
+      Banners: Banners,
       Plan: BillingPlan,
       PaymentMethod,
       Invoices,
diff --git a/src/fragments/Site/Onchain/EnsRecords/EnsRecords.styles.ts b/src/fragments/Site/Onchain/EnsRecords/EnsRecords.styles.ts
index 656f0308..da0e7fd8 100644
--- a/src/fragments/Site/Onchain/EnsRecords/EnsRecords.styles.ts
+++ b/src/fragments/Site/Onchain/EnsRecords/EnsRecords.styles.ts
@@ -1,4 +1,4 @@
-import { Modal } from '@/components';
+import { Modal } from '@/components/Modal/Modal';
 import { styled } from '@/theme';
 import { Box, Icon, Text } from '@/ui';
 
diff --git a/src/hooks/useCredits.ts b/src/hooks/useCredits.ts
new file mode 100644
index 00000000..c96ed773
--- /dev/null
+++ b/src/hooks/useCredits.ts
@@ -0,0 +1,146 @@
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { useToast } from './useToast';
+import { openPopUpWindow } from '@/utils/openPopUpWindow';
+import { formatCurrency } from '@/utils/formatCurrency';
+import { useCookies } from '@/providers/CookiesProvider';
+import { BackendApiClient } from '@/integrations/new-be/BackendApi';
+import { useState } from 'react';
+
+type Credits = {
+  balance: number;
+  projectId: string;
+  teamId: string;
+};
+
+type CreditsCheckoutResponse = {
+  url: string;
+};
+
+export const useCreditsCheckout = () => {
+  const cookies = useCookies();
+  const toast = useToast();
+  const [isCheckoutSessionOpen, setIsCheckoutSessionOpen] =
+    useState<boolean>(false);
+
+  const backendApi = new BackendApiClient({
+    accessToken: cookies.values.accessToken,
+  });
+
+  const createCreditsCheckout = async (): Promise<CreditsCheckoutResponse> => {
+    const url = '/api/v1/credits/checkout';
+
+    const response = await backendApi.fetch({
+      url,
+      method: 'POST',
+      body: JSON.stringify({ projectId: cookies.values.projectId }),
+    });
+
+    if (!response.ok) {
+      throw response.statusText;
+    }
+
+    return await response.json();
+  };
+
+  const { refetchCredits } = useCredits();
+
+  const createCheckoutMutation = useMutation({
+    mutationFn: createCreditsCheckout,
+  });
+
+  const handleAddCredits = async ({
+    onClose = () => {},
+  }: { onClose?: () => void } = {}) => {
+    try {
+      if (createCheckoutMutation.isLoading || isCheckoutSessionOpen) return;
+
+      const { url } =
+        createCheckoutMutation.data ||
+        (await createCheckoutMutation.mutateAsync());
+
+      if (!url) {
+        throw new Error('Failed to create checkout session for credits');
+      }
+
+      setIsCheckoutSessionOpen(true);
+
+      const popUp = openPopUpWindow({
+        url,
+        onClose: () => {
+          refetchCredits();
+          setIsCheckoutSessionOpen(false);
+          onClose();
+        },
+      });
+
+      if (!popUp) {
+        setIsCheckoutSessionOpen(false);
+        return;
+      }
+    } catch (error) {
+      toast.error({
+        message: 'Failed to create checkout session, please try again later.',
+      });
+    }
+  };
+
+  return {
+    createCheckout: createCheckoutMutation.mutate,
+    createCheckoutAsync: createCheckoutMutation.mutateAsync,
+    isCreatingCheckout:
+      createCheckoutMutation.isLoading || isCheckoutSessionOpen,
+    handleAddCredits,
+  };
+};
+
+export const useCredits = () => {
+  const cookies = useCookies();
+  const backendApi = new BackendApiClient({
+    accessToken: cookies.values.accessToken,
+  });
+
+  const getCredits = async (): Promise<Credits> => {
+    const url = `/api/v1/credits?projectId=${cookies.values.projectId}`;
+
+    const response = await backendApi.fetch({
+      url,
+    });
+
+    if (!response.ok) {
+      const errorText = await response.text();
+      throw new Error(`HTTP Error ${response.status}: ${errorText}`);
+    }
+
+    return response.json();
+  };
+
+  const {
+    data: credits,
+    isLoading: isCreditsLoading,
+    isError: isCreditsError,
+    refetch: refetchCredits,
+  } = useQuery({
+    queryKey: ['credits'],
+    queryFn: getCredits,
+    select: (data) => {
+      const balance = (data?.balance ?? 0) / 100;
+      const formatted = formatCurrency(balance);
+      const positiveBalance = Math.max(balance, 0);
+
+      return {
+        rawBalance: data?.balance,
+        balance,
+        formatted,
+        positiveBalance,
+        positiveFormatted: formatCurrency(positiveBalance),
+      };
+    },
+  });
+
+  return {
+    credits,
+    isCreditsLoading,
+    isCreditsError,
+    refetchCredits,
+  };
+};
diff --git a/src/hooks/useFleekCheckout.ts b/src/hooks/useFleekCheckout.ts
index 8c632716..c7a3405a 100644
--- a/src/hooks/useFleekCheckout.ts
+++ b/src/hooks/useFleekCheckout.ts
@@ -4,10 +4,11 @@ import { useMutation } from '@tanstack/react-query';
 
 import { BackendApiClient } from '@/integrations/new-be/BackendApi';
 import { useCookies } from '@/providers/CookiesProvider';
-import { CheckoutResponse, PlanResponse } from '@/types/Billing';
+import type { CheckoutResponse, PlanResponse } from '@/types/Billing';
 
 import { useRouter } from './useRouter';
 import { sleep } from '../utils/timeout';
+import { useCredits } from './useCredits';
 
 declare global {
   interface Window {
@@ -27,39 +28,84 @@ const getCookie = (name: string): string | null => {
 const FLEEK_CHECKOUT_MAX_RETRIES = 3;
 const FLEEK_CHECKOUT_RETRY_DELAY_MS = 1800;
 
+type MutationResult =
+  | {
+      content: CheckoutResponse;
+      type: 'CHECKOUT';
+    }
+  | { content: unknown; type: 'CREDITS' };
+
 export const useFleekCheckout = () => {
   const router = useRouter();
   const projectId = router.query.projectId!;
+  const { refetchCredits } = useCredits();
   const cookies = useCookies();
   const backendApi = new BackendApiClient({
     accessToken: cookies.values.accessToken,
   });
 
-  const checkout = async () => {
+  const checkout = async (): Promise<MutationResult> => {
     let attempts = 0;
 
     while (attempts < FLEEK_CHECKOUT_MAX_RETRIES) {
       try {
-        const plan = await backendApi.fetch({
+        const plans = await backendApi.fetch({
           url: '/api/v1/plans',
         });
 
-        if (!plan.ok) {
-          throw plan.statusText;
+        if (!plans.ok) {
+          throw plans.statusText;
         }
 
-        const PlansResponse: PlanResponse[] = await plan.json();
-        const planId = PlansResponse.find(
+        const PlansResponse: PlanResponse[] = await plans.json();
+        const plan = PlansResponse.find(
           (plan) => plan.name.toUpperCase() === 'PRO',
-        )?.id;
+        );
+
+        const planId = plan.id;
 
         if (!planId) {
           throw new Error('Plan not found');
         }
 
+        const { rawBalance } = (await refetchCredits()).data;
+
         const referralId =
           window.tolt_referral || getCookie('tolt_referral') || '';
 
+        // Create plan with credits
+        if (rawBalance >= plan?.price) {
+          const response = await backendApi.fetch({
+            url: '/api/v1/subscriptions',
+            method: 'POST',
+            body: JSON.stringify({
+              projectId,
+              product: {
+                id: planId,
+                quantity: 1,
+              },
+              metadata: {
+                referralId,
+              },
+            }),
+          });
+
+          if (!response.ok) {
+            throw new Error(
+              'There was an error trying to upgrade your plan. Please try again.',
+            );
+          }
+
+          await refetchCredits();
+
+          const result = await response.json();
+
+          return {
+            content: result,
+            type: 'CREDITS',
+          };
+        }
+
         const response = await backendApi.fetch({
           url: '/api/v1/subscriptions/checkout',
           method: 'POST',
@@ -81,18 +127,21 @@ export const useFleekCheckout = () => {
 
         const result: CheckoutResponse = await response.json();
 
-        return result;
+        return {
+          content: result,
+          type: 'CHECKOUT',
+        };
       } catch (error) {
         attempts++;
-      
+
         if (attempts >= FLEEK_CHECKOUT_MAX_RETRIES) {
           console.error(`Checkout failed after ${attempts} attempts:`, error);
           throw error;
         }
 
         await sleep(FLEEK_CHECKOUT_RETRY_DELAY_MS);
-      } 
-    }    
+      }
+    }
   };
 
   return useMutation({
diff --git a/src/pages/projects/[projectId]/billing/index.tsx b/src/pages/projects/[projectId]/billing/index.tsx
index 13bc43f0..f1676a02 100644
--- a/src/pages/projects/[projectId]/billing/index.tsx
+++ b/src/pages/projects/[projectId]/billing/index.tsx
@@ -1,6 +1,7 @@
 import { routes } from '@fleek-platform/utils-routes';
 import { useEffect, useRef } from 'react';
 
+import { Box } from '@/ui';
 import { ComingSoon } from '@/components';
 import { constants } from '@/constants';
 import { Projects } from '@/fragments';
@@ -46,7 +47,11 @@ const FeaturePage: React.FC = () => {
 
   return (
     <>
-      <Projects.Settings.Sections.Billing.Plan isLoading={session.loading} />
+      <Projects.Settings.Sections.Billing.Banners isLoading={session.loading} />
+      <Box className="flex-row gap-5 w-full">
+        <Projects.Settings.Sections.Billing.Plan isLoading={session.loading} />
+        <Projects.Settings.Sections.Billing.Credits />
+      </Box>
       <Projects.Settings.Sections.Billing.PaymentMethod
         isLoading={session.loading}
       />
diff --git a/src/types/Billing.d.ts b/src/types/Billing.d.ts
index d4aec0c0..9ceaaa7b 100644
--- a/src/types/Billing.d.ts
+++ b/src/types/Billing.d.ts
@@ -90,6 +90,7 @@ export type SubscriptionResponse = {
 export type PlanResponse = {
   id: string;
   name: string;
+  price: number;
   description: string;
   createdAt: string;
   updatedAt: string;
diff --git a/src/utils/formatCurrency.ts b/src/utils/formatCurrency.ts
new file mode 100644
index 00000000..87d5c8e1
--- /dev/null
+++ b/src/utils/formatCurrency.ts
@@ -0,0 +1,18 @@
+/**
+ * Formats a number into a currency string.
+ *
+ * @param value - The numeric value to format.
+ * @param currency - The currency code (e.g., 'USD', 'EUR'). Defaults to 'USD'.
+ * @param locale - The locale string (e.g., 'en-US', 'de-DE'). Defaults to 'navigator.language' or 'en-US'.
+ * @returns The formatted currency string.
+ */
+export function formatCurrency(
+  value: number,
+  currency = 'USD',
+  locale = navigator.language,
+): string {
+  return new Intl.NumberFormat(locale || 'en-US', {
+    style: 'currency',
+    currency,
+  }).format(value);
+}
diff --git a/src/utils/timeout.ts b/src/utils/timeout.ts
index c5374b00..a8c24f36 100644
--- a/src/utils/timeout.ts
+++ b/src/utils/timeout.ts
@@ -1 +1,2 @@
-export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
+export const sleep = (ms: number) =>
+  new Promise((resolve) => setTimeout(resolve, ms));


Reviews:
user: login: bcamarneiro
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T09:05:13Z
user: login: bcamarneiro
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T09:08:39Z
user: login: bcamarneiro
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T09:11:34Z
user: login: bcamarneiro
body: Left just a couple of suggestions, mostly because I don't love spacings like "bla bla bla{' '}more bla". But not a blocker
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T09:15:27Z
user: login: bcamarneiro
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T16:03:03Z

Review comments:
diff_hunk: @@ -244,6 +204,13 @@ const ButtonsContainer: React.FC<ButtonsContainerProps> = ({
           {billingPlanType === 'trial' ? 'Add billing info' : 'Upgrade to Pro'}
         </Button>
       </PermissionsTooltip>
+      <Button
+        size="sm"
+        onClick={() => handleAddCredits()}
path: src/fragments/Projects/Settings/Sections/Billing/BillingPlan/BillingPlan.tsx
user: login: bcamarneiro
body: ```suggestion
        onClick={handleAddCredits}
```
created_at: 2025-05-06T09:05:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -283,3 +250,60 @@ const getPlanData = (plan: BillingPlanType, endPlanDate?: string): PlanData => {
     },
   }[plan];
 };
+
+export const Banners = ({ isLoading }: LoadingProps) => {
+  const { subscription, billingPlanType, paymentMethod } = useBillingContext();
+  const shouldShowCancellationBanner = useMemo(() => {
+    if (subscription.data?.endDate) {
+      const targetTime = DateTime.fromISO(subscription.data.endDate);
+      const currentTime = DateTime.now();
+      const diff = targetTime.diff(currentTime);
+
+      return Math.floor(diff.as('months')) <= 1;
+    }
+
+    return null;
+  }, [subscription.data?.endDate]);
+
+  const trialEndDate = useMemo(() => {
+    if (subscription.data?.trialEndDate) {
+      return dateFormat({
+        dateISO: subscription.data.trialEndDate,
+        format: DateTime.DATE_FULL,
+      });
+    }
+
+    return '';
+  }, [subscription.data?.trialEndDate]);
+
+  const endPlanDate = useMemo(() => {
+    if (subscription.data?.endDate) {
+      return dateFormat({
+        dateISO: subscription.data.endDate,
+        format: DateTime.DATE_FULL,
+      });
+    }
+
+    return '';
+  }, [subscription.data?.endDate]);
+
+  return (
+    <>
+      {shouldShowCancellationBanner && !isLoading && (
+        <AlertBox size="sm" className="font-medium">
+          Your Pro Plan is expiring. You will be converted to a Free plan on{' '}
+          {endPlanDate}.
path: src/fragments/Projects/Settings/Sections/Billing/BillingPlan/BillingPlan.tsx
user: login: bcamarneiro
body: Maybe something like:
{`Your Pro Plan is expiring. You will be converted to a Free plan on ${endPlanDate}.`}
created_at: 2025-05-06T09:08:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -283,3 +250,60 @@ const getPlanData = (plan: BillingPlanType, endPlanDate?: string): PlanData => {
     },
   }[plan];
 };
+
+export const Banners = ({ isLoading }: LoadingProps) => {
+  const { subscription, billingPlanType, paymentMethod } = useBillingContext();
+  const shouldShowCancellationBanner = useMemo(() => {
+    if (subscription.data?.endDate) {
+      const targetTime = DateTime.fromISO(subscription.data.endDate);
+      const currentTime = DateTime.now();
+      const diff = targetTime.diff(currentTime);
+
+      return Math.floor(diff.as('months')) <= 1;
+    }
+
+    return null;
+  }, [subscription.data?.endDate]);
+
+  const trialEndDate = useMemo(() => {
+    if (subscription.data?.trialEndDate) {
+      return dateFormat({
+        dateISO: subscription.data.trialEndDate,
+        format: DateTime.DATE_FULL,
+      });
+    }
+
+    return '';
+  }, [subscription.data?.trialEndDate]);
+
+  const endPlanDate = useMemo(() => {
+    if (subscription.data?.endDate) {
+      return dateFormat({
+        dateISO: subscription.data.endDate,
+        format: DateTime.DATE_FULL,
+      });
+    }
+
+    return '';
+  }, [subscription.data?.endDate]);
+
+  return (
+    <>
+      {shouldShowCancellationBanner && !isLoading && (
+        <AlertBox size="sm" className="font-medium">
+          Your Pro Plan is expiring. You will be converted to a Free plan on{' '}
+          {endPlanDate}.
+        </AlertBox>
+      )}
+
+      {billingPlanType === 'trial' && !isLoading && (
+        <AlertBox size="sm" className="font-medium">
+          Your trial period expires on {trialEndDate}.{' '}
+          {paymentMethod.data?.id
+            ? 'You will be billed after that date.'
+            : 'Be sure to add your billing info before your trial ends.'}
path: src/fragments/Projects/Settings/Sections/Billing/BillingPlan/BillingPlan.tsx
user: login: bcamarneiro
body: Something like:
`Your trial period expires on ${trialEndDate}. ${paymentMethod.data?.id ? 'You will be billed after that date.' : 'Be sure to add your billing info before your trial ends.'}`
created_at: 2025-05-06T09:11:34Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-05-06T03:08:22Z
body: ###  🦋  Changeset detected

Latest commit: afd99673c88cdff6465a7d43ed6f32c46b65d58d

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                      | Type  |
| ------------------------- | ----- |
| @fleek-platform/dashboard | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/fleek-platform/dashboard/new/feat/billing/credits?filename=.changeset/fresh-queens-hammer.md&value=---%0A%22%40fleek-platform%2Fdashboard%22%3A%20patch%0A---%0A%0Afeat%3A%20%F0%9F%8E%B8%20Adds%20credits%20to%20billing%20flows%0A)


author_association: NONE
user: login: angarita-dev
created_at: 2025-05-07T15:55:05Z
body: Thanks @bcamarneiro! pushed a fix for the `onClick` handler as it was a type issue.

For the copy changes those are happening on #46 
author_association: CONTRIBUTOR