@mohasinac/appkit 2.7.33 → 2.7.35

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.
Files changed (54) hide show
  1. package/dist/_internal/server/jobs/core/auctionSettlement.js +14 -14
  2. package/dist/_internal/server/jobs/core/offerExpiry.js +3 -2
  3. package/dist/_internal/server/jobs/core/onBidPlaced.js +14 -12
  4. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +6 -71
  5. package/dist/_internal/server/jobs/core/onSupportTicketCreate.js +6 -8
  6. package/dist/_internal/server/jobs/core/onSupportTicketUpdate.js +6 -7
  7. package/dist/_internal/server/jobs/core/pendingOrderTimeout.js +11 -10
  8. package/dist/_internal/server/jobs/core/prizeRevealExpiry.js +2 -2
  9. package/dist/_internal/server/jobs/core/prizeRevealOpen.js +2 -2
  10. package/dist/_internal/server/jobs/core/prizeRevealReminder.js +2 -2
  11. package/dist/_internal/shared/actions/action-registry.js +48 -0
  12. package/dist/client.d.ts +4 -0
  13. package/dist/client.js +2 -0
  14. package/dist/features/admin/actions/notification-actions.js +18 -8
  15. package/dist/features/admin/schemas/firestore.d.ts +6 -2
  16. package/dist/features/admin/schemas/firestore.js +4 -0
  17. package/dist/features/homepage/components/FeaturedAuctionsSection.d.ts +4 -1
  18. package/dist/features/homepage/components/FeaturedAuctionsSection.js +2 -2
  19. package/dist/features/homepage/components/FeaturedPreOrdersSection.d.ts +4 -1
  20. package/dist/features/homepage/components/FeaturedPreOrdersSection.js +2 -2
  21. package/dist/features/homepage/components/FeaturedProductsSection.d.ts +3 -6
  22. package/dist/features/homepage/components/FeaturedProductsSection.js +3 -47
  23. package/dist/features/homepage/lib/section-renderer.js +3 -3
  24. package/dist/features/orders/actions/refund-actions.js +2 -2
  25. package/dist/features/seller/actions/offer-actions.js +5 -5
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +1 -0
  28. package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
  29. package/dist/seed/index.d.ts +1 -0
  30. package/dist/seed/index.js +1 -0
  31. package/dist/seed/manifest.js +5 -0
  32. package/dist/seed/offers-seed-data.d.ts +10 -0
  33. package/dist/seed/offers-seed-data.js +264 -0
  34. package/dist/seed/orders-seed-data.js +116 -2
  35. package/dist/seed/support-tickets-seed-data.js +43 -0
  36. package/dist/server.d.ts +1 -0
  37. package/dist/server.js +1 -0
  38. package/dist/tailwind-utilities.css +1 -1
  39. package/dist/ui/components/DateInput.d.ts +27 -0
  40. package/dist/ui/components/DateInput.js +11 -0
  41. package/dist/ui/components/FormField.d.ts +2 -1
  42. package/dist/ui/components/FormField.js +6 -4
  43. package/dist/ui/components/FormField.style.css +13 -0
  44. package/dist/ui/components/HorizontalScroller.js +25 -30
  45. package/dist/ui/components/Input.style.css +42 -4
  46. package/dist/ui/components/OtpInput.d.ts +14 -0
  47. package/dist/ui/components/OtpInput.js +55 -0
  48. package/dist/ui/components/OtpInput.style.css +13 -0
  49. package/dist/ui/components/Select.style.css +23 -6
  50. package/dist/ui/components/Textarea.style.css +31 -4
  51. package/dist/ui/components/index.style.css +1 -0
  52. package/dist/ui/index.d.ts +4 -0
  53. package/dist/ui/index.js +2 -0
  54. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
- import { bidRepository, notificationRepository, orderRepository, productRepository, } from "../../../../repositories";
1
+ import { bidRepository, orderRepository, productRepository, } from "../../../../repositories";
2
+ import { sendNotification } from "../../../../features/admin/actions/notification-actions";
2
3
  import { ProductStatusValues } from "../../../../features/products/schemas/firestore";
3
4
  import { AUCTION_MESSAGES } from "../handlers/messages";
4
5
  async function settleAuction(ctx, product) {
@@ -25,7 +26,9 @@ async function settleAuction(ctx, product) {
25
26
  auctionProductId: product.id,
26
27
  });
27
28
  productRepository.updateStatusInBatch(batch, product.id, ProductStatusValues.SOLD);
28
- notificationRepository.createInBatch(batch, {
29
+ await batch.commit();
30
+ // Fan-out notifications after batch commits.
31
+ await sendNotification({
29
32
  userId: winnerEntry.data.userId,
30
33
  type: "bid_won",
31
34
  priority: "high",
@@ -34,18 +37,15 @@ async function settleAuction(ctx, product) {
34
37
  relatedId: product.id,
35
38
  relatedType: "product",
36
39
  });
37
- loserEntries.slice(0, 50).forEach(({ data: bid }) => {
38
- notificationRepository.createInBatch(batch, {
39
- userId: bid.userId,
40
- type: "bid_lost",
41
- priority: "normal",
42
- title: AUCTION_MESSAGES.LOST_TITLE,
43
- message: AUCTION_MESSAGES.LOST_MESSAGE(product.title),
44
- relatedId: product.id,
45
- relatedType: "product",
46
- });
47
- });
48
- await batch.commit();
40
+ await Promise.allSettled(loserEntries.slice(0, 50).map(({ data: bid }) => sendNotification({
41
+ userId: bid.userId,
42
+ type: "bid_lost",
43
+ priority: "normal",
44
+ title: AUCTION_MESSAGES.LOST_TITLE,
45
+ message: AUCTION_MESSAGES.LOST_MESSAGE(product.title),
46
+ relatedId: product.id,
47
+ relatedType: "product",
48
+ })));
49
49
  ctx.logger.info(`Settled auction ${product.id}`, {
50
50
  winner: winnerEntry.data.userId,
51
51
  winningBid: winnerEntry.data.bidAmount,
@@ -1,4 +1,5 @@
1
- import { offerRepository, notificationRepository } from "../../../../repositories";
1
+ import { offerRepository } from "../../../../repositories";
2
+ import { sendNotification } from "../../../../features/admin/actions/notification-actions";
2
3
  export async function runOfferExpiry(ctx) {
3
4
  ctx.logger.info("Starting offer expiry sweep");
4
5
  let expiredOffers;
@@ -18,7 +19,7 @@ export async function runOfferExpiry(ctx) {
18
19
  for (const offer of expiredOffers) {
19
20
  try {
20
21
  expiredIds.push(offer.id);
21
- await notificationRepository.create({
22
+ await sendNotification({
22
23
  userId: offer.buyerUid,
23
24
  type: "offer_expired",
24
25
  priority: "normal",
@@ -1,5 +1,6 @@
1
- import { bidRepository, notificationRepository, productRepository, } from "../../../../repositories";
1
+ import { bidRepository, productRepository, } from "../../../../repositories";
2
2
  import { decryptPii } from "../../../../security/index";
3
+ import { sendNotification } from "../../../../features/admin/actions/notification-actions";
3
4
  import { getAdminRealtimeDb } from "../../../../providers/db-firebase";
4
5
  import { BID_MESSAGES } from "../handlers/messages";
5
6
  const BIDS_COLLECTION = "bids";
@@ -15,33 +16,34 @@ export async function handleBidPlaced(input, ctx) {
15
16
  if (prevWinning && prevWinning.data.userId !== newBid.userId) {
16
17
  outbidUserId = prevWinning.data.userId;
17
18
  bidRepository.markOutbid(batch, prevWinning.ref);
18
- notificationRepository.createInBatch(batch, {
19
+ }
20
+ productRepository.incrementBidCountInBatch(batch, newBid.productId, newBid.bidAmount);
21
+ await batch.commit();
22
+ if (outbidUserId) {
23
+ const outbidMessage = BID_MESSAGES.OUTBID_MESSAGE(newBid.productTitle, newBid.currency, newBid.bidAmount);
24
+ await sendNotification({
19
25
  userId: outbidUserId,
20
26
  type: "bid_outbid",
21
27
  priority: "high",
22
28
  title: BID_MESSAGES.OUTBID_TITLE,
23
- message: BID_MESSAGES.OUTBID_MESSAGE(newBid.productTitle, newBid.currency, newBid.bidAmount),
29
+ message: outbidMessage,
24
30
  relatedId: newBid.productId,
25
31
  relatedType: "product",
26
32
  });
27
- }
28
- productRepository.incrementBidCountInBatch(batch, newBid.productId, newBid.bidAmount);
29
- await batch.commit();
30
- try {
31
- if (outbidUserId) {
33
+ try {
32
34
  await getAdminRealtimeDb()
33
35
  .ref(`notifications/${outbidUserId}`)
34
36
  .push({
35
37
  type: "bid_outbid",
36
38
  title: BID_MESSAGES.OUTBID_TITLE,
37
- message: BID_MESSAGES.OUTBID_MESSAGE(newBid.productTitle, newBid.currency, newBid.bidAmount),
39
+ message: outbidMessage,
38
40
  timestamp: Date.now(),
39
41
  read: false,
40
42
  });
41
43
  }
42
- }
43
- catch (rtdbError) {
44
- ctx.logger.error("Realtime DB push failed (non-fatal)", rtdbError);
44
+ catch (rtdbError) {
45
+ ctx.logger.error("Realtime DB push failed (non-fatal)", rtdbError);
46
+ }
45
47
  }
46
48
  ctx.logger.info(`Bid placed on ${newBid.productId}`, {
47
49
  bidId,
@@ -1,87 +1,33 @@
1
- import { notificationRepository } from "../../../../repositories";
2
- import { decryptPii } from "../../../../security/index";
3
1
  import { getAdminRealtimeDb } from "../../../../providers/db-firebase";
4
- import { ORDER_MESSAGES, EMAIL_SUBJECTS, JOB_ERROR_MESSAGES } from "../handlers/messages";
2
+ import { decryptPii } from "../../../../security/index";
3
+ import { sendNotification } from "../../../../features/admin/actions/notification-actions";
4
+ import { ORDER_MESSAGES } from "../handlers/messages";
5
5
  const STATUS_CONFIG = {
6
6
  confirmed: {
7
7
  type: "order_confirmed",
8
8
  title: ORDER_MESSAGES.CONFIRMED_TITLE,
9
9
  message: (o) => ORDER_MESSAGES.CONFIRMED_MESSAGE(o.productTitle),
10
10
  priority: "normal",
11
- sendEmail: true,
12
11
  },
13
12
  shipped: {
14
13
  type: "order_shipped",
15
14
  title: ORDER_MESSAGES.SHIPPED_TITLE,
16
15
  message: (o) => ORDER_MESSAGES.SHIPPED_MESSAGE(o.productTitle, o.trackingNumber),
17
16
  priority: "high",
18
- sendEmail: true,
19
17
  },
20
18
  delivered: {
21
19
  type: "order_delivered",
22
20
  title: ORDER_MESSAGES.DELIVERED_TITLE,
23
21
  message: (o) => ORDER_MESSAGES.DELIVERED_MESSAGE(o.productTitle),
24
22
  priority: "normal",
25
- sendEmail: true,
26
23
  },
27
24
  cancelled: {
28
25
  type: "order_cancelled",
29
26
  title: ORDER_MESSAGES.CANCELLED_TITLE,
30
27
  message: (o) => `Your order for "${o.productTitle}" has been cancelled.`,
31
28
  priority: "normal",
32
- sendEmail: false,
33
29
  },
34
30
  };
35
- async function sendResendEmail(params) {
36
- const subject = params.status === "confirmed"
37
- ? EMAIL_SUBJECTS.ORDER_CONFIRMED(params.productTitle)
38
- : params.status === "shipped"
39
- ? EMAIL_SUBJECTS.ORDER_SHIPPED(params.productTitle)
40
- : params.status === "delivered"
41
- ? EMAIL_SUBJECTS.ORDER_DELIVERED(params.productTitle)
42
- : EMAIL_SUBJECTS.ORDER_UPDATE_FALLBACK(params.productTitle);
43
- const response = await fetch("https://api.resend.com/emails", {
44
- method: "POST",
45
- headers: {
46
- Authorization: `Bearer ${params.apiKey}`,
47
- "Content-Type": "application/json",
48
- },
49
- body: JSON.stringify({
50
- from: params.fromAddress,
51
- to: [params.to],
52
- subject,
53
- html: `<p>Hi,</p><p>Your order for <strong>${params.productTitle}</strong> is now <strong>${params.status}</strong>.</p>${params.trackingNumber ? `<p>Tracking number: ${params.trackingNumber}</p>` : ""}<p>Thanks,<br/>${params.brandName} Team</p>`,
54
- }),
55
- });
56
- if (!response.ok) {
57
- const body = await response.text();
58
- throw new Error(JOB_ERROR_MESSAGES.RESEND_API_ERROR(response.status, body));
59
- }
60
- }
61
- async function sendStatusChangeEmail(ctx, userEmail, newStatus, orderId, after, apiKey) {
62
- try {
63
- const fromAddress = ctx.env("ORDER_EMAIL_FROM") ?? ctx.env("RESEND_FROM_ADDRESS") ?? "";
64
- const brandName = ctx.env("APPKIT_BRAND_NAME") ?? "";
65
- if (!fromAddress || !brandName) {
66
- ctx.logger.error("ORDER_EMAIL_FROM / APPKIT_BRAND_NAME not configured — skipping order status email", null, { orderId });
67
- }
68
- else {
69
- await sendResendEmail({
70
- to: userEmail,
71
- status: newStatus,
72
- orderId,
73
- productTitle: after.productTitle,
74
- trackingNumber: after.trackingNumber,
75
- apiKey,
76
- fromAddress,
77
- brandName,
78
- });
79
- }
80
- }
81
- catch (emailError) {
82
- ctx.logger.error("Email send failed (non-fatal)", emailError, { orderId });
83
- }
84
- }
85
31
  export async function handleOrderStatusChange(input, ctx) {
86
32
  const { orderId, before, after } = input;
87
33
  if (!before || !after)
@@ -100,7 +46,7 @@ export async function handleOrderStatusChange(input, ctx) {
100
46
  productTitle: after.productTitle,
101
47
  trackingNumber: after.trackingNumber,
102
48
  });
103
- await notificationRepository.create({
49
+ await sendNotification({
104
50
  userId: after.userId,
105
51
  type: config.type,
106
52
  priority: config.priority,
@@ -108,6 +54,7 @@ export async function handleOrderStatusChange(input, ctx) {
108
54
  message: messageText,
109
55
  relatedId: orderId,
110
56
  relatedType: "order",
57
+ userEmail,
111
58
  });
112
59
  try {
113
60
  await getAdminRealtimeDb().ref(`notifications/${after.userId}`).push({
@@ -121,19 +68,7 @@ export async function handleOrderStatusChange(input, ctx) {
121
68
  catch (rtdbError) {
122
69
  ctx.logger.error("Realtime DB push failed (non-fatal)", rtdbError);
123
70
  }
124
- if (config.sendEmail) {
125
- const apiKey = ctx.env("RESEND_API_KEY") ?? "";
126
- if (!apiKey) {
127
- ctx.logger.error(JOB_ERROR_MESSAGES.RESEND_KEY_MISSING, null);
128
- }
129
- else {
130
- await sendStatusChangeEmail(ctx, userEmail, newStatus, orderId, after, apiKey);
131
- }
132
- }
133
- ctx.logger.info(`Order ${orderId} status → ${newStatus}`, {
134
- userId: after.userId,
135
- emailSent: config.sendEmail,
136
- });
71
+ ctx.logger.info(`Order ${orderId} status → ${newStatus}`, { userId: after.userId });
137
72
  }
138
73
  catch (error) {
139
74
  ctx.logger.error("Error handling order status change", error, { orderId });
@@ -1,4 +1,4 @@
1
- import { notificationRepository } from "../../../../repositories";
1
+ import { sendNotification } from "../../../../features/admin/actions/notification-actions";
2
2
  export async function handleSupportTicketCreate(input, ctx) {
3
3
  const { ticketId, ticket } = input;
4
4
  const userId = ticket.userId;
@@ -6,17 +6,15 @@ export async function handleSupportTicketCreate(input, ctx) {
6
6
  ctx.logger.warn("onSupportTicketCreate: no userId on ticket", { ticketId });
7
7
  return;
8
8
  }
9
- // Confirm to the user their ticket was received
10
9
  try {
11
- await notificationRepository.create({
10
+ await sendNotification({
12
11
  userId,
13
12
  type: "account_action",
13
+ priority: "normal",
14
14
  title: "Support ticket received",
15
- body: `We received your support request: "${ticket.subject ?? "your ticket"}". We'll get back to you soon.`,
16
- isRead: false,
17
- entityId: ticketId,
18
- entityType: "support_ticket",
19
- createdAt: new Date(),
15
+ message: `We received your support request: "${ticket.subject ?? "your ticket"}". We'll get back to you soon.`,
16
+ relatedId: ticketId,
17
+ relatedType: "support_ticket",
20
18
  });
21
19
  }
22
20
  catch (err) {
@@ -1,4 +1,4 @@
1
- import { notificationRepository } from "../../../../repositories";
1
+ import { sendNotification } from "../../../../features/admin/actions/notification-actions";
2
2
  const USER_NOTIFY_STATUSES = new Set(["resolved", "closed", "waiting_on_user", "in_progress"]);
3
3
  const STATUS_MESSAGES = {
4
4
  resolved: {
@@ -32,15 +32,14 @@ export async function handleSupportTicketUpdate(input, ctx) {
32
32
  if (!msg)
33
33
  return;
34
34
  try {
35
- await notificationRepository.create({
35
+ await sendNotification({
36
36
  userId,
37
37
  type: "account_action",
38
+ priority: "normal",
38
39
  title: msg.title,
39
- body: msg.body(subject),
40
- isRead: false,
41
- entityId: ticketId,
42
- entityType: "support_ticket",
43
- createdAt: new Date(),
40
+ message: msg.body(subject),
41
+ relatedId: ticketId,
42
+ relatedType: "support_ticket",
44
43
  });
45
44
  }
46
45
  catch (err) {
@@ -1,4 +1,5 @@
1
- import { notificationRepository, orderRepository } from "../../../../repositories";
1
+ import { orderRepository } from "../../../../repositories";
2
+ import { sendNotification } from "../../../../features/admin/actions/notification-actions";
2
3
  import { ORDER_MESSAGES } from "../handlers/messages";
3
4
  const DEFAULT_TIMEOUT_HOURS = 24;
4
5
  export async function runPendingOrderTimeout(ctx) {
@@ -12,16 +13,16 @@ export async function runPendingOrderTimeout(ctx) {
12
13
  const batch = ctx.db.batch();
13
14
  for (const entry of timedOut) {
14
15
  orderRepository.cancelInBatch(batch, entry.ref);
15
- notificationRepository.createInBatch(batch, {
16
- userId: entry.data.userId,
17
- type: "order_cancelled",
18
- priority: "normal",
19
- title: ORDER_MESSAGES.CANCELLED_TITLE,
20
- message: ORDER_MESSAGES.CANCELLED_TIMEOUT_MESSAGE(entry.data.productTitle, timeoutHours),
21
- relatedId: entry.data.id,
22
- relatedType: "order",
23
- });
24
16
  }
25
17
  await batch.commit();
18
+ await Promise.allSettled(timedOut.map((entry) => sendNotification({
19
+ userId: entry.data.userId,
20
+ type: "order_cancelled",
21
+ priority: "normal",
22
+ title: ORDER_MESSAGES.CANCELLED_TITLE,
23
+ message: ORDER_MESSAGES.CANCELLED_TIMEOUT_MESSAGE(entry.data.productTitle, timeoutHours),
24
+ relatedId: entry.data.id,
25
+ relatedType: "order",
26
+ })));
26
27
  ctx.logger.info("Pending order timeout complete", { cancelled: timedOut.length });
27
28
  }
@@ -1,4 +1,4 @@
1
- import { notificationRepository } from "../../../../repositories";
1
+ import { sendNotification } from "../../../../features/admin/actions/notification-actions";
2
2
  import { ORDER_FIELDS, PRODUCT_FIELDS, COMMON_FIELDS } from "../../../../constants/field-names";
3
3
  const ORDER_COLLECTION = "orders";
4
4
  export async function runPrizeRevealExpiry(ctx) {
@@ -34,7 +34,7 @@ export async function runPrizeRevealExpiry(ctx) {
34
34
  });
35
35
  if (order.userId) {
36
36
  try {
37
- await notificationRepository.create({
37
+ await sendNotification({
38
38
  userId: order.userId,
39
39
  type: "prize_reveal_expired",
40
40
  priority: "high",
@@ -1,4 +1,4 @@
1
- import { notificationRepository } from "../../../../repositories";
1
+ import { sendNotification } from "../../../../features/admin/actions/notification-actions";
2
2
  import { PRODUCT_FIELDS, ORDER_FIELDS, COMMON_FIELDS } from "../../../../constants/field-names";
3
3
  const PRODUCT_COLLECTION = "products";
4
4
  const ORDER_COLLECTION = "orders";
@@ -38,7 +38,7 @@ export async function runPrizeRevealOpen(ctx) {
38
38
  if (!order.userId || order.prizeWon)
39
39
  continue;
40
40
  try {
41
- await notificationRepository.create({
41
+ await sendNotification({
42
42
  userId: order.userId,
43
43
  type: "prize_reveal_ready",
44
44
  priority: "high",
@@ -1,4 +1,4 @@
1
- import { notificationRepository } from "../../../../repositories";
1
+ import { sendNotification } from "../../../../features/admin/actions/notification-actions";
2
2
  import { ORDER_FIELDS, PRODUCT_FIELDS } from "../../../../constants/field-names";
3
3
  const ORDER_COLLECTION = "orders";
4
4
  const ONE_DAY_MS = 86400000;
@@ -23,7 +23,7 @@ export async function runPrizeRevealReminder(ctx) {
23
23
  if (!order.userId || order.prizeWon || !order.prizeDrawProductId)
24
24
  continue;
25
25
  try {
26
- await notificationRepository.create({
26
+ await sendNotification({
27
27
  userId: order.userId,
28
28
  type: "prize_reveal_reminder",
29
29
  priority: "normal",
@@ -620,6 +620,54 @@ export const ACTIONS = {
620
620
  description: "Submit the order with the chosen payment method.",
621
621
  kind: "primary",
622
622
  },
623
+ "continue-to-verification": {
624
+ id: "checkout.continue-to-verification",
625
+ label: "Continue to Verification",
626
+ description: "Advance from address selection to identity verification step.",
627
+ kind: "primary",
628
+ },
629
+ "send-otp": {
630
+ id: "checkout.send-otp",
631
+ label: "Send verification code",
632
+ description: "Send a one-time code to the buyer's registered email to verify identity before checkout.",
633
+ kind: "primary",
634
+ },
635
+ "verify-otp": {
636
+ id: "checkout.verify-otp",
637
+ label: "Verify & Continue",
638
+ description: "Submit the one-time code and proceed to payment selection.",
639
+ kind: "primary",
640
+ },
641
+ "resend-otp": {
642
+ id: "checkout.resend-otp",
643
+ label: "Resend code",
644
+ description: "Re-send the verification code to the buyer's registered email.",
645
+ kind: "ghost",
646
+ },
647
+ "pay-online": {
648
+ id: "checkout.pay-online",
649
+ label: "Pay Online (Razorpay)",
650
+ description: "Initiate an online payment via Razorpay UPI/Card/NetBanking.",
651
+ kind: "primary",
652
+ },
653
+ "pay-cod": {
654
+ id: "checkout.pay-cod",
655
+ label: "Cash on Delivery",
656
+ description: "Place the order for cash-on-delivery payment.",
657
+ kind: "secondary",
658
+ },
659
+ "admin-bypass": {
660
+ id: "checkout.admin-bypass",
661
+ label: "Skip Verification — Admin Bypass",
662
+ description: "Admin test mode: skip identity verification and place a test order without payment.",
663
+ kind: "secondary",
664
+ },
665
+ "admin-bypass-payment": {
666
+ id: "checkout.admin-bypass-payment",
667
+ label: "No Payment — Admin Bypass Order",
668
+ description: "Admin test mode: place a real order record without charging any payment.",
669
+ kind: "secondary",
670
+ },
623
671
  },
624
672
  NAV: {
625
673
  "sign-in": {
package/dist/client.d.ts CHANGED
@@ -53,6 +53,10 @@ export { Badge } from "./ui/components/Badge";
53
53
  export { Button } from "./ui/components/Button";
54
54
  export { Checkbox } from "./ui/components/Checkbox";
55
55
  export { Input } from "./ui/components/Input";
56
+ export { OtpInput } from "./ui/components/OtpInput";
57
+ export type { OtpInputProps } from "./ui/components/OtpInput";
58
+ export { DateInput, DateRangeInput } from "./ui/components/DateInput";
59
+ export type { DateInputProps, DateRangeInputProps } from "./ui/components/DateInput";
56
60
  export { Select } from "./ui/components/Select";
57
61
  export type { SelectOption, SelectProps } from "./ui/components/Select";
58
62
  export { Heading } from "./ui/components/Typography";
package/dist/client.js CHANGED
@@ -121,6 +121,8 @@ export { Badge } from "./ui/components/Badge";
121
121
  export { Button } from "./ui/components/Button";
122
122
  export { Checkbox } from "./ui/components/Checkbox";
123
123
  export { Input } from "./ui/components/Input";
124
+ export { OtpInput } from "./ui/components/OtpInput";
125
+ export { DateInput, DateRangeInput } from "./ui/components/DateInput";
124
126
  export { Select } from "./ui/components/Select";
125
127
  export { Heading } from "./ui/components/Typography";
126
128
  export { Label, Text } from "./ui/components/Typography";
@@ -4,6 +4,7 @@ import { userRepository } from "../../auth/repository/user.repository";
4
4
  import { createResendProvider } from "../../../providers/email-resend/provider";
5
5
  import { sendWhatsAppBusinessMessage } from "../../whatsapp-bot/helpers/whatsapp";
6
6
  import { serverLogger } from "../../../monitoring";
7
+ import { decryptPii } from "../../../security/index";
7
8
  import { DEFAULT_NOTIFICATION_CHANNELS, meetsMinPriority, } from "../schemas/firestore";
8
9
  export async function markNotificationRead(id) {
9
10
  await notificationRepository.markAsRead(id);
@@ -53,14 +54,20 @@ export async function sendNotification(input) {
53
54
  // Load user notification preferences (best-effort — if missing, all channels allowed).
54
55
  let userChannelPrefs = {};
55
56
  let userTypePrefs = {};
57
+ let fetchedUserDoc;
56
58
  try {
57
- const userDoc = await userRepository.findById(input.userId);
58
- userChannelPrefs = userDoc?.notificationPreferences?.channels ?? {};
59
- userTypePrefs = userDoc?.notificationPreferences?.types ?? {};
59
+ fetchedUserDoc = await userRepository.findById(input.userId);
60
+ userChannelPrefs = fetchedUserDoc?.notificationPreferences?.channels ?? {};
61
+ userTypePrefs = fetchedUserDoc?.notificationPreferences?.types ?? {};
60
62
  }
61
63
  catch {
62
64
  // non-fatal: fall through with empty prefs (all enabled)
63
65
  }
66
+ // If caller didn't supply userEmail, try to decrypt it from the already-fetched user doc.
67
+ const resolvedEmail = userEmail ??
68
+ (fetchedUserDoc?.email ? decryptPii(fetchedUserDoc.email) : undefined);
69
+ const resolvedPhone = userPhone ??
70
+ (fetchedUserDoc?.phoneNumber ? decryptPii(fetchedUserDoc.phoneNumber) : undefined);
64
71
  // Map NotificationType to a NotificationTypePrefs key.
65
72
  const typeToPrefsKey = {
66
73
  order_placed: "orderUpdates", order_confirmed: "orderUpdates",
@@ -69,11 +76,14 @@ export async function sendNotification(input) {
69
76
  bid_placed: "bids", bid_outbid: "bids", bid_won: "bids", bid_lost: "bids",
70
77
  review_approved: "reviews", review_replied: "reviews",
71
78
  promotion: "promotions",
72
- system: "system", welcome: "system",
79
+ system: "system", welcome: "system", account_action: "system",
73
80
  offer_received: "offers", offer_responded: "offers",
74
81
  offer_expired: "offers", offer_counter_accepted: "offers",
75
82
  refund_initiated: "orderUpdates",
76
83
  product_available: "system",
84
+ prize_reveal_ready: "orderUpdates",
85
+ prize_reveal_expired: "orderUpdates",
86
+ prize_reveal_reminder: "orderUpdates",
77
87
  };
78
88
  const typeKey = typeToPrefsKey[type];
79
89
  // If the user has explicitly disabled this notification type, skip all external channels.
@@ -84,7 +94,7 @@ export async function sendNotification(input) {
84
94
  let emailStatus = "skipped";
85
95
  if (channels.email.enabled &&
86
96
  userChannelPrefs.email !== false &&
87
- userEmail &&
97
+ resolvedEmail &&
88
98
  meetsMinPriority(priority, channels.email.minPriority) &&
89
99
  (!channels.email.types?.length || channels.email.types.includes(type))) {
90
100
  const apiKey = creds.resendApiKey?.trim() ?? "";
@@ -94,7 +104,7 @@ export async function sendNotification(input) {
94
104
  try {
95
105
  const emailProvider = createResendProvider({ apiKey, fromEmail, fromName });
96
106
  await emailProvider.send({
97
- to: userEmail,
107
+ to: resolvedEmail,
98
108
  subject: title,
99
109
  html: emailHtml ?? `<p>${message}</p>`,
100
110
  text: message,
@@ -111,7 +121,7 @@ export async function sendNotification(input) {
111
121
  let whatsappStatus = "skipped";
112
122
  if (channels.whatsapp.enabled &&
113
123
  userChannelPrefs.whatsapp !== false &&
114
- userPhone &&
124
+ resolvedPhone &&
115
125
  meetsMinPriority(priority, channels.whatsapp.minPriority) &&
116
126
  (!channels.whatsapp.types?.length || channels.whatsapp.types.includes(type))) {
117
127
  const phoneNumberId = creds.whatsappPhoneNumberId?.trim() ?? "";
@@ -119,7 +129,7 @@ export async function sendNotification(input) {
119
129
  if (phoneNumberId && accessToken) {
120
130
  try {
121
131
  const ok = await sendWhatsAppBusinessMessage({
122
- toPhone: userPhone,
132
+ toPhone: resolvedPhone,
123
133
  message: `*${title}*\n${message}`,
124
134
  phoneNumberId,
125
135
  accessToken,
@@ -2,7 +2,7 @@
2
2
  * Admin Feature Firestore Document Types & Constants
3
3
  * Covers: notifications, chat rooms, site settings
4
4
  */
5
- export type NotificationType = "order_placed" | "order_confirmed" | "order_shipped" | "order_delivered" | "order_cancelled" | "bid_placed" | "bid_outbid" | "bid_won" | "bid_lost" | "review_approved" | "review_replied" | "product_available" | "promotion" | "system" | "welcome" | "offer_received" | "offer_responded" | "offer_expired" | "offer_counter_accepted" | "refund_initiated";
5
+ export type NotificationType = "order_placed" | "order_confirmed" | "order_shipped" | "order_delivered" | "order_cancelled" | "bid_placed" | "bid_outbid" | "bid_won" | "bid_lost" | "review_approved" | "review_replied" | "product_available" | "promotion" | "system" | "welcome" | "account_action" | "offer_received" | "offer_responded" | "offer_expired" | "offer_counter_accepted" | "refund_initiated" | "prize_reveal_ready" | "prize_reveal_expired" | "prize_reveal_reminder";
6
6
  export type NotificationPriority = "low" | "normal" | "high";
7
7
  export interface NotificationDocument {
8
8
  id: string;
@@ -17,7 +17,7 @@ export interface NotificationDocument {
17
17
  isRead: boolean;
18
18
  readAt?: Date;
19
19
  relatedId?: string;
20
- relatedType?: "order" | "product" | "bid" | "review" | "blog" | "user" | "offer";
20
+ relatedType?: "order" | "product" | "bid" | "review" | "blog" | "user" | "offer" | "support_ticket";
21
21
  createdAt: Date;
22
22
  updatedAt: Date;
23
23
  }
@@ -60,6 +60,10 @@ export declare const NOTIFICATION_FIELDS: {
60
60
  readonly OFFER_EXPIRED: NotificationType;
61
61
  readonly OFFER_COUNTER_ACCEPTED: NotificationType;
62
62
  readonly REFUND_INITIATED: NotificationType;
63
+ readonly ACCOUNT_ACTION: NotificationType;
64
+ readonly PRIZE_REVEAL_READY: NotificationType;
65
+ readonly PRIZE_REVEAL_EXPIRED: NotificationType;
66
+ readonly PRIZE_REVEAL_REMINDER: NotificationType;
63
67
  };
64
68
  readonly PRIORITY_VALUES: {
65
69
  readonly LOW: NotificationPriority;
@@ -46,6 +46,10 @@ export const NOTIFICATION_FIELDS = {
46
46
  OFFER_EXPIRED: "offer_expired",
47
47
  OFFER_COUNTER_ACCEPTED: "offer_counter_accepted",
48
48
  REFUND_INITIATED: "refund_initiated",
49
+ ACCOUNT_ACTION: "account_action",
50
+ PRIZE_REVEAL_READY: "prize_reveal_ready",
51
+ PRIZE_REVEAL_EXPIRED: "prize_reveal_expired",
52
+ PRIZE_REVEAL_REMINDER: "prize_reveal_reminder",
49
53
  },
50
54
  PRIORITY_VALUES: {
51
55
  LOW: "low",
@@ -7,5 +7,8 @@ export interface FeaturedAuctionsSectionProps {
7
7
  className?: string;
8
8
  filterByBrand?: string;
9
9
  initialItems?: ProductItem[];
10
+ rows?: number;
11
+ autoScroll?: boolean;
12
+ scrollInterval?: number;
10
13
  }
11
- export declare function FeaturedAuctionsSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, }: FeaturedAuctionsSectionProps): import("react/jsx-runtime").JSX.Element;
14
+ export declare function FeaturedAuctionsSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, autoScroll, scrollInterval, }: FeaturedAuctionsSectionProps): import("react/jsx-runtime").JSX.Element;
@@ -4,7 +4,7 @@ import { THEME_CONSTANTS } from "../../../tokens";
4
4
  import { SectionCarousel } from "./SectionCarousel";
5
5
  import { useFeaturedAuctions } from "../hooks/useFeaturedAuctions";
6
6
  import { MarketplaceAuctionCard } from "../../auctions/components/MarketplaceAuctionCard";
7
- export function FeaturedAuctionsSection({ title = "Live Auctions", description, viewMoreHref, viewMoreLabel = "View all auctions →", className = "", filterByBrand, initialItems, }) {
7
+ export function FeaturedAuctionsSection({ title = "Live Auctions", description, viewMoreHref, viewMoreLabel = "View all auctions →", className = "", filterByBrand, initialItems, rows = 1, autoScroll = false, scrollInterval = 5000, }) {
8
8
  const { data: items = [], isLoading } = useFeaturedAuctions({ filterByBrand, initialData: initialItems });
9
- return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: "Live Auctions", headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: THEME_CONSTANTS.carousel.perView.standard, gap: 16, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(MarketplaceAuctionCard, { product: product })), className: className }));
9
+ return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: "Live Auctions", headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: THEME_CONSTANTS.carousel.perView.standard, gap: 16, rows: Math.min(Math.max(rows, 1), 4), autoScroll: autoScroll, autoScrollInterval: scrollInterval, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(MarketplaceAuctionCard, { product: product })), className: className }));
10
10
  }
@@ -7,5 +7,8 @@ export interface FeaturedPreOrdersSectionProps {
7
7
  className?: string;
8
8
  filterByBrand?: string;
9
9
  initialItems?: ProductItem[];
10
+ rows?: number;
11
+ autoScroll?: boolean;
12
+ scrollInterval?: number;
10
13
  }
11
- export declare function FeaturedPreOrdersSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, }: FeaturedPreOrdersSectionProps): import("react/jsx-runtime").JSX.Element;
14
+ export declare function FeaturedPreOrdersSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, autoScroll, scrollInterval, }: FeaturedPreOrdersSectionProps): import("react/jsx-runtime").JSX.Element;
@@ -4,7 +4,7 @@ import { THEME_CONSTANTS } from "../../../tokens";
4
4
  import { SectionCarousel } from "./SectionCarousel";
5
5
  import { useFeaturedPreOrders } from "../hooks/useFeaturedPreOrders";
6
6
  import { MarketplacePreorderCard } from "../../pre-orders/components/MarketplacePreorderCard";
7
- export function FeaturedPreOrdersSection({ title = "Reserve Before It Ships", description, viewMoreHref, viewMoreLabel = "View all pre-orders →", className = "", filterByBrand, initialItems, }) {
7
+ export function FeaturedPreOrdersSection({ title = "Reserve Before It Ships", description, viewMoreHref, viewMoreLabel = "View all pre-orders →", className = "", filterByBrand, initialItems, rows = 1, autoScroll = false, scrollInterval = 5000, }) {
8
8
  const { data: items = [], isLoading } = useFeaturedPreOrders({ filterByBrand, initialData: initialItems });
9
- return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: "Pre-Order Incoming", headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: THEME_CONSTANTS.carousel.perView.standard, gap: 16, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(MarketplacePreorderCard, { product: product })), className: className }));
9
+ return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: "Pre-Order Incoming", headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: THEME_CONSTANTS.carousel.perView.standard, gap: 16, rows: Math.min(Math.max(rows, 1), 4), autoScroll: autoScroll, autoScrollInterval: scrollInterval, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(MarketplacePreorderCard, { product: product })), className: className }));
10
10
  }