@afosto/checkout-ui 0.0.15 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +333 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1 +1,333 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @afosto/checkout-ui
|
|
2
|
+
|
|
3
|
+
React components and state for building a checkout on the Afosto storefront API. The package contains a complete checkout with information, shipping, payment and review steps. Render it as a whole with `<Checkout />`, or compose your own checkout from the separate layout, form, sections, fields and hooks.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @afosto/checkout-ui @afosto/storefront @emotion/react @emotion/styled
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer dependencies are `react` and `react-dom` 18, `@emotion/react` and `@emotion/styled`. The package ships ES modules only. The components and providers are marked `'use client'`, so in Next.js you render them from a client component.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
A checkout needs three things:
|
|
16
|
+
|
|
17
|
+
1. A storefront client from `@afosto/storefront`, created with the storefront token of your channel.
|
|
18
|
+
2. The id of the cart the customer is checking out.
|
|
19
|
+
3. A `CheckoutProvider` that loads the cart, with a `Checkout` inside that renders it.
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
'use client';
|
|
23
|
+
|
|
24
|
+
import { createStorefrontClient } from '@afosto/storefront';
|
|
25
|
+
import { Checkout, CheckoutProvider } from '@afosto/checkout-ui';
|
|
26
|
+
|
|
27
|
+
const storefront = createStorefrontClient({
|
|
28
|
+
storefrontToken: '<your storefront token>',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (typeof window !== 'undefined') {
|
|
32
|
+
window.dataLayer = window.dataLayer || [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const CheckoutPage = ({ cartId }: { cartId: string }) => (
|
|
36
|
+
<CheckoutProvider storefront={storefront} cartId={cartId}>
|
|
37
|
+
<Checkout />
|
|
38
|
+
</CheckoutProvider>
|
|
39
|
+
);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The `dataLayer` line is only needed if you want analytics events. See [Analytics](#analytics).
|
|
43
|
+
|
|
44
|
+
This renders the full checkout: a top bar with the logo, a stepper, the step forms, the order summary, sign-in for customers with an account and a message when the cart doesn't exist. Logo, colours, currency, locale and links come from the channel. You don't have to configure anything else for a working checkout.
|
|
45
|
+
|
|
46
|
+
## Cart flow and order flow
|
|
47
|
+
|
|
48
|
+
`CheckoutProvider` takes either a `cartId` or an `orderId`, never both. Passing both throws an error.
|
|
49
|
+
|
|
50
|
+
- With `cartId` the provider runs the cart flow (`flow: 'cart'`). The customer fills in the steps and the last step confirms the cart, which turns it into an order.
|
|
51
|
+
- With `orderId` the provider runs the order flow (`flow: 'order'`). It runs the same steps against an existing order, for example an order that still needs a payment.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
<CheckoutProvider storefront={storefront} orderId={orderId}>
|
|
55
|
+
<Checkout />
|
|
56
|
+
</CheckoutProvider>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
After the last step the package redirects the browser. For a `pin` payment method it goes to `{channelLinks.checkout}/payment/execute?order_id=...`. Otherwise it follows the first proceeding option the API returns, usually the payment provider's page.
|
|
60
|
+
|
|
61
|
+
If `cartId` or `orderId` is empty, the checkout shows the not-found message.
|
|
62
|
+
|
|
63
|
+
## What the provider does
|
|
64
|
+
|
|
65
|
+
`CheckoutProvider` sets up everything the components need:
|
|
66
|
+
|
|
67
|
+
1. It fetches the channel through `storefront.getChannel()`. The channel supplies the locale, currency, logo, store name, branding colours, links (terms, privacy, my account) and the B2B/B2C and required-field options.
|
|
68
|
+
2. It merges those channel values with the `settings` prop. A value in `settings` always wins over the channel.
|
|
69
|
+
3. It wraps the tree in a MUI theme from `@afosto/components`, with the primary palette built from the channel's primary colour.
|
|
70
|
+
4. It creates a React Query client, unless you pass your own through `queryClient`.
|
|
71
|
+
5. It loads the cart or order and exposes all state and actions through the `useCheckout()` hook.
|
|
72
|
+
|
|
73
|
+
### Provider props
|
|
74
|
+
|
|
75
|
+
| Prop | Description |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| `storefront` | Required. A client from `createStorefrontClient()` in `@afosto/storefront`. |
|
|
78
|
+
| `cartId` / `orderId` | Required, pick one. Decides the flow. |
|
|
79
|
+
| `settings` | Overrides the channel settings. See [Settings](#settings). |
|
|
80
|
+
| `translations` | Text overrides per component. See [Translations](#translations). |
|
|
81
|
+
| `locale` | Overrides the channel locale. Falls back to `en`. |
|
|
82
|
+
| `theme` | Partial MUI theme, deep-merged over the Afosto theme. |
|
|
83
|
+
| `steps` | Replaces the default steps. See [Steps](#steps). |
|
|
84
|
+
| `operations` | Overrides or extends the API calls. See [Operations](#operations). |
|
|
85
|
+
| `analytics` | Overrides or extends the analytics events. See [Analytics](#analytics). |
|
|
86
|
+
| `createSummary` | Replaces the function that turns a cart or order into the summary data. |
|
|
87
|
+
| `expressPaymentMethodsComponentMapping` | Maps express payment method codes to button components. PayPal Express is built in. |
|
|
88
|
+
| `isLoading` | Delays fetching the channel and the cart until it is `false`. Use it while you are still resolving the cart id or token. |
|
|
89
|
+
| `queryClient` | Your own React Query client. |
|
|
90
|
+
|
|
91
|
+
### Settings
|
|
92
|
+
|
|
93
|
+
Commonly used fields of `settings` (type `CheckoutSettings`):
|
|
94
|
+
|
|
95
|
+
- `emailOptIn`: show the newsletter opt-in.
|
|
96
|
+
- `salutation`, `isSalutationRequired`: show and require the salutation field.
|
|
97
|
+
- `isPhoneNumberRequired`, `isCocNumberRequired`, `isVatNumberRequired`, `isSharedOrganisationRequired`: required fields. Default to the channel options.
|
|
98
|
+
- `isB2bExcluded`, `isB2cExcluded`: hide the business or consumer option. Default to the channel options.
|
|
99
|
+
- `isAccountEnabled`: show sign-in. Defaults to `true` when the channel has a my account link.
|
|
100
|
+
- `disableCouponModifications`: hide adding and removing coupons.
|
|
101
|
+
- `disableAnalytics`: stop all analytics events.
|
|
102
|
+
- `links`, `logo`, `storeName`, `currency`, `branding`: override what the channel returns.
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<CheckoutProvider
|
|
106
|
+
storefront={storefront}
|
|
107
|
+
cartId={cartId}
|
|
108
|
+
settings={{
|
|
109
|
+
emailOptIn: true,
|
|
110
|
+
salutation: true,
|
|
111
|
+
isPhoneNumberRequired: true,
|
|
112
|
+
links: { termsConditions: 'https://example.com/terms' },
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Steps
|
|
118
|
+
|
|
119
|
+
The package exports four default steps:
|
|
120
|
+
|
|
121
|
+
| Key | Export | Contents |
|
|
122
|
+
| --- | --- | --- |
|
|
123
|
+
| `information` | `informationStep` | Sign in, account selection, contact details, business details, shipping and billing address. |
|
|
124
|
+
| `shipping` | `shippingStep` | Shipping method and pickup point. |
|
|
125
|
+
| `payment` | `paymentStep` | Payment method, issuer, terminal and agreement (down payment, post paid). |
|
|
126
|
+
| `review` | `reviewStep` | Overview, comment and confirm. |
|
|
127
|
+
|
|
128
|
+
A step is a plain object of type `CheckoutStep`:
|
|
129
|
+
|
|
130
|
+
- `key`, `label`: identifier and stepper label.
|
|
131
|
+
- `component`: the React component rendered for the step.
|
|
132
|
+
- `defaultValues`: the starting form values, or a function that returns them.
|
|
133
|
+
- `initialize(data, responseData, context)`: builds the form values from the loaded cart or order.
|
|
134
|
+
- `validationSchema(context)`: a `yup` schema for the step's values.
|
|
135
|
+
- `isComplete(values, context)`: decides whether the customer can skip past this step. On load the checkout opens on the first step that is not complete.
|
|
136
|
+
- `submit(values, formData, context)`: runs when the customer clicks next. This is where the step sends its data to the API.
|
|
137
|
+
- `onEvent` (optional): reacts to events such as `signedIn`, `accountSelected` and `clearSession`.
|
|
138
|
+
- `reviewTargets`, `getSubStepForReviewTarget` (optional): let the review step link back to a field in this step.
|
|
139
|
+
|
|
140
|
+
To change the flow, pass your own array. You can reuse the defaults:
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
import { informationStep, shippingStep, paymentStep, reviewStep } from '@afosto/checkout-ui';
|
|
144
|
+
|
|
145
|
+
<CheckoutProvider
|
|
146
|
+
storefront={storefront}
|
|
147
|
+
cartId={cartId}
|
|
148
|
+
steps={[informationStep, { ...shippingStep, label: 'Delivery' }, paymentStep, reviewStep]}
|
|
149
|
+
>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
An empty array falls back to the defaults.
|
|
153
|
+
|
|
154
|
+
## Operations
|
|
155
|
+
|
|
156
|
+
Every API call goes through an operation. An operation has four functions:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
{
|
|
160
|
+
queryFn: (params, context) => Promise<Response>; // calls the API
|
|
161
|
+
formatter: (response, context) => FormattedResponse; // reshapes the response
|
|
162
|
+
success: (data, context) => void; // updates checkout state, tracks analytics
|
|
163
|
+
error: (error) => void;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The package exports the defaults as `cartOperations`, `orderOperations` (same keys, for the order flow) and `authOperations`. Examples of keys are `initial`, `addCustomer`, `addInformation`, `addShippingMethod`, `addPaymentMethod`, `addCoupon`, `checkout`, `signIn` and `signUp`.
|
|
168
|
+
|
|
169
|
+
The `operations` prop merges per operation and per function. You only pass what you want to change. The other functions keep their default:
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
<CheckoutProvider
|
|
173
|
+
storefront={storefront}
|
|
174
|
+
cartId={cartId}
|
|
175
|
+
operations={{
|
|
176
|
+
addCustomer: {
|
|
177
|
+
error: (error) => reportError(error),
|
|
178
|
+
},
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The `context` argument holds the storefront client, settings, the current flow, `defaultOperations` (so you can call the original from your override), `setSummary`, `track` and the rest of the checkout state.
|
|
184
|
+
|
|
185
|
+
Keys that don't exist yet are added as custom operations. The package does not call them by itself. You can read them from `useCheckout().operations` in your own components.
|
|
186
|
+
|
|
187
|
+
## Analytics
|
|
188
|
+
|
|
189
|
+
The package pushes GA4 ecommerce events to `window.dataLayer`. Events only fire when `window.dataLayer` exists, so create it before the checkout renders:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
window.dataLayer = window.dataLayer || [];
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Default events:
|
|
196
|
+
|
|
197
|
+
| Key | dataLayer event |
|
|
198
|
+
| --- | --- |
|
|
199
|
+
| `beginCheckout` | `begin_checkout` |
|
|
200
|
+
| `checkoutStep` | `checkout_step` |
|
|
201
|
+
| `addShippingInfo` | `add_shipping_info` |
|
|
202
|
+
| `addPaymentInfo` | `add_payment_info` |
|
|
203
|
+
| `signIn` | `login` |
|
|
204
|
+
| `signUp` | `sign_up` |
|
|
205
|
+
|
|
206
|
+
Each event has an optional `formatter(eventData, context)` and a `send(formattedData, rawData, context)`. The `analytics` prop merges per event, like operations. Use it to change the payload or to send events somewhere else.
|
|
207
|
+
|
|
208
|
+
`stepSubmitted` fires after every step submit, but has no default `send`. Add one if you need it:
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
analytics={{
|
|
212
|
+
stepSubmitted: {
|
|
213
|
+
send: (eventData, rawData, context) => {
|
|
214
|
+
window.dataLayer?.push({ event: 'step_submitted', step: rawData.step.key });
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
}}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Set `settings.disableAnalytics` to turn off all events.
|
|
221
|
+
|
|
222
|
+
## Translations
|
|
223
|
+
|
|
224
|
+
Every component has English default texts. The `translations` prop on the provider overrides them per component. The keys are listed in the `CheckoutTranslations` type, for example `summary`, `addressFields`, `shippingMethodsField`, `paymentMethodSection` and `validation`.
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
translations={{
|
|
228
|
+
summary: { title: 'Overzicht', subtotal: 'Subtotaal' },
|
|
229
|
+
validation: { required: 'Dit veld is verplicht' },
|
|
230
|
+
}}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The package only ships English. For another language, pass all texts through `translations`.
|
|
234
|
+
|
|
235
|
+
## Customising the UI
|
|
236
|
+
|
|
237
|
+
### Slots
|
|
238
|
+
|
|
239
|
+
`Checkout`, `CheckoutLayout` and most larger components accept `slots` and `slotProps`. A slot replaces a part with your own component. `slotProps` passes extra props to that part. `slotProps` can also be a function that gets the owner state (for example `{ flow, notFound }`) and returns props.
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
<Checkout
|
|
243
|
+
slots={{ notFoundFeedback: MyNotFoundPage }}
|
|
244
|
+
slotProps={{
|
|
245
|
+
layout: { hideTopBar: true },
|
|
246
|
+
summary: { hideTitle: true },
|
|
247
|
+
}}
|
|
248
|
+
/>
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Theme
|
|
252
|
+
|
|
253
|
+
Pass `theme` to override the MUI theme. The primary palette comes from `settings.branding.colors.primary` (or the channel branding) and is overridden by `theme.palette.primary`.
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
<CheckoutProvider
|
|
257
|
+
storefront={storefront}
|
|
258
|
+
cartId={cartId}
|
|
259
|
+
theme={{ palette: { primary: { main: '#0b5cff' } } }}
|
|
260
|
+
>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Composing your own checkout
|
|
264
|
+
|
|
265
|
+
Every part of `<Checkout />` is exported. Inside a `CheckoutProvider` you can put them together yourself:
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
import {
|
|
269
|
+
CheckoutForm,
|
|
270
|
+
CheckoutLayout,
|
|
271
|
+
CheckoutProvider,
|
|
272
|
+
CheckoutSignIn,
|
|
273
|
+
useCheckout,
|
|
274
|
+
} from '@afosto/checkout-ui';
|
|
275
|
+
|
|
276
|
+
const MyCheckout = () => {
|
|
277
|
+
const { showSignIn } = useCheckout();
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<CheckoutLayout>
|
|
281
|
+
<MyIntroText />
|
|
282
|
+
<CheckoutForm hideForm={showSignIn} />
|
|
283
|
+
{showSignIn && <CheckoutSignIn />}
|
|
284
|
+
</CheckoutLayout>
|
|
285
|
+
);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
export const CheckoutPage = ({ cartId }: { cartId: string }) => (
|
|
289
|
+
<CheckoutProvider storefront={storefront} cartId={cartId}>
|
|
290
|
+
<MyCheckout />
|
|
291
|
+
</CheckoutProvider>
|
|
292
|
+
);
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
What each part does:
|
|
296
|
+
|
|
297
|
+
- `CheckoutLayout` renders the top bar and a container with the steps on the left and the summary on the right. The top bar holds the logo, the stepper and, on mobile, the summary drawer. With `hideTopBar` those disappear too.
|
|
298
|
+
- `CheckoutForm` renders the active step, the next and back buttons and error messages, and submits the step.
|
|
299
|
+
- `CheckoutSignIn`, `CheckoutForgotPassword` and `CheckoutAccountSelection` are the screens around signing in. `<Checkout />` shows them based on `showSignIn`, `showForgotPassword` and `showAccountSelection` from `useCheckout()`. If you compose your own checkout, render them yourself.
|
|
300
|
+
- `CheckoutSummary`, `CheckoutStepper` and `CheckoutTopBar` can also be placed on their own.
|
|
301
|
+
- Below the steps sit the sections (`CheckoutShippingAddressSection`, `CheckoutPaymentMethodSection`, ...) and fields (`CheckoutShippingMethodsField`, `CheckoutPaymentMethodsField`, ...). Use these to build your own step components.
|
|
302
|
+
|
|
303
|
+
`useCheckout()` gives access to the full state, including `flow`, `summary`, `activeStep`, `steps`, `settings`, `translations`, `isLoading`, `notFound`, the actions (`addCoupon`, `addShippingMethod`, `checkout`, ...) and `track`.
|
|
304
|
+
|
|
305
|
+
The package also exports its GraphQL `queries`, `mutations` and `fragments`, the `hooks`, `utils`, `constants` and the payment method, issuer and carrier `icons`. Besides the root import you can import a single folder, for example `@afosto/checkout-ui/steps`.
|
|
306
|
+
|
|
307
|
+
## Development
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
npx nx build @afosto/checkout-ui
|
|
311
|
+
npx nx lint @afosto/checkout-ui
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
The build turns every `.ts` file under `src` into its own entry point. Add new components to `src/index.ts` to export them from the root.
|
|
315
|
+
|
|
316
|
+
### Project layout
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
src/
|
|
320
|
+
Checkout*/ one folder per component, each with an index.ts
|
|
321
|
+
CheckoutProvider/ provider, theme provider and query client provider
|
|
322
|
+
steps/ default step definitions
|
|
323
|
+
operations/ default API operations for cart, order and auth
|
|
324
|
+
queries/ GraphQL queries
|
|
325
|
+
mutations/ GraphQL mutations
|
|
326
|
+
fragments/ GraphQL fragments
|
|
327
|
+
hooks/ state hooks behind the providers
|
|
328
|
+
constants/ analytics events, icon mappings, address field config
|
|
329
|
+
icons/ payment method, issuer and carrier icons
|
|
330
|
+
utils/ formatting and helper functions
|
|
331
|
+
types.ts shared types
|
|
332
|
+
index.ts public exports
|
|
333
|
+
```
|