diff --git a/src/app/features/profile/components/profile-information/profile-information.component.ts b/src/app/features/profile/components/profile-information/profile-information.component.ts index 3304a8ce2..382282167 100644 --- a/src/app/features/profile/components/profile-information/profile-information.component.ts +++ b/src/app/features/profile/components/profile-information/profile-information.component.ts @@ -9,6 +9,7 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { EducationHistoryComponent } from '@osf/shared/components/education-history/education-history.component'; import { EmploymentHistoryComponent } from '@osf/shared/components/employment-history/employment-history.component'; import { SOCIAL_LINKS } from '@osf/shared/constants/social-links.const'; +import { ExternalIdentityStatus } from '@osf/shared/enums/external-identity-status.enum'; import { IS_MEDIUM } from '@osf/shared/helpers/breakpoints.tokens'; import { UserModel } from '@osf/shared/models/user/user.models'; import { SortByDatePipe } from '@osf/shared/pipes/sort-by-date.pipe'; @@ -45,7 +46,7 @@ export class ProfileInformationComponent { orcidId = computed(() => { const orcid = this.currentUser()?.external_identity?.ORCID; - return orcid?.status?.toUpperCase() === 'VERIFIED' ? orcid.id : undefined; + return orcid?.status?.toUpperCase() === ExternalIdentityStatus.VERIFIED ? orcid.id : undefined; }); toProfileSettings() { diff --git a/src/app/features/settings/profile-settings/components/authenticated-identity/authenticated-identity.component.html b/src/app/features/settings/profile-settings/components/authenticated-identity/authenticated-identity.component.html new file mode 100644 index 000000000..0e10c22ea --- /dev/null +++ b/src/app/features/settings/profile-settings/components/authenticated-identity/authenticated-identity.component.html @@ -0,0 +1,39 @@ +
+
+

+ {{ 'settings.profileSettings.social.labels.authenticatedIdentity' | translate }} +

+
+
+
+ @if (existingOrcid()) { + + } @else { + + +

+ } +
+
+
diff --git a/src/app/features/settings/profile-settings/components/authenticated-identity/authenticated-identity.component.scss b/src/app/features/settings/profile-settings/components/authenticated-identity/authenticated-identity.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/settings/profile-settings/components/authenticated-identity/authenticated-identity.component.spec.ts b/src/app/features/settings/profile-settings/components/authenticated-identity/authenticated-identity.component.spec.ts new file mode 100644 index 000000000..5f2583462 --- /dev/null +++ b/src/app/features/settings/profile-settings/components/authenticated-identity/authenticated-identity.component.spec.ts @@ -0,0 +1,71 @@ +import { MockProvider } from 'ng-mocks'; + +import { signal } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AccountSettingsSelectors } from '@osf/features/settings/account-settings/store/account-settings.selectors'; +import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; + +import { AuthenticatedIdentityComponent } from './authenticated-identity.component'; + +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { + CustomConfirmationServiceMock, + CustomConfirmationServiceMockType, +} from '@testing/providers/custom-confirmation-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('AuthenticatedIdentityComponent', () => { + let component: AuthenticatedIdentityComponent; + let fixture: ComponentFixture; + let customConfirmationServiceMock: CustomConfirmationServiceMockType; + + const mockExternalIdentities = signal([ + { + id: 'ORCID', + externalId: '0001-0002-0003-0004', + status: 'VERIFIED', + }, + ]); + + beforeEach(async () => { + customConfirmationServiceMock = CustomConfirmationServiceMock.simple(); + await TestBed.configureTestingModule({ + imports: [AuthenticatedIdentityComponent, OSFTestingModule], + providers: [ + MockProvider(CustomConfirmationService, customConfirmationServiceMock), + provideMockStore({ + signals: [ + { + selector: AccountSettingsSelectors.getExternalIdentities, + value: mockExternalIdentities, + }, + ], + }), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(AuthenticatedIdentityComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should show existing user ORCID when present in external identities', () => { + expect(component.existingOrcid()).toEqual('0001-0002-0003-0004'); + expect(component.orcidUrl()).toEqual('https://orcid.org/0001-0002-0003-0004'); + component.disconnectOrcid(); + expect(customConfirmationServiceMock.confirmDelete).toHaveBeenCalled(); + }); + + it('should show connect button when no existing ORCID is present in external identities', () => { + mockExternalIdentities.set([]); + fixture.detectChanges(); + + expect(component.existingOrcid()).toBeUndefined(); + expect(component.orcidUrl()).toBeNull(); + }); +}); diff --git a/src/app/features/settings/profile-settings/components/authenticated-identity/authenticated-identity.component.ts b/src/app/features/settings/profile-settings/components/authenticated-identity/authenticated-identity.component.ts new file mode 100644 index 000000000..ee32a80c6 --- /dev/null +++ b/src/app/features/settings/profile-settings/components/authenticated-identity/authenticated-identity.component.ts @@ -0,0 +1,77 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { Tooltip } from 'primeng/tooltip'; + +import { finalize } from 'rxjs'; + +import { NgOptimizedImage } from '@angular/common'; +import { ChangeDetectionStrategy, Component, computed, inject, OnInit } from '@angular/core'; + +import { ExternalIdentityStatus } from '@osf/shared/enums/external-identity-status.enum'; +import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; +import { LoaderService } from '@osf/shared/services/loader.service'; +import { ToastService } from '@osf/shared/services/toast.service'; + +import { + AccountSettingsSelectors, + DeleteExternalIdentity, + GetExternalIdentities, +} from '../../../account-settings/store'; + +@Component({ + selector: 'osf-authenticated-identity', + imports: [NgOptimizedImage, Button, Tooltip, TranslatePipe], + templateUrl: './authenticated-identity.component.html', + styleUrl: './authenticated-identity.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AuthenticatedIdentityComponent implements OnInit { + private readonly customConfirmationService = inject(CustomConfirmationService); + private readonly toastService = inject(ToastService); + private readonly loaderService = inject(LoaderService); + + private readonly ORCID_PROVIDER = 'ORCID'; + + ngOnInit() { + this.actions.getExternalIdentities(); + } + + readonly actions = createDispatchMap({ + deleteExternalIdentity: DeleteExternalIdentity, + getExternalIdentities: GetExternalIdentities, + }); + + readonly externalIdentities = select(AccountSettingsSelectors.getExternalIdentities); + + readonly orcidUrl = computed(() => { + return this.existingOrcid() ? `https://orcid.org/${this.existingOrcid()}` : null; + }); + + readonly existingOrcid = computed( + (): string | undefined => + this.externalIdentities()?.find((i) => i.id === 'ORCID' && i.status === ExternalIdentityStatus.VERIFIED) + ?.externalId + ); + + disconnectOrcid(): void { + this.customConfirmationService.confirmDelete({ + headerKey: 'settings.accountSettings.connectedIdentities.deleteDialog.header', + messageParams: { name: this.ORCID_PROVIDER }, + messageKey: 'settings.accountSettings.connectedIdentities.deleteDialog.message', + onConfirm: () => { + this.loaderService.show(); + this.actions + .deleteExternalIdentity(this.ORCID_PROVIDER) + .pipe(finalize(() => this.loaderService.hide())) + .subscribe(() => this.toastService.showSuccess('settings.accountSettings.connectedIdentities.successDelete')); + }, + }); + } + + connectOrcid(): void { + /* no-op for now*/ + } +} diff --git a/src/app/features/settings/profile-settings/components/social/social.component.html b/src/app/features/settings/profile-settings/components/social/social.component.html index e34553fe1..777d4fd88 100644 --- a/src/app/features/settings/profile-settings/components/social/social.component.html +++ b/src/app/features/settings/profile-settings/components/social/social.component.html @@ -1,3 +1,5 @@ + +