diff --git a/README.md b/README.md index 8ab07a7e..9d233bd9 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ http://guidesmiths-react-form-builder.s3-website.eu-central-1.amazonaws.com/ ##### CountryAndRegionsData example: -```yaml +```json [ { "cn": "MyOwnCountry1", "cs": "MC1" }, { "cn": "MyOwnCountry2", "cs": "MC2" }, @@ -114,7 +114,7 @@ Reminder: A custom link it will be indicated by the start of a '#' in the markdo #### Checkbox example -```yaml +```json { "name": "terms_and_conditions", "label": "This is the label of the checkbox with a [customLink](#customLink) and [normalLink](https://www.dcsl.com/careers/)", @@ -155,7 +155,7 @@ https://user-images.githubusercontent.com/79102959/134894112-e4f38ced-0992-428c- Reminder: the 'countryAndRegions' prop that can be sent in the ReactFormBuilder will be rendered in this component and will replace the default list. ### Country example: -```yaml +```json { "name": "country_of_residence", "type": "country", @@ -230,7 +230,7 @@ https://user-images.githubusercontent.com/79102959/134897712-95e4391c-cfbb-42cd- ### Date examples Basic date example -```yaml +```json { "name": "dob", "type": "date", @@ -246,7 +246,7 @@ Basic date example } ``` Under age date example -```yaml +```json { "name": "dob", "type": "date", @@ -292,7 +292,7 @@ https://user-images.githubusercontent.com/79102959/134897303-817957ba-12d1-4c0c- ### Input examples Basic input example -```yaml +```json { "name": "email", "type": "input", @@ -308,7 +308,7 @@ Basic input example ``` Input with pattern control example -```yaml +```json { "name": "email", "type": "input", @@ -326,7 +326,7 @@ Input with pattern control example ``` Input with custom error (for example, if the user tries to sign up with an email that has already beeen used) -```yaml +```json { "name": "email", "type": "input", @@ -346,7 +346,7 @@ Input with custom error (for example, if the user tries to sign up with an email ``` Input with field descriptions -```yaml +```json { "name": "password", "type": "password", @@ -432,7 +432,7 @@ Markdown example #### Multiplecheckbox examples Basic multiplecheckbox -```yaml +```json { "name": "multiplecheckbox_name", "type": "multiple_checkboxes", @@ -465,7 +465,7 @@ Basic multiplecheckbox ``` Multiplecheckbox with images and labels -```yaml +```json { "name": "multiplecheckbox_name", "type": "multiple_checkboxes", @@ -499,7 +499,7 @@ Multiplecheckbox with images and labels ``` Multiplecheckbox with minimumLen -```yaml +```json { "name": "multiplecheckbox_name", "type": "multiple_checkboxes", @@ -555,7 +555,7 @@ Reminder: The isoCode prop that can also be sent in the ReactFormBuilder compone Basic phone -```yaml +```json { "name": "phone", "type": "phone", @@ -592,7 +592,7 @@ https://user-images.githubusercontent.com/79102959/134948115-4f461d76-8d06-4cb8- #### RadioButton example Basic radiobutton -```yaml +```json { "name": "radio_name", "label": "este es el texto de la pregunta", @@ -649,7 +649,7 @@ https://user-images.githubusercontent.com/79102959/134948337-03618f4c-6cc7-409a- Select basic example: -```yaml +```json { "name": "color", "type": "select", @@ -683,7 +683,7 @@ Select basic example: Select example with custom arrows in the `public/icons` folder: -```yaml +```json { "name": "color", "type": "select", @@ -746,7 +746,7 @@ It works the same as select field, but searchable option is available. Autocomplete basic example -```yaml +```json { "name": "color", "type": "autocomplete", @@ -778,6 +778,26 @@ Autocomplete basic example ``` +## ReCAPTCHA + +| Option | Description | Type | Default | +|--- |--- |:---: |:---: | +| name* | ReCAPTCHA name | string | - | +| type* | Must be `recaptcha`| string | - | +| recaptchaKey* | Key to make ReCAPTCHA work. Failing to provide this will lead to the ReCAPTCHA element showing with an error text inside. | string | - | + +This feature is implemented using `react-google-recaptcha`. By default it shows at the bottom-right of your app and will only show the ReCAPTCHA modal when bot-like behavior is detected filling the form. + +#### ReCAPTCHA example + +```json + { + "name": "my-recaptcha", + "type": "recaptcha", + "recaptchaKey": "your-recaptcha-key" + } +``` + # Accessibility The accessibility requirements for all the form tags are already configured in the library. For components (input, checkbox, select, radio…) different attributes have been introduced that can be configured through props. @@ -803,7 +823,7 @@ Next, we can see different attributes and tags to adjust the accessibility of th ### Main form -```yaml +```json { "en": { "contact": { diff --git a/example/package-lock.json b/example/package-lock.json index 2ab9dd81..0d10db1f 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -20,12 +20,13 @@ }, "..": { "name": "@onebeyond/react-form-builder", - "version": "1.0.1", + "version": "1.1.1", "license": "MIT", "dependencies": { "debounce-promise": "^3.1.2", "install": "^0.13.0", "react-datepicker": "^4.23.0", + "react-google-recaptcha": "^3.1.0", "react-hook-form": "^7.49.3", "react-markdown": "^5.0.3", "react-phone-number-input": "^3.3.9", diff --git a/package-lock.json b/package-lock.json index 6765db68..4675a2f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "debounce-promise": "^3.1.2", "install": "^0.13.0", "react-datepicker": "^4.23.0", + "react-google-recaptcha": "^3.1.0", "react-hook-form": "^7.49.3", "react-markdown": "^5.0.3", "react-phone-number-input": "^3.3.9", @@ -29126,6 +29127,18 @@ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "dev": true }, + "node_modules/react-async-script": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-async-script/-/react-async-script-1.2.0.tgz", + "integrity": "sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==", + "dependencies": { + "hoist-non-react-statics": "^3.3.0", + "prop-types": "^15.5.0" + }, + "peerDependencies": { + "react": ">=16.4.1" + } + }, "node_modules/react-colorful": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", @@ -29374,6 +29387,18 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" }, + "node_modules/react-google-recaptcha": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz", + "integrity": "sha512-cYW2/DWas8nEKZGD7SCu9BSuVz8iOcOLHChHyi7upUuVhkpkhYG/6N3KDiTQ3XAiZ2UAZkfvYKMfAHOzBOcGEg==", + "dependencies": { + "prop-types": "^15.5.0", + "react-async-script": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.4.1" + } + }, "node_modules/react-hook-form": { "version": "7.49.3", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.3.tgz", diff --git a/package.json b/package.json index ce3f6f7d..fba44eaf 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "debounce-promise": "^3.1.2", "install": "^0.13.0", "react-datepicker": "^4.23.0", + "react-google-recaptcha": "^3.1.0", "react-hook-form": "^7.49.3", "react-markdown": "^5.0.3", "react-phone-number-input": "^3.3.9", diff --git a/src/Questions/Recaptcha/__tests__/recaptcha.test.js b/src/Questions/Recaptcha/__tests__/recaptcha.test.js new file mode 100644 index 00000000..5fac58ad --- /dev/null +++ b/src/Questions/Recaptcha/__tests__/recaptcha.test.js @@ -0,0 +1,27 @@ +import React from 'react' +import { render } from '@testing-library/react' +import '@testing-library/jest-dom' + +import QuestionRecaptcha from '..' + +const formDataValues = {} +const recaptchaRef = React.createRef(null) +const onSubmitForm = () => {} +const question = { + name: 'recaptcha', + type: 'recaptcha', + recaptchaKey: 'random' +} + +test('renders a reCAPTCHA', () => { + const { getByTestId } = render( + + ) + + expect(getByTestId('recaptcha-test')).toBeTruthy() +}) diff --git a/src/Questions/Recaptcha/index.js b/src/Questions/Recaptcha/index.js new file mode 100644 index 00000000..6d007fb8 --- /dev/null +++ b/src/Questions/Recaptcha/index.js @@ -0,0 +1,29 @@ +import { forwardRef } from 'react' +import ReCAPTCHA from 'react-google-recaptcha' + +/** @jsxRuntime classic */ +/** @jsx jsx */ +import { jsx } from 'theme-ui' + +const QuestionRecaptcha = forwardRef(({ formDataValues, onSubmitForm, question }, ref) => { + const onReCAPTCHAChange = async (captchaCode) => { + if (!captchaCode) { + return + } + + ref?.current?.reset() + onSubmitForm(formDataValues) + } + + return ( + + ) +}) + +export default QuestionRecaptcha diff --git a/src/__tests__/builder.test.js b/src/__tests__/builder.test.js index 394b2852..f534aa2f 100644 --- a/src/__tests__/builder.test.js +++ b/src/__tests__/builder.test.js @@ -99,3 +99,34 @@ describe('form builder with custom errors', () => { expect(doesNotMatchError).not.toBe(null) }) }) + +describe('form builder with reCAPTCHA', () => { + beforeEach(() => { + mockHandler = jest.fn() + component = render( + + ) + }) + + afterEach(cleanup) + + test('check it does not submit forms when they have a reCAPTCHA', async () => { + const button = component.getByText('Submit') + fireEvent.click(button) + expect(mockHandler).toHaveBeenCalledTimes(0) + + const inputComponents = component.getAllByTestId('question-input') + fireEvent.change(inputComponents[0], { target: { value: 'name testing' } }) + expect(inputComponents[0].value).toBe('name testing') + + await act(async () => { + fireEvent.click(button) + }) + expect(mockHandler).toHaveBeenCalledTimes(0) + }) +}) diff --git a/src/__tests__/forms.json b/src/__tests__/forms.json index 0367ed28..42193a54 100644 --- a/src/__tests__/forms.json +++ b/src/__tests__/forms.json @@ -158,5 +158,54 @@ } ], "id": "4HHq4GwrEE2bDL9YJUbUyi" + }, + "recaptcha": { + "name": "Survey Form", + "title": "Survey Form", + "layout": "survey", + "caption": "Survey", + "subCaption": "Want 5 more chances to win? While you are here we would love to know more about you. Answer these quick questions and get 5 more chances to win!", + "enabled": true, + "questions": [ + { + "name": "inputName", + "type": "input", + "label": "input label", + "placeholder": "input placeholder", + "icon": { + "name": "question", + "fill": "red" + }, + "tooltip": { + "text": "tooltip text example", + "config": { + "backgroundColor": "green" + } + }, + "errorMessages": { + "required": "This field is required", + "pattern": "This is not the right pattern" + }, + "registerConfig": { + "required": true + } + }, + { + "name": "recaptcha", + "type": "recaptcha", + "recaptchaKey": "random" + } + ], + "textToShow": { + "privacy": "Texto sobre la privacidad", + "T&C": "Los términos y condiciones" + }, + "callForAction": [ + { + "caption": "Submit", + "type": "submit" + } + ], + "id": "4HHq4GwrEE2bDL9YJUbUyi" } } \ No newline at end of file diff --git a/src/builder.js b/src/builder.js index 704e811e..77903cca 100644 --- a/src/builder.js +++ b/src/builder.js @@ -1,6 +1,6 @@ /** @jsxRuntime classic */ /** @jsx jsx */ -import React, { useEffect } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { jsx } from 'theme-ui' import { useForm } from 'react-hook-form' import { DevTool } from '@hookform/devtools' @@ -24,6 +24,7 @@ import QuestionGender from './Questions/Genre' import QuestionAge from './Questions/Age' import QuestionAutocomplete from './Questions/Autocomplete' import QuestionImageInput from './Questions/ImageInput' +import QuestionRecaptcha from './Questions/Recaptcha' import styles from './styles.js' @@ -42,6 +43,9 @@ const FormBuilder = ({ ...props }) => { const useFormObj = useForm({ defaultValues: { formatDate: '' } }) + const [formDataValues, setFormDataValues] = useState({}) + const hasRecaptcha = form.questions.some(question => question.type === 'recaptcha') + const recaptchaRef = useRef(null) useEffect(() => { if (formErrors && formErrors.length > 0) { @@ -214,6 +218,14 @@ const FormBuilder = ({ currentPath={currentPath} onLinkOpen={onLinkOpen} /> + ), + recaptcha: ( + ) } } @@ -299,7 +311,13 @@ const FormBuilder = ({ const onSubmit = async (data) => { if (isLoading) return - onSubmitForm(await formatData(data)) + if (hasRecaptcha) { + recaptchaRef.current?.execute() + setFormDataValues(await formatData(data)) + + } else { + onSubmitForm(await formatData(data)) + } } return (