Add push notification functionality with tests and digest scheduler
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m14s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m14s
- Implemented push subscription API with tests for subscribing and unsubscribing users. - Created web push notification tests triggered by child actions. - Added digest scheduler to send email digests to users at 9 PM local time. - Developed utility functions for creating and validating digest action tokens. - Integrated web push sender to handle sending notifications to users. - Added service worker for handling push notifications in the frontend. - Created a push opt-in component for user notification preferences. - Implemented tests for the push opt-in component to ensure correct behavior. - Updated frontend services to manage push subscriptions and permissions.
This commit is contained in:
@@ -60,4 +60,83 @@ def send_pin_setup_email(to_email: str, code: str) -> None:
|
||||
Mail(current_app).send(msg)
|
||||
print(f"[EMAIL to {to_email}] Parent PIN setup code: {code}")
|
||||
except Exception:
|
||||
print(f"Failed to send email to {to_email}. Parent PIN setup code: {code}")
|
||||
print(f"Failed to send email to {to_email}. Parent PIN setup code: {code}")
|
||||
|
||||
|
||||
def send_digest_email(to_email: str, items: list, unsubscribe_token: str) -> None:
|
||||
"""
|
||||
Send the nightly digest email listing pending chore/reward items.
|
||||
|
||||
Each item in `items` is a dict with keys:
|
||||
child_name, entity_name, entity_type, view_url, approve_url, deny_url
|
||||
Items are grouped by child_name in the email.
|
||||
"""
|
||||
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)
|
||||
|
||||
child_sections = ''
|
||||
for child_name, child_items in by_child.items():
|
||||
rows = ''
|
||||
for item in child_items:
|
||||
entity_label = 'Chore' if item['entity_type'] == 'chore' else 'Reward'
|
||||
rows += f"""
|
||||
<tr>
|
||||
<td style="padding:8px 4px;border-bottom:1px solid #eee;">
|
||||
<span style="font-weight:500;">{item['entity_name']}</span>
|
||||
<span style="color:#888;font-size:0.85em;margin-left:6px;">{entity_label}</span>
|
||||
</td>
|
||||
<td style="padding:8px 4px;border-bottom:1px solid #eee;text-align:center;">
|
||||
<a href="{item['view_url']}"
|
||||
style="color:#0066cc;text-decoration:underline;margin-right:8px;">View</a>
|
||||
<a href="{item['approve_url']}"
|
||||
style="color:#22863a;font-weight:bold;text-decoration:underline;margin-right:8px;">Approve</a>
|
||||
<a href="{item['deny_url']}"
|
||||
style="color:#cb2431;font-weight:bold;text-decoration:underline;">Deny</a>
|
||||
</td>
|
||||
</tr>"""
|
||||
|
||||
child_sections += f"""
|
||||
<div style="margin-bottom:24px;">
|
||||
<h3 style="color:#333;margin:0 0 8px 0;font-size:1rem;">{child_name}</h3>
|
||||
<table style="width:100%;border-collapse:collapse;font-size:0.92rem;">
|
||||
<tbody>{rows}</tbody>
|
||||
</table>
|
||||
</div>"""
|
||||
|
||||
html_body = f"""
|
||||
<div style="font-family:sans-serif;max-width:600px;margin:0 auto;">
|
||||
<div style="background:#4a90e2;color:#fff;padding:18px 24px;border-radius:6px 6px 0 0;">
|
||||
<h2 style="margin:0;font-size:1.2rem;">Reward App — Daily Summary</h2>
|
||||
</div>
|
||||
<div style="background:#fff;padding:24px;border:1px solid #ddd;border-top:none;border-radius:0 0 6px 6px;">
|
||||
<p style="color:#555;margin:0 0 20px 0;">
|
||||
You have pending items that need your attention:
|
||||
</p>
|
||||
{child_sections}
|
||||
</div>
|
||||
<div style="color:#aaa;font-size:0.8rem;padding:12px 0;text-align:center;">
|
||||
Reward App •
|
||||
<a href="{unsubscribe_url}" style="color:#aaa;">Unsubscribe from daily digest</a>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
msg = Message(
|
||||
subject='Reward App — Daily Summary',
|
||||
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
|
||||
Mail(current_app).send(msg)
|
||||
print(f"[EMAIL to {to_email}] Daily digest sent ({len(items)} items)")
|
||||
except Exception:
|
||||
print(f"Failed to send digest email to {to_email}")
|
||||
Reference in New Issue
Block a user