/******************************************************************** * Copyright 2024 Adobe * All Rights Reserved. * * NOTICE: Adobe permits you to use, modify, and distribute this * file in accordance with the terms of the Adobe license agreement * accompanying it. *******************************************************************/ import { ReCaptchaV3Response, PropsFormTypes, ReCaptchaV3Model, } from './types/recaptcha.types'; import { recaptchaMessage, recaptchaBadgeSelector } from './configs'; import { extendConfig, setConfigStorage, getConfigStorage, checkRecaptchaBadge, convertKeysToCamelCase, } from './lib'; import { getRecaptchaToken, verifyReCaptchaLoad, } from './services/recaptcha.service'; import { RECAPTCHA_CONFIGURATION_V3 } from './graphql/recaptchaConfig.graphql'; import { FetchGraphQL } from '@adobe-commerce/fetch-graphql'; export const recaptchaFetchApi = new FetchGraphQL().getMethods(); export class RecaptchaModule { _enableReCAPTCHA: boolean = false; _recaptchaBackendEndpoint: string = recaptchaFetchApi.getConfig()?.endpoint || ''; _recaptchaScriptUrl: string = 'https://www.google.com/recaptcha/api.js'; _configStorageKey: string = 'recaptchaConfig'; _logger: boolean = false; async _updateBadgePosition( badgeId: string, config: ReCaptchaV3Model ): Promise { if (!config) return; if (config?.badgePosition === 'inline') { await verifyReCaptchaLoad(badgeId, config, this._logger); } else { const isBadgeLoaded = await checkRecaptchaBadge(); if (!isBadgeLoaded) return; const recaptchaBadge = document.querySelector( recaptchaBadgeSelector ) as HTMLIFrameElement; const shouldUpdateSrc = config.theme && recaptchaBadge && !recaptchaBadge.src.includes('theme=dark') && !recaptchaBadge.src.includes('theme=light'); if (shouldUpdateSrc) { recaptchaBadge.setAttribute( 'src', `${recaptchaBadge.src}&theme=${config.theme}` ); } } } async _addRecaptchaScript(): Promise { const config = await this._loadConfig(); if (!document.getElementById('recaptchaId') && config) { const webApiKey = config.websiteKey; const isBadgeGlobal = config.badgePosition === 'inline'; const languageCode = config.languageCode; if (!webApiKey) return; const script = document.createElement('script'); script.setAttribute('id', 'recaptchaId'); script.defer = true; script.src = isBadgeGlobal ? `${this._recaptchaScriptUrl}?render=${webApiKey}&badge=none&hl=${languageCode}` : `${this._recaptchaScriptUrl}?render=${webApiKey}&badge=${config.badgePosition}&hl=${languageCode}`; document.head.appendChild(script); } } async _fetchStoreConfig(): Promise { try { const response = await recaptchaFetchApi.fetchGraphQl( RECAPTCHA_CONFIGURATION_V3, { method: 'GET', cache: 'force-cache', } ); if (response?.errors?.length) { this._logger && console.error(response.errors[0].message); return; } return response; } catch (error) { this._logger && console.error(`${recaptchaMessage.failedFetch}:`, error); } } async _loadConfig(): Promise { const config = await getConfigStorage(this._configStorageKey); if (!config) { this._logger && console.error(recaptchaMessage.failedGetStorageConfig); return null; } this._enableReCAPTCHA = !!config.isEnabled; return config; } setEndpoint(url: string) { if (!url) return; this._recaptchaBackendEndpoint = url; recaptchaFetchApi.setEndpoint(url); } async setConfig(configList: PropsFormTypes[]) { try { const config = await this._fetchStoreConfig(); if (!config?.data?.recaptchaV3Config) { sessionStorage.removeItem(this._configStorageKey); return; } const transformConfig: ReCaptchaV3Model = convertKeysToCamelCase( config?.data?.recaptchaV3Config ); const extendedRecaptchaConfig = extendConfig(transformConfig, configList); if (extendedRecaptchaConfig) { setConfigStorage( this._configStorageKey, extendedRecaptchaConfig, this._logger ); } } catch (error) { this._logger && console.error(recaptchaMessage.failedSetStorageConfig, error); sessionStorage.removeItem(this._configStorageKey); } } async initReCaptcha(lazyLoadTimeout = 3000) { // IIFE added to fix SonarQube error "Promise returned in function argument where a void return was expected" setTimeout(() => { (async () => { try { const config = await this._loadConfig(); if (!config?.forms || !config.isEnabled) { return; } await this._addRecaptchaScript(); if (config.badgePosition === 'inline') { await Promise.all( (config.forms as PropsFormTypes[]).map((element) => this._updateBadgePosition(element.badgeId, config) ) ); } else { await this._updateBadgePosition('', config); } } catch (error) { this._logger && console.error(recaptchaMessage.failedInitializing, error); } })(); }, lazyLoadTimeout); } async verifyReCaptcha(): Promise { try { const config = await this._loadConfig(); if (!config?.forms || !config.websiteKey || !config.isEnabled) { return undefined; } return await getRecaptchaToken(config.websiteKey); } catch (error) { this._logger && console.error(error); } } enableLogger(logger: boolean) { this._logger = logger; } getMethods() { return { enableLogger: this.enableLogger.bind(this), setEndpoint: this.setEndpoint.bind(this), setConfig: this.setConfig.bind(this), initReCaptcha: this.initReCaptcha.bind(this), verifyReCaptcha: this.verifyReCaptcha.bind(this), }; } } const recaptcha = new RecaptchaModule(); const { initReCaptcha, verifyReCaptcha, setEndpoint, setConfig, enableLogger } = recaptcha.getMethods(); export { setEndpoint, setConfig, initReCaptcha, verifyReCaptcha, enableLogger };