diff --git a/backend/utils/email_sender.py b/backend/utils/email_sender.py
index afaded2..bf7ecbb 100644
--- a/backend/utils/email_sender.py
+++ b/backend/utils/email_sender.py
@@ -1,62 +1,186 @@
+from collections import defaultdict
from flask import current_app
from flask_mail import Mail, Message
import os
+
+# ---------------------------------------------------------------------------
+# Shared email template
+# ---------------------------------------------------------------------------
+
+def _build_email_html(content_html: str, frontend_url: str) -> str:
+ """Wrap content in the standard Chorly branded email shell."""
+ logo_url = f"{frontend_url}/images/full_logo.png"
+ return f"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+ | |
+
+
+
+
+ |
+ {content_html}
+ |
+
+
+
+
+ |
+ Chorly — Making Chores Fun
+ |
+
+
+
+ |
+
+
+
+
+"""
+
+
+def _primary_button(label: str, url: str) -> str:
+ return (
+ f''
+ f'{label}'
+ )
+
+
+def _action_button(label: str, url: str, bg: str, fg: str = '#ffffff') -> str:
+ return (
+ f''
+ f'{label}'
+ )
+
+
+# ---------------------------------------------------------------------------
+# Email senders
+# ---------------------------------------------------------------------------
+
def send_verification_email(to_email: str, token: str) -> None:
- verify_url = f"{current_app.config['FRONTEND_URL']}/auth/verify?token={token}"
- html_body = f'Click here to verify your account.'
+ frontend_url = current_app.config['FRONTEND_URL']
+ verify_url = f"{frontend_url}/auth/verify?token={token}"
+
+ content = f"""
+ Verify your account
+
+ Welcome to Chorly! Click the button below to confirm your email address
+ and activate your account.
+
+
+ {_primary_button('Verify My Account', verify_url)}
+
+
+ If you didn't create a Chorly account, you can safely ignore this email.
+ Or copy this link into your browser: {verify_url}
+
+ """
+ html_body = _build_email_html(content, frontend_url)
+
msg = Message(
- subject="Verify your account",
+ subject="Verify your Chorly account",
recipients=[to_email],
html=html_body,
sender=current_app.config.get('MAIL_DEFAULT_SENDER', 'no-reply@reward-app.local')
)
try:
- if os.environ.get('DB_ENV') =='e2e':
- return # Skip sending emails during e2e tests to avoid cluttering inboxes and logs
+ if os.environ.get('DB_ENV') == 'e2e':
+ return
Mail(current_app).send(msg)
print(f"[EMAIL to {to_email}] Verification: {verify_url}")
except Exception:
print(f"Failed to send email to {to_email}. Verification link: {verify_url}")
+
def send_reset_password_email(to_email: str, token: str) -> None:
- reset_url = f"{current_app.config['FRONTEND_URL']}/auth/reset-password?token={token}"
- html_body = f'Click here to reset your password.'
+ frontend_url = current_app.config['FRONTEND_URL']
+ reset_url = f"{frontend_url}/auth/reset-password?token={token}"
+
+ content = f"""
+ Reset your password
+
+ We received a request to reset your Chorly password. Click the button below
+ to choose a new one. This link expires in 10 minutes.
+
+
+ {_primary_button('Reset My Password', reset_url)}
+
+
+ If you didn't request a password reset, you can safely ignore this email —
+ your password will not change.
+ Or copy this link into your browser: {reset_url}
+
+ """
+ html_body = _build_email_html(content, frontend_url)
+
msg = Message(
- subject="Reset your password",
+ subject="Reset your Chorly password",
recipients=[to_email],
html=html_body,
sender=current_app.config.get('MAIL_DEFAULT_SENDER', 'no-reply@reward-app.local')
)
try:
- if os.environ.get('DB_ENV') =='e2e':
- return # Skip sending emails during e2e tests to avoid cluttering inboxes and logs
+ if os.environ.get('DB_ENV') == 'e2e':
+ return
Mail(current_app).send(msg)
print(f"[EMAIL to {to_email}] Reset password: {reset_url}")
except Exception:
print(f"Failed to send email to {to_email}. Reset link: {reset_url}")
+
def send_pin_setup_email(to_email: str, code: str) -> None:
- html_body = f"""
-
-
Set up your Parent PIN
-
To set your Parent PIN, enter the following code in the app:
-
{code}
-
This code is valid for 10 minutes.
-
If you did not request this, you can ignore this email.
-
-
Reward App
-
+ frontend_url = current_app.config.get('FRONTEND_URL', 'https://localhost:5173')
+
+ content = f"""
+ Set up your Parent PIN
+
+ To set your Parent PIN, enter the following code in the app.
+ It is valid for 10 minutes.
+
+
+ {code}
+
+
+ If you did not request this, you can safely ignore this email.
+
"""
+ html_body = _build_email_html(content, frontend_url)
+
msg = Message(
- subject="Set up your Parent PIN",
+ subject="Set up your Chorly Parent PIN",
recipients=[to_email],
html=html_body,
sender=current_app.config.get('MAIL_DEFAULT_SENDER', 'no-reply@reward-app.local')
)
try:
- if os.environ.get('DB_ENV') =='e2e':
- return # Skip sending emails during e2e tests to avoid cluttering inboxes and logs
+ if os.environ.get('DB_ENV') == 'e2e':
+ return
Mail(current_app).send(msg)
print(f"[EMAIL to {to_email}] Parent PIN setup code: {code}")
except Exception:
@@ -74,8 +198,6 @@ def send_digest_email(to_email: str, items: list, unsubscribe_token: str) -> Non
frontend_url = current_app.config.get('FRONTEND_URL', 'https://localhost:5173')
unsubscribe_url = f"{frontend_url}/api/digest-unsubscribe/{unsubscribe_token}"
- # Group items by child
- from collections import defaultdict
by_child: dict = defaultdict(list)
for item in items:
by_child[item['child_name']].append(item)
@@ -87,48 +209,46 @@ def send_digest_email(to_email: str, items: list, unsubscribe_token: str) -> Non
entity_label = 'Chore' if item['entity_type'] == 'chore' else 'Reward'
rows += f"""
- |
- {item['entity_name']}
- {entity_label}
+ |
+ {item['entity_name']}
+ {entity_label}
|
-
- View
- Approve
- Deny
+ |
+ {_action_button('View', item['view_url'], '#667eea')}
+ {_action_button('Approve', item['approve_url'], '#22c55e')}
+ {_action_button('Deny', item['deny_url'], '#ef4444')}
|
"""
child_sections += f"""
-
-
{child_name}
-
+ """
- html_body = f"""
-
-
-
Reward App — Daily Summary
-
-
-
- You have pending items that need your attention:
-
- {child_sections}
-
-
-
+ content = f"""
+ Daily Summary
+
+ You have pending items that need your attention:
+
+ {child_sections}
+
+
+ Unsubscribe from daily digest
+
+
"""
+ html_body = _build_email_html(content, frontend_url)
msg = Message(
- subject='Reward App — Daily Summary',
+ subject='Chorly \u2014 Daily Summary',
recipients=[to_email],
html=html_body,
sender=current_app.config.get('MAIL_DEFAULT_SENDER', 'no-reply@reward-app.local'),