Skip to content

Add custom RealUnit mail template#3598

Merged
TaprootFreak merged 31 commits intodevelopfrom
feat/realunit-mail-template
Apr 30, 2026
Merged

Add custom RealUnit mail template#3598
TaprootFreak merged 31 commits intodevelopfrom
feat/realunit-mail-template

Conversation

@TaprootFreak
Copy link
Copy Markdown
Collaborator

@TaprootFreak TaprootFreak commented Apr 22, 2026

Summary

Wallet-spezifisches Mail-Template für die RealUnit App auf Basis der DAS-Textvorlagen v1.0 (1.4.26).

  • Neuer realunit.hbs mit RealUnit-Branding (heller Hintergrund #F5F7F8, Logo, Sie-Form, Closing „RealUnit Schweiz AG", Footer mit Adresse, Datenschutz, Impressum, YouTube + LinkedIn)
  • Wallet-aware Translation-System mit mail-realunit.json (Override mail.Xmail-realunit.X, Fallback auf DFX-Default)
  • Body-Text-Injection in MailFactory: leitet .body-Key vom Title-Key ab und prepended ihn vor die regulären Texts
  • Config.mail.wallet.RealUnit.template = 'realUnit' in config.ts
  • "App öffnen"-Button auf https://app.realunit.ch/open in jeder Mail
  • Automatisierte-Mail-Disclaimer über dem Footer
  • Kein info@realunit.ch-Kontakt — Support läuft ausschliesslich über die In-App-Support-Sektion (RealUnit AG Decision)
  • Security/Fraud-Warnings deaktiviert (RealUnit AG Decision)
  • Externe DFX-Support-URL entfernt
  • Preview-Script scripts/generate-realunit-previews.js generiert 38 HTML-Mails + Index nach scripts/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:

# Original-Slot-Titel DAS-Betreff DFX-Trigger
12 Guthaben wurde erstattet „Verkauf RealUnit-Token – Rückerstattung" BUY_FIAT_RETURN (limit)
15 Guthaben wurde erstattet „Verkauf RealUnit-Token aktuell nicht möglich" BUY_FIAT_RETURN (unavailable)
17 Guthaben wurde erstattet „Verkauf RealUnit-Token – Überweisung erfolgt" 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.body einen DAS-spezifischen Text (Bestätigung der Banküberweisung) statt den DFX-Default-Wortlaut.

Korrekturen gegenüber DAS-Doc v1.0

  • Tippfehler-Fixes: wirwird, VerizierungVerifizierung, erfolgreichterfolgreich, VerkaufauftragVerkaufsauftrag, Möglichmöglich, fehlendes da in Kraken exchange service #12 ergänzt
  • „Aktientoken"/"Token"-Differenzierung wie im Original (Aktientoken in Erst-Erwähnung, Token im Verkaufs-Flow)

Bewusst NICHT umgesetzt

#13 „Aktion ausstehend" und #6 „Problem bei der Einzahlung" wurden NICHT hart deaktiviert, obwohl DAS sie infrage gestellt hatte. Begründung:

  • Feature/kyc mail #13 (BUY_*_CHARGEBACK_UNCONFIRMED): Trigger feuert im RealUnit-Setup (verifizierte Kunden-IBAN, SEPA/SIC) faktisch nicht
  • Hotfix/kyc check #6 („Problem bei Einzahlung"): DAS' eigene Annahme — „Mail sollte ja gar nicht zur Anwendung kommen, da erst nach vollständigem KYC der Kunde Token kaufen kann" — bestätigt sich
  • Falls Edge-Cases doch auftreten, geht eine Mail mit RealUnit-Branding raus (Override existiert für payment.chargeback.unconfirmed); manuelle Eskalation läuft über In-App-Support

Eine technische Deaktivierung würde eine Schema-Erweiterung der Wallet-Entity erfordern (disabledMailTypes operiert nur auf grober MailContextType-Ebene, nicht auf einzelnen MailContext-Werten). Falls real störend, wird das in einem separaten PR nachgezogen.

Offene Klärungen mit DAS

  • App-Öffnen-URL https://app.realunit.ch/open ist gesetzt — RealUnit App-Team muss Universal-Link-Konfiguration (apple-app-site-association, assetlinks.json) aufsetzen
  • DAS-Doc v1.0 endet mit „Prüfen ob vorhanden:" — angefangene Liste; offen welche zusätzlichen Templates DAS ergänzen wollte (Login, Account-Deactivation, Limit-Request, Recommendation sind bereits abgedeckt)

Server-Konfig erforderlich

Auf DEV + PRD müssen REALUNIT_MAIL_USER und REALUNIT_MAIL_PASS gesetzt sein, damit Config.mail.wallet.RealUnit aktiv wird.

Test plan

  • Lokal: node scripts/generate-realunit-previews.js und alle 38 Preview-HTMLs visuell prüfen
  • DEV-Smoke-Test: RealUnit-Testuser durch Kauf-, Verkaufs-, KYC- und Login-Flow → Mails kommen mit RealUnit-Branding und Sie-Form an
  • DEV-Test: andere Wallets (z. B. DFX) erhalten weiterhin das user-v2-Template
  • Outlook/MSO-Rendering der Buttons (App öffnen, Verifizierung) prüfen
  • Stichprobe: kein Treffer für info@realunit.ch oder mailto:info@realunit.ch in den Preview-HTMLs (grep -r "info@realunit.ch" scripts/email-previews/realunit/)

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)
@TaprootFreak TaprootFreak force-pushed the feat/realunit-mail-template branch from ab46a5f to ab6245f Compare April 22, 2026 11:27
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.
- #3: Add "von CHF 1'000.-" to monthly limit text
- #4: Add "von CHF 100'000.-" to annual limit text
- #7: Add "bei DFX AG" to video ident line2
- #12: Add body key for chargeback.crypto with doc text
- #17: Add missing first sentence to fiat_output.body
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
- 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.
@github-actions
Copy link
Copy Markdown

ℹ️ 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
@TaprootFreak
Copy link
Copy Markdown
Collaborator Author

#3654

Copy link
Copy Markdown
Member

@davidleomay davidleomay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing from issue #3654

  1. walletName parameter threading through the translation chaintranslate(), translateParams(), getMailAffix() , mapMailAffix() all got walletName added 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 of createUserV2Mail, rather than threaded throu
    gh every method call. F1 partially addresses the notification service side, but the MailFactory internal plumbing is still
    messy.

  2. The .filter((a) => a.text || a.url || a.mail) added to getMailAffix — this filters out empty lines (DefaultEmpt yLine has text: ''), 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)).
Copy link
Copy Markdown
Collaborator Author

@TaprootFreak TaprootFreak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beide Punkte adressiert:

  1. Filter-Regression-Fix gepusht als f73b36e: Der .filter((a) => a.text || a.url || a.mail) ist aus getMailAffix raus. Stattdessen filtert mapMailAffix im default-Branch leere Translations früh weg (if (!text && !specialTag) return [];). DefaultEmptyLine bleibt durchlässig, leere RealUnit-Overrides (z. B. payment.fiat_input.unassigned.transaction_button: '') werden korrekt übersprungen — kein Layout-Impact für DFX-Default-Mails.

  2. 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.
@TaprootFreak TaprootFreak marked this pull request as ready for review April 30, 2026 11:12
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.
@TaprootFreak TaprootFreak merged commit 1339592 into develop Apr 30, 2026
7 checks passed
@TaprootFreak TaprootFreak deleted the feat/realunit-mail-template branch April 30, 2026 12:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants