Skip to content

Commit

Permalink
fix(component): improve reCAPTCHA v2 and v3 interoperability
Browse files Browse the repository at this point in the history
If one wants to diplay both v2 and v3 inside the same app, then they should always inject `RECAPTCHA_V3_SITE_KEY`. closes #152
  • Loading branch information
DethAriel committed Sep 14, 2020
1 parent e2a9f7f commit 79fc85b
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 53 deletions.
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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).
### <a name="playground"></a>Playground
Expand Down
14 changes: 9 additions & 5 deletions index.ts
Original file line number Diff line number Diff line change
@@ -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';
29 changes: 29 additions & 0 deletions recaptcha/load-script.ts
Original file line number Diff line number Diff line change
@@ -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);
}
49 changes: 14 additions & 35 deletions recaptcha/recaptcha-loader.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>('recaptcha-language');
export const RECAPTCHA_BASE_URL = new InjectionToken<string>('recaptcha-base-url');
export const RECAPTCHA_NONCE = new InjectionToken<string>('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 {
Expand All @@ -58,17 +31,21 @@ export class RecaptchaLoaderService {
private baseUrl: string;
/** @internal */
private nonce: string;
/** @internal */
private v3SiteKey: string;

constructor(
// tslint:disable-next-line:no-any
@Inject(PLATFORM_ID) private readonly platformId: any,
@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();
}
Expand All @@ -82,7 +59,9 @@ export class RecaptchaLoaderService {
const subject = new BehaviorSubject<ReCaptchaV2.ReCaptcha>(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);
}
}
}
4 changes: 0 additions & 4 deletions recaptcha/recaptcha-settings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import { InjectionToken } from '@angular/core';

export const RECAPTCHA_SETTINGS = new InjectionToken<RecaptchaSettings>('recaptcha-settings');

export interface RecaptchaSettings {
siteKey?: string;
theme?: ReCaptchaV2.Theme;
Expand Down
7 changes: 3 additions & 4 deletions recaptcha/recaptcha-v3.service.ts
Original file line number Diff line number Diff line change
@@ -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<string>('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 {
/**
Expand Down
3 changes: 2 additions & 1 deletion recaptcha/recaptcha.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
11 changes: 11 additions & 0 deletions recaptcha/tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {
InjectionToken,
} from '@angular/core';

import { RecaptchaSettings } from './recaptcha-settings';

export const RECAPTCHA_LANGUAGE = new InjectionToken<string>('recaptcha-language');
export const RECAPTCHA_BASE_URL = new InjectionToken<string>('recaptcha-base-url');
export const RECAPTCHA_NONCE = new InjectionToken<string>('recaptcha-nonce-tag');
export const RECAPTCHA_SETTINGS = new InjectionToken<RecaptchaSettings>('recaptcha-settings');
export const RECAPTCHA_V3_SITE_KEY = new InjectionToken<string>('recaptcha-v3-site-key');

0 comments on commit 79fc85b

Please sign in to comment.