diff --git a/README.md b/README.md index 3cdf0ac..9ce1eb6 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,15 @@ A simple, configurable, easy-to-start component for handling reCAPTCHA v2 and v3 ## Table of contents 1. [Installation](#installation) -2. [Basic Usage](#example-basic) +1. [Basic Usage](#example-basic) + * [reCAPTCHA v3 Usage](#example-basic-v3) * [Playground](#playground) -3. [Working with `@angular/forms`](#forms-ready) -4. [API](#api) +1. [Working with `@angular/forms`](#forms-ready) +1. [API](#api) * [Input Options](#api-options) * [Events](#api-events) * [Methods](#api-methods) -5. [Examples](#examples) +. [Examples](#examples) * [Configuring the component globally](#example-global-config) * [Specifying a different language](#example-language) * [Loading the reCAPTCHA API by yourself](#example-preload-api) @@ -137,6 +138,8 @@ export class RecaptchaV3DemoComponent { As always with subscriptions, please don't forget to **unsubscribe**. +❗️ **Important note**: If your site uses both v2 and v3, then you should _always_ provide `RECAPTCHA_V3_SITE_KEY` (even in modules that only rely on v2 functionality). This will prevent bugs in your code by allowing `ng-recaptcha` to follow reCAPTCHA development guidelines properly ([this one](https://developers.google.com/recaptcha/docs/faq#can-i-run-recaptcha-v2-and-v3-on-the-same-page) in particular). + A more advanced v3 usage scenario includes listening to all actions and their respectively emitted tokens. This is covered [later on this page](#example-v3-all-actions). ### Playground diff --git a/index.ts b/index.ts index 601ae4a..14e255d 100644 --- a/index.ts +++ b/index.ts @@ -1,20 +1,24 @@ export { RecaptchaComponent } from './recaptcha/recaptcha.component'; export { RecaptchaLoaderService, - RECAPTCHA_LANGUAGE, - RECAPTCHA_BASE_URL, - RECAPTCHA_NONCE, } from './recaptcha/recaptcha-loader.service'; export { RecaptchaModule } from './recaptcha/recaptcha.module'; -export { RECAPTCHA_SETTINGS, RecaptchaSettings } from './recaptcha/recaptcha-settings'; +export { RecaptchaSettings } from './recaptcha/recaptcha-settings'; export { RecaptchaV3Module } from './recaptcha/recaptcha-v3.module'; export { OnExecuteData, OnExecuteErrorData, ReCaptchaV3Service, - RECAPTCHA_V3_SITE_KEY, } from './recaptcha/recaptcha-v3.service'; export { RecaptchaFormsModule } from './recaptcha/recaptcha-forms.module'; export { RecaptchaValueAccessorDirective } from './recaptcha/recaptcha-value-accessor.directive'; + +export { + RECAPTCHA_LANGUAGE, + RECAPTCHA_BASE_URL, + RECAPTCHA_NONCE, + RECAPTCHA_SETTINGS, + RECAPTCHA_V3_SITE_KEY, +} from './recaptcha/tokens'; diff --git a/recaptcha/load-script.ts b/recaptcha/load-script.ts new file mode 100644 index 0000000..f8c8bc3 --- /dev/null +++ b/recaptcha/load-script.ts @@ -0,0 +1,29 @@ +declare global { + interface Window { + ng2recaptchaloaded: () => void; + } +} + +export function loadScript( + renderMode: 'explicit' | string, + onLoaded: (grecaptcha: ReCaptchaV2.ReCaptcha) => void, + urlParams: string, + url?: string, + nonce?: string, +) { + window.ng2recaptchaloaded = () => { + onLoaded(grecaptcha); + }; + const script = document.createElement('script'); + script.innerHTML = ''; + const baseUrl = url || 'https://www.google.com/recaptcha/api.js'; + + script.src = `${baseUrl}?render=${renderMode}&onload=ng2recaptchaloaded${urlParams}`; + if (nonce) { + // tslint:disable-next-line:no-any Remove "any" cast once we upgrade Angular to 7 and TypeScript along with it + (script as any).nonce = nonce; + } + script.async = true; + script.defer = true; + document.head.appendChild(script); +} diff --git a/recaptcha/recaptcha-loader.service.ts b/recaptcha/recaptcha-loader.service.ts index af2c631..5920613 100644 --- a/recaptcha/recaptcha-loader.service.ts +++ b/recaptcha/recaptcha-loader.service.ts @@ -2,45 +2,18 @@ import { isPlatformBrowser } from '@angular/common'; import { Inject, Injectable, - InjectionToken, Optional, PLATFORM_ID, } from '@angular/core'; import { BehaviorSubject, Observable, of } from 'rxjs'; -export const RECAPTCHA_LANGUAGE = new InjectionToken('recaptcha-language'); -export const RECAPTCHA_BASE_URL = new InjectionToken('recaptcha-base-url'); -export const RECAPTCHA_NONCE = new InjectionToken('recaptcha-nonce-tag'); - -declare global { - interface Window { - ng2recaptchaloaded: () => void; - } -} - -export function loadScript( - renderMode: 'explicit' | string, - onLoaded: (grecaptcha: ReCaptchaV2.ReCaptcha) => void, - urlParams: string, - url?: string, - nonce?: string, -) { - window.ng2recaptchaloaded = () => { - onLoaded(grecaptcha); - }; - const script = document.createElement('script'); - script.innerHTML = ''; - const baseUrl = url || 'https://www.google.com/recaptcha/api.js'; - - script.src = `${baseUrl}?render=${renderMode}&onload=ng2recaptchaloaded${urlParams}`; - if (nonce) { - // tslint:disable-next-line:no-any Remove "any" cast once we upgrade Angular to 7 and TypeScript along with it - (script as any).nonce = nonce; - } - script.async = true; - script.defer = true; - document.head.appendChild(script); -} +import { loadScript } from './load-script'; +import { + RECAPTCHA_BASE_URL, + RECAPTCHA_LANGUAGE, + RECAPTCHA_NONCE, + RECAPTCHA_V3_SITE_KEY, +} from './tokens'; @Injectable() export class RecaptchaLoaderService { @@ -58,6 +31,8 @@ export class RecaptchaLoaderService { private baseUrl: string; /** @internal */ private nonce: string; + /** @internal */ + private v3SiteKey: string; constructor( // tslint:disable-next-line:no-any @@ -65,10 +40,12 @@ export class RecaptchaLoaderService { @Optional() @Inject(RECAPTCHA_LANGUAGE) language?: string, @Optional() @Inject(RECAPTCHA_BASE_URL) baseUrl?: string, @Optional() @Inject(RECAPTCHA_NONCE) nonce?: string, + @Optional() @Inject(RECAPTCHA_V3_SITE_KEY) v3SiteKey?: string, ) { this.language = language; this.baseUrl = baseUrl; this.nonce = nonce; + this.v3SiteKey = v3SiteKey; this.init(); this.ready = isPlatformBrowser(this.platformId) ? RecaptchaLoaderService.ready.asObservable() : of(); } @@ -82,7 +59,9 @@ export class RecaptchaLoaderService { const subject = new BehaviorSubject(null); RecaptchaLoaderService.ready = subject; const langParam = this.language ? '&hl=' + this.language : ''; - loadScript('explicit', (grecaptcha) => subject.next(grecaptcha), langParam, this.baseUrl, this.nonce); + + const renderMode = this.v3SiteKey || 'explicit'; + loadScript(renderMode, (grecaptcha) => subject.next(grecaptcha), langParam, this.baseUrl, this.nonce); } } } diff --git a/recaptcha/recaptcha-settings.ts b/recaptcha/recaptcha-settings.ts index b87e592..d4286d4 100644 --- a/recaptcha/recaptcha-settings.ts +++ b/recaptcha/recaptcha-settings.ts @@ -1,7 +1,3 @@ -import { InjectionToken } from '@angular/core'; - -export const RECAPTCHA_SETTINGS = new InjectionToken('recaptcha-settings'); - export interface RecaptchaSettings { siteKey?: string; theme?: ReCaptchaV2.Theme; diff --git a/recaptcha/recaptcha-v3.service.ts b/recaptcha/recaptcha-v3.service.ts index 209ae73..b16fb8a 100644 --- a/recaptcha/recaptcha-v3.service.ts +++ b/recaptcha/recaptcha-v3.service.ts @@ -1,10 +1,9 @@ import { isPlatformBrowser } from '@angular/common'; -import { Inject, Injectable, InjectionToken, NgZone, Optional, PLATFORM_ID } from '@angular/core'; +import { Inject, Injectable, NgZone, Optional, PLATFORM_ID } from '@angular/core'; import { Observable, Subject } from 'rxjs'; -import { loadScript, RECAPTCHA_BASE_URL, RECAPTCHA_LANGUAGE, RECAPTCHA_NONCE } from './recaptcha-loader.service'; - -export const RECAPTCHA_V3_SITE_KEY = new InjectionToken('recaptcha-v3-site-key'); +import { loadScript } from './load-script'; +import { RECAPTCHA_BASE_URL, RECAPTCHA_LANGUAGE, RECAPTCHA_NONCE, RECAPTCHA_V3_SITE_KEY } from './tokens'; export interface OnExecuteData { /** diff --git a/recaptcha/recaptcha.component.ts b/recaptcha/recaptcha.component.ts index b7666fb..56330d1 100644 --- a/recaptcha/recaptcha.component.ts +++ b/recaptcha/recaptcha.component.ts @@ -14,7 +14,8 @@ import { import { Subscription } from 'rxjs'; import { RecaptchaLoaderService } from './recaptcha-loader.service'; -import { RECAPTCHA_SETTINGS, RecaptchaSettings } from './recaptcha-settings'; +import { RecaptchaSettings } from './recaptcha-settings'; +import { RECAPTCHA_SETTINGS } from './tokens'; let nextId = 0; diff --git a/recaptcha/tokens.ts b/recaptcha/tokens.ts new file mode 100644 index 0000000..219c67f --- /dev/null +++ b/recaptcha/tokens.ts @@ -0,0 +1,11 @@ +import { + InjectionToken, +} from '@angular/core'; + +import { RecaptchaSettings } from './recaptcha-settings'; + +export const RECAPTCHA_LANGUAGE = new InjectionToken('recaptcha-language'); +export const RECAPTCHA_BASE_URL = new InjectionToken('recaptcha-base-url'); +export const RECAPTCHA_NONCE = new InjectionToken('recaptcha-nonce-tag'); +export const RECAPTCHA_SETTINGS = new InjectionToken('recaptcha-settings'); +export const RECAPTCHA_V3_SITE_KEY = new InjectionToken('recaptcha-v3-site-key');