@laconius/cart 0.1.0
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/LICENSE +73 -0
- package/README.md +24 -0
- package/lib/module/adapter.js +167 -0
- package/lib/module/adapter.js.map +1 -0
- package/lib/module/components.js +212 -0
- package/lib/module/components.js.map +1 -0
- package/lib/module/config.js +53 -0
- package/lib/module/config.js.map +1 -0
- package/lib/module/defaults.js +57 -0
- package/lib/module/defaults.js.map +1 -0
- package/lib/module/index.js +11 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/models.js +4 -0
- package/lib/module/models.js.map +1 -0
- package/lib/module/normalizer.js +36 -0
- package/lib/module/normalizer.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/queries.js +287 -0
- package/lib/module/queries.js.map +1 -0
- package/lib/module/store.js +42 -0
- package/lib/module/store.js.map +1 -0
- package/lib/module/translations.js +31 -0
- package/lib/module/translations.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/adapter.d.ts +25 -0
- package/lib/typescript/src/adapter.d.ts.map +1 -0
- package/lib/typescript/src/components.d.ts +49 -0
- package/lib/typescript/src/components.d.ts.map +1 -0
- package/lib/typescript/src/config.d.ts +57 -0
- package/lib/typescript/src/config.d.ts.map +1 -0
- package/lib/typescript/src/defaults.d.ts +16 -0
- package/lib/typescript/src/defaults.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +10 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/models.d.ts +122 -0
- package/lib/typescript/src/models.d.ts.map +1 -0
- package/lib/typescript/src/normalizer.d.ts +24 -0
- package/lib/typescript/src/normalizer.d.ts.map +1 -0
- package/lib/typescript/src/queries.d.ts +47 -0
- package/lib/typescript/src/queries.d.ts.map +1 -0
- package/lib/typescript/src/store.d.ts +25 -0
- package/lib/typescript/src/store.d.ts.map +1 -0
- package/lib/typescript/src/translations.d.ts +29 -0
- package/lib/typescript/src/translations.d.ts.map +1 -0
- package/package.json +78 -0
- package/src/adapter.ts +187 -0
- package/src/components.tsx +237 -0
- package/src/config.ts +81 -0
- package/src/defaults.tsx +53 -0
- package/src/index.ts +61 -0
- package/src/models.ts +142 -0
- package/src/normalizer.ts +39 -0
- package/src/queries.ts +288 -0
- package/src/store.ts +46 -0
- package/src/translations.ts +28 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { getErrorMessage, getPrimaryImage, useTranslation } from '@laconius/core';
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Divider,
|
|
5
|
+
Media,
|
|
6
|
+
Price,
|
|
7
|
+
QuantityStepper,
|
|
8
|
+
Text,
|
|
9
|
+
useStyles,
|
|
10
|
+
useUiComponent,
|
|
11
|
+
type Theme,
|
|
12
|
+
} from '@laconius/ui';
|
|
13
|
+
import {
|
|
14
|
+
AccessibilityInfo,
|
|
15
|
+
StyleSheet,
|
|
16
|
+
View,
|
|
17
|
+
type ImageStyle,
|
|
18
|
+
type StyleProp,
|
|
19
|
+
type TextStyle,
|
|
20
|
+
type ViewStyle,
|
|
21
|
+
} from 'react-native';
|
|
22
|
+
|
|
23
|
+
import type { Cart, OrderEntry } from './models';
|
|
24
|
+
import { useAddToCart, useRemoveCartEntry, useUpdateCartEntry } from './queries';
|
|
25
|
+
|
|
26
|
+
export type AddToCartButtonProps = {
|
|
27
|
+
productCode: string;
|
|
28
|
+
quantity?: number;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
/** Root style. */
|
|
31
|
+
style?: StyleProp<ViewStyle>;
|
|
32
|
+
/** Named inner parts — public, versioned API. */
|
|
33
|
+
styles?: {
|
|
34
|
+
container?: StyleProp<ViewStyle>;
|
|
35
|
+
error?: StyleProp<TextStyle>;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The one component with no UI hook: its logic *is* the mutation
|
|
41
|
+
* ([chapter 07](../../../docs/spec/07-ui-kit.md)). An OCC business error — out of stock, most
|
|
42
|
+
* likely — reaches the user as a translated sentence, per hook, with no message bus.
|
|
43
|
+
*/
|
|
44
|
+
export function AddToCartButton({
|
|
45
|
+
productCode,
|
|
46
|
+
quantity = 1,
|
|
47
|
+
disabled = false,
|
|
48
|
+
style,
|
|
49
|
+
styles: parts,
|
|
50
|
+
}: AddToCartButtonProps) {
|
|
51
|
+
const t = useTranslation();
|
|
52
|
+
const sheet = useStyles(createStyles);
|
|
53
|
+
const Action = useUiComponent('Button', Button);
|
|
54
|
+
const addToCart = useAddToCart();
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<View style={[sheet.addToCart, parts?.container, style]}>
|
|
58
|
+
<Action
|
|
59
|
+
title={t('cart.addToCart')}
|
|
60
|
+
accessibilityLabel={t('cart.addToCart')}
|
|
61
|
+
loading={addToCart.isPending}
|
|
62
|
+
disabled={disabled || addToCart.isPending || !productCode}
|
|
63
|
+
onPress={() =>
|
|
64
|
+
addToCart.mutate(
|
|
65
|
+
{ productCode, quantity },
|
|
66
|
+
{
|
|
67
|
+
onSuccess: () =>
|
|
68
|
+
AccessibilityInfo.announceForAccessibility(t('cart.itemAdded')),
|
|
69
|
+
onError: (error) =>
|
|
70
|
+
AccessibilityInfo.announceForAccessibility(getErrorMessage(error, t)),
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
/>
|
|
75
|
+
{addToCart.error ? (
|
|
76
|
+
<Text variant="caption" style={[sheet.error, parts?.error]}>
|
|
77
|
+
{getErrorMessage(addToCart.error, t)}
|
|
78
|
+
</Text>
|
|
79
|
+
) : null}
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type CartEntryRowProps = {
|
|
85
|
+
entry: OrderEntry;
|
|
86
|
+
/** Root style. */
|
|
87
|
+
style?: StyleProp<ViewStyle>;
|
|
88
|
+
styles?: {
|
|
89
|
+
container?: StyleProp<ViewStyle>;
|
|
90
|
+
media?: StyleProp<ImageStyle>;
|
|
91
|
+
info?: StyleProp<ViewStyle>;
|
|
92
|
+
title?: StyleProp<TextStyle>;
|
|
93
|
+
price?: StyleProp<TextStyle>;
|
|
94
|
+
error?: StyleProp<TextStyle>;
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Quantity and remove, one mutation hook each so each keeps its own `isPending` and `error`. */
|
|
99
|
+
export function CartEntryRow({ entry, style, styles: parts }: CartEntryRowProps) {
|
|
100
|
+
const t = useTranslation();
|
|
101
|
+
const sheet = useStyles(createStyles);
|
|
102
|
+
const Remove = useUiComponent('Button', Button);
|
|
103
|
+
const update = useUpdateCartEntry();
|
|
104
|
+
const remove = useRemoveCartEntry();
|
|
105
|
+
|
|
106
|
+
const entryNumber = entry.entryNumber;
|
|
107
|
+
const busy = update.isPending || remove.isPending;
|
|
108
|
+
const error = update.error ?? remove.error;
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<View style={[sheet.row, parts?.container, style]}>
|
|
112
|
+
<Media
|
|
113
|
+
media={getPrimaryImage(entry.product?.images)}
|
|
114
|
+
mediaRole="cartIcon"
|
|
115
|
+
decorative
|
|
116
|
+
style={[sheet.rowMedia, parts?.media]}
|
|
117
|
+
/>
|
|
118
|
+
<View style={[sheet.rowInfo, parts?.info]}>
|
|
119
|
+
<Text variant="label" numberOfLines={2} style={parts?.title}>
|
|
120
|
+
{entry.product?.name}
|
|
121
|
+
</Text>
|
|
122
|
+
<Price price={entry.totalPrice} variant="label" style={parts?.price} />
|
|
123
|
+
<View style={sheet.rowActions}>
|
|
124
|
+
<QuantityStepper
|
|
125
|
+
value={entry.quantity ?? 1}
|
|
126
|
+
max={entry.product?.stock?.stockLevel}
|
|
127
|
+
disabled={entryNumber === undefined || busy}
|
|
128
|
+
onChange={(value) => {
|
|
129
|
+
if (entryNumber === undefined) return;
|
|
130
|
+
update.mutate(
|
|
131
|
+
{ entryNumber, quantity: value },
|
|
132
|
+
{
|
|
133
|
+
onSuccess: () =>
|
|
134
|
+
AccessibilityInfo.announceForAccessibility(t('cart.quantityUpdated')),
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
<Remove
|
|
140
|
+
variant="ghost"
|
|
141
|
+
title={t('cart.remove')}
|
|
142
|
+
accessibilityLabel={t('cart.removeItem', { name: entry.product?.name ?? '' })}
|
|
143
|
+
disabled={entryNumber === undefined || busy}
|
|
144
|
+
onPress={() => {
|
|
145
|
+
if (entryNumber === undefined) return;
|
|
146
|
+
remove.mutate(
|
|
147
|
+
{ entryNumber },
|
|
148
|
+
{
|
|
149
|
+
onSuccess: () =>
|
|
150
|
+
AccessibilityInfo.announceForAccessibility(t('cart.itemRemoved')),
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
}}
|
|
154
|
+
/>
|
|
155
|
+
</View>
|
|
156
|
+
{error ? (
|
|
157
|
+
<Text variant="caption" style={[sheet.error, parts?.error]}>
|
|
158
|
+
{getErrorMessage(error, t)}
|
|
159
|
+
</Text>
|
|
160
|
+
) : null}
|
|
161
|
+
</View>
|
|
162
|
+
</View>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export type CartTotalsProps = {
|
|
167
|
+
cart?: Cart;
|
|
168
|
+
/** Root style. */
|
|
169
|
+
style?: StyleProp<ViewStyle>;
|
|
170
|
+
styles?: {
|
|
171
|
+
container?: StyleProp<ViewStyle>;
|
|
172
|
+
row?: StyleProp<ViewStyle>;
|
|
173
|
+
label?: StyleProp<TextStyle>;
|
|
174
|
+
value?: StyleProp<TextStyle>;
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/** Pure component: the cart arrives as a prop, exactly like `Price` ([chapter 07](../../../docs/spec/07-ui-kit.md)). */
|
|
179
|
+
export function CartTotals({ cart, style, styles: parts }: CartTotalsProps) {
|
|
180
|
+
const t = useTranslation();
|
|
181
|
+
const sheet = useStyles(createStyles);
|
|
182
|
+
if (!cart) return null;
|
|
183
|
+
|
|
184
|
+
const rows: { key: string; label: string; price: Cart['subTotal'] }[] = [];
|
|
185
|
+
if (cart.subTotal) rows.push({ key: 'subtotal', label: t('cart.subtotal'), price: cart.subTotal });
|
|
186
|
+
if (cart.totalDiscounts?.value) {
|
|
187
|
+
rows.push({ key: 'discounts', label: t('cart.discounts'), price: cart.totalDiscounts });
|
|
188
|
+
}
|
|
189
|
+
if (cart.deliveryCost) {
|
|
190
|
+
rows.push({ key: 'delivery', label: t('cart.delivery'), price: cart.deliveryCost });
|
|
191
|
+
}
|
|
192
|
+
if (cart.totalTax?.value) rows.push({ key: 'tax', label: t('cart.tax'), price: cart.totalTax });
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<View style={[sheet.totals, parts?.container, style]}>
|
|
196
|
+
{rows.map((row) => (
|
|
197
|
+
<View key={row.key} style={[sheet.totalsRow, parts?.row]}>
|
|
198
|
+
<Text variant="caption" muted style={parts?.label}>
|
|
199
|
+
{row.label}
|
|
200
|
+
</Text>
|
|
201
|
+
<Price price={row.price} variant="caption" style={parts?.value} />
|
|
202
|
+
</View>
|
|
203
|
+
))}
|
|
204
|
+
<Divider />
|
|
205
|
+
<View style={[sheet.totalsRow, parts?.row]}>
|
|
206
|
+
<Text variant="label" style={parts?.label}>
|
|
207
|
+
{t('cart.total')}
|
|
208
|
+
</Text>
|
|
209
|
+
<Price
|
|
210
|
+
price={cart.totalPriceWithTax ?? cart.totalPrice}
|
|
211
|
+
variant="subheading"
|
|
212
|
+
style={parts?.value}
|
|
213
|
+
/>
|
|
214
|
+
</View>
|
|
215
|
+
</View>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const createStyles = (theme: Theme) =>
|
|
220
|
+
StyleSheet.create({
|
|
221
|
+
addToCart: { gap: theme.spacing.xs },
|
|
222
|
+
error: { color: theme.colors.danger },
|
|
223
|
+
row: { flexDirection: 'row', gap: theme.spacing.md },
|
|
224
|
+
rowMedia: { width: 64, height: 64, borderRadius: theme.radii.sm },
|
|
225
|
+
rowInfo: { flex: 1, gap: theme.spacing.xs },
|
|
226
|
+
rowActions: {
|
|
227
|
+
flexDirection: 'row',
|
|
228
|
+
alignItems: 'center',
|
|
229
|
+
justifyContent: 'space-between',
|
|
230
|
+
},
|
|
231
|
+
totals: { gap: theme.spacing.xs },
|
|
232
|
+
totalsRow: {
|
|
233
|
+
flexDirection: 'row',
|
|
234
|
+
alignItems: 'center',
|
|
235
|
+
justifyContent: 'space-between',
|
|
236
|
+
},
|
|
237
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { LaconiusConfigChunk } from '@laconius/core';
|
|
2
|
+
import type { CmsComponentMapping } from '@laconius/cms';
|
|
3
|
+
|
|
4
|
+
import { defaultCartConverters, type CartAdapter } from './adapter';
|
|
5
|
+
import { CmsMiniCart } from './defaults';
|
|
6
|
+
import { defaultCartTranslations } from './translations';
|
|
7
|
+
|
|
8
|
+
declare module '@laconius/ui' {
|
|
9
|
+
/** The cart components, replaceable through the typed UI registry. */
|
|
10
|
+
interface LaconiusUiComponents {
|
|
11
|
+
AddToCartButton: unknown;
|
|
12
|
+
CartEntryRow: unknown;
|
|
13
|
+
CartTotals: unknown;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
declare module '@laconius/core' {
|
|
18
|
+
interface LaconiusEndpoints {
|
|
19
|
+
carts: string | Record<string, string>;
|
|
20
|
+
cart: string | Record<string, string>;
|
|
21
|
+
createCart: string | Record<string, string>;
|
|
22
|
+
deleteCart: string | Record<string, string>;
|
|
23
|
+
addEntries: string | Record<string, string>;
|
|
24
|
+
updateEntries: string | Record<string, string>;
|
|
25
|
+
removeEntries: string | Record<string, string>;
|
|
26
|
+
cartApplyVoucher: string | Record<string, string>;
|
|
27
|
+
cartRemoveVoucher: string | Record<string, string>;
|
|
28
|
+
cartVoucher: string | Record<string, string>;
|
|
29
|
+
validate: string | Record<string, string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface LaconiusConverters {
|
|
33
|
+
cart: unknown;
|
|
34
|
+
cartModification: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface LaconiusAdapters {
|
|
38
|
+
cart: CartAdapter;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Ported from `feature-libs/cart/base/occ/config/default-occ-cart-config-factory.ts`, minus the
|
|
44
|
+
* saved-cart fields (`saveTime`, `name`, `description` stay off the wire — v1 ships one active
|
|
45
|
+
* cart). `stock(FULL)` on entry products is what lets the stepper cap at the stock level.
|
|
46
|
+
*/
|
|
47
|
+
const CART_FIELDS =
|
|
48
|
+
'DEFAULT,potentialProductPromotions,appliedProductPromotions,potentialOrderPromotions,appliedOrderPromotions,entries(totalPrice(formattedValue),product(images(FULL),stock(FULL)),basePrice(formattedValue,value),updateable),totalPrice(formattedValue),totalItems,totalPriceWithTax(formattedValue),totalDiscounts(value,formattedValue),subTotal(formattedValue),totalUnitCount,deliveryItemsQuantity,deliveryCost(formattedValue),totalTax(formattedValue,value),pickupItemsQuantity,net,appliedVouchers,productDiscounts(formattedValue),user';
|
|
49
|
+
|
|
50
|
+
export const defaultCartEndpoints = {
|
|
51
|
+
carts: 'users/${userId}/carts?fields=carts(' + CART_FIELDS + ')',
|
|
52
|
+
cart: 'users/${userId}/carts/${cartId}?fields=' + CART_FIELDS,
|
|
53
|
+
createCart: 'users/${userId}/carts?fields=' + CART_FIELDS,
|
|
54
|
+
deleteCart: 'users/${userId}/carts/${cartId}',
|
|
55
|
+
addEntries: 'users/${userId}/carts/${cartId}/entries',
|
|
56
|
+
updateEntries: 'users/${userId}/carts/${cartId}/entries/${entryNumber}',
|
|
57
|
+
removeEntries: 'users/${userId}/carts/${cartId}/entries/${entryNumber}',
|
|
58
|
+
cartApplyVoucher: 'users/${userId}/carts/${cartId}/applyVoucher',
|
|
59
|
+
cartRemoveVoucher: 'users/${userId}/carts/${cartId}/removeVoucher',
|
|
60
|
+
/** The legacy pair, used when `capabilities.jsonVouchers` is off. */
|
|
61
|
+
cartVoucher: {
|
|
62
|
+
default: 'users/${userId}/carts/${cartId}/vouchers',
|
|
63
|
+
delete: 'users/${userId}/carts/${cartId}/vouchers/${voucherId}',
|
|
64
|
+
},
|
|
65
|
+
validate: 'users/${userId}/carts/${cartId}/validate?fields=DEFAULT',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The content types that read cart data ship here rather than in `@laconius/cms`, which keeps
|
|
70
|
+
* the dependency arrow `cart -> cms` ([chapter 01](../../../docs/spec/01-packages.md)).
|
|
71
|
+
*/
|
|
72
|
+
export const defaultCartCmsComponents: Record<string, CmsComponentMapping> = {
|
|
73
|
+
MiniCartComponent: { component: CmsMiniCart },
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const defaultCartConfig: LaconiusConfigChunk = {
|
|
77
|
+
backend: { occ: { endpoints: defaultCartEndpoints } },
|
|
78
|
+
cms: { components: defaultCartCmsComponents },
|
|
79
|
+
converters: defaultCartConverters,
|
|
80
|
+
i18n: { translations: defaultCartTranslations },
|
|
81
|
+
};
|
package/src/defaults.tsx
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useTranslation } from '@laconius/core';
|
|
2
|
+
import { useCmsComponentData, useCmsNavigate, type CmsComponent } from '@laconius/cms';
|
|
3
|
+
import { Price, Text, useStyles, type Theme } from '@laconius/ui';
|
|
4
|
+
import { Pressable, StyleSheet } from 'react-native';
|
|
5
|
+
|
|
6
|
+
import { useActiveCart } from './queries';
|
|
7
|
+
|
|
8
|
+
/** Spartacus' `CmsMiniCartComponent` fields; the lightbox banner is web furniture and dropped. */
|
|
9
|
+
export interface CmsMiniCartData extends CmsComponent {
|
|
10
|
+
title?: string;
|
|
11
|
+
shownProductCount?: string;
|
|
12
|
+
/** `TOTAL` or `SUBTOTAL`. */
|
|
13
|
+
totalDisplay?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Item count and total. It ships here rather than in `@laconius/cms` because it reads cart data,
|
|
18
|
+
* which is what keeps the dependency arrow `cart -> cms`
|
|
19
|
+
* ([chapter 01](../../../docs/spec/01-packages.md)) — the same reason Spartacus ships
|
|
20
|
+
* `MiniCartComponent` from `feature-libs/cart/base/components/mini-cart/`.
|
|
21
|
+
*/
|
|
22
|
+
export function CmsMiniCart() {
|
|
23
|
+
const data = useCmsComponentData<CmsMiniCartData>();
|
|
24
|
+
const t = useTranslation();
|
|
25
|
+
const sheet = useStyles(createStyles);
|
|
26
|
+
const navigate = useCmsNavigate();
|
|
27
|
+
const { data: cart } = useActiveCart();
|
|
28
|
+
|
|
29
|
+
const count = cart?.totalUnitCount ?? cart?.totalItems ?? 0;
|
|
30
|
+
const total = data.totalDisplay === 'SUBTOTAL' ? cart?.subTotal : cart?.totalPrice;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Pressable
|
|
34
|
+
accessibilityRole="button"
|
|
35
|
+
accessibilityLabel={`${data.title ?? t('cart.title')}: ${t('cart.entryCount', { count })}`}
|
|
36
|
+
onPress={() => navigate({ kind: 'url', url: '/cart', external: false })}
|
|
37
|
+
style={sheet.root}
|
|
38
|
+
>
|
|
39
|
+
<Text variant="label">{t('cart.entryCount', { count })}</Text>
|
|
40
|
+
{count > 0 && total ? <Price price={total} variant="label" /> : null}
|
|
41
|
+
</Pressable>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const createStyles = (theme: Theme) =>
|
|
46
|
+
StyleSheet.create({
|
|
47
|
+
root: {
|
|
48
|
+
flexDirection: 'row',
|
|
49
|
+
alignItems: 'center',
|
|
50
|
+
gap: theme.spacing.sm,
|
|
51
|
+
minHeight: 44,
|
|
52
|
+
},
|
|
53
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
AddToCartInput,
|
|
3
|
+
Cart,
|
|
4
|
+
CartMergeFailure,
|
|
5
|
+
CartMergeResult,
|
|
6
|
+
CartModification,
|
|
7
|
+
CartModificationList,
|
|
8
|
+
CartMutationResult,
|
|
9
|
+
CartValidationStatusCode,
|
|
10
|
+
DeliveryMode,
|
|
11
|
+
OrderEntry,
|
|
12
|
+
PromotionOrderEntryConsumed,
|
|
13
|
+
PromotionResult,
|
|
14
|
+
Voucher,
|
|
15
|
+
} from './models';
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
getCartId,
|
|
19
|
+
normalizeCartImages,
|
|
20
|
+
type OccCart,
|
|
21
|
+
type OccOrderEntry,
|
|
22
|
+
} from './normalizer';
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
cartAdapter,
|
|
26
|
+
defaultCartAdapter,
|
|
27
|
+
defaultCartConverters,
|
|
28
|
+
type CartAdapter,
|
|
29
|
+
} from './adapter';
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
cartQueries,
|
|
33
|
+
useActiveCart,
|
|
34
|
+
useAddToCart,
|
|
35
|
+
useApplyVoucher,
|
|
36
|
+
useMergeAnonymousCart,
|
|
37
|
+
useRemoveCartEntry,
|
|
38
|
+
useRemoveVoucher,
|
|
39
|
+
useUpdateCartEntry,
|
|
40
|
+
} from './queries';
|
|
41
|
+
|
|
42
|
+
export { useActiveCartIdStore, type ActiveCartState } from './store';
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
AddToCartButton,
|
|
46
|
+
CartEntryRow,
|
|
47
|
+
CartTotals,
|
|
48
|
+
type AddToCartButtonProps,
|
|
49
|
+
type CartEntryRowProps,
|
|
50
|
+
type CartTotalsProps,
|
|
51
|
+
} from './components';
|
|
52
|
+
|
|
53
|
+
export { CmsMiniCart, type CmsMiniCartData } from './defaults';
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
defaultCartCmsComponents,
|
|
57
|
+
defaultCartConfig,
|
|
58
|
+
defaultCartEndpoints,
|
|
59
|
+
} from './config';
|
|
60
|
+
|
|
61
|
+
export { defaultCartTranslations } from './translations';
|
package/src/models.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { Currency, Price, Principal, Product, Promotion } from '@laconius/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cart models, ported from Spartacus `feature-libs/cart/base/root/models/cart.model.ts`
|
|
5
|
+
* (Apache-2.0, see NOTICE), minus the B2B, saved-cart and pickup-in-store noise
|
|
6
|
+
* ([chapter 03](../../../docs/spec/03-occ-layer.md)).
|
|
7
|
+
*
|
|
8
|
+
* `CartModification` is copied **exactly**: it encodes protocol rather than data — a cart
|
|
9
|
+
* mutation answers with an adjusted quantity and a status, and an API returning `void` there
|
|
10
|
+
* would be wrong.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface Voucher {
|
|
14
|
+
appliedValue?: Price;
|
|
15
|
+
code?: string;
|
|
16
|
+
currency?: Currency;
|
|
17
|
+
description?: string;
|
|
18
|
+
freeShipping?: boolean;
|
|
19
|
+
name?: string;
|
|
20
|
+
value?: number;
|
|
21
|
+
valueFormatted?: string;
|
|
22
|
+
valueString?: string;
|
|
23
|
+
voucherCode?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PromotionOrderEntryConsumed {
|
|
27
|
+
adjustedUnitPrice?: number;
|
|
28
|
+
code?: string;
|
|
29
|
+
orderEntryNumber?: number;
|
|
30
|
+
quantity?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PromotionResult {
|
|
34
|
+
consumedEntries?: PromotionOrderEntryConsumed[];
|
|
35
|
+
description?: string;
|
|
36
|
+
promotion?: Promotion;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DeliveryMode {
|
|
40
|
+
code?: string;
|
|
41
|
+
deliveryCost?: Price;
|
|
42
|
+
description?: string;
|
|
43
|
+
name?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface OrderEntry {
|
|
47
|
+
basePrice?: Price;
|
|
48
|
+
deliveryMode?: DeliveryMode;
|
|
49
|
+
entryNumber?: number;
|
|
50
|
+
product?: Product;
|
|
51
|
+
quantity?: number;
|
|
52
|
+
totalPrice?: Price;
|
|
53
|
+
updateable?: boolean;
|
|
54
|
+
promotions?: PromotionResult[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface Cart {
|
|
58
|
+
appliedOrderPromotions?: PromotionResult[];
|
|
59
|
+
appliedProductPromotions?: PromotionResult[];
|
|
60
|
+
appliedVouchers?: Voucher[];
|
|
61
|
+
calculated?: boolean;
|
|
62
|
+
code?: string;
|
|
63
|
+
deliveryCost?: Price;
|
|
64
|
+
deliveryItemsQuantity?: number;
|
|
65
|
+
deliveryMode?: DeliveryMode;
|
|
66
|
+
description?: string;
|
|
67
|
+
entries?: OrderEntry[];
|
|
68
|
+
/** ISO string: OCC returns strings and never `Date`. */
|
|
69
|
+
expirationTime?: string;
|
|
70
|
+
guid?: string;
|
|
71
|
+
name?: string;
|
|
72
|
+
net?: boolean;
|
|
73
|
+
orderDiscounts?: Price;
|
|
74
|
+
pickupItemsQuantity?: number;
|
|
75
|
+
potentialOrderPromotions?: PromotionResult[];
|
|
76
|
+
potentialProductPromotions?: PromotionResult[];
|
|
77
|
+
productDiscounts?: Price;
|
|
78
|
+
site?: string;
|
|
79
|
+
store?: string;
|
|
80
|
+
subTotal?: Price;
|
|
81
|
+
totalDiscounts?: Price;
|
|
82
|
+
totalItems?: number;
|
|
83
|
+
totalPrice?: Price;
|
|
84
|
+
totalPriceWithTax?: Price;
|
|
85
|
+
totalTax?: Price;
|
|
86
|
+
totalUnitCount?: number;
|
|
87
|
+
user?: Principal;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface CartModification {
|
|
91
|
+
deliveryModeChanged?: boolean;
|
|
92
|
+
entry?: OrderEntry;
|
|
93
|
+
quantity?: number;
|
|
94
|
+
quantityAdded?: number;
|
|
95
|
+
statusCode?: string;
|
|
96
|
+
statusMessage?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface CartModificationList {
|
|
100
|
+
cartModifications?: CartModification[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type CartValidationStatusCode =
|
|
104
|
+
| 'noStock'
|
|
105
|
+
| 'lowStock'
|
|
106
|
+
| 'reviewConfiguration'
|
|
107
|
+
| 'pricingError'
|
|
108
|
+
| 'unresolvableIssues'
|
|
109
|
+
| 'below_min_quantity'
|
|
110
|
+
| 'exceed_max_quantity';
|
|
111
|
+
|
|
112
|
+
export type AddToCartInput = {
|
|
113
|
+
productCode: string;
|
|
114
|
+
quantity?: number;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* What every cart mutation resolves to. It carries the cart id it acted on: with lazy creation,
|
|
119
|
+
* an `onSuccess` closing over `activeCartId` writes the wrong cache entry
|
|
120
|
+
* ([chapter 04](../../../docs/spec/04-state.md)).
|
|
121
|
+
*/
|
|
122
|
+
export type CartMutationResult = {
|
|
123
|
+
cart: Cart;
|
|
124
|
+
cartId: string;
|
|
125
|
+
modification?: CartModification;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export type CartMergeFailure = {
|
|
129
|
+
entry: OrderEntry;
|
|
130
|
+
/** The `LaconiusHttpError` that rejected the replay. */
|
|
131
|
+
error: unknown;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The one place Laconius returns errors as data rather than throwing them: a partial replay is
|
|
136
|
+
* not a failure, and silently dropping the rejected entries is exactly what users notice
|
|
137
|
+
* ([chapter 09](../../../docs/spec/09-i18n-and-errors.md)).
|
|
138
|
+
*/
|
|
139
|
+
export type CartMergeResult = {
|
|
140
|
+
replayed: OrderEntry[];
|
|
141
|
+
failed: CartMergeFailure[];
|
|
142
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { normalizeImages, type Image, type Product } from '@laconius/core';
|
|
2
|
+
|
|
3
|
+
import type { Cart, OrderEntry } from './models';
|
|
4
|
+
|
|
5
|
+
/** An entry as OCC answers it: the product's images are still a flat list. */
|
|
6
|
+
export type OccOrderEntry = Omit<OrderEntry, 'product'> & {
|
|
7
|
+
product?: Omit<Product, 'images'> & { images?: Image[] };
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type OccCart = Omit<Cart, 'entries'> & { entries?: OccOrderEntry[] };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Reshapes every entry product's flat image list through core's `normalizeImages` — the same
|
|
14
|
+
* mechanism the product adapter uses ([chapter 08](../../../docs/spec/08-media.md)). URLs are
|
|
15
|
+
* not absolutised here: converters are pure, so the adapter absolutises the whole payload
|
|
16
|
+
* afterwards.
|
|
17
|
+
*/
|
|
18
|
+
export function normalizeCartImages(source: OccCart, target?: Cart): Cart {
|
|
19
|
+
const cart = target ?? ({ ...source } as unknown as Cart);
|
|
20
|
+
if (source.entries) {
|
|
21
|
+
cart.entries = source.entries.map((entry) =>
|
|
22
|
+
entry.product?.images
|
|
23
|
+
? ({
|
|
24
|
+
...entry,
|
|
25
|
+
product: { ...entry.product, images: normalizeImages(entry.product.images) },
|
|
26
|
+
} as OrderEntry)
|
|
27
|
+
: (entry as OrderEntry),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return cart;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Ported from `feature-libs/cart/base/core/utils/utils.ts`: an anonymous cart is addressed by
|
|
35
|
+
* its `guid`, a logged-in user's cart by its `code`.
|
|
36
|
+
*/
|
|
37
|
+
export function getCartId(cart: Cart | undefined, userId: string): string {
|
|
38
|
+
return (userId === 'anonymous' ? cart?.guid : cart?.code) ?? '';
|
|
39
|
+
}
|