Skip to content

Commit

Permalink
Merge 0888d75 into afb8a4e
Browse files Browse the repository at this point in the history
  • Loading branch information
dbrudner committed Feb 18, 2020
2 parents afb8a4e + 0888d75 commit 56a421b
Show file tree
Hide file tree
Showing 9 changed files with 853 additions and 84 deletions.
7 changes: 5 additions & 2 deletions demo/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,21 @@
margin: 20px 0;
padding: 0;
}
h2 {
h2, h3 {
color: #555;
padding: 0;
margin: 0;
font-size: 1.2em;
font-weight: 400;
margin-bottom: 0.5em;
}
h3 {
font-size: 1em;
}
p {
font-size: 0.8em;
}
input {
input, select {
border: 1px solid #d7d7d9;
color: rgb(84, 84, 87);
margin: 0 1em 0 0;
Expand Down
164 changes: 164 additions & 0 deletions demo/src/checkout-pricing.js
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>
);
};
4 changes: 4 additions & 0 deletions demo/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { render } from 'react-dom';
import { CardElementDemo } from './card-element-demo';
import { IndividualCardElementsDemo } from './individual-card-elements-demo';
import { ThreeDSecureDemo } from './three-d-secure-demo';
import { CheckoutPricing } from './checkout-pricing';

function App () {
return (
Expand All @@ -31,6 +32,9 @@ function App () {

<h2>3D Secure</h2>
<ThreeDSecureDemo />

<h2>Checkout Pricing</h2>
<CheckoutPricing />
</div>
);
};
Expand Down
1 change: 1 addition & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Adapter from 'enzyme-adapter-react-16';

// TODO: Lock to CDN distribution
import recurly from 'recurly.js';
import 'regenerator-runtime/runtime';

global.recurly = recurly;

Expand Down
6 changes: 4 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import CardNumberElement from './element/card-number';
import CardMonthElement from './element/card-month';
import CardYearElement from './element/card-year';
import CardCvvElement from './element/card-cvv';
import ThreeDSecureAction from './three-d-secure-action'
import ThreeDSecureAction from './three-d-secure-action';
import useCheckoutPricing from './use-checkout-pricing';

export {
RecurlyProvider,
Expand All @@ -17,5 +18,6 @@ export {
CardMonthElement,
CardYearElement,
CardCvvElement,
ThreeDSecureAction
ThreeDSecureAction,
useCheckoutPricing
};
144 changes: 144 additions & 0 deletions lib/use-checkout-pricing.js
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;
};
Loading

0 comments on commit 56a421b

Please sign in to comment.