Skip to content
Merged
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
9 changes: 8 additions & 1 deletion src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

import { GetCurrentUser, UserState } from '@core/store/user';
import { UserEmailsState } from '@core/store/user-emails';

import { FullScreenLoaderComponent, ToastComponent } from './shared/components';
import { TranslateServiceMock } from './shared/mocks';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
Expand All @@ -19,7 +21,12 @@ describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent, ...MockComponents(ToastComponent, FullScreenLoaderComponent)],
providers: [provideStore([UserState]), provideHttpClient(), provideHttpClientTesting()],
providers: [
provideStore([UserState, UserEmailsState]),
provideHttpClient(),
provideHttpClientTesting(),
TranslateServiceMock,
],
}).compileComponents();

fixture = TestBed.createComponent(AppComponent);
Expand Down
45 changes: 35 additions & 10 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { createDispatchMap } from '@ngxs/store';
import { createDispatchMap, select } from '@ngxs/store';

import { TranslateService } from '@ngx-translate/core';

import { DialogService } from 'primeng/dynamicdialog';

import { filter } from 'rxjs/operators';

import { ChangeDetectionStrategy, Component, DestroyRef, inject, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NavigationEnd, Router, RouterOutlet } from '@angular/router';

import { GetCurrentUser } from '@core/store/user';
import { GetEmails, UserEmailsSelectors } from '@core/store/user-emails';
import { ConfirmEmailComponent } from '@shared/components';

import { FullScreenLoaderComponent, ToastComponent } from './shared/components';
import { MetaTagsService } from './shared/services/meta-tags.service';
Expand All @@ -17,23 +23,31 @@ import { MetaTagsService } from './shared/services/meta-tags.service';
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [DialogService],
})
export class AppComponent implements OnInit {
private destroyRef = inject(DestroyRef);
private readonly destroyRef = inject(DestroyRef);
private readonly dialogService = inject(DialogService);
private readonly router = inject(Router);
private readonly translateService = inject(TranslateService);
private readonly metaTagsService = inject(MetaTagsService);

private readonly actions = createDispatchMap({ getCurrentUser: GetCurrentUser, getEmails: GetEmails });

actions = createDispatchMap({
getCurrentUser: GetCurrentUser,
});
unverifiedEmails = select(UserEmailsSelectors.getUnverifiedEmails);

constructor(
private router: Router,
private metaTagsService: MetaTagsService
) {
constructor() {
this.setupMetaTagsCleanup();
effect(() => {
if (this.unverifiedEmails().length) {
this.showEmailDialog();
}
});
}

ngOnInit(): void {
this.actions.getCurrentUser();
this.actions.getEmails();
}

private setupMetaTagsCleanup(): void {
Expand All @@ -44,4 +58,15 @@ export class AppComponent implements OnInit {
)
.subscribe((event: NavigationEnd) => this.metaTagsService.clearMetaTagsIfNeeded(event.url));
}

private showEmailDialog() {
this.dialogService.open(ConfirmEmailComponent, {
width: '448px',
focusOnShow: false,
header: this.translateService.instant('home.confirmEmail.title'),
modal: true,
closable: false,
data: this.unverifiedEmails(),
});
}
}
2 changes: 2 additions & 0 deletions src/app/core/constants/ngxs-states.constant.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ProviderState } from '@core/store/provider';
import { UserState } from '@core/store/user';
import { UserEmailsState } from '@core/store/user-emails';
import { FilesState } from '@osf/features/files/store';
import { ProjectMetadataState } from '@osf/features/project/metadata/store';
import { ProjectOverviewState } from '@osf/features/project/overview/store';
Expand All @@ -13,6 +14,7 @@ import { RegionsState } from '@shared/stores/regions';
export const STATES = [
AddonsState,
UserState,
UserEmailsState,
ProviderState,
MyResourcesState,
InstitutionsState,
Expand Down
1 change: 1 addition & 0 deletions src/app/core/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { AuthService } from './auth.service';
export { RequestAccessService } from './request-access.service';
export { UserService } from './user.service';
export { UserEmailsService } from './user-emails.service';
97 changes: 97 additions & 0 deletions src/app/core/services/user-emails.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { map, Observable } from 'rxjs';

import { inject, Injectable } from '@angular/core';

import { MapEmail, MapEmails } from '@osf/shared/mappers';
import { AccountEmailModel, EmailResponseJsonApi, EmailsDataJsonApi, EmailsResponseJsonApi } from '@osf/shared/models';
import { JsonApiService } from '@osf/shared/services';

import { environment } from 'src/environments/environment';

@Injectable({
providedIn: 'root',
})
export class UserEmailsService {
private readonly jsonApiService = inject(JsonApiService);
private readonly baseUrl = `${environment.apiUrl}/users`;

getEmails(): Observable<AccountEmailModel[]> {
const params: Record<string, string> = {
page: '1',
'page[size]': '10',
};

return this.jsonApiService
.get<EmailsResponseJsonApi>(`${this.baseUrl}/me/settings/emails/`, params)
.pipe(map((response) => MapEmails(response.data)));
}

resendConfirmation(emailId: string): Observable<AccountEmailModel> {
const params: Record<string, string> = {
resend_confirmation: 'true',
};

return this.jsonApiService
.get<EmailResponseJsonApi>(`${this.baseUrl}/me/settings/emails/${emailId}/`, params)
.pipe(map((response) => MapEmail(response.data)));
}

addEmail(userId: string, email: string): Observable<AccountEmailModel> {
const body = {
data: {
attributes: {
email_address: email,
},
relationships: {
user: {
data: {
id: userId,
type: 'users',
},
},
},
type: 'user_emails',
},
};

return this.jsonApiService
.post<EmailResponseJsonApi>(`${this.baseUrl}/${userId}/settings/emails/`, body)
.pipe(map((response) => MapEmail(response.data)));
}

verifyEmail(emailId: string): Observable<AccountEmailModel> {
const body = {
data: {
id: emailId,
attributes: {
verified: true,
},
type: 'user_emails',
},
};

return this.jsonApiService
.patch<EmailsDataJsonApi>(`${this.baseUrl}/me/settings/emails/${emailId}/`, body)
.pipe(map((response) => MapEmail(response)));
}

makePrimary(emailId: string): Observable<AccountEmailModel> {
const body = {
data: {
id: emailId,
attributes: {
primary: true,
},
type: 'user_emails',
},
};

return this.jsonApiService
.patch<EmailsDataJsonApi>(`${this.baseUrl}/me/settings/emails/${emailId}/`, body)
.pipe(map((response) => MapEmail(response)));
}

deleteEmail(emailId: string): Observable<void> {
return this.jsonApiService.delete(`${this.baseUrl}/me/settings/emails/${emailId}/`);
}
}
2 changes: 1 addition & 1 deletion src/app/core/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
UserSettingsGetResponse,
} from '@osf/shared/models';

import { JsonApiService } from '../../shared/services/json-api.service';
import { JsonApiService } from '../../shared/services';

import { environment } from 'src/environments/environment';

Expand Down
4 changes: 4 additions & 0 deletions src/app/core/store/user-emails/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './user-emails.actions';
export * from './user-emails.model';
export * from './user-emails.selectors';
export * from './user-emails.state';
33 changes: 33 additions & 0 deletions src/app/core/store/user-emails/user-emails.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export class GetEmails {
static readonly type = '[UserEmails] Get Emails';
}

export class AddEmail {
static readonly type = '[UserEmails] Add Email';

constructor(public email: string) {}
}

export class DeleteEmail {
static readonly type = '[UserEmails] Remove Email';

constructor(public emailId: string) {}
}

export class ResendConfirmation {
static readonly type = '[UserEmails] Resend Confirmation';

constructor(public emailId: string) {}
}

export class VerifyEmail {
static readonly type = '[UserEmails] Verify Email';

constructor(public emailId: string) {}
}

export class MakePrimary {
static readonly type = '[UserEmails] Make Primary';

constructor(public emailId: string) {}
}
14 changes: 14 additions & 0 deletions src/app/core/store/user-emails/user-emails.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { AccountEmailModel, AsyncStateModel } from '@shared/models';

export interface UserEmailsStateModel {
emails: AsyncStateModel<AccountEmailModel[]>;
}

export const USER_EMAILS_STATE_DEFAULTS: UserEmailsStateModel = {
emails: {
data: [],
isLoading: false,
error: null,
isSubmitting: false,
},
};
28 changes: 28 additions & 0 deletions src/app/core/store/user-emails/user-emails.selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Selector } from '@ngxs/store';

import { AccountEmailModel } from '@osf/shared/models';

import { UserEmailsStateModel } from './user-emails.model';
import { UserEmailsState } from './user-emails.state';

export class UserEmailsSelectors {
@Selector([UserEmailsState])
static getEmails(state: UserEmailsStateModel): AccountEmailModel[] {
return state.emails.data;
}

@Selector([UserEmailsState])
static isEmailsLoading(state: UserEmailsStateModel): boolean {
return state.emails.isLoading;
}

@Selector([UserEmailsState])
static isEmailsSubmitting(state: UserEmailsStateModel): boolean | undefined {
return state.emails.isSubmitting;
}

@Selector([UserEmailsState])
static getUnverifiedEmails(state: UserEmailsStateModel): AccountEmailModel[] {
return state.emails.data.filter((email) => email.confirmed && !email.verified);
}
}
Loading
Loading