fix: apply webapp theme accent in emails and deeplinks
This commit is contained in:
@@ -8,11 +8,18 @@
|
||||
<style nonce="__NONCE__">
|
||||
:root {
|
||||
color-scheme: dark light;
|
||||
--accent: #14b86f;
|
||||
--accent-contrast: #03120b;
|
||||
--bg: #0b1017;
|
||||
--panel-3: #344052;
|
||||
--border: #2d3847;
|
||||
--text: #f7fafc;
|
||||
--muted: #aeb8c5;
|
||||
font-family:
|
||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", sans-serif;
|
||||
background: #0b1017;
|
||||
color: #f7fafc;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -39,7 +46,7 @@
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #aeb8c5;
|
||||
color: var(--muted);
|
||||
font-size: 15px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
@@ -57,8 +64,8 @@
|
||||
justify-content: center;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: #14b86f;
|
||||
color: #03120b;
|
||||
background: var(--accent);
|
||||
color: var(--accent-contrast);
|
||||
padding: 0 18px;
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
@@ -68,15 +75,15 @@
|
||||
}
|
||||
|
||||
.button.secondary {
|
||||
border-color: #2d3847;
|
||||
border-color: var(--border);
|
||||
background: transparent;
|
||||
color: #f7fafc;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.button[aria-disabled="true"] {
|
||||
pointer-events: none;
|
||||
background: #344052;
|
||||
color: #aeb8c5;
|
||||
background: var(--panel-3);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
|
||||
@@ -5,6 +5,7 @@ import gzip
|
||||
from config.webapp_themes_config import (
|
||||
default_webapp_theme_asset_file,
|
||||
default_webapp_theme_css_files,
|
||||
effective_webapp_theme_accent,
|
||||
ensure_default_webapp_theme_descriptor_files,
|
||||
public_theme_payload,
|
||||
public_themes_catalog_payload,
|
||||
@@ -1188,6 +1189,11 @@ async def app_deeplink_route(request: web.Request) -> web.Response:
|
||||
|
||||
nonce = html.escape(str(request.get("csp_nonce", "")), quote=True)
|
||||
query = getattr(request, "query", {}) or {}
|
||||
themes_catalog = getattr(settings, "webapp_themes_catalog", None)
|
||||
primary_color = getattr(settings, "WEBAPP_PRIMARY_COLOR", None) or "#00fe7a"
|
||||
initial_theme = (
|
||||
_initial_theme_for_request(request, themes_catalog) if themes_catalog is not None else None
|
||||
)
|
||||
lang = _normalize_language(query.get("lang") or getattr(settings, "DEFAULT_LANGUAGE", "ru"))
|
||||
messages = _app_deeplink_i18n_payload(request, lang)
|
||||
page_title = _webapp_page_title(settings, messages["title"])
|
||||
@@ -1204,6 +1210,14 @@ async def app_deeplink_route(request: web.Request) -> web.Response:
|
||||
.replace("__NONCE__", nonce)
|
||||
.replace("__MESSAGES_JSON__", messages_json)
|
||||
)
|
||||
initial_theme_markup = _app_deeplink_theme_head_markup(
|
||||
request,
|
||||
initial_theme,
|
||||
themes_catalog,
|
||||
primary_color,
|
||||
)
|
||||
if initial_theme_markup:
|
||||
html_text = html_text.replace("</head>", f"{initial_theme_markup}\n</head>", 1)
|
||||
html_text = _apply_webapp_head_metadata(html_text, page_title, favicon_url)
|
||||
response = web.Response(text=html_text, content_type="text/html", charset="utf-8")
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
@@ -1578,7 +1592,8 @@ def _theme_css_href_for_html(theme: Any) -> str:
|
||||
|
||||
|
||||
def _initial_theme_for_request(request: web.Request, catalog: Any) -> Any:
|
||||
preview_key = str(request.query.get("theme_preview") or "").strip()
|
||||
query = getattr(request, "query", {}) or {}
|
||||
preview_key = str(query.get("theme_preview") or "").strip()
|
||||
if preview_key:
|
||||
preview_theme = catalog.theme_by_key(preview_key)
|
||||
if preview_theme is not None and preview_theme.enabled:
|
||||
@@ -1590,13 +1605,16 @@ def _initial_theme_for_request(request: web.Request, catalog: Any) -> Any:
|
||||
return catalog.enabled_themes()[0] if catalog.enabled_themes() else None
|
||||
|
||||
|
||||
def _initial_theme_head_markup(request: web.Request, theme: Any, primary_color: str) -> str:
|
||||
def _initial_theme_tokens(theme: Any, primary_color: str) -> Dict[str, Any]:
|
||||
if theme is None:
|
||||
return ""
|
||||
return {}
|
||||
|
||||
payload = public_theme_payload(theme, primary_color)
|
||||
tokens = payload.get("tokens") if isinstance(payload, dict) else {}
|
||||
tokens = tokens if isinstance(tokens, dict) else {}
|
||||
return tokens if isinstance(tokens, dict) else {}
|
||||
|
||||
|
||||
def _initial_theme_declarations(tokens: Dict[str, Any]) -> List[str]:
|
||||
declarations = []
|
||||
for token_key, css_name in _INITIAL_THEME_TOKEN_CSS_MAP.items():
|
||||
if token_key in _INITIAL_THEME_LOGO_SCALE_TOKENS:
|
||||
@@ -1610,6 +1628,15 @@ def _initial_theme_head_markup(request: web.Request, theme: Any, primary_color:
|
||||
value = str(tokens.get(token_key) or "").strip()
|
||||
if value:
|
||||
declarations.append(f"{css_name}:{value}")
|
||||
return declarations
|
||||
|
||||
|
||||
def _initial_theme_head_markup(request: web.Request, theme: Any, primary_color: str) -> str:
|
||||
if theme is None:
|
||||
return ""
|
||||
|
||||
tokens = _initial_theme_tokens(theme, primary_color)
|
||||
declarations = _initial_theme_declarations(tokens)
|
||||
|
||||
scheme = "light" if tokens.get("color_scheme") == "light" else "dark"
|
||||
bg = str(tokens.get("bg") or "").strip()
|
||||
@@ -1633,6 +1660,37 @@ def _initial_theme_head_markup(request: web.Request, theme: Any, primary_color:
|
||||
return stylesheet + "\n" + style_tag
|
||||
|
||||
|
||||
def _app_deeplink_theme_head_markup(
|
||||
request: web.Request,
|
||||
theme: Any,
|
||||
catalog: Any,
|
||||
primary_color: str,
|
||||
) -> str:
|
||||
tokens = _initial_theme_tokens(theme, primary_color)
|
||||
declarations = _initial_theme_declarations(tokens)
|
||||
try:
|
||||
accent = effective_webapp_theme_accent(
|
||||
catalog,
|
||||
primary_color,
|
||||
theme_key=str(getattr(theme, "key", "") or "") or None,
|
||||
)
|
||||
except Exception:
|
||||
accent = str(primary_color or "#00fe7a").strip() or "#00fe7a"
|
||||
if accent and not any(item.startswith("--accent:") for item in declarations):
|
||||
declarations.insert(0, f"--accent:{accent}")
|
||||
if not declarations:
|
||||
return ""
|
||||
|
||||
scheme = "light" if tokens.get("color_scheme") == "light" else "dark"
|
||||
nonce = html.escape(str(request.get("csp_nonce", "")), quote=True)
|
||||
return (
|
||||
f'<style id="webapp-initial-theme" nonce="{nonce}">'
|
||||
f"html{{color-scheme:{scheme};}}"
|
||||
f":root{{{';'.join(declarations)}}}"
|
||||
"</style>"
|
||||
)
|
||||
|
||||
|
||||
def _favicon_head_markup(favicon_url: str) -> str:
|
||||
href = str(favicon_url or "").strip()
|
||||
if not href:
|
||||
|
||||
@@ -16,6 +16,8 @@ from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Optional, Sequence, Tuple
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from config.webapp_themes_config import effective_webapp_theme_accent
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from config.settings import Settings
|
||||
@@ -74,6 +76,17 @@ def _safe_color(value: Optional[str]) -> str:
|
||||
return _DEFAULT_ACCENT
|
||||
|
||||
|
||||
def _theme_accent(settings: Settings) -> str:
|
||||
primary = _safe_color(getattr(settings, "WEBAPP_PRIMARY_COLOR", None))
|
||||
try:
|
||||
catalog = getattr(settings, "webapp_themes_catalog", None)
|
||||
if catalog is None:
|
||||
return primary
|
||||
return _safe_color(effective_webapp_theme_accent(catalog, primary))
|
||||
except Exception:
|
||||
return primary
|
||||
|
||||
|
||||
def _public_logo_url(settings: Settings) -> Optional[str]:
|
||||
"""Email recipients can't reach the in-app /webapp-logo proxy, so only a
|
||||
stored public https URL can be used directly. Anything else is dropped."""
|
||||
@@ -174,8 +187,9 @@ def _layout(
|
||||
intro_html: str,
|
||||
body_html: str,
|
||||
footer_html: str,
|
||||
accent: Optional[str] = None,
|
||||
) -> _EmailLayout:
|
||||
accent = _safe_color(settings.WEBAPP_PRIMARY_COLOR)
|
||||
accent = _safe_color(accent) if accent else _theme_accent(settings)
|
||||
brand_title = html.escape(_brand_title(settings))
|
||||
logo_url, inline_images = _email_logo(settings)
|
||||
html_lang = html.escape((language_code or "en").replace("_", "-"), quote=True)
|
||||
@@ -344,7 +358,7 @@ def render_login_code(
|
||||
i18n = _resolve_i18n(i18n)
|
||||
lang = _normalize_lang(language_code, settings)
|
||||
minutes = _format_minutes(settings.EMAIL_CODE_TTL_SECONDS)
|
||||
accent = _safe_color(settings.WEBAPP_PRIMARY_COLOR)
|
||||
accent = _theme_accent(settings)
|
||||
brand = _brand_title(settings)
|
||||
template_prefix = "email_set_password_code" if purpose == "set_password" else "email_login_code"
|
||||
safe_magic_link = (magic_link or "").strip() if template_prefix == "email_login_code" else ""
|
||||
@@ -404,6 +418,7 @@ def render_login_code(
|
||||
intro_html=html.escape(intro),
|
||||
body_html=body_html,
|
||||
footer_html=footer,
|
||||
accent=accent,
|
||||
)
|
||||
return _email_content(subject=subject, text="\n".join(text_lines), layout=rendered)
|
||||
|
||||
@@ -477,7 +492,7 @@ def render_payment_success(
|
||||
) -> EmailContent:
|
||||
i18n = _resolve_i18n(i18n)
|
||||
lang = _normalize_lang(language_code, settings)
|
||||
accent = _safe_color(settings.WEBAPP_PRIMARY_COLOR)
|
||||
accent = _theme_accent(settings)
|
||||
brand = _brand_title(settings)
|
||||
sale_base = (sale_mode or "").split("@", 1)[0].split("|", 1)[0]
|
||||
is_traffic = sale_base in {
|
||||
@@ -591,6 +606,7 @@ def render_payment_success(
|
||||
intro_html=html.escape(intro),
|
||||
body_html="".join(body_parts),
|
||||
footer_html=footer,
|
||||
accent=accent,
|
||||
)
|
||||
return _email_content(subject=subject, text="\n".join(text_lines), layout=rendered)
|
||||
|
||||
@@ -609,7 +625,7 @@ def render_user_notification(
|
||||
) -> EmailContent:
|
||||
i18n = _resolve_i18n(i18n)
|
||||
lang = _normalize_lang(language_code, settings)
|
||||
accent = _safe_color(settings.WEBAPP_PRIMARY_COLOR)
|
||||
accent = _theme_accent(settings)
|
||||
brand = _brand_title(settings)
|
||||
safe_dashboard_url = (dashboard_url or "").strip()
|
||||
final_subject = (subject or "").strip() or _t_text(
|
||||
@@ -642,6 +658,7 @@ def render_user_notification(
|
||||
intro_html=html.escape(final_intro),
|
||||
body_html="".join(body_parts),
|
||||
footer_html=footer,
|
||||
accent=accent,
|
||||
)
|
||||
text_lines = [final_subject, "", _telegram_html_to_text(message_text)]
|
||||
if safe_dashboard_url:
|
||||
@@ -667,7 +684,7 @@ def render_subscription_expiring(
|
||||
) -> EmailContent:
|
||||
i18n = _resolve_i18n(i18n)
|
||||
lang = _normalize_lang(language_code, settings)
|
||||
accent = _safe_color(settings.WEBAPP_PRIMARY_COLOR)
|
||||
accent = _theme_accent(settings)
|
||||
brand = _brand_title(settings)
|
||||
safe_dashboard_url = (dashboard_url or "").strip()
|
||||
days = max(0, int(days_left))
|
||||
@@ -716,6 +733,7 @@ def render_subscription_expiring(
|
||||
intro_html=html.escape(intro),
|
||||
body_html="".join(body_parts),
|
||||
footer_html=footer,
|
||||
accent=accent,
|
||||
)
|
||||
return _email_content(subject=subject, text="\n".join(text_lines), layout=rendered)
|
||||
|
||||
@@ -764,7 +782,7 @@ def render_subscription_lifecycle_notification(
|
||||
) -> EmailContent:
|
||||
i18n = _resolve_i18n(i18n)
|
||||
lang = _normalize_lang(language_code, settings)
|
||||
accent = _safe_color(settings.WEBAPP_PRIMARY_COLOR)
|
||||
accent = _theme_accent(settings)
|
||||
brand = _brand_title(settings)
|
||||
safe_dashboard_url = (dashboard_url or "").strip()
|
||||
end_date = end_date_text or "—"
|
||||
@@ -804,6 +822,7 @@ def render_subscription_lifecycle_notification(
|
||||
intro_html=html.escape(intro),
|
||||
body_html="".join(body_parts),
|
||||
footer_html=footer,
|
||||
accent=accent,
|
||||
)
|
||||
|
||||
text_lines = [subject, "", message_text]
|
||||
@@ -838,7 +857,7 @@ def _support_email(
|
||||
i18n = _resolve_i18n(i18n)
|
||||
lang = _normalize_lang(language, settings)
|
||||
brand = _brand_title(settings)
|
||||
accent = _safe_color(settings.WEBAPP_PRIMARY_COLOR)
|
||||
accent = _theme_accent(settings)
|
||||
safe_url = (ticket_url or "").strip()
|
||||
footer = _t_html(i18n, lang, "email_footer_auto", brand=brand)
|
||||
localized_rows = [
|
||||
@@ -861,6 +880,7 @@ def _support_email(
|
||||
intro_html=html.escape(intro),
|
||||
body_html="".join(body_parts),
|
||||
footer_html=footer,
|
||||
accent=accent,
|
||||
)
|
||||
text_lines = [
|
||||
intro,
|
||||
|
||||
Reference in New Issue
Block a user