I'm trying to integrate an Invisible reCAPTCHA with my Angular 4 form.
In the TS part of the component I have a function like this:
onSubmit(token: string) {
alert(token);
}
And my HTML looks like this (only the relevant parts shown):
<form (ngSubmit)="onSubmit()" #myForm="ngForm">
<button type="submit" class="btn g-recaptcha" data-sitekey="mykey" data-callback="onSubmit">Submit</button>
</form>
When clicking on that button I get either an alert saying the token is undefined, or I get an error message like this: "Cannot contact reCAPTCHA. Check your connection and try again." Not really deterministic - sometimes I see one behavior, sometimes the other.
I thought that maybe the (ngSubmit)
in the form tag could interfere with the reCAPTCHA. But if I remove (ngSubmit)
then the onSubmit
function is not called at all...
What is the correct way for integrating this?
There is a similar question for AngularJS here on StackOverflow, but I don't really know how to adapt that solution to Angular 4.
ReCAPTCHA may be implemented in Angular as follows. Note that this example does not include the required server-side code.
Include the reCAPTCHA api.
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
The ReCAPTCHA variable may be declared in Typescript as follows:
declare var grecaptcha: any;
However, for more type support, the interface ReCAPTCHA
may be used.
declare var grecaptcha: ReCAPTCHA;
import { ElementRef } from '@angular/core';
/**
* Interface for Google's reCAPTCHA JavaScript API.
*
* Display API
* @see {@link https://developers.google.com/recaptcha/docs/display}
*
* Invisible API
* @see {@link https://developers.google.com/recaptcha/docs/invisible}
*/
export interface ReCAPTCHA {
/**
* Programatically invoke the reCAPTCHA check. Used if the invisible reCAPTCHA is on a div
* instead of a button.
*
* @param {string} opt_widget_id Optional widget ID, defaults to the first widget created if
* unspecified.
*/
execute(opt_widget_id?: string): void;
/**
* Renders the container as a reCAPTCHA widget and returns the ID of the newly created widget.
*
* @param {ElementRef|string} container The HTML element to render the reCAPTCHA widget. Specify
* either the ID of the container (string) or the DOM element itself.
* @param {Object} parameters An object containing parameters as key=value pairs, for example,
* {"sitekey": "your_site_key", "theme": "light"}.
*/
render(container: ElementRef|string, parameters: {[key: string]: string}): void;
/**
* Resets the reCAPTCHA widget.
*
* @param {string} opt_widget_id Optional widget ID, defaults to the first widget created if
* unspecified.
*/
reset(opt_widget_id?: string): void;
/**
* Gets the response for the reCAPTCHA widget. Returns a null if reCaptcha is not validated.
*
* @param {string} opt_widget_id Optional widget ID, defaults to the first widget created if
* unspecified.
*/
getResponse(opt_widget_id?: string): string;
}
import { Injectable, OnInit } from '@angular/core'
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
export interface ReCAPTCHAResponse {
success: boolean;
status_code: number,
error_codes?: Array<string>;
}
const httpOptions = {
headers: {'Content-Type': 'application/json'},
observe: "body",
responseType: "json",
withCredentials: true
}
@Injectable()
export class ReCAPTCHAService {
public constructor(private http: HttpClient) {}
public verifyUserResponse(userResponseToken: string): Observable<ReCAPTCHAResponse> {
return this.http.post<ReCAPTCHAResponse>('/auth/captcha', {'g-recaptcha-response': userResponseToken}, httpOptions).
pipe(map( (resp: HttpResponse) => { ... resp.body }))
}
}
import { Component, OnDestroy, OnInit } from '@angular/core'
import { ReCAPTCHA } from './recaptcha'
import { ReCAPTCHAResponse, ReCAPTCHAService } from './recaptcha.service'
declare var grecaptcha: ReCAPTCHA;
declare var window: any;
@Component ({
moduleId: module.id,
selector: 'create-account',
templateUrl: 'create-account.component.html'
})
export class CreateAccountComponent implements OnDestroy, OnInit {
public constructor(private recaptcha: ReCAPTCHAService) {}
public ngOnInit(): void {
// Export reCAPTCHACallback to global scope.
window['reCAPTCHACallback'] = this.reCAPTCHACallback.bind(this);
grecaptcha.render('create-account-captcha', {
'sitekey': 'your-site-key',
'size': 'invisible', // Optional (see docs)
'callback': 'reCAPTCHACallback'
});
}
/**
* Verify reCAPTCHA response on server.
*/
public reCAPTCHACallback(token: string) {
if (token == null || token.length === 0) {
grecaptcha.reset();
// TODO: report that captcha was invalid
} else {
this.recaptcha.verifyUserResponse(token).subscribe(
(r: ReCAPTCHAResponse) => {
if (r.success) {
// TODO: create the new user account
} else {
grecaptcha.reset();
// TODO: report that captcha was invalid
}
},
(error: any) => {
// TODO: handle server error
}
})
}
}
<!-- Invisible reCAPTCHA dynamically rendered. -->
<div id='create-account-captcha' class="g-recaptcha"></div>