Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
7 changes: 7 additions & 0 deletions src/app/core/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,11 @@ export class AuthService {

return this.jsonApiService.post(baseUrl, body);
}

resendConfirmationUrl(email: string) {
const baseUrl = `${this.apiUrl}/resend_confirmation/`;
const params: Record<string, string> = { email };

return this.jsonApiService.get(baseUrl, params);
}
}
3 changes: 3 additions & 0 deletions src/app/features/auth/models/resend-confirmation.model.ts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name file like this: resend-confirmation.model.ts.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { FormControl, FormGroup } from '@angular/forms';

export type ResendConfirmationFormGroupType = FormGroup<{ email: FormControl }>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<div class="flex flex-column my-7">
<section class="resend-confirmation-container p-3 md:p-4">
<h2 class="text-center">{{ 'auth.resendConfirmation.title' | translate }}</h2>
<p>{{ 'auth.resendConfirmation.description' | translate }}</p>

<form [formGroup]="resendConfirmationForm" (ngSubmit)="onSubmit()">
<osf-text-input
[control]="resendConfirmationForm.controls['email']"
[label]="'common.labels.email'"
[placeholder]="'common.labels.emailPlaceholder'"
type="email"
[maxLength]="emailLimit"
></osf-text-input>

<p-button
class="btn-full-width block mt-6"
type="submit"
[label]="'auth.common.resendConfirmation' | translate"
[disabled]="!resendConfirmationForm.valid"
></p-button>
</form>
</section>

<div class="my-4">
@if (message(); as msg) {
<p-message styleClass="w-full" [severity]="msg.severity" closable="true" (onClose)="onCloseMessage()">
{{ msg.content | translate }}
</p-message>
}
</div>
</div>
Original file line number Diff line number Diff line change
@@ -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;
}
Comment thread
nsemets marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -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<ResendConfirmationComponent>;
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();
});
});
Original file line number Diff line number Diff line change
@@ -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<MessageInfo | null>(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);
}
}
10 changes: 9 additions & 1 deletion src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/testing/providers/auth-service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type AuthServiceMockType = Partial<AuthService> & {
register: Mock;
forgotPassword: Mock;
resetPassword: Mock;
resendConfirmationUrl: Mock;
};

export const AuthServiceMock = {
Expand All @@ -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({})),
};
},
};
Loading