@mohasinac/appkit 4.63.4 → 4.64.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/.build-stamp.json +3 -3
- package/dist/_internal/server/features/categories/adapters.d.ts +1 -1
- package/dist/_internal/server/features/categories/adapters.js +13 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +2 -0
- package/dist/_internal/server/features/checkout/actions.js +22 -5
- package/dist/_internal/server/features/events/like-actions.d.ts +25 -0
- package/dist/_internal/server/features/events/like-actions.js +67 -0
- package/dist/_internal/server/features/events/likes.d.ts +65 -0
- package/dist/_internal/server/features/events/likes.js +146 -0
- package/dist/_internal/server/features/orders/adapters.js +3 -1
- package/dist/_internal/server/features/seo/index.d.ts +3 -0
- package/dist/_internal/server/features/seo/index.js +2 -0
- package/dist/_internal/server/features/seo/product-detail-types.d.ts +1 -0
- package/dist/_internal/server/features/seo/product-detail-types.js +26 -0
- package/dist/_internal/server/features/seo/robots.js +20 -1
- package/dist/_internal/server/features/seo/sitemap.js +125 -27
- package/dist/_internal/server/features/seo/static-params.d.ts +107 -0
- package/dist/_internal/server/features/seo/static-params.js +228 -0
- package/dist/_internal/server/features/seo/taxonomy-links.d.ts +44 -0
- package/dist/_internal/server/features/seo/taxonomy-links.js +85 -0
- package/dist/_internal/server/features/site-settings/adapters.js +20 -2
- package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
- package/dist/_internal/server/features/stores/adapters.js +88 -1
- package/dist/_internal/server/functions/scheduled.d.ts +16 -1
- package/dist/_internal/server/functions/scheduled.js +23 -1
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +5 -2
- package/dist/_internal/server/jobs/core/jobRunners.js +5 -0
- package/dist/_internal/server/jobs/core/onOrderCreate.js +104 -10
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +2 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +1 -0
- package/dist/_internal/server/jobs/core/onScamReportCreate.js +1 -0
- package/dist/_internal/server/jobs/core/onScamReportRejected.js +1 -0
- package/dist/_internal/server/jobs/core/onScamReportVerified.js +1 -0
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +1 -0
- package/dist/_internal/server/jobs/core/pendingOrderTimeout.js +1 -0
- package/dist/_internal/server/jobs/core/sellerStandingRollup.d.ts +63 -0
- package/dist/_internal/server/jobs/core/sellerStandingRollup.js +412 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +35 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.js +140 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +1 -0
- package/dist/_internal/server/jobs/handlers/sellerStandingRollup.d.ts +12 -0
- package/dist/_internal/server/jobs/handlers/sellerStandingRollup.js +14 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +8 -6
- package/dist/_internal/server/notifications/channel-health.js +2 -2
- package/dist/_internal/server/notifications/send-guard.d.ts +1 -1
- package/dist/_internal/server/notifications/send-guard.js +4 -2
- package/dist/_internal/shared/actions/action-registry.js +89 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +1 -1
- package/dist/_internal/shared/checkout/order-math.js +1 -1
- package/dist/_internal/shared/features/checkout/config.d.ts +2 -0
- package/dist/_internal/shared/features/checkout/config.js +2 -0
- package/dist/_internal/shared/features/messaging/config.d.ts +5 -7
- package/dist/_internal/shared/features/messaging/config.js +4 -1
- package/dist/_internal/shared/features/seller-standing/config.d.ts +130 -0
- package/dist/_internal/shared/features/seller-standing/config.js +153 -0
- package/dist/_internal/shared/features/site-settings/fees.d.ts +2 -0
- package/dist/_internal/shared/features/site-settings/fees.js +2 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +27 -2
- package/dist/_internal/shared/fees/calculator.d.ts +8 -0
- package/dist/_internal/shared/fees/calculator.js +7 -0
- package/dist/cli/index.js +2 -0
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/constants/api-endpoints.d.ts +39 -0
- package/dist/constants/api-endpoints.js +16 -0
- package/dist/constants/notification-types.d.ts +1 -1
- package/dist/constants/notification-types.js +1 -1
- package/dist/core/integration-keys.d.ts +5 -0
- package/dist/core/integration-keys.js +10 -0
- package/dist/features/about/components/FeesView.js +6 -0
- package/dist/features/about/components/HelpPageView.js +9 -1
- package/dist/features/account/components/BecomeSellerView.d.ts +15 -3
- package/dist/features/account/components/BecomeSellerView.js +2 -2
- package/dist/features/account/components/BuyerShoppingGuideView.js +1 -1
- package/dist/features/account/components/NotificationPreferencesPanel.js +4 -3
- package/dist/features/account/types/index.d.ts +2 -0
- package/dist/features/admin/actions/notification-actions.d.ts +17 -2
- package/dist/features/admin/actions/notification-actions.js +154 -9
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +1 -1
- package/dist/features/admin/components/AdminOrdersView.js +2 -0
- package/dist/features/admin/components/AdminSiteSettingsView.js +66 -6
- package/dist/features/admin/components/AdminStoreEditorView.d.ts +12 -1
- package/dist/features/admin/components/AdminStoreEditorView.js +31 -2
- package/dist/features/admin/components/AdminStoresGuideView.js +1 -1
- package/dist/features/admin/components/AdminStoresView.js +59 -2
- package/dist/features/admin/components/AdminTrustGuideView.js +10 -0
- package/dist/features/admin/constants/site-settings-tabs.d.ts +12 -7
- package/dist/features/admin/constants/site-settings-tabs.js +12 -6
- package/dist/features/admin/repository/site-settings.repository.js +1 -1
- package/dist/features/admin/schemas/admin-editor-forms.d.ts +10 -0
- package/dist/features/admin/schemas/admin-editor-forms.js +34 -0
- package/dist/features/admin/schemas/admin-user-form.d.ts +61 -0
- package/dist/features/admin/schemas/admin-user-form.js +44 -3
- package/dist/features/admin/schemas/firestore.d.ts +73 -0
- package/dist/features/admin/schemas/firestore.js +8 -3
- package/dist/features/auth/checkout-value-otp.d.ts +1 -1
- package/dist/features/auth/checkout-value-otp.js +1 -1
- package/dist/features/auth/permissions/constants.d.ts +2 -2
- package/dist/features/auth/permissions/constants.js +2 -1
- package/dist/features/blog/components/BlogIndexPageView.js +2 -2
- package/dist/features/blog/components/BlogListView.js +2 -1
- package/dist/features/blog/components/BlogPostView.js +2 -1
- package/dist/features/blog/constants/category-badge.d.ts +33 -0
- package/dist/features/blog/constants/category-badge.js +40 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
- package/dist/features/cart/components/StoreAddonsPicker.d.ts +5 -2
- package/dist/features/cart/components/StoreAddonsPicker.js +5 -4
- package/dist/features/cart/repository/cart.repository.js +2 -1
- package/dist/features/cart/schemas/firestore.d.ts +2 -1
- package/dist/features/categories/components/TaxonomyLinkIndex.d.ts +39 -0
- package/dist/features/categories/components/TaxonomyLinkIndex.js +11 -0
- package/dist/features/categories/schemas/firestore.d.ts +25 -0
- package/dist/features/categories/utils/load-category-options.d.ts +33 -0
- package/dist/features/categories/utils/load-category-options.js +50 -0
- package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +5 -10
- package/dist/features/checkout/actions/checkout-value-otp-actions.js +88 -2
- package/dist/features/events/components/EventCard.js +4 -12
- package/dist/features/events/components/EventInsights.d.ts +27 -0
- package/dist/features/events/components/EventInsights.js +10 -0
- package/dist/features/events/components/EventLikeButton.d.ts +21 -0
- package/dist/features/events/components/EventLikeButton.js +59 -0
- package/dist/features/events/components/EventLikersRail.d.ts +23 -0
- package/dist/features/events/components/EventLikersRail.js +54 -0
- package/dist/features/events/components/EventTypeBadge.d.ts +26 -0
- package/dist/features/events/components/EventTypeBadge.js +7 -0
- package/dist/features/events/components/EventsListPageView.js +2 -2
- package/dist/features/events/components/SpinWheel.d.ts +41 -0
- package/dist/features/events/components/SpinWheel.js +128 -0
- package/dist/features/events/components/SpinWheelView.js +36 -15
- package/dist/features/events/constants/event-type-meta.d.ts +54 -0
- package/dist/features/events/constants/event-type-meta.js +109 -0
- package/dist/features/events/repository/event-likes.repository.d.ts +57 -0
- package/dist/features/events/repository/event-likes.repository.js +151 -0
- package/dist/features/events/schemas/firestore.d.ts +111 -0
- package/dist/features/events/schemas/likes.d.ts +81 -0
- package/dist/features/events/schemas/likes.js +59 -0
- package/dist/features/events/types/index.d.ts +7 -0
- package/dist/features/events/utils/carousel-slide.d.ts +42 -0
- package/dist/features/events/utils/carousel-slide.js +174 -0
- package/dist/features/events/utils/event-charts.d.ts +65 -0
- package/dist/features/events/utils/event-charts.js +172 -0
- package/dist/features/faq/constants/help-guides.js +9 -0
- package/dist/features/faq/schemas/firestore.js +1 -0
- package/dist/features/faq/types/index.d.ts +19 -0
- package/dist/features/homepage/components/BrandsSection.js +25 -1
- package/dist/features/homepage/components/HeroCarousel.js +47 -10
- package/dist/features/homepage/lib/section-renderer.js +1 -1
- package/dist/features/homepage/types/index.d.ts +7 -0
- package/dist/features/orders/actions/refund-actions.js +1 -0
- package/dist/features/orders/components/OrderAddonBadges.d.ts +3 -1
- package/dist/features/orders/components/OrderAddonBadges.js +4 -3
- package/dist/features/orders/repository/orders.repository.d.ts +8 -0
- package/dist/features/orders/repository/orders.repository.js +2 -0
- package/dist/features/orders/schemas/firestore.d.ts +4 -0
- package/dist/features/orders/types/index.d.ts +5 -2
- package/dist/features/search/action-index/settings-entries.js +9 -0
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +6 -1
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +10 -1
- package/dist/features/store-extensions/schemas/firestore.d.ts +17 -1
- package/dist/features/store-extensions/schemas/firestore.js +10 -0
- package/dist/features/stores/components/InteractiveStoreCard.js +2 -1
- package/dist/features/stores/components/SellerStandingPanel.d.ts +31 -0
- package/dist/features/stores/components/SellerStandingPanel.js +25 -0
- package/dist/features/stores/components/SellerTierBadge.d.ts +57 -0
- package/dist/features/stores/components/SellerTierBadge.js +19 -0
- package/dist/features/stores/components/StoreCapabilitiesGuideView.js +33 -2
- package/dist/features/stores/components/StoreCreateForm.d.ts +46 -0
- package/dist/features/stores/components/StoreCreateForm.js +107 -0
- package/dist/features/stores/components/StoreGuideHubView.js +18 -5
- package/dist/features/stores/components/StoreHeader.js +2 -1
- package/dist/features/stores/components/StoreSettingsGuideView.js +6 -2
- package/dist/features/stores/repository/store.repository.d.ts +23 -1
- package/dist/features/stores/repository/store.repository.js +52 -1
- package/dist/features/stores/schemas/firestore.d.ts +76 -7
- package/dist/features/stores/schemas/firestore.js +21 -6
- package/dist/features/stores/schemas/index.d.ts +88 -2
- package/dist/features/stores/schemas/index.js +14 -0
- package/dist/features/stores/schemas/store-create.d.ts +47 -0
- package/dist/features/stores/schemas/store-create.js +86 -0
- package/dist/features/stores/types/index.d.ts +42 -0
- package/dist/features/whatsapp-bot/api/send-status/route.d.ts +12 -0
- package/dist/features/whatsapp-bot/api/send-status/route.js +38 -0
- package/dist/features/whatsapp-bot/api/webhook/route.d.ts +12 -0
- package/dist/features/whatsapp-bot/api/webhook/route.js +43 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +57 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +76 -0
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +9 -0
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +7 -0
- package/dist/features/whatsapp-bot/components/index.d.ts +3 -0
- package/dist/features/whatsapp-bot/components/index.js +2 -0
- package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +52 -0
- package/dist/features/whatsapp-bot/helpers/whatsapp.js +288 -0
- package/dist/features/whatsapp-bot/index.d.ts +3 -0
- package/dist/features/whatsapp-bot/index.js +3 -0
- package/dist/features/whatsapp-bot/manifest.d.ts +2 -0
- package/dist/features/whatsapp-bot/manifest.js +13 -0
- package/dist/features/whatsapp-bot/messages/en.json +7 -0
- package/dist/features/whatsapp-bot/server.d.ts +7 -0
- package/dist/features/whatsapp-bot/server.js +7 -0
- package/dist/features/whatsapp-bot/types/index.d.ts +126 -0
- package/dist/features/whatsapp-bot/types/index.js +1 -0
- package/dist/index.d.ts +23 -6
- package/dist/index.js +38 -12
- package/dist/next/routing/route-map.d.ts +6 -1
- package/dist/next/routing/route-map.js +14 -0
- package/dist/providers/db-firebase/base.repository.d.ts +1 -1
- package/dist/providers/db-firebase/base.repository.js +1 -1
- package/dist/repositories/index.d.ts +1 -1
- package/dist/repositories/index.js +1 -1
- package/dist/schemas/registry.d.ts +115 -0
- package/dist/schemas/registry.js +2 -1
- package/dist/security/pii-encrypt.js +1 -1
- package/dist/server.d.ts +15 -2
- package/dist/server.js +32 -2
- package/dist/styles.css +36 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/HorizontalScroller.js +15 -2
- package/dist/ui/components/NavigationLoader.d.ts +14 -1
- package/dist/ui/components/NavigationLoader.js +79 -30
- package/dist/ui/components/PageHeroHeading.d.ts +40 -0
- package/dist/ui/components/PageHeroHeading.js +15 -0
- package/dist/ui/components/Typography.d.ts +3 -3
- package/dist/ui/components/Typography.js +59 -31
- package/dist/ui/components/Typography.style.css +35 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +1 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +4 -2
- package/dist/features/orders/utils/order-announcement-message.d.ts +0 -18
- package/dist/features/orders/utils/order-announcement-message.js +0 -16
|
@@ -2,13 +2,15 @@ import { normalizeError } from "../../../errors/normalize.js";
|
|
|
2
2
|
import { notificationRepository } from "../repository/notification.repository.js";
|
|
3
3
|
import { siteSettingsRepository } from "../repository/site-settings.repository.js";
|
|
4
4
|
import { userRepository } from "../../auth/repository/user.repository.js";
|
|
5
|
+
import { storeRepository } from "../../stores/repository/store.repository.js";
|
|
6
|
+
import { safeRead } from "../../../errors/safe-read.js";
|
|
5
7
|
import { createResendProvider } from "../../../providers/email-resend/provider.js";
|
|
6
8
|
import { isChannelHealthy, withChannelRetry } from "../../../_internal/server/notifications/channel-health.js";
|
|
9
|
+
import { enqueueJob } from "../../jobs/actions/enqueue-job.js";
|
|
7
10
|
import { serverLogger } from "../../../monitoring/index.js";
|
|
8
11
|
import { resolveNotificationActionUrl, TYPE_AUDIENCE, } from "../../../_internal/shared/features/notifications/action-url.js";
|
|
9
12
|
import { isEmailEligible } from "../../../_internal/shared/features/notifications/email-eligibility.js";
|
|
10
13
|
import { renderNotificationEmail } from "../../email/notification-templates.js";
|
|
11
|
-
import { DEFAULT_NOTIFICATION_CHANNELS, meetsMinPriority, } from "../schemas/firestore.js";
|
|
12
14
|
/**
|
|
13
15
|
* A notification's `actionUrl` is a site-relative path — correct for the
|
|
14
16
|
* in-app bell, useless in an email, where a bare `/user/orders/x` is not a
|
|
@@ -31,6 +33,85 @@ function absoluteActionUrl(path) {
|
|
|
31
33
|
const base = process.env.NEXT_PUBLIC_APP_URL?.replace(/\/+$/, "");
|
|
32
34
|
return base ? `${base}${path}` : path;
|
|
33
35
|
}
|
|
36
|
+
import { DEFAULT_NOTIFICATION_CHANNELS, meetsMinPriority, } from "../schemas/firestore.js";
|
|
37
|
+
/**
|
|
38
|
+
* Notification types whose WhatsApp send is gated behind the SELLER's
|
|
39
|
+
* standing WhatsApp-notifications subscription — see `storeId` on
|
|
40
|
+
* `SendNotificationInput`.
|
|
41
|
+
*
|
|
42
|
+
* 🛑 This replaced a per-ORDER ₹10 addon the buyer paid at checkout
|
|
43
|
+
* (`OrderDocument.whatsappNotifyAddon`). The axis moved deliberately: the
|
|
44
|
+
* platform owns one WhatsApp number, so the cost is a standing monthly one
|
|
45
|
+
* borne by the seller who benefits from it, not a line item a buyer opts into
|
|
46
|
+
* per order. A buyer should not have to purchase their own delivery updates.
|
|
47
|
+
*
|
|
48
|
+
* Every other type (bid_won, offers, scam reports, support tickets, admin
|
|
49
|
+
* alerts, …) keeps the original two gates — admin channel enabled, user has
|
|
50
|
+
* not opted out — unchanged.
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* Does this store subscribe to WhatsApp order notifications?
|
|
54
|
+
*
|
|
55
|
+
* The subscription is a monthly add-on priced by the admin
|
|
56
|
+
* (`commissions.whatsappNotifyMonthlyFee`); this reads only whether it is
|
|
57
|
+
* currently active. Billing lives with the other recurring charges, not here.
|
|
58
|
+
*
|
|
59
|
+
* 🛑 Fails CLOSED. A store we cannot read, or one that has not subscribed,
|
|
60
|
+
* gets no WhatsApp send — the buyer still receives the in-app notification and
|
|
61
|
+
* the email, which are ungated. Failing open would message customers on behalf
|
|
62
|
+
* of a seller who is not paying for it, and bill the platform for the template.
|
|
63
|
+
*/
|
|
64
|
+
async function storeWhatsAppSubscribed(storeId) {
|
|
65
|
+
if (!storeId)
|
|
66
|
+
return false;
|
|
67
|
+
const store = await safeRead(() => storeRepository.findBySlug(storeId), {
|
|
68
|
+
route: "sendNotification",
|
|
69
|
+
key: `whatsapp-subscription:${storeId}`,
|
|
70
|
+
fallback: null,
|
|
71
|
+
});
|
|
72
|
+
return store?.whatsappNotifySubscribed === true;
|
|
73
|
+
}
|
|
74
|
+
const ORDER_GATED_TYPES = new Set([
|
|
75
|
+
"order_placed",
|
|
76
|
+
"order_confirmed",
|
|
77
|
+
"order_shipped",
|
|
78
|
+
"order_delivered",
|
|
79
|
+
"order_cancelled",
|
|
80
|
+
"refund_initiated",
|
|
81
|
+
]);
|
|
82
|
+
/**
|
|
83
|
+
* Notification type → the flat `SiteSettingsCredentials` field holding its
|
|
84
|
+
* approved Meta template name.
|
|
85
|
+
*
|
|
86
|
+
* 🛑 **`Partial` here is CORRECT and is not the `typeToPrefsKey` mistake.**
|
|
87
|
+
*
|
|
88
|
+
* The distinction is what a missing key MEANS. A missing `typeToPrefsKey`
|
|
89
|
+
* entry silently ignored a user's opt-out — a real setting that read as
|
|
90
|
+
* honoured and was not. A missing entry here means "no Meta-approved template
|
|
91
|
+
* exists for this type", which is a fact about the Meta account, not a bug:
|
|
92
|
+
* six of the 28 types have one, and the rest legitimately fall back to
|
|
93
|
+
* free-form (which Meta accepts inside the 24-hour customer-service window
|
|
94
|
+
* and rejects outside it — the runner already logs that rejection).
|
|
95
|
+
*
|
|
96
|
+
* Getting six approved templates is a Meta Business review process, not a
|
|
97
|
+
* code change, so listing all 28 here with empty values would be a
|
|
98
|
+
* `Record<…>` that lies about coverage.
|
|
99
|
+
*
|
|
100
|
+
* The six that ARE approved are the order lifecycle, which is also the set
|
|
101
|
+
* gated behind the buyer's ₹10 addon above — the two lists agree on purpose.
|
|
102
|
+
*/
|
|
103
|
+
const WHATSAPP_TEMPLATE_FIELD = {
|
|
104
|
+
order_placed: "whatsappTemplateOrderPlaced",
|
|
105
|
+
order_confirmed: "whatsappTemplateOrderConfirmed",
|
|
106
|
+
order_shipped: "whatsappTemplateOrderShipped",
|
|
107
|
+
order_delivered: "whatsappTemplateOrderDelivered",
|
|
108
|
+
order_cancelled: "whatsappTemplateOrderCancelled",
|
|
109
|
+
refund_initiated: "whatsappTemplateRefundInitiated",
|
|
110
|
+
};
|
|
111
|
+
function resolveWhatsAppTemplateName(type, creds) {
|
|
112
|
+
const field = WHATSAPP_TEMPLATE_FIELD[type];
|
|
113
|
+
return (field ? creds[field] : undefined)?.trim() ?? "";
|
|
114
|
+
}
|
|
34
115
|
export async function markNotificationRead(id) {
|
|
35
116
|
await notificationRepository.markAsRead(id);
|
|
36
117
|
}
|
|
@@ -53,15 +134,15 @@ export async function getUnreadNotificationCount(userId) {
|
|
|
53
134
|
/**
|
|
54
135
|
* Create an in-app notification and fan out to enabled external channels.
|
|
55
136
|
*
|
|
56
|
-
* In-app is always written.
|
|
137
|
+
* In-app is always written. Email / WhatsApp are sent only when:
|
|
57
138
|
* 1. The channel is enabled in siteSettings.notificationChannels
|
|
58
139
|
* 2. Notification priority meets the channel's minPriority threshold
|
|
59
140
|
* 3. The optional `types` filter includes this notification type (or is absent)
|
|
60
|
-
* 4. The contact field (userEmail) is supplied by the caller
|
|
141
|
+
* 4. The contact field (userEmail / userPhone) is supplied by the caller
|
|
61
142
|
* 5. The required credentials are set in siteSettings.credentials
|
|
62
143
|
*/
|
|
63
144
|
export async function sendNotification(input) {
|
|
64
|
-
const { userEmail, emailHtml, audience, eligibilityOverride, ...notifInput } = input;
|
|
145
|
+
const { userEmail, userPhone, emailHtml, audience, eligibilityOverride, ...notifInput } = input;
|
|
65
146
|
/*
|
|
66
147
|
* Fill `actionUrl` when the caller did not.
|
|
67
148
|
*
|
|
@@ -103,12 +184,12 @@ export async function sendNotification(input) {
|
|
|
103
184
|
* suppresses the emailed/messaged COPY, not the notification.
|
|
104
185
|
*
|
|
105
186
|
* One map gates both channels deliberately. If a type is ever genuinely
|
|
106
|
-
*
|
|
187
|
+
* WhatsApp-worthy but not email-worthy, split the map then rather than
|
|
107
188
|
* loosening this early return.
|
|
108
189
|
*/
|
|
109
190
|
if (!isEmailEligible(notifInput.type) &&
|
|
110
191
|
eligibilityOverride !== "staff_requested") {
|
|
111
|
-
return { notification, email: "skipped" };
|
|
192
|
+
return { notification, email: "skipped", whatsapp: "skipped" };
|
|
112
193
|
}
|
|
113
194
|
// Load channel config + credentials (one Firestore read).
|
|
114
195
|
let settings;
|
|
@@ -118,7 +199,7 @@ export async function sendNotification(input) {
|
|
|
118
199
|
catch (err) {
|
|
119
200
|
void normalizeError(err);
|
|
120
201
|
serverLogger.warn("sendNotification: could not load siteSettings — skipping external channels", { err });
|
|
121
|
-
return { notification, email: "skipped" };
|
|
202
|
+
return { notification, email: "skipped", whatsapp: "skipped" };
|
|
122
203
|
}
|
|
123
204
|
const channels = settings.notificationChannels ?? DEFAULT_NOTIFICATION_CHANNELS;
|
|
124
205
|
const creds = settings.credentials ?? {};
|
|
@@ -150,6 +231,7 @@ export async function sendNotification(input) {
|
|
|
150
231
|
? userRepository.revealUser(fetchedUserDoc)
|
|
151
232
|
: null;
|
|
152
233
|
const resolvedEmail = userEmail ?? fetchedUserPlain?.email ?? undefined;
|
|
234
|
+
const resolvedPhone = userPhone ?? fetchedUserPlain?.phoneNumber ?? undefined;
|
|
153
235
|
// Map NotificationType to a NotificationTypePrefs key.
|
|
154
236
|
/*
|
|
155
237
|
* 🛑 COMPLETE and compile-checked: `Record<NotificationType, …>`, not
|
|
@@ -212,7 +294,7 @@ export async function sendNotification(input) {
|
|
|
212
294
|
const typeKey = typeToPrefsKey[type];
|
|
213
295
|
// If the user has explicitly disabled this notification type, skip all external channels.
|
|
214
296
|
if (typeKey && userTypePrefs[typeKey] === false) {
|
|
215
|
-
return { notification, email: "skipped" };
|
|
297
|
+
return { notification, email: "skipped", whatsapp: "skipped" };
|
|
216
298
|
}
|
|
217
299
|
// Email channel — admin enabled AND user hasn't opted out
|
|
218
300
|
let emailStatus = "skipped";
|
|
@@ -259,10 +341,73 @@ export async function sendNotification(input) {
|
|
|
259
341
|
}
|
|
260
342
|
}
|
|
261
343
|
}
|
|
344
|
+
// WhatsApp channel — admin enabled AND user hasn't opted out AND (for
|
|
345
|
+
// order-lifecycle types) the buyer paid the per-order WhatsApp addon.
|
|
346
|
+
let whatsappStatus = "skipped";
|
|
347
|
+
if (channels.whatsapp.enabled &&
|
|
348
|
+
userChannelPrefs.whatsapp !== false &&
|
|
349
|
+
resolvedPhone &&
|
|
350
|
+
meetsMinPriority(priority, channels.whatsapp.minPriority) &&
|
|
351
|
+
(!channels.whatsapp.types?.length || channels.whatsapp.types.includes(type)) &&
|
|
352
|
+
(!ORDER_GATED_TYPES.has(type) || (await storeWhatsAppSubscribed(input.storeId)))) {
|
|
353
|
+
/*
|
|
354
|
+
* ENV FIRST, site settings second.
|
|
355
|
+
*
|
|
356
|
+
* The platform owns one WhatsApp number, so its credentials are
|
|
357
|
+
* deployment configuration rather than admin-editable content — they
|
|
358
|
+
* belong beside the other secrets in the env, not in a Firestore document
|
|
359
|
+
* an admin screen can rewrite. Rotating a leaked Meta token should be a
|
|
360
|
+
* redeploy, not a form submission.
|
|
361
|
+
*
|
|
362
|
+
* 🛑 `WHATSAPP_PHONE_NUMBER_ID` is the Cloud API's numeric id, NOT the
|
|
363
|
+
* dialable number: sends go to `/{phone-number-id}/messages`, and a `+1…`
|
|
364
|
+
* string there is a 404 that looks like a routing bug.
|
|
365
|
+
*
|
|
366
|
+
* The site-settings fallback is kept so an install that configured these
|
|
367
|
+
* through the admin screen keeps working — this changes precedence, not
|
|
368
|
+
* behaviour, and nothing has to be migrated.
|
|
369
|
+
*/
|
|
370
|
+
const phoneNumberId = process.env.WHATSAPP_PHONE_NUMBER_ID?.trim() ||
|
|
371
|
+
(creds.whatsappPhoneNumberId?.trim() ?? "");
|
|
372
|
+
const accessToken = process.env.WHATSAPP_MESSAGES_TOKEN?.trim() ||
|
|
373
|
+
(creds.whatsappCloudApiToken?.trim() ?? "");
|
|
374
|
+
if (phoneNumberId && accessToken) {
|
|
375
|
+
try {
|
|
376
|
+
const { jobId } = await enqueueJob({
|
|
377
|
+
jobType: "whatsappNotify",
|
|
378
|
+
payload: {
|
|
379
|
+
toPhone: resolvedPhone,
|
|
380
|
+
title,
|
|
381
|
+
message,
|
|
382
|
+
type,
|
|
383
|
+
templateName: resolveWhatsAppTemplateName(type, creds),
|
|
384
|
+
templateLanguage: creds.whatsappTemplateLanguage ?? "en",
|
|
385
|
+
notificationId: notification.id,
|
|
386
|
+
phoneNumberId,
|
|
387
|
+
accessToken,
|
|
388
|
+
},
|
|
389
|
+
requestedBy: input.userId,
|
|
390
|
+
});
|
|
391
|
+
whatsappStatus = "queued";
|
|
392
|
+
await notificationRepository
|
|
393
|
+
.update(notification.id, { whatsappStatus: "queued", whatsappJobId: jobId })
|
|
394
|
+
.catch((err) => {
|
|
395
|
+
void normalizeError(err);
|
|
396
|
+
serverLogger.warn("sendNotification: failed to write whatsappJobId onto notification doc", { notificationId: notification.id });
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
void normalizeError(err);
|
|
401
|
+
serverLogger.error("sendNotification: WhatsApp job enqueue failed", { userId: input.userId, err });
|
|
402
|
+
whatsappStatus = "failed";
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
262
406
|
serverLogger.info("sendNotification: dispatched", {
|
|
263
407
|
userId: input.userId,
|
|
264
408
|
type,
|
|
265
409
|
email: emailStatus,
|
|
410
|
+
whatsapp: whatsappStatus,
|
|
266
411
|
});
|
|
267
|
-
return { notification, email: emailStatus };
|
|
412
|
+
return { notification, email: emailStatus, whatsapp: whatsappStatus };
|
|
268
413
|
}
|
|
@@ -37,7 +37,7 @@ export interface AdminOrderEditorViewProps {
|
|
|
37
37
|
paymentReviewOutcome?: string;
|
|
38
38
|
/**
|
|
39
39
|
* Paid add-ons + applied coupon for this order. Operational, not decorative:
|
|
40
|
-
* `
|
|
40
|
+
* `whatsappNotifyAddon` is who a status change should message, and the
|
|
41
41
|
* coupon is what an admin needs when reconciling a discounted order.
|
|
42
42
|
*/
|
|
43
43
|
addons?: OrderAddonBadgesOrder;
|
|
@@ -255,6 +255,8 @@ export function AdminOrdersView({ children, ...props }) {
|
|
|
255
255
|
// Straight off the order document — the admin list route returns
|
|
256
256
|
// raw docs, so these need no serializer entry (cf. Root Cause #38).
|
|
257
257
|
addons: {
|
|
258
|
+
whatsappNotifyAddon: Boolean(item.whatsappNotifyAddon),
|
|
259
|
+
whatsappNotifyFee: Number(item.whatsappNotifyFee ?? 0) || undefined,
|
|
258
260
|
giftWrapAddon: Boolean(item.giftWrapAddon),
|
|
259
261
|
giftWrapFee: Number(item.giftWrapFee ?? 0) || undefined,
|
|
260
262
|
giftWrapMessage: toStringValue(item.giftWrapMessage) || undefined,
|
|
@@ -201,7 +201,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
201
201
|
// ⑦ Fees — all read/written under commissions key
|
|
202
202
|
const [laborHourlyRate, setLaborHourlyRate] = React.useState(200);
|
|
203
203
|
const [laborMaxHoursPerDay, setLaborMaxHoursPerDay] = React.useState(6);
|
|
204
|
-
//
|
|
204
|
+
// ⑯ EMI — site-wide installment settings
|
|
205
205
|
const [emiEnabled, setEmiEnabled] = React.useState(false);
|
|
206
206
|
const [emiMinOrderValue, setEmiMinOrderValue] = React.useState(10000);
|
|
207
207
|
const [emiTenureOptionsText, setEmiTenureOptionsText] = React.useState("2,3,4,5,6");
|
|
@@ -209,7 +209,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
209
209
|
const [emiBillingDay, setEmiBillingDay] = React.useState(5);
|
|
210
210
|
const [emiSurchargePercentPerMonth, setEmiSurchargePercentPerMonth] = React.useState(2);
|
|
211
211
|
const [emiSurchargeSellerSharePercent, setEmiSurchargeSellerSharePercent] = React.useState(50);
|
|
212
|
-
//
|
|
212
|
+
// ⑰ GST — Indian tax compliance
|
|
213
213
|
const [gstEnabled, setGstEnabled] = React.useState(false);
|
|
214
214
|
const [gstin, setGstin] = React.useState("");
|
|
215
215
|
const [gstLegalName, setGstLegalName] = React.useState("");
|
|
@@ -243,6 +243,8 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
243
243
|
const [preOrderListingFee, setPreOrderListingFee] = React.useState(0);
|
|
244
244
|
const [featuredSlotFee, setFeaturedSlotFee] = React.useState(999);
|
|
245
245
|
const [promotedSlotFee, setPromotedSlotFee] = React.useState(499);
|
|
246
|
+
const [whatsappNotifyFeeEnabled, setWhatsappNotifyFeeEnabled] = React.useState(false);
|
|
247
|
+
const [whatsappNotifyFee, setWhatsappNotifyFee] = React.useState(10);
|
|
246
248
|
const [giftWrapFeeEnabled, setGiftWrapFeeEnabled] = React.useState(false);
|
|
247
249
|
const [giftWrapFee, setGiftWrapFee] = React.useState(49);
|
|
248
250
|
const [shipmentProtectionFeeEnabled, setShipmentProtectionFeeEnabled] = React.useState(false);
|
|
@@ -316,6 +318,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
316
318
|
const [categoryTypeFlags, setCategoryTypeFlags] = React.useState({});
|
|
317
319
|
// Per-channel notification-type allowlists — empty array means "all types".
|
|
318
320
|
const [notifEmailTypes, setNotifEmailTypes] = React.useState([]);
|
|
321
|
+
const [notifWhatsappTypes, setNotifWhatsappTypes] = React.useState([]);
|
|
319
322
|
// ⑩ Auction
|
|
320
323
|
const [bidIncrementTiers, setBidIncrementTiers] = React.useState(DEFAULT_AUCTION_BID_INCREMENT_TIERS);
|
|
321
324
|
const [autoExtendWindow, setAutoExtendWindow] = React.useState(5);
|
|
@@ -346,6 +349,17 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
346
349
|
*/
|
|
347
350
|
const [cancellationHtml, setCancellationHtml] = React.useState("");
|
|
348
351
|
const [grievanceHtml, setGrievanceHtml] = React.useState("");
|
|
352
|
+
// ⑬ WhatsApp Business Cloud API (platform-level)
|
|
353
|
+
const [waPhoneNumberId, setWaPhoneNumberId] = React.useState("");
|
|
354
|
+
const [waCloudApiToken, setWaCloudApiToken] = React.useState("");
|
|
355
|
+
const [waAdminNotifyNumbers, setWaAdminNotifyNumbers] = React.useState("");
|
|
356
|
+
const [waTemplateLanguage, setWaTemplateLanguage] = React.useState("en");
|
|
357
|
+
const [waTemplateOrderPlaced, setWaTemplateOrderPlaced] = React.useState("");
|
|
358
|
+
const [waTemplateOrderConfirmed, setWaTemplateOrderConfirmed] = React.useState("");
|
|
359
|
+
const [waTemplateOrderShipped, setWaTemplateOrderShipped] = React.useState("");
|
|
360
|
+
const [waTemplateOrderDelivered, setWaTemplateOrderDelivered] = React.useState("");
|
|
361
|
+
const [waTemplateOrderCancelled, setWaTemplateOrderCancelled] = React.useState("");
|
|
362
|
+
const [waTemplateRefundInitiated, setWaTemplateRefundInitiated] = React.useState("");
|
|
349
363
|
// ⓪ About
|
|
350
364
|
const [aboutTitle, setAboutTitle] = React.useState("");
|
|
351
365
|
const [aboutSubtitle, setAboutSubtitle] = React.useState("");
|
|
@@ -364,9 +378,12 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
364
378
|
const [aboutCtaTitle, setAboutCtaTitle] = React.useState("");
|
|
365
379
|
const [aboutCtaSell, setAboutCtaSell] = React.useState("");
|
|
366
380
|
const [aboutCtaShop, setAboutCtaShop] = React.useState("");
|
|
367
|
-
//
|
|
381
|
+
// ⑭ Notification channels
|
|
368
382
|
const [notifEmailEnabled, setNotifEmailEnabled] = React.useState(false);
|
|
369
383
|
const [notifEmailMinPriority, setNotifEmailMinPriority] = React.useState("normal");
|
|
384
|
+
const [notifWhatsappEnabled, setNotifWhatsappEnabled] = React.useState(false);
|
|
385
|
+
const [notifWhatsappMinPriority, setNotifWhatsappMinPriority] = React.useState("high");
|
|
386
|
+
const [notifWhatsappOtpEnabled, setNotifWhatsappOtpEnabled] = React.useState(false);
|
|
370
387
|
const [notifSmsEnabled, setNotifSmsEnabled] = React.useState(false);
|
|
371
388
|
const [notifSmsMinPriority, setNotifSmsMinPriority] = React.useState("high");
|
|
372
389
|
// Resend API key + sender identity (used by the email channel)
|
|
@@ -384,7 +401,9 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
384
401
|
* off customer email must never also blind the operator.
|
|
385
402
|
*/
|
|
386
403
|
const [msgEmailEnabled, setMsgEmailEnabled] = React.useState(false);
|
|
404
|
+
const [msgWhatsappEnabled, setMsgWhatsappEnabled] = React.useState(false);
|
|
387
405
|
const [msgCeilingEmail, setMsgCeilingEmail] = React.useState(80);
|
|
406
|
+
const [msgCeilingWhatsapp, setMsgCeilingWhatsapp] = React.useState(200);
|
|
388
407
|
// Snapshot of the masked placeholder strings the server returned for every
|
|
389
408
|
// credentials.* field, captured once per load. A combined single-save has
|
|
390
409
|
// to send the FULL credentials object on every save (not just the group the
|
|
@@ -472,6 +491,8 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
472
491
|
setPreOrderListingFee(s.commissions?.preOrderListingFee ?? 0);
|
|
473
492
|
setFeaturedSlotFee(s.commissions?.featuredSlotFee ?? 999);
|
|
474
493
|
setPromotedSlotFee(s.commissions?.promotedSlotFee ?? 499);
|
|
494
|
+
setWhatsappNotifyFeeEnabled(s.commissions?.whatsappNotifyFeeEnabled ?? false);
|
|
495
|
+
setWhatsappNotifyFee(s.commissions?.whatsappNotifyFee ?? 10);
|
|
475
496
|
setGiftWrapFeeEnabled(s.commissions?.giftWrapFeeEnabled ?? false);
|
|
476
497
|
setGiftWrapFee(s.commissions?.giftWrapFee ?? 49);
|
|
477
498
|
setShipmentProtectionFeeEnabled(s.commissions?.shipmentProtectionFeeEnabled ?? false);
|
|
@@ -537,6 +558,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
537
558
|
setListingTypeFlags(s.listings?.listingTypes ?? {});
|
|
538
559
|
setCategoryTypeFlags(s.listings?.categoryTypes ?? {});
|
|
539
560
|
setNotifEmailTypes(s.notificationChannels?.email?.types ?? []);
|
|
561
|
+
setNotifWhatsappTypes(s.notificationChannels?.whatsapp?.types ?? []);
|
|
540
562
|
setBidIncrementTiers(s.auctionConfig?.bidIncrementTiers ?? DEFAULT_AUCTION_BID_INCREMENT_TIERS);
|
|
541
563
|
setAutoExtendWindow(s.auctionConfig?.autoExtendWindowMinutes ?? 5);
|
|
542
564
|
setSettlementGrace(s.auctionConfig?.settlementGracePeriodHours ?? 24);
|
|
@@ -569,8 +591,21 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
569
591
|
setAboutCtaTitle(s.aboutContent?.ctaTitle ?? "");
|
|
570
592
|
setAboutCtaSell(s.aboutContent?.ctaSell ?? "");
|
|
571
593
|
setAboutCtaShop(s.aboutContent?.ctaShop ?? "");
|
|
594
|
+
setWaPhoneNumberId(s.credentialsMasked?.whatsappPhoneNumberId ?? "");
|
|
595
|
+
setWaCloudApiToken(s.credentialsMasked?.whatsappCloudApiToken ?? "");
|
|
596
|
+
setWaAdminNotifyNumbers(s.credentialsMasked?.whatsappAdminNotifyNumbers ?? "");
|
|
597
|
+
setWaTemplateLanguage(s.credentialsMasked?.whatsappTemplateLanguage ?? "en");
|
|
598
|
+
setWaTemplateOrderPlaced(s.credentialsMasked?.whatsappTemplateOrderPlaced ?? "");
|
|
599
|
+
setWaTemplateOrderConfirmed(s.credentialsMasked?.whatsappTemplateOrderConfirmed ?? "");
|
|
600
|
+
setWaTemplateOrderShipped(s.credentialsMasked?.whatsappTemplateOrderShipped ?? "");
|
|
601
|
+
setWaTemplateOrderDelivered(s.credentialsMasked?.whatsappTemplateOrderDelivered ?? "");
|
|
602
|
+
setWaTemplateOrderCancelled(s.credentialsMasked?.whatsappTemplateOrderCancelled ?? "");
|
|
603
|
+
setWaTemplateRefundInitiated(s.credentialsMasked?.whatsappTemplateRefundInitiated ?? "");
|
|
572
604
|
setNotifEmailEnabled(s.notificationChannels?.email?.enabled ?? false);
|
|
573
605
|
setNotifEmailMinPriority(s.notificationChannels?.email?.minPriority ?? "normal");
|
|
606
|
+
setNotifWhatsappEnabled(s.notificationChannels?.whatsapp?.enabled ?? false);
|
|
607
|
+
setNotifWhatsappMinPriority(s.notificationChannels?.whatsapp?.minPriority ?? "high");
|
|
608
|
+
setNotifWhatsappOtpEnabled(s.notificationChannels?.whatsapp?.otpEnabled ?? false);
|
|
574
609
|
setNotifSmsEnabled(s.notificationChannels?.sms?.enabled ?? false);
|
|
575
610
|
setNotifSmsMinPriority(s.notificationChannels?.sms?.minPriority ?? "high");
|
|
576
611
|
setResendApiKey(s.credentialsMasked?.resendApiKey ?? "");
|
|
@@ -582,7 +617,9 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
582
617
|
// settings document written before this feature existed reads as quiet
|
|
583
618
|
// rather than as "on and unmetered".
|
|
584
619
|
setMsgEmailEnabled(s.messaging?.emailEnabled ?? false);
|
|
620
|
+
setMsgWhatsappEnabled(s.messaging?.whatsappEnabled ?? false);
|
|
585
621
|
setMsgCeilingEmail(s.messaging?.dailyCeiling?.email ?? 80);
|
|
622
|
+
setMsgCeilingWhatsapp(s.messaging?.dailyCeiling?.whatsapp ?? 200);
|
|
586
623
|
originalMaskedRef.current = {
|
|
587
624
|
razorpayKeyId: s.credentialsMasked?.razorpayKeyId ?? "",
|
|
588
625
|
razorpayKeySecret: s.credentialsMasked?.razorpayKeySecret ?? "",
|
|
@@ -602,6 +639,16 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
602
639
|
tiktokAccessToken: s.credentialsMasked?.tiktokAccessToken ?? "",
|
|
603
640
|
deviantartClientId: s.credentialsMasked?.deviantartClientId ?? "",
|
|
604
641
|
deviantartClientSecret: s.credentialsMasked?.deviantartClientSecret ?? "",
|
|
642
|
+
whatsappPhoneNumberId: s.credentialsMasked?.whatsappPhoneNumberId ?? "",
|
|
643
|
+
whatsappCloudApiToken: s.credentialsMasked?.whatsappCloudApiToken ?? "",
|
|
644
|
+
whatsappAdminNotifyNumbers: s.credentialsMasked?.whatsappAdminNotifyNumbers ?? "",
|
|
645
|
+
whatsappTemplateLanguage: s.credentialsMasked?.whatsappTemplateLanguage ?? "en",
|
|
646
|
+
whatsappTemplateOrderPlaced: s.credentialsMasked?.whatsappTemplateOrderPlaced ?? "",
|
|
647
|
+
whatsappTemplateOrderConfirmed: s.credentialsMasked?.whatsappTemplateOrderConfirmed ?? "",
|
|
648
|
+
whatsappTemplateOrderShipped: s.credentialsMasked?.whatsappTemplateOrderShipped ?? "",
|
|
649
|
+
whatsappTemplateOrderDelivered: s.credentialsMasked?.whatsappTemplateOrderDelivered ?? "",
|
|
650
|
+
whatsappTemplateOrderCancelled: s.credentialsMasked?.whatsappTemplateOrderCancelled ?? "",
|
|
651
|
+
whatsappTemplateRefundInitiated: s.credentialsMasked?.whatsappTemplateRefundInitiated ?? "",
|
|
605
652
|
resendApiKey: s.credentialsMasked?.resendApiKey ?? "",
|
|
606
653
|
};
|
|
607
654
|
}, [data]);
|
|
@@ -663,7 +710,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
663
710
|
offsetX: watermarkOffsetX,
|
|
664
711
|
offsetY: watermarkOffsetY,
|
|
665
712
|
},
|
|
666
|
-
commissions: { platformFeePercent, gstPercent, minimumTransactionFee, platformFeeMax, gatewayFeePercent, payoutHoldDays, minPayoutAmount, auctionListingFee, preOrderListingFee, featuredSlotFee, promotedSlotFee, giftWrapFeeEnabled, giftWrapFee, shipmentProtectionFeeEnabled, shipmentProtectionFeePercent, shipmentProtectionFeeMin, saleProtectionFeeEnabled, saleProtectionFeePercent, saleProtectionFeeMin, codHandlingFeePercent, codHandlingFeeCap, codDepositPercent, sellerShippingFixed, platformShippingPercent, platformShippingFixedMin },
|
|
713
|
+
commissions: { platformFeePercent, gstPercent, minimumTransactionFee, platformFeeMax, gatewayFeePercent, payoutHoldDays, whatsappNotifyFee, whatsappNotifyFeeEnabled, minPayoutAmount, auctionListingFee, preOrderListingFee, featuredSlotFee, promotedSlotFee, giftWrapFeeEnabled, giftWrapFee, shipmentProtectionFeeEnabled, shipmentProtectionFeePercent, shipmentProtectionFeeMin, saleProtectionFeeEnabled, saleProtectionFeePercent, saleProtectionFeeMin, codHandlingFeePercent, codHandlingFeeCap, codDepositPercent, sellerShippingFixed, platformShippingPercent, platformShippingFixedMin },
|
|
667
714
|
laborRate: { hourlyRate: laborHourlyRate, maxHoursPerDay: laborMaxHoursPerDay },
|
|
668
715
|
emi: {
|
|
669
716
|
enabled: emiEnabled,
|
|
@@ -706,6 +753,16 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
706
753
|
tiktokAccessToken: maskedOrReal("tiktokAccessToken", tiktokAccessToken),
|
|
707
754
|
deviantartClientId: maskedOrReal("deviantartClientId", deviantartClientId),
|
|
708
755
|
deviantartClientSecret: maskedOrReal("deviantartClientSecret", deviantartClientSecret),
|
|
756
|
+
whatsappPhoneNumberId: maskedOrReal("whatsappPhoneNumberId", waPhoneNumberId),
|
|
757
|
+
whatsappCloudApiToken: maskedOrReal("whatsappCloudApiToken", waCloudApiToken),
|
|
758
|
+
whatsappAdminNotifyNumbers: maskedOrReal("whatsappAdminNotifyNumbers", waAdminNotifyNumbers),
|
|
759
|
+
whatsappTemplateLanguage: maskedOrReal("whatsappTemplateLanguage", waTemplateLanguage),
|
|
760
|
+
whatsappTemplateOrderPlaced: maskedOrReal("whatsappTemplateOrderPlaced", waTemplateOrderPlaced),
|
|
761
|
+
whatsappTemplateOrderConfirmed: maskedOrReal("whatsappTemplateOrderConfirmed", waTemplateOrderConfirmed),
|
|
762
|
+
whatsappTemplateOrderShipped: maskedOrReal("whatsappTemplateOrderShipped", waTemplateOrderShipped),
|
|
763
|
+
whatsappTemplateOrderDelivered: maskedOrReal("whatsappTemplateOrderDelivered", waTemplateOrderDelivered),
|
|
764
|
+
whatsappTemplateOrderCancelled: maskedOrReal("whatsappTemplateOrderCancelled", waTemplateOrderCancelled),
|
|
765
|
+
whatsappTemplateRefundInitiated: maskedOrReal("whatsappTemplateRefundInitiated", waTemplateRefundInitiated),
|
|
709
766
|
resendApiKey: maskedOrReal("resendApiKey", resendApiKey),
|
|
710
767
|
},
|
|
711
768
|
emailSettings: {
|
|
@@ -722,8 +779,10 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
722
779
|
},
|
|
723
780
|
messaging: {
|
|
724
781
|
emailEnabled: msgEmailEnabled,
|
|
782
|
+
whatsappEnabled: msgWhatsappEnabled,
|
|
725
783
|
dailyCeiling: {
|
|
726
784
|
email: msgCeilingEmail,
|
|
785
|
+
whatsapp: msgCeilingWhatsapp,
|
|
727
786
|
// No SMS sender exists; the ceiling is carried so one cannot ship
|
|
728
787
|
// uncapped if somebody adds a provider later.
|
|
729
788
|
sms: 50,
|
|
@@ -743,6 +802,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
743
802
|
notificationChannels: {
|
|
744
803
|
inApp: { enabled: true, readOnly: true },
|
|
745
804
|
email: { enabled: notifEmailEnabled, minPriority: notifEmailMinPriority, types: notifEmailTypes },
|
|
805
|
+
whatsapp: { enabled: notifWhatsappEnabled, minPriority: notifWhatsappMinPriority, otpEnabled: notifWhatsappOtpEnabled, types: notifWhatsappTypes },
|
|
746
806
|
sms: { enabled: notifSmsEnabled, minPriority: notifSmsMinPriority },
|
|
747
807
|
},
|
|
748
808
|
};
|
|
@@ -851,7 +911,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
851
911
|
{ label: "Top right", value: "top-right" },
|
|
852
912
|
{ label: "Bottom left", value: "bottom-left" },
|
|
853
913
|
{ label: "Bottom right", value: "bottom-right" },
|
|
854
|
-
], value: watermarkPosition, onValueChange: (v) => setWatermarkPosition(v), disabled: watermarkUseCustomOffset }), _jsx(Toggle, { checked: watermarkUseCustomOffset, onChange: setWatermarkUseCustomOffset, label: "Use a custom X/Y offset instead" }), watermarkUseCustomOffset && (_jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "X offset (%) \u2014 + right / \u2212 left of center", value: String(watermarkOffsetX), onChange: (e) => setWatermarkOffsetX(Math.max(-45, Math.min(45, parseInt(e.target.value) || 0))), type: "number", min: -45, max: 45 }), _jsx(Input, { label: "Y offset (%) \u2014 + down / \u2212 up from center", value: String(watermarkOffsetY), onChange: (e) => setWatermarkOffsetY(Math.max(-45, Math.min(45, parseInt(e.target.value) || 0))), type: "number", min: -45, max: 45 })] })), _jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "md", children: [_jsx(Text, { size: "xs", color: "muted", children: "Preview (text watermark only)" }), _jsx(Row, { surface: "default", justify: "end", align: "end", className: `relative h-32 ${__O.hidden}`, rounded: "default", children: _jsx(Span, { ref: watermarkPreviewRef, weight: "medium", color: "faint", padding: "1", className: "select-none", children: watermarkText }) })] })] }) }), _jsx(TabsContent, { value: "fees", children: _jsxs(SettingsTabForm, { id: "fees", label: "Fees", children: [_jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Platform fee \u2014 our cut (%)", helperText: "% charged on order value. Buyer pays this.", value: String(platformFeePercent), onChange: (e) => setPlatformFeePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1 }), _jsx(Input, { label: "GST on platform fee (%)", helperText: "Applied to our fee only (not full order). Usually 18%.", value: String(gstPercent), onChange: (e) => setGstPercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1 }), _jsx(Input, { label: "PhonePe gateway cost (%)", helperText: "Gateway's own fee \u2014 absorbed by platform, not passed through.", value: String(gatewayFeePercent), onChange: (e) => setGatewayFeePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 10, step: 0.01 }), _jsx(Input, { label: "Minimum transaction fee (\u20B9)", helperText: "Per-transaction floor. Total charge will never be below base + this.", value: String(minimumTransactionFee), onChange: (e) => setMinimumTransactionFee(parseFloat(e.target.value) || 0), type: "number", min: 0, step: 0.01 }), _jsx(Input, { label: "Maximum platform fee (\u20B9)", helperText: "Ceiling on the buyer's platform commission, charged once per checkout on every payment method. GST is calculated on the capped amount.", value: String(platformFeeMax), onChange: (e) => setPlatformFeeMax(parseFloat(e.target.value) || 0), type: "number", min: 0, step: 0.01 }), _jsx(Input, { label: "Seller payout hold (days)", value: String(payoutHoldDays), onChange: (e) => setPayoutHoldDays(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Minimum payout amount (\u20B9)", value: String(minPayoutAmount), onChange: (e) => setMinPayoutAmount(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Auction listing fee (\u20B9)", helperText: "Not charged yet \u2014 no billing path exists for seller listing fees. Setting a value records the intended price; nothing collects it.", value: String(auctionListingFee), onChange: (e) => setAuctionListingFee(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Pre-order listing fee (\u20B9)", helperText: "Not charged yet \u2014 no billing path exists for seller listing fees. Setting a value records the intended price; nothing collects it.", value: String(preOrderListingFee), onChange: (e) => setPreOrderListingFee(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Featured slot fee (\u20B9)", helperText: "Not charged yet \u2014 no billing path exists for seller listing fees. Setting a value records the intended price; nothing collects it. /admin/deals sets isPromoted for free today.", value: String(featuredSlotFee), onChange: (e) => setFeaturedSlotFee(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Promoted slot fee (\u20B9)", helperText: "Not charged yet \u2014 no billing path exists for seller listing fees. Setting a value records the intended price; nothing collects it. /admin/deals sets isPromoted for free today.", value: String(promotedSlotFee), onChange: (e) => setPromotedSlotFee(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "COD deposit (%)", helperText: "% of order collected upfront on COD orders; remainder paid on delivery.", value: String(codDepositPercent), onChange: (e) => setCodDepositPercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1 })] }), _jsx(Toggle, { checked: giftWrapFeeEnabled, onChange: setGiftWrapFeeEnabled, label: "Offer the gift-wrap addon at checkout" }), _jsx(Grid, { cols: 2, gap: "md", children: _jsx(Input, { label: "Gift wrap fee (\u20B9)", helperText: "Flat fee charged to the buyer when they opt in.", value: String(giftWrapFee), onChange: (e) => setGiftWrapFee(parseFloat(e.target.value) || 0), type: "number", min: 0, disabled: !giftWrapFeeEnabled }) }), _jsx(Toggle, { checked: shipmentProtectionFeeEnabled, onChange: setShipmentProtectionFeeEnabled, label: "Offer the shipment-protection addon at checkout" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Shipment protection (%)", helperText: "% of order subtotal, charged when the buyer opts in.", value: String(shipmentProtectionFeePercent), onChange: (e) => setShipmentProtectionFeePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1, disabled: !shipmentProtectionFeeEnabled }), _jsx(Input, { label: "Shipment protection minimum (\u20B9)", helperText: "Floor for the shipment-protection fee.", value: String(shipmentProtectionFeeMin), onChange: (e) => setShipmentProtectionFeeMin(parseFloat(e.target.value) || 0), type: "number", min: 0, disabled: !shipmentProtectionFeeEnabled })] }), _jsx(Toggle, { checked: saleProtectionFeeEnabled, onChange: setSaleProtectionFeeEnabled, label: "Offer the sale-protection addon at checkout" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Sale protection (%)", helperText: "% of order subtotal, charged when the buyer opts in. Covers the item against not-as-described and counterfeit claims.", value: String(saleProtectionFeePercent), onChange: (e) => setSaleProtectionFeePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1, disabled: !saleProtectionFeeEnabled }), _jsx(Input, { label: "Sale protection minimum (\u20B9)", helperText: "Floor for the sale-protection fee.", value: String(saleProtectionFeeMin), onChange: (e) => setSaleProtectionFeeMin(parseFloat(e.target.value) || 0), type: "number", min: 0, disabled: !saleProtectionFeeEnabled })] }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "COD handling fee (%)", helperText: "% of order subtotal, charged on Cash-on-Delivery orders only.", value: String(codHandlingFeePercent), onChange: (e) => setCodHandlingFeePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1 }), _jsx(Input, { label: "COD handling fee cap (\u20B9)", helperText: "Ceiling for the COD handling fee, however large the order.", value: String(codHandlingFeeCap), onChange: (e) => setCodHandlingFeeCap(parseFloat(e.target.value) || 0), type: "number", min: 0 })] })] }) }), _jsx(TabsContent, { value: "procurement", children: _jsxs(SettingsTabForm, { id: "procurement", label: "Procurement", children: [_jsx(Text, { size: "xs", color: "muted", children: "Used to compute each procurement shipment's labor cost (hours spent \u00D7 hourly rate) and its estimated processing time (hours spent \u00F7 max hours/day)." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Hourly rate (\u20B9)", helperText: "Your effective hourly wage for sorting/categorizing/listing a shipment.", value: String(laborHourlyRate), onChange: (e) => setLaborHourlyRate(Math.round((parseFloat(e.target.value) || 0) * 100) / 100), type: "number", min: 0 }), _jsx(Input, { label: "Max hours per day", helperText: "Used only to project how many days a shipment's tracked hours will take.", value: String(laborMaxHoursPerDay), onChange: (e) => setLaborMaxHoursPerDay(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 24 })] })] }) }), _jsx(TabsContent, { value: "emi", children: _jsxs(SettingsTabForm, { id: "emi", label: "Emi", children: [_jsx(Text, { size: "xs", color: "muted", children: "A seller must also opt in via their own Payout Settings for EMI to appear at checkout on their items. See the \"How EMI Works\" public page for buyer-facing copy." }), _jsx(Toggle, { checked: emiEnabled, onChange: setEmiEnabled, label: "Enable EMI platform-wide" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Minimum order value (\u20B9)", helperText: "A seller's cart subtotal must exceed this for EMI to appear as an option.", value: String(emiMinOrderValue), onChange: (e) => setEmiMinOrderValue(Math.round((parseFloat(e.target.value) || 0) * 100) / 100), type: "number", min: 0 }), _jsx(Input, { label: "Tenure options (months, comma-separated)", helperText: "e.g. 2,3,4,5,6", value: emiTenureOptionsText, onChange: (e) => setEmiTenureOptionsText(e.target.value) }), _jsx(Input, { label: "Token / down-payment (%)", helperText: "Collected upfront at checkout.", value: String(emiTokenPercent), onChange: (e) => setEmiTokenPercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100 }), _jsx(Input, { label: "Billing day (1\u201310)", helperText: "Day of month each installment is due.", value: String(emiBillingDay), onChange: (e) => setEmiBillingDay(parseFloat(e.target.value) || 1), type: "number", min: 1, max: 10 }), _jsx(Input, { label: "Surcharge (% of principal per month)", helperText: "The 'excess EMI fee' the buyer pays for spreading payment.", value: String(emiSurchargePercentPerMonth), onChange: (e) => setEmiSurchargePercentPerMonth(parseFloat(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Seller's share of surcharge (%)", helperText: "The rest of the surcharge goes to the platform.", value: String(emiSurchargeSellerSharePercent), onChange: (e) => setEmiSurchargeSellerSharePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100 })] })] }) }), _jsx(TabsContent, { value: "gst", children: _jsxs(SettingsTabForm, { id: "gst", label: "Gst", children: [_jsx(Text, { size: "xs", color: "muted", children: "Required before enabling GST-inclusive checkout (P-8). Also set a GST rate + HSN code on each taxable product for the tax breakdown to appear." }), _jsx(Toggle, { checked: gstEnabled, onChange: setGstEnabled, label: "Enable GST on checkout" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "GSTIN", helperText: "15-character GST Identification Number.", error: gstProblems.gstin, value: gstin, onChange: (e) => setGstin(e.target.value), placeholder: "29AAAAA0000A1Z5", maxLength: 15 }), _jsx(Input, { label: "Legal name", helperText: "Registered business name for invoices.", error: gstProblems.legalName, value: gstLegalName, onChange: (e) => setGstLegalName(e.target.value), placeholder: "LetItRip Collectibles Pvt Ltd" })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Textarea, { label: "Registered address", value: gstAddress, onChange: (e) => setGstAddress(e.target.value), placeholder: "Registered business address for GST invoices\u2026", rows: 3 }), gstProblems.address ? (_jsx(Text, { size: "sm", variant: "error", role: "alert", children: gstProblems.address })) : null] })] }) }), _jsx(TabsContent, { value: "listings", children: _jsxs(SettingsTabForm, { id: "listings", label: "Listings", children: [_jsx(Text, { size: "xs", color: "muted", children: "Which listing and category types the marketplace offers. A disabled type is hidden from every browse surface and rejected on create and add-to-cart \u2014 existing data is untouched, it just becomes unreachable until re-enabled." }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Listing types" }), _jsx(Text, { size: "xs", color: "muted", children: "Disabled types are hidden from listings and reject create/add-to-cart." }), _jsx(Grid, { cols: 2, gap: "sm", children: LISTING_TYPE_KEYS.map((key) => (_jsx(Toggle, {
|
|
914
|
+
], value: watermarkPosition, onValueChange: (v) => setWatermarkPosition(v), disabled: watermarkUseCustomOffset }), _jsx(Toggle, { checked: watermarkUseCustomOffset, onChange: setWatermarkUseCustomOffset, label: "Use a custom X/Y offset instead" }), watermarkUseCustomOffset && (_jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "X offset (%) \u2014 + right / \u2212 left of center", value: String(watermarkOffsetX), onChange: (e) => setWatermarkOffsetX(Math.max(-45, Math.min(45, parseInt(e.target.value) || 0))), type: "number", min: -45, max: 45 }), _jsx(Input, { label: "Y offset (%) \u2014 + down / \u2212 up from center", value: String(watermarkOffsetY), onChange: (e) => setWatermarkOffsetY(Math.max(-45, Math.min(45, parseInt(e.target.value) || 0))), type: "number", min: -45, max: 45 })] })), _jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "md", children: [_jsx(Text, { size: "xs", color: "muted", children: "Preview (text watermark only)" }), _jsx(Row, { surface: "default", justify: "end", align: "end", className: `relative h-32 ${__O.hidden}`, rounded: "default", children: _jsx(Span, { ref: watermarkPreviewRef, weight: "medium", color: "faint", padding: "1", className: "select-none", children: watermarkText }) })] })] }) }), _jsx(TabsContent, { value: "fees", children: _jsxs(SettingsTabForm, { id: "fees", label: "Fees", children: [_jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Platform fee \u2014 our cut (%)", helperText: "% charged on order value. Buyer pays this.", value: String(platformFeePercent), onChange: (e) => setPlatformFeePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1 }), _jsx(Input, { label: "GST on platform fee (%)", helperText: "Applied to our fee only (not full order). Usually 18%.", value: String(gstPercent), onChange: (e) => setGstPercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1 }), _jsx(Input, { label: "PhonePe gateway cost (%)", helperText: "Gateway's own fee \u2014 absorbed by platform, not passed through.", value: String(gatewayFeePercent), onChange: (e) => setGatewayFeePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 10, step: 0.01 }), _jsx(Input, { label: "Minimum transaction fee (\u20B9)", helperText: "Per-transaction floor. Total charge will never be below base + this.", value: String(minimumTransactionFee), onChange: (e) => setMinimumTransactionFee(parseFloat(e.target.value) || 0), type: "number", min: 0, step: 0.01 }), _jsx(Input, { label: "Maximum platform fee (\u20B9)", helperText: "Ceiling on the buyer's platform commission, charged once per checkout on every payment method. GST is calculated on the capped amount.", value: String(platformFeeMax), onChange: (e) => setPlatformFeeMax(parseFloat(e.target.value) || 0), type: "number", min: 0, step: 0.01 }), _jsx(Input, { label: "Seller payout hold (days)", value: String(payoutHoldDays), onChange: (e) => setPayoutHoldDays(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Minimum payout amount (\u20B9)", value: String(minPayoutAmount), onChange: (e) => setMinPayoutAmount(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Auction listing fee (\u20B9)", helperText: "Not charged yet \u2014 no billing path exists for seller listing fees. Setting a value records the intended price; nothing collects it.", value: String(auctionListingFee), onChange: (e) => setAuctionListingFee(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Pre-order listing fee (\u20B9)", helperText: "Not charged yet \u2014 no billing path exists for seller listing fees. Setting a value records the intended price; nothing collects it.", value: String(preOrderListingFee), onChange: (e) => setPreOrderListingFee(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Featured slot fee (\u20B9)", helperText: "Not charged yet \u2014 no billing path exists for seller listing fees. Setting a value records the intended price; nothing collects it. /admin/deals sets isPromoted for free today.", value: String(featuredSlotFee), onChange: (e) => setFeaturedSlotFee(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Promoted slot fee (\u20B9)", helperText: "Not charged yet \u2014 no billing path exists for seller listing fees. Setting a value records the intended price; nothing collects it. /admin/deals sets isPromoted for free today.", value: String(promotedSlotFee), onChange: (e) => setPromotedSlotFee(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "COD deposit (%)", helperText: "% of order collected upfront on COD orders; remainder paid on delivery.", value: String(codDepositPercent), onChange: (e) => setCodDepositPercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1 })] }), _jsx(Toggle, { checked: whatsappNotifyFeeEnabled, onChange: setWhatsappNotifyFeeEnabled, label: "Offer the WhatsApp order-updates addon at checkout" }), _jsx(Grid, { cols: 2, gap: "md", children: _jsx(Input, { label: "WhatsApp addon fee (\u20B9)", helperText: "Flat fee charged to the buyer when they opt in. Requires WhatsApp credentials + at least one approved template \u2014 see the WhatsApp tab.", value: String(whatsappNotifyFee), onChange: (e) => setWhatsappNotifyFee(parseFloat(e.target.value) || 0), type: "number", min: 0, disabled: !whatsappNotifyFeeEnabled }) }), _jsx(Toggle, { checked: giftWrapFeeEnabled, onChange: setGiftWrapFeeEnabled, label: "Offer the gift-wrap addon at checkout" }), _jsx(Grid, { cols: 2, gap: "md", children: _jsx(Input, { label: "Gift wrap fee (\u20B9)", helperText: "Flat fee charged to the buyer when they opt in.", value: String(giftWrapFee), onChange: (e) => setGiftWrapFee(parseFloat(e.target.value) || 0), type: "number", min: 0, disabled: !giftWrapFeeEnabled }) }), _jsx(Toggle, { checked: shipmentProtectionFeeEnabled, onChange: setShipmentProtectionFeeEnabled, label: "Offer the shipment-protection addon at checkout" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Shipment protection (%)", helperText: "% of order subtotal, charged when the buyer opts in.", value: String(shipmentProtectionFeePercent), onChange: (e) => setShipmentProtectionFeePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1, disabled: !shipmentProtectionFeeEnabled }), _jsx(Input, { label: "Shipment protection minimum (\u20B9)", helperText: "Floor for the shipment-protection fee.", value: String(shipmentProtectionFeeMin), onChange: (e) => setShipmentProtectionFeeMin(parseFloat(e.target.value) || 0), type: "number", min: 0, disabled: !shipmentProtectionFeeEnabled })] }), _jsx(Toggle, { checked: saleProtectionFeeEnabled, onChange: setSaleProtectionFeeEnabled, label: "Offer the sale-protection addon at checkout" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Sale protection (%)", helperText: "% of order subtotal, charged when the buyer opts in. Covers the item against not-as-described and counterfeit claims.", value: String(saleProtectionFeePercent), onChange: (e) => setSaleProtectionFeePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1, disabled: !saleProtectionFeeEnabled }), _jsx(Input, { label: "Sale protection minimum (\u20B9)", helperText: "Floor for the sale-protection fee.", value: String(saleProtectionFeeMin), onChange: (e) => setSaleProtectionFeeMin(parseFloat(e.target.value) || 0), type: "number", min: 0, disabled: !saleProtectionFeeEnabled })] }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "COD handling fee (%)", helperText: "% of order subtotal, charged on Cash-on-Delivery orders only.", value: String(codHandlingFeePercent), onChange: (e) => setCodHandlingFeePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1 }), _jsx(Input, { label: "COD handling fee cap (\u20B9)", helperText: "Ceiling for the COD handling fee, however large the order.", value: String(codHandlingFeeCap), onChange: (e) => setCodHandlingFeeCap(parseFloat(e.target.value) || 0), type: "number", min: 0 })] })] }) }), _jsx(TabsContent, { value: "procurement", children: _jsxs(SettingsTabForm, { id: "procurement", label: "Procurement", children: [_jsx(Text, { size: "xs", color: "muted", children: "Used to compute each procurement shipment's labor cost (hours spent \u00D7 hourly rate) and its estimated processing time (hours spent \u00F7 max hours/day)." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Hourly rate (\u20B9)", helperText: "Your effective hourly wage for sorting/categorizing/listing a shipment.", value: String(laborHourlyRate), onChange: (e) => setLaborHourlyRate(Math.round((parseFloat(e.target.value) || 0) * 100) / 100), type: "number", min: 0 }), _jsx(Input, { label: "Max hours per day", helperText: "Used only to project how many days a shipment's tracked hours will take.", value: String(laborMaxHoursPerDay), onChange: (e) => setLaborMaxHoursPerDay(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 24 })] })] }) }), _jsx(TabsContent, { value: "emi", children: _jsxs(SettingsTabForm, { id: "emi", label: "Emi", children: [_jsx(Text, { size: "xs", color: "muted", children: "A seller must also opt in via their own Payout Settings for EMI to appear at checkout on their items. See the \"How EMI Works\" public page for buyer-facing copy." }), _jsx(Toggle, { checked: emiEnabled, onChange: setEmiEnabled, label: "Enable EMI platform-wide" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Minimum order value (\u20B9)", helperText: "A seller's cart subtotal must exceed this for EMI to appear as an option.", value: String(emiMinOrderValue), onChange: (e) => setEmiMinOrderValue(Math.round((parseFloat(e.target.value) || 0) * 100) / 100), type: "number", min: 0 }), _jsx(Input, { label: "Tenure options (months, comma-separated)", helperText: "e.g. 2,3,4,5,6", value: emiTenureOptionsText, onChange: (e) => setEmiTenureOptionsText(e.target.value) }), _jsx(Input, { label: "Token / down-payment (%)", helperText: "Collected upfront at checkout.", value: String(emiTokenPercent), onChange: (e) => setEmiTokenPercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100 }), _jsx(Input, { label: "Billing day (1\u201310)", helperText: "Day of month each installment is due.", value: String(emiBillingDay), onChange: (e) => setEmiBillingDay(parseFloat(e.target.value) || 1), type: "number", min: 1, max: 10 }), _jsx(Input, { label: "Surcharge (% of principal per month)", helperText: "The 'excess EMI fee' the buyer pays for spreading payment.", value: String(emiSurchargePercentPerMonth), onChange: (e) => setEmiSurchargePercentPerMonth(parseFloat(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Seller's share of surcharge (%)", helperText: "The rest of the surcharge goes to the platform.", value: String(emiSurchargeSellerSharePercent), onChange: (e) => setEmiSurchargeSellerSharePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100 })] })] }) }), _jsx(TabsContent, { value: "gst", children: _jsxs(SettingsTabForm, { id: "gst", label: "Gst", children: [_jsx(Text, { size: "xs", color: "muted", children: "Required before enabling GST-inclusive checkout (P-8). Also set a GST rate + HSN code on each taxable product for the tax breakdown to appear." }), _jsx(Toggle, { checked: gstEnabled, onChange: setGstEnabled, label: "Enable GST on checkout" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "GSTIN", helperText: "15-character GST Identification Number.", error: gstProblems.gstin, value: gstin, onChange: (e) => setGstin(e.target.value), placeholder: "29AAAAA0000A1Z5", maxLength: 15 }), _jsx(Input, { label: "Legal name", helperText: "Registered business name for invoices.", error: gstProblems.legalName, value: gstLegalName, onChange: (e) => setGstLegalName(e.target.value), placeholder: "LetItRip Collectibles Pvt Ltd" })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Textarea, { label: "Registered address", value: gstAddress, onChange: (e) => setGstAddress(e.target.value), placeholder: "Registered business address for GST invoices\u2026", rows: 3 }), gstProblems.address ? (_jsx(Text, { size: "sm", variant: "error", role: "alert", children: gstProblems.address })) : null] })] }) }), _jsx(TabsContent, { value: "listings", children: _jsxs(SettingsTabForm, { id: "listings", label: "Listings", children: [_jsx(Text, { size: "xs", color: "muted", children: "Which listing and category types the marketplace offers. A disabled type is hidden from every browse surface and rejected on create and add-to-cart \u2014 existing data is untouched, it just becomes unreachable until re-enabled." }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Listing types" }), _jsx(Text, { size: "xs", color: "muted", children: "Disabled types are hidden from listings and reject create/add-to-cart." }), _jsx(Grid, { cols: 2, gap: "sm", children: LISTING_TYPE_KEYS.map((key) => (_jsx(Toggle, {
|
|
855
915
|
// The plugin's own display label, not the raw key — the
|
|
856
916
|
// literal version rendered "pre-order" and
|
|
857
917
|
// "digital-code" at an admin.
|
|
@@ -870,7 +930,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
870
930
|
...bidIncrementTiers.slice(0, -1),
|
|
871
931
|
{ upTo: 100, increment: 10 },
|
|
872
932
|
bidIncrementTiers[bidIncrementTiers.length - 1] ?? { upTo: null, increment: 1000 },
|
|
873
|
-
]), children: "+ Add tier" })] }), _jsx(Input, { label: "Auto-extend window (minutes before end)", value: String(autoExtendWindow), onChange: (e) => setAutoExtendWindow(parseInt(e.target.value) || 0), type: "number", min: 0, helperText: "Extend auction end time if a bid arrives within this window." }), _jsx(Input, { label: "Settlement grace period (hours)", value: String(settlementGrace), onChange: (e) => setSettlementGrace(parseInt(e.target.value) || 0), type: "number", min: 1, helperText: "Time winner has to pay before the auction is re-listed." })] }) }), _jsx(TabsContent, { value: "limits", children: _jsx(SettingsTabForm, { id: "limits", label: "Limits", children: _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Max products per store", value: String(maxProductsPerStore), onChange: (e) => setMaxProductsPerStore(parseInt(e.target.value) || 0), type: "number", min: 1 }), _jsx(Input, { label: "Max custom fields per product", value: String(maxCustomFields), onChange: (e) => setMaxCustomFields(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Order cancellation window (hours)", value: String(orderCancelWindow), onChange: (e) => setOrderCancelWindow(parseInt(e.target.value) || 0), type: "number", min: 0 })] }) }) }), _jsx(TabsContent, { value: "notifications", children: _jsxs(SettingsTabForm, { id: "notifications", label: "Notifications", children: [_jsx(Text, { size: "xs", color: "muted", children: "In-app notifications are always on. Enable external channels below to let the platform fan out to email or SMS. Users can further restrict which types they receive." }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Outbound messaging budget" }), _jsxs(Text, { size: "xs", color: "muted", children: ["The master switches. These suppress ", _jsx(Span, { weight: "semibold", children: "user-facing" }), " messages only \u2014 the daily status digest always reaches staff, so turning customer email off never leaves the site unwatched. Transactional messages (checkout verification codes, order receipts) also keep sending, because suppressing those breaks checkout rather than quietening it."] }), _jsx(Toggle, { label: "Send user-facing email", checked: msgEmailEnabled, onChange: setMsgEmailEnabled }), _jsx(Grid, { cols: 2, gap: "md", children: _jsx(Input, { label: "Daily email ceiling", type: "number", value: String(msgCeilingEmail), onChange: (e) => setMsgCeilingEmail(Number(e.target.value) || 0), helperText: "Resend's free tier is 100/day. 80 leaves headroom for password-reset mail and in-flight sends." }) }), _jsx(Text, { size: "xs", color: "muted", children: "Some notification types never email regardless of these settings \u2014 losing an auction, being outbid, marketing, review activity and ticket updates are shown in the notification bell only. That is deliberate: those were the highest-volume sends and the ones most likely to exhaust the day's allowance. A support reply can still be emailed one-off from the ticket screen." })] }), _jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "md", children: [_jsxs(Row, { justify: "between", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "In-app (notification bell)" }), _jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "semibold", rounded: "full", padding: "pill-xs", children: "Always on" })] }), _jsx(Text, { size: "xs", color: "muted", children: "Displayed in the notification bell and inbox. Cannot be disabled." })] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Div, { id: "setting-email-notifications", children: _jsx(Toggle, { label: "Email notifications", checked: notifEmailEnabled, onChange: setNotifEmailEnabled }) }), notifEmailEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send email", options: PRIORITY_OPTIONS, value: notifEmailMinPriority, onValueChange: setNotifEmailMinPriority }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Resend API (for transactional email)" }), _jsx(MaskedInput, { label: "Resend API Key", value: resendApiKey, onChange: setResendApiKey, placeholder: "re_live_\u2026", helperText: "Get your key at resend.com \u2014 used for all transactional notifications." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "From email", value: notifFromEmail, onChange: (e) => setNotifFromEmail(e.target.value), placeholder: "noreply@letitrip.in", type: "email" }), _jsx(Input, { label: "From name", value: notifFromName, onChange: (e) => setNotifFromName(e.target.value), placeholder: "LetItRip" })] }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Notification types (leave empty to send all types)" }), _jsx(PaginatedSelect, { ariaLabel: "Notification types", multiple: true, options: NOTIF_TYPE_OPTIONS, value: notifEmailTypes, onChange: (values) => setNotifEmailTypes(values), placeholder: "All notification types" })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Div, { id: "setting-daily-digest", children: _jsx(Toggle, { label: "Daily status digest", checked: digestEnabled, onChange: setDigestEnabled }) }), digestEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Text, { size: "xs", color: "muted", children: "Sent every morning at 10:00 IST with the previous 24 hours of order activity. Its arrival doubles as the platform health check \u2014 if it stops landing, something needs a look." }), _jsx(Textarea, { label: "Recipients (To)", value: digestRecipients, onChange: (e) => setDigestRecipients(e.target.value), placeholder: "mohasin@letitrip.in", rows: 3, helperText: "One address per line. Each address is billed as a separate send against the daily allowance \u2014 keep this list short." })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Toggle, { label: "SMS notifications", checked: notifSmsEnabled, onChange: setNotifSmsEnabled }), notifSmsEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send SMS", options: PRIORITY_OPTIONS, value: notifSmsMinPriority, onValueChange: setNotifSmsMinPriority }), _jsx(Text, { size: "xs", color: "muted", children: "SMS gateway credentials (e.g. Twilio, MSG91) can be configured in the Integrations tab once an SMS provider is connected." })] }))] })] }) }), _jsx(TabsContent, { value: "legal", children: _jsx(SettingsTabForm, { id: "legal", label: "Legal", children: [
|
|
933
|
+
]), children: "+ Add tier" })] }), _jsx(Input, { label: "Auto-extend window (minutes before end)", value: String(autoExtendWindow), onChange: (e) => setAutoExtendWindow(parseInt(e.target.value) || 0), type: "number", min: 0, helperText: "Extend auction end time if a bid arrives within this window." }), _jsx(Input, { label: "Settlement grace period (hours)", value: String(settlementGrace), onChange: (e) => setSettlementGrace(parseInt(e.target.value) || 0), type: "number", min: 1, helperText: "Time winner has to pay before the auction is re-listed." })] }) }), _jsx(TabsContent, { value: "limits", children: _jsx(SettingsTabForm, { id: "limits", label: "Limits", children: _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Max products per store", value: String(maxProductsPerStore), onChange: (e) => setMaxProductsPerStore(parseInt(e.target.value) || 0), type: "number", min: 1 }), _jsx(Input, { label: "Max custom fields per product", value: String(maxCustomFields), onChange: (e) => setMaxCustomFields(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Order cancellation window (hours)", value: String(orderCancelWindow), onChange: (e) => setOrderCancelWindow(parseInt(e.target.value) || 0), type: "number", min: 0 })] }) }) }), _jsx(TabsContent, { value: "whatsapp", children: _jsxs(SettingsTabForm, { id: "whatsapp", label: "Whatsapp", children: [_jsx(Text, { size: "xs", color: "muted", children: "Platform-level WhatsApp Business Cloud API credentials. Used for automated purchase announcements to admin numbers when orders are placed. Store owners configure their own credentials in Store \u2192 WhatsApp." }), _jsx(Input, { label: "Phone Number ID", value: waPhoneNumberId, onChange: (e) => setWaPhoneNumberId(e.target.value), placeholder: "987654321098765", helperText: "From Meta for Developers \u2192 App \u2192 WhatsApp \u2192 API Setup" }), _jsx(MaskedInput, { label: "Cloud API System User Token", value: waCloudApiToken, onChange: setWaCloudApiToken, placeholder: "EAAxxxxxxxx\u2026", helperText: "Long-lived system user access token with WhatsApp Business permissions" }), _jsx(Input, { label: "Admin Notify Numbers", value: waAdminNotifyNumbers, onChange: (e) => setWaAdminNotifyNumbers(e.target.value), placeholder: "919876543210,918765432109", helperText: "Comma-separated, digits-only, include country code. These receive a WhatsApp message when any order is placed." }), _jsx(Text, { size: "xs", color: "muted", children: "Approved Meta message templates for the buyer-facing WhatsApp order-updates addon (Fees tab). Meta rejects business-initiated free-text WhatsApp messages outside a 24h customer-service window \u2014 a real template name here is required for that notification type to actually deliver. Leave blank to skip WhatsApp for that type (in-app + email still fire)." }), _jsx(Input, { label: "Template language code", value: waTemplateLanguage, onChange: (e) => setWaTemplateLanguage(e.target.value), placeholder: "en", helperText: "BCP-47 code the templates below were approved in, e.g. en or en_US." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Order placed template name", value: waTemplateOrderPlaced, onChange: (e) => setWaTemplateOrderPlaced(e.target.value), placeholder: "order_placed_update" }), _jsx(Input, { label: "Order confirmed template name", value: waTemplateOrderConfirmed, onChange: (e) => setWaTemplateOrderConfirmed(e.target.value), placeholder: "order_confirmed_update" }), _jsx(Input, { label: "Order shipped template name", value: waTemplateOrderShipped, onChange: (e) => setWaTemplateOrderShipped(e.target.value), placeholder: "order_shipped_update" }), _jsx(Input, { label: "Order delivered template name", value: waTemplateOrderDelivered, onChange: (e) => setWaTemplateOrderDelivered(e.target.value), placeholder: "order_delivered_update" }), _jsx(Input, { label: "Order cancelled template name", value: waTemplateOrderCancelled, onChange: (e) => setWaTemplateOrderCancelled(e.target.value), placeholder: "order_cancelled_update" }), _jsx(Input, { label: "Refund initiated template name", value: waTemplateRefundInitiated, onChange: (e) => setWaTemplateRefundInitiated(e.target.value), placeholder: "refund_initiated_update" })] })] }) }), _jsx(TabsContent, { value: "notifications", children: _jsxs(SettingsTabForm, { id: "notifications", label: "Notifications", children: [_jsx(Text, { size: "xs", color: "muted", children: "In-app notifications are always on. Enable external channels below to let the platform fan out to email, WhatsApp, or SMS. Users can further restrict which types they receive." }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Outbound messaging budget" }), _jsxs(Text, { size: "xs", color: "muted", children: ["The master switches. These suppress ", _jsx(Span, { weight: "semibold", children: "user-facing" }), " messages only \u2014 the daily status digest always reaches staff, so turning customer email off never leaves the site unwatched. Transactional messages (checkout verification codes, order receipts) also keep sending, because suppressing those breaks checkout rather than quietening it."] }), _jsx(Toggle, { label: "Send user-facing email", checked: msgEmailEnabled, onChange: setMsgEmailEnabled }), _jsx(Toggle, { label: "Send user-facing WhatsApp", checked: msgWhatsappEnabled, onChange: setMsgWhatsappEnabled }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Daily email ceiling", type: "number", value: String(msgCeilingEmail), onChange: (e) => setMsgCeilingEmail(Number(e.target.value) || 0), helperText: "Resend's free tier is 100/day. 80 leaves headroom for password-reset mail and in-flight sends." }), _jsx(Input, { label: "Daily WhatsApp ceiling", type: "number", value: String(msgCeilingWhatsapp), onChange: (e) => setMsgCeilingWhatsapp(Number(e.target.value) || 0), helperText: "A sanity bound. Meta bills per conversation, not per message." })] }), _jsx(Text, { size: "xs", color: "muted", children: "Some notification types never email regardless of these settings \u2014 losing an auction, being outbid, marketing, review activity and ticket updates are shown in the notification bell only. That is deliberate: those were the highest-volume sends and the ones most likely to exhaust the day's allowance. A support reply can still be emailed one-off from the ticket screen." })] }), _jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "md", children: [_jsxs(Row, { justify: "between", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "In-app (notification bell)" }), _jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "semibold", rounded: "full", padding: "pill-xs", children: "Always on" })] }), _jsx(Text, { size: "xs", color: "muted", children: "Displayed in the notification bell and inbox. Cannot be disabled." })] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Div, { id: "setting-email-notifications", children: _jsx(Toggle, { label: "Email notifications", checked: notifEmailEnabled, onChange: setNotifEmailEnabled }) }), notifEmailEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send email", options: PRIORITY_OPTIONS, value: notifEmailMinPriority, onValueChange: setNotifEmailMinPriority }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Resend API (for transactional email)" }), _jsx(MaskedInput, { label: "Resend API Key", value: resendApiKey, onChange: setResendApiKey, placeholder: "re_live_\u2026", helperText: "Get your key at resend.com \u2014 used for all transactional notifications." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "From email", value: notifFromEmail, onChange: (e) => setNotifFromEmail(e.target.value), placeholder: "noreply@letitrip.in", type: "email" }), _jsx(Input, { label: "From name", value: notifFromName, onChange: (e) => setNotifFromName(e.target.value), placeholder: "LetItRip" })] }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Notification types (leave empty to send all types)" }), _jsx(PaginatedSelect, { ariaLabel: "Notification types", multiple: true, options: NOTIF_TYPE_OPTIONS, value: notifEmailTypes, onChange: (values) => setNotifEmailTypes(values), placeholder: "All notification types" })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Div, { id: "setting-daily-digest", children: _jsx(Toggle, { label: "Daily status digest", checked: digestEnabled, onChange: setDigestEnabled }) }), digestEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Text, { size: "xs", color: "muted", children: "Sent every morning at 10:00 IST with the previous 24 hours of order activity. Its arrival doubles as the platform health check \u2014 if it stops landing, something needs a look." }), _jsx(Textarea, { label: "Recipients (To)", value: digestRecipients, onChange: (e) => setDigestRecipients(e.target.value), placeholder: "mohasin@letitrip.in", rows: 3, helperText: "One address per line. Each address is billed as a separate send against the daily allowance \u2014 keep this list short." })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Div, { id: "setting-whatsapp-notifications", children: _jsx(Toggle, { label: "WhatsApp notifications", checked: notifWhatsappEnabled, onChange: setNotifWhatsappEnabled }) }), notifWhatsappEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send WhatsApp", options: PRIORITY_OPTIONS, value: notifWhatsappMinPriority, onValueChange: setNotifWhatsappMinPriority }), _jsx(Toggle, { label: "Enable WhatsApp OTP (for login and verification)", checked: notifWhatsappOtpEnabled, onChange: setNotifWhatsappOtpEnabled }), _jsx(Text, { size: "xs", color: "muted", children: "WhatsApp credentials are configured in the WhatsApp tab (\u246C). OTP messages use the same phone number." }), _jsx(PaginatedSelect, { multiple: true, ariaLabel: "Notification types to send over WhatsApp", options: NOTIF_TYPE_OPTIONS, value: notifWhatsappTypes, onChange: (values) => setNotifWhatsappTypes(values), placeholder: "All notification types" }), _jsx(Text, { size: "xs", color: "muted", children: "Leave empty to send all types." })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Toggle, { label: "SMS notifications", checked: notifSmsEnabled, onChange: setNotifSmsEnabled }), notifSmsEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send SMS", options: PRIORITY_OPTIONS, value: notifSmsMinPriority, onValueChange: setNotifSmsMinPriority }), _jsx(Text, { size: "xs", color: "muted", children: "SMS gateway credentials (e.g. Twilio, MSG91) can be configured in the Integrations tab once an SMS provider is connected." })] }))] })] }) }), _jsx(TabsContent, { value: "legal", children: _jsx(SettingsTabForm, { id: "legal", label: "Legal", children: [
|
|
874
934
|
["Terms of Service", termsHtml, setTermsHtml],
|
|
875
935
|
["Privacy Policy", privacyHtml, setPrivacyHtml],
|
|
876
936
|
["Refund Policy", refundHtml, setRefundHtml],
|
|
@@ -7,6 +7,17 @@ export interface AdminStoreEditorViewProps {
|
|
|
7
7
|
currentStatus?: string;
|
|
8
8
|
currentIsVerified?: boolean;
|
|
9
9
|
currentIsFeatured?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* 🛑 Both REQUIRED for the controls to show the stored value, and both are
|
|
12
|
+
* the Root Cause #38 trap: `AdminStoresView` seeds this editor from a LIST
|
|
13
|
+
* row, so a field the list serializer omits arrives `undefined`, the control
|
|
14
|
+
* defaults to off, and the save handler sends that wrong default back —
|
|
15
|
+
* silently un-trusting a trusted store while the admin was editing
|
|
16
|
+
* something else. The list route emits `trustedSeller` already; `sellerTier`
|
|
17
|
+
* had to be added to it in the same change.
|
|
18
|
+
*/
|
|
19
|
+
currentSellerTier?: "seller" | "trusted";
|
|
20
|
+
currentTrustedSeller?: boolean;
|
|
10
21
|
currentCapabilities?: string[];
|
|
11
22
|
/**
|
|
12
23
|
* 🛑 Required for the note to DISPLAY. Without it `adminNotes` was hardcoded
|
|
@@ -19,4 +30,4 @@ export interface AdminStoreEditorViewProps {
|
|
|
19
30
|
*/
|
|
20
31
|
currentAdminNotes?: string;
|
|
21
32
|
}
|
|
22
|
-
export declare function AdminStoreEditorView({ open, onClose, storeId, storeName, currentStatus, currentIsVerified, currentIsFeatured, currentCapabilities, currentAdminNotes, }: AdminStoreEditorViewProps): React.JSX.Element;
|
|
33
|
+
export declare function AdminStoreEditorView({ open, onClose, storeId, storeName, currentStatus, currentIsVerified, currentIsFeatured, currentSellerTier, currentTrustedSeller, currentCapabilities, currentAdminNotes, }: AdminStoreEditorViewProps): React.JSX.Element;
|