-
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.
Adds stories/documentation for useCheckoutPricing
- Loading branch information
Showing
3 changed files
with
493 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
import { Subtitle } from '@storybook/addon-docs/blocks'; | ||
import { PropsTable } from '@storybook/components'; | ||
|
||
<Meta title="Hooks/useCheckoutPricing" /> | ||
|
||
# useCheckoutPricing | ||
|
||
<Subtitle slot={() => 'To create a pricing preview'} /> | ||
|
||
`import { useCheckoutPricing } from '@recurly/react-recurly';` | ||
|
||
A [custom hook][react-hooks] for interacting with [Recurly.js' pricing API][rjs-pricing] for providing users with an estimate of a purchase before they check out. | ||
|
||
To initialize, call `useCheckoutPricing` [with an initial checkout pricing input][use-checkout-pricing-input]. | ||
|
||
```js | ||
export default function PricingPreview() { | ||
const initialPricingInput = { | ||
subscriptions: [ | ||
{ | ||
plan: 'my-plan', | ||
}, | ||
], | ||
}; | ||
|
||
const [{ price, loading }, setCheckoutPricing] = useCheckoutPricing(initialPricingInput); | ||
|
||
if (!loading) { | ||
return <div>{price.now.subtotal}</div> | ||
} | ||
} | ||
``` | ||
|
||
Calling `useCheckoutPricing` returns an array of two items: | ||
|
||
1. A [useCheckoutPricingState][use-checkout-pricing-state] object for reading output. | ||
2. A [setCheckoutPricing][set-checkout-pricing] function for updating input values. | ||
|
||
For complete documentation on what `useCheckoutPricing` returns, see the [useCheckoutPricingReturn reference page][use-checkout-pricing-return]. | ||
|
||
## Initializing | ||
|
||
It's important to know that immediately after calling `useCheckoutPricing`, `useCheckoutPricingState.price` will default to an empty object while waiting for asynchronous actions to resolve. | ||
|
||
The below example will throw a type error because of this. | ||
```js | ||
export default function PricingPreview() { | ||
const initialPricingInput = { | ||
subscriptions: [ | ||
{ | ||
plan: 'my-plan', | ||
}, | ||
], | ||
}; | ||
|
||
const [{ price, loading }, setCheckoutPricing] = useCheckoutPricing(initialPricingInput); | ||
|
||
return <div>{price.now && price.now.subtotal}</div> | ||
} | ||
``` | ||
|
||
The loading state can be checked before rendering anything that uses `useCheckoutPricingState.price` to circumvent the error. | ||
|
||
```js | ||
export default function PricingPreview() { | ||
const initialPricingInput = { | ||
subscriptions: [ | ||
{ | ||
plan: 'my-plan', | ||
}, | ||
], | ||
}; | ||
|
||
const [{ price }, setCheckoutPricing] = useCheckoutPricing(initialPricingInput); | ||
|
||
return <div>{price.now && price.now.subtotal}</div> | ||
} | ||
``` | ||
|
||
Existence checking can be used as well. | ||
|
||
```js | ||
export default function PricingPreview() { | ||
const initialPricingInput = { | ||
subscriptions: [ | ||
{ | ||
plan: 'my-plan', | ||
}, | ||
], | ||
}; | ||
|
||
const [{ price }, setCheckoutPricing] = useCheckoutPricing(initialPricingInput); | ||
|
||
return <div>{price.now && price.now.subtotal}</div> | ||
} | ||
``` | ||
|
||
## Updating | ||
|
||
The [setCheckoutPricing][set-checkout-pricing] function can be used to update the checkout pricing input if needed with a new [useCheckoutPricingInput][use-checkout-pricing-input]. | ||
|
||
For example, if a customer wishes to choose a different plan for their subscription on a checkout page and the price preview needs to be updated, calling `setCheckoutPricing` with new inputs will update its associated `useCheckoutPricingState` and trigger a re-render. | ||
|
||
```js | ||
export default function PricingPreview() { | ||
const [plan, setPlan] = useState("") | ||
const initialPricingInput = { | ||
subscriptions: [ | ||
{ | ||
plan: 'my-plan', | ||
}, | ||
], | ||
}; | ||
|
||
const [{ price, loading }, setCheckoutPricing] = useCheckoutPricing(initialPricingInput); | ||
|
||
const handleChange = e => { | ||
const newPlan = e.target.value; | ||
setPlan(newPlan); | ||
|
||
// calling setCheckoutPricing here with new input values will update `price` and trigger a re-render | ||
setCheckoutPricing({ subscriptions: [{ plan: newPlan }] }); | ||
} | ||
|
||
if (!loading) { | ||
return <div> | ||
<div> | ||
{price.now.subtotal} | ||
</div> | ||
<select value={plan} onChange={handleChange}> | ||
<option value="">Select a plan</option> | ||
<option value="my-plan">My plan</option> | ||
<option value="my-second-plan">My second plan</option> | ||
</select> | ||
</div> | ||
} | ||
} | ||
``` | ||
|
||
`setCheckoutPricing` is an implementation of [react state][react-use-state] and follows the same rules and patterns. It's also possible pass a function to `setCheckoutPricing` that accepts its previous inputs as the first and only argument: | ||
|
||
```js | ||
setCheckoutPricing(previous => ({ ...previous, subscriptions: [{ plan: newPlan }] })); | ||
``` | ||
|
||
## Loading state | ||
|
||
Because Recurly.js' pricing API is asynchronous, a loading state is provided as a boolean. | ||
|
||
```js | ||
export default function PricingPreview() { | ||
const [plan, setPlan] = useState(null) | ||
|
||
const initialPricingInput = { | ||
subscriptions: [ | ||
{ | ||
plan: 'my-plan', | ||
}, | ||
], | ||
}; | ||
|
||
const [{ loading }, setCheckoutPricing] = useCheckoutPricing(initialPricingInput); | ||
|
||
if (loading) { | ||
return <div>Loading...</div> | ||
} | ||
... | ||
} | ||
``` | ||
|
||
When `useCheckoutPricing` is first initiated or when it's updated with `setCheckoutPricing`, `useCheckoutPricingState.loading` will be set to `true` until its asynchronous actions have resolved, then set back to `false`. | ||
## Error handling | ||
|
||
Any errors from `useCheckoutPricing` will be thrown by default. Passing a function as the second argument to `useCheckoutPricing` allows errors to be caught and handled. | ||
|
||
```js | ||
export default function PricingPreview() { | ||
const [recurlyError, setRecurlyError] = useState(null); | ||
|
||
const initialPricingInput = { | ||
subscriptions: [ | ||
{ | ||
plan: 'my-plan', | ||
}, | ||
], | ||
}; | ||
|
||
const [{ price, loading }, setCheckoutPricing] = useCheckoutPricing(initialPricingInput, setRecurlyError); | ||
|
||
if (recurlyError && !loading) { | ||
return <div>Error: {recurlyError.message}</div> | ||
} | ||
... | ||
} | ||
``` | ||
|
||
Errors will always take the shape of a [Recurly.js error][recurly-error]. | ||
|
||
## Demo | ||
|
||
For a full example of `useCheckoutPricing`, see our [interactive demo][interactive-demo]. | ||
|
||
[react-hooks]: https://reactjs.org/docs/hooks-intro.html | ||
[rjs-pricing]: https://developers.recurly.com/reference/recurly-js/#pricing | ||
[react-use-state]: https://reactjs.org/docs/hooks-state.html | ||
[recurly-error]: https://developers.recurly.com/reference/recurly-js/#errors | ||
[interactive-demo]: /?path=/docs/introduction-interactive-demo--page | ||
[set-checkout-pricing]: /?path=/docs/reference-usecheckoutpricingreturn--page#setcheckoutpricing) | ||
[use-checkout-pricing-state]: /?path=/docs/reference-usecheckoutpricingreturn--page#usecheckoutpricingstate | ||
[use-checkout-pricing-return]: /?path=/docs/reference-usecheckoutpricingreturn--page | ||
[use-checkout-pricing-input]: /?path=/docs/reference-usecheckoutpricinginput--page | ||
[use-checkout-pricing-updating]: /?path=/docs/reference-usecheckoutpricinginput--page#updating |
Oops, something went wrong.