mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #11525 from overleaf/ii-payment-page-migration-init
[web] Payment page migration initialisation GitOrigin-RevId: f33b73a13b96fad3cddf0c0205d05df678ce5300
This commit is contained in:
parent
1d04aa0315
commit
db54475bd0
20 changed files with 919 additions and 47 deletions
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -8548,6 +8548,12 @@
|
|||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz",
|
||||
"integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA=="
|
||||
},
|
||||
"node_modules/@types/recurly__recurly-js": {
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/recurly__recurly-js/-/recurly__recurly-js-4.22.0.tgz",
|
||||
"integrity": "sha512-2dJ1QnwcyCmxeIAzOaBx/r1JqMIqZ7rohxJMY0UynSQidEDfb9X2x3OHMthBXDtTzSFJ1usY934wakxgm7d+Wg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/redis": {
|
||||
"version": "2.8.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.31.tgz",
|
||||
|
@ -34591,6 +34597,7 @@
|
|||
"@types/react": "^17.0.40",
|
||||
"@types/react-bootstrap": "^0.32.29",
|
||||
"@types/react-dom": "^17.0.13",
|
||||
"@types/recurly__recurly-js": "^4.22.0",
|
||||
"@types/sinon-chai": "^3.2.8",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.12.1",
|
||||
|
@ -43599,6 +43606,7 @@
|
|||
"@types/react": "^17.0.40",
|
||||
"@types/react-bootstrap": "^0.32.29",
|
||||
"@types/react-dom": "^17.0.13",
|
||||
"@types/recurly__recurly-js": "*",
|
||||
"@types/sinon-chai": "^3.2.8",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.12.1",
|
||||
|
@ -46607,6 +46615,12 @@
|
|||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/recurly__recurly-js": {
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/recurly__recurly-js/-/recurly__recurly-js-4.22.0.tgz",
|
||||
"integrity": "sha512-2dJ1QnwcyCmxeIAzOaBx/r1JqMIqZ7rohxJMY0UynSQidEDfb9X2x3OHMthBXDtTzSFJ1usY934wakxgm7d+Wg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/redis": {
|
||||
"version": "2.8.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.31.tgz",
|
||||
|
|
|
@ -175,38 +175,22 @@ async function _paymentReactPage(req, res) {
|
|||
currency = recommendedCurrency
|
||||
}
|
||||
|
||||
const refreshedPaymentPageAssignment =
|
||||
await SplitTestHandler.promises.getAssignment(
|
||||
req,
|
||||
res,
|
||||
'payment-page-refresh'
|
||||
)
|
||||
const useRefreshedPaymentPage =
|
||||
refreshedPaymentPageAssignment &&
|
||||
refreshedPaymentPageAssignment.variant === 'refreshed-payment-page'
|
||||
|
||||
await SplitTestHandler.promises.getAssignment(
|
||||
req,
|
||||
res,
|
||||
'student-check-modal'
|
||||
)
|
||||
|
||||
// TODO
|
||||
const template = useRefreshedPaymentPage
|
||||
? 'subscriptions/new-react'
|
||||
: 'subscriptions/new-react'
|
||||
|
||||
res.render(template, {
|
||||
res.render('subscriptions/new-react', {
|
||||
title: 'subscribe',
|
||||
currency,
|
||||
countryCode,
|
||||
plan,
|
||||
recurlyConfig: JSON.stringify({
|
||||
currency,
|
||||
subdomain: Settings.apis.recurly.subdomain,
|
||||
}),
|
||||
couponCode: req.query.cc,
|
||||
showCouponField: !!req.query.scf,
|
||||
showVatField: !!req.query.svf,
|
||||
itm_campaign: req.query.itm_campaign,
|
||||
itm_content: req.query.itm_content,
|
||||
itm_referrer: req.query.itm_referrer,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,10 +5,36 @@ include ./_new_mixins
|
|||
block entrypointVar
|
||||
- entrypoint = 'pages/user/subscription/new'
|
||||
|
||||
block vars
|
||||
- var suppressNavbarRight = true
|
||||
- var suppressFooter = 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-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-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', "#{plan_code}")
|
||||
|
||||
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'])}
|
||||
|
||||
+studentCheckModal
|
||||
|
|
|
@ -15,7 +15,9 @@
|
|||
"acct_linked_to_institution_acct_2": "",
|
||||
"actions": "",
|
||||
"add_affiliation": "",
|
||||
"add_another_address_line": "",
|
||||
"add_another_email": "",
|
||||
"add_company_details": "",
|
||||
"add_email_to_claim_features": "",
|
||||
"add_files": "",
|
||||
"add_new_email": "",
|
||||
|
@ -23,8 +25,11 @@
|
|||
"add_role_and_department": "",
|
||||
"add_to_folder": "",
|
||||
"additional_licenses": "",
|
||||
"address_line_1": "",
|
||||
"address_second_line_optional": "",
|
||||
"all_projects": "",
|
||||
"also": "",
|
||||
"an_error_occurred_when_verifying_the_coupon_code": "",
|
||||
"anyone_with_link_can_edit": "",
|
||||
"anyone_with_link_can_view": "",
|
||||
"approaching_compile_timeout_limit_upgrade_for_more_compile_time": "",
|
||||
|
@ -53,16 +58,22 @@
|
|||
"blank_project": "",
|
||||
"blocked_filename": "",
|
||||
"browser": "",
|
||||
"by_subscribing_you_agree_to_our_terms_of_service": "",
|
||||
"can_edit": "",
|
||||
"can_link_institution_email_acct_to_institution_acct": "",
|
||||
"can_link_your_institution_acct_2": "",
|
||||
"can_now_relink_dropbox": "",
|
||||
"cancel": "",
|
||||
"cancel_anytime": "",
|
||||
"cancel_your_subscription": "",
|
||||
"cannot_invite_non_user": "",
|
||||
"cannot_invite_self": "",
|
||||
"cannot_verify_user_not_robot": "",
|
||||
"cant_see_what_youre_looking_for_question": "",
|
||||
"card_details": "",
|
||||
"card_details_are_not_valid": "",
|
||||
"card_must_be_authenticated_by_3dsecure": "",
|
||||
"card_payment": "",
|
||||
"category_arrows": "",
|
||||
"category_greek": "",
|
||||
"category_misc": "",
|
||||
|
@ -97,6 +108,7 @@
|
|||
"common": "",
|
||||
"commons_plan_tooltip": "",
|
||||
"compact": "",
|
||||
"company_name": "",
|
||||
"compile_error_entry_description": "",
|
||||
"compile_error_handling": "",
|
||||
"compile_larger_projects": "",
|
||||
|
@ -119,6 +131,8 @@
|
|||
"copying": "",
|
||||
"country": "",
|
||||
"country_flag": "",
|
||||
"coupon_code": "",
|
||||
"coupon_code_is_not_valid_for_selected_plan": "",
|
||||
"create": "",
|
||||
"create_first_project": "",
|
||||
"create_new_folder": "",
|
||||
|
@ -508,13 +522,17 @@
|
|||
"please_select_an_output_file": "",
|
||||
"please_set_main_file": "",
|
||||
"plus_upgraded_accounts_receive": "",
|
||||
"postal_code": "",
|
||||
"premium_feature": "",
|
||||
"premium_makes_collab_easier_with_features": "",
|
||||
"premium_plan_label": "",
|
||||
"press_shortcut_to_open_advanced_reference_search": "",
|
||||
"priority_support": "",
|
||||
"privacy_policy": "",
|
||||
"private": "",
|
||||
"problem_with_subscription_contact_us": "",
|
||||
"proceed_to_paypal": "",
|
||||
"proceeding_to_paypal_takes_you_to_the_paypal_site_to_pay": "",
|
||||
"processing": "",
|
||||
"professional": "",
|
||||
"project": "",
|
||||
|
@ -606,6 +624,7 @@
|
|||
"search_search_for": "",
|
||||
"search_whole_word": "",
|
||||
"select_a_file": "",
|
||||
"select_a_payment_method": "",
|
||||
"select_a_project": "",
|
||||
"select_all_projects": "",
|
||||
"select_an_output_file": "",
|
||||
|
@ -648,6 +667,7 @@
|
|||
"showing_x_results": "",
|
||||
"showing_x_results_of_total": "",
|
||||
"something_went_wrong_loading_pdf_viewer": "",
|
||||
"something_went_wrong_processing_the_request": "",
|
||||
"something_went_wrong_rendering_pdf": "",
|
||||
"something_went_wrong_rendering_pdf_expected": "<0></0>",
|
||||
"something_went_wrong_server": "",
|
||||
|
@ -665,6 +685,7 @@
|
|||
"stop_on_first_error_enabled_title": "",
|
||||
"stop_on_validation_error": "",
|
||||
"store_your_work": "",
|
||||
"student_disclaimer": "",
|
||||
"subject": "",
|
||||
"subject_to_additional_vat": "",
|
||||
"submit_title": "",
|
||||
|
@ -694,6 +715,8 @@
|
|||
"thanks_settings_updated": "",
|
||||
"the_following_files_already_exist_in_this_project": "",
|
||||
"this_action_cannot_be_undone": "",
|
||||
"this_address_will_be_shown_on_the_invoice": "",
|
||||
"this_field_is_required": "",
|
||||
"this_grants_access_to_features_2": "",
|
||||
"this_project_is_public": "",
|
||||
"this_project_is_public_read_only": "",
|
||||
|
@ -751,6 +774,7 @@
|
|||
"update_dropbox_settings": "",
|
||||
"update_your_billing_details": "",
|
||||
"upgrade": "",
|
||||
"upgrade_cc_btn": "",
|
||||
"upgrade_for_longer_compiles": "",
|
||||
"upgrade_now": "",
|
||||
"upgrade_to_get_feature": "",
|
||||
|
@ -764,6 +788,7 @@
|
|||
"user_deletion_password_reset_tip": "",
|
||||
"user_sessions": "",
|
||||
"validation_issue_entry_description": "",
|
||||
"vat_number": "",
|
||||
"view_all": "",
|
||||
"view_logs": "",
|
||||
"view_metrics": "",
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
function CheckoutPanel() {
|
||||
return <h3>Checkout panel</h3>
|
||||
}
|
||||
|
||||
export default CheckoutPanel
|
|
@ -0,0 +1,5 @@
|
|||
function PaymentPreviewPanel() {
|
||||
return <h3>Preview panel</h3>
|
||||
}
|
||||
|
||||
export default PaymentPreviewPanel
|
|
@ -1,4 +1,9 @@
|
|||
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()
|
||||
|
@ -7,7 +12,26 @@ function Root() {
|
|||
return null
|
||||
}
|
||||
|
||||
return <h2>React Subscription New</h2>
|
||||
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,0 +1,356 @@
|
|||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useRef,
|
||||
useContext,
|
||||
createContext,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import getMeta from '../../../utils/meta'
|
||||
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 currencySymbols: Record<
|
||||
PaymentContextValue['currencyCode'],
|
||||
PaymentContextValue['currencySymbol']
|
||||
> = getMeta('ol-currencySymbols')
|
||||
const initiallySelectedCurrencyCode: string = getMeta(
|
||||
'ol-recommendedCurrency'
|
||||
)
|
||||
|
||||
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([initiallySelectedCurrencyCode, 'USD', 'EUR', 'GBP'])
|
||||
)
|
||||
const limitedCurrencies = limitedCurrencyCodes.reduce((prev, cur) => {
|
||||
return { ...prev, [cur]: currencySymbols[cur] }
|
||||
}, {} as Record<string, string>)
|
||||
const currencySymbol = limitedCurrencies[currencyCode]
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (typeof recurly === 'undefined' || !recurly) {
|
||||
setRecurlyLoadError(true)
|
||||
return
|
||||
}
|
||||
|
||||
eventTracking.sendMB('payment-page-view', { plan: plan.planCode })
|
||||
eventTracking.send(
|
||||
'subscription-funnel',
|
||||
'subscription-form-viewed',
|
||||
plan.planCode
|
||||
)
|
||||
|
||||
recurly.configure({ publicKey })
|
||||
pricing.current =
|
||||
recurly.Pricing.Subscription() as SubscriptionPricingInstanceCustom
|
||||
|
||||
const setupPricing = () => {
|
||||
setRecurlyLoading(true)
|
||||
|
||||
pricing.current
|
||||
?.plan(plan.planCode, { quantity: 1 })
|
||||
.address({
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
country: initialCountry,
|
||||
})
|
||||
.tax({ tax_code: 'digital', vat_number: '' })
|
||||
.currency(initiallySelectedCurrencyCode)
|
||||
.coupon(initialCouponCode)
|
||||
.catch(function (err) {
|
||||
if (
|
||||
initiallySelectedCurrencyCode !== '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()
|
||||
}, [
|
||||
initialCountry,
|
||||
initialCouponCode,
|
||||
initiallySelectedCurrencyCode,
|
||||
plan.planCode,
|
||||
publicKey,
|
||||
t,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
pricing.current?.on('change', function () {
|
||||
if (!pricing.current) return
|
||||
|
||||
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)
|
||||
|
||||
// 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
|
||||
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)
|
||||
})
|
||||
}, [])
|
||||
|
||||
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)
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
// TODO - for preview panel
|
||||
const changeCurrency = useCallback(
|
||||
(newCurrency: string) => {
|
||||
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,
|
||||
pricing,
|
||||
recurlyLoading,
|
||||
recurlyLoadError,
|
||||
recurlyPrice,
|
||||
monthlyBilling,
|
||||
taxes,
|
||||
coupon,
|
||||
couponError,
|
||||
trialLength,
|
||||
addCoupon,
|
||||
applyVatNumber,
|
||||
changeCurrency,
|
||||
updateCountry,
|
||||
}),
|
||||
[
|
||||
currencyCode,
|
||||
setCurrencyCode,
|
||||
currencySymbol,
|
||||
limitedCurrencies,
|
||||
pricingFormState,
|
||||
setPricingFormState,
|
||||
plan,
|
||||
pricing,
|
||||
recurlyLoading,
|
||||
recurlyLoadError,
|
||||
recurlyPrice,
|
||||
monthlyBilling,
|
||||
taxes,
|
||||
coupon,
|
||||
couponError,
|
||||
trialLength,
|
||||
addCoupon,
|
||||
applyVatNumber,
|
||||
changeCurrency,
|
||||
updateCountry,
|
||||
]
|
||||
)
|
||||
|
||||
return { value }
|
||||
}
|
||||
|
||||
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,0 +1,67 @@
|
|||
import countries from '../../data/countries'
|
||||
import { Plan } from '../../../../../../types/subscription/plan'
|
||||
import { SubscriptionPricingStateTax } from 'recurly__recurly-js'
|
||||
import { SubscriptionPricingInstanceCustom } from '../../../../../../types/recurly/pricing/subscription'
|
||||
|
||||
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: string
|
||||
setCurrencyCode: React.Dispatch<
|
||||
React.SetStateAction<PaymentContextValue['currencyCode']>
|
||||
>
|
||||
currencySymbol: string
|
||||
limitedCurrencies: Record<
|
||||
PaymentContextValue['currencyCode'],
|
||||
PaymentContextValue['currencySymbol']
|
||||
>
|
||||
pricingFormState: PricingFormState
|
||||
setPricingFormState: React.Dispatch<
|
||||
React.SetStateAction<PaymentContextValue['pricingFormState']>
|
||||
>
|
||||
plan: Plan
|
||||
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: string) => void
|
||||
updateCountry: (country: PricingFormState['country']) => void
|
||||
}
|
279
services/web/frontend/js/features/subscription/data/countries.ts
Normal file
279
services/web/frontend/js/features/subscription/data/countries.ts
Normal file
|
@ -0,0 +1,279 @@
|
|||
// list taken from Recurly (see https://docs.recurly.com/docs/countries-provinces-and-states). Country code must exist on Recurly, so update with care
|
||||
const countries = <const>[
|
||||
{ code: 'AF', name: 'Afghanistan' },
|
||||
{ code: 'AX', name: 'Åland Islands' },
|
||||
{ code: 'AL', name: 'Albania' },
|
||||
{ code: 'DZ', name: 'Algeria' },
|
||||
{ code: 'AS', name: 'American Samoa' },
|
||||
{ code: 'AD', name: 'Andorra' },
|
||||
{ code: 'AO', name: 'Angola' },
|
||||
{ code: 'AI', name: 'Anguilla' },
|
||||
{ code: 'AQ', name: 'Antarctica' },
|
||||
{ code: 'AG', name: 'Antigua and Barbuda' },
|
||||
{ code: 'AR', name: 'Argentina' },
|
||||
{ code: 'AM', name: 'Armenia' },
|
||||
{ code: 'AW', name: 'Aruba' },
|
||||
{ code: 'AC', name: 'Ascension Island' },
|
||||
{ code: 'AU', name: 'Australia' },
|
||||
{ code: 'AT', name: 'Austria' },
|
||||
{ code: 'AZ', name: 'Azerbaijan' },
|
||||
{ code: 'BS', name: 'Bahamas' },
|
||||
{ code: 'BH', name: 'Bahrain' },
|
||||
{ code: 'BD', name: 'Bangladesh' },
|
||||
{ code: 'BB', name: 'Barbados' },
|
||||
{ code: 'BY', name: 'Belarus' },
|
||||
{ code: 'BE', name: 'Belgium' },
|
||||
{ code: 'BZ', name: 'Belize' },
|
||||
{ code: 'BJ', name: 'Benin' },
|
||||
{ code: 'BM', name: 'Bermuda' },
|
||||
{ code: 'BT', name: 'Bhutan' },
|
||||
{ code: 'BO', name: 'Bolivia' },
|
||||
{ code: 'BA', name: 'Bosnia and Herzegovina' },
|
||||
{ code: 'BW', name: 'Botswana' },
|
||||
{ code: 'BV', name: 'Bouvet Island' },
|
||||
{ code: 'BR', name: 'Brazil' },
|
||||
{ code: 'BQ', name: 'British Antarctic Territory' },
|
||||
{ code: 'IO', name: 'British Indian Ocean Territory' },
|
||||
{ code: 'VG', name: 'British Virgin Islands' },
|
||||
{ code: 'BN', name: 'Brunei' },
|
||||
{ code: 'BG', name: 'Bulgaria' },
|
||||
{ code: 'BF', name: 'Burkina Faso' },
|
||||
{ code: 'BI', name: 'Burundi' },
|
||||
{ code: 'CV', name: 'Cabo Verde' },
|
||||
{ code: 'KH', name: 'Cambodia' },
|
||||
{ code: 'CM', name: 'Cameroon' },
|
||||
{ code: 'CA', name: 'Canada' },
|
||||
{ code: 'IC', name: 'Canary Islands' },
|
||||
{ code: 'CT', name: 'Canton and Enderbury Islands' },
|
||||
{ code: 'KY', name: 'Cayman Islands' },
|
||||
{ code: 'CF', name: 'Central African Republic' },
|
||||
{ code: 'EA', name: 'Ceuta and Melilla' },
|
||||
{ code: 'TD', name: 'Chad' },
|
||||
{ code: 'CL', name: 'Chile' },
|
||||
{ code: 'CN', name: 'China' },
|
||||
{ code: 'CX', name: 'Christmas Island' },
|
||||
{ code: 'CP', name: 'Clipperton Island' },
|
||||
{ code: 'CC', name: 'Cocos [Keeling] Islands' },
|
||||
{ code: 'CO', name: 'Colombia' },
|
||||
{ code: 'KM', name: 'Comoros' },
|
||||
{ code: 'CG', name: 'Congo - Brazzaville' },
|
||||
{ code: 'CD', name: 'Congo - Kinshasa' },
|
||||
{ code: 'CD', name: 'Congo [DRC]' },
|
||||
{ code: 'CG', name: 'Congo [Republic]' },
|
||||
{ code: 'CK', name: 'Cook Islands' },
|
||||
{ code: 'CR', name: 'Costa Rica' },
|
||||
{ code: 'CI', name: 'Côte d’Ivoire' },
|
||||
{ code: 'HR', name: 'Croatia' },
|
||||
{ code: 'CU', name: 'Cuba' },
|
||||
{ code: 'CY', name: 'Cyprus' },
|
||||
{ code: 'CZ', name: 'Czech Republic' },
|
||||
{ code: 'DK', name: 'Denmark' },
|
||||
{ code: 'DG', name: 'Diego Garcia' },
|
||||
{ code: 'DJ', name: 'Djibouti' },
|
||||
{ code: 'DM', name: 'Dominica' },
|
||||
{ code: 'DO', name: 'Dominican Republic' },
|
||||
{ code: 'NQ', name: 'Dronning Maud Land' },
|
||||
{ code: 'TL', name: 'East Timor' },
|
||||
{ code: 'EC', name: 'Ecuador' },
|
||||
{ code: 'EG', name: 'Egypt' },
|
||||
{ code: 'SV', name: 'El Salvador' },
|
||||
{ code: 'GQ', name: 'Equatorial Guinea' },
|
||||
{ code: 'ER', name: 'Eritrea' },
|
||||
{ code: 'EE', name: 'Estonia' },
|
||||
{ code: 'ET', name: 'Ethiopia' },
|
||||
{ code: 'FK', name: 'Falkland Islands [Islas Malvinas]' },
|
||||
{ code: 'FK', name: 'Falkland Islands' },
|
||||
{ code: 'FO', name: 'Faroe Islands' },
|
||||
{ code: 'FJ', name: 'Fiji' },
|
||||
{ code: 'FI', name: 'Finland' },
|
||||
{ code: 'FR', name: 'France' },
|
||||
{ code: 'GF', name: 'French Guiana' },
|
||||
{ code: 'PF', name: 'French Polynesia' },
|
||||
{ code: 'FQ', name: 'French Southern and Antarctic Territories' },
|
||||
{ code: 'TF', name: 'French Southern Territories' },
|
||||
{ code: 'GA', name: 'Gabon' },
|
||||
{ code: 'GM', name: 'Gambia' },
|
||||
{ code: 'GE', name: 'Georgia' },
|
||||
{ code: 'DE', name: 'Germany' },
|
||||
{ code: 'GH', name: 'Ghana' },
|
||||
{ code: 'GI', name: 'Gibraltar' },
|
||||
{ code: 'GR', name: 'Greece' },
|
||||
{ code: 'GL', name: 'Greenland' },
|
||||
{ code: 'GD', name: 'Grenada' },
|
||||
{ code: 'GP', name: 'Guadeloupe' },
|
||||
{ code: 'GU', name: 'Guam' },
|
||||
{ code: 'GT', name: 'Guatemala' },
|
||||
{ code: 'GG', name: 'Guernsey' },
|
||||
{ code: 'GW', name: 'Guinea-Bissau' },
|
||||
{ code: 'GN', name: 'Guinea' },
|
||||
{ code: 'GY', name: 'Guyana' },
|
||||
{ code: 'HT', name: 'Haiti' },
|
||||
{ code: 'HM', name: 'Heard Island and McDonald Islands' },
|
||||
{ code: 'HN', name: 'Honduras' },
|
||||
{ code: 'HK', name: 'Hong Kong' },
|
||||
{ code: 'HU', name: 'Hungary' },
|
||||
{ code: 'IS', name: 'Iceland' },
|
||||
{ code: 'IN', name: 'India' },
|
||||
{ code: 'ID', name: 'Indonesia' },
|
||||
{ code: 'IR', name: 'Iran' },
|
||||
{ code: 'IQ', name: 'Iraq' },
|
||||
{ code: 'IE', name: 'Ireland' },
|
||||
{ code: 'IM', name: 'Isle of Man' },
|
||||
{ code: 'IL', name: 'Israel' },
|
||||
{ code: 'IT', name: 'Italy' },
|
||||
{ code: 'CI', name: 'Ivory Coast' },
|
||||
{ code: 'JM', name: 'Jamaica' },
|
||||
{ code: 'JP', name: 'Japan' },
|
||||
{ code: 'JE', name: 'Jersey' },
|
||||
{ code: 'JT', name: 'Johnston Island' },
|
||||
{ code: 'JO', name: 'Jordan' },
|
||||
{ code: 'KZ', name: 'Kazakhstan' },
|
||||
{ code: 'KE', name: 'Kenya' },
|
||||
{ code: 'KI', name: 'Kiribati' },
|
||||
{ code: 'KW', name: 'Kuwait' },
|
||||
{ code: 'KG', name: 'Kyrgyzstan' },
|
||||
{ code: 'LA', name: 'Laos' },
|
||||
{ code: 'LV', name: 'Latvia' },
|
||||
{ code: 'LB', name: 'Lebanon' },
|
||||
{ code: 'LS', name: 'Lesotho' },
|
||||
{ code: 'LR', name: 'Liberia' },
|
||||
{ code: 'LY', name: 'Libya' },
|
||||
{ code: 'LI', name: 'Liechtenstein' },
|
||||
{ code: 'LT', name: 'Lithuania' },
|
||||
{ code: 'LU', name: 'Luxembourg' },
|
||||
{ code: 'MO', name: 'Macau SAR China' },
|
||||
{ code: 'MO', name: 'Macau' },
|
||||
{ code: 'MK', name: 'Macedonia [FYROM]' },
|
||||
{ code: 'MK', name: 'Macedonia' },
|
||||
{ code: 'MG', name: 'Madagascar' },
|
||||
{ code: 'MW', name: 'Malawi' },
|
||||
{ code: 'MY', name: 'Malaysia' },
|
||||
{ code: 'MV', name: 'Maldives' },
|
||||
{ code: 'ML', name: 'Mali' },
|
||||
{ code: 'MT', name: 'Malta' },
|
||||
{ code: 'MH', name: 'Marshall Islands' },
|
||||
{ code: 'MQ', name: 'Martinique' },
|
||||
{ code: 'MR', name: 'Mauritania' },
|
||||
{ code: 'MU', name: 'Mauritius' },
|
||||
{ code: 'YT', name: 'Mayotte' },
|
||||
{ code: 'FX', name: 'Metropolitan France' },
|
||||
{ code: 'MX', name: 'Mexico' },
|
||||
{ code: 'FM', name: 'Micronesia' },
|
||||
{ code: 'MI', name: 'Midway Islands' },
|
||||
{ code: 'MD', name: 'Moldova' },
|
||||
{ code: 'MC', name: 'Monaco' },
|
||||
{ code: 'MN', name: 'Mongolia' },
|
||||
{ code: 'ME', name: 'Montenegro' },
|
||||
{ code: 'MS', name: 'Montserrat' },
|
||||
{ code: 'MA', name: 'Morocco' },
|
||||
{ code: 'MZ', name: 'Mozambique' },
|
||||
{ code: 'MM', name: 'Myanmar [Burma]' },
|
||||
{ code: 'NA', name: 'Namibia' },
|
||||
{ code: 'NR', name: 'Nauru' },
|
||||
{ code: 'NP', name: 'Nepal' },
|
||||
{ code: 'AN', name: 'Netherlands Antilles' },
|
||||
{ code: 'NL', name: 'Netherlands' },
|
||||
{ code: 'NC', name: 'New Caledonia' },
|
||||
{ code: 'NZ', name: 'New Zealand' },
|
||||
{ code: 'NI', name: 'Nicaragua' },
|
||||
{ code: 'NE', name: 'Niger' },
|
||||
{ code: 'NG', name: 'Nigeria' },
|
||||
{ code: 'NU', name: 'Niue' },
|
||||
{ code: 'NF', name: 'Norfolk Island' },
|
||||
{ code: 'KP', name: 'North Korea' },
|
||||
{ code: 'VD', name: 'North Vietnam' },
|
||||
{ code: 'MP', name: 'Northern Mariana Islands' },
|
||||
{ code: 'NO', name: 'Norway' },
|
||||
{ code: 'OM', name: 'Oman' },
|
||||
{ code: 'QO', name: 'Outlying Oceania' },
|
||||
{ code: 'PC', name: 'Pacific Islands Trust Territory' },
|
||||
{ code: 'PK', name: 'Pakistan' },
|
||||
{ code: 'PW', name: 'Palau' },
|
||||
{ code: 'PS', name: 'Palestinian Territories' },
|
||||
{ code: 'PZ', name: 'Panama Canal Zone' },
|
||||
{ code: 'PA', name: 'Panama' },
|
||||
{ code: 'PG', name: 'Papua New Guinea' },
|
||||
{ code: 'PY', name: 'Paraguay' },
|
||||
{ code: 'YD', name: "People's Democratic Republic of Yemen" },
|
||||
{ code: 'PE', name: 'Peru' },
|
||||
{ code: 'PH', name: 'Philippines' },
|
||||
{ code: 'PN', name: 'Pitcairn Islands' },
|
||||
{ code: 'PL', name: 'Poland' },
|
||||
{ code: 'PT', name: 'Portugal' },
|
||||
{ code: 'PR', name: 'Puerto Rico' },
|
||||
{ code: 'QA', name: 'Qatar' },
|
||||
{ code: 'RE', name: 'Réunion' },
|
||||
{ code: 'RO', name: 'Romania' },
|
||||
{ code: 'RU', name: 'Russia' },
|
||||
{ code: 'RW', name: 'Rwanda' },
|
||||
{ code: 'BL', name: 'Saint Barthélemy' },
|
||||
{ code: 'SH', name: 'Saint Helena' },
|
||||
{ code: 'KN', name: 'Saint Kitts and Nevis' },
|
||||
{ code: 'LC', name: 'Saint Lucia' },
|
||||
{ code: 'MF', name: 'Saint Martin' },
|
||||
{ code: 'PM', name: 'Saint Pierre and Miquelon' },
|
||||
{ code: 'VC', name: 'Saint Vincent and the Grenadines' },
|
||||
{ code: 'WS', name: 'Samoa' },
|
||||
{ code: 'SM', name: 'San Marino' },
|
||||
{ code: 'ST', name: 'São Tomé and Príncipe' },
|
||||
{ code: 'SA', name: 'Saudi Arabia' },
|
||||
{ code: 'SN', name: 'Senegal' },
|
||||
{ code: 'CS', name: 'Serbia and Montenegro' },
|
||||
{ code: 'RS', name: 'Serbia' },
|
||||
{ code: 'SC', name: 'Seychelles' },
|
||||
{ code: 'SL', name: 'Sierra Leone' },
|
||||
{ code: 'SG', name: 'Singapore' },
|
||||
{ code: 'SK', name: 'Slovakia' },
|
||||
{ code: 'SI', name: 'Slovenia' },
|
||||
{ code: 'SB', name: 'Solomon Islands' },
|
||||
{ code: 'SO', name: 'Somalia' },
|
||||
{ code: 'ZA', name: 'South Africa' },
|
||||
{ code: 'GS', name: 'South Georgia and the South Sandwich Islands' },
|
||||
{ code: 'KR', name: 'South Korea' },
|
||||
{ code: 'ES', name: 'Spain' },
|
||||
{ code: 'LK', name: 'Sri Lanka' },
|
||||
{ code: 'SD', name: 'Sudan' },
|
||||
{ code: 'SR', name: 'Suriname' },
|
||||
{ code: 'SJ', name: 'Svalbard and Jan Mayen' },
|
||||
{ code: 'SZ', name: 'Swaziland' },
|
||||
{ code: 'SE', name: 'Sweden' },
|
||||
{ code: 'CH', name: 'Switzerland' },
|
||||
{ code: 'SY', name: 'Syria' },
|
||||
{ code: 'TW', name: 'Taiwan' },
|
||||
{ code: 'TJ', name: 'Tajikistan' },
|
||||
{ code: 'TZ', name: 'Tanzania' },
|
||||
{ code: 'TH', name: 'Thailand' },
|
||||
{ code: 'TL', name: 'Timor-Leste' },
|
||||
{ code: 'TG', name: 'Togo' },
|
||||
{ code: 'TK', name: 'Tokelau' },
|
||||
{ code: 'TO', name: 'Tonga' },
|
||||
{ code: 'TT', name: 'Trinidad and Tobago' },
|
||||
{ code: 'TA', name: 'Tristan da Cunha' },
|
||||
{ code: 'TN', name: 'Tunisia' },
|
||||
{ code: 'TR', name: 'Turkey' },
|
||||
{ code: 'TM', name: 'Turkmenistan' },
|
||||
{ code: 'TC', name: 'Turks and Caicos Islands' },
|
||||
{ code: 'TV', name: 'Tuvalu' },
|
||||
{ code: 'UM', name: 'U.S. Minor Outlying Islands' },
|
||||
{ code: 'PU', name: 'U.S. Miscellaneous Pacific Islands' },
|
||||
{ code: 'VI', name: 'U.S. Virgin Islands' },
|
||||
{ code: 'UG', name: 'Uganda' },
|
||||
{ code: 'UA', name: 'Ukraine' },
|
||||
{ code: 'AE', name: 'United Arab Emirates' },
|
||||
{ code: 'GB', name: 'United Kingdom' },
|
||||
{ code: 'US', name: 'United States' },
|
||||
{ code: 'UY', name: 'Uruguay' },
|
||||
{ code: 'UZ', name: 'Uzbekistan' },
|
||||
{ code: 'VU', name: 'Vanuatu' },
|
||||
{ code: 'VA', name: 'Vatican City' },
|
||||
{ code: 'VE', name: 'Venezuela' },
|
||||
{ code: 'VN', name: 'Vietnam' },
|
||||
{ code: 'WK', name: 'Wake Island' },
|
||||
{ code: 'WF', name: 'Wallis and Futuna' },
|
||||
{ code: 'EH', name: 'Western Sahara' },
|
||||
{ code: 'YE', name: 'Yemen' },
|
||||
{ code: 'ZM', name: 'Zambia' },
|
||||
{ code: 'ZW', name: 'Zimbabwe' },
|
||||
]
|
||||
|
||||
export default countries
|
|
@ -81,7 +81,8 @@
|
|||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.three-d-secure-container {
|
||||
.three-d-secure-container,
|
||||
.three-d-secure-container--react {
|
||||
> .three-d-secure-recurly-container {
|
||||
height: 400px;
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
"activation_token_expired": "Your activation token has expired, you will need to get another one sent to you.",
|
||||
"add": "Add",
|
||||
"add_affiliation": "Add Affiliation",
|
||||
"add_another_address_line": "Add another address line",
|
||||
"add_another_email": "Add another email",
|
||||
"add_comma_separated_emails_help": "Separate multiple email addresses using the comma (,) character.",
|
||||
"add_comment": "Add comment",
|
||||
|
@ -81,6 +82,7 @@
|
|||
"also_available_as_on_premises": "Also available as On-Premises",
|
||||
"also_provides_free_plan": "__appName__ also provides a free plan -- simply <0>register here</0> to get started.",
|
||||
"alternatively_create_new_institution_account": "Alternatively, you can create a <b>new account</b> with your institution email (<b>__email__</b>) by clicking <b>__clickText__</b>.",
|
||||
"an_error_occurred_when_verifying_the_coupon_code": "An error occurred when verifying the coupon code",
|
||||
"and": "and",
|
||||
"annual": "Annual",
|
||||
"annual_billing_enabled": "Annual billing enabled",
|
||||
|
@ -176,6 +178,7 @@
|
|||
"cant_find_page": "Sorry, we can’t find the page you are looking for.",
|
||||
"cant_see_what_youre_looking_for_question": "Can’t see what you’re looking for?",
|
||||
"card_details": "Card details",
|
||||
"card_details_are_not_valid": "Card details are not valid",
|
||||
"card_must_be_authenticated_by_3dsecure": "Your card must be authenticated with 3D Secure before continuing",
|
||||
"card_payment": "Card payment",
|
||||
"careers": "Careers",
|
||||
|
@ -274,6 +277,7 @@
|
|||
"country": "Country",
|
||||
"country_flag": "__country__ country flag",
|
||||
"coupon_code": "Coupon code",
|
||||
"coupon_code_is_not_valid_for_selected_plan": "Coupon code is not valid for selected plan",
|
||||
"coupons_not_included": "This does not include your current discounts, which will be applied automatically before your next payment",
|
||||
"create": "Create",
|
||||
"create_a_new_password_for_your_account": "Create a new password for your account",
|
||||
|
@ -1339,6 +1343,7 @@
|
|||
"sl_included_history_of_changes_blurb": "__appName__ includes a history of all of your changes so you can see exactly who changed what, and when. This makes it extremely easy to keep up to date with any progress made by your collaborators and allows you to review recent work.",
|
||||
"something_went_wrong_canceling_your_subscription": "Something went wrong canceling your subscription. Please contact support.",
|
||||
"something_went_wrong_loading_pdf_viewer": "Something went wrong loading the PDF viewer. This might be caused by issues like <0>temporary network problems</0> or an <0>outdated web browser</0>. Please follow the <1>troubleshooting steps for access, loading and display problems</1>. If the issue persists, please <2>let us know</2>.",
|
||||
"something_went_wrong_processing_the_request": "Something went wrong processing the request",
|
||||
"something_went_wrong_rendering_pdf": "Something went wrong while rendering this PDF.",
|
||||
"something_went_wrong_rendering_pdf_expected": "There was an issue displaying this PDF. <0>Please recompile</0>",
|
||||
"something_went_wrong_server": "Something went wrong. Please try again.",
|
||||
|
|
|
@ -265,6 +265,7 @@
|
|||
"@types/react": "^17.0.40",
|
||||
"@types/react-bootstrap": "^0.32.29",
|
||||
"@types/react-dom": "^17.0.13",
|
||||
"@types/recurly__recurly-js": "^4.22.0",
|
||||
"@types/sinon-chai": "^3.2.8",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.12.1",
|
||||
|
|
|
@ -11,11 +11,13 @@ import {
|
|||
describe('<PersonalSubscription />', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
// @ts-ignore
|
||||
window.recurly = {}
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
// @ts-ignore
|
||||
delete window.recurly
|
||||
})
|
||||
|
||||
|
@ -116,6 +118,7 @@ describe('<PersonalSubscription />', function () {
|
|||
'Sorry, there was an error talking to our payment provider. Please try again in a few moments. If you are using any ad or script blocking extensions in your browser, you may need to temporarily disable them.'
|
||||
|
||||
it('shows an alert and hides "Change plan" option when Recurly did not load', function () {
|
||||
// @ts-ignore
|
||||
delete window.recurly
|
||||
render(
|
||||
<SubscriptionDashboardProvider>
|
||||
|
|
|
@ -28,7 +28,6 @@ export const annualActiveSubscription: Subscription = {
|
|||
name: 'Standard (Collaborator) Annual',
|
||||
price_in_cents: 21900,
|
||||
annual: true,
|
||||
features: {},
|
||||
featureDescription: [],
|
||||
},
|
||||
recurly: {
|
||||
|
@ -68,7 +67,6 @@ export const pastDueExpiredSubscription: Subscription = {
|
|||
name: 'Standard (Collaborator) Annual',
|
||||
price_in_cents: 21900,
|
||||
annual: true,
|
||||
features: {},
|
||||
featureDescription: [],
|
||||
},
|
||||
recurly: {
|
||||
|
@ -108,7 +106,6 @@ export const canceledSubscription: Subscription = {
|
|||
name: 'Standard (Collaborator) Annual',
|
||||
price_in_cents: 21900,
|
||||
annual: true,
|
||||
features: {},
|
||||
featureDescription: [],
|
||||
},
|
||||
recurly: {
|
||||
|
@ -148,7 +145,6 @@ export const pendingSubscriptionChange: Subscription = {
|
|||
name: 'Standard (Collaborator) Annual',
|
||||
price_in_cents: 21900,
|
||||
annual: true,
|
||||
features: {},
|
||||
featureDescription: [],
|
||||
},
|
||||
recurly: {
|
||||
|
@ -175,7 +171,6 @@ export const pendingSubscriptionChange: Subscription = {
|
|||
name: 'Professional Annual',
|
||||
price_in_cents: 42900,
|
||||
annual: true,
|
||||
features: {},
|
||||
featureDescription: [],
|
||||
},
|
||||
}
|
||||
|
@ -198,7 +193,6 @@ export const groupActiveSubscription: GroupSubscription = {
|
|||
hideFromUsers: true,
|
||||
price_in_cents: 129000,
|
||||
annual: true,
|
||||
features: {},
|
||||
groupPlan: true,
|
||||
membersLimit: 10,
|
||||
membersLimitAddOn: 'additional-license',
|
||||
|
@ -243,7 +237,6 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription
|
|||
hideFromUsers: true,
|
||||
price_in_cents: 129000,
|
||||
annual: true,
|
||||
features: {},
|
||||
groupPlan: true,
|
||||
membersLimit: 10,
|
||||
membersLimitAddOn: 'additional-license',
|
||||
|
@ -286,7 +279,6 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription
|
|||
hideFromUsers: true,
|
||||
price_in_cents: 129000,
|
||||
annual: true,
|
||||
features: {},
|
||||
groupPlan: true,
|
||||
membersLimit: 10,
|
||||
membersLimitAddOn: 'additional-license',
|
||||
|
@ -308,7 +300,6 @@ export const trialSubscription: Subscription = {
|
|||
planCode: 'paid-personal_free_trial_7_days',
|
||||
name: 'Personal',
|
||||
price_in_cents: 1500,
|
||||
features: {},
|
||||
featureDescription: [],
|
||||
hideFromUsers: true,
|
||||
},
|
||||
|
|
64
services/web/types/recurly/pricing/subscription.ts
Normal file
64
services/web/types/recurly/pricing/subscription.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import {
|
||||
SubscriptionPricingInstance,
|
||||
SubscriptionPricingState,
|
||||
Address,
|
||||
Tax,
|
||||
} from 'recurly__recurly-js'
|
||||
|
||||
interface Plan {
|
||||
code: string
|
||||
name: string
|
||||
period: {
|
||||
interval: string
|
||||
length: number
|
||||
}
|
||||
price: Record<
|
||||
string,
|
||||
{
|
||||
unit_amount: number
|
||||
symbol: string
|
||||
setup_fee: number
|
||||
}
|
||||
>
|
||||
quantity: number
|
||||
tax_code: string
|
||||
tax_exempt: boolean
|
||||
trial?: {
|
||||
interval: string
|
||||
length: number
|
||||
}
|
||||
}
|
||||
|
||||
interface Coupon {
|
||||
code: string
|
||||
name: string
|
||||
discount: {
|
||||
type: string
|
||||
rate: number
|
||||
}
|
||||
single_use: boolean
|
||||
applies_for_months: number
|
||||
duration: string
|
||||
temporal_unit: string
|
||||
temporal_amount: number
|
||||
plans: unknown[]
|
||||
applies_to_non_plan_charges: boolean
|
||||
applies_to_plans: boolean
|
||||
applies_to_all_plans: boolean
|
||||
redemption_resource: string
|
||||
}
|
||||
|
||||
// Extending the default interface as it lacks the `items` prop
|
||||
export interface SubscriptionPricingInstanceCustom
|
||||
extends SubscriptionPricingInstance,
|
||||
SubscriptionPricingState {
|
||||
id: string
|
||||
items: {
|
||||
addons: unknown[]
|
||||
address?: Address
|
||||
coupon?: Coupon
|
||||
currency: string
|
||||
plan?: Plan
|
||||
tax?: Tax
|
||||
}
|
||||
}
|
6
services/web/types/subscription/api.ts
Normal file
6
services/web/types/subscription/api.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export interface CreateError {
|
||||
data: {
|
||||
message?: string
|
||||
threeDSecureActionTokenId?: string
|
||||
}
|
||||
}
|
|
@ -1,17 +1,5 @@
|
|||
import { Nullable } from '../../utils'
|
||||
|
||||
type Plan = {
|
||||
planCode: string
|
||||
name: string
|
||||
price_in_cents: number
|
||||
annual?: boolean
|
||||
features: object
|
||||
hideFromUsers?: boolean
|
||||
featureDescription?: object[]
|
||||
groupPlan?: boolean
|
||||
membersLimit?: number
|
||||
membersLimitAddOn?: string
|
||||
}
|
||||
import { Plan } from '../plan'
|
||||
|
||||
type SubscriptionState = 'active' | 'canceled' | 'expired'
|
||||
|
||||
|
|
29
services/web/types/subscription/plan.ts
Normal file
29
services/web/types/subscription/plan.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
type Features = {
|
||||
collaborators: number
|
||||
compileGroup: string
|
||||
compileTimeout: number
|
||||
dropbox: boolean
|
||||
gitBridge: boolean
|
||||
github: boolean
|
||||
mendeley: boolean
|
||||
references: boolean
|
||||
referencesSearch: boolean
|
||||
symbolPalette: boolean
|
||||
templates: boolean
|
||||
trackChanges: boolean
|
||||
versioning: boolean
|
||||
zotero: boolean
|
||||
}
|
||||
|
||||
export type Plan = {
|
||||
annual?: boolean
|
||||
featureDescription?: Record<string, unknown>[]
|
||||
features?: Features
|
||||
groupPlan?: boolean
|
||||
hideFromUsers?: boolean
|
||||
membersLimit?: number
|
||||
membersLimitAddOn?: string
|
||||
name: string
|
||||
planCode: string
|
||||
price_in_cents: number
|
||||
}
|
|
@ -31,6 +31,5 @@ declare global {
|
|||
_reportAcePerf: () => void
|
||||
MathJax: Record<string, any>
|
||||
overallThemes: OverallThemeMeta[]
|
||||
recurly?: object
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue