Skip to content

Commit 32317c6

Browse files
authored
feat: 🎸 Subscribe modal paywall (#46)
## Why? - Show non-closing modal for users who are not on trial / pro plan. - Adds option to top-credits & continue subscription with credits. ## How? - Done A (replace with a breakdown of the steps) - Done B - Done C ## Tickets? Related PLAT-2860 ## Contribution checklist? - [ ] The commit messages are detailed - [ ] 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/6828bf80-833f-4bf8-ad01-bf69bbf971e0
1 parent bfc5e91 commit 32317c6

File tree

7 files changed

+138
-32
lines changed

7 files changed

+138
-32
lines changed

.changeset/rich-experts-warn.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@fleek-platform/dashboard": patch
3+
---
4+
5+
Deprecate free-tier

.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/LegacyPlanUpgradeModal/LegacyPlanUpgradeModal.tsx

+62-22
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { useToast } from '@/hooks/useToast';
99
import { Icon } from '@/ui';
1010
import { useBillingContext } from '@/providers/BillingProvider';
1111
import { useSessionContext } from '@/providers/SessionProvider';
12+
import { getDefined } from '@/defined';
13+
import { useCreditsCheckout } from '@/hooks/useCredits';
14+
import { useProPlan } from '@/hooks/useProPlan';
1215

1316
const PERKS = [
1417
'Unlimited team members',
@@ -27,8 +30,20 @@ export const LegacyPlanUpgradeModal = () => {
2730
const session = useSessionContext();
2831
const shownKey = `${SHOWN_KEY_PREFIX}${session.project.id}`;
2932
const checkout = useFleekCheckout();
33+
34+
const isFreeTierDeprecated =
35+
Date.now() >=
36+
Date.parse(getDefined('NEXT_PUBLIC_BILLING_FREE_PLAN_DEPRECATION_DATE'));
37+
3038
const toast = useToast();
3139
const [isLoading, setLoading] = useState(false);
40+
const { hasAvailableCredits } = useProPlan({
41+
onError: () =>
42+
toast.error({
43+
message: 'Unexpected error fetching pro plan, please try again later.',
44+
}),
45+
});
46+
const { handleAddCredits, isCreatingCheckout } = useCreditsCheckout();
3247
const {
3348
billingPlanType,
3449
loading: billingPlanTypeLoading,
@@ -37,15 +52,21 @@ export const LegacyPlanUpgradeModal = () => {
3752
} = useBillingContext();
3853

3954
useEffect(() => {
40-
if (
41-
billingPlanTypeLoading ||
42-
(billingPlanType !== 'none' && billingPlanType !== 'free')
43-
)
55+
if (billingPlanTypeLoading) return;
56+
57+
if (isFreeTierDeprecated && !['pro', 'trial'].includes(billingPlanType)) {
58+
setIsOpen(true);
4459
return;
60+
}
61+
62+
if (billingPlanType !== 'none' && billingPlanType !== 'free') return;
63+
4564
const shown = localStorage.getItem(shownKey);
65+
4666
if (shown) return;
67+
4768
setIsOpen(true);
48-
}, [shownKey, billingPlanType]);
69+
}, [shownKey, billingPlanType, isFreeTierDeprecated, billingPlanTypeLoading]);
4970

5071
const flagAsShown = () => {
5172
localStorage.setItem(shownKey, 'true');
@@ -68,6 +89,7 @@ export const LegacyPlanUpgradeModal = () => {
6889
}
6990

7091
await Promise.all([subscription.refetch(), team.refetch()]);
92+
setIsOpen(false);
7193
toast.success({ message: 'Subscribed successfully!' });
7294
} catch (error) {
7395
toast.error({ error, log: 'Error upgrading plan. Please try again' });
@@ -78,7 +100,7 @@ export const LegacyPlanUpgradeModal = () => {
78100

79101
return (
80102
<Dialog.Root open={isOpen} onOpenChange={handleOpenChange}>
81-
<Dialog.Overlay />
103+
<Dialog.Overlay className="opacity-80" />
82104
<Dialog.Portal>
83105
<Modal.Content
84106
onPointerDownOutside={(e) => e.preventDefault()}
@@ -87,16 +109,19 @@ export const LegacyPlanUpgradeModal = () => {
87109
<Box className="gap-4">
88110
<Modal.Heading className="flex justify-between items-center">
89111
<Box>Upgrade your plan</Box>
90-
<Dialog.Close asChild>
91-
<Button size="xs" intent="ghost" className="size-6">
92-
<Icon name="close" className="size-4 shrink-0" />
93-
</Button>
94-
</Dialog.Close>
112+
{!isFreeTierDeprecated && (
113+
<Dialog.Close asChild>
114+
<Button size="xs" intent="ghost" className="size-6">
115+
<Icon name="close" className="size-4 shrink-0" />
116+
</Button>
117+
</Dialog.Close>
118+
)}
95119
</Modal.Heading>
96120
<Text variant="secondary">
97-
Your legacy Free Plan is being phased out. To continue hosting on
121+
{`${!isFreeTierDeprecated ? 'Your legacy Free Plan is being phased out. ' : ''}To continue hosting on
98122
Fleek without interruption, please upgrade your plan as soon as
99123
possible.
124+
`}
100125
{LEARN_MORE_LINK && (
101126
<>
102127
{' '}
@@ -120,7 +145,7 @@ export const LegacyPlanUpgradeModal = () => {
120145
</ul>
121146
</Box>
122147
<DividerElement />
123-
<Box className="flex-row items-center justify-between">
148+
<Box className="flex-row items-start justify-between">
124149
<Box>
125150
<Box className="flex-row items-center gap-1">
126151
<Text variant="primary" size="lg" weight={700}>
@@ -134,15 +159,30 @@ export const LegacyPlanUpgradeModal = () => {
134159
+ resource usage
135160
</Text>
136161
</Box>
137-
<Button
138-
loading={isLoading}
139-
intent="accent"
140-
size="md"
141-
className="min-w-[150px]"
142-
onClick={handleCheckout}
143-
>
144-
Upgrade
145-
</Button>
162+
<Box className="items-end gap-2">
163+
<Button
164+
loading={isLoading || isCreatingCheckout}
165+
intent="accent"
166+
size="md"
167+
className="min-w-[150px]"
168+
onClick={handleCheckout}
169+
>
170+
{hasAvailableCredits ? 'Continue with credits' : 'Upgrade'}
171+
</Button>
172+
{!hasAvailableCredits && (
173+
<Text className="flex items-center">
174+
Want to pay with crypto?&nbsp;
175+
<Text
176+
className="hover:underline text-accent-9 cursor-pointer"
177+
variant="primary"
178+
onClick={handleAddCredits as () => void}
179+
>
180+
Add credits
181+
</Text>
182+
.
183+
</Text>
184+
)}
185+
</Box>
146186
</Box>
147187
</Modal.Content>
148188
</Dialog.Portal>

src/components/ftw/RootLayout/RootLayout.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { routes } from '@fleek-platform/utils-routes';
2-
import React, { useEffect, useState } from 'react';
2+
import type React from 'react';
33

44
import { ExternalLink, Link, LinkButton } from '@/components';
55
import { VersionTags } from '@/components/Version/VersionTags';
@@ -12,26 +12,25 @@ import { useUserHasScrolled } from '@/hooks/useUserHasScrolled';
1212
import { useBillingContext } from '@/providers/BillingProvider';
1313
import { useFeedbackModal } from '@/providers/FeedbackModalProvider';
1414
import { useSessionContext } from '@/providers/SessionProvider';
15-
import { ChildrenProps } from '@/types/Props';
15+
import type { ChildrenProps } from '@/types/Props';
1616
import {
1717
Box,
1818
Button,
1919
Icon,
20-
IconName,
20+
type IconName,
2121
Scrollable,
2222
SidebarSidepanel,
2323
Skeleton,
2424
Text,
2525
} from '@/ui';
2626
import { cn } from '@/utils/cn';
27-
import { isServerSide } from '@/utils/isServerSide';
2827

2928
import { BadgeText } from '../../BadgeText/BadgeText';
3029
import { FleekLogo } from '../../FleekLogo/FleekLogo';
3130
import { LayoutHead } from '../../LayoutHead/LayoutHead';
3231
import { AccountDropdown } from '../AccountDropdown/AccountDropdown';
3332
import { Announcement } from '../Announcement/Announcement';
34-
import { BreadcrumbItem, Breadcrumbs } from '../Breadcrumbs/Breadcrumbs';
33+
import { type BreadcrumbItem, Breadcrumbs } from '../Breadcrumbs/Breadcrumbs';
3534
import { useCredits, useCreditsCheckout } from '@/hooks/useCredits';
3635
import { usePermissions } from '@/hooks/usePermissions';
3736

@@ -148,6 +147,7 @@ const ExternalLinkWrapper: React.FC<
148147

149148
return (
150149
<button
150+
type="button"
151151
onClick={onClick}
152152
className="group px-3 flex justify-between items-center rounded-sm ring-0 outline-0 focus-visible:ring-2 ring-neutral-8"
153153
>

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@ import { dateFormat } from '@/utils/dateFormats';
2222

2323
import { CancelPlanModal } from './CancelPlanModal';
2424
import { getDefined } from '@/defined';
25-
import { useCreditsCheckout } from '@/hooks/useCredits';
25+
import { useCredits, useCreditsCheckout } from '@/hooks/useCredits';
2626

2727
export const BillingPlan: React.FC<LoadingProps> = ({ isLoading }) => {
2828
const toast = useToast();
2929
const { subscription, billingPlanType, paymentMethod, team } =
3030
useBillingContext();
3131

3232
const [isCancelModalOpen, setIsCancelModalOpen] = useState(false);
33+
const { refetchCredits } = useCredits();
3334

3435
const checkout = useFleekCheckout();
3536
// eslint-disable-next-line fleek-custom/valid-gql-hooks-destructuring
@@ -290,8 +291,7 @@ export const Banners = ({ isLoading }: LoadingProps) => {
290291
<>
291292
{shouldShowCancellationBanner && !isLoading && (
292293
<AlertBox size="sm" className="font-medium">
293-
Your Pro Plan is expiring. You will be converted to a Free plan on{' '}
294-
{endPlanDate}.
294+
Your Pro Plan is expiring on {endPlanDate}.
295295
</AlertBox>
296296
)}
297297

@@ -300,7 +300,7 @@ export const Banners = ({ isLoading }: LoadingProps) => {
300300
Your trial period expires on {trialEndDate}.{' '}
301301
{paymentMethod.data?.id
302302
? 'You will be billed after that date.'
303-
: 'Be sure to add your billing info before your trial ends.'}
303+
: 'Be sure to add your billing info or add credits before your trial ends.'}
304304
</AlertBox>
305305
)}
306306
</>

src/hooks/useFleekCheckout.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export const useFleekCheckout = () => {
6262
(plan) => plan.name.toUpperCase() === 'PRO',
6363
);
6464

65-
const planId = plan.id;
65+
const planId = plan?.id;
6666

6767
if (!planId) {
6868
throw new Error('Plan not found');

src/hooks/useProPlan.ts

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { BackendApiClient } from '@/integrations/new-be/BackendApi';
2+
import { useCookies } from '@/providers/CookiesProvider';
3+
import type { PlanResponse } from '@/types/Billing';
4+
import { useQuery } from '@tanstack/react-query';
5+
import { useCredits } from './useCredits';
6+
import { useMemo } from 'react';
7+
8+
export const useProPlan = ({
9+
onError = () => {},
10+
}: { onError?: () => void }) => {
11+
const cookies = useCookies();
12+
const backendApi = new BackendApiClient({
13+
accessToken: cookies.values.accessToken,
14+
});
15+
const { credits } = useCredits();
16+
17+
const proPlanQuery = useQuery({
18+
queryKey: ['pro-plan'],
19+
onError,
20+
queryFn: async () => {
21+
const response = await backendApi.fetch({
22+
url: '/api/v1/plans',
23+
});
24+
25+
if (!response.ok) {
26+
throw response.statusText;
27+
}
28+
29+
const plans: PlanResponse[] = await response.json();
30+
31+
const plan = plans.find((plan) => plan.name.toUpperCase() === 'PRO');
32+
33+
const planId = plan?.id;
34+
35+
if (!planId) {
36+
throw new Error('Plan not found');
37+
}
38+
39+
return plan;
40+
},
41+
});
42+
43+
const hasAvailableCredits = useMemo(() => {
44+
if (!proPlanQuery?.data?.price || !credits?.rawBalance) {
45+
return null;
46+
}
47+
48+
return credits.rawBalance >= proPlanQuery.data.price;
49+
}, [credits, proPlanQuery.data]);
50+
51+
return {
52+
proPlan: proPlanQuery.data,
53+
isFetching: proPlanQuery.isFetching,
54+
hasAvailableCredits,
55+
};
56+
};

0 commit comments

Comments
 (0)