-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
853 additions
and
84 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import React, { useState } from 'react'; | ||
import { Elements, RecurlyProvider, useCheckoutPricing } from '@recurly/react-recurly'; | ||
|
||
export function CheckoutPricing () { | ||
return ( | ||
<RecurlyProvider publicKey={process.env.REACT_APP_RECURLY_PUBLIC_KEY}> | ||
<Elements> | ||
<CheckoutPricingForm /> | ||
</Elements> | ||
</RecurlyProvider> | ||
); | ||
}; | ||
|
||
function CheckoutPricingForm () { | ||
const [recurlyError, setRecurlyError] = useState(null); | ||
const [plan, setPlan] = useState(''); | ||
const [planQuantity, setPlanQuantity] = useState(''); | ||
const [itemCode, setItemCode] = useState(''); | ||
const [itemQuantity, setItemQuantity] = useState(''); | ||
const [coupon, setCoupon] = useState(''); | ||
const [giftCard, setGiftCard] = useState(''); | ||
const [currency, setCurrency] = useState(''); | ||
const [billingCountry, setBillingCountry] = useState(''); | ||
const [billingPostalCode, setBillingPostalCode] = useState(''); | ||
const [billingVatNumber, setBillingVatNumber] = useState(''); | ||
const [shippingCountry, setShippingCountry] = useState(''); | ||
const [shippingPostalCode, setShippingPostalCode] = useState(''); | ||
const [shippingVatNumber, setShippingVatNumber] = useState(''); | ||
const [{ price, loading }, setPricing] = useCheckoutPricing({}, setRecurlyError); | ||
const showPrice = !loading && !recurlyError; | ||
|
||
function updatePlan (e) { | ||
const plan = e.target.value; | ||
setPlan(plan); | ||
}; | ||
|
||
function updatePlanQuantity (e) { | ||
const planQuantity = e.target.value; | ||
setPlanQuantity(planQuantity); | ||
}; | ||
|
||
function handleSubmit (e) { | ||
e.preventDefault(); | ||
const address = { | ||
billingCountry, | ||
billingPostalCode, | ||
billingVatNumber, | ||
}; | ||
setRecurlyError(null); | ||
setPricing({ | ||
subscriptions: [{ plan, quantity: planQuantity }], | ||
adjustments: [{ itemCode, quantity: itemQuantity }], | ||
coupon, | ||
giftCard, | ||
address | ||
}); | ||
}; | ||
|
||
return ( | ||
<form onSubmit={handleSubmit} className="DemoSection"> | ||
<div> | ||
<select value={plan} onChange={updatePlan}> | ||
<option value="">Select a plan</option> | ||
<option value="basic">Basic</option> | ||
<option value="advanced">Advanced</option> | ||
<option value="error">Error</option> | ||
</select> | ||
<input | ||
type="number" | ||
value={planQuantity} | ||
onChange={updatePlanQuantity} | ||
placeholder="Plan quantity" | ||
min="0" | ||
/> | ||
</div> | ||
<div style={{ marginTop: '10px' }}> | ||
<input | ||
type="text" | ||
value={itemCode} | ||
onChange={e => setItemCode(e.target.value)} | ||
placeholder="Item code" | ||
/> | ||
<input | ||
type="number" | ||
onChange={e => setItemQuantity(e.target.value)} | ||
placeholder="Quantity" | ||
min="0" | ||
/> | ||
</div> | ||
<div style={{ marginTop: '10px' }}> | ||
<input | ||
type="text" | ||
value={coupon} | ||
onChange={e => setCoupon(e.target.value)} | ||
placeholder="Coupon" | ||
/> | ||
<input | ||
type="text" | ||
value={giftCard} | ||
onChange={e => setGiftCard(e.target.value)} | ||
placeholder="Gift card" | ||
/> | ||
<input | ||
type="text" | ||
value={currency} | ||
onChange={e => setCurrency(e.target.value)} | ||
placeholder="Currency" | ||
/> | ||
</div> | ||
<div style={{ marginTop: '10px' }}> | ||
<h3>Billing address</h3> | ||
<input | ||
type="text" | ||
value={billingCountry} | ||
onChange={e => setBillingCountry(e.target.value)} | ||
placeholder="Country" | ||
/> | ||
<input | ||
type="text" | ||
value={billingPostalCode} | ||
onChange={e => setBillingPostalCode(e.target.value)} | ||
placeholder="Postal code" | ||
/> | ||
<input | ||
type="text" | ||
value={billingVatNumber} | ||
onChange={e => setBillingVatNumber(e.target.value)} | ||
placeholder="Vat number" | ||
/> | ||
</div> | ||
<div style={{ marginTop: '10px' }}> | ||
<h3>Shipping address</h3> | ||
<input | ||
type="text" | ||
value={shippingCountry} | ||
onChange={e => setShippingCountry(e.target.value)} | ||
placeholder="Country" | ||
/> | ||
<input | ||
type="text" | ||
value={shippingPostalCode} | ||
onChange={e => setShippingPostalCode(e.target.value)} | ||
placeholder="Postal code" | ||
/> | ||
<input | ||
type="text" | ||
value={shippingVatNumber} | ||
onChange={e => setShippingVatNumber(e.target.value)} | ||
placeholder="Vat number" | ||
/> | ||
</div> | ||
<div style={{ marginTop: '10px' }}> | ||
{recurlyError ? <span style={{ color: 'red' }}>{recurlyError.message}</span> : ''} | ||
{showPrice ? ( | ||
<span> | ||
{'Subtotal: '} | ||
<span style={{ visibility: loading ? 'hidden' : '' }}>${price.now.subtotal}</span> | ||
</span> | ||
) : null} | ||
</div> | ||
<button>Submit</button> | ||
</form> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
import { useEffect, useState } from 'react'; | ||
import useRecurly from './use-recurly'; | ||
import cloneDeep from 'lodash/cloneDeep'; | ||
|
||
/** | ||
* @typedef {Object} address | ||
* @property {String} country | ||
* @property {String} postalCode | ||
* @property {String} vatNumber | ||
*/ | ||
|
||
/** | ||
* useCheckoutPricing interface | ||
* @typedef {Object} useCheckoutPricingInterface | ||
* @property {Array} subscriptions | ||
* @property {Array} adjustments | ||
* @property {String} currency | ||
* @property {address} address | ||
* @property {address} shippingAddress | ||
* @property {String} coupon | ||
* @property {String} giftCard | ||
* @property {Object} tax | ||
*/ | ||
|
||
/** | ||
* A custom hook for interfacing with recurly.js' checkoutPricing API meant to mimic the call signature, return | ||
* type, and behavior of `react.useState`. | ||
* | ||
* Accepts an `initialInputs` param (same as useState) and an error handling function. | ||
* | ||
* Returns a tuple with an output object and an update function similar to useState. | ||
* | ||
* @typedef {Object} output | ||
* @property {Object} price | ||
* @property {Object} pricing | ||
* @property {Boolean} loading | ||
* | ||
* @typedef {Function} setPricing | ||
* @typedef {[output, setPricing]} useCheckoutPricingInstance | ||
* | ||
* @param {PricingInput} useCheckoutPricingInterface | ||
* @param {function} handleError | ||
* @returns {useCheckoutPricingInstance} useCheckoutPricingInstance | ||
*/ | ||
export default function useCheckoutPricing(initialInputs = {}, handleError = throwError) { | ||
const recurly = useRecurly(); | ||
const [loading, setLoading] = useState(true); | ||
const [input, setInput] = useState(initialInputs); | ||
const [pricing, setPricing] = useState(recurly.Pricing.Checkout()); | ||
|
||
useEffect(() => { | ||
setLoading(true); | ||
const { subscriptions = [], adjustments = [], ...restInputs } = input; | ||
let checkoutPricing = recurly.Pricing.Checkout(); | ||
|
||
if (Object.keys(restInputs).length) { | ||
checkoutPricing = addRestInputs(restInputs, checkoutPricing); | ||
}; | ||
|
||
if (adjustments.length) { | ||
checkoutPricing = addAdjustments(adjustments, checkoutPricing); | ||
}; | ||
|
||
addSubscriptions(subscriptions, checkoutPricing) | ||
.then(() => { | ||
checkoutPricing = checkoutPricing.reprice().done(() => { | ||
setPricing(checkoutPricing); | ||
setLoading(false); | ||
}); | ||
}) | ||
.catch(error => { | ||
handleError(error); | ||
setLoading(false); | ||
}); | ||
|
||
function addAdjustments (adjustments, checkoutPricing) { | ||
return adjustments | ||
.reduce((checkoutPricing, adjustment) => { | ||
if (adjustment.itemCode) { | ||
return checkoutPricing.adjustment(adjustment); | ||
} | ||
return checkoutPricing; | ||
}, checkoutPricing).catch(handleError); | ||
}; | ||
|
||
function addRestInputs(restInputs, checkoutPricing) { | ||
const { PRICING_METHODS } = checkoutPricing; | ||
const exclude = ['reset', 'remove', 'reprice', 'subscriptions', 'adjustments', 'addon', 'plan']; | ||
const permittedInputs = PRICING_METHODS.filter(method => !exclude.includes(method)); | ||
|
||
return Object.entries(restInputs).reduce((acc, input) => { | ||
const [method, value] = input; | ||
const shouldCallPricingMethod = value && permittedInputs.includes(method); | ||
return shouldCallPricingMethod ? acc[method](value).catch(handleError) : acc; | ||
}, checkoutPricing); | ||
}; | ||
|
||
function addSubscriptions(subscriptions, checkoutPricing) { | ||
const { subscriptionPricings } = subscriptions.reduce( | ||
({ checkoutPricing, subscriptionPricings }, { plan, tax, addons = [], quantity }) => { | ||
let subscriptionPricing = recurly.Pricing.Subscription().plan(plan, { quantity }); | ||
|
||
if (addons.length) { | ||
subscriptionPricing = addAddons(addons, subscriptionPricing); | ||
} | ||
|
||
if (tax) { | ||
subscriptionPricing = subscriptionPricing.tax(tax); | ||
} | ||
|
||
subscriptionPricing = subscriptionPricing.catch(handleError); | ||
|
||
return { | ||
checkoutPricing: checkoutPricing.subscription(subscriptionPricing.done()), | ||
subscriptionPricings: [...subscriptionPricings, subscriptionPricing], | ||
}; | ||
}, | ||
{ checkoutPricing, subscriptionPricings: [] }, | ||
); | ||
|
||
return Promise.all(subscriptionPricings); | ||
}; | ||
|
||
function addAddons(addons = [], subscriptionPricing) { | ||
return addons | ||
.reduce((subscriptionPricing, { code, quantity }) => { | ||
return subscriptionPricing.addon(code, { quantity }); | ||
}, subscriptionPricing) | ||
.catch(handleError); | ||
}; | ||
}, [input, handleError, recurly.Pricing]); | ||
|
||
const output = { | ||
price: (pricing && cloneDeep(pricing.price)) || {}, | ||
pricing, | ||
loading, | ||
}; | ||
|
||
return [output, setInput]; | ||
}; | ||
|
||
function throwError(err) { | ||
throw err; | ||
}; |
Oops, something went wrong.