├── .bowerrc ├── .gitignore ├── .travis.yml ├── .whitesource ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── demo ├── angular-recaptcha.js └── usage.html ├── index.js ├── karma.conf.js ├── package.json ├── release ├── angular-recaptcha.js └── angular-recaptcha.min.js ├── src ├── directive.js ├── module.js └── service.js └── tests ├── directive_test.js ├── provider.driver.js ├── provider_test.js ├── service.driver.js └── service_test.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory" : "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE Files 2 | .idea 3 | 4 | # OS Files 5 | .DS_Store 6 | 7 | # Node, Grunt, Bower, general build process files 8 | bower_components 9 | node_modules 10 | 11 | # Coverage folder 12 | coverage 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 'stable' 5 | 6 | before_script: 7 | - npm install -g grunt-cli bower 8 | - bower install 9 | - "export CHROME_BIN=chromium-browser" 10 | - "export DISPLAY=:99.0" 11 | - "sh -e /etc/init.d/xvfb start" 12 | 13 | script: 14 | - grunt karma:ci 15 | 16 | after_script: 17 | - grunt coverage 18 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "settingsInheritedFrom": "VividCortex/whitesource-config@master" 3 | } -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | meta: { 7 | banner: '/**\n' + 8 | ' * @license <%= pkg.name %> build:<%= grunt.template.today("yyyy-mm-dd") %>\n' + 9 | ' * <%= pkg.homepage %>\n' + 10 | ' * Copyright (c) <%= grunt.template.today("yyyy") %> VividCortex\n' + 11 | '**/\n\n' 12 | }, 13 | concat: { 14 | options: { banner: '<%= meta.banner %>' }, 15 | dist_js: { 16 | src: [ 17 | '', 18 | 'src/module.js', 19 | 'src/service.js', 20 | 'src/directive.js' 21 | ], 22 | dest: 'release/<%= pkg.name %>.js' 23 | } 24 | }, 25 | uglify: { 26 | options: { banner: '<%= meta.banner %>' }, 27 | release: { 28 | src: 'release/<%= pkg.name %>.js', 29 | dest: 'release/<%= pkg.name %>.min.js' 30 | } 31 | }, 32 | bump: { 33 | options: { 34 | files: ['package.json', 'bower.json'], 35 | updateConfigs: ['pkg'], 36 | commit: true, 37 | commitMessage: '[release]', 38 | commitFiles: ['package.json', 'bower.json', 'README.md', 'release'], 39 | createTag: true, 40 | tagName: '%VERSION%', 41 | tagMessage: '%VERSION%', 42 | push: true, 43 | pushTo: 'origin', 44 | gitDescribeOptions: '--tags --always --abbrev=1 --dirty=-d' // options to use with '$ git describe' 45 | } 46 | }, 47 | karma: { 48 | unit: { 49 | configFile: 'karma.conf.js', 50 | browsers: ['Chrome'], 51 | singleRun: true 52 | }, 53 | ci: { 54 | configFile: 'karma.conf.js', 55 | browsers: ['Chrome_travis_ci', 'Firefox', 'FirefoxNightly'], 56 | singleRun: true 57 | } 58 | }, 59 | coveralls: { 60 | options: { 61 | coverageDir: 'coverage' 62 | } 63 | } 64 | }); 65 | 66 | // Load the plugin that provides the needed tasks. 67 | grunt.loadNpmTasks('grunt-contrib-concat'); 68 | grunt.loadNpmTasks('grunt-contrib-uglify'); 69 | grunt.loadNpmTasks('grunt-bump'); 70 | grunt.loadNpmTasks('grunt-karma'); 71 | grunt.loadNpmTasks('grunt-karma-coveralls'); 72 | 73 | // Default task(s). 74 | grunt.registerTask('default', ['karma:unit', 'concat', 'uglify']); 75 | 76 | // Unit Test task(s). 77 | grunt.registerTask('test', ['karma:unit']); 78 | grunt.registerTask('coverage', ['coveralls']); 79 | }; 80 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013 VividCortex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | UNMAINTAINED 2 | ============ 3 | 4 | Please fork this repo if you are interested in continuing using it. 5 | 6 | AngularJS reCaptcha 7 | =================== 8 | 9 | [![Build Status](https://travis-ci.org/VividCortex/angular-recaptcha.svg?branch=master)](https://travis-ci.org/VividCortex/angular-recaptcha) 10 | [![Coverage Status](https://coveralls.io/repos/VividCortex/angular-recaptcha/badge.svg?branch=master)](https://coveralls.io/r/VividCortex/angular-recaptcha?branch=master) 11 | [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/angular-recaptcha/badge?style=rounded)](https://www.jsdelivr.com/package/npm/angular-recaptcha) 12 | ![image](https://img.shields.io/npm/dm/angular-recaptcha.svg) 13 | 14 | Add a [reCaptcha](https://www.google.com/recaptcha/intro/index.html) to your [AngularJS](angularjs.org) project. 15 | 16 | 17 | Demo: http://vividcortex.github.io/angular-recaptcha/ 18 | 19 | 20 | Installation 21 | ------------ 22 | 23 | #### Manual 24 | 25 | Download the [latest release](https://github.com/VividCortex/angular-recaptcha/releases/latest). 26 | 27 | #### Bower 28 | 29 | ``` 30 | bower install --save angular-recaptcha 31 | ``` 32 | 33 | #### npm 34 | 35 | ``` 36 | npm install --save angular-recaptcha 37 | ``` 38 | 39 | 40 | Usage 41 | ----- 42 | 43 | See [the demo file](demo/usage.html) for a quick usage example. 44 | 45 | IMPORTANT: Keep in mind that the captcha only works when used from a real domain 46 | and with a valid re-captcha key, so this file won't work if you just load it in 47 | your browser. 48 | 49 | - First, you need to get a valid recaptcha key for your domain. Go to http://www.google.com/recaptcha. 50 | 51 | - Include the vc-recaptcha script and make your angular app depend on the `vcRecaptcha` module. 52 | 53 | ```html 54 | 55 | ``` 56 | 57 | ```javascript 58 | var app = angular.module('myApp', ['vcRecaptcha']); 59 | ``` 60 | 61 | - After that, you can place a container for the captcha widget in your view, and call the `vc-recaptcha` directive on it like this: 62 | 63 | ```html 64 |
68 | ``` 69 | 70 | Here, the `key` attribute is passed to the directive's scope, so you can use either a property in your scope or just a hardcoded string. Be careful to use your public key, not your private one. 71 | 72 | Form Validation 73 | --------------- 74 | 75 | **By default**, if placed in a [form](https://docs.angularjs.org/api/ng/directive/form) using [formControl](https://docs.angularjs.org/api/ng/type/form.FormController) the captcha will need to be checked for the form to be valid. 76 | If the captcha is not checked (if the user has not checked the box or the check has expired) the form will be marked as invalid. The validation key is `recaptcha`. 77 | You can **opt out** of this feature by setting the `required` attribute to `false` or a scoped variable 78 | that will evaluate to `false`. Any other value, or omitting the attribute will opt in to this feature. 79 | 80 | You can also trigger the validation programatically if the captcha is not required, for example: 81 | 82 | ```js 83 | vcRecaptchaService.execute(widgetId); 84 | ``` 85 | 86 | If no widget ID is provided, the first created widget will be executed. 87 | 88 | Response Validation 89 | ------------------- 90 | 91 | To validate this object from your server, you need to use the API described in the [verify section](https://developers.google.com/recaptcha/docs/verify). Validation is outside of the scope of this tool, since is mandatory to do that at the server side. 92 | 93 | You can simple supply a value for `ng-model` which will be dynamically populated and cleared as the _response_ becomes available and expires, respectively. When you want the value of the response, you can grab it from the scoped variable that was passed to `ng-model`. It works just like adding `ng-model` to any other input in your form. 94 | 95 | ```html 96 | ... 97 |
98 | ... 99 |
103 | ... 104 |
105 | ... 106 | ``` 107 | 108 | ```js 109 | ... 110 | $scope.mySubmit = function(myFields){ 111 | console.log(myFields.myRecaptchaResponse); 112 | } 113 | ... 114 | ``` 115 | 116 | Or you can programmatically get the _response_ that you need to send to your server, use the method `getResponse()` from the `vcRecaptchaService` angular service. This method receives an optional argument `widgetId`, useful for getting the response of a specific reCaptcha widget (in case you render more than one widget). If no widget ID is provided, the response for the first created widget will be returned. 117 | 118 | ```js 119 | var response = vcRecaptchaService.getResponse(widgetId); // returns the string response 120 | ``` 121 | 122 | Using `ng-model` is recommended for normal use as the value is tied directly to the reCaptcha instance through the directive and there is no need to manage or pass a _widgetId_. 123 | 124 | Other Parameters 125 | ---------------- 126 | 127 | You can optionally pass a `theme` the captcha should use, as an HTML attribute: 128 | 129 | ```html 130 |
139 | ``` 140 | 141 | **Language Codes**: https://developers.google.com/recaptcha/docs/language 142 | 143 | In this case we are specifying that the captcha should use the theme named _light_. 144 | 145 | Listeners 146 | --------- 147 | 148 | There are three listeners you can use with the directive, `on-create`, `on-success`, and `on-expire`. 149 | 150 | * __on-create__: It's called right after the widget is created. It receives a widget ID, which could be helpful if you have more than one reCaptcha in your site. 151 | * __on-success__: It's called once the user resolves the captcha. It receives the response string you would need for verifying the response. 152 | * __on-expire__: It's called when the captcha response expires and the user needs to solve a new captcha. 153 | 154 | ```html 155 |
164 | ``` 165 | 166 | ### Example 167 | 168 | ```js 169 | app.controller('myController', ['$scope', 'vcRecaptchaService', function ($scope, recaptcha) { 170 | $scope.setWidgetId = function (widgetId) { 171 | // store the `widgetId` for future usage. 172 | // For example for getting the response with 173 | // `recaptcha.getResponse(widgetId)`. 174 | }; 175 | 176 | $scope.setResponse = function (response) { 177 | // send the `response` to your server for verification. 178 | }; 179 | 180 | $scope.cbExpiration = function() { 181 | // reset the 'response' object that is on scope 182 | }; 183 | }]); 184 | ``` 185 | 186 | Secure Token 187 | ------------ 188 | 189 | If you want to use a secure token pass it along with the site key as an HTML attribute. 190 | 191 | ```html 192 |
197 | ``` 198 | 199 | Please note that you have to encrypt your token yourself with your private key upfront! 200 | To learn more about secure tokens and how to generate & encrypt them please refer to the [reCAPTCHA Docs](https://developers.google.com/recaptcha/docs/secure_token). 201 | 202 | Service Provider 203 | ---------------- 204 | You can use the `vcRecaptchaServiceProvider` to configure the recaptcha service once in your application's config function. 205 | This is a convenient way to set your reCaptcha site key, theme, stoken, size, and type in one place instead of each `vc-recaptcha` directive element instance. 206 | The defaults defined in the service provider will be overrode by any values passed to the vc-recaptcha directive element for that instance. 207 | 208 | ```javascript 209 | myApp.config(function(vcRecaptchaServiceProvider){ 210 | vcRecaptchaServiceProvider.setSiteKey('---- YOUR PUBLIC KEY GOES HERE ----') 211 | vcRecaptchaServiceProvider.setTheme('---- light or dark ----') 212 | vcRecaptchaServiceProvider.setStoken('--- YOUR GENERATED SECURE TOKEN ---') 213 | vcRecaptchaServiceProvider.setSize('---- compact, normal or invisible ----') 214 | vcRecaptchaServiceProvider.setType('---- audio or image ----') 215 | vcRecaptchaServiceProvider.setLang('---- language code ----') 216 | }); 217 | ``` 218 | 219 | **Language Codes**: https://developers.google.com/recaptcha/docs/language 220 | 221 | You can also set all of the values at once. 222 | 223 | ```javascript 224 | myApp.config(function(vcRecaptchaServiceProvider){ 225 | vcRecaptchaServiceProvider.setDefaults({ 226 | key: '---- YOUR PUBLIC KEY GOES HERE ----', 227 | theme: '---- light or dark ----', 228 | stoken: '--- YOUR GENERATED SECURE TOKEN ---', 229 | size: '---- compact, normal or invisible ----', 230 | type: '---- audio or image ----', 231 | lang: '---- language code ----' 232 | }); 233 | }); 234 | ``` 235 | Note: any value omitted will be undefined, even if previously set. 236 | 237 | Differences with the old reCaptcha 238 | ---------------------------------- 239 | 240 | - If you want to force a language, you'll need to add a `hl` parameter to the script of the reCaptcha API (`?onload=onloadCallback&render=explicit&hl=es`). 241 | - Parameter _tabindex_ is no longer used by reCaptcha and its usage has no effect. 242 | - Access to the input text is no longer supported. 243 | - _Challenge_ is no longer provided by reCaptcha. The response text is used along with the private key and user's IP address for verification. 244 | - Switching between image and audio is now handled by reCaptcha. 245 | - Help display is now handled by reCaptcha. 246 | 247 | 248 | Recent Changelog 249 | ---------------- 250 | 251 | - 3.0.0 - Removed the need to include the Google recaptcha api. 252 | - 2.2.3 - Removed _cleanup_ after creating the captcha element. 253 | - 2.0.1 - Fixed onload when using ng-route and recaptcha is placed in a secondary view. 254 | - 2.0.0 - Rewritten service to support new reCaptcha 255 | - 1.0.2 - added extra `Recaptcha` object methods to the service, i.e. `switch_type`, `showhelp`, etc. 256 | - 1.0.0 - the `key` attribute is now a scope property of the directive 257 | - Added the `destroy()` method to the service. Thanks to @endorama. 258 | - We added a different integration method (see demo/2.html) which is safer because it doesn't relies on a timeout on the reload event of the recaptcha. Thanks to [@sboisse](https://github.com/sboisse) for reporting the issue and suggesting the solution. 259 | - The release is now built using [GruntJS](http://gruntjs.com/) so if you were using the source files (the `src` directory) in your projects you should now use the files in the release directory. 260 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-recaptcha", 3 | "version": "4.2.0", 4 | "keywords": ["angular", "captcha", "recaptcha", "vividcortex", "human", "form", "validation", "signup", "security", "login"], 5 | "main": "release/angular-recaptcha.js", 6 | "ignore": [ 7 | "**/.*", 8 | "Gruntfile.js", 9 | "src", 10 | "node_modules", 11 | "bower_components" 12 | ], 13 | "dependencies": { 14 | "angular": "1.*" 15 | }, 16 | "devDependencies": { 17 | "angular-mocks": "~1.*" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demo/angular-recaptcha.js: -------------------------------------------------------------------------------- 1 | ../release/angular-recaptcha.js -------------------------------------------------------------------------------- /demo/usage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | VividCortex reCaptcha Directive Example 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 71 | 72 | 73 | 74 |
75 | 76 |

VividCortex reCaptcha Directive Example

77 |

78 | This the recommended way of using the reCaptcha directive. 79 | Instead of using ng-model to get the challenge and response, you use a service. 80 | This way it's safer because we don't depend on a timeout hack that we are currently using in the directive. 81 |

82 | 83 | 84 |
85 |
93 | 94 |
95 | 96 |
97 | 98 | 99 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./release/angular-recaptcha.js'); 2 | module.exports = 'vcRecaptcha'; 3 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Wed Dec 24 2014 19:30:10 GMT-0200 (Horário brasileiro de verão) 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'bower_components/angular/angular.js', 19 | 20 | 'bower_components/angular-mocks/angular-mocks.js', 21 | 22 | 'src/module.js', 23 | 'src/*.js', 24 | 25 | 'tests/*.driver.js', 26 | 'tests/*_test.js' 27 | ], 28 | 29 | 30 | // list of files to exclude 31 | exclude: [], 32 | 33 | 34 | // preprocess matching files before serving them to the browser 35 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 36 | preprocessors: { 37 | 'src/*.js': 'coverage' 38 | }, 39 | 40 | coverageReporter: { 41 | type: 'lcov', 42 | dir: 'coverage' 43 | }, 44 | 45 | 46 | // test results reporter to use 47 | // possible values: 'dots', 'progress' 48 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 49 | reporters: ['progress', 'coverage'], 50 | 51 | 52 | // web server port 53 | port: 9876, 54 | 55 | 56 | // enable / disable colors in the output (reporters and logs) 57 | colors: true, 58 | 59 | 60 | // level of logging 61 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 62 | logLevel: config.LOG_INFO, 63 | 64 | 65 | // enable / disable watching file and executing tests whenever any file changes 66 | autoWatch: true, 67 | 68 | 69 | // start these browsers 70 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 71 | browsers: ['PhantomJS', 'Chrome', 'IE', 'Safari', 'Firefox', 'FirefoxNightly', 'ChromeCanary'], 72 | 73 | customLaunchers: { 74 | Chrome_travis_ci: { 75 | base: 'Chrome', 76 | flags: ['--no-sandbox'] 77 | } 78 | }, 79 | 80 | // Continuous Integration mode 81 | // if true, Karma captures browsers, runs the tests and exits 82 | singleRun: false 83 | }); 84 | }; 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-recaptcha", 3 | "version": "4.2.0", 4 | "description": "An AngularJS module to ease usage of reCaptcha inside a form", 5 | "author": "VividCortex", 6 | "license": "MIT", 7 | "homepage": "https://github.com/vividcortex/angular-recaptcha", 8 | "contributors": [ 9 | { 10 | "name" : "Eduardo Daniel Cuomo", 11 | "email" : "reduardo7@gmail.com", 12 | "url" : "https://github.com/reduardo7/angular-recaptcha" 13 | } 14 | ], 15 | "main": "index.js", 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/vividcortex/angular-recaptcha.git" 19 | }, 20 | "scripts": { 21 | "test": "grunt test" 22 | }, 23 | "devDependencies": { 24 | "bower": "^1.8.0", 25 | "grunt": "^1.0.1", 26 | "grunt-bump": "^0.8.0", 27 | "grunt-cli": "^1.2.0", 28 | "grunt-contrib-concat": "^1.0.1", 29 | "grunt-contrib-jshint": "^1.1.0", 30 | "grunt-contrib-uglify": "^2.0.0", 31 | "grunt-karma": "^2.0.0", 32 | "grunt-karma-coveralls": "^2.5.4", 33 | "jasmine-core": "^2.5.2", 34 | "karma": "^1.3.0", 35 | "karma-chrome-launcher": "^2.0.0", 36 | "karma-coverage": "^1.1.1", 37 | "karma-firefox-launcher": "^1.0.0", 38 | "karma-ie-launcher": "^1.0.0", 39 | "karma-jasmine": "^1.0.2", 40 | "karma-phantomjs-launcher": "^1.0.2", 41 | "karma-safari-launcher": "^1.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /release/angular-recaptcha.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license angular-recaptcha build:2018-07-30 3 | * https://github.com/vividcortex/angular-recaptcha 4 | * Copyright (c) 2018 VividCortex 5 | **/ 6 | 7 | /*global angular, Recaptcha */ 8 | (function (ng) { 9 | 'use strict'; 10 | 11 | ng.module('vcRecaptcha', []); 12 | 13 | }(angular)); 14 | 15 | /*global angular */ 16 | (function (ng) { 17 | 'use strict'; 18 | 19 | function throwNoKeyException() { 20 | throw new Error('You need to set the "key" attribute to your public reCaptcha key. If you don\'t have a key, please get one from https://www.google.com/recaptcha/admin/create'); 21 | } 22 | 23 | var app = ng.module('vcRecaptcha'); 24 | 25 | /** 26 | * An angular service to wrap the reCaptcha API 27 | */ 28 | app.provider('vcRecaptchaService', function(){ 29 | var provider = this; 30 | var config = {}; 31 | provider.onLoadFunctionName = 'vcRecaptchaApiLoaded'; 32 | 33 | /** 34 | * Sets the reCaptcha configuration values which will be used by default is not specified in a specific directive instance. 35 | * 36 | * @since 2.5.0 37 | * @param defaults object which overrides the current defaults object. 38 | */ 39 | provider.setDefaults = function(defaults){ 40 | ng.copy(defaults, config); 41 | }; 42 | 43 | /** 44 | * Sets the reCaptcha key which will be used by default is not specified in a specific directive instance. 45 | * 46 | * @since 2.5.0 47 | * @param siteKey the reCaptcha public key (refer to the README file if you don't know what this is). 48 | */ 49 | provider.setSiteKey = function(siteKey){ 50 | config.key = siteKey; 51 | }; 52 | 53 | /** 54 | * Sets the reCaptcha theme which will be used by default is not specified in a specific directive instance. 55 | * 56 | * @since 2.5.0 57 | * @param theme The reCaptcha theme. 58 | */ 59 | provider.setTheme = function(theme){ 60 | config.theme = theme; 61 | }; 62 | 63 | /** 64 | * Sets the reCaptcha stoken which will be used by default is not specified in a specific directive instance. 65 | * 66 | * @since 2.5.0 67 | * @param stoken The reCaptcha stoken. 68 | */ 69 | provider.setStoken = function(stoken){ 70 | config.stoken = stoken; 71 | }; 72 | 73 | /** 74 | * Sets the reCaptcha size which will be used by default is not specified in a specific directive instance. 75 | * 76 | * @since 2.5.0 77 | * @param size The reCaptcha size. 78 | */ 79 | provider.setSize = function(size){ 80 | config.size = size; 81 | }; 82 | 83 | /** 84 | * Sets the reCaptcha type which will be used by default is not specified in a specific directive instance. 85 | * 86 | * @since 2.5.0 87 | * @param type The reCaptcha type. 88 | */ 89 | provider.setType = function(type){ 90 | config.type = type; 91 | }; 92 | 93 | /** 94 | * Sets the reCaptcha language which will be used by default is not specified in a specific directive instance. 95 | * 96 | * @param lang The reCaptcha language. 97 | */ 98 | provider.setLang = function(lang){ 99 | config.lang = lang; 100 | }; 101 | 102 | /** 103 | * Sets the reCaptcha badge position which will be used by default if not specified in a specific directive instance. 104 | * 105 | * @param badge The reCaptcha badge position. 106 | */ 107 | provider.setBadge = function(badge){ 108 | config.badge = badge; 109 | }; 110 | 111 | /** 112 | * Sets the reCaptcha configuration values which will be used by default is not specified in a specific directive instance. 113 | * 114 | * @since 2.5.0 115 | * @param onLoadFunctionName string name which overrides the name of the onload function. Should match what is in the recaptcha script querystring onload value. 116 | */ 117 | provider.setOnLoadFunctionName = function(onLoadFunctionName){ 118 | provider.onLoadFunctionName = onLoadFunctionName; 119 | }; 120 | 121 | provider.$get = ['$rootScope','$window', '$q', '$document', '$interval', function ($rootScope, $window, $q, $document, $interval) { 122 | var deferred = $q.defer(), promise = deferred.promise, instances = {}, recaptcha; 123 | 124 | $window.vcRecaptchaApiLoadedCallback = $window.vcRecaptchaApiLoadedCallback || []; 125 | 126 | var callback = function () { 127 | recaptcha = $window.grecaptcha; 128 | 129 | deferred.resolve(recaptcha); 130 | }; 131 | 132 | $window.vcRecaptchaApiLoadedCallback.push(callback); 133 | 134 | $window[provider.onLoadFunctionName] = function () { 135 | $window.vcRecaptchaApiLoadedCallback.forEach(function(callback) { 136 | callback(); 137 | }); 138 | }; 139 | 140 | 141 | function getRecaptcha() { 142 | if (!!recaptcha) { 143 | return $q.when(recaptcha); 144 | } 145 | 146 | return promise; 147 | } 148 | 149 | function validateRecaptchaInstance() { 150 | if (!recaptcha) { 151 | throw new Error('reCaptcha has not been loaded yet.'); 152 | } 153 | } 154 | 155 | function isRenderFunctionAvailable() { 156 | return ng.isFunction(($window.grecaptcha || {}).render); 157 | } 158 | 159 | 160 | // Check if grecaptcha.render is not defined already. 161 | if (isRenderFunctionAvailable()) { 162 | callback(); 163 | } else if ($window.document.querySelector('script[src^="https://www.google.com/recaptcha/api.js"]')) { 164 | // wait for script to be loaded. 165 | var intervalWait = $interval(function() { 166 | if (isRenderFunctionAvailable()) { 167 | $interval.cancel(intervalWait); 168 | callback(); 169 | } 170 | }, 25); 171 | } else { 172 | // Generate link on demand 173 | var script = $window.document.createElement('script'); 174 | script.async = true; 175 | script.defer = true; 176 | script.src = 'https://www.google.com/recaptcha/api.js?onload='+provider.onLoadFunctionName+'&render=explicit'; 177 | $document.find('body')[0].appendChild(script); 178 | } 179 | 180 | return { 181 | 182 | /** 183 | * Creates a new reCaptcha object 184 | * 185 | * @param elm the DOM element where to put the captcha 186 | * @param conf the captcha object configuration 187 | * @throws NoKeyException if no key is provided in the provider config or the directive instance (via attribute) 188 | */ 189 | create: function (elm, conf) { 190 | 191 | conf.sitekey = conf.key || config.key; 192 | conf.theme = conf.theme || config.theme; 193 | conf.stoken = conf.stoken || config.stoken; 194 | conf.size = conf.size || config.size; 195 | conf.type = conf.type || config.type; 196 | conf.hl = conf.lang || config.lang; 197 | conf.badge = conf.badge || config.badge; 198 | 199 | if (!conf.sitekey) { 200 | throwNoKeyException(); 201 | } 202 | return getRecaptcha().then(function (recaptcha) { 203 | var widgetId = recaptcha.render(elm, conf); 204 | instances[widgetId] = elm; 205 | return widgetId; 206 | }); 207 | }, 208 | 209 | /** 210 | * Reloads the reCaptcha 211 | */ 212 | reload: function (widgetId) { 213 | validateRecaptchaInstance(); 214 | 215 | recaptcha.reset(widgetId); 216 | 217 | // Let everyone know this widget has been reset. 218 | $rootScope.$broadcast('reCaptchaReset', widgetId); 219 | }, 220 | 221 | /** 222 | * Executes the reCaptcha 223 | */ 224 | execute: function (widgetId) { 225 | validateRecaptchaInstance(); 226 | 227 | recaptcha.execute(widgetId); 228 | }, 229 | 230 | /** 231 | * Get/Set reCaptcha language 232 | */ 233 | useLang: function (widgetId, lang) { 234 | var instance = instances[widgetId]; 235 | 236 | if (instance) { 237 | var iframe = instance.querySelector('iframe'); 238 | if (lang) { 239 | // Setter 240 | if (iframe && iframe.src) { 241 | var s = iframe.src; 242 | if (/[?&]hl=/.test(s)) { 243 | s = s.replace(/([?&]hl=)\w+/, '$1' + lang); 244 | } else { 245 | s += ((s.indexOf('?') === -1) ? '?' : '&') + 'hl=' + lang; 246 | } 247 | 248 | iframe.src = s; 249 | } 250 | } else { 251 | // Getter 252 | if (iframe && iframe.src && /[?&]hl=\w+/.test(iframe.src)) { 253 | return iframe.src.replace(/.+[?&]hl=(\w+)([^\w].+)?/, '$1'); 254 | } else { 255 | return null; 256 | } 257 | } 258 | } else { 259 | throw new Error('reCaptcha Widget ID not exists', widgetId); 260 | } 261 | }, 262 | 263 | /** 264 | * Gets the response from the reCaptcha widget. 265 | * 266 | * @see https://developers.google.com/recaptcha/docs/display#js_api 267 | * 268 | * @returns {String} 269 | */ 270 | getResponse: function (widgetId) { 271 | validateRecaptchaInstance(); 272 | 273 | return recaptcha.getResponse(widgetId); 274 | }, 275 | 276 | /** 277 | * Gets reCaptcha instance and configuration 278 | */ 279 | getInstance: function (widgetId) { 280 | return instances[widgetId]; 281 | }, 282 | 283 | /** 284 | * Destroy reCaptcha instance. 285 | */ 286 | destroy: function (widgetId) { 287 | delete instances[widgetId]; 288 | } 289 | }; 290 | 291 | }]; 292 | }); 293 | 294 | }(angular)); 295 | 296 | /*global angular, Recaptcha */ 297 | (function (ng) { 298 | 'use strict'; 299 | 300 | var app = ng.module('vcRecaptcha'); 301 | 302 | app.directive('vcRecaptcha', ['$document', '$timeout', 'vcRecaptchaService', function ($document, $timeout, vcRecaptcha) { 303 | 304 | return { 305 | restrict: 'A', 306 | require: "?^^form", 307 | scope: { 308 | response: '=?ngModel', 309 | key: '=?', 310 | stoken: '=?', 311 | theme: '=?', 312 | size: '=?', 313 | type: '=?', 314 | lang: '=?', 315 | badge: '=?', 316 | tabindex: '=?', 317 | required: '=?', 318 | onCreate: '&', 319 | onSuccess: '&', 320 | onExpire: '&', 321 | onError: '&' 322 | }, 323 | link: function (scope, elm, attrs, ctrl) { 324 | scope.widgetId = null; 325 | 326 | if(ctrl && ng.isDefined(attrs.required)){ 327 | scope.$watch('required', validate); 328 | } 329 | 330 | var removeCreationListener = scope.$watch('key', function (key) { 331 | var callback = function (gRecaptchaResponse) { 332 | // Safe $apply 333 | $timeout(function () { 334 | scope.response = gRecaptchaResponse; 335 | validate(); 336 | 337 | // Notify about the response availability 338 | scope.onSuccess({response: gRecaptchaResponse, widgetId: scope.widgetId}); 339 | }); 340 | }; 341 | 342 | vcRecaptcha.create(elm[0], { 343 | 344 | callback: callback, 345 | key: key, 346 | stoken: scope.stoken || attrs.stoken || null, 347 | theme: scope.theme || attrs.theme || null, 348 | type: scope.type || attrs.type || null, 349 | lang: scope.lang || attrs.lang || null, 350 | tabindex: scope.tabindex || attrs.tabindex || null, 351 | size: scope.size || attrs.size || null, 352 | badge: scope.badge || attrs.badge || null, 353 | 'expired-callback': expired, 354 | 'error-callback': attrs.onError ? error : undefined 355 | 356 | }).then(function (widgetId) { 357 | // The widget has been created 358 | validate(); 359 | scope.widgetId = widgetId; 360 | scope.onCreate({widgetId: widgetId}); 361 | 362 | scope.$on('$destroy', destroy); 363 | 364 | scope.$on('reCaptchaReset', function(event, resetWidgetId){ 365 | if(ng.isUndefined(resetWidgetId) || widgetId === resetWidgetId){ 366 | scope.response = ""; 367 | validate(); 368 | } 369 | }) 370 | 371 | }); 372 | 373 | // Remove this listener to avoid creating the widget more than once. 374 | removeCreationListener(); 375 | }); 376 | 377 | function destroy() { 378 | if (ctrl) { 379 | // reset the validity of the form if we were removed 380 | ctrl.$setValidity('recaptcha', null); 381 | } 382 | 383 | cleanup(); 384 | } 385 | 386 | function expired(){ 387 | // Safe $apply 388 | $timeout(function () { 389 | scope.response = ""; 390 | validate(); 391 | 392 | // Notify about the response availability 393 | scope.onExpire({ widgetId: scope.widgetId }); 394 | }); 395 | } 396 | 397 | function error() { 398 | var args = arguments; 399 | $timeout(function () { 400 | scope.response = ""; 401 | validate(); 402 | 403 | // Notify about the response availability 404 | scope.onError({ widgetId: scope.widgetId, arguments: args }); 405 | }); 406 | } 407 | 408 | function validate(){ 409 | if(ctrl){ 410 | ctrl.$setValidity('recaptcha', scope.required === false ? null : Boolean(scope.response)); 411 | } 412 | } 413 | 414 | function cleanup(){ 415 | vcRecaptcha.destroy(scope.widgetId); 416 | 417 | // removes elements reCaptcha added. 418 | ng.element($document[0].querySelectorAll('.pls-container')).parent().remove(); 419 | } 420 | } 421 | }; 422 | }]); 423 | 424 | }(angular)); 425 | -------------------------------------------------------------------------------- /release/angular-recaptcha.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license angular-recaptcha build:2018-07-30 3 | * https://github.com/vividcortex/angular-recaptcha 4 | * Copyright (c) 2018 VividCortex 5 | **/ 6 | 7 | 8 | !function(a){"use strict";a.module("vcRecaptcha",[])}(angular),function(a){"use strict";function b(){throw new Error('You need to set the "key" attribute to your public reCaptcha key. If you don\'t have a key, please get one from https://www.google.com/recaptcha/admin/create')}a.module("vcRecaptcha").provider("vcRecaptchaService",function(){var c=this,d={};c.onLoadFunctionName="vcRecaptchaApiLoaded",c.setDefaults=function(b){a.copy(b,d)},c.setSiteKey=function(a){d.key=a},c.setTheme=function(a){d.theme=a},c.setStoken=function(a){d.stoken=a},c.setSize=function(a){d.size=a},c.setType=function(a){d.type=a},c.setLang=function(a){d.lang=a},c.setBadge=function(a){d.badge=a},c.setOnLoadFunctionName=function(a){c.onLoadFunctionName=a},c.$get=["$rootScope","$window","$q","$document","$interval",function(e,f,g,h,i){function j(){return m?g.when(m):o}function k(){if(!m)throw new Error("reCaptcha has not been loaded yet.")}function l(){return a.isFunction((f.grecaptcha||{}).render)}var m,n=g.defer(),o=n.promise,p={};f.vcRecaptchaApiLoadedCallback=f.vcRecaptchaApiLoadedCallback||[];var q=function(){m=f.grecaptcha,n.resolve(m)};if(f.vcRecaptchaApiLoadedCallback.push(q),f[c.onLoadFunctionName]=function(){f.vcRecaptchaApiLoadedCallback.forEach(function(a){a()})},l())q();else if(f.document.querySelector('script[src^="https://www.google.com/recaptcha/api.js"]'))var r=i(function(){l()&&(i.cancel(r),q())},25);else{var s=f.document.createElement("script");s.async=!0,s.defer=!0,s.src="https://www.google.com/recaptcha/api.js?onload="+c.onLoadFunctionName+"&render=explicit",h.find("body")[0].appendChild(s)}return{create:function(a,c){return c.sitekey=c.key||d.key,c.theme=c.theme||d.theme,c.stoken=c.stoken||d.stoken,c.size=c.size||d.size,c.type=c.type||d.type,c.hl=c.lang||d.lang,c.badge=c.badge||d.badge,c.sitekey||b(),j().then(function(b){var d=b.render(a,c);return p[d]=a,d})},reload:function(a){k(),m.reset(a),e.$broadcast("reCaptchaReset",a)},execute:function(a){k(),m.execute(a)},useLang:function(a,b){var c=p[a];if(!c)throw new Error("reCaptcha Widget ID not exists",a);var d=c.querySelector("iframe");if(!b)return d&&d.src&&/[?&]hl=\w+/.test(d.src)?d.src.replace(/.+[?&]hl=(\w+)([^\w].+)?/,"$1"):null;if(d&&d.src){var e=d.src;/[?&]hl=/.test(e)?e=e.replace(/([?&]hl=)\w+/,"$1"+b):e+=(-1===e.indexOf("?")?"?":"&")+"hl="+b,d.src=e}},getResponse:function(a){return k(),m.getResponse(a)},getInstance:function(a){return p[a]},destroy:function(a){delete p[a]}}}]})}(angular),function(a){"use strict";a.module("vcRecaptcha").directive("vcRecaptcha",["$document","$timeout","vcRecaptchaService",function(b,c,d){return{restrict:"A",require:"?^^form",scope:{response:"=?ngModel",key:"=?",stoken:"=?",theme:"=?",size:"=?",type:"=?",lang:"=?",badge:"=?",tabindex:"=?",required:"=?",onCreate:"&",onSuccess:"&",onExpire:"&",onError:"&"},link:function(e,f,g,h){function i(){h&&h.$setValidity("recaptcha",null),m()}function j(){c(function(){e.response="",l(),e.onExpire({widgetId:e.widgetId})})}function k(){var a=arguments;c(function(){e.response="",l(),e.onError({widgetId:e.widgetId,arguments:a})})}function l(){h&&h.$setValidity("recaptcha",!1===e.required?null:Boolean(e.response))}function m(){d.destroy(e.widgetId),a.element(b[0].querySelectorAll(".pls-container")).parent().remove()}e.widgetId=null,h&&a.isDefined(g.required)&&e.$watch("required",l);var n=e.$watch("key",function(b){var h=function(a){c(function(){e.response=a,l(),e.onSuccess({response:a,widgetId:e.widgetId})})};d.create(f[0],{callback:h,key:b,stoken:e.stoken||g.stoken||null,theme:e.theme||g.theme||null,type:e.type||g.type||null,lang:e.lang||g.lang||null,tabindex:e.tabindex||g.tabindex||null,size:e.size||g.size||null,badge:e.badge||g.badge||null,"expired-callback":j,"error-callback":g.onError?k:void 0}).then(function(b){l(),e.widgetId=b,e.onCreate({widgetId:b}),e.$on("$destroy",i),e.$on("reCaptchaReset",function(c,d){(a.isUndefined(d)||b===d)&&(e.response="",l())})}),n()})}}}])}(angular); -------------------------------------------------------------------------------- /src/directive.js: -------------------------------------------------------------------------------- 1 | /*global angular, Recaptcha */ 2 | (function (ng) { 3 | 'use strict'; 4 | 5 | var app = ng.module('vcRecaptcha'); 6 | 7 | app.directive('vcRecaptcha', ['$document', '$timeout', 'vcRecaptchaService', function ($document, $timeout, vcRecaptcha) { 8 | 9 | return { 10 | restrict: 'A', 11 | require: "?^^form", 12 | scope: { 13 | response: '=?ngModel', 14 | key: '=?', 15 | stoken: '=?', 16 | theme: '=?', 17 | size: '=?', 18 | type: '=?', 19 | lang: '=?', 20 | badge: '=?', 21 | tabindex: '=?', 22 | required: '=?', 23 | onCreate: '&', 24 | onSuccess: '&', 25 | onExpire: '&', 26 | onError: '&' 27 | }, 28 | link: function (scope, elm, attrs, ctrl) { 29 | scope.widgetId = null; 30 | 31 | if(ctrl && ng.isDefined(attrs.required)){ 32 | scope.$watch('required', validate); 33 | } 34 | 35 | var removeCreationListener = scope.$watch('key', function (key) { 36 | var callback = function (gRecaptchaResponse) { 37 | // Safe $apply 38 | $timeout(function () { 39 | scope.response = gRecaptchaResponse; 40 | validate(); 41 | 42 | // Notify about the response availability 43 | scope.onSuccess({response: gRecaptchaResponse, widgetId: scope.widgetId}); 44 | }); 45 | }; 46 | 47 | vcRecaptcha.create(elm[0], { 48 | 49 | callback: callback, 50 | key: key, 51 | stoken: scope.stoken || attrs.stoken || null, 52 | theme: scope.theme || attrs.theme || null, 53 | type: scope.type || attrs.type || null, 54 | lang: scope.lang || attrs.lang || null, 55 | tabindex: scope.tabindex || attrs.tabindex || null, 56 | size: scope.size || attrs.size || null, 57 | badge: scope.badge || attrs.badge || null, 58 | 'expired-callback': expired, 59 | 'error-callback': attrs.onError ? error : undefined 60 | 61 | }).then(function (widgetId) { 62 | // The widget has been created 63 | validate(); 64 | scope.widgetId = widgetId; 65 | scope.onCreate({widgetId: widgetId}); 66 | 67 | scope.$on('$destroy', destroy); 68 | 69 | scope.$on('reCaptchaReset', function(event, resetWidgetId){ 70 | if(ng.isUndefined(resetWidgetId) || widgetId === resetWidgetId){ 71 | scope.response = ""; 72 | validate(); 73 | } 74 | }) 75 | 76 | }); 77 | 78 | // Remove this listener to avoid creating the widget more than once. 79 | removeCreationListener(); 80 | }); 81 | 82 | function destroy() { 83 | if (ctrl) { 84 | // reset the validity of the form if we were removed 85 | ctrl.$setValidity('recaptcha', null); 86 | } 87 | 88 | cleanup(); 89 | } 90 | 91 | function expired(){ 92 | // Safe $apply 93 | $timeout(function () { 94 | scope.response = ""; 95 | validate(); 96 | 97 | // Notify about the response availability 98 | scope.onExpire({ widgetId: scope.widgetId }); 99 | }); 100 | } 101 | 102 | function error() { 103 | var args = arguments; 104 | $timeout(function () { 105 | scope.response = ""; 106 | validate(); 107 | 108 | // Notify about the response availability 109 | scope.onError({ widgetId: scope.widgetId, arguments: args }); 110 | }); 111 | } 112 | 113 | function validate(){ 114 | if(ctrl){ 115 | ctrl.$setValidity('recaptcha', scope.required === false ? null : Boolean(scope.response)); 116 | } 117 | } 118 | 119 | function cleanup(){ 120 | vcRecaptcha.destroy(scope.widgetId); 121 | 122 | // removes elements reCaptcha added. 123 | ng.element($document[0].querySelectorAll('.pls-container')).parent().remove(); 124 | } 125 | } 126 | }; 127 | }]); 128 | 129 | }(angular)); 130 | -------------------------------------------------------------------------------- /src/module.js: -------------------------------------------------------------------------------- 1 | /*global angular, Recaptcha */ 2 | (function (ng) { 3 | 'use strict'; 4 | 5 | ng.module('vcRecaptcha', []); 6 | 7 | }(angular)); 8 | -------------------------------------------------------------------------------- /src/service.js: -------------------------------------------------------------------------------- 1 | /*global angular */ 2 | (function (ng) { 3 | 'use strict'; 4 | 5 | function throwNoKeyException() { 6 | throw new Error('You need to set the "key" attribute to your public reCaptcha key. If you don\'t have a key, please get one from https://www.google.com/recaptcha/admin/create'); 7 | } 8 | 9 | var app = ng.module('vcRecaptcha'); 10 | 11 | /** 12 | * An angular service to wrap the reCaptcha API 13 | */ 14 | app.provider('vcRecaptchaService', function(){ 15 | var provider = this; 16 | var config = {}; 17 | provider.onLoadFunctionName = 'vcRecaptchaApiLoaded'; 18 | 19 | /** 20 | * Sets the reCaptcha configuration values which will be used by default is not specified in a specific directive instance. 21 | * 22 | * @since 2.5.0 23 | * @param defaults object which overrides the current defaults object. 24 | */ 25 | provider.setDefaults = function(defaults){ 26 | ng.copy(defaults, config); 27 | }; 28 | 29 | /** 30 | * Sets the reCaptcha key which will be used by default is not specified in a specific directive instance. 31 | * 32 | * @since 2.5.0 33 | * @param siteKey the reCaptcha public key (refer to the README file if you don't know what this is). 34 | */ 35 | provider.setSiteKey = function(siteKey){ 36 | config.key = siteKey; 37 | }; 38 | 39 | /** 40 | * Sets the reCaptcha theme which will be used by default is not specified in a specific directive instance. 41 | * 42 | * @since 2.5.0 43 | * @param theme The reCaptcha theme. 44 | */ 45 | provider.setTheme = function(theme){ 46 | config.theme = theme; 47 | }; 48 | 49 | /** 50 | * Sets the reCaptcha stoken which will be used by default is not specified in a specific directive instance. 51 | * 52 | * @since 2.5.0 53 | * @param stoken The reCaptcha stoken. 54 | */ 55 | provider.setStoken = function(stoken){ 56 | config.stoken = stoken; 57 | }; 58 | 59 | /** 60 | * Sets the reCaptcha size which will be used by default is not specified in a specific directive instance. 61 | * 62 | * @since 2.5.0 63 | * @param size The reCaptcha size. 64 | */ 65 | provider.setSize = function(size){ 66 | config.size = size; 67 | }; 68 | 69 | /** 70 | * Sets the reCaptcha type which will be used by default is not specified in a specific directive instance. 71 | * 72 | * @since 2.5.0 73 | * @param type The reCaptcha type. 74 | */ 75 | provider.setType = function(type){ 76 | config.type = type; 77 | }; 78 | 79 | /** 80 | * Sets the reCaptcha language which will be used by default is not specified in a specific directive instance. 81 | * 82 | * @param lang The reCaptcha language. 83 | */ 84 | provider.setLang = function(lang){ 85 | config.lang = lang; 86 | }; 87 | 88 | /** 89 | * Sets the reCaptcha badge position which will be used by default if not specified in a specific directive instance. 90 | * 91 | * @param badge The reCaptcha badge position. 92 | */ 93 | provider.setBadge = function(badge){ 94 | config.badge = badge; 95 | }; 96 | 97 | /** 98 | * Sets the reCaptcha configuration values which will be used by default is not specified in a specific directive instance. 99 | * 100 | * @since 2.5.0 101 | * @param onLoadFunctionName string name which overrides the name of the onload function. Should match what is in the recaptcha script querystring onload value. 102 | */ 103 | provider.setOnLoadFunctionName = function(onLoadFunctionName){ 104 | provider.onLoadFunctionName = onLoadFunctionName; 105 | }; 106 | 107 | provider.$get = ['$rootScope','$window', '$q', '$document', '$interval', function ($rootScope, $window, $q, $document, $interval) { 108 | var deferred = $q.defer(), promise = deferred.promise, instances = {}, recaptcha; 109 | 110 | $window.vcRecaptchaApiLoadedCallback = $window.vcRecaptchaApiLoadedCallback || []; 111 | 112 | var callback = function () { 113 | recaptcha = $window.grecaptcha; 114 | 115 | deferred.resolve(recaptcha); 116 | }; 117 | 118 | $window.vcRecaptchaApiLoadedCallback.push(callback); 119 | 120 | $window[provider.onLoadFunctionName] = function () { 121 | $window.vcRecaptchaApiLoadedCallback.forEach(function(callback) { 122 | callback(); 123 | }); 124 | }; 125 | 126 | 127 | function getRecaptcha() { 128 | if (!!recaptcha) { 129 | return $q.when(recaptcha); 130 | } 131 | 132 | return promise; 133 | } 134 | 135 | function validateRecaptchaInstance() { 136 | if (!recaptcha) { 137 | throw new Error('reCaptcha has not been loaded yet.'); 138 | } 139 | } 140 | 141 | function isRenderFunctionAvailable() { 142 | return ng.isFunction(($window.grecaptcha || {}).render); 143 | } 144 | 145 | 146 | // Check if grecaptcha.render is not defined already. 147 | if (isRenderFunctionAvailable()) { 148 | callback(); 149 | } else if ($window.document.querySelector('script[src^="https://www.google.com/recaptcha/api.js"]')) { 150 | // wait for script to be loaded. 151 | var intervalWait = $interval(function() { 152 | if (isRenderFunctionAvailable()) { 153 | $interval.cancel(intervalWait); 154 | callback(); 155 | } 156 | }, 25); 157 | } else { 158 | // Generate link on demand 159 | var script = $window.document.createElement('script'); 160 | script.async = true; 161 | script.defer = true; 162 | script.src = 'https://www.google.com/recaptcha/api.js?onload='+provider.onLoadFunctionName+'&render=explicit'; 163 | $document.find('body')[0].appendChild(script); 164 | } 165 | 166 | return { 167 | 168 | /** 169 | * Creates a new reCaptcha object 170 | * 171 | * @param elm the DOM element where to put the captcha 172 | * @param conf the captcha object configuration 173 | * @throws NoKeyException if no key is provided in the provider config or the directive instance (via attribute) 174 | */ 175 | create: function (elm, conf) { 176 | 177 | conf.sitekey = conf.key || config.key; 178 | conf.theme = conf.theme || config.theme; 179 | conf.stoken = conf.stoken || config.stoken; 180 | conf.size = conf.size || config.size; 181 | conf.type = conf.type || config.type; 182 | conf.hl = conf.lang || config.lang; 183 | conf.badge = conf.badge || config.badge; 184 | 185 | if (!conf.sitekey) { 186 | throwNoKeyException(); 187 | } 188 | return getRecaptcha().then(function (recaptcha) { 189 | var widgetId = recaptcha.render(elm, conf); 190 | instances[widgetId] = elm; 191 | return widgetId; 192 | }); 193 | }, 194 | 195 | /** 196 | * Reloads the reCaptcha 197 | */ 198 | reload: function (widgetId) { 199 | validateRecaptchaInstance(); 200 | 201 | recaptcha.reset(widgetId); 202 | 203 | // Let everyone know this widget has been reset. 204 | $rootScope.$broadcast('reCaptchaReset', widgetId); 205 | }, 206 | 207 | /** 208 | * Executes the reCaptcha 209 | */ 210 | execute: function (widgetId) { 211 | validateRecaptchaInstance(); 212 | 213 | recaptcha.execute(widgetId); 214 | }, 215 | 216 | /** 217 | * Get/Set reCaptcha language 218 | */ 219 | useLang: function (widgetId, lang) { 220 | var instance = instances[widgetId]; 221 | 222 | if (instance) { 223 | var iframe = instance.querySelector('iframe'); 224 | if (lang) { 225 | // Setter 226 | if (iframe && iframe.src) { 227 | var s = iframe.src; 228 | if (/[?&]hl=/.test(s)) { 229 | s = s.replace(/([?&]hl=)\w+/, '$1' + lang); 230 | } else { 231 | s += ((s.indexOf('?') === -1) ? '?' : '&') + 'hl=' + lang; 232 | } 233 | 234 | iframe.src = s; 235 | } 236 | } else { 237 | // Getter 238 | if (iframe && iframe.src && /[?&]hl=\w+/.test(iframe.src)) { 239 | return iframe.src.replace(/.+[?&]hl=(\w+)([^\w].+)?/, '$1'); 240 | } else { 241 | return null; 242 | } 243 | } 244 | } else { 245 | throw new Error('reCaptcha Widget ID not exists', widgetId); 246 | } 247 | }, 248 | 249 | /** 250 | * Gets the response from the reCaptcha widget. 251 | * 252 | * @see https://developers.google.com/recaptcha/docs/display#js_api 253 | * 254 | * @returns {String} 255 | */ 256 | getResponse: function (widgetId) { 257 | validateRecaptchaInstance(); 258 | 259 | return recaptcha.getResponse(widgetId); 260 | }, 261 | 262 | /** 263 | * Gets reCaptcha instance and configuration 264 | */ 265 | getInstance: function (widgetId) { 266 | return instances[widgetId]; 267 | }, 268 | 269 | /** 270 | * Destroy reCaptcha instance. 271 | */ 272 | destroy: function (widgetId) { 273 | delete instances[widgetId]; 274 | } 275 | }; 276 | 277 | }]; 278 | }); 279 | 280 | }(angular)); 281 | -------------------------------------------------------------------------------- /tests/directive_test.js: -------------------------------------------------------------------------------- 1 | describe('directive: vcRecaptcha', function () { 2 | 'use strict'; 3 | 4 | 5 | var $scope, $compile, $timeout, vcRecaptchaService, 6 | 7 | TIMEOUT_SESSION_CAPTCHA = 2 * 60 * 1000, // 2 minutes 8 | VALID_KEY = '1234567890123456789012345678901234567890'; 9 | 10 | beforeEach(function () { 11 | module('vcRecaptcha'); 12 | 13 | 14 | inject(function ($rootScope, _$compile_, _$timeout_, _vcRecaptchaService_) { 15 | $scope = $rootScope.$new(); 16 | $compile = _$compile_; 17 | $timeout = _$timeout_; 18 | 19 | vcRecaptchaService = _vcRecaptchaService_; 20 | }); 21 | }); 22 | 23 | describe('invalid key', function () { 24 | var elementHtml, expectedMessage; 25 | 26 | afterEach(function () { 27 | expect(function () { 28 | var element = angular.element(elementHtml); 29 | 30 | $compile(element)($scope); 31 | $scope.$digest(); 32 | }).toThrow(new Error(expectedMessage)); 33 | }); 34 | 35 | it('should throw an error - no key', function () { 36 | elementHtml = '
'; 37 | expectedMessage = 'You need to set the "key" attribute to your public reCaptcha key. If you don\'t have a key, please get one from https://www.google.com/recaptcha/admin/create'; 38 | }); 39 | }); 40 | 41 | describe('widgetId', function () { 42 | it('should be null at start', function () { 43 | var element = angular.element('
'); 44 | 45 | $scope.key = VALID_KEY; 46 | 47 | expect(function () { 48 | $compile(element)($scope); 49 | $scope.$digest(); 50 | }).not.toThrow(); 51 | 52 | expect(element.isolateScope().widgetId).toBeNull(); 53 | }); 54 | }); 55 | 56 | describe('form validation', function () { 57 | beforeEach(function () { 58 | $scope.key = VALID_KEY; 59 | $scope.onCreate = jasmine.createSpy('onCreate'); 60 | $scope.onSuccess = jasmine.createSpy('onSuccess'); 61 | $scope.onError = jasmine.createSpy('onError'); 62 | }); 63 | 64 | afterEach(function () { 65 | expect(vcRecaptchaService.create).toHaveBeenCalled(); 66 | }); 67 | 68 | it('should change the validation to false, widget just created', function () { 69 | var element = angular.element( 70 | '
' + 71 | '' + 72 | '
' + 73 | '' 74 | ), 75 | 76 | _fakeCreate = function () { 77 | return { 78 | then: function (cb) { 79 | var _widgetId = 'a'; 80 | 81 | cb(_widgetId); 82 | } 83 | }; 84 | }; 85 | 86 | spyOn(vcRecaptchaService, 'create').and.callFake(_fakeCreate); 87 | 88 | $compile(element)($scope); 89 | $scope.$digest(); 90 | 91 | expect($scope.form.$valid).toBeFalsy(); // widgetCreated 92 | expect($scope.onCreate).toHaveBeenCalledWith({widgetId: 'a'}); 93 | }); 94 | 95 | it('should change the validation to true - first timeout flushed', function () { 96 | var element = angular.element('
' + 97 | '' + 98 | '
' + 99 | ''), 100 | 101 | _fakeCreate = function (element, config) { 102 | config.callback('response from google'); 103 | 104 | return { 105 | then: function (cb) { 106 | cb(); 107 | } 108 | }; 109 | }; 110 | 111 | spyOn(vcRecaptchaService, 'create').and.callFake(_fakeCreate); 112 | 113 | $compile(element)($scope); 114 | $scope.$digest(); 115 | 116 | $timeout.flush(TIMEOUT_SESSION_CAPTCHA - 1); 117 | 118 | expect($scope.form.$valid).toBeTruthy(); 119 | }); 120 | 121 | it('should change the validation to false - session expired', function () { 122 | var element = angular.element('
' + 123 | '' + 124 | '
' + 125 | ''), 126 | 127 | _fakeCreate = function (element, config) { 128 | // Call the expiration callback as recaptcha would do. 129 | config['expired-callback'](); 130 | 131 | return { 132 | then: function (cb) { 133 | cb(); 134 | } 135 | }; 136 | }; 137 | 138 | spyOn(vcRecaptchaService, 'create').and.callFake(_fakeCreate); 139 | 140 | 141 | $compile(element)($scope); 142 | $scope.$digest(); 143 | 144 | $timeout.flush(TIMEOUT_SESSION_CAPTCHA + 1); 145 | 146 | expect($scope.form.$valid).toBeFalsy(); // widgetCreated 147 | }); 148 | 149 | it('should call the onSuccess callback with the right params', function () { 150 | var element = angular.element('
' + 151 | '' + 152 | '
' + 153 | ''), 154 | 155 | _fakeCreate = function (element, config) { 156 | config.callback('response from google'); 157 | 158 | return { 159 | then: function (cb) { 160 | cb(); 161 | } 162 | }; 163 | }; 164 | 165 | spyOn(vcRecaptchaService, 'create').and.callFake(_fakeCreate); 166 | 167 | $compile(element)($scope); 168 | $scope.$digest(); 169 | $timeout.flush(); 170 | 171 | expect($scope.onSuccess).toHaveBeenCalledWith({ 172 | response: 'response from google', 173 | widgetId: undefined 174 | }); 175 | }); 176 | 177 | it('should call the onError callback', function () { 178 | var element = angular.element('
' + 179 | '' + 180 | '
' + 181 | ''), 182 | 183 | _fakeCreate = function (element, config) { 184 | config['error-callback']('something about the error'); 185 | 186 | return { 187 | then: function (cb) { 188 | cb(); 189 | } 190 | }; 191 | }; 192 | 193 | spyOn(vcRecaptchaService, 'create').and.callFake(_fakeCreate); 194 | 195 | $compile(element)($scope); 196 | $scope.$digest(); 197 | $timeout.flush(); 198 | 199 | expect($scope.onError).toHaveBeenCalled(); 200 | }); 201 | 202 | it('the widget should be using the setted language', function () { 203 | var element = angular.element('
' + 204 | '' + 205 | '
' + 206 | ''), 207 | 208 | _fakeCreate = function (element, config) { 209 | config.callback(config.lang); 210 | return { 211 | then: function (cb) { 212 | cb(); 213 | } 214 | }; 215 | }; 216 | 217 | spyOn(vcRecaptchaService, 'create').and.callFake(_fakeCreate); 218 | 219 | $compile(element)($scope); 220 | $scope.$digest(); 221 | $timeout.flush(); 222 | 223 | expect($scope.onSuccess).toHaveBeenCalledWith({ 224 | response: 'es', 225 | widgetId: undefined 226 | }); 227 | }); 228 | }); 229 | }); 230 | -------------------------------------------------------------------------------- /tests/provider.driver.js: -------------------------------------------------------------------------------- 1 | function ProviderDriver() { 2 | var _this = this; 3 | var mockModules = { 4 | $window: {} 5 | }; 6 | 7 | module(mockModules); // mock all the properties 8 | 9 | this.given = { 10 | recaptchaLoaded: function (recaptchaMock) { 11 | mockModules.$window.grecaptcha = recaptchaMock; 12 | return _this; 13 | } 14 | }; 15 | 16 | this.when = { 17 | created: function () { 18 | module(function (vcRecaptchaServiceProvider) { 19 | _this.provider = vcRecaptchaServiceProvider; 20 | }); 21 | 22 | inject(); // needed for angular-mocks to kick off 23 | 24 | return _this; 25 | }, 26 | callingCreate: function () { 27 | inject(function (vcRecaptchaService, $rootScope) { 28 | vcRecaptchaService.create(null, {}); 29 | $rootScope.$digest(); 30 | }); 31 | 32 | return this; 33 | } 34 | }; 35 | } -------------------------------------------------------------------------------- /tests/provider_test.js: -------------------------------------------------------------------------------- 1 | describe('provider', function () { 2 | 'use strict'; 3 | 4 | var driver, 5 | recaptchaMock, 6 | key; 7 | 8 | beforeEach(module('vcRecaptcha')); 9 | 10 | beforeEach(function () { 11 | driver = new ProviderDriver(); 12 | driver.given.recaptchaLoaded(recaptchaMock = jasmine.createSpyObj('recaptchaMock', ['render'])) 13 | .when.created(); 14 | 15 | driver.provider.setSiteKey(key = '1234567890123456789012345678901234567890'); 16 | }); 17 | 18 | it('should setDefaults', function () { 19 | var modifiedKey = key.substring(0, 39) + 'x'; 20 | 21 | driver.provider.setDefaults({key: modifiedKey}); 22 | 23 | driver.when.callingCreate(); 24 | 25 | var callArgs = recaptchaMock.render.calls.mostRecent().args[1]; 26 | 27 | expect(callArgs).toEqual(jasmine.objectContaining({sitekey: modifiedKey})); 28 | }); 29 | 30 | it('should setSiteKey', function () { 31 | driver.provider.setSiteKey(key); 32 | 33 | driver.when.callingCreate(); 34 | 35 | var callArgs = recaptchaMock.render.calls.mostRecent().args[1]; 36 | 37 | expect(callArgs).toEqual(jasmine.objectContaining({sitekey: key})); 38 | }); 39 | 40 | it('should setTheme', function () { 41 | var theme = 'theme'; 42 | driver.provider.setTheme(theme); 43 | 44 | driver.when.callingCreate(); 45 | 46 | var callArgs = recaptchaMock.render.calls.mostRecent().args[1]; 47 | 48 | expect(callArgs).toEqual(jasmine.objectContaining({theme: theme})); 49 | }); 50 | 51 | it('should setStoken', function () { 52 | var stoken = 'stoken'; 53 | driver.provider.setStoken(stoken); 54 | 55 | driver.when.callingCreate(); 56 | 57 | var callArgs = recaptchaMock.render.calls.mostRecent().args[1]; 58 | 59 | expect(callArgs).toEqual(jasmine.objectContaining({stoken: stoken})); 60 | }); 61 | 62 | it('should setSize', function () { 63 | var size = 'size'; 64 | driver.provider.setSize(size); 65 | 66 | driver.when.callingCreate(); 67 | 68 | var callArgs = recaptchaMock.render.calls.mostRecent().args[1]; 69 | 70 | expect(callArgs).toEqual(jasmine.objectContaining({size: size})); 71 | }); 72 | 73 | it('should setType', function () { 74 | var type = 'type'; 75 | driver.provider.setType(type); 76 | 77 | driver.when.callingCreate(); 78 | 79 | var callArgs = recaptchaMock.render.calls.mostRecent().args[1]; 80 | 81 | expect(callArgs).toEqual(jasmine.objectContaining({type: type})); 82 | }); 83 | 84 | it('should setLang', function () { 85 | var lang = 'en'; 86 | driver.provider.setLang(lang); 87 | 88 | driver.when.callingCreate(); 89 | 90 | var callArgs = recaptchaMock.render.calls.mostRecent().args[1]; 91 | 92 | expect(callArgs).toEqual(jasmine.objectContaining({hl: lang})); 93 | }); 94 | 95 | it('should setBadge', function () { 96 | var badge = 'bottomright'; 97 | driver.provider.setBadge(badge); 98 | 99 | driver.when.callingCreate(); 100 | 101 | var callArgs = recaptchaMock.render.calls.mostRecent().args[1]; 102 | 103 | expect(callArgs).toEqual(jasmine.objectContaining({badge: badge})); 104 | }); 105 | 106 | }); 107 | -------------------------------------------------------------------------------- /tests/service.driver.js: -------------------------------------------------------------------------------- 1 | function ServiceDriver() { 2 | var _this = this; 3 | var mockModules = { 4 | $window: {}, 5 | $document: {} 6 | }; 7 | 8 | module(mockModules); // mock all the properties 9 | 10 | this.given = { 11 | apiLoaded: function (mockRecaptcha) { 12 | mockModules.$window.grecaptcha = mockRecaptcha; 13 | 14 | return _this; 15 | }, 16 | onLoadFunctionName: function (funcName) { 17 | module(function (vcRecaptchaServiceProvider) { 18 | vcRecaptchaServiceProvider.setOnLoadFunctionName(funcName); 19 | }); 20 | return _this; 21 | }, 22 | mockDocument: function (mockDocument) { 23 | mockModules.$document = mockDocument; 24 | mockModules.$window.document = mockDocument[0]; 25 | 26 | return _this; 27 | } 28 | }; 29 | 30 | this.when = { 31 | created: function () { 32 | inject(function (vcRecaptchaService, _$interval_) { 33 | _this.service = vcRecaptchaService; 34 | _this.$interval = _$interval_; 35 | }) 36 | }, 37 | notifyThatApiLoaded: function () { 38 | mockModules.$window.vcRecaptchaApiLoaded(); 39 | return _this; 40 | } 41 | }; 42 | } 43 | 44 | ServiceDriver.prototype.applyChanges = function () { 45 | inject(function ($rootScope) { 46 | $rootScope.$digest(); 47 | }); 48 | }; -------------------------------------------------------------------------------- /tests/service_test.js: -------------------------------------------------------------------------------- 1 | describe('service', function () { 2 | 'use strict'; 3 | 4 | var driver; 5 | 6 | beforeEach(module('vcRecaptcha')); 7 | 8 | beforeEach(function () { 9 | driver = new ServiceDriver(); 10 | }); 11 | 12 | describe('with loaded api', function () { 13 | var grecaptchaMock; 14 | 15 | beforeEach(function () { 16 | driver 17 | .given.apiLoaded(grecaptchaMock = jasmine.createSpyObj('grecaptcha', ['render', 'getResponse', 'reset','execute'])) 18 | .when.created(); 19 | }); 20 | 21 | it('should call recaptcha.render', function () { 22 | var _element = '
', 23 | _key = '1234567890123456789012345678901234567890', 24 | _fn = angular.noop, 25 | _confRender = { 26 | sitekey: _key, 27 | key: _key, 28 | callback: _fn, 29 | theme: undefined, 30 | stoken: undefined, 31 | size: undefined, 32 | type: undefined, 33 | hl: undefined, 34 | badge: undefined 35 | }; 36 | 37 | driver.when.notifyThatApiLoaded(); 38 | 39 | driver.service.create(_element, { 40 | key: _confRender.key, 41 | callback: _fn 42 | }); 43 | 44 | driver.applyChanges(); 45 | 46 | expect(grecaptchaMock.render).toHaveBeenCalledWith(_element, _confRender); 47 | }); 48 | 49 | it('should call reset', function () { 50 | var _widgetId = 123; 51 | 52 | driver.service.reload(_widgetId); 53 | 54 | expect(grecaptchaMock.reset).toHaveBeenCalledWith(_widgetId); 55 | }); 56 | 57 | it('should call execute', function () { 58 | var _widgetId = 123; 59 | 60 | driver.service.execute(_widgetId); 61 | 62 | expect(grecaptchaMock.execute).toHaveBeenCalledWith(_widgetId); 63 | }); 64 | 65 | it('should call getResponse', function () { 66 | var _widgetId = 123; 67 | 68 | driver.service.getResponse(_widgetId); 69 | 70 | expect(grecaptchaMock.getResponse).toHaveBeenCalledWith(_widgetId); 71 | }); 72 | 73 | it('should call useLang', function () { 74 | var _element = angular.element('
')[0], 75 | _key = '1234567890123456789012345678901234567890'; 76 | 77 | driver.when.notifyThatApiLoaded(); 78 | 79 | driver.service.create(_element, { 80 | key: _key 81 | }).then(function (widgetId) { 82 | var instance = driver.service.getInstance(widgetId); 83 | expect(instance).toEqual(_element); 84 | 85 | driver.service.useLang(widgetId, 'es'); 86 | expect(driver.service.useLang(widgetId)).toEqual('es'); 87 | }); 88 | 89 | driver.applyChanges(); 90 | }); 91 | }); 92 | 93 | // Regresion test for https://git.io/vp2SO 94 | describe('without loaded api, loaded grecaptcha from cache without render func', () => { 95 | const grecaptchaMock = {}; 96 | const _key = '1234567890123456789012345678901234567890'; 97 | 98 | beforeEach(function () { 99 | const doc = [{ 100 | querySelector: () => ({}), 101 | }]; 102 | 103 | driver 104 | .given.apiLoaded(grecaptchaMock) 105 | .given.mockDocument(doc) 106 | .when.created(); 107 | }); 108 | 109 | it('should not try to render recaptcha', () => { 110 | const spy = jasmine.createSpy('recaptchaCreate'); 111 | 112 | driver.service.create('
', { 113 | key: _key, 114 | callback: spy 115 | }); 116 | 117 | expect(() => driver.applyChanges()).not.toThrow(); 118 | expect(spy).not.toHaveBeenCalled(); 119 | }); 120 | }); 121 | 122 | describe('without loaded api, with script tag', function () { 123 | var createElement, 124 | appendSpy, 125 | funcName, 126 | grecaptchaMock; 127 | 128 | beforeEach(function () { 129 | createElement = jasmine.createSpy('createElement'); 130 | appendSpy = jasmine.createSpy('appendSpy'); 131 | 132 | var doc = [{ 133 | createElement: createElement, 134 | querySelector: function() { 135 | return {}; 136 | } 137 | }]; 138 | doc.find = function () { 139 | return [{ 140 | appendChild: appendSpy 141 | }]; 142 | }; 143 | 144 | driver 145 | .given.onLoadFunctionName(funcName = 'my-func') 146 | .given.mockDocument(doc) 147 | .when.created(); 148 | }); 149 | 150 | it('should not add script tag to body', function () { 151 | expect(createElement).not.toHaveBeenCalled(); 152 | }); 153 | 154 | it('should validate that recaptcha is not loaded', function () { 155 | expect(driver.service.reload).toThrowError('reCaptcha has not been loaded yet.'); 156 | }); 157 | 158 | it('should validate that recaptcha is loaded after script is loaded', function () { 159 | driver 160 | .given.apiLoaded(grecaptchaMock = jasmine.createSpyObj('grecaptcha', ['render', 'getResponse', 'reset','execute'])); 161 | 162 | driver.$interval.flush(25); 163 | 164 | var _widgetId = 123; 165 | 166 | driver.service.execute(_widgetId); 167 | 168 | expect(grecaptchaMock.execute).toHaveBeenCalledWith(_widgetId); 169 | }); 170 | 171 | it('should not proceed if render function not available in grecaptcha', function () { 172 | driver.given.apiLoaded(grecaptchaMock = jasmine.createSpyObj('grecaptcha', ['reset'])); 173 | driver.$interval.flush(25); 174 | 175 | expect(() => driver.service.execute(123)).toThrowError('reCaptcha has not been loaded yet.'); 176 | }); 177 | }); 178 | 179 | describe('without loaded api', function () { 180 | var scriptTagSpy, 181 | appendSpy, 182 | funcName; 183 | 184 | beforeEach(function () { 185 | scriptTagSpy = jasmine.createSpy('scriptTagSpy'); 186 | appendSpy = jasmine.createSpy('appendSpy'); 187 | 188 | var doc = [{ 189 | createElement: function () { 190 | return scriptTagSpy; 191 | }, 192 | querySelector: function() { 193 | return null; 194 | } 195 | }]; 196 | doc.find = function () { 197 | return [{ 198 | appendChild: appendSpy 199 | }]; 200 | }; 201 | 202 | driver 203 | .given.onLoadFunctionName(funcName = 'my-func') 204 | .given.mockDocument(doc) 205 | .when.created(); 206 | }); 207 | 208 | it('should add script tag to body', function () { 209 | expect(scriptTagSpy.async).toBe(true); 210 | expect(scriptTagSpy.defer).toBe(true); 211 | expect(appendSpy).toHaveBeenCalledWith(scriptTagSpy); 212 | }); 213 | 214 | it('should add callback function name to src', function () { 215 | expect(scriptTagSpy.src).toBe('https://www.google.com/recaptcha/api.js?onload=' + funcName + '&render=explicit'); 216 | }); 217 | 218 | it('should validate that recaptcha is loaded', function () { 219 | expect(driver.service.reload).toThrowError('reCaptcha has not been loaded yet.'); 220 | }); 221 | }); 222 | }); 223 | --------------------------------------------------------------------------------