From 7565fc807c3d1c074e2e5c8ae59bdcb2212deabb Mon Sep 17 00:00:00 2001 From: Piotr Bochenek Date: Thu, 26 Oct 2023 19:54:42 +0200 Subject: [PATCH] 18 descope component poc (#27) * initial component * onSuccess, onFailure events * full api * add base headers to sdk * fix lint and test * better layout for demo app * fix for afterRequestHooks * dont create webcomponent after each change * fix tests --------- Co-authored-by: Piotr Bochenek --- .eslintrc.json | 1 - projects/angular-sdk/.eslintrc.json | 5 +- projects/angular-sdk/src/lib/constants.ts | 6 + .../src/lib/descope-auth.module.ts | 7 +- .../src/lib/descope-auth.service.ts | 4 +- .../src/lib/descope/descope.component.spec.ts | 48 ++++++++ .../src/lib/descope/descope.component.ts | 109 ++++++++++++++++++ projects/angular-sdk/src/public-api.ts | 1 + projects/demo-app/src/app/app.component.html | 20 ++-- projects/demo-app/src/app/app.component.scss | 16 +-- .../demo-app/src/app/home/home.component.html | 11 ++ .../demo-app/src/app/home/home.component.scss | 5 + .../src/app/home/home.component.spec.ts | 2 + .../demo-app/src/app/home/home.component.ts | 17 +++ tsconfig.json | 1 + 15 files changed, 230 insertions(+), 23 deletions(-) create mode 100644 projects/angular-sdk/src/lib/constants.ts create mode 100644 projects/angular-sdk/src/lib/descope/descope.component.spec.ts create mode 100644 projects/angular-sdk/src/lib/descope/descope.component.ts diff --git a/.eslintrc.json b/.eslintrc.json index 0cdf0de..b9c7f97 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -23,7 +23,6 @@ "error", { "type": "element", - "prefix": "lib", "style": "kebab-case" } ] diff --git a/projects/angular-sdk/.eslintrc.json b/projects/angular-sdk/.eslintrc.json index 2ccb471..a3fe19e 100644 --- a/projects/angular-sdk/.eslintrc.json +++ b/projects/angular-sdk/.eslintrc.json @@ -17,10 +17,11 @@ "error", { "type": "element", - "prefix": "lib", + "prefix": "", "style": "kebab-case" } - ] + ], + "@angular-eslint/no-output-native": "off" } }, { diff --git a/projects/angular-sdk/src/lib/constants.ts b/projects/angular-sdk/src/lib/constants.ts new file mode 100644 index 0000000..dbcc3d7 --- /dev/null +++ b/projects/angular-sdk/src/lib/constants.ts @@ -0,0 +1,6 @@ +// declare const BUILD_VERSION: string; + +export const baseHeaders = { + 'x-descope-sdk-name': 'angular', + 'x-descope-sdk-version': '1.1.1' +}; diff --git a/projects/angular-sdk/src/lib/descope-auth.module.ts b/projects/angular-sdk/src/lib/descope-auth.module.ts index a38978e..2fa4f87 100644 --- a/projects/angular-sdk/src/lib/descope-auth.module.ts +++ b/projects/angular-sdk/src/lib/descope-auth.module.ts @@ -1,9 +1,11 @@ import { + CUSTOM_ELEMENTS_SCHEMA, ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; +import { DescopeComponent } from './descope/descope.component'; export class DescopeAuthConfig { projectId = ''; @@ -11,7 +13,9 @@ export class DescopeAuthConfig { @NgModule({ imports: [], - exports: [] + schemas: [CUSTOM_ELEMENTS_SCHEMA], + exports: [DescopeComponent], + declarations: [DescopeComponent] }) export class DescopeAuthModule { constructor(@Optional() @SkipSelf() parentModule?: DescopeAuthModule) { @@ -21,6 +25,7 @@ export class DescopeAuthModule { ); } } + static forRoot( config?: DescopeAuthConfig ): ModuleWithProviders { diff --git a/projects/angular-sdk/src/lib/descope-auth.service.ts b/projects/angular-sdk/src/lib/descope-auth.service.ts index 10db982..5ca566e 100644 --- a/projects/angular-sdk/src/lib/descope-auth.service.ts +++ b/projects/angular-sdk/src/lib/descope-auth.service.ts @@ -4,6 +4,7 @@ import type { UserResponse } from '@descope/web-js-sdk'; import createSdk from '@descope/web-js-sdk'; import { BehaviorSubject, finalize, Observable, tap } from 'rxjs'; import { observabilify, Observablefied } from './helpers'; +import { baseHeaders } from './constants'; type DescopeSDK = ReturnType; type AngularDescopeSDK = Observablefied; @@ -39,7 +40,8 @@ export class DescopeAuthService { createSdk({ ...config, persistTokens: true, - autoRefresh: true + autoRefresh: true, + baseHeaders }) ); this.sdk.onSessionTokenChange(this.setSession.bind(this)); diff --git a/projects/angular-sdk/src/lib/descope/descope.component.spec.ts b/projects/angular-sdk/src/lib/descope/descope.component.spec.ts new file mode 100644 index 0000000..d401786 --- /dev/null +++ b/projects/angular-sdk/src/lib/descope/descope.component.spec.ts @@ -0,0 +1,48 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DescopeComponent } from './descope.component'; +import { DescopeAuthConfig } from '../descope-auth.module'; +import createSdk from '@descope/web-js-sdk'; +import mocked = jest.mocked; + +jest.mock('@descope/web-js-sdk'); +//Mock DescopeWebComponent +jest.mock('@descope/web-component', () => { + return jest.fn(); +}); + +describe('DescopeComponent', () => { + let component: DescopeComponent; + let fixture: ComponentFixture; + let mockedCreateSdk: jest.Mock; + const onSessionTokenChangeSpy = jest.fn(); + const onUserChangeSpy = jest.fn(); + const mockConfig: DescopeAuthConfig = { + projectId: 'someProject' + }; + + beforeEach(() => { + mockedCreateSdk = mocked(createSdk); + + mockedCreateSdk.mockReturnValue({ + onSessionTokenChange: onSessionTokenChangeSpy, + onUserChange: onUserChangeSpy + }); + + TestBed.configureTestingModule({ + declarations: [DescopeComponent], + providers: [ + DescopeAuthConfig, + { provide: DescopeAuthConfig, useValue: mockConfig } + ] + }); + + fixture = TestBed.createComponent(DescopeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/angular-sdk/src/lib/descope/descope.component.ts b/projects/angular-sdk/src/lib/descope/descope.component.ts new file mode 100644 index 0000000..93725df --- /dev/null +++ b/projects/angular-sdk/src/lib/descope/descope.component.ts @@ -0,0 +1,109 @@ +import { + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + Output +} from '@angular/core'; +import DescopeWebComponent from '@descope/web-component'; +import DescopeWc, { ILogger } from '@descope/web-component'; +import { DescopeAuthService } from '../descope-auth.service'; +import { from } from 'rxjs'; +import { baseHeaders } from '../constants'; + +@Component({ + selector: 'descope[projectId][flowId]', + template: '' +}) +export class DescopeComponent implements OnChanges { + @Input() projectId: string; + @Input() flowId: string; + + @Input() locale: string; + @Input() theme: 'light' | 'dark' | 'os'; + @Input() tenant: string; + @Input() telemetryKey: string; + @Input() redirectUrl: string; + @Input() autoFocus: true | false | 'skipFirstScreen'; + + @Input() debug: boolean; + @Input() errorTransformer: (error: { text: string; type: string }) => string; + @Input() logger: ILogger; + + @Output() success: EventEmitter = new EventEmitter(); + @Output() error: EventEmitter = new EventEmitter(); + + private readonly webComponent: DescopeWebComponent; + + constructor( + private elementRef: ElementRef, + private authService: DescopeAuthService + ) { + DescopeWc.sdkConfigOverrides = { baseHeaders }; + this.webComponent = new DescopeWebComponent(); + } + ngOnChanges(): void { + this.setupWebComponent(); + } + + private setupWebComponent() { + this.webComponent.setAttribute('project-id', this.projectId); + this.webComponent.setAttribute('flow-id', this.flowId); + if (this.locale) { + this.webComponent.setAttribute('locale', this.locale); + } + if (this.theme) { + this.webComponent.setAttribute('theme', this.theme); + } + if (this.tenant) { + this.webComponent.setAttribute('tenant', this.tenant); + } + if (this.telemetryKey) { + this.webComponent.setAttribute('telemetryKey', this.telemetryKey); + } + if (this.redirectUrl) { + this.webComponent.setAttribute('redirect-url', this.redirectUrl); + } + if (this.autoFocus) { + this.webComponent.setAttribute('auto-focus', this.autoFocus.toString()); + } + if (this.debug) { + this.webComponent.setAttribute('debug', this.debug.toString()); + } + + if (this.errorTransformer) { + this.webComponent.errorTransformer = this.errorTransformer; + } + + if (this.logger) { + this.webComponent.logger = this.logger; + } + + if (this.success) { + this.webComponent.addEventListener('success', () => { + from( + this.authService.sdk.httpClient.hooks?.afterRequest!( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + new Response(JSON.stringify({})) + ) as Promise + ).subscribe(() => { + this.success?.emit(); + }); + }); + } + + if (this.error) { + this.webComponent.addEventListener('error', () => { + this.error?.emit(); + }); + } + + const nativeElement = this.elementRef.nativeElement; + while (nativeElement.lastElementChild) { + nativeElement.removeChild(nativeElement.lastElementChild); + } + nativeElement.appendChild(this.webComponent); + } +} diff --git a/projects/angular-sdk/src/public-api.ts b/projects/angular-sdk/src/public-api.ts index 0f6c6c4..d099f18 100644 --- a/projects/angular-sdk/src/public-api.ts +++ b/projects/angular-sdk/src/public-api.ts @@ -5,3 +5,4 @@ export * from './lib/descope-auth.service'; export * from './lib/descope-auth.guard'; export * from './lib/descope-auth.module'; +export * from './lib/descope/descope.component'; diff --git a/projects/demo-app/src/app/app.component.html b/projects/demo-app/src/app/app.component.html index dbed1b0..0dfca23 100644 --- a/projects/demo-app/src/app/app.component.html +++ b/projects/demo-app/src/app/app.component.html @@ -1,13 +1,13 @@ -
-
- {{ session$ | async | json }} -
-
- {{ user$ | async | json }} -
-
- - +
+
+ +
{{ session$ | async | json }}
+
+
+ +
{{ user$ | async | json }}
+
+
diff --git a/projects/demo-app/src/app/app.component.scss b/projects/demo-app/src/app/app.component.scss index a823c9d..b24db30 100644 --- a/projects/demo-app/src/app/app.component.scss +++ b/projects/demo-app/src/app/app.component.scss @@ -1,4 +1,8 @@ -header { +main { + padding-top: 4rem; +} + +.card-wrapper { display: flex; align-items: center; justify-content: center; @@ -16,13 +20,9 @@ header { word-wrap: break-word; font-family: 'Arial', serif; margin: 2rem; -} - -main { - display: flex; - flex-direction: column; - align-items: center; button { - margin-bottom: 1rem; + width: 100%; + margin: auto; } + overflow: scroll; } diff --git a/projects/demo-app/src/app/home/home.component.html b/projects/demo-app/src/app/home/home.component.html index 9e3327b..4a2469d 100644 --- a/projects/demo-app/src/app/home/home.component.html +++ b/projects/demo-app/src/app/home/home.component.html @@ -1,2 +1,13 @@ +
+ + +
+ diff --git a/projects/demo-app/src/app/home/home.component.scss b/projects/demo-app/src/app/home/home.component.scss index be25e3c..ed83f16 100644 --- a/projects/demo-app/src/app/home/home.component.scss +++ b/projects/demo-app/src/app/home/home.component.scss @@ -6,3 +6,8 @@ flex-direction: column; gap: 20px; } + +.theme-wrapper { + display: flex; + gap: 20px; +} diff --git a/projects/demo-app/src/app/home/home.component.spec.ts b/projects/demo-app/src/app/home/home.component.spec.ts index 79b5f83..111c61c 100644 --- a/projects/demo-app/src/app/home/home.component.spec.ts +++ b/projects/demo-app/src/app/home/home.component.spec.ts @@ -4,6 +4,7 @@ import { HomeComponent } from './home.component'; import { DescopeAuthConfig } from '../../../../angular-sdk/src/lib/descope-auth.module'; import createSdk from '@descope/web-js-sdk'; import mocked = jest.mocked; +import { NO_ERRORS_SCHEMA } from '@angular/core'; jest.mock('@descope/web-js-sdk'); @@ -23,6 +24,7 @@ describe('HomeComponent', () => { }); TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], declarations: [HomeComponent], providers: [ DescopeAuthConfig, diff --git a/projects/demo-app/src/app/home/home.component.ts b/projects/demo-app/src/app/home/home.component.ts index 8b2dd8b..8680793 100644 --- a/projects/demo-app/src/app/home/home.component.ts +++ b/projects/demo-app/src/app/home/home.component.ts @@ -1,6 +1,7 @@ import { Component } from '@angular/core'; import { DescopeAuthService } from '../../../../angular-sdk/src/lib/descope-auth.service'; import { Router } from '@angular/router'; +import { environment } from '../../environments/environment'; @Component({ selector: 'app-home', @@ -8,6 +9,9 @@ import { Router } from '@angular/router'; styleUrls: ['./home.component.scss'] }) export class HomeComponent { + projectId: string = environment.descopeProjectId; + theme: 'light' | 'dark' = 'light'; + constructor( private router: Router, private authService: DescopeAuthService @@ -55,4 +59,17 @@ export class HomeComponent { } }); } + + onSuccess() { + console.log('SUCCESSFULLY LOGGED IN FROM WEB COMPONENT'); + this.router.navigate(['/protected']).catch((err) => console.error(err)); + } + + onError() { + console.log('ERROR FROM LOG IN FLOW FROM WEB COMPONENT'); + } + + changeTheme(theme: 'light' | 'dark') { + this.theme = theme; + } } diff --git a/tsconfig.json b/tsconfig.json index 51b3487..fba3ad7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, + "strictPropertyInitialization": false, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true,