Răsfoiți Sursa

Move checkout to subscriptions module (#15022)

* Move checkout to subscriptions module

GitOrigin-RevId: 0ad6587ddd7042aed7f2e18d9d0668e02942eb1e
Thomas 2 ani în urmă
părinte
comite
843b4f3940
58 a modificat fișierele cu 3 adăugiri și 4594 ștergeri
  1. 3 160
      services/web/app/src/Features/Subscription/SubscriptionController.js
  2. 0 14
      services/web/app/src/Features/Subscription/SubscriptionRouter.js
  3. 0 37
      services/web/app/views/subscriptions/new-react.pug
  4. 0 11
      services/web/app/views/subscriptions/restricted-country.pug
  5. 0 11
      services/web/app/views/subscriptions/unconfirmed-primary-email.pug
  6. 0 64
      services/web/frontend/js/features/subscription/components/new/checkout/address-first-line.tsx
  7. 0 53
      services/web/frontend/js/features/subscription/components/new/checkout/address-second-line.tsx
  8. 0 78
      services/web/frontend/js/features/subscription/components/new/checkout/card-element.tsx
  9. 0 421
      services/web/frontend/js/features/subscription/components/new/checkout/checkout-panel.tsx
  10. 0 82
      services/web/frontend/js/features/subscription/components/new/checkout/company-details.tsx
  11. 0 68
      services/web/frontend/js/features/subscription/components/new/checkout/country-select.tsx
  12. 0 34
      services/web/frontend/js/features/subscription/components/new/checkout/coupon-code.tsx
  13. 0 46
      services/web/frontend/js/features/subscription/components/new/checkout/first-name.tsx
  14. 0 46
      services/web/frontend/js/features/subscription/components/new/checkout/last-name.tsx
  15. 0 60
      services/web/frontend/js/features/subscription/components/new/checkout/payment-method-toggle.tsx
  16. 0 46
      services/web/frontend/js/features/subscription/components/new/checkout/postal-code.tsx
  17. 0 32
      services/web/frontend/js/features/subscription/components/new/checkout/price-switch-header.tsx
  18. 0 32
      services/web/frontend/js/features/subscription/components/new/checkout/submit-button.tsx
  19. 0 58
      services/web/frontend/js/features/subscription/components/new/checkout/three-d-secure.tsx
  20. 0 17
      services/web/frontend/js/features/subscription/components/new/checkout/tos-agreement-notice.tsx
  21. 0 38
      services/web/frontend/js/features/subscription/components/new/payment-preview/collaborators.tsx
  22. 0 40
      services/web/frontend/js/features/subscription/components/new/payment-preview/currency-dropdown.tsx
  23. 0 33
      services/web/frontend/js/features/subscription/components/new/payment-preview/features-list.tsx
  24. 0 38
      services/web/frontend/js/features/subscription/components/new/payment-preview/no-discount-price.tsx
  25. 0 29
      services/web/frontend/js/features/subscription/components/new/payment-preview/payment-preview-panel.tsx
  26. 0 49
      services/web/frontend/js/features/subscription/components/new/payment-preview/price-for-first-x-period.tsx
  27. 0 84
      services/web/frontend/js/features/subscription/components/new/payment-preview/price-summary.tsx
  28. 0 24
      services/web/frontend/js/features/subscription/components/new/payment-preview/trial-coupon-summary.tsx
  29. 0 37
      services/web/frontend/js/features/subscription/components/new/payment-preview/trial-price.tsx
  30. 0 37
      services/web/frontend/js/features/subscription/components/new/root.tsx
  31. 0 365
      services/web/frontend/js/features/subscription/context/payment-context.tsx
  32. 0 67
      services/web/frontend/js/features/subscription/context/types/payment-context-value.tsx
  33. 0 64
      services/web/frontend/stories/subscription/new/checkout/address-first-line.stories.tsx
  34. 0 64
      services/web/frontend/stories/subscription/new/checkout/address-second-line.stories.tsx
  35. 0 40
      services/web/frontend/stories/subscription/new/checkout/card-element.stories.tsx
  36. 0 65
      services/web/frontend/stories/subscription/new/checkout/company-details.stories.tsx
  37. 0 74
      services/web/frontend/stories/subscription/new/checkout/country-select.stories.tsx
  38. 0 47
      services/web/frontend/stories/subscription/new/checkout/coupon-code.stories.tsx
  39. 0 61
      services/web/frontend/stories/subscription/new/checkout/first-name.stories.tsx
  40. 0 61
      services/web/frontend/stories/subscription/new/checkout/last-name.stories.tsx
  41. 0 28
      services/web/frontend/stories/subscription/new/checkout/payment-method-toggle.stories.tsx
  42. 0 64
      services/web/frontend/stories/subscription/new/checkout/postal-code.stories.tsx
  43. 0 43
      services/web/frontend/stories/subscription/new/checkout/price-switch-header.stories.tsx
  44. 0 47
      services/web/frontend/stories/subscription/new/checkout/submit-button.stories.tsx
  45. 0 17
      services/web/frontend/stories/subscription/new/checkout/tos-agreement-notice.stories.tsx
  46. 0 13
      services/web/frontend/stories/subscription/new/helpers/context-provider.tsx
  47. 0 40
      services/web/frontend/stories/subscription/new/payment-preview/collaborators.stories.tsx
  48. 0 33
      services/web/frontend/stories/subscription/new/payment-preview/features-list.stories.tsx
  49. 0 93
      services/web/frontend/stories/subscription/new/payment-preview/no-discount-price.stories.tsx
  50. 0 75
      services/web/frontend/stories/subscription/new/payment-preview/price-for-first-x-period.stories.tsx
  51. 0 51
      services/web/frontend/stories/subscription/new/payment-preview/price-summary.stories.tsx
  52. 0 33
      services/web/frontend/stories/subscription/new/payment-preview/trial-price.stories.tsx
  53. 0 452
      services/web/test/frontend/features/subscription/components/new/checkout.spec.tsx
  54. 0 68
      services/web/test/frontend/features/subscription/components/new/common.spec.tsx
  55. 0 310
      services/web/test/frontend/features/subscription/components/new/payment-preview.spec.tsx
  56. 0 232
      services/web/test/frontend/features/subscription/fixtures/recurly-mock.ts
  57. 0 12
      services/web/test/frontend/features/subscription/helpers/payment.ts
  58. 0 296
      services/web/test/unit/src/Subscription/SubscriptionControllerTests.js

+ 3 - 160
services/web/app/src/Features/Subscription/SubscriptionController.js

@@ -1,6 +1,5 @@
 const SessionManager = require('../Authentication/SessionManager')
 const SubscriptionHandler = require('./SubscriptionHandler')
-const PlansLocator = require('./PlansLocator')
 const SubscriptionViewModelBuilder = require('./SubscriptionViewModelBuilder')
 const LimitationsManager = require('./LimitationsManager')
 const RecurlyWrapper = require('./RecurlyWrapper')
@@ -13,9 +12,6 @@ const plansConfig = require('./plansConfig')
 const interstitialPaymentConfig = require('./interstitialPaymentConfig')
 const GroupPlansData = require('./GroupPlansData')
 const V1SubscriptionManager = require('./V1SubscriptionManager')
-const Errors = require('../Errors/Errors')
-const HttpErrorHandler = require('../Errors/HttpErrorHandler')
-const SubscriptionErrors = require('./Errors')
 const AnalyticsManager = require('../Analytics/AnalyticsManager')
 const RecurlyEventHandler = require('./RecurlyEventHandler')
 const { expressify } = require('../../util/promises')
@@ -23,8 +19,6 @@ const OError = require('@overleaf/o-error')
 const SplitTestHandler = require('../SplitTests/SplitTestHandler')
 const SubscriptionHelper = require('./SubscriptionHelper')
 const Features = require('../../infrastructure/Features')
-const UserGetter = require('../User/UserGetter')
-const Modules = require('../../infrastructure/Modules')
 const AuthorizationManager = require('../Authorization/AuthorizationManager')
 
 const groupPlanModalOptions = Settings.groupPlanModalOptions
@@ -188,83 +182,6 @@ async function plansPage(req, res) {
   })
 }
 
-/**
- * @param {import('express').Request} req
- * @param {import('express').Response} res
- * @returns {Promise<void>}
- */
-async function paymentPage(req, res) {
-  const user = SessionManager.getSessionUser(req.session)
-  const plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
-  if (!plan) {
-    return HttpErrorHandler.unprocessableEntity(req, res, 'Plan not found')
-  }
-  const hasSubscription =
-    await LimitationsManager.promises.userHasV1OrV2Subscription(user)
-  if (hasSubscription) {
-    res.redirect('/user/subscription?hasSubscription=true')
-  } else {
-    // LimitationsManager.userHasV2Subscription only checks Mongo. Double check with
-    // Recurly as well at this point (we don't do this most places for speed).
-    const valid =
-      await SubscriptionHandler.promises.validateNoSubscriptionInRecurly(
-        user._id
-      )
-    if (!valid) {
-      res.redirect('/user/subscription?hasSubscription=true')
-    } else {
-      let currency = null
-      if (req.query.currency) {
-        const queryCurrency = req.query.currency.toUpperCase()
-        if (GeoIpLookup.isValidCurrencyParam(queryCurrency)) {
-          currency = queryCurrency
-        }
-      }
-      const { recommendedCurrency, countryCode } =
-        await _getRecommendedCurrency(req, res)
-      if (recommendedCurrency && currency == null) {
-        currency = recommendedCurrency
-      }
-
-      // Block web sales to restricted countries
-      if (Settings.restrictedCountries.includes(countryCode)) {
-        return res.render('subscriptions/restricted-country', {
-          title: 'restricted',
-        })
-      }
-
-      res.render('subscriptions/new-react', {
-        title: 'subscribe',
-        currency,
-        countryCode,
-        plan,
-        planCode: req.query.planCode,
-        couponCode: req.query.cc,
-        showCouponField: !!req.query.scf,
-        itm_campaign: req.query.itm_campaign,
-        itm_content: req.query.itm_content,
-        itm_referrer: req.query.itm_referrer,
-      })
-    }
-  }
-}
-
-async function requireConfirmedPrimaryEmailAddress(req, res, next) {
-  const userData = await UserGetter.promises.getUser(req.user._id, {
-    email: 1,
-    emails: 1,
-  })
-  const userPrimaryEmail = userData.emails.find(
-    emailEntry => emailEntry.email === userData.email
-  )
-  if (userPrimaryEmail?.confirmedAt != null) return next()
-
-  res.status(422).render('subscriptions/unconfirmed-primary-email', {
-    title: 'confirm_email',
-    email: userData.email,
-  })
-}
-
 function formatGroupPlansDataForDash() {
   return {
     plans: [...groupPlanModalOptions.plan_codes],
@@ -493,78 +410,6 @@ async function interstitialPaymentPage(req, res) {
   }
 }
 
-async function createSubscription(req, res) {
-  const user = SessionManager.getSessionUser(req.session)
-  const recurlyTokenIds = {
-    billing: req.body.recurly_token_id,
-    threeDSecureActionResult:
-      req.body.recurly_three_d_secure_action_result_token_id,
-  }
-  const { subscriptionDetails } = req.body
-
-  const hasSubscription =
-    await LimitationsManager.promises.userHasV1OrV2Subscription(user)
-
-  if (hasSubscription) {
-    logger.warn({ userId: user._id }, 'user already has subscription')
-    return res.sendStatus(409) // conflict
-  }
-
-  const { countryCode } = await _getRecommendedCurrency(req, res)
-
-  // Block web sales to restricted countries
-  if (Settings.restrictedCountries.includes(countryCode)) {
-    return HttpErrorHandler.unprocessableEntity(
-      req,
-      res,
-      req.i18n.translate('sorry_detected_sales_restricted_region', {
-        link: '/contact',
-      })
-    )
-  }
-
-  const result = {}
-  await Modules.promises.hooks.fire(
-    'createSubscription',
-    req,
-    res,
-    user,
-    result
-  )
-  if (result.error) {
-    return HttpErrorHandler.unprocessableEntity(req, res)
-  }
-
-  try {
-    await SubscriptionHandler.promises.createSubscription(
-      user,
-      subscriptionDetails,
-      recurlyTokenIds
-    )
-
-    res.sendStatus(201)
-  } catch (err) {
-    if (
-      err instanceof SubscriptionErrors.RecurlyTransactionError ||
-      err instanceof Errors.InvalidError
-    ) {
-      logger.error({ err }, 'recurly transaction error, potential 422')
-      HttpErrorHandler.unprocessableEntity(
-        req,
-        res,
-        err.message,
-        OError.getFullInfo(err).public
-      )
-    } else {
-      logger.warn(
-        { err, userId: user._id },
-        'something went wrong creating subscription'
-      )
-      throw err
-    }
-  }
-}
-
 /**
  * @param {import('express').Request} req
  * @param {import('express').Response} res
@@ -922,10 +767,8 @@ async function _getRecommendedCurrency(req, res) {
 
 module.exports = {
   plansPage: expressify(plansPage),
-  paymentPage: expressify(paymentPage),
   userSubscriptionPage: expressify(userSubscriptionPage),
   interstitialPaymentPage: expressify(interstitialPaymentPage),
-  createSubscription: expressify(createSubscription),
   successfulSubscription: expressify(successfulSubscription),
   cancelSubscription,
   canceledSubscription,
@@ -941,7 +784,7 @@ module.exports = {
   recurlyNotificationParser,
   refreshUserFeatures: expressify(refreshUserFeatures),
   redirectToHostedPage: expressify(redirectToHostedPage),
-  requireConfirmedPrimaryEmailAddress: expressify(
-    requireConfirmedPrimaryEmailAddress
-  ),
+  promises: {
+    getRecommendedCurrency: _getRecommendedCurrency,
+  },
 }

+ 0 - 14
services/web/app/src/Features/Subscription/SubscriptionRouter.js

@@ -27,13 +27,6 @@ module.exports = {
       SubscriptionController.userSubscriptionPage
     )
 
-    webRouter.get(
-      '/user/subscription/new',
-      AuthenticationController.requireLogin(),
-      SubscriptionController.requireConfirmedPrimaryEmailAddress,
-      SubscriptionController.paymentPage
-    )
-
     webRouter.get(
       '/user/subscription/choose-your-plan',
       AuthenticationController.requireLogin(),
@@ -90,13 +83,6 @@ module.exports = {
     )
 
     // user changes their account state
-    webRouter.post(
-      '/user/subscription/create',
-      AuthenticationController.requireLogin(),
-      PermissionsController.requirePermission('start-subscription'),
-      SubscriptionController.requireConfirmedPrimaryEmailAddress,
-      SubscriptionController.createSubscription
-    )
     webRouter.post(
       '/user/subscription/update',
       AuthenticationController.requireLogin(),

+ 0 - 37
services/web/app/views/subscriptions/new-react.pug

@@ -1,37 +0,0 @@
-extends ../layout-marketing
-
-block entrypointVar
-	- entrypoint = 'pages/user/subscription/new'
-
-block vars
-	- var suppressNavbarRight = true
-	- var suppressFooter = true
-	- var suppressCookieBanner = true
-
-block append meta
-	meta(name="ol-countryCode" content=countryCode)
-	meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey)
-	meta(name="ol-recommendedCurrency" content=String(currency).slice(0, 3))
-	meta(name="ol-plan" data-type="json" content=plan)
-	meta(name="ol-planCode" data-type="string" content=planCode)
-	meta(name="ol-showCouponField" data-type="boolean" content=showCouponField)
-	meta(name="ol-couponCode" content=couponCode)
-	meta(name="ol-itm_campaign" content=itm_campaign)
-	meta(name="ol-itm_content" content=itm_content)
-	meta(name="ol-itm_referrer" content=itm_referrer)
-
-block head-scripts
-	script(type="text/javascript", nonce=scriptNonce, src="https://js.recurly.com/v4/recurly.js")
-
-block content
-	main.content.content-alt#subscription-new-root
-
-	script(type="text/javascript", nonce=scriptNonce).
-		ga('send', 'event', 'pageview', 'payment_form', "#{planCode}")
-
-	script(
-		type="text/ng-template"
-		id="cvv-tooltip-tpl.html"
-	)
-		p !{translate("for_visa_mastercard_and_discover", {}, ['strong', 'strong', 'strong'])}
-		p !{translate("for_american_express", {}, ['strong', 'strong', 'strong'])}

+ 0 - 11
services/web/app/views/subscriptions/restricted-country.pug

@@ -1,11 +0,0 @@
-extends ../layout-marketing
-
-block content
-	main.content.content-alt#main-content
-		.container
-			.error-container
-				.error-details
-					p.error-status #{translate("restricted")}
-					p.error-description !{translate("sorry_detected_sales_restricted_region", {link: "/contact"})}
-					p.error-actions
-						a.error-btn(href="/") #{translate("home")}

+ 0 - 11
services/web/app/views/subscriptions/unconfirmed-primary-email.pug

@@ -1,11 +0,0 @@
-extends ../layout-marketing
-
-block content
-	main.content.content-alt#main-content
-		.container
-			.error-container
-				.error-details
-					p.error-status #{translate("confirm_email")}
-					p.error-description !{translate("please_confirm_email", {emailAddress: email})}
-					p.error-actions
-						a.error-btn(href="/user/settings") #{translate("account_settings")}

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

@@ -1,64 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { FormGroup, ControlLabel } from 'react-bootstrap'
-import Tooltip from '../../../../../shared/components/tooltip'
-import Icon from '../../../../../shared/components/icon'
-import useValidateField from '../../../hooks/use-validate-field'
-import classnames from 'classnames'
-import { callFnsInSequence } from '../../../../../utils/functions'
-
-type AddressFirstLineProps = {
-  errorFields: Record<string, boolean> | undefined
-  value: string
-  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
-}
-
-function AddressFirstLine({
-  errorFields,
-  value,
-  onChange,
-}: AddressFirstLineProps) {
-  const { t } = useTranslation()
-  const { validate, isValid } = useValidateField()
-
-  return (
-    <FormGroup
-      controlId="address-line-1"
-      className={classnames({
-        'has-error': !isValid || errorFields?.address1,
-      })}
-    >
-      <ControlLabel>
-        {t('address_line_1')}{' '}
-        <Tooltip
-          id="tooltip-address"
-          description={t('this_address_will_be_shown_on_the_invoice')}
-          overlayProps={{ placement: 'right' }}
-        >
-          <Icon
-            type="question-circle"
-            aria-label={t('this_address_will_be_shown_on_the_invoice')}
-          />
-        </Tooltip>
-      </ControlLabel>
-      <input
-        id="address-line-1"
-        className="form-control"
-        name="address1"
-        data-recurly="address1"
-        type="text"
-        required
-        maxLength={255}
-        onBlur={validate}
-        onChange={callFnsInSequence(validate, onChange)}
-        value={value}
-      />
-      {!isValid && (
-        <span className="input-feedback-message">
-          {t('this_field_is_required')}
-        </span>
-      )}
-    </FormGroup>
-  )
-}
-
-export default AddressFirstLine

+ 0 - 53
services/web/frontend/js/features/subscription/components/new/checkout/address-second-line.tsx

@@ -1,53 +0,0 @@
-import { useState } from 'react'
-import { useTranslation } from 'react-i18next'
-import { FormGroup, ControlLabel, Button } from 'react-bootstrap'
-import classnames from 'classnames'
-
-type AddressSecondLineProps = {
-  errorFields: Record<string, boolean> | undefined
-  value: string
-  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
-}
-
-function AddressSecondLine({
-  errorFields,
-  value,
-  onChange,
-}: AddressSecondLineProps) {
-  const { t } = useTranslation()
-  const [showAddressSecondLine, setShowAddressSecondLine] = useState(false)
-
-  if (showAddressSecondLine) {
-    return (
-      <FormGroup
-        controlId="address-line-2"
-        className={classnames({ 'has-error': errorFields?.address2 })}
-      >
-        <ControlLabel>{t('address_second_line_optional')}</ControlLabel>
-        <input
-          id="address-line-2"
-          className="form-control"
-          name="address2"
-          data-recurly="address2"
-          type="text"
-          required
-          maxLength={255}
-          onChange={onChange}
-          value={value}
-        />
-      </FormGroup>
-    )
-  }
-
-  return (
-    <Button
-      bsStyle="link"
-      onClick={() => setShowAddressSecondLine(true)}
-      className="mb-2 p-0"
-    >
-      + {t('add_another_address_line')}
-    </Button>
-  )
-}
-
-export default AddressSecondLine

+ 0 - 78
services/web/frontend/js/features/subscription/components/new/checkout/card-element.tsx

@@ -1,78 +0,0 @@
-import { useState, useEffect, useRef } from 'react'
-import { useTranslation } from 'react-i18next'
-import { FormGroup, ControlLabel } from 'react-bootstrap'
-import { CardElementChangeState } from '../../../../../../../types/recurly/elements'
-import { ElementsInstance } from 'recurly__recurly-js'
-import classnames from 'classnames'
-import getMeta from '../../../../../utils/meta'
-
-type CardElementProps = {
-  className?: string
-  elements: ElementsInstance
-  onChange: (state: CardElementChangeState) => void
-}
-
-function CardElement({ className, elements, onChange }: CardElementProps) {
-  const { t } = useTranslation()
-  const [showCardElementInvalid, setShowCardElementInvalid] =
-    useState<boolean>()
-  const cardRef = useRef<HTMLDivElement | null>(null)
-
-  // Card initialization
-  useEffect(() => {
-    if (!cardRef.current) return
-
-    const showNewDesign =
-      getMeta('ol-splitTestVariants')?.['design-system-updates'] === 'enabled'
-
-    const style = showNewDesign
-      ? {
-          fontColor: '#1b222c',
-          placeholder: {
-            color: '#677283',
-          },
-          invalid: {
-            fontColor: '#b83a33',
-          },
-        }
-      : {
-          fontColor: '#5d6879',
-          placeholder: {},
-          invalid: {
-            fontColor: '#a93529',
-          },
-        }
-
-    const card = elements.CardElement({
-      displayIcon: true,
-      inputType: 'mobileSelect',
-      style,
-    })
-
-    card.attach(cardRef.current)
-    card.on('change', state => {
-      setShowCardElementInvalid(!state.focus && !state.empty && !state.valid)
-      onChange(state)
-    })
-
-    return () => {
-      cardRef.current = null
-    }
-  }, [elements, onChange])
-
-  return (
-    <FormGroup
-      className={classnames(className, { 'has-error': showCardElementInvalid })}
-    >
-      <ControlLabel>{t('card_details')}</ControlLabel>
-      <div ref={cardRef} />
-      {showCardElementInvalid && (
-        <span className="input-feedback-message">
-          {t('card_details_are_not_valid')}
-        </span>
-      )}
-    </FormGroup>
-  )
-}
-
-export default CardElement

+ 0 - 421
services/web/frontend/js/features/subscription/components/new/checkout/checkout-panel.tsx

@@ -1,421 +0,0 @@
-import { useRef, useState, useEffect, useCallback } from 'react'
-import { useTranslation } from 'react-i18next'
-import { usePaymentContext } from '../../../context/payment-context'
-import { Row, Col, Alert } from 'react-bootstrap'
-import PriceSwitchHeader from './price-switch-header'
-import PaymentMethodToggle from './payment-method-toggle'
-import CardElement from './card-element'
-import FirstName from './first-name'
-import LastName from './last-name'
-import AddressFirstLine from './address-first-line'
-import AddressSecondLine from './address-second-line'
-import PostalCode from './postal-code'
-import CountrySelect from './country-select'
-import CompanyDetails from './company-details'
-import CouponCode from './coupon-code'
-import TosAgreementNotice from './tos-agreement-notice'
-import SubmitButton from './submit-button'
-import ThreeDSecure from './three-d-secure'
-import getMeta from '../../../../../utils/meta'
-import { getSplitTestVariant } from '../../../../../../../frontend/js/utils/splitTestUtils'
-import { postJSON } from '../../../../../infrastructure/fetch-json'
-import * as eventTracking from '../../../../../infrastructure/event-tracking'
-import classnames from 'classnames'
-import {
-  TokenPayload,
-  RecurlyError,
-  ElementsInstance,
-  PayPalInstance,
-} from 'recurly__recurly-js'
-import { PricingFormState } from '../../../context/types/payment-context-value'
-import { CreateError } from '../../../../../../../types/subscription/api'
-import { CardElementChangeState } from '../../../../../../../types/recurly/elements'
-import { useLocation } from '../../../../../shared/hooks/use-location'
-
-function CheckoutPanel() {
-  const { t } = useTranslation()
-  const {
-    couponError,
-    currencyCode,
-    planCode,
-    planName,
-    pricingFormState,
-    pricing,
-    recurlyLoadError,
-    setPricingFormState,
-    trialLength,
-    taxes,
-  } = usePaymentContext()
-  const showCouponField: boolean = getMeta('ol-showCouponField')
-  const ITMCampaign: string = getMeta('ol-itm_campaign', '')
-  const ITMContent: string = getMeta('ol-itm_content', '')
-  const ITMReferrer: string = getMeta('ol-itm_referrer', '')
-  const formRef = useRef<HTMLFormElement>(null)
-  const cachedRecurlyBillingToken = useRef<TokenPayload>()
-  const elements = useRef<ElementsInstance | undefined>(recurly?.Elements())
-  const [isProcessing, setIsProcessing] = useState(false)
-  const [errorFields, setErrorFields] = useState<Record<string, boolean>>()
-  const [genericError, setGenericError] = useState('')
-  const [paymentMethod, setPaymentMethod] = useState('credit_card')
-  const [cardIsValid, setCardIsValid] = useState<boolean>()
-  const [formIsValid, setFormIsValid] = useState<boolean>()
-  const [threeDSecureActionTokenId, setThreeDSecureActionTokenId] =
-    useState<string>()
-  const location = useLocation()
-
-  const isCreditCardPaymentMethod = paymentMethod === 'credit_card'
-  const isPayPalPaymentMethod = paymentMethod === 'paypal'
-  const isAddCompanyDetailsChecked = Boolean(
-    formRef.current?.querySelector<HTMLInputElement>(
-      '#add-company-details-checkbox'
-    )?.checked
-  )
-  const designSystemUpdatesVariant = getSplitTestVariant(
-    'design-system-updates',
-    'default'
-  )
-
-  const completeSubscription = useCallback(
-    async (
-      err?: RecurlyError | null,
-      recurlyBillingToken?: TokenPayload,
-      threeDResultToken?: TokenPayload
-    ) => {
-      if (recurlyBillingToken) {
-        // temporary store the billing token as it might be needed when
-        // re-sending the request after SCA authentication
-        cachedRecurlyBillingToken.current = recurlyBillingToken
-      }
-
-      setErrorFields(undefined)
-
-      if (err) {
-        eventTracking.sendMB('payment-page-form-error', err)
-        eventTracking.send('subscription-funnel', 'subscription-error')
-
-        setIsProcessing(false)
-        setGenericError(err.message)
-
-        const errFields = err.fields?.reduce<typeof errorFields>(
-          (prev, cur) => {
-            return { ...prev, [cur]: true }
-          },
-          {}
-        )
-        setErrorFields(errFields)
-      } else {
-        const billingFields = ['company', 'vat_number'] as const
-        const billingInfo = billingFields.reduce((prev, cur) => {
-          if (isPayPalPaymentMethod && isAddCompanyDetailsChecked) {
-            prev[cur] = pricingFormState[cur]
-          }
-
-          return prev
-        }, {} as Partial<Pick<PricingFormState, typeof billingFields[number]>>)
-
-        const postData = {
-          _csrf: getMeta('ol-csrfToken'),
-          recurly_token_id: cachedRecurlyBillingToken.current?.id,
-          recurly_three_d_secure_action_result_token_id: threeDResultToken?.id,
-          subscriptionDetails: {
-            currencyCode: pricing.current?.items.currency,
-            plan_code: pricing.current?.items.plan?.code,
-            coupon_code: pricing.current?.items.coupon?.code ?? '',
-            first_name: pricingFormState.first_name,
-            last_name: pricingFormState.last_name,
-            isPaypal: isPayPalPaymentMethod,
-            address: {
-              address1: pricingFormState.address1,
-              address2: pricingFormState.address2,
-              country: pricingFormState.country,
-              state: pricingFormState.state,
-              zip: pricingFormState.postal_code,
-            },
-            ITMCampaign,
-            ITMContent,
-            ITMReferrer,
-            ...(Object.keys(billingInfo).length && {
-              billing_info: billingInfo,
-            }),
-          },
-        }
-
-        eventTracking.sendMB('payment-page-form-submit', {
-          currencyCode: postData.subscriptionDetails.currencyCode,
-          plan_code: postData.subscriptionDetails.plan_code,
-          coupon_code: postData.subscriptionDetails.coupon_code,
-          isPaypal: postData.subscriptionDetails.isPaypal,
-          'split-test-design-system-updates': designSystemUpdatesVariant,
-        })
-        eventTracking.send(
-          'subscription-funnel',
-          'subscription-form-submitted',
-          postData.subscriptionDetails.plan_code
-        )
-
-        try {
-          await postJSON(`/user/subscription/create`, { body: postData })
-
-          eventTracking.sendMB('payment-page-form-success')
-          eventTracking.send(
-            'subscription-funnel',
-            'subscription-submission-success',
-            planCode
-          )
-          location.assign('/user/subscription/thank-you')
-        } catch (error) {
-          setIsProcessing(false)
-
-          const { data } = error as CreateError
-          const errorMessage: string =
-            data.message || t('something_went_wrong_processing_the_request')
-          setGenericError(errorMessage)
-
-          if (data.threeDSecureActionTokenId) {
-            setThreeDSecureActionTokenId(data.threeDSecureActionTokenId)
-          }
-        }
-      }
-    },
-    [
-      designSystemUpdatesVariant,
-      ITMCampaign,
-      ITMContent,
-      ITMReferrer,
-      isAddCompanyDetailsChecked,
-      isPayPalPaymentMethod,
-      location,
-      planCode,
-      pricing,
-      pricingFormState,
-      t,
-    ]
-  )
-
-  const payPal = useRef<PayPalInstance>()
-
-  useEffect(() => {
-    if (!recurly) return
-
-    payPal.current = recurly.PayPal({
-      display: { displayName: planName },
-    })
-
-    payPal.current.on('token', token => {
-      completeSubscription(null, token)
-    })
-    payPal.current.on('error', err => {
-      completeSubscription(err)
-    })
-    payPal.current.on('cancel', () => {
-      setIsProcessing(false)
-    })
-
-    const payPalCopy = payPal.current
-
-    return () => {
-      payPalCopy.destroy()
-    }
-  }, [completeSubscription, planName])
-
-  const handleCardChange = useCallback((state: CardElementChangeState) => {
-    setCardIsValid(state.valid)
-  }, [])
-
-  const hidePaypal = ['INR', 'COP', 'CLP', 'PEN'].includes(currencyCode)
-  if (hidePaypal && paymentMethod !== 'credit_card') {
-    setPaymentMethod('credit_card')
-  }
-
-  if (recurlyLoadError) {
-    return (
-      <Alert bsStyle="danger">
-        <strong>{t('payment_provider_unreachable_error')}</strong>
-      </Alert>
-    )
-  }
-
-  const handleThreeDToken = (token: TokenPayload) => {
-    // on SCA verification success: show payment UI in processing mode and
-    // resubmit the payment with the new token final success or error will be
-    // handled by `completeSubscription`
-    completeSubscription(null, undefined, token)
-    setGenericError('')
-    setThreeDSecureActionTokenId(undefined)
-    setIsProcessing(true)
-  }
-
-  const handleThreeDError = (error: RecurlyError) => {
-    // on SCA verification error: show payment UI with the error message
-    setGenericError(`Error: ${error.message}`)
-    setThreeDSecureActionTokenId(undefined)
-  }
-
-  const handlePaymentMethod = (e: React.ChangeEvent<HTMLInputElement>) => {
-    setPaymentMethod(e.target.value)
-  }
-
-  const handleChange = (
-    e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
-    name: keyof PricingFormState
-  ) => {
-    setPricingFormState(s => ({ ...s, [name]: e.target.value }))
-  }
-
-  const handleSubmit = (e: React.FormEvent) => {
-    e.preventDefault()
-
-    if (!recurly || !elements.current) return
-
-    setIsProcessing(true)
-
-    if (isPayPalPaymentMethod) {
-      payPal.current?.start()
-      return
-    }
-
-    const {
-      company: _1,
-      vat_number: _2,
-      ...tokenDataWithoutCompanyDetails
-    } = pricingFormState
-
-    const tokenData = isAddCompanyDetailsChecked
-      ? { ...pricingFormState }
-      : tokenDataWithoutCompanyDetails
-
-    recurly.token(elements.current, tokenData, completeSubscription)
-  }
-
-  const handleFormValidation = () => {
-    setFormIsValid(Boolean(formRef.current?.checkValidity()))
-  }
-
-  const isFormValid = (): boolean => {
-    if (isPayPalPaymentMethod) {
-      return pricingFormState.country !== ''
-    } else {
-      return Boolean(formIsValid && cardIsValid)
-    }
-  }
-
-  return (
-    <>
-      {threeDSecureActionTokenId && (
-        <ThreeDSecure
-          actionTokenId={threeDSecureActionTokenId}
-          onToken={handleThreeDToken}
-          onError={handleThreeDError}
-        />
-      )}
-      <div className={classnames({ hidden: threeDSecureActionTokenId })}>
-        <PriceSwitchHeader
-          planCode={planCode}
-          planCodes={[
-            'student-annual',
-            'student-monthly',
-            'student_free_trial_7_days',
-          ]}
-        />
-        <form
-          noValidate
-          onSubmit={handleSubmit}
-          onChange={handleFormValidation}
-          ref={formRef}
-          data-testid="checkout-form"
-        >
-          {genericError && (
-            <Alert bsStyle="warning" className="small">
-              <strong>{genericError}</strong>
-            </Alert>
-          )}
-          {couponError && (
-            <Alert bsStyle="warning" className="small">
-              <strong>{couponError}</strong>
-            </Alert>
-          )}
-          {hidePaypal ? null : (
-            <PaymentMethodToggle
-              onChange={handlePaymentMethod}
-              paymentMethod={paymentMethod}
-            />
-          )}
-          {elements.current && (
-            <CardElement
-              className={classnames({ hidden: !isCreditCardPaymentMethod })}
-              elements={elements.current}
-              onChange={handleCardChange}
-            />
-          )}
-          {isCreditCardPaymentMethod && (
-            <Row>
-              <Col xs={6}>
-                <FirstName
-                  errorFields={errorFields}
-                  value={pricingFormState.first_name}
-                  onChange={e => handleChange(e, 'first_name')}
-                />
-              </Col>
-              <Col xs={6}>
-                <LastName
-                  errorFields={errorFields}
-                  value={pricingFormState.last_name}
-                  onChange={e => handleChange(e, 'last_name')}
-                />
-              </Col>
-            </Row>
-          )}
-          <AddressFirstLine
-            errorFields={errorFields}
-            value={pricingFormState.address1}
-            onChange={e => handleChange(e, 'address1')}
-          />
-          <AddressSecondLine
-            errorFields={errorFields}
-            value={pricingFormState.address2}
-            onChange={e => handleChange(e, 'address2')}
-          />
-          <Row>
-            <Col xs={4}>
-              <PostalCode
-                errorFields={errorFields}
-                value={pricingFormState.postal_code}
-                onChange={e => handleChange(e, 'postal_code')}
-              />
-            </Col>
-            <Col xs={8}>
-              <CountrySelect
-                errorFields={errorFields}
-                value={pricingFormState.country}
-                onChange={e => handleChange(e, 'country')}
-              />
-            </Col>
-          </Row>
-          <CompanyDetails taxesCount={taxes.length} />
-          {showCouponField && (
-            <CouponCode
-              value={pricingFormState.coupon}
-              onChange={e => handleChange(e, 'coupon')}
-            />
-          )}
-          {isPayPalPaymentMethod &&
-            t('proceeding_to_paypal_takes_you_to_the_paypal_site_to_pay')}
-          <hr className="thin" />
-          <div className="payment-submit">
-            <SubmitButton
-              isProcessing={isProcessing}
-              isFormValid={isFormValid()}
-            >
-              {isCreditCardPaymentMethod &&
-                (trialLength ? t('upgrade_cc_btn') : t('upgrade_now'))}
-              {isPayPalPaymentMethod && t('proceed_to_paypal')}
-            </SubmitButton>
-          </div>
-          <TosAgreementNotice />
-        </form>
-      </div>
-    </>
-  )
-}
-
-export default CheckoutPanel

+ 0 - 82
services/web/frontend/js/features/subscription/components/new/checkout/company-details.tsx

@@ -1,82 +0,0 @@
-import { useState } from 'react'
-import { useTranslation } from 'react-i18next'
-import { FormGroup, ControlLabel } from 'react-bootstrap'
-import { usePaymentContext } from '../../../context/payment-context'
-import { PricingFormState } from '../../../context/types/payment-context-value'
-
-type CompanyDetailsProps = {
-  taxesCount: number
-}
-
-function CompanyDetails(props: CompanyDetailsProps) {
-  const { t } = useTranslation()
-  const [addCompanyDetailsChecked, setAddCompanyDetailsChecked] =
-    useState(false)
-  const { pricingFormState, setPricingFormState, applyVatNumber } =
-    usePaymentContext()
-
-  const handleAddCompanyDetails = (e: React.ChangeEvent<HTMLInputElement>) => {
-    setAddCompanyDetailsChecked(e.target.checked)
-  }
-
-  const handleChange = (
-    e: React.ChangeEvent<HTMLInputElement>,
-    name: keyof PricingFormState
-  ) => {
-    setPricingFormState(s => ({ ...s, [name]: e.target.value }))
-  }
-
-  const handleApplyVatNumber = (e: React.FocusEvent<HTMLInputElement>) => {
-    applyVatNumber(e.target.value)
-  }
-
-  return (
-    <>
-      <FormGroup>
-        <div className="checkbox">
-          <ControlLabel>
-            <input
-              type="checkbox"
-              id="add-company-details-checkbox"
-              onChange={handleAddCompanyDetails}
-            />
-            {t('add_company_details')}
-          </ControlLabel>
-        </div>
-      </FormGroup>
-      {addCompanyDetailsChecked && (
-        <>
-          <FormGroup controlId="company-name">
-            <ControlLabel>{t('company_name')}</ControlLabel>
-            <input
-              id="company-name"
-              className="form-control"
-              name="companyName"
-              data-recurly="company"
-              type="text"
-              onChange={e => handleChange(e, 'company')}
-              value={pricingFormState.company}
-            />
-          </FormGroup>
-          {props.taxesCount > 0 && (
-            <FormGroup controlId="vat-number">
-              <ControlLabel>{t('vat_number')}</ControlLabel>
-              <input
-                id="vat-number"
-                className="form-control"
-                name="vatNumber"
-                data-recurly="vat_number"
-                type="text"
-                onChange={e => handleChange(e, 'vat_number')}
-                onBlur={handleApplyVatNumber}
-                value={pricingFormState.vat_number}
-              />
-            </FormGroup>
-          )}
-        </>
-      )}
-    </>
-  )
-}
-
-export default CompanyDetails

+ 0 - 68
services/web/frontend/js/features/subscription/components/new/checkout/country-select.tsx

@@ -1,68 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { FormGroup, ControlLabel } from 'react-bootstrap'
-import classnames from 'classnames'
-import useValidateField from '../../../hooks/use-validate-field'
-import countries from '../../../data/countries'
-import { callFnsInSequence } from '../../../../../utils/functions'
-import { PricingFormState } from '../../../context/types/payment-context-value'
-import { usePaymentContext } from '../../../context/payment-context'
-
-type CountrySelectProps = {
-  errorFields: Record<string, boolean> | undefined
-  value: PricingFormState['country']
-  onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void
-}
-
-function CountrySelect(props: CountrySelectProps) {
-  const { t } = useTranslation()
-  const { validate, isValid } = useValidateField()
-  const { updateCountry } = usePaymentContext()
-
-  const handleUpdateCountry = (e: React.ChangeEvent<HTMLSelectElement>) => {
-    updateCountry(e.target.value as PricingFormState['country'])
-  }
-
-  return (
-    <FormGroup
-      controlId="country"
-      className={classnames({
-        'has-error': !isValid || props.errorFields?.country,
-      })}
-    >
-      <ControlLabel>{t('country')}</ControlLabel>
-      <select
-        id="country"
-        className="form-control"
-        name="country"
-        data-recurly="country"
-        required
-        onBlur={validate}
-        onChange={callFnsInSequence(
-          validate,
-          props.onChange,
-          handleUpdateCountry
-        )}
-        value={props.value}
-      >
-        <option disabled value="">
-          {t('country')}
-        </option>
-        <option disabled value="-">
-          --------------
-        </option>
-        {countries.map(country => (
-          <option value={country.code} key={country.name}>
-            {country.name}
-          </option>
-        ))}
-      </select>
-      {!isValid && (
-        <span className="input-feedback-message">
-          {t('this_field_is_required')}
-        </span>
-      )}
-    </FormGroup>
-  )
-}
-
-export default CountrySelect

+ 0 - 34
services/web/frontend/js/features/subscription/components/new/checkout/coupon-code.tsx

@@ -1,34 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { FormGroup, ControlLabel } from 'react-bootstrap'
-import { usePaymentContext } from '../../../context/payment-context'
-
-type CouponCodeProps = {
-  value: string
-  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
-}
-
-function CouponCode(props: CouponCodeProps) {
-  const { t } = useTranslation()
-  const { addCoupon } = usePaymentContext()
-
-  const handleApplyCoupon = (e: React.FocusEvent<HTMLInputElement>) => {
-    addCoupon(e.target.value)
-  }
-
-  return (
-    <FormGroup controlId="coupon-code">
-      <ControlLabel>{t('coupon_code')}</ControlLabel>
-      <input
-        id="coupon-code"
-        className="form-control"
-        data-recurly="coupon"
-        type="text"
-        onBlur={handleApplyCoupon}
-        onChange={props.onChange}
-        value={props.value}
-      />
-    </FormGroup>
-  )
-}
-
-export default CouponCode

+ 0 - 46
services/web/frontend/js/features/subscription/components/new/checkout/first-name.tsx

@@ -1,46 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { FormGroup, ControlLabel } from 'react-bootstrap'
-import useValidateField from '../../../hooks/use-validate-field'
-import classnames from 'classnames'
-import { callFnsInSequence } from '../../../../../utils/functions'
-
-type FirstNameProps = {
-  errorFields: Record<string, boolean> | undefined
-  value: string
-  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
-}
-
-function FirstName(props: FirstNameProps) {
-  const { t } = useTranslation()
-  const { validate, isValid } = useValidateField()
-
-  return (
-    <FormGroup
-      controlId="first-name"
-      className={classnames({
-        'has-error': !isValid || props.errorFields?.first_name,
-      })}
-    >
-      <ControlLabel>{t('first_name')}</ControlLabel>
-      <input
-        id="first-name"
-        className="form-control"
-        name="firstName"
-        data-recurly="first_name"
-        type="text"
-        required
-        maxLength={255}
-        onBlur={validate}
-        onChange={callFnsInSequence(validate, props.onChange)}
-        value={props.value}
-      />
-      {!isValid && (
-        <span className="input-feedback-message">
-          {t('this_field_is_required')}
-        </span>
-      )}
-    </FormGroup>
-  )
-}
-
-export default FirstName

+ 0 - 46
services/web/frontend/js/features/subscription/components/new/checkout/last-name.tsx

@@ -1,46 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { FormGroup, ControlLabel } from 'react-bootstrap'
-import useValidateField from '../../../hooks/use-validate-field'
-import classnames from 'classnames'
-import { callFnsInSequence } from '../../../../../utils/functions'
-
-type LastNameProps = {
-  errorFields: Record<string, boolean> | undefined
-  value: string
-  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
-}
-
-function LastName(props: LastNameProps) {
-  const { t } = useTranslation()
-  const { validate, isValid } = useValidateField()
-
-  return (
-    <FormGroup
-      controlId="last-name"
-      className={classnames({
-        'has-error': !isValid || props.errorFields?.last_name,
-      })}
-    >
-      <ControlLabel>{t('last_name')}</ControlLabel>
-      <input
-        id="last-name"
-        className="form-control"
-        name="lastName"
-        data-recurly="last_name"
-        type="text"
-        required
-        maxLength={255}
-        onBlur={validate}
-        onChange={callFnsInSequence(validate, props.onChange)}
-        value={props.value}
-      />
-      {!isValid && (
-        <span className="input-feedback-message">
-          {t('this_field_is_required')}
-        </span>
-      )}
-    </FormGroup>
-  )
-}
-
-export default LastName

+ 0 - 60
services/web/frontend/js/features/subscription/components/new/checkout/payment-method-toggle.tsx

@@ -1,60 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { FormGroup, ControlLabel, Col } from 'react-bootstrap'
-import Icon from '../../../../../shared/components/icon'
-
-type PaymentMethodToggleProps = {
-  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
-  paymentMethod: string
-}
-
-function PaymentMethodToggle(props: PaymentMethodToggleProps) {
-  const { t } = useTranslation()
-
-  return (
-    <FormGroup
-      className="payment-method-toggle"
-      data-testid="payment-method-toggle"
-    >
-      <hr className="thin" />
-      <div className="radio">
-        <Col xs={8}>
-          <ControlLabel>
-            <input
-              type="radio"
-              name="payment_method"
-              value="credit_card"
-              onChange={props.onChange}
-              checked={props.paymentMethod === 'credit_card'}
-            />
-            <strong>
-              {t('card_payment')}&nbsp;
-              <span className="hidden-xs">
-                <Icon type="cc-visa" /> <Icon type="cc-mastercard" />{' '}
-                <Icon type="cc-amex" />
-              </span>
-            </strong>
-          </ControlLabel>
-        </Col>
-        <Col xs={4}>
-          <ControlLabel>
-            <input
-              type="radio"
-              name="payment_method"
-              value="paypal"
-              onChange={props.onChange}
-              checked={props.paymentMethod === 'paypal'}
-            />
-            <strong>
-              PayPal&nbsp;
-              <span className="hidden-xs">
-                <Icon type="cc-paypal" />
-              </span>
-            </strong>
-          </ControlLabel>
-        </Col>
-      </div>
-    </FormGroup>
-  )
-}
-
-export default PaymentMethodToggle

+ 0 - 46
services/web/frontend/js/features/subscription/components/new/checkout/postal-code.tsx

@@ -1,46 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { FormGroup, ControlLabel } from 'react-bootstrap'
-import useValidateField from '../../../hooks/use-validate-field'
-import classnames from 'classnames'
-import { callFnsInSequence } from '../../../../../utils/functions'
-
-type PostalCodeProps = {
-  errorFields: Record<string, boolean> | undefined
-  value: string
-  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
-}
-
-function PostalCode(props: PostalCodeProps) {
-  const { t } = useTranslation()
-  const { validate, isValid } = useValidateField()
-
-  return (
-    <FormGroup
-      controlId="postal-code"
-      className={classnames({
-        'has-error': !isValid || props.errorFields?.postal_code,
-      })}
-    >
-      <ControlLabel>{t('postal_code')}</ControlLabel>
-      <input
-        id="postal-code"
-        className="form-control"
-        name="postalCode"
-        data-recurly="postal_code"
-        type="text"
-        required
-        maxLength={255}
-        onBlur={validate}
-        onChange={callFnsInSequence(validate, props.onChange)}
-        value={props.value}
-      />
-      {!isValid && (
-        <span className="input-feedback-message">
-          {t('this_field_is_required')}
-        </span>
-      )}
-    </FormGroup>
-  )
-}
-
-export default PostalCode

+ 0 - 32
services/web/frontend/js/features/subscription/components/new/checkout/price-switch-header.tsx

@@ -1,32 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { Row, Col } from 'react-bootstrap'
-import { Plan } from '../../../../../../../types/subscription/plan'
-
-type PriceSwitchHeaderProps = {
-  planCode: Plan['planCode']
-  planCodes: Array<Plan['planCode']>
-}
-
-function PriceSwitchHeader({ planCode, planCodes }: PriceSwitchHeaderProps) {
-  const { t } = useTranslation()
-  const showStudentDisclaimer = planCodes.includes(planCode)
-
-  return (
-    <div className="price-switch-header">
-      <Row>
-        <Col xs={9}>
-          <h2>{t('select_a_payment_method')}</h2>
-        </Col>
-      </Row>
-      {showStudentDisclaimer && (
-        <Row>
-          <Col xs={12}>
-            <p className="student-disclaimer">{t('student_disclaimer')}</p>
-          </Col>
-        </Row>
-      )}
-    </div>
-  )
-}
-
-export default PriceSwitchHeader

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

@@ -1,32 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { Button } from 'react-bootstrap'
-import Icon from '../../../../../shared/components/icon'
-
-type SubmitButtonProps = {
-  isProcessing: boolean
-  isFormValid: boolean
-  children: React.ReactNode
-}
-
-function SubmitButton(props: SubmitButtonProps) {
-  const { t } = useTranslation()
-
-  return (
-    <Button
-      type="submit"
-      bsStyle="primary"
-      className="btn-block"
-      disabled={props.isProcessing || !props.isFormValid}
-    >
-      {props.isProcessing && (
-        <>
-          <Icon type="spinner" spin />
-          <span className="sr-only">{t('processing')}</span>
-        </>
-      )}{' '}
-      {props.children}
-    </Button>
-  )
-}
-
-export default SubmitButton

+ 0 - 58
services/web/frontend/js/features/subscription/components/new/checkout/three-d-secure.tsx

@@ -1,58 +0,0 @@
-import { useEffect, useRef } from 'react'
-import { useTranslation } from 'react-i18next'
-import { Alert } from 'react-bootstrap'
-import { RecurlyError, TokenPayload } from 'recurly__recurly-js'
-
-type ThreeDSecureProps = {
-  actionTokenId: string
-  onToken: (token: TokenPayload) => void
-  onError: (error: RecurlyError) => void
-}
-
-function ThreeDSecure({ actionTokenId, onToken, onError }: ThreeDSecureProps) {
-  const { t } = useTranslation()
-  const container = useRef<HTMLDivElement>(null)
-  const recurlyContainer = useRef<HTMLDivElement | null>(null)
-
-  useEffect(() => {
-    // scroll the UI into view (timeout needed to make sure the element is
-    // visible)
-    const timeout = setTimeout(() => {
-      container.current?.scrollIntoView()
-    }, 0)
-
-    return () => {
-      clearTimeout(timeout)
-    }
-  }, [])
-
-  useEffect(() => {
-    if (!recurly || !recurlyContainer.current) return
-
-    // instanciate and configure Recurly 3DSecure flow
-    const risk = recurly.Risk()
-    const threeDSecure = risk.ThreeDSecure({ actionTokenId })
-
-    threeDSecure.on('token', onToken)
-    threeDSecure.on('error', onError)
-    threeDSecure.attach(recurlyContainer.current)
-
-    return () => {
-      recurlyContainer.current = null
-    }
-  }, [actionTokenId, onToken, onError])
-
-  return (
-    <div className="three-d-secure-container--react" ref={container}>
-      <Alert bsStyle="info" className="small" aria-live="assertive">
-        <strong>{t('card_must_be_authenticated_by_3dsecure')}</strong>
-      </Alert>
-      <div
-        className="three-d-secure-recurly-container"
-        ref={recurlyContainer}
-      />
-    </div>
-  )
-}
-
-export default ThreeDSecure

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

@@ -1,17 +0,0 @@
-import { Trans } from 'react-i18next'
-
-function TosAgreementNotice() {
-  return (
-    <p className="tos-agreement-notice">
-      <Trans
-        i18nKey="by_subscribing_you_agree_to_our_terms_of_service"
-        components={[
-          // eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
-          <a href="/legal#Terms" target="_blank" rel="noopener noreferrer" />,
-        ]}
-      />
-    </p>
-  )
-}
-
-export default TosAgreementNotice

+ 0 - 38
services/web/frontend/js/features/subscription/components/new/payment-preview/collaborators.tsx

@@ -1,38 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { Plan } from '../../../../../../../types/subscription/plan'
-
-function CollaboratorsWrapper({ children }: { children: React.ReactNode }) {
-  return <div className="text-small number-of-collaborators">{children}</div>
-}
-
-type CollaboratorsProps = {
-  count: NonNullable<Plan['features']>['collaborators']
-}
-
-function Collaborators({ count }: CollaboratorsProps) {
-  const { t } = useTranslation()
-
-  if (count === 1) {
-    return (
-      <CollaboratorsWrapper>
-        {t('collabs_per_proj_single', { collabcount: 1 })}
-      </CollaboratorsWrapper>
-    )
-  }
-
-  if (count > 1) {
-    return (
-      <CollaboratorsWrapper>
-        {t('collabs_per_proj', { collabcount: count })}
-      </CollaboratorsWrapper>
-    )
-  }
-
-  if (count === -1) {
-    return <CollaboratorsWrapper>{t('unlimited_collabs')}</CollaboratorsWrapper>
-  }
-
-  return null
-}
-
-export default Collaborators

+ 0 - 40
services/web/frontend/js/features/subscription/components/new/payment-preview/currency-dropdown.tsx

@@ -1,40 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { Dropdown, DropdownProps, MenuItem } from 'react-bootstrap'
-import Icon from '../../../../../shared/components/icon'
-import { usePaymentContext } from '../../../context/payment-context'
-
-function CurrencyDropdown(props: DropdownProps) {
-  const { t } = useTranslation()
-  const { currencyCode, limitedCurrencies, changeCurrency } =
-    usePaymentContext()
-
-  return (
-    <Dropdown {...props}>
-      <Dropdown.Toggle
-        className="change-currency-toggle"
-        bsStyle="link"
-        noCaret
-      >
-        {t('change_currency')}
-      </Dropdown.Toggle>
-      <Dropdown.Menu>
-        {Object.entries(limitedCurrencies).map(([currency, symbol]) => (
-          <MenuItem
-            eventKey={currency}
-            key={currency}
-            onSelect={eventKey => changeCurrency(eventKey)}
-          >
-            {currency === currencyCode && (
-              <span className="change-currency-dropdown-selected-icon">
-                <Icon type="check" accessibilityLabel={t('selected')} />
-              </span>
-            )}
-            {currency} ({symbol})
-          </MenuItem>
-        ))}
-      </Dropdown.Menu>
-    </Dropdown>
-  )
-}
-
-export default CurrencyDropdown

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

@@ -1,33 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { Plan } from '../../../../../../../types/subscription/plan'
-
-type FeaturesListProps = {
-  features: NonNullable<Plan['features']>
-}
-
-function FeaturesList({ features }: FeaturesListProps) {
-  const { t } = useTranslation()
-
-  return (
-    <>
-      <div className="text-small">{t('all_premium_features_including')}</div>
-      <ul className="small" data-testid="features-list">
-        {features.compileTimeout > 1 && (
-          <li>{t('increased_compile_timeout')}</li>
-        )}
-        {features.dropbox && features.github && (
-          <li>{t('sync_dropbox_github')}</li>
-        )}
-        {features.versioning && <li>{t('full_doc_history')}</li>}
-        {features.trackChanges && <li>{t('track_changes')}</li>}
-        {features.references && <li>{t('reference_search')}</li>}
-        {(features.mendeley || features.zotero) && (
-          <li>{t('reference_sync')}</li>
-        )}
-        {features.symbolPalette && <li>{t('symbol_palette')}</li>}
-      </ul>
-    </>
-  )
-}
-
-export default FeaturesList

+ 0 - 38
services/web/frontend/js/features/subscription/components/new/payment-preview/no-discount-price.tsx

@@ -1,38 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { usePaymentContext } from '../../../context/payment-context'
-
-function NoDiscountPrice() {
-  const { t } = useTranslation()
-  const { currencySymbol, monthlyBilling, coupon } = usePaymentContext()
-
-  if (coupon?.normalPrice === undefined) {
-    return null
-  }
-
-  const price = `${currencySymbol}${coupon.normalPrice.toFixed(2)}`
-
-  return (
-    <div>
-      {!coupon.singleUse &&
-        coupon.discountMonths &&
-        coupon.discountMonths > 0 &&
-        monthlyBilling &&
-        t('then_x_price_per_month', { price })}
-      {!coupon.singleUse &&
-        !coupon.discountMonths &&
-        monthlyBilling &&
-        t('normally_x_price_per_month', { price })}
-      {!coupon.singleUse &&
-        !monthlyBilling &&
-        t('normally_x_price_per_year', { price })}
-      {coupon.singleUse &&
-        monthlyBilling &&
-        t('then_x_price_per_month', { price })}
-      {coupon.singleUse &&
-        !monthlyBilling &&
-        t('then_x_price_per_year', { price })}
-    </div>
-  )
-}
-
-export default NoDiscountPrice

+ 0 - 29
services/web/frontend/js/features/subscription/components/new/payment-preview/payment-preview-panel.tsx

@@ -1,29 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import Collaborators from './collaborators'
-import FeaturesList from './features-list'
-import PriceSummary from './price-summary'
-import TrialCouponSummary from './trial-coupon-summary'
-import { usePaymentContext } from '../../../context/payment-context'
-
-function PaymentPreviewPanel() {
-  const { t } = useTranslation()
-  const { plan, planName } = usePaymentContext()
-
-  return (
-    <div className="price-feature-description">
-      <h4>{planName}</h4>
-      {plan.features && (
-        <>
-          <Collaborators count={plan.features.collaborators} />
-          <FeaturesList features={plan.features} />
-        </>
-      )}
-      <PriceSummary />
-      <TrialCouponSummary />
-      <hr className="thin" />
-      <p className="price-cancel-anytime text-center">{t('cancel_anytime')}</p>
-    </div>
-  )
-}
-
-export default PaymentPreviewPanel

+ 0 - 49
services/web/frontend/js/features/subscription/components/new/payment-preview/price-for-first-x-period.tsx

@@ -1,49 +0,0 @@
-import { Trans } from 'react-i18next'
-import { usePaymentContext } from '../../../context/payment-context'
-
-function PriceForFirstXPeriod() {
-  const { currencySymbol, monthlyBilling, coupon, recurlyPrice } =
-    usePaymentContext()
-
-  if (!recurlyPrice || !coupon) {
-    return null
-  }
-
-  const price = `${currencySymbol}${recurlyPrice.total}`
-
-  return (
-    <div>
-      {coupon.discountMonths &&
-        coupon.discountMonths > 0 &&
-        !coupon.singleUse &&
-        monthlyBilling && (
-          <Trans
-            i18nKey="x_price_for_y_months"
-            components={[<strong />]} // eslint-disable-line react/jsx-key
-            values={{
-              discountMonths: coupon.discountMonths,
-              price,
-            }}
-          />
-        )}
-      {coupon.singleUse && monthlyBilling && (
-        <Trans
-          i18nKey="x_price_for_first_month"
-          components={[<strong />]} // eslint-disable-line react/jsx-key
-          values={{ price }}
-        />
-      )}
-      {coupon.singleUse && !monthlyBilling && (
-        <div>
-          <Trans
-            i18nKey="x_price_for_first_year"
-            components={[<strong />]} // eslint-disable-line react/jsx-key
-            values={{ price }}
-          />
-        </div>
-      )}
-    </div>
-  )
-}
-
-export default PriceForFirstXPeriod

+ 0 - 84
services/web/frontend/js/features/subscription/components/new/payment-preview/price-summary.tsx

@@ -1,84 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { usePaymentContext } from '../../../context/payment-context'
-import CurrencyDropdown from './currency-dropdown'
-
-function PriceSummary() {
-  const { t } = useTranslation()
-  const {
-    coupon,
-    currencySymbol,
-    recurlyPrice,
-    planName,
-    taxes,
-    monthlyBilling,
-  } = usePaymentContext()
-
-  if (!recurlyPrice) {
-    return null
-  }
-
-  const rate = parseFloat(taxes?.[0]?.rate)
-  const subtotal =
-    coupon?.normalPriceWithoutTax.toFixed(2) ?? recurlyPrice.subtotal
-
-  return (
-    <>
-      <hr />
-      <div className="price-summary" data-testid="price-summary">
-        <h4>{t('payment_summary')}</h4>
-        <div className="small">
-          <div className="price-summary-line" data-testid="price-summary-plan">
-            <span>{planName}</span>
-            <span>
-              {currencySymbol}
-              {subtotal}
-            </span>
-          </div>
-          {coupon && (
-            <div
-              className="price-summary-line"
-              data-testid="price-summary-coupon"
-            >
-              <span>{coupon.name}</span>
-              <span aria-hidden>
-                &ndash;{currencySymbol}
-                {recurlyPrice.discount}
-              </span>
-              <span className="sr-only">
-                {t('discount_of', {
-                  amount: `${currencySymbol}${recurlyPrice.discount}`,
-                })}
-              </span>
-            </div>
-          )}
-          {rate > 0 && (
-            <div className="price-summary-line" data-testid="price-summary-vat">
-              <span>
-                {t('vat')} {rate * 100}%
-              </span>
-              <span>
-                {currencySymbol}
-                {recurlyPrice.tax}
-              </span>
-            </div>
-          )}
-          <div
-            className="price-summary-line price-summary-total-line"
-            data-testid="price-summary-total"
-          >
-            <b>{monthlyBilling ? t('total_per_month') : t('total_per_year')}</b>
-            <b>
-              {currencySymbol}
-              {recurlyPrice.total}
-            </b>
-          </div>
-        </div>
-        <div className="change-currency">
-          <CurrencyDropdown id="change-currency-dropdown" />
-        </div>
-      </div>
-    </>
-  )
-}
-
-export default PriceSummary

+ 0 - 24
services/web/frontend/js/features/subscription/components/new/payment-preview/trial-coupon-summary.tsx

@@ -1,24 +0,0 @@
-import TrialPrice from './trial-price'
-import NoDiscountPrice from './no-discount-price'
-import PriceForFirstXPeriod from './price-for-first-x-period'
-
-function TrialCouponSummary() {
-  const children = [TrialPrice, NoDiscountPrice, PriceForFirstXPeriod].map(
-    (Component, index) => <Component key={index} />
-  )
-
-  const showChildren = children.some(child => child.type() != null)
-
-  if (!showChildren) return null
-
-  return (
-    <>
-      <hr className="thin" />
-      <div className="trial-coupon-summary" data-testid="trial-coupon-summary">
-        {children}
-      </div>
-    </>
-  )
-}
-
-export default TrialCouponSummary

+ 0 - 37
services/web/frontend/js/features/subscription/components/new/payment-preview/trial-price.tsx

@@ -1,37 +0,0 @@
-import { Trans } from 'react-i18next'
-import { usePaymentContext } from '../../../context/payment-context'
-
-function TrialPrice() {
-  const { currencySymbol, plan, recurlyPrice, trialLength } =
-    usePaymentContext()
-
-  if (!trialLength || !recurlyPrice) {
-    return null
-  }
-
-  return (
-    <div>
-      {plan.annual ? (
-        <Trans
-          i18nKey="first_x_days_free_after_that_y_per_year"
-          components={[<strong />, <strong />]} // eslint-disable-line react/jsx-key
-          values={{
-            trialLen: trialLength,
-            price: `${currencySymbol}${recurlyPrice.total}`,
-          }}
-        />
-      ) : (
-        <Trans
-          i18nKey="first_x_days_free_after_that_y_per_month"
-          components={[<strong />, <strong />]} // eslint-disable-line react/jsx-key
-          values={{
-            trialLen: trialLength,
-            price: `${currencySymbol}${recurlyPrice.total}`,
-          }}
-        />
-      )}
-    </div>
-  )
-}
-
-export default TrialPrice

+ 0 - 37
services/web/frontend/js/features/subscription/components/new/root.tsx

@@ -1,37 +0,0 @@
-import useWaitForI18n from '../../../../shared/hooks/use-wait-for-i18n'
-import PaymentPreviewPanel from './payment-preview/payment-preview-panel'
-import CheckoutPanel from './checkout/checkout-panel'
-import { Col, Row } from 'react-bootstrap'
-import { PaymentProvider } from '../../context/payment-context'
-import getMeta from '../../../../utils/meta'
-
-function Root() {
-  const { isReady } = useWaitForI18n()
-
-  if (!isReady) {
-    return null
-  }
-
-  const publicKey = getMeta('ol-recurlyApiKey')
-
-  return (
-    <PaymentProvider publicKey={publicKey}>
-      <div className="container">
-        <Row className="card-group">
-          <Col md={3} mdPush={1}>
-            <div className="card card-highlighted">
-              <PaymentPreviewPanel />
-            </div>
-          </Col>
-          <Col md={5} mdPush={1}>
-            <div className="card card-highlighted card-border">
-              <CheckoutPanel />
-            </div>
-          </Col>
-        </Row>
-      </div>
-    </PaymentProvider>
-  )
-}
-
-export default Root

+ 0 - 365
services/web/frontend/js/features/subscription/context/payment-context.tsx

@@ -1,365 +0,0 @@
-import {
-  useState,
-  useEffect,
-  useLayoutEffect,
-  useMemo,
-  useCallback,
-  useRef,
-  useContext,
-  createContext,
-} from 'react'
-import { currencies, CurrencyCode, CurrencySymbol } from '../data/currency'
-import { useTranslation } from 'react-i18next'
-import getMeta from '../../../utils/meta'
-import { getSplitTestVariant } from '../../../../../frontend/js/utils/splitTestUtils'
-import * as eventTracking from '../../../infrastructure/event-tracking'
-import {
-  PaymentContextValue,
-  PricingFormState,
-} from './types/payment-context-value'
-import { Plan } from '../../../../../types/subscription/plan'
-import {
-  RecurlyOptions,
-  SubscriptionPricingStateTax,
-} from 'recurly__recurly-js'
-import { SubscriptionPricingInstanceCustom } from '../../../../../types/recurly/pricing/subscription'
-
-function usePayment({ publicKey }: RecurlyOptions) {
-  const { t } = useTranslation()
-  const plan: Plan = getMeta('ol-plan')
-  const initialCountry: PricingFormState['country'] = getMeta(
-    'ol-countryCode',
-    ''
-  )
-  const initialCouponCode: PricingFormState['coupon'] = getMeta(
-    'ol-couponCode',
-    ''
-  )
-  const initiallySelectedCurrencyCode: CurrencyCode = getMeta(
-    'ol-recommendedCurrency'
-  )
-  const planCode: string = getMeta('ol-planCode')
-  const designSystemUpdatesVariant = getSplitTestVariant(
-    'design-system-updates',
-    'default'
-  )
-
-  const [planName, setPlanName] = useState(plan.name)
-  const [recurlyLoading, setRecurlyLoading] = useState(true)
-  const [recurlyLoadError, setRecurlyLoadError] = useState(false)
-  const [recurlyPrice, setRecurlyPrice] = useState<{
-    subtotal: string
-    plan: string
-    addons: string
-    setup_fee: string
-    discount: string
-    tax: string
-    total: string
-  }>()
-  const [monthlyBilling, setMonthlyBilling] = useState<boolean>()
-  const [taxes, setTaxes] = useState<SubscriptionPricingStateTax[]>([])
-  const [coupon, setCoupon] = useState<{
-    discountMonths?: number
-    discountRate?: number
-    singleUse: boolean
-    normalPrice: number
-    name: string
-    normalPriceWithoutTax: number
-  }>()
-  const [couponError, setCouponError] = useState('')
-  const [trialLength, setTrialLength] = useState<number>()
-  const [currencyCode, setCurrencyCode] = useState(
-    initiallySelectedCurrencyCode
-  )
-  const [pricingFormState, setPricingFormState] = useState<PricingFormState>({
-    first_name: '',
-    last_name: '',
-    postal_code: '',
-    address1: '',
-    address2: '',
-    state: '',
-    city: '',
-    company: '',
-    vat_number: '',
-    country: initialCountry,
-    coupon: initialCouponCode,
-  })
-  const pricing = useRef<SubscriptionPricingInstanceCustom>()
-
-  const limitedCurrencyCodes = Array.from(
-    new Set<CurrencyCode>([initiallySelectedCurrencyCode, 'USD', 'EUR', 'GBP'])
-  )
-  const limitedCurrencies = limitedCurrencyCodes.reduce((prev, cur) => {
-    return { ...prev, [cur]: currencies[cur] }
-  }, {} as Partial<typeof currencies>)
-  const currencySymbol = limitedCurrencies[currencyCode] as CurrencySymbol
-
-  useLayoutEffect(() => {
-    if (typeof recurly === 'undefined' || !recurly) {
-      setRecurlyLoadError(true)
-      return
-    }
-
-    eventTracking.sendMB('payment-page-view', {
-      plan: planCode,
-      currency: currencyCode,
-      'split-test-design-system-updates': designSystemUpdatesVariant,
-    })
-    eventTracking.send(
-      'subscription-funnel',
-      'subscription-form-viewed',
-      planCode
-    )
-
-    recurly.configure({ publicKey })
-    pricing.current =
-      recurly.Pricing.Subscription() as SubscriptionPricingInstanceCustom
-
-    const setupPricing = () => {
-      setRecurlyLoading(true)
-
-      pricing.current
-        ?.plan(planCode, { quantity: 1 })
-        .address({
-          first_name: '',
-          last_name: '',
-          country: initialCountry,
-        })
-        .tax({ tax_code: 'digital', vat_number: '' })
-        .currency(currencyCode)
-        .coupon(initialCouponCode)
-        .catch(function (err) {
-          if (currencyCode !== 'USD' && err.name === 'invalid-currency') {
-            setCurrencyCode('USD')
-            setupPricing()
-          } else if (err.name === 'api-error' && err.code === 'not-found') {
-            // not-found here should refer to the coupon code, plan_code should be valid
-            setCouponError(t('coupon_code_is_not_valid_for_selected_plan'))
-          } else {
-            // Bail out on other errors, form state will not be correct
-            setRecurlyLoadError(true)
-            throw err
-          }
-        })
-        .done(() => {
-          setRecurlyLoading(false)
-        })
-    }
-
-    setupPricing()
-  }, [
-    designSystemUpdatesVariant,
-    initialCountry,
-    initialCouponCode,
-    initiallySelectedCurrencyCode,
-    planCode,
-    publicKey,
-    currencyCode,
-    t,
-  ])
-
-  useEffect(() => {
-    pricing.current?.on('change', function () {
-      if (!pricing.current) return
-
-      const planName = pricing.current.items.plan?.name
-      if (planName) {
-        setPlanName(planName)
-      }
-
-      const trialLength = pricing.current.items.plan?.trial?.length
-      setTrialLength(trialLength)
-
-      const recurlyPrice = trialLength
-        ? pricing.current.price.next
-        : pricing.current.price.now
-      setRecurlyPrice(recurlyPrice)
-
-      const monthlyBilling = pricing.current.items.plan?.period.length === 1
-      setMonthlyBilling(monthlyBilling)
-
-      setTaxes(pricing.current.price.taxes)
-
-      const couponData = (() => {
-        if (pricing.current.items.coupon?.discount.type === 'percent') {
-          const coupon = pricing.current.items.coupon
-          const basePrice = parseInt(pricing.current.price.base.plan.unit, 10)
-          const discountData =
-            coupon.applies_for_months > 0 && coupon.discount.rate
-              ? {
-                  discountMonths: coupon.applies_for_months,
-                  discountRate: coupon.discount.rate * 100,
-                }
-              : {}
-
-          const couponData = {
-            singleUse: coupon.single_use,
-            normalPrice: basePrice,
-            name: coupon.name,
-            normalPriceWithoutTax: basePrice,
-            ...discountData,
-          }
-
-          if (pricing.current.price.taxes[0]?.rate) {
-            couponData.normalPrice +=
-              basePrice * parseFloat(pricing.current.price.taxes[0].rate)
-          }
-
-          return couponData
-        }
-      })()
-      setCoupon(couponData)
-    })
-  }, [currencyCode])
-
-  const addCoupon = useCallback(
-    (coupon: PricingFormState['coupon']) => {
-      setRecurlyLoading(true)
-      setCouponError('')
-
-      pricing.current
-        ?.coupon(coupon)
-        .catch(function (err) {
-          if (err.name === 'api-error' && err.code === 'not-found') {
-            setCouponError(t('coupon_code_is_not_valid_for_selected_plan'))
-          } else {
-            setCouponError(
-              t('an_error_occurred_when_verifying_the_coupon_code')
-            )
-            throw err
-          }
-        })
-        .done(() => {
-          setRecurlyLoading(false)
-        })
-    },
-    [t]
-  )
-
-  const updateCountry = useCallback(
-    (country: PricingFormState['country']) => {
-      setRecurlyLoading(true)
-
-      pricing.current
-        ?.address({
-          country,
-          first_name: pricingFormState.first_name,
-          last_name: pricingFormState.last_name,
-        })
-        .done(() => {
-          setRecurlyLoading(false)
-        })
-    },
-    [pricingFormState.first_name, pricingFormState.last_name]
-  )
-
-  const applyVatNumber = useCallback(
-    (vatNumber: PricingFormState['vat_number']) => {
-      setRecurlyLoading(true)
-
-      pricing.current
-        ?.tax({ tax_code: 'digital', vat_number: vatNumber })
-        .done(() => {
-          setRecurlyLoading(false)
-        })
-    },
-    []
-  )
-
-  const changeCurrency = useCallback(
-    (newCurrency: CurrencyCode) => {
-      setRecurlyLoading(true)
-      setCurrencyCode(newCurrency)
-
-      pricing.current
-        ?.currency(newCurrency)
-        .catch(function (err) {
-          if (currencyCode !== 'USD' && err.name === 'invalid-currency') {
-            setCurrencyCode('USD')
-          } else {
-            throw err
-          }
-        })
-        .done(() => {
-          setRecurlyLoading(false)
-        })
-    },
-    [currencyCode]
-  )
-
-  const value = useMemo<PaymentContextValue>(
-    () => ({
-      currencyCode,
-      setCurrencyCode,
-      currencySymbol,
-      limitedCurrencies,
-      pricingFormState,
-      setPricingFormState,
-      plan,
-      planCode,
-      planName,
-      pricing,
-      recurlyLoading,
-      recurlyLoadError,
-      recurlyPrice,
-      monthlyBilling,
-      taxes,
-      coupon,
-      couponError,
-      trialLength,
-      addCoupon,
-      applyVatNumber,
-      changeCurrency,
-      updateCountry,
-    }),
-    [
-      currencyCode,
-      setCurrencyCode,
-      currencySymbol,
-      limitedCurrencies,
-      pricingFormState,
-      setPricingFormState,
-      plan,
-      planCode,
-      planName,
-      pricing,
-      recurlyLoading,
-      recurlyLoadError,
-      recurlyPrice,
-      monthlyBilling,
-      taxes,
-      coupon,
-      couponError,
-      trialLength,
-      addCoupon,
-      applyVatNumber,
-      changeCurrency,
-      updateCountry,
-    ]
-  )
-
-  return { value }
-}
-
-export const PaymentContext = createContext<PaymentContextValue | undefined>(
-  undefined
-)
-
-type PaymentProviderProps = {
-  publicKey: string
-  children?: React.ReactNode
-}
-
-export function PaymentProvider({ publicKey, ...props }: PaymentProviderProps) {
-  const { value } = usePayment({ publicKey })
-
-  return <PaymentContext.Provider value={value} {...props} />
-}
-
-export function usePaymentContext() {
-  const context = useContext(PaymentContext)
-  if (!context) {
-    throw new Error('PaymentContext is only available inside PaymentProvider')
-  }
-  return context
-}

+ 0 - 67
services/web/frontend/js/features/subscription/context/types/payment-context-value.tsx

@@ -1,67 +0,0 @@
-import countries from '../../data/countries'
-import { Plan } from '../../../../../../types/subscription/plan'
-import { SubscriptionPricingStateTax } from 'recurly__recurly-js'
-import { SubscriptionPricingInstanceCustom } from '../../../../../../types/recurly/pricing/subscription'
-import { currencies, CurrencyCode, CurrencySymbol } from '../../data/currency'
-
-export type PricingFormState = {
-  first_name: string
-  last_name: string
-  postal_code: string
-  address1: string
-  address2: string
-  state: string
-  city: string
-  company: string
-  vat_number: string
-  country: typeof countries[number]['code'] | ''
-  coupon: string
-}
-
-export type PaymentContextValue = {
-  currencyCode: CurrencyCode
-  setCurrencyCode: React.Dispatch<
-    React.SetStateAction<PaymentContextValue['currencyCode']>
-  >
-  currencySymbol: CurrencySymbol
-  limitedCurrencies: Partial<typeof currencies>
-  pricingFormState: PricingFormState
-  setPricingFormState: React.Dispatch<
-    React.SetStateAction<PaymentContextValue['pricingFormState']>
-  >
-  plan: Plan
-  planCode: string
-  planName: string
-  pricing: React.MutableRefObject<SubscriptionPricingInstanceCustom | undefined>
-  recurlyLoading: boolean
-  recurlyLoadError: boolean
-  recurlyPrice:
-    | {
-        subtotal: string
-        plan: string
-        addons: string
-        setup_fee: string
-        discount: string
-        tax: string
-        total: string
-      }
-    | undefined
-  monthlyBilling: boolean | undefined
-  taxes: SubscriptionPricingStateTax[]
-  coupon:
-    | {
-        discountMonths?: number
-        discountRate?: number
-        singleUse: boolean
-        normalPrice: number
-        name: string
-        normalPriceWithoutTax: number
-      }
-    | undefined
-  couponError: string
-  trialLength: number | undefined
-  applyVatNumber: (vatNumber: PricingFormState['vat_number']) => void
-  addCoupon: (coupon: PricingFormState['coupon']) => void
-  changeCurrency: (newCurrency: CurrencyCode) => void
-  updateCountry: (country: PricingFormState['country']) => void
-}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -1,13 +0,0 @@
-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>
-  )
-}

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

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

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

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

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

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

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

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

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

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

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

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

+ 0 - 452
services/web/test/frontend/features/subscription/components/new/checkout.spec.tsx

@@ -1,452 +0,0 @@
-import CheckoutPanel from '../../../../../../frontend/js/features/subscription/components/new/checkout/checkout-panel'
-import { PaymentProvider } from '../../../../../../frontend/js/features/subscription/context/payment-context'
-import { plans } from '../../fixtures/plans'
-import {
-  createFakeRecurly,
-  defaultSubscription,
-  ElementsBase,
-} from '../../fixtures/recurly-mock'
-import { fillForm } from '../../helpers/payment'
-import { cloneDeep } from 'lodash'
-import { TokenHandler, RecurlyError } from 'recurly__recurly-js'
-
-function CheckoutPanelWithPaymentProvider() {
-  return (
-    <PaymentProvider publicKey="0000">
-      <CheckoutPanel />
-    </PaymentProvider>
-  )
-}
-
-describe('checkout panel', function () {
-  const itmCampaign = 'fake_itm_campaign'
-  const itmContent = 'fake_itm_content'
-  const itmReferrer = 'fake_itm_referrer'
-
-  beforeEach(function () {
-    const plan = plans.find(({ planCode }) => planCode === 'student-annual')
-
-    if (!plan) {
-      throw new Error('No plan was found while running the test!')
-    }
-
-    cy.window().then(win => {
-      win.metaAttributesCache = new Map()
-      win.metaAttributesCache.set('ol-countryCode', '')
-      win.metaAttributesCache.set('ol-recurlyApiKey', '0000')
-      win.metaAttributesCache.set('ol-recommendedCurrency', 'USD')
-      win.metaAttributesCache.set('ol-plan', plan)
-      win.metaAttributesCache.set('ol-planCode', plan.planCode)
-      win.metaAttributesCache.set('ol-showCouponField', true)
-      win.metaAttributesCache.set('ol-itm_campaign', itmCampaign)
-      win.metaAttributesCache.set('ol-itm_content', itmContent)
-      win.metaAttributesCache.set('ol-itm_referrer', itmReferrer)
-
-      cy.wrap(plan).as('plan')
-
-      // init default recurly
-      win.recurly = createFakeRecurly(defaultSubscription)
-
-      cy.interceptEvents()
-    })
-
-    cy.mount(<CheckoutPanelWithPaymentProvider />)
-    cy.findByTestId('checkout-form').as('form')
-  })
-
-  it('renders heading', function () {
-    cy.contains(/select a payment method/i)
-  })
-
-  it('renders student disclaimer', function () {
-    cy.contains(
-      'The educational discount applies to all students at secondary and postsecondary institutions ' +
-        '(schools and universities). We may contact you to confirm that you’re eligible for the discount.'
-    )
-  })
-
-  it('renders payment method toggle', function () {
-    cy.findByTestId('payment-method-toggle').within(() => {
-      cy.findByLabelText(/card payment/i)
-      cy.findByLabelText(/paypal/i)
-    })
-  })
-
-  it('renders address first line input', function () {
-    cy.get('@form').within(() => {
-      cy.findByLabelText('Address')
-      cy.findByLabelText(/this address will be shown on the invoice/i)
-    })
-  })
-
-  it('renders address second line input', function () {
-    cy.get('@form').within(() => {
-      cy.findByLabelText(/address second line/i).should('not.exist')
-      cy.findByRole('button', { name: /add another address line/i }).click()
-      cy.findByLabelText(/address second line/i)
-    })
-  })
-
-  it('renders postal code input', function () {
-    cy.get('@form').within(() => {
-      cy.findByLabelText(/postal code/i)
-    })
-  })
-
-  it('renders country dropdown', function () {
-    cy.get('@form').within(() => {
-      cy.findByLabelText(/country/i)
-    })
-  })
-
-  it('renders company details', function () {
-    cy.get('@form').within(() => {
-      cy.findByLabelText(/add company details/i).as('checkbox')
-      cy.get('@checkbox').should('not.be.checked')
-      cy.findByLabelText(/company name/i).should('not.exist')
-      cy.findByLabelText(/vat number/i).should('not.exist')
-      cy.get('@checkbox').click()
-      cy.findByLabelText(/company name/i)
-      cy.findByLabelText(/vat number/i)
-    })
-  })
-
-  it('renders coupon field', function () {
-    cy.get('@form').within(() => {
-      cy.findByLabelText(/coupon code/i)
-    })
-  })
-
-  it('renders tos agreement notice', function () {
-    cy.contains(/by subscribing, you agree to our terms of service/i)
-  })
-
-  it('renders recurly error', function () {
-    cy.window().then(win => {
-      win.recurly = undefined!
-    })
-    cy.mount(<CheckoutPanelWithPaymentProvider />)
-    cy.contains(
-      /sorry, there was an error talking to our payment provider. Please try again in a few moments/i
-    )
-    cy.contains(
-      /if you are using any ad or script blocking extensions in your browser, you may need to temporarily disable them/i
-    )
-  })
-
-  it('calls recurly.token on submit', function () {
-    cy.window().then(win => {
-      cy.stub(win.recurly, 'token')
-    })
-    cy.mount(<CheckoutPanelWithPaymentProvider />)
-    cy.get('@form').within(() => fillForm())
-    cy.findByRole('button', { name: /upgrade now/i }).click()
-
-    cy.window().then(win => {
-      expect(win.recurly.token).to.be.calledOnceWith(
-        Cypress.sinon.match.instanceOf(ElementsBase),
-        {
-          first_name: '1',
-          last_name: '1',
-          postal_code: '1',
-          address1: '1',
-          address2: '',
-          state: '',
-          city: '',
-          country: 'BG',
-          coupon: '',
-        },
-        Cypress.sinon.match.func
-      )
-    })
-  })
-
-  it('renders generic error', function () {
-    const errorMessage = 'generic error'
-    cy.window().then(win => {
-      win.recurly = createFakeRecurly(defaultSubscription, {
-        token: (_1: unknown, _2: unknown, handler: TokenHandler) => {
-          const err = new Error(errorMessage) as RecurlyError
-          setTimeout(() => handler(err, { id: '1', type: 'abc' }), 100)
-        },
-      })
-    })
-    cy.mount(<CheckoutPanelWithPaymentProvider />)
-    cy.get('@form').within(() => fillForm())
-
-    cy.findByRole('button', { name: /upgrade now/i }).as('button')
-    cy.get('@button').click()
-    cy.get('@button').within(() => {
-      cy.contains(/processing/i)
-    })
-    cy.findByRole('alert').should('have.text', errorMessage)
-    cy.get('@button').within(() => {
-      cy.findByText(/processing/i).should('not.exist')
-    })
-  })
-
-  it('renders prefilled coupon input', function () {
-    const couponCode = 'promo_code'
-    cy.window().then(win => {
-      win.metaAttributesCache.set('ol-couponCode', couponCode)
-    })
-    cy.mount(<CheckoutPanelWithPaymentProvider />)
-    cy.findByLabelText(/coupon code/i).should('have.value', couponCode)
-  })
-
-  it('calls coupon method when entering coupon code', function () {
-    const couponCode = 'promo_code'
-    cy.window().then(win => {
-      const couponStub = cy.stub().as('coupon')
-      couponStub.returnsThis()
-      win.recurly = createFakeRecurly({
-        ...defaultSubscription,
-        coupon: couponStub,
-      })
-    })
-    cy.mount(<CheckoutPanelWithPaymentProvider />)
-    cy.get('@coupon').should('have.been.calledOnce')
-    cy.findByTestId('checkout-form').within(() => {
-      cy.findByLabelText(/coupon code/i).type(couponCode, { delay: 0 })
-      cy.findByLabelText(/coupon code/i).blur()
-    })
-    cy.get('@coupon')
-      .should('have.been.calledTwice')
-      .and('have.been.calledWith', couponCode)
-  })
-
-  it('enters invalid coupon code', function () {
-    cy.window().then(win => {
-      const catchStub = cy.stub().as('catch')
-      catchStub.onFirstCall().returnsThis()
-      catchStub
-        .onSecondCall()
-        .callsFake(function (this: unknown, cb: (err: RecurlyError) => void) {
-          const err = {
-            name: 'api-error',
-            code: 'not-found',
-          } as RecurlyError
-
-          cb(err)
-          return this
-        })
-
-      win.recurly = createFakeRecurly({
-        ...defaultSubscription,
-        catch: catchStub,
-      })
-    })
-    cy.mount(<CheckoutPanelWithPaymentProvider />)
-    cy.findByTestId('checkout-form').within(() => {
-      cy.findByLabelText(/coupon code/i).type('promo_code', { delay: 0 })
-      cy.findByLabelText(/coupon code/i).blur()
-    })
-    cy.findByRole('alert').within(() => {
-      cy.contains(/coupon code is not valid for selected plan/i)
-    })
-  })
-
-  it('fails coupon verification', function () {
-    cy.window().then(win => {
-      const catchStub = cy.stub().as('catch')
-      // call original method on change event
-      catchStub.onFirstCall().returnsThis()
-      catchStub
-        .onSecondCall()
-        .callsFake(function (this: unknown, cb: (err: RecurlyError) => void) {
-          const err = {} as RecurlyError
-
-          try {
-            cb(err)
-          } catch (e) {}
-
-          return this
-        })
-
-      win.recurly = createFakeRecurly({
-        ...defaultSubscription,
-        catch: catchStub,
-      })
-    })
-    cy.mount(<CheckoutPanelWithPaymentProvider />)
-    cy.get('@catch').should('have.been.calledOnce')
-    cy.findByTestId('checkout-form').within(() => {
-      cy.findByLabelText(/coupon code/i).type('promo_code', { delay: 0 })
-      cy.findByLabelText(/coupon code/i).blur()
-    })
-    cy.get('@catch').should('have.been.calledTwice')
-    cy.findByRole('alert').within(() => {
-      cy.contains(/an error occurred when verifying the coupon code/i)
-    })
-  })
-
-  /* The test is disabled due to https://github.com/overleaf/internal/issues/12004
-  it.skip('creates a new subscription', function () {
-    cy.stub(locationModule, 'assign').as('assign')
-    cy.intercept('POST', 'user/subscription/create', {
-      statusCode: 201,
-    }).as('create')
-
-    cy.mount(<CheckoutPanelWithPaymentProvider />)
-    cy.findByTestId('checkout-form').within(() => fillForm())
-    cy.findByRole('button', { name: /upgrade now/i }).click()
-    // verify itm params are also passed
-    cy.get('@create')
-      .its('request.body.subscriptionDetails')
-      .should('contain', {
-        ITMCampaign: itmCampaign,
-        ITMContent: itmContent,
-        ITMReferrer: itmReferrer,
-      })
-    cy.get('@assign')
-      .should('have.been.calledOnce')
-      .and('have.been.calledWith', '/user/subscription/thank-you')
-  })
-  */
-
-  it('fails to create a new subscription', function () {
-    cy.intercept('POST', 'user/subscription/create', {
-      statusCode: 404,
-    })
-
-    cy.mount(<CheckoutPanelWithPaymentProvider />)
-    cy.findByTestId('checkout-form').within(() => fillForm())
-    cy.findByRole('button', { name: /upgrade now/i }).click()
-    cy.findByRole('alert').within(() => {
-      cy.contains(/something went wrong processing the request/i)
-    })
-  })
-
-  describe('3DS challenge', function () {
-    it('shows three d secure challenge', function () {
-      cy.intercept('POST', 'user/subscription/create', {
-        statusCode: 404,
-        body: {
-          threeDSecureActionTokenId: '123',
-        },
-      })
-
-      cy.mount(<CheckoutPanelWithPaymentProvider />)
-      cy.findByTestId('checkout-form').within(() => fillForm())
-      cy.findByRole('button', { name: /upgrade now/i }).click()
-      cy.findByRole('alert').within(() => {
-        cy.contains(
-          /your card must be authenticated with 3D Secure before continuing/i
-        )
-      })
-      cy.contains('3D challenge content')
-    })
-  })
-
-  describe('card payments', function () {
-    beforeEach(function () {
-      cy.findByLabelText(/card payment/i).click()
-    })
-
-    it('renders card element', function () {
-      cy.get('@form').within(() => {
-        cy.findByText(/card details/i, { selector: 'label' })
-        cy.findByTestId('test-card-element')
-      })
-    })
-
-    it('verifies the card element does not disappear when switching between payment methods', function () {
-      cy.get('@form').within(() => {
-        cy.findByText(/card details/i, { selector: 'label' })
-        cy.findByTestId('test-card-element')
-        cy.findByLabelText(/paypal/i).click()
-        cy.findByLabelText(/card payment/i).click()
-        cy.findByText(/card details/i, { selector: 'label' })
-        cy.findByTestId('test-card-element')
-      })
-    })
-
-    it('renders first name input', function () {
-      cy.get('@form').within(() => {
-        cy.findByLabelText(/first name/i)
-      })
-    })
-
-    it('renders last name input', function () {
-      cy.get('@form').within(() => {
-        cy.findByLabelText(/last name/i)
-      })
-    })
-
-    describe('submit button', function () {
-      it('renders trial button', function () {
-        cy.get('@form').within(() => {
-          cy.findByRole('button', { name: /upgrade now, pay after \d+ days/i })
-        })
-      })
-
-      it('renders non-trial button', function () {
-        cy.window().then(win => {
-          const clone = cloneDeep(defaultSubscription)
-          clone.items.plan!.trial = undefined
-          win.recurly = createFakeRecurly(clone)
-        })
-        cy.mount(<CheckoutPanelWithPaymentProvider />)
-        cy.findByTestId('checkout-form').within(() => {
-          cy.findByRole('button', { name: 'Upgrade Now' })
-        })
-      })
-
-      it('handles the disabled state of submit button', function () {
-        cy.get('@form').within(() => {
-          cy.findByRole('button', { name: /upgrade now/i }).should(
-            'be.disabled'
-          )
-          fillForm()
-          cy.findByRole('button', { name: /upgrade now/i }).should(
-            'not.be.disabled'
-          )
-        })
-      })
-    })
-  })
-
-  describe('paypal payments', function () {
-    beforeEach(function () {
-      cy.findByLabelText(/paypal/i).click()
-    })
-
-    it('should not render card element', function () {
-      cy.get('@form').within(() => {
-        cy.findByLabelText(/card details/i).should('not.exist')
-      })
-    })
-
-    it('should not render first name input', function () {
-      cy.get('@form').within(() => {
-        cy.findByLabelText(/first name/i).should('not.exist')
-      })
-    })
-
-    it('should not render last name input', function () {
-      cy.get('@form').within(() => {
-        cy.findByLabelText(/last name/i).should('not.exist')
-      })
-    })
-
-    it('renders proceeding to PayPal notice', function () {
-      cy.get('@form').within(() => {
-        cy.contains(
-          /proceeding to PayPal will take you to the PayPal site to pay for your subscription/i
-        )
-      })
-    })
-
-    it('handles the disabled state of submit button', function () {
-      cy.get('@form').within(() => {
-        cy.findByRole('button', { name: /proceed to paypal/i }).should(
-          'be.disabled'
-        )
-        cy.findByLabelText(/country/i).select('Bulgaria')
-        cy.findByRole('button', { name: /proceed to paypal/i }).should(
-          'not.be.disabled'
-        )
-      })
-    })
-  })
-})

+ 0 - 68
services/web/test/frontend/features/subscription/components/new/common.spec.tsx

@@ -1,68 +0,0 @@
-import {
-  createFakeRecurly,
-  defaultSubscription,
-} from '../../fixtures/recurly-mock'
-import { PaymentProvider } from '../../../../../../frontend/js/features/subscription/context/payment-context'
-import { plans } from '../../fixtures/plans'
-import PaymentPreviewPanel from '../../../../../../frontend/js/features/subscription/components/new/payment-preview/payment-preview-panel'
-import CheckoutPanel from '../../../../../../frontend/js/features/subscription/components/new/checkout/checkout-panel'
-import { fillForm } from '../../helpers/payment'
-
-describe('common recurly validations', function () {
-  beforeEach(function () {
-    const plan = plans.find(({ planCode }) => planCode === 'collaborator')
-
-    if (!plan) {
-      throw new Error('No plan was found while running the test!')
-    }
-
-    cy.window().then(win => {
-      win.metaAttributesCache = new Map()
-      win.metaAttributesCache.set('ol-countryCode', '')
-      win.metaAttributesCache.set('ol-recurlyApiKey', '1234')
-      win.metaAttributesCache.set('ol-recommendedCurrency', 'USD')
-      win.metaAttributesCache.set('ol-plan', plan)
-      win.metaAttributesCache.set('ol-planCode', plan.planCode)
-      win.metaAttributesCache.set('ol-showCouponField', true)
-      win.recurly = createFakeRecurly(defaultSubscription)
-      cy.interceptEvents()
-    })
-  })
-
-  it('initializes recurly', function () {
-    cy.window().then(win => {
-      cy.spy(win.recurly, 'configure')
-      cy.spy(win.recurly.Pricing, 'Subscription')
-    })
-
-    cy.mount(<PaymentProvider publicKey="0000" />)
-
-    cy.window().then(win => {
-      expect(win.recurly.configure).to.be.calledOnce
-      expect(win.recurly.Pricing.Subscription).to.be.calledOnce
-    })
-  })
-
-  it('shows three d secure challenge content only once when changing currency', function () {
-    cy.intercept('POST', 'user/subscription/create', {
-      statusCode: 404,
-      body: {
-        threeDSecureActionTokenId: '123',
-      },
-    })
-
-    cy.mount(
-      <PaymentProvider publicKey="0000">
-        <PaymentPreviewPanel />
-        <CheckoutPanel />
-      </PaymentProvider>
-    )
-    cy.findByTestId('checkout-form').within(() => fillForm())
-    cy.findByRole('button', { name: /upgrade now/i }).click()
-    cy.findByRole('button', { name: /change currency/i }).click()
-    cy.findByRole('menu').within(() => {
-      cy.findByRole('menuitem', { name: /gbp/i }).click()
-    })
-    cy.findAllByText('3D challenge content').should('have.length', 1)
-  })
-})

+ 0 - 310
services/web/test/frontend/features/subscription/components/new/payment-preview.spec.tsx

@@ -1,310 +0,0 @@
-import PaymentPreviewPanel from '../../../../../../frontend/js/features/subscription/components/new/payment-preview/payment-preview-panel'
-import { PaymentProvider } from '../../../../../../frontend/js/features/subscription/context/payment-context'
-import { plans } from '../../fixtures/plans'
-import {
-  createFakeRecurly,
-  defaultSubscription,
-} from '../../fixtures/recurly-mock'
-import { cloneDeep } from 'lodash'
-import { Plan } from '../../../../../../types/subscription/plan'
-
-function PaymentPreviewPanelWithPaymentProvider() {
-  return (
-    <PaymentProvider publicKey="0000">
-      <PaymentPreviewPanel />
-    </PaymentProvider>
-  )
-}
-
-describe('payment preview panel', function () {
-  beforeEach(function () {
-    const plan = plans.find(({ planCode }) => planCode === 'collaborator')
-
-    if (!plan) {
-      throw new Error('No plan was found while running the test!')
-    }
-
-    cy.window().then(win => {
-      win.metaAttributesCache = new Map()
-      win.metaAttributesCache.set('ol-countryCode', '')
-      win.metaAttributesCache.set('ol-recurlyApiKey', '0000')
-      win.metaAttributesCache.set('ol-recommendedCurrency', 'USD')
-      win.metaAttributesCache.set('ol-plan', plan)
-      win.metaAttributesCache.set('ol-planCode', plan.planCode)
-      cy.wrap(plan).as('plan')
-
-      // init default recurly
-      win.recurly = createFakeRecurly(defaultSubscription)
-      cy.interceptEvents()
-    })
-  })
-
-  it('renders plan name', function () {
-    cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
-
-    cy.contains(defaultSubscription.items.plan!.name)
-  })
-
-  it('renders collaborators per project', function () {
-    cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
-
-    cy.get<Plan>('@plan').then(plan => {
-      cy.contains(`${plan.features?.collaborators} collaborators per project`)
-    })
-  })
-
-  it('renders features list', function () {
-    cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
-
-    cy.contains(/all premium features/i)
-    cy.findByTestId('features-list').within(() => {
-      cy.get(':nth-child(1)').contains(/increased compile timeout/i)
-      cy.get(':nth-child(2)').contains(/sync with dropbox and github/i)
-      cy.get(':nth-child(3)').contains(/full document history/i)
-      cy.get(':nth-child(4)').contains(/track changes/i)
-      cy.get(':nth-child(5)').contains(/advanced reference search/i)
-      cy.get(':nth-child(6)').contains(/reference manager sync/i)
-      cy.get(':nth-child(7)').contains(/symbol palette/i)
-    })
-  })
-
-  it('renders no features list', function () {
-    cy.window().then(win => {
-      cy.get<Plan>('@plan').then(plan => {
-        const { features: _, ...noFeaturesPlan } = plan
-        win.metaAttributesCache.set('ol-plan', noFeaturesPlan)
-      })
-    })
-
-    cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
-
-    cy.findByTestId('features-list').should('not.exist')
-  })
-
-  describe('price summary', function () {
-    beforeEach(function () {
-      cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
-      cy.findByTestId('price-summary').as('priceSummary')
-      cy.findByTestId('price-summary-plan').as('priceSummaryPlan')
-      cy.findByTestId('price-summary-coupon').as('priceSummaryCoupon')
-      cy.findByTestId('price-summary-vat').as('priceSummaryVat')
-      cy.findByTestId('price-summary-total').as('priceSummaryTotal')
-    })
-
-    it('renders title', function () {
-      cy.get('@priceSummary').contains(/payment summary/i)
-    })
-
-    it('renders plan info', function () {
-      cy.get('@priceSummaryPlan').contains(defaultSubscription.items.plan!.name)
-      cy.get('@priceSummaryPlan').contains(
-        `$${defaultSubscription.price.base.plan.unit}`
-      )
-    })
-
-    it('renders coupon info', function () {
-      cy.get('@priceSummaryCoupon').contains(
-        defaultSubscription.items.coupon!.name
-      )
-      cy.get('@priceSummaryCoupon').contains(
-        `Discount of $${defaultSubscription.price.now.discount}`
-      )
-    })
-
-    it('does not render coupon info when there is no coupon', function () {
-      cy.window().then(win => {
-        const { coupon: _, ...items } = defaultSubscription.items
-        win.recurly = createFakeRecurly({ ...defaultSubscription, items })
-      })
-      cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
-      cy.findByTestId('price-summary-coupon').should('not.exist')
-    })
-
-    it('renders VAT', function () {
-      cy.get('@priceSummaryVat').contains(
-        `VAT ${parseFloat(defaultSubscription.price.taxes[0].rate) * 100}%`
-      )
-      cy.get('@priceSummaryVat').contains(
-        `$${defaultSubscription.price.now.tax}`
-      )
-    })
-
-    describe('total amount', function () {
-      it('renders "total per month" text', function () {
-        cy.window().then(win => {
-          const clone = cloneDeep(defaultSubscription)
-          clone.items.plan!.period.length = 1
-          win.recurly = createFakeRecurly(clone)
-        })
-        cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
-        cy.findByTestId('price-summary-total').contains(/total per month/i)
-      })
-
-      it('renders "total per year" text', function () {
-        cy.window().then(win => {
-          const clone = cloneDeep(defaultSubscription)
-          clone.items.plan!.period.length = 2
-          win.recurly = createFakeRecurly(clone)
-        })
-        cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
-        cy.findByTestId('price-summary-total').contains(/total per year/i)
-      })
-
-      it('renders total amount', function () {
-        cy.get('@priceSummaryTotal').contains(
-          `$${defaultSubscription.price.now.total}`
-        )
-      })
-    })
-
-    it('renders "change currency" dropdown and changes currency', function () {
-      cy.get('@priceSummary').within(() => {
-        cy.get('@priceSummary')
-          .findByRole('button', { name: /change currency/i })
-          .as('button')
-        cy.findByRole('menu').should('not.exist')
-        cy.get('@button').click()
-        cy.findByRole('menu').within(() => {
-          cy.findByRole('menuitem', { name: /usd \(\$\)/i }).contains(
-            /selected/i
-          )
-          cy.findByRole('menuitem', { name: /eur \(€\)/i })
-          cy.findByRole('menuitem', { name: /gbp \(£\)/i }).click()
-
-          cy.get('@priceSummaryPlan').contains(
-            `£${defaultSubscription.price.base.plan.unit}`
-          )
-          cy.get('@priceSummaryCoupon').contains(
-            `Discount of £${defaultSubscription.price.now.discount}`
-          )
-          cy.get('@priceSummaryVat').contains(
-            `£${defaultSubscription.price.now.tax}`
-          )
-          cy.get('@priceSummaryTotal').contains(
-            `£${defaultSubscription.price.now.total}`
-          )
-        })
-      })
-      cy.findByTestId('trial-coupon-summary')
-        .should('not.contain.text', '$')
-        .should('contain.text', '£')
-    })
-  })
-
-  describe('trial coupon summary', function () {
-    it('renders trial price', function () {
-      cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
-      cy.findByTestId('trial-coupon-summary').contains(
-        `First ${defaultSubscription.items.plan!.trial!.length} days free, ` +
-          `after that $${defaultSubscription.price.now.total} per month`
-      )
-    })
-
-    it('renders "X price for Y months"', function () {
-      cy.window()
-        .then(win => {
-          const clone = cloneDeep(defaultSubscription)
-          clone.items.coupon!.applies_for_months = 6
-          clone.items.coupon!.single_use = false
-          clone.items.plan!.period.length = 1
-          win.recurly = createFakeRecurly(clone)
-          return clone
-        })
-        .then((clone: typeof defaultSubscription) => {
-          cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
-          cy.findByTestId('trial-coupon-summary').contains(
-            `$${clone.price.now.total} for your first ${
-              clone.items.coupon!.applies_for_months
-            } months`
-          )
-        })
-    })
-
-    it('renders "X price for first month"', function () {
-      cy.window()
-        .then(win => {
-          const clone = cloneDeep(defaultSubscription)
-          clone.items.plan!.period.length = 1
-          win.recurly = createFakeRecurly(clone)
-        })
-        .then(() => {
-          cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
-          cy.findByTestId('trial-coupon-summary').contains(
-            `$${defaultSubscription.price.now.total} for your first month`
-          )
-        })
-    })
-
-    it('renders "X price for first year"', function () {
-      cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
-      cy.findByTestId('trial-coupon-summary').contains(
-        `$${defaultSubscription.price.now.total} for your first year`
-      )
-    })
-
-    it('renders "then X price per month"', function () {
-      cy.window()
-        .then(win => {
-          const clone = cloneDeep(defaultSubscription)
-          clone.items.coupon!.applies_for_months = 6
-          clone.items.coupon!.single_use = false
-          clone.items.plan!.period.length = 1
-          win.recurly = createFakeRecurly(clone)
-        })
-        .then(() => {
-          cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
-          cy.findByTestId('trial-coupon-summary').contains(
-            `Then $26.00 per month`
-          )
-        })
-    })
-
-    it('renders "then X price per year"', function () {
-      cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
-      cy.findByTestId('trial-coupon-summary').contains(`Then $26.00 per year`)
-    })
-
-    it('renders "normally X price per month"', function () {
-      cy.window()
-        .then(win => {
-          const clone = cloneDeep(defaultSubscription)
-          clone.items.coupon!.applies_for_months = 0
-          clone.items.coupon!.single_use = false
-          clone.items.plan!.period.length = 1
-          win.recurly = createFakeRecurly(clone)
-        })
-        .then(() => {
-          cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
-          cy.findByTestId('trial-coupon-summary').contains(
-            `Normally $26.00 per month`
-          )
-        })
-    })
-
-    it('renders "normally X price per year"', function () {
-      cy.window()
-        .then(win => {
-          const clone = cloneDeep(defaultSubscription)
-          clone.items.coupon!.single_use = false
-          clone.items.plan!.period.length = 2
-          win.recurly = createFakeRecurly(clone)
-        })
-        .then(() => {
-          cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
-          cy.findByTestId('trial-coupon-summary').contains(
-            `Normally $26.00 per year`
-          )
-        })
-    })
-  })
-
-  it('renders "cancel anytime" content', function () {
-    cy.mount(<PaymentPreviewPanelWithPaymentProvider />)
-
-    cy.contains(
-      /we’re confident that you’ll love Overleaf, but if not you can cancel anytime/i
-    ).contains(
-      /we’ll give you your money back, no questions asked, if you let us know within 30 days/i
-    )
-  })
-})

+ 0 - 232
services/web/test/frontend/features/subscription/fixtures/recurly-mock.ts

@@ -1,232 +0,0 @@
-import {
-  Address,
-  CardElementOptions,
-  ElementsInstance,
-  PayPalConfig,
-  PlanOptions,
-  Recurly,
-  RecurlyError,
-  RecurlyOptions,
-  RiskOptions,
-  Tax,
-  TokenPayload,
-} from 'recurly__recurly-js'
-import { SubscriptionPricingInstanceCustom } from '../../../../../types/recurly/pricing/subscription'
-
-export const defaultSubscription = {
-  id: '123',
-  price: {
-    base: {
-      plan: {
-        unit: '20.00',
-        setup_fee: '0.00',
-      },
-    },
-    next: {
-      addons: '0.00',
-      discount: '1.00',
-      plan: '10.00',
-      setup_fee: '0.00',
-      subtotal: '9.00',
-      tax: '1.20',
-      total: '12.00',
-    },
-    now: {
-      addons: '0.00',
-      discount: '1.00',
-      plan: '10.00',
-      setup_fee: '0.00',
-      subtotal: '9.00',
-      tax: '1.20',
-      total: '12.00',
-    },
-    taxes: [
-      {
-        tax_type: 'tax_type_1',
-        region: 'EU',
-        rate: '0.3',
-      },
-    ],
-  },
-  items: {
-    coupon: {
-      applies_for_months: 2,
-      code: 'react',
-      discount: {
-        type: 'percent',
-        rate: 0.2,
-      },
-      name: 'fake coupon',
-      single_use: true,
-    },
-    currency: 'USD',
-    plan: {
-      code: 'asd',
-      name: 'Standard (Collaborator)',
-      period: {
-        interval: '2',
-        length: 5,
-      },
-      price: {
-        '15': {
-          unit_amount: 5,
-          symbol: '$',
-          setup_fee: 2,
-        },
-      },
-      quantity: 1,
-      tax_code: 'digital',
-      tax_exempt: false,
-      trial: {
-        interval: 'weekly',
-        length: 7,
-      },
-    },
-  },
-} as unknown as SubscriptionPricingInstanceCustom
-
-class PayPalBase {
-  protected constructor(config?: PayPalConfig) {
-    Object.assign(this, config)
-  }
-
-  static PayPal = () => new this()
-
-  destroy() {}
-
-  on(_eventName: string, _callback: () => void) {}
-}
-
-class Card {
-  protected fakeCardEl
-
-  constructor() {
-    this.fakeCardEl = document.createElement('div')
-  }
-
-  attach(el: HTMLElement) {
-    this.fakeCardEl.dataset.testid = 'test-card-element'
-    const input = document.createElement('input')
-    input.style.border = '1px solid black'
-    input.style.width = '50px'
-    const cardNumberInput = input.cloneNode(true) as HTMLInputElement
-    cardNumberInput.style.width = '200px'
-    cardNumberInput.placeholder = 'XXXX-XXXX-XXXX-XXXX'
-
-    this.fakeCardEl.appendChild(cardNumberInput)
-    this.fakeCardEl.appendChild(input.cloneNode(true))
-    this.fakeCardEl.appendChild(input.cloneNode(true))
-    this.fakeCardEl.appendChild(input.cloneNode(true))
-    el.appendChild(this.fakeCardEl)
-  }
-
-  on(eventName = 'change', callback: (state: Record<string, unknown>) => void) {
-    this.fakeCardEl.querySelectorAll('input').forEach(node => {
-      node.addEventListener(eventName, () => {
-        const state = {
-          valid: true,
-        }
-        callback(state)
-      })
-    })
-  }
-}
-
-export class ElementsBase {
-  protected constructor(config?: unknown) {
-    Object.assign(this, config)
-  }
-
-  static Elements = () => new this()
-
-  CardElement(_cardElementOptions?: CardElementOptions) {
-    return new Card()
-  }
-}
-
-export class ThreeDSecureBase {
-  protected constructor(riskOptions?: RiskOptions) {
-    Object.assign(this, riskOptions)
-  }
-
-  static ThreeDSecure = (_riskOptions: RiskOptions) => new this()
-
-  on(_eventName = 'change', _callback: () => void) {}
-
-  attach(el: HTMLElement) {
-    const div = document.createElement('div')
-    div.appendChild(document.createTextNode('3D challenge content'))
-    el.appendChild(div)
-  }
-}
-
-abstract class PricingBase {
-  plan(_planCode: string, _options: PlanOptions) {
-    return this
-  }
-
-  address(_address: Address) {
-    return this
-  }
-
-  tax(_tax: Tax) {
-    return this
-  }
-
-  currency(_currency: string) {
-    return this
-  }
-
-  coupon(_coupon: string) {
-    return this
-  }
-
-  catch(_callback?: (reason?: RecurlyError) => void) {
-    return this
-  }
-
-  done(callback?: () => unknown) {
-    callback?.()
-    return this
-  }
-
-  on(_eventName: string, callback: () => void) {
-    callback()
-  }
-}
-
-const createSubscriptionClass = (classProps: unknown) => {
-  return class extends PricingBase {
-    protected constructor() {
-      super()
-      Object.assign(this, classProps)
-    }
-
-    static Subscription = () => new this()
-  }
-}
-
-// Using `overrides` as currently can't stub/spy external files with cypress
-export const createFakeRecurly = (classProps: unknown, overrides = {}) => {
-  return {
-    configure: (_options: RecurlyOptions) => {},
-    token: (
-      _elements: ElementsInstance,
-      _second: unknown,
-      handler: (
-        err?: RecurlyError | null,
-        recurlyBillingToken?: TokenPayload,
-        threeDResultToken?: TokenPayload
-      ) => void
-    ) => {
-      handler(undefined, undefined, { id: '123', type: '456' })
-    },
-    Elements: ElementsBase.Elements,
-    PayPal: PayPalBase.PayPal,
-    Pricing: createSubscriptionClass(classProps),
-    Risk: () => ({
-      ThreeDSecure: ThreeDSecureBase.ThreeDSecure,
-    }),
-    ...overrides,
-  } as unknown as Recurly
-}

+ 0 - 12
services/web/test/frontend/features/subscription/helpers/payment.ts

@@ -1,12 +0,0 @@
-export function fillForm() {
-  cy.findByTestId('test-card-element').within(() => {
-    cy.get('input').each(el => {
-      cy.wrap(el).type('1', { delay: 0 })
-    })
-  })
-  cy.findByLabelText(/first name/i).type('1', { delay: 0 })
-  cy.findByLabelText(/last name/i).type('1', { delay: 0 })
-  cy.findByLabelText('Address').type('1', { delay: 0 })
-  cy.findByLabelText(/postal code/i).type('1', { delay: 0 })
-  cy.findByLabelText(/country/i).select('Bulgaria')
-}

+ 0 - 296
services/web/test/unit/src/Subscription/SubscriptionControllerTests.js

@@ -5,7 +5,6 @@ const MockRequest = require('../helpers/MockRequest')
 const MockResponse = require('../helpers/MockResponse')
 const modulePath =
   '../../../../app/src/Features/Subscription/SubscriptionController'
-const Errors = require('../../../../app/src/Features/Errors/Errors')
 const SubscriptionErrors = require('../../../../app/src/Features/Subscription/Errors')
 
 const mockSubscriptions = {
@@ -85,7 +84,6 @@ describe('SubscriptionController', function () {
         .returns({ plans: [], planCodesChangingAtTermEnd: [] }),
     }
     this.settings = {
-      restrictedCountries: ['KP'],
       coupon_codes: {
         upgradeToAnnualPromo: {
           student: 'STUDENTCODEHERE',
@@ -305,154 +303,6 @@ describe('SubscriptionController', function () {
     })
   })
 
-  describe('paymentPage', function () {
-    beforeEach(function () {
-      this.req.headers = {}
-      this.SubscriptionHandler.promises.validateNoSubscriptionInRecurly = sinon
-        .stub()
-        .resolves(true)
-      this.GeoIpLookup.promises.getCurrencyCode.resolves({
-        currencyCode: this.stubbedCurrencyCode,
-      })
-    })
-
-    describe('with a user without a subscription', function () {
-      beforeEach(function () {
-        this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
-          false
-        )
-        this.PlansLocator.findLocalPlanInSettings.returns({})
-      })
-
-      describe('with a valid plan code', function () {
-        it('should render the new subscription page', function (done) {
-          this.res.render = (page, opts) => {
-            page.should.equal('subscriptions/new-react')
-            done()
-          }
-          this.SubscriptionController.paymentPage(this.req, this.res, done)
-        })
-      })
-    })
-
-    describe('with a user with subscription', function () {
-      it('should redirect to the subscription dashboard', function (done) {
-        this.PlansLocator.findLocalPlanInSettings.returns({})
-        this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
-          true
-        )
-        this.res.redirect = url => {
-          url.should.equal('/user/subscription?hasSubscription=true')
-          done()
-        }
-        this.SubscriptionController.paymentPage(this.req, this.res)
-      })
-    })
-
-    describe('with an invalid plan code', function () {
-      it('should return 422 error - Unprocessable Entity', function (done) {
-        this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
-          false
-        )
-        this.PlansLocator.findLocalPlanInSettings.returns(null)
-        this.HttpErrorHandler.unprocessableEntity = sinon.spy(
-          (req, res, message) => {
-            expect(req).to.exist
-            expect(res).to.exist
-            expect(message).to.deep.equal('Plan not found')
-            done()
-          }
-        )
-        this.SubscriptionController.paymentPage(this.req, this.res)
-      })
-    })
-
-    describe('which currency to use', function () {
-      beforeEach(function () {
-        this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
-          false
-        )
-        this.PlansLocator.findLocalPlanInSettings.returns({})
-      })
-
-      it('should use the set currency from the query string', function (done) {
-        this.req.query.currency = 'EUR'
-        this.res.render = (page, opts) => {
-          opts.currency.should.equal('EUR')
-          opts.currency.should.not.equal(this.stubbedCurrencyCode)
-          done()
-        }
-        this.SubscriptionController.paymentPage(this.req, this.res)
-      })
-
-      it('should upercase the currency code', function (done) {
-        this.req.query.currency = 'eur'
-        this.res.render = (page, opts) => {
-          opts.currency.should.equal('EUR')
-          done()
-        }
-        this.SubscriptionController.paymentPage(this.req, this.res)
-      })
-
-      it('should use the geo ip currency if non is provided', function (done) {
-        this.req.query.currency = null
-        this.res.render = (page, opts) => {
-          opts.currency.should.equal(this.stubbedCurrencyCode)
-          done()
-        }
-        this.SubscriptionController.paymentPage(this.req, this.res)
-      })
-
-      it('should use the geo ip currency if not valid', function (done) {
-        this.req.query.currency = 'WAT'
-        this.GeoIpLookup.isValidCurrencyParam.returns(false)
-        this.res.render = (page, opts) => {
-          opts.currency.should.equal(this.stubbedCurrencyCode)
-          done()
-        }
-        this.SubscriptionController.paymentPage(this.req, this.res)
-      })
-    })
-
-    describe('with a recurly subscription already', function () {
-      it('should redirect to the subscription dashboard', function (done) {
-        this.PlansLocator.findLocalPlanInSettings.returns({})
-        this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
-          false
-        )
-        this.SubscriptionHandler.promises.validateNoSubscriptionInRecurly.resolves(
-          false
-        )
-        this.res.redirect = url => {
-          url.should.equal('/user/subscription?hasSubscription=true')
-          done()
-        }
-        this.SubscriptionController.paymentPage(this.req, this.res)
-      })
-    })
-
-    describe('with a user from a restricted country', function () {
-      beforeEach(function () {
-        this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
-          false
-        )
-        this.PlansLocator.findLocalPlanInSettings.returns({})
-        this.GeoIpLookup.promises.getCurrencyCode.resolves({
-          currencyCode: this.stubbedCurrencyCode,
-          countryCode: 'KP',
-        })
-      })
-
-      it('should render the restricted country page', function (done) {
-        this.res.render = (page, opts) => {
-          page.should.equal('subscriptions/restricted-country')
-          done()
-        }
-        this.SubscriptionController.paymentPage(this.req, this.res, done)
-      })
-    })
-  })
-
   describe('successfulSubscription', function () {
     it('without a personal subscription', function (done) {
       this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel.resolves(
@@ -550,128 +400,6 @@ describe('SubscriptionController', function () {
     })
   })
 
-  describe('createSubscription', function () {
-    beforeEach(function (done) {
-      this.res = {
-        sendStatus() {
-          done()
-        },
-      }
-      sinon.spy(this.res, 'sendStatus')
-      this.subscriptionDetails = {
-        card: '1234',
-        cvv: '123',
-      }
-      this.recurlyTokenIds = {
-        billing: '1234',
-        threeDSecureActionResult: '5678',
-      }
-      this.req.body.recurly_token_id = this.recurlyTokenIds.billing
-      this.req.body.recurly_three_d_secure_action_result_token_id =
-        this.recurlyTokenIds.threeDSecureActionResult
-      this.req.body.subscriptionDetails = this.subscriptionDetails
-      this.LimitationsManager.userHasV1OrV2Subscription.yields(null, false)
-      this.SubscriptionController.createSubscription(this.req, this.res)
-    })
-
-    it('should send the user and subscriptionId to the handler', function (done) {
-      this.SubscriptionHandler.promises.createSubscription
-        .calledWithMatch(
-          this.user,
-          this.subscriptionDetails,
-          this.recurlyTokenIds
-        )
-        .should.equal(true)
-      done()
-    })
-
-    it('should redirect to the subscription page', function (done) {
-      this.res.sendStatus.calledWith(201).should.equal(true)
-      done()
-    })
-  })
-
-  describe('createSubscription with errors', function () {
-    it('should handle users with subscription', function (done) {
-      this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(true)
-      this.SubscriptionController.createSubscription(this.req, {
-        sendStatus: status => {
-          expect(status).to.equal(409)
-          this.SubscriptionHandler.promises.createSubscription.called.should.equal(
-            false
-          )
-
-          done()
-        },
-      })
-    })
-
-    it('should handle restricted country', function (done) {
-      this.GeoIpLookup.promises.getCurrencyCode.resolves({
-        countryCode: 'KP',
-      })
-      this.res.callback = () => {
-        expect(this.res.statusCode).to.equal(422)
-        expect(this.res.body).to.include(
-          'sorry_detected_sales_restricted_region'
-        )
-        this.SubscriptionHandler.promises.createSubscription.called.should.equal(
-          false
-        )
-        done()
-      }
-      this.SubscriptionController.createSubscription(this.req, this.res)
-    })
-
-    it('should handle 3DSecure errors/recurly transaction errors', function (done) {
-      this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(false)
-      this.SubscriptionHandler.promises.createSubscription.rejects(
-        new SubscriptionErrors.RecurlyTransactionError({})
-      )
-      this.HttpErrorHandler.unprocessableEntity = sinon.spy(
-        (req, res, message) => {
-          expect(req).to.exist
-          expect(res).to.exist
-          expect(message).to.deep.equal('Unknown transaction error')
-          done()
-        }
-      )
-      this.SubscriptionController.createSubscription(this.req, this.res)
-    })
-
-    it('should handle validation errors', function (done) {
-      this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(false)
-      this.SubscriptionHandler.promises.createSubscription.rejects(
-        new Errors.InvalidError('invalid error test')
-      )
-      this.HttpErrorHandler.unprocessableEntity = sinon.spy(
-        (req, res, message) => {
-          expect(req).to.exist
-          expect(res).to.exist
-          expect(message).to.deep.equal('invalid error test')
-          done()
-        }
-      )
-      this.SubscriptionController.createSubscription(this.req, this.res)
-    })
-
-    it('should throw errors from createSubscription that are not handled', function (done) {
-      const genericError = new Error('generic error')
-      this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(false)
-      this.SubscriptionHandler.promises.createSubscription.rejects(genericError)
-
-      this.SubscriptionController.createSubscription(
-        this.req,
-        this.res,
-        error => {
-          expect(error).to.be.instanceof(Error)
-          expect(error.message).to.equal(genericError.message)
-          done()
-        }
-      )
-    })
-  })
-
   describe('updateSubscription via post', function () {
     beforeEach(function (done) {
       this.res = {
@@ -1035,28 +763,4 @@ describe('SubscriptionController', function () {
       this.SubscriptionController.processUpgradeToAnnualPlan(this.req, this.res)
     })
   })
-
-  describe('requireConfirmedPrimaryEmailAddress', function () {
-    describe('when user does not have confirmed email address', function () {
-      beforeEach(function () {
-        this.req.user = { _id: 'testing' }
-        this.UserGetter.promises.getUser.resolves({
-          email: 'test@example.com',
-          emails: [{ email: 'test@example.com' }],
-        })
-      })
-
-      it('should show unconfirmed primary email page', function (done) {
-        this.res.render = (page, opts) => {
-          page.should.equal('subscriptions/unconfirmed-primary-email')
-          opts.email.should.equal('test@example.com')
-          done()
-        }
-        this.SubscriptionController.requireConfirmedPrimaryEmailAddress(
-          this.req,
-          this.res
-        )
-      })
-    })
-  })
 })