diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index ace56b109..36b92bc58 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -67,6 +67,14 @@ export const routes: Routes = [
(mod) => mod.ChooseRepositoryComponent
),
},
+ {
+ path: 'resend',
+ loadComponent: () =>
+ import('./features/auth/pages/resend/resend-confirmation.component').then(
+ (mod) => mod.ResendConfirmationComponent
+ ),
+ data: { skipBreadcrumbs: true },
+ },
{
path: 'search',
loadComponent: () => import('./features/search/search.component').then((mod) => mod.SearchComponent),
diff --git a/src/app/core/services/auth.service.ts b/src/app/core/services/auth.service.ts
index 09f73bbb8..2ced17078 100644
--- a/src/app/core/services/auth.service.ts
+++ b/src/app/core/services/auth.service.ts
@@ -117,4 +117,17 @@ export class AuthService {
return this.jsonApiService.post(baseUrl, body);
}
+
+ resendConfirmationUrl(email: string) {
+ const baseUrl = `${this.apiUrl}/resend_confirmation/`;
+ const body = {
+ data: {
+ attributes: {
+ email: email,
+ },
+ },
+ };
+
+ return this.jsonApiService.post(baseUrl, body);
+ }
}
diff --git a/src/app/features/auth/models/resend-confirmation.model.ts b/src/app/features/auth/models/resend-confirmation.model.ts
new file mode 100644
index 000000000..9dfc48661
--- /dev/null
+++ b/src/app/features/auth/models/resend-confirmation.model.ts
@@ -0,0 +1,3 @@
+import { FormControl, FormGroup } from '@angular/forms';
+
+export type ResendConfirmationFormGroupType = FormGroup<{ email: FormControl }>;
diff --git a/src/app/features/auth/pages/resend/resend-confirmation.component.html b/src/app/features/auth/pages/resend/resend-confirmation.component.html
new file mode 100644
index 000000000..f283449cf
--- /dev/null
+++ b/src/app/features/auth/pages/resend/resend-confirmation.component.html
@@ -0,0 +1,31 @@
+
+
+ {{ 'auth.resendConfirmation.title' | translate }}
+ {{ 'auth.resendConfirmation.description' | translate }}
+
+
+
+
+
+ @if (message(); as msg) {
+
+ {{ msg.content | translate }}
+
+ }
+
+
diff --git a/src/app/features/auth/pages/resend/resend-confirmation.component.scss b/src/app/features/auth/pages/resend/resend-confirmation.component.scss
new file mode 100644
index 000000000..45db00704
--- /dev/null
+++ b/src/app/features/auth/pages/resend/resend-confirmation.component.scss
@@ -0,0 +1,17 @@
+@use "styles/mixins" as mix;
+
+:host {
+ @include mix.flex-center;
+ flex: 1;
+ background: var(--gradient-3);
+}
+
+.resend-confirmation-container {
+ @include mix.flex-column;
+ background: var(--white);
+ border-radius: mix.rem(12px);
+ box-shadow: 0 2px 4px var(--grey-outline);
+ max-width: mix.rem(448px);
+ gap: mix.rem(24px);
+ flex: 1;
+}
diff --git a/src/app/features/auth/pages/resend/resend-confirmation.component.spec.ts b/src/app/features/auth/pages/resend/resend-confirmation.component.spec.ts
new file mode 100644
index 000000000..f5877b0de
--- /dev/null
+++ b/src/app/features/auth/pages/resend/resend-confirmation.component.spec.ts
@@ -0,0 +1,75 @@
+import { MockComponent, MockProvider } from 'ng-mocks';
+
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AuthService } from '@core/services/auth.service';
+import { TextInputComponent } from '@osf/shared/components/text-input/text-input.component';
+
+import { provideOSFCore } from '@testing/osf.testing.provider';
+import { AuthServiceMock, AuthServiceMockType } from '@testing/providers/auth-service.mock';
+
+import { ResendConfirmationComponent } from './resend-confirmation.component';
+
+describe('ResendConfirmationComponent', () => {
+ let component: ResendConfirmationComponent;
+ let fixture: ComponentFixture;
+ let authService: AuthServiceMockType;
+
+ beforeEach(() => {
+ authService = AuthServiceMock.simple();
+
+ TestBed.configureTestingModule({
+ imports: [ResendConfirmationComponent, MockComponent(TextInputComponent)],
+ providers: [provideOSFCore(), MockProvider(AuthService, authService)],
+ });
+
+ fixture = TestBed.createComponent(ResendConfirmationComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should not call resendConfirmationUrl when form is invalid', () => {
+ component.resendConfirmationForm.setValue({ email: '' });
+
+ component.onSubmit();
+
+ expect(authService.resendConfirmationUrl).not.toHaveBeenCalled();
+ expect(component.message()).toBeNull();
+ });
+
+ it('should not call resendConfirmationUrl when email is not a valid email address', () => {
+ component.resendConfirmationForm.setValue({ email: 'not-an-email' });
+
+ component.onSubmit();
+
+ expect(authService.resendConfirmationUrl).not.toHaveBeenCalled();
+ expect(component.message()).toBeNull();
+ });
+
+ it('should call resendConfirmationUrl, reset the form, and set the success message when form is valid', () => {
+ component.resendConfirmationForm.setValue({ email: 'user@example.com' });
+
+ component.onSubmit();
+
+ expect(authService.resendConfirmationUrl).toHaveBeenCalledWith('user@example.com');
+ expect(component.resendConfirmationForm.getRawValue()).toEqual({ email: null });
+ expect(component.message()).toEqual({
+ severity: 'success',
+ content: 'auth.resendConfirmation.messages.success',
+ });
+ });
+
+ it('should clear the message on onCloseMessage', () => {
+ component.resendConfirmationForm.setValue({ email: 'user@example.com' });
+ component.onSubmit();
+ expect(component.message()).not.toBeNull();
+
+ component.onCloseMessage();
+
+ expect(component.message()).toBeNull();
+ });
+});
diff --git a/src/app/features/auth/pages/resend/resend-confirmation.component.ts b/src/app/features/auth/pages/resend/resend-confirmation.component.ts
new file mode 100644
index 000000000..f2713271e
--- /dev/null
+++ b/src/app/features/auth/pages/resend/resend-confirmation.component.ts
@@ -0,0 +1,54 @@
+import { TranslatePipe } from '@ngx-translate/core';
+
+import { Button } from 'primeng/button';
+import { Message } from 'primeng/message';
+
+import { Component, inject, signal } from '@angular/core';
+import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
+
+import { AuthService } from '@core/services/auth.service';
+import { MessageInfo } from '@osf/features/auth/models';
+import { ResendConfirmationFormGroupType } from '@osf/features/auth/models/resend-confirmation.model';
+import { TextInputComponent } from '@osf/shared/components/text-input/text-input.component';
+import { InputLimits } from '@osf/shared/constants/input-limits.const';
+import { CustomValidators } from '@osf/shared/helpers/custom-form-validators.helper';
+
+@Component({
+ selector: 'osf-resend-confirmation',
+ imports: [ReactiveFormsModule, Button, Message, TextInputComponent, TranslatePipe],
+ templateUrl: './resend-confirmation.component.html',
+ styleUrl: './resend-confirmation.component.scss',
+})
+export class ResendConfirmationComponent {
+ private readonly fb = inject(FormBuilder);
+ private readonly authService = inject(AuthService);
+
+ readonly emailLimit = InputLimits.email.maxLength;
+
+ resendConfirmationForm: ResendConfirmationFormGroupType = this.fb.group({
+ email: ['', [CustomValidators.requiredTrimmed(), Validators.email]],
+ });
+
+ message = signal(null);
+
+ onSubmit(): void {
+ if (this.resendConfirmationForm.invalid) {
+ return;
+ }
+
+ const emailForm = this.resendConfirmationForm.getRawValue();
+
+ this.authService.resendConfirmationUrl(emailForm.email).subscribe(() => {
+ this.resendConfirmationForm.reset();
+
+ this.message.set({
+ severity: 'success',
+ content: 'auth.resendConfirmation.messages.success',
+ });
+ });
+ }
+
+ onCloseMessage(): void {
+ this.message.set(null);
+ }
+}
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index da47adf5c..eb6e70e29 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -240,7 +240,8 @@
"required": "Password is required."
}
},
- "resetPassword": "Reset password"
+ "resetPassword": "Reset password",
+ "resendConfirmation": "Resend"
},
"forgotPassword": {
"description": "Enter your email address and we'll send a link to reset your password",
@@ -249,6 +250,13 @@
},
"title": "Forgot Your Password?"
},
+ "resendConfirmation": {
+ "description": "Enter your email address and we'll resend your confirmation link.",
+ "messages": {
+ "success": "Thanks. Check your email for confirmation link."
+ },
+ "title": "Resend Confirmation Email"
+ },
"resetPassword": {
"success": {
"backToSignin": "Back to Sign In",
diff --git a/src/testing/providers/auth-service.mock.ts b/src/testing/providers/auth-service.mock.ts
index 6bd0cd2d9..f79b5a234 100644
--- a/src/testing/providers/auth-service.mock.ts
+++ b/src/testing/providers/auth-service.mock.ts
@@ -15,6 +15,7 @@ export type AuthServiceMockType = Partial & {
register: Mock;
forgotPassword: Mock;
resetPassword: Mock;
+ resendConfirmationUrl: Mock;
};
export const AuthServiceMock = {
@@ -30,6 +31,7 @@ export const AuthServiceMock = {
register: vi.fn().mockReturnValue(of({})),
forgotPassword: vi.fn().mockReturnValue(of({})),
resetPassword: vi.fn().mockReturnValue(of({})),
+ resendConfirmationUrl: vi.fn().mockReturnValue(of({})),
};
},
};