Skip to content

Commit eac2dcd

Browse files
authored
feat: registration email confirmation dialog (#18349)
* feat: registration email confirmation dialog During registration, users will be prompted to confirm their email address visually prior to proceeding. By presenting the user with the dialog, they have a chance to review the submitted email address one last time, hopefully preventing them from getting into a registration "stuck" loop as a result of a typo. Signed-off-by: Mike Fiedler <[email protected]> * make translations Signed-off-by: Mike Fiedler <[email protected]> --------- Signed-off-by: Mike Fiedler <[email protected]>
1 parent 582bfe8 commit eac2dcd

File tree

4 files changed

+213
-36
lines changed

4 files changed

+213
-36
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/* SPDX-License-Identifier: Apache-2.0 */
2+
3+
/* global expect, beforeEach, describe, it, jest */
4+
5+
import { Application } from "@hotwired/stimulus";
6+
import EmailConfirmationController from "../../warehouse/static/js/warehouse/controllers/email-confirmation_controller";
7+
import { fireEvent } from "@testing-library/dom";
8+
9+
describe("Email confirmation controller", () => {
10+
beforeEach(() => {
11+
document.body.innerHTML = `
12+
<div data-controller="email-confirmation">
13+
<dialog data-email-confirmation-target="dialog">
14+
<p>Please confirm that your email address is <strong data-email-confirmation-target="email"></strong>.</p>
15+
<button data-action="click->email-confirmation#confirm">Confirm</button>
16+
<button data-action="click->email-confirmation#close">Cancel</button>
17+
</dialog>
18+
<form data-email-confirmation-target="form">
19+
<input type="email" value="[email protected]">
20+
<input type="submit">
21+
</form>
22+
</div>
23+
`;
24+
25+
const application = Application.start();
26+
application.register("email-confirmation", EmailConfirmationController);
27+
});
28+
29+
it("shows the dialog on form submit", () => {
30+
const dialog = document.querySelector("dialog");
31+
const form = document.querySelector("form");
32+
const email = document.querySelector("strong");
33+
34+
// The dialog is not visible by default
35+
expect(dialog.open).toBe(false);
36+
37+
// The `showModal` method is mocked, so we can check if it was called
38+
dialog.showModal = jest.fn();
39+
40+
// When the form is submitted
41+
fireEvent.submit(form);
42+
43+
// The dialog should be visible and the email should be displayed
44+
expect(dialog.showModal).toHaveBeenCalled();
45+
expect(email.textContent).toBe("[email protected]");
46+
});
47+
48+
it("submits the form when confirmed", () => {
49+
const dialog = document.querySelector("dialog");
50+
const form = document.querySelector("form");
51+
const confirmButton = document.querySelector("[data-action='click->email-confirmation#confirm']");
52+
53+
// The `showModal` and `requestSubmit` methods are mocked, so we can check if they were called
54+
dialog.showModal = jest.fn();
55+
form.requestSubmit = jest.fn();
56+
57+
// When the form is submitted and the dialog is confirmed
58+
fireEvent.submit(form);
59+
fireEvent.click(confirmButton);
60+
61+
// The form should be submitted
62+
expect(form.requestSubmit).toHaveBeenCalled();
63+
});
64+
65+
it("closes the dialog when canceled", () => {
66+
const dialog = document.querySelector("dialog");
67+
const form = document.querySelector("form");
68+
const cancelButton = document.querySelector("[data-action='click->email-confirmation#close']");
69+
70+
// The dialog is not visible by default
71+
expect(dialog.open).toBe(false);
72+
73+
// The `showModal` and `close` methods are mocked, so we can check if they were called
74+
dialog.showModal = jest.fn();
75+
dialog.close = jest.fn();
76+
77+
// When the form is submitted and the dialog is canceled
78+
fireEvent.submit(form);
79+
fireEvent.click(cancelButton);
80+
81+
// The dialog should be visible and then closed
82+
expect(dialog.showModal).toHaveBeenCalled();
83+
expect(dialog.close).toHaveBeenCalled();
84+
});
85+
86+
it("does not show the dialog if already confirmed", () => {
87+
// this shouldn't happen in practice
88+
const dialog = document.querySelector("dialog");
89+
const form = document.querySelector("form");
90+
const confirmButton = document.querySelector("[data-action='click->email-confirmation#confirm']");
91+
92+
dialog.showModal = jest.fn();
93+
form.requestSubmit = jest.fn();
94+
95+
// Confirm once
96+
fireEvent.submit(form);
97+
fireEvent.click(confirmButton);
98+
99+
// Reset mock
100+
dialog.showModal.mockClear();
101+
102+
// Submit again
103+
fireEvent.submit(form);
104+
105+
// The dialog should not be visible
106+
expect(dialog.showModal).not.toHaveBeenCalled();
107+
});
108+
});

warehouse/locale/messages.pot

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1491,8 +1491,8 @@ msgstr ""
14911491
msgid "Error processing form"
14921492
msgstr ""
14931493

1494-
#: warehouse/templates/accounts/register.html:166
1495-
#: warehouse/templates/accounts/register.html:171
1494+
#: warehouse/templates/accounts/register.html:178
1495+
#: warehouse/templates/accounts/register.html:183
14961496
#: warehouse/templates/accounts/reset-password.html:62
14971497
#: warehouse/templates/accounts/reset-password.html:67
14981498
#: warehouse/templates/manage/account.html:453
@@ -1505,7 +1505,7 @@ msgid "Confirm password to continue"
15051505
msgstr ""
15061506

15071507
#: warehouse/templates/accounts/login.html:56
1508-
#: warehouse/templates/accounts/register.html:130
1508+
#: warehouse/templates/accounts/register.html:142
15091509
#: warehouse/templates/accounts/reset-password.html:28
15101510
#: warehouse/templates/manage/manage_base.html:451
15111511
#: warehouse/templates/re-auth.html:49
@@ -1515,11 +1515,11 @@ msgstr ""
15151515
#: warehouse/templates/accounts/login.html:34
15161516
#: warehouse/templates/accounts/login.html:58
15171517
#: warehouse/templates/accounts/recovery-code.html:28
1518-
#: warehouse/templates/accounts/register.html:57
1519-
#: warehouse/templates/accounts/register.html:79
1520-
#: warehouse/templates/accounts/register.html:107
1521-
#: warehouse/templates/accounts/register.html:132
1522-
#: warehouse/templates/accounts/register.html:168
1518+
#: warehouse/templates/accounts/register.html:69
1519+
#: warehouse/templates/accounts/register.html:91
1520+
#: warehouse/templates/accounts/register.html:119
1521+
#: warehouse/templates/accounts/register.html:144
1522+
#: warehouse/templates/accounts/register.html:180
15231523
#: warehouse/templates/accounts/request-password-reset.html:28
15241524
#: warehouse/templates/accounts/reset-password.html:30
15251525
#: warehouse/templates/accounts/reset-password.html:64
@@ -1684,7 +1684,7 @@ msgstr ""
16841684

16851685
#: warehouse/templates/accounts/login.html:32
16861686
#: warehouse/templates/accounts/profile.html:25
1687-
#: warehouse/templates/accounts/register.html:105
1687+
#: warehouse/templates/accounts/register.html:117
16881688
#: warehouse/templates/email/organization-member-added/body.html:14
16891689
#: warehouse/templates/email/organization-member-invited/body.html:14
16901690
#: warehouse/templates/email/organization-member-removed/body.html:14
@@ -1847,57 +1847,74 @@ msgstr ""
18471847
msgid "Create an account"
18481848
msgstr ""
18491849

1850-
#: warehouse/templates/accounts/register.html:18
1850+
#: warehouse/templates/accounts/register.html:19
18511851
#, python-format
18521852
msgid "Create an account on %(title)s"
18531853
msgstr ""
18541854

1855-
#: warehouse/templates/accounts/register.html:21
1855+
#: warehouse/templates/accounts/register.html:22
18561856
#, python-format
18571857
msgid ""
18581858
"Before creating an account on %(title)s, familiarize yourself with the "
18591859
"following guidelines:"
18601860
msgstr ""
18611861

1862-
#: warehouse/templates/accounts/register.html:24
1862+
#: warehouse/templates/accounts/register.html:25
18631863
#, python-format
18641864
msgid "Do not use %(title)s for any illegal or harmful activities."
18651865
msgstr ""
18661866

1867-
#: warehouse/templates/accounts/register.html:25
1867+
#: warehouse/templates/accounts/register.html:26
18681868
msgid ""
18691869
"Do not impersonate others or post private information without their "
18701870
"consent."
18711871
msgstr ""
18721872

1873-
#: warehouse/templates/accounts/register.html:26
1873+
#: warehouse/templates/accounts/register.html:27
18741874
msgid "Be respectful of other users and avoid abusive or discriminatory language."
18751875
msgstr ""
18761876

1877-
#: warehouse/templates/accounts/register.html:27
1877+
#: warehouse/templates/accounts/register.html:28
18781878
msgid "Do not post spam or distribute malware."
18791879
msgstr ""
18801880

1881-
#: warehouse/templates/accounts/register.html:28
1881+
#: warehouse/templates/accounts/register.html:29
18821882
#, python-format
18831883
msgid "Do not use %(title)s to conduct security research."
18841884
msgstr ""
18851885

1886-
#: warehouse/templates/accounts/register.html:31
1886+
#: warehouse/templates/accounts/register.html:32
18871887
#, python-format
18881888
msgid ""
18891889
"For more information, please read the full <a href=\"%(aup)s\" "
18901890
"rel=\"noopener\">Acceptable Use Policy</a>."
18911891
msgstr ""
18921892

1893-
#: warehouse/templates/accounts/register.html:36
1893+
#: warehouse/templates/accounts/register.html:37
18941894
#, python-format
18951895
msgid ""
18961896
"By registering, you agree to the <a href=\"%(tos)s\">PyPI Terms of "
18971897
"Service</a>."
18981898
msgstr ""
18991899

1900-
#: warehouse/templates/accounts/register.html:55
1900+
#: warehouse/templates/accounts/register.html:47
1901+
msgid ""
1902+
"Please confirm that your email address is <strong data-email-"
1903+
"confirmation-target=\"email\"></strong>."
1904+
msgstr ""
1905+
1906+
#: warehouse/templates/accounts/register.html:50
1907+
msgid "Confirm"
1908+
msgstr ""
1909+
1910+
#: warehouse/templates/accounts/register.html:51
1911+
#: warehouse/templates/manage/manage_base.html:398
1912+
#: warehouse/templates/manage/manage_base.html:473
1913+
#: warehouse/templates/manage/organization/activate_subscription.html:41
1914+
msgid "Cancel"
1915+
msgstr ""
1916+
1917+
#: warehouse/templates/accounts/register.html:67
19011918
#: warehouse/templates/manage/account.html:149
19021919
#: warehouse/templates/manage/account.html:491
19031920
#: warehouse/templates/manage/project/history.html:284
@@ -1911,47 +1928,47 @@ msgstr ""
19111928
msgid "Name"
19121929
msgstr ""
19131930

1914-
#: warehouse/templates/accounts/register.html:60
1931+
#: warehouse/templates/accounts/register.html:72
19151932
msgid "Your name"
19161933
msgstr ""
19171934

1918-
#: warehouse/templates/accounts/register.html:77
1935+
#: warehouse/templates/accounts/register.html:89
19191936
#: warehouse/templates/manage/account.html:350
19201937
#: warehouse/templates/manage/unverified-account.html:225
19211938
msgid "Email address"
19221939
msgstr ""
19231940

1924-
#: warehouse/templates/accounts/register.html:82
1941+
#: warehouse/templates/accounts/register.html:94
19251942
#: warehouse/templates/manage/account.html:370
19261943
msgid "Your email address"
19271944
msgstr ""
19281945

1929-
#: warehouse/templates/accounts/register.html:99
1946+
#: warehouse/templates/accounts/register.html:111
19301947
msgid "Confirm form"
19311948
msgstr ""
19321949

1933-
#: warehouse/templates/accounts/register.html:110
1950+
#: warehouse/templates/accounts/register.html:122
19341951
msgid "Select a username"
19351952
msgstr ""
19361953

1937-
#: warehouse/templates/accounts/register.html:140
1954+
#: warehouse/templates/accounts/register.html:152
19381955
#: warehouse/templates/accounts/reset-password.html:39
19391956
#: warehouse/templates/manage/account.html:409
19401957
msgid "Show passwords"
19411958
msgstr ""
19421959

1943-
#: warehouse/templates/accounts/register.html:143
1960+
#: warehouse/templates/accounts/register.html:155
19441961
msgid "Select a password"
19451962
msgstr ""
19461963

1947-
#: warehouse/templates/accounts/register.html:195
1964+
#: warehouse/templates/accounts/register.html:207
19481965
msgid ""
19491966
"This password appears in a security breach or has been compromised and "
19501967
"cannot be used. Please refer to the <a href=\"/help/#compromised-"
19511968
"password\">FAQ</a> for more information."
19521969
msgstr ""
19531970

1954-
#: warehouse/templates/accounts/register.html:202
1971+
#: warehouse/templates/accounts/register.html:214
19551972
msgid "Create account"
19561973
msgstr ""
19571974

@@ -4237,12 +4254,6 @@ msgstr ""
42374254
msgid "Confirm the %(item)s to continue."
42384255
msgstr ""
42394256

4240-
#: warehouse/templates/manage/manage_base.html:398
4241-
#: warehouse/templates/manage/manage_base.html:473
4242-
#: warehouse/templates/manage/organization/activate_subscription.html:41
4243-
msgid "Cancel"
4244-
msgstr ""
4245-
42464257
#: warehouse/templates/manage/manage_base.html:428
42474258
msgid "close"
42484259
msgstr ""
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/* SPDX-License-Identifier: Apache-2.0 */
2+
3+
/**
4+
* This controller handles the confirmation dialog that appears
5+
* when a user submits their email address during registration.
6+
* It ensures that the user confirms their email
7+
* before proceeding with the form submission.
8+
*/
9+
10+
import { Controller } from "@hotwired/stimulus";
11+
12+
export default class extends Controller {
13+
static targets = ["dialog", "email", "form"];
14+
15+
connect() {
16+
this.formTarget.addEventListener("submit", this.check.bind(this));
17+
}
18+
19+
disconnect() {
20+
this.formTarget.removeEventListener("submit", this.check.bind(this));
21+
}
22+
23+
check(event) {
24+
if (this.data.get("confirmed") === "true") {
25+
return;
26+
}
27+
28+
event.preventDefault();
29+
this.emailTarget.textContent = this.emailValue;
30+
this.dialogTarget.showModal();
31+
}
32+
33+
close() {
34+
this.dialogTarget.close();
35+
}
36+
37+
confirm(event) {
38+
event.preventDefault();
39+
this.data.set("confirmed", "true");
40+
this.formTarget.requestSubmit();
41+
}
42+
43+
get emailValue() {
44+
return this.formTarget.querySelector("input[type='email']").value;
45+
}
46+
}

warehouse/templates/accounts/register.html

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
{% set title = "PyPI" %}
1515
{% endif %}
1616
<div class="horizontal-section">
17-
<div class="site-container">
17+
<div class="site-container"
18+
data-controller="password password-match password-strength-gauge password-breach email-confirmation">
1819
<h1 class="page-title">{% trans title=title %}Create an account on {{ title }}{% endtrans %}</h1>
1920
<aside>
2021
<p>
@@ -39,9 +40,20 @@ <h1 class="page-title">{% trans title=title %}Create an account on {{ title }}{%
3940
</p>
4041
</aside>
4142
<hr>
43+
<dialog data-action="cancel->email-confirmation#close"
44+
data-email-confirmation-target="dialog">
45+
<form method="dialog">
46+
<p>
47+
{% trans %}Please confirm that your email address is <strong data-email-confirmation-target="email"></strong>.{% endtrans %}
48+
</p>
49+
<button class="button button--primary"
50+
data-action="click->email-confirmation#confirm">{% trans %}Confirm{% endtrans %}</button>
51+
<button class="button" formmethod="dialog" value="cancel">{% trans %}Cancel{% endtrans %}</button>
52+
</form>
53+
</dialog>
4254
<form method="post"
4355
action="{{ request.current_route_path() }}"
44-
data-controller="password password-match password-strength-gauge password-breach">
56+
data-email-confirmation-target="form">
4557
<input name="csrf_token"
4658
type="hidden"
4759
value="{{ request.session.get_csrf_token() }}">

0 commit comments

Comments
 (0)