@mohasinac/appkit 4.63.4 → 4.63.5
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/checkout/actions.d.ts +2 -0
- package/dist/_internal/server/features/checkout/actions.js +22 -5
- package/dist/_internal/server/features/orders/adapters.js +3 -1
- package/dist/_internal/server/features/site-settings/adapters.js +20 -2
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +1 -0
- 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/whatsappNotify.d.ts +35 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.js +140 -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 +28 -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/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 +15 -0
- package/dist/constants/api-endpoints.js +8 -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/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.js +1 -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-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/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/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/homepage/lib/section-renderer.js +1 -1
- 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/StoreCapabilitiesGuideView.js +1 -0
- package/dist/features/stores/components/StoreGuideHubView.js +1 -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 +29 -1
- package/dist/features/stores/schemas/firestore.d.ts +16 -6
- package/dist/features/stores/schemas/firestore.js +3 -6
- package/dist/features/stores/schemas/index.d.ts +86 -0
- package/dist/features/stores/schemas/index.js +14 -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 +3 -0
- package/dist/next/routing/route-map.js +1 -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 +105 -0
- package/dist/schemas/registry.js +2 -1
- package/dist/security/pii-encrypt.js +1 -1
- package/dist/server.d.ts +10 -2
- package/dist/server.js +22 -2
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/NavigationLoader.d.ts +14 -1
- package/dist/ui/components/NavigationLoader.js +79 -30
- package/package.json +1 -1
- package/dist/features/orders/utils/order-announcement-message.d.ts +0 -18
- package/dist/features/orders/utils/order-announcement-message.js +0 -16
|
@@ -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],
|
|
@@ -78,6 +78,7 @@ const CAPABILITY_META = {
|
|
|
78
78
|
lower_commission_rate: { group: CAP_GROUP.PLATFORM, label: "Lower Commission Rate" },
|
|
79
79
|
early_access_features: { group: CAP_GROUP.PLATFORM, label: "Early Access Features" },
|
|
80
80
|
advanced_analytics: { group: CAP_GROUP.PLATFORM, label: "Advanced Analytics" },
|
|
81
|
+
whatsapp_catalog_sync: { group: CAP_GROUP.PLATFORM, label: "WhatsApp Catalog Sync" },
|
|
81
82
|
};
|
|
82
83
|
const CAPABILITY_GROUP_ORDER = [
|
|
83
84
|
CAP_GROUP.LISTINGS,
|
|
@@ -76,33 +76,38 @@ export declare const SITE_SETTINGS_TABS: readonly [{
|
|
|
76
76
|
readonly label: "⑫ Legal";
|
|
77
77
|
readonly description: "Terms, privacy policy, refund policy and the GST registration details.";
|
|
78
78
|
readonly keywords: ["terms", "privacy", "policy", "gstin", "refund policy", "compliance"];
|
|
79
|
+
}, {
|
|
80
|
+
readonly id: "whatsapp";
|
|
81
|
+
readonly label: "⑬ WhatsApp";
|
|
82
|
+
readonly description: "WhatsApp Business credentials and which messages go out over it.";
|
|
83
|
+
readonly keywords: ["whatsapp", "meta", "business api", "order updates"];
|
|
79
84
|
}, {
|
|
80
85
|
readonly id: "notifications";
|
|
81
|
-
readonly label: "
|
|
82
|
-
readonly description: "Which channels are on — email
|
|
86
|
+
readonly label: "⑭ Notifications";
|
|
87
|
+
readonly description: "Which channels are on — email, WhatsApp, SMS — and the daily digest.";
|
|
83
88
|
readonly keywords: ["email", "sms", "alerts", "digest", "notify"];
|
|
84
89
|
}, {
|
|
85
90
|
readonly id: "procurement";
|
|
86
|
-
readonly label: "
|
|
91
|
+
readonly label: "⑮ Procurement";
|
|
87
92
|
readonly description: "Labour rate and the defaults used to price inbound shipments.";
|
|
88
93
|
readonly keywords: ["labour rate", "labor", "import", "landed cost", "supplier"];
|
|
89
94
|
}, {
|
|
90
95
|
readonly id: "emi";
|
|
91
|
-
readonly label: "
|
|
96
|
+
readonly label: "⑯ EMI";
|
|
92
97
|
readonly description: "Instalment plans: eligibility threshold, tenures and the surcharge split.";
|
|
93
98
|
readonly keywords: ["instalments", "installments", "emi", "pay monthly", "finance"];
|
|
94
99
|
}, {
|
|
95
100
|
readonly id: "gst";
|
|
96
|
-
readonly label: "
|
|
101
|
+
readonly label: "⑰ GST";
|
|
97
102
|
readonly description: "GST rates, HSN defaults and the registered business details.";
|
|
98
103
|
readonly keywords: ["tax", "gst", "hsn", "vat", "invoice tax"];
|
|
99
104
|
}, {
|
|
100
105
|
readonly id: "listings";
|
|
101
|
-
readonly label: "
|
|
106
|
+
readonly label: "⑱ Listings";
|
|
102
107
|
readonly description: "Which listing types sellers may create, and their defaults.";
|
|
103
108
|
readonly keywords: ["listing types", "auctions on", "enable", "categories"];
|
|
104
109
|
}];
|
|
105
|
-
export declare const SITE_SETTINGS_TAB_IDS: ("about" | "shipping" | "notifications" | "contact" | "auction" | "seo" | "listings" | "emi" | "legal" | "appearance" | "gst" | "watermark" | "integrations" | "limits" | "announcement" | "branding" | "themes" | "fees" | "procurement")[];
|
|
110
|
+
export declare const SITE_SETTINGS_TAB_IDS: ("about" | "shipping" | "notifications" | "contact" | "auction" | "seo" | "listings" | "whatsapp" | "emi" | "legal" | "appearance" | "gst" | "watermark" | "integrations" | "limits" | "announcement" | "branding" | "themes" | "fees" | "procurement")[];
|
|
106
111
|
/**
|
|
107
112
|
* The union `?tab=` is validated against.
|
|
108
113
|
*
|
|
@@ -108,33 +108,39 @@ export const SITE_SETTINGS_TABS = [
|
|
|
108
108
|
description: "Terms, privacy policy, refund policy and the GST registration details.",
|
|
109
109
|
keywords: ["terms", "privacy", "policy", "gstin", "refund policy", "compliance"],
|
|
110
110
|
},
|
|
111
|
+
{
|
|
112
|
+
id: "whatsapp",
|
|
113
|
+
label: "⑬ WhatsApp",
|
|
114
|
+
description: "WhatsApp Business credentials and which messages go out over it.",
|
|
115
|
+
keywords: ["whatsapp", "meta", "business api", "order updates"],
|
|
116
|
+
},
|
|
111
117
|
{
|
|
112
118
|
id: "notifications",
|
|
113
|
-
label: "
|
|
114
|
-
description: "Which channels are on — email
|
|
119
|
+
label: "⑭ Notifications",
|
|
120
|
+
description: "Which channels are on — email, WhatsApp, SMS — and the daily digest.",
|
|
115
121
|
keywords: ["email", "sms", "alerts", "digest", "notify"],
|
|
116
122
|
},
|
|
117
123
|
{
|
|
118
124
|
id: "procurement",
|
|
119
|
-
label: "
|
|
125
|
+
label: "⑮ Procurement",
|
|
120
126
|
description: "Labour rate and the defaults used to price inbound shipments.",
|
|
121
127
|
keywords: ["labour rate", "labor", "import", "landed cost", "supplier"],
|
|
122
128
|
},
|
|
123
129
|
{
|
|
124
130
|
id: "emi",
|
|
125
|
-
label: "
|
|
131
|
+
label: "⑯ EMI",
|
|
126
132
|
description: "Instalment plans: eligibility threshold, tenures and the surcharge split.",
|
|
127
133
|
keywords: ["instalments", "installments", "emi", "pay monthly", "finance"],
|
|
128
134
|
},
|
|
129
135
|
{
|
|
130
136
|
id: "gst",
|
|
131
|
-
label: "
|
|
137
|
+
label: "⑰ GST",
|
|
132
138
|
description: "GST rates, HSN defaults and the registered business details.",
|
|
133
139
|
keywords: ["tax", "gst", "hsn", "vat", "invoice tax"],
|
|
134
140
|
},
|
|
135
141
|
{
|
|
136
142
|
id: "listings",
|
|
137
|
-
label: "
|
|
143
|
+
label: "⑱ Listings",
|
|
138
144
|
description: "Which listing types sellers may create, and their defaults.",
|
|
139
145
|
keywords: ["listing types", "auctions on", "enable", "categories"],
|
|
140
146
|
},
|
|
@@ -137,7 +137,7 @@ export class SiteSettingsRepository extends BaseRepository {
|
|
|
137
137
|
encrypted[key] = encryptSecret(value.trim());
|
|
138
138
|
}
|
|
139
139
|
else if (value !== undefined && typeof value !== "string") {
|
|
140
|
-
// Non-string credential fields (e.g.
|
|
140
|
+
// Non-string credential fields (e.g. whatsappTemplates: Record<string,string>)
|
|
141
141
|
// are stored as-is — they are not secret values to encrypt.
|
|
142
142
|
encrypted[key] = value;
|
|
143
143
|
}
|
|
@@ -65,6 +65,67 @@ export declare function toPublicProfilePayload(v: {
|
|
|
65
65
|
linkedin?: string;
|
|
66
66
|
}): Record<string, JsonValue> | undefined;
|
|
67
67
|
export type AdminUserUpdateValues = z.infer<typeof adminUserUpdateSchema>;
|
|
68
|
+
/**
|
|
69
|
+
* A store's WhatsApp Business connection.
|
|
70
|
+
*
|
|
71
|
+
* `accessToken` is optional because the form shows a masked placeholder and
|
|
72
|
+
* only sends the field when the seller types a NEW value — so "absent" means
|
|
73
|
+
* "unchanged", not "clear it". The `.min(20)` therefore applies only to a real
|
|
74
|
+
* replacement, and is there because a truncated token fails silently at sync
|
|
75
|
+
* time rather than on save.
|
|
76
|
+
*/
|
|
77
|
+
export declare const whatsappSettingsSchema: z.ZodObject<{
|
|
78
|
+
catalogSyncEnabled: z.ZodBoolean;
|
|
79
|
+
phoneNumber: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
80
|
+
wabaId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
81
|
+
catalogId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
82
|
+
accessToken: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
83
|
+
}, "strip", z.ZodTypeAny, {
|
|
84
|
+
catalogSyncEnabled: boolean;
|
|
85
|
+
phoneNumber?: string | undefined;
|
|
86
|
+
accessToken?: string | undefined;
|
|
87
|
+
wabaId?: string | undefined;
|
|
88
|
+
catalogId?: string | undefined;
|
|
89
|
+
}, {
|
|
90
|
+
catalogSyncEnabled: boolean;
|
|
91
|
+
phoneNumber?: string | undefined;
|
|
92
|
+
accessToken?: string | undefined;
|
|
93
|
+
wabaId?: string | undefined;
|
|
94
|
+
catalogId?: string | undefined;
|
|
95
|
+
}>;
|
|
96
|
+
export type WhatsappSettingsValues = z.infer<typeof whatsappSettingsSchema>;
|
|
97
|
+
/**
|
|
98
|
+
* The CONNECTION form's fields — credentials only.
|
|
99
|
+
*
|
|
100
|
+
* `catalogSyncEnabled` is deliberately absent. Its toggle lives in a different
|
|
101
|
+
* card and owns its own write (`syncToggleMutation`), because the credentials
|
|
102
|
+
* form used to send it unconditionally from two cards away: a seller flipped
|
|
103
|
+
* the switch, saw it move, and nothing persisted it. A field in this schema is
|
|
104
|
+
* a field this form's Save button owns.
|
|
105
|
+
*
|
|
106
|
+
* 🛑 `.omit()` rather than a second literal, so the two cannot drift — it
|
|
107
|
+
* carries the SAME field instances into the new shape, which is what keeps the
|
|
108
|
+
* `annotate()` WeakMap entries alive. (A wrapper such as `.superRefine()` is
|
|
109
|
+
* transparent for the same reason; a field-level re-wrap is not.)
|
|
110
|
+
*/
|
|
111
|
+
export declare const whatsappConnectionSchema: z.ZodObject<Omit<{
|
|
112
|
+
catalogSyncEnabled: z.ZodBoolean;
|
|
113
|
+
phoneNumber: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
114
|
+
wabaId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
115
|
+
catalogId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
116
|
+
accessToken: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
117
|
+
}, "catalogSyncEnabled">, "strip", z.ZodTypeAny, {
|
|
118
|
+
phoneNumber?: string | undefined;
|
|
119
|
+
accessToken?: string | undefined;
|
|
120
|
+
wabaId?: string | undefined;
|
|
121
|
+
catalogId?: string | undefined;
|
|
122
|
+
}, {
|
|
123
|
+
phoneNumber?: string | undefined;
|
|
124
|
+
accessToken?: string | undefined;
|
|
125
|
+
wabaId?: string | undefined;
|
|
126
|
+
catalogId?: string | undefined;
|
|
127
|
+
}>;
|
|
128
|
+
export type WhatsappConnectionValues = z.infer<typeof whatsappConnectionSchema>;
|
|
68
129
|
/**
|
|
69
130
|
* A buyer's lottery entry.
|
|
70
131
|
*
|
|
@@ -8,20 +8,24 @@
|
|
|
8
8
|
* false, and saving any unrelated field silently stripped a real
|
|
9
9
|
* tester's access.
|
|
10
10
|
*
|
|
11
|
+
* `SellerWhatsAppSettingsView` writes `accessToken` — a live Meta
|
|
12
|
+
* credential — plus the WABA and catalog ids the sync runs against. A
|
|
13
|
+
* malformed id there fails at sync time, hours later, with no obvious
|
|
14
|
+
* cause.
|
|
11
15
|
*
|
|
12
16
|
* `LotteryPullForm` is a buyer-facing entry: a transaction id, a payment
|
|
13
17
|
* time and an item number, all parsed with a bare `parseInt` and a `new
|
|
14
18
|
* Date(...)` that yields `Invalid Date` on anything unparseable.
|
|
15
19
|
*
|
|
16
20
|
* EXPORTS:
|
|
17
|
-
* adminUserUpdateSchema, lotteryPullSchema,
|
|
21
|
+
* adminUserUpdateSchema, whatsappSettingsSchema, lotteryPullSchema,
|
|
18
22
|
* and their inferred value types
|
|
19
23
|
*
|
|
20
|
-
* @tag domain:admin,lottery
|
|
24
|
+
* @tag domain:admin,whatsapp,lottery
|
|
21
25
|
* @tag layer:schema
|
|
22
26
|
* @tag pattern:none
|
|
23
27
|
* @tag access:isomorphic
|
|
24
|
-
* @tag consumers:AdminUserEditorView,LotteryPullForm
|
|
28
|
+
* @tag consumers:AdminUserEditorView,SellerWhatsAppSettingsView,LotteryPullForm
|
|
25
29
|
* @tag sideEffects:none
|
|
26
30
|
*/
|
|
27
31
|
import { z } from "zod";
|
|
@@ -127,6 +131,43 @@ export function toPublicProfilePayload(v) {
|
|
|
127
131
|
publicProfile.socialLinks = socialLinks;
|
|
128
132
|
return Object.keys(publicProfile).length > 0 ? publicProfile : undefined;
|
|
129
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* A store's WhatsApp Business connection.
|
|
136
|
+
*
|
|
137
|
+
* `accessToken` is optional because the form shows a masked placeholder and
|
|
138
|
+
* only sends the field when the seller types a NEW value — so "absent" means
|
|
139
|
+
* "unchanged", not "clear it". The `.min(20)` therefore applies only to a real
|
|
140
|
+
* replacement, and is there because a truncated token fails silently at sync
|
|
141
|
+
* time rather than on save.
|
|
142
|
+
*/
|
|
143
|
+
export const whatsappSettingsSchema = z.object({
|
|
144
|
+
catalogSyncEnabled: annotate(z.boolean(), {
|
|
145
|
+
section: "sync", sectionLabel: "Catalog sync", sectionRequired: true, quick: true, order: 1, row: "quarter",
|
|
146
|
+
}),
|
|
147
|
+
phoneNumber: annotate(z.string().trim().regex(PHONE_RE, "Enter the 10-digit WhatsApp Business number.").optional().or(z.literal("")), { section: "connection", sectionLabel: "Connection", order: 1, row: "pair" }),
|
|
148
|
+
// Meta ids are numeric strings; a pasted URL or a stray space is the usual
|
|
149
|
+
// mistake and produces a sync that never runs.
|
|
150
|
+
wabaId: annotate(z.string().trim().regex(/^\d{5,32}$/, "A WABA ID is a numeric id, not a URL.").optional().or(z.literal("")), { section: "connection", order: 2, row: "pair" }),
|
|
151
|
+
catalogId: annotate(z.string().trim().regex(/^\d{5,32}$/, "A catalog ID is a numeric id, not a URL.").optional().or(z.literal("")), { section: "connection", order: 3, row: "pair" }),
|
|
152
|
+
accessToken: annotate(z.string().trim().min(20, "That access token looks truncated.").optional().or(z.literal("")), { section: "connection", order: 4, row: "full", help: "Leave blank to keep the current token." }),
|
|
153
|
+
});
|
|
154
|
+
/**
|
|
155
|
+
* The CONNECTION form's fields — credentials only.
|
|
156
|
+
*
|
|
157
|
+
* `catalogSyncEnabled` is deliberately absent. Its toggle lives in a different
|
|
158
|
+
* card and owns its own write (`syncToggleMutation`), because the credentials
|
|
159
|
+
* form used to send it unconditionally from two cards away: a seller flipped
|
|
160
|
+
* the switch, saw it move, and nothing persisted it. A field in this schema is
|
|
161
|
+
* a field this form's Save button owns.
|
|
162
|
+
*
|
|
163
|
+
* 🛑 `.omit()` rather than a second literal, so the two cannot drift — it
|
|
164
|
+
* carries the SAME field instances into the new shape, which is what keeps the
|
|
165
|
+
* `annotate()` WeakMap entries alive. (A wrapper such as `.superRefine()` is
|
|
166
|
+
* transparent for the same reason; a field-level re-wrap is not.)
|
|
167
|
+
*/
|
|
168
|
+
export const whatsappConnectionSchema = whatsappSettingsSchema.omit({
|
|
169
|
+
catalogSyncEnabled: true,
|
|
170
|
+
});
|
|
130
171
|
/**
|
|
131
172
|
* A buyer's lottery entry.
|
|
132
173
|
*
|