@mohasinac/appkit 4.11.3 → 4.12.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/checkout/actions.d.ts +36 -3
- package/dist/_internal/server/features/checkout/actions.js +122 -44
- package/dist/_internal/server/features/orders/adapters.js +13 -0
- package/dist/_internal/server/features/products/list-public.d.ts +28 -0
- package/dist/_internal/server/features/products/list-public.js +92 -0
- package/dist/_internal/server/jobs/core/auctionSettlement.js +5 -2
- package/dist/_internal/shared/checkout/lanes.d.ts +20 -0
- package/dist/_internal/shared/checkout/lanes.js +28 -0
- package/dist/_internal/shared/features/checkout/config.d.ts +4 -0
- package/dist/_internal/shared/features/checkout/config.js +8 -0
- package/dist/_internal/shared/fees/calculator.d.ts +34 -2
- package/dist/_internal/shared/fees/calculator.js +45 -1
- package/dist/client.d.ts +7 -1
- package/dist/client.js +4 -1
- package/dist/constants/api-endpoints.d.ts +6 -0
- package/dist/constants/api-endpoints.js +2 -0
- package/dist/features/admin/components/AdminAddressesView.js +18 -10
- package/dist/features/admin/components/AdminArtView.d.ts +9 -0
- package/dist/features/admin/components/AdminArtView.js +11 -35
- package/dist/features/admin/components/AdminClassifiedView.d.ts +9 -0
- package/dist/features/admin/components/AdminClassifiedView.js +11 -35
- package/dist/features/admin/components/AdminDigitalCodesView.d.ts +9 -0
- package/dist/features/admin/components/AdminDigitalCodesView.js +11 -42
- package/dist/features/admin/components/AdminLiveView.d.ts +9 -0
- package/dist/features/admin/components/AdminLiveView.js +11 -41
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +8 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +4 -3
- package/dist/features/admin/components/AdminOrdersView.js +14 -1
- package/dist/features/admin/components/AdminPaymentMethodsView.js +15 -10
- package/dist/features/admin/components/AdminSiteConfigGuideView.js +1 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +4 -2
- package/dist/features/admin/components/AdminStickersView.d.ts +9 -0
- package/dist/features/admin/components/AdminStickersView.js +11 -35
- package/dist/features/admin/hooks/useAdminListing.js +7 -2
- package/dist/features/admin/schemas/firestore.d.ts +5 -0
- package/dist/features/admin/schemas/firestore.js +1 -0
- package/dist/features/auctions/actions/bid-actions.js +2 -2
- package/dist/features/cart/components/CartDrawer.d.ts +9 -1
- package/dist/features/cart/components/CartDrawer.js +4 -3
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +61 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +25 -0
- package/dist/features/cart/components/StoreAddonsPicker.d.ts +47 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +21 -0
- package/dist/features/cart/components/index.d.ts +4 -0
- package/dist/features/cart/components/index.js +2 -0
- package/dist/features/cart/repository/cart.repository.d.ts +20 -2
- package/dist/features/cart/repository/cart.repository.js +49 -0
- package/dist/features/cart/schemas/firestore.d.ts +26 -0
- package/dist/features/layout/BottomActions.js +37 -4
- package/dist/features/layout/BottomActionsContext.d.ts +12 -0
- package/dist/features/layout/BottomActionsContext.js +5 -0
- package/dist/features/layout/hooks/useBottomActions.d.ts +6 -0
- package/dist/features/layout/hooks/useBottomActions.js +11 -1
- package/dist/features/orders/components/OrderAddonBadges.d.ts +36 -0
- package/dist/features/orders/components/OrderAddonBadges.js +28 -0
- package/dist/features/orders/components/index.d.ts +1 -0
- package/dist/features/orders/components/index.js +1 -0
- package/dist/features/orders/repository/orders.repository.d.ts +32 -0
- package/dist/features/orders/repository/orders.repository.js +14 -0
- package/dist/features/orders/types/index.d.ts +22 -0
- package/dist/features/products/components/ArtStickersListView.js +5 -4
- package/dist/features/products/components/PrizeDrawsListingView.js +18 -33
- package/dist/features/products/components/ProductsIndexListing.js +87 -11
- package/dist/features/products/components/ProductsIndexPageView.js +14 -2
- package/dist/features/products/config/listing-type-listing-config.d.ts +31 -0
- package/dist/features/products/config/listing-type-listing-config.js +74 -0
- package/dist/features/products/hooks/useProducts.js +14 -0
- package/dist/features/products/types/index.d.ts +15 -0
- package/dist/features/seller/components/SellerOrdersView.js +2 -1
- package/dist/features/stores/components/StoreArtStickersPageView.d.ts +21 -0
- package/dist/features/stores/components/StoreArtStickersPageView.js +30 -0
- package/dist/features/stores/components/StoreAuctionsPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreAuctionsPageView.js +17 -17
- package/dist/features/stores/components/StoreClassifiedsListing.js +7 -0
- package/dist/features/stores/components/StoreClassifiedsPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreClassifiedsPageView.js +16 -12
- package/dist/features/stores/components/StoreDetailLayoutView.js +4 -3
- package/dist/features/stores/components/StoreDigitalCodesListing.js +5 -0
- package/dist/features/stores/components/StoreDigitalCodesPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreDigitalCodesPageView.js +16 -12
- package/dist/features/stores/components/StoreLiveItemsListing.js +7 -0
- package/dist/features/stores/components/StoreLiveItemsPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreLiveItemsPageView.js +16 -12
- package/dist/features/stores/components/StorePreOrdersPageView.d.ts +4 -1
- package/dist/features/stores/components/StorePreOrdersPageView.js +17 -17
- package/dist/features/stores/components/StorePrizeDrawsPageView.d.ts +4 -8
- package/dist/features/stores/components/StorePrizeDrawsPageView.js +17 -22
- package/dist/features/stores/components/StoreProductsListing.d.ts +7 -1
- package/dist/features/stores/components/StoreProductsListing.js +3 -2
- package/dist/features/stores/components/StoreProductsPageView.d.ts +1 -1
- package/dist/features/stores/components/StoreProductsPageView.js +16 -21
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +602 -0
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +5 -0
- package/dist/seed/reviews-seed-data.js +52 -0
- package/dist/server-entry.d.ts +1 -0
- package/dist/server-entry.js +1 -0
- package/dist/styles.css +12 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Checkbox.d.ts +10 -1
- package/dist/ui/components/Checkbox.js +14 -2
- package/dist/ui/components/Checkbox.style.css +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Paid add-on checkboxes for ONE store.
|
|
4
|
+
*
|
|
5
|
+
* Add-on fees are billed per order group (= per store), so they are chosen per
|
|
6
|
+
* store: ticking "WhatsApp updates" for one seller must not bill it for every
|
|
7
|
+
* seller in the cart. Rendered inside that store's card in the cart, and inside
|
|
8
|
+
* that store's row of the checkout summary — Buy Now skips the cart entirely,
|
|
9
|
+
* so both surfaces have to be able to edit them.
|
|
10
|
+
*/
|
|
11
|
+
export interface StoreAddonsValue {
|
|
12
|
+
whatsappNotifyAddon?: boolean;
|
|
13
|
+
giftWrapAddon?: boolean;
|
|
14
|
+
giftWrapMessage?: string;
|
|
15
|
+
shipmentProtectionAddon?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** The subset of `siteSettings.commissions` that gates and prices the add-ons. */
|
|
18
|
+
export interface StoreAddonsRates {
|
|
19
|
+
whatsappNotifyFeeEnabled?: boolean;
|
|
20
|
+
whatsappNotifyFee?: number;
|
|
21
|
+
giftWrapFeeEnabled?: boolean;
|
|
22
|
+
giftWrapFee?: number;
|
|
23
|
+
shipmentProtectionFeeEnabled?: boolean;
|
|
24
|
+
shipmentProtectionFeePercent?: number;
|
|
25
|
+
shipmentProtectionFeeMin?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface StoreAddonsPickerProps {
|
|
28
|
+
storeId: string;
|
|
29
|
+
/** This store's subtotal — shipment protection is a percentage of it. */
|
|
30
|
+
storeSubtotal: number;
|
|
31
|
+
value: StoreAddonsValue;
|
|
32
|
+
onChange: (storeId: string, next: StoreAddonsValue) => void;
|
|
33
|
+
rates: StoreAddonsRates | null;
|
|
34
|
+
/**
|
|
35
|
+
* When true the controls render disabled with `disabledReason`. Used in the
|
|
36
|
+
* cart when none of this store's items are selected — the fees genuinely
|
|
37
|
+
* aren't being charged, and silently hiding the controls would read as the
|
|
38
|
+
* add-ons having vanished.
|
|
39
|
+
*/
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
disabledReason?: string;
|
|
42
|
+
/** Gift messages don't belong in a collapsed cart sheet — checkout only. */
|
|
43
|
+
showGiftMessage?: boolean;
|
|
44
|
+
currency?: string;
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function StoreAddonsPicker({ storeId, storeSubtotal, value, onChange, rates, disabled, disabledReason, showGiftMessage, currency, className, }: StoreAddonsPickerProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Gift, MessageCircle, ShieldCheck } from "lucide-react";
|
|
4
|
+
import { Div, Row, Stack, Text } from "../../../ui";
|
|
5
|
+
import { IconBox } from "../../../ui/components/IconBox";
|
|
6
|
+
import { FieldCheckbox } from "../../../ui/forms/FieldCheckbox";
|
|
7
|
+
import { FieldTextarea } from "../../../ui/forms/FieldTextarea";
|
|
8
|
+
import { formatCurrency } from "../../../utils/number.formatter";
|
|
9
|
+
export function StoreAddonsPicker({ storeId, storeSubtotal, value, onChange, rates, disabled = false, disabledReason, showGiftMessage = false, currency = "INR", className = "", }) {
|
|
10
|
+
const anyEnabled = !!rates &&
|
|
11
|
+
(rates.whatsappNotifyFeeEnabled ||
|
|
12
|
+
rates.giftWrapFeeEnabled ||
|
|
13
|
+
rates.shipmentProtectionFeeEnabled);
|
|
14
|
+
if (!anyEnabled)
|
|
15
|
+
return null;
|
|
16
|
+
const money = (value) => formatCurrency(value, currency);
|
|
17
|
+
const set = (patch) => onChange(storeId, { ...value, ...patch });
|
|
18
|
+
// Mirrors computeShipmentProtectionFee so the label states the real charge.
|
|
19
|
+
const protectionFee = Math.max(rates.shipmentProtectionFeeMin ?? 30, Math.round(storeSubtotal * ((rates.shipmentProtectionFeePercent ?? 2) / 100) * 100) / 100);
|
|
20
|
+
return (_jsxs(Stack, { gap: "xs", className: `min-w-0 ${className}`, children: [disabled && disabledReason && (_jsx(Text, { size: "xs", color: "muted", children: disabledReason })), rates.whatsappNotifyFeeEnabled && (_jsxs(Row, { gap: "sm", align: "start", className: "min-w-0", children: [_jsx(IconBox, { size: "sm", tone: "brand", children: _jsx(MessageCircle, { size: 14 }) }), _jsx(Div, { className: "flex-1 min-w-0", children: _jsx(FieldCheckbox, { name: `whatsappNotifyAddon-${storeId}`, label: `WhatsApp order updates (+${money(rates.whatsappNotifyFee ?? 10)})`, checked: !!value.whatsappNotifyAddon, onChange: (checked) => set({ whatsappNotifyAddon: checked }), disabled: disabled }) })] })), rates.giftWrapFeeEnabled && (_jsxs(Row, { gap: "sm", align: "start", className: "min-w-0", children: [_jsx(IconBox, { size: "sm", tone: "brand", children: _jsx(Gift, { size: 14 }) }), _jsxs(Stack, { gap: "xs", className: "flex-1 min-w-0", children: [_jsx(FieldCheckbox, { name: `giftWrapAddon-${storeId}`, label: `Gift wrap (+${money(rates.giftWrapFee ?? 49)})`, checked: !!value.giftWrapAddon, onChange: (checked) => set({ giftWrapAddon: checked }), disabled: disabled }), showGiftMessage && value.giftWrapAddon && (_jsx(FieldTextarea, { name: `giftWrapMessage-${storeId}`, label: "Gift message (optional)", value: value.giftWrapMessage ?? "", onChange: (next) => set({ giftWrapMessage: next.slice(0, 500) }), rows: 2, maxLength: 500, showCharCount: true, placeholder: "Add a note for the recipient\u2026", disabled: disabled }))] })] })), rates.shipmentProtectionFeeEnabled && storeSubtotal > 0 && (_jsxs(Row, { gap: "sm", align: "start", className: "min-w-0", children: [_jsx(IconBox, { size: "sm", tone: "brand", children: _jsx(ShieldCheck, { size: 14 }) }), _jsx(Div, { className: "flex-1 min-w-0", children: _jsx(FieldCheckbox, { name: `shipmentProtectionAddon-${storeId}`, label: `Shipment protection (+${money(protectionFee)})`, checked: !!value.shipmentProtectionAddon, onChange: (checked) => set({ shipmentProtectionAddon: checked }), disabled: disabled }) })] }))] }));
|
|
21
|
+
}
|
|
@@ -3,6 +3,10 @@ export { CartView } from "./CartView";
|
|
|
3
3
|
export type { CartViewProps, CartOrderGroup } from "./CartView";
|
|
4
4
|
export { CartSummary } from "./CartSummary";
|
|
5
5
|
export type { CartSummaryProps } from "./CartSummary";
|
|
6
|
+
export { StoreAddonsPicker } from "./StoreAddonsPicker";
|
|
7
|
+
export type { StoreAddonsPickerProps, StoreAddonsValue, StoreAddonsRates, } from "./StoreAddonsPicker";
|
|
8
|
+
export { CartPriceBreakdown } from "./CartPriceBreakdown";
|
|
9
|
+
export type { CartPriceBreakdownProps, CartPriceBreakdownData, CartPriceBreakdownStore, } from "./CartPriceBreakdown";
|
|
6
10
|
export { CheckoutView } from "./CheckoutView";
|
|
7
11
|
export type { CheckoutViewProps } from "./CheckoutView";
|
|
8
12
|
export { CheckoutAddressStep } from "./CheckoutAddressStep";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { CartItemRow, CartDrawer } from "./CartDrawer";
|
|
2
2
|
export { CartView } from "./CartView";
|
|
3
3
|
export { CartSummary } from "./CartSummary";
|
|
4
|
+
export { StoreAddonsPicker } from "./StoreAddonsPicker";
|
|
5
|
+
export { CartPriceBreakdown } from "./CartPriceBreakdown";
|
|
4
6
|
export { CheckoutView } from "./CheckoutView";
|
|
5
7
|
export { CheckoutAddressStep } from "./CheckoutAddressStep";
|
|
6
8
|
export { CheckoutSuccessView } from "./CheckoutSuccessView";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type DocumentReference } from "firebase-admin/firestore";
|
|
2
2
|
import type { DocumentSnapshot } from "../../../providers/db-firebase";
|
|
3
3
|
import { BaseRepository, type FirebaseSieveResult, type SieveModel } from "../../../providers/db-firebase";
|
|
4
|
-
import { type AddToCartInput, type CartAppliedCoupon, type CartDocument, type CartItemDocument, type UpdateCartItemInput } from "../schemas";
|
|
4
|
+
import { type AddToCartInput, type CartAppliedCoupon, type CartDocument, type CartStoreAddons, type CartItemDocument, type UpdateCartItemInput } from "../schemas";
|
|
5
5
|
export declare class CartRepository extends BaseRepository<CartDocument> {
|
|
6
6
|
private static readonly SIEVE_FIELDS;
|
|
7
7
|
constructor();
|
|
@@ -45,6 +45,24 @@ export declare class CartRepository extends BaseRepository<CartDocument> {
|
|
|
45
45
|
updateItemShipping(userId: string, itemId: string, providerId: string, fee: number): Promise<CartDocument>;
|
|
46
46
|
clearAllCoupons(userId: string): Promise<void>;
|
|
47
47
|
setSelectedItems(userId: string, itemIds: string[] | null): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Set (or clear) one store's add-on selections.
|
|
50
|
+
*
|
|
51
|
+
* Scoped to a single storeId so two stores' selections can't clobber each
|
|
52
|
+
* other, and written with a dotted field path so a concurrent write to a
|
|
53
|
+
* different store merges rather than overwriting the whole map.
|
|
54
|
+
*
|
|
55
|
+
* Passing an empty selection deletes the entry outright instead of leaving
|
|
56
|
+
* `{}` behind — an all-false record and an absent record mean the same thing,
|
|
57
|
+
* and keeping both shapes around invites readers that handle only one.
|
|
58
|
+
*/
|
|
59
|
+
setStoreAddons(userId: string, storeId: string, addons: CartStoreAddons | null): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Drop a store's add-on entry once its last item leaves the cart, so the map
|
|
62
|
+
* doesn't accumulate orphans. Cosmetic — an orphan is already unreachable,
|
|
63
|
+
* because a store with no items forms no order group.
|
|
64
|
+
*/
|
|
65
|
+
pruneStoreAddons(userId: string, keepStoreIds: string[]): Promise<void>;
|
|
48
66
|
/**
|
|
49
67
|
* Cloud Functions: return refs of stale carts not updated within TTL.
|
|
50
68
|
*/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
|
+
import { FieldValue } from "firebase-admin/firestore";
|
|
2
3
|
import { DatabaseError, NotFoundError, ValidationError } from "../../../errors";
|
|
3
4
|
import { CART_FIELDS } from "../../../constants/field-names";
|
|
4
5
|
import { normalizeError } from "../../../errors/normalize";
|
|
@@ -337,6 +338,54 @@ export class CartRepository extends BaseRepository {
|
|
|
337
338
|
.doc(userId)
|
|
338
339
|
.set({ selectedItemIds: itemIds ?? null, updatedAt: new Date() }, { merge: true });
|
|
339
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Set (or clear) one store's add-on selections.
|
|
343
|
+
*
|
|
344
|
+
* Scoped to a single storeId so two stores' selections can't clobber each
|
|
345
|
+
* other, and written with a dotted field path so a concurrent write to a
|
|
346
|
+
* different store merges rather than overwriting the whole map.
|
|
347
|
+
*
|
|
348
|
+
* Passing an empty selection deletes the entry outright instead of leaving
|
|
349
|
+
* `{}` behind — an all-false record and an absent record mean the same thing,
|
|
350
|
+
* and keeping both shapes around invites readers that handle only one.
|
|
351
|
+
*/
|
|
352
|
+
async setStoreAddons(userId, storeId, addons) {
|
|
353
|
+
const isEmpty = !addons ||
|
|
354
|
+
(!addons.whatsappNotifyAddon &&
|
|
355
|
+
!addons.giftWrapAddon &&
|
|
356
|
+
!addons.shipmentProtectionAddon);
|
|
357
|
+
await this.db
|
|
358
|
+
.collection(this.collection)
|
|
359
|
+
.doc(userId)
|
|
360
|
+
.set({
|
|
361
|
+
storeAddons: {
|
|
362
|
+
[storeId]: isEmpty ? FieldValue.delete() : addons,
|
|
363
|
+
},
|
|
364
|
+
updatedAt: new Date(),
|
|
365
|
+
}, { merge: true });
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Drop a store's add-on entry once its last item leaves the cart, so the map
|
|
369
|
+
* doesn't accumulate orphans. Cosmetic — an orphan is already unreachable,
|
|
370
|
+
* because a store with no items forms no order group.
|
|
371
|
+
*/
|
|
372
|
+
async pruneStoreAddons(userId, keepStoreIds) {
|
|
373
|
+
const cart = await this.findByUserId(userId);
|
|
374
|
+
const existing = cart?.storeAddons;
|
|
375
|
+
if (!existing)
|
|
376
|
+
return;
|
|
377
|
+
const keep = new Set(keepStoreIds);
|
|
378
|
+
const stale = Object.keys(existing).filter((storeId) => !keep.has(storeId));
|
|
379
|
+
if (stale.length === 0)
|
|
380
|
+
return;
|
|
381
|
+
await this.db
|
|
382
|
+
.collection(this.collection)
|
|
383
|
+
.doc(userId)
|
|
384
|
+
.set({
|
|
385
|
+
storeAddons: Object.fromEntries(stale.map((storeId) => [storeId, FieldValue.delete()])),
|
|
386
|
+
updatedAt: new Date(),
|
|
387
|
+
}, { merge: true });
|
|
388
|
+
}
|
|
340
389
|
/**
|
|
341
390
|
* Cloud Functions: return refs of stale carts not updated within TTL.
|
|
342
391
|
*/
|
|
@@ -83,6 +83,21 @@ export interface CartAppliedCoupon {
|
|
|
83
83
|
/** Item IDs (CartItemDocument.itemId) this coupon was calculated against */
|
|
84
84
|
applicableItemIds?: string[];
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Paid add-ons the buyer opted into, for ONE store.
|
|
88
|
+
*
|
|
89
|
+
* Add-on fees have always been billed per order group (= per store) by
|
|
90
|
+
* `createOrderForGroup` / `previewCheckoutPricing`, but the selection used to be
|
|
91
|
+
* a single cart-wide boolean — so one tick on "WhatsApp updates" quietly billed
|
|
92
|
+
* ₹10 × every store in the cart, with no way to opt in for just one seller.
|
|
93
|
+
* Keying the selection by storeId makes the choice match the billing.
|
|
94
|
+
*/
|
|
95
|
+
export interface CartStoreAddons {
|
|
96
|
+
whatsappNotifyAddon?: boolean;
|
|
97
|
+
giftWrapAddon?: boolean;
|
|
98
|
+
giftWrapMessage?: string;
|
|
99
|
+
shipmentProtectionAddon?: boolean;
|
|
100
|
+
}
|
|
86
101
|
export interface CartDocument extends BaseDocument {
|
|
87
102
|
userId: string;
|
|
88
103
|
items: CartItemDocument[];
|
|
@@ -90,6 +105,17 @@ export interface CartDocument extends BaseDocument {
|
|
|
90
105
|
appliedCoupons?: CartAppliedCoupon[];
|
|
91
106
|
/** Item IDs the user has selected for the next checkout (undefined = all items) */
|
|
92
107
|
selectedItemIds?: string[];
|
|
108
|
+
/**
|
|
109
|
+
* Per-store add-on selections, keyed by `storeId` — the same key
|
|
110
|
+
* `splitCartIntoOrderGroups` groups on.
|
|
111
|
+
*
|
|
112
|
+
* This map is the source of truth for what gets charged; the checkout request
|
|
113
|
+
* no longer carries addon booleans. A store with no selected items forms no
|
|
114
|
+
* order group, so its entry is simply never read — that is what makes
|
|
115
|
+
* "only charge a store whose items are actually checking out" structural
|
|
116
|
+
* rather than a flag someone has to remember to check.
|
|
117
|
+
*/
|
|
118
|
+
storeAddons?: Record<string, CartStoreAddons>;
|
|
93
119
|
}
|
|
94
120
|
export declare const CART_COLLECTION: "carts";
|
|
95
121
|
export declare const CART_INDEXED_FIELDS: string[];
|
|
@@ -29,7 +29,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
29
29
|
* // Automatically rendered by LayoutClient — no manual usage required.
|
|
30
30
|
* // Features use `useBottomActions` to register their actions.
|
|
31
31
|
*/
|
|
32
|
-
import { useState, useRef, useEffect } from "react";
|
|
32
|
+
import { useState, useRef, useEffect, useId } from "react";
|
|
33
33
|
import { X, ChevronUp, ChevronDown, Check } from "lucide-react";
|
|
34
34
|
import { useBottomActionsContext } from "./BottomActionsContext";
|
|
35
35
|
import { useClickOutside } from "../../react";
|
|
@@ -68,6 +68,27 @@ function BulkPickerPanel({ bulkActions, selectedActionId, pickerOpen, onSelect,
|
|
|
68
68
|
.join(" "), children: [action.icon && (_jsx(Span, { className: `flex-shrink-0 w-5 h-5 ${FLEX_CENTER}`, "aria-hidden": "true", children: action.icon })), _jsx(Span, { className: "flex-1 truncate", children: action.label }), isSelected && (_jsx(Check, { className: "w-4 h-4 flex-shrink-0 text-primary-600 dark:text-primary-400", "aria-hidden": "true" }))] }, action.id));
|
|
69
69
|
}) }));
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Expandable detail that opens UPWARD from the bar.
|
|
73
|
+
*
|
|
74
|
+
* Same recipe as BulkPickerPanel above — absolutely positioned at `bottom-full`
|
|
75
|
+
* so it grows into the space above the bar rather than pushing the page, and
|
|
76
|
+
* animated on max-height/opacity so it can't be tabbed into while closed.
|
|
77
|
+
* Scrolls internally, since a many-store cart's breakdown can outgrow the
|
|
78
|
+
* screen.
|
|
79
|
+
*/
|
|
80
|
+
function InfoPanel({ id, open, children, }) {
|
|
81
|
+
return (_jsx(Div, { id: id, className: [
|
|
82
|
+
"absolute bottom-full left-0 right-0 overflow-hidden",
|
|
83
|
+
BOTTOM_NAV_BG,
|
|
84
|
+
"border-t border-zinc-200/80 border-[var(--appkit-color-border)]/80",
|
|
85
|
+
"shadow-[0_-8px_24px_rgba(0,0,0,0.10)] dark:shadow-[0_-8px_24px_rgba(0,0,0,0.35)]",
|
|
86
|
+
"transition-[max-height,opacity] duration-200 ease-out",
|
|
87
|
+
open ? "max-h-[60vh] opacity-100" : "max-h-0 opacity-0 pointer-events-none",
|
|
88
|
+
]
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.join(" "), "aria-hidden": !open, children: _jsx(Div, { className: "max-h-[60vh]", overflow: "y-auto", padding: "md", children: children }) }));
|
|
91
|
+
}
|
|
71
92
|
function PageActionsRow({ pageActions, dispatchAction, }) {
|
|
72
93
|
return (_jsx(_Fragment, { children: pageActions.map((action) => {
|
|
73
94
|
const isIconOnly = !action.label;
|
|
@@ -86,10 +107,15 @@ function PageActionsRow({ pageActions, dispatchAction, }) {
|
|
|
86
107
|
export default function BottomActions() {
|
|
87
108
|
const { state, actionCallbacksRef, bulkCallbacksRef, bulkClearRef } = useBottomActionsContext();
|
|
88
109
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
110
|
+
const [infoOpen, setInfoOpen] = useState(false);
|
|
89
111
|
const [selectedActionId, setSelectedActionId] = useState(null);
|
|
90
112
|
const containerRef = useRef(null);
|
|
91
|
-
|
|
92
|
-
|
|
113
|
+
const infoPanelId = useId();
|
|
114
|
+
useClickOutside(containerRef, () => {
|
|
115
|
+
setPickerOpen(false);
|
|
116
|
+
setInfoOpen(false);
|
|
117
|
+
});
|
|
118
|
+
const { actions, bulk, infoLabel, secondaryLabel, infoPanel, desktop } = state;
|
|
93
119
|
// Desktop reveal-on-scroll. Kept as a plain passive listener to match BackToTop —
|
|
94
120
|
// there is no shared scroll-position hook in the codebase and one consumer doesn't
|
|
95
121
|
// justify inventing one. Only armed when a page actually opts into "after-scroll".
|
|
@@ -107,11 +133,18 @@ export default function BottomActions() {
|
|
|
107
133
|
}, [wantsScrollGate]);
|
|
108
134
|
const showOnDesktop = desktop === "always" || (wantsScrollGate && scrolledPastFold);
|
|
109
135
|
const isBulkMode = !!(bulk && bulk.selectedCount > 0);
|
|
136
|
+
const hasInfoPanel = infoPanel != null && !isBulkMode;
|
|
110
137
|
const bulkActions = bulk?.actions ?? [];
|
|
111
138
|
const pageActions = actions;
|
|
112
139
|
const isVisible = (isBulkMode ? bulkActions.length > 0 || !!bulk : pageActions.length > 0) ||
|
|
113
140
|
!!infoLabel ||
|
|
114
141
|
!!secondaryLabel;
|
|
142
|
+
// A panel left open after its content is withdrawn (bulk mode taking over, or
|
|
143
|
+
// the page clearing it) would sit there as an empty sheet.
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (!hasInfoPanel)
|
|
146
|
+
setInfoOpen(false);
|
|
147
|
+
}, [hasInfoPanel]);
|
|
115
148
|
// Keep selectedActionId in sync with available bulk actions
|
|
116
149
|
useEffect(() => {
|
|
117
150
|
if (!isBulkMode) {
|
|
@@ -151,7 +184,7 @@ export default function BottomActions() {
|
|
|
151
184
|
isVisible ? "translate-y-0" : "translate-y-full pointer-events-none",
|
|
152
185
|
]
|
|
153
186
|
.filter(Boolean)
|
|
154
|
-
.join(" "), "data-section": "bottomactions-div-401", children: [isBulkMode && (_jsx(BulkPickerPanel, { bulkActions: bulkActions, selectedActionId: selectedActionId, pickerOpen: pickerOpen, onSelect: (id) => { setSelectedActionId(id); setPickerOpen(false); } })), isBulkMode && (_jsx(Div, { className: "h-[3px] w-full bg-[image:var(--appkit-gradient-brand-tri)]" })), secondaryLabel && !isBulkMode && (_jsx(Div, { border: "subtle", className: "pt-[var(--appkit-space-2)] pb-[var(--appkit-space-0)] border-b /80", padding: "x-md", children: _jsx(Text, { className: "leading-5 truncate", color: "muted", size: "xs", weight: "semibold", children: secondaryLabel }) })), infoLabel && !isBulkMode && (_jsx(Div, { border: "subtle", className: "pt-[var(--appkit-space-2)] pb-[var(--appkit-space-0)] border-b /80", padding: "x-md", children: _jsx(Text, { className: "leading-5 truncate", color: "muted", size: "xs", weight: "semibold", children: infoLabel }) })), _jsx(Row, { className: `${BOTTOM_NAV_HEIGHT}`, gap: "sm", padding: "x-sm", children: isBulkMode && bulk ? (_jsxs(_Fragment, { children: [_jsxs(Button, { rounded: "full", gap: "xs", type: "button", variant: "ghost", onClick: dispatchBulkClear, className: "inline- flex-shrink-0 bg-primary-50 hover:bg-primary-100 active:bg-primary-200 dark:bg-primary-950/30 dark:hover:bg-primary-900/50 text-primary-700 dark:text-primary-300 pl-2 pr-3 h-8 border border-primary-200/70 dark:border-primary-800/50 transition-colors min-h-0", "aria-label": "Clear selection", children: [_jsx(X, { className: "w-3.5 h-3.5 flex-shrink-0", "aria-hidden": "true" }), _jsx(Span, { size: "xs", weight: "semibold", className: "tabular-nums whitespace-nowrap leading-none", children: bulk.noun
|
|
187
|
+
.join(" "), "data-section": "bottomactions-div-401", children: [isBulkMode && (_jsx(BulkPickerPanel, { bulkActions: bulkActions, selectedActionId: selectedActionId, pickerOpen: pickerOpen, onSelect: (id) => { setSelectedActionId(id); setPickerOpen(false); } })), hasInfoPanel && !isBulkMode && (_jsx(InfoPanel, { id: infoPanelId, open: infoOpen, children: infoPanel })), isBulkMode && (_jsx(Div, { className: "h-[3px] w-full bg-[image:var(--appkit-gradient-brand-tri)]" })), secondaryLabel && !isBulkMode && (_jsx(Div, { border: "subtle", className: "pt-[var(--appkit-space-2)] pb-[var(--appkit-space-0)] border-b /80", padding: "x-md", children: _jsx(Text, { className: "leading-5 truncate", color: "muted", size: "xs", weight: "semibold", children: secondaryLabel }) })), infoLabel && !isBulkMode && !hasInfoPanel && (_jsx(Div, { border: "subtle", className: "pt-[var(--appkit-space-2)] pb-[var(--appkit-space-0)] border-b /80", padding: "x-md", children: _jsx(Text, { className: "leading-5 truncate", color: "muted", size: "xs", weight: "semibold", children: infoLabel }) })), infoLabel && !isBulkMode && hasInfoPanel && (_jsxs(Button, { type: "button", variant: "ghost", onClick: () => setInfoOpen((o) => !o), "aria-expanded": infoOpen, "aria-controls": infoPanelId, className: "w-full flex items-center justify-between gap-[var(--appkit-space-2)] rounded-none border-b border-[var(--appkit-color-border)]/80 px-[var(--appkit-space-4)] pt-[var(--appkit-space-2)] pb-[var(--appkit-space-1)] min-h-0", children: [_jsx(Text, { className: "leading-5 truncate", color: "muted", size: "xs", weight: "semibold", children: infoLabel }), _jsxs(Span, { className: "flex-shrink-0 flex items-center gap-[var(--appkit-space-1)]", children: [_jsx(Text, { as: "span", size: "xs", color: "muted", children: infoOpen ? "Hide" : "Details" }), infoOpen ? (_jsx(ChevronDown, { className: "w-4 h-4 text-zinc-400", "aria-hidden": "true" })) : (_jsx(ChevronUp, { className: "w-4 h-4 text-zinc-400", "aria-hidden": "true" }))] })] })), _jsx(Row, { className: `${BOTTOM_NAV_HEIGHT}`, gap: "sm", padding: "x-sm", children: isBulkMode && bulk ? (_jsxs(_Fragment, { children: [_jsxs(Button, { rounded: "full", gap: "xs", type: "button", variant: "ghost", onClick: dispatchBulkClear, className: "inline- flex-shrink-0 bg-primary-50 hover:bg-primary-100 active:bg-primary-200 dark:bg-primary-950/30 dark:hover:bg-primary-900/50 text-primary-700 dark:text-primary-300 pl-2 pr-3 h-8 border border-primary-200/70 dark:border-primary-800/50 transition-colors min-h-0", "aria-label": "Clear selection", children: [_jsx(X, { className: "w-3.5 h-3.5 flex-shrink-0", "aria-hidden": "true" }), _jsx(Span, { size: "xs", weight: "semibold", className: "tabular-nums whitespace-nowrap leading-none", children: bulk.noun
|
|
155
188
|
? `${bulk.selectedCount} ${bulk.noun}`
|
|
156
189
|
: `${bulk.selectedCount} selected` })] }), bulkActions.length > 0 && (_jsxs(Button, { type: "button", variant: "ghost", onClick: () => setPickerOpen((o) => !o), "aria-haspopup": "listbox", "aria-expanded": pickerOpen, className: [
|
|
157
190
|
"flex-1 min-w-0 h-10 flex items-center gap-[var(--appkit-space-2)] px-[var(--appkit-space-3)] rounded-lg border text-[length:var(--appkit-text-sm)] font-medium transition-colors",
|
|
@@ -99,6 +99,16 @@ export interface BottomActionsState {
|
|
|
99
99
|
* the auction detail page's live countdown ("Ends in 2d 5h").
|
|
100
100
|
*/
|
|
101
101
|
secondaryLabel?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Optional expandable content revealed ABOVE the bar when the buyer taps the
|
|
104
|
+
* `infoLabel` row, which becomes a disclosure toggle while this is set.
|
|
105
|
+
*
|
|
106
|
+
* Exists so a summarised figure in `infoLabel` ("Total: ₹777.00") can be
|
|
107
|
+
* opened up into the detail behind it without navigating away — the mobile
|
|
108
|
+
* counterpart of a "Show details" expander in a desktop sidebar. Ignored in
|
|
109
|
+
* bulk mode, which owns the same space for its action picker.
|
|
110
|
+
*/
|
|
111
|
+
infoPanel?: React.ReactNode;
|
|
102
112
|
}
|
|
103
113
|
interface BottomActionsContextValue {
|
|
104
114
|
state: BottomActionsState;
|
|
@@ -119,6 +129,8 @@ interface BottomActionsContextValue {
|
|
|
119
129
|
setInfoLabel: (label: string | undefined) => void;
|
|
120
130
|
/** Set or clear the secondary label (rendered above infoLabel). */
|
|
121
131
|
setSecondaryLabel: (label: string | undefined) => void;
|
|
132
|
+
/** Set or clear the expandable panel revealed above the bar. */
|
|
133
|
+
setInfoPanel: (panel: React.ReactNode | undefined) => void;
|
|
122
134
|
/** Set the desktop visibility policy. */
|
|
123
135
|
setDesktopMode: (mode: BottomActionsDesktopMode | undefined) => void;
|
|
124
136
|
/** Clear all state (called on feature unmount). */
|
|
@@ -87,6 +87,9 @@ export function BottomActionsProvider({ children, }) {
|
|
|
87
87
|
const setSecondaryLabel = useCallback((secondaryLabel) => {
|
|
88
88
|
setState((prev) => ({ ...prev, secondaryLabel }));
|
|
89
89
|
}, []);
|
|
90
|
+
const setInfoPanel = useCallback((infoPanel) => {
|
|
91
|
+
setState((prev) => ({ ...prev, infoPanel }));
|
|
92
|
+
}, []);
|
|
90
93
|
const setDesktopMode = useCallback((desktop) => {
|
|
91
94
|
setState((prev) => (prev.desktop === desktop ? prev : { ...prev, desktop }));
|
|
92
95
|
}, []);
|
|
@@ -105,6 +108,7 @@ export function BottomActionsProvider({ children, }) {
|
|
|
105
108
|
bulkClearRef,
|
|
106
109
|
setInfoLabel,
|
|
107
110
|
setSecondaryLabel,
|
|
111
|
+
setInfoPanel,
|
|
108
112
|
setDesktopMode,
|
|
109
113
|
clearAll,
|
|
110
114
|
}), [
|
|
@@ -113,6 +117,7 @@ export function BottomActionsProvider({ children, }) {
|
|
|
113
117
|
setBulkConfig,
|
|
114
118
|
setInfoLabel,
|
|
115
119
|
setSecondaryLabel,
|
|
120
|
+
setInfoPanel,
|
|
116
121
|
setDesktopMode,
|
|
117
122
|
clearAll,
|
|
118
123
|
]);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type React from "react";
|
|
1
2
|
import { type BottomAction, type BottomActionsDesktopMode, type BottomBulkConfig } from "../BottomActionsContext";
|
|
2
3
|
export interface UseBottomActionsOptions {
|
|
3
4
|
actions?: BottomAction[];
|
|
@@ -5,6 +6,11 @@ export interface UseBottomActionsOptions {
|
|
|
5
6
|
infoLabel?: string;
|
|
6
7
|
/** Rendered as its own row above infoLabel. See BottomActionsState.secondaryLabel. */
|
|
7
8
|
secondaryLabel?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Expandable detail revealed above the bar when the infoLabel row is tapped.
|
|
11
|
+
* See BottomActionsState.infoPanel.
|
|
12
|
+
*/
|
|
13
|
+
infoPanel?: React.ReactNode;
|
|
8
14
|
/**
|
|
9
15
|
* Whether the bar may also show on desktop. Defaults to "hidden" (mobile/tablet only),
|
|
10
16
|
* so pages that don't opt in behave exactly as before.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
import { useBottomActionsContext, } from "../BottomActionsContext";
|
|
4
4
|
export function useBottomActions(options = {}) {
|
|
5
|
-
const { setActions, setBulkConfig, setInfoLabel, setSecondaryLabel, setDesktopMode, clearAll, } = useBottomActionsContext();
|
|
5
|
+
const { setActions, setBulkConfig, setInfoLabel, setSecondaryLabel, setInfoPanel, setDesktopMode, clearAll, } = useBottomActionsContext();
|
|
6
6
|
const actionsRef = useRef(options.actions ?? []);
|
|
7
7
|
actionsRef.current = options.actions ?? [];
|
|
8
8
|
const bulkRef = useRef(options.bulk);
|
|
@@ -25,6 +25,16 @@ export function useBottomActions(options = {}) {
|
|
|
25
25
|
useEffect(() => {
|
|
26
26
|
setSecondaryLabel(options.secondaryLabel);
|
|
27
27
|
}, [setSecondaryLabel, options.secondaryLabel]);
|
|
28
|
+
// A ReactNode is a fresh object on every render, so it can't be a dependency
|
|
29
|
+
// without looping. Mirror it through a ref and re-publish whenever anything
|
|
30
|
+
// the panel actually derives from changes — callers pass `infoLabel`, which
|
|
31
|
+
// moves with the same data the panel shows.
|
|
32
|
+
const infoPanelRef = useRef(options.infoPanel);
|
|
33
|
+
infoPanelRef.current = options.infoPanel;
|
|
34
|
+
const hasInfoPanel = options.infoPanel != null;
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
setInfoPanel(infoPanelRef.current);
|
|
37
|
+
}, [setInfoPanel, hasInfoPanel, options.infoLabel, options.secondaryLabel]);
|
|
28
38
|
useEffect(() => {
|
|
29
39
|
setDesktopMode(options.desktop);
|
|
30
40
|
}, [setDesktopMode, options.desktop]);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Icon badges for an order's paid add-ons and applied coupon.
|
|
4
|
+
*
|
|
5
|
+
* These are fulfilment instructions, not decoration: WhatsApp tells the
|
|
6
|
+
* status-change notifier to message this buyer, gift wrap tells the packer to
|
|
7
|
+
* wrap the parcel and enclose the message, shipment protection changes how a
|
|
8
|
+
* loss claim is handled. They are recorded on the order (and filterable in the
|
|
9
|
+
* `orders` Sieve config) so nobody has to keep a separate list of who asked for
|
|
10
|
+
* what — the order IS the record.
|
|
11
|
+
*
|
|
12
|
+
* Rendered on every order surface — buyer detail, seller list + detail, admin
|
|
13
|
+
* list + detail — from one implementation, so a new add-on shows up everywhere
|
|
14
|
+
* at once instead of being wired per screen.
|
|
15
|
+
*/
|
|
16
|
+
export interface OrderAddonBadgesOrder {
|
|
17
|
+
whatsappNotifyAddon?: boolean;
|
|
18
|
+
whatsappNotifyFee?: number;
|
|
19
|
+
giftWrapAddon?: boolean;
|
|
20
|
+
giftWrapFee?: number;
|
|
21
|
+
giftWrapMessage?: string;
|
|
22
|
+
shipmentProtectionAddon?: boolean;
|
|
23
|
+
shipmentProtectionFee?: number;
|
|
24
|
+
couponCode?: string;
|
|
25
|
+
couponDiscount?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface OrderAddonBadgesProps {
|
|
28
|
+
order: OrderAddonBadgesOrder;
|
|
29
|
+
/** `"compact"` — icon chips only, for list rows. `"detail"` — chips + the gift message and fees. */
|
|
30
|
+
variant?: "compact" | "detail";
|
|
31
|
+
/** Hide the coupon chip where the coupon is already shown in a price breakdown. */
|
|
32
|
+
showCoupon?: boolean;
|
|
33
|
+
currency?: string;
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function OrderAddonBadges({ order, variant, showCoupon, currency, className, }: OrderAddonBadgesProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Gift, MessageCircle, ShieldCheck, Tag } from "lucide-react";
|
|
4
|
+
import { Row, Span, Stack, Text } from "../../../ui";
|
|
5
|
+
import { formatCurrency } from "../../../utils/number.formatter";
|
|
6
|
+
const CHIP_BASE = "inline-flex items-center gap-[var(--appkit-space-1)] rounded-full px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-medium";
|
|
7
|
+
function Chip({ icon, label, tone, title, }) {
|
|
8
|
+
const toneCls = tone === "success"
|
|
9
|
+
? "bg-success-surface text-success"
|
|
10
|
+
: tone === "info"
|
|
11
|
+
? "bg-info-surface text-info"
|
|
12
|
+
: "bg-primary-50 text-primary-700 dark:bg-primary-950/40 dark:text-primary-300";
|
|
13
|
+
return (_jsxs(Span, { className: `${CHIP_BASE} ${toneCls}`, title: title, children: [_jsx(Span, { className: "flex-shrink-0", "aria-hidden": "true", children: icon }), label] }));
|
|
14
|
+
}
|
|
15
|
+
export function OrderAddonBadges({ order, variant = "compact", showCoupon = true, currency = "INR", className = "", }) {
|
|
16
|
+
const money = (v) => (typeof v === "number" && v > 0 ? formatCurrency(v, currency) : undefined);
|
|
17
|
+
const hasCoupon = showCoupon && !!order.couponCode;
|
|
18
|
+
const hasAny = order.whatsappNotifyAddon ||
|
|
19
|
+
order.giftWrapAddon ||
|
|
20
|
+
order.shipmentProtectionAddon ||
|
|
21
|
+
hasCoupon;
|
|
22
|
+
if (!hasAny)
|
|
23
|
+
return null;
|
|
24
|
+
const isDetail = variant === "detail";
|
|
25
|
+
return (_jsxs(Stack, { gap: "xs", className: `min-w-0 ${className}`, children: [_jsxs(Row, { gap: "xs", align: "center", wrap: true, className: "min-w-0", children: [order.whatsappNotifyAddon && (_jsx(Chip, { icon: _jsx(MessageCircle, { size: 12 }), label: isDetail && money(order.whatsappNotifyFee) ? `WhatsApp updates · ${money(order.whatsappNotifyFee)}` : "WhatsApp updates", tone: "success", title: "Buyer opted into WhatsApp order-status updates \u2014 status changes on this order should notify them on WhatsApp." })), order.giftWrapAddon && (_jsx(Chip, { icon: _jsx(Gift, { size: 12 }), label: isDetail && money(order.giftWrapFee) ? `Gift wrap · ${money(order.giftWrapFee)}` : "Gift wrap", tone: "brand", title: "This parcel must be gift-wrapped before dispatch." })), order.shipmentProtectionAddon && (_jsx(Chip, { icon: _jsx(ShieldCheck, { size: 12 }), label: isDetail && money(order.shipmentProtectionFee) ? `Protected · ${money(order.shipmentProtectionFee)}` : "Protected", tone: "info", title: "Shipment protection purchased \u2014 loss or damage in transit is covered." })), hasCoupon && (_jsx(Chip, { icon: _jsx(Tag, { size: 12 }), label: isDetail && money(order.couponDiscount)
|
|
26
|
+
? `${order.couponCode} · −${money(order.couponDiscount)}`
|
|
27
|
+
: order.couponCode, tone: "success", title: `Coupon ${order.couponCode} applied to this order.` }))] }), isDetail && order.giftWrapAddon && order.giftWrapMessage && (_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsx(Text, { size: "xs", color: "muted", weight: "semibold", transform: "uppercase", className: "tracking-wide", children: "Gift message" }), _jsx(Text, { size: "sm", color: "primary", className: "min-w-0 whitespace-pre-wrap break-words", children: order.giftWrapMessage })] }))] }));
|
|
28
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { OrderCard, OrdersList } from "./OrdersList";
|
|
2
|
+
export { OrderAddonBadges, type OrderAddonBadgesOrder, type OrderAddonBadgesProps, } from "./OrderAddonBadges";
|
|
2
3
|
export { MarketplaceOrderCard, type MarketplaceOrderCardLabels, type MarketplaceOrderCardLinks, type MarketplaceOrderCardOrder, type MarketplaceOrderCardProps, } from "./MarketplaceOrderCard";
|
|
3
4
|
export { OrderFilters, ORDER_FILTER_KEYS, ORDER_ADMIN_SORT_OPTIONS, ORDER_SELLER_SORT_OPTIONS, ORDER_USER_SORT_OPTIONS, getOrderFilterKeys, getOrderSortOptions, } from "./OrderFilters";
|
|
4
5
|
export type { OrderFiltersProps, OrderFilterVariant } from "./OrderFilters";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { OrderCard, OrdersList } from "./OrdersList";
|
|
2
|
+
export { OrderAddonBadges, } from "./OrderAddonBadges";
|
|
2
3
|
export { MarketplaceOrderCard, } from "./MarketplaceOrderCard";
|
|
3
4
|
export { OrderFilters, ORDER_FILTER_KEYS, ORDER_ADMIN_SORT_OPTIONS, ORDER_SELLER_SORT_OPTIONS, ORDER_USER_SORT_OPTIONS, getOrderFilterKeys, getOrderSortOptions, } from "./OrderFilters";
|
|
4
5
|
export { OrderSiblingPayments } from "./OrderSiblingPayments";
|
|
@@ -101,6 +101,22 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
|
|
|
101
101
|
canFilter: boolean;
|
|
102
102
|
canSort: boolean;
|
|
103
103
|
};
|
|
104
|
+
whatsappNotifyAddon: {
|
|
105
|
+
canFilter: boolean;
|
|
106
|
+
canSort: boolean;
|
|
107
|
+
};
|
|
108
|
+
giftWrapAddon: {
|
|
109
|
+
canFilter: boolean;
|
|
110
|
+
canSort: boolean;
|
|
111
|
+
};
|
|
112
|
+
shipmentProtectionAddon: {
|
|
113
|
+
canFilter: boolean;
|
|
114
|
+
canSort: boolean;
|
|
115
|
+
};
|
|
116
|
+
couponCode: {
|
|
117
|
+
canFilter: boolean;
|
|
118
|
+
canSort: boolean;
|
|
119
|
+
};
|
|
104
120
|
};
|
|
105
121
|
listForSeller(productIds: string[], model: SieveModel): Promise<FirebaseSieveResult<OrderDocument>>;
|
|
106
122
|
listForUser(userId: string, model: SieveModel): Promise<FirebaseSieveResult<OrderDocument>>;
|
|
@@ -179,6 +195,22 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
|
|
|
179
195
|
canFilter: boolean;
|
|
180
196
|
canSort: boolean;
|
|
181
197
|
};
|
|
198
|
+
whatsappNotifyAddon: {
|
|
199
|
+
canFilter: boolean;
|
|
200
|
+
canSort: boolean;
|
|
201
|
+
};
|
|
202
|
+
giftWrapAddon: {
|
|
203
|
+
canFilter: boolean;
|
|
204
|
+
canSort: boolean;
|
|
205
|
+
};
|
|
206
|
+
shipmentProtectionAddon: {
|
|
207
|
+
canFilter: boolean;
|
|
208
|
+
canSort: boolean;
|
|
209
|
+
};
|
|
210
|
+
couponCode: {
|
|
211
|
+
canFilter: boolean;
|
|
212
|
+
canSort: boolean;
|
|
213
|
+
};
|
|
182
214
|
};
|
|
183
215
|
listAll(model: SieveModel): Promise<FirebaseSieveResult<OrderDocument>>;
|
|
184
216
|
/**
|
|
@@ -496,6 +496,13 @@ OrderRepository.SELLER_SIEVE_FIELDS = {
|
|
|
496
496
|
orderDate: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
|
|
497
497
|
createdAt: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
|
|
498
498
|
contestable: { canFilter: true, canSort: false },
|
|
499
|
+
// Fulfilment flags. Filterable so "which of my orders need gift wrap?" is a
|
|
500
|
+
// query against the orders themselves — the whole point of recording the
|
|
501
|
+
// add-on on the order rather than keeping a side list of who asked for what.
|
|
502
|
+
whatsappNotifyAddon: { canFilter: true, canSort: false },
|
|
503
|
+
giftWrapAddon: { canFilter: true, canSort: false },
|
|
504
|
+
shipmentProtectionAddon: { canFilter: true, canSort: false },
|
|
505
|
+
couponCode: { canFilter: true, canSort: false },
|
|
499
506
|
};
|
|
500
507
|
OrderRepository.ADMIN_SIEVE_FIELDS = {
|
|
501
508
|
id: { canFilter: true, canSort: false },
|
|
@@ -520,6 +527,13 @@ OrderRepository.ADMIN_SIEVE_FIELDS = {
|
|
|
520
527
|
// Never filtered as "standard" via Firestore == (see listForUser callers):
|
|
521
528
|
// orders written before this field existed have no value to match.
|
|
522
529
|
orderType: { canFilter: true, canSort: false },
|
|
530
|
+
// Same fulfilment flags as SELLER_SIEVE_FIELDS — admin needs to answer
|
|
531
|
+
// "who opted into WhatsApp updates?" when a status change goes out, without
|
|
532
|
+
// maintaining a parallel tracker.
|
|
533
|
+
whatsappNotifyAddon: { canFilter: true, canSort: false },
|
|
534
|
+
giftWrapAddon: { canFilter: true, canSort: false },
|
|
535
|
+
shipmentProtectionAddon: { canFilter: true, canSort: false },
|
|
536
|
+
couponCode: { canFilter: true, canSort: false },
|
|
523
537
|
};
|
|
524
538
|
const orderRepository = new OrderRepository();
|
|
525
539
|
export { OrderRepository, orderRepository };
|
|
@@ -79,8 +79,30 @@ export interface Order {
|
|
|
79
79
|
* store plus one platform-wide coupon and this scalar shows only one of them.
|
|
80
80
|
*/
|
|
81
81
|
couponCode?: string;
|
|
82
|
+
/** Total rupee value of every discount applied to this order. */
|
|
83
|
+
couponDiscount?: number;
|
|
82
84
|
/** Every discount applied to this order — the full stack, not just the first. */
|
|
83
85
|
appliedDiscounts?: AppliedOrderDiscount[];
|
|
86
|
+
/**
|
|
87
|
+
* Paid add-ons the buyer opted into for THIS order (= this store).
|
|
88
|
+
*
|
|
89
|
+
* These are operational flags, not just billing records: `whatsappNotifyAddon`
|
|
90
|
+
* is what the status-change notifier checks to decide whether to send a
|
|
91
|
+
* WhatsApp message, and `giftWrapAddon`/`shipmentProtectionAddon` are what
|
|
92
|
+
* the packer needs to see before dispatch. They live on the order itself and
|
|
93
|
+
* are filterable (`orders` SIEVE_FIELDS) precisely so nobody has to keep a
|
|
94
|
+
* separate list of who asked for what.
|
|
95
|
+
*/
|
|
96
|
+
whatsappNotifyAddon?: boolean;
|
|
97
|
+
whatsappNotifyFee?: number;
|
|
98
|
+
giftWrapAddon?: boolean;
|
|
99
|
+
giftWrapFee?: number;
|
|
100
|
+
/** The buyer's gift message — the packer has to physically include this. */
|
|
101
|
+
giftWrapMessage?: string;
|
|
102
|
+
shipmentProtectionAddon?: boolean;
|
|
103
|
+
shipmentProtectionFee?: number;
|
|
104
|
+
/** Buyer-facing platform commission apportioned to this order. */
|
|
105
|
+
platformFee?: number;
|
|
84
106
|
trackingNumber?: string;
|
|
85
107
|
shippingCarrier?: string;
|
|
86
108
|
/** Carrier tracking page URL — rendered as an external link that opens in a new tab. No live-tracking API integration; this is just the passthrough URL a seller/admin entered. */
|
|
@@ -4,9 +4,8 @@ import { AdSlot } from "../../homepage/components/AdSlot";
|
|
|
4
4
|
import { ProductsIndexListing } from "./ProductsIndexListing";
|
|
5
5
|
import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
6
6
|
import { sortBy } from "../../../constants/sort";
|
|
7
|
-
import { ART_STICKERS_TYPE_FILTER_TABS } from "../constants/listing-tabs";
|
|
8
|
-
import { listPublicProducts, parsePublicProductParams, } from "../../../_internal/server/features/products/list-public";
|
|
9
|
-
const ART_STICKERS_LISTING_TYPES = ["art", "stickers"];
|
|
7
|
+
import { ART_STICKERS_TYPE_FILTER_TABS, ART_STICKERS_LISTING_TYPES, } from "../constants/listing-tabs";
|
|
8
|
+
import { listPublicProducts, parsePublicProductParams, defaultTogglesForListingTypes, } from "../../../_internal/server/features/products/list-public";
|
|
10
9
|
const DEFAULT_PAGE_SIZE = 24;
|
|
11
10
|
const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.CREATED_AT);
|
|
12
11
|
export async function ArtStickersListView({ searchParams = {} }) {
|
|
@@ -17,7 +16,9 @@ export async function ArtStickersListView({ searchParams = {} }) {
|
|
|
17
16
|
listingTypes: ART_STICKERS_LISTING_TYPES,
|
|
18
17
|
pageSize: DEFAULT_PAGE_SIZE,
|
|
19
18
|
sorts: DEFAULT_SORT,
|
|
20
|
-
|
|
19
|
+
// Derived, so this page and ProductsIndexListing can never disagree
|
|
20
|
+
// about the default view (Root Cause #30).
|
|
21
|
+
...defaultTogglesForListingTypes(ART_STICKERS_LISTING_TYPES),
|
|
21
22
|
}));
|
|
22
23
|
return (_jsxs(Main, { children: [_jsx(Section, { border: "bottom-subtle", paddingY: "b-md", padding: "t-xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, size: "3xl", weight: "bold", color: "primary", children: "Art & Stickers" }), _jsx(Text, { className: "mt-1", color: "muted", size: "sm", children: "Original art prints and sticker packs from independent sellers" })] }) }), _jsxs(Container, { size: "xl", padding: "x-md", children: [_jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-4 mt-4" }), _jsx(ProductsIndexListing, { initialData: result ?? null, listingTypes: ART_STICKERS_LISTING_TYPES, typeTabs: ART_STICKERS_TYPE_FILTER_TABS, searchPlaceholder: "Search art & stickers..." }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] })] }));
|
|
23
24
|
}
|