Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
bda23f7
Add email-link capture plumbing: gated action types, pending store, r…
Jul 30, 2026
d0c8afd
Add email capture drawer on a shared bottom-sheet primitive
Jul 30, 2026
8d3173a
Gate logged-out consumer actions into the email capture flow
Jul 30, 2026
72a52fe
Add confirm-email banner and dead-link recovery
Jul 30, 2026
67ed655
Carry gated actions through the social OAuth flow
Jul 30, 2026
aa83bb1
Fall back to getRandomValues for CSRF tokens in insecure contexts
Jul 30, 2026
58a82af
Merge remote-tracking branch 'origin/main' into lightweight-accounts-p1
Aug 5, 2026
ce09eba
Polish mobile drawers and consumer question page spacing
Aug 5, 2026
aba76bf
Add mobile share drawer on a reusable action-grid pattern
Aug 5, 2026
72f6482
Add subscribe-capture flow experiment (PostHog A/B)
Aug 5, 2026
5ef922c
Translate lightweight-accounts copy into all locales
Aug 5, 2026
ee106d4
Address PR review: Turnstile reuse, silent failures, CTA viewport
Aug 5, 2026
0839cc7
Polish capture flow: persistent action-aware toast, optimistic send, …
Aug 7, 2026
9ddc267
Merge remote-tracking branch 'origin/main' into lightweight-accounts-p1
Aug 7, 2026
f061666
Keep the tutorial away from lightweight accounts, fix Enter and terms…
Aug 7, 2026
ca30419
Skip the tutorial for Google sign-ins from the capture drawer too
Aug 7, 2026
fba0152
Drop the onboarding request on logout instead of only hiding it
Aug 7, 2026
3f244df
Merge remote-tracking branch 'origin/main' into lightweight-accounts-p1
Aug 10, 2026
91c3916
Merge branch 'main' into lightweight-accounts-p1
aseckin Aug 13, 2026
9ec3431
Merge remote-tracking branch 'origin/main' into lightweight-accounts-p1
aseckin Sep 1, 2026
bea2b77
Reach for the shared storage helpers, drop the randomUUID branch
aseckin Sep 1, 2026
6ada525
Merge remote-tracking branch 'origin/lightweight-accounts-p1' into li…
aseckin Sep 1, 2026
f54bc6c
Tell the sharer when the copy failed, and teach the test the new toke…
aseckin Sep 1, 2026
a4f8622
Name the bottom drawer for where it comes from
aseckin Sep 1, 2026
31fd4e2
Start lightweight signups in the consumer view
aseckin Sep 2, 2026
ac1ca45
Take the decoration and the false exit off "Check your email"
aseckin Sep 2, 2026
432e371
Land from a sign-in link without the page rearranging itself
aseckin Sep 2, 2026
013edf0
Let the sent-step note wrap normally and drop the confirmation greeting
aseckin Sep 2, 2026
a425869
Pass the signup source through rather than a flag standing in for it
aseckin Sep 2, 2026
e5237c3
Trace the Metaculus mark while a sign-in link is being consumed
aseckin Sep 2, 2026
e41082b
Record when a sign-in link is actually used
aseckin Sep 2, 2026
239f1aa
Stop the loader cancelling its own way out of the trace
aseckin Sep 2, 2026
0907f80
Hold the trace to one stroke whatever size the mark is drawn at
aseckin Sep 2, 2026
2e935b6
Merge branch 'main' into lightweight-accounts-p1
aseckin Sep 2, 2026
8310096
Merge remote-tracking branch 'origin/main' into lightweight-accounts-p1
aseckin Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions authentication/social_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
from users.models import User
from users.services.username_generator import generate_username

# Marks a signup that came through the email-capture drawer rather than the
# ordinary signup modal; sent by the frontend on the code-exchange request.
EMAIL_CAPTURE_SOURCE = "email_capture"


def associate_by_email(backend, details, user=None, *args, **kwargs):
"""
Expand Down Expand Up @@ -53,10 +57,20 @@ def create_user(strategy, details, backend, user=None, *args, **kwargs):
if not settings.PUBLIC_ALLOW_SIGNUP:
raise ValidationError("Signup is disabled")

# Signing up from the email-capture drawer means arriving as a reader, so
# start in the consumer view rather than switching layouts on arrival. The
# ordinary signup modal sends no source and keeps the forecaster default.
from_capture = strategy.request_data().get("signup_source") == EMAIL_CAPTURE_SOURCE

user = User.objects.create_user(
username=generate_username(),
email=kwargs.get("email", details.get("email")),
username_set_at=None,
interface_type=(
User.InterfaceType.CONSUMER_VIEW
if from_capture
else User.InterfaceType.FORECASTER_VIEW
),
)

return {
Expand Down
4 changes: 0 additions & 4 deletions authentication/templates/emails/email_link_auth.mjml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@
<mj-section>
<mj-column>
<mj-text padding-bottom="0px">
{% blocktrans %}Hello {{ username }}{% endblocktrans %}
</mj-text>

<mj-text padding-top="8px">
{% blocktrans %}
Confirm your email address to activate your Metaculus account.
{% endblocktrans %}
Expand Down
4 changes: 4 additions & 0 deletions authentication/views/email_link.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ def email_link_request_api_view(request):
password=None, # unusable password: email-link is the only way in
is_active=False,
is_bot=False,
# Someone who arrived by following one question is a reader, not a
# forecaster: start them in the view the site already gives them
# while signed out, rather than switching layouts under them.
interface_type=User.InterfaceType.CONSUMER_VIEW,
metadata={
"signup_details": {"method": "email_link", "action_type": action_type}
},
Expand Down
76 changes: 74 additions & 2 deletions front_end/messages/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2427,9 +2427,81 @@
"newsHotnessColCluster": "Klastr",
"newsHotnessColWeight": "Váha",
"newsHotnessColContribution": "Příspěvek",
"feedTileQuestionsRecentlyResolved": "{count, plural, one {# otázka nedávno vyřešena} other {# otázek nedávno vyřešeno}}",
"emailCaptureOptionsTitle": "Dostávejte novinky k této otázce.",
"emailCaptureOptionsSubtitle": "Vyberte, o čem chcete být informováni.",
"emailCaptureOptionResolve": "Když se vyřeší",
"emailCaptureOptionForecast": "Změny prognózy",
"emailCaptureOptionDiscussion": "Nová diskuse",
"emailCaptureOptionsContinue": "Pokračovat",
"emailCaptureOptionsCaptionNone": "Pro pokračování vyberte alespoň jednu možnost.",
"emailCaptureSubscribeTitle": "Zadejte svůj e-mail pro sledování této otázky",
"emailCaptureSubscribeBody": "Pošleme vám odkaz k dokončení této akce.",
"emailCaptureVoteTitle": "Uložte svůj hlas",
"emailCaptureVoteBody": "Zadejte svůj e-mail. Pošleme vám odkaz, který uloží váš hlas.",
"emailCaptureVoteBodyRepeat": "Pošleme nový odkaz na {email}. Uloží místo toho tento hlas.",
"emailCaptureVoteCaption": "Odkaz vás přihlásí a uloží váš hlas. Platí jeden den.",
"emailCaptureForecastTitle": "Uložte svou prognózu",
"emailCaptureForecastBody": "Zadejte svůj e-mail. Pošleme vám odkaz, který uloží vaši prognózu.",
"emailCaptureForecastBodyRepeat": "Pošleme nový odkaz na {email}. Uloží místo toho tuto prognózu.",
"emailCaptureForecastCaption": "Odkaz vás přihlásí a uloží vaši prognózu. Platí jeden den.",
"emailCaptureForecastBodyNoDraft": "Zadejte svůj e-mail. Pošleme vám odkaz, který vás přihlásí k prognózování.",
"emailCaptureSignInCaption": "Odkaz vás přihlásí. Platí jeden den.",
"emailCaptureBodyRepeat": "Pošleme nový odkaz na {email}.",
"emailCaptureEmailLabel": "E-mail",
"emailCaptureSend": "Odeslat odkaz",
"emailCaptureSendNew": "Odeslat nový odkaz",
"emailCaptureSending": "Odesílání...",
"emailCaptureTryAgain": "Zkusit znovu",
"emailCaptureFormatError": "To nevypadá jako e-mailová adresa. Zkontrolujte ji a zkuste to znovu.",
"emailCaptureServerError": "Na naší straně došlo k chybě. Váš e-mail nebyl odeslán. Zkuste to znovu.",
"emailCaptureSentTitle": "Zkontrolujte svůj e-mail",
"emailCaptureSentBody": "Poslali jsme odkaz na {email}.",
"emailCaptureSentActionVote": "Klikněte na něj a uložte svůj hlas.",
"emailCaptureSentActionForecast": "Klikněte na něj a uložte svou prognózu.",
"emailCaptureSentActionSubscribeAll": "Klikněte na něj a dostávejte novinky k této otázce.",
"emailCaptureSentActionSignIn": "Klikněte na něj, přihlaste se a vytvořte svou prognózu.",
"emailCaptureSentActionSubscribeOne": "Klikněte na něj a dostávejte novinky, když {a}.",
"emailCaptureSentActionSubscribeTwo": "Klikněte na něj a dostávejte novinky, když {a} nebo {b}.",
"emailCapturePhraseResolve": "se to vyřeší",
"emailCapturePhraseForecast": "se prognóza změní",
"emailCapturePhraseDiscussion": "proběhne nová diskuse",
"emailCaptureSentNote": "Odkaz vás přihlásí a platí jeden den. Můžete jej otevřít na jakémkoli zařízení.",
"emailCaptureSentNoteRepeat": "Tento odkaz nahrazuje ten předchozí. Funguje pouze váš nejnovější odkaz.",
"emailCaptureWrongAddress": "Špatná adresa? Použijte jiný e-mail",
"emailCaptureUseDifferentEmail": "Použít jiný e-mail",
"emailCaptureRecapSentTo": "Odesláno na {email}.",
"emailCaptureRecapActionVote": "Klikněte na odkaz v tomto e-mailu a uložte svůj hlas.",
"emailCaptureRecapActionForecast": "Klikněte na odkaz v tomto e-mailu a uložte svou prognózu.",
"emailCaptureRecapActionSubscribe": "Klikněte na odkaz v tomto e-mailu a zapněte si odběr novinek.",
"emailCaptureResend": "Odeslat e-mail znovu",
"emailCaptureResendIn": "Znovu odeslat za {seconds}s",
"emailCaptureResendFeedback": "Odesláno. Zkontrolujte {email}.",
"emailCaptureGoogle": "Přihlásit se přes Google",
"emailCapturePassword": "Přihlásit se heslem",
"emailCaptureBack": "Zpět",
"emailConfirmBannerText": "Poslali jsme potvrzovací e-mail na <email></email>.",
"emailLinkRequestNew": "Odeslat nový odkaz",
"emailLinkNewSent": "Poslali jsme nový odkaz na {email}.",
"emailLinkNewSentWithAction": "Poslali jsme nový odkaz na {email}. Nese stejnou akci.",
"notifyMeCtaLabel": "Upozornit mě, až se vyřeší",
"notifyMeCtaLabelUpdates": "Upozorňovat mě na novinky",
"notifyMeCtaFollowing": "Sledujete tuto otázku",
"emailCaptureSubscribeBodyRepeat": "Pošleme nový odkaz na {email}. Místo toho zapne tyto novinky.",
"shareOpenedInNewTab": "{target} otevřeno v nové kartě.",
"copiedUrlMessage": "URL byla zkopírována do schránky",
"copyUrlErrorMessage": "Odkaz se nepodařilo zkopírovat. Zkuste to prosím znovu.",
"emailLinkSignedInSubscribe": "Jste přihlášeni a nyní sledujete tuto otázku.",
"emailLinkSignedInVote": "Jste přihlášeni a váš hlas je uložen.",
"emailLinkSignedInForecast": "Jste přihlášeni a vaše prognóza je uložena.",
"emailCaptureSignInTitle": "Přihlásit se pomocí kouzelného odkazu",
"emailCaptureSignInBody": "Zadejte svůj e-mail. Pošleme vám odkaz, který vás přihlásí.",
"emailCaptureSentActionSignInOnly": "Klikněte na něj a přihlaste se.",
"emailCaptureRecapActionSignIn": "Klikněte na odkaz v tomto e-mailu a přihlaste se.",
"loginMagicLink": "Přihlásit se pomocí kouzelného odkazu",
"registrationMagicLink": "Zaregistrovat se pomocí kouzelného odkazu",
"loadFullComment": "Komentář zkrácen — načíst zbytek",
"onlyBots": "Pouze boti",
"humansOnly": "Pouze lidé",
"humansAndBots": "Lidé a boti",
"feedTileQuestionsRecentlyResolved": "{count, plural, one {# otázka nedávno vyřešena} other {# otázek nedávno vyřešeno}}"
"humansAndBots": "Lidé a boti"
}
96 changes: 83 additions & 13 deletions front_end/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2040,7 +2040,6 @@
"copyQuestionLinkToAccount": "Copy",
"privateNote": "Private Note",
"privateNoteUpdatedFrom": "from <date></date>",
"privateNoteAutosaveHint": "changes are saved automatically",
"savedAgo": "saved <date></date>",
"loadMore": "Load More",
"noPrivateNotes": "No private notes yet",
Expand Down Expand Up @@ -2239,12 +2238,10 @@
"sortMostMindsChanged": "Most Minds Changed",
"sortRelevance": "Relevance",
"timeWindow": "Time Window",
"bots": "Bots",
"timeWindowAllTime": "All Time",
"timeWindowPastWeek": "Past Week",
"timeWindowPastMonth": "Past Month",
"timeWindowPastYear": "Past Year",
"excludeBots": "Exclude Bots",
"searchComments": "Search comments...",
"noCommentsFound": "No comments found",
"whyTrustMetaculusTitle": "Less Noise, More Truth: Metaculus' method for clear decisions in a complex world",
Expand Down Expand Up @@ -2334,7 +2331,6 @@
"midtermsHubChamberGovernor": "Governor",
"midtermsHubChamberControl": "Chamber control",
"midtermsHubCongressForecast": "Congressional Control",
"midtermsHubChamberToday": "Today:",
"midtermsHubChamberForecast": "Forecast:",
"midtermsHubChamberTooltipBody": "Forecasters give {party} a <b>{pct}% chance</b> of holding the most {chamber} seats.",
"midtermsHubChamberTooltipDisclaimer": "Click to view forecasting question",
Expand Down Expand Up @@ -2372,15 +2368,10 @@
"midtermsHubScrollRight": "Scroll insights right",
"midtermsHubDemocrat": "Democrat",
"midtermsHubRepublican": "Republican",
"midtermsHubRaceLeanSummary": "<b>{count}</b> of {total} races lean <b>{party}</b>",
"midtermsHubCloseRacesSummary": "{count} too close to call",
"midtermsHubNotContested": "Not contested",
"midtermsHubDemPct": "{pct}% Democrat",
"midtermsHubRepPct": "{pct}% Republican",
"midtermsHubNoForecast": "No forecast",
"midtermsHubSafeDem": "Safe Democrat",
"midtermsHubSafeRep": "Safe Republican",
"midtermsHubViewForecastAria": "{state} — view forecast question",
"midtermsHubFooterDisclaimer": "Not affiliated with any political party.",
"midtermsHubHeroTitleLine1": "2026 US",
"midtermsHubHeroTitleLine2": "Midterm Elections",
Expand All @@ -2397,15 +2388,94 @@
"midtermsHubDemSeatAdvantage": "Democratic Seat Advantage",
"midtermsHubRepSeatAdvantage": "Republican Seat Advantage",
"midtermsHubEven": "EVEN",
"midtermsHubMedianLabel": "Median:",
"midtermsHubMedianDem": "D +{count, plural, one {# seat} other {# seats}}",
"midtermsHubMedianRep": "R +{count, plural, one {# seat} other {# seats}}",
"midtermsHubMedianEven": "Even split",
"midtermsHubForecastUnavailable": "Forecast unavailable",
"midtermsHubSeatAdvantageTooltip": "{count} seat advantage",
"midtermsHubSeatAdvantageOverTooltip": ">{count} seat advantage",
"midtermsHubProbabilityTooltip": "{value}% probability",
"midtermsHubEvenTooltip": "Even",
"emailCaptureOptionsTitle": "Get updates on this question.",
"emailCaptureOptionsSubtitle": "Pick what you want to hear about.",
"emailCaptureOptionResolve": "When it resolves",
"emailCaptureOptionForecast": "Forecast changes",
"emailCaptureOptionDiscussion": "New discussion",
"emailCaptureOptionsContinue": "Continue",
"emailCaptureOptionsCaptionNone": "Pick at least one to continue.",
"emailCaptureSubscribeTitle": "Enter your email to follow this question",
"emailCaptureSubscribeBody": "We'll send you a link to complete this action.",
"emailCaptureVoteTitle": "Save your vote",
"emailCaptureVoteBody": "Enter your email. We'll send a link that saves your vote.",
"emailCaptureVoteBodyRepeat": "We'll send a new link to {email}. It saves this vote instead.",
"emailCaptureVoteCaption": "The link signs you in and saves your vote. It works for a day.",
"emailCaptureForecastTitle": "Save your forecast",
"emailCaptureForecastBody": "Enter your email. We'll send a link that saves your forecast.",
"emailCaptureForecastBodyRepeat": "We'll send a new link to {email}. It saves this forecast instead.",
"emailCaptureForecastCaption": "The link signs you in and saves your forecast. It works for a day.",
"emailCaptureForecastBodyNoDraft": "Enter your email. We'll send a link that signs you in to forecast.",
"emailCaptureSignInCaption": "The link signs you in. It works for a day.",
"emailCaptureBodyRepeat": "We'll send a new link to {email}.",
"emailCaptureEmailLabel": "Email",
"emailCaptureSend": "Send link",
"emailCaptureSendNew": "Send new link",
"emailCaptureSending": "Sending...",
"emailCaptureTryAgain": "Try again",
"emailCaptureFormatError": "That doesn't look like an email address. Check it and try again.",
"emailCaptureServerError": "Something went wrong on our end. Your email was not sent. Try again.",
"emailCaptureSentTitle": "Check your email",
"emailCaptureSentBody": "We sent a link to {email}.",
"emailCaptureSentActionVote": "Click it to save your vote.",
"emailCaptureSentActionForecast": "Click it to save your forecast.",
"emailCaptureSentActionSubscribeAll": "Click it to get updates on this question.",
"emailCaptureSentActionSignIn": "Click it to sign in and make your forecast.",
"emailCaptureSentActionSubscribeOne": "Click it to get updates when {a}.",
"emailCaptureSentActionSubscribeTwo": "Click it to get updates when {a} or {b}.",
"emailCapturePhraseResolve": "this resolves",
"emailCapturePhraseForecast": "the forecast moves",
"emailCapturePhraseDiscussion": "there is new discussion",
"emailCaptureSentNote": "The link signs you in and works for a day. You can open it on any device.",
"emailCaptureSentNoteRepeat": "This link replaces your earlier one. Only your newest link works.",
"emailCaptureWrongAddress": "Wrong address? Use a different email",
"emailCaptureUseDifferentEmail": "Use a different email",
"emailCaptureRecapSentTo": "Sent to {email}.",
"emailCaptureRecapActionVote": "Click the link in that email to save your vote.",
"emailCaptureRecapActionForecast": "Click the link in that email to save your forecast.",
"emailCaptureRecapActionSubscribe": "Click the link in that email to turn on your updates.",
"emailCaptureResend": "Resend email",
"emailCaptureResendIn": "Resend in {seconds}s",
"emailCaptureResendFeedback": "Sent. Check {email}.",
"emailCaptureGoogle": "Sign in with Google",
"emailCapturePassword": "Sign in with password",
"emailCaptureBack": "Back",
"emailConfirmBannerText": "We sent a confirmation email to <email></email>.",
"emailLinkRequestNew": "Send a new link",
"emailLinkNewSent": "We sent a new link to {email}.",
"emailLinkNewSentWithAction": "We sent a new link to {email}. It carries the same action.",
"notifyMeCtaLabel": "Notify me when this resolves",
"notifyMeCtaLabelUpdates": "Notify me of updates",
"notifyMeCtaFollowing": "You're following this question",
"emailCaptureSubscribeBodyRepeat": "We'll send a new link to {email}. It turns on these updates instead.",
"shareOpenedInNewTab": "Opened {target} in a new tab.",
"copiedUrlMessage": "URL is now copied to your clipboard",
"copyUrlErrorMessage": "Couldn't copy the link. Please try again.",
"emailLinkSignedInSubscribe": "You're signed in, and you're now following this question.",
"emailLinkSignedInVote": "You're signed in, and your vote is saved.",
"emailLinkSignedInForecast": "You're signed in, and your forecast is saved.",
"emailCaptureSignInTitle": "Sign in with a magic link",
"emailCaptureSignInBody": "Enter your email. We'll send a link that signs you in.",
"emailCaptureSentActionSignInOnly": "Click it to sign in.",
"emailCaptureRecapActionSignIn": "Click the link in that email to sign in.",
"loginMagicLink": "Sign in with a magic link",
"registrationMagicLink": "Sign up with a magic link",
"privateNoteAutosaveHint": "changes are saved automatically",
"midtermsHubChamberToday": "Today:",
"midtermsHubRaceLeanSummary": "<b>{count}</b> of {total} races lean <b>{party}</b>",
"midtermsHubCloseRacesSummary": "{count} too close to call",
"midtermsHubSafeDem": "Safe Democrat",
"midtermsHubSafeRep": "Safe Republican",
"midtermsHubViewForecastAria": "{state} — view forecast question",
"midtermsHubMedianLabel": "Median:",
"midtermsHubMedianDem": "D +{count, plural, one {# seat} other {# seats}}",
"midtermsHubMedianRep": "R +{count, plural, one {# seat} other {# seats}}",
"midtermsHubMedianEven": "Even split",
"midtermsHubTimelineUnavailable": "Forecast history unavailable",
"midtermsHubEngagementReachTitle": "Reach out to Metaculus",
"midtermsHubEngagementReachBody": "Have thoughts, questions, or spotted something unclear? Your input helps us improve this dashboard.",
Expand Down
Loading
Loading