@rovela-ai/sdk 0.14.0 → 0.15.0
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/dist/admin/api/emails.d.ts +2 -1
- package/dist/admin/api/emails.d.ts.map +1 -1
- package/dist/admin/api/emails.js +203 -34
- package/dist/admin/api/emails.js.map +1 -1
- package/dist/admin/components/AdminNav.d.ts.map +1 -1
- package/dist/admin/components/AdminNav.js +2 -1
- package/dist/admin/components/AdminNav.js.map +1 -1
- package/dist/admin/components/EmailsManager.d.ts.map +1 -1
- package/dist/admin/components/EmailsManager.js +168 -51
- package/dist/admin/components/EmailsManager.js.map +1 -1
- package/dist/admin/components/emails/BlocksEditor.d.ts +17 -0
- package/dist/admin/components/emails/BlocksEditor.d.ts.map +1 -0
- package/dist/admin/components/emails/BlocksEditor.js +110 -0
- package/dist/admin/components/emails/BlocksEditor.js.map +1 -0
- package/dist/admin/components/emails/EmailPreviewPane.d.ts +18 -0
- package/dist/admin/components/emails/EmailPreviewPane.d.ts.map +1 -0
- package/dist/admin/components/emails/EmailPreviewPane.js +52 -0
- package/dist/admin/components/emails/EmailPreviewPane.js.map +1 -0
- package/dist/admin/components/emails/EmailThemePanel.d.ts +27 -0
- package/dist/admin/components/emails/EmailThemePanel.d.ts.map +1 -0
- package/dist/admin/components/emails/EmailThemePanel.js +25 -0
- package/dist/admin/components/emails/EmailThemePanel.js.map +1 -0
- package/dist/admin/components/emails/FontPicker.d.ts +11 -0
- package/dist/admin/components/emails/FontPicker.d.ts.map +1 -0
- package/dist/admin/components/emails/FontPicker.js +55 -0
- package/dist/admin/components/emails/FontPicker.js.map +1 -0
- package/dist/admin/components/emails/TextBlockEditor.d.ts +7 -0
- package/dist/admin/components/emails/TextBlockEditor.d.ts.map +1 -0
- package/dist/admin/components/emails/TextBlockEditor.js +70 -0
- package/dist/admin/components/emails/TextBlockEditor.js.map +1 -0
- package/dist/core/db/queries.d.ts +38 -29
- package/dist/core/db/queries.d.ts.map +1 -1
- package/dist/core/db/queries.js +20 -0
- package/dist/core/db/queries.js.map +1 -1
- package/dist/core/db/schema.d.ts +35 -1
- package/dist/core/db/schema.d.ts.map +1 -1
- package/dist/core/db/schema.js +4 -0
- package/dist/core/db/schema.js.map +1 -1
- package/dist/emails/blocks.d.ts +95 -0
- package/dist/emails/blocks.d.ts.map +1 -0
- package/dist/emails/blocks.js +239 -0
- package/dist/emails/blocks.js.map +1 -0
- package/dist/emails/google-fonts.d.ts +13 -0
- package/dist/emails/google-fonts.d.ts.map +1 -0
- package/dist/emails/google-fonts.js +1943 -0
- package/dist/emails/google-fonts.js.map +1 -0
- package/dist/emails/overrides.d.ts +6 -0
- package/dist/emails/overrides.d.ts.map +1 -1
- package/dist/emails/overrides.js +10 -2
- package/dist/emails/overrides.js.map +1 -1
- package/dist/emails/registry.d.ts +49 -5
- package/dist/emails/registry.d.ts.map +1 -1
- package/dist/emails/registry.js +584 -122
- package/dist/emails/registry.js.map +1 -1
- package/dist/emails/send/auth.d.ts.map +1 -1
- package/dist/emails/send/auth.js +40 -25
- package/dist/emails/send/auth.js.map +1 -1
- package/dist/emails/send/orders.d.ts.map +1 -1
- package/dist/emails/send/orders.js +85 -55
- package/dist/emails/send/orders.js.map +1 -1
- package/dist/emails/templates/base.d.ts +7 -0
- package/dist/emails/templates/base.d.ts.map +1 -1
- package/dist/emails/templates/base.js +34 -12
- package/dist/emails/templates/base.js.map +1 -1
- package/dist/emails/templates/email-verification.d.ts +4 -0
- package/dist/emails/templates/email-verification.d.ts.map +1 -1
- package/dist/emails/templates/email-verification.js +10 -2
- package/dist/emails/templates/email-verification.js.map +1 -1
- package/dist/emails/templates/order-cancelled.d.ts +4 -0
- package/dist/emails/templates/order-cancelled.d.ts.map +1 -1
- package/dist/emails/templates/order-cancelled.js +18 -10
- package/dist/emails/templates/order-cancelled.js.map +1 -1
- package/dist/emails/templates/order-confirmation.d.ts +18 -0
- package/dist/emails/templates/order-confirmation.d.ts.map +1 -1
- package/dist/emails/templates/order-confirmation.js +43 -21
- package/dist/emails/templates/order-confirmation.js.map +1 -1
- package/dist/emails/templates/order-delivered.d.ts +6 -0
- package/dist/emails/templates/order-delivered.d.ts.map +1 -1
- package/dist/emails/templates/order-delivered.js +22 -16
- package/dist/emails/templates/order-delivered.js.map +1 -1
- package/dist/emails/templates/order-ready-for-pickup.d.ts +10 -0
- package/dist/emails/templates/order-ready-for-pickup.d.ts.map +1 -1
- package/dist/emails/templates/order-ready-for-pickup.js +38 -22
- package/dist/emails/templates/order-ready-for-pickup.js.map +1 -1
- package/dist/emails/templates/order-shipped.d.ts +6 -0
- package/dist/emails/templates/order-shipped.d.ts.map +1 -1
- package/dist/emails/templates/order-shipped.js +17 -9
- package/dist/emails/templates/order-shipped.js.map +1 -1
- package/dist/emails/templates/password-reset.d.ts +4 -0
- package/dist/emails/templates/password-reset.d.ts.map +1 -1
- package/dist/emails/templates/password-reset.js +10 -2
- package/dist/emails/templates/password-reset.js.map +1 -1
- package/dist/emails/templates/refund-processed.d.ts +6 -0
- package/dist/emails/templates/refund-processed.d.ts.map +1 -1
- package/dist/emails/templates/refund-processed.js +17 -7
- package/dist/emails/templates/refund-processed.js.map +1 -1
- package/dist/emails/templates/welcome.d.ts +2 -0
- package/dist/emails/templates/welcome.d.ts.map +1 -1
- package/dist/emails/templates/welcome.js +8 -4
- package/dist/emails/templates/welcome.js.map +1 -1
- package/dist/emails/theme.d.ts +35 -12
- package/dist/emails/theme.d.ts.map +1 -1
- package/dist/emails/theme.js +178 -53
- package/dist/emails/theme.js.map +1 -1
- package/package.json +7 -1
package/dist/emails/registry.js
CHANGED
|
@@ -4,16 +4,34 @@
|
|
|
4
4
|
* The typed registry of every transactional email the store sends (Phase
|
|
5
5
|
* D). This single object powers the whole /admin/emails surface: the list
|
|
6
6
|
* page (name, trigger, category), the HTML previews (renderSample calls the
|
|
7
|
-
* REAL template with realistic demo data), the test sends,
|
|
8
|
-
*
|
|
7
|
+
* REAL template with realistic demo data), the test sends, the per-email
|
|
8
|
+
* variable chips, AND the block editor — each entry declares its dynamic
|
|
9
|
+
* slots (order items, totals, tracking…) and the default block program that
|
|
10
|
+
* seeds "Customize layout".
|
|
9
11
|
*
|
|
10
12
|
* No registry entry, no store_emails row, no saved theme = the emails ship
|
|
11
|
-
* byte-identical to today.
|
|
13
|
+
* byte-identical to today. The shipped template functions stay the default
|
|
14
|
+
* render path; blocks only take over when the merchant saved a custom body,
|
|
15
|
+
* and tryRenderBlocksEmail() falls back to null (→ shipped template) on any
|
|
16
|
+
* render surprise.
|
|
12
17
|
*/
|
|
13
|
-
import {
|
|
18
|
+
import { renderEmailBlocks } from './blocks';
|
|
19
|
+
import { baseLayout, renderEmailVerification, getEmailVerificationSubject, renderWelcomeEmail, getWelcomeEmailSubject, renderPasswordResetEmail, getPasswordResetSubject, renderOrderConfirmation, getOrderConfirmationSubject, renderOrderShipped, getOrderShippedSubject, renderOrderReadyForPickup, getOrderReadyForPickupSubject, renderOrderDelivered, getOrderDeliveredSubject, renderOrderCancelled, getOrderCancelledSubject, renderRefundProcessed, getRefundProcessedSubject, } from './templates';
|
|
20
|
+
import { renderOrderDetailsBox, renderPaymentInstructionsBox, renderOrderItemsSection, renderOrderSummary, renderOrderAddress, renderReceiptButton, } from './templates/order-confirmation';
|
|
21
|
+
import { renderTrackingDetails, renderDeliveryEstimate, renderShippingTo, } from './templates/order-shipped';
|
|
22
|
+
import { renderPickupOrderInfo, renderPickupLocation, renderOpeningHours, renderPickupInstructions, renderPickupPhoneLine, } from './templates/order-ready-for-pickup';
|
|
23
|
+
import { renderDeliveryDetails, renderReviewRequest, renderReturnCenter, } from './templates/order-delivered';
|
|
24
|
+
import { renderCancellationDetails, renderRefundInfo, } from './templates/order-cancelled';
|
|
25
|
+
import { renderRefundAmountBox, renderRefundMethodBox, renderRefundTiming, } from './templates/refund-processed';
|
|
26
|
+
import { renderVerifyButton, renderVerifyLinkFallback } from './templates/email-verification';
|
|
27
|
+
import { renderResetButton, renderResetLinkFallback } from './templates/password-reset';
|
|
28
|
+
import { renderAccountDetails } from './templates/welcome';
|
|
14
29
|
// =============================================================================
|
|
15
30
|
// Sample data (realistic, shared across previews)
|
|
16
31
|
// =============================================================================
|
|
32
|
+
/** Inline SVG product placeholder — previews never depend on the network. */
|
|
33
|
+
export const SAMPLE_PRODUCT_IMAGE = 'data:image/svg+xml;charset=utf-8,' +
|
|
34
|
+
encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160"><rect width="160" height="160" rx="16" fill="#EEECE7"/><path d="M55 64h50l6 54H49l6-54z" fill="none" stroke="#B9B2A6" stroke-width="6" stroke-linejoin="round"/><path d="M65 64v-9a15 15 0 0 1 30 0v9" fill="none" stroke="#B9B2A6" stroke-width="6" stroke-linecap="round"/></svg>');
|
|
17
35
|
const SAMPLE_ADDRESS = {
|
|
18
36
|
name: 'Alex Martin',
|
|
19
37
|
line1: '12 Riverside Avenue',
|
|
@@ -24,10 +42,23 @@ const SAMPLE_ADDRESS = {
|
|
|
24
42
|
country: 'US',
|
|
25
43
|
};
|
|
26
44
|
const SAMPLE_ITEMS = [
|
|
27
|
-
{ name: 'Classic Ceramic Mug — Set of 2', quantity: 1, price: '$34.00' },
|
|
28
|
-
{ name: 'Organic Cotton Tote', quantity: 2, price: '$18.00' },
|
|
45
|
+
{ name: 'Classic Ceramic Mug — Set of 2', quantity: 1, price: '$34.00', imageUrl: SAMPLE_PRODUCT_IMAGE },
|
|
46
|
+
{ name: 'Organic Cotton Tote', quantity: 2, price: '$18.00', imageUrl: SAMPLE_PRODUCT_IMAGE },
|
|
29
47
|
];
|
|
30
|
-
const SAMPLE_ORDER_NUMBER = 'A1B2C3D4';
|
|
48
|
+
export const SAMPLE_ORDER_NUMBER = 'A1B2C3D4';
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// Block helpers
|
|
51
|
+
// =============================================================================
|
|
52
|
+
let blockSeq = 0;
|
|
53
|
+
function b(block) {
|
|
54
|
+
blockSeq += 1;
|
|
55
|
+
return { id: `d${blockSeq}`, ...block };
|
|
56
|
+
}
|
|
57
|
+
const dyn = (slot) => b({ type: 'dynamic', slot });
|
|
58
|
+
const txt = (html, align = 'center') => b({ type: 'text', html, align });
|
|
59
|
+
const head = (text) => b({ type: 'heading', text });
|
|
60
|
+
const badge = (text, emoji) => b({ type: 'badge', text, emoji });
|
|
61
|
+
const btn = (label, url, style = 'filled') => b({ type: 'button', label, url, style });
|
|
31
62
|
// =============================================================================
|
|
32
63
|
// Registry
|
|
33
64
|
// =============================================================================
|
|
@@ -39,27 +70,86 @@ export const EMAIL_REGISTRY = [
|
|
|
39
70
|
category: 'orders',
|
|
40
71
|
trigger: 'Sent to the customer the moment their order is placed.',
|
|
41
72
|
canDisable: false,
|
|
42
|
-
variables: ['storeName', 'orderNumber', 'customerName'],
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
variables: ['storeName', 'orderNumber', 'customerName', 'orderDate', 'orderUrl'],
|
|
74
|
+
slots: {
|
|
75
|
+
order_details: {
|
|
76
|
+
label: 'Order details',
|
|
77
|
+
description: 'Order number, date and payment status.',
|
|
78
|
+
render: (d, _c) => renderOrderDetailsBox(d),
|
|
79
|
+
},
|
|
80
|
+
payment_instructions: {
|
|
81
|
+
label: 'Payment instructions',
|
|
82
|
+
description: 'Manual-payment instructions (wire, cash on delivery) when the order has any.',
|
|
83
|
+
render: (d, _c) => renderPaymentInstructionsBox(d),
|
|
84
|
+
},
|
|
85
|
+
order_items: {
|
|
86
|
+
label: 'Order items',
|
|
87
|
+
description: 'The purchased products with quantities and prices.',
|
|
88
|
+
render: (d, _c) => renderOrderItemsSection(d),
|
|
89
|
+
},
|
|
90
|
+
order_totals: {
|
|
91
|
+
label: 'Order totals',
|
|
92
|
+
description: 'Subtotal, discount, shipping, tax and total.',
|
|
93
|
+
render: (d, _c) => renderOrderSummary(d),
|
|
94
|
+
},
|
|
95
|
+
shipping_address: {
|
|
96
|
+
label: 'Shipping address',
|
|
97
|
+
description: 'Where the order ships (or the pickup location on click & collect).',
|
|
98
|
+
render: (d, _c) => renderOrderAddress(d),
|
|
99
|
+
},
|
|
100
|
+
receipt_button: {
|
|
101
|
+
label: 'Receipt download',
|
|
102
|
+
description: 'A download button for the card receipt, when one exists.',
|
|
103
|
+
render: (d, _c) => renderReceiptButton(d),
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
defaultBlocks: [
|
|
107
|
+
badge('Order Confirmed'),
|
|
108
|
+
head('Thank You for Your Order!'),
|
|
109
|
+
txt("Hi {{customerName}}, we've received your order and we're getting it ready. We'll email you again as soon as it's on its way or ready to collect."),
|
|
110
|
+
dyn('order_details'),
|
|
111
|
+
dyn('payment_instructions'),
|
|
112
|
+
dyn('order_items'),
|
|
113
|
+
dyn('order_totals'),
|
|
114
|
+
dyn('shipping_address'),
|
|
115
|
+
btn('View Order Details', '{{orderUrl}}'),
|
|
116
|
+
dyn('receipt_button'),
|
|
117
|
+
],
|
|
118
|
+
buildVars: (d, config) => {
|
|
119
|
+
const data = d;
|
|
120
|
+
return {
|
|
121
|
+
storeName: config.storeName,
|
|
122
|
+
customerName: data.customerName,
|
|
123
|
+
orderNumber: data.orderNumber,
|
|
124
|
+
orderDate: data.orderDate,
|
|
125
|
+
orderUrl: data.orderTrackingUrl,
|
|
126
|
+
shopUrl: config.shopUrl,
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
sampleData: (config) => ({
|
|
130
|
+
config,
|
|
131
|
+
customerName: 'Alex',
|
|
132
|
+
customerEmail: 'alex@example.com',
|
|
133
|
+
orderNumber: SAMPLE_ORDER_NUMBER,
|
|
134
|
+
orderDate: 'March 12, 2026',
|
|
135
|
+
paymentStatus: 'Paid',
|
|
136
|
+
items: SAMPLE_ITEMS,
|
|
137
|
+
subtotal: '$70.00',
|
|
138
|
+
discount: '-$10.00',
|
|
139
|
+
discountCode: 'WELCOME10',
|
|
140
|
+
shippingCost: '$6.00',
|
|
141
|
+
tax: '$5.28',
|
|
142
|
+
total: '$71.28',
|
|
143
|
+
shippingAddress: SAMPLE_ADDRESS,
|
|
144
|
+
orderTrackingUrl: `${config.shopUrl}/account/orders`,
|
|
62
145
|
}),
|
|
146
|
+
renderSample: (config) => {
|
|
147
|
+
const entry = EMAIL_REGISTRY[0];
|
|
148
|
+
return {
|
|
149
|
+
subject: getOrderConfirmationSubject(config.storeName, SAMPLE_ORDER_NUMBER),
|
|
150
|
+
html: renderOrderConfirmation(entry.sampleData(config)),
|
|
151
|
+
};
|
|
152
|
+
},
|
|
63
153
|
sampleVars: (config) => ({
|
|
64
154
|
storeName: config.storeName,
|
|
65
155
|
orderNumber: SAMPLE_ORDER_NUMBER,
|
|
@@ -72,21 +162,63 @@ export const EMAIL_REGISTRY = [
|
|
|
72
162
|
category: 'orders',
|
|
73
163
|
trigger: 'Sent when you mark an order as shipped (with tracking when available).',
|
|
74
164
|
canDisable: false,
|
|
75
|
-
variables: ['storeName', 'orderNumber', 'customerName'],
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
165
|
+
variables: ['storeName', 'orderNumber', 'customerName', 'trackingNumber', 'carrierName', 'trackingUrl'],
|
|
166
|
+
slots: {
|
|
167
|
+
tracking_details: {
|
|
168
|
+
label: 'Tracking details',
|
|
169
|
+
description: 'Order number, tracking number and carrier.',
|
|
170
|
+
render: (d, _c) => renderTrackingDetails(d),
|
|
171
|
+
},
|
|
172
|
+
delivery_estimate: {
|
|
173
|
+
label: 'Delivery estimate',
|
|
174
|
+
description: 'The estimated delivery date, highlighted.',
|
|
175
|
+
render: (d, _c) => renderDeliveryEstimate(d),
|
|
176
|
+
},
|
|
177
|
+
shipping_address: {
|
|
178
|
+
label: 'Shipping address',
|
|
179
|
+
description: "Where the package is heading.",
|
|
180
|
+
render: (d, _c) => renderShippingTo(d),
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
defaultBlocks: [
|
|
184
|
+
badge('Order Shipped', '📦'),
|
|
185
|
+
head('Your Order is On Its Way!'),
|
|
186
|
+
txt('Hi {{customerName}}, great news! Your order has been shipped and is heading your way.'),
|
|
187
|
+
dyn('tracking_details'),
|
|
188
|
+
dyn('delivery_estimate'),
|
|
189
|
+
dyn('shipping_address'),
|
|
190
|
+
btn('Track Your Package', '{{trackingUrl}}'),
|
|
191
|
+
],
|
|
192
|
+
buildVars: (d, config) => {
|
|
193
|
+
const data = d;
|
|
194
|
+
return {
|
|
195
|
+
storeName: config.storeName,
|
|
196
|
+
customerName: data.customerName,
|
|
197
|
+
orderNumber: data.orderNumber,
|
|
198
|
+
trackingNumber: data.trackingNumber,
|
|
199
|
+
carrierName: data.carrierName,
|
|
200
|
+
trackingUrl: data.trackingUrl,
|
|
201
|
+
shopUrl: config.shopUrl,
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
sampleData: (config) => ({
|
|
205
|
+
config,
|
|
206
|
+
customerName: 'Alex',
|
|
207
|
+
customerEmail: 'alex@example.com',
|
|
208
|
+
orderNumber: SAMPLE_ORDER_NUMBER,
|
|
209
|
+
trackingNumber: '1Z999AA10123456784',
|
|
210
|
+
carrierName: 'UPS',
|
|
211
|
+
estimatedDeliveryDate: 'March 16, 2026',
|
|
212
|
+
shippingAddress: SAMPLE_ADDRESS,
|
|
213
|
+
trackingUrl: 'https://www.ups.com/track?tracknum=1Z999AA10123456784',
|
|
89
214
|
}),
|
|
215
|
+
renderSample: (config) => {
|
|
216
|
+
const entry = EMAIL_REGISTRY[1];
|
|
217
|
+
return {
|
|
218
|
+
subject: getOrderShippedSubject(config.storeName, SAMPLE_ORDER_NUMBER),
|
|
219
|
+
html: renderOrderShipped(entry.sampleData(config)),
|
|
220
|
+
};
|
|
221
|
+
},
|
|
90
222
|
sampleVars: (config) => ({
|
|
91
223
|
storeName: config.storeName,
|
|
92
224
|
orderNumber: SAMPLE_ORDER_NUMBER,
|
|
@@ -99,22 +231,75 @@ export const EMAIL_REGISTRY = [
|
|
|
99
231
|
category: 'orders',
|
|
100
232
|
trigger: 'Sent on click & collect orders when you mark them ready to collect.',
|
|
101
233
|
canDisable: false,
|
|
102
|
-
variables: ['storeName', 'orderNumber', 'customerName'],
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
234
|
+
variables: ['storeName', 'orderNumber', 'customerName', 'locationName', 'orderUrl'],
|
|
235
|
+
slots: {
|
|
236
|
+
pickup_details: {
|
|
237
|
+
label: 'Pickup details',
|
|
238
|
+
description: 'Order number and pickup location name.',
|
|
239
|
+
render: (d, _c) => renderPickupOrderInfo(d),
|
|
240
|
+
},
|
|
241
|
+
pickup_address: {
|
|
242
|
+
label: 'Pickup address',
|
|
243
|
+
description: "The store location's address.",
|
|
244
|
+
render: (d, _c) => renderPickupLocation(d),
|
|
245
|
+
},
|
|
246
|
+
opening_hours: {
|
|
247
|
+
label: 'Opening hours',
|
|
248
|
+
description: "The location's opening hours, when set.",
|
|
249
|
+
render: (d, _c) => renderOpeningHours(d),
|
|
250
|
+
},
|
|
251
|
+
pickup_instructions: {
|
|
252
|
+
label: 'Pickup instructions',
|
|
253
|
+
description: 'Your pickup instructions (counter, what to bring), when set.',
|
|
254
|
+
render: (d, _c) => renderPickupInstructions(d),
|
|
255
|
+
},
|
|
256
|
+
phone_line: {
|
|
257
|
+
label: 'Store phone',
|
|
258
|
+
description: "A 'Questions? Call the store' line, when the location has a phone.",
|
|
259
|
+
render: (d, _c) => renderPickupPhoneLine(d),
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
defaultBlocks: [
|
|
263
|
+
badge('Ready for Pickup', '🛍️'),
|
|
264
|
+
head('Your Order is Ready for Pickup!'),
|
|
265
|
+
txt('Hi {{customerName}}, your order is packed and waiting for you at {{locationName}}. Bring your order number when you come by.'),
|
|
266
|
+
dyn('pickup_details'),
|
|
267
|
+
dyn('pickup_address'),
|
|
268
|
+
dyn('opening_hours'),
|
|
269
|
+
dyn('pickup_instructions'),
|
|
270
|
+
btn('View Your Order', '{{orderUrl}}'),
|
|
271
|
+
dyn('phone_line'),
|
|
272
|
+
],
|
|
273
|
+
buildVars: (d, config) => {
|
|
274
|
+
const data = d;
|
|
275
|
+
return {
|
|
276
|
+
storeName: config.storeName,
|
|
277
|
+
customerName: data.customerName,
|
|
278
|
+
orderNumber: data.orderNumber,
|
|
279
|
+
locationName: data.locationName,
|
|
280
|
+
orderUrl: data.orderUrl,
|
|
281
|
+
shopUrl: config.shopUrl,
|
|
282
|
+
};
|
|
283
|
+
},
|
|
284
|
+
sampleData: (config) => ({
|
|
285
|
+
config,
|
|
286
|
+
customerName: 'Alex',
|
|
287
|
+
customerEmail: 'alex@example.com',
|
|
288
|
+
orderNumber: SAMPLE_ORDER_NUMBER,
|
|
289
|
+
locationName: 'Downtown Store',
|
|
290
|
+
pickupAddress: { ...SAMPLE_ADDRESS, name: 'Downtown Store', line2: undefined },
|
|
291
|
+
openingHoursLines: ['Mon–Fri 9:00–18:00', 'Sat 10:00–16:00'],
|
|
292
|
+
pickupInstructions: 'Come to the collection counter with your order number.',
|
|
293
|
+
locationPhone: '+1 (555) 010-0100',
|
|
294
|
+
orderUrl: `${config.shopUrl}/account/orders`,
|
|
117
295
|
}),
|
|
296
|
+
renderSample: (config) => {
|
|
297
|
+
const entry = EMAIL_REGISTRY[2];
|
|
298
|
+
return {
|
|
299
|
+
subject: getOrderReadyForPickupSubject(config.storeName, SAMPLE_ORDER_NUMBER),
|
|
300
|
+
html: renderOrderReadyForPickup(entry.sampleData(config)),
|
|
301
|
+
};
|
|
302
|
+
},
|
|
118
303
|
sampleVars: (config) => ({
|
|
119
304
|
storeName: config.storeName,
|
|
120
305
|
orderNumber: SAMPLE_ORDER_NUMBER,
|
|
@@ -127,19 +312,59 @@ export const EMAIL_REGISTRY = [
|
|
|
127
312
|
category: 'orders',
|
|
128
313
|
trigger: 'Sent when you mark an order as delivered.',
|
|
129
314
|
canDisable: true,
|
|
130
|
-
variables: ['storeName', 'orderNumber', 'customerName'],
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
315
|
+
variables: ['storeName', 'orderNumber', 'customerName', 'deliveryDate', 'shopUrl'],
|
|
316
|
+
slots: {
|
|
317
|
+
delivery_details: {
|
|
318
|
+
label: 'Delivery details',
|
|
319
|
+
description: 'Order number, delivery date and where it was left.',
|
|
320
|
+
render: (d, _c) => renderDeliveryDetails(d),
|
|
321
|
+
},
|
|
322
|
+
review_request: {
|
|
323
|
+
label: 'Review request',
|
|
324
|
+
description: 'A highlighted ask-for-a-review card, when reviews are set up.',
|
|
325
|
+
render: (d, _c) => renderReviewRequest(d),
|
|
326
|
+
},
|
|
327
|
+
return_center: {
|
|
328
|
+
label: 'Returns link',
|
|
329
|
+
description: 'A return/exchange prompt with a button, when returns are enabled.',
|
|
330
|
+
render: (d, _c) => renderReturnCenter(d),
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
defaultBlocks: [
|
|
334
|
+
badge('Delivered'),
|
|
335
|
+
head('Your Order Has Been Delivered!'),
|
|
336
|
+
txt('Hi {{customerName}}, your order has been successfully delivered. We hope you love your purchase!'),
|
|
337
|
+
dyn('delivery_details'),
|
|
338
|
+
dyn('review_request'),
|
|
339
|
+
dyn('return_center'),
|
|
340
|
+
btn('Continue Shopping', '{{shopUrl}}'),
|
|
341
|
+
],
|
|
342
|
+
buildVars: (d, config) => {
|
|
343
|
+
const data = d;
|
|
344
|
+
return {
|
|
345
|
+
storeName: config.storeName,
|
|
346
|
+
customerName: data.customerName,
|
|
347
|
+
orderNumber: data.orderNumber,
|
|
348
|
+
deliveryDate: data.deliveryDate,
|
|
349
|
+
shopUrl: config.shopUrl,
|
|
350
|
+
};
|
|
351
|
+
},
|
|
352
|
+
sampleData: (config) => ({
|
|
353
|
+
config,
|
|
354
|
+
customerName: 'Alex',
|
|
355
|
+
customerEmail: 'alex@example.com',
|
|
356
|
+
orderNumber: SAMPLE_ORDER_NUMBER,
|
|
357
|
+
deliveryDate: 'March 16, 2026',
|
|
358
|
+
deliveryLocation: 'Front door',
|
|
359
|
+
returnCenterUrl: `${config.shopUrl}/account/orders`,
|
|
142
360
|
}),
|
|
361
|
+
renderSample: (config) => {
|
|
362
|
+
const entry = EMAIL_REGISTRY[3];
|
|
363
|
+
return {
|
|
364
|
+
subject: getOrderDeliveredSubject(config.storeName, SAMPLE_ORDER_NUMBER),
|
|
365
|
+
html: renderOrderDelivered(entry.sampleData(config)),
|
|
366
|
+
};
|
|
367
|
+
},
|
|
143
368
|
sampleVars: (config) => ({
|
|
144
369
|
storeName: config.storeName,
|
|
145
370
|
orderNumber: SAMPLE_ORDER_NUMBER,
|
|
@@ -152,19 +377,54 @@ export const EMAIL_REGISTRY = [
|
|
|
152
377
|
category: 'orders',
|
|
153
378
|
trigger: 'Sent when an order is cancelled.',
|
|
154
379
|
canDisable: false,
|
|
155
|
-
variables: ['storeName', 'orderNumber', 'customerName'],
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
380
|
+
variables: ['storeName', 'orderNumber', 'customerName', 'shopUrl'],
|
|
381
|
+
slots: {
|
|
382
|
+
cancellation_details: {
|
|
383
|
+
label: 'Cancellation details',
|
|
384
|
+
description: 'Order number, cancellation date and the reason when one was given.',
|
|
385
|
+
render: (d, _c) => renderCancellationDetails(d),
|
|
386
|
+
},
|
|
387
|
+
refund_info: {
|
|
388
|
+
label: 'Refund information',
|
|
389
|
+
description: 'The refund status message for this cancellation.',
|
|
390
|
+
render: (d, _c) => renderRefundInfo(d),
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
defaultBlocks: [
|
|
394
|
+
head('Order Cancelled'),
|
|
395
|
+
txt('Hi {{customerName}},', 'left'),
|
|
396
|
+
txt('Your order #{{orderNumber}} has been cancelled as requested.', 'left'),
|
|
397
|
+
dyn('cancellation_details'),
|
|
398
|
+
dyn('refund_info'),
|
|
399
|
+
txt("If you have any questions about this cancellation, please don't hesitate to reach out to our support team.", 'left'),
|
|
400
|
+
btn('Continue Shopping', '{{shopUrl}}'),
|
|
401
|
+
],
|
|
402
|
+
buildVars: (d, config) => {
|
|
403
|
+
const data = d;
|
|
404
|
+
return {
|
|
405
|
+
storeName: config.storeName,
|
|
406
|
+
customerName: data.customerName,
|
|
407
|
+
orderNumber: data.orderNumber,
|
|
408
|
+
cancellationDate: data.cancellationDate,
|
|
409
|
+
shopUrl: config.shopUrl,
|
|
410
|
+
};
|
|
411
|
+
},
|
|
412
|
+
sampleData: (config) => ({
|
|
413
|
+
config,
|
|
414
|
+
customerName: 'Alex',
|
|
415
|
+
customerEmail: 'alex@example.com',
|
|
416
|
+
orderNumber: SAMPLE_ORDER_NUMBER,
|
|
417
|
+
cancellationReason: 'Requested by the customer',
|
|
418
|
+
cancellationDate: 'March 13, 2026',
|
|
419
|
+
refundMessage: 'Your payment has been refunded to your original payment method.',
|
|
167
420
|
}),
|
|
421
|
+
renderSample: (config) => {
|
|
422
|
+
const entry = EMAIL_REGISTRY[4];
|
|
423
|
+
return {
|
|
424
|
+
subject: getOrderCancelledSubject(config.storeName, SAMPLE_ORDER_NUMBER),
|
|
425
|
+
html: renderOrderCancelled(entry.sampleData(config)),
|
|
426
|
+
};
|
|
427
|
+
},
|
|
168
428
|
sampleVars: (config) => ({
|
|
169
429
|
storeName: config.storeName,
|
|
170
430
|
orderNumber: SAMPLE_ORDER_NUMBER,
|
|
@@ -177,21 +437,64 @@ export const EMAIL_REGISTRY = [
|
|
|
177
437
|
category: 'orders',
|
|
178
438
|
trigger: 'Sent when a refund is issued (full or from an approved return).',
|
|
179
439
|
canDisable: false,
|
|
180
|
-
variables: ['storeName', 'orderNumber', 'customerName'],
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
440
|
+
variables: ['storeName', 'orderNumber', 'customerName', 'refundAmount', 'shopUrl'],
|
|
441
|
+
slots: {
|
|
442
|
+
refund_amount: {
|
|
443
|
+
label: 'Refund amount',
|
|
444
|
+
description: 'The refunded amount with order number and date.',
|
|
445
|
+
render: (d, _c) => renderRefundAmountBox(d),
|
|
446
|
+
},
|
|
447
|
+
refund_method: {
|
|
448
|
+
label: 'Refund method',
|
|
449
|
+
description: 'The payment method and estimated arrival window.',
|
|
450
|
+
render: (d, _c) => renderRefundMethodBox(d),
|
|
451
|
+
},
|
|
452
|
+
refund_timing: {
|
|
453
|
+
label: 'Timing note',
|
|
454
|
+
description: "The 'allow N business days' expectations line.",
|
|
455
|
+
render: (d, _c) => renderRefundTiming(d),
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
defaultBlocks: [
|
|
459
|
+
badge('Refund Processed'),
|
|
460
|
+
head('Your Refund Has Been Processed'),
|
|
461
|
+
txt('Hi {{customerName}},', 'left'),
|
|
462
|
+
txt('Good news! Your refund for order #{{orderNumber}} has been processed and issued to your original payment method.', 'left'),
|
|
463
|
+
dyn('refund_amount'),
|
|
464
|
+
dyn('refund_method'),
|
|
465
|
+
dyn('refund_timing'),
|
|
466
|
+
txt("If you have any questions about this refund, please don't hesitate to contact us."),
|
|
467
|
+
btn('Continue Shopping', '{{shopUrl}}'),
|
|
468
|
+
],
|
|
469
|
+
buildVars: (d, config) => {
|
|
470
|
+
const data = d;
|
|
471
|
+
return {
|
|
472
|
+
storeName: config.storeName,
|
|
473
|
+
customerName: data.customerName,
|
|
474
|
+
orderNumber: data.orderNumber,
|
|
475
|
+
refundAmount: data.refundAmount,
|
|
476
|
+
refundDate: data.refundDate,
|
|
477
|
+
shopUrl: config.shopUrl,
|
|
478
|
+
};
|
|
479
|
+
},
|
|
480
|
+
sampleData: (config) => ({
|
|
481
|
+
config,
|
|
482
|
+
customerName: 'Alex',
|
|
483
|
+
customerEmail: 'alex@example.com',
|
|
484
|
+
orderNumber: SAMPLE_ORDER_NUMBER,
|
|
485
|
+
refundAmount: '$71.28',
|
|
486
|
+
refundDate: 'March 14, 2026',
|
|
487
|
+
paymentMethod: 'Original payment method',
|
|
488
|
+
estimatedArrival: '5-10 business days',
|
|
489
|
+
businessDays: '5-10',
|
|
194
490
|
}),
|
|
491
|
+
renderSample: (config) => {
|
|
492
|
+
const entry = EMAIL_REGISTRY[5];
|
|
493
|
+
return {
|
|
494
|
+
subject: getRefundProcessedSubject(config.storeName, SAMPLE_ORDER_NUMBER),
|
|
495
|
+
html: renderRefundProcessed(entry.sampleData(config)),
|
|
496
|
+
};
|
|
497
|
+
},
|
|
195
498
|
sampleVars: (config) => ({
|
|
196
499
|
storeName: config.storeName,
|
|
197
500
|
orderNumber: SAMPLE_ORDER_NUMBER,
|
|
@@ -205,17 +508,51 @@ export const EMAIL_REGISTRY = [
|
|
|
205
508
|
category: 'account',
|
|
206
509
|
trigger: 'Sent when a customer creates an account, to confirm their address.',
|
|
207
510
|
canDisable: false,
|
|
208
|
-
variables: ['storeName', 'customerName'],
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
511
|
+
variables: ['storeName', 'customerName', 'expiryTime'],
|
|
512
|
+
slots: {
|
|
513
|
+
verify_button: {
|
|
514
|
+
label: 'Verification button',
|
|
515
|
+
description: "The 'Verify Email Address' button with the secure link.",
|
|
516
|
+
render: (d, _c) => renderVerifyButton(d),
|
|
517
|
+
},
|
|
518
|
+
link_fallback: {
|
|
519
|
+
label: 'Link fallback',
|
|
520
|
+
description: 'The copy-paste link box for clients where the button fails.',
|
|
521
|
+
render: (d, _c) => renderVerifyLinkFallback(d),
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
defaultBlocks: [
|
|
525
|
+
head('Verify Your Email Address'),
|
|
526
|
+
txt('Hi {{customerName}},', 'left'),
|
|
527
|
+
txt('Thanks for signing up with {{storeName}}! To complete your registration and start shopping, please verify your email address by clicking the button below.', 'left'),
|
|
528
|
+
dyn('verify_button'),
|
|
529
|
+
txt('This link will expire in {{expiryTime}} hours for security reasons.', 'left'),
|
|
530
|
+
txt("If you didn't create an account with us, you can safely ignore this email.", 'left'),
|
|
531
|
+
dyn('link_fallback'),
|
|
532
|
+
],
|
|
533
|
+
buildVars: (d, config) => {
|
|
534
|
+
const data = d;
|
|
535
|
+
return {
|
|
536
|
+
storeName: config.storeName,
|
|
537
|
+
customerName: data.customerName,
|
|
538
|
+
expiryTime: data.expiryTime,
|
|
539
|
+
shopUrl: config.shopUrl,
|
|
540
|
+
};
|
|
541
|
+
},
|
|
542
|
+
sampleData: (config) => ({
|
|
543
|
+
config,
|
|
544
|
+
customerName: 'Alex',
|
|
545
|
+
customerEmail: 'alex@example.com',
|
|
546
|
+
verificationLink: `${config.shopUrl}/auth/verify-email?token=sample`,
|
|
547
|
+
expiryTime: '24',
|
|
218
548
|
}),
|
|
549
|
+
renderSample: (config) => {
|
|
550
|
+
const entry = EMAIL_REGISTRY[6];
|
|
551
|
+
return {
|
|
552
|
+
subject: getEmailVerificationSubject(config.storeName),
|
|
553
|
+
html: renderEmailVerification(entry.sampleData(config)),
|
|
554
|
+
};
|
|
555
|
+
},
|
|
219
556
|
sampleVars: (config) => ({
|
|
220
557
|
storeName: config.storeName,
|
|
221
558
|
customerName: 'Alex',
|
|
@@ -227,16 +564,43 @@ export const EMAIL_REGISTRY = [
|
|
|
227
564
|
category: 'account',
|
|
228
565
|
trigger: 'Sent once, right after a customer verifies their email.',
|
|
229
566
|
canDisable: true,
|
|
230
|
-
variables: ['storeName', 'customerName'],
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
567
|
+
variables: ['storeName', 'customerName', 'shopUrl'],
|
|
568
|
+
slots: {
|
|
569
|
+
account_details: {
|
|
570
|
+
label: 'Account details',
|
|
571
|
+
description: "The customer's email + account creation date box.",
|
|
572
|
+
render: (d, _c) => renderAccountDetails(d),
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
defaultBlocks: [
|
|
576
|
+
head('Welcome to {{storeName}}!'),
|
|
577
|
+
txt('Hi {{customerName}},', 'left'),
|
|
578
|
+
txt("Thank you for creating an account with us! We're excited to have you join our community.", 'left'),
|
|
579
|
+
txt('Your account is now set up and ready to go. You can start exploring our products and enjoy a seamless shopping experience.', 'left'),
|
|
580
|
+
btn('Start Shopping', '{{shopUrl}}'),
|
|
581
|
+
dyn('account_details'),
|
|
582
|
+
],
|
|
583
|
+
buildVars: (d, config) => {
|
|
584
|
+
const data = d;
|
|
585
|
+
return {
|
|
586
|
+
storeName: config.storeName,
|
|
587
|
+
customerName: data.customerName,
|
|
588
|
+
shopUrl: config.shopUrl,
|
|
589
|
+
};
|
|
590
|
+
},
|
|
591
|
+
sampleData: (config) => ({
|
|
592
|
+
config,
|
|
593
|
+
customerName: 'Alex',
|
|
594
|
+
customerEmail: 'alex@example.com',
|
|
595
|
+
createdDate: 'March 12, 2026',
|
|
239
596
|
}),
|
|
597
|
+
renderSample: (config) => {
|
|
598
|
+
const entry = EMAIL_REGISTRY[7];
|
|
599
|
+
return {
|
|
600
|
+
subject: getWelcomeEmailSubject(config.storeName),
|
|
601
|
+
html: renderWelcomeEmail(entry.sampleData(config)),
|
|
602
|
+
};
|
|
603
|
+
},
|
|
240
604
|
sampleVars: (config) => ({
|
|
241
605
|
storeName: config.storeName,
|
|
242
606
|
customerName: 'Alex',
|
|
@@ -248,16 +612,50 @@ export const EMAIL_REGISTRY = [
|
|
|
248
612
|
category: 'account',
|
|
249
613
|
trigger: 'Sent when a customer asks to reset their password.',
|
|
250
614
|
canDisable: false,
|
|
251
|
-
variables: ['storeName', 'customerName'],
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
615
|
+
variables: ['storeName', 'customerName', 'expiryTime'],
|
|
616
|
+
slots: {
|
|
617
|
+
reset_button: {
|
|
618
|
+
label: 'Reset button',
|
|
619
|
+
description: "The 'Reset Password' button with the secure link.",
|
|
620
|
+
render: (d, _c) => renderResetButton(d),
|
|
621
|
+
},
|
|
622
|
+
link_fallback: {
|
|
623
|
+
label: 'Link fallback',
|
|
624
|
+
description: 'The copy-paste link box for clients where the button fails.',
|
|
625
|
+
render: (d, _c) => renderResetLinkFallback(d),
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
defaultBlocks: [
|
|
629
|
+
head('Reset Your Password'),
|
|
630
|
+
txt('Hi {{customerName}},', 'left'),
|
|
631
|
+
txt('We received a request to reset your password. Click the button below to create a new password:', 'left'),
|
|
632
|
+
dyn('reset_button'),
|
|
633
|
+
txt('This link will expire in {{expiryTime}} hours for security reasons.', 'left'),
|
|
634
|
+
txt("If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.", 'left'),
|
|
635
|
+
dyn('link_fallback'),
|
|
636
|
+
],
|
|
637
|
+
buildVars: (d, config) => {
|
|
638
|
+
const data = d;
|
|
639
|
+
return {
|
|
640
|
+
storeName: config.storeName,
|
|
641
|
+
customerName: data.customerName,
|
|
642
|
+
expiryTime: data.expiryTime,
|
|
643
|
+
shopUrl: config.shopUrl,
|
|
644
|
+
};
|
|
645
|
+
},
|
|
646
|
+
sampleData: (config) => ({
|
|
647
|
+
config,
|
|
648
|
+
customerName: 'Alex',
|
|
649
|
+
resetLink: `${config.shopUrl}/auth/reset-password?token=sample`,
|
|
650
|
+
expiryTime: '1',
|
|
260
651
|
}),
|
|
652
|
+
renderSample: (config) => {
|
|
653
|
+
const entry = EMAIL_REGISTRY[8];
|
|
654
|
+
return {
|
|
655
|
+
subject: getPasswordResetSubject(config.storeName),
|
|
656
|
+
html: renderPasswordResetEmail(entry.sampleData(config)),
|
|
657
|
+
};
|
|
658
|
+
},
|
|
261
659
|
sampleVars: (config) => ({
|
|
262
660
|
storeName: config.storeName,
|
|
263
661
|
customerName: 'Alex',
|
|
@@ -267,4 +665,68 @@ export const EMAIL_REGISTRY = [
|
|
|
267
665
|
export function getEmailRegistryEntry(key) {
|
|
268
666
|
return EMAIL_REGISTRY.find((e) => e.key === key);
|
|
269
667
|
}
|
|
668
|
+
// =============================================================================
|
|
669
|
+
// Block-body rendering (the fail-open custom path)
|
|
670
|
+
// =============================================================================
|
|
671
|
+
/**
|
|
672
|
+
* Render a customized (block) body for a BUILT-IN email against real or
|
|
673
|
+
* sample data. Returns the full email HTML, or null on ANY surprise — the
|
|
674
|
+
* caller then falls back to the shipped template. A merchant can never
|
|
675
|
+
* break their own transactional emails.
|
|
676
|
+
*/
|
|
677
|
+
export function tryRenderBlocksEmail(key, blocks, data, config, title) {
|
|
678
|
+
try {
|
|
679
|
+
const entry = getEmailRegistryEntry(key);
|
|
680
|
+
if (!entry || blocks.length === 0)
|
|
681
|
+
return null;
|
|
682
|
+
const slots = {};
|
|
683
|
+
for (const [slotKey, slot] of Object.entries(entry.slots)) {
|
|
684
|
+
slots[slotKey] = () => slot.render(data, config);
|
|
685
|
+
}
|
|
686
|
+
const content = renderEmailBlocks(blocks, {
|
|
687
|
+
config,
|
|
688
|
+
vars: entry.buildVars(data, config),
|
|
689
|
+
slots,
|
|
690
|
+
});
|
|
691
|
+
return baseLayout({ config, title, content });
|
|
692
|
+
}
|
|
693
|
+
catch (error) {
|
|
694
|
+
console.error(`[Emails] Custom body for '${key}' failed to render — using the standard template:`, error);
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// =============================================================================
|
|
699
|
+
// Custom emails (merchant-created, block-built)
|
|
700
|
+
// =============================================================================
|
|
701
|
+
export const CUSTOM_EMAIL_PREFIX = 'custom_';
|
|
702
|
+
export function isCustomEmailKey(key) {
|
|
703
|
+
return key.startsWith(CUSTOM_EMAIL_PREFIX);
|
|
704
|
+
}
|
|
705
|
+
/** Variables available inside custom emails. */
|
|
706
|
+
export const CUSTOM_EMAIL_VARIABLES = ['storeName', 'customerName', 'shopUrl'];
|
|
707
|
+
export function customEmailVars(config, customerName) {
|
|
708
|
+
return {
|
|
709
|
+
storeName: config.storeName,
|
|
710
|
+
customerName: customerName?.trim() || 'there',
|
|
711
|
+
shopUrl: config.shopUrl,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
/** Starter body for a freshly created custom email. */
|
|
715
|
+
export function customStarterBlocks(name) {
|
|
716
|
+
return [
|
|
717
|
+
head(name || 'A note from {{storeName}}'),
|
|
718
|
+
txt('Hi {{customerName}},', 'left'),
|
|
719
|
+
txt('Write your message here. Use the panel on the left to add headings, buttons, images and more.', 'left'),
|
|
720
|
+
btn('Visit the store', '{{shopUrl}}'),
|
|
721
|
+
];
|
|
722
|
+
}
|
|
723
|
+
/** Render a custom email's blocks into full HTML. Throws on surprises. */
|
|
724
|
+
export function renderCustomEmail(blocks, config, options) {
|
|
725
|
+
const content = renderEmailBlocks(blocks, {
|
|
726
|
+
config,
|
|
727
|
+
vars: customEmailVars(config, options.customerName),
|
|
728
|
+
slots: {},
|
|
729
|
+
});
|
|
730
|
+
return baseLayout({ config, title: options.title, content });
|
|
731
|
+
}
|
|
270
732
|
//# sourceMappingURL=registry.js.map
|