Merge pull request #11751 from overleaf/ii-payment-page-stories

[web] Payment page stories

GitOrigin-RevId: 9f8aff0cf839bc811d8612e97bd627b577860cc8
This commit is contained in:
ilkin-overleaf 2023-02-14 10:17:02 +02:00 committed by Copybot
parent 9eff0140a9
commit c09e29c040
23 changed files with 1049 additions and 3 deletions

View file

@ -2,13 +2,13 @@ import { useTranslation } from 'react-i18next'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import Icon from '../../../../../shared/components/icon' import Icon from '../../../../../shared/components/icon'
type CardSubmitButtonProps = { type SubmitButtonProps = {
isProcessing: boolean isProcessing: boolean
isFormValid: boolean isFormValid: boolean
children: React.ReactNode children: React.ReactNode
} }
function SubmitButton(props: CardSubmitButtonProps) { function SubmitButton(props: SubmitButtonProps) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (

View file

@ -333,7 +333,9 @@ function usePayment({ publicKey }: RecurlyOptions) {
return { value } return { value }
} }
const PaymentContext = createContext<PaymentContextValue | undefined>(undefined) export const PaymentContext = createContext<PaymentContextValue | undefined>(
undefined
)
type PaymentProviderProps = { type PaymentProviderProps = {
publicKey: string publicKey: string

View file

@ -0,0 +1,31 @@
import { useState, useEffect } from 'react'
type ExternalScriptLoaderProps = {
children: JSX.Element
src: string
}
function ExternalScriptLoader({ children, src }: ExternalScriptLoaderProps) {
const [loaded, setLoaded] = useState(false)
useEffect(() => {
const body = document.querySelector('body')
const script = document.createElement('script')
script.async = true
script.src = src
script.onload = () => {
setLoaded(true)
}
body?.appendChild(script)
return () => {
body?.removeChild(script)
}
}, [src])
return loaded ? children : null
}
export default ExternalScriptLoader

View file

@ -0,0 +1,64 @@
import { useState } from 'react'
import AddressFirstLineComponent from '../../../../js/features/subscription/components/new/checkout/address-first-line'
type Args = Pick<
React.ComponentProps<typeof AddressFirstLineComponent>,
'errorFields'
>
const Template = ({ errorFields }: Args) => {
const [value, setValue] = useState('')
return (
<AddressFirstLineComponent
errorFields={errorFields}
value={value}
onChange={e => setValue(e.target.value)}
/>
)
}
export const AddressFirstLineDefault = Template.bind({}) as typeof Template & {
args: Args
}
AddressFirstLineDefault.args = {
errorFields: {
address1: false,
},
}
export const AddressFirstLineError = Template.bind({}) as typeof Template & {
args: Args
}
AddressFirstLineError.args = {
errorFields: {
address1: true,
},
}
export default {
title: 'Subscription / New / Checkout / Form Fields',
component: AddressFirstLineComponent,
argTypes: {
value: {
table: {
disable: true,
},
},
onChange: {
table: {
disable: true,
},
},
},
decorators: [
(Story: React.ComponentType) => (
<div
className="card card-highlighted card-border"
style={{ maxWidth: '500px' }}
>
<Story />
</div>
),
],
}

View file

@ -0,0 +1,64 @@
import { useState } from 'react'
import AddressSecondLineComponent from '../../../../js/features/subscription/components/new/checkout/address-second-line'
type Args = Pick<
React.ComponentProps<typeof AddressSecondLineComponent>,
'errorFields'
>
const Template = ({ errorFields }: Args) => {
const [value, setValue] = useState('')
return (
<AddressSecondLineComponent
errorFields={errorFields}
value={value}
onChange={e => setValue(e.target.value)}
/>
)
}
export const AddressSecondLineDefault = Template.bind({}) as typeof Template & {
args: Args
}
AddressSecondLineDefault.args = {
errorFields: {
address2: false,
},
}
export const AddressSecondLineError = Template.bind({}) as typeof Template & {
args: Args
}
AddressSecondLineError.args = {
errorFields: {
address2: true,
},
}
export default {
title: 'Subscription / New / Checkout / Form Fields',
component: AddressSecondLineComponent,
argTypes: {
value: {
table: {
disable: true,
},
},
onChange: {
table: {
disable: true,
},
},
},
decorators: [
(Story: React.ComponentType) => (
<div
className="card card-highlighted card-border"
style={{ maxWidth: '500px' }}
>
<Story />
</div>
),
],
}

View file

@ -0,0 +1,40 @@
import { useRef } from 'react'
import CardElementComponent from '../../../../js/features/subscription/components/new/checkout/card-element'
import ExternalScriptLoader from '../../../../js/shared/utils/external-script-loader'
export const CardElement = () => {
const elements = useRef(recurly.Elements())
return (
<CardElementComponent elements={elements.current} onChange={() => {}} />
)
}
export default {
title: 'Subscription / New / Checkout / Form Fields',
component: CardElementComponent,
argTypes: {
elements: {
table: {
disable: true,
},
},
onChange: {
table: {
disable: true,
},
},
},
decorators: [
(Story: React.ComponentType) => (
<div
className="card card-highlighted card-border"
style={{ maxWidth: '500px' }}
>
<ExternalScriptLoader src="https://js.recurly.com/v4/recurly.js">
<Story />
</ExternalScriptLoader>
</div>
),
],
}

View file

@ -0,0 +1,65 @@
import { useState } from 'react'
import CompanyDetailsComponent from '../../../../js/features/subscription/components/new/checkout/company-details'
import { PaymentProvider } from '../helpers/context-provider'
import {
PaymentContextValue,
PricingFormState,
} from '../../../../js/features/subscription/context/types/payment-context-value'
type Args = Pick<
React.ComponentProps<typeof CompanyDetailsComponent>,
'taxesCount'
>
export const CompanyDetails = (args: Args) => {
const [pricingFormState, setPricingFormState] = useState<PricingFormState>({
first_name: '',
last_name: '',
postal_code: '',
address1: '',
address2: '',
state: '',
city: '',
company: '',
vat_number: '',
country: 'GB',
coupon: '',
})
const providerValue = {
applyVatNumber: () => {},
pricingFormState,
setPricingFormState,
} as unknown as PaymentContextValue
return (
<PaymentProvider value={providerValue}>
<CompanyDetailsComponent {...args} />
</PaymentProvider>
)
}
export default {
title: 'Subscription / New / Checkout / Form Fields',
component: CompanyDetailsComponent,
argTypes: {
taxesCount: {
control: {
type: 'number',
},
},
},
args: {
taxesCount: 1,
},
decorators: [
(Story: React.ComponentType) => (
<div
className="card card-highlighted card-border"
style={{ maxWidth: '500px' }}
>
<Story />
</div>
),
],
}

View file

@ -0,0 +1,74 @@
import { useState } from 'react'
import countries from '../../../../js/features/subscription/data/countries'
import CountrySelectComponent from '../../../../js/features/subscription/components/new/checkout/country-select'
import { PaymentProvider } from '../helpers/context-provider'
import { PaymentContextValue } from '../../../../js/features/subscription/context/types/payment-context-value'
type Args = Pick<
React.ComponentProps<typeof CountrySelectComponent>,
'errorFields'
>
const Template = ({ errorFields }: Args) => {
const [value, setValue] = useState<typeof countries[number]['code']>('GB')
const providerValue = {
updateCountry: () => {},
} as unknown as PaymentContextValue
return (
<PaymentProvider value={providerValue}>
<CountrySelectComponent
errorFields={errorFields}
value={value}
onChange={e =>
setValue(e.target.value as typeof countries[number]['code'])
}
/>
</PaymentProvider>
)
}
export const CountrySelectDefault = Template.bind({}) as typeof Template & {
args: Args
}
CountrySelectDefault.args = {
errorFields: {
country: false,
},
}
export const CountrySelectError = Template.bind({}) as typeof Template & {
args: Args
}
CountrySelectError.args = {
errorFields: {
country: true,
},
}
export default {
title: 'Subscription / New / Checkout / Form Fields',
component: CountrySelectComponent,
argTypes: {
value: {
table: {
disable: true,
},
},
onChange: {
table: {
disable: true,
},
},
},
decorators: [
(Story: React.ComponentType) => (
<div
className="card card-highlighted card-border"
style={{ maxWidth: '500px' }}
>
<Story />
</div>
),
],
}

View file

@ -0,0 +1,47 @@
import { useState } from 'react'
import CouponCodeComponent from '../../../../js/features/subscription/components/new/checkout/coupon-code'
import { PaymentProvider } from '../helpers/context-provider'
import { PaymentContextValue } from '../../../../js/features/subscription/context/types/payment-context-value'
export const CouponCode = () => {
const [value, setValue] = useState('')
const providerValue = {
addCoupon: () => {},
} as unknown as PaymentContextValue
return (
<PaymentProvider value={providerValue}>
<CouponCodeComponent
value={value}
onChange={e => setValue(e.target.value)}
/>
</PaymentProvider>
)
}
export default {
title: 'Subscription / New / Checkout / Form Fields',
component: CouponCodeComponent,
argTypes: {
value: {
table: {
disable: true,
},
},
onChange: {
table: {
disable: true,
},
},
},
decorators: [
(Story: React.ComponentType) => (
<div
className="card card-highlighted card-border"
style={{ maxWidth: '500px' }}
>
<Story />
</div>
),
],
}

View file

@ -0,0 +1,61 @@
import { useState } from 'react'
import FirstNameComponent from '../../../../js/features/subscription/components/new/checkout/first-name'
type Args = Pick<React.ComponentProps<typeof FirstNameComponent>, 'errorFields'>
const Template = ({ errorFields }: Args) => {
const [value, setValue] = useState('')
return (
<FirstNameComponent
errorFields={errorFields}
value={value}
onChange={e => setValue(e.target.value)}
/>
)
}
export const FirstNameDefault = Template.bind({}) as typeof Template & {
args: Args
}
FirstNameDefault.args = {
errorFields: {
first_name: false,
},
}
export const FirstNameError = Template.bind({}) as typeof Template & {
args: Args
}
FirstNameError.args = {
errorFields: {
first_name: true,
},
}
export default {
title: 'Subscription / New / Checkout / Form Fields',
component: FirstNameComponent,
argTypes: {
value: {
table: {
disable: true,
},
},
onChange: {
table: {
disable: true,
},
},
},
decorators: [
(Story: React.ComponentType) => (
<div
className="card card-highlighted card-border"
style={{ maxWidth: '500px' }}
>
<Story />
</div>
),
],
}

View file

@ -0,0 +1,61 @@
import { useState } from 'react'
import LastNameComponent from '../../../../js/features/subscription/components/new/checkout/last-name'
type Args = Pick<React.ComponentProps<typeof LastNameComponent>, 'errorFields'>
const Template = ({ errorFields }: Args) => {
const [value, setValue] = useState('')
return (
<LastNameComponent
errorFields={errorFields}
value={value}
onChange={e => setValue(e.target.value)}
/>
)
}
export const LastNameDefault = Template.bind({}) as typeof Template & {
args: Args
}
LastNameDefault.args = {
errorFields: {
last_name: false,
},
}
export const LastNameError = Template.bind({}) as typeof Template & {
args: Args
}
LastNameError.args = {
errorFields: {
last_name: true,
},
}
export default {
title: 'Subscription / New / Checkout / Form Fields',
component: LastNameComponent,
argTypes: {
value: {
table: {
disable: true,
},
},
onChange: {
table: {
disable: true,
},
},
},
decorators: [
(Story: React.ComponentType) => (
<div
className="card card-highlighted card-border"
style={{ maxWidth: '500px' }}
>
<Story />
</div>
),
],
}

View file

@ -0,0 +1,28 @@
import { useState } from 'react'
import PaymentMethodToggleComponent from '../../../../js/features/subscription/components/new/checkout/payment-method-toggle'
export const PaymentMethodToggle = () => {
const [paymentMethod, setPaymentMethod] = useState('credit_card')
return (
<PaymentMethodToggleComponent
paymentMethod={paymentMethod}
onChange={e => setPaymentMethod(e.target.value)}
/>
)
}
export default {
title: 'Subscription / New / Checkout',
component: PaymentMethodToggleComponent,
decorators: [
(Story: React.ComponentType) => (
<div
className="card card-highlighted card-border"
style={{ maxWidth: '500px' }}
>
<Story />
</div>
),
],
}

View file

@ -0,0 +1,64 @@
import { useState } from 'react'
import PostalCodeComponent from '../../../../js/features/subscription/components/new/checkout/postal-code'
type Args = Pick<
React.ComponentProps<typeof PostalCodeComponent>,
'errorFields'
>
const Template = ({ errorFields }: Args) => {
const [value, setValue] = useState('')
return (
<PostalCodeComponent
errorFields={errorFields}
value={value}
onChange={e => setValue(e.target.value)}
/>
)
}
export const PostalCodeDefault = Template.bind({}) as typeof Template & {
args: Args
}
PostalCodeDefault.args = {
errorFields: {
postal_code: false,
},
}
export const PostalCodeError = Template.bind({}) as typeof Template & {
args: Args
}
PostalCodeError.args = {
errorFields: {
postal_code: true,
},
}
export default {
title: 'Subscription / New / Checkout / Form Fields',
component: PostalCodeComponent,
argTypes: {
value: {
table: {
disable: true,
},
},
onChange: {
table: {
disable: true,
},
},
},
decorators: [
(Story: React.ComponentType) => (
<div
className="card card-highlighted card-border"
style={{ maxWidth: '500px' }}
>
<Story />
</div>
),
],
}

View file

@ -0,0 +1,43 @@
import PriceSwitchHeaderComponent from '../../../../js/features/subscription/components/new/checkout/price-switch-header'
type Args = React.ComponentProps<typeof PriceSwitchHeaderComponent>
export const PriceSwitchHeader = (args: Args) => (
<PriceSwitchHeaderComponent {...args} />
)
const options = {
current: 'fake_plan',
other: 'fake_plan_new',
}
export default {
title: 'Subscription / New / Checkout',
component: PriceSwitchHeaderComponent,
argTypes: {
planCode: {
options: Object.values(options),
control: {
type: 'select',
labels: Object.entries(options).reduce(
(prev, [key, value]) => ({ ...prev, [String(value)]: key }),
{}
),
},
},
},
args: {
planCode: 'fake_plan',
planCodes: ['fake_plan'],
},
decorators: [
(Story: React.ComponentType) => (
<div
className="card card-highlighted card-border"
style={{ maxWidth: '500px' }}
>
<Story />
</div>
),
],
}

View file

@ -0,0 +1,47 @@
import SubmitButtonComponent from '../../../../js/features/subscription/components/new/checkout/submit-button'
type Args = React.ComponentProps<typeof SubmitButtonComponent>
const Template = (args: Args) => <SubmitButtonComponent {...args} />
export const SubmitButton = Template.bind({}) as typeof Template & {
args: Args
}
SubmitButton.args = {
isProcessing: false,
isFormValid: true,
children: 'Submit',
}
export const SubmitButtonDisabled = Template.bind({}) as typeof Template & {
args: Args
}
SubmitButtonDisabled.args = {
isProcessing: false,
isFormValid: false,
children: 'Submit',
}
export const SubmitButtonProcessing = Template.bind({}) as typeof Template & {
args: Args
}
SubmitButtonProcessing.args = {
isProcessing: true,
isFormValid: true,
children: 'Submit',
}
export default {
title: 'Subscription / New / Checkout / Submit Button',
component: SubmitButtonComponent,
decorators: [
(Story: React.ComponentType) => (
<div
className="card card-highlighted card-border"
style={{ maxWidth: '500px' }}
>
<Story />
</div>
),
],
}

View file

@ -0,0 +1,17 @@
import TosAgreementNoticeComponent from '../../../../js/features/subscription/components/new/checkout/tos-agreement-notice'
export const TosAgreementNotice = () => <TosAgreementNoticeComponent />
export default {
title: 'Subscription / New / Checkout',
decorators: [
(Story: React.ComponentType) => (
<div
className="card card-highlighted card-border"
style={{ maxWidth: '500px' }}
>
<Story />
</div>
),
],
}

View file

@ -0,0 +1,13 @@
import { PaymentContext } from '../../../../js/features/subscription/context/payment-context'
import { PaymentContextValue } from '../../../../js/features/subscription/context/types/payment-context-value'
type PaymentProviderProps = {
children: React.ReactNode
value: PaymentContextValue
}
export function PaymentProvider({ value, children }: PaymentProviderProps) {
return (
<PaymentContext.Provider value={value}>{children}</PaymentContext.Provider>
)
}

View file

@ -0,0 +1,40 @@
import CollaboratorsComponent from '../../../../js/features/subscription/components/new/payment-preview/collaborators'
type Args = React.ComponentProps<typeof CollaboratorsComponent>
export const Collaborators = (args: Args) => (
<CollaboratorsComponent {...args} />
)
const options = {
unlimited: -1,
single: 1,
multiple: 2,
}
export default {
title: 'Subscription / New / Payment Preview',
component: CollaboratorsComponent,
argTypes: {
count: {
options: Object.values(options),
control: {
type: 'select',
labels: Object.entries(options).reduce(
(prev, [key, value]) => ({ ...prev, [String(value)]: key }),
{}
),
},
},
},
args: {
count: options.single, // default
},
decorators: [
(Story: React.ComponentType) => (
<div style={{ maxWidth: '300px' }}>
<Story />
</div>
),
],
}

View file

@ -0,0 +1,33 @@
import FeaturesListComponent from '../../../../js/features/subscription/components/new/payment-preview/features-list'
import { Plan } from '../../../../../types/subscription/plan'
type Args = React.ComponentProps<typeof FeaturesListComponent>
export const FeaturesList = (args: Args) => <FeaturesListComponent {...args} />
const features = {
compileTimeout: 2,
dropbox: true,
github: true,
versioning: true,
trackChanges: true,
references: true,
mendeley: true,
zotero: true,
symbolPalette: true,
} as unknown as Plan['features']
export default {
title: 'Subscription / New / Payment Preview',
component: FeaturesListComponent,
args: {
features,
},
decorators: [
(Story: React.ComponentType) => (
<div style={{ maxWidth: '300px' }}>
<Story />
</div>
),
],
}

View file

@ -0,0 +1,93 @@
import NoDiscountPriceComponent from '../../../../js/features/subscription/components/new/payment-preview/no-discount-price'
import { PaymentProvider } from '../helpers/context-provider'
import { PaymentContextValue } from '../../../../js/features/subscription/context/types/payment-context-value'
type Args = Pick<React.ComponentProps<typeof PaymentProvider>, 'value'>
const Template = (args: Args) => {
return (
<PaymentProvider value={args.value}>
<NoDiscountPriceComponent />
</PaymentProvider>
)
}
const commonValues = {
currencySymbol: '$',
coupon: {
normalPrice: 2,
},
}
export const ThenXPricePerMonth = Template.bind({}) as typeof Template & {
args: Args
}
const thenXPricePerMonthValue = {
...commonValues,
monthlyBilling: true,
coupon: {
...commonValues.coupon,
discountMonths: 2,
singleUse: false,
},
} as PaymentContextValue
ThenXPricePerMonth.args = {
value: thenXPricePerMonthValue,
}
export const ThenXPricePerYear = Template.bind({}) as typeof Template & {
args: Args
}
const thenXPricePerYearValue = {
...commonValues,
monthlyBilling: false,
coupon: {
...commonValues.coupon,
singleUse: true,
},
} as PaymentContextValue
ThenXPricePerYear.args = {
value: thenXPricePerYearValue,
}
export const NormallyXPricePerMonth = Template.bind({}) as typeof Template & {
args: Args
}
const normallyXPricePerMonthValue = {
...commonValues,
monthlyBilling: true,
coupon: {
...commonValues.coupon,
singleUse: false,
},
} as PaymentContextValue
NormallyXPricePerMonth.args = {
value: normallyXPricePerMonthValue,
}
export const NormallyXPricePerYear = Template.bind({}) as typeof Template & {
args: Args
}
const normallyXPricePerYearValue = {
...commonValues,
monthlyBilling: false,
coupon: {
...commonValues.coupon,
singleUse: false,
},
} as PaymentContextValue
NormallyXPricePerYear.args = {
value: normallyXPricePerYearValue,
}
export default {
title: 'Subscription / New / Payment Preview / No Discount Price',
component: NoDiscountPriceComponent,
decorators: [
(Story: React.ComponentType) => (
<div style={{ maxWidth: '300px' }}>
<Story />
</div>
),
],
}

View file

@ -0,0 +1,75 @@
import PriceForFirstXPeriod from '../../../../js/features/subscription/components/new/payment-preview/price-for-first-x-period'
import { PaymentProvider } from '../helpers/context-provider'
import { PaymentContextValue } from '../../../../js/features/subscription/context/types/payment-context-value'
type Args = Pick<React.ComponentProps<typeof PaymentProvider>, 'value'>
const Template = (args: Args) => {
return (
<PaymentProvider value={args.value}>
<PriceForFirstXPeriod />
</PaymentProvider>
)
}
const commonValues = {
currencySymbol: '$',
recurlyPrice: {
total: '10.00',
},
}
export const XPriceForYMonths = Template.bind({}) as typeof Template & {
args: Args
}
const xPriceForYMonthsValue = {
...commonValues,
monthlyBilling: true,
coupon: {
discountMonths: 2,
singleUse: false,
},
} as PaymentContextValue
XPriceForYMonths.args = {
value: xPriceForYMonthsValue,
}
export const XPriceForFirstMonth = Template.bind({}) as typeof Template & {
args: Args
}
const xPriceForFirstMonthValue = {
...commonValues,
monthlyBilling: true,
coupon: {
singleUse: true,
},
} as PaymentContextValue
XPriceForFirstMonth.args = {
value: xPriceForFirstMonthValue,
}
export const XPriceForFirstYear = Template.bind({}) as typeof Template & {
args: Args
}
const xPriceForFirstYearValue = {
...commonValues,
monthlyBilling: false,
coupon: {
singleUse: true,
},
} as PaymentContextValue
XPriceForFirstYear.args = {
value: xPriceForFirstYearValue,
}
export default {
title: 'Subscription / New / Payment Preview / Price For First X Period',
component: PriceForFirstXPeriod,
decorators: [
(Story: React.ComponentType) => (
<div style={{ maxWidth: '300px' }}>
<Story />
</div>
),
],
}

View file

@ -0,0 +1,51 @@
import PriceSummaryComponent from '../../../../js/features/subscription/components/new/payment-preview/price-summary'
import { PaymentProvider } from '../helpers/context-provider'
type Args = Pick<React.ComponentProps<typeof PaymentProvider>, 'value'>
export const PriceSummary = (args: Args) => {
return (
<PaymentProvider value={args.value}>
<PriceSummaryComponent />
</PaymentProvider>
)
}
export default {
title: 'Subscription / New / Payment Preview',
component: PriceSummaryComponent,
args: {
value: {
currencyCode: 'USD',
currencySymbol: '$',
coupon: {
name: 'react',
normalPriceWithoutTax: 15,
},
changeCurrency: (_eventKey: string) => {},
limitedCurrencies: {
USD: '$',
EUR: '€',
},
monthlyBilling: true,
planName: 'Test plan',
recurlyPrice: {
discount: '3',
tax: '5.00',
total: '10.00',
},
taxes: [
{
rate: '1',
},
],
},
},
decorators: [
(Story: React.ComponentType) => (
<div style={{ maxWidth: '300px' }}>
<Story />
</div>
),
],
}

View file

@ -0,0 +1,33 @@
import TrialPriceComponent from '../../../../js/features/subscription/components/new/payment-preview/trial-price'
import { PaymentProvider } from '../helpers/context-provider'
type Args = Pick<React.ComponentProps<typeof PaymentProvider>, 'value'>
export const TrialPrice = (args: Args) => {
return (
<PaymentProvider value={args.value}>
<TrialPriceComponent />
</PaymentProvider>
)
}
export default {
title: 'Subscription / New / Payment Preview',
component: TrialPriceComponent,
args: {
value: {
currencySymbol: '$',
recurlyPrice: {
total: '10.00',
},
trialLength: 7,
},
},
decorators: [
(Story: React.ComponentType) => (
<div style={{ maxWidth: '300px' }}>
<Story />
</div>
),
],
}