@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.
Files changed (105) hide show
  1. package/dist/admin/api/emails.d.ts +2 -1
  2. package/dist/admin/api/emails.d.ts.map +1 -1
  3. package/dist/admin/api/emails.js +203 -34
  4. package/dist/admin/api/emails.js.map +1 -1
  5. package/dist/admin/components/AdminNav.d.ts.map +1 -1
  6. package/dist/admin/components/AdminNav.js +2 -1
  7. package/dist/admin/components/AdminNav.js.map +1 -1
  8. package/dist/admin/components/EmailsManager.d.ts.map +1 -1
  9. package/dist/admin/components/EmailsManager.js +168 -51
  10. package/dist/admin/components/EmailsManager.js.map +1 -1
  11. package/dist/admin/components/emails/BlocksEditor.d.ts +17 -0
  12. package/dist/admin/components/emails/BlocksEditor.d.ts.map +1 -0
  13. package/dist/admin/components/emails/BlocksEditor.js +110 -0
  14. package/dist/admin/components/emails/BlocksEditor.js.map +1 -0
  15. package/dist/admin/components/emails/EmailPreviewPane.d.ts +18 -0
  16. package/dist/admin/components/emails/EmailPreviewPane.d.ts.map +1 -0
  17. package/dist/admin/components/emails/EmailPreviewPane.js +52 -0
  18. package/dist/admin/components/emails/EmailPreviewPane.js.map +1 -0
  19. package/dist/admin/components/emails/EmailThemePanel.d.ts +27 -0
  20. package/dist/admin/components/emails/EmailThemePanel.d.ts.map +1 -0
  21. package/dist/admin/components/emails/EmailThemePanel.js +25 -0
  22. package/dist/admin/components/emails/EmailThemePanel.js.map +1 -0
  23. package/dist/admin/components/emails/FontPicker.d.ts +11 -0
  24. package/dist/admin/components/emails/FontPicker.d.ts.map +1 -0
  25. package/dist/admin/components/emails/FontPicker.js +55 -0
  26. package/dist/admin/components/emails/FontPicker.js.map +1 -0
  27. package/dist/admin/components/emails/TextBlockEditor.d.ts +7 -0
  28. package/dist/admin/components/emails/TextBlockEditor.d.ts.map +1 -0
  29. package/dist/admin/components/emails/TextBlockEditor.js +70 -0
  30. package/dist/admin/components/emails/TextBlockEditor.js.map +1 -0
  31. package/dist/core/db/queries.d.ts +38 -29
  32. package/dist/core/db/queries.d.ts.map +1 -1
  33. package/dist/core/db/queries.js +20 -0
  34. package/dist/core/db/queries.js.map +1 -1
  35. package/dist/core/db/schema.d.ts +35 -1
  36. package/dist/core/db/schema.d.ts.map +1 -1
  37. package/dist/core/db/schema.js +4 -0
  38. package/dist/core/db/schema.js.map +1 -1
  39. package/dist/emails/blocks.d.ts +95 -0
  40. package/dist/emails/blocks.d.ts.map +1 -0
  41. package/dist/emails/blocks.js +239 -0
  42. package/dist/emails/blocks.js.map +1 -0
  43. package/dist/emails/google-fonts.d.ts +13 -0
  44. package/dist/emails/google-fonts.d.ts.map +1 -0
  45. package/dist/emails/google-fonts.js +1943 -0
  46. package/dist/emails/google-fonts.js.map +1 -0
  47. package/dist/emails/overrides.d.ts +6 -0
  48. package/dist/emails/overrides.d.ts.map +1 -1
  49. package/dist/emails/overrides.js +10 -2
  50. package/dist/emails/overrides.js.map +1 -1
  51. package/dist/emails/registry.d.ts +49 -5
  52. package/dist/emails/registry.d.ts.map +1 -1
  53. package/dist/emails/registry.js +584 -122
  54. package/dist/emails/registry.js.map +1 -1
  55. package/dist/emails/send/auth.d.ts.map +1 -1
  56. package/dist/emails/send/auth.js +40 -25
  57. package/dist/emails/send/auth.js.map +1 -1
  58. package/dist/emails/send/orders.d.ts.map +1 -1
  59. package/dist/emails/send/orders.js +85 -55
  60. package/dist/emails/send/orders.js.map +1 -1
  61. package/dist/emails/templates/base.d.ts +7 -0
  62. package/dist/emails/templates/base.d.ts.map +1 -1
  63. package/dist/emails/templates/base.js +34 -12
  64. package/dist/emails/templates/base.js.map +1 -1
  65. package/dist/emails/templates/email-verification.d.ts +4 -0
  66. package/dist/emails/templates/email-verification.d.ts.map +1 -1
  67. package/dist/emails/templates/email-verification.js +10 -2
  68. package/dist/emails/templates/email-verification.js.map +1 -1
  69. package/dist/emails/templates/order-cancelled.d.ts +4 -0
  70. package/dist/emails/templates/order-cancelled.d.ts.map +1 -1
  71. package/dist/emails/templates/order-cancelled.js +18 -10
  72. package/dist/emails/templates/order-cancelled.js.map +1 -1
  73. package/dist/emails/templates/order-confirmation.d.ts +18 -0
  74. package/dist/emails/templates/order-confirmation.d.ts.map +1 -1
  75. package/dist/emails/templates/order-confirmation.js +43 -21
  76. package/dist/emails/templates/order-confirmation.js.map +1 -1
  77. package/dist/emails/templates/order-delivered.d.ts +6 -0
  78. package/dist/emails/templates/order-delivered.d.ts.map +1 -1
  79. package/dist/emails/templates/order-delivered.js +22 -16
  80. package/dist/emails/templates/order-delivered.js.map +1 -1
  81. package/dist/emails/templates/order-ready-for-pickup.d.ts +10 -0
  82. package/dist/emails/templates/order-ready-for-pickup.d.ts.map +1 -1
  83. package/dist/emails/templates/order-ready-for-pickup.js +38 -22
  84. package/dist/emails/templates/order-ready-for-pickup.js.map +1 -1
  85. package/dist/emails/templates/order-shipped.d.ts +6 -0
  86. package/dist/emails/templates/order-shipped.d.ts.map +1 -1
  87. package/dist/emails/templates/order-shipped.js +17 -9
  88. package/dist/emails/templates/order-shipped.js.map +1 -1
  89. package/dist/emails/templates/password-reset.d.ts +4 -0
  90. package/dist/emails/templates/password-reset.d.ts.map +1 -1
  91. package/dist/emails/templates/password-reset.js +10 -2
  92. package/dist/emails/templates/password-reset.js.map +1 -1
  93. package/dist/emails/templates/refund-processed.d.ts +6 -0
  94. package/dist/emails/templates/refund-processed.d.ts.map +1 -1
  95. package/dist/emails/templates/refund-processed.js +17 -7
  96. package/dist/emails/templates/refund-processed.js.map +1 -1
  97. package/dist/emails/templates/welcome.d.ts +2 -0
  98. package/dist/emails/templates/welcome.d.ts.map +1 -1
  99. package/dist/emails/templates/welcome.js +8 -4
  100. package/dist/emails/templates/welcome.js.map +1 -1
  101. package/dist/emails/theme.d.ts +35 -12
  102. package/dist/emails/theme.d.ts.map +1 -1
  103. package/dist/emails/theme.js +178 -53
  104. package/dist/emails/theme.js.map +1 -1
  105. package/package.json +7 -1
@@ -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, and the
8
- * per-email variable chips for subject editing.
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 { renderEmailVerification, getEmailVerificationSubject, renderWelcomeEmail, getWelcomeEmailSubject, renderPasswordResetEmail, getPasswordResetSubject, renderOrderConfirmation, getOrderConfirmationSubject, renderOrderShipped, getOrderShippedSubject, renderOrderReadyForPickup, getOrderReadyForPickupSubject, renderOrderDelivered, getOrderDeliveredSubject, renderOrderCancelled, getOrderCancelledSubject, renderRefundProcessed, getRefundProcessedSubject, } from './templates';
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
- renderSample: (config) => ({
44
- subject: getOrderConfirmationSubject(config.storeName, SAMPLE_ORDER_NUMBER),
45
- html: renderOrderConfirmation({
46
- config,
47
- customerName: 'Alex',
48
- customerEmail: 'alex@example.com',
49
- orderNumber: SAMPLE_ORDER_NUMBER,
50
- orderDate: 'March 12, 2026',
51
- paymentStatus: 'Paid',
52
- items: SAMPLE_ITEMS,
53
- subtotal: '$70.00',
54
- discount: '-$10.00',
55
- discountCode: 'WELCOME10',
56
- shippingCost: '$6.00',
57
- tax: '$5.28',
58
- total: '$71.28',
59
- shippingAddress: SAMPLE_ADDRESS,
60
- orderTrackingUrl: `${config.shopUrl}/account/orders`,
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
- renderSample: (config) => ({
77
- subject: getOrderShippedSubject(config.storeName, SAMPLE_ORDER_NUMBER),
78
- html: renderOrderShipped({
79
- config,
80
- customerName: 'Alex',
81
- customerEmail: 'alex@example.com',
82
- orderNumber: SAMPLE_ORDER_NUMBER,
83
- trackingNumber: '1Z999AA10123456784',
84
- carrierName: 'UPS',
85
- estimatedDeliveryDate: 'March 16, 2026',
86
- shippingAddress: SAMPLE_ADDRESS,
87
- trackingUrl: 'https://www.ups.com/track?tracknum=1Z999AA10123456784',
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
- renderSample: (config) => ({
104
- subject: getOrderReadyForPickupSubject(config.storeName, SAMPLE_ORDER_NUMBER),
105
- html: renderOrderReadyForPickup({
106
- config,
107
- customerName: 'Alex',
108
- customerEmail: 'alex@example.com',
109
- orderNumber: SAMPLE_ORDER_NUMBER,
110
- locationName: 'Downtown Store',
111
- pickupAddress: { ...SAMPLE_ADDRESS, name: 'Downtown Store', line2: undefined },
112
- openingHoursLines: ['Mon–Fri 9:00–18:00', 'Sat 10:00–16:00'],
113
- pickupInstructions: 'Come to the collection counter with your order number.',
114
- locationPhone: '+1 (555) 010-0100',
115
- orderUrl: `${config.shopUrl}/account/orders`,
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
- renderSample: (config) => ({
132
- subject: getOrderDeliveredSubject(config.storeName, SAMPLE_ORDER_NUMBER),
133
- html: renderOrderDelivered({
134
- config,
135
- customerName: 'Alex',
136
- customerEmail: 'alex@example.com',
137
- orderNumber: SAMPLE_ORDER_NUMBER,
138
- deliveryDate: 'March 16, 2026',
139
- deliveryLocation: 'Front door',
140
- returnCenterUrl: `${config.shopUrl}/account/orders`,
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
- renderSample: (config) => ({
157
- subject: getOrderCancelledSubject(config.storeName, SAMPLE_ORDER_NUMBER),
158
- html: renderOrderCancelled({
159
- config,
160
- customerName: 'Alex',
161
- customerEmail: 'alex@example.com',
162
- orderNumber: SAMPLE_ORDER_NUMBER,
163
- cancellationReason: 'Requested by the customer',
164
- cancellationDate: 'March 13, 2026',
165
- refundMessage: 'Your payment has been refunded to your original payment method.',
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
- renderSample: (config) => ({
182
- subject: getRefundProcessedSubject(config.storeName, SAMPLE_ORDER_NUMBER),
183
- html: renderRefundProcessed({
184
- config,
185
- customerName: 'Alex',
186
- customerEmail: 'alex@example.com',
187
- orderNumber: SAMPLE_ORDER_NUMBER,
188
- refundAmount: '$71.28',
189
- refundDate: 'March 14, 2026',
190
- paymentMethod: 'Original payment method',
191
- estimatedArrival: '5-10 business days',
192
- businessDays: '5-10',
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
- renderSample: (config) => ({
210
- subject: getEmailVerificationSubject(config.storeName),
211
- html: renderEmailVerification({
212
- config,
213
- customerName: 'Alex',
214
- customerEmail: 'alex@example.com',
215
- verificationLink: `${config.shopUrl}/auth/verify-email?token=sample`,
216
- expiryTime: '24',
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
- renderSample: (config) => ({
232
- subject: getWelcomeEmailSubject(config.storeName),
233
- html: renderWelcomeEmail({
234
- config,
235
- customerName: 'Alex',
236
- customerEmail: 'alex@example.com',
237
- createdDate: 'March 12, 2026',
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
- renderSample: (config) => ({
253
- subject: getPasswordResetSubject(config.storeName),
254
- html: renderPasswordResetEmail({
255
- config,
256
- customerName: 'Alex',
257
- resetLink: `${config.shopUrl}/auth/reset-password?token=sample`,
258
- expiryTime: '1',
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