@semantic-components/re-captcha
Version:
**@semantic-components/re-captcha** is an Angular library designed to simplify the integration of Google reCAPTCHA into your Angular applications. It supports reCAPTCHA v2 and v3, providing an easy-to-use API and seamless setup for enhancing your app's se
429 lines (418 loc) • 18 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, makeEnvironmentProviders, inject, signal, Injectable, afterNextRender, APP_ID, ChangeDetectorRef, input, computed, Directive, ElementRef, effect, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
const SC_RE_CAPTCHA_V2_SITE_KEY = new InjectionToken('SC_RE_CAPTCHA_V2_SITE_KEY');
const SC_RE_CAPTCHA_V3_SITE_KEY = new InjectionToken('SC_RE_CAPTCHA_V3_SITE_KEY');
const SC_RE_CAPTCHA_LANGUAGE_CODE = new InjectionToken('SC_RE_CAPTCHA_LANGUAGE_CODE');
function provideScReCaptchaSettings(settings) {
const providers = [];
if (settings.v2SiteKey) {
providers.push({
provide: SC_RE_CAPTCHA_V2_SITE_KEY,
useValue: settings.v2SiteKey,
});
}
if (settings.v3SiteKey) {
providers.push({
provide: SC_RE_CAPTCHA_V3_SITE_KEY,
useValue: settings.v3SiteKey,
});
}
if (settings.languageCode) {
providers.push({
provide: SC_RE_CAPTCHA_LANGUAGE_CODE,
useValue: settings.languageCode,
});
}
return makeEnvironmentProviders(providers);
}
class ScReCaptchaService {
v3SiteKey = inject(SC_RE_CAPTCHA_V3_SITE_KEY, { optional: true });
languageCode = inject(SC_RE_CAPTCHA_LANGUAGE_CODE, { optional: true });
scriptId = 'recaptcha-script';
apiUrl = 'https://www.google.com/recaptcha/api.js';
scriptStatus = signal(null);
loadPromise = null;
/**
* Check if the script already exists in the document
*/
checkScriptExists() {
const existingScript = document.getElementById(this.scriptId);
if (existingScript) {
// Check if grecaptcha is actually available in window object
if (window['grecaptcha'] && typeof window['grecaptcha'].render === 'function') {
this.scriptStatus.set(true);
}
}
}
/**
* Load the reCAPTCHA script dynamically
* @returns Promise that resolves to true when script is loaded
*/
loadScript() {
// First check if script exists and is loaded
this.checkScriptExists();
// If script is already loaded, return success immediately
if (this.scriptStatus() === true) {
return Promise.resolve(true);
}
// If script is currently loading, return the existing promise
if (this.loadPromise) {
return this.loadPromise;
}
// Create a new loading promise
this.loadPromise = new Promise((resolve, reject) => {
// Create a unique callback function name if not provided
const callbackName = `onRecaptchaLoaded_${Date.now()}`;
// Define the callback function in window scope
window[callbackName] = () => {
// Start checking for grecaptcha object
this.checkGrecaptchaAvailability(3, resolve, reject);
};
// Build URL with parameters if provided
let url = this.apiUrl;
const params = [];
if (this.v3SiteKey) {
params.push(`render=${this.v3SiteKey}`);
}
else {
params.push(`render=explicit`);
}
if (this.languageCode) {
params.push(`hl=${this.languageCode}`);
}
// Always include onload parameter
params.push(`onload=${callbackName}`);
if (params.length > 0) {
url = `${url}?${params.join('&')}`;
}
// Create script element
const script = document.createElement('script');
script.id = this.scriptId;
script.src = url;
script.async = true;
script.defer = true;
// Set up error handler
script.onerror = () => {
this.loadPromise = null;
console.error('Error loading reCAPTCHA script');
this.scriptStatus.set(false);
reject(new Error('Failed to load reCAPTCHA script'));
};
// Add script to document
document.head.appendChild(script);
});
return this.loadPromise;
}
/**
* Check repeatedly if grecaptcha is available
* @param attempts Number of attempts to check
* @param resolve Promise resolve function
* @param reject Promise reject function
*/
checkGrecaptchaAvailability(attempts, resolve, reject) {
if (window['grecaptcha'] && typeof window['grecaptcha'].render === 'function') {
this.scriptStatus.set(true);
this.loadPromise = null;
resolve(true);
return;
}
if (attempts <= 0) {
this.loadPromise = null;
console.error('reCAPTCHA script loaded but grecaptcha object not available');
this.scriptStatus.set(false);
reject(new Error('reCAPTCHA script loaded but grecaptcha object not available'));
return;
}
// Try again after a short delay
setTimeout(() => this.checkGrecaptchaAvailability(attempts - 1, resolve, reject), 200);
}
/**
* Remove the reCAPTCHA script from the DOM
*/
removeScript() {
const script = document.getElementById(this.scriptId);
if (script) {
script.remove();
delete window['grecaptcha'];
this.scriptStatus.set(null);
this.loadPromise = null;
}
}
/**
* Reset all reCAPTCHA instances on the page
*/
resetAll() {
if (this.scriptStatus() === true && window['grecaptcha']) {
try {
window['grecaptcha'].reset();
}
catch (e) {
console.warn('Error resetting reCAPTCHA:', e);
}
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: ScReCaptchaService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: ScReCaptchaService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: ScReCaptchaService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class ScScoreReCaptcha {
v3SiteKey = inject(SC_RE_CAPTCHA_V3_SITE_KEY);
scReCaptchaService = inject(ScReCaptchaService);
constructor() {
afterNextRender(async () => {
await this.scReCaptchaService.loadScript();
});
}
async execute(actionName) {
return new Promise((resolve) => {
grecaptcha.ready(() => {
grecaptcha.execute(this.v3SiteKey, { action: actionName }).then((token) => {
resolve(token);
});
});
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: ScScoreReCaptcha, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: ScScoreReCaptcha, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: ScScoreReCaptcha, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [] });
/**
* Keeps track of the ID count per prefix. This helps us make the IDs a bit more deterministic
* like they were before the service was introduced. Note that ideally we wouldn't have to do
* this, but there are some internal tests that rely on the IDs.
*/
const counters = {};
/** Service that generates unique IDs for DOM nodes. */
class IdGenerator {
appId = inject(APP_ID);
/**
* Generates a unique ID with a specific prefix.
* @param prefix Prefix to add to the ID.
*/
getId(prefix) {
// Omit the app ID if it's the default `ng`. Since the vast majority of pages have one
// Angular app on them, we can reduce the amount of breakages by not adding it.
if (this.appId !== 'ng') {
prefix += this.appId;
}
// eslint-disable-next-line no-prototype-builtins
if (!counters.hasOwnProperty(prefix)) {
counters[prefix] = 0;
}
return `${prefix}${counters[prefix]++}`;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: IdGenerator, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: IdGenerator, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: IdGenerator, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
class ScReCaptchaBase {
id = inject(IdGenerator).getId('sc-re-captcha-');
widgetId = '';
changeDetectorRef = inject(ChangeDetectorRef);
scReCaptchaService = inject(ScReCaptchaService);
v2SiteKey = inject(SC_RE_CAPTCHA_V2_SITE_KEY, {
optional: true,
});
siteKeyInput = input('', {
alias: 'siteKey',
});
siteKey = computed(() => {
if (this.siteKeyInput()) {
return this.siteKeyInput();
}
return this.v2SiteKey ?? '';
});
tabindex = input('0');
callback = input(undefined);
expiredCallback = input(undefined, {
alias: 'expired-callback',
});
errorCallback = input(undefined, {
alias: 'error-callback',
});
value = signal(null);
constructor() {
afterNextRender(async () => {
await this.scReCaptchaService.loadScript();
this.render();
});
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
render() { }
renderWidget(themeOrBadge, themeOrBadgeValue, sizeOrInvisible) {
this.widgetId = grecaptcha.render(this.id, {
sitekey: this.siteKey(),
[themeOrBadge]: themeOrBadgeValue,
size: sizeOrInvisible,
tabindex: this.tabindex(),
callback: this.callback() ? this.callback() : this.defaultCallback.bind(this),
'expired-callback': this.expiredCallback()
? this.expiredCallback()
: this.defaultExpiredCallback.bind(this),
'error-callback': this.errorCallback()
? this.errorCallback()
: this.defaultErrorCallback.bind(this),
}, true);
}
defaultCallback(token) {
this.setValue(token);
}
defaultExpiredCallback() {
this.setValue(null);
}
defaultErrorCallback() {
console.error('error');
this.setValue(null);
}
getResponse() {
grecaptcha.getResponse(this.widgetId);
}
reset() {
grecaptcha.reset(this.widgetId);
}
setValue(newValue) {
this.value.set(newValue);
this.onChange(newValue);
this.changeDetectorRef.markForCheck();
}
writeValue(obj) {
this.value.set(obj);
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = () => { };
// eslint-disable-next-line @typescript-eslint/no-empty-function
onTouch = () => { };
registerOnChange(fn) {
this.onChange = fn;
}
registerOnTouched(fn) {
this.onTouch = fn;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: ScReCaptchaBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.0.4", type: ScReCaptchaBase, isStandalone: true, inputs: { siteKeyInput: { classPropertyName: "siteKeyInput", publicName: "siteKey", isSignal: true, isRequired: false, transformFunction: null }, tabindex: { classPropertyName: "tabindex", publicName: "tabindex", isSignal: true, isRequired: false, transformFunction: null }, callback: { classPropertyName: "callback", publicName: "callback", isSignal: true, isRequired: false, transformFunction: null }, expiredCallback: { classPropertyName: "expiredCallback", publicName: "expired-callback", isSignal: true, isRequired: false, transformFunction: null }, errorCallback: { classPropertyName: "errorCallback", publicName: "error-callback", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "id", "class.g-recaptcha": "true" } }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: ScReCaptchaBase, decorators: [{
type: Directive,
args: [{
host: {
'[id]': 'id',
'[class.g-recaptcha]': 'true',
},
}]
}], ctorParameters: () => [] });
class ScCheckboxReCaptcha extends ScReCaptchaBase {
host = inject(ElementRef);
theme = input('light');
size = input('normal');
render() {
this.renderWidget('theme', this.theme(), this.size());
}
isFirstRun = true;
firstTheme;
constructor() {
super();
effect(() => {
this.updateRecaptchaTheme(this.theme());
});
}
reset() {
super.reset();
if (this.firstTheme !== this.theme()) {
this.updateRecaptchaTheme(this.theme());
}
}
updateRecaptchaTheme(newTheme) {
if (this.isFirstRun) {
this.firstTheme = newTheme;
this.isFirstRun = false;
return;
}
// Find the reCAPTCHA iframe
const recaptchaFrame = this.host.nativeElement.querySelector('iframe[src*="recaptcha"]');
if (recaptchaFrame) {
// Get the iframe URL
let frameSource = recaptchaFrame.src;
// Replace the theme parameter in the URL
if (frameSource.includes('theme=')) {
frameSource = frameSource.replace(/theme=(light|dark)/, `theme=${newTheme}`);
}
else {
frameSource += `&theme=${newTheme}`;
}
// Update the iframe source
recaptchaFrame.src = frameSource;
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: ScCheckboxReCaptcha, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.0.4", type: ScCheckboxReCaptcha, isStandalone: true, selector: "div[sc-checkbox-re-captcha]", inputs: { theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ScCheckboxReCaptcha),
multi: true,
},
], exportAs: ["scCheckboxReCaptcha"], usesInheritance: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: ScCheckboxReCaptcha, decorators: [{
type: Directive,
args: [{
selector: 'div[sc-checkbox-re-captcha]',
exportAs: 'scCheckboxReCaptcha',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ScCheckboxReCaptcha),
multi: true,
},
],
}]
}], ctorParameters: () => [] });
class ScInvisibleReCaptcha extends ScReCaptchaBase {
badge = input('bottomright');
render() {
this.renderWidget('badge', this.badge(), 'invisible');
}
execute() {
grecaptcha.execute(this.widgetId);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: ScInvisibleReCaptcha, deps: null, target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.0.4", type: ScInvisibleReCaptcha, isStandalone: true, selector: "div[sc-invisible-re-captcha], button[sc-invisible-re-captcha]", inputs: { badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ScInvisibleReCaptcha),
multi: true,
},
], exportAs: ["scInvisibleReCaptcha"], usesInheritance: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: ScInvisibleReCaptcha, decorators: [{
type: Directive,
args: [{
selector: 'div[sc-invisible-re-captcha], button[sc-invisible-re-captcha]',
exportAs: 'scInvisibleReCaptcha',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ScInvisibleReCaptcha),
multi: true,
},
],
}]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { ScCheckboxReCaptcha, ScInvisibleReCaptcha, ScScoreReCaptcha, provideScReCaptchaSettings };
//# sourceMappingURL=semantic-components-re-captcha.mjs.map