feat(campaigns/templates): visible wrapper-expiry date in the email
Two new template variables are auto-derived from r.gift_expires_at at
render time (separately by the worker and the /view fallback to keep
them consistent):
{{expires_at_date}} locale-formatted FR/EN long date — "21 août 2026"
/ "August 21, 2026". Empty when no wrapper token.
{{expires_in_days}} remaining days as string (rounded up). Useful
for tight deadlines where a date is too distant
to convey urgency.
Templates: a small centered badge appears between the CTA button and
the prorata disclaimer, wrapped in a Mustache section so it disappears
cleanly on campaigns that pre-date the wrapper feature.
⏰ Cadeau valide jusqu'au <strong>21 août 2026</strong>
⏰ Gift valid until <strong>August 21, 2026</strong>
Editor merge-tag panel updated so authors can drop these into custom
copy without remembering the exact variable names. The legacy
{{expiry}} field stays — it's still the right tool for promotion-end
dates that don't track the gift link's own deadline.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
d529019106
commit
feeae6eb40
|
|
@ -280,7 +280,9 @@ const editorOptions = {
|
||||||
mergeTags: [
|
mergeTags: [
|
||||||
{ name: 'Montant', value: '{{amount}}', sample: '60 $' },
|
{ name: 'Montant', value: '{{amount}}', sample: '60 $' },
|
||||||
{ name: 'Lien cadeau (URL)', value: '{{gift_url}}', sample: 'https://gft.link/abc' },
|
{ name: 'Lien cadeau (URL)', value: '{{gift_url}}', sample: 'https://gft.link/abc' },
|
||||||
{ name: "Date d'expiration", value: '{{expiry}}', sample: '31 décembre 2026' },
|
{ name: "Date d'expiration (texte)", value: '{{expiry}}', sample: '31 décembre 2026' },
|
||||||
|
{ name: 'Expiration auto (date)', value: '{{expires_at_date}}', sample: '21 août 2026' },
|
||||||
|
{ name: 'Expiration auto (jours)', value: '{{expires_in_days}}', sample: '90' },
|
||||||
{ name: 'Engagement (mois)', value: '{{commitment_months}}', sample: '3' },
|
{ name: 'Engagement (mois)', value: '{{commitment_months}}', sample: '3' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -493,6 +493,21 @@ table, td { color: #1B2E24; } #u_body a { color: #00C853; text-decoration: under
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
|
||||||
|
<!-- ════════ WRAPPER EXPIRY BADGE (auto-derived) ════════ -->
|
||||||
|
{{#expires_at_date}}
|
||||||
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||||
|
<tbody><tr><td style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;direction:ltr;font-size:0px;padding:0 36px 4px;text-align:center;">
|
||||||
|
<div style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.4;text-align:center;color:#64748B;">
|
||||||
|
⏰ Gift valid until <strong style="color:#1B2E24;">{{expires_at_date}}</strong>
|
||||||
|
</div>
|
||||||
|
</td></tr></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
{{/expires_at_date}}
|
||||||
|
|
||||||
<!-- ════════ PRORATA REFUND DISCLAIMER ════════ -->
|
<!-- ════════ PRORATA REFUND DISCLAIMER ════════ -->
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -497,6 +497,27 @@ table, td { color: #1B2E24; } #u_body a { color: #00C853; text-decoration: under
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
|
||||||
|
<!-- ════════ WRAPPER EXPIRY BADGE (auto-derived) ════════
|
||||||
|
Visible juste sous le bouton CTA pour que le destinataire sache
|
||||||
|
qu'il a un délai limité. {{expires_at_date}} est calculé à
|
||||||
|
partir de gift_expires_at (sent_at + gift_expiry_days) — il
|
||||||
|
tombe à vide si la campagne n'a pas de wrapper, auquel cas
|
||||||
|
la section Mustache {{#expires_at_date}}…{{/expires_at_date}}
|
||||||
|
collapse silencieusement. -->
|
||||||
|
{{#expires_at_date}}
|
||||||
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||||
|
<tbody><tr><td style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;direction:ltr;font-size:0px;padding:0 36px 4px;text-align:center;">
|
||||||
|
<div style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.4;text-align:center;color:#64748B;">
|
||||||
|
⏰ Cadeau valide jusqu'au <strong style="color:#1B2E24;">{{expires_at_date}}</strong>
|
||||||
|
</div>
|
||||||
|
</td></tr></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
{{/expires_at_date}}
|
||||||
|
|
||||||
<!-- ════════ PRORATA REFUND DISCLAIMER ════════ -->
|
<!-- ════════ PRORATA REFUND DISCLAIMER ════════ -->
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -948,6 +948,20 @@ async function sendCampaignAsync (id) {
|
||||||
r.gift_redirected_count = 0
|
r.gift_redirected_count = 0
|
||||||
tokenIndex.set(r.gift_token, { campaign_id: id, row: i })
|
tokenIndex.set(r.gift_token, { campaign_id: id, row: i })
|
||||||
}
|
}
|
||||||
|
// Format the wrapper's own expiry date for display in the email body.
|
||||||
|
// Locale matches the recipient's language so "May 31, 2026" / "31 mai 2026"
|
||||||
|
// appear naturally. expires_in_days is a friendlier shorter format for
|
||||||
|
// tight deadlines (≤ 60 days). The two are intentionally redundant
|
||||||
|
// so the template author can pick the one that fits their layout.
|
||||||
|
const expiryLocale = lang === 'en' ? 'en-CA' : 'fr-CA'
|
||||||
|
let expiresAtDate = ''
|
||||||
|
let expiresInDays = ''
|
||||||
|
if (r.gift_expires_at) {
|
||||||
|
const exp = new Date(r.gift_expires_at)
|
||||||
|
expiresAtDate = exp.toLocaleDateString(expiryLocale, { day: 'numeric', month: 'long', year: 'numeric' })
|
||||||
|
const days = Math.max(0, Math.ceil((exp - new Date()) / 86400000))
|
||||||
|
expiresInDays = String(days)
|
||||||
|
}
|
||||||
const vars = {
|
const vars = {
|
||||||
firstname: r.firstname || (lang === 'en' ? 'dear customer' : 'cher client'),
|
firstname: r.firstname || (lang === 'en' ? 'dear customer' : 'cher client'),
|
||||||
lastname: r.lastname || '',
|
lastname: r.lastname || '',
|
||||||
|
|
@ -958,6 +972,11 @@ async function sendCampaignAsync (id) {
|
||||||
gift_url: r.gift_token ? wrapperUrl(r.gift_token) : r.gift_url,
|
gift_url: r.gift_token ? wrapperUrl(r.gift_token) : r.gift_url,
|
||||||
amount: displayAmount,
|
amount: displayAmount,
|
||||||
expiry: p.expiry || '',
|
expiry: p.expiry || '',
|
||||||
|
// Auto-derived from the wrapper's gift_expires_at — distinct from
|
||||||
|
// the manual {{expiry}} field above which is for promotion-end dates
|
||||||
|
// unrelated to the gift link wrapper.
|
||||||
|
expires_at_date: expiresAtDate,
|
||||||
|
expires_in_days: expiresInDays,
|
||||||
commitment_months: p.commitment_months || '3',
|
commitment_months: p.commitment_months || '3',
|
||||||
year: new Date().getFullYear(),
|
year: new Date().getFullYear(),
|
||||||
view_url: viewUrl,
|
view_url: viewUrl,
|
||||||
|
|
@ -1764,6 +1783,17 @@ async function handle (req, res, method, path) {
|
||||||
? `${cents / 100} $`
|
? `${cents / 100} $`
|
||||||
: `${(cents / 100).toFixed(2)} $`
|
: `${(cents / 100).toFixed(2)} $`
|
||||||
}
|
}
|
||||||
|
// Locale-formatted wrapper expiry — same logic as the worker so the
|
||||||
|
// /view fallback renders the exact same date the recipient saw at first.
|
||||||
|
const expiryLocale = lang === 'en' ? 'en-CA' : 'fr-CA'
|
||||||
|
let expiresAtDate = ''
|
||||||
|
let expiresInDays = ''
|
||||||
|
if (r.gift_expires_at) {
|
||||||
|
const exp = new Date(r.gift_expires_at)
|
||||||
|
expiresAtDate = exp.toLocaleDateString(expiryLocale, { day: 'numeric', month: 'long', year: 'numeric' })
|
||||||
|
const days = Math.max(0, Math.ceil((exp - new Date()) / 86400000))
|
||||||
|
expiresInDays = String(days)
|
||||||
|
}
|
||||||
const vars = {
|
const vars = {
|
||||||
firstname: r.firstname || (lang === 'en' ? 'dear customer' : 'cher client'),
|
firstname: r.firstname || (lang === 'en' ? 'dear customer' : 'cher client'),
|
||||||
lastname: r.lastname || '',
|
lastname: r.lastname || '',
|
||||||
|
|
@ -1775,6 +1805,8 @@ async function handle (req, res, method, path) {
|
||||||
gift_url: r.gift_token ? wrapperUrl(r.gift_token) : r.gift_url,
|
gift_url: r.gift_token ? wrapperUrl(r.gift_token) : r.gift_url,
|
||||||
amount: displayAmount,
|
amount: displayAmount,
|
||||||
expiry: p.expiry || '',
|
expiry: p.expiry || '',
|
||||||
|
expires_at_date: expiresAtDate,
|
||||||
|
expires_in_days: expiresInDays,
|
||||||
commitment_months: p.commitment_months || '3',
|
commitment_months: p.commitment_months || '3',
|
||||||
year: new Date().getFullYear(),
|
year: new Date().getFullYear(),
|
||||||
// Empty so the {{#view_url}} section block in the template collapses
|
// Empty so the {{#view_url}} section block in the template collapses
|
||||||
|
|
|
||||||
|
|
@ -493,6 +493,21 @@ table, td { color: #1B2E24; } #u_body a { color: #00C853; text-decoration: under
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
|
||||||
|
<!-- ════════ WRAPPER EXPIRY BADGE (auto-derived) ════════ -->
|
||||||
|
{{#expires_at_date}}
|
||||||
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||||
|
<tbody><tr><td style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;direction:ltr;font-size:0px;padding:0 36px 4px;text-align:center;">
|
||||||
|
<div style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.4;text-align:center;color:#64748B;">
|
||||||
|
⏰ Gift valid until <strong style="color:#1B2E24;">{{expires_at_date}}</strong>
|
||||||
|
</div>
|
||||||
|
</td></tr></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
{{/expires_at_date}}
|
||||||
|
|
||||||
<!-- ════════ PRORATA REFUND DISCLAIMER ════════ -->
|
<!-- ════════ PRORATA REFUND DISCLAIMER ════════ -->
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -497,6 +497,27 @@ table, td { color: #1B2E24; } #u_body a { color: #00C853; text-decoration: under
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
|
||||||
|
<!-- ════════ WRAPPER EXPIRY BADGE (auto-derived) ════════
|
||||||
|
Visible juste sous le bouton CTA pour que le destinataire sache
|
||||||
|
qu'il a un délai limité. {{expires_at_date}} est calculé à
|
||||||
|
partir de gift_expires_at (sent_at + gift_expiry_days) — il
|
||||||
|
tombe à vide si la campagne n'a pas de wrapper, auquel cas
|
||||||
|
la section Mustache {{#expires_at_date}}…{{/expires_at_date}}
|
||||||
|
collapse silencieusement. -->
|
||||||
|
{{#expires_at_date}}
|
||||||
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||||
|
<tbody><tr><td style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;direction:ltr;font-size:0px;padding:0 36px 4px;text-align:center;">
|
||||||
|
<div style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.4;text-align:center;color:#64748B;">
|
||||||
|
⏰ Cadeau valide jusqu'au <strong style="color:#1B2E24;">{{expires_at_date}}</strong>
|
||||||
|
</div>
|
||||||
|
</td></tr></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
{{/expires_at_date}}
|
||||||
|
|
||||||
<!-- ════════ PRORATA REFUND DISCLAIMER ════════ -->
|
<!-- ════════ PRORATA REFUND DISCLAIMER ════════ -->
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user