medusa-contact-us
v0.0.26
Published
Manage storefront email subscriptions (opt-ins and opt-outs) in Medusa Admin.
Maintainers
Readme
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.
Features:
- 📝 Contact request management with status workflow and email notifications
- ✉️ Email subscription management (opt-ins and opt-outs)
- 🔄 Configurable status transitions and validation
- 📧 Automatic email notifications on status changes
- 🎯 Payload validation against configurable field definitions
Features
Email Subscriptions
- ✉️ Storefront opt-in helper + admin list for email subscriptions (subscribed/unsubscribed)
- 🖥️ React Admin extension with list view, filters, and search
- 🔌 Frontend helper (
upsertContactSubscription) to abstract API wiring - 🧪 Table-driven tests for helper logic
Contact Requests
- 📝 Contact request management with configurable status workflow
- 🔄 Status transition validation with configurable allowed transitions
- 📧 Email notifications on status changes (configurable per transition)
- 🎯 Payload validation against configurable field definitions
- 📊 Admin API for listing, filtering, and managing requests
- 🔌 Storefront helper (
submitContactRequest) for easy integration - 📈 Status history tracking with timestamps and actor information
Installation
yarn add medusa-contact-us
# or
npm install medusa-contact-usConfiguration
Inside medusa-config.ts register the plugin:
import type { ConfigModule } from "@medusajs/framework/types"
import {
ContactSubscriptionModule,
ContactRequestModule,
} from "medusa-contact-us"
const plugins = [
{
resolve: "medusa-contact-us",
options: {
// Contact Request Configuration
default_status: "pending",
payload_fields: [
{
key: "subject",
type: "text",
required: true,
label: "Subject",
placeholder: "Enter subject",
},
{
key: "message",
type: "textarea",
required: true,
label: "Message",
placeholder: "Enter your message",
},
{
key: "priority",
type: "select",
required: false,
label: "Priority",
options: [
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High" },
],
},
],
allowed_statuses: ["pending", "in_progress", "resolved", "closed"],
status_transitions: [
{
from: null,
to: "pending",
send_email: false,
},
{
from: "pending",
to: "in_progress",
send_email: true,
email_subject: "Your request is being processed",
email_template: null, // Optional: path to email template
},
{
from: "in_progress",
to: "resolved",
send_email: true,
email_subject: "Your request has been resolved",
},
{
from: "resolved",
to: "closed",
send_email: false,
},
],
email: {
enabled: true,
default_subject: "Contact Request Status Update",
default_template: null, // Optional: default email template path
},
},
},
]
// Register the modules
const modules = [ContactSubscriptionModule, ContactRequestModule]
export default {
projectConfig: {
// Your Medusa project configuration
// database_url: process.env.DATABASE_URL,
// ...
},
plugins,
modules,
} satisfies ConfigModuleConfiguration Options
Contact Request Options
default_status(string, optional): Default status for new requests. Default:"pending"payload_fields(array, optional): Field definitions for payload validation. Each field supports:key(string, required): Field identifiertype(string, required): Field type -"text","textarea","number","email","select","checkbox"required(boolean, optional): Whether field is requiredlabel(string, optional): Display labelplaceholder(string, optional): Placeholder textoptions(array, optional): For select type -[{ value: string, label: string }]validation(object, optional): Validation rules (min, max, pattern)
allowed_statuses(array, optional): List of allowed status values. Default:["pending", "in_progress", "resolved", "closed"]status_transitions(array, optional): Allowed status transitions. Each transition supports:from(string | null): Source status (null = initial status)to(string, required): Target statussend_email(boolean, optional): Whether to send email on this transitionemail_subject(string, optional): Custom email subject for this transitionemail_template(string | null, optional): Custom email template path
email(object, optional): Email notification settingsenabled(boolean, optional): Enable/disable email notifications. Default:truedefault_subject(string, optional): Default email subject. Default:"Contact Request Status Update"default_template(string | null, optional): Default email template path
REST API
Storefront (open)
Contact Requests
Create a contact request:
curl -X POST https://your-medusa.com/store/contact-requests \
-H "Content-Type: application/json" \
-H "x-publishable-api-key: pk_storefront" \
-d '{
"email": "[email protected]",
"payload": {
"subject": "Order inquiry",
"message": "I need help with my order #12345"
},
"metadata": {
"source_page": "contact"
},
"source": "storefront"
}'Body fields:
email– required, valid email addresspayload– optional, JSON object with custom fields (validated against configuredpayload_fields)metadata– optional, additional metadatasource– optional, request source identifier (default:"storefront")
Response:
{
"request": {
"id": "creq_123",
"email": "[email protected]",
"payload": {
"subject": "Order inquiry",
"message": "I need help with my order #12345"
},
"status": "pending",
"status_history": [
{
"from": null,
"to": "pending",
"changed_at": "2024-11-29T16:33:17.000Z"
}
],
"metadata": {
"source_page": "contact"
},
"source": "storefront",
"created_at": "2024-11-29T16:33:17.000Z",
"updated_at": "2024-11-29T16:33:17.000Z"
}
}Email subscriptions
curl -X POST https://your-medusa.com/store/contact-email-subscriptions \
-H "Content-Type: application/json" \
-H "x-publishable-api-key: pk_storefront" \
-d '{
"email": "[email protected]",
"status": "subscribed",
"source": "footer"
}'Body fields:
email– required, unique per entry (case-insensitive).status– optional, defaults tosubscribed. Pass"unsubscribed"to honor opt-outs.metadata/source– optional context stored alongside the entry.
Response:
{
"subscription": {
"id": "csub_123",
"email": "[email protected]",
"status": "subscribed",
"source": "footer",
"created_at": "2024-11-26T10:00:00.000Z"
}
}Admin (requires admin auth cookie/token)
Contact Requests
List contact requests:
curl -X GET "https://your-medusa.com/admin/contact-requests?status=pending&[email protected]&limit=20&offset=0" \
-H "Authorization: Bearer <token>"Query parameters:
email– optional, filter by email (partial match)status– optional, filter by statussource– optional, filter by sourcecreated_at.gte– optional, filter by creation date (ISO 8601)created_at.lte– optional, filter by creation date (ISO 8601)limit– optional, number of results (default: 20, max: 100)offset– optional, pagination offset (default: 0)order– optional, sort field:created_at,updated_at,email(default:created_at)order_direction– optional, sort direction:ASCorDESC(default:DESC)
Get contact request details:
curl -X GET "https://your-medusa.com/admin/contact-requests/creq_123" \
-H "Authorization: Bearer <token>"Response includes the request and next_allowed_statuses array for the admin UI:
{
"request": {
"id": "creq_123",
"email": "[email protected]",
"payload": { ... },
"status": "pending",
"status_history": [ ... ],
"metadata": { ... },
"source": "storefront",
"created_at": "2024-11-29T16:33:17.000Z",
"updated_at": "2024-11-29T16:33:17.000Z"
},
"next_allowed_statuses": ["in_progress"]
}Create contact request (admin):
curl -X POST https://your-medusa.com/admin/contact-requests \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"email": "[email protected]",
"payload": {
"subject": "Support request",
"message": "Need assistance"
},
"source": "admin"
}'Update contact request status:
curl -X POST https://your-medusa.com/admin/contact-requests/creq_123/status \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"status": "in_progress"
}'Note: Only status transitions defined in status_transitions configuration are allowed. The API will return an error if an invalid transition is attempted.
Email Subscriptions
List subscriptions:
curl -X GET "https://your-medusa.com/admin/contact-email-subscriptions?status=subscribed&limit=20" \
-H "Authorization: Bearer <token>"Query parameters:
status– optional, filter bysubscribedorunsubscribedq– optional, search by emaillimit– optional, number of results (default: 20)offset– optional, pagination offset
Admin UI
After running medusa admin dev --plugins medusa-contact-us, the sidebar will include Contact email list:
- List view – search by email, filter by status (subscribed/unsubscribed), inspect creation dates and unsubscribe timestamps.
- All UI components follow the Medusa UI kit spacing (8pt grid), color, and accessibility guidelines.
Frontend helper
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.
Contact Requests
Submit a contact request from your storefront:
import { submitContactRequest } from "medusa-contact-us/helpers"
const result = await submitContactRequest(
{
email: "[email protected]",
payload: {
subject: "Order inquiry",
message: "I need help with my order #12345",
priority: "high",
},
metadata: {
source_page: "contact",
user_agent: navigator.userAgent,
},
source: "storefront",
},
{
baseUrl: "https://store.myshop.com",
publishableApiKey: "pk_test_storefront",
}
)
console.log("Request created:", result.request.id)Using a Medusa JS client:
import Medusa from "@medusajs/medusa-js"
import { submitContactRequest } from "medusa-contact-us/helpers"
const medusa = new Medusa({
baseUrl: "https://store.myshop.com",
publishableKey: "pk_live_client",
})
await submitContactRequest(
{
email: "[email protected]",
payload: {
subject: "Support request",
message: "Need assistance",
},
},
{
client: medusa,
publishableApiKey: "pk_live_client",
}
)For SSR or edge runtimes, preconfigure the helper:
import { createSubmitContactRequest } from "medusa-contact-us/helpers"
export const submitRequest = createSubmitContactRequest({
baseUrl: process.env.NEXT_PUBLIC_MEDUSA_URL,
publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
})
export async function action(formData: FormData) {
await submitRequest({
email: formData.get("email") as string,
payload: {
subject: formData.get("subject") as string,
message: formData.get("message") as string,
},
source: "contact_form",
})
}Email subscriptions
import { upsertContactSubscription } from "medusa-contact-us/helpers"
await upsertContactSubscription(
{
email: "[email protected]",
status: "subscribed",
metadata: { channel: "footer" },
},
{
baseUrl: "https://store.myshop.com",
publishableApiKey: "pk_test_storefront",
}
)Using a Medusa JS client keeps credentials in one place while still letting you override headers (including publishable keys) per call:
import Medusa from "@medusajs/medusa-js"
import { upsertContactSubscription } from "medusa-contact-us/helpers"
const medusa = new Medusa({
baseUrl: "https://store.myshop.com",
publishableKey: "pk_live_client",
})
await upsertContactSubscription(
{
email: "[email protected]",
status: "subscribed",
},
{
client: medusa,
publishableApiKey: "pk_live_client",
headers: {
Cookie: "connect.sid=...",
},
}
)For SSR or edge runtimes, preconfigure the helper once:
import { createUpsertContactSubscription } from "medusa-contact-us/helpers"
export const upsertSubscription = createUpsertContactSubscription({
baseUrl: process.env.NEXT_PUBLIC_MEDUSA_URL,
publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
})
export async function action(formData: FormData) {
await upsertSubscription({
email: formData.get("email") as string,
status: formData.get("unsubscribe") ? "unsubscribed" : "subscribed",
source: "footer_form",
})
}Shared helper options
publishableApiKey– Automatically sets thex-publishable-api-keyheader when no Medusa client is used.baseUrl– Storefront API origin (ignored whenclientis provided).client– Pre-configured Medusa JS/SDK instance (reuses its base URL and publishable key).fetchImpl– Custom fetch implementation (SSR, React Native, etc.).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.
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.
Status Workflow
Contact requests follow a configurable status workflow:
- Initial Status: New requests are created with the
default_status(typically"pending") - Status Transitions: Only transitions defined in
status_transitionsare allowed - Status History: All status changes are tracked with timestamps and actor information
- Email Notifications: Emails can be sent on specific transitions when
send_email: true
Example Workflow
pending → in_progress → resolved → closedIn this example:
- Admin can move requests from
pendingtoin_progress(email sent) - Admin can move requests from
in_progresstoresolved(email sent) - Admin can move requests from
resolvedtoclosed(no email)
Admin UI Integration
When fetching a contact request via the admin API, the response includes next_allowed_statuses:
{
"request": { ... },
"next_allowed_statuses": ["in_progress"]
}Use this array to populate a status dropdown in your admin UI, ensuring only valid transitions are shown.
Database Migrations
After installing the plugin, run migrations to create the required tables:
npx medusa db:migrateThis will create:
contact_email_subscriptiontable (for email subscriptions)contact_requesttable (for contact requests)
Note: If you're developing the plugin locally, generate migrations after model changes:
npx medusa plugin:db:generateTesting & build
yarn test # runs Vitest table-driven suites
yarn build # compiles the plugin via `medusa plugin:build`Always run yarn build after development to ensure the bundler succeeds before publishing or yalc linking.
Troubleshooting
- Admin UI blank: Rebuild the plugin (
yarn build) and restart the Admin app with the plugin registered inmedusa-config.ts. - Status transition errors: Ensure the transition is defined in
status_transitionsconfiguration. Only transitions from the current status to an allowed next status are permitted. - Payload validation errors: Check that all required fields defined in
payload_fieldsare provided and match the expected types. - Email notifications not sending: Verify that
email.enabledistruein configuration and that the transition hassend_email: true. Ensure the notification service is properly configured in your Medusa instance. - Service resolution errors: Make sure both
ContactSubscriptionModuleandContactRequestModuleare registered in themodulesarray inmedusa-config.ts.
