diff --git a/README.md b/README.md index 9ce1eb69..64b2bd95 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 `` configuration - see [Configuring the component globally](#example-global-config) section below. ### Events @@ -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. ### Methods @@ -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). +### 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: ``, +}) 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. + ### 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 diff --git a/demo/package.json b/demo/package.json index cef5cef4..5bad7e18 100644 --- a/demo/package.json +++ b/demo/package.json @@ -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": { diff --git a/demo/v-all/src/app/examples/basic/basic-demo.component.html b/demo/v-all/src/app/examples/basic/basic-demo.component.html index c49d077f..feef1f65 100644 --- a/demo/v-all/src/app/examples/basic/basic-demo.component.html +++ b/demo/v-all/src/app/examples/basic/basic-demo.component.html @@ -1,4 +1,6 @@ diff --git a/demo/v-all/src/app/examples/basic/basic-demo.component.ts b/demo/v-all/src/app/examples/basic/basic-demo.component.ts index 2155ba0c..69562269 100644 --- a/demo/v-all/src/app/examples/basic/basic-demo.component.ts +++ b/demo/v-all/src/app/examples/basic/basic-demo.component.ts @@ -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); + } } diff --git a/demo/v-all/src/app/examples/invisible/invisible-demo.component.html b/demo/v-all/src/app/examples/invisible/invisible-demo.component.html index a47c7530..b5c964ff 100644 --- a/demo/v-all/src/app/examples/invisible/invisible-demo.component.html +++ b/demo/v-all/src/app/examples/invisible/invisible-demo.component.html @@ -1,6 +1,8 @@ diff --git a/demo/v-all/src/app/examples/invisible/invisible-demo.component.ts b/demo/v-all/src/app/examples/invisible/invisible-demo.component.ts index ac05d77e..fa365ea6 100644 --- a/demo/v-all/src/app/examples/invisible/invisible-demo.component.ts +++ b/demo/v-all/src/app/examples/invisible/invisible-demo.component.ts @@ -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); + } } diff --git a/recaptcha/recaptcha.component.ts b/recaptcha/recaptcha.component.ts index 56330d1e..26a968e7 100644 --- a/recaptcha/recaptcha.component.ts +++ b/recaptcha/recaptcha.component.ts @@ -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(); + @Output() public error = new EventEmitter(); /** @internal */ private subscription: Subscription; @@ -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); @@ -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)); @@ -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;