Skip to content

Commit c76d067

Browse files
committed
Simplified pricing + Show real interval + Fixes for paddle price handling
1 parent 9db8e9c commit c76d067

22 files changed

Lines changed: 454 additions & 289 deletions

landing/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"start": "next -p 3001"
1515
},
1616
"dependencies": {
17-
"@brunolemos/devhub-core": "0.99.0",
17+
"@brunolemos/devhub-core": "0.99.0-2",
1818
"@zeit/next-css": "1.0.1",
1919
"autoprefixer": "9.6.4",
2020
"classnames": "2.2.6",

landing/pages/purchase.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ const Purchase: NextPage<SubscribePageProps> = () => {
1313

1414
useEffect(() => {
1515
if (!activePaidPlans.some(p => !!p.interval)) return
16+
1617
Router.replace(
17-
`/purchase${qs.stringify(Router.query, { addQueryPrefix: true })}`,
18+
`/subscribe${qs.stringify(Router.query, { addQueryPrefix: true })}`,
1819
)
1920
}, [])
2021

landing/pages/subscribe.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ const Buy: NextPage<SubscribePageProps> = () => {
1313

1414
useEffect(() => {
1515
if (activePaidPlans.some(p => !!p.interval)) return
16+
1617
Router.replace(
17-
`/buy${qs.stringify(Router.query, { addQueryPrefix: true })}`,
18+
`/purchase${qs.stringify(Router.query, { addQueryPrefix: true })}`,
1819
)
1920
}, [])
2021

landing/src/components/common/Select.tsx

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import classNames from 'classnames'
22
import React, { ReactElement } from 'react'
33

44
export interface SelectProps<T extends string> {
5-
children: Array<ReactElement<SelectOptionProps<T>>>
5+
children: Array<ReactElement<SelectOptionProps<T>> | false>
66
onChange: (option: T) => void
77
placeholder?: string
88
}
@@ -18,7 +18,7 @@ export function Select<T extends string>(props: SelectProps<T>) {
1818

1919
const childrenArr = React.Children.toArray(children)
2020
const selectedIndex = childrenArr.findIndex(
21-
child => child.props.selected === true,
21+
child => child && child.props.selected === true,
2222
)
2323

2424
return (
@@ -51,29 +51,33 @@ export function Select<T extends string>(props: SelectProps<T>) {
5151
}}
5252
>
5353
<span className="flex flex-col bg-default border border-bg-less-2 rounded-lg shadow overflow-hidden">
54-
{React.Children.map(children, child => (
55-
<span
56-
className={classNames(
57-
'w-full px-2 py-1 whitespace-no-wrap',
58-
childrenArr.length > 1 &&
59-
'hover:bg-less-2 cursor-pointer',
60-
)}
61-
onClick={
62-
onChange ? () => onChange(child.props.id) : undefined
63-
}
64-
>
65-
<span className="text-default">{child}</span>
66-
<small
67-
className={classNames(
68-
'text-default',
69-
!child.props.selected && 'opacity-0',
70-
)}
71-
style={{ fontSize: '75%' }}
72-
>
73-
{` ▼`}
74-
</small>
75-
</span>
76-
))}
54+
{React.Children.map(
55+
children,
56+
child =>
57+
!!child && (
58+
<span
59+
className={classNames(
60+
'w-full px-2 py-1 whitespace-no-wrap',
61+
childrenArr.length > 1 &&
62+
'hover:bg-less-2 cursor-pointer',
63+
)}
64+
onClick={
65+
onChange ? () => onChange(child.props.id) : undefined
66+
}
67+
>
68+
<span className="text-default">{child}</span>
69+
<small
70+
className={classNames(
71+
'text-default',
72+
!child.props.selected && 'opacity-0',
73+
)}
74+
style={{ fontSize: '75%' }}
75+
>
76+
{` ▼`}
77+
</small>
78+
</span>
79+
),
80+
)}
7781
</span>
7882
</span>
7983
)}

landing/src/components/sections/CTAButtons.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import {
22
activePaidPlans,
33
activePlans,
44
constants,
5+
formatPrice,
56
freeTrialDays,
67
} from '@brunolemos/devhub-core'
78
import classNames from 'classnames'
89

910
import { useAuth } from '../../context/AuthContext'
1011
import { getSystemLabel } from '../../helpers'
11-
import { useFormattedPlanPrice } from '../../hooks/use-formatted-plan-price'
12+
import { useLocalizedPlanDetails } from '../../hooks/use-localized-plan-details'
1213
import { useSystem } from '../../hooks/use-system'
1314
import Button from '../common/buttons/Button'
1415

@@ -22,10 +23,8 @@ export default function CTAButtons(props: CTAButtonsProps) {
2223

2324
const { os } = useSystem()
2425
const { authData } = useAuth()
25-
const priceLabel = useFormattedPlanPrice(
26-
activePaidPlans[0].amount,
27-
activePaidPlans[0],
28-
)
26+
const localizedPlan = useLocalizedPlanDetails(activePaidPlans[0])
27+
const priceLabel = localizedPlan ? formatPrice(localizedPlan) : ''
2928

3029
return (
3130
<div
@@ -59,7 +58,7 @@ export default function CTAButtons(props: CTAButtonsProps) {
5958
href={authData.appToken ? '/purchase' : '/purchase?autologin'}
6059
type="primary"
6160
>
62-
{`Purchase for ${priceLabel}`}
61+
{`Purchase${priceLabel ? ` for ${priceLabel}` : ''}`}
6362
</Button>
6463
) : activePaidPlans.length === 1 && activePaidPlans[0] ? (
6564
<Button

landing/src/components/sections/pricing/PricingPlanBlock.tsx

Lines changed: 82 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
1-
import { activePlans, Plan } from '@brunolemos/devhub-core'
1+
import {
2+
activePlans,
3+
formatInterval,
4+
formatPrice,
5+
formatPriceAndInterval,
6+
Plan,
7+
} from '@brunolemos/devhub-core'
28
import classNames from 'classnames'
39
import qs from 'qs'
410
import React from 'react'
511

612
import { useAuth } from '../../../context/AuthContext'
7-
import { useFormattedPlanPrice } from '../../../hooks/use-formatted-plan-price'
13+
import { useLocalizedPlanDetails } from '../../../hooks/use-localized-plan-details'
814
import Button from '../../common/buttons/Button'
915
import CheckLabel from '../../common/CheckLabel'
1016

1117
export interface PricingPlanBlockProps {
1218
banner?: string | boolean
1319
buttonLabel?: string
1420
buttonLink: string
21+
forceShowAsMonthly?: boolean
1522
plan: Plan
1623
totalNumberOfVisiblePlans?: number
1724
}
@@ -21,46 +28,52 @@ export function PricingPlanBlock(props: PricingPlanBlockProps) {
2128
banner: _banner,
2229
buttonLabel,
2330
buttonLink,
31+
forceShowAsMonthly = false,
2432
plan,
2533
totalNumberOfVisiblePlans,
2634
} = props
2735

2836
const { authData } = useAuth()
2937

38+
const localizedPlan = useLocalizedPlanDetails(plan)
3039
const userPlan = authData && authData.plan
3140
const userPlanIsActive =
3241
userPlan && userPlan.id && activePlans.find(p => p.id === userPlan!.id)
33-
const isMyPlan = userPlan && userPlan.id === plan.id
42+
const isMyPlan = userPlan && userPlan.id === localizedPlan.id
3443

3544
const banner = userPlanIsActive
3645
? isMyPlan
37-
? plan.interval
38-
? 'Current plan'
46+
? localizedPlan.interval
47+
? 'Current localizedPlan'
3948
: 'You bought this'
4049
: _banner || true
4150
: _banner
4251

43-
const estimatedMonthlyPrice =
44-
(plan.interval === 'day'
45-
? plan.amount * 30
46-
: plan.interval === 'week'
47-
? plan.amount * 4
48-
: plan.interval === 'year'
49-
? plan.amount / 12
50-
: plan.amount) /
51-
(plan.intervalCount || 1) /
52-
(plan.transformUsage && plan.transformUsage.divideBy > 1
53-
? plan.transformUsage.divideBy
54-
: 1)
55-
56-
const _priceLabel = useFormattedPlanPrice(estimatedMonthlyPrice, plan)
57-
const _roundedPriceLabelWithInterval = useFormattedPlanPrice(
58-
plan.amount % 100 > 50
59-
? plan.amount + (100 - (plan.amount % 100))
60-
: plan.amount,
61-
plan,
62-
{ includeInterval: true },
63-
)
52+
const modifiedAmount = forceShowAsMonthly
53+
? (localizedPlan.interval === 'day'
54+
? localizedPlan.amount * 30
55+
: localizedPlan.interval === 'week'
56+
? localizedPlan.amount * 4
57+
: localizedPlan.interval === 'year'
58+
? localizedPlan.amount / 12
59+
: localizedPlan.amount) /
60+
(localizedPlan.intervalCount || 1) /
61+
(localizedPlan.transformUsage && localizedPlan.transformUsage.divideBy > 1
62+
? localizedPlan.transformUsage.divideBy
63+
: 1)
64+
: localizedPlan.amount
65+
66+
const _priceLabel = formatPrice({
67+
...localizedPlan,
68+
amount: modifiedAmount,
69+
})
70+
const _roundedPriceLabelWithInterval = formatPriceAndInterval({
71+
...localizedPlan,
72+
amount:
73+
localizedPlan.amount % 100 > 50
74+
? localizedPlan.amount + (100 - (localizedPlan.amount % 100))
75+
: localizedPlan.amount,
76+
})
6477

6578
const priceLabelCents =
6679
_priceLabel[_priceLabel.length - 3] === '.'
@@ -72,40 +85,60 @@ export function PricingPlanBlock(props: PricingPlanBlockProps) {
7285
: _priceLabel
7386

7487
const subtitle = `${
75-
plan.type === 'team' ||
76-
(plan.transformUsage && plan.transformUsage.divideBy > 1)
88+
localizedPlan.type === 'team' ||
89+
(localizedPlan.transformUsage && localizedPlan.transformUsage.divideBy > 1)
7790
? '/user'
7891
: ''
79-
}${plan.interval ? '/month' : ''}${
80-
estimatedMonthlyPrice !== plan.amount ? '*' : ''
81-
}`.trim()
92+
}${
93+
forceShowAsMonthly
94+
? '/month'
95+
: localizedPlan.interval
96+
? formatInterval(localizedPlan)
97+
: ''
98+
}${modifiedAmount !== localizedPlan.amount ? '*' : ''}`.trim()
8299

83100
let footerText = ''
84101

85-
if (!plan.amount && plan.trialPeriodDays) {
102+
if (!localizedPlan.amount && localizedPlan.trialPeriodDays) {
86103
footerText =
87104
(footerText ? `${footerText}\n` : footerText) +
88-
`Free for ${plan.trialPeriodDays} days`
105+
`Free for ${localizedPlan.trialPeriodDays} days`
89106
}
90107

91-
if (estimatedMonthlyPrice !== plan.amount) {
108+
if (modifiedAmount !== localizedPlan.amount) {
92109
footerText =
93110
(footerText ? `${footerText}\n` : footerText) +
94111
`*Billed ${
95-
plan.amount % 100 > 50 ? '~' : ''
112+
localizedPlan.amount % 100 > 50 ? '~' : ''
96113
}${_roundedPriceLabelWithInterval}`
97114
}
98115

99-
if (!plan.interval && plan.amount) {
116+
if (!localizedPlan.interval && localizedPlan.amount) {
100117
footerText =
101118
(footerText ? `${footerText}\n` : footerText) +
102119
'One-time payment (no subscription)'
103120
}
104121

122+
if (
123+
!footerText &&
124+
localizedPlan.interval &&
125+
localizedPlan.amount &&
126+
totalNumberOfVisiblePlans === 1
127+
) {
128+
if (localizedPlan.trialPeriodDays) {
129+
footerText =
130+
(footerText ? `${footerText}\n` : footerText) +
131+
`${localizedPlan.trialPeriodDays}-day free trial`
132+
}
133+
footerText =
134+
(footerText ? `${footerText}\n` : footerText) +
135+
'Cancel anytime with one click'
136+
}
137+
105138
return (
106139
<section
107140
className={classNames(
108-
'pricing-plan flex flex-col flex-shrink-0',
141+
'pricing-localizedPlan flex flex-col flex-shrink-0',
109142
totalNumberOfVisiblePlans === 1 ? 'w-full lg:w-84' : 'w-72',
110143
)}
111144
>
@@ -137,11 +170,11 @@ export function PricingPlanBlock(props: PricingPlanBlockProps) {
137170

138171
<div className="p-6 text-center">
139172
<div className="text-base leading-loose font-bold text-default">
140-
{plan.label}
173+
{localizedPlan.label}
141174
</div>
142175

143176
<div className="mb-2 text-sm text-muted-65 whitespace-pre-line">
144-
{plan.description}
177+
{localizedPlan.description}
145178
</div>
146179

147180
<div className="text-5xl leading-snug font-bold text-default">
@@ -167,10 +200,10 @@ export function PricingPlanBlock(props: PricingPlanBlockProps) {
167200
<div className="mb-2 text-sm text-muted-65">
168201
&nbsp;{subtitle}&nbsp;
169202
</div>
170-
{/* {plan.interval ? (
203+
{/* {localizedPlan.interval ? (
171204
<div className="text-sm text-muted-65">{`/${
172-
plan.intervalCount > 1 ? `${plan.intervalCount}-` : ''
173-
}${plan.interval}`}</div>
205+
localizedPlan.intervalCount > 1 ? `${localizedPlan.intervalCount}-` : ''
206+
}${localizedPlan.interval}`}</div>
174207
) : (
175208
<div className="text-sm text-muted-65">&nbsp;</div>
176209
)} */}
@@ -191,11 +224,11 @@ export function PricingPlanBlock(props: PricingPlanBlockProps) {
191224
<div className="pb-6" />
192225

193226
{isMyPlan ? (
194-
plan.type === 'team' ? (
227+
localizedPlan.type === 'team' ? (
195228
<Button
196229
type="primary"
197230
href={`/purchase${qs.stringify(
198-
{ plan: userPlan && userPlan.id },
231+
{ localizedPlan: userPlan && userPlan.id },
199232
{ addQueryPrefix: true },
200233
)}`}
201234
>
@@ -216,13 +249,15 @@ export function PricingPlanBlock(props: PricingPlanBlockProps) {
216249
</div>
217250
</div>
218251

219-
{!!(plan.featureLabels && plan.featureLabels.length) && (
252+
{!!(
253+
localizedPlan.featureLabels && localizedPlan.featureLabels.length
254+
) && (
220255
<div className="p-4">
221256
<div className="pb-2" />
222257

223-
{plan.featureLabels.map((feature, index) => (
258+
{localizedPlan.featureLabels.map((feature, index) => (
224259
<CheckLabel
225-
key={`pricing-plan-${plan.id}-feature-${index}`}
260+
key={`pricing-localizedPlan-${localizedPlan.id}-feature-${index}`}
226261
label={feature.label}
227262
checkProps={{
228263
className: feature.available ? 'text-primary' : 'invisible',

0 commit comments

Comments
 (0)