Explorar o código

Merge pull request #11646 from overleaf/ii-payment-page-payment-preview-panel

[web] Payment page payment preview panel

GitOrigin-RevId: 35f2e11a9a80e8b240dc8485b8062cf33d5b40b4
ilkin-overleaf %!s(int64=3) %!d(string=hai) anos
pai
achega
b0841592c7
Modificáronse 16 ficheiros con 406 adicións e 40 borrados
  1. 1 0
      services/web/app/src/Features/Subscription/SubscriptionController.js
  2. 2 2
      services/web/app/views/subscriptions/new-react.pug
  3. 22 0
      services/web/frontend/extracted-translations.json
  4. 7 6
      services/web/frontend/js/features/subscription/components/new/checkout/checkout-panel.tsx
  5. 38 0
      services/web/frontend/js/features/subscription/components/new/payment-preview/collaborators.tsx
  6. 40 0
      services/web/frontend/js/features/subscription/components/new/payment-preview/currency-dropdown.tsx
  7. 33 0
      services/web/frontend/js/features/subscription/components/new/payment-preview/features-list.tsx
  8. 38 0
      services/web/frontend/js/features/subscription/components/new/payment-preview/no-discount-price.tsx
  9. 39 1
      services/web/frontend/js/features/subscription/components/new/payment-preview/payment-preview-panel.tsx
  10. 49 0
      services/web/frontend/js/features/subscription/components/new/payment-preview/price-for-first-x-period.tsx
  11. 78 0
      services/web/frontend/js/features/subscription/components/new/payment-preview/price-summary.tsx
  12. 25 0
      services/web/frontend/js/features/subscription/components/new/payment-preview/trial-price.tsx
  13. 22 23
      services/web/frontend/js/features/subscription/context/payment-context.tsx
  14. 7 7
      services/web/frontend/js/features/subscription/context/types/payment-context-value.tsx
  15. 3 1
      services/web/frontend/js/features/subscription/data/currency.ts
  16. 2 0
      services/web/locales/en.json

+ 1 - 0
services/web/app/src/Features/Subscription/SubscriptionController.js

@@ -186,6 +186,7 @@ async function _paymentReactPage(req, res) {
         currency,
         countryCode,
         plan,
+        planCode: req.query.planCode,
         couponCode: req.query.cc,
         showCouponField: !!req.query.scf,
         itm_campaign: req.query.itm_campaign,

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

@@ -14,7 +14,7 @@ block append meta
 	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-currencySymbols" data-type="json" content=settings.groupPlanModalOptions.currencySymbols)
+	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)
@@ -28,7 +28,7 @@ block content
 	main.content.content-alt#subscription-new-root
 
 	script(type="text/javascript", nonce=scriptNonce).
-		ga('send', 'event', 'pageview', 'payment_form', "#{plan_code}")
+		ga('send', 'event', 'pageview', 'payment_form', "#{planCode}")
 
 	script(
 		type="text/ng-template"

+ 22 - 0
services/web/frontend/extracted-translations.json

@@ -32,6 +32,7 @@
   "additional_licenses": "",
   "address_line_1": "",
   "address_second_line_optional": "",
+  "all_premium_features_including": "",
   "all_projects": "",
   "also": "",
   "an_error_occurred_when_verifying_the_coupon_code": "",
@@ -69,6 +70,7 @@
   "can_link_your_institution_acct_2": "",
   "can_now_relink_dropbox": "",
   "cancel": "",
+  "cancel_anytime": "",
   "cancel_your_subscription": "",
   "cannot_invite_non_user": "",
   "cannot_invite_self": "",
@@ -84,6 +86,7 @@
   "category_operators": "",
   "category_relations": "",
   "change": "",
+  "change_currency": "",
   "change_or_cancel-cancel": "",
   "change_or_cancel-change": "",
   "change_or_cancel-or": "",
@@ -108,6 +111,7 @@
   "code_check_failed_explanation": "",
   "collaborate_online_and_offline": "",
   "collabs_per_proj": "",
+  "collabs_per_proj_single": "",
   "collapse": "",
   "commit": "",
   "common": "",
@@ -169,6 +173,7 @@
   "did_you_know_institution_providing_professional": "",
   "did_you_know_that_overleaf_offers": "",
   "disable_stop_on_first_error": "",
+  "discount_of": "",
   "dismiss": "",
   "dismiss_error_popup": "",
   "do_you_want_to_overwrite_them": "",
@@ -239,6 +244,7 @@
   "find_out_more_nt": "",
   "find_the_symbols_you_need_with_premium": "",
   "first_name": "",
+  "first_x_days_free_after_that_y_per_month": "",
   "fold_line": "",
   "following_paths_conflict": "",
   "font_family": "",
@@ -360,6 +366,7 @@
   "importing_and_merging_changes_in_github": "",
   "in_order_to_match_institutional_metadata_2": "",
   "in_order_to_match_institutional_metadata_associated": "",
+  "increased_compile_timeout": "",
   "institution": "",
   "institution_account": "",
   "institution_acct_successfully_linked_2": "",
@@ -488,6 +495,8 @@
   "no_search_results": "",
   "no_symbols_found": "",
   "normal": "",
+  "normally_x_price_per_month": "",
+  "normally_x_price_per_year": "",
   "notification_project_invite_accepted_message": "",
   "notification_project_invite_message": "",
   "oauth_orcid_description": "",
@@ -515,6 +524,7 @@
   "password_managed_externally": "",
   "password_was_detected_on_a_public_list_of_known_compromised_passwords": "",
   "payment_provider_unreachable_error": "",
+  "payment_summary": "",
   "pdf_compile_in_progress_error": "",
   "pdf_compile_rate_limit_hit": "",
   "pdf_compile_try_again": "",
@@ -722,7 +732,9 @@
   "sure_you_want_to_delete": "",
   "switch_to_editor": "",
   "switch_to_pdf": "",
+  "symbol_palette": "",
   "sync": "",
+  "sync_dropbox_github": "",
   "sync_project_to_github_explanation": "",
   "sync_to_dropbox": "",
   "sync_to_github": "",
@@ -741,6 +753,8 @@
   "thank_you_exclamation": "",
   "thanks_settings_updated": "",
   "the_following_files_already_exist_in_this_project": "",
+  "then_x_price_per_month": "",
+  "then_x_price_per_year": "",
   "this_action_cannot_be_undone": "",
   "this_address_will_be_shown_on_the_invoice": "",
   "this_field_is_required": "",
@@ -762,7 +776,10 @@
   "too_many_requests": "",
   "too_many_search_results": "",
   "too_recently_compiled": "",
+  "total_per_month": "",
+  "total_per_year": "",
   "total_words": "",
+  "track_changes": "",
   "trash": "",
   "trash_projects": "",
   "trashed": "",
@@ -784,6 +801,7 @@
   "unconfirmed": "",
   "unfold_line": "",
   "university": "",
+  "unlimited_collabs": "",
   "unlimited_projects": "",
   "unlink": "",
   "unlink_dropbox_folder": "",
@@ -815,6 +833,7 @@
   "user_deletion_password_reset_tip": "",
   "user_sessions": "",
   "validation_issue_entry_description": "",
+  "vat": "",
   "vat_number": "",
   "view_all": "",
   "view_logs": "",
@@ -831,6 +850,9 @@
   "word_count": "",
   "work_offline": "",
   "work_with_non_overleaf_users": "",
+  "x_price_for_first_month": "",
+  "x_price_for_first_year": "",
+  "x_price_for_y_months": "",
   "year": "",
   "you_are_a_manager_and_member_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "",
   "you_are_a_manager_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "",

+ 7 - 6
services/web/frontend/js/features/subscription/components/new/checkout/checkout-panel.tsx

@@ -34,7 +34,8 @@ function CheckoutPanel() {
   const { t } = useTranslation()
   const {
     couponError,
-    plan,
+    planCode,
+    planName,
     pricingFormState,
     pricing,
     recurlyLoadError,
@@ -150,7 +151,7 @@ function CheckoutPanel() {
           eventTracking.send(
             'subscription-funnel',
             'subscription-submission-success',
-            plan.planCode
+            planCode
           )
           window.location.assign('/user/subscription/thank-you')
         } catch (error) {
@@ -173,7 +174,7 @@ function CheckoutPanel() {
       ITMReferrer,
       isAddCompanyDetailsChecked,
       isPayPalPaymentMethod,
-      plan.planCode,
+      planCode,
       pricing,
       pricingFormState,
       t,
@@ -184,7 +185,7 @@ function CheckoutPanel() {
 
   useEffect(() => {
     payPal.current = recurly.PayPal({
-      display: { displayName: plan.name },
+      display: { displayName: planName },
     })
 
     payPal.current.on('token', token => {
@@ -202,7 +203,7 @@ function CheckoutPanel() {
     return () => {
       payPalCopy.destroy()
     }
-  }, [completeSubscription, plan.name])
+  }, [completeSubscription, planName])
 
   const handleCardChange = useCallback((state: CardElementChangeState) => {
     setCardIsValid(state.valid)
@@ -291,7 +292,7 @@ function CheckoutPanel() {
       )}
       <div className={classnames({ hidden: threeDSecureActionTokenId })}>
         <PriceSwitchHeader
-          planCode={plan.planCode}
+          planCode={planCode}
           planCodes={[
             'student-annual',
             'student-monthly',

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

@@ -0,0 +1,38 @@
+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

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

@@ -0,0 +1,40 @@
+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" />
+              </span>
+            )}
+            {currency} ({symbol})
+          </MenuItem>
+        ))}
+      </Dropdown.Menu>
+    </Dropdown>
+  )
+}
+
+export default CurrencyDropdown

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

@@ -0,0 +1,33 @@
+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">
+        {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

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

@@ -0,0 +1,38 @@
+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

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

@@ -1,5 +1,43 @@
+import { useTranslation } from 'react-i18next'
+import Collaborators from './collaborators'
+import FeaturesList from './features-list'
+import PriceSummary from './price-summary'
+import TrialPrice from './trial-price'
+import NoDiscountPrice from './no-discount-price'
+import PriceForFirstXPeriod from './price-for-first-x-period'
+import { usePaymentContext } from '../../../context/payment-context'
+
 function PaymentPreviewPanel() {
-  return <h3>Preview panel</h3>
+  const { t } = useTranslation()
+  const { plan, planName } = usePaymentContext()
+  const trialPrice = <TrialPrice />
+  const priceForFirstXPeriod = <PriceForFirstXPeriod />
+  const noDiscountPrice = <NoDiscountPrice />
+
+  return (
+    <div className="price-feature-description">
+      <h4>{planName}</h4>
+      {plan.features && (
+        <>
+          <Collaborators count={plan.features.collaborators} />
+          <FeaturesList features={plan.features} />
+        </>
+      )}
+      <PriceSummary />
+      {(trialPrice || priceForFirstXPeriod || noDiscountPrice) && (
+        <>
+          <hr className="thin" />
+          <div className="trial-coupon-summary">
+            {trialPrice}
+            {priceForFirstXPeriod}
+            {noDiscountPrice}
+          </div>
+        </>
+      )}
+      <hr className="thin" />
+      <p className="price-cancel-anytime text-center">{t('cancel_anytime')}</p>
+    </div>
+  )
 }
 
 export default PaymentPreviewPanel

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

@@ -0,0 +1,49 @@
+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

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

@@ -0,0 +1,78 @@
+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">
+        <h4>{t('payment_summary')}</h4>
+        <div className="small">
+          <div className="price-summary-line">
+            <span>{planName}</span>
+            <span>
+              {currencySymbol}
+              {subtotal}
+            </span>
+          </div>
+          {coupon && (
+            <div className="price-summary-line">
+              <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">
+              <span>
+                {t('vat')} {rate * 100}%
+              </span>
+              <span>
+                {currencySymbol}
+                {recurlyPrice.tax}
+              </span>
+            </div>
+          )}
+          <div className="price-summary-line price-summary-total-line">
+            <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

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

@@ -0,0 +1,25 @@
+import { Trans } from 'react-i18next'
+import { usePaymentContext } from '../../../context/payment-context'
+
+function TrialPrice() {
+  const { currencySymbol, trialLength, recurlyPrice } = usePaymentContext()
+
+  if (!trialLength || !recurlyPrice) {
+    return null
+  }
+
+  return (
+    <div>
+      <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

+ 22 - 23
services/web/frontend/js/features/subscription/context/payment-context.tsx

@@ -8,6 +8,7 @@ import {
   useContext,
   createContext,
 } from 'react'
+import { currencies, CurrencyCode, CurrencySymbol } from '../data/currency'
 import { useTranslation } from 'react-i18next'
 import getMeta from '../../../utils/meta'
 import * as eventTracking from '../../../infrastructure/event-tracking'
@@ -33,14 +34,12 @@ function usePayment({ publicKey }: RecurlyOptions) {
     'ol-couponCode',
     ''
   )
-  const currencySymbols: Record<
-    PaymentContextValue['currencyCode'],
-    PaymentContextValue['currencySymbol']
-  > = getMeta('ol-currencySymbols')
-  const initiallySelectedCurrencyCode: string = getMeta(
+  const initiallySelectedCurrencyCode: CurrencyCode = getMeta(
     'ol-recommendedCurrency'
   )
+  const planCode: string = getMeta('ol-planCode')
 
+  const [planName, setPlanName] = useState(plan.name)
   const [recurlyLoading, setRecurlyLoading] = useState(true)
   const [recurlyLoadError, setRecurlyLoadError] = useState(false)
   const [recurlyPrice, setRecurlyPrice] = useState<{
@@ -83,12 +82,12 @@ function usePayment({ publicKey }: RecurlyOptions) {
   const pricing = useRef<SubscriptionPricingInstanceCustom>()
 
   const limitedCurrencyCodes = Array.from(
-    new Set([initiallySelectedCurrencyCode, 'USD', 'EUR', 'GBP'])
+    new Set<CurrencyCode>([initiallySelectedCurrencyCode, 'USD', 'EUR', 'GBP'])
   )
   const limitedCurrencies = limitedCurrencyCodes.reduce((prev, cur) => {
-    return { ...prev, [cur]: currencySymbols[cur] }
-  }, {} as Record<string, string>)
-  const currencySymbol = limitedCurrencies[currencyCode]
+    return { ...prev, [cur]: currencies[cur] }
+  }, {} as Partial<typeof currencies>)
+  const currencySymbol = limitedCurrencies[currencyCode] as CurrencySymbol
 
   useLayoutEffect(() => {
     if (typeof recurly === 'undefined' || !recurly) {
@@ -96,11 +95,11 @@ function usePayment({ publicKey }: RecurlyOptions) {
       return
     }
 
-    eventTracking.sendMB('payment-page-view', { plan: plan.planCode })
+    eventTracking.sendMB('payment-page-view', { plan: planCode })
     eventTracking.send(
       'subscription-funnel',
       'subscription-form-viewed',
-      plan.planCode
+      planCode
     )
 
     recurly.configure({ publicKey })
@@ -111,7 +110,7 @@ function usePayment({ publicKey }: RecurlyOptions) {
       setRecurlyLoading(true)
 
       pricing.current
-        ?.plan(plan.planCode, { quantity: 1 })
+        ?.plan(planCode, { quantity: 1 })
         .address({
           first_name: '',
           last_name: '',
@@ -146,7 +145,7 @@ function usePayment({ publicKey }: RecurlyOptions) {
     initialCountry,
     initialCouponCode,
     initiallySelectedCurrencyCode,
-    plan.planCode,
+    planCode,
     publicKey,
     t,
   ])
@@ -155,6 +154,11 @@ function usePayment({ publicKey }: RecurlyOptions) {
     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)
 
@@ -168,14 +172,6 @@ function usePayment({ publicKey }: RecurlyOptions) {
 
       setTaxes(pricing.current.price.taxes)
 
-      // TODO availableCurrencies for preview section (limitedCurrencies is implemented)
-      // for (const currencyCode in pricing.items.plan.price) {
-      //   if (MultiCurrencyPricing.plans[currencyCode]) {
-      //     $scope.availableCurrencies[currencyCode] =
-      //       MultiCurrencyPricing.plans[currencyCode]
-      //   }
-      // }
-
       const couponData = (() => {
         if (pricing.current.items.coupon?.discount.type === 'percent') {
           const coupon = pricing.current.items.coupon
@@ -262,9 +258,8 @@ function usePayment({ publicKey }: RecurlyOptions) {
     []
   )
 
-  // TODO - for preview panel
   const changeCurrency = useCallback(
-    (newCurrency: string) => {
+    (newCurrency: CurrencyCode) => {
       setRecurlyLoading(true)
       setCurrencyCode(newCurrency)
 
@@ -293,6 +288,8 @@ function usePayment({ publicKey }: RecurlyOptions) {
       pricingFormState,
       setPricingFormState,
       plan,
+      planCode,
+      planName,
       pricing,
       recurlyLoading,
       recurlyLoadError,
@@ -315,6 +312,8 @@ function usePayment({ publicKey }: RecurlyOptions) {
       pricingFormState,
       setPricingFormState,
       plan,
+      planCode,
+      planName,
       pricing,
       recurlyLoading,
       recurlyLoadError,

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

@@ -2,6 +2,7 @@ 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
@@ -18,20 +19,19 @@ export type PricingFormState = {
 }
 
 export type PaymentContextValue = {
-  currencyCode: string
+  currencyCode: CurrencyCode
   setCurrencyCode: React.Dispatch<
     React.SetStateAction<PaymentContextValue['currencyCode']>
   >
-  currencySymbol: string
-  limitedCurrencies: Record<
-    PaymentContextValue['currencyCode'],
-    PaymentContextValue['currencySymbol']
-  >
+  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
@@ -62,6 +62,6 @@ export type PaymentContextValue = {
   trialLength: number | undefined
   applyVatNumber: (vatNumber: PricingFormState['vat_number']) => void
   addCoupon: (coupon: PricingFormState['coupon']) => void
-  changeCurrency: (newCurrency: string) => void
+  changeCurrency: (newCurrency: CurrencyCode) => void
   updateCountry: (country: PricingFormState['country']) => void
 }

+ 3 - 1
services/web/frontend/js/features/subscription/data/currency.ts

@@ -12,4 +12,6 @@ export const currencies = <const>{
   SGD: '$',
 }
 
-export type CurrencyCode = keyof typeof currencies
+type Currency = typeof currencies
+export type CurrencyCode = keyof Currency
+export type CurrencySymbol = Currency[CurrencyCode]

+ 2 - 0
services/web/locales/en.json

@@ -188,6 +188,7 @@
   "category_operators": "Operators",
   "category_relations": "Relations",
   "change": "Change",
+  "change_currency": "Change currency",
   "change_or_cancel-cancel": "cancel",
   "change_or_cancel-change": "Change",
   "change_or_cancel-or": "or",
@@ -334,6 +335,7 @@
   "direct_link": "Direct Link",
   "disable_stop_on_first_error": "Disable “Stop on first error”",
   "disconnected": "Disconnected",
+  "discount_of": "Discount of __amount__",
   "discounted_group_accounts": "discounted group accounts",
   "dismiss": "Dismiss",
   "dismiss_error_popup": "Dismiss first error alert",