mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-28 21:13:20 -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,
|
currency,
|
||||||
countryCode,
|
countryCode,
|
||||||
plan,
|
plan,
|
||||||
|
planCode: req.query.planCode,
|
||||||
couponCode: req.query.cc,
|
couponCode: req.query.cc,
|
||||||
showCouponField: !!req.query.scf,
|
showCouponField: !!req.query.scf,
|
||||||
itm_campaign: req.query.itm_campaign,
|
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-recurlyApiKey" content=settings.apis.recurly.publicKey)
|
||||||
meta(name="ol-recommendedCurrency" content=String(currency).slice(0, 3))
|
meta(name="ol-recommendedCurrency" content=String(currency).slice(0, 3))
|
||||||
meta(name="ol-plan" data-type="json" content=plan)
|
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-showCouponField" data-type="boolean" content=showCouponField)
|
||||||
meta(name="ol-couponCode" content=couponCode)
|
meta(name="ol-couponCode" content=couponCode)
|
||||||
meta(name="ol-itm_campaign" content=itm_campaign)
|
meta(name="ol-itm_campaign" content=itm_campaign)
|
||||||
|
@ -28,7 +28,7 @@ block content
|
||||||
main.content.content-alt#subscription-new-root
|
main.content.content-alt#subscription-new-root
|
||||||
|
|
||||||
script(type="text/javascript", nonce=scriptNonce).
|
script(type="text/javascript", nonce=scriptNonce).
|
||||||
ga('send', 'event', 'pageview', 'payment_form', "#{plan_code}")
|
ga('send', 'event', 'pageview', 'payment_form', "#{planCode}")
|
||||||
|
|
||||||
script(
|
script(
|
||||||
type="text/ng-template"
|
type="text/ng-template"
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"additional_licenses": "",
|
"additional_licenses": "",
|
||||||
"address_line_1": "",
|
"address_line_1": "",
|
||||||
"address_second_line_optional": "",
|
"address_second_line_optional": "",
|
||||||
|
"all_premium_features_including": "",
|
||||||
"all_projects": "",
|
"all_projects": "",
|
||||||
"also": "",
|
"also": "",
|
||||||
"an_error_occurred_when_verifying_the_coupon_code": "",
|
"an_error_occurred_when_verifying_the_coupon_code": "",
|
||||||
|
@ -69,6 +70,7 @@
|
||||||
"can_link_your_institution_acct_2": "",
|
"can_link_your_institution_acct_2": "",
|
||||||
"can_now_relink_dropbox": "",
|
"can_now_relink_dropbox": "",
|
||||||
"cancel": "",
|
"cancel": "",
|
||||||
|
"cancel_anytime": "",
|
||||||
"cancel_your_subscription": "",
|
"cancel_your_subscription": "",
|
||||||
"cannot_invite_non_user": "",
|
"cannot_invite_non_user": "",
|
||||||
"cannot_invite_self": "",
|
"cannot_invite_self": "",
|
||||||
|
@ -84,6 +86,7 @@
|
||||||
"category_operators": "",
|
"category_operators": "",
|
||||||
"category_relations": "",
|
"category_relations": "",
|
||||||
"change": "",
|
"change": "",
|
||||||
|
"change_currency": "",
|
||||||
"change_or_cancel-cancel": "",
|
"change_or_cancel-cancel": "",
|
||||||
"change_or_cancel-change": "",
|
"change_or_cancel-change": "",
|
||||||
"change_or_cancel-or": "",
|
"change_or_cancel-or": "",
|
||||||
|
@ -108,6 +111,7 @@
|
||||||
"code_check_failed_explanation": "",
|
"code_check_failed_explanation": "",
|
||||||
"collaborate_online_and_offline": "",
|
"collaborate_online_and_offline": "",
|
||||||
"collabs_per_proj": "",
|
"collabs_per_proj": "",
|
||||||
|
"collabs_per_proj_single": "",
|
||||||
"collapse": "",
|
"collapse": "",
|
||||||
"commit": "",
|
"commit": "",
|
||||||
"common": "",
|
"common": "",
|
||||||
|
@ -169,6 +173,7 @@
|
||||||
"did_you_know_institution_providing_professional": "",
|
"did_you_know_institution_providing_professional": "",
|
||||||
"did_you_know_that_overleaf_offers": "",
|
"did_you_know_that_overleaf_offers": "",
|
||||||
"disable_stop_on_first_error": "",
|
"disable_stop_on_first_error": "",
|
||||||
|
"discount_of": "",
|
||||||
"dismiss": "",
|
"dismiss": "",
|
||||||
"dismiss_error_popup": "",
|
"dismiss_error_popup": "",
|
||||||
"do_you_want_to_overwrite_them": "",
|
"do_you_want_to_overwrite_them": "",
|
||||||
|
@ -239,6 +244,7 @@
|
||||||
"find_out_more_nt": "",
|
"find_out_more_nt": "",
|
||||||
"find_the_symbols_you_need_with_premium": "",
|
"find_the_symbols_you_need_with_premium": "",
|
||||||
"first_name": "",
|
"first_name": "",
|
||||||
|
"first_x_days_free_after_that_y_per_month": "",
|
||||||
"fold_line": "",
|
"fold_line": "",
|
||||||
"following_paths_conflict": "",
|
"following_paths_conflict": "",
|
||||||
"font_family": "",
|
"font_family": "",
|
||||||
|
@ -360,6 +366,7 @@
|
||||||
"importing_and_merging_changes_in_github": "",
|
"importing_and_merging_changes_in_github": "",
|
||||||
"in_order_to_match_institutional_metadata_2": "",
|
"in_order_to_match_institutional_metadata_2": "",
|
||||||
"in_order_to_match_institutional_metadata_associated": "",
|
"in_order_to_match_institutional_metadata_associated": "",
|
||||||
|
"increased_compile_timeout": "",
|
||||||
"institution": "",
|
"institution": "",
|
||||||
"institution_account": "",
|
"institution_account": "",
|
||||||
"institution_acct_successfully_linked_2": "",
|
"institution_acct_successfully_linked_2": "",
|
||||||
|
@ -488,6 +495,8 @@
|
||||||
"no_search_results": "",
|
"no_search_results": "",
|
||||||
"no_symbols_found": "",
|
"no_symbols_found": "",
|
||||||
"normal": "",
|
"normal": "",
|
||||||
|
"normally_x_price_per_month": "",
|
||||||
|
"normally_x_price_per_year": "",
|
||||||
"notification_project_invite_accepted_message": "",
|
"notification_project_invite_accepted_message": "",
|
||||||
"notification_project_invite_message": "",
|
"notification_project_invite_message": "",
|
||||||
"oauth_orcid_description": "",
|
"oauth_orcid_description": "",
|
||||||
|
@ -515,6 +524,7 @@
|
||||||
"password_managed_externally": "",
|
"password_managed_externally": "",
|
||||||
"password_was_detected_on_a_public_list_of_known_compromised_passwords": "",
|
"password_was_detected_on_a_public_list_of_known_compromised_passwords": "",
|
||||||
"payment_provider_unreachable_error": "",
|
"payment_provider_unreachable_error": "",
|
||||||
|
"payment_summary": "",
|
||||||
"pdf_compile_in_progress_error": "",
|
"pdf_compile_in_progress_error": "",
|
||||||
"pdf_compile_rate_limit_hit": "",
|
"pdf_compile_rate_limit_hit": "",
|
||||||
"pdf_compile_try_again": "",
|
"pdf_compile_try_again": "",
|
||||||
|
@ -722,7 +732,9 @@
|
||||||
"sure_you_want_to_delete": "",
|
"sure_you_want_to_delete": "",
|
||||||
"switch_to_editor": "",
|
"switch_to_editor": "",
|
||||||
"switch_to_pdf": "",
|
"switch_to_pdf": "",
|
||||||
|
"symbol_palette": "",
|
||||||
"sync": "",
|
"sync": "",
|
||||||
|
"sync_dropbox_github": "",
|
||||||
"sync_project_to_github_explanation": "",
|
"sync_project_to_github_explanation": "",
|
||||||
"sync_to_dropbox": "",
|
"sync_to_dropbox": "",
|
||||||
"sync_to_github": "",
|
"sync_to_github": "",
|
||||||
|
@ -741,6 +753,8 @@
|
||||||
"thank_you_exclamation": "",
|
"thank_you_exclamation": "",
|
||||||
"thanks_settings_updated": "",
|
"thanks_settings_updated": "",
|
||||||
"the_following_files_already_exist_in_this_project": "",
|
"the_following_files_already_exist_in_this_project": "",
|
||||||
|
"then_x_price_per_month": "",
|
||||||
|
"then_x_price_per_year": "",
|
||||||
"this_action_cannot_be_undone": "",
|
"this_action_cannot_be_undone": "",
|
||||||
"this_address_will_be_shown_on_the_invoice": "",
|
"this_address_will_be_shown_on_the_invoice": "",
|
||||||
"this_field_is_required": "",
|
"this_field_is_required": "",
|
||||||
|
@ -762,7 +776,10 @@
|
||||||
"too_many_requests": "",
|
"too_many_requests": "",
|
||||||
"too_many_search_results": "",
|
"too_many_search_results": "",
|
||||||
"too_recently_compiled": "",
|
"too_recently_compiled": "",
|
||||||
|
"total_per_month": "",
|
||||||
|
"total_per_year": "",
|
||||||
"total_words": "",
|
"total_words": "",
|
||||||
|
"track_changes": "",
|
||||||
"trash": "",
|
"trash": "",
|
||||||
"trash_projects": "",
|
"trash_projects": "",
|
||||||
"trashed": "",
|
"trashed": "",
|
||||||
|
@ -784,6 +801,7 @@
|
||||||
"unconfirmed": "",
|
"unconfirmed": "",
|
||||||
"unfold_line": "",
|
"unfold_line": "",
|
||||||
"university": "",
|
"university": "",
|
||||||
|
"unlimited_collabs": "",
|
||||||
"unlimited_projects": "",
|
"unlimited_projects": "",
|
||||||
"unlink": "",
|
"unlink": "",
|
||||||
"unlink_dropbox_folder": "",
|
"unlink_dropbox_folder": "",
|
||||||
|
@ -815,6 +833,7 @@
|
||||||
"user_deletion_password_reset_tip": "",
|
"user_deletion_password_reset_tip": "",
|
||||||
"user_sessions": "",
|
"user_sessions": "",
|
||||||
"validation_issue_entry_description": "",
|
"validation_issue_entry_description": "",
|
||||||
|
"vat": "",
|
||||||
"vat_number": "",
|
"vat_number": "",
|
||||||
"view_all": "",
|
"view_all": "",
|
||||||
"view_logs": "",
|
"view_logs": "",
|
||||||
|
@ -831,6 +850,9 @@
|
||||||
"word_count": "",
|
"word_count": "",
|
||||||
"work_offline": "",
|
"work_offline": "",
|
||||||
"work_with_non_overleaf_users": "",
|
"work_with_non_overleaf_users": "",
|
||||||
|
"x_price_for_first_month": "",
|
||||||
|
"x_price_for_first_year": "",
|
||||||
|
"x_price_for_y_months": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"you_are_a_manager_and_member_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "",
|
"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": "",
|
"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 { t } = useTranslation()
|
||||||
const {
|
const {
|
||||||
couponError,
|
couponError,
|
||||||
plan,
|
planCode,
|
||||||
|
planName,
|
||||||
pricingFormState,
|
pricingFormState,
|
||||||
pricing,
|
pricing,
|
||||||
recurlyLoadError,
|
recurlyLoadError,
|
||||||
|
@ -150,7 +151,7 @@ function CheckoutPanel() {
|
||||||
eventTracking.send(
|
eventTracking.send(
|
||||||
'subscription-funnel',
|
'subscription-funnel',
|
||||||
'subscription-submission-success',
|
'subscription-submission-success',
|
||||||
plan.planCode
|
planCode
|
||||||
)
|
)
|
||||||
window.location.assign('/user/subscription/thank-you')
|
window.location.assign('/user/subscription/thank-you')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -173,7 +174,7 @@ function CheckoutPanel() {
|
||||||
ITMReferrer,
|
ITMReferrer,
|
||||||
isAddCompanyDetailsChecked,
|
isAddCompanyDetailsChecked,
|
||||||
isPayPalPaymentMethod,
|
isPayPalPaymentMethod,
|
||||||
plan.planCode,
|
planCode,
|
||||||
pricing,
|
pricing,
|
||||||
pricingFormState,
|
pricingFormState,
|
||||||
t,
|
t,
|
||||||
|
@ -184,7 +185,7 @@ function CheckoutPanel() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
payPal.current = recurly.PayPal({
|
payPal.current = recurly.PayPal({
|
||||||
display: { displayName: plan.name },
|
display: { displayName: planName },
|
||||||
})
|
})
|
||||||
|
|
||||||
payPal.current.on('token', token => {
|
payPal.current.on('token', token => {
|
||||||
|
@ -202,7 +203,7 @@ function CheckoutPanel() {
|
||||||
return () => {
|
return () => {
|
||||||
payPalCopy.destroy()
|
payPalCopy.destroy()
|
||||||
}
|
}
|
||||||
}, [completeSubscription, plan.name])
|
}, [completeSubscription, planName])
|
||||||
|
|
||||||
const handleCardChange = useCallback((state: CardElementChangeState) => {
|
const handleCardChange = useCallback((state: CardElementChangeState) => {
|
||||||
setCardIsValid(state.valid)
|
setCardIsValid(state.valid)
|
||||||
|
@ -291,7 +292,7 @@ function CheckoutPanel() {
|
||||||
)}
|
)}
|
||||||
<div className={classnames({ hidden: threeDSecureActionTokenId })}>
|
<div className={classnames({ hidden: threeDSecureActionTokenId })}>
|
||||||
<PriceSwitchHeader
|
<PriceSwitchHeader
|
||||||
planCode={plan.planCode}
|
planCode={planCode}
|
||||||
planCodes={[
|
planCodes={[
|
||||||
'student-annual',
|
'student-annual',
|
||||||
'student-monthly',
|
'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() {
|
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
|
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,
|
useContext,
|
||||||
createContext,
|
createContext,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
import { currencies, CurrencyCode, CurrencySymbol } from '../data/currency'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import getMeta from '../../../utils/meta'
|
import getMeta from '../../../utils/meta'
|
||||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||||
|
@ -33,14 +34,12 @@ function usePayment({ publicKey }: RecurlyOptions) {
|
||||||
'ol-couponCode',
|
'ol-couponCode',
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
const currencySymbols: Record<
|
const initiallySelectedCurrencyCode: CurrencyCode = getMeta(
|
||||||
PaymentContextValue['currencyCode'],
|
|
||||||
PaymentContextValue['currencySymbol']
|
|
||||||
> = getMeta('ol-currencySymbols')
|
|
||||||
const initiallySelectedCurrencyCode: string = getMeta(
|
|
||||||
'ol-recommendedCurrency'
|
'ol-recommendedCurrency'
|
||||||
)
|
)
|
||||||
|
const planCode: string = getMeta('ol-planCode')
|
||||||
|
|
||||||
|
const [planName, setPlanName] = useState(plan.name)
|
||||||
const [recurlyLoading, setRecurlyLoading] = useState(true)
|
const [recurlyLoading, setRecurlyLoading] = useState(true)
|
||||||
const [recurlyLoadError, setRecurlyLoadError] = useState(false)
|
const [recurlyLoadError, setRecurlyLoadError] = useState(false)
|
||||||
const [recurlyPrice, setRecurlyPrice] = useState<{
|
const [recurlyPrice, setRecurlyPrice] = useState<{
|
||||||
|
@ -83,12 +82,12 @@ function usePayment({ publicKey }: RecurlyOptions) {
|
||||||
const pricing = useRef<SubscriptionPricingInstanceCustom>()
|
const pricing = useRef<SubscriptionPricingInstanceCustom>()
|
||||||
|
|
||||||
const limitedCurrencyCodes = Array.from(
|
const limitedCurrencyCodes = Array.from(
|
||||||
new Set([initiallySelectedCurrencyCode, 'USD', 'EUR', 'GBP'])
|
new Set<CurrencyCode>([initiallySelectedCurrencyCode, 'USD', 'EUR', 'GBP'])
|
||||||
)
|
)
|
||||||
const limitedCurrencies = limitedCurrencyCodes.reduce((prev, cur) => {
|
const limitedCurrencies = limitedCurrencyCodes.reduce((prev, cur) => {
|
||||||
return { ...prev, [cur]: currencySymbols[cur] }
|
return { ...prev, [cur]: currencies[cur] }
|
||||||
}, {} as Record<string, string>)
|
}, {} as Partial<typeof currencies>)
|
||||||
const currencySymbol = limitedCurrencies[currencyCode]
|
const currencySymbol = limitedCurrencies[currencyCode] as CurrencySymbol
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (typeof recurly === 'undefined' || !recurly) {
|
if (typeof recurly === 'undefined' || !recurly) {
|
||||||
|
@ -96,11 +95,11 @@ function usePayment({ publicKey }: RecurlyOptions) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
eventTracking.sendMB('payment-page-view', { plan: plan.planCode })
|
eventTracking.sendMB('payment-page-view', { plan: planCode })
|
||||||
eventTracking.send(
|
eventTracking.send(
|
||||||
'subscription-funnel',
|
'subscription-funnel',
|
||||||
'subscription-form-viewed',
|
'subscription-form-viewed',
|
||||||
plan.planCode
|
planCode
|
||||||
)
|
)
|
||||||
|
|
||||||
recurly.configure({ publicKey })
|
recurly.configure({ publicKey })
|
||||||
|
@ -111,7 +110,7 @@ function usePayment({ publicKey }: RecurlyOptions) {
|
||||||
setRecurlyLoading(true)
|
setRecurlyLoading(true)
|
||||||
|
|
||||||
pricing.current
|
pricing.current
|
||||||
?.plan(plan.planCode, { quantity: 1 })
|
?.plan(planCode, { quantity: 1 })
|
||||||
.address({
|
.address({
|
||||||
first_name: '',
|
first_name: '',
|
||||||
last_name: '',
|
last_name: '',
|
||||||
|
@ -146,7 +145,7 @@ function usePayment({ publicKey }: RecurlyOptions) {
|
||||||
initialCountry,
|
initialCountry,
|
||||||
initialCouponCode,
|
initialCouponCode,
|
||||||
initiallySelectedCurrencyCode,
|
initiallySelectedCurrencyCode,
|
||||||
plan.planCode,
|
planCode,
|
||||||
publicKey,
|
publicKey,
|
||||||
t,
|
t,
|
||||||
])
|
])
|
||||||
|
@ -155,6 +154,11 @@ function usePayment({ publicKey }: RecurlyOptions) {
|
||||||
pricing.current?.on('change', function () {
|
pricing.current?.on('change', function () {
|
||||||
if (!pricing.current) return
|
if (!pricing.current) return
|
||||||
|
|
||||||
|
const planName = pricing.current.items.plan?.name
|
||||||
|
if (planName) {
|
||||||
|
setPlanName(planName)
|
||||||
|
}
|
||||||
|
|
||||||
const trialLength = pricing.current.items.plan?.trial?.length
|
const trialLength = pricing.current.items.plan?.trial?.length
|
||||||
setTrialLength(trialLength)
|
setTrialLength(trialLength)
|
||||||
|
|
||||||
|
@ -168,14 +172,6 @@ function usePayment({ publicKey }: RecurlyOptions) {
|
||||||
|
|
||||||
setTaxes(pricing.current.price.taxes)
|
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 = (() => {
|
const couponData = (() => {
|
||||||
if (pricing.current.items.coupon?.discount.type === 'percent') {
|
if (pricing.current.items.coupon?.discount.type === 'percent') {
|
||||||
const coupon = pricing.current.items.coupon
|
const coupon = pricing.current.items.coupon
|
||||||
|
@ -262,9 +258,8 @@ function usePayment({ publicKey }: RecurlyOptions) {
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO - for preview panel
|
|
||||||
const changeCurrency = useCallback(
|
const changeCurrency = useCallback(
|
||||||
(newCurrency: string) => {
|
(newCurrency: CurrencyCode) => {
|
||||||
setRecurlyLoading(true)
|
setRecurlyLoading(true)
|
||||||
setCurrencyCode(newCurrency)
|
setCurrencyCode(newCurrency)
|
||||||
|
|
||||||
|
@ -293,6 +288,8 @@ function usePayment({ publicKey }: RecurlyOptions) {
|
||||||
pricingFormState,
|
pricingFormState,
|
||||||
setPricingFormState,
|
setPricingFormState,
|
||||||
plan,
|
plan,
|
||||||
|
planCode,
|
||||||
|
planName,
|
||||||
pricing,
|
pricing,
|
||||||
recurlyLoading,
|
recurlyLoading,
|
||||||
recurlyLoadError,
|
recurlyLoadError,
|
||||||
|
@ -315,6 +312,8 @@ function usePayment({ publicKey }: RecurlyOptions) {
|
||||||
pricingFormState,
|
pricingFormState,
|
||||||
setPricingFormState,
|
setPricingFormState,
|
||||||
plan,
|
plan,
|
||||||
|
planCode,
|
||||||
|
planName,
|
||||||
pricing,
|
pricing,
|
||||||
recurlyLoading,
|
recurlyLoading,
|
||||||
recurlyLoadError,
|
recurlyLoadError,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import countries from '../../data/countries'
|
||||||
import { Plan } from '../../../../../../types/subscription/plan'
|
import { Plan } from '../../../../../../types/subscription/plan'
|
||||||
import { SubscriptionPricingStateTax } from 'recurly__recurly-js'
|
import { SubscriptionPricingStateTax } from 'recurly__recurly-js'
|
||||||
import { SubscriptionPricingInstanceCustom } from '../../../../../../types/recurly/pricing/subscription'
|
import { SubscriptionPricingInstanceCustom } from '../../../../../../types/recurly/pricing/subscription'
|
||||||
|
import { currencies, CurrencyCode, CurrencySymbol } from '../../data/currency'
|
||||||
|
|
||||||
export type PricingFormState = {
|
export type PricingFormState = {
|
||||||
first_name: string
|
first_name: string
|
||||||
|
@ -18,20 +19,19 @@ export type PricingFormState = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PaymentContextValue = {
|
export type PaymentContextValue = {
|
||||||
currencyCode: string
|
currencyCode: CurrencyCode
|
||||||
setCurrencyCode: React.Dispatch<
|
setCurrencyCode: React.Dispatch<
|
||||||
React.SetStateAction<PaymentContextValue['currencyCode']>
|
React.SetStateAction<PaymentContextValue['currencyCode']>
|
||||||
>
|
>
|
||||||
currencySymbol: string
|
currencySymbol: CurrencySymbol
|
||||||
limitedCurrencies: Record<
|
limitedCurrencies: Partial<typeof currencies>
|
||||||
PaymentContextValue['currencyCode'],
|
|
||||||
PaymentContextValue['currencySymbol']
|
|
||||||
>
|
|
||||||
pricingFormState: PricingFormState
|
pricingFormState: PricingFormState
|
||||||
setPricingFormState: React.Dispatch<
|
setPricingFormState: React.Dispatch<
|
||||||
React.SetStateAction<PaymentContextValue['pricingFormState']>
|
React.SetStateAction<PaymentContextValue['pricingFormState']>
|
||||||
>
|
>
|
||||||
plan: Plan
|
plan: Plan
|
||||||
|
planCode: string
|
||||||
|
planName: string
|
||||||
pricing: React.MutableRefObject<SubscriptionPricingInstanceCustom | undefined>
|
pricing: React.MutableRefObject<SubscriptionPricingInstanceCustom | undefined>
|
||||||
recurlyLoading: boolean
|
recurlyLoading: boolean
|
||||||
recurlyLoadError: boolean
|
recurlyLoadError: boolean
|
||||||
|
@ -62,6 +62,6 @@ export type PaymentContextValue = {
|
||||||
trialLength: number | undefined
|
trialLength: number | undefined
|
||||||
applyVatNumber: (vatNumber: PricingFormState['vat_number']) => void
|
applyVatNumber: (vatNumber: PricingFormState['vat_number']) => void
|
||||||
addCoupon: (coupon: PricingFormState['coupon']) => void
|
addCoupon: (coupon: PricingFormState['coupon']) => void
|
||||||
changeCurrency: (newCurrency: string) => void
|
changeCurrency: (newCurrency: CurrencyCode) => void
|
||||||
updateCountry: (country: PricingFormState['country']) => void
|
updateCountry: (country: PricingFormState['country']) => void
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,4 +12,6 @@ export const currencies = <const>{
|
||||||
SGD: '$',
|
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_operators": "Operators",
|
||||||
"category_relations": "Relations",
|
"category_relations": "Relations",
|
||||||
"change": "Change",
|
"change": "Change",
|
||||||
|
"change_currency": "Change currency",
|
||||||
"change_or_cancel-cancel": "cancel",
|
"change_or_cancel-cancel": "cancel",
|
||||||
"change_or_cancel-change": "Change",
|
"change_or_cancel-change": "Change",
|
||||||
"change_or_cancel-or": "or",
|
"change_or_cancel-or": "or",
|
||||||
|
@ -334,6 +335,7 @@
|
||||||
"direct_link": "Direct Link",
|
"direct_link": "Direct Link",
|
||||||
"disable_stop_on_first_error": "Disable “Stop on first error”",
|
"disable_stop_on_first_error": "Disable “Stop on first error”",
|
||||||
"disconnected": "Disconnected",
|
"disconnected": "Disconnected",
|
||||||
|
"discount_of": "Discount of __amount__",
|
||||||
"discounted_group_accounts": "discounted group accounts",
|
"discounted_group_accounts": "discounted group accounts",
|
||||||
"dismiss": "Dismiss",
|
"dismiss": "Dismiss",
|
||||||
"dismiss_error_popup": "Dismiss first error alert",
|
"dismiss_error_popup": "Dismiss first error alert",
|
||||||
|
|
Loading…
Reference in a new issue