File

libs/core-forms/src/lib/turnstile-component-plugin/turnstile.component.ts

Index

Properties

Properties

action (Optional)
Type string
appearance (Optional)
Type "always" | "execute" | "interaction-only"
callback (Optional)
Type function
cData (Optional)
Type string
error-callback (Optional)
Type function
expired-callback (Optional)
Type function
language (Optional)
Type string
sitekey
Type string
tabindex (Optional)
Type number
theme (Optional)
Type "light" | "dark" | "auto"
import { type ValidationConfig, APP_ROOT_ELEMENT_REF } 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 {
  afterNextRender,
  AfterViewInit,
  Component,
  computed,
  ElementRef,
  inject,
  Injector,
  LOCALE_ID,
  OnDestroy,
  Renderer2,
  signal,
  type Signal,
  ViewChild
} from '@angular/core';
import { Validators } from '@angular/forms';
import { LocalizeFn } from '@angular/localize/init';
import { AclService } from '@allianz/taly-acl/angular';
import { filter, Subject, switchMap } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { PfeBusinessService } from '@allianz/ngx-pfe';

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, 'action'>;

@Component({
  selector: 'df-turnstile-component',
  imports: [CommonModule, ValidationErrorsModule, DfBaseModule],
  templateUrl: './turnstile.component.html'
})
export class TurnstileComponent
  extends DfCustomComponent<TurnstileConfig>
  implements AfterViewInit, OnDestroy
{
  @ViewChild('turnstileContainer') turnstileContainer!: ElementRef<HTMLDivElement>;

  appRootElementRef = inject(APP_ROOT_ELEMENT_REF);
  private localeId: string = inject(LOCALE_ID);
  private aclService = inject(AclService);
  private pfeBusinessService = inject(PfeBusinessService);
  private scriptElement?: HTMLScriptElement;
  private injector = inject(Injector);
  private renderer = inject(Renderer2);
  private scriptLoaded$ = new Subject<void>();

  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
      ];
    });
  }

  ngAfterViewInit() {
    this.handleTurstileInitialization();
    this.addTurnstileScript();
  }

  ngOnDestroy() {
    if (this.scriptElement) {
      this.scriptElement.onload = null;
    }

    if (this.appRootElementRef.shadowRoot && this.turnstileContainer.nativeElement) {
      this.renderer.removeChild(
        this.appRootElementRef.nativeElement(),
        this.turnstileContainer.nativeElement
      );
    }
  }

  private addTurnstileScript() {
    if (window.turnstile) {
      this.scriptLoaded$.next();
      return;
    }

    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;
    this.scriptElement.onload = () => this.scriptLoaded$.next();

    document.head.appendChild(this.scriptElement);
  }

  private handleTurstileInitialization() {
    const aclPath = this.formAclPath() + this.aclResource;

    this.scriptLoaded$
      .pipe(
        switchMap(() => this.aclService.isHidden$(aclPath)),
        filter((hidden) => !hidden),
        switchMap(() =>
          this.pfeBusinessService.getObservableForExpressionKey('$._taly.turnstileSitekey', true)
        ),
        takeUntilDestroyed(this.destroyRef)
      )
      .subscribe((sitekey) =>
        afterNextRender(() => this.showTurnstile(sitekey), { injector: this.injector })
      );
  }

  private showTurnstile(sitekey?: string) {
    this.removeTurnstile();

    if (!sitekey) {
      console.error('Not able to initialize turnstile component because sitekey is missing.');
      return;
    }

    const config = this.config().config;
    const action = config?.action || '';

    if (window?.turnstile === undefined) return;

    if (this.appRootElementRef.shadowRoot) {
      this.renderer.appendChild(
        this.appRootElementRef.nativeElement(),
        this.turnstileContainer.nativeElement
      );
      this.renderer.setAttribute(this.turnstileContainer.nativeElement, 'slot', 'turnstile');
    }

    window.turnstile.ready(() => {
      window.turnstile.render(this.turnstileContainer.nativeElement, {
        language: this.localeId.toLocaleLowerCase(),
        action: action as string,
        sitekey: sitekey,
        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');
        }
      });
    });
  }

  private removeTurnstile() {
    if (this.turnstileContainer && this.turnstileContainer.nativeElement) {
      this.turnstileContainer.nativeElement.innerHTML = '';
    }
  }
}

results matching ""

    No results matching ""