Skip to content

Commit 941efce

Browse files
authored
Pro yearly option (codesandbox#3381)
* Add Pro Yearly * Add more details * Change name * Fix typing * Fix messaging * Process feedback * Update text * Fix reactivating subscriptions * Small content tweaks * Design tweaks * Update styling
1 parent 307c20a commit 941efce

File tree

10 files changed

+151
-77
lines changed

10 files changed

+151
-77
lines changed

packages/app/src/app/overmind/effects/api/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,18 @@ export default {
4545

4646
return response.token;
4747
},
48-
createPatronSubscription(token: string, amount: number, coupon: string) {
48+
createPatronSubscription(
49+
token: string,
50+
amount: number,
51+
duration: 'monthly' | 'yearly',
52+
coupon: string
53+
) {
4954
return api.post<CurrentUser>('/users/current_user/subscription', {
5055
subscription: {
5156
amount,
5257
coupon,
5358
token,
59+
duration,
5460
},
5561
});
5662
},

packages/app/src/app/overmind/namespaces/patron/actions.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@ export const priceChanged: Action<{ price: number }> = (
1414
export const createSubscriptionClicked: AsyncAction<{
1515
token: string;
1616
coupon: string;
17-
}> = async ({ state, effects, actions }, { token, coupon }) => {
18-
effects.analytics.track('Create Patron Subscription');
17+
duration: 'yearly' | 'monthly';
18+
}> = async ({ state, effects, actions }, { token, coupon, duration }) => {
19+
effects.analytics.track('Create Patron Subscription', { duration });
1920
state.patron.error = null;
2021
state.patron.isUpdatingSubscription = true;
2122
try {
2223
state.user = await effects.api.createPatronSubscription(
2324
token,
2425
state.patron.price,
26+
duration,
2527
coupon
2628
);
2729
effects.notificationToast.success('Thank you very much for your support!');
@@ -62,10 +64,9 @@ export const createSubscriptionClicked: AsyncAction<{
6264
state.patron.isUpdatingSubscription = false;
6365
};
6466

65-
export const updateSubscriptionClicked: AsyncAction<string> = async (
66-
{ state, effects },
67-
coupon
68-
) => {
67+
export const updateSubscriptionClicked: AsyncAction<{
68+
coupon: string;
69+
}> = async ({ state, effects }, { coupon }) => {
6970
effects.analytics.track('Update Patron Subscription');
7071
state.patron.error = null;
7172
state.patron.isUpdatingSubscription = true;
@@ -74,9 +75,7 @@ export const updateSubscriptionClicked: AsyncAction<string> = async (
7475
state.patron.price,
7576
coupon
7677
);
77-
effects.notificationToast.success(
78-
'Subscription updated, thanks for helping out!'
79-
);
78+
effects.notificationToast.success('Subscription updated!');
8079
} catch (error) {
8180
state.patron.error = error.message;
8281
}
@@ -99,7 +98,7 @@ export const cancelSubscriptionClicked: AsyncAction = async ({
9998
try {
10099
state.user = await effects.api.cancelPatronSubscription();
101100
effects.notificationToast.success(
102-
'Sorry to see you go, but thanks a bunch for the support this far!'
101+
'Sorry to see you go, but thanks for using CodeSandbox!'
103102
);
104103
} catch (error) {
105104
/* ignore */

packages/app/src/app/overmind/namespaces/preferences/actions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export const paymentDetailsUpdated: AsyncAction<string> = async (
9595
token
9696
);
9797
state.preferences.isLoadingPaymentDetails = false;
98+
99+
effects.notificationToast.success('Successfully updated payment details');
98100
};
99101

100102
export const keybindingChanged: Action<{

packages/app/src/app/pages/Patron/PricingModal/PricingChoice/ChangeSubscription/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const ChangeSubscription: FunctionComponent = () => {
6363
</StripeInputContainer>
6464

6565
<Buttons>
66-
<Button onClick={() => updateSubscriptionClicked(coupon)}>
66+
<Button onClick={() => updateSubscriptionClicked({ coupon: '' })}>
6767
Update
6868
</Button>
6969
</Buttons>
@@ -79,7 +79,7 @@ export const ChangeSubscription: FunctionComponent = () => {
7979
if (subscription.cancelAtPeriodEnd) {
8080
buttons = (
8181
<Buttons>
82-
<Button onClick={() => updateSubscriptionClicked('')}>
82+
<Button onClick={() => updateSubscriptionClicked({ coupon: '' })}>
8383
Reactivate Subscription
8484
</Button>
8585
</Buttons>

packages/app/src/app/pages/Patron/PricingModal/PricingChoice/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@ export const PricingChoice: FunctionComponent<Props> = ({ badge }) => {
7373
<Centered style={{ marginTop: '2rem' }} horizontal>
7474
<SubscribeForm
7575
subscribe={({ token, coupon }) =>
76-
createSubscriptionClicked({ token, coupon })
76+
createSubscriptionClicked({
77+
token,
78+
coupon,
79+
duration: 'monthly',
80+
})
7781
}
7882
isLoading={patron.isUpdatingSubscription}
7983
hasCoupon

packages/app/src/app/pages/Pro/elements.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,26 @@ export const SubHeading = styled.span(
4141
})
4242
);
4343

44+
export const BillText = styled.span<{ on: boolean }>(props =>
45+
css({
46+
color: props.on ? 'white' : 'grays.300',
47+
margin: '0 1rem',
48+
})
49+
);
50+
51+
export const DurationChoice = styled.div(() =>
52+
css({
53+
display: 'flex',
54+
textAlign: 'center',
55+
justifyContent: 'center',
56+
marginBottom: 8,
57+
58+
'[data-component=SwitchBackground]': {
59+
backgroundColor: 'grays.700',
60+
},
61+
})
62+
);
63+
4464
export const Form = styled.form<{ disabled: boolean }>(props =>
4565
css({
4666
fontSize: 3,
@@ -158,7 +178,7 @@ export const HelpText = styled.p(
158178

159179
export const LinkButton = styled(AppLinkButton)(
160180
css({
161-
color: 'grays.300',
181+
color: 'grays.200',
162182
})
163183
);
164184

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

Lines changed: 94 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { format } from 'date-fns';
22
import { Helmet } from 'react-helmet';
33
import React, { useEffect } from 'react';
4-
import { ThemeProvider } from 'styled-components';
54

65
import MaxWidth from '@codesandbox/common/lib/components/flex/MaxWidth';
76
import Margin from '@codesandbox/common/lib/components/spacing/Margin';
87
import Centered from '@codesandbox/common/lib/components/flex/Centered';
8+
import codeSandboxBlackTheme from '@codesandbox/common/lib/themes/codesandbox-black';
9+
import { ThemeProvider, Switch } from '@codesandbox/components';
10+
911
import { useOvermind } from 'app/overmind';
1012
import { Navigation } from 'app/pages/common/Navigation';
1113

12-
import theme from '@codesandbox/common/lib/design-language/theme';
1314
import { SubscribeForm } from './subscribe-form';
1415
import {
1516
Page,
@@ -25,6 +26,8 @@ import {
2526
SignInModal,
2627
SignInButton,
2728
SubHeading,
29+
DurationChoice,
30+
BillText,
2831
} from './elements';
2932

3033
const ProPage: React.FC = () => {
@@ -100,7 +103,7 @@ const ProPage: React.FC = () => {
100103
};
101104

102105
return (
103-
<ThemeProvider theme={theme}>
106+
<ThemeProvider theme={codeSandboxBlackTheme}>
104107
<Page>
105108
<Helmet>
106109
<title>Pro - CodeSandbox</title>
@@ -128,46 +131,58 @@ const LoggedOut = () => (
128131
</>
129132
);
130133

131-
const Pro = ({ user, modalOpened, cancelSubscriptionClicked }) => (
132-
<MaxWidth width={400}>
133-
<Centered horizontal>
134-
<Avatar src={user.avatarUrl} />
135-
<Badge type="pro">Pro</Badge>
136-
<Heading>You&apos;re a Pro!</Heading>
134+
const Pro = ({ user, modalOpened, cancelSubscriptionClicked }) => {
135+
const subscriptionDate = new Date(user.subscription.since);
136+
return (
137+
<MaxWidth width={400}>
138+
<Centered horizontal>
139+
<Avatar src={user.avatarUrl} />
140+
<Badge type="pro">Pro</Badge>
141+
<Heading>You&apos;re a Pro!</Heading>
137142

138-
<ButtonAsLink href="/s/" style={{ marginTop: 30 }}>
139-
Create a sandbox
140-
</ButtonAsLink>
143+
<ButtonAsLink href="/s/" style={{ marginTop: 30 }}>
144+
Create a sandbox
145+
</ButtonAsLink>
141146

142-
<HelpText>
143-
You will be billed on the{' '}
144-
<b>{format(new Date(user.subscription.since), 'do')}</b> of each month.
145-
You can{' '}
146-
<LinkButton
147-
onClick={e => {
148-
e.preventDefault();
149-
modalOpened({
150-
modal: 'preferences',
151-
itemId: 'paymentInfo',
152-
});
153-
}}
154-
>
155-
update your payment details
156-
</LinkButton>{' '}
157-
or{' '}
158-
<LinkButton
159-
onClick={e => {
160-
e.preventDefault();
161-
cancelSubscriptionClicked();
162-
}}
163-
>
164-
cancel your subscription
165-
</LinkButton>{' '}
166-
at any time.
167-
</HelpText>
168-
</Centered>
169-
</MaxWidth>
170-
);
147+
<HelpText>
148+
You will be billed{' '}
149+
{user.subscription.duration === 'yearly' ? (
150+
<>
151+
and charged{' '}
152+
<b>annually on {format(subscriptionDate, 'MMM dd')}</b>
153+
</>
154+
) : (
155+
<>
156+
on the <b>{format(subscriptionDate, 'do')} of each month</b>
157+
</>
158+
)}
159+
. You can{' '}
160+
<LinkButton
161+
onClick={e => {
162+
e.preventDefault();
163+
modalOpened({
164+
modal: 'preferences',
165+
itemId: 'paymentInfo',
166+
});
167+
}}
168+
>
169+
update your payment details
170+
</LinkButton>{' '}
171+
or{' '}
172+
<LinkButton
173+
onClick={e => {
174+
e.preventDefault();
175+
cancelSubscriptionClicked();
176+
}}
177+
>
178+
cancel your subscription
179+
</LinkButton>{' '}
180+
at any time.
181+
</HelpText>
182+
</Centered>
183+
</MaxWidth>
184+
);
185+
};
171186

172187
const Patron = ({ user }) => (
173188
<MaxWidth width={400}>
@@ -196,24 +211,40 @@ const NotPro = ({
196211
user,
197212
patron,
198213
checkoutDisabled,
199-
}) => (
200-
<>
201-
<Heading>CodeSandbox Pro</Heading>
202-
<SubHeading>$12/month</SubHeading>
203-
<Centered horizontal>
204-
<SubscribeForm
205-
subscribe={({ token, coupon }) =>
206-
createSubscriptionClicked({ token, coupon })
207-
}
208-
isLoading={patron.isUpdatingSubscription}
209-
hasCoupon
210-
name={user && user.name}
211-
error={patron.error}
212-
disabled={checkoutDisabled}
213-
/>
214-
</Centered>
215-
</>
216-
);
214+
}) => {
215+
const [duration, setDuration] = React.useState('yearly');
216+
217+
return (
218+
<>
219+
<Heading>CodeSandbox Pro</Heading>
220+
<SubHeading>
221+
{duration === 'yearly' ? '$9/month billed annually' : '12$/month'}
222+
</SubHeading>
223+
<DurationChoice>
224+
<BillText on={duration === 'monthly'}>Bill monthly</BillText>
225+
<Switch
226+
onChange={() =>
227+
setDuration(d => (d === 'yearly' ? 'monthly' : 'yearly'))
228+
}
229+
on={duration === 'yearly'}
230+
/>
231+
<BillText on={duration === 'yearly'}>Bill annually</BillText>
232+
</DurationChoice>
233+
<Centered horizontal>
234+
<SubscribeForm
235+
subscribe={({ token, coupon }) =>
236+
createSubscriptionClicked({ token, coupon, duration })
237+
}
238+
isLoading={patron.isUpdatingSubscription}
239+
hasCoupon
240+
name={user && user.name}
241+
error={patron.error}
242+
disabled={checkoutDisabled}
243+
/>
244+
</Centered>
245+
</>
246+
);
247+
};
217248

218249
const Expiring = ({
219250
user,
@@ -239,7 +270,10 @@ const Expiring = ({
239270
Creating subscription...
240271
</Button>
241272
) : (
242-
<Button onClick={updateSubscriptionClicked} style={{ marginTop: 30 }}>
273+
<Button
274+
onClick={() => updateSubscriptionClicked({ coupon: '' })}
275+
style={{ marginTop: 30 }}
276+
>
243277
Reactivate subscription
244278
</Button>
245279
)}

packages/common/src/types/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ export type CurrentUser = {
114114
since: string;
115115
amount: number;
116116
cancelAtPeriodEnd: boolean;
117-
plan?: 'pro' | 'patron';
117+
plan: 'pro' | 'patron';
118+
duration: 'monthly' | 'yearly';
118119
} | null;
119120
curatorAt: string;
120121
badges: Badge[];

packages/homepage/src/pages/pricing/_elements.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ export const Price = styled.h6`
2929
color: ${props => props.theme.homepage.white};
3030
`;
3131

32+
export const PriceSubText = styled.p`
33+
font-size: 13px;
34+
margin-bottom: 0;
35+
`;
36+
3237
export const List = styled.ul`
3338
list-style: none;
3439
margin: 0;

packages/homepage/src/pages/pricing/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Card,
1111
CardTitle,
1212
Price,
13+
PriceSubText,
1314
List,
1415
Button,
1516
FeaturesTableHeader,
@@ -62,10 +63,12 @@ export default () => (
6263
>
6364
<div>
6465
<CardTitle>Pro</CardTitle>
65-
<Price>$12/month</Price>
66+
<Price style={{ marginBottom: '0.5rem' }}>$9/Month</Price>
67+
<PriceSubText>billed annually or $12 month-to-month</PriceSubText>
6668
<List
6769
css={`
6870
color: white;
71+
margin-top: 1.5rem;
6972
`}
7073
>
7174
<li

0 commit comments

Comments
 (0)