Merge pull request #11525 from overleaf/ii-payment-page-migration-init

[web] Payment page migration initialisation

GitOrigin-RevId: f33b73a13b96fad3cddf0c0205d05df678ce5300
This commit is contained in:
ilkin-overleaf 2023-02-02 11:15:04 +02:00 committed by Copybot
parent 1d04aa0315
commit db54475bd0
20 changed files with 919 additions and 47 deletions

14
package-lock.json generated
View file

@ -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",

View file

@ -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,
})
}
}

View file

@ -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

View file

@ -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": "",

View file

@ -0,0 +1,5 @@
function CheckoutPanel() {
return <h3>Checkout panel</h3>
}
export default CheckoutPanel

View file

@ -0,0 +1,5 @@
function PaymentPreviewPanel() {
return <h3>Preview panel</h3>
}
export default PaymentPreviewPanel

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View 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 dIvoire' },
{ 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

View file

@ -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;

View file

@ -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 cant find the page you are looking for.",
"cant_see_what_youre_looking_for_question": "Cant see what youre 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.",

View file

@ -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",

View file

@ -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>

View file

@ -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,
},

View 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
}
}

View file

@ -0,0 +1,6 @@
export interface CreateError {
data: {
message?: string
threeDSecureActionTokenId?: string
}
}

View file

@ -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'

View 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
}

View file

@ -31,6 +31,5 @@ declare global {
_reportAcePerf: () => void
MathJax: Record<string, any>
overallThemes: OverallThemeMeta[]
recurly?: object
}
}