0df05c83c6
* fix(email): calm, correctly-signed consent expiry notification The consent expiry email was signed with the recipient's own company name instead of the app, used red alarm chrome (header pill + button), and never said why the recipient got it. After the #1271 health probe drained a backlog of 25 dead sessions in one 05:00 cron run, that design read as phishing to a batch of users at once. - Sign off as the app; the company the connection belongs to moves into a details row and the why-did-I-get-this footer - Drop all red/orange chrome; neutral editorial layout, pill button - Explain that PSD2 consent expiry is routine and that no data is lost - Show the destination URL as plain text next to the button - Calmer subjects (renewal framing instead of 'synkronisering stoppad') - Reply-to support instead of dead-ending at noreply - Add template tests (was untested) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(enable-banking): pause consent expiry emails behind env flag Founder call 2026-07-29: the in-app surfaces already flag a dead connection, so the cron email adds noise. Status transitions keep running; set BANK_CONSENT_EXPIRY_EMAILS=true to resume sending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
163 lines
7.0 KiB
TypeScript
163 lines
7.0 KiB
TypeScript
import { getBranding } from '@/lib/branding/service'
|
|
|
|
export interface ConsentExpiryEmailData {
|
|
bankName: string
|
|
daysUntilExpiry: number
|
|
renewalUrl: string
|
|
/** The company the bank connection belongs to. Shown so the recipient knows
|
|
* which of their companies the email concerns; never used as the sender. */
|
|
companyName: string
|
|
isExpired: boolean
|
|
}
|
|
|
|
/**
|
|
* Consent expiry notification emails.
|
|
*
|
|
* Tone and layout are deliberately calm: a PSD2 consent running out is
|
|
* routine, not an incident. No red, no urgency chrome; the email is signed
|
|
* by the app (never the recipient's own company), states plainly why the
|
|
* recipient got it, and shows the destination URL in plain text next to the
|
|
* button so it does not pattern-match phishing.
|
|
*/
|
|
|
|
const SERIF = `Georgia, 'Times New Roman', serif`
|
|
const SANS = `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif`
|
|
|
|
function dagar(n: number): string {
|
|
return `${n} ${n === 1 ? 'dag' : 'dagar'}`
|
|
}
|
|
|
|
/**
|
|
* Generate HTML email for consent expiry notification
|
|
*/
|
|
export function generateConsentExpiryEmailHtml(data: ConsentExpiryEmailData): string {
|
|
const { bankName, daysUntilExpiry, renewalUrl, companyName, isExpired } = data
|
|
const { appName, supportEmail } = getBranding()
|
|
|
|
const title = isExpired
|
|
? 'Bankkopplingen behöver förnyas'
|
|
: `Bankkopplingen löper ut om ${dagar(daysUntilExpiry)}`
|
|
|
|
const intro = isExpired
|
|
? `Banksamtycket för <strong>${bankName}</strong> har löpt ut och den automatiska hämtningen av nya transaktioner är pausad.`
|
|
: `Banksamtycket för <strong>${bankName}</strong> löper ut om ${dagar(daysUntilExpiry)}.`
|
|
|
|
const explanation =
|
|
'Det här är väntat: av säkerhetsskäl gäller ett banksamtycke (PSD2) bara en begränsad tid, och därefter behöver det godkännas på nytt hos banken.'
|
|
|
|
const consequence = isExpired
|
|
? 'Ingenting har försvunnit. Redan hämtade transaktioner och din bokföring påverkas inte, och när kopplingen är förnyad hämtas mellanliggande transaktioner ikapp.'
|
|
: 'Förnya gärna i förväg så fortsätter transaktionerna att hämtas utan avbrott. Din bokföring påverkas inte.'
|
|
|
|
return `
|
|
<!DOCTYPE html>
|
|
<html lang="sv">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${title}</title>
|
|
</head>
|
|
<body style="margin: 0; padding: 0; font-family: ${SANS}; line-height: 1.6; color: #374151; background-color: #f5f4f1;">
|
|
<div style="max-width: 560px; margin: 0 auto; padding: 40px 20px;">
|
|
<div style="background: #ffffff; border: 1px solid #e7e5e0; border-radius: 12px; padding: 40px;">
|
|
|
|
<div style="font-family: ${SERIF}; font-size: 19px; color: #111111; margin-bottom: 28px;">
|
|
${appName}
|
|
</div>
|
|
|
|
<h1 style="margin: 0 0 16px 0; font-family: ${SERIF}; font-size: 23px; font-weight: 400; color: #111111; line-height: 1.3;">
|
|
${title}
|
|
</h1>
|
|
|
|
<p style="margin: 0 0 14px 0; font-size: 15px;">${intro}</p>
|
|
<p style="margin: 0 0 14px 0; font-size: 15px;">${explanation}</p>
|
|
<p style="margin: 0 0 28px 0; font-size: 15px;">${consequence}</p>
|
|
|
|
<table role="presentation" style="width: 100%; border-collapse: collapse; margin-bottom: 28px; font-size: 14px;">
|
|
<tr>
|
|
<td style="padding: 8px 0; border-top: 1px solid #ececea; color: #9ca3af; width: 90px;">Bank</td>
|
|
<td style="padding: 8px 0; border-top: 1px solid #ececea; color: #111111;">${bankName}</td>
|
|
</tr>
|
|
${companyName ? `
|
|
<tr>
|
|
<td style="padding: 8px 0; border-top: 1px solid #ececea; border-bottom: 1px solid #ececea; color: #9ca3af;">Företag</td>
|
|
<td style="padding: 8px 0; border-top: 1px solid #ececea; border-bottom: 1px solid #ececea; color: #111111;">${companyName}</td>
|
|
</tr>
|
|
` : ''}
|
|
</table>
|
|
|
|
<div style="margin-bottom: 12px;">
|
|
<a href="${renewalUrl}" style="display: inline-block; background: #1a1a1a; color: #ffffff; padding: 12px 26px; border-radius: 99px; text-decoration: none; font-weight: 500; font-size: 14px;">
|
|
Förnya bankkopplingen
|
|
</a>
|
|
</div>
|
|
|
|
<p style="margin: 0 0 32px 0; font-size: 13px; color: #9ca3af;">
|
|
Knappen leder till ${renewalUrl}.<br>
|
|
Du kan också logga in som vanligt och gå till Inställningar och sedan Bank.
|
|
</p>
|
|
|
|
<div style="padding-top: 20px; border-top: 1px solid #ececea;">
|
|
<p style="margin: 0 0 12px 0; font-size: 14px; color: #374151;">
|
|
Med vänliga hälsningar,<br>
|
|
<strong>${appName}</strong>
|
|
</p>
|
|
<p style="margin: 0; font-size: 12.5px; color: #9ca3af;">
|
|
Du får det här mejlet eftersom det finns en bankkoppling i ${appName}${companyName ? ` för ${companyName}` : ''}.
|
|
Undrar du något? Mejla <a href="mailto:${supportEmail}" style="color: #6b7280;">${supportEmail}</a>.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`
|
|
}
|
|
|
|
/**
|
|
* Generate plain text email for consent expiry notification
|
|
*/
|
|
export function generateConsentExpiryEmailText(data: ConsentExpiryEmailData): string {
|
|
const { bankName, daysUntilExpiry, renewalUrl, companyName, isExpired } = data
|
|
const { appName, supportEmail } = getBranding()
|
|
|
|
let text = ''
|
|
|
|
if (isExpired) {
|
|
text += `Bankkopplingen behöver förnyas\n\n`
|
|
text += `Banksamtycket för ${bankName} har löpt ut och den automatiska hämtningen av nya transaktioner är pausad.\n\n`
|
|
} else {
|
|
text += `Bankkopplingen löper ut om ${dagar(daysUntilExpiry)}\n\n`
|
|
text += `Banksamtycket för ${bankName} löper ut om ${dagar(daysUntilExpiry)}.\n\n`
|
|
}
|
|
|
|
text += `Det här är väntat: av säkerhetsskäl gäller ett banksamtycke (PSD2) bara en begränsad tid, och därefter behöver det godkännas på nytt hos banken.\n\n`
|
|
|
|
if (isExpired) {
|
|
text += `Ingenting har försvunnit. Redan hämtade transaktioner och din bokföring påverkas inte, och när kopplingen är förnyad hämtas mellanliggande transaktioner ikapp.\n\n`
|
|
} else {
|
|
text += `Förnya gärna i förväg så fortsätter transaktionerna att hämtas utan avbrott. Din bokföring påverkas inte.\n\n`
|
|
}
|
|
|
|
text += `Bank: ${bankName}\n`
|
|
if (companyName) text += `Företag: ${companyName}\n`
|
|
text += `\nFörnya bankkopplingen: ${renewalUrl}\n`
|
|
text += `Du kan också logga in som vanligt och gå till Inställningar och sedan Bank.\n\n`
|
|
text += `Med vänliga hälsningar,\n`
|
|
text += `${appName}\n\n`
|
|
text += `Du får det här mejlet eftersom det finns en bankkoppling i ${appName}${companyName ? ` för ${companyName}` : ''}. Undrar du något? Mejla ${supportEmail}.\n`
|
|
|
|
return text
|
|
}
|
|
|
|
/**
|
|
* Generate email subject for consent expiry notification
|
|
*/
|
|
export function generateConsentExpiryEmailSubject(data: ConsentExpiryEmailData): string {
|
|
const suffix = data.companyName ? ` - ${data.companyName}` : ''
|
|
if (data.isExpired) {
|
|
return `Förnya bankkopplingen till ${data.bankName}${suffix}`
|
|
}
|
|
return `Bankkopplingen till ${data.bankName} löper ut om ${dagar(data.daysUntilExpiry)}${suffix}`
|
|
}
|