medusa-contact-us 0.0.3 → 0.0.11

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 (23) hide show
  1. package/.medusa/server/src/admin/index.js +176 -1
  2. package/.medusa/server/src/admin/index.mjs +176 -1
  3. package/.medusa/server/src/api/admin/contact-email-subscriptions/route.js +29 -0
  4. package/.medusa/server/src/api/admin/contact-email-subscriptions/validators.js +12 -0
  5. package/.medusa/server/src/api/store/contact-email-subscriptions/route.js +19 -0
  6. package/.medusa/server/src/api/store/contact-email-subscriptions/validators.js +15 -0
  7. package/.medusa/server/src/constants.js +3 -2
  8. package/.medusa/server/src/helpers/__tests__/contact-subscription.test.js +109 -0
  9. package/.medusa/server/src/helpers/__tests__/submit-contact-request.test.js +33 -1
  10. package/.medusa/server/src/helpers/base-client.js +36 -0
  11. package/.medusa/server/src/helpers/contact-subscription.js +45 -0
  12. package/.medusa/server/src/helpers/index.js +5 -2
  13. package/.medusa/server/src/helpers/submit-contact-request.js +6 -35
  14. package/.medusa/server/src/index.js +7 -2
  15. package/.medusa/server/src/modules/contact-requests/migrations/Migration20241124090000.js +7 -2
  16. package/.medusa/server/src/modules/contact-requests/models/contact-request-comment.js +1 -2
  17. package/.medusa/server/src/modules/contact-requests/models/contact-request.js +1 -3
  18. package/.medusa/server/src/modules/contact-subscriptions/index.js +15 -0
  19. package/.medusa/server/src/modules/contact-subscriptions/migrations/Migration20241126103000.js +38 -0
  20. package/.medusa/server/src/modules/contact-subscriptions/models/contact-email-subscription.js +13 -0
  21. package/.medusa/server/src/modules/contact-subscriptions/service.js +57 -0
  22. package/README.md +107 -16
  23. package/package.json +1 -1
@@ -1,10 +1,175 @@
1
1
  "use strict";
2
2
  const jsxRuntime = require("react/jsx-runtime");
3
3
  const react = require("react");
4
- const reactRouterDom = require("react-router-dom");
5
4
  const adminSdk = require("@medusajs/admin-sdk");
6
5
  const ui = require("@medusajs/ui");
7
6
  const icons = require("@medusajs/icons");
7
+ const reactRouterDom = require("react-router-dom");
8
+ const useDebounce$1 = (value, delay) => {
9
+ const [debouncedValue, setDebouncedValue] = react.useState(value);
10
+ react.useEffect(() => {
11
+ const handler = setTimeout(() => setDebouncedValue(value), delay);
12
+ return () => clearTimeout(handler);
13
+ }, [value, delay]);
14
+ return debouncedValue;
15
+ };
16
+ const badgeClass = (status) => {
17
+ if (status === "subscribed") {
18
+ return "bg-ui-tag-green-bg text-ui-tag-green-text";
19
+ }
20
+ return "bg-ui-tag-red-bg text-ui-tag-red-text";
21
+ };
22
+ const statusFilters = [
23
+ { value: "all", label: "All" },
24
+ { value: "subscribed", label: "Subscribed" },
25
+ { value: "unsubscribed", label: "Unsubscribed" }
26
+ ];
27
+ const ContactEmailSubscriptionsPage = () => {
28
+ const [items, setItems] = react.useState([]);
29
+ const [statusFilter, setStatusFilter] = react.useState("all");
30
+ const [query, setQuery] = react.useState("");
31
+ const debouncedQuery = useDebounce$1(query, 300);
32
+ const [isLoading, setIsLoading] = react.useState(true);
33
+ const [isFetchingMore, setIsFetchingMore] = react.useState(false);
34
+ const [error, setError] = react.useState(null);
35
+ const [offset, setOffset] = react.useState(0);
36
+ const [count, setCount] = react.useState(0);
37
+ const limit = 50;
38
+ const loadSubscriptions = react.useCallback(
39
+ async (nextOffset, replace = false) => {
40
+ var _a;
41
+ try {
42
+ if (replace) {
43
+ setIsLoading(true);
44
+ } else {
45
+ setIsFetchingMore(true);
46
+ }
47
+ setError(null);
48
+ const params = new URLSearchParams();
49
+ params.set("limit", String(limit));
50
+ params.set("offset", String(nextOffset));
51
+ if (statusFilter !== "all") {
52
+ params.set("status", statusFilter);
53
+ }
54
+ if (debouncedQuery.trim()) {
55
+ params.set("q", debouncedQuery.trim());
56
+ }
57
+ const response = await fetch(
58
+ `/admin/contact-email-subscriptions?${params.toString()}`,
59
+ { credentials: "include" }
60
+ );
61
+ if (!response.ok) {
62
+ const message = await response.text();
63
+ throw new Error(message || "Unable to load subscriptions");
64
+ }
65
+ const payload = await response.json();
66
+ setCount(payload.count ?? 0);
67
+ setOffset(nextOffset + (((_a = payload.subscriptions) == null ? void 0 : _a.length) ?? 0));
68
+ setItems(
69
+ (prev) => replace ? payload.subscriptions ?? [] : [...prev, ...payload.subscriptions ?? []]
70
+ );
71
+ } catch (loadError) {
72
+ const message = loadError instanceof Error ? loadError.message : "Unable to load subscriptions";
73
+ setError(message);
74
+ } finally {
75
+ setIsLoading(false);
76
+ setIsFetchingMore(false);
77
+ }
78
+ },
79
+ [statusFilter, debouncedQuery]
80
+ );
81
+ react.useEffect(() => {
82
+ void loadSubscriptions(0, true);
83
+ }, [statusFilter, debouncedQuery, loadSubscriptions]);
84
+ const hasMore = react.useMemo(() => offset < count, [offset, count]);
85
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 p-6", children: [
86
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
87
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
88
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Contact email list" }),
89
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Track opt-ins and unsubscribes collected from the storefront helper." })
90
+ ] }),
91
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "primary", onClick: () => loadSubscriptions(0, true), children: "Refresh" })
92
+ ] }),
93
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
94
+ /* @__PURE__ */ jsxRuntime.jsx(
95
+ ui.Input,
96
+ {
97
+ placeholder: "Search email",
98
+ value: query,
99
+ onChange: (event) => setQuery(event.target.value),
100
+ className: "md:max-w-sm"
101
+ }
102
+ ),
103
+ /* @__PURE__ */ jsxRuntime.jsx(
104
+ "select",
105
+ {
106
+ value: statusFilter,
107
+ onChange: (event) => setStatusFilter(event.target.value),
108
+ className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive md:max-w-xs",
109
+ children: statusFilters.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option.value, children: option.label }, option.value))
110
+ }
111
+ )
112
+ ] }),
113
+ error ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-strong p-6 text-center", children: [
114
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "plus", className: "text-ui-fg-error", children: error }),
115
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4 flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
116
+ ui.Button,
117
+ {
118
+ variant: "secondary",
119
+ onClick: () => loadSubscriptions(0, true),
120
+ children: "Try again"
121
+ }
122
+ ) })
123
+ ] }) : null,
124
+ isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center py-16", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Loading email list..." }) }) : items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-dashed border-ui-border-strong p-10 text-center", children: [
125
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", className: "text-xl", children: "No entries yet" }),
126
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "mt-2 text-ui-fg-subtle", children: "Submissions created through the storefront helper will appear here with their latest consent state." })
127
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-xl border border-ui-border-base", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "min-w-full divide-y divide-ui-border-base", children: [
128
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
129
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Email" }),
130
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Status" }),
131
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Source" }),
132
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Last updated" })
133
+ ] }) }),
134
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "divide-y divide-ui-border-subtle", children: items.map((subscription) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "hover:bg-ui-bg-subtle/60", children: [
135
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4 font-medium text-ui-fg-base", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-0.5", children: [
136
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: subscription.email }),
137
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: subscription.id })
138
+ ] }) }),
139
+ /* @__PURE__ */ jsxRuntime.jsxs("td", { className: "px-4 py-4", children: [
140
+ /* @__PURE__ */ jsxRuntime.jsx(
141
+ ui.Badge,
142
+ {
143
+ size: "2xsmall",
144
+ className: `uppercase ${badgeClass(subscription.status)}`,
145
+ children: subscription.status
146
+ }
147
+ ),
148
+ subscription.unsubscribed_at ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "mt-1 block text-ui-fg-subtle", children: [
149
+ "Unsubscribed on",
150
+ " ",
151
+ new Date(subscription.unsubscribed_at).toLocaleString()
152
+ ] }) : null
153
+ ] }),
154
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: subscription.source ?? "storefront" }),
155
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: new Date(subscription.updated_at).toLocaleString() })
156
+ ] }, subscription.id)) })
157
+ ] }) }),
158
+ hasMore ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
159
+ ui.Button,
160
+ {
161
+ variant: "secondary",
162
+ isLoading: isFetchingMore,
163
+ onClick: () => loadSubscriptions(offset, false),
164
+ children: "Load more"
165
+ }
166
+ ) }) : null
167
+ ] }) });
168
+ };
169
+ const config$1 = adminSdk.defineRouteConfig({
170
+ label: "Contact email list",
171
+ icon: icons.Envelope
172
+ });
8
173
  const useDebounce = (value, delay) => {
9
174
  const [debouncedValue, setDebouncedValue] = react.useState(value);
10
175
  react.useEffect(() => {
@@ -487,6 +652,10 @@ const i18nTranslations0 = {
487
652
  const widgetModule = { widgets: [] };
488
653
  const routeModule = {
489
654
  routes: [
655
+ {
656
+ Component: ContactEmailSubscriptionsPage,
657
+ path: "/contact-email-subscriptions"
658
+ },
490
659
  {
491
660
  Component: ContactRequestsPage,
492
661
  path: "/contact-requests"
@@ -504,6 +673,12 @@ const menuItemModule = {
504
673
  icon: config.icon,
505
674
  path: "/contact-requests",
506
675
  nested: void 0
676
+ },
677
+ {
678
+ label: config$1.label,
679
+ icon: config$1.icon,
680
+ path: "/contact-email-subscriptions",
681
+ nested: void 0
507
682
  }
508
683
  ]
509
684
  };
@@ -1,9 +1,174 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useState, useCallback, useEffect, useMemo } from "react";
3
- import { Link, useParams, useNavigate } from "react-router-dom";
4
3
  import { defineRouteConfig } from "@medusajs/admin-sdk";
5
4
  import { Container, Heading, Text, Button, Input, Badge, toast, Textarea } from "@medusajs/ui";
6
5
  import { Envelope, ArrowUturnLeft } from "@medusajs/icons";
6
+ import { Link, useParams, useNavigate } from "react-router-dom";
7
+ const useDebounce$1 = (value, delay) => {
8
+ const [debouncedValue, setDebouncedValue] = useState(value);
9
+ useEffect(() => {
10
+ const handler = setTimeout(() => setDebouncedValue(value), delay);
11
+ return () => clearTimeout(handler);
12
+ }, [value, delay]);
13
+ return debouncedValue;
14
+ };
15
+ const badgeClass = (status) => {
16
+ if (status === "subscribed") {
17
+ return "bg-ui-tag-green-bg text-ui-tag-green-text";
18
+ }
19
+ return "bg-ui-tag-red-bg text-ui-tag-red-text";
20
+ };
21
+ const statusFilters = [
22
+ { value: "all", label: "All" },
23
+ { value: "subscribed", label: "Subscribed" },
24
+ { value: "unsubscribed", label: "Unsubscribed" }
25
+ ];
26
+ const ContactEmailSubscriptionsPage = () => {
27
+ const [items, setItems] = useState([]);
28
+ const [statusFilter, setStatusFilter] = useState("all");
29
+ const [query, setQuery] = useState("");
30
+ const debouncedQuery = useDebounce$1(query, 300);
31
+ const [isLoading, setIsLoading] = useState(true);
32
+ const [isFetchingMore, setIsFetchingMore] = useState(false);
33
+ const [error, setError] = useState(null);
34
+ const [offset, setOffset] = useState(0);
35
+ const [count, setCount] = useState(0);
36
+ const limit = 50;
37
+ const loadSubscriptions = useCallback(
38
+ async (nextOffset, replace = false) => {
39
+ var _a;
40
+ try {
41
+ if (replace) {
42
+ setIsLoading(true);
43
+ } else {
44
+ setIsFetchingMore(true);
45
+ }
46
+ setError(null);
47
+ const params = new URLSearchParams();
48
+ params.set("limit", String(limit));
49
+ params.set("offset", String(nextOffset));
50
+ if (statusFilter !== "all") {
51
+ params.set("status", statusFilter);
52
+ }
53
+ if (debouncedQuery.trim()) {
54
+ params.set("q", debouncedQuery.trim());
55
+ }
56
+ const response = await fetch(
57
+ `/admin/contact-email-subscriptions?${params.toString()}`,
58
+ { credentials: "include" }
59
+ );
60
+ if (!response.ok) {
61
+ const message = await response.text();
62
+ throw new Error(message || "Unable to load subscriptions");
63
+ }
64
+ const payload = await response.json();
65
+ setCount(payload.count ?? 0);
66
+ setOffset(nextOffset + (((_a = payload.subscriptions) == null ? void 0 : _a.length) ?? 0));
67
+ setItems(
68
+ (prev) => replace ? payload.subscriptions ?? [] : [...prev, ...payload.subscriptions ?? []]
69
+ );
70
+ } catch (loadError) {
71
+ const message = loadError instanceof Error ? loadError.message : "Unable to load subscriptions";
72
+ setError(message);
73
+ } finally {
74
+ setIsLoading(false);
75
+ setIsFetchingMore(false);
76
+ }
77
+ },
78
+ [statusFilter, debouncedQuery]
79
+ );
80
+ useEffect(() => {
81
+ void loadSubscriptions(0, true);
82
+ }, [statusFilter, debouncedQuery, loadSubscriptions]);
83
+ const hasMore = useMemo(() => offset < count, [offset, count]);
84
+ return /* @__PURE__ */ jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsxs(Container, { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 p-6", children: [
85
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
86
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
87
+ /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Contact email list" }),
88
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Track opt-ins and unsubscribes collected from the storefront helper." })
89
+ ] }),
90
+ /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: () => loadSubscriptions(0, true), children: "Refresh" })
91
+ ] }),
92
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
93
+ /* @__PURE__ */ jsx(
94
+ Input,
95
+ {
96
+ placeholder: "Search email",
97
+ value: query,
98
+ onChange: (event) => setQuery(event.target.value),
99
+ className: "md:max-w-sm"
100
+ }
101
+ ),
102
+ /* @__PURE__ */ jsx(
103
+ "select",
104
+ {
105
+ value: statusFilter,
106
+ onChange: (event) => setStatusFilter(event.target.value),
107
+ className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive md:max-w-xs",
108
+ children: statusFilters.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
109
+ }
110
+ )
111
+ ] }),
112
+ error ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-strong p-6 text-center", children: [
113
+ /* @__PURE__ */ jsx(Text, { weight: "plus", className: "text-ui-fg-error", children: error }),
114
+ /* @__PURE__ */ jsx("div", { className: "mt-4 flex justify-center", children: /* @__PURE__ */ jsx(
115
+ Button,
116
+ {
117
+ variant: "secondary",
118
+ onClick: () => loadSubscriptions(0, true),
119
+ children: "Try again"
120
+ }
121
+ ) })
122
+ ] }) : null,
123
+ isLoading ? /* @__PURE__ */ jsx("div", { className: "flex justify-center py-16", children: /* @__PURE__ */ jsx(Text, { children: "Loading email list..." }) }) : items.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-dashed border-ui-border-strong p-10 text-center", children: [
124
+ /* @__PURE__ */ jsx(Heading, { level: "h3", className: "text-xl", children: "No entries yet" }),
125
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "mt-2 text-ui-fg-subtle", children: "Submissions created through the storefront helper will appear here with their latest consent state." })
126
+ ] }) : /* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-xl border border-ui-border-base", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full divide-y divide-ui-border-base", children: [
127
+ /* @__PURE__ */ jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxs("tr", { children: [
128
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Email" }),
129
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Status" }),
130
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Source" }),
131
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Last updated" })
132
+ ] }) }),
133
+ /* @__PURE__ */ jsx("tbody", { className: "divide-y divide-ui-border-subtle", children: items.map((subscription) => /* @__PURE__ */ jsxs("tr", { className: "hover:bg-ui-bg-subtle/60", children: [
134
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-4 font-medium text-ui-fg-base", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
135
+ /* @__PURE__ */ jsx("span", { children: subscription.email }),
136
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: subscription.id })
137
+ ] }) }),
138
+ /* @__PURE__ */ jsxs("td", { className: "px-4 py-4", children: [
139
+ /* @__PURE__ */ jsx(
140
+ Badge,
141
+ {
142
+ size: "2xsmall",
143
+ className: `uppercase ${badgeClass(subscription.status)}`,
144
+ children: subscription.status
145
+ }
146
+ ),
147
+ subscription.unsubscribed_at ? /* @__PURE__ */ jsxs(Text, { size: "small", className: "mt-1 block text-ui-fg-subtle", children: [
148
+ "Unsubscribed on",
149
+ " ",
150
+ new Date(subscription.unsubscribed_at).toLocaleString()
151
+ ] }) : null
152
+ ] }),
153
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: subscription.source ?? "storefront" }),
154
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: new Date(subscription.updated_at).toLocaleString() })
155
+ ] }, subscription.id)) })
156
+ ] }) }),
157
+ hasMore ? /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx(
158
+ Button,
159
+ {
160
+ variant: "secondary",
161
+ isLoading: isFetchingMore,
162
+ onClick: () => loadSubscriptions(offset, false),
163
+ children: "Load more"
164
+ }
165
+ ) }) : null
166
+ ] }) });
167
+ };
168
+ const config$1 = defineRouteConfig({
169
+ label: "Contact email list",
170
+ icon: Envelope
171
+ });
7
172
  const useDebounce = (value, delay) => {
8
173
  const [debouncedValue, setDebouncedValue] = useState(value);
9
174
  useEffect(() => {
@@ -486,6 +651,10 @@ const i18nTranslations0 = {
486
651
  const widgetModule = { widgets: [] };
487
652
  const routeModule = {
488
653
  routes: [
654
+ {
655
+ Component: ContactEmailSubscriptionsPage,
656
+ path: "/contact-email-subscriptions"
657
+ },
489
658
  {
490
659
  Component: ContactRequestsPage,
491
660
  path: "/contact-requests"
@@ -503,6 +672,12 @@ const menuItemModule = {
503
672
  icon: config.icon,
504
673
  path: "/contact-requests",
505
674
  nested: void 0
675
+ },
676
+ {
677
+ label: config$1.label,
678
+ icon: config$1.icon,
679
+ path: "/contact-email-subscriptions",
680
+ nested: void 0
506
681
  }
507
682
  ]
508
683
  };
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GET = void 0;
4
+ const constants_1 = require("../../../constants");
5
+ const validators_1 = require("./validators");
6
+ const GET = async (req, res) => {
7
+ const query = validators_1.AdminListContactEmailSubscriptionsSchema.parse(req.query ?? {});
8
+ const service = req.scope.resolve(constants_1.CONTACT_SUBSCRIPTION_MODULE);
9
+ const selector = {};
10
+ if (query.status) {
11
+ selector.status = query.status;
12
+ }
13
+ if (query.q) {
14
+ selector.email = query.q.trim().toLowerCase();
15
+ }
16
+ const [subscriptions, count] = await service.listWithFilters(selector, {
17
+ take: query.limit,
18
+ skip: query.offset,
19
+ order: { created_at: "DESC" },
20
+ });
21
+ res.status(200).json({
22
+ subscriptions,
23
+ count,
24
+ offset: query.offset,
25
+ limit: query.limit,
26
+ });
27
+ };
28
+ exports.GET = GET;
29
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL2NvbnRhY3QtZW1haWwtc3Vic2NyaXB0aW9ucy9yb3V0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFDQSxrREFBZ0U7QUFFaEUsNkNBQXVFO0FBRWhFLE1BQU0sR0FBRyxHQUFHLEtBQUssRUFBRSxHQUFrQixFQUFFLEdBQW1CLEVBQUUsRUFBRTtJQUNuRSxNQUFNLEtBQUssR0FBRyxxREFBd0MsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLEtBQUssSUFBSSxFQUFFLENBQUMsQ0FBQTtJQUM3RSxNQUFNLE9BQU8sR0FBRyxHQUFHLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FDL0IsdUNBQTJCLENBQzVCLENBQUE7SUFFRCxNQUFNLFFBQVEsR0FBNEIsRUFBRSxDQUFBO0lBRTVDLElBQUksS0FBSyxDQUFDLE1BQU0sRUFBRSxDQUFDO1FBQ2pCLFFBQVEsQ0FBQyxNQUFNLEdBQUcsS0FBSyxDQUFDLE1BQU0sQ0FBQTtJQUNoQyxDQUFDO0lBQ0QsSUFBSSxLQUFLLENBQUMsQ0FBQyxFQUFFLENBQUM7UUFDWixRQUFRLENBQUMsS0FBSyxHQUFHLEtBQUssQ0FBQyxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUMsV0FBVyxFQUFFLENBQUE7SUFDL0MsQ0FBQztJQUVELE1BQU0sQ0FBQyxhQUFhLEVBQUUsS0FBSyxDQUFDLEdBQUcsTUFBTSxPQUFPLENBQUMsZUFBZSxDQUFDLFFBQVEsRUFBRTtRQUNyRSxJQUFJLEVBQUUsS0FBSyxDQUFDLEtBQUs7UUFDakIsSUFBSSxFQUFFLEtBQUssQ0FBQyxNQUFNO1FBQ2xCLEtBQUssRUFBRSxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUU7S0FDOUIsQ0FBQyxDQUFBO0lBRUYsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUM7UUFDbkIsYUFBYTtRQUNiLEtBQUs7UUFDTCxNQUFNLEVBQUUsS0FBSyxDQUFDLE1BQU07UUFDcEIsS0FBSyxFQUFFLEtBQUssQ0FBQyxLQUFLO0tBQ25CLENBQUMsQ0FBQTtBQUNKLENBQUMsQ0FBQTtBQTNCWSxRQUFBLEdBQUcsT0EyQmYifQ==
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AdminListContactEmailSubscriptionsSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const validators_1 = require("../../store/contact-email-subscriptions/validators");
6
+ exports.AdminListContactEmailSubscriptionsSchema = zod_1.z.object({
7
+ status: validators_1.ContactSubscriptionStatusEnum.optional(),
8
+ q: zod_1.z.string().optional(),
9
+ limit: zod_1.z.coerce.number().min(1).max(100).default(50),
10
+ offset: zod_1.z.coerce.number().min(0).default(0),
11
+ });
12
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsaWRhdG9ycy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uL3NyYy9hcGkvYWRtaW4vY29udGFjdC1lbWFpbC1zdWJzY3JpcHRpb25zL3ZhbGlkYXRvcnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsNkJBQXVCO0FBQ3ZCLG1GQUFrRztBQUVyRixRQUFBLHdDQUF3QyxHQUFHLE9BQUMsQ0FBQyxNQUFNLENBQUM7SUFDL0QsTUFBTSxFQUFFLDBDQUE2QixDQUFDLFFBQVEsRUFBRTtJQUNoRCxDQUFDLEVBQUUsT0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLFFBQVEsRUFBRTtJQUN4QixLQUFLLEVBQUUsT0FBQyxDQUFDLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUM7SUFDcEQsTUFBTSxFQUFFLE9BQUMsQ0FBQyxNQUFNLENBQUMsTUFBTSxFQUFFLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUM7Q0FDNUMsQ0FBQyxDQUFBIn0=
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.POST = exports.AUTHENTICATE = void 0;
4
+ const constants_1 = require("../../../constants");
5
+ const validators_1 = require("./validators");
6
+ exports.AUTHENTICATE = false;
7
+ const POST = async (req, res) => {
8
+ const body = validators_1.StoreUpsertContactSubscriptionSchema.parse(req.body ?? {});
9
+ const service = req.scope.resolve(constants_1.CONTACT_SUBSCRIPTION_MODULE);
10
+ const subscription = await service.upsertSubscription({
11
+ email: body.email,
12
+ status: body.status,
13
+ metadata: body.metadata,
14
+ source: body.source ?? "storefront",
15
+ });
16
+ res.status(200).json({ subscription });
17
+ };
18
+ exports.POST = POST;
19
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL3N0b3JlL2NvbnRhY3QtZW1haWwtc3Vic2NyaXB0aW9ucy9yb3V0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFDQSxrREFBZ0U7QUFFaEUsNkNBQW1FO0FBRXRELFFBQUEsWUFBWSxHQUFHLEtBQUssQ0FBQTtBQUUxQixNQUFNLElBQUksR0FBRyxLQUFLLEVBQUUsR0FBa0IsRUFBRSxHQUFtQixFQUFFLEVBQUU7SUFDcEUsTUFBTSxJQUFJLEdBQUcsaURBQW9DLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxJQUFJLElBQUksRUFBRSxDQUFDLENBQUE7SUFFdkUsTUFBTSxPQUFPLEdBQUcsR0FBRyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQy9CLHVDQUEyQixDQUM1QixDQUFBO0lBRUQsTUFBTSxZQUFZLEdBQUcsTUFBTSxPQUFPLENBQUMsa0JBQWtCLENBQUM7UUFDcEQsS0FBSyxFQUFFLElBQUksQ0FBQyxLQUFLO1FBQ2pCLE1BQU0sRUFBRSxJQUFJLENBQUMsTUFBTTtRQUNuQixRQUFRLEVBQUUsSUFBSSxDQUFDLFFBQVE7UUFDdkIsTUFBTSxFQUFFLElBQUksQ0FBQyxNQUFNLElBQUksWUFBWTtLQUNwQyxDQUFDLENBQUE7SUFFRixHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLFlBQVksRUFBRSxDQUFDLENBQUE7QUFDeEMsQ0FBQyxDQUFBO0FBZlksUUFBQSxJQUFJLFFBZWhCIn0=
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StoreUpsertContactSubscriptionSchema = exports.ContactSubscriptionStatusEnum = void 0;
4
+ const zod_1 = require("zod");
5
+ exports.ContactSubscriptionStatusEnum = zod_1.z.enum([
6
+ "subscribed",
7
+ "unsubscribed",
8
+ ]);
9
+ exports.StoreUpsertContactSubscriptionSchema = zod_1.z.object({
10
+ email: zod_1.z.string().email(),
11
+ status: exports.ContactSubscriptionStatusEnum.optional(),
12
+ metadata: zod_1.z.record(zod_1.z.any()).optional(),
13
+ source: zod_1.z.string().optional(),
14
+ });
15
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsaWRhdG9ycy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uL3NyYy9hcGkvc3RvcmUvY29udGFjdC1lbWFpbC1zdWJzY3JpcHRpb25zL3ZhbGlkYXRvcnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsNkJBQXVCO0FBRVYsUUFBQSw2QkFBNkIsR0FBRyxPQUFDLENBQUMsSUFBSSxDQUFDO0lBQ2xELFlBQVk7SUFDWixjQUFjO0NBQ2YsQ0FBQyxDQUFBO0FBRVcsUUFBQSxvQ0FBb0MsR0FBRyxPQUFDLENBQUMsTUFBTSxDQUFDO0lBQzNELEtBQUssRUFBRSxPQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsS0FBSyxFQUFFO0lBQ3pCLE1BQU0sRUFBRSxxQ0FBNkIsQ0FBQyxRQUFRLEVBQUU7SUFDaEQsUUFBUSxFQUFFLE9BQUMsQ0FBQyxNQUFNLENBQUMsT0FBQyxDQUFDLEdBQUcsRUFBRSxDQUFDLENBQUMsUUFBUSxFQUFFO0lBQ3RDLE1BQU0sRUFBRSxPQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsUUFBUSxFQUFFO0NBQzlCLENBQUMsQ0FBQSJ9
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CONTACT_REQUEST_MODULE = void 0;
3
+ exports.CONTACT_SUBSCRIPTION_MODULE = exports.CONTACT_REQUEST_MODULE = void 0;
4
4
  exports.CONTACT_REQUEST_MODULE = "contact_requests";
5
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uc3RhbnRzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2NvbnN0YW50cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBYSxRQUFBLHNCQUFzQixHQUFHLGtCQUFrQixDQUFBIn0=
5
+ exports.CONTACT_SUBSCRIPTION_MODULE = "contact_email_subscriptions";
6
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uc3RhbnRzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2NvbnN0YW50cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBYSxRQUFBLHNCQUFzQixHQUFHLGtCQUFrQixDQUFBO0FBQzNDLFFBQUEsMkJBQTJCLEdBQUcsNkJBQTZCLENBQUEifQ==
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const contact_subscription_1 = require("../contact-subscription");
5
+ const buildPayload = () => ({
6
+ email: "sub@example.com",
7
+ status: "subscribed",
8
+ metadata: { channel: "footer" },
9
+ });
10
+ (0, vitest_1.describe)("upsertContactSubscription helper", () => {
11
+ const fetchCases = [
12
+ {
13
+ name: "uses provided fetch implementation when no client exists",
14
+ setup: () => {
15
+ const mockFetch = vitest_1.vi.fn().mockResolvedValue({
16
+ ok: true,
17
+ json: () => Promise.resolve({
18
+ subscription: { id: "sub_1" },
19
+ }),
20
+ });
21
+ return {
22
+ fetchImpl: mockFetch,
23
+ expectedUrl: "https://demo.store/store/contact-email-subscriptions",
24
+ };
25
+ },
26
+ },
27
+ {
28
+ name: "throws when response is not ok",
29
+ setup: () => {
30
+ const mockFetch = vitest_1.vi.fn().mockResolvedValue({
31
+ ok: false,
32
+ status: 400,
33
+ text: () => Promise.resolve("invalid"),
34
+ });
35
+ return {
36
+ fetchImpl: mockFetch,
37
+ expectedError: /invalid/i,
38
+ };
39
+ },
40
+ },
41
+ ];
42
+ fetchCases.forEach(({ name, setup }) => {
43
+ (0, vitest_1.it)(name, async () => {
44
+ const config = setup();
45
+ const { fetchImpl } = config;
46
+ if ("expectedError" in config) {
47
+ await (0, vitest_1.expect)((0, contact_subscription_1.upsertContactSubscription)(buildPayload(), {
48
+ fetchImpl,
49
+ baseUrl: "https://demo.store",
50
+ })).rejects.toMatchObject({
51
+ message: vitest_1.expect.stringMatching(config.expectedError),
52
+ });
53
+ return;
54
+ }
55
+ const result = await (0, contact_subscription_1.upsertContactSubscription)(buildPayload(), {
56
+ fetchImpl,
57
+ baseUrl: "https://demo.store",
58
+ });
59
+ (0, vitest_1.expect)(result).toEqual({ subscription: { id: "sub_1" } });
60
+ (0, vitest_1.expect)(fetchImpl).toHaveBeenCalledWith(config.expectedUrl, vitest_1.expect.anything());
61
+ });
62
+ });
63
+ (0, vitest_1.it)("uses medusa client when provided", async () => {
64
+ const mockClient = {
65
+ request: vitest_1.vi.fn().mockResolvedValue({
66
+ subscription: { id: "sub_2" },
67
+ }),
68
+ };
69
+ const result = await (0, contact_subscription_1.upsertContactSubscription)(buildPayload(), {
70
+ client: mockClient,
71
+ });
72
+ (0, vitest_1.expect)(result.subscription.id).toBe("sub_2");
73
+ (0, vitest_1.expect)(mockClient.request).toHaveBeenCalledWith("/store/contact-email-subscriptions", vitest_1.expect.objectContaining({
74
+ method: "POST",
75
+ }));
76
+ });
77
+ (0, vitest_1.it)("attaches publishable API key to headers", async () => {
78
+ const mockFetch = vitest_1.vi.fn().mockResolvedValue({
79
+ ok: true,
80
+ json: () => Promise.resolve({
81
+ subscription: { id: "sub_3" },
82
+ }),
83
+ });
84
+ await (0, contact_subscription_1.upsertContactSubscription)(buildPayload(), {
85
+ fetchImpl: mockFetch,
86
+ baseUrl: "https://demo.store",
87
+ publishableApiKey: "pk_subscribe",
88
+ });
89
+ const [, init] = mockFetch.mock.calls[0];
90
+ (0, vitest_1.expect)(init?.headers).toMatchObject({
91
+ "x-publishable-api-key": "pk_subscribe",
92
+ });
93
+ });
94
+ (0, vitest_1.it)("allows helper factory to preconfigure options", async () => {
95
+ const mockFetch = vitest_1.vi.fn().mockResolvedValue({
96
+ ok: true,
97
+ json: () => Promise.resolve({
98
+ subscription: { id: "sub_4" },
99
+ }),
100
+ });
101
+ const updateSubscription = (0, contact_subscription_1.createUpsertContactSubscription)({
102
+ fetchImpl: mockFetch,
103
+ baseUrl: "https://demo.store",
104
+ });
105
+ const result = await updateSubscription(buildPayload());
106
+ (0, vitest_1.expect)(result.subscription.id).toBe("sub_4");
107
+ });
108
+ });
109
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29udGFjdC1zdWJzY3JpcHRpb24udGVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy9oZWxwZXJzL19fdGVzdHNfXy9jb250YWN0LXN1YnNjcmlwdGlvbi50ZXN0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEsbUNBQWlEO0FBQ2pELGtFQUdnQztBQUVoQyxNQUFNLFlBQVksR0FBRyxHQUFHLEVBQUUsQ0FBQyxDQUFDO0lBQzFCLEtBQUssRUFBRSxpQkFBaUI7SUFDeEIsTUFBTSxFQUFFLFlBQXFCO0lBQzdCLFFBQVEsRUFBRSxFQUFFLE9BQU8sRUFBRSxRQUFRLEVBQUU7Q0FDaEMsQ0FBQyxDQUFBO0FBRUYsSUFBQSxpQkFBUSxFQUFDLGtDQUFrQyxFQUFFLEdBQUcsRUFBRTtJQUNoRCxNQUFNLFVBQVUsR0FBRztRQUNqQjtZQUNFLElBQUksRUFBRSwwREFBMEQ7WUFDaEUsS0FBSyxFQUFFLEdBQUcsRUFBRTtnQkFDVixNQUFNLFNBQVMsR0FBRyxXQUFFLENBQUMsRUFBRSxFQUFFLENBQUMsaUJBQWlCLENBQUM7b0JBQzFDLEVBQUUsRUFBRSxJQUFJO29CQUNSLElBQUksRUFBRSxHQUFHLEVBQUUsQ0FDVCxPQUFPLENBQUMsT0FBTyxDQUFDO3dCQUNkLFlBQVksRUFBRSxFQUFFLEVBQUUsRUFBRSxPQUFPLEVBQUU7cUJBQzlCLENBQUM7aUJBQ0wsQ0FBQyxDQUFBO2dCQUNGLE9BQU87b0JBQ0wsU0FBUyxFQUFFLFNBQVM7b0JBQ3BCLFdBQVcsRUFBRSxzREFBc0Q7aUJBQ3BFLENBQUE7WUFDSCxDQUFDO1NBQ0Y7UUFDRDtZQUNFLElBQUksRUFBRSxnQ0FBZ0M7WUFDdEMsS0FBSyxFQUFFLEdBQUcsRUFBRTtnQkFDVixNQUFNLFNBQVMsR0FBRyxXQUFFLENBQUMsRUFBRSxFQUFFLENBQUMsaUJBQWlCLENBQUM7b0JBQzFDLEVBQUUsRUFBRSxLQUFLO29CQUNULE1BQU0sRUFBRSxHQUFHO29CQUNY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