medusa-contact-us 0.0.11 → 0.0.20
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/.medusa/server/src/admin/index.js +4 -494
- package/.medusa/server/src/admin/index.mjs +6 -496
- package/.medusa/server/src/api/admin/contact-email-subscriptions/route.js +3 -3
- package/.medusa/server/src/api/admin/contact-requests/[id]/route.js +10 -34
- package/.medusa/server/src/api/admin/contact-requests/[id]/status/route.js +24 -0
- package/.medusa/server/src/api/admin/contact-requests/route.js +47 -11
- package/.medusa/server/src/api/admin/contact-requests/validators.js +22 -12
- package/.medusa/server/src/api/store/contact-email-subscriptions/route.js +3 -3
- package/.medusa/server/src/api/store/contact-email-subscriptions/validators.js +2 -2
- package/.medusa/server/src/api/store/contact-requests/route.js +4 -10
- package/.medusa/server/src/api/store/contact-requests/validators.js +4 -8
- package/.medusa/server/src/constants.js +10 -4
- package/.medusa/server/src/helpers/contact-request.js +52 -0
- package/.medusa/server/src/helpers/contact-subscription.js +15 -4
- package/.medusa/server/src/helpers/index.js +5 -4
- package/.medusa/server/src/index.js +16 -13
- package/.medusa/server/src/modules/contact-requests/index.js +4 -5
- package/.medusa/server/src/modules/contact-requests/migrations/Migration20241129163317.js +47 -0
- package/.medusa/server/src/modules/contact-requests/models/contact-request.js +4 -3
- package/.medusa/server/src/modules/contact-requests/service.js +191 -141
- package/.medusa/server/src/modules/contact-requests/utils/resolve-options.js +48 -0
- package/.medusa/server/src/modules/contact-subscriptions/index.js +4 -5
- package/.medusa/server/src/modules/contact-subscriptions/service.js +20 -12
- package/.medusa/server/src/types/contact-request.js +3 -0
- package/.medusa/server/src/types.js +1 -208
- package/.medusa/server/src/workflows/create-contact-request-workflow.js +13 -0
- package/.medusa/server/src/workflows/index.js +5 -5
- package/.medusa/server/src/workflows/steps/create-contact-request-step.js +3 -7
- package/.medusa/server/src/workflows/steps/resolve-status-transition-step.js +15 -0
- package/.medusa/server/src/workflows/steps/send-status-notification-step.js +56 -0
- package/.medusa/server/src/workflows/steps/update-contact-request-status-step.js +7 -7
- package/.medusa/server/src/workflows/update-contact-request-status-workflow.js +33 -0
- package/README.md +329 -211
- package/package.json +2 -2
- package/.medusa/server/src/api/admin/contact-requests/[id]/comments/route.js +0 -22
- package/.medusa/server/src/api/admin/plugin/route.js +0 -11
- package/.medusa/server/src/api/store/plugin/route.js +0 -11
- package/.medusa/server/src/helpers/__tests__/submit-contact-request.test.js +0 -125
- package/.medusa/server/src/helpers/submit-contact-request.js +0 -45
- package/.medusa/server/src/modules/contact-requests/migrations/Migration20241124090000.js +0 -51
- package/.medusa/server/src/modules/contact-requests/models/contact-request-comment.js +0 -11
- package/.medusa/server/src/plugin-options.js +0 -16
- package/.medusa/server/src/types/__tests__/contact-options.test.js +0 -83
- package/.medusa/server/src/utils/__tests__/payload-validator.test.js +0 -81
- package/.medusa/server/src/utils/payload.js +0 -127
- package/.medusa/server/src/workflows/create-contact-request.js +0 -30
- package/.medusa/server/src/workflows/steps/send-contact-notification-step.js +0 -48
- package/.medusa/server/src/workflows/update-contact-request-status.js +0 -32
package/README.md
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
<h1 align="center">Medusa Contact Us Plugin</h1>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A comprehensive Medusa v2 plugin for managing contact requests and email subscriptions. The plugin provides a complete solution: database-backed contact requests with configurable status workflows, email subscriptions, admin APIs, helper utilities, and a polished admin UI.
|
|
4
|
+
|
|
5
|
+
**Features:**
|
|
6
|
+
- 📝 Contact request management with status workflow and email notifications
|
|
7
|
+
- ✉️ Email subscription management (opt-ins and opt-outs)
|
|
8
|
+
- 🔄 Configurable status transitions and validation
|
|
9
|
+
- 📧 Automatic email notifications on status changes
|
|
10
|
+
- 🎯 Payload validation against configurable field definitions
|
|
4
11
|
|
|
5
12
|
## Features
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
- 🧩 Status lifecycle definition (initial/intermediate/final, allowed transitions, metadata)
|
|
9
|
-
- ✉️ Workflow-powered notifications on submission and on final status (re-uses Medusa notification module)
|
|
10
|
-
- 🗒️ Admin-only comment trail per request with rich status history
|
|
11
|
-
- 🖥️ React Admin extension with list + detail views, filters, status change controls, and comment composer
|
|
12
|
-
- 🔌 Frontend helper (`submitContactRequest`) to abstract API wiring
|
|
14
|
+
### Email Subscriptions
|
|
13
15
|
- ✉️ Storefront opt-in helper + admin list for email subscriptions (subscribed/unsubscribed)
|
|
14
|
-
-
|
|
16
|
+
- 🖥️ React Admin extension with list view, filters, and search
|
|
17
|
+
- 🔌 Frontend helper (`upsertContactSubscription`) to abstract API wiring
|
|
18
|
+
- 🧪 Table-driven tests for helper logic
|
|
19
|
+
|
|
20
|
+
### Contact Requests
|
|
21
|
+
- 📝 Contact request management with configurable status workflow
|
|
22
|
+
- 🔄 Status transition validation with configurable allowed transitions
|
|
23
|
+
- 📧 Email notifications on status changes (configurable per transition)
|
|
24
|
+
- 🎯 Payload validation against configurable field definitions
|
|
25
|
+
- 📊 Admin API for listing, filtering, and managing requests
|
|
26
|
+
- 🔌 Storefront helper (`submitContactRequest`) for easy integration
|
|
27
|
+
- 📈 Status history tracking with timestamps and actor information
|
|
15
28
|
|
|
16
29
|
## Installation
|
|
17
30
|
|
|
@@ -23,160 +36,85 @@ npm install medusa-contact-us
|
|
|
23
36
|
|
|
24
37
|
## Configuration
|
|
25
38
|
|
|
26
|
-
Inside `medusa-config.ts` register the plugin
|
|
39
|
+
Inside `medusa-config.ts` register the plugin:
|
|
27
40
|
|
|
28
41
|
```ts
|
|
29
42
|
import type { ConfigModule } from "@medusajs/framework/types"
|
|
30
43
|
import {
|
|
31
|
-
defineContactUsPluginOptions,
|
|
32
|
-
ContactRequestModule,
|
|
33
44
|
ContactSubscriptionModule,
|
|
45
|
+
ContactRequestModule,
|
|
34
46
|
} from "medusa-contact-us"
|
|
35
47
|
|
|
36
48
|
const plugins = [
|
|
37
49
|
{
|
|
38
50
|
resolve: "medusa-contact-us",
|
|
39
|
-
options:
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
key: "message",
|
|
57
|
-
label: "Message",
|
|
58
|
-
description: "Detailed message about your inquiry",
|
|
59
|
-
type: "textarea",
|
|
60
|
-
required: true,
|
|
61
|
-
placeholder: "Please describe your issue or question...",
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
key: "priority",
|
|
65
|
-
label: "Priority",
|
|
66
|
-
description: "How urgent is your request?",
|
|
67
|
-
type: "select",
|
|
68
|
-
required: false,
|
|
69
|
-
options: [
|
|
70
|
-
{ value: "low", label: "Low" },
|
|
71
|
-
{ value: "medium", label: "Medium" },
|
|
72
|
-
{ value: "high", label: "High" },
|
|
73
|
-
{ value: "urgent", label: "Urgent" },
|
|
74
|
-
],
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
key: "order_number",
|
|
78
|
-
label: "Order Number",
|
|
79
|
-
description: "If this relates to an order, please provide the order number",
|
|
80
|
-
type: "text",
|
|
81
|
-
required: false,
|
|
82
|
-
placeholder: "order_123456",
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
key: "phone",
|
|
86
|
-
label: "Phone Number",
|
|
87
|
-
type: "text",
|
|
88
|
-
required: false,
|
|
89
|
-
placeholder: "+1 (555) 123-4567",
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
key: "preferred_contact_method",
|
|
93
|
-
label: "Preferred Contact Method",
|
|
94
|
-
type: "select",
|
|
95
|
-
required: false,
|
|
96
|
-
options: [
|
|
97
|
-
{ value: "email", label: "Email" },
|
|
98
|
-
{ value: "phone", label: "Phone" },
|
|
99
|
-
],
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
key: "is_return_request",
|
|
103
|
-
label: "Is this a return request?",
|
|
104
|
-
type: "boolean",
|
|
105
|
-
required: false,
|
|
106
|
-
},
|
|
107
|
-
],
|
|
108
|
-
},
|
|
109
|
-
// Status lifecycle configuration
|
|
110
|
-
statuses: {
|
|
111
|
-
// Initial status when a request is created
|
|
112
|
-
initial: "new",
|
|
113
|
-
// Intermediate statuses (work in progress)
|
|
114
|
-
intermediates: ["in_review", "waiting_for_customer"],
|
|
115
|
-
// Final status (request is complete)
|
|
116
|
-
final: "closed",
|
|
117
|
-
// Allowed status transitions
|
|
118
|
-
transitions: {
|
|
119
|
-
new: ["in_review", "closed"],
|
|
120
|
-
in_review: ["waiting_for_customer", "closed"],
|
|
121
|
-
waiting_for_customer: ["in_review", "closed"],
|
|
51
|
+
options: {
|
|
52
|
+
// Contact Request Configuration
|
|
53
|
+
default_status: "pending",
|
|
54
|
+
payload_fields: [
|
|
55
|
+
{
|
|
56
|
+
key: "subject",
|
|
57
|
+
type: "text",
|
|
58
|
+
required: true,
|
|
59
|
+
label: "Subject",
|
|
60
|
+
placeholder: "Enter subject",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
key: "message",
|
|
64
|
+
type: "textarea",
|
|
65
|
+
required: true,
|
|
66
|
+
label: "Message",
|
|
67
|
+
placeholder: "Enter your message",
|
|
122
68
|
},
|
|
123
|
-
},
|
|
124
|
-
// Status options with labels and notification settings
|
|
125
|
-
status_options: [
|
|
126
69
|
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
70
|
+
key: "priority",
|
|
71
|
+
type: "select",
|
|
72
|
+
required: false,
|
|
73
|
+
label: "Priority",
|
|
74
|
+
options: [
|
|
75
|
+
{ value: "low", label: "Low" },
|
|
76
|
+
{ value: "medium", label: "Medium" },
|
|
77
|
+
{ value: "high", label: "High" },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
allowed_statuses: ["pending", "in_progress", "resolved", "closed"],
|
|
82
|
+
status_transitions: [
|
|
83
|
+
{
|
|
84
|
+
from: null,
|
|
85
|
+
to: "pending",
|
|
86
|
+
send_email: false,
|
|
132
87
|
},
|
|
133
88
|
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
89
|
+
from: "pending",
|
|
90
|
+
to: "in_progress",
|
|
91
|
+
send_email: true,
|
|
92
|
+
email_subject: "Your request is being processed",
|
|
93
|
+
email_template: null, // Optional: path to email template
|
|
138
94
|
},
|
|
139
95
|
{
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
template: "emails/contact-waiting.mjml",
|
|
96
|
+
from: "in_progress",
|
|
97
|
+
to: "resolved",
|
|
98
|
+
send_email: true,
|
|
99
|
+
email_subject: "Your request has been resolved",
|
|
145
100
|
},
|
|
146
101
|
{
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
notify_customer: true,
|
|
151
|
-
template: "emails/contact-final.mjml",
|
|
102
|
+
from: "resolved",
|
|
103
|
+
to: "closed",
|
|
104
|
+
send_email: false,
|
|
152
105
|
},
|
|
153
106
|
],
|
|
154
|
-
|
|
155
|
-
notifications: {
|
|
156
|
-
// Send email when a contact request is created
|
|
157
|
-
send_on_create: true,
|
|
158
|
-
// Template for acknowledgement email
|
|
159
|
-
acknowledgement_template: "emails/contact-received.mjml",
|
|
160
|
-
// Send email when request reaches final status
|
|
161
|
-
send_on_final_status: true,
|
|
162
|
-
// Optional: Custom from address
|
|
163
|
-
from_address: "support@yourstore.com",
|
|
164
|
-
// Optional: Reply-to address
|
|
165
|
-
reply_to: "support@yourstore.com",
|
|
166
|
-
},
|
|
167
|
-
// Comments configuration
|
|
168
|
-
comments: {
|
|
169
|
-
// Enable admin comments on contact requests
|
|
107
|
+
email: {
|
|
170
108
|
enabled: true,
|
|
171
|
-
|
|
172
|
-
|
|
109
|
+
default_subject: "Contact Request Status Update",
|
|
110
|
+
default_template: null, // Optional: default email template path
|
|
173
111
|
},
|
|
174
|
-
}
|
|
112
|
+
},
|
|
175
113
|
},
|
|
176
114
|
]
|
|
177
115
|
|
|
178
|
-
// Register the
|
|
179
|
-
const modules = [
|
|
116
|
+
// Register the modules
|
|
117
|
+
const modules = [ContactSubscriptionModule, ContactRequestModule]
|
|
180
118
|
|
|
181
119
|
export default {
|
|
182
120
|
projectConfig: {
|
|
@@ -189,50 +127,87 @@ export default {
|
|
|
189
127
|
} satisfies ConfigModule
|
|
190
128
|
```
|
|
191
129
|
|
|
192
|
-
###
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
-
|
|
197
|
-
-
|
|
198
|
-
- `
|
|
199
|
-
- `
|
|
200
|
-
- `boolean
|
|
201
|
-
- `
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
-
|
|
206
|
-
-
|
|
207
|
-
-
|
|
208
|
-
-
|
|
130
|
+
### Configuration Options
|
|
131
|
+
|
|
132
|
+
#### Contact Request Options
|
|
133
|
+
|
|
134
|
+
- **`default_status`** (string, optional): Default status for new requests. Default: `"pending"`
|
|
135
|
+
- **`payload_fields`** (array, optional): Field definitions for payload validation. Each field supports:
|
|
136
|
+
- `key` (string, required): Field identifier
|
|
137
|
+
- `type` (string, required): Field type - `"text"`, `"textarea"`, `"number"`, `"email"`, `"select"`, `"checkbox"`
|
|
138
|
+
- `required` (boolean, optional): Whether field is required
|
|
139
|
+
- `label` (string, optional): Display label
|
|
140
|
+
- `placeholder` (string, optional): Placeholder text
|
|
141
|
+
- `options` (array, optional): For select type - `[{ value: string, label: string }]`
|
|
142
|
+
- `validation` (object, optional): Validation rules (min, max, pattern)
|
|
143
|
+
- **`allowed_statuses`** (array, optional): List of allowed status values. Default: `["pending", "in_progress", "resolved", "closed"]`
|
|
144
|
+
- **`status_transitions`** (array, optional): Allowed status transitions. Each transition supports:
|
|
145
|
+
- `from` (string | null): Source status (null = initial status)
|
|
146
|
+
- `to` (string, required): Target status
|
|
147
|
+
- `send_email` (boolean, optional): Whether to send email on this transition
|
|
148
|
+
- `email_subject` (string, optional): Custom email subject for this transition
|
|
149
|
+
- `email_template` (string | null, optional): Custom email template path
|
|
150
|
+
- **`email`** (object, optional): Email notification settings
|
|
151
|
+
- `enabled` (boolean, optional): Enable/disable email notifications. Default: `true`
|
|
152
|
+
- `default_subject` (string, optional): Default email subject. Default: `"Contact Request Status Update"`
|
|
153
|
+
- `default_template` (string | null, optional): Default email template path
|
|
209
154
|
|
|
210
155
|
## REST API
|
|
211
156
|
|
|
212
157
|
### Storefront (open)
|
|
213
158
|
|
|
159
|
+
#### Contact Requests
|
|
160
|
+
|
|
161
|
+
Create a contact request:
|
|
162
|
+
|
|
214
163
|
```bash
|
|
215
164
|
curl -X POST https://your-medusa.com/store/contact-requests \
|
|
216
165
|
-H "Content-Type: application/json" \
|
|
166
|
+
-H "x-publishable-api-key: pk_storefront" \
|
|
217
167
|
-d '{
|
|
218
168
|
"email": "customer@example.com",
|
|
219
|
-
"payload": {
|
|
220
|
-
|
|
169
|
+
"payload": {
|
|
170
|
+
"subject": "Order inquiry",
|
|
171
|
+
"message": "I need help with my order #12345"
|
|
172
|
+
},
|
|
173
|
+
"metadata": {
|
|
174
|
+
"source_page": "contact"
|
|
175
|
+
},
|
|
176
|
+
"source": "storefront"
|
|
221
177
|
}'
|
|
222
178
|
```
|
|
223
179
|
|
|
180
|
+
Body fields:
|
|
181
|
+
- `email` – required, valid email address
|
|
182
|
+
- `payload` – optional, JSON object with custom fields (validated against configured `payload_fields`)
|
|
183
|
+
- `metadata` – optional, additional metadata
|
|
184
|
+
- `source` – optional, request source identifier (default: `"storefront"`)
|
|
185
|
+
|
|
224
186
|
Response:
|
|
225
187
|
|
|
226
188
|
```json
|
|
227
189
|
{
|
|
228
|
-
"
|
|
229
|
-
"id": "
|
|
190
|
+
"request": {
|
|
191
|
+
"id": "creq_123",
|
|
230
192
|
"email": "customer@example.com",
|
|
231
|
-
"
|
|
232
|
-
|
|
193
|
+
"payload": {
|
|
194
|
+
"subject": "Order inquiry",
|
|
195
|
+
"message": "I need help with my order #12345"
|
|
196
|
+
},
|
|
197
|
+
"status": "pending",
|
|
233
198
|
"status_history": [
|
|
234
|
-
{
|
|
235
|
-
|
|
199
|
+
{
|
|
200
|
+
"from": null,
|
|
201
|
+
"to": "pending",
|
|
202
|
+
"changed_at": "2024-11-29T16:33:17.000Z"
|
|
203
|
+
}
|
|
204
|
+
],
|
|
205
|
+
"metadata": {
|
|
206
|
+
"source_page": "contact"
|
|
207
|
+
},
|
|
208
|
+
"source": "storefront",
|
|
209
|
+
"created_at": "2024-11-29T16:33:17.000Z",
|
|
210
|
+
"updated_at": "2024-11-29T16:33:17.000Z"
|
|
236
211
|
}
|
|
237
212
|
}
|
|
238
213
|
```
|
|
@@ -271,75 +246,142 @@ Response:
|
|
|
271
246
|
|
|
272
247
|
### Admin (requires admin auth cookie/token)
|
|
273
248
|
|
|
274
|
-
|
|
249
|
+
#### Contact Requests
|
|
250
|
+
|
|
251
|
+
List contact requests:
|
|
275
252
|
|
|
276
253
|
```bash
|
|
277
|
-
curl -X GET "https://your-medusa.com/admin/contact-requests?status=
|
|
254
|
+
curl -X GET "https://your-medusa.com/admin/contact-requests?status=pending&email=customer@example.com&limit=20&offset=0" \
|
|
278
255
|
-H "Authorization: Bearer <token>"
|
|
279
256
|
```
|
|
280
257
|
|
|
281
|
-
|
|
258
|
+
Query parameters:
|
|
259
|
+
- `email` – optional, filter by email (partial match)
|
|
260
|
+
- `status` – optional, filter by status
|
|
261
|
+
- `source` – optional, filter by source
|
|
262
|
+
- `created_at.gte` – optional, filter by creation date (ISO 8601)
|
|
263
|
+
- `created_at.lte` – optional, filter by creation date (ISO 8601)
|
|
264
|
+
- `limit` – optional, number of results (default: 20, max: 100)
|
|
265
|
+
- `offset` – optional, pagination offset (default: 0)
|
|
266
|
+
- `order` – optional, sort field: `created_at`, `updated_at`, `email` (default: `created_at`)
|
|
267
|
+
- `order_direction` – optional, sort direction: `ASC` or `DESC` (default: `DESC`)
|
|
268
|
+
|
|
269
|
+
Get contact request details:
|
|
282
270
|
|
|
283
271
|
```bash
|
|
284
|
-
curl -X GET https://your-medusa.com/admin/contact-requests/
|
|
272
|
+
curl -X GET "https://your-medusa.com/admin/contact-requests/creq_123" \
|
|
285
273
|
-H "Authorization: Bearer <token>"
|
|
286
274
|
```
|
|
287
275
|
|
|
288
|
-
|
|
276
|
+
Response includes the request and `next_allowed_statuses` array for the admin UI:
|
|
277
|
+
|
|
278
|
+
```json
|
|
279
|
+
{
|
|
280
|
+
"request": {
|
|
281
|
+
"id": "creq_123",
|
|
282
|
+
"email": "customer@example.com",
|
|
283
|
+
"payload": { ... },
|
|
284
|
+
"status": "pending",
|
|
285
|
+
"status_history": [ ... ],
|
|
286
|
+
"metadata": { ... },
|
|
287
|
+
"source": "storefront",
|
|
288
|
+
"created_at": "2024-11-29T16:33:17.000Z",
|
|
289
|
+
"updated_at": "2024-11-29T16:33:17.000Z"
|
|
290
|
+
},
|
|
291
|
+
"next_allowed_statuses": ["in_progress"]
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Create contact request (admin):
|
|
289
296
|
|
|
290
297
|
```bash
|
|
291
|
-
curl -X
|
|
292
|
-
-H "Authorization: Bearer <token>" \
|
|
298
|
+
curl -X POST https://your-medusa.com/admin/contact-requests \
|
|
293
299
|
-H "Content-Type: application/json" \
|
|
294
|
-
-
|
|
300
|
+
-H "Authorization: Bearer <token>" \
|
|
301
|
+
-d '{
|
|
302
|
+
"email": "customer@example.com",
|
|
303
|
+
"payload": {
|
|
304
|
+
"subject": "Support request",
|
|
305
|
+
"message": "Need assistance"
|
|
306
|
+
},
|
|
307
|
+
"source": "admin"
|
|
308
|
+
}'
|
|
295
309
|
```
|
|
296
310
|
|
|
297
|
-
|
|
311
|
+
Update contact request status:
|
|
298
312
|
|
|
299
313
|
```bash
|
|
300
|
-
curl -X POST https://your-medusa.com/admin/contact-requests/
|
|
301
|
-
-H "Authorization: Bearer <token>" \
|
|
314
|
+
curl -X POST https://your-medusa.com/admin/contact-requests/creq_123/status \
|
|
302
315
|
-H "Content-Type: application/json" \
|
|
303
|
-
-
|
|
316
|
+
-H "Authorization: Bearer <token>" \
|
|
317
|
+
-d '{
|
|
318
|
+
"status": "in_progress"
|
|
319
|
+
}'
|
|
304
320
|
```
|
|
305
321
|
|
|
306
|
-
|
|
322
|
+
**Note**: Only status transitions defined in `status_transitions` configuration are allowed. The API will return an error if an invalid transition is attempted.
|
|
307
323
|
|
|
308
|
-
|
|
324
|
+
#### Email Subscriptions
|
|
309
325
|
|
|
310
|
-
|
|
311
|
-
- **Detail view** – see submitted fields, live status badge, change status (with optional notes), inspect the full history timeline, and append internal comments.
|
|
312
|
-
- **Comments** – only admins can add comments. Comments are persisted and shown newest first.
|
|
313
|
-
- **Contact email list** – dedicated table that displays every storefront opt-in, highlights unsubscribes, and supports filtering/searching by status or email.
|
|
326
|
+
List subscriptions:
|
|
314
327
|
|
|
315
|
-
|
|
328
|
+
```bash
|
|
329
|
+
curl -X GET "https://your-medusa.com/admin/contact-email-subscriptions?status=subscribed&limit=20" \
|
|
330
|
+
-H "Authorization: Bearer <token>"
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Query parameters:
|
|
334
|
+
- `status` – optional, filter by `subscribed` or `unsubscribed`
|
|
335
|
+
- `q` – optional, search by email
|
|
336
|
+
- `limit` – optional, number of results (default: 20)
|
|
337
|
+
- `offset` – optional, pagination offset
|
|
338
|
+
|
|
339
|
+
## Admin UI
|
|
340
|
+
|
|
341
|
+
After running `medusa admin dev --plugins medusa-contact-us`, the sidebar will include **Contact email list**:
|
|
342
|
+
|
|
343
|
+
- **List view** – search by email, filter by status (subscribed/unsubscribed), inspect creation dates and unsubscribe timestamps.
|
|
344
|
+
- All UI components follow the Medusa UI kit spacing (8pt grid), color, and accessibility guidelines.
|
|
316
345
|
|
|
317
346
|
## Frontend helper
|
|
318
347
|
|
|
319
348
|
Skip hand-writing `fetch` calls by importing the provided helpers. **Storefront requests must include a publishable API key** (create one under `Settings → API Keys` in the Medusa admin). The helpers automatically attach the header for you.
|
|
320
349
|
|
|
321
|
-
### Contact
|
|
350
|
+
### Contact Requests
|
|
351
|
+
|
|
352
|
+
Submit a contact request from your storefront:
|
|
322
353
|
|
|
323
354
|
```ts
|
|
324
|
-
import { submitContactRequest } from "medusa-contact-us"
|
|
355
|
+
import { submitContactRequest } from "medusa-contact-us/helpers"
|
|
325
356
|
|
|
326
|
-
await submitContactRequest(
|
|
357
|
+
const result = await submitContactRequest(
|
|
327
358
|
{
|
|
328
359
|
email: "customer@example.com",
|
|
329
|
-
payload: {
|
|
360
|
+
payload: {
|
|
361
|
+
subject: "Order inquiry",
|
|
362
|
+
message: "I need help with my order #12345",
|
|
363
|
+
priority: "high",
|
|
364
|
+
},
|
|
365
|
+
metadata: {
|
|
366
|
+
source_page: "contact",
|
|
367
|
+
user_agent: navigator.userAgent,
|
|
368
|
+
},
|
|
369
|
+
source: "storefront",
|
|
330
370
|
},
|
|
331
371
|
{
|
|
332
372
|
baseUrl: "https://store.myshop.com",
|
|
333
373
|
publishableApiKey: "pk_test_storefront",
|
|
334
374
|
}
|
|
335
375
|
)
|
|
376
|
+
|
|
377
|
+
console.log("Request created:", result.request.id)
|
|
336
378
|
```
|
|
337
379
|
|
|
338
|
-
Using a Medusa JS client
|
|
380
|
+
Using a Medusa JS client:
|
|
339
381
|
|
|
340
382
|
```ts
|
|
341
383
|
import Medusa from "@medusajs/medusa-js"
|
|
342
|
-
import { submitContactRequest } from "medusa-contact-us"
|
|
384
|
+
import { submitContactRequest } from "medusa-contact-us/helpers"
|
|
343
385
|
|
|
344
386
|
const medusa = new Medusa({
|
|
345
387
|
baseUrl: "https://store.myshop.com",
|
|
@@ -349,35 +391,36 @@ const medusa = new Medusa({
|
|
|
349
391
|
await submitContactRequest(
|
|
350
392
|
{
|
|
351
393
|
email: "customer@example.com",
|
|
352
|
-
payload: {
|
|
394
|
+
payload: {
|
|
395
|
+
subject: "Support request",
|
|
396
|
+
message: "Need assistance",
|
|
397
|
+
},
|
|
353
398
|
},
|
|
354
399
|
{
|
|
355
400
|
client: medusa,
|
|
356
401
|
publishableApiKey: "pk_live_client",
|
|
357
|
-
headers: {
|
|
358
|
-
Cookie: "connect.sid=...",
|
|
359
|
-
},
|
|
360
402
|
}
|
|
361
403
|
)
|
|
362
404
|
```
|
|
363
405
|
|
|
364
|
-
For SSR or edge runtimes, preconfigure the helper
|
|
406
|
+
For SSR or edge runtimes, preconfigure the helper:
|
|
365
407
|
|
|
366
408
|
```ts
|
|
367
|
-
import { createSubmitContactRequest } from "medusa-contact-us"
|
|
409
|
+
import { createSubmitContactRequest } from "medusa-contact-us/helpers"
|
|
368
410
|
|
|
369
|
-
const
|
|
411
|
+
export const submitRequest = createSubmitContactRequest({
|
|
370
412
|
baseUrl: process.env.NEXT_PUBLIC_MEDUSA_URL,
|
|
371
413
|
publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
|
|
372
414
|
})
|
|
373
415
|
|
|
374
416
|
export async function action(formData: FormData) {
|
|
375
|
-
await
|
|
417
|
+
await submitRequest({
|
|
376
418
|
email: formData.get("email") as string,
|
|
377
419
|
payload: {
|
|
378
|
-
subject: formData.get("subject"),
|
|
379
|
-
message: formData.get("message"),
|
|
420
|
+
subject: formData.get("subject") as string,
|
|
421
|
+
message: formData.get("message") as string,
|
|
380
422
|
},
|
|
423
|
+
source: "contact_form",
|
|
381
424
|
})
|
|
382
425
|
}
|
|
383
426
|
```
|
|
@@ -400,7 +443,33 @@ await upsertContactSubscription(
|
|
|
400
443
|
)
|
|
401
444
|
```
|
|
402
445
|
|
|
403
|
-
|
|
446
|
+
Using a Medusa JS client keeps credentials in one place while still letting you override headers (including publishable keys) per call:
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
import Medusa from "@medusajs/medusa-js"
|
|
450
|
+
import { upsertContactSubscription } from "medusa-contact-us"
|
|
451
|
+
|
|
452
|
+
const medusa = new Medusa({
|
|
453
|
+
baseUrl: "https://store.myshop.com",
|
|
454
|
+
publishableKey: "pk_live_client",
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
await upsertContactSubscription(
|
|
458
|
+
{
|
|
459
|
+
email: "newsletter@example.com",
|
|
460
|
+
status: "subscribed",
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
client: medusa,
|
|
464
|
+
publishableApiKey: "pk_live_client",
|
|
465
|
+
headers: {
|
|
466
|
+
Cookie: "connect.sid=...",
|
|
467
|
+
},
|
|
468
|
+
}
|
|
469
|
+
)
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
For SSR or edge runtimes, preconfigure the helper once:
|
|
404
473
|
|
|
405
474
|
```ts
|
|
406
475
|
import { createUpsertContactSubscription } from "medusa-contact-us"
|
|
@@ -410,11 +479,13 @@ export const upsertSubscription = createUpsertContactSubscription({
|
|
|
410
479
|
publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
|
|
411
480
|
})
|
|
412
481
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
482
|
+
export async function action(formData: FormData) {
|
|
483
|
+
await upsertSubscription({
|
|
484
|
+
email: formData.get("email") as string,
|
|
485
|
+
status: input.unsubscribe ? "unsubscribed" : "subscribed",
|
|
486
|
+
source: input.source ?? "footer_form",
|
|
487
|
+
})
|
|
488
|
+
}
|
|
418
489
|
```
|
|
419
490
|
|
|
420
491
|
### Shared helper options
|
|
@@ -425,13 +496,58 @@ await upsertSubscription({
|
|
|
425
496
|
- `fetchImpl` – Custom fetch implementation (SSR, React Native, etc.).
|
|
426
497
|
- `headers` – Additional headers merged into the request (e.g., session cookie, localization). Values you pass here override the defaults, including the publishable key header if you need a per-request key.
|
|
427
498
|
|
|
428
|
-
Customer-authenticated endpoints
|
|
499
|
+
Customer-authenticated endpoints still require the appropriate session cookie or JWT. Provide those via the `headers` option if they're not already managed by the browser fetch call.
|
|
500
|
+
|
|
501
|
+
## Status Workflow
|
|
502
|
+
|
|
503
|
+
Contact requests follow a configurable status workflow:
|
|
504
|
+
|
|
505
|
+
1. **Initial Status**: New requests are created with the `default_status` (typically `"pending"`)
|
|
506
|
+
2. **Status Transitions**: Only transitions defined in `status_transitions` are allowed
|
|
507
|
+
3. **Status History**: All status changes are tracked with timestamps and actor information
|
|
508
|
+
4. **Email Notifications**: Emails can be sent on specific transitions when `send_email: true`
|
|
509
|
+
|
|
510
|
+
### Example Workflow
|
|
511
|
+
|
|
512
|
+
```
|
|
513
|
+
pending → in_progress → resolved → closed
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
In this example:
|
|
517
|
+
- Admin can move requests from `pending` to `in_progress` (email sent)
|
|
518
|
+
- Admin can move requests from `in_progress` to `resolved` (email sent)
|
|
519
|
+
- Admin can move requests from `resolved` to `closed` (no email)
|
|
520
|
+
|
|
521
|
+
### Admin UI Integration
|
|
429
522
|
|
|
430
|
-
|
|
523
|
+
When fetching a contact request via the admin API, the response includes `next_allowed_statuses`:
|
|
431
524
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
525
|
+
```json
|
|
526
|
+
{
|
|
527
|
+
"request": { ... },
|
|
528
|
+
"next_allowed_statuses": ["in_progress"]
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Use this array to populate a status dropdown in your admin UI, ensuring only valid transitions are shown.
|
|
533
|
+
|
|
534
|
+
## Database Migrations
|
|
535
|
+
|
|
536
|
+
After installing the plugin, run migrations to create the required tables:
|
|
537
|
+
|
|
538
|
+
```bash
|
|
539
|
+
npx medusa db:migrate
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
This will create:
|
|
543
|
+
- `contact_email_subscription` table (for email subscriptions)
|
|
544
|
+
- `contact_request` table (for contact requests)
|
|
545
|
+
|
|
546
|
+
**Note**: If you're developing the plugin locally, generate migrations after model changes:
|
|
547
|
+
|
|
548
|
+
```bash
|
|
549
|
+
npx medusa plugin:db:generate
|
|
550
|
+
```
|
|
435
551
|
|
|
436
552
|
## Testing & build
|
|
437
553
|
|
|
@@ -444,6 +560,8 @@ Always run `yarn build` after development to ensure the bundler succeeds before
|
|
|
444
560
|
|
|
445
561
|
## Troubleshooting
|
|
446
562
|
|
|
447
|
-
-
|
|
448
|
-
-
|
|
449
|
-
-
|
|
563
|
+
- **Admin UI blank**: Rebuild the plugin (`yarn build`) and restart the Admin app with the plugin registered in `medusa-config.ts`.
|
|
564
|
+
- **Status transition errors**: Ensure the transition is defined in `status_transitions` configuration. Only transitions from the current status to an allowed next status are permitted.
|
|
565
|
+
- **Payload validation errors**: Check that all required fields defined in `payload_fields` are provided and match the expected types.
|
|
566
|
+
- **Email notifications not sending**: Verify that `email.enabled` is `true` in configuration and that the transition has `send_email: true`. Ensure the notification service is properly configured in your Medusa instance.
|
|
567
|
+
- **Service resolution errors**: Make sure both `ContactSubscriptionModule` and `ContactRequestModule` are registered in the `modules` array in `medusa-config.ts`.
|