Skip to content

Commit

Permalink
feat(component): add the ability to handle reCAPTCHA errors
Browse files Browse the repository at this point in the history
Use `errorMode="handled"` together with `(error)` callback to get notified about reCAPTCHA errors. closes #199
  • Loading branch information
Ruslan Arkhipau authored and Ruslan Arkhipau committed Dec 1, 2020
1 parent dea31e5 commit 80c9e6e
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 3 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ A simple, configurable, easy-to-start component for handling reCAPTCHA v2 and v3
. [Examples](#examples)
* [Configuring the component globally](#example-global-config)
* [Specifying a different language](#example-language)
* [Handling errors](#example-error-handling)
* [Loading the reCAPTCHA API by yourself](#example-preload-api)
* [Usage with `required` in forms](#example-forms)
* [Working with invisible reCAPTCHA](#example-invisible)
Expand Down Expand Up @@ -174,6 +175,8 @@ The component supports this options:
They are all pretty well described either in the [reCAPTCHA docs](https://developers.google.com/recaptcha/docs/display), or in the [invisible reCAPTCHA docs](https://developers.google.com/recaptcha/docs/invisible),
so I won't duplicate it here.
One additional option that component accepts is `errorMode`. You can learn more about it in the [Handling errors](#example-error-handling) section below.
Besides specifying these options on the component itself, you can provide a global `<re-captcha>` configuration - see [Configuring the component globally](#example-global-config) section below.
### <a name="api-events"></a>Events
Expand All @@ -184,6 +187,8 @@ Besides specifying these options on the component itself, you can provide a glob

If the captcha has expired prior to submitting its value to the server, the component
will reset the captcha, and trigger the `resolved` event with `response === null`.
* `error(errorDetails: any[])`. Occurs when reCAPTCHA encounters an error (usually a connectivity problem) **if and only if** `errorMode` input has been set to `"handled"`.
`errorDetails` is a simple propagation of any arguments that the original `error-callback` has provided, and is documented here for the purposes of completeness and future-proofing. This array will most often (if not always) be empty. A good strategy would be to rely on just the fact that this event got triggered, and show a message to your app's user telling them to retry.

### <a name="api-methods"></a>Methods

Expand Down Expand Up @@ -235,6 +240,37 @@ import { RECAPTCHA_LANGUAGE } from 'ng-recaptcha';

You can find the list of supported languages in [reCAPTCHA docs](https://developers.google.com/recaptcha/docs/language).

### <a name="example-error-handling"></a>Handling errors

Sometimes reCAPTCHA encounters an error, which is usually a network connectivity problem. It cannot continue until connectivity is restored. By default, reCAPTCHA lets the user know that an error has happened (it's a built-in functionality of reCAPTCHA itself, and this lib is not in control of it). The downside of such behavior is that you, as a developer, don't get notified about this in any way. Opting into such notifications is easy, but comes at a cost of assuming responsibility for informing the user that they should retry. Here's how you would do this:

```typescript
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `<re-captcha
(resolved)="resolved($event)"
(error)="errored($event)"
errorMode="handled"
></re-captcha>`,
}) export class MyApp {
resolved(captchaResponse: string) {
console.log(`Resolved captcha with response: ${captchaResponse}`);
}
errored() {
console.warn(`reCAPTCHA error encountered`);
}
}
```

You can see this in action by navigating to either [basic example demo](https://dethariel.github.io/ng-recaptcha/basic) or [invisible demo](https://dethariel.github.io/ng-recaptcha/invisible) and trying to interact with reCAPTCHA after setting the network to "Offline".

The `errorMode` input has two possible values -- `"handled"` and `"default"`, with latter being the default as the name suggests. Not specifying `errorMode`, or setting it to anything other than `"handled"` will not invoke your `(error)` callback, and will instead result in default reCAPTCHA functionality.

The `(error)` callback will propagate all of the parameters that it receives from `grecaptcha['error-callback']` (which might be none) as an array.

### <a name="example-preload-api"></a>Loading the reCAPTCHA API by yourself [(see in action)](https://dethariel.github.io/ng-recaptcha/v8/preload-api)

By default, the component assumes that the reCAPTCHA API loading will be handled
Expand Down
2 changes: 1 addition & 1 deletion demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"install:v9": "cd v9 && yarn install",
"install:v10": "cd v10 && yarn install",
"install:all": "yarn generate && yarn install:v6 && yarn install:v7 && yarn install:v8 && yarn install:v9 && yarn install:v10",
"serve-latest": "yarn generate && yarn pack-latest && yarn clean:v10 && yarn build:v10 && yarn serve",
"serve-latest": "yarn generate && yarn pack-latest && yarn clean:v10 && yarn generate && yarn build:v10 && cd v10 && yarn serve",
"pack-latest": "cd .. && yarn pack-latest"
},
"engines": {
Expand Down
2 changes: 2 additions & 0 deletions demo/v-all/src/app/examples/basic/basic-demo.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<re-captcha
(resolved)="resolved($event)"
(error)="onError($event)"
errorMode="handled"
siteKey="6LcOuyYTAAAAAHTjFuqhA52fmfJ_j5iFk5PsfXaU"
></re-captcha>
4 changes: 4 additions & 0 deletions demo/v-all/src/app/examples/basic/basic-demo.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ export class BasicDemoComponent {
public resolved(captchaResponse: string) {
console.log(`Resolved captcha with response: ${captchaResponse}`);
}

public onError(errorDetails: any[]) {
console.log(`reCAPTCHA error encountered; details:`, errorDetails);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<re-captcha
#captchaRef="reCaptcha"
(resolved)="resolved($event)"
(error)="onError($event)"
errorMode="handled"
siteKey="6Ldp0xgUAAAAAF_iIss_hpFaVrjLbPGjwyfJwebB"
size="invisible"
></re-captcha>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ export class InvisibleDemoComponent {
: captchaResponse;
this.captchaResponse += `${JSON.stringify(newResponse)}\n`;
}

public onError(errorDetails: any[]) {
this.captchaResponse += `ERROR; error details (if any) have been logged to console\n`;
console.log(`reCAPTCHA error encountered; details:`, errorDetails);
}
}
20 changes: 18 additions & 2 deletions recaptcha/recaptcha.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ export class RecaptchaComponent implements AfterViewInit, OnDestroy {
@Input() public size: ReCaptchaV2.Size;
@Input() public tabIndex: number;
@Input() public badge: ReCaptchaV2.Badge;
@Input() public errorMode: 'handled' | 'default' = 'default';

@Output() public resolved = new EventEmitter<string>();
@Output() public error = new EventEmitter<any[]>();

/** @internal */
private subscription: Subscription;
Expand Down Expand Up @@ -115,6 +117,11 @@ export class RecaptchaComponent implements AfterViewInit, OnDestroy {
this.resolved.emit(null);
}

/** @internal */
private errored(args: any[]) {
this.error.emit(args);
}

/** @internal */
private captchaResponseCallback(response: string) {
this.resolved.emit(response);
Expand All @@ -129,7 +136,8 @@ export class RecaptchaComponent implements AfterViewInit, OnDestroy {

/** @internal */
private renderRecaptcha() {
this.widget = this.grecaptcha.render(this.elementRef.nativeElement, {
// This `any` can be removed after @types/grecaptcha get updated
const renderOptions: any = {
badge: this.badge,
callback: (response: string) => {
this.zone.run(() => this.captchaResponseCallback(response));
Expand All @@ -142,7 +150,15 @@ export class RecaptchaComponent implements AfterViewInit, OnDestroy {
tabindex: this.tabIndex,
theme: this.theme,
type: this.type,
});
};

if (this.errorMode === 'handled') {
renderOptions['error-callback'] = (...args: any[]) => {
this.zone.run(() => this.errored(args));
};
}

this.widget = this.grecaptcha.render(this.elementRef.nativeElement, renderOptions);

if (this.executeRequested === true) {
this.executeRequested = false;
Expand Down

0 comments on commit 80c9e6e

Please sign in to comment.