mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #11646 from overleaf/ii-payment-page-payment-preview-panel
[web] Payment page payment preview panel GitOrigin-RevId: 35f2e11a9a80e8b240dc8485b8062cf33d5b40b4
This commit is contained in:
parent
a010822246
commit
b0841592c7
16 changed files with 406 additions and 40 deletions
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
–{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
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue