customer-registration 0.0.20 → 0.0.23

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 (39) hide show
  1. package/.medusa/server/src/admin/index.js +1 -2
  2. package/.medusa/server/src/admin/index.mjs +1 -2
  3. package/.medusa/server/src/api/store/customers/email/otp/verify/route.js +49 -0
  4. package/.medusa/server/src/api/store/customers/phone/otp/send/route.js +48 -0
  5. package/.medusa/server/src/api/store/customers/phone/otp/verify/route.js +49 -0
  6. package/.medusa/server/src/api/store/customers/route.js +35 -8
  7. package/.medusa/server/src/loaders/index.js +12 -0
  8. package/.medusa/server/src/modules/customer-registration/__tests__/config.spec.js +61 -0
  9. package/.medusa/server/src/modules/customer-registration/config.js +73 -0
  10. package/.medusa/server/src/modules/customer-registration/constants.js +5 -0
  11. package/.medusa/server/src/modules/customer-registration/index.js +13 -0
  12. package/.medusa/server/src/modules/customer-registration/migrations/Migration20250118000000AddEmailVerifiedColumn.js +21 -0
  13. package/.medusa/server/src/modules/customer-registration/migrations/Migration20250118001000CreateCustomerOtpTable.js +48 -0
  14. package/.medusa/server/src/modules/customer-registration/models/customer-otp.js +35 -0
  15. package/.medusa/server/src/modules/customer-registration/service.js +242 -0
  16. package/.medusa/server/src/modules/customer-registration/types.js +3 -0
  17. package/README.md +60 -21
  18. package/package.json +6 -29
  19. package/.medusa/server/src/api/admin/plugin/route.js +0 -7
  20. package/.medusa/server/src/api/store/plugin/route.js +0 -7
  21. package/.medusa/server/src/migrations/Migration20250118000000AddEmailVerifiedColumn.js +0 -21
  22. package/.medusa/server/src/subscribers/customer-created.js +0 -60
  23. package/src/admin/README.md +0 -31
  24. package/src/admin/i18n/README.md +0 -58
  25. package/src/admin/i18n/index.ts +0 -1
  26. package/src/admin/tsconfig.json +0 -24
  27. package/src/admin/vite-env.d.ts +0 -1
  28. package/src/api/README.md +0 -133
  29. package/src/api/admin/plugin/route.ts +0 -8
  30. package/src/api/store/customers/route.ts +0 -81
  31. package/src/api/store/plugin/route.ts +0 -8
  32. package/src/jobs/README.md +0 -36
  33. package/src/links/README.md +0 -26
  34. package/src/migrations/Migration20250118000000AddEmailVerifiedColumn.ts +0 -24
  35. package/src/modules/README.md +0 -116
  36. package/src/providers/README.md +0 -30
  37. package/src/subscribers/README.md +0 -59
  38. package/src/subscribers/customer-created.ts +0 -93
  39. package/src/workflows/README.md +0 -79
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("@medusajs/framework/utils");
4
+ const crypto_1 = require("crypto");
5
+ const customer_otp_1 = require("./models/customer-otp");
6
+ const config_1 = require("./config");
7
+ const constants_1 = require("./constants");
8
+ const OTP_CHARSET_NUMERIC = "0123456789";
9
+ const OTP_CHARSET_ALPHANUMERIC = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
10
+ class CustomerRegistrationModuleService extends (0, utils_1.MedusaService)({
11
+ CustomerOtp: customer_otp_1.CustomerOtp,
12
+ }) {
13
+ constructor(container, moduleOptions, moduleDeclaration) {
14
+ super(container, moduleOptions, moduleDeclaration);
15
+ this.customerModuleService_ = container[utils_1.Modules.CUSTOMER];
16
+ if (!this.customerModuleService_) {
17
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.UNEXPECTED_STATE, "Customer module is required for customer-registration plugin.");
18
+ }
19
+ this.notificationModuleService_ = container[utils_1.Modules.NOTIFICATION];
20
+ this.options_ =
21
+ container[constants_1.CUSTOMER_REGISTRATION_OPTIONS_KEY] ??
22
+ (0, config_1.buildCustomerRegistrationOptions)();
23
+ }
24
+ async sendEmailOtp(identifier, sharedContext) {
25
+ const customer = await this.resolveCustomer(identifier, sharedContext);
26
+ if (!customer.email) {
27
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "Customer is missing an email address.");
28
+ }
29
+ await this.generateAndDispatchOtp({
30
+ customer,
31
+ purpose: "email",
32
+ deliveryTarget: customer.email,
33
+ sharedContext,
34
+ channelOptions: this.options_.email,
35
+ });
36
+ }
37
+ async sendPhoneOtp(identifier, sharedContext) {
38
+ const customer = await this.resolveCustomer(identifier, sharedContext);
39
+ if (!customer.phone) {
40
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "Customer phone number is required before requesting a phone OTP.");
41
+ }
42
+ await this.generateAndDispatchOtp({
43
+ customer,
44
+ purpose: "phone",
45
+ deliveryTarget: customer.phone,
46
+ sharedContext,
47
+ channelOptions: this.options_.phone,
48
+ });
49
+ }
50
+ async verifyEmailOtp(payload, sharedContext) {
51
+ const customer = await this.resolveCustomer(payload, sharedContext);
52
+ await this.verifyOtpForCustomer({
53
+ customer,
54
+ purpose: "email",
55
+ code: payload.code,
56
+ sharedContext,
57
+ });
58
+ return await this.customerModuleService_.updateCustomers(customer.id, { email_verified: true }, sharedContext);
59
+ }
60
+ async verifyPhoneOtp(payload, sharedContext) {
61
+ const customer = await this.resolveCustomer(payload, sharedContext);
62
+ if (!customer.phone) {
63
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "Customer phone number is required before verification.");
64
+ }
65
+ await this.verifyOtpForCustomer({
66
+ customer,
67
+ purpose: "phone",
68
+ code: payload.code,
69
+ sharedContext,
70
+ });
71
+ return await this.customerModuleService_.updateCustomers(customer.id, { phone_verified: true }, sharedContext);
72
+ }
73
+ async resolveCustomer(identifier, sharedContext) {
74
+ if (identifier.customerId) {
75
+ return await this.customerModuleService_.retrieveCustomer(identifier.customerId, {}, sharedContext);
76
+ }
77
+ if (identifier.email) {
78
+ const [customer] = await this.customerModuleService_.listCustomers({ email: identifier.email }, {}, sharedContext);
79
+ if (customer) {
80
+ return customer;
81
+ }
82
+ }
83
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_FOUND, "Unable to locate customer for OTP operation.");
84
+ }
85
+ async generateAndDispatchOtp({ customer, purpose, deliveryTarget, channelOptions, sharedContext, }) {
86
+ await this.assertThrottleWindow(customer.id, purpose, channelOptions.resendThrottleSeconds, sharedContext);
87
+ await this.invalidateActiveOtps(customer.id, purpose, sharedContext);
88
+ const code = this.generateOtpCode();
89
+ const expiresAt = this.calculateExpiry();
90
+ const payload = {
91
+ customer_id: customer.id,
92
+ type: purpose,
93
+ code_hash: this.hashOtp(code),
94
+ delivery_target: deliveryTarget,
95
+ channel: channelOptions.channel,
96
+ expires_at: expiresAt,
97
+ attempt_count: 0,
98
+ };
99
+ const otpRecord = await this.createCustomerOtps(payload, sharedContext);
100
+ try {
101
+ await this.dispatchNotification({
102
+ customer,
103
+ purpose,
104
+ deliveryTarget,
105
+ code,
106
+ channelOptions,
107
+ expiresAt,
108
+ sharedContext,
109
+ });
110
+ }
111
+ catch (error) {
112
+ await this.deleteCustomerOtps(otpRecord.id, sharedContext);
113
+ throw error;
114
+ }
115
+ }
116
+ async verifyOtpForCustomer({ customer, purpose, code, sharedContext, }) {
117
+ const otp = await this.findActiveOtp(customer.id, purpose, sharedContext);
118
+ if (!otp) {
119
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_FOUND, "No OTP found. Please request a new verification code.");
120
+ }
121
+ if (otp.expires_at < new Date()) {
122
+ await this.consumeOtp(otp.id, sharedContext);
123
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_ALLOWED, "OTP has expired. Please request a new verification code.");
124
+ }
125
+ if (otp.code_hash !== this.hashOtp(code)) {
126
+ await this.handleFailedAttempt(otp, sharedContext);
127
+ }
128
+ await this.updateCustomerOtps({
129
+ selector: { id: otp.id },
130
+ data: { consumed_at: new Date() },
131
+ }, sharedContext);
132
+ }
133
+ async handleFailedAttempt(otp, sharedContext) {
134
+ const nextAttempt = (otp.attempt_count ?? 0) + 1;
135
+ await this.updateCustomerOtps({
136
+ selector: { id: otp.id },
137
+ data: { attempt_count: nextAttempt },
138
+ }, sharedContext);
139
+ if (nextAttempt >= this.options_.maxAttempts) {
140
+ await this.consumeOtp(otp.id, sharedContext);
141
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_ALLOWED, "Maximum OTP attempts exceeded. Please request a new code.");
142
+ }
143
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "Invalid OTP supplied.");
144
+ }
145
+ async findActiveOtp(customerId, purpose, sharedContext) {
146
+ const [otp] = await this.listCustomerOtps({
147
+ customer_id: customerId,
148
+ type: purpose,
149
+ consumed_at: null,
150
+ }, {
151
+ order: { created_at: "DESC" },
152
+ take: 1,
153
+ }, sharedContext);
154
+ return otp;
155
+ }
156
+ async consumeOtp(id, sharedContext) {
157
+ await this.updateCustomerOtps({
158
+ selector: { id },
159
+ data: { consumed_at: new Date() },
160
+ }, sharedContext);
161
+ }
162
+ async assertThrottleWindow(customerId, purpose, throttleSeconds, sharedContext) {
163
+ if (!throttleSeconds) {
164
+ return;
165
+ }
166
+ const otp = await this.findActiveOtp(customerId, purpose, sharedContext);
167
+ if (!otp) {
168
+ return;
169
+ }
170
+ const createdAt = new Date(otp.created_at);
171
+ const now = new Date();
172
+ const secondsSince = (now.getTime() - createdAt.getTime()) / 1000;
173
+ if (secondsSince < throttleSeconds) {
174
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_ALLOWED, `Please wait ${Math.ceil(throttleSeconds - secondsSince)} seconds before requesting a new OTP.`);
175
+ }
176
+ }
177
+ async invalidateActiveOtps(customerId, purpose, sharedContext) {
178
+ await this.updateCustomerOtps({
179
+ selector: {
180
+ customer_id: customerId,
181
+ type: purpose,
182
+ consumed_at: null,
183
+ },
184
+ data: { consumed_at: new Date() },
185
+ }, sharedContext);
186
+ }
187
+ generateOtpCode() {
188
+ const { otpLength, otpCharset } = this.options_;
189
+ const charset = otpCharset === "alphanumeric" ? OTP_CHARSET_ALPHANUMERIC : OTP_CHARSET_NUMERIC;
190
+ let code = "";
191
+ for (let i = 0; i < otpLength; i++) {
192
+ const index = (0, crypto_1.randomInt)(0, charset.length);
193
+ code += charset[index];
194
+ }
195
+ return code;
196
+ }
197
+ calculateExpiry() {
198
+ const expiresAt = new Date();
199
+ expiresAt.setMinutes(expiresAt.getMinutes() + this.options_.otpExpiryMinutes);
200
+ return expiresAt;
201
+ }
202
+ hashOtp(code) {
203
+ return (0, crypto_1.createHash)("sha256").update(code).digest("hex");
204
+ }
205
+ async dispatchNotification({ customer, purpose, deliveryTarget, code, channelOptions, expiresAt, sharedContext, }) {
206
+ const notificationService = this.notificationModuleService_;
207
+ if (!notificationService) {
208
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.UNEXPECTED_STATE, "Notification module is not configured. Please register at least one provider.");
209
+ }
210
+ const payload = {
211
+ to: deliveryTarget,
212
+ channel: channelOptions.channel,
213
+ template: channelOptions.template,
214
+ trigger_type: `customer-registration.${purpose}-otp`,
215
+ resource_type: "customer",
216
+ resource_id: customer.id,
217
+ receiver_id: customer.id,
218
+ data: {
219
+ code,
220
+ expires_at: expiresAt.toISOString(),
221
+ purpose,
222
+ customer_id: customer.id,
223
+ },
224
+ content: this.buildNotificationContent(purpose, code, channelOptions),
225
+ };
226
+ await notificationService.createNotifications(payload, sharedContext);
227
+ }
228
+ buildNotificationContent(purpose, code, channelOptions) {
229
+ if ("subject" in channelOptions) {
230
+ return {
231
+ subject: channelOptions.subject,
232
+ text: `Your ${purpose} verification code is ${code}`,
233
+ html: `<p>Your ${purpose} verification code is <strong>${code}</strong>.</p>`,
234
+ };
235
+ }
236
+ return {
237
+ text: `Your ${purpose} verification code is ${code}`,
238
+ };
239
+ }
240
+ }
241
+ exports.default = CustomerRegistrationModuleService;
242
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy9tb2R1bGVzL2N1c3RvbWVyLXJlZ2lzdHJhdGlvbi9zZXJ2aWNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEscURBSWtDO0FBVWxDLG1DQUE4QztBQUM5Qyx3REFBbUQ7QUFLbkQscUNBQTJEO0FBQzNELDJDQUErRDtBQWtCL0QsTUFBTSxtQkFBbUIsR0FBRyxZQUFZLENBQUE7QUFDeEMsTUFBTSx3QkFBd0IsR0FBRyxrQ0FBa0MsQ0FBQTtBQVFuRSxNQUFNLGlDQUFrQyxTQUFRLElBQUEscUJBQWEsRUFBQztJQUM1RCxXQUFXLEVBQVgsMEJBQVc7Q0FDWixDQUFDO0lBS0EsWUFBWSxTQUErQixFQUFFLGFBQXVCLEVBQUUsaUJBQTJCO1FBQy9GLEtBQUssQ0FBQyxTQUFTLEVBQUUsYUFBYSxFQUFFLGlCQUFpQixDQUFDLENBQUE7UUFFbEQsSUFBSSxDQUFDLHNCQUFzQixHQUFHLFNBQVMsQ0FBQyxlQUFPLENBQUMsUUFBUSxDQUEyQixDQUFBO1FBQ25GLElBQUksQ0FBQyxJQUFJLENBQUMsc0JBQXNCLEVBQUUsQ0FBQztZQUNqQyxNQUFNLElBQUksbUJBQVcsQ0FDbkIsbUJBQVcsQ0FBQyxLQUFLLENBQUMsZ0JBQWdCLEVBQ2xDLCtEQUErRCxDQUNoRSxDQUFBO1FBQ0gsQ0FBQztRQUVELElBQUksQ0FBQywwQkFBMEIsR0FBRyxTQUFTLENBQUMsZUFBTyxDQUFDLFlBQVksQ0FBQyxDQUFBO1FBQ2pFLElBQUksQ0FBQyxRQUFRO1lBQ1gsU0FBUyxDQUFDLDZDQUFpQyxDQUFDO2dCQUM1QyxJQUFBLHlDQUFnQyxHQUFFLENBQUE7SUFDdEMsQ0FBQztJQUVELEtBQUssQ0FBQyxZQUFZLENBQUMsVUFBOEIsRUFBRSxhQUF1QjtRQUN4RSxNQUFNLFFBQVEsR0FBRyxNQUFNLElBQUksQ0FBQyxlQUFlLENBQUMsVUFBVSxFQUFFLGFBQWEsQ0FBQyxD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
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi9zcmMvbW9kdWxlcy9jdXN0b21lci1yZWdpc3RyYXRpb24vdHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiJ9
package/README.md CHANGED
@@ -1,13 +1,15 @@
1
- # Medusa Plugin: Customer Email Verified Registration Override
1
+ # Medusa Plugin: Customer Registration Guard
2
2
 
3
- A Medusa v2 plugin that overrides the customer registration endpoint to set `email_verified = false` by default.
3
+ A lean Medusa v2 plugin that hardens customer onboarding, guarantees both `email_verified` and `phone_verified` start as `false`, and now ships a full OTP verification flow for email + phone.
4
4
 
5
5
  ## Features
6
6
 
7
- - Overrides the `POST /store/customers` registration endpoint
8
- - Forces `email_verified = false` and `phone_verified = false` for every new customer no direct DB mutation required
9
- - Emits a notification-driven welcome email every time a customer registers successfully
10
- - Maintains all existing customer registration functionality
7
+ - Overrides `POST /store/customers` with a drop-in route handler.
8
+ - Forces `email_verified = false` and `phone_verified = false` for every new customer, regardless of the request payload.
9
+ - Automatically generates & sends an email OTP right after registration (configurable) using the Notification module + whatever providers your project already registered.
10
+ - Adds dedicated email + phone OTP endpoints for resending and verifying codes while marking the corresponding `*_verified` flags to `true`.
11
+ - Ships a lightweight Medusa module (`customer-registration`) that exposes OTP helpers and bundles Mikro-ORM migrations (email/phone columns + OTP table) that run during `npx medusa db:migrate`.
12
+ - OTP length, charset, expiry, throttling, and provider metadata (channel, template, etc.) are all configurable via plugin options.
11
13
 
12
14
  ## Installation
13
15
 
@@ -52,28 +54,65 @@ yarn dev
52
54
 
53
55
  ## Usage
54
56
 
55
- ### Registration
56
-
57
- When a customer registers via `POST /store/customers`, the plugin automatically forces `email_verified = false` before persisting the customer with the Customer Module. No raw SQL access is required.
58
-
59
- ### Welcome email subscriber
57
+ ### Registration override
58
+
59
+ When a shopper registers via `POST /store/customers`, the plugin intercepts the request, normalizes the payload, and always persists `email_verified = false` and `phone_verified = false`. The rest of Medusa’s default behavior is preserved by deferring to the Customer Module service. If `email.autoSendOnRegistration` is enabled (default), the plugin immediately issues an email OTP through the Notification module using the user's configured providers.
60
+
61
+ ### OTP endpoints
62
+
63
+ The plugin adds a tiny surface area of Store API routes so storefronts can request and verify codes without touching the Admin API:
64
+
65
+ | Endpoint | Body | Description |
66
+ | --- | --- | --- |
67
+ | `POST /store/customers/email/otp/verify` | `{ email?: string, customer_id?: string, code: string }` | Validates an email OTP and sets `email_verified = true`. |
68
+ | `POST /store/customers/phone/otp/send` | `{ email?: string, customer_id?: string }` | Generates an OTP and sends it to the customer's phone number (stored on the customer record). |
69
+ | `POST /store/customers/phone/otp/verify` | `{ email?: string, customer_id?: string, code: string }` | Validates a phone OTP and sets `phone_verified = true`. |
70
+
71
+ All notification dispatches go through Medusa's Notification module. Whatever providers / templates you configure globally will be reused by the plugin.
72
+
73
+ ### Configuration
74
+
75
+ You can override OTP behavior by passing options when registering the plugin:
76
+
77
+ ```ts
78
+ plugins: [
79
+ {
80
+ resolve: "customer-registration",
81
+ options: {
82
+ otpLength: 6,
83
+ otpCharset: "numeric", // or "alphanumeric"
84
+ otpExpiryMinutes: 15,
85
+ maxAttempts: 5,
86
+ email: {
87
+ channel: "email",
88
+ template: "otp-template", // optional provider template handle
89
+ subject: "Verify your Medusa account",
90
+ resendThrottleSeconds: 90,
91
+ autoSendOnRegistration: true,
92
+ },
93
+ phone: {
94
+ channel: "sms",
95
+ template: null,
96
+ resendThrottleSeconds: 60,
97
+ },
98
+ },
99
+ },
100
+ ]
101
+ ```
60
102
 
61
- - A subscriber listening on `customer.created` collects the newly created customer(s) and calls the Notification Module to dispatch an email using the template ID stored in `CUSTOMER_REGISTRATION_WELCOME_TEMPLATE_ID` (defaults to `customer-registration-success`).
62
- - Configure the Notification Module in your Medusa project and ensure at least one provider handles the `email` channel for the template ID above.
63
- - The notification payload includes the customer's `email`, `first_name`, `last_name`, and `full_name`, so templates can be fully personalized.
103
+ ### Database migrations
64
104
 
65
- ### Database migration
105
+ - `Migration20250118000000AddEmailVerifiedColumn`: ensures the customer table includes `email_verified` and `phone_verified` booleans.
106
+ - `Migration20250118001000CreateCustomerOtpTable`: creates the `customer_registration_otp` table for storing hashed OTPs, expiry metadata, and delivery context.
66
107
 
67
- - The plugin ships with a Mikro-ORM migration (`Migration20250118000000AddEmailVerifiedColumn`) that adds both `email_verified BOOLEAN NOT NULL DEFAULT FALSE` and `phone_verified BOOLEAN NOT NULL DEFAULT FALSE` columns to the core `customer` table.
68
- - After installing or updating the plugin, run `npx medusa db:migrate` (or the equivalent package manager command) in your Medusa project to apply the migration before handling registrations.
108
+ After installing or updating the plugin, run `npx medusa db:migrate` in your Medusa project so both migrations are applied before handling registrations.
69
109
 
70
110
  ## Requirements
71
111
 
72
112
  - Medusa v2.11.2 or higher
73
- - Customer table must have `email_verified` and `phone_verified` boolean columns (default: `false`)
74
- - Run the plugin-provided migration so the columns are created (`npx medusa db:migrate`)
75
- - Notification Module configured with at least one provider that supports the `email` channel
76
- - `CUSTOMER_REGISTRATION_WELCOME_TEMPLATE_ID` set in your `.env`/deployment (optional – falls back to `customer-registration-success`)
113
+ - Customer table must have `email_verified` and `phone_verified` boolean columns handled by the bundled migration.
114
+ - At least one Notification provider (email + optional SMS) configured in your Medusa project so OTPs can be delivered automatically.
115
+ - Run `npx medusa db:migrate` after installation so the module migrations are applied.
77
116
 
78
117
  ## Development
79
118
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "customer-registration",
3
- "version": "0.0.20",
4
- "description": "Medusa plugin to override customer registration endpoint to set email_verified = false by default.",
3
+ "version": "0.0.23",
4
+ "description": "Medusa plugin that overrides store customer registration and enforces email/phone verification flags.",
5
5
  "author": "Medusa (https://medusajs.com)",
6
6
  "license": "MIT",
7
7
  "files": [
@@ -9,16 +9,9 @@
9
9
  ],
10
10
  "exports": {
11
11
  "./package.json": "./package.json",
12
- "./workflows": "./.medusa/server/src/workflows/index.js",
13
12
  "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js",
14
13
  "./modules/*": "./.medusa/server/src/modules/*/index.js",
15
- "./providers/*": "./.medusa/server/src/providers/*/index.js",
16
- "./*": "./.medusa/server/src/*.js",
17
- "./admin": {
18
- "import": "./.medusa/server/src/admin/index.mjs",
19
- "require": "./.medusa/server/src/admin/index.js",
20
- "default": "./.medusa/server/src/admin/index.js"
21
- }
14
+ "./*": "./.medusa/server/src/*.js"
22
15
  },
23
16
  "keywords": [
24
17
  "medusa",
@@ -31,36 +24,20 @@
31
24
  "build": "medusa plugin:build",
32
25
  "dev": "medusa plugin:develop",
33
26
  "prepublishOnly": "medusa plugin:build",
34
- "publish": "npx medusa plugin:publish"
27
+ "test": "vitest run"
35
28
  },
36
29
  "devDependencies": {
37
- "@medusajs/admin-sdk": "2.11.2",
38
30
  "@medusajs/cli": "2.11.2",
39
31
  "@medusajs/framework": "2.11.2",
40
- "@medusajs/icons": "2.11.2",
41
32
  "@medusajs/medusa": "2.11.2",
42
- "@medusajs/test-utils": "2.11.2",
43
- "@medusajs/ui": "4.0.25",
44
- "@swc/core": "1.5.7",
45
- "@types/node": "^20.0.0",
46
- "@types/react": "^18.3.2",
47
- "@types/react-dom": "^18.2.25",
48
- "prop-types": "^15.8.1",
49
- "react": "^18.2.0",
50
- "react-dom": "^18.2.0",
51
33
  "ts-node": "^10.9.2",
52
34
  "typescript": "^5.6.2",
53
- "vite": "^5.2.11",
54
- "yalc": "^1.0.0-pre.53"
35
+ "vitest": "^1.5.2"
55
36
  },
56
37
  "peerDependencies": {
57
- "@medusajs/admin-sdk": "2.11.2",
58
38
  "@medusajs/cli": "2.11.2",
59
39
  "@medusajs/framework": "2.11.2",
60
- "@medusajs/icons": "2.11.2",
61
- "@medusajs/medusa": "2.11.2",
62
- "@medusajs/test-utils": "2.11.2",
63
- "@medusajs/ui": "4.0.25"
40
+ "@medusajs/medusa": "2.11.2"
64
41
  },
65
42
  "engines": {
66
43
  "node": ">=20"
@@ -1,7 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GET = GET;
4
- async function GET(req, res) {
5
- res.sendStatus(200);
6
- }
7
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL3BsdWdpbi9yb3V0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUVBLGtCQUtDO0FBTE0sS0FBSyxVQUFVLEdBQUcsQ0FDdkIsR0FBa0IsRUFDbEIsR0FBbUI7SUFFbkIsR0FBRyxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsQ0FBQztBQUN0QixDQUFDIn0=
@@ -1,7 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GET = GET;
4
- async function GET(req, res) {
5
- res.sendStatus(200);
6
- }
7
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL3N0b3JlL3BsdWdpbi9yb3V0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUVBLGtCQUtDO0FBTE0sS0FBSyxVQUFVLEdBQUcsQ0FDdkIsR0FBa0IsRUFDbEIsR0FBbUI7SUFFbkIsR0FBRyxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsQ0FBQztBQUN0QixDQUFDIn0=
@@ -1,21 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Migration20250118000000 = void 0;
4
- const migrations_1 = require("@medusajs/framework/mikro-orm/migrations");
5
- const TABLE_NAME = "customer";
6
- const EMAIL_COLUMN = "email_verified";
7
- const PHONE_COLUMN = "phone_verified";
8
- class Migration20250118000000 extends migrations_1.Migration {
9
- async up() {
10
- this.addSql(`alter table "${TABLE_NAME}"
11
- add column if not exists "${EMAIL_COLUMN}" boolean not null default false,
12
- add column if not exists "${PHONE_COLUMN}" boolean not null default false;`);
13
- }
14
- async down() {
15
- this.addSql(`alter table "${TABLE_NAME}"
16
- drop column if exists "${EMAIL_COLUMN}",
17
- drop column if exists "${PHONE_COLUMN}";`);
18
- }
19
- }
20
- exports.Migration20250118000000 = Migration20250118000000;
21
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiTWlncmF0aW9uMjAyNTAxMTgwMDAwMDBBZGRFbWFpbFZlcmlmaWVkQ29sdW1uLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL21pZ3JhdGlvbnMvTWlncmF0aW9uMjAyNTAxMTgwMDAwMDBBZGRFbWFpbFZlcmlmaWVkQ29sdW1uLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLHlFQUFvRTtBQUVwRSxNQUFNLFVBQVUsR0FBRyxVQUFVLENBQUE7QUFDN0IsTUFBTSxZQUFZLEdBQUcsZ0JBQWdCLENBQUE7QUFDckMsTUFBTSxZQUFZLEdBQUcsZ0JBQWdCLENBQUE7QUFFckMsTUFBYSx1QkFBd0IsU0FBUSxzQkFBUztJQUMzQyxLQUFLLENBQUMsRUFBRTtRQUNmLElBQUksQ0FBQyxNQUFNLENBQ1QsZ0JBQWdCLFVBQVU7b0NBQ0ksWUFBWTtvQ0FDWixZQUFZLG1DQUFtQyxDQUM5RSxDQUFBO0lBQ0gsQ0FBQztJQUVRLEtBQUssQ0FBQyxJQUFJO1FBQ2pCLElBQUksQ0FBQyxNQUFNLENBQ1QsZ0JBQWdCLFVBQVU7aUNBQ0MsWUFBWTtpQ0FDWixZQUFZLElBQUksQ0FDNUMsQ0FBQTtJQUNILENBQUM7Q0FDRjtBQWhCRCwwREFnQkMifQ==
@@ -1,60 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.config = void 0;
4
- exports.default = handleCustomerCreated;
5
- const utils_1 = require("@medusajs/framework/utils");
6
- const WELCOME_TEMPLATE_ID = process.env.CUSTOMER_REGISTRATION_WELCOME_TEMPLATE_ID ||
7
- "customer-registration-success";
8
- async function handleCustomerCreated({ event: { data }, container, }) {
9
- const logger = container.hasRegistration(utils_1.ContainerRegistrationKeys.LOGGER)
10
- ? container.resolve(utils_1.ContainerRegistrationKeys.LOGGER)
11
- : null;
12
- if (!Array.isArray(data) || !data.length) {
13
- return;
14
- }
15
- const customerIds = data
16
- .map((payload) => payload?.id)
17
- .filter((id) => typeof id === "string" && !!id);
18
- if (!customerIds.length) {
19
- return;
20
- }
21
- try {
22
- const customerModuleService = container.resolve(utils_1.Modules.CUSTOMER);
23
- const customers = await customerModuleService.listCustomers({
24
- id: customerIds,
25
- });
26
- if (!customers.length) {
27
- return;
28
- }
29
- if (!container.hasRegistration(utils_1.Modules.NOTIFICATION)) {
30
- logger?.warn?.("[customer-registration] Notification module is not configured. Skipping welcome email.");
31
- return;
32
- }
33
- const notificationModuleService = container.resolve(utils_1.Modules.NOTIFICATION);
34
- await notificationModuleService.createNotifications(customers.map((customer) => ({
35
- to: customer.email,
36
- channel: "email",
37
- template: WELCOME_TEMPLATE_ID,
38
- resource_id: customer.id,
39
- resource_type: "customer",
40
- receiver_id: customer.id,
41
- trigger_type: "customer-registration",
42
- data: {
43
- email: customer.email,
44
- first_name: customer.first_name,
45
- last_name: customer.last_name,
46
- full_name: [customer.first_name, customer.last_name]
47
- .filter(Boolean)
48
- .join(" ")
49
- .trim(),
50
- },
51
- })));
52
- }
53
- catch (error) {
54
- logger?.error?.("[customer-registration] Failed to send welcome email", error);
55
- }
56
- }
57
- exports.config = {
58
- event: utils_1.CustomerWorkflowEvents.CREATED,
59
- };
60
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3VzdG9tZXItY3JlYXRlZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9zdWJzY3JpYmVycy9jdXN0b21lci1jcmVhdGVkLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQW1CQSx3Q0FvRUM7QUFsRkQscURBSWtDO0FBTWxDLE1BQU0sbUJBQW1CLEdBQ3ZCLE9BQU8sQ0FBQyxHQUFHLENBQUMseUNBQXlDO0lBQ3JELCtCQUErQixDQUFBO0FBRWxCLEtBQUssVUFBVSxxQkFBcUIsQ0FBQyxFQUNsRCxLQUFLLEVBQUUsRUFBRSxJQUFJLEVBQUUsRUFDZixTQUFTLEdBQzhCO0lBQ3ZDLE1BQU0sTUFBTSxHQUFrQixTQUFTLENBQUMsZUFBZSxDQUNyRCxpQ0FBeUIsQ0FBQyxNQUFNLENBQ2pDO1FBQ0MsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsaUNBQXlCLENBQUMsTUFBTSxDQUFDO1FBQ3JELENBQUMsQ0FBQyxJQUFJLENBQUE7SUFFUixJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxNQUFNLEVBQUUsQ0FBQztRQUN6QyxPQUFNO0lBQ1IsQ0FBQztJQUVELE1BQU0sV0FBVyxHQUFHLElBQUk7U0FDckIsR0FBRyxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQyxPQUFPLEVBQUUsRUFBRSxDQUFDO1NBQzdCLE1BQU0sQ0FBQyxDQUFDLEVBQUUsRUFBZ0IsRUFBRSxDQUFDLE9BQU8sRUFBRSxLQUFLLFFBQVEsSUFBSSxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUE7SUFFL0QsSUFBSSxDQUFDLFdBQVcsQ0FBQyxNQUFNLEVBQUUsQ0FBQztRQUN4QixPQUFNO0lBQ1IsQ0FBQztJQUVELElBQUksQ0FBQztRQUNILE1BQU0scUJBQXFCLEdBQUcsU0FBUyxDQUFDLE9BQU8sQ0FBQyxlQUFPLENBQUMsUUFBUSxDQUFDLENBQUE7UUFFakUsTUFBTSxTQUFTLEdBQUcsTUFBTSxxQkFBcUIsQ0FBQyxhQUFhLENBQUM7WUFDMUQsRUFBRSxFQUFFLFdBQVc7U0FDaEIsQ0FBQyxDQUFBO1FBRUYsSUFBSSxDQUFDLFNBQVMsQ0FBQyxNQUFNLEVBQUUsQ0FBQztZQUN0QixPQUFNO1FBQ1IsQ0FBQztRQUVELElBQUksQ0FBQyxTQUFTLENBQUMsZUFBZSxDQUFDLGVBQU8sQ0FBQyxZQUFZLENBQUMsRUFBRSxDQUFDO1lBQ3JELE1BQU0sRUFBRSxJQUFJLEVBQUUsQ0FDWix3RkFBd0YsQ0FDekYsQ0FBQTtZQUNELE9BQU07UUFDUixDQUFDO1FBRUQsTUFBTSx5QkFBeUIsR0FBRyxTQUFTLENBQUMsT0FBTyxDQUFDLGVBQU8sQ0FBQyxZQUFZLENBQUMsQ0FBQTtRQUV6RSxNQUFNLHlCQUF5QixDQUFDLG1CQUFtQixDQUNqRCxTQUFTLENBQUMsR0FBRyxDQUFDLENBQUMsUUFBUSxFQUFFLEVBQUUsQ0FBQyxDQUFDO1lBQzNCLEVBQUUsRUFBRSxRQUFRLENBQUMsS0FBSztZQUNsQixPQUFPLEVBQUUsT0FBTztZQUNoQixRQUFRLEVBQUUsbUJBQW1CO1lBQzdCLFdBQVcsRUFBRSxRQUFRLENBQUMsRUFBRTtZQUN4QixhQUFhLEVBQUUsVUFBVTtZQUN6QixXQUFXLEVBQUUsUUFBUSxDQUFDLEVBQUU7WUFDeEIsWUFBWSxFQUFFLHVCQUF1QjtZQUNyQyxJQUFJLEVBQUU7Z0JBQ0osS0FBSyxFQUFFLFFBQVEsQ0FBQyxLQUFLO2dCQUNyQixVQUFVLEVBQUUsUUFBUSxDQUFDLFVBQVU7Z0JBQy9CLFNBQVMsRUFBRSxRQUFRLENBQUMsU0FBUztnQkFDN0IsU0FBUyxFQUFFLENBQUMsUUFBUSxDQUFDLFVBQVUsRUFBRSxRQUFRLENBQUMsU0FBUyxDQUFDO3FCQUNqRCxNQUFNLENBQUMsT0FBTyxDQUFDO3FCQUNmLElBQUksQ0FBQyxHQUFHLENBQUM7cUJBQ1QsSUFBSSxFQUFFO2FBQ1Y7U0FDRixDQUFDLENBQUMsQ0FDSixDQUFBO0lBQ0gsQ0FBQztJQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7UUFDZixNQUFNLEVBQUUsS0FBSyxFQUFFLENBQ2Isc0RBQXNELEVBQ3RELEtBQUssQ0FDTixDQUFBO0lBQ0gsQ0FBQztBQUNILENBQUM7QUFFWSxRQUFBLE1BQU0sR0FBcUI7SUFDdEMsS0FBSyxFQUFFLDhCQUFzQixDQUFDLE9BQU87Q0FDdEMsQ0FBQSJ9
@@ -1,31 +0,0 @@
1
- # Admin Customizations
2
-
3
- You can extend the Medusa Admin to add widgets and new pages. Your customizations interact with API routes to provide merchants with custom functionalities.
4
-
5
- ## Example: Create a Widget
6
-
7
- A widget is a React component that can be injected into an existing page in the admin dashboard.
8
-
9
- For example, create the file `src/admin/widgets/product-widget.tsx` with the following content:
10
-
11
- ```tsx title="src/admin/widgets/product-widget.tsx"
12
- import { defineWidgetConfig } from "@medusajs/admin-sdk"
13
-
14
- // The widget
15
- const ProductWidget = () => {
16
- return (
17
- <div>
18
- <h2>Product Widget</h2>
19
- </div>
20
- )
21
- }
22
-
23
- // The widget's configurations
24
- export const config = defineWidgetConfig({
25
- zone: "product.details.after",
26
- })
27
-
28
- export default ProductWidget
29
- ```
30
-
31
- This inserts a widget with the text “Product Widget” at the end of a product’s details page.
@@ -1,58 +0,0 @@
1
- # Admin Customizations Translations
2
-
3
- The Medusa Admin dashboard supports multiple languages for its interface. Medusa uses [react-i18next](https://react.i18next.com/) to manage translations in the admin dashboard.
4
-
5
- To add translations, create JSON translation files for each language under the `src/admin/i18n/json` directory. For example, create the `src/admin/i18n/json/en.json` file with the following content:
6
-
7
- ```json
8
- {
9
- "brands": {
10
- "title": "Brands",
11
- "description": "Manage your product brands"
12
- },
13
- "done": "Done"
14
- }
15
- ```
16
-
17
- Then, export the translations in `src/admin/i18n/index.ts`:
18
-
19
- ```ts
20
- import en from "./json/en.json" with { type: "json" }
21
-
22
- export default {
23
- en: {
24
- translation: en,
25
- },
26
- }
27
- ```
28
-
29
- Finally, use translations in your admin widgets and routes using the `useTranslation` hook:
30
-
31
- ```tsx
32
- import { defineWidgetConfig } from "@medusajs/admin-sdk"
33
- import { Button, Container, Heading } from "@medusajs/ui"
34
- import { useTranslation } from "react-i18next"
35
-
36
- const ProductWidget = () => {
37
- const { t } = useTranslation()
38
- return (
39
- <Container className="p-0">
40
- <div className="flex items-center justify-between px-6 py-4">
41
- <Heading level="h2">{t("brands.title")}</Heading>
42
- <p>{t("brands.description")}</p>
43
- </div>
44
- <div className="flex justify-end px-6 py-4">
45
- <Button variant="primary">{t("done")}</Button>
46
- </div>
47
- </Container>
48
- )
49
- }
50
-
51
- export const config = defineWidgetConfig({
52
- zone: "product.details.before",
53
- })
54
-
55
- export default ProductWidget
56
- ```
57
-
58
- Learn more about translating admin extensions in the [Translate Admin Customizations](https://docs.medusajs.com/learn/fundamentals/admin/translations) documentation.
@@ -1 +0,0 @@
1
- export default {}