ui: collapsible settings sections + welcome-email + larger auth inputs
Settings page tidy-up driven by user feedback that it had grown too busy:
- Each section (Import, Invite, Email digests, Cloud sync) is now a
native <details>/<summary> accordion. Import stays open by default
because /settings#import is the deep-link target from the dashboard
CTA; the others collapse so the page lands quiet.
- Manage subscription is a right-aligned gear-icon button instead of
a rectangular text button — the descriptive copy moves into the
tooltip. Frees up the Tier row of visual weight.
Auth + modal inputs were too small (verify code box, portfolio restore
PIN): the auth-card selector now covers text inputs as well, and a new
.modal-input class standardises 16px / 12px-padding fields used in the
cloud-sync enable modal and the portfolio restore prompt.
The verify page no longer carries the "Email me the digest" checkbox —
it was misleading on repeat logins (server-side it only applied on
first sign-up but rendered every time). Default-opt-in lives in the
User row at creation; per-user changes happen on /settings. First
successful verify now triggers a one-shot welcome email explaining the
digest cadence and pointing at /settings for opt-out; SMTP failure is
logged but does not block the login.
Tests rewritten to cover the new welcome-email path:
- first login sends exactly one welcome email
- returning user gets none
- SMTP failure does not break the redirect
- regression guard: returning user who opted out stays opted out
Also lands the paddle merchant-summary doc that was written earlier
during the Paddle → Polar → Stripe onboarding pivot.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
a07fd144ea
commit
00211fec02
8 changed files with 553 additions and 124 deletions
|
|
@ -20,41 +20,42 @@
|
|||
|
||||
<div class="settings-row">
|
||||
<div class="settings-row__label">Tier</div>
|
||||
<div class="settings-row__value">
|
||||
<span class="badge {% if paid and paid.active %}badge--ok{% else %}badge--ver{% endif %}">
|
||||
{{ user.tier }}{% if paid and paid.active and paid.source == "credit" %} · credit{% endif %}
|
||||
</span>
|
||||
{% if paid and paid.active %}
|
||||
{% if paid.source == "credit" %}
|
||||
<span class="settings-row__hint">
|
||||
Paid features active via credit · {{ paid.days_remaining }} day(s) remaining
|
||||
(expires {{ paid.expires_at.strftime("%Y-%m-%d") }}).
|
||||
</span>
|
||||
{% else %}
|
||||
{% if trial_days_remaining %}
|
||||
<div class="settings-row__value" style="display:flex; align-items:flex-start; gap:10px; flex:1;">
|
||||
<div style="flex:1; min-width:0;">
|
||||
<span class="badge {% if paid and paid.active %}badge--ok{% else %}badge--ver{% endif %}">
|
||||
{{ user.tier }}{% if paid and paid.active and paid.source == "credit" %} · credit{% endif %}
|
||||
</span>
|
||||
{% if paid and paid.active %}
|
||||
{% if paid.source == "credit" %}
|
||||
<span class="settings-row__hint">
|
||||
<strong>Free trial</strong> — {{ trial_days_remaining }}
|
||||
day{{ '' if trial_days_remaining == 1 else 's' }} remaining.
|
||||
Cancel before the trial ends and you won’t be charged.
|
||||
Paid features active via credit · {{ paid.days_remaining }} day(s) remaining
|
||||
(expires {{ paid.expires_at.strftime("%Y-%m-%d") }}).
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="settings-row__hint">Paid subscription active.</span>
|
||||
{% endif %}
|
||||
{% if user.stripe_customer_id %}
|
||||
<button type="button" id="stripe-portal-btn"
|
||||
class="btn-secondary"
|
||||
style="margin-left:10px; padding:6px 14px; font-size:12px;">
|
||||
Manage subscription
|
||||
</button>
|
||||
<div class="settings-row__hint" style="margin-top:6px;">
|
||||
Update payment method, view invoices, switch monthly ↔ annual, or cancel any time. Opens the Stripe-hosted billing portal.
|
||||
</div>
|
||||
{% if trial_days_remaining %}
|
||||
<span class="settings-row__hint">
|
||||
<strong>Free trial</strong> — {{ trial_days_remaining }}
|
||||
day{{ '' if trial_days_remaining == 1 else 's' }} remaining.
|
||||
Cancel before the trial ends and you won’t be charged.
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="settings-row__hint">Paid subscription active.</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="settings-row__hint">
|
||||
Free tier — <a href="/pricing">upgrade for £7/month or £70/year</a>.
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="settings-row__hint">
|
||||
Free tier — <a href="/pricing">upgrade for £7/month or £70/year</a>.
|
||||
</span>
|
||||
</div>
|
||||
{% if paid and paid.active and paid.source != "credit" and user.stripe_customer_id %}
|
||||
<button type="button" id="stripe-portal-btn" class="settings-icon-btn"
|
||||
title="Manage subscription — payment method, invoices, plan, cancel"
|
||||
aria-label="Manage subscription">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
|
||||
</svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -91,8 +92,11 @@
|
|||
{% endif %}
|
||||
|
||||
{# --- Import portfolio --------------------------------------------- #}
|
||||
<div class="settings-section" id="import">
|
||||
<div class="settings-section__head">Import portfolio (Trading 212 CSV)</div>
|
||||
{# Open by default because /settings#import is the deep-link target
|
||||
from the dashboard's "import a portfolio" CTA — if you arrive via
|
||||
that link the section should already be expanded. #}
|
||||
<details class="settings-section" id="import" open>
|
||||
<summary class="settings-section__head">Import portfolio (Trading 212 CSV)</summary>
|
||||
<p class="settings-section__lede">
|
||||
Export your pie from T212
|
||||
(<span class="neu">Investing → Your Pie → ··· → Export</span>)
|
||||
|
|
@ -110,11 +114,11 @@
|
|||
|
||||
<div id="import-preview" hidden style="margin-top:14px;"></div>
|
||||
<div id="import-result" class="result" hidden style="margin-top:14px;"></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{# --- Referral block ---------------------------------------------- #}
|
||||
<div class="settings-section">
|
||||
<div class="settings-section__head">Invite a friend</div>
|
||||
<details class="settings-section">
|
||||
<summary class="settings-section__head">Invite a friend</summary>
|
||||
<p class="settings-section__lede">
|
||||
Share your invite link. When your friend subscribes, you and
|
||||
they each get <strong>50% off for 3 months</strong>.
|
||||
|
|
@ -145,11 +149,11 @@
|
|||
<div class="invite-stats__value settings-row__hint">— (D.3)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{# --- Email digests block ------------------------------------------ #}
|
||||
<div class="settings-section">
|
||||
<div class="settings-section__head">Email digests</div>
|
||||
<details class="settings-section">
|
||||
<summary class="settings-section__head">Email digests</summary>
|
||||
<p class="settings-section__lede">
|
||||
Editorial commentary delivered to your inbox. Daily for paid (Mon–Sat) plus the Sunday recap; free tier gets the Sunday recap.
|
||||
</p>
|
||||
|
|
@ -188,7 +192,7 @@
|
|||
</div>
|
||||
|
||||
<div id="digest-feedback" class="settings-row__hint" style="margin-top:6px;"></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
|
|
@ -213,8 +217,8 @@
|
|||
</script>
|
||||
|
||||
{# --- Cloud sync block --------------------------------------------- #}
|
||||
<div class="settings-section">
|
||||
<div class="settings-section__head">Cloud sync (encrypted)</div>
|
||||
<details class="settings-section">
|
||||
<summary class="settings-section__head">Cloud sync (encrypted)</summary>
|
||||
<p class="settings-section__lede">
|
||||
Manage the encrypted server-side copy of your portfolio. Sync is
|
||||
opted-in per import (see the Import section above).
|
||||
|
|
@ -235,7 +239,7 @@
|
|||
above to enable cloud sync.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{# Future: Paddle subscription block, AI-spend ledger summary, etc. #}
|
||||
|
||||
|
|
@ -261,10 +265,10 @@
|
|||
<form id="sync-modal-form" autocomplete="off">
|
||||
<label style="display:block;margin-bottom:6px;font-size:12px;">PIN</label>
|
||||
<input id="sync-pin1" type="password" inputmode="numeric"
|
||||
style="width:100%;padding:8px;margin-bottom:10px;" required>
|
||||
class="modal-input" required>
|
||||
<label style="display:block;margin-bottom:6px;font-size:12px;">Confirm PIN</label>
|
||||
<input id="sync-pin2" type="password" inputmode="numeric"
|
||||
style="width:100%;padding:8px;margin-bottom:10px;" required>
|
||||
class="modal-input" required>
|
||||
<label style="display:flex;align-items:flex-start;gap:8px;
|
||||
font-size:12px;color:var(--muted,#666);margin:10px 0 16px;">
|
||||
<input id="sync-ack" type="checkbox" required>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue