-
Notifications
You must be signed in to change notification settings - Fork 86
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: display captcha component according to the related ICM captcha …
…service (#200) - introduces lazy components for captcha BREAKING CHANGE: feature toggles captchaV2 and captchaV3 are obsolete component ish-captcha is replaced by ish-lazy-captcha with mandatory topic input
- Loading branch information
Showing
31 changed files
with
500 additions
and
326 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
src/app/extensions/captcha/exports/captcha-exports.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { CommonModule } from '@angular/common'; | ||
import { NgModule } from '@angular/core'; | ||
|
||
import { SitekeyProviderService } from '../services/sitekey-provider/sitekey-provider.service'; | ||
|
||
import { LazyCaptchaComponent } from './captcha/lazy-captcha/lazy-captcha.component'; | ||
|
||
@NgModule({ | ||
imports: [CommonModule], | ||
declarations: [LazyCaptchaComponent], | ||
exports: [LazyCaptchaComponent], | ||
providers: [SitekeyProviderService], | ||
}) | ||
export class CaptchaExportsModule {} |
1 change: 1 addition & 0 deletions
1
src/app/extensions/captcha/exports/captcha/lazy-captcha/lazy-captcha.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<ng-template #anchor></ng-template> |
112 changes: 112 additions & 0 deletions
112
src/app/extensions/captcha/exports/captcha/lazy-captcha/lazy-captcha.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import { ComponentFixture, TestBed, async, fakeAsync, tick } from '@angular/core/testing'; | ||
import { FormControl, FormGroup } from '@angular/forms'; | ||
import { By } from '@angular/platform-browser'; | ||
import { RouterTestingModule } from '@angular/router/testing'; | ||
import { TranslateModule } from '@ngx-translate/core'; | ||
import { MockComponent } from 'ng-mocks'; | ||
import { RECAPTCHA_V3_SITE_KEY, RecaptchaComponent } from 'ng-recaptcha'; | ||
import { EMPTY, of } from 'rxjs'; | ||
import { anyString, instance, mock, when } from 'ts-mockito'; | ||
|
||
import { configurationReducer } from 'ish-core/store/configuration/configuration.reducer'; | ||
import { ngrxTesting } from 'ish-core/utils/dev/ngrx-testing'; | ||
|
||
import { CaptchaFacade } from '../../../facades/captcha.facade'; | ||
import { CaptchaV2Component, CaptchaV2ComponentModule } from '../../../shared/captcha/captcha-v2/captcha-v2.component'; | ||
import { CaptchaV3Component, CaptchaV3ComponentModule } from '../../../shared/captcha/captcha-v3/captcha-v3.component'; | ||
|
||
import { LazyCaptchaComponent } from './lazy-captcha.component'; | ||
|
||
describe('Lazy Captcha Component', () => { | ||
let fixture: ComponentFixture<LazyCaptchaComponent>; | ||
let component: LazyCaptchaComponent; | ||
let element: HTMLElement; | ||
let captchaFacade: CaptchaFacade; | ||
|
||
beforeEach(async(() => { | ||
captchaFacade = mock(CaptchaFacade); | ||
when(captchaFacade.captchaVersion$).thenReturn(EMPTY); | ||
when(captchaFacade.captchaSiteKey$).thenReturn(of('captchaSiteKeyASDF')); | ||
when(captchaFacade.captchaActive$(anyString())).thenReturn(of(true)); | ||
|
||
TestBed.configureTestingModule({ | ||
declarations: [MockComponent(RecaptchaComponent)], | ||
imports: [ | ||
CaptchaV2ComponentModule, | ||
CaptchaV3ComponentModule, | ||
RouterTestingModule, | ||
TranslateModule.forRoot(), | ||
ngrxTesting({ | ||
reducers: { configuration: configurationReducer }, | ||
}), | ||
], | ||
providers: [ | ||
{ provide: CaptchaFacade, useFactory: () => instance(captchaFacade) }, | ||
{ provide: RECAPTCHA_V3_SITE_KEY, useValue: 'captchaSiteKeyQWERTY' }, | ||
], | ||
}) | ||
.overrideModule(CaptchaV2ComponentModule, { set: { entryComponents: [CaptchaV2Component] } }) | ||
.overrideModule(CaptchaV3ComponentModule, { set: { entryComponents: [CaptchaV3Component] } }) | ||
.compileComponents(); | ||
})); | ||
|
||
beforeEach(() => { | ||
fixture = TestBed.createComponent(LazyCaptchaComponent); | ||
component = fixture.componentInstance; | ||
element = fixture.nativeElement; | ||
component.form = new FormGroup({ | ||
captcha: new FormControl(''), | ||
captchaAction: new FormControl(''), | ||
}); | ||
component.cssClass = 'd-none'; | ||
component.topic = 'register'; | ||
}); | ||
|
||
it('should be created', () => { | ||
expect(component).toBeTruthy(); | ||
expect(element).toBeTruthy(); | ||
expect(() => fixture.detectChanges()).not.toThrow(); | ||
}); | ||
|
||
it('should render v2 component when configured', fakeAsync(() => { | ||
when(captchaFacade.captchaVersion$).thenReturn(of(2 as 2)); | ||
fixture.detectChanges(); | ||
|
||
tick(500); | ||
expect(element).toMatchInlineSnapshot(`<ish-captcha-v2></ish-captcha-v2>`); | ||
const v2Cmp: CaptchaV2Component = fixture.debugElement.query(By.css('ish-captcha-v2'))?.componentInstance; | ||
expect(v2Cmp).toBeTruthy(); | ||
expect(v2Cmp.cssClass).toEqual('d-none'); | ||
})); | ||
it('should render v3 component when configured', fakeAsync(() => { | ||
when(captchaFacade.captchaVersion$).thenReturn(of(3 as 3)); | ||
fixture.detectChanges(); | ||
|
||
tick(500); | ||
expect(element).toMatchInlineSnapshot(`<ish-captcha-v3></ish-captcha-v3>`); | ||
const v3Cmp: CaptchaV3Component = fixture.debugElement.query(By.css('ish-captcha-v3'))?.componentInstance; | ||
expect(v3Cmp).toBeTruthy(); | ||
})); | ||
|
||
// errors are thrown if required input parameters are missing | ||
it('should throw an error if there is no form set as input parameter', () => { | ||
component.form = undefined; | ||
expect(() => fixture.detectChanges()).toThrowErrorMatchingInlineSnapshot( | ||
`"required input parameter <form> is missing for LazyCaptchaComponent"` | ||
); | ||
}); | ||
|
||
it('should throw an error if there is no control "captcha" in the given form', () => { | ||
delete component.form.controls.captcha; | ||
expect(() => fixture.detectChanges()).toThrowErrorMatchingInlineSnapshot( | ||
`"form control 'captcha' does not exist in the given form for LazyCaptchaComponent"` | ||
); | ||
}); | ||
|
||
it('should throw an error if there is no control "captchaAction" in the given form', () => { | ||
delete component.form.controls.captchaAction; | ||
expect(() => fixture.detectChanges()).toThrowErrorMatchingInlineSnapshot( | ||
`"form control 'captchaAction' does not exist in the given form for LazyCaptchaComponent"` | ||
); | ||
}); | ||
}); |
130 changes: 130 additions & 0 deletions
130
src/app/extensions/captcha/exports/captcha/lazy-captcha/lazy-captcha.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import { | ||
AfterViewInit, | ||
ChangeDetectionStrategy, | ||
Compiler, | ||
Component, | ||
Injector, | ||
Input, | ||
NgModuleFactory, | ||
OnInit, | ||
ViewChild, | ||
ViewContainerRef, | ||
} from '@angular/core'; | ||
import { AbstractControl, FormGroup, Validators } from '@angular/forms'; | ||
import { switchMapTo, take } from 'rxjs/operators'; | ||
|
||
import { whenTruthy } from 'ish-core/utils/operators'; | ||
|
||
import { CaptchaFacade } from '../../../facades/captcha.facade'; | ||
|
||
/** | ||
* The Captcha Component | ||
* | ||
* Displays a captcha form control (V2) or widget (V3) if the captchaV2 or the captchaV3 feature is enabled. | ||
* It expects the given form to have the form controls for the captcha (controlName) and the captcha action (actionControlName). | ||
* If the captcha is confirmed the captcha form control contains the captcha response token provided by the captcha service. | ||
* | ||
* The parent form supplied must have controls for 'captcha' and 'captchaAction' | ||
* | ||
* @example | ||
* <ish-lazy-captcha [form]="form" cssClass="offset-md-2 col-md-8" topic="contactUs"></ish-lazy-captcha> | ||
*/ | ||
@Component({ | ||
selector: 'ish-lazy-captcha', | ||
templateUrl: './lazy-captcha.component.html', | ||
changeDetection: ChangeDetectionStrategy.Default, | ||
}) | ||
export class LazyCaptchaComponent implements OnInit, AfterViewInit { | ||
@ViewChild('anchor', { read: ViewContainerRef, static: true }) anchor: ViewContainerRef; | ||
|
||
/** | ||
form containing the captcha form controls | ||
*/ | ||
@Input() form: FormGroup; | ||
|
||
/** | ||
css Class for rendering the captcha V2 control, default='offset-md-4 col-md-8' | ||
*/ | ||
@Input() cssClass = 'offset-md-4 col-md-8'; | ||
|
||
@Input() topic: | ||
| 'contactUs' | ||
| 'emailShoppingCart' | ||
| 'forgotPassword' | ||
| 'redemptionOfGiftCardsAndCertificates' | ||
| 'register'; | ||
|
||
constructor(private captchaFacade: CaptchaFacade, private compiler: Compiler, private injector: Injector) {} | ||
|
||
ngOnInit() { | ||
this.sanityCheck(); | ||
} | ||
ngAfterViewInit() { | ||
this.captchaFacade | ||
.captchaActive$(this.topic) | ||
.pipe(whenTruthy(), switchMapTo(this.captchaFacade.captchaVersion$), whenTruthy(), take(1)) | ||
.subscribe(async version => { | ||
if (version === 3) { | ||
this.actionFormControl.setValue(this.topic); | ||
|
||
const { CaptchaV3Component, CaptchaV3ComponentModule } = await import( | ||
'../../../shared/captcha/captcha-v3/captcha-v3.component' | ||
); | ||
const moduleFactory = await this.loadModuleFactory(CaptchaV3ComponentModule); | ||
const moduleRef = moduleFactory.create(this.injector); | ||
const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(CaptchaV3Component); | ||
|
||
const componentRef = this.anchor.createComponent(factory); | ||
|
||
componentRef.instance.parentForm = this.form; | ||
} else if (version === 2) { | ||
this.formControl.setValidators([Validators.required]); | ||
this.formControl.updateValueAndValidity(); | ||
|
||
const { CaptchaV2Component, CaptchaV2ComponentModule } = await import( | ||
'../../../shared/captcha/captcha-v2/captcha-v2.component' | ||
); | ||
const moduleFactory = await this.loadModuleFactory(CaptchaV2ComponentModule); | ||
const moduleRef = moduleFactory.create(this.injector); | ||
const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(CaptchaV2Component); | ||
|
||
const componentRef = this.anchor.createComponent(factory); | ||
|
||
componentRef.instance.cssClass = this.cssClass; | ||
componentRef.instance.parentForm = this.form; | ||
} | ||
}); | ||
} | ||
|
||
// tslint:disable-next-line: no-any | ||
private async loadModuleFactory(t: any) { | ||
if (t instanceof NgModuleFactory) { | ||
return t; | ||
} else { | ||
return await this.compiler.compileModuleAsync(t); | ||
} | ||
} | ||
|
||
private sanityCheck() { | ||
if (!this.form) { | ||
throw new Error('required input parameter <form> is missing for LazyCaptchaComponent'); | ||
} | ||
if (!this.formControl) { | ||
throw new Error(`form control 'captcha' does not exist in the given form for LazyCaptchaComponent`); | ||
} | ||
if (!this.actionFormControl) { | ||
throw new Error(`form control 'captchaAction' does not exist in the given form for LazyCaptchaComponent`); | ||
} | ||
if (!this.topic) { | ||
throw new Error(`required input püarameter <topic> is missing for LazyCaptchaComponent`); | ||
} | ||
} | ||
|
||
private get formControl(): AbstractControl { | ||
return this.form?.get('captcha'); | ||
} | ||
|
||
private get actionFormControl(): AbstractControl { | ||
return this.form?.get('captchaAction'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { Injectable } from '@angular/core'; | ||
import { Store, select } from '@ngrx/store'; | ||
import { Observable } from 'rxjs'; | ||
import { filter, map, switchMap, switchMapTo } from 'rxjs/operators'; | ||
|
||
import { getServerConfigParameter } from 'ish-core/store/configuration'; | ||
import { whenTruthy } from 'ish-core/utils/operators'; | ||
|
||
// tslint:disable:member-ordering | ||
@Injectable({ providedIn: 'root' }) | ||
export class CaptchaFacade { | ||
constructor(private store: Store) {} | ||
|
||
captchaVersion$: Observable<2 | 3 | undefined> = this.store.pipe( | ||
select( | ||
getServerConfigParameter<{ | ||
ReCaptchaV2ServiceDefinition: { runnable: boolean }; | ||
ReCaptchaV3ServiceDefinition: { runnable: boolean }; | ||
}>('services') | ||
), | ||
whenTruthy(), | ||
map(services => | ||
services.ReCaptchaV3ServiceDefinition && services.ReCaptchaV3ServiceDefinition.runnable | ||
? 3 | ||
: services.ReCaptchaV2ServiceDefinition && services.ReCaptchaV2ServiceDefinition.runnable | ||
? 2 | ||
: undefined | ||
) | ||
); | ||
|
||
captchaSiteKey$ = this.captchaVersion$.pipe( | ||
whenTruthy(), | ||
switchMap(version => | ||
this.store.pipe( | ||
select(getServerConfigParameter<string>(`services.ReCaptchaV${version}ServiceDefinition.SiteKey`)), | ||
whenTruthy() | ||
) | ||
) | ||
); | ||
|
||
/** | ||
* @param key feature name according to the captcha ICM configuration, e.g. register, forgotPassword, contactUs | ||
*/ | ||
captchaActive$(key: string): Observable<boolean> { | ||
return this.store.pipe( | ||
filter(() => !!key), | ||
switchMapTo(this.captchaVersion$), | ||
map(x => !!x), | ||
whenTruthy() | ||
/* TODO: replace with the following when configuration REST API sends correct values | ||
select(getServerConfigParameter<boolean>('captcha.' + key)) | ||
*/ | ||
); | ||
} | ||
} |
Oops, something went wrong.