diff --git a/.gitignore b/.gitignore
index 1947c24fc9d..b18dca911f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,6 +29,7 @@ src/schema.rs.orig
# insta
*.pending-snap
+*.snap.new
# playwright
/test-results/
diff --git a/app/components/email-input.hbs b/app/components/email-input.hbs
index d212ffdf4b0..e28db2ef722 100644
--- a/app/components/email-input.hbs
+++ b/app/components/email-input.hbs
@@ -1,101 +1,64 @@
- {{#unless @user.email}}
-
-
- Please add your email address. We will only use
- it to contact you about your account. We promise we'll never share it!
-
+ {{#if this.email.id }}
+
+
+
+ {{ this.email.email }}
+
+ {{#if this.email.verified}}
+ Verified
+ {{#if this.email.primary }}
+ Primary email
+ {{/if}}
+ {{else}}
+ {{#if this.email.verification_email_sent}}
+ Unverified - email sent
+ {{else}}
+ Unverified
+ {{/if}}
+ {{/if}}
+
+
- {{/unless}}
-
- {{#if this.isEditing }}
-
-
- Email
-
-
+
+ {{#unless this.email.verified}}
+
+ {{#if this.disableResend}}
+ Sent!
+ {{else if this.email.verification_email_sent}}
+ Resend
+ {{else}}
+ Verify
+ {{/if}}
+
+ {{/unless}}
+ {{#if (and (not this.email.primary) this.email.verified)}}
+
+ Mark as primary
+
+ {{/if}}
+ {{#if @canDelete}}
+
+ Remove
+
+ {{/if}}
+
{{else}}
-
-
-
Email
-
-
-
- {{ @user.email }}
- {{#if @user.email_verified}}
- Verified!
- {{/if}}
-
-
+
- {{#if (and @user.email (not @user.email_verified))}}
-
-
- {{#if @user.email_verification_sent}}
-
We have sent a verification email to your address.
- {{/if}}
-
Your email has not yet been verified.
-
-
-
- {{#if this.disableResend}}
- Sent!
- {{else if @user.email_verification_sent}}
- Resend
- {{else}}
- Send verification email
- {{/if}}
-
-
-
- {{/if}}
+
+
{{/if}}
-
\ No newline at end of file
+
diff --git a/app/components/email-input.js b/app/components/email-input.js
index 993958d9bef..5e4e2c90b46 100644
--- a/app/components/email-input.js
+++ b/app/components/email-input.js
@@ -8,13 +8,18 @@ import { task } from 'ember-concurrency';
export default class EmailInput extends Component {
@service notifications;
+ @tracked email = this.args.email || { email: '', id: null };
+ @tracked isValid = false;
@tracked value;
- @tracked isEditing = false;
@tracked disableResend = false;
+ @action validate(event) {
+ this.isValid = event.target.value.trim().length !== 0 && event.target.checkValidity();
+ }
+
resendEmailTask = task(async () => {
try {
- await this.args.user.resendVerificationEmail();
+ await this.args.user.resendVerificationEmail(this.email.id);
this.disableResend = true;
} catch (error) {
let detail = error.errors?.[0]?.detail;
@@ -26,30 +31,47 @@ export default class EmailInput extends Component {
}
});
- @action
- editEmail() {
- this.value = this.args.user.email;
- this.isEditing = true;
- }
+ deleteEmailTask = task(async () => {
+ try {
+ await this.args.user.deleteEmail(this.email.id);
+ } catch (error) {
+ console.error('Error deleting email:', error);
+ let detail = error.errors?.[0]?.detail;
+ if (detail && !detail.startsWith('{')) {
+ this.notifications.error(`Error in deleting email: ${detail}`);
+ } else {
+ this.notifications.error('Unknown error in deleting email');
+ }
+ }
+ });
saveEmailTask = task(async () => {
- let userEmail = this.value;
- let user = this.args.user;
-
try {
- await user.changeEmail(userEmail);
-
- this.isEditing = false;
- this.disableResend = false;
+ this.email = await this.args.user.addEmail(this.value);
+ this.disableResend = true;
+ await this.args.onAddEmail?.();
} catch (error) {
let detail = error.errors?.[0]?.detail;
- let msg =
- detail && !detail.startsWith('{')
- ? `An error occurred while saving this email, ${detail}`
- : 'An unknown error occurred while saving this email.';
+ if (detail && !detail.startsWith('{')) {
+ this.notifications.error(`Error in saving email: ${detail}`);
+ } else {
+ console.error('Error saving email:', error);
+ this.notifications.error('Unknown error in saving email');
+ }
+ }
+ });
- this.notifications.error(`Error in saving email: ${msg}`);
+ markAsPrimaryTask = task(async () => {
+ try {
+ await this.args.user.updatePrimaryEmail(this.email.id);
+ } catch (error) {
+ let detail = error.errors?.[0]?.detail;
+ if (detail && !detail.startsWith('{')) {
+ this.notifications.error(`Error in marking email as primary: ${detail}`);
+ } else {
+ this.notifications.error('Unknown error in marking email as primary');
+ }
}
});
}
diff --git a/app/components/email-input.module.css b/app/components/email-input.module.css
index 1313e662caa..14522ac9c10 100644
--- a/app/components/email-input.module.css
+++ b/app/components/email-input.module.css
@@ -1,7 +1,3 @@
-.friendly-message {
- margin-top: 0;
-}
-
.row {
width: 100%;
border: 1px solid var(--gray-border);
@@ -9,6 +5,7 @@
padding: var(--space-2xs) var(--space-s);
display: flex;
align-items: center;
+ justify-content: space-between;
&:last-child {
border-bottom-width: 1px;
@@ -22,12 +19,41 @@
}
.email-column {
+ padding: var(--space-xs) 0;
+}
+
+.email-column dd {
+ margin: 0;
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-3xs);
flex: 20;
}
+.email-column .badges {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-3xs);
+}
+
+.badge {
+ padding: var(--space-4xs) var(--space-2xs);
+ background-color: var(--main-bg-dark);
+ font-size: 0.8rem;
+ border-radius: 100px;
+}
+
.verified {
- color: green;
- font-weight: bold;
+ background-color: var(--green800);
+ color: var(--grey200);
+}
+
+.pending-verification {
+ background-color: light-dark(var(--orange-200), var(--orange-500));
+}
+
+.unverified {
+ background-color: light-dark(var(--orange-300), var(--orange-600));
}
.email-form {
@@ -38,13 +64,22 @@
}
.input {
- width: 400px;
+ background-color: var(--main-bg);
+ border: 0;
+ flex: 1;
+ margin: calc(var(--space-3xs) * -1) calc(var(--space-2xs) * -1);
+ padding: var(--space-3xs) var(--space-2xs);
margin-right: var(--space-xs);
}
+.input:focus {
+ outline: none;
+}
+
.actions {
display: flex;
align-items: center;
+ gap: var(--space-3xs);
}
.save-button {
diff --git a/app/controllers/settings/profile.js b/app/controllers/settings/profile.js
index ad0649ce9a4..b16f86e8372 100644
--- a/app/controllers/settings/profile.js
+++ b/app/controllers/settings/profile.js
@@ -8,6 +8,8 @@ import { task } from 'ember-concurrency';
export default class extends Controller {
@service notifications;
+ @tracked isAddingEmail = false;
+
@tracked publishNotifications;
@action handleNotificationsChange(event) {
diff --git a/app/models/user.js b/app/models/user.js
index 1c7b51d72d7..62dc2f62481 100644
--- a/app/models/user.js
+++ b/app/models/user.js
@@ -7,9 +7,7 @@ import { apiAction } from '@mainmatter/ember-api-actions';
export default class User extends Model {
@service store;
- @attr email;
- @attr email_verified;
- @attr email_verification_sent;
+ @attr emails;
@attr name;
@attr is_admin;
@attr login;
@@ -22,15 +20,45 @@ export default class User extends Model {
return await waitForPromise(apiAction(this, { method: 'GET', path: 'stats' }));
}
- async changeEmail(email) {
- await waitForPromise(apiAction(this, { method: 'PUT', data: { user: { email } } }));
+ async addEmail(emailAddress) {
+ let email = await waitForPromise(
+ apiAction(this, {
+ method: 'POST',
+ path: 'emails',
+ data: { email: emailAddress },
+ }),
+ );
this.store.pushPayload({
user: {
id: this.id,
- email,
- email_verified: false,
- email_verification_sent: true,
+ emails: [...this.emails, email],
+ },
+ });
+ }
+
+ async resendVerificationEmail(emailId) {
+ return await waitForPromise(apiAction(this, { method: 'PUT', path: `emails/${emailId}/resend` }));
+ }
+
+ async deleteEmail(emailId) {
+ await waitForPromise(apiAction(this, { method: 'DELETE', path: `emails/${emailId}` }));
+
+ this.store.pushPayload({
+ user: {
+ id: this.id,
+ emails: this.emails.filter(email => email.id !== emailId),
+ },
+ });
+ }
+
+ async updatePrimaryEmail(emailId) {
+ await waitForPromise(apiAction(this, { method: 'PUT', path: `emails/${emailId}/set_primary` }));
+
+ this.store.pushPayload({
+ user: {
+ id: this.id,
+ emails: this.emails.map(email => ({ ...email, primary: email.id === emailId })),
},
});
}
@@ -45,8 +73,4 @@ export default class User extends Model {
},
});
}
-
- async resendVerificationEmail() {
- return await waitForPromise(apiAction(this, { method: 'PUT', path: 'resend' }));
- }
}
diff --git a/app/routes/confirm.js b/app/routes/confirm.js
index 1ef7fc2142e..16ecf8dbac8 100644
--- a/app/routes/confirm.js
+++ b/app/routes/confirm.js
@@ -11,14 +11,22 @@ export default class ConfirmRoute extends Route {
async model(params) {
try {
- await ajax(`/api/v1/confirm/${params.email_token}`, { method: 'PUT', body: '{}' });
+ let response = await ajax(`/api/v1/confirm/${params.email_token}`, { method: 'PUT', body: '{}' });
// wait for the `GET /api/v1/me` call to complete before
// trying to update the Ember Data store
await this.session.loadUserTask.last;
if (this.session.currentUser) {
- this.store.pushPayload({ user: { id: this.session.currentUser.id, email_verified: true } });
+ this.store.pushPayload({
+ user: {
+ id: this.session.currentUser.id,
+ emails: [
+ ...this.session.currentUser.emails.filter(email => email.id !== response.email.id),
+ response.email,
+ ].sort((a, b) => a.id - b.id),
+ },
+ });
}
this.notifications.success('Thank you for confirming your email! :)');
diff --git a/app/routes/settings/profile.js b/app/routes/settings/profile.js
index bb4faabbd53..84f54e18a16 100644
--- a/app/routes/settings/profile.js
+++ b/app/routes/settings/profile.js
@@ -12,5 +12,6 @@ export default class ProfileSettingsRoute extends AuthenticatedRoute {
setupController(controller, model) {
super.setupController(...arguments);
controller.publishNotifications = model.user.publish_notifications;
+ controller.primaryEmailId = model.user.emails.find(email => email.primary)?.id;
}
}
diff --git a/app/styles/settings/profile.module.css b/app/styles/settings/profile.module.css
index bdabd5a791c..ee59e7071f4 100644
--- a/app/styles/settings/profile.module.css
+++ b/app/styles/settings/profile.module.css
@@ -53,6 +53,26 @@
column-gap: var(--space-xs);
}
+.friendly-message {
+ margin: 0;
+ margin-bottom: var(--space-s);
+}
+
+.email-selector {
+ display: flex;
+ flex-direction: column;
+ margin-bottom: var(--space-s);
+}
+
+.select-label {
+ margin-bottom: var(--space-3xs);
+}
+
+.add-email {
+ margin-top: var(--space-xs);
+ display: flex;
+}
+
.label {
grid-area: label;
font-weight: bold;
diff --git a/app/templates/crate/settings/index.hbs b/app/templates/crate/settings/index.hbs
index f5321ee8be2..1f37235150b 100644
--- a/app/templates/crate/settings/index.hbs
+++ b/app/templates/crate/settings/index.hbs
@@ -47,7 +47,11 @@
{{/if}}
- {{user.email}}
+ {{#each user.emails as |email|}}
+ {{#if email.primary}}
+ {{email.email}}
+ {{/if}}
+ {{/each}}
Remove
diff --git a/app/templates/settings/email-notifications.hbs b/app/templates/settings/email-notifications.hbs
index e0c78191e29..41045a69e16 100644
--- a/app/templates/settings/email-notifications.hbs
+++ b/app/templates/settings/email-notifications.hbs
@@ -49,4 +49,4 @@
{{/if}}
-
\ No newline at end of file
+
diff --git a/app/templates/settings/profile.hbs b/app/templates/settings/profile.hbs
index cc33b80ec67..23ca904e415 100644
--- a/app/templates/settings/profile.hbs
+++ b/app/templates/settings/profile.hbs
@@ -25,13 +25,38 @@