├── .editorconfig ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── angular.json ├── e2e ├── protractor-ci.conf.js ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── jsr.json ├── package-lock.json ├── package.json ├── projects └── ng-hcaptcha │ ├── karma.conf.js │ ├── ng-package.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── hcaptcha-utils.ts │ │ ├── ng-hcaptcha-config.ts │ │ ├── ng-hcaptcha-invisible-button.directive.spec.ts │ │ ├── ng-hcaptcha-invisible-button.directive.ts │ │ ├── ng-hcaptcha.component.spec.ts │ │ ├── ng-hcaptcha.component.ts │ │ ├── ng-hcaptcha.module.ts │ │ └── ng-hcaptcha.service.ts │ ├── public_api.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ └── tslint.json ├── src ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── form │ │ ├── HcaptchaFormModel.ts │ │ ├── form.component.html │ │ ├── form.component.scss │ │ ├── form.component.spec.ts │ │ └── form.component.ts │ ├── invisible │ │ ├── invisible.component.html │ │ ├── invisible.component.scss │ │ ├── invisible.component.spec.ts │ │ └── invisible.component.ts │ └── programmatically │ │ ├── programmatically.component.html │ │ ├── programmatically.component.scss │ │ ├── programmatically.component.spec.ts │ │ └── programmatically.component.ts ├── assets │ ├── .gitkeep │ └── logo.svg ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: Build ng-hcaptcha 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | node-version: [20.x] 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | 13 | - name: Cache node_modules 14 | uses: actions/cache@v1 15 | with: 16 | path: ~/.npm 17 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 18 | restore-keys: | 19 | ${{ runner.os }}-node- 20 | - name: Node ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - name: npm ci and npm run build 25 | run: | 26 | npm ci 27 | npm run build:lib 28 | #- name: Push Build to Releases 29 | # uses: ncipollo/release-action@v1 30 | # with: 31 | # artifacts: "dist/angular-githubaction/*" 32 | # token: ${{ secrets.TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.angular/cache 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | 38 | # System Files 39 | .DS_Store 40 | Thumbs.db 41 | /projects/ng-hcaptcha/node_modules/* 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: false 3 | 4 | language: node_js 5 | nodejs: 6 | - "8" 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | 12 | addons: 13 | apt: 14 | sources: 15 | - google-chrome 16 | packages: 17 | - google-chrome-stable 18 | 19 | install: 20 | - npm install 21 | 22 | script: 23 | - npm run test:ci 24 | - npm run build-lib 25 | - npm run e2e:ci -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.0-beta.3 (2018-12-21) 2 | 3 | ## Fixes 4 | - Fix error with function calls inside the forRoot function 5 | 6 | # 0.1.0-beta.1 (2018-12-21) 7 | 8 | Add support vor Angular 7.x 9 | 10 | # 0.1.0-beta.0 (2018-10-04) 11 | 12 | ## Features 13 | - Implement ControlValueAccessor (allows data binding through ngModel and basic validation) 14 | - Add proper demo page 15 | 16 | ## Fixes 17 | - Fix issues with multiple hCaptcha instances 18 | 19 | # 0.0.1 (2018-10-01) 20 | Initial release 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Niclas Stelzer 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub license](https://img.shields.io/github/license/leNicDev/ng-hcaptcha.svg)](https://github.com/leNicDev/ng-hcaptcha/blob/master/LICENSE)  2 | [![GitHub issues](https://img.shields.io/github/issues/leNicDev/ng-hcaptcha.svg)](https://GitHub.com/leNicDev/ng-hcaptcha/issues/)  3 | [![GitHub pull-requests](https://img.shields.io/github/issues-pr/leNicDev/ng-hcaptcha.svg)](https://GitHub.com/leNicDev/ng-hcaptcha/pull/) 4 | 5 | # ng-hcaptcha - hCaptcha Component for Angular 6 | 7 | ng-hcaptcha provides an easy to use component for [hCaptcha](https://hcaptcha.com). 8 | 9 | ## Table of Contents 10 | 11 | - [Supported Angular versions](#supported-angular-versions) 12 | - [Installation](#installation) 13 | - [Usage](#usage) 14 | - [Execute hCaptcha programmatically](#execute-hcaptcha-programmatically) 15 | - [Bugs? Ideas?](#bugs-ideas) 16 | - [Support me](#support-me) 17 | 18 | ## Supported Angular versions 19 | 20 | The latest version of `ng-hcaptcha` currently supports Angular v9 to v20. 21 | 22 | ## Installation 23 | 24 | ### Step 1 - Install the ng-hcaptcha dependency 25 | 26 | ```shell 27 | # NPM 28 | npm install --save ng-hcaptcha 29 | 30 | # Yarn 31 | yarn add ng-hcaptcha 32 | ``` 33 | 34 | > You can use the tag 'next' to get the latest beta version. 35 | 36 | ### Step 2 - Import the NgHcaptchaModule 37 | 38 | ```ts 39 | import { NgHcaptchaModule } from 'ng-hcaptcha'; 40 | 41 | @NgModule({ 42 | imports: [ 43 | // Option #1 44 | // Set the sitekey globally for every hCaptcha component 45 | NgHcaptchaModule.forRoot({ 46 | siteKey: 'YOUR_SITEKEY', 47 | languageCode: 'de' // optional, will default to browser language 48 | }), 49 | 50 | // Option #2 51 | // This option requires you to set the [siteKey] property for every hCaptcha component 52 | NgHcaptchaModule.forRoot() 53 | ] 54 | }) 55 | ``` 56 | 57 | ## Usage 58 | 59 | Template: 60 | 61 | ```html 62 | 63 | 64 | 65 | 66 | 67 |
68 | 69 |
70 | 71 | 72 | 73 | ``` 74 | 75 | TS: 76 | 77 | ```ts 78 | onVerify(token: string) { 79 | // The verification process was successful. 80 | // You can verify the token on your server now. 81 | } 82 | 83 | onExpired(response: any) { 84 | // The verification expired. 85 | } 86 | 87 | onError(error: any) { 88 | // An error occured during the verification process. 89 | } 90 | ``` 91 | 92 | ## Execute hCaptcha programmatically 93 | 94 | The hCaptcha verification process can also be executed programmatically: 95 | 96 | ```ts 97 | @Component({ 98 | selector: "hc-programmatically", 99 | templateUrl: "./programmatically.component.html", 100 | styleUrls: ["./programmatically.component.scss"], 101 | }) 102 | export class ProgrammaticallyComponent { 103 | constructor(private hcaptchaService: NgHcaptchaService) {} 104 | 105 | verify() { 106 | this.hcaptchaService.verify().subscribe( 107 | (result) => { 108 | console.log("SUCCESS", result); 109 | }, 110 | (err) => { 111 | console.log("FAILED", err); 112 | }, 113 | () => { 114 | console.log("COMPLETE"); 115 | } 116 | ); 117 | } 118 | } 119 | ``` 120 | 121 | ## Properties 122 | 123 | The properties below exist for all captcha components. 124 | 125 | `siteKey` Allows you to set the site key per captcha instance. 126 | 127 | `languageCode` Allows you to force a specific language. See https://docs.hcaptcha.com/languages 128 | 129 | ## Bugs? Ideas? 130 | 131 | If you found a bug or something you don't like, feel free to [open an issue](https://github.com/leNicDev/ng-hcaptcha/issues/new). If you have any ideas for new features or improvements, feel free to contribute or [open an issue](https://github.com/leNicDev/ng-hcaptcha/issues/new) :wink: 132 | 133 | ## Support me 134 | 135 | If you would like to support me for free, just create your hCaptcha account using my referral link :relaxed: 136 | [https://hCaptcha.com/?r=4afcb2d42338](https://hCaptcha.com/?r=4afcb2d42338) 137 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ng-hcaptcha-app": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "hc", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "scss" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:application", 19 | "options": { 20 | "outputPath": { 21 | "base": "dist/ng-hcaptcha-app" 22 | }, 23 | "index": "src/index.html", 24 | "polyfills": [ 25 | "src/polyfills.ts" 26 | ], 27 | "tsConfig": "src/tsconfig.app.json", 28 | "assets": [ 29 | "src/favicon.ico", 30 | "src/assets", 31 | "src/README.md" 32 | ], 33 | "styles": [ 34 | "src/styles.scss" 35 | ], 36 | "scripts": [], 37 | "extractLicenses": false, 38 | "sourceMap": true, 39 | "optimization": false, 40 | "namedChunks": true, 41 | "browser": "src/main.ts" 42 | }, 43 | "configurations": { 44 | "production": { 45 | "budgets": [ 46 | { 47 | "type": "anyComponentStyle", 48 | "maximumWarning": "6kb" 49 | } 50 | ], 51 | "fileReplacements": [ 52 | { 53 | "replace": "src/environments/environment.ts", 54 | "with": "src/environments/environment.prod.ts" 55 | } 56 | ], 57 | "optimization": true, 58 | "outputHashing": "all", 59 | "sourceMap": false, 60 | "namedChunks": false, 61 | "extractLicenses": true 62 | } 63 | }, 64 | "defaultConfiguration": "" 65 | }, 66 | "serve": { 67 | "builder": "@angular-devkit/build-angular:dev-server", 68 | "options": { 69 | "buildTarget": "ng-hcaptcha-app:build" 70 | }, 71 | "configurations": { 72 | "production": { 73 | "buildTarget": "ng-hcaptcha-app:build:production" 74 | } 75 | } 76 | }, 77 | "extract-i18n": { 78 | "builder": "@angular-devkit/build-angular:extract-i18n", 79 | "options": { 80 | "buildTarget": "ng-hcaptcha-app:build" 81 | } 82 | }, 83 | "test": { 84 | "builder": "@angular-devkit/build-angular:karma", 85 | "options": { 86 | "main": "src/test.ts", 87 | "polyfills": "src/polyfills.ts", 88 | "tsConfig": "src/tsconfig.spec.json", 89 | "karmaConfig": "src/karma.conf.js", 90 | "styles": [ 91 | "src/styles.scss" 92 | ], 93 | "scripts": [], 94 | "assets": [ 95 | "src/favicon.ico", 96 | "src/assets" 97 | ] 98 | } 99 | } 100 | } 101 | }, 102 | "ng-hcaptcha-app-e2e": { 103 | "root": "e2e/", 104 | "projectType": "application", 105 | "architect": { 106 | "e2e": { 107 | "builder": "@angular-devkit/build-angular:protractor", 108 | "options": { 109 | "protractorConfig": "e2e/protractor.conf.js", 110 | "devServerTarget": "ng-hcaptcha-app:serve" 111 | }, 112 | "configurations": { 113 | "production": { 114 | "devServerTarget": "ng-hcaptcha-app:serve:production" 115 | } 116 | } 117 | } 118 | } 119 | }, 120 | "ng-hcaptcha": { 121 | "root": "projects/ng-hcaptcha", 122 | "sourceRoot": "projects/ng-hcaptcha/src", 123 | "projectType": "library", 124 | "prefix": "lib", 125 | "architect": { 126 | "build": { 127 | "builder": "@angular-devkit/build-angular:ng-packagr", 128 | "options": { 129 | "tsConfig": "projects/ng-hcaptcha/tsconfig.lib.json", 130 | "project": "projects/ng-hcaptcha/ng-package.json" 131 | }, 132 | "configurations": { 133 | "production": { 134 | "tsConfig": "projects/ng-hcaptcha/tsconfig.lib.prod.json" 135 | } 136 | } 137 | }, 138 | "test": { 139 | "builder": "@angular-devkit/build-angular:karma", 140 | "options": { 141 | "main": "projects/ng-hcaptcha/src/test.ts", 142 | "tsConfig": "projects/ng-hcaptcha/tsconfig.spec.json", 143 | "karmaConfig": "projects/ng-hcaptcha/karma.conf.js" 144 | } 145 | } 146 | } 147 | } 148 | }, 149 | "cli": { 150 | "analytics": false 151 | }, 152 | "schematics": { 153 | "@schematics/angular:component": { 154 | "type": "component" 155 | }, 156 | "@schematics/angular:directive": { 157 | "type": "directive" 158 | }, 159 | "@schematics/angular:service": { 160 | "type": "service" 161 | }, 162 | "@schematics/angular:guard": { 163 | "typeSeparator": "." 164 | }, 165 | "@schematics/angular:interceptor": { 166 | "typeSeparator": "." 167 | }, 168 | "@schematics/angular:module": { 169 | "typeSeparator": "." 170 | }, 171 | "@schematics/angular:pipe": { 172 | "typeSeparator": "." 173 | }, 174 | "@schematics/angular:resolver": { 175 | "typeSeparator": "." 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /e2e/protractor-ci.conf.js: -------------------------------------------------------------------------------- 1 | const config = require('./protractor.conf').config; 2 | 3 | config.capabilities = { 4 | browserName: 'chrome', 5 | chromeOptions: { 6 | args: ['--headless', '--no-sandbox', '--disable-gpu'] 7 | } 8 | }; 9 | 10 | exports.config = config; -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('ng-hcaptcha component', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display captcha container', () => { 11 | page.navigateTo(); 12 | expect(page.getCaptchaContainer()).toBeDefined(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getCaptchaContainer() { 9 | return element(by.css('div.hcaptcha')); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /jsr.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lenicdev/ng-hcaptcha", 3 | "version": "2.3.1", 4 | "exports": "./projects/ng-hcaptcha/src/public_api.ts" 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-hcaptcha-app", 3 | "version": "2.6.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "test:ci": "ng test --browsers=ChromeHeadlessCI --watch=false --progress=false ng-hcaptcha", 10 | "e2e:ci": "ng e2e --protractor-config=./e2e/protractor-ci.conf.js ng-hcaptcha", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e", 13 | "build:lib": "ng build ng-hcaptcha --configuration production && npx copyfiles README.md dist/ng-hcaptcha/" 14 | }, 15 | "private": true, 16 | "dependencies": { 17 | "@angular/animations": "^20.0.0", 18 | "@angular/common": "^20.0.0", 19 | "@angular/compiler": "^20.0.0", 20 | "@angular/core": "^20.0.0", 21 | "@angular/forms": "^20.0.0", 22 | "@angular/platform-browser": "^20.0.0", 23 | "@angular/platform-browser-dynamic": "^20.0.0", 24 | "@angular/router": "^20.0.0", 25 | "core-js": "^3.40.0", 26 | "rxjs": "~7.8.1", 27 | "tslib": "^2.8.1", 28 | "zone.js": "~0.15.0" 29 | }, 30 | "devDependencies": { 31 | "@angular-devkit/build-angular": "^20.0.0", 32 | "@angular/cli": "^20.0.0", 33 | "@angular/compiler-cli": "^20.0.0", 34 | "@angular/language-service": "^20.0.0", 35 | "@types/jasmine": "~5.1.5", 36 | "@types/jasminewd2": "~2.0.13", 37 | "@types/node": "^22.10.9", 38 | "codelyzer": "^6.0.2", 39 | "copyfiles": "^2.4.1", 40 | "jasmine-core": "~5.5.0", 41 | "jasmine-spec-reporter": "~7.0.0", 42 | "karma": "~6.4.4", 43 | "karma-chrome-launcher": "~3.2.0", 44 | "karma-coverage-istanbul-reporter": "~3.0.3", 45 | "karma-jasmine": "~5.1.0", 46 | "karma-jasmine-html-reporter": "^2.1.0", 47 | "ng-packagr": "^20.0.0", 48 | "protractor": "~7.0.0", 49 | "ts-node": "~10.9.2", 50 | "tslint": "~6.1.3", 51 | "typescript": "~5.8.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /projects/ng-hcaptcha/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | customLaunchers: { 30 | ChromeHeadlessCI: { 31 | base: 'ChromeHeadless', 32 | flags: ['--no-sandbox', '--disable-gpu'] 33 | } 34 | }, 35 | singleRun: false 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /projects/ng-hcaptcha/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ng-hcaptcha", 4 | "lib": { 5 | "entryFile": "src/public_api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /projects/ng-hcaptcha/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-hcaptcha", 3 | "version": "2.5.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "ng-hcaptcha", 9 | "version": "2.5.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "tslib": "^2.8.1" 13 | }, 14 | "peerDependencies": { 15 | "@angular/common": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.01 || ^19.0.0", 16 | "@angular/core": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", 17 | "@angular/forms": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" 18 | } 19 | }, 20 | "node_modules/@angular/common": { 21 | "version": "19.1.3", 22 | "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.1.3.tgz", 23 | "integrity": "sha512-r1P0W6FKrON83szIJboF8z6UNCVL4HIxyD+nhmHMMT/iJpu4kDHVugaN/+w2jYLb4oelAJK5xzkzA+1IaHpzLg==", 24 | "peer": true, 25 | "dependencies": { 26 | "tslib": "^2.3.0" 27 | }, 28 | "engines": { 29 | "node": "^18.19.1 || ^20.11.1 || >=22.0.0" 30 | }, 31 | "peerDependencies": { 32 | "@angular/core": "19.1.3", 33 | "rxjs": "^6.5.3 || ^7.4.0" 34 | } 35 | }, 36 | "node_modules/@angular/core": { 37 | "version": "19.1.3", 38 | "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.1.3.tgz", 39 | "integrity": "sha512-Hh1eHvi+y+gsTRODiEEEWnRj5zqv9WNoou1KmQ1mv1NTOf0Pv61Hg9P2rBWDr0mPIXFSzqUKjyzW30BgdQ+AEA==", 40 | "peer": true, 41 | "dependencies": { 42 | "tslib": "^2.3.0" 43 | }, 44 | "engines": { 45 | "node": "^18.19.1 || ^20.11.1 || >=22.0.0" 46 | }, 47 | "peerDependencies": { 48 | "rxjs": "^6.5.3 || ^7.4.0", 49 | "zone.js": "~0.15.0" 50 | } 51 | }, 52 | "node_modules/@angular/forms": { 53 | "version": "19.1.3", 54 | "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.1.3.tgz", 55 | "integrity": "sha512-M6eEJBysJm9zSUhm8ggljZCsgHLccZl70P34tyddb8erh9it2uoOXW0aVaZgDt1UAiF5a1EzjdVdN4TZTT/OGA==", 56 | "peer": true, 57 | "dependencies": { 58 | "tslib": "^2.3.0" 59 | }, 60 | "engines": { 61 | "node": "^18.19.1 || ^20.11.1 || >=22.0.0" 62 | }, 63 | "peerDependencies": { 64 | "@angular/common": "19.1.3", 65 | "@angular/core": "19.1.3", 66 | "@angular/platform-browser": "19.1.3", 67 | "rxjs": "^6.5.3 || ^7.4.0" 68 | } 69 | }, 70 | "node_modules/@angular/platform-browser": { 71 | "version": "19.1.3", 72 | "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.1.3.tgz", 73 | "integrity": "sha512-bLgnM2hRyzUdoWRoUhe+IMenlr74EvrgwyG7anJ27bjg5PcvhQPXrGqU0hri5yPDb9SHVJZminr7OjNCN8QJkQ==", 74 | "peer": true, 75 | "dependencies": { 76 | "tslib": "^2.3.0" 77 | }, 78 | "engines": { 79 | "node": "^18.19.1 || ^20.11.1 || >=22.0.0" 80 | }, 81 | "peerDependencies": { 82 | "@angular/animations": "19.1.3", 83 | "@angular/common": "19.1.3", 84 | "@angular/core": "19.1.3" 85 | }, 86 | "peerDependenciesMeta": { 87 | "@angular/animations": { 88 | "optional": true 89 | } 90 | } 91 | }, 92 | "node_modules/rxjs": { 93 | "version": "7.8.1", 94 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", 95 | "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", 96 | "peer": true, 97 | "dependencies": { 98 | "tslib": "^2.1.0" 99 | } 100 | }, 101 | "node_modules/tslib": { 102 | "version": "2.8.1", 103 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 104 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" 105 | }, 106 | "node_modules/zone.js": { 107 | "version": "0.15.0", 108 | "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.0.tgz", 109 | "integrity": "sha512-9oxn0IIjbCZkJ67L+LkhYWRyAy7axphb3VgE2MBDlOqnmHMPWGYMxJxBYFueFq/JGY2GMwS0rU+UCLunEmy5UA==", 110 | "peer": true 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /projects/ng-hcaptcha/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-hcaptcha", 3 | "version": "2.6.0", 4 | "description": "hCaptcha Component for Angular", 5 | "keywords": [ 6 | "hcaptcha", 7 | "captcha", 8 | "angular", 9 | "verification" 10 | ], 11 | "bugs": { 12 | "url": "https://github.com/leNicDev/ng-hcaptcha/issues" 13 | }, 14 | "contributors": [ 15 | "leNicDev" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/leNicDev/ng-hcaptcha.git" 20 | }, 21 | "license": "MIT", 22 | "peerDependencies": { 23 | "@angular/common": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.01 || ^19.0.0 || ^20.0.0", 24 | "@angular/core": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0", 25 | "@angular/forms": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0" 26 | }, 27 | "dependencies": { 28 | "tslib": "^2.8.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /projects/ng-hcaptcha/src/lib/hcaptcha-utils.ts: -------------------------------------------------------------------------------- 1 | import { Observable, Subscriber } from 'rxjs'; 2 | 3 | declare const window: any; 4 | 5 | /** 6 | * Load the hCaptcha script by appending a script element to the head element. 7 | * The script won't be loaded again if it has already been loaded. 8 | * Async and defer are set to prevent blocking the renderer while loading hCaptcha. 9 | */ 10 | export function loadHCaptcha(languageCode?: string): Observable { 11 | return new Observable((observer: Subscriber) => { 12 | // No window object (ssr) 13 | if (!window) { 14 | return; 15 | } 16 | 17 | // The hCaptcha script has already been loaded 18 | if (typeof window.hcaptcha !== 'undefined') { 19 | observer.next(); 20 | observer.complete(); 21 | return; 22 | } 23 | 24 | let src = 'https://hcaptcha.com/1/api.js?render=explicit'; 25 | 26 | // Set language code 27 | if (languageCode) { 28 | src += `&hl=${languageCode}`; 29 | } 30 | 31 | const script = document.createElement('script'); 32 | script.src = src; 33 | script.async = true; 34 | script.defer = true; 35 | script.onerror = (e) => observer.error(e); 36 | script.onload = () => { 37 | observer.next(); 38 | observer.complete(); 39 | }; 40 | document.head.appendChild(script); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /projects/ng-hcaptcha/src/lib/ng-hcaptcha-config.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export interface CaptchaConfig { 4 | /** 5 | * The sitekey to use when no sitekey has been 6 | * specified on the hcaptcha element. 7 | */ 8 | siteKey?: string; 9 | /** 10 | * The language code to use instead of the 11 | * language automatically detected by hCaptcha. 12 | * @see {@link https://docs.hcaptcha.com/languages} 13 | */ 14 | languageCode?: string; 15 | } 16 | 17 | export const CAPTCHA_CONFIG: InjectionToken = new InjectionToken('CAPTCHA_CONFIG'); 18 | -------------------------------------------------------------------------------- /projects/ng-hcaptcha/src/lib/ng-hcaptcha-invisible-button.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgHcaptchaInvisibleButtonDirective } from './ng-hcaptcha-invisible-button.directive'; 2 | 3 | describe('ncaptchaInvisibleButtonDirective', () => { 4 | it('should create an instance', () => { 5 | const directive = new NgHcaptchaInvisibleButtonDirective(); 6 | expect(directive).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /projects/ng-hcaptcha/src/lib/ng-hcaptcha-invisible-button.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, EventEmitter, HostListener, Inject, Input, NgZone, OnInit, Output, PLATFORM_ID, OnDestroy } from '@angular/core'; 2 | import { isPlatformBrowser, isPlatformServer } from '@angular/common'; 3 | import { Subscription } from 'rxjs'; 4 | import { CAPTCHA_CONFIG, CaptchaConfig } from './ng-hcaptcha-config'; 5 | import { loadHCaptcha } from './hcaptcha-utils'; 6 | 7 | declare const window: any; 8 | 9 | @Directive({ 10 | selector: '[ngHcaptchaInvisibleButton]', 11 | standalone: false 12 | }) 13 | export class NgHcaptchaInvisibleButtonDirective implements OnInit, OnDestroy { 14 | 15 | @Input() siteKey: string; 16 | @Input() languageCode: string; 17 | 18 | @Output() verify: EventEmitter = new EventEmitter(); 19 | @Output() expired: EventEmitter = new EventEmitter(); 20 | @Output() error: EventEmitter = new EventEmitter(); 21 | @Output() click: EventEmitter = new EventEmitter(); 22 | 23 | private lastClickEvent: any; 24 | private captcha$: Subscription; 25 | private widgetId: string; 26 | 27 | constructor(private elRef: ElementRef, 28 | @Inject(CAPTCHA_CONFIG) private config: CaptchaConfig, 29 | private zone: NgZone, 30 | @Inject(PLATFORM_ID) private platformId: Object) { } 31 | 32 | ngOnInit() { 33 | // Use language code from module config when input parameter is not set 34 | if (!this.languageCode) { 35 | this.languageCode = this.config.languageCode; 36 | } 37 | 38 | // Do not load hCaptcha if platform is server 39 | if (isPlatformServer(this.platformId)) { 40 | return; 41 | } 42 | 43 | // Load the hCaptcha script 44 | this.captcha$ = loadHCaptcha(this.languageCode).subscribe( 45 | () => { 46 | setTimeout((context) => { 47 | // Configure hCaptcha 48 | const options = { 49 | sitekey: (context.siteKey || context.config.siteKey), 50 | size: 'invisible', 51 | callback: (res) => { context.zone.run(() => context.onVerify(res)); }, 52 | 'expired-callback': (res) => { context.zone.run(() => context.onExpired(res)); }, 53 | 'error-callback': (err) => { context.zone.run(() => context.onError(err)); } 54 | }; 55 | 56 | // Render hCaptcha using the defined options 57 | context.widgetId = window.hcaptcha.render(context.elRef.nativeElement, options); 58 | }, 50, this); 59 | }); 60 | } 61 | 62 | ngOnDestroy() { 63 | if (isPlatformServer(this.platformId)) { 64 | return; 65 | } 66 | 67 | this.captcha$.unsubscribe(); 68 | } 69 | 70 | @HostListener('click', ['$event']) 71 | onClick(event: any): boolean { 72 | if (event.hCaptchaToken) { 73 | return; 74 | } 75 | 76 | this.lastClickEvent = event; 77 | event.stopPropagation(); 78 | event.preventDefault(); 79 | event.cancelBubble = true; 80 | event.stopImmediatePropagation(); 81 | 82 | // Only execute hCaptcha if platform is browser 83 | if (isPlatformBrowser(this.platformId)) { 84 | window.hcaptcha.execute(this.widgetId); 85 | } 86 | 87 | return false; 88 | } 89 | 90 | reset() { 91 | window.hcaptcha.reset(this.widgetId); 92 | } 93 | 94 | /** 95 | * Is called when the verification was successful 96 | * @param response The verification token 97 | */ 98 | private onVerify(response: string): void { 99 | const event = this.lastClickEvent || {}; 100 | event.hCaptchaToken = response; 101 | this.click.emit(event); 102 | this.verify.emit(response); 103 | } 104 | 105 | /** 106 | * Is called when the verification has expired 107 | * @param response The verification response 108 | */ 109 | private onExpired(response: any): void { 110 | this.expired.emit(response); 111 | } 112 | 113 | /** 114 | * Is called when an error occurs during the verification process 115 | * @param error The error returned by hCaptcha 116 | */ 117 | private onError(error: any): void { 118 | this.error.emit(error); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /projects/ng-hcaptcha/src/lib/ng-hcaptcha.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NgHcaptchaComponent } from './ng-hcaptcha.component'; 4 | import { CAPTCHA_CONFIG } from './ng-hcaptcha-config'; 5 | 6 | describe('NgHcaptchaComponent', () => { 7 | let component: NgHcaptchaComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ NgHcaptchaComponent ], 13 | providers: [ 14 | { provide: CAPTCHA_CONFIG, useValue: { siteKey: '6de09d9c-8f26-4501-8141-49f4fa644d38' } } 15 | ] 16 | }) 17 | .compileComponents(); 18 | })); 19 | 20 | beforeEach(() => { 21 | fixture = TestBed.createComponent(NgHcaptchaComponent); 22 | component = fixture.componentInstance; 23 | fixture.detectChanges(); 24 | }); 25 | 26 | it('should create', () => { 27 | expect(component).toBeTruthy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /projects/ng-hcaptcha/src/lib/ng-hcaptcha.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | ViewChild, 5 | ElementRef, 6 | OnInit, 7 | Inject, 8 | NgZone, 9 | Output, 10 | EventEmitter, 11 | forwardRef, 12 | PLATFORM_ID, 13 | OnDestroy 14 | } from '@angular/core'; 15 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 16 | import { isPlatformBrowser, isPlatformServer } from '@angular/common'; 17 | import { Subscription } from 'rxjs'; 18 | import { CAPTCHA_CONFIG, CaptchaConfig } from './ng-hcaptcha-config'; 19 | import { loadHCaptcha } from './hcaptcha-utils'; 20 | 21 | declare const window: any; 22 | 23 | @Component({ 24 | selector: 'ng-hcaptcha', 25 | template: '
', 26 | styles: [], 27 | providers: [ 28 | { 29 | provide: NG_VALUE_ACCESSOR, 30 | useExisting: forwardRef(() => NgHcaptchaComponent), 31 | multi: true 32 | } 33 | ], 34 | standalone: false 35 | }) 36 | export class NgHcaptchaComponent implements OnInit, OnDestroy, ControlValueAccessor { 37 | 38 | @Input() siteKey: string; 39 | @Input() theme: string; 40 | @Input() size: string; 41 | @Input() tabIndex: number; 42 | @Input() languageCode: string; 43 | 44 | @ViewChild('captcha', { static: true }) captcha: ElementRef; 45 | 46 | @Output() verify: EventEmitter = new EventEmitter(); 47 | @Output() expired: EventEmitter = new EventEmitter(); 48 | @Output() error: EventEmitter = new EventEmitter(); 49 | 50 | private _value: string; 51 | private widgetId: string; 52 | private captcha$: Subscription; 53 | 54 | onChange: any = () => {}; 55 | onTouched: any = () => {}; 56 | 57 | 58 | constructor( 59 | @Inject(CAPTCHA_CONFIG) private config: CaptchaConfig, 60 | private zone: NgZone, 61 | @Inject(PLATFORM_ID) private platformId: Object 62 | ) {} 63 | 64 | 65 | // Initialization 66 | 67 | ngOnInit() { 68 | // Use language code from module config when input parameter is not set 69 | if (!this.languageCode) { 70 | this.languageCode = this.config.languageCode; 71 | } 72 | 73 | // Do not load hCaptcha if platform is server 74 | if (isPlatformServer(this.platformId)) { 75 | return; 76 | } 77 | 78 | this.captcha$ = loadHCaptcha(this.languageCode).subscribe( 79 | () => { 80 | setTimeout((context) => { 81 | // Configure hCaptcha 82 | const options = { 83 | sitekey: (context.siteKey || context.config.siteKey), 84 | theme: context.theme, 85 | size: context.size, 86 | tabindex: context.tabIndex, 87 | callback: (res) => { context.zone.run(() => context.onVerify(res)); }, 88 | 'expired-callback': (res) => { context.zone.run(() => context.onExpired(res)); }, 89 | 'error-callback': (err) => { context.zone.run(() => context.onError(err)); } 90 | }; 91 | 92 | // Render hCaptcha using the defined options 93 | context.widgetId = window.hcaptcha.render(context.captcha.nativeElement, options); 94 | }, 50, this); 95 | }, 96 | (error) => { 97 | console.error('Failed to load hCaptcha script', error); 98 | } 99 | ); 100 | } 101 | 102 | ngOnDestroy() { 103 | if (isPlatformServer(this.platformId)) { 104 | return; 105 | } 106 | 107 | this.captcha$.unsubscribe(); 108 | } 109 | 110 | // ControlValueAccessor implementation 111 | 112 | writeValue(value: string) { 113 | // Needs to be implemented to make the FormGroup's reset function work 114 | this.value = value; 115 | 116 | // Reset hCaptcha. 117 | // We need to check whether window.hcaptcha is defined because 118 | // writeValue(value: any) can be called before hCaptcha has been intialized. 119 | if (isPlatformBrowser(this.platformId) && !this.value && window.hcaptcha) { 120 | window.hcaptcha.reset(this.widgetId); 121 | } 122 | } 123 | 124 | registerOnChange(fn: any) { 125 | this.onChange = fn; 126 | } 127 | 128 | registerOnTouched(fn: any) { 129 | this.onTouched = fn; 130 | } 131 | 132 | reset() { 133 | window.hcaptcha.reset(this.widgetId); 134 | } 135 | 136 | get value(): string { 137 | return this._value; 138 | } 139 | 140 | set value(value: string) { 141 | this._value = value; 142 | this.onChange(value); 143 | this.onTouched(); 144 | } 145 | 146 | 147 | // Internal functions 148 | 149 | /** 150 | * Is called when the verification was successful 151 | * @param response The verification token 152 | */ 153 | private onVerify(response: string) { 154 | this.value = response; 155 | this.verify.emit(response); 156 | } 157 | 158 | /** 159 | * Is called when the verification has expired 160 | * @param response The verification response 161 | */ 162 | private onExpired(response: any) { 163 | this.expired.emit(response); 164 | } 165 | 166 | /** 167 | * Is called when an error occurs during the verification process 168 | * @param error The error returned by hCaptcha 169 | */ 170 | private onError(error: any) { 171 | this.error.emit(error); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /projects/ng-hcaptcha/src/lib/ng-hcaptcha.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { NgHcaptchaComponent } from './ng-hcaptcha.component'; 3 | import { CAPTCHA_CONFIG, CaptchaConfig } from './ng-hcaptcha-config'; 4 | import { NgHcaptchaInvisibleButtonDirective } from './ng-hcaptcha-invisible-button.directive'; 5 | import { NgHcaptchaService } from './ng-hcaptcha.service'; 6 | 7 | @NgModule({ 8 | imports: [], 9 | declarations: [NgHcaptchaComponent, NgHcaptchaInvisibleButtonDirective], 10 | exports: [NgHcaptchaComponent, NgHcaptchaInvisibleButtonDirective], 11 | }) 12 | export class NgHcaptchaModule { 13 | 14 | static forRoot(config?: CaptchaConfig): ModuleWithProviders { 15 | return { 16 | ngModule: NgHcaptchaModule, 17 | providers: [ 18 | NgHcaptchaService, 19 | { 20 | provide: CAPTCHA_CONFIG, 21 | useValue: config || [] 22 | }, 23 | ] 24 | }; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /projects/ng-hcaptcha/src/lib/ng-hcaptcha.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, NgZone } from "@angular/core"; 2 | import { Observable, Subscriber } from "rxjs"; 3 | import { loadHCaptcha } from "./hcaptcha-utils"; 4 | import { CaptchaConfig, CAPTCHA_CONFIG } from "./ng-hcaptcha-config"; 5 | 6 | declare const window: any; 7 | 8 | @Injectable() 9 | export class NgHcaptchaService { 10 | 11 | private hCaptchaElement: HTMLElement; 12 | private hCaptchaWidgetId: string; 13 | 14 | constructor( 15 | @Inject(CAPTCHA_CONFIG) private captchaConfig: CaptchaConfig, 16 | private zone: NgZone) { } 17 | 18 | verify(): Observable { 19 | return new Observable((subscriber: Subscriber) => { 20 | loadHCaptcha(this.captchaConfig.languageCode).subscribe(() => { 21 | setTimeout((context) => { 22 | // Create hCaptcha element 23 | if (!this.hCaptchaElement) { 24 | this.hCaptchaElement = document.createElement('div'); 25 | document.body.appendChild(this.hCaptchaElement); 26 | } 27 | 28 | // Render hCaptcha using the defined options 29 | if (!this.hCaptchaWidgetId) { 30 | // Configure hCaptcha 31 | const options = { 32 | sitekey: this.captchaConfig.siteKey, 33 | size: 'invisible', 34 | callback: (res) => { 35 | this.zone.run(() => { 36 | subscriber.next(res); 37 | subscriber.complete(); 38 | this.resetHcaptcha(); 39 | }); 40 | }, 41 | 'expired-callback': (res) => { 42 | this.zone.run(() => { 43 | subscriber.error(res); 44 | this.resetHcaptcha(); 45 | }); 46 | }, 47 | 'error-callback': (err) => { 48 | this.zone.run(() => { 49 | subscriber.error(err); 50 | this.resetHcaptcha(); 51 | }); 52 | }, 53 | }; 54 | this.hCaptchaWidgetId = window.hcaptcha.render(this.hCaptchaElement, options); 55 | } 56 | 57 | // Immediately execute hCaptcha 58 | window.hcaptcha.execute(this.hCaptchaWidgetId); 59 | }, 50, this); 60 | }); 61 | }); 62 | } 63 | 64 | private resetHcaptcha() { 65 | window.hcaptcha.remove(this.hCaptchaWidgetId); 66 | this.hCaptchaElement = null; 67 | this.hCaptchaWidgetId = null; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /projects/ng-hcaptcha/src/public_api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ng-hcaptcha 3 | */ 4 | 5 | export * from './lib/ng-hcaptcha-config'; 6 | export * from './lib/ng-hcaptcha.component'; 7 | export * from './lib/ng-hcaptcha-invisible-button.directive'; 8 | export * from './lib/ng-hcaptcha.service'; 9 | export * from './lib/ng-hcaptcha.module'; 10 | export * from './lib/hcaptcha-utils'; 11 | -------------------------------------------------------------------------------- /projects/ng-hcaptcha/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'core-js/es7/reflect'; 4 | import 'zone.js'; 5 | import 'zone.js/testing'; 6 | import { getTestBed } from '@angular/core/testing'; 7 | import { 8 | BrowserDynamicTestingModule, 9 | platformBrowserDynamicTesting 10 | } from '@angular/platform-browser-dynamic/testing'; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting(), { 16 | teardown: { destroyAfterEach: false } 17 | } 18 | ); 19 | -------------------------------------------------------------------------------- /projects/ng-hcaptcha/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "declarationMap": true, 6 | "module": "es2015", 7 | "moduleResolution": "bundler", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "types": [], 14 | "lib": [ 15 | "dom", 16 | "es2015" 17 | ] 18 | }, 19 | "angularCompilerOptions": { 20 | "skipTemplateCodegen": true, 21 | "strictMetadataEmit": true, 22 | "fullTemplateTypeCheck": true, 23 | "strictInjectionParameters": true, 24 | "enableResourceInlining": true 25 | }, 26 | "exclude": [ 27 | "src/test.ts", 28 | "**/*.spec.ts" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /projects/ng-hcaptcha/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "compilationMode": "partial" 8 | } 9 | } -------------------------------------------------------------------------------- /projects/ng-hcaptcha/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/ng-hcaptcha/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | false, 6 | "attribute", 7 | "lib", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | false, 12 | "element", 13 | "lib", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 7 | 8 |
9 | v{{ appVersion }} 10 |
11 |
12 | 13 |
14 | 15 |
16 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | header { 2 | width: 100%; 3 | height: 80px; 4 | margin-bottom: 50px; 5 | 6 | .app-version { 7 | position: absolute; 8 | top: 10px; 9 | right: 15px; 10 | opacity: 0.5; 11 | } 12 | } 13 | 14 | .logo { 15 | height: 100%; 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | 20 | span { 21 | font-size: 3.5em; 22 | font-weight: bold; 23 | color: #4a4a4a; 24 | 25 | } 26 | 27 | .logo-prefix { 28 | margin-right: 15px; 29 | } 30 | 31 | .logo-suffix { 32 | margin-left: 15px; 33 | } 34 | 35 | img { 36 | width: auto; 37 | height: 100%; 38 | } 39 | } 40 | 41 | main { 42 | width: 100%; 43 | } 44 | 45 | .demo-form { 46 | width: 30%; 47 | margin: 0 auto; 48 | } -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'ng-hcaptcha-app'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app.title).toEqual('ng-hcaptcha-app'); 23 | }); 24 | 25 | it('should render title in a h1 tag', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to ng-hcaptcha-app!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { environment } from '../environments/environment'; 3 | 4 | @Component({ 5 | selector: 'hc-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.scss'], 8 | standalone: false 9 | }) 10 | export class AppComponent { 11 | appVersion = environment.version; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from "@angular/platform-browser"; 2 | import { NgModule } from "@angular/core"; 3 | 4 | import { AppComponent } from "./app.component"; 5 | import { NgHcaptchaModule } from "ng-hcaptcha"; 6 | import { environment } from "../environments/environment"; 7 | import { FormsModule, ReactiveFormsModule } from "@angular/forms"; 8 | import { InvisibleComponent } from "./invisible/invisible.component"; 9 | import { FormComponent } from "./form/form.component"; 10 | import { RouterModule } from "@angular/router"; 11 | import { ProgrammaticallyComponent } from "./programmatically/programmatically.component"; 12 | 13 | @NgModule({ 14 | declarations: [ 15 | AppComponent, 16 | InvisibleComponent, 17 | FormComponent, 18 | ProgrammaticallyComponent, 19 | ], 20 | imports: [ 21 | BrowserModule, 22 | RouterModule.forRoot([ 23 | { 24 | path: "", 25 | pathMatch: "full", 26 | redirectTo: "/form", 27 | }, 28 | { 29 | path: "form", 30 | component: FormComponent, 31 | }, 32 | { 33 | path: "invisible", 34 | component: InvisibleComponent, 35 | }, 36 | { 37 | path: "programmatically", 38 | component: ProgrammaticallyComponent, 39 | }, 40 | ]), 41 | FormsModule, 42 | ReactiveFormsModule, 43 | NgHcaptchaModule.forRoot({ 44 | siteKey: environment.siteKey, 45 | languageCode: "en", 46 | }), 47 | ], 48 | providers: [], 49 | bootstrap: [AppComponent], 50 | }) 51 | export class AppModule {} 52 | -------------------------------------------------------------------------------- /src/app/form/HcaptchaFormModel.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl } from "@angular/forms"; 2 | 3 | export interface HcaptchaFormModel { 4 | email: AbstractControl; 5 | password: AbstractControl; 6 | confirmPassword: AbstractControl; 7 | captcha: AbstractControl; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/form/form.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 |
13 | 14 |
15 | 16 | 17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /src/app/form/form.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leNicDev/ng-hcaptcha/70f3e1ecaffe2c344efc56f2795ecbcd00206d60/src/app/form/form.component.scss -------------------------------------------------------------------------------- /src/app/form/form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FormComponent } from './form.component'; 4 | 5 | describe('FormComponent', () => { 6 | let component: FormComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ FormComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FormComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/form/form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { 3 | FormBuilder, 4 | FormControl, 5 | FormGroup, 6 | Validators, 7 | } from '@angular/forms'; 8 | import { HcaptchaFormModel } from './HcaptchaFormModel'; 9 | 10 | @Component({ 11 | selector: 'hc-form', 12 | templateUrl: './form.component.html', 13 | styleUrls: ['./form.component.scss'], 14 | standalone: false 15 | }) 16 | export class FormComponent { 17 | signUpForm: FormGroup; 18 | 19 | constructor(fb: FormBuilder) { 20 | this.signUpForm = fb.group({ 21 | email: new FormControl('', { 22 | nonNullable: true, 23 | validators: Validators.compose([Validators.email, Validators.required]), 24 | }), 25 | password: new FormControl('', { nonNullable: true }), 26 | confirmPassword: new FormControl('', { nonNullable: true }), 27 | captcha: new FormControl('', { 28 | nonNullable: true, 29 | validators: Validators.required, 30 | }), 31 | }); 32 | } 33 | 34 | onSubmit() { 35 | this.signUpForm.reset(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/invisible/invisible.component.html: -------------------------------------------------------------------------------- 1 |
2 | 7 |
8 | -------------------------------------------------------------------------------- /src/app/invisible/invisible.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leNicDev/ng-hcaptcha/70f3e1ecaffe2c344efc56f2795ecbcd00206d60/src/app/invisible/invisible.component.scss -------------------------------------------------------------------------------- /src/app/invisible/invisible.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { InvisibleComponent } from './invisible.component'; 4 | 5 | describe('InvisibleComponent', () => { 6 | let component: InvisibleComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ InvisibleComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(InvisibleComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/invisible/invisible.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'hc-invisible', 5 | templateUrl: './invisible.component.html', 6 | styleUrls: ['./invisible.component.scss'], 7 | standalone: false 8 | }) 9 | export class InvisibleComponent implements OnInit { 10 | 11 | constructor() { } 12 | 13 | ngOnInit(): void { 14 | } 15 | 16 | onVerify(response) { 17 | console.log('Invisible verified', response); 18 | } 19 | 20 | onError(error) { 21 | console.log('Invisible error', error); 22 | } 23 | 24 | onExpired(response) { 25 | console.log('Invisible expired', response); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/app/programmatically/programmatically.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/programmatically/programmatically.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leNicDev/ng-hcaptcha/70f3e1ecaffe2c344efc56f2795ecbcd00206d60/src/app/programmatically/programmatically.component.scss -------------------------------------------------------------------------------- /src/app/programmatically/programmatically.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProgrammaticallyComponent } from './programmatically.component'; 4 | 5 | describe('ProgrammaticallyComponent', () => { 6 | let component: ProgrammaticallyComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ProgrammaticallyComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProgrammaticallyComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/programmatically/programmatically.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NgHcaptchaService } from 'ng-hcaptcha'; 3 | 4 | @Component({ 5 | selector: 'hc-programmatically', 6 | templateUrl: './programmatically.component.html', 7 | styleUrls: ['./programmatically.component.scss'], 8 | standalone: false 9 | }) 10 | export class ProgrammaticallyComponent { 11 | 12 | constructor(private hcaptchaService: NgHcaptchaService) { } 13 | 14 | verify() { 15 | this.hcaptchaService.verify().subscribe( 16 | (result) => { 17 | console.log('SUCCESS', result); 18 | }, 19 | (err) => { 20 | console.log('FAILED', err); 21 | }, 22 | () => { 23 | console.log('COMPLETE'); 24 | } 25 | ); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leNicDev/ng-hcaptcha/70f3e1ecaffe2c344efc56f2795ecbcd00206d60/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Asset 8 -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | version: require('../../package.json').version, 4 | siteKey: '6de09d9c-8f26-4501-8141-49f4fa644d38' 5 | }; 6 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | version: require('../../package.json').version, 8 | siteKey: '10000000-ffff-ffff-ffff-000000000001' 9 | }; 10 | 11 | /* 12 | * For easier debugging in development mode, you can import the following file 13 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 14 | * 15 | * This import should be commented out in production mode because it will have a negative impact 16 | * on performance if an error is thrown. 17 | */ 18 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 19 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leNicDev/ng-hcaptcha/70f3e1ecaffe2c344efc56f2795ecbcd00206d60/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgHcaptchaApp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | 14 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | /** 4 | * By default, zone.js will patch all possible macroTask and DomEvents 5 | * user can disable parts of macroTask/DomEvents patch by setting following flags 6 | */ 7 | 8 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 9 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 10 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 11 | 12 | /* 13 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 14 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 15 | */ 16 | // (window as any).__Zone_enable_cross_context_check = true; 17 | 18 | /*************************************************************************************************** 19 | * Zone JS is required by default for Angular itself. 20 | */ 21 | import 'zone.js'; // Included with Angular CLI. 22 | 23 | 24 | 25 | /*************************************************************************************************** 26 | * APPLICATION IMPORTS 27 | */ 28 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=IBM+Plex+Sans:400,700'); 2 | 3 | $border-radius: 5px; 4 | 5 | html * { 6 | font-family: 'IBM Plex Sans', 'Roboto', 'Arial', sans-serif; 7 | outline: none; 8 | } 9 | 10 | .form-control { 11 | width: 100%; 12 | margin-bottom: 10px; 13 | 14 | input { 15 | box-sizing: border-box; 16 | width: 100%; 17 | } 18 | } 19 | 20 | input[type=text], input[type=email], input[type=password] { 21 | border: solid 1px #c4c4c4; 22 | border-radius: $border-radius; 23 | padding: 10px 15px; 24 | box-shadow: 2px 2px rgba(#c4c4c4, 0.3); 25 | 26 | &:focus { 27 | border: solid 2px #00cbbe; 28 | box-shadow: 2px 2px rgba(#00cbbe, 0.3); 29 | } 30 | 31 | &.ng-invalid:not(form):not(.ng-untouched) { 32 | border-color: #fc6964; 33 | 34 | &:focus { 35 | box-shadow: 2px 2px rgba(#fc6964, 0.3); 36 | } 37 | } 38 | } 39 | 40 | .btn { 41 | border: none; 42 | color: white; 43 | padding: 0 20px; 44 | font-size: 1.2em; 45 | cursor: pointer; 46 | border-radius: $border-radius; 47 | } 48 | 49 | .btn:disabled { 50 | background-color: linear-gradient(to bottom, #dfdfdf, #c4c4c4); 51 | color: #585858; 52 | cursor: not-allowed; 53 | } 54 | 55 | .btn.primary:not(:disabled) { 56 | background: linear-gradient(to bottom, #00cbbe, #00c2bf); 57 | 58 | &:hover { 59 | background: linear-gradient(to bottom, #00e7d8, #00dad6); 60 | } 61 | } 62 | 63 | .flex { 64 | display: flex; 65 | } 66 | 67 | .flex.justify-content-center { 68 | justify-content: center; 69 | } 70 | 71 | .flex.justify-content-between { 72 | justify-content: space-between; 73 | } 74 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting(), { 14 | teardown: { destroyAfterEach: false } 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": ["node"] 6 | }, 7 | "files": [ 8 | "main.ts", 9 | "polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | false, 6 | "attribute", 7 | "hc", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | false, 12 | "element", 13 | "hc", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "importHelpers": true, 6 | "esModuleInterop": true, 7 | "outDir": "./dist/out-tsc", 8 | "sourceMap": true, 9 | "declaration": false, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "experimentalDecorators": true, 13 | "target": "ES2022", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2017", 19 | "dom" 20 | ], 21 | "paths": { 22 | "ng-hcaptcha": [ 23 | "dist/ng-hcaptcha" 24 | ], 25 | "ng-hcaptcha/*": [ 26 | "dist/ng-hcaptcha/*" 27 | ] 28 | }, 29 | "useDefineForClassFields": false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-redundant-jsdoc": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "no-output-on-prefix": true, 120 | "no-inputs-metadata-property": true, 121 | "no-outputs-metadata-property": true, 122 | "no-host-metadata-property": true, 123 | "no-input-rename": true, 124 | "no-output-rename": true, 125 | "use-lifecycle-interface": true, 126 | "use-pipe-transform-interface": true, 127 | "component-class-suffix": true, 128 | "directive-class-suffix": true 129 | } 130 | } 131 | --------------------------------------------------------------------------------