Преглед изворни кода

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

[web] Payment page stories

GitOrigin-RevId: 9f8aff0cf839bc811d8612e97bd627b577860cc8
ilkin-overleaf пре 3 година
родитељ
комит
c09e29c040
23 измењених фајлова са 1049 додато и 3 уклоњено
  1. 2 2
      services/web/frontend/js/features/subscription/components/new/checkout/submit-button.tsx
  2. 3 1
      services/web/frontend/js/features/subscription/context/payment-context.tsx
  3. 31 0
      services/web/frontend/js/shared/utils/external-script-loader.ts
  4. 64 0
      services/web/frontend/stories/subscription/new/checkout/address-first-line.stories.tsx
  5. 64 0
      services/web/frontend/stories/subscription/new/checkout/address-second-line.stories.tsx
  6. 40 0
      services/web/frontend/stories/subscription/new/checkout/card-element.stories.tsx
  7. 65 0
      services/web/frontend/stories/subscription/new/checkout/company-details.stories.tsx
  8. 74 0
      services/web/frontend/stories/subscription/new/checkout/country-select.stories.tsx
  9. 47 0
      services/web/frontend/stories/subscription/new/checkout/coupon-code.stories.tsx
  10. 61 0
      services/web/frontend/stories/subscription/new/checkout/first-name.stories.tsx
  11. 61 0
      services/web/frontend/stories/subscription/new/checkout/last-name.stories.tsx
  12. 28 0
      services/web/frontend/stories/subscription/new/checkout/payment-method-toggle.stories.tsx
  13. 64 0
      services/web/frontend/stories/subscription/new/checkout/postal-code.stories.tsx
  14. 43 0
      services/web/frontend/stories/subscription/new/checkout/price-switch-header.stories.tsx
  15. 47 0
      services/web/frontend/stories/subscription/new/checkout/submit-button.stories.tsx
  16. 17 0
      services/web/frontend/stories/subscription/new/checkout/tos-agreement-notice.stories.tsx
  17. 13 0
      services/web/frontend/stories/subscription/new/helpers/context-provider.tsx
  18. 40 0
      services/web/frontend/stories/subscription/new/payment-preview/collaborators.stories.tsx
  19. 33 0
      services/web/frontend/stories/subscription/new/payment-preview/features-list.stories.tsx
  20. 93 0
      services/web/frontend/stories/subscription/new/payment-preview/no-discount-price.stories.tsx
  21. 75 0
      services/web/frontend/stories/subscription/new/payment-preview/price-for-first-x-period.stories.tsx
  22. 51 0
      services/web/frontend/stories/subscription/new/payment-preview/price-summary.stories.tsx
  23. 33 0
      services/web/frontend/stories/subscription/new/payment-preview/trial-price.stories.tsx

+ 2 - 2
services/web/frontend/js/features/subscription/components/new/checkout/submit-button.tsx

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

+ 3 - 1
services/web/frontend/js/features/subscription/context/payment-context.tsx

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

+ 31 - 0
services/web/frontend/js/shared/utils/external-script-loader.ts

@@ -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

+ 64 - 0
services/web/frontend/stories/subscription/new/checkout/address-first-line.stories.tsx

@@ -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>
+    ),
+  ],
+}

+ 64 - 0
services/web/frontend/stories/subscription/new/checkout/address-second-line.stories.tsx

@@ -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>
+    ),
+  ],
+}

+ 40 - 0
services/web/frontend/stories/subscription/new/checkout/card-element.stories.tsx

@@ -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>
+    ),
+  ],
+}

+ 65 - 0
services/web/frontend/stories/subscription/new/checkout/company-details.stories.tsx

@@ -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>
+    ),
+  ],
+}

+ 74 - 0
services/web/frontend/stories/subscription/new/checkout/country-select.stories.tsx

@@ -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>
+    ),
+  ],
+}

+ 47 - 0
services/web/frontend/stories/subscription/new/checkout/coupon-code.stories.tsx

@@ -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>
+    ),
+  ],
+}

+ 61 - 0
services/web/frontend/stories/subscription/new/checkout/first-name.stories.tsx

@@ -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>
+    ),
+  ],
+}

+ 61 - 0
services/web/frontend/stories/subscription/new/checkout/last-name.stories.tsx

@@ -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>
+    ),
+  ],
+}

+ 28 - 0
services/web/frontend/stories/subscription/new/checkout/payment-method-toggle.stories.tsx

@@ -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>
+    ),
+  ],
+}

+ 64 - 0
services/web/frontend/stories/subscription/new/checkout/postal-code.stories.tsx

@@ -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>
+    ),
+  ],
+}

+ 43 - 0
services/web/frontend/stories/subscription/new/checkout/price-switch-header.stories.tsx

@@ -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>
+    ),
+  ],
+}

+ 47 - 0
services/web/frontend/stories/subscription/new/checkout/submit-button.stories.tsx

@@ -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>
+    ),
+  ],
+}

+ 17 - 0
services/web/frontend/stories/subscription/new/checkout/tos-agreement-notice.stories.tsx

@@ -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>
+    ),
+  ],
+}

+ 13 - 0
services/web/frontend/stories/subscription/new/helpers/context-provider.tsx

@@ -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>
+  )
+}

+ 40 - 0
services/web/frontend/stories/subscription/new/payment-preview/collaborators.stories.tsx

@@ -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>
+    ),
+  ],
+}

+ 33 - 0
services/web/frontend/stories/subscription/new/payment-preview/features-list.stories.tsx

@@ -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>
+    ),
+  ],
+}

+ 93 - 0
services/web/frontend/stories/subscription/new/payment-preview/no-discount-price.stories.tsx

@@ -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>
+    ),
+  ],
+}

+ 75 - 0
services/web/frontend/stories/subscription/new/payment-preview/price-for-first-x-period.stories.tsx

@@ -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>
+    ),
+  ],
+}

+ 51 - 0
services/web/frontend/stories/subscription/new/payment-preview/price-summary.stories.tsx

@@ -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>
+    ),
+  ],
+}

+ 33 - 0
services/web/frontend/stories/subscription/new/payment-preview/trial-price.stories.tsx

@@ -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>
+    ),
+  ],
+}