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.
- package/.medusa/server/src/admin/index.js +1 -2
- package/.medusa/server/src/admin/index.mjs +1 -2
- package/.medusa/server/src/api/store/customers/email/otp/verify/route.js +49 -0
- package/.medusa/server/src/api/store/customers/phone/otp/send/route.js +48 -0
- package/.medusa/server/src/api/store/customers/phone/otp/verify/route.js +49 -0
- package/.medusa/server/src/api/store/customers/route.js +35 -8
- package/.medusa/server/src/loaders/index.js +12 -0
- package/.medusa/server/src/modules/customer-registration/__tests__/config.spec.js +61 -0
- package/.medusa/server/src/modules/customer-registration/config.js +73 -0
- package/.medusa/server/src/modules/customer-registration/constants.js +5 -0
- package/.medusa/server/src/modules/customer-registration/index.js +13 -0
- package/.medusa/server/src/modules/customer-registration/migrations/Migration20250118000000AddEmailVerifiedColumn.js +21 -0
- package/.medusa/server/src/modules/customer-registration/migrations/Migration20250118001000CreateCustomerOtpTable.js +48 -0
- package/.medusa/server/src/modules/customer-registration/models/customer-otp.js +35 -0
- package/.medusa/server/src/modules/customer-registration/service.js +242 -0
- package/.medusa/server/src/modules/customer-registration/types.js +3 -0
- package/README.md +60 -21
- package/package.json +6 -29
- package/.medusa/server/src/api/admin/plugin/route.js +0 -7
- package/.medusa/server/src/api/store/plugin/route.js +0 -7
- package/.medusa/server/src/migrations/Migration20250118000000AddEmailVerifiedColumn.js +0 -21
- package/.medusa/server/src/subscribers/customer-created.js +0 -60
- package/src/admin/README.md +0 -31
- package/src/admin/i18n/README.md +0 -58
- package/src/admin/i18n/index.ts +0 -1
- package/src/admin/tsconfig.json +0 -24
- package/src/admin/vite-env.d.ts +0 -1
- package/src/api/README.md +0 -133
- package/src/api/admin/plugin/route.ts +0 -8
- package/src/api/store/customers/route.ts +0 -81
- package/src/api/store/plugin/route.ts +0 -8
- package/src/jobs/README.md +0 -36
- package/src/links/README.md +0 -26
- package/src/migrations/Migration20250118000000AddEmailVerifiedColumn.ts +0 -24
- package/src/modules/README.md +0 -116
- package/src/providers/README.md +0 -30
- package/src/subscribers/README.md +0 -59
- package/src/subscribers/customer-created.ts +0 -93
- 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
|
|
1
|
+
# Medusa Plugin: Customer Registration Guard
|
|
2
2
|
|
|
3
|
-
A Medusa v2 plugin that
|
|
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
|
|
8
|
-
- Forces `email_verified = false` and `phone_verified = false` for every new customer
|
|
9
|
-
-
|
|
10
|
-
-
|
|
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
|
|
58
|
-
|
|
59
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
74
|
-
-
|
|
75
|
-
-
|
|
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.
|
|
4
|
-
"description": "Medusa plugin
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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/
|
|
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
|
package/src/admin/README.md
DELETED
|
@@ -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.
|
package/src/admin/i18n/README.md
DELETED
|
@@ -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.
|
package/src/admin/i18n/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default {}
|