Skip to content

Commit a2f6dda

Browse files
authored
feat: add all checkout options on the pro page (#8082)
* feat: add all checkout options on the pro page * refactor: handle error cases in useCreateCheckout * fix(hooks): handle error when activeTeam or user is invalid * fix(Import): fix CTA URL for PrivateRepoFreeTeam component
1 parent cb2bf19 commit a2f6dda

File tree

16 files changed

+101
-92
lines changed

16 files changed

+101
-92
lines changed

packages/app/src/app/components/CreateSandbox/CreateSandbox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export const CreateSandbox: React.FC<CreateSandboxProps> = ({
183183

184184
const onCreateCheckout = () => {
185185
createCheckout({
186-
utm_source: 'dashboard_upgrade_banner',
186+
trackingLocation: 'dashboard_upgrade_banner',
187187
});
188188
};
189189

packages/app/src/app/components/CreateSandbox/Import/InactiveTeam.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const InactiveTeam: React.FC = () => {
2323
<MessageStripe.Action
2424
onClick={() => {
2525
createCheckout({
26-
utm_source: 'max_public_repos',
26+
trackingLocation: 'max_public_repos',
2727
});
2828

2929
track(getEventName(false, isBillingManager), EVENT_PROPS);

packages/app/src/app/components/CreateSandbox/Import/MaxPublicRepos.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const MaxPublicRepos: React.FC = () => {
2626
<MessageStripe.Action
2727
onClick={() => {
2828
createCheckout({
29-
utm_source: 'max_public_repos',
29+
trackingLocation: 'max_public_repos',
3030
});
3131

3232
track(

packages/app/src/app/components/CreateSandbox/Import/PrivateRepoFreeTeam.tsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import track from '@codesandbox/common/lib/utils/analytics';
2-
import { Element, Link as StyledLink } from '@codesandbox/components';
2+
import { Link as StyledLink } from '@codesandbox/components';
33
import { useCreateCheckout } from 'app/hooks';
44
import { useWorkspaceSubscription } from 'app/hooks/useWorkspaceSubscription';
55
import { useWorkspaceAuthorization } from 'app/hooks/useWorkspaceAuthorization';
@@ -20,14 +20,16 @@ export const PrivateRepoFreeTeam: React.FC = () => {
2020
* Can't checkout? '/docs/learn/introduction/workspace#managing-teams-and-subscriptions'
2121
*/
2222

23-
const ctaURL = ((): string | false => {
23+
const ctaURL = ((): string | null => {
24+
if (isPersonalSpace) {
25+
return '/pro';
26+
}
27+
2428
if (!canCheckout) {
25-
return isPersonalSpace
26-
? '/pro'
27-
: '/docs/learn/plans/workspace#managing-teams-and-subscriptions';
29+
return '/docs/learn/plans/workspace#managing-teams-and-subscriptions';
2830
}
2931

30-
return false;
32+
return null;
3133
})();
3234

3335
return (
@@ -49,7 +51,7 @@ export const PrivateRepoFreeTeam: React.FC = () => {
4951
modals.newSandboxModal.close();
5052
} else {
5153
createCheckout({
52-
utm_source: 'dashboard_import_limits',
54+
trackingLocation: 'dashboard_import_limits',
5355
});
5456
}
5557

@@ -59,11 +61,7 @@ export const PrivateRepoFreeTeam: React.FC = () => {
5961
});
6062
}}
6163
>
62-
upgrade to{' '}
63-
<Element as="span" css={{ textTransform: 'uppercase' }}>
64-
pro
65-
</Element>
66-
.
64+
upgrade to Pro.
6765
</StyledLink>
6866
</>
6967
);

packages/app/src/app/hooks/useCreateCheckout.ts

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useState } from 'react';
2-
import { useAppState, useEffects } from 'app/overmind';
2+
import { useActions, useAppState, useEffects } from 'app/overmind';
33
import { useLocation } from 'react-router';
44
import { dashboard as dashboardURLs } from '@codesandbox/common/lib/utils/url-generator';
55
import track from '@codesandbox/common/lib/utils/analytics';
@@ -12,11 +12,10 @@ type CheckoutStatus =
1212
| { status: 'error'; error: string };
1313

1414
export type CheckoutOptions = {
15-
recurring_interval?: 'month' | 'year';
16-
success_path?: string;
17-
cancel_path?: string;
18-
team_id?: string;
19-
utm_source:
15+
interval?: 'month' | 'year';
16+
cancelPath?: string;
17+
createTeam?: boolean;
18+
trackingLocation:
2019
| 'settings_upgrade'
2120
| 'dashboard_upgrade_banner'
2221
| 'dashboard_import_limits'
@@ -67,7 +66,8 @@ export const useCreateCheckout = (): [
6766
(args: CheckoutOptions) => Promise<void>,
6867
boolean
6968
] => {
70-
const { activeTeam, isProcessingPayment } = useAppState();
69+
const { activeTeam, isProcessingPayment, user } = useAppState();
70+
const actions = useActions();
7171
const { isFree } = useWorkspaceSubscription();
7272
const { isBillingManager } = useWorkspaceAuthorization();
7373
const [status, setStatus] = useState<CheckoutStatus>({ status: 'idle' });
@@ -85,30 +85,50 @@ export const useCreateCheckout = (): [
8585
}, [isProcessingPayment]);
8686

8787
async function createCheckout({
88-
recurring_interval = 'month',
89-
cancel_path = pathname + search,
90-
success_path = dashboardURLs.settings(activeTeam),
91-
team_id = activeTeam!,
92-
utm_source,
88+
interval = 'month',
89+
cancelPath = pathname + search,
90+
createTeam,
91+
trackingLocation,
9392
}: CheckoutOptions): Promise<void> {
9493
try {
9594
setStatus({ status: 'loading' });
9695

96+
if (!activeTeam || !user) {
97+
// Should not happen but it's done for typing reasons
98+
throw new Error('Invalid activeTeam or user');
99+
}
100+
101+
let teamId = activeTeam;
102+
103+
if (createTeam) {
104+
const newTeam = await actions.dashboard.createTeam({
105+
teamName: `${user.username}'s pro`,
106+
});
107+
108+
teamId = newTeam.id;
109+
}
110+
111+
const successPath = createTeam
112+
? dashboardURLs.recent(teamId, {
113+
new_workspace: 'true',
114+
})
115+
: dashboardURLs.settings(teamId);
116+
97117
const payload = await api.stripeCreateCheckout({
98-
success_path: addStripeSuccessParam(success_path, utm_source),
99-
cancel_path: addStripeCancelParam(cancel_path),
100-
team_id,
101-
recurring_interval,
118+
success_path: addStripeSuccessParam(successPath, trackingLocation),
119+
cancel_path: addStripeCancelParam(cancelPath),
120+
team_id: teamId,
121+
recurring_interval: interval,
102122
});
103123

104124
if (payload.stripeCheckoutUrl) {
105125
track('Subscription - Checkout successfully created', {
106-
source: utm_source,
126+
source: trackingLocation,
107127
});
108128
window.location.href = payload.stripeCheckoutUrl;
109129
} else {
110130
track('Subscription - Failed to create checkout', {
111-
source: utm_source,
131+
source: trackingLocation,
112132
});
113133
}
114134
} catch (err) {

packages/app/src/app/pages/Dashboard/Components/Repository/stripes.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const PrivateRepoFreeTeam: React.FC = () => {
3939
window.location.href = '/pro';
4040
} else {
4141
createCheckout({
42-
utm_source: 'dashboard_private_repo_upgrade',
42+
trackingLocation: 'dashboard_private_repo_upgrade',
4343
});
4444
}
4545
}}
@@ -66,7 +66,7 @@ export const MaxReposFreeTeam: React.FC = () => {
6666
disabled={checkout.status === 'loading'}
6767
onClick={() => {
6868
createCheckout({
69-
utm_source: 'dashboard_private_repo_upgrade',
69+
trackingLocation: 'dashboard_private_repo_upgrade',
7070
});
7171

7272
track(getEventName(isEligibleForTrial, isBillingManager), {

packages/app/src/app/pages/Dashboard/Components/TeamSubscriptionOptions/TeamSubscriptionOptions.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,55 @@
1-
import React from 'react';
1+
import React, { useEffect } from 'react';
22
import { ComboButton, Stack, Text } from '@codesandbox/components';
33
import track from '@codesandbox/common/lib/utils/analytics';
44
import { useCreateCheckout } from 'app/hooks';
55
import { useWorkspaceSubscription } from 'app/hooks/useWorkspaceSubscription';
6+
import { useEffects } from 'app/overmind';
67

78
type TeamSubscriptionOptionsProps = {
89
buttonVariant?: React.ComponentProps<typeof ComboButton>['variant'];
910
buttonStyles?: React.ComponentProps<typeof ComboButton>['customStyles'];
1011
ctaCopy?: string;
11-
trackingLocation: string;
12+
trackingLocation: 'settings_upgrade' | 'pro_page';
13+
createTeam?: boolean;
1214
};
1315
export const TeamSubscriptionOptions: React.FC<TeamSubscriptionOptionsProps> = ({
1416
buttonVariant,
1517
buttonStyles,
1618
ctaCopy,
1719
trackingLocation,
20+
createTeam,
1821
}) => {
1922
const { isEligibleForTrial } = useWorkspaceSubscription();
23+
const effects = useEffects();
2024

2125
const [checkout, createCheckout, canCheckout] = useCreateCheckout();
2226
const disabled = checkout.status === 'loading';
2327

28+
useEffect(() => {
29+
if (checkout.status === 'error') {
30+
effects.notificationToast.error(
31+
`Could not create stripe checkout link. ${checkout.error}`
32+
);
33+
}
34+
}, [checkout]);
35+
2436
const createMonthlyCheckout = () => {
2537
createCheckout({
26-
utm_source: 'settings_upgrade',
38+
trackingLocation,
39+
createTeam,
2740
});
2841
};
2942

3043
const createYearlyCheckout = () => {
3144
createCheckout({
32-
recurring_interval: 'year',
33-
utm_source: 'settings_upgrade',
45+
interval: 'year',
46+
trackingLocation,
47+
createTeam,
3448
});
3549
};
3650

37-
if (!canCheckout) {
51+
// Only show this for existing teams, not for the create team flow
52+
if (!createTeam && !canCheckout) {
3853
return (
3954
<Text as="p" variant="body" css={{ marginTop: 0 }}>
4055
Contact your team admin to upgrade.

packages/app/src/app/pages/Dashboard/Components/UpgradeBanner/UpgradeBanner.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export const UpgradeBanner: React.FC = () => {
7474
}
7575

7676
createCheckout({
77-
utm_source: 'restrictions_banner',
77+
trackingLocation: 'restrictions_banner',
7878
});
7979
}}
8080
autoWidth

packages/app/src/app/pages/Dashboard/Components/shared/InactiveTeamStripe.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const InactiveTeamStripe: React.FC = ({ children }) => {
2020
});
2121

2222
createCheckout({
23-
utm_source: 'restrictions_banner',
23+
trackingLocation: 'restrictions_banner',
2424
});
2525
}}
2626
>

packages/app/src/app/pages/Dashboard/Content/routes/Sandboxes/MaxSandboxesRestrictionsBanner.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const MaxSandboxesRestrictionsBanner: React.FC = () => {
3333
}
3434

3535
createCheckout({
36-
utm_source: 'restrictions_banner',
36+
trackingLocation: 'restrictions_banner',
3737
});
3838
}}
3939
>

packages/app/src/app/pages/Dashboard/Content/routes/Settings/TeamSettings/ManageSubscription/upgrade.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const Upgrade = () => {
5151

5252
<TeamSubscriptionOptions
5353
buttonVariant="trial"
54-
trackingLocation="Team Settings"
54+
trackingLocation="settings_upgrade"
5555
/>
5656
</Stack>
5757
</Card>

packages/app/src/app/pages/Dashboard/Content/routes/Settings/TeamSettings/WorkspaceSettings.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ export const WorkspaceSettings: React.FC = () => {
251251
}
252252

253253
createCheckout({
254-
utm_source: 'dashboard_workspace_settings',
254+
trackingLocation: 'dashboard_workspace_settings',
255255
});
256256
}}
257257
>

packages/app/src/app/pages/Pro/Create.tsx

Lines changed: 18 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import React, { useEffect, useState } from 'react';
1+
import React from 'react';
22
import { Helmet } from 'react-helmet';
3-
import { useAppState, useActions, useEffects } from 'app/overmind';
3+
import { useAppState } from 'app/overmind';
44
import {
55
ThemeProvider,
66
Stack,
@@ -17,30 +17,14 @@ import {
1717
ORGANIZATION_CONTACT_LINK,
1818
} from 'app/constants';
1919
import { usePriceCalculation } from 'app/hooks/usePriceCalculation';
20-
import { useCreateCheckout } from 'app/hooks';
21-
import { dashboard as dashboardURLs } from '@codesandbox/common/lib/utils/url-generator';
2220
import { SubscriptionCard } from './components/SubscriptionCard';
23-
import type { CTA } from './components/SubscriptionCard';
2421
import { PricingTable } from './components/PricingTable';
2522
import { StyledPricingDetailsText } from './components/elements';
2623
import { NewTeamModal } from '../Dashboard/Components/NewTeamModal';
24+
import { TeamSubscriptionOptions } from '../Dashboard/Components/TeamSubscriptionOptions/TeamSubscriptionOptions';
2725

2826
export const ProCreate = () => {
29-
const { hasLoadedApp, isLoggedIn, userCanStartTrial, user } = useAppState();
30-
const actions = useActions();
31-
const effects = useEffects();
32-
const [isLoading, setIsLoading] = useState(false);
33-
const [checkout, createCheckout] = useCreateCheckout();
34-
35-
useEffect(() => {
36-
if (checkout.status === 'error') {
37-
setIsLoading(false);
38-
39-
effects.notificationToast.error(
40-
`Could not create stripe checkout link. ${checkout.error}`
41-
);
42-
}
43-
}, [checkout]);
27+
const { hasLoadedApp, isLoggedIn } = useAppState();
4428

4529
const oneSeatPrice = usePriceCalculation({
4630
billingInterval: 'year',
@@ -54,27 +38,6 @@ export const ProCreate = () => {
5438

5539
if (!hasLoadedApp || !isLoggedIn) return null;
5640

57-
const newWorkspaceCTA: CTA = {
58-
text: userCanStartTrial ? 'Start trial' : 'Upgrade to Pro',
59-
isLoading,
60-
variant: 'dark',
61-
onClick: async () => {
62-
setIsLoading(true);
63-
const newTeam = await actions.dashboard.createTeam({
64-
teamName: `${user.username}'s pro`,
65-
});
66-
67-
await createCheckout({
68-
team_id: newTeam.id,
69-
success_path: dashboardURLs.recent(newTeam.id, {
70-
new_workspace: 'true',
71-
}),
72-
utm_source: 'pro_page',
73-
});
74-
setIsLoading(false);
75-
},
76-
};
77-
7841
return (
7942
<ThemeProvider>
8043
<Helmet>
@@ -141,7 +104,20 @@ export const ProCreate = () => {
141104
title="Pro"
142105
features={TEAM_PRO_FEATURES_WITH_PILLS}
143106
isHighlighted
144-
cta={newWorkspaceCTA}
107+
customCta={
108+
<TeamSubscriptionOptions
109+
buttonVariant="dark"
110+
buttonStyles={{
111+
padding: '12px 20px !important', // Otherwise it gets overridden.
112+
fontSize: '16px',
113+
lineHeight: '24px',
114+
fontWeight: 500,
115+
height: 'auto',
116+
}}
117+
createTeam
118+
trackingLocation="pro_page"
119+
/>
120+
}
145121
>
146122
<Stack gap={1} direction="vertical">
147123
<Text size={32} weight="500">

packages/app/src/app/pages/Pro/Upgrade.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export const ProUpgrade = () => {
224224
fontWeight: 500,
225225
height: 'auto',
226226
}}
227-
trackingLocation="subscription page"
227+
trackingLocation="pro_page"
228228
/>
229229
),
230230
}

0 commit comments

Comments
 (0)