@mohasinac/appkit 4.25.0 → 4.27.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/dist/_internal/server/features/bundles/actions.d.ts +4 -1
- package/dist/_internal/server/features/bundles/actions.js +54 -5
- package/dist/_internal/server/features/bundles/data.d.ts +19 -0
- package/dist/_internal/server/features/bundles/data.js +32 -0
- package/dist/_internal/server/features/bundles/index.d.ts +1 -1
- package/dist/_internal/server/features/bundles/index.js +1 -1
- package/dist/_internal/server/features/checkout/actions.js +38 -68
- package/dist/_internal/server/features/checkout/expand-order-items.d.ts +35 -0
- package/dist/_internal/server/features/checkout/expand-order-items.js +85 -0
- package/dist/_internal/server/features/grouped/index.d.ts +2 -0
- package/dist/_internal/server/features/grouped/index.js +2 -0
- package/dist/_internal/server/features/grouped/metadata.d.ts +17 -0
- package/dist/_internal/server/features/grouped/metadata.js +48 -0
- package/dist/_internal/server/features/grouped/og.d.ts +16 -0
- package/dist/_internal/server/features/grouped/og.js +24 -0
- package/dist/_internal/server/features/seo/sitemap.js +24 -1
- package/dist/_internal/shared/checkout/order-math.d.ts +63 -4
- package/dist/_internal/shared/checkout/order-math.js +119 -7
- package/dist/client.d.ts +7 -3
- package/dist/client.js +3 -2
- package/dist/constants/api-endpoints.d.ts +0 -6
- package/dist/constants/api-endpoints.js +0 -2
- package/dist/features/analytics/types.d.ts +1 -1
- package/dist/features/analytics/types.js +3 -0
- package/dist/features/cart/components/CartGroupLineRow.d.ts +42 -0
- package/dist/features/cart/components/CartGroupLineRow.js +33 -0
- package/dist/features/cart/components/index.d.ts +2 -0
- package/dist/features/cart/components/index.js +1 -0
- package/dist/features/cart/repository/cart.repository.js +6 -0
- package/dist/features/cart/schemas/firestore.d.ts +7 -2
- package/dist/features/categories/components/BundleBuyNowCta.d.ts +18 -5
- package/dist/features/categories/components/BundleBuyNowCta.js +24 -10
- package/dist/features/categories/components/BundleDetailView.d.ts +8 -1
- package/dist/features/categories/components/BundleDetailView.js +2 -2
- package/dist/features/categories/components/BundlesListView.d.ts +1 -0
- package/dist/features/categories/components/CategoryBundlesListing.d.ts +3 -0
- package/dist/features/grouped/components/GroupedListingDetailView.d.ts +21 -0
- package/dist/features/grouped/components/GroupedListingDetailView.js +17 -0
- package/dist/features/grouped/components/GroupedListingsCarousel.js +6 -1
- package/dist/features/orders/index.d.ts +1 -1
- package/dist/features/orders/index.js +1 -1
- package/dist/features/orders/schemas/firestore.d.ts +11 -0
- package/dist/features/orders/utils/bundle-grouping.d.ts +29 -19
- package/dist/features/orders/utils/bundle-grouping.js +32 -22
- package/dist/features/tester/seed-data/categories-tester-seed-data.js +6 -0
- package/dist/features/tester/seed-data/products-tester-seed-data.js +150 -1
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +118 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/next/routing/route-map.d.ts +12 -0
- package/dist/next/routing/route-map.js +6 -0
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +1 -1
- package/dist/server.d.ts +3 -1
- package/dist/server.js +5 -1
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/package.json +1 -1
|
@@ -210,6 +210,27 @@ async function fetchCategoryTypeUrls(baseUrl, categoryType, buildUrl, label) {
|
|
|
210
210
|
const fetchCategoryUrls = (baseUrl) => fetchCategoryTypeUrls(baseUrl, "listing", (slug) => ROUTES.PUBLIC.CATEGORY_DETAIL(slug), "category");
|
|
211
211
|
const fetchBrandUrls = (baseUrl) => fetchCategoryTypeUrls(baseUrl, "brand", (slug) => ROUTES.PUBLIC.BRAND_DETAIL(slug), "brand");
|
|
212
212
|
const fetchBundleUrls = (baseUrl) => fetchCategoryTypeUrls(baseUrl, "bundle", (slug) => ROUTES.PUBLIC.BUNDLE_DETAIL(slug), "bundle");
|
|
213
|
+
/**
|
|
214
|
+
* `groupedListings` documents. `listSitemapGroupedListings` has existed since
|
|
215
|
+
* the feature was built, but there was no public page for it to point at until
|
|
216
|
+
* `/groups/{slug}` — so it was never wired into the sitemap.
|
|
217
|
+
*/
|
|
218
|
+
async function fetchGroupedListingUrls(baseUrl) {
|
|
219
|
+
try {
|
|
220
|
+
const { listSitemapGroupedListings } = await import("../grouped/data");
|
|
221
|
+
const groups = await listSitemapGroupedListings();
|
|
222
|
+
return groups.map((g) => ({
|
|
223
|
+
url: `${baseUrl}${ROUTES.PUBLIC.GROUP_DETAIL(g.slug)}`,
|
|
224
|
+
lastModified: g.updatedAt,
|
|
225
|
+
changeFrequency: "weekly",
|
|
226
|
+
priority: 0.5,
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
void normalizeError(err);
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
213
234
|
async function fetchBlogPostUrls(baseUrl) {
|
|
214
235
|
try {
|
|
215
236
|
const db = getAdminDb();
|
|
@@ -293,11 +314,12 @@ async function fetchScammerUrls(baseUrl) {
|
|
|
293
314
|
}
|
|
294
315
|
}
|
|
295
316
|
export async function buildSitemap({ baseUrl }) {
|
|
296
|
-
const [productUrls, categoryUrls, brandUrls, bundleUrls, eventUrls, blogUrls, auctionUrls, preOrderUrls, prizeDrawUrls, classifiedUrls, digitalCodeUrls, liveUrls, storeUrls, scammerUrls,] = await Promise.all([
|
|
317
|
+
const [productUrls, categoryUrls, brandUrls, bundleUrls, groupedListingUrls, eventUrls, blogUrls, auctionUrls, preOrderUrls, prizeDrawUrls, classifiedUrls, digitalCodeUrls, liveUrls, storeUrls, scammerUrls,] = await Promise.all([
|
|
297
318
|
fetchProductUrls(baseUrl),
|
|
298
319
|
fetchCategoryUrls(baseUrl),
|
|
299
320
|
fetchBrandUrls(baseUrl),
|
|
300
321
|
fetchBundleUrls(baseUrl),
|
|
322
|
+
fetchGroupedListingUrls(baseUrl),
|
|
301
323
|
fetchEventUrls(baseUrl),
|
|
302
324
|
fetchBlogPostUrls(baseUrl),
|
|
303
325
|
fetchAuctionUrls(baseUrl),
|
|
@@ -314,6 +336,7 @@ export async function buildSitemap({ baseUrl }) {
|
|
|
314
336
|
...categoryUrls,
|
|
315
337
|
...brandUrls,
|
|
316
338
|
...bundleUrls,
|
|
339
|
+
...groupedListingUrls,
|
|
317
340
|
...blogUrls,
|
|
318
341
|
...productUrls,
|
|
319
342
|
...auctionUrls,
|
|
@@ -17,13 +17,14 @@ import type { ProductDocument } from "../../../features/products/schemas/firesto
|
|
|
17
17
|
* copies of this function looked at `lockedPrice`, so an accepted offer was
|
|
18
18
|
* billed at the seller's listed price while the cart UI displayed the agreed
|
|
19
19
|
* one (`item.lockedPrice ?? item.price`) — the buyer saw ₹X and paid ₹Y.
|
|
20
|
-
* 2. **A
|
|
20
|
+
* 2. **A bundle line** — `item.price` is the bundle's own locked price, which
|
|
21
|
+
* is deliberately LESS than the sum of its members. Checked before the
|
|
22
|
+
* members branch for exactly that reason. Covers both modern bundle lines
|
|
23
|
+
* (`lineKind: "bundle"`) and legacy ones (SB-UNI-5 2026-05-13).
|
|
24
|
+
* 3. **A grouped line** — the sum of its members' own snapshotted prices,
|
|
21
25
|
* weighted by each member's per-copy quantity. A `"group"` line pins
|
|
22
26
|
* `item.quantity` to 1, so this is both the unit price and the line total;
|
|
23
27
|
* see the invariant on `CartLineMember`.
|
|
24
|
-
* 3. **A legacy bundle line** (SB-UNI-5 2026-05-13) — written before
|
|
25
|
-
* `groupMembers` existed. `item.price` is the bundle price locked at
|
|
26
|
-
* add-time.
|
|
27
28
|
* 4. **Everything else** — the live Firestore price, so a stale cart-cached
|
|
28
29
|
* price is never charged on a COD/UPI order.
|
|
29
30
|
*
|
|
@@ -44,6 +45,64 @@ export declare function unitPriceFor(item: CartItemDocument, product: ProductDoc
|
|
|
44
45
|
* itself — that multiplication is where the six hand-rolled copies diverged.
|
|
45
46
|
*/
|
|
46
47
|
export declare function lineTotalFor(item: CartItemDocument, product: ProductDocument | null): number;
|
|
48
|
+
/** One taxable slice of a line. A single-product line has exactly one. */
|
|
49
|
+
export interface LineTaxComponent {
|
|
50
|
+
productId: string;
|
|
51
|
+
/** Rupees this slice is taxed on. Slices of a line sum to its line total. */
|
|
52
|
+
taxable: number;
|
|
53
|
+
gstRate: number;
|
|
54
|
+
hsnCode?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Split a line into its taxable slices.
|
|
58
|
+
*
|
|
59
|
+
* GST cannot go through `unitPriceFor`: that returns a scalar, and a
|
|
60
|
+
* multi-member line genuinely has SEVERAL rates — a ₹200 sticker at 12% next to
|
|
61
|
+
* a ₹2,000 top at 18% inside one selection. A scalar can only ever carry one of
|
|
62
|
+
* them, which is why a bundle line has paid **zero** GST for its whole
|
|
63
|
+
* existence: `previewCheckoutPricing` looked its product up by `item.productId`,
|
|
64
|
+
* which on a bundle is a CATEGORY id, so `product` was null and
|
|
65
|
+
* `product?.gstRate ?? 0` quietly resolved to 0.
|
|
66
|
+
*
|
|
67
|
+
* Three shapes:
|
|
68
|
+
*
|
|
69
|
+
* - **Plain line** — one slice, taxed on the whole line total. Identical to
|
|
70
|
+
* what the two GST loops did before.
|
|
71
|
+
* - **Grouped line** — one slice per member, each taxed on its own
|
|
72
|
+
* `unitPrice × member.quantity × item.quantity`. Slices sum to the line
|
|
73
|
+
* total exactly, because a grouped line has no price of its own.
|
|
74
|
+
* - **Bundle line** — the taxable base is the bundle's LOCKED price, not the
|
|
75
|
+
* sum of the members' list prices, so each member's slice is a prorated
|
|
76
|
+
* share of it (see `allocateBundlePrice`). Charging tax on the undiscounted
|
|
77
|
+
* total would tax the buyer on money they never paid.
|
|
78
|
+
*
|
|
79
|
+
* `productById` supplies the live `gstRate`; each member's snapshotted rate is
|
|
80
|
+
* the fallback for a member whose listing has since been archived.
|
|
81
|
+
*/
|
|
82
|
+
export declare function lineTaxComponentsFor(item: CartItemDocument, productById: Map<string, ProductDocument>): LineTaxComponent[];
|
|
83
|
+
/**
|
|
84
|
+
* Total GST for one order group, and the base it was charged on.
|
|
85
|
+
*
|
|
86
|
+
* Exists so `createOrderForGroup` (what the buyer is CHARGED) and
|
|
87
|
+
* `previewCheckoutPricing` (what the buyer is SHOWN) cannot disagree. They were
|
|
88
|
+
* two hand-written loops, and Root Cause #59 is precisely the pattern of fixing
|
|
89
|
+
* one and not the other — here that would mean quoting one tax figure and
|
|
90
|
+
* charging another.
|
|
91
|
+
*/
|
|
92
|
+
export declare function sumGroupGst(items: readonly CartItemDocument[], productById: Map<string, ProductDocument>, intraState: boolean): {
|
|
93
|
+
taxableAmount: number;
|
|
94
|
+
gstAmount: number;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Split `total` across members in proportion to their list prices, with the
|
|
98
|
+
* rounding remainder pushed onto the LAST member so the parts sum to exactly
|
|
99
|
+
* `total`, to the last decimal place. Rounding each share independently leaves
|
|
100
|
+
* an order whose `taxableAmount` doesn't reconcile with the sum of its lines.
|
|
101
|
+
*
|
|
102
|
+
* Falls back to an even split when every weight is zero (a bundle of free
|
|
103
|
+
* items), which is the only case where proportion is undefined.
|
|
104
|
+
*/
|
|
105
|
+
export declare function allocateAcrossMembers(total: number, weights: number[]): number[];
|
|
47
106
|
/**
|
|
48
107
|
* P-6 — pre-order groups charge each product's own `preOrderDepositPercent`
|
|
49
108
|
* (falling back to the generic COD deposit % when a product doesn't have one
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { roundRupees } from "../../../utils/number.formatter";
|
|
2
|
+
import { calculateGst } from "../fees/calculator";
|
|
2
3
|
/**
|
|
3
4
|
* The single source of truth for "what do we charge for ONE COPY of this line".
|
|
4
5
|
*
|
|
@@ -10,13 +11,14 @@ import { roundRupees } from "../../../utils/number.formatter";
|
|
|
10
11
|
* copies of this function looked at `lockedPrice`, so an accepted offer was
|
|
11
12
|
* billed at the seller's listed price while the cart UI displayed the agreed
|
|
12
13
|
* one (`item.lockedPrice ?? item.price`) — the buyer saw ₹X and paid ₹Y.
|
|
13
|
-
* 2. **A
|
|
14
|
+
* 2. **A bundle line** — `item.price` is the bundle's own locked price, which
|
|
15
|
+
* is deliberately LESS than the sum of its members. Checked before the
|
|
16
|
+
* members branch for exactly that reason. Covers both modern bundle lines
|
|
17
|
+
* (`lineKind: "bundle"`) and legacy ones (SB-UNI-5 2026-05-13).
|
|
18
|
+
* 3. **A grouped line** — the sum of its members' own snapshotted prices,
|
|
14
19
|
* weighted by each member's per-copy quantity. A `"group"` line pins
|
|
15
20
|
* `item.quantity` to 1, so this is both the unit price and the line total;
|
|
16
21
|
* see the invariant on `CartLineMember`.
|
|
17
|
-
* 3. **A legacy bundle line** (SB-UNI-5 2026-05-13) — written before
|
|
18
|
-
* `groupMembers` existed. `item.price` is the bundle price locked at
|
|
19
|
-
* add-time.
|
|
20
22
|
* 4. **Everything else** — the live Firestore price, so a stale cart-cached
|
|
21
23
|
* price is never charged on a COD/UPI order.
|
|
22
24
|
*
|
|
@@ -34,12 +36,18 @@ export function unitPriceFor(item, product) {
|
|
|
34
36
|
if (typeof item.lockedPrice === "number" && item.lockedPrice > 0) {
|
|
35
37
|
return item.lockedPrice;
|
|
36
38
|
}
|
|
39
|
+
// A BUNDLE is priced as a whole, and that price is the entire point of it
|
|
40
|
+
// being a bundle — it is deliberately less than the sum of its members. This
|
|
41
|
+
// branch must come BEFORE the members branch: a bundle line carries
|
|
42
|
+
// `groupMembers` too (so the cart can show what's inside and checkout can
|
|
43
|
+
// prorate GST), and summing them would silently charge the undiscounted total.
|
|
44
|
+
if (item.lineKind === "bundle" || (item.bundleCategorySlug && item.bundleProductIds?.length)) {
|
|
45
|
+
return item.price;
|
|
46
|
+
}
|
|
47
|
+
// A GROUP line has no price of its own — it is exactly what the buyer picked.
|
|
37
48
|
if (item.groupMembers?.length) {
|
|
38
49
|
return roundRupees(item.groupMembers.reduce((sum, m) => sum + m.unitPrice * m.quantity, 0));
|
|
39
50
|
}
|
|
40
|
-
if (item.bundleCategorySlug && item.bundleProductIds?.length) {
|
|
41
|
-
return item.price;
|
|
42
|
-
}
|
|
43
51
|
// Null-safe fallback: one of the four call sites this replaced (the cart
|
|
44
52
|
// summary/preview path) already guarded with `product?.price ?? item.price`,
|
|
45
53
|
// and collapsing onto a non-null assertion would have regressed it into a
|
|
@@ -54,6 +62,110 @@ export function unitPriceFor(item, product) {
|
|
|
54
62
|
export function lineTotalFor(item, product) {
|
|
55
63
|
return unitPriceFor(item, product) * item.quantity;
|
|
56
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Split a line into its taxable slices.
|
|
67
|
+
*
|
|
68
|
+
* GST cannot go through `unitPriceFor`: that returns a scalar, and a
|
|
69
|
+
* multi-member line genuinely has SEVERAL rates — a ₹200 sticker at 12% next to
|
|
70
|
+
* a ₹2,000 top at 18% inside one selection. A scalar can only ever carry one of
|
|
71
|
+
* them, which is why a bundle line has paid **zero** GST for its whole
|
|
72
|
+
* existence: `previewCheckoutPricing` looked its product up by `item.productId`,
|
|
73
|
+
* which on a bundle is a CATEGORY id, so `product` was null and
|
|
74
|
+
* `product?.gstRate ?? 0` quietly resolved to 0.
|
|
75
|
+
*
|
|
76
|
+
* Three shapes:
|
|
77
|
+
*
|
|
78
|
+
* - **Plain line** — one slice, taxed on the whole line total. Identical to
|
|
79
|
+
* what the two GST loops did before.
|
|
80
|
+
* - **Grouped line** — one slice per member, each taxed on its own
|
|
81
|
+
* `unitPrice × member.quantity × item.quantity`. Slices sum to the line
|
|
82
|
+
* total exactly, because a grouped line has no price of its own.
|
|
83
|
+
* - **Bundle line** — the taxable base is the bundle's LOCKED price, not the
|
|
84
|
+
* sum of the members' list prices, so each member's slice is a prorated
|
|
85
|
+
* share of it (see `allocateBundlePrice`). Charging tax on the undiscounted
|
|
86
|
+
* total would tax the buyer on money they never paid.
|
|
87
|
+
*
|
|
88
|
+
* `productById` supplies the live `gstRate`; each member's snapshotted rate is
|
|
89
|
+
* the fallback for a member whose listing has since been archived.
|
|
90
|
+
*/
|
|
91
|
+
export function lineTaxComponentsFor(item, productById) {
|
|
92
|
+
const lineTotal = lineTotalFor(item, productById.get(item.productId) ?? null);
|
|
93
|
+
const members = item.groupMembers;
|
|
94
|
+
if (!members?.length) {
|
|
95
|
+
const product = productById.get(item.productId);
|
|
96
|
+
return [{
|
|
97
|
+
productId: item.productId,
|
|
98
|
+
taxable: lineTotal,
|
|
99
|
+
gstRate: product?.gstRate ?? 0,
|
|
100
|
+
hsnCode: product?.hsnCode,
|
|
101
|
+
}];
|
|
102
|
+
}
|
|
103
|
+
const rateFor = (m) => productById.get(m.productId)?.gstRate ?? m.gstRate ?? 0;
|
|
104
|
+
const hsnFor = (m) => productById.get(m.productId)?.hsnCode ?? m.hsnCode;
|
|
105
|
+
const isBundle = item.lineKind === "bundle" || Boolean(item.bundleCategorySlug);
|
|
106
|
+
if (!isBundle) {
|
|
107
|
+
return members.map((m) => ({
|
|
108
|
+
productId: m.productId,
|
|
109
|
+
taxable: roundRupees(m.unitPrice * m.quantity * item.quantity),
|
|
110
|
+
gstRate: rateFor(m),
|
|
111
|
+
hsnCode: hsnFor(m),
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
const shares = allocateAcrossMembers(lineTotal, members.map((m) => m.unitPrice * m.quantity));
|
|
115
|
+
return members.map((m, i) => ({
|
|
116
|
+
productId: m.productId,
|
|
117
|
+
taxable: shares[i],
|
|
118
|
+
gstRate: rateFor(m),
|
|
119
|
+
hsnCode: hsnFor(m),
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Total GST for one order group, and the base it was charged on.
|
|
124
|
+
*
|
|
125
|
+
* Exists so `createOrderForGroup` (what the buyer is CHARGED) and
|
|
126
|
+
* `previewCheckoutPricing` (what the buyer is SHOWN) cannot disagree. They were
|
|
127
|
+
* two hand-written loops, and Root Cause #59 is precisely the pattern of fixing
|
|
128
|
+
* one and not the other — here that would mean quoting one tax figure and
|
|
129
|
+
* charging another.
|
|
130
|
+
*/
|
|
131
|
+
export function sumGroupGst(items, productById, intraState) {
|
|
132
|
+
let taxableAmount = 0;
|
|
133
|
+
let gstAmount = 0;
|
|
134
|
+
for (const item of items) {
|
|
135
|
+
for (const slice of lineTaxComponentsFor(item, productById)) {
|
|
136
|
+
if (slice.gstRate <= 0)
|
|
137
|
+
continue;
|
|
138
|
+
taxableAmount += slice.taxable;
|
|
139
|
+
gstAmount += calculateGst(slice.gstRate, intraState, slice.taxable).gstAmount;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { taxableAmount, gstAmount };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Split `total` across members in proportion to their list prices, with the
|
|
146
|
+
* rounding remainder pushed onto the LAST member so the parts sum to exactly
|
|
147
|
+
* `total`, to the last decimal place. Rounding each share independently leaves
|
|
148
|
+
* an order whose `taxableAmount` doesn't reconcile with the sum of its lines.
|
|
149
|
+
*
|
|
150
|
+
* Falls back to an even split when every weight is zero (a bundle of free
|
|
151
|
+
* items), which is the only case where proportion is undefined.
|
|
152
|
+
*/
|
|
153
|
+
export function allocateAcrossMembers(total, weights) {
|
|
154
|
+
if (weights.length === 0)
|
|
155
|
+
return [];
|
|
156
|
+
const sum = weights.reduce((a, b) => a + b, 0);
|
|
157
|
+
const out = [];
|
|
158
|
+
let running = 0;
|
|
159
|
+
for (let i = 0; i < weights.length - 1; i++) {
|
|
160
|
+
const share = sum > 0
|
|
161
|
+
? roundRupees((total * weights[i]) / sum) // audit-money-units-ok: proportional split of decimal rupees
|
|
162
|
+
: roundRupees(total / weights.length);
|
|
163
|
+
out.push(share);
|
|
164
|
+
running += share;
|
|
165
|
+
}
|
|
166
|
+
out.push(roundRupees(total - running));
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
57
169
|
/**
|
|
58
170
|
* P-6 — pre-order groups charge each product's own `preOrderDepositPercent`
|
|
59
171
|
* (falling back to the generic COD deposit % when a product doesn't have one
|
package/dist/client.d.ts
CHANGED
|
@@ -225,12 +225,14 @@ export { ProtectedRoute, AuthStatusPanel, ForgotPasswordView, LoginForm, Registe
|
|
|
225
225
|
export type { AuthGuardUser, ForgotPasswordViewProps, LoginFormProps, LoginFormValues, RegisterFormProps, RegisterFormValues, ResetPasswordViewProps, VerifyEmailViewProps, } from "./features/auth/index";
|
|
226
226
|
export { useLogout, useLogin, useGoogleLogin, useLinkGoogleAccount, useRegister, useForgotPassword, useResetPassword, useVerifyEmail, useChangeEmail } from "./features/auth/index";
|
|
227
227
|
export type { LoginCredentials, RegisterData, ForgotPasswordData, ResetPasswordData, VerifyEmailData, ChangeEmailData } from "./features/auth/index";
|
|
228
|
-
export { CartView, CartItemRow, CartSummary, CartPriceBreakdown, CartDrawer, CheckoutView, CheckoutSuccessView, CheckoutAddressStep, useGuestCart, useCartCount, useAddToCart, useCart, useGuestCartMerge, useCartQuery } from "./features/cart/index";
|
|
228
|
+
export { CartView, CartItemRow, CartGroupLineRow, CartSummary, CartPriceBreakdown, CartDrawer, CheckoutView, CheckoutSuccessView, CheckoutAddressStep, useGuestCart, useCartCount, useAddToCart, useCart, useGuestCartMerge, useCartQuery } from "./features/cart/index";
|
|
229
229
|
export { StoreAddonsPicker } from "./features/cart/index";
|
|
230
230
|
export type { CartPriceBreakdownData, CartPriceBreakdownStore, StoreAddonsValue, StoreAddonsRates } from "./features/cart/index";
|
|
231
231
|
export { getCartOps, CART_OPS_CHANGE_EVENT } from "./features/cart/utils/pending-ops";
|
|
232
232
|
export type { CartOp } from "./features/cart/utils/pending-ops";
|
|
233
233
|
export type { CartItem, CartItemMeta, CartData, GuestCartItem } from "./features/cart/index";
|
|
234
|
+
export type { CartLineKind, CartGroupSource, CartLineMember, } from "./features/cart/schemas/firestore";
|
|
235
|
+
export type { CartGroupLineRowProps } from "./features/cart/components/CartGroupLineRow";
|
|
234
236
|
export { useAddresses, useCreateAddress, useUpdateAddress, useDeleteAddress, useSetDefaultAddress, useAddress } from "./features/account/index";
|
|
235
237
|
export type { Address, AddressFormData } from "./features/account/index";
|
|
236
238
|
export { AddressBook, AddressCard, AddressForm } from "./features/account/index";
|
|
@@ -262,6 +264,8 @@ export type { WishlistCapEventDetail } from "./features/wishlist/hooks/useWishli
|
|
|
262
264
|
export { getGuestWishlistItems, addToGuestWishlist, removeFromGuestWishlist, isInGuestWishlist, clearGuestWishlist, getGuestWishlistCount, getGuestWishlistByType, } from "./features/wishlist/utils/guest-wishlist";
|
|
263
265
|
export { InteractiveProductCard } from "./features/products/index";
|
|
264
266
|
export type { InteractiveProductCardProps } from "./features/products/index";
|
|
267
|
+
export { GroupedListingDetailView } from "./features/grouped/components/GroupedListingDetailView";
|
|
268
|
+
export type { GroupedListingDetailViewProps } from "./features/grouped/components/GroupedListingDetailView";
|
|
265
269
|
export { GroupMemberPicker } from "./features/products/components/GroupMemberPicker";
|
|
266
270
|
export type { GroupMemberPickerProps, GroupPickerMember, } from "./features/products/components/GroupMemberPicker";
|
|
267
271
|
export { MarketplaceBundleCard } from "./features/products/components/MarketplaceBundleCard";
|
|
@@ -420,8 +424,8 @@ export type { BecomeSellerResult, SellerCreateProductViewProps, SellerEditProduc
|
|
|
420
424
|
export { BecomeSellerView } from "./features/account/index";
|
|
421
425
|
export type { BidDocument } from "./features/auctions/index";
|
|
422
426
|
export type { ClaimedCouponDocument } from "./features/promotions/schemas";
|
|
423
|
-
export {
|
|
424
|
-
export type {
|
|
427
|
+
export { groupOrderItemsByLine } from "./features/orders/index";
|
|
428
|
+
export type { LineOrderGroup, OutOfStockPolicy } from "./features/orders/index";
|
|
425
429
|
export { MediaUploadField } from "./features/media/index";
|
|
426
430
|
export { PrizeRevealModal } from "./features/products/components/PrizeRevealModal";
|
|
427
431
|
export { useProduct } from "./features/products/index";
|
package/dist/client.js
CHANGED
|
@@ -260,7 +260,7 @@ export { PAGE_VIEW_ENTITY_TYPES } from "./features/analytics/types";
|
|
|
260
260
|
export { AuctionBidsTable } from "./features/auctions/components/AuctionBidsTable";
|
|
261
261
|
export { ProtectedRoute, AuthStatusPanel, ForgotPasswordView, LoginForm, RegisterForm, ResetPasswordView, VerifyEmailView } from "./features/auth/index";
|
|
262
262
|
export { useLogout, useLogin, useGoogleLogin, useLinkGoogleAccount, useRegister, useForgotPassword, useResetPassword, useVerifyEmail, useChangeEmail } from "./features/auth/index";
|
|
263
|
-
export { CartView, CartItemRow, CartSummary, CartPriceBreakdown, CartDrawer, CheckoutView, CheckoutSuccessView, CheckoutAddressStep, useGuestCart, useCartCount, useAddToCart, useCart, useGuestCartMerge, useCartQuery } from "./features/cart/index";
|
|
263
|
+
export { CartView, CartItemRow, CartGroupLineRow, CartSummary, CartPriceBreakdown, CartDrawer, CheckoutView, CheckoutSuccessView, CheckoutAddressStep, useGuestCart, useCartCount, useAddToCart, useCart, useGuestCartMerge, useCartQuery } from "./features/cart/index";
|
|
264
264
|
export { StoreAddonsPicker } from "./features/cart/index";
|
|
265
265
|
export { getCartOps, CART_OPS_CHANGE_EVENT } from "./features/cart/utils/pending-ops";
|
|
266
266
|
export { useAddresses, useCreateAddress, useUpdateAddress, useDeleteAddress, useSetDefaultAddress, useAddress } from "./features/account/index";
|
|
@@ -282,6 +282,7 @@ export { SyncManagerMount } from "./core/components/SyncManagerMount";
|
|
|
282
282
|
export { useWishlistCount, useWishlistCountWithLimit, WISHLIST_CAP_EVENT } from "./features/wishlist/hooks/useWishlistCount";
|
|
283
283
|
export { getGuestWishlistItems, addToGuestWishlist, removeFromGuestWishlist, isInGuestWishlist, clearGuestWishlist, getGuestWishlistCount, getGuestWishlistByType, } from "./features/wishlist/utils/guest-wishlist";
|
|
284
284
|
export { InteractiveProductCard } from "./features/products/index";
|
|
285
|
+
export { GroupedListingDetailView } from "./features/grouped/components/GroupedListingDetailView";
|
|
285
286
|
export { GroupMemberPicker } from "./features/products/components/GroupMemberPicker";
|
|
286
287
|
export { MarketplaceBundleCard } from "./features/products/components/MarketplaceBundleCard";
|
|
287
288
|
export { CompareOverlay } from "./features/products/components/CompareOverlay";
|
|
@@ -405,7 +406,7 @@ export { SpinWheelView, EventRaffleEntryForm } from "./features/events/index";
|
|
|
405
406
|
export { Card, CardBody } from "./ui/index";
|
|
406
407
|
export { SellerCouponsView, SellerCreateProductView, SellerEditProductView, useBecomeSeller, } from "./features/seller/index";
|
|
407
408
|
export { BecomeSellerView } from "./features/account/index";
|
|
408
|
-
export {
|
|
409
|
+
export { groupOrderItemsByLine } from "./features/orders/index";
|
|
409
410
|
export { MediaUploadField } from "./features/media/index";
|
|
410
411
|
export { PrizeRevealModal } from "./features/products/components/PrizeRevealModal";
|
|
411
412
|
export { useProduct } from "./features/products/index";
|
|
@@ -246,8 +246,6 @@ export declare const CART_ENDPOINTS: {
|
|
|
246
246
|
* discriminated union would put this feature's blast radius on it.
|
|
247
247
|
*/
|
|
248
248
|
readonly GROUP: "/api/cart/group";
|
|
249
|
-
/** Replace a grouped line's member quantities (whole array, not a delta). */
|
|
250
|
-
readonly GROUP_MEMBERS: (itemId: string) => string;
|
|
251
249
|
};
|
|
252
250
|
export declare const CATEGORY_ENDPOINTS: {
|
|
253
251
|
readonly LIST: "/api/categories";
|
|
@@ -711,8 +709,6 @@ export declare const API_ENDPOINTS: {
|
|
|
711
709
|
* discriminated union would put this feature's blast radius on it.
|
|
712
710
|
*/
|
|
713
711
|
readonly GROUP: "/api/cart/group";
|
|
714
|
-
/** Replace a grouped line's member quantities (whole array, not a delta). */
|
|
715
|
-
readonly GROUP_MEMBERS: (itemId: string) => string;
|
|
716
712
|
};
|
|
717
713
|
readonly CATEGORIES: {
|
|
718
714
|
readonly LIST: "/api/categories";
|
|
@@ -1178,8 +1174,6 @@ export declare const API_ROUTES: {
|
|
|
1178
1174
|
* discriminated union would put this feature's blast radius on it.
|
|
1179
1175
|
*/
|
|
1180
1176
|
readonly GROUP: "/api/cart/group";
|
|
1181
|
-
/** Replace a grouped line's member quantities (whole array, not a delta). */
|
|
1182
|
-
readonly GROUP_MEMBERS: (itemId: string) => string;
|
|
1183
1177
|
};
|
|
1184
1178
|
readonly CATEGORIES: {
|
|
1185
1179
|
readonly LIST: "/api/categories";
|
|
@@ -286,8 +286,6 @@ export const CART_ENDPOINTS = {
|
|
|
286
286
|
* discriminated union would put this feature's blast radius on it.
|
|
287
287
|
*/
|
|
288
288
|
GROUP: "/api/cart/group",
|
|
289
|
-
/** Replace a grouped line's member quantities (whole array, not a delta). */
|
|
290
|
-
GROUP_MEMBERS: (itemId) => `/api/cart/${encodeURIComponent(itemId)}/members`,
|
|
291
289
|
};
|
|
292
290
|
// ---------------------------------------------------------------------------
|
|
293
291
|
// Categories
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* page-views.repository.ts (server-only, imports firebase-admin) so the
|
|
3
3
|
* client-side tracker component can import just this without pulling
|
|
4
4
|
* firebase-admin into the client bundle. */
|
|
5
|
-
export declare const PAGE_VIEW_ENTITY_TYPES: readonly ["product", "store", "category", "homepage", "auction", "review", "user-profile", "blog", "event", "pre-order", "prize-draw", "classified", "digital-code", "live", "bundle"];
|
|
5
|
+
export declare const PAGE_VIEW_ENTITY_TYPES: readonly ["product", "store", "category", "homepage", "auction", "review", "user-profile", "blog", "event", "pre-order", "prize-draw", "classified", "digital-code", "live", "bundle", "grouped-listing"];
|
|
6
6
|
export type PageViewEntityType = (typeof PAGE_VIEW_ENTITY_TYPES)[number];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CartGroupLineRow — one cart line that stands for several products.
|
|
3
|
+
*
|
|
4
|
+
* Two shapes behind one component, because the difference is exactly the
|
|
5
|
+
* bundle-vs-group distinction and putting them in separate files would let it
|
|
6
|
+
* drift:
|
|
7
|
+
*
|
|
8
|
+
* BUNDLE — all-or-nothing. Members render read-only; the only stepper is the
|
|
9
|
+
* line's own and it means COPIES OF THE BUNDLE.
|
|
10
|
+
* GROUP — pick-as-you-wish. One stepper PER MEMBER, and no line-level
|
|
11
|
+
* stepper at all (a group line's `quantity` is pinned to 1, so a
|
|
12
|
+
* second multiplier would double-count).
|
|
13
|
+
*
|
|
14
|
+
* Never render both steppers. That is the invariant on `CartLineMember`
|
|
15
|
+
* expressed in UI.
|
|
16
|
+
*/
|
|
17
|
+
import React from "react";
|
|
18
|
+
import type { CartLineMember } from "../schemas";
|
|
19
|
+
export interface CartGroupLineRowProps {
|
|
20
|
+
/** Line identity (`CartItemDocument.itemId`). */
|
|
21
|
+
id: string;
|
|
22
|
+
title: string;
|
|
23
|
+
image?: string;
|
|
24
|
+
currency?: string;
|
|
25
|
+
/** `"bundle"` → copies stepper; `"group"` → per-member steppers. */
|
|
26
|
+
lineKind: "bundle" | "group";
|
|
27
|
+
members: CartLineMember[];
|
|
28
|
+
/** Copies of the whole line. Bundles only — a group line is always 1. */
|
|
29
|
+
quantity: number;
|
|
30
|
+
/** Detail href for the bundle / group itself. */
|
|
31
|
+
href?: string;
|
|
32
|
+
locked?: boolean;
|
|
33
|
+
/** Bundle only: change the number of copies. */
|
|
34
|
+
onQtyChange?: (id: string, qty: number) => void;
|
|
35
|
+
/** Group only: change one member's per-copy quantity (0 removes it). */
|
|
36
|
+
onMemberQtyChange?: (id: string, productId: string, qty: number) => void;
|
|
37
|
+
onRemove?: (id: string) => void;
|
|
38
|
+
/** Per-member stock ceiling, keyed by productId. */
|
|
39
|
+
maxByProductId?: Record<string, number>;
|
|
40
|
+
variant?: "card" | "row";
|
|
41
|
+
}
|
|
42
|
+
export declare function CartGroupLineRow({ id, title, image, currency, lineKind, members, quantity, href, locked, onQtyChange, onMemberQtyChange, onRemove, maxByProductId, variant, }: CartGroupLineRowProps): React.JSX.Element;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* CartGroupLineRow — one cart line that stands for several products.
|
|
5
|
+
*
|
|
6
|
+
* Two shapes behind one component, because the difference is exactly the
|
|
7
|
+
* bundle-vs-group distinction and putting them in separate files would let it
|
|
8
|
+
* drift:
|
|
9
|
+
*
|
|
10
|
+
* BUNDLE — all-or-nothing. Members render read-only; the only stepper is the
|
|
11
|
+
* line's own and it means COPIES OF THE BUNDLE.
|
|
12
|
+
* GROUP — pick-as-you-wish. One stepper PER MEMBER, and no line-level
|
|
13
|
+
* stepper at all (a group line's `quantity` is pinned to 1, so a
|
|
14
|
+
* second multiplier would double-count).
|
|
15
|
+
*
|
|
16
|
+
* Never render both steppers. That is the invariant on `CartLineMember`
|
|
17
|
+
* expressed in UI.
|
|
18
|
+
*/
|
|
19
|
+
import { useState } from "react";
|
|
20
|
+
import { Button, Div, Row, Span, Stack, Text, TextLink } from "../../../ui";
|
|
21
|
+
import { QuantityStepper } from "../../../ui/components/QuantityStepper";
|
|
22
|
+
import { MediaImage } from "../../media/MediaImage";
|
|
23
|
+
import { formatCurrency } from "../../../utils/number.formatter";
|
|
24
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
25
|
+
const __O = { hidden: "overflow-hidden" };
|
|
26
|
+
export function CartGroupLineRow({ id, title, image, currency = "INR", lineKind, members, quantity, href, locked = false, onQtyChange, onMemberQtyChange, onRemove, maxByProductId, variant = "card", }) {
|
|
27
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
28
|
+
const isRow = variant === "row";
|
|
29
|
+
const onePrice = members.reduce((sum, m) => sum + m.unitPrice * m.quantity, 0);
|
|
30
|
+
const lineTotal = onePrice * quantity;
|
|
31
|
+
const unitCount = members.reduce((sum, m) => sum + m.quantity, 0);
|
|
32
|
+
return (_jsxs(Div, { layout: "flex", gap: isRow ? "3" : "4", surface: isRow ? "none" : "card", padding: isRow ? "none" : "sm", className: "min-w-0", children: [_jsx(Div, { surface: "muted", className: `relative ${isRow ? "h-16 w-16" : "h-20 w-20"} flex-shrink-0 ${__O.hidden}`, rounded: "lg", children: image && _jsx(MediaImage, { src: image, alt: title, size: "thumbnail" }) }), _jsxs(Stack, { justify: "between", gap: "xs", className: "flex-1 min-w-0", children: [_jsxs(Row, { gap: "xs", align: "start", className: "min-w-0", children: [href ? (_jsx(TextLink, { href: href, weight: "medium", className: "text-[var(--appkit-color-text)] hover:underline underline-offset-2 line-clamp-2", children: title })) : (_jsx(Text, { truncate: 2, weight: "medium", color: "primary", children: title })), _jsx(Span, { padding: "pill-2xs", surface: "subtle", color: "muted", rounded: "default", className: "flex-shrink-0 text-[10px] tracking-wide", transform: "uppercase", children: lineKind === "bundle" ? "Bundle" : "Group" })] }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setIsOpen((v) => !v), "aria-expanded": isOpen, className: "self-start text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline", children: [isOpen ? "▼" : "▶", " ", members.length, " item", members.length === 1 ? "" : "s", unitCount !== members.length ? ` · ${unitCount} units` : ""] }), isOpen && (_jsx(Stack, { gap: "xs", className: "w-full", children: members.map((m) => (_jsxs(Row, { gap: "sm", align: "center", justify: "between", className: "min-w-0", children: [_jsxs(Row, { gap: "sm", align: "center", className: "min-w-0 flex-1", children: [_jsx(Div, { className: `h-10 w-10 flex-shrink-0 ${__O.hidden}`, rounded: "default", surface: "muted", children: m.image && _jsx(MediaImage, { src: m.image, alt: m.title, size: "thumbnail" }) }), _jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsx(Text, { size: "sm", truncate: 1, color: "primary", children: m.title }), _jsxs(Text, { size: "xs", color: "muted", children: [formatCurrency(m.unitPrice, currency), lineKind === "bundle" ? ` × ${m.quantity}` : ""] })] })] }), lineKind === "group" && onMemberQtyChange && !locked && (_jsxs(Row, { gap: "xs", align: "center", className: "flex-shrink-0", children: [_jsx(QuantityStepper, { value: m.quantity, onChange: (next) => onMemberQtyChange(id, m.productId, next), min: 0, max: maxByProductId?.[m.productId], ariaLabel: `Quantity for ${m.title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": ACTIONS.CART["remove-group-member"].ariaLabel, onClick: () => onMemberQtyChange(id, m.productId, 0), className: "text-[var(--appkit-color-text-faint)] transition hover:text-error", children: "\u2715" })] })), lineKind === "bundle" && (_jsxs(Span, { size: "xs", color: "muted", className: "flex-shrink-0", children: ["\u00D7 ", m.quantity] }))] }, m.productId))) })), _jsxs(Row, { justify: "between", gap: "sm", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", className: "text-[var(--appkit-color-text)]", children: formatCurrency(lineTotal, currency) }), lineKind === "bundle" && onQtyChange && !locked && (_jsx(QuantityStepper, { value: quantity, onChange: (next) => onQtyChange(id, next), min: 1, ariaLabel: `Number of copies of ${title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel }))] })] }), onRemove && !locked && (_jsx(Button, { onClick: () => onRemove(id), variant: "ghost", size: "sm", "aria-label": ACTIONS.CART["remove-item"].ariaLabel, className: "self-start text-[var(--appkit-color-text-faint)] transition hover:text-error", children: "\u2715" }))] }));
|
|
33
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { CartItemRow, CartDrawer } from "./CartDrawer";
|
|
2
|
+
export { CartGroupLineRow } from "./CartGroupLineRow";
|
|
3
|
+
export type { CartGroupLineRowProps } from "./CartGroupLineRow";
|
|
2
4
|
export { CartView } from "./CartView";
|
|
3
5
|
export type { CartViewProps, CartOrderGroup } from "./CartView";
|
|
4
6
|
export { CartSummary } from "./CartSummary";
|
|
@@ -291,6 +291,12 @@ export class CartRepository extends BaseRepository {
|
|
|
291
291
|
throw new NotFoundError(ERR_CART_ITEM_NOT_FOUND);
|
|
292
292
|
if (cart.items[itemIndex].locked)
|
|
293
293
|
throw new ValidationError(ERR_CART_ITEM_LOCKED);
|
|
294
|
+
// A grouped line's quantity is pinned to 1 — the member quantities carry
|
|
295
|
+
// the whole selection, so a copies multiplier on top would double every
|
|
296
|
+
// price, tax and stock decrement. Edit its members instead.
|
|
297
|
+
if (cart.items[itemIndex].lineKind === "group") {
|
|
298
|
+
throw new ValidationError("Change the quantities of the individual items in this group instead.");
|
|
299
|
+
}
|
|
294
300
|
const items = [...cart.items];
|
|
295
301
|
items[itemIndex] = {
|
|
296
302
|
...items[itemIndex],
|
|
@@ -48,8 +48,13 @@ export interface CartLineMember {
|
|
|
48
48
|
unitPrice: number;
|
|
49
49
|
title: string;
|
|
50
50
|
image?: string;
|
|
51
|
-
/**
|
|
52
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Snapshot for per-member GST proration at checkout (`lineTaxComponentsFor`).
|
|
53
|
+
* Same literal union as `ProductDocument.gstRate` on purpose — a loose
|
|
54
|
+
* `number` here would not survive assignment to `OrderDocumentItem` and would
|
|
55
|
+
* only be fixed by a cast, which is how a wrong rate reaches an invoice.
|
|
56
|
+
*/
|
|
57
|
+
gstRate?: 0 | 5 | 12 | 18 | 28;
|
|
53
58
|
hsnCode?: string;
|
|
54
59
|
listingType?: ListingType;
|
|
55
60
|
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* BundleBuyNowCta —
|
|
2
|
+
* BundleBuyNowCta — purchase CTAs for bundle detail pages.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A bundle is ALL-OR-NOTHING: its members are not pickable, and the only
|
|
5
|
+
* quantity the buyer controls is how many COPIES of the whole bundle they want.
|
|
6
|
+
* That is why there is one stepper here and no per-member controls anywhere on
|
|
7
|
+
* the bundle page — the mirror image of `GroupMemberPicker`, which has
|
|
8
|
+
* per-member steppers and no line-level one.
|
|
9
|
+
*
|
|
10
|
+
* Two CTAs, not one. "Buy now" adds and jumps straight to checkout (the
|
|
11
|
+
* pre-existing express path). "Add to cart" adds and stays — without it a
|
|
12
|
+
* bundle could never sit in the cart to be edited, because the only route in
|
|
13
|
+
* pushed the buyer past the cart immediately.
|
|
7
14
|
*/
|
|
8
15
|
import React from "react";
|
|
9
16
|
export interface BundleBuyNowCtaProps {
|
|
@@ -11,8 +18,14 @@ export interface BundleBuyNowCtaProps {
|
|
|
11
18
|
outOfStock?: boolean;
|
|
12
19
|
onBuyNow: (input: {
|
|
13
20
|
bundleSlug: string;
|
|
21
|
+
quantity: number;
|
|
22
|
+
}) => Promise<unknown>;
|
|
23
|
+
/** Adds without leaving the page. Omit to render Buy-now only. */
|
|
24
|
+
onAddToCart?: (input: {
|
|
25
|
+
bundleSlug: string;
|
|
26
|
+
quantity: number;
|
|
14
27
|
}) => Promise<unknown>;
|
|
15
28
|
/** Renders a smaller single-line button suited for list cards. */
|
|
16
29
|
compact?: boolean;
|
|
17
30
|
}
|
|
18
|
-
export declare function BundleBuyNowCta({ bundleSlug, outOfStock, onBuyNow, compact, }: BundleBuyNowCtaProps): React.JSX.Element;
|
|
31
|
+
export declare function BundleBuyNowCta({ bundleSlug, outOfStock, onBuyNow, onAddToCart, compact, }: BundleBuyNowCtaProps): React.JSX.Element;
|