| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- import {
- useState,
- useEffect,
- useLayoutEffect,
- useMemo,
- useCallback,
- useRef,
- useContext,
- createContext,
- } from 'react'
- import { currencies, CurrencyCode, CurrencySymbol } from '../data/currency'
- import { useTranslation } from 'react-i18next'
- import getMeta from '../../../utils/meta'
- import { getSplitTestVariant } from '../../../../../frontend/js/utils/splitTestUtils'
- 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 initiallySelectedCurrencyCode: CurrencyCode = getMeta(
- 'ol-recommendedCurrency'
- )
- const planCode: string = getMeta('ol-planCode')
- const designSystemUpdatesVariant = getSplitTestVariant(
- 'design-system-updates',
- 'default'
- )
- const [planName, setPlanName] = useState(plan.name)
- 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<CurrencyCode>([initiallySelectedCurrencyCode, 'USD', 'EUR', 'GBP'])
- )
- const limitedCurrencies = limitedCurrencyCodes.reduce((prev, cur) => {
- return { ...prev, [cur]: currencies[cur] }
- }, {} as Partial<typeof currencies>)
- const currencySymbol = limitedCurrencies[currencyCode] as CurrencySymbol
- useLayoutEffect(() => {
- if (typeof recurly === 'undefined' || !recurly) {
- setRecurlyLoadError(true)
- return
- }
- eventTracking.sendMB('payment-page-view', {
- plan: planCode,
- currency: currencyCode,
- 'split-test-design-system-updates': designSystemUpdatesVariant,
- })
- eventTracking.send(
- 'subscription-funnel',
- 'subscription-form-viewed',
- planCode
- )
- recurly.configure({ publicKey })
- pricing.current =
- recurly.Pricing.Subscription() as SubscriptionPricingInstanceCustom
- const setupPricing = () => {
- setRecurlyLoading(true)
- pricing.current
- ?.plan(planCode, { quantity: 1 })
- .address({
- first_name: '',
- last_name: '',
- country: initialCountry,
- })
- .tax({ tax_code: 'digital', vat_number: '' })
- .currency(currencyCode)
- .coupon(initialCouponCode)
- .catch(function (err) {
- if (currencyCode !== '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()
- }, [
- designSystemUpdatesVariant,
- initialCountry,
- initialCouponCode,
- initiallySelectedCurrencyCode,
- planCode,
- publicKey,
- currencyCode,
- t,
- ])
- useEffect(() => {
- pricing.current?.on('change', function () {
- if (!pricing.current) return
- const planName = pricing.current.items.plan?.name
- if (planName) {
- setPlanName(planName)
- }
- const trialLength = pricing.current.items.plan?.trial?.length
- setTrialLength(trialLength)
- 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)
- 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)
- })
- }, [currencyCode])
- 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)
- })
- },
- []
- )
- const changeCurrency = useCallback(
- (newCurrency: CurrencyCode) => {
- 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,
- planCode,
- planName,
- pricing,
- recurlyLoading,
- recurlyLoadError,
- recurlyPrice,
- monthlyBilling,
- taxes,
- coupon,
- couponError,
- trialLength,
- addCoupon,
- applyVatNumber,
- changeCurrency,
- updateCountry,
- }),
- [
- currencyCode,
- setCurrencyCode,
- currencySymbol,
- limitedCurrencies,
- pricingFormState,
- setPricingFormState,
- plan,
- planCode,
- planName,
- pricing,
- recurlyLoading,
- recurlyLoadError,
- recurlyPrice,
- monthlyBilling,
- taxes,
- coupon,
- couponError,
- trialLength,
- addCoupon,
- applyVatNumber,
- changeCurrency,
- updateCountry,
- ]
- )
- return { value }
- }
- export 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
- }
|