Skip to content

Commit 5e5d9a9

Browse files
authored
feat: 🎸 Adds credits to billing flows (#45)
## 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
1 parent 5a073d8 commit 5e5d9a9

File tree

15 files changed

+489
-91
lines changed

15 files changed

+489
-91
lines changed

.changeset/sharp-walls-hide.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@fleek-platform/dashboard": patch
3+
---
4+
5+
Adds credit support

src/components/Billing/HorizontalPlanCard.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const HorizontalPlanCard: React.FC<HorizontalPlanCardProps> = ({
1111
...headerProps
1212
}) => {
1313
return (
14-
<Box variant="container">
14+
<Box variant="container" className="h-full justify-between">
1515
<Header {...headerProps} />
1616
{children}
1717
</Box>

src/components/LegacyPlanUpgradeModal/LegacyPlanUpgradeModal.tsx

+18-3
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,19 @@ export const LegacyPlanUpgradeModal = () => {
2929
const checkout = useFleekCheckout();
3030
const toast = useToast();
3131
const [isLoading, setLoading] = useState(false);
32-
const { billingPlanType, loading: billingPlanTypeLoading } = useBillingContext();
32+
const {
33+
billingPlanType,
34+
loading: billingPlanTypeLoading,
35+
subscription,
36+
team,
37+
} = useBillingContext();
3338

3439
useEffect(() => {
35-
if (billingPlanTypeLoading || billingPlanType !== 'none' && billingPlanType !== 'free') return;
40+
if (
41+
billingPlanTypeLoading ||
42+
(billingPlanType !== 'none' && billingPlanType !== 'free')
43+
)
44+
return;
3645
const shown = localStorage.getItem(shownKey);
3746
if (shown) return;
3847
setIsOpen(true);
@@ -53,7 +62,13 @@ export const LegacyPlanUpgradeModal = () => {
5362
setLoading(true);
5463
try {
5564
const response = await checkout.mutateAsync();
56-
window.location.href = response.url;
65+
66+
if (response.type === 'CHECKOUT') {
67+
window.location.href = response.content.url;
68+
}
69+
70+
await Promise.all([subscription.refetch(), team.refetch()]);
71+
toast.success({ message: 'Subscribed successfully!' });
5772
} catch (error) {
5873
toast.error({ error, log: 'Error upgrading plan. Please try again' });
5974
}

src/components/ftw/RootLayout/RootLayout.tsx

+59-10
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import { LayoutHead } from '../../LayoutHead/LayoutHead';
3232
import { AccountDropdown } from '../AccountDropdown/AccountDropdown';
3333
import { Announcement } from '../Announcement/Announcement';
3434
import { BreadcrumbItem, Breadcrumbs } from '../Breadcrumbs/Breadcrumbs';
35+
import { useCredits, useCreditsCheckout } from '@/hooks/useCredits';
36+
import { usePermissions } from '@/hooks/usePermissions';
3537

3638
export type NavigationItem = {
3739
icon: IconName;
@@ -174,6 +176,38 @@ const VersionTagsWrapper: React.FC = () => {
174176
return <VersionTags />;
175177
};
176178

179+
const Credits = () => {
180+
const { credits, isCreditsLoading } = useCredits();
181+
const { handleAddCredits, isCreatingCheckout } = useCreditsCheckout();
182+
183+
return (
184+
<Box className="flex-row justify-between items-center border border-neutral-6 bg-neutral-2 rounded-t-lg p-3">
185+
<Text
186+
className="text-neutral-12"
187+
variant="tertiary"
188+
size="sm"
189+
weight={500}
190+
>
191+
{isCreditsLoading ? (
192+
<Skeleton variant="text" />
193+
) : (
194+
credits.positiveFormatted
195+
)}
196+
</Text>
197+
<Button
198+
className="py-0 h-auto"
199+
variant="outline"
200+
intent="ghost"
201+
onClick={handleAddCredits as () => void}
202+
>
203+
<Text className="text-neutral-11" variant="tertiary" size="sm">
204+
{isCreatingCheckout ? <Icon name="spinner" /> : 'Buy credits'}
205+
</Text>
206+
</Button>
207+
</Box>
208+
);
209+
};
210+
177211
type SidebarProps = {
178212
slotSidebar: React.ReactNode;
179213
navigation: NavigationItem[];
@@ -185,6 +219,10 @@ const Sidebar: React.FC<SidebarProps> = ({
185219
navigation,
186220
isNavigationLoading,
187221
}) => {
222+
const hasBillingPermission = usePermissions({
223+
action: [constants.PERMISSION.BILLING.MANAGE],
224+
});
225+
188226
return (
189227
<Box
190228
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> = ({
235273
<Box className="gap-2.5">
236274
<VersionTagsWrapper />
237275
<Announcement />
238-
<Box className="border border-neutral-6 py-2 pt-2.5 rounded-lg">
239-
<FeedbackModalLink />
240-
<ExternalLinkWrapper href={constants.EXTERNAL_LINK.FLEEK_DOCS}>
241-
Documentation
242-
</ExternalLinkWrapper>
243-
<ExternalLinkWrapper href={constants.EXTERNAL_LINK.FLEEK_SUPPORT}>
244-
Support
245-
</ExternalLinkWrapper>
246-
<Box className="h-[1px] my-2.5 mx-3 bg-neutral-6" />
247-
<AccountDropdown />
276+
277+
<Box className="gap-0">
278+
{hasBillingPermission && <Credits />}
279+
<Box
280+
className={cn(
281+
'border-neutral-6 py-2 pt-2.5 rounded-b-lg',
282+
hasBillingPermission
283+
? 'border-b border-x rounded-b-lg'
284+
: 'border rounded-lg',
285+
)}
286+
>
287+
<FeedbackModalLink />
288+
<ExternalLinkWrapper href={constants.EXTERNAL_LINK.FLEEK_DOCS}>
289+
Documentation
290+
</ExternalLinkWrapper>
291+
<ExternalLinkWrapper href={constants.EXTERNAL_LINK.FLEEK_SUPPORT}>
292+
Support
293+
</ExternalLinkWrapper>
294+
<Box className="h-[1px] my-2.5 mx-3 bg-neutral-6" />
295+
<AccountDropdown />
296+
</Box>
248297
</Box>
249298
</Box>
250299
</Box>

src/fragments/Projects/Settings/Sections/Billing/BillingPlan/BillingPlan.tsx

+78-55
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { constants } from '@/constants';
1313
import { useCancelMockedMutation } from '@/hooks/useCancelSubscription';
1414
import { useFleekCheckout } from '@/hooks/useFleekCheckout';
1515
import { usePermissions } from '@/hooks/usePermissions';
16-
import { useRouter } from '@/hooks/useRouter';
1716
import { useToast } from '@/hooks/useToast';
1817
import { useBillingContext } from '@/providers/BillingProvider';
1918
import type { BillingPlanType } from '@/types/Billing';
@@ -23,11 +22,12 @@ import { dateFormat } from '@/utils/dateFormats';
2322

2423
import { CancelPlanModal } from './CancelPlanModal';
2524
import { getDefined } from '@/defined';
25+
import { useCreditsCheckout } from '@/hooks/useCredits';
2626

2727
export const BillingPlan: React.FC<LoadingProps> = ({ isLoading }) => {
2828
const toast = useToast();
29-
const router = useRouter();
30-
const { subscription, billingPlanType, paymentMethod } = useBillingContext();
29+
const { subscription, billingPlanType, paymentMethod, team } =
30+
useBillingContext();
3131

3232
const [isCancelModalOpen, setIsCancelModalOpen] = useState(false);
3333

@@ -53,7 +53,14 @@ export const BillingPlan: React.FC<LoadingProps> = ({ isLoading }) => {
5353
// TODO: This fails for some reason
5454
// router.replace(response.url);
5555

56-
window.location.href = response.url;
56+
if (response.type === 'CHECKOUT') {
57+
window.location.href = response.content.url;
58+
59+
return;
60+
}
61+
62+
await Promise.all([subscription.refetch(), team.refetch()]);
63+
toast.success({ message: 'Subscribed successfully!' });
5764
} catch (error) {
5865
toast.error({ error, log: 'Error upgrading plan. Please try again' });
5966
}
@@ -71,17 +78,6 @@ export const BillingPlan: React.FC<LoadingProps> = ({ isLoading }) => {
7178
}
7279
};
7380

74-
const endPlanDate = useMemo(() => {
75-
if (subscription.data?.endDate) {
76-
return dateFormat({
77-
dateISO: subscription.data.endDate,
78-
format: DateTime.DATE_FULL,
79-
});
80-
}
81-
82-
return '';
83-
}, [subscription.data?.endDate]);
84-
8581
const endPeriodDate = useMemo(() => {
8682
if (subscription.data?.periodEndDate) {
8783
return dateFormat({
@@ -93,57 +89,19 @@ export const BillingPlan: React.FC<LoadingProps> = ({ isLoading }) => {
9389
return '';
9490
}, [subscription.data?.periodEndDate]);
9591

96-
const shouldShowCancellationBanner = useMemo(() => {
97-
if (subscription.data?.endDate) {
98-
const targetTime = DateTime.fromISO(subscription.data.endDate);
99-
const currentTime = DateTime.now();
100-
const diff = targetTime.diff(currentTime);
101-
102-
return Math.floor(diff.as('months')) <= 1;
103-
}
104-
105-
return null;
106-
}, [subscription.data?.endDate]);
107-
108-
const trialEndDate = useMemo(() => {
109-
if (subscription.data?.trialEndDate) {
110-
return dateFormat({
111-
dateISO: subscription.data.trialEndDate,
112-
format: DateTime.DATE_FULL,
113-
});
114-
}
115-
116-
return '';
117-
}, [subscription.data?.trialEndDate]);
118-
11992
const { title, description, price } = getPlanData(
12093
billingPlanType,
12194
endPeriodDate,
12295
);
12396

12497
return (
125-
<>
98+
<Box className="w-full">
12699
<CancelPlanModal
127100
isOpen={isCancelModalOpen}
128101
onOpenChange={setIsCancelModalOpen}
129102
onCancelPlan={handleCancelPlan}
130103
dueDate={endPeriodDate}
131104
/>
132-
{shouldShowCancellationBanner && !isLoading && (
133-
<AlertBox size="sm" className="font-medium">
134-
Your Pro Plan is expiring. You will be converted to a Free plan on{' '}
135-
{endPlanDate}.
136-
</AlertBox>
137-
)}
138-
139-
{billingPlanType === 'trial' && !isLoading && (
140-
<AlertBox size="sm" className="font-medium">
141-
Your trial period expires on {trialEndDate}.{' '}
142-
{paymentMethod.data?.id
143-
? 'You will be billed after that date.'
144-
: 'Be sure to add your billing info before your trial ends.'}
145-
</AlertBox>
146-
)}
147105
<Billing.HorizontalPlanCard
148106
isLoading={isLoading}
149107
title={title}
@@ -172,7 +130,7 @@ export const BillingPlan: React.FC<LoadingProps> = ({ isLoading }) => {
172130
</Box>
173131
</SettingsBox.ActionRow>
174132
</Billing.HorizontalPlanCard>
175-
</>
133+
</Box>
176134
);
177135
};
178136

@@ -192,6 +150,7 @@ const ButtonsContainer: React.FC<ButtonsContainerProps> = ({
192150
onCancelPlan,
193151
}) => {
194152
const [isLoading, setIsLoading] = useState(false);
153+
const { handleAddCredits, isCreatingCheckout } = useCreditsCheckout();
195154
const hasManageBillingPermission = usePermissions({
196155
action: [constants.PERMISSION.BILLING.MANAGE],
197156
});
@@ -244,6 +203,13 @@ const ButtonsContainer: React.FC<ButtonsContainerProps> = ({
244203
{billingPlanType === 'trial' ? 'Add billing info' : 'Upgrade to Pro'}
245204
</Button>
246205
</PermissionsTooltip>
206+
<Button
207+
size="sm"
208+
onClick={handleAddCredits as () => void}
209+
loading={isCreatingCheckout}
210+
>
211+
Add credits
212+
</Button>
247213
</>
248214
);
249215
};
@@ -283,3 +249,60 @@ const getPlanData = (plan: BillingPlanType, endPlanDate?: string): PlanData => {
283249
},
284250
}[plan];
285251
};
252+
253+
export const Banners = ({ isLoading }: LoadingProps) => {
254+
const { subscription, billingPlanType, paymentMethod } = useBillingContext();
255+
const shouldShowCancellationBanner = useMemo(() => {
256+
if (subscription.data?.endDate) {
257+
const targetTime = DateTime.fromISO(subscription.data.endDate);
258+
const currentTime = DateTime.now();
259+
const diff = targetTime.diff(currentTime);
260+
261+
return Math.floor(diff.as('months')) <= 1;
262+
}
263+
264+
return null;
265+
}, [subscription.data?.endDate]);
266+
267+
const trialEndDate = useMemo(() => {
268+
if (subscription.data?.trialEndDate) {
269+
return dateFormat({
270+
dateISO: subscription.data.trialEndDate,
271+
format: DateTime.DATE_FULL,
272+
});
273+
}
274+
275+
return '';
276+
}, [subscription.data?.trialEndDate]);
277+
278+
const endPlanDate = useMemo(() => {
279+
if (subscription.data?.endDate) {
280+
return dateFormat({
281+
dateISO: subscription.data.endDate,
282+
format: DateTime.DATE_FULL,
283+
});
284+
}
285+
286+
return '';
287+
}, [subscription.data?.endDate]);
288+
289+
return (
290+
<>
291+
{shouldShowCancellationBanner && !isLoading && (
292+
<AlertBox size="sm" className="font-medium">
293+
Your Pro Plan is expiring. You will be converted to a Free plan on{' '}
294+
{endPlanDate}.
295+
</AlertBox>
296+
)}
297+
298+
{billingPlanType === 'trial' && !isLoading && (
299+
<AlertBox size="sm" className="font-medium">
300+
Your trial period expires on {trialEndDate}.{' '}
301+
{paymentMethod.data?.id
302+
? 'You will be billed after that date.'
303+
: 'Be sure to add your billing info before your trial ends.'}
304+
</AlertBox>
305+
)}
306+
</>
307+
);
308+
};

0 commit comments

Comments
 (0)