Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | 1x 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 4x 4x 1x 3x 3x 3x 3x 3x 2x 1x 2x 2x 2x | /* eslint-disable import-x/max-dependencies -- 11 dependencies */
import { AsyncPipe } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
inject,
signal,
} from '@angular/core';
import type { Signal, WritableSignal } from '@angular/core';
import { EmailAuthProvider, reauthenticateWithCredential, verifyBeforeUpdateEmail } from '@angular/fire/auth';
import type { User } from '@angular/fire/auth';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import type { FormControl, ValidationErrors } from '@angular/forms';
import { USER$ } from '~/app/core/user.token';
import type { MaybeUser$ } from '~/app/core/user.token';
import { SpinnerComponent } from '~/app/shared/spinner/spinner.component';
import { AuthErrorMessagesComponent } from '../auth-error-messages/auth-error-messages.component';
import type { SendVerifyEmailStatuses } from '../confirm-email/send-confirm-email';
import { getErrorCode } from '../error-code';
import { createEmailControl, createPasswordControl, PASSWORDS } from '../identity-forms';
import { confirmMatch, confirmMatchFormErrors } from '../validators/confirm-match';
/** Collects the new email with confirmation and the current password. */
type ChangeEmailFormGroup = FormGroup<{
email1: FormControl<string | null>;
email2: FormControl<string | null>;
password: FormControl<string | null>;
}>;
/**
* Form for user to change their email address with Firebase Authentication. The new email address
* must be verified by clicking a link and applying an oobCode before it actually updates.
*/
@Component({
selector: 'app-change-email',
imports: [
AsyncPipe,
AuthErrorMessagesComponent,
ReactiveFormsModule,
SpinnerComponent,
],
templateUrl: './change-email.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChangeEmailComponent {
/** Errors specific to the first new email field. */
public readonly $email1CntrlErrors: Signal<ValidationErrors | undefined>;
/** Aria-invalid attribute for the first new email field. */
public readonly $email1CntrlInvalid: Signal<boolean>;
/** Errors specific to the second new email field. */
public readonly $email2CntrlErrors: Signal<ValidationErrors | undefined>;
/** Aria-invalid attribute for the second new email field. */
public readonly $email2CntrlInvalid: Signal<boolean>;
/** Firebase response error code. */
public readonly $errorCode: WritableSignal<string>;
/** Aria-invalid attribute for the form validation. */
public readonly $formEmailsInvalid: Signal<boolean>;
/** Errors specific to the current password field. */
public readonly $passwordCntrlErrors: Signal<ValidationErrors | undefined>;
/** Aria-invalid attribute for the current password field. */
public readonly $passwordCntrlInvalid: Signal<boolean>;
/** Cycles through the process of sending a verification email to set the new email address for the User. */
public readonly $verificationStatus: WritableSignal<SendVerifyEmailStatuses>;
public readonly changeEmailForm: ChangeEmailFormGroup;
public readonly email1Cntrl: FormControl<string | null>;
public readonly email2Cntrl: FormControl<string | null>;
/** Used in error message for password maximum length. */
public readonly maxPasswordLength: number = PASSWORDS.maxLength;
/** Used in error message for password minimum length. */
public readonly minPasswordLength: number = PASSWORDS.minLength;
public readonly passwordCntrl: FormControl<string | null>;
public readonly user$: MaybeUser$;
constructor() {
({ $errors: this.$email1CntrlErrors, $invalid: this.$email1CntrlInvalid, control: this.email1Cntrl } = createEmailControl());
({ $errors: this.$email2CntrlErrors, $invalid: this.$email2CntrlInvalid, control: this.email2Cntrl } = createEmailControl());
({ $errors: this.$passwordCntrlErrors, $invalid: this.$passwordCntrlInvalid, control: this.passwordCntrl } = createPasswordControl());
this.changeEmailForm = new FormGroup(
{
email1: this.email1Cntrl,
email2: this.email2Cntrl,
password: this.passwordCntrl,
},
confirmMatch('email1', 'email2'),
);
this.$formEmailsInvalid = confirmMatchFormErrors(this.changeEmailForm, this.email1Cntrl, this.email2Cntrl);
this.$errorCode = signal<string>('');
this.$verificationStatus = signal<SendVerifyEmailStatuses>('unsent');
// Not handling non-logged in users because the Route guards should.
this.user$ = inject(USER$);
}
/**
* Re-authenticates the current user using the submitted password then sends an email to the new
* address to confirm ownership. Clicking the link in the email will apply the oobCode using
* VerifyEmailComponent.
*/
public async onSubmit(user: User): Promise<void> {
const { email1, password } = this.changeEmailForm.value;
// Validators prevent email1 or password being falsy, but TypeScript doesn't know that.
// Additionally, all users are expected to have an email address.
if (this.changeEmailForm.invalid || !email1 || !password || !user.email) {
throw new Error('Invalid form submitted');
}
this.$errorCode.set(''); // Clear out any existing errors
this.$verificationStatus.set('sending');
try {
const emailCreds = EmailAuthProvider.credential(user.email, password);
const credentials = await reauthenticateWithCredential(user, emailCreds);
await verifyBeforeUpdateEmail(credentials.user, email1);
this.$verificationStatus.set('sent');
} catch (err: unknown) {
const code = getErrorCode(err);
this.$errorCode.set(code);
this.$verificationStatus.set('unsent');
}
}
}
|