import * as i0 from '@angular/core'; import { Injectable, EventEmitter, InjectFlags, Directive, Input, Output, forwardRef, Component, ViewChild, NgModule } from '@angular/core'; import { NgControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { CommonModule } from '@angular/common'; class ScriptService { constructor(zone) { this.zone = zone; this.scriptElemId = "ngx-catpcha-script"; /** * Name of the global google recaptcha script */ this.windowGrecaptcha = "grecaptcha"; /** * Name of enterpise property in the global google recaptcha script */ this.windowGrecaptchaEnterprise = "enterprise"; /** * Name of the global callback */ this.windowOnLoadCallbackProperty = "ngx_captcha_onload_callback"; /** * Name of the global callback for enterprise */ this.windowOnLoadEnterpriseCallbackProperty = "ngx_captcha_onload_enterprise_callback"; this.globalDomain = "recaptcha.net"; this.defaultDomain = "google.com"; this.enterpriseApi = "enterprise.js"; this.defaultApi = "api.js"; } registerCaptchaScript(config, render, onLoad, language) { if (this.grecaptchaScriptLoaded(config.useEnterprise)) { // recaptcha script is already loaded // just call the callback if (config.useEnterprise) { this.zone.run(() => { onLoad(window[this.windowGrecaptcha][this.windowGrecaptchaEnterprise]); }); } else { this.zone.run(() => { onLoad(window[this.windowGrecaptcha]); }); } return; } // we need to patch the callback through global variable, otherwise callback is not accessible // note: https://github.com/Enngage/ngx-captcha/issues/2 if (config.useEnterprise) { window[this.getCallbackName(true)] = ((() => this.zone.run(onLoad.bind(this, window[this.windowGrecaptcha][this.windowGrecaptchaEnterprise])))); } else { window[this.getCallbackName(false)] = ((() => this.zone.run(onLoad.bind(this, window[this.windowGrecaptcha])))); } // prepare script elem const scriptElem = document.createElement("script"); scriptElem.id = this.scriptElemId; scriptElem.innerHTML = ""; scriptElem.src = this.getCaptchaScriptUrl(config, render, language); scriptElem.async = true; scriptElem.defer = true; // add script to header document.getElementsByTagName("head")[0].appendChild(scriptElem); } cleanup() { const elem = document.getElementById(this.scriptElemId); if (elem) { elem.remove(); } window[this.getCallbackName()] = undefined; window[this.windowGrecaptcha] = undefined; } /** * Indicates if google recaptcha script is available and ready to be used */ grecaptchaScriptLoaded(useEnterprise) { if (!window[this.getCallbackName(useEnterprise)] || !window[this.windowGrecaptcha]) { return false; } else if (useEnterprise && window[this.windowGrecaptcha][this.windowGrecaptchaEnterprise]) { return true; // if only enterprise script is loaded we need to check some v3's method } else if (window[this.windowGrecaptcha].execute) { return true; } return false; } /** * Gets global callback name * @param useEnterprise Optional flag for enterprise script * @private */ getCallbackName(useEnterprise) { return useEnterprise ? this.windowOnLoadEnterpriseCallbackProperty : this.windowOnLoadCallbackProperty; } /** * Gets language param used in script url */ getLanguageParam(hl) { if (!hl) { return ""; } return `&hl=${hl}`; } /** * Url to google api script */ getCaptchaScriptUrl(config, render, language) { const domain = config.useGlobalDomain ? this.globalDomain : this.defaultDomain; const api = config.useEnterprise ? this.enterpriseApi : this.defaultApi; const callback = this.getCallbackName(config.useEnterprise); return `https://www.${domain}/recaptcha/${api}?onload=${callback}&render=${render}${this.getLanguageParam(language)}`; } } /** @nocollapse */ ScriptService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.4", ngImport: i0, type: ScriptService, deps: [{ token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ ScriptService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.0.4", ngImport: i0, type: ScriptService, providedIn: "root" }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.4", ngImport: i0, type: ScriptService, decorators: [{ type: Injectable, args: [{ providedIn: "root", }] }], ctorParameters: function () { return [{ type: i0.NgZone }]; } }); class BaseReCaptchaComponentDirective { constructor(renderer, zone, injector, scriptService) { this.renderer = renderer; this.zone = zone; this.injector = injector; this.scriptService = scriptService; /** * Prefix of the captcha element */ this.captchaElemPrefix = "ngx_captcha_id_"; this.setupCaptcha = true; /** * Indicates if global domain 'recaptcha.net' should be used instead of default domain ('google.com') */ this.useGlobalDomain = false; this.useEnterprise = false; /** * Type */ this.type = "image"; /** * Tab index */ this.tabIndex = 0; /** * Called when captcha receives successful response. * Captcha response token is passed to event. */ this.success = new EventEmitter(); /** * Called when captcha is loaded. Event receives id of the captcha */ this.load = new EventEmitter(); /** * Called when captcha is reset. */ this.reset = new EventEmitter(); /** * Called when captcha is loaded & ready. I.e. when you need to execute captcha on component load. */ this.ready = new EventEmitter(); /** * Error callback */ this.error = new EventEmitter(); /** * Expired callback */ this.expire = new EventEmitter(); /** * Indicates if captcha should be set on load */ this.setupAfterLoad = false; /** * If enabled, captcha will reset after receiving success response. This is useful * when invisible captcha need to be resolved multiple times on same page */ this.resetCaptchaAfterSuccess = false; /** * Required by ControlValueAccessor */ this.onChange = (val) => { }; this.onTouched = (val) => { }; /** * Indicates if captcha is loaded */ this.isLoaded = false; } ngAfterViewInit() { this.control = this.injector.get(NgControl, undefined, InjectFlags.Optional)?.control; } ngAfterViewChecked() { if (this.setupCaptcha) { this.setupCaptcha = false; this.setupComponent(); } } ngOnChanges(changes) { // cleanup scripts if language changed because they need to be reloaded if (changes && changes.hl) { // cleanup scripts when language changes if (!changes.hl.firstChange && changes.hl.currentValue !== changes.hl.previousValue) { this.scriptService.cleanup(); } } if (changes && changes.useGlobalDomain) { // cleanup scripts when domain changes if (!changes.useGlobalDomain.firstChange && changes.useGlobalDomain.currentValue !== changes.useGlobalDomain.previousValue) { this.scriptService.cleanup(); } } this.setupCaptcha = true; } /** * Gets captcha response as per reCaptcha docs */ getResponse() { return this.reCaptchaApi.getResponse(this.captchaId); } /** * Gets Id of captcha widget */ getCaptchaId() { return this.captchaId; } /** * Resets captcha */ resetCaptcha() { this.zone.run(() => { // reset captcha using Google js api this.reCaptchaApi.reset(); // required due to forms this.onChange(undefined); this.onTouched(undefined); // trigger reset event this.reset.next(); }); } /** * Gets last submitted captcha response */ getCurrentResponse() { return this.currentResponse; } /** * Reload captcha. Useful when properties (i.e. theme) changed and captcha need to reflect them */ reloadCaptcha() { this.setupComponent(); } ensureCaptchaElem(captchaElemId) { const captchaElem = document.getElementById(captchaElemId); if (!captchaElem) { throw Error(`Captcha element with id '${captchaElemId}' was not found`); } // assign captcha alem this.captchaElem = captchaElem; } /** * Responsible for instantiating captcha element */ renderReCaptcha() { // run outside angular zone due to timeout issues when testing // details: https://github.com/Enngage/ngx-captcha/issues/26 this.zone.runOutsideAngular(() => { // to fix reCAPTCHA placeholder element must be an element or id // https://github.com/Enngage/ngx-captcha/issues/96 setTimeout(() => { this.captchaId = this.reCaptchaApi.render(this.captchaElemId, this.getCaptchaProperties()); this.ready.next(); }, 0); }); } /** * Called when captcha receives response * @param callback Callback */ handleCallback(callback) { this.currentResponse = callback; this.success.next(callback); this.zone.run(() => { this.onChange(callback); this.onTouched(callback); }); if (this.resetCaptchaAfterSuccess) { this.resetCaptcha(); } } getPseudoUniqueNumber() { return new Date().getUTCMilliseconds() + Math.floor(Math.random() * 9999); } setupComponent() { // captcha specific setup this.captchaSpecificSetup(); // create captcha wrapper this.createAndSetCaptchaElem(); this.scriptService.registerCaptchaScript({ useGlobalDomain: this.useGlobalDomain, useEnterprise: this.useEnterprise, }, "explicit", (grecaptcha) => { this.onloadCallback(grecaptcha); }, this.hl); } /** * Called when google's recaptcha script is ready */ onloadCallback(grecapcha) { // assign reference to reCaptcha Api once its loaded this.reCaptchaApi = grecapcha; if (!this.reCaptchaApi) { throw Error(`ReCaptcha Api was not initialized correctly`); } // loaded flag this.isLoaded = true; // fire load event this.load.next(); // render captcha this.renderReCaptcha(); // setup component if it was flagged as such if (this.setupAfterLoad) { this.setupAfterLoad = false; this.setupComponent(); } } generateNewElemId() { return this.captchaElemPrefix + this.getPseudoUniqueNumber(); } createAndSetCaptchaElem() { // generate new captcha id this.captchaElemId = this.generateNewElemId(); if (!this.captchaElemId) { throw Error(`Captcha elem Id is not set`); } if (!this.captchaWrapperElem) { throw Error(`Captcha DOM element is not initialized`); } // remove old html this.captchaWrapperElem.nativeElement.innerHTML = ""; // create new wrapper for captcha const newElem = this.renderer.createElement("div"); newElem.id = this.captchaElemId; this.renderer.appendChild(this.captchaWrapperElem.nativeElement, newElem); // when use captcha in cdk stepper then throwing error Captcha element with id 'ngx_captcha_id_XXXX' not found // to fix it checking ensureCaptchaElem in timeout so that its check in next call and its able to find element setTimeout(() => { // update captcha elem if (this.captchaElemId) { this.ensureCaptchaElem(this.captchaElemId); } }, 0); } /** * To be aligned with the ControlValueAccessor interface we need to implement this method * However as we don't want to update the recaptcha, this doesn't need to be implemented */ writeValue(obj) { } /** * This method helps us tie together recaptcha and our formControl values */ registerOnChange(fn) { this.onChange = fn; } /** * At some point we might be interested whether the user has touched our component */ registerOnTouched(fn) { this.onTouched = fn; } /** * Handles error callback */ handleErrorCallback() { this.zone.run(() => { this.onChange(undefined); this.onTouched(undefined); }); this.error.next(); } /** * Handles expired callback */ handleExpireCallback() { this.expire.next(); // reset captcha on expire callback this.resetCaptcha(); } } /** @nocollapse */ BaseReCaptchaComponentDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.4", ngImport: i0, type: BaseReCaptchaComponentDirective, deps: [{ token: i0.Renderer2 }, { token: i0.NgZone }, { token: i0.Injector }, { token: ScriptService }], target: i0.ɵɵFactoryTarget.Directive }); /** @nocollapse */ BaseReCaptchaComponentDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.0.4", type: BaseReCaptchaComponentDirective, inputs: { siteKey: "siteKey", useGlobalDomain: "useGlobalDomain", useEnterprise: "useEnterprise", type: "type", hl: "hl", tabIndex: "tabIndex" }, outputs: { success: "success", load: "load", reset: "reset", ready: "ready", error: "error", expire: "expire" }, usesOnChanges: true, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.4", ngImport: i0, type: BaseReCaptchaComponentDirective, decorators: [{ type: Directive }], ctorParameters: function () { return [{ type: i0.Renderer2 }, { type: i0.NgZone }, { type: i0.Injector }, { type: ScriptService }]; }, propDecorators: { siteKey: [{ type: Input }], useGlobalDomain: [{ type: Input }], useEnterprise: [{ type: Input }], type: [{ type: Input }], hl: [{ type: Input }], tabIndex: [{ type: Input }], success: [{ type: Output }], load: [{ type: Output }], reset: [{ type: Output }], ready: [{ type: Output }], error: [{ type: Output }], expire: [{ type: Output }] } }); var ReCaptchaType; (function (ReCaptchaType) { ReCaptchaType[ReCaptchaType["InvisibleReCaptcha"] = 0] = "InvisibleReCaptcha"; ReCaptchaType[ReCaptchaType["ReCaptcha2"] = 1] = "ReCaptcha2"; })(ReCaptchaType || (ReCaptchaType = {})); class InvisibleReCaptchaComponent extends BaseReCaptchaComponentDirective { constructor(renderer, zone, injector, scriptService) { super(renderer, zone, injector, scriptService); this.renderer = renderer; this.zone = zone; this.injector = injector; this.scriptService = scriptService; /** * This size representing invisible captcha */ this.size = 'invisible'; /** * Theme */ this.theme = 'light'; /** * Badge */ this.badge = 'bottomright'; this.recaptchaType = ReCaptchaType.InvisibleReCaptcha; } ngOnChanges(changes) { super.ngOnChanges(changes); } /** * Programatically invoke the reCAPTCHA check. Used if the invisible reCAPTCHA is on a div instead of a button. */ execute() { // execute captcha this.zone.runOutsideAngular(() => this.reCaptchaApi.execute(this.captchaId)); } captchaSpecificSetup() { } /** * Gets reCaptcha properties */ getCaptchaProperties() { return { 'sitekey': this.siteKey, 'callback': (response) => this.zone.run(() => this.handleCallback(response)), 'expired-callback': () => this.zone.run(() => this.handleExpireCallback()), 'error-callback': () => this.zone.run(() => this.handleErrorCallback()), 'badge': this.badge, 'type': this.type, 'tabindex': this.tabIndex, 'size': this.size, 'theme': this.theme }; } } /** @nocollapse */ InvisibleReCaptchaComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.4", ngImport: i0, type: InvisibleReCaptchaComponent, deps: [{ token: i0.Renderer2 }, { token: i0.NgZone }, { token: i0.Injector }, { token: ScriptService }], target: i0.ɵɵFactoryTarget.Component }); /** @nocollapse */ InvisibleReCaptchaComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.0.4", type: InvisibleReCaptchaComponent, selector: "ngx-invisible-recaptcha", inputs: { theme: "theme", badge: "badge" }, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef((() => InvisibleReCaptchaComponent)), multi: true, } ], viewQueries: [{ propertyName: "captchaWrapperElem", first: true, predicate: ["captchaWrapperElem"], descendants: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: `
`, isInline: true }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.4", ngImport: i0, type: InvisibleReCaptchaComponent, decorators: [{ type: Component, args: [{ selector: 'ngx-invisible-recaptcha', template: `
`, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef((() => InvisibleReCaptchaComponent)), multi: true, } ] }] }], ctorParameters: function () { return [{ type: i0.Renderer2 }, { type: i0.NgZone }, { type: i0.Injector }, { type: ScriptService }]; }, propDecorators: { theme: [{ type: Input }], badge: [{ type: Input }], captchaWrapperElem: [{ type: ViewChild, args: ['captchaWrapperElem', { static: false }] }] } }); class ReCaptcha2Component extends BaseReCaptchaComponentDirective { constructor(renderer, zone, injector, scriptService) { super(renderer, zone, injector, scriptService); this.renderer = renderer; this.zone = zone; this.injector = injector; this.scriptService = scriptService; /** * Name of the global expire callback */ this.windowOnErrorCallbackProperty = 'ngx_captcha_error_callback'; /** * Name of the global error callback */ this.windowOnExpireCallbackProperty = 'ngx_captcha_expire_callback'; /** * Theme */ this.theme = 'light'; /** * Size */ this.size = 'normal'; this.recaptchaType = ReCaptchaType.ReCaptcha2; } ngOnChanges(changes) { super.ngOnChanges(changes); } ngOnDestroy() { window[this.windowOnErrorCallbackProperty] = {}; window[this.windowOnExpireCallbackProperty] = {}; } captchaSpecificSetup() { this.registerCallbacks(); } /** * Gets reCaptcha properties */ getCaptchaProperties() { return { 'sitekey': this.siteKey, 'callback': (response) => this.zone.run(() => this.handleCallback(response)), 'expired-callback': () => this.zone.run(() => this.handleExpireCallback()), 'error-callback': () => this.zone.run(() => this.handleErrorCallback()), 'theme': this.theme, 'type': this.type, 'size': this.size, 'tabindex': this.tabIndex }; } /** * Registers global callbacks */ registerCallbacks() { window[this.windowOnErrorCallbackProperty] = super.handleErrorCallback.bind(this); window[this.windowOnExpireCallbackProperty] = super.handleExpireCallback.bind(this); } } /** @nocollapse */ ReCaptcha2Component.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.4", ngImport: i0, type: ReCaptcha2Component, deps: [{ token: i0.Renderer2 }, { token: i0.NgZone }, { token: i0.Injector }, { token: ScriptService }], target: i0.ɵɵFactoryTarget.Component }); /** @nocollapse */ ReCaptcha2Component.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.0.4", type: ReCaptcha2Component, selector: "ngx-recaptcha2", inputs: { theme: "theme", size: "size" }, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef((() => ReCaptcha2Component)), multi: true, } ], viewQueries: [{ propertyName: "captchaWrapperElem", first: true, predicate: ["captchaWrapperElem"], descendants: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: `
`, isInline: true }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.4", ngImport: i0, type: ReCaptcha2Component, decorators: [{ type: Component, args: [{ selector: 'ngx-recaptcha2', template: `
`, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef((() => ReCaptcha2Component)), multi: true, } ] }] }], ctorParameters: function () { return [{ type: i0.Renderer2 }, { type: i0.NgZone }, { type: i0.Injector }, { type: ScriptService }]; }, propDecorators: { theme: [{ type: Input }], size: [{ type: Input }], captchaWrapperElem: [{ type: ViewChild, args: ['captchaWrapperElem', { static: false }] }] } }); class ReCaptchaV3Service { constructor(scriptService, zone) { this.scriptService = scriptService; this.zone = zone; } /** * Executes reCaptcha v3/Enterprise with given action and passes token via callback. You need to verify * this callback in your backend to get meaningful results. * * For more information see https://developers.google.com/recaptcha/docs/v3 * For enterprise see https://cloud.google.com/recaptcha-enterprise/docs * * @param siteKey Site key found in your google admin panel * @param action Action to log * @param callback Callback function to to handle token * @param config Optional configuration like useGlobalDomain to be provided * @param errorCallback Optional Callback function to handle errors */ execute(siteKey, action, callback, config, errorCallback) { this.executeAsPromise(siteKey, action, config) .then(callback) .catch((error) => errorCallback ? errorCallback(error) : console.error(error)); } /** * Executes reCaptcha v3/Enterprise with given action and returns token via Promise. You need to verify * this token in your backend to get meaningful results. * * For more information see https://developers.google.com/recaptcha/docs/v3 * For enterprise see https://cloud.google.com/recaptcha-enterprise/docs * * @param siteKey Site key found in your google admin panel * @param action Action to log * @param config Optional configuration like useGlobalDomain to be provided */ executeAsPromise(siteKey, action, config) { return new Promise((resolve, reject) => { const configuration = config || {}; const onRegister = (grecaptcha) => { this.zone.runOutsideAngular(() => { try { grecaptcha .execute(siteKey, { action }) .then((token) => this.zone.run(() => resolve(token))); } catch (error) { reject(error); } }); }; this.scriptService.registerCaptchaScript(configuration, siteKey, onRegister); }); } } /** @nocollapse */ ReCaptchaV3Service.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.4", ngImport: i0, type: ReCaptchaV3Service, deps: [{ token: ScriptService }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ ReCaptchaV3Service.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.0.4", ngImport: i0, type: ReCaptchaV3Service, providedIn: "root" }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.4", ngImport: i0, type: ReCaptchaV3Service, decorators: [{ type: Injectable, args: [{ providedIn: "root", }] }], ctorParameters: function () { return [{ type: ScriptService }, { type: i0.NgZone }]; } }); class NgxCaptchaModule { } /** @nocollapse */ NgxCaptchaModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.4", ngImport: i0, type: NgxCaptchaModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); /** @nocollapse */ NgxCaptchaModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.0.4", ngImport: i0, type: NgxCaptchaModule, declarations: [ReCaptcha2Component, InvisibleReCaptchaComponent], imports: [CommonModule], exports: [ReCaptcha2Component, InvisibleReCaptchaComponent] }); /** @nocollapse */ NgxCaptchaModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.0.4", ngImport: i0, type: NgxCaptchaModule, providers: [ ScriptService, ReCaptchaV3Service ], imports: [CommonModule] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.4", ngImport: i0, type: NgxCaptchaModule, decorators: [{ type: NgModule, args: [{ imports: [ CommonModule ], declarations: [ ReCaptcha2Component, InvisibleReCaptchaComponent ], providers: [ ScriptService, ReCaptchaV3Service ], exports: [ ReCaptcha2Component, InvisibleReCaptchaComponent ] }] }] }); /* * Public API */ /** * Generated bundle index. Do not edit. */ export { BaseReCaptchaComponentDirective, InvisibleReCaptchaComponent, NgxCaptchaModule, ReCaptcha2Component, ReCaptchaType, ReCaptchaV3Service, ScriptService }; //# sourceMappingURL=ngx-captcha.mjs.map