Skip to content

Commit 7d2f132

Browse files
refactor: modernise Payment modal (#7004)
1 parent db4e578 commit 7d2f132

17 files changed

Lines changed: 658 additions & 9 deletions

File tree

frontend/common/hooks/useScript.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useEffect, useState } from 'react'
2+
3+
type ScriptState = {
4+
ready: boolean
5+
error: boolean
6+
}
7+
8+
export const useScript = (url: string): ScriptState => {
9+
const [state, setState] = useState<ScriptState>({
10+
error: false,
11+
ready: false,
12+
})
13+
14+
useEffect(() => {
15+
const existing = document.querySelector(`script[src="${url}"]`)
16+
if (existing) {
17+
setState({ error: false, ready: true })
18+
return
19+
}
20+
21+
const script = document.createElement('script')
22+
script.src = url
23+
script.async = true
24+
25+
script.addEventListener('load', () => {
26+
setState({ error: false, ready: true })
27+
})
28+
29+
script.addEventListener('error', () => {
30+
setState({ error: true, ready: false })
31+
})
32+
33+
document.head.appendChild(script)
34+
}, [url])
35+
36+
return state
37+
}

frontend/global.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ declare global {
9292
}
9393
const PanelSearch: typeof Component
9494
const CodeHelp: typeof Component
95+
// Chargebee SDK (loaded via useScript)
96+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
97+
const Chargebee: any
98+
9599
interface Window {
96100
E2E: boolean
97101
$crisp: Crisp
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import React, { FC, useEffect } from 'react'
2+
import Constants from 'common/constants'
3+
import InfoMessage from 'components/InfoMessage'
4+
import BlockedOrgInfo from 'components/BlockedOrgInfo'
5+
import { Organisation } from 'common/types/responses'
6+
import { PricingToggle } from './PricingToggle'
7+
import { PricingPanel } from './PricingPanel'
8+
import { STARTUP_FEATURES, ENTERPRISE_FEATURES } from './pricingFeatures'
9+
import {
10+
CHARGEBEE_SCRIPT_URL,
11+
CONTACT_US_URL,
12+
ON_PREMISE_HOSTING_URL,
13+
SUPPORT_EMAIL,
14+
SUPPORT_EMAIL_URL,
15+
} from './constants'
16+
import { useScript } from 'common/hooks/useScript'
17+
import { usePaymentState } from './hooks'
18+
import { initChargebee } from './chargebee'
19+
20+
export type PaymentProps = {
21+
isDisableAccountText?: string
22+
organisation: Organisation
23+
isPaymentsEnabled?: boolean
24+
}
25+
26+
export const Payment: FC<PaymentProps> = ({
27+
isDisableAccountText,
28+
isPaymentsEnabled = false,
29+
organisation,
30+
}) => {
31+
const { error, ready } = useScript(CHARGEBEE_SCRIPT_URL)
32+
const { hasActiveSubscription, isAWS, plan, setYearly, yearly } =
33+
usePaymentState({ organisation })
34+
35+
useEffect(() => {
36+
API.trackPage(Constants.modals.PAYMENT)
37+
}, [])
38+
39+
useEffect(() => {
40+
if (ready && !error) {
41+
initChargebee({ isPaymentsEnabled })
42+
}
43+
}, [ready, error, isPaymentsEnabled])
44+
45+
if (isAWS) {
46+
return (
47+
<div className='col-md-8'>
48+
<InfoMessage collapseId='aws-marketplace'>
49+
Customers with AWS Marketplace subscriptions will need to{' '}
50+
<a href={CONTACT_US_URL} target='_blank' rel='noreferrer'>
51+
contact us
52+
</a>
53+
</InfoMessage>
54+
</div>
55+
)
56+
}
57+
58+
if (!ready) {
59+
return (
60+
<div className='text-center'>
61+
<Loader />
62+
</div>
63+
)
64+
}
65+
66+
return (
67+
<div>
68+
<div className='col-md-12'>
69+
<Row space className='mb-4'>
70+
{isDisableAccountText && (
71+
<div className='d-lg-flex flex-lg-row align-items-end justify-content-between w-100 gap-4'>
72+
<div>
73+
<h4>
74+
{isDisableAccountText}{' '}
75+
<a target='_blank' href={SUPPORT_EMAIL_URL} rel='noreferrer'>
76+
{SUPPORT_EMAIL}
77+
</a>
78+
</h4>
79+
</div>
80+
<div>
81+
<BlockedOrgInfo />
82+
</div>
83+
</div>
84+
)}
85+
</Row>
86+
87+
<PricingToggle isYearly={yearly} onChange={setYearly} />
88+
89+
<Row className='pricing-container align-start'>
90+
<PricingPanel
91+
title='Start-Up'
92+
priceYearly='40'
93+
priceMonthly='45'
94+
isYearly={yearly}
95+
chargebeePlanId={
96+
yearly
97+
? Project.plans?.startup?.annual
98+
: Project.plans?.startup?.monthly
99+
}
100+
isPurchased={plan.includes('startup')}
101+
isDisableAccount={isDisableAccountText}
102+
features={STARTUP_FEATURES}
103+
hasActiveSubscription={hasActiveSubscription}
104+
organisationId={organisation.id}
105+
/>
106+
107+
<PricingPanel
108+
title='Enterprise'
109+
isYearly={yearly}
110+
isEnterprise
111+
features={ENTERPRISE_FEATURES}
112+
hasActiveSubscription={hasActiveSubscription}
113+
organisationId={organisation.id}
114+
headerContent={
115+
<>
116+
Optional{' '}
117+
<a
118+
className='text-primary fw-bold'
119+
target='_blank'
120+
href={ON_PREMISE_HOSTING_URL}
121+
rel='noreferrer'
122+
>
123+
On Premise
124+
</a>{' '}
125+
or{' '}
126+
<a
127+
className='text-primary fw-bold'
128+
target='_blank'
129+
href={ON_PREMISE_HOSTING_URL}
130+
rel='noreferrer'
131+
>
132+
Private Cloud
133+
</a>{' '}
134+
Install
135+
</>
136+
}
137+
/>
138+
</Row>
139+
<div className='text-center mt-4'>
140+
*Need something in-between our Enterprise plan for users or API
141+
limits?
142+
<div>
143+
<a href={CONTACT_US_URL}>Reach out</a> to us and we'll help you out
144+
</div>
145+
</div>
146+
</div>
147+
</div>
148+
)
149+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React, { FC, ReactNode } from 'react'
2+
import { useChargebeeCheckout } from './hooks'
3+
4+
type PaymentButtonProps = {
5+
'data-cb-plan-id'?: string
6+
className?: string
7+
children?: ReactNode
8+
isDisableAccount?: string
9+
hasActiveSubscription: boolean
10+
organisationId: number
11+
}
12+
13+
export const PaymentButton: FC<PaymentButtonProps> = ({
14+
children,
15+
className,
16+
hasActiveSubscription,
17+
isDisableAccount,
18+
organisationId,
19+
...rest
20+
}) => {
21+
const planId = rest['data-cb-plan-id']
22+
const { isLoading, openCheckout } = useChargebeeCheckout({
23+
onSuccess: isDisableAccount
24+
? () => {
25+
window.location.href = '/organisations'
26+
}
27+
: undefined,
28+
organisationId,
29+
})
30+
31+
if (hasActiveSubscription) {
32+
return (
33+
<button
34+
onClick={() => planId && openCheckout(planId)}
35+
className={className}
36+
type='button'
37+
disabled={isLoading}
38+
>
39+
{isLoading ? 'Processing...' : children}
40+
</button>
41+
)
42+
}
43+
44+
return (
45+
<button
46+
type='button'
47+
data-cb-type='checkout'
48+
data-cb-plan-id={planId}
49+
className={className}
50+
>
51+
{children}
52+
</button>
53+
)
54+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react'
2+
import Icon from 'components/Icon'
3+
import { PricingFeature } from './types'
4+
5+
export type PricingFeaturesListProps = {
6+
features: PricingFeature[]
7+
iconClass?: string
8+
}
9+
10+
export const PricingFeaturesList = ({
11+
features,
12+
iconClass = 'text-success',
13+
}: PricingFeaturesListProps) => {
14+
return (
15+
<ul className='pricing-features mb-0 px-2'>
16+
{features.map((feature, index) => (
17+
<li key={index}>
18+
<Row className='mb-3 pricing-features-item'>
19+
<span className={iconClass}>
20+
<Icon name='checkmark-circle' />
21+
</span>
22+
<div className='ml-2'>{feature.text}</div>
23+
</Row>
24+
</li>
25+
))}
26+
</ul>
27+
)
28+
}

0 commit comments

Comments
 (0)