Add custom RealUnit mail template#3598
Conversation
Replace the generic user-v2 template with a dedicated RealUnit-branded template featuring light background, RealUnit logo, blue accent color, formal language, RealUnit social links, and legal footer.
Introduce wallet-aware translations in MailFactory with automatic
fallback: tries mail-{wallet}.json namespace first, falls back to
default mail.json. Includes complete German translations for RealUnit
with formal language, custom subjects, and RealUnit-specific content.
Suppresses DFX_TEAM_CLOSING for wallets with custom templates.
- Add wallet body text injection: factory auto-derives .body key from title key and prepends it to texts array if wallet translation exists - Filter empty text blocks from rendered mail affix array - Add body texts for crypto_output, fiat_output, processing, chargeback - Set general.support/thanks/link/transaction_button to empty (template handles closing) to avoid duplicate content - Fix template closing: "Freundliche Grüsse / RealUnit Schweiz AG" - Fix template support: info@realunit.ch + DFX tech support link - Update merge_incomplete text per Textvorlagen (#9)
ab46a5f to
ab6245f
Compare
RealUnit customers must never receive links to DFX pages. All links must be deeplinks to the RealUnit App. Until app deeplinks for support are implemented, route all inquiries through info@realunit.ch.
Add all missing translation keys that would fall back to informal DFX texts. Follows the Textvorlagen author's principles throughout: - RealUnit is the brand, DFX only mentioned where regulatory required - Product-specific language (RealUnit-Token, RealUnit-Aktientoken) - RealUnit App as the central touchpoint - Formal Sie throughout, no informal du anywhere New keys added: - All chargeback reasons (30+ keys) in formal language - All pending phone/olky/bank_tx variants with RealUnit context - Referral/Recommendation adapted to RealUnit App - KYC success: mentions RealUnit-Token trading capability - KYC reminder/missing_data: references RealUnit App - Account deactivation: explains token remain in wallet - Support message: references RealUnit App instead of URL - Fiat input titles: "Kauf RealUnit-Token" prefix - currency_exchange, kyc_start, payment_link in formal language Only intentionally missing: dfx_team_closing (suppressed by factory), black_squad (DFX-exclusive, not applicable to RealUnit).
Remove payment_data (Zession) and phone check pending variants - these features are not used in the RealUnit App context.
Add scripts/generate-realunit-previews.js alongside the existing DFX preview generator. Uses wallet-aware translations with fallback, same as production. Output goes to scripts/email-previews/realunit/ (gitignored). Usage: node scripts/generate-realunit-previews.js
- Remove hardcoded support contact block from realunit.hbs (in-app support only per RealUnit AG) - Update verification_code closing to reference in-app support - Add "App öffnen" button linking to https://app.realunit.ch/open in every mail - Add automated-mail disclaimer above the legal footer
- Add 38 customer-facing trigger explanations (when the mail fires) plus the technical MailContext and DB/state condition that fires it - Render a yellow info box above each mail in the standalone HTML; hide it inside iframes (via window.parent check) so index card previews show only the mail - Add the customer-facing "when" text as a third line on each index card
…o-to-crypto wording (not applicable for RealUnit)
…tus, KYC, login, verification, ref-reward and support mails
…d-tx button with body text - mail.factory: keep the welcome line first when prepending wallet-specific body overrides (welcome → body → rest) - realunit.hbs: drop the large standalone salutation block; render salutation small/bold inline right after the welcome text - mail-realunit.json: add body text for fiat_input.unassigned, clear transaction_button so the dedicated 'Klick hier' button is dropped
- Reorganize categories: drop 'Eingang', map fiat-input/currency-exchange/unassigned-tx to Kauf and crypto-input to Verkauf - Hide trigger details behind a clickable ? icon (collapsed by default; auto-hidden inside index iframes) - Add 'Betreff' section to the trigger info box - Rewrite all 38 trigger 'when' texts in technical, non-personal voice (no Sie/Ihr/Ihnen) - Wipe output dir before regenerating to avoid stale files when ordering changes
…it currency-exchange notice
- payment.fiat_input.body: confirmation that the bank transfer is being processed and the customer will be informed once tokens are in the wallet - payment.crypto_input.body: confirmation that the tokens have arrived in the sell flow and the customer will be informed once the bank payout is done. Also fix mismatched title and salutation (was 'Kauf...', now 'Verkauf RealUnit-Token – Eingang erhalten') - payment.chargeback.unconfirmed.body: hint that the refund is processed and customer informed once the action is taken - payment.pending.bank_release_pending.line3: hint that the buy is processed and customer informed once the bank releases the transaction - preview script: render the new body texts in the corresponding previews
For RealUnit-wallet customers DFX handles these cases by phone instead of email: - pending mails for amlReason in [monthly_limit, annual_limit, annual_limit_without_kyc, high_risk_kyc_needed, kyc_data_needed, name_check_without_kyc, asset_kyc_needed, bank_release_pending, olky_no_kyc, bank_tx_needed] - chargeback mails (BUY_*_RETURN) - chargeback-unconfirmed mails (BUY_*_CHARGEBACK_UNCONFIRMED) - limit-request approval mails Manual_check, video_ident_needed and merge_incomplete pending mails remain enabled because they are part of regular flows that require customer action.
mail-realunit.json: - sell-completed body: mention 1-2 banking days for the incoming transfer; explain the bank statement sender will read 'DFX AG, Payment Partner of RealUnit' - currency_exchange: drop the IBAN placeholder, keep it generic preview script: reflect the opt-out — drop the 9 disabled pending variants, all 3 chargeback mails and the limit-request mail; remove the now-empty 'Rückerstattung' category
…Mail-Adresse spelling, add 'next step' hints - pending.merge_incomplete: spell 'E-Mail-Adresse' consistently with hyphen (was mixed with 'E-Mailadresse') - payment.fiat_input.body: drop 'Ihre Banküberweisung ist eingegangen und wird verarbeitet' (already in salutation), keep only the next-step hint - payment.crypto_input.body: same — drop the duplicate 'sind eingetroffen und werden verarbeitet' - login.salutation: replace duplicate 'RealUnit Login' headline with the more meaningful 'Sie haben einen Login angefordert' - payment.fiat_input.unassigned.body: append 'Sobald die Zuordnung erfolgt ist, werden Sie wieder per Mail informiert' - account_merge.request.message: append 'Nach der Bestätigung werden Ihre Accounts zusammengelegt' - kyc.failed.message: append 'Bitte wiederholen Sie den Schritt. Sobald die Verifizierung erfolgreich abgeschlossen ist, werden Sie wieder per Mail informiert'
- MailFactory.createUserV2Mail and createPersonalMail now prepend the personal welcome line ('Guten Tag {name}') as the first body element. Removes the same 28 welcome+SPACE blocks scattered across 14 service files (single source of truth)
- WalletMailConfig gains a 'forcedLang' field. When set, every UserMailV2 for that wallet renders in this language regardless of userData.language
- RealUnit wallet config gets forcedLang='de' so RealUnit mails always render in German even for EN/FR/IT customers
- account-merge.service.ts: drop the unused 'name' computation that was only used for the now-centralized welcome (the receiver of the mail is now greeted instead of the mentioned account)
The previous commit (911ae1b) included 4998 files from .claude/worktrees/ because git add -A picked them up. Add .claude/ to .gitignore and untrack everything under it. The previous commit's intended Mail-Factory and forced-language changes remain — only the worktree noise is removed.
ℹ️ New TODOs/FIXMEs (20)+// TODO: Re-enable when EIP-7702 delegation is reactivated
+ * TODO: Re-enable once Pimlico integration is complete.
+ // TODO: Implement dynamic gas estimation once relayer has sufficient balance for simulation
+ // TODO: Hier habe ich nur mal angenommen, dass es die "txId" ist ...
+// TODO: activate
+ // TODO BFS Cluster Level
+ // TODO: switch back
+// TODO: remove
+ minDeposit: { amount: minVolume, asset: dto.currency.name }, // TODO: remove
+ // TODO: remove
+ // TODO wait for guaranteed prices PR
+ // TODO: Implement bridge out (dEURO → EUR stablecoins)
+ // TODO: Implement bridge out (JUSD → USD stablecoins)
+ // TODO add fiatFiatUpdate here
+ // TODO: remove
+ // TODO: remove
+ // TODO Dilisense JSON solution
+ (log) => allowedLevels.some((l) => log.comment.includes(l)) || log.comment === 'Verified', // TODO: remove compatibility code
+ // TODO: temporary code to update empty signatures (remove?)
+ // TODO: find PDF result |
- New src/subdomains/supporting/notification/realunit-mail-rules.ts exports REALUNIT_WALLET_NAME and REALUNIT_DISABLED_PENDING_REASONS - Replace the duplicate inline RealUnitDisabledPendingReasons const in buy-crypto-notification and buy-fiat-notification with the shared list - Replace the magic string 'RealUnit' (7 occurrences across buy-crypto/buy-fiat/limit-request) with the REALUNIT_WALLET_NAME constant - Drop the leftover ' SPACE 3' filler lines in user-data-notification mails that hung in front of the (now-centralized) welcome line
davidleomay
left a comment
There was a problem hiding this comment.
Missing from issue #3654
-
walletNameparameter threading through the translation chain —translate(),translateParams(),getMailAffix(),mapMailAffix()all gotwalletNameadded as a parameter that's passed down 4 levels deep. This should be refactored i
nto a translation context/strategy object that's resolved once at the top ofcreateUserV2Mail, rather than threaded throu
gh every method call. F1 partially addresses the notification service side, but the MailFactory internal plumbing is still
messy. -
The
.filter((a) => a.text || a.url || a.mail)added togetMailAffix— this filters out empty lines (DefaultEmpt yLinehastext: ''), which changes spacing behavior for ALL wallets, not just RealUnit. That's a regression risk for exi
sting DFX mails.
The trailing .filter((a) => a.text || a.url || a.mail) in getMailAffix dropped any MailAffix with text:'' — including DefaultEmptyLine which is exactly what every MailKey.SPACE gets rendered into. As a result every SPACE block (used heavily by all DFX default mails for vertical padding) would have disappeared. Move the empty-translation guard into the default branch of mapMailAffix where it belongs: only skip when the translated text is empty AND there is no special tag — that way an explicitly cleared transaction_button (e.g. RealUnit fiat_input.unassigned) is dropped, but DefaultEmptyLine for SPACE keeps coming through. Caught by davidleomay's PR review (#3598 (review)).
TaprootFreak
left a comment
There was a problem hiding this comment.
Beide Punkte adressiert:
-
Filter-Regression-Fix gepusht als f73b36e: Der
.filter((a) => a.text || a.url || a.mail)ist ausgetMailAffixraus. Stattdessen filtertmapMailAffixim default-Branch leere Translations früh weg (if (!text && !specialTag) return [];).DefaultEmptyLinebleibt durchlässig, leere RealUnit-Overrides (z. B.payment.fiat_input.unassigned.transaction_button: '') werden korrekt übersprungen — kein Layout-Impact für DFX-Default-Mails. -
walletName-Threading als F9 in #3654 ergänzt — gehört in die Folge-PR (Translation-Context-Objekt statt Parameter durch 4 Ebenen).
Danke für die Review!
…ce files User clarification: the only mail-related changes that should land in this PR are RealUnit-specific. Any change that affects DFX default mails was unintended and must be undone. - WalletMailConfig.centralizedWelcome flag introduced; MailFactory.createUserV2Mail and createPersonalMail now only prepend the welcome line when the wallet opts in (currently RealUnit only) - Config.mail.wallet.RealUnit gets centralizedWelcome: true (alongside template + forcedLang) - All previously-touched DFX-default service files reverted to merge-base state — no welcome inserts added, no SPACE-3 fillers removed: ref-reward-notification, kyc-notification, tfa, auth, account-merge, recommendation, user-data-notification, bank-tx-return-notification, payin-notification, transaction-notification, support-issue-notification - limit-request-notification: keep the existing manual welcome line in prefix; only the RealUnit opt-out filter remains as a real change
Was: any wallet with a custom template skipped the DFX_TEAM_CLOSING block, which silently changed the closing for onchainlabs (an existing custom-template wallet) compared to develop. Now: only wallets that opt in to RealUnit-style branding via centralizedWelcome=true skip the DFX closing. onchainlabs keeps the unchanged pre-PR behavior.
… it globally Was: any empty translation result without a special tag was dropped in mapMailAffix's default branch — that would silently change DFX-default rendering if a translation key ever resolved to an empty string for a non-RealUnit wallet. Now: skip only for wallets that opt in to RealUnit-style branding (centralizedWelcome=true). DFX-default mails keep the unchanged pre-PR behavior of rendering an empty <p> block.
The same 8-line welcomeTexts construction was duplicated between createUserV2Mail and createPersonalMail. Pull it out into MailFactory.getCentralizedWelcomeTexts so both call sites stay in sync.
Summary
Wallet-spezifisches Mail-Template für die RealUnit App auf Basis der DAS-Textvorlagen v1.0 (1.4.26).
realunit.hbsmit RealUnit-Branding (heller Hintergrund#F5F7F8, Logo, Sie-Form, Closing „RealUnit Schweiz AG", Footer mit Adresse, Datenschutz, Impressum, YouTube + LinkedIn)mail-realunit.json(Overridemail.X→mail-realunit.X, Fallback auf DFX-Default)MailFactory: leitet.body-Key vom Title-Key ab und prepended ihn vor die regulären TextsConfig.mail.wallet.RealUnit.template = 'realUnit'inconfig.tshttps://app.realunit.ch/openin jeder Mailinfo@realunit.ch-Kontakt — Support läuft ausschliesslich über die In-App-Support-Sektion (RealUnit AG Decision)scripts/generate-realunit-previews.jsgeneriert 38 HTML-Mails + Index nachscripts/email-previews/realunit/Slot-Umwidmung #17 (DAS-spezifisch)
DAS hat im Original-Doc drei Slots als „Guthaben wurde erstattet" betitelt (#12, #15, #17). Der Slot-Titel stammt aus der DFX-Default-Liste, aber DAS hat die jeweiligen Betreffe bewusst überschrieben:
BUY_FIAT_RETURN(limit)BUY_FIAT_RETURN(unavailable)BUY_FIAT_COMPLETED← Slot-Umwidmung!DAS hat absichtlich den DFX-„Guthaben erstattet"-Slot für eine Verkauf-erfolgreich-Mail umgewidmet. Deshalb hat
payment.fiat_output.bodyeinen DAS-spezifischen Text (Bestätigung der Banküberweisung) statt den DFX-Default-Wortlaut.Korrekturen gegenüber DAS-Doc v1.0
wir→wird,Verizierung→Verifizierung,erfolgreicht→erfolgreich,Verkaufauftrag→Verkaufsauftrag,Möglich→möglich, fehlendesdain Kraken exchange service #12 ergänztBewusst NICHT umgesetzt
#13 „Aktion ausstehend" und #6 „Problem bei der Einzahlung" wurden NICHT hart deaktiviert, obwohl DAS sie infrage gestellt hatte. Begründung:
BUY_*_CHARGEBACK_UNCONFIRMED): Trigger feuert im RealUnit-Setup (verifizierte Kunden-IBAN, SEPA/SIC) faktisch nichtpayment.chargeback.unconfirmed); manuelle Eskalation läuft über In-App-SupportEine technische Deaktivierung würde eine Schema-Erweiterung der Wallet-Entity erfordern (
disabledMailTypesoperiert nur auf groberMailContextType-Ebene, nicht auf einzelnenMailContext-Werten). Falls real störend, wird das in einem separaten PR nachgezogen.Offene Klärungen mit DAS
https://app.realunit.ch/openist gesetzt — RealUnit App-Team muss Universal-Link-Konfiguration (apple-app-site-association,assetlinks.json) aufsetzenServer-Konfig erforderlich
Auf DEV + PRD müssen
REALUNIT_MAIL_USERundREALUNIT_MAIL_PASSgesetzt sein, damitConfig.mail.wallet.RealUnitaktiv wird.Test plan
node scripts/generate-realunit-previews.jsund alle 38 Preview-HTMLs visuell prüfenuser-v2-Templateinfo@realunit.chodermailto:info@realunit.chin den Preview-HTMLs (grep -r "info@realunit.ch" scripts/email-previews/realunit/)