File
turnstile
|
Type
|
literal type
|
import type { ValidationConfig } from '@allianz/taly-core';
import { DfBaseModule, DfCustomComponent } from '@allianz/taly-core/dynamic-form';
import { ValidationErrorsModule } from '@allianz/taly-core/validation-errors';
import { CommonModule } from '@angular/common';
import {
AfterViewInit,
Component,
computed,
inject,
LOCALE_ID,
signal,
type Signal
} from '@angular/core';
import { Validators } from '@angular/forms';
import { LocalizeFn } from '@angular/localize/init';
import { AclService } from '@allianz/taly-acl/angular';
import { skip } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
declare let $localize: LocalizeFn;
declare global {
interface Window {
onloadTurnstileCallback: () => void;
turnstile: {
render: (idOrContainer: string | HTMLElement, options: TurnstileOptions) => string;
reset: (widgetIdOrContainer: string | HTMLElement) => void;
getResponse: (widgetIdOrContainer: string | HTMLElement) => string | undefined;
remove: (widgetIdOrContainer: string | HTMLElement) => void;
ready: (callback: () => void) => void;
};
}
}
// Interface extracted of the Turnstile documentation:
// https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#configurations
interface TurnstileOptions {
sitekey: string;
action?: string;
cData?: string;
callback?: (token: string) => void;
'error-callback'?: (errorCode: string) => void;
'expired-callback'?: () => void;
theme?: 'light' | 'dark' | 'auto';
language?: string;
tabindex?: number;
appearance?: 'always' | 'execute' | 'interaction-only';
}
export type TurnstileConfig = Pick<TurnstileOptions, 'sitekey' | 'action'>;
@Component({
selector: 'df-turnstile-component',
standalone: true,
imports: [CommonModule, ValidationErrorsModule, DfBaseModule],
template: `<ng-container *aclTag="aclResource">
<div id="js-turnstile-container"></div>
<taly-validation-errors
*ngIf="control()?.touched"
ngProjectAs="nx-error"
nxFormfieldError
[errorMessages]="enrichedValidationConfigs()"
[controlErrors]="control()?.errors"
>
</taly-validation-errors>
</ng-container>`
})
export class TurnstileComponent
extends DfCustomComponent<TurnstileConfig>
implements AfterViewInit
{
private localeId: string = inject(LOCALE_ID);
private aclService = inject(AclService);
private scriptElement?: HTMLScriptElement;
protected enrichedValidationConfigs: Signal<ValidationConfig[]> = signal([]);
constructor() {
super();
this.control()?.addValidators(Validators.required);
this.enrichedValidationConfigs = computed(() => {
const requiredValidationConfig: ValidationConfig = {
validator: Validators.required,
validatorName: 'required',
errorMessage: $localize`:@@validation.error.required:This field is required!`
};
const turnstileErrorValidationConfig: ValidationConfig = {
validatorName: 'turnstileError',
errorMessage: $localize`:@@validation.error.turnstile-error:An unexpected error occurred. Please try again later.`
} as unknown as ValidationConfig;
return [
...(this.validationConfigs() || []),
requiredValidationConfig,
turnstileErrorValidationConfig
];
});
this.addTurnstileScript();
}
ngAfterViewInit() {
const aclPath = this.formAclPath() + this.aclResource;
this.aclService
.isHidden$(aclPath)
// Skip first emission from isHidden$ as addTurnstileScript() already handles initial rendering of Turnstile after script loading
.pipe(skip(1), takeUntilDestroyed(this.destroyRef))
.subscribe((isHidden) => {
if (!isHidden) {
this.showTurnstile();
}
});
}
private addTurnstileScript() {
this.scriptElement = document.createElement('script');
this.scriptElement.src =
'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
this.scriptElement.async = false;
this.scriptElement.defer = false;
// setTimeout ensures Turnstile API is fully initialized before accessing it, preventing race conditions
this.scriptElement.onload = () => {
setTimeout(() => {
const aclPath = this.formAclPath() + this.aclResource;
if (!this.aclService.isHidden(aclPath)) {
this.showTurnstile();
}
}, 0);
};
document.head.appendChild(this.scriptElement);
}
private showTurnstile() {
const config = this.config().config;
if (!config?.sitekey) {
console.error(
'Could not initialize turnstile component. Please provide a sitekey in the config.'
);
return;
}
const sitekey = config.sitekey;
const action = config.action || '';
if (window?.turnstile === undefined) return;
window.turnstile.ready(() => {
window.turnstile.render('#js-turnstile-container', {
language: this.localeId.toLocaleLowerCase(),
action: action as string,
sitekey: sitekey as string,
callback: (token) => {
this.control()?.setValue(token);
},
'error-callback': (errorCode) => {
this.control()?.setValue(null);
this.control()?.setErrors({ turnstileError: true });
console.error(
'Turnstile Error. Open https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes/ for more details on this error code: ',
errorCode
);
},
'expired-callback': () => {
this.control()?.setValue(null);
this.control()?.setErrors({ turnstileError: true });
console.error('Turnstile token expired and did not reset the widget');
}
});
});
}
}