Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions Example.App.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,20 @@ const App = () => {

const onMessage = event => {
if (event && event.nativeEvent.data) {
if (['cancel'].includes(event.nativeEvent.data)) {
captchaForm.current.hide();
if (event.nativeEvent.data === 'open') {
console.log('Visual challenge opened');
} else if (event.success) {
setCode(event.nativeEvent.data);
} else if (['error'].includes(event.nativeEvent.data)) {
captchaForm.current.hide();
setCode(event.nativeEvent.data);
console.log('Verification failed', event.nativeEvent.data);
} else if (event.nativeEvent.data === 'expired') {
event.markUsed();
console.log('Verified code from hCaptcha', event.nativeEvent.data);
} else if (event.nativeEvent.data === 'challenge-expired') {
event.reset();
console.log('Visual challenge expired, reset...', event.nativeEvent.data);
} else if (event.nativeEvent.data === 'open') {
console.log('Visual challenge opened');
} else {
console.log('Verified code from hCaptcha', event.nativeEvent.data);
captchaForm.current.hide();
event.markUsed();
} else /* other errors */ {
setCode(event.nativeEvent.data);
captchaForm.current.hide();
console.log('Verification failed', event.nativeEvent.data);
}
}
};
Expand Down
25 changes: 14 additions & 11 deletions Hcaptcha.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,33 +136,33 @@ const Hcaptcha = ({
// have loaded by this point; render is sync.
console.log("challenge render complete");
} catch (e) {
console.log("challenge failed to render");
window.ReactNativeWebView.postMessage("error");
console.log("challenge failed to render:", e);
window.ReactNativeWebView.postMessage(e.name);
}
try {
console.log("showing challenge");
hcaptcha.execute(getExecuteOpts());
} catch (e) {
console.log("failed to show challenge");
window.ReactNativeWebView.postMessage("error");
console.log("failed to show challenge:", e);
window.ReactNativeWebView.postMessage(e.name);
}
};
var onDataCallback = function(response) {
window.ReactNativeWebView.postMessage(response);
};
var onCancel = function() {
window.ReactNativeWebView.postMessage("cancel");
window.ReactNativeWebView.postMessage("challenge-closed");
};
var onOpen = function() {
document.body.style.backgroundColor = '${backgroundColor}';
window.ReactNativeWebView.postMessage("open");
console.log("challenge opened");
};
var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage(error); };
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage(error); };
var onDataErrorCallback = function(error) {
console.log("challenge error callback fired");
window.ReactNativeWebView.postMessage("error");
console.warn("challenge error callback fired");
window.ReactNativeWebView.postMessage(error);
};
const getRenderConfig = function(siteKey, theme, size) {
var config = {
Expand Down Expand Up @@ -239,11 +239,14 @@ const Hcaptcha = ({
mixedContentMode={'always'}
onMessage={(e) => {
e.reset = reset;
e.success = true;
if (e.nativeEvent.data === 'open') {
setIsLoading(false);
} else if (e.nativeEvent.data.length > 16) {
const expiredTokenTimerId = setTimeout(() => onMessage({ nativeEvent: { data: 'expired' }, reset }), tokenTimeout);
} else if (e.nativeEvent.data.length > 35) {
const expiredTokenTimerId = setTimeout(() => onMessage({ nativeEvent: { data: 'expired' }, success: false, reset }), tokenTimeout);
e.markUsed = () => clearTimeout(expiredTokenTimerId);
} else /* error */ {
e.success = false;
}
onMessage(e);
}}
Expand Down
26 changes: 9 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,9 @@ For users familiar with the hCaptcha JS API, calling `show()` in this wrapper tr

This means that if you are an Enterprise user with a 99.9% passive or purely passive sitekey configured, no additional work is required to get the expected behavior: either a visual challenge will be shown or a token will be returned immediately via `onMessage`, in accordance with your configuration.

Also, please note the following special message strings that can be returned via `onMessage`:

| name | purpose |
| --- | --- |
| expired | passcode response expired and the user must re-verify, or did not answer before session expired |
| error | there was an error displaying the challenge |
| cancel | the user closed the challenge |
| open | the visual challenge was opened |


Any other string returned by `onMessage` will be a passcode.
Also, please note the following special message strings that can be returned via `onMessage` for [error cases](https://docs.hcaptcha.com/configuration#error-codes)

The even returned by `onMessage` with `success === true` will be a passcode.

### Handling the post-issuance expiration lifecycle

Expand All @@ -57,16 +48,17 @@ Once you've utilized hCaptcha's token, call `markUsed` on the event object in `o
```js
const onMessage = event => {
if (event && event.nativeEvent.data) {
if (['cancel'].includes(event.nativeEvent.data)) {
captchaForm.current.hide();
} else if (['error'].includes(event.nativeEvent.data)) {
captchaForm.current.hide();
// handle error
} else {
if (event.nativeEvent.data === 'open') {
// hCaptcha shown
} else if (event.success) {
captchaForm.current.hide();
const token = event.nativeEvent.data;
// utilize token and call markUsed once you are done with it
event.markUsed();
} else if (event.nativeEvent.data === 'challenge-closed') {
captchaForm.current.hide();
} else {
// handle rest errors
}
}
};
Expand Down
2 changes: 1 addition & 1 deletion __mocks__/global.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
jest.mock('react-native-modal', () => 'Modal');
jest.mock('react-native-webview', () => 'WebView');
jest.mock('react-native-webview');
jest.mock('react', () => {
let ActualReact = jest.requireActual('react');
return {
Expand Down
23 changes: 23 additions & 0 deletions __mocks__/react-native-webview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// __mocks__/react-native-webview.js
import React from 'react';

let messageDataToSend = null;

export const setWebViewMessageData = (data) => {
messageDataToSend = data;
};

const WebView = (props) => {
const { onMessage } = props;

React.useEffect(() => {
if (messageDataToSend && onMessage) {
onMessage({ nativeEvent: { data: messageDataToSend } });
messageDataToSend = null;
}
}, [onMessage]);

return React.createElement('WebView', props);
};

export default WebView;
50 changes: 48 additions & 2 deletions __tests__/Hcaptcha.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { render, waitFor } from '@testing-library/react-native';
import Hcaptcha from '../Hcaptcha';
import { setWebViewMessageData } from 'react-native-webview';

describe('Hcaptcha snapshot tests', () => {
it('renders Hcaptcha with minimum props', () => {
Expand Down Expand Up @@ -32,7 +33,7 @@ describe('Hcaptcha snapshot tests', () => {
expect(component).toMatchSnapshot();
});

it('test debug', () => {
it('renders Hcaptcha with debug', () => {
const component = render(
<Hcaptcha
siteKey="00000000-0000-0000-0000-000000000000"
Expand All @@ -43,5 +44,50 @@ describe('Hcaptcha snapshot tests', () => {
);
expect(component).toMatchSnapshot();
});

[
{
data: 'open',
expectedSuccess: true,
},
{
data: '10000000-aaaa-bbbb-cccc-000000000001',
expectedSuccess: true,
},
{
data: 'webview-error',
expectedSuccess: false,
},
].forEach(({ data, expectedSuccess }) => {
it(`test ${data} forwarding`, async () => {
jest.useFakeTimers();
let setTimeoutSpy = jest.spyOn(global, 'setTimeout');
setWebViewMessageData(data);
const onMessageMock = jest.fn();

render(
<Hcaptcha
siteKey="00000000-0000-0000-0000-000000000000"
url="https://hcaptcha.com"
languageCode="en"
onMessage={onMessageMock}
/>
);

await waitFor(() => {
expect(onMessageMock).toHaveBeenCalledTimes(1);
expect(onMessageMock).toHaveBeenCalledWith(
expect.objectContaining({
success: expectedSuccess,
nativeEvent: expect.objectContaining({ data }),
})
);

if (data === 'token') {
expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
}
});
});
});
});

38 changes: 19 additions & 19 deletions __tests__/__snapshots__/ConfirmHcaptcha.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with all props 1
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script type="text/javascript">
Object.entries({"rnver_0_0_0":true,"dep_mocked-md5":true,"sdk_1_8_2":true}).forEach(function (entry) { window[entry[0]] = entry[1] })
Object.entries({"rnver_0_0_0":true,"dep_mocked-md5":true,"sdk_2_0_0":true}).forEach(function (entry) { window[entry[0]] = entry[1] })
</script>
<script src="https://all.props/api-endpoint?render=explicit&onload=onloadCallback&host=all-props-host&hl=en&sentry=true&endpoint=https%3A%2F%2Fall.props%2Fendpoint&assethost=https%3A%2F%2Fall.props%2Fassethost&imghost=https%3A%2F%2Fall.props%2Fimghost&reportapi=https%3A%2F%2Fall.props%2Freportapi&orientation=portrait" async defer></script>
<script type="text/javascript">
Expand All @@ -93,33 +93,33 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with all props 1
// have loaded by this point; render is sync.
console.log("challenge render complete");
} catch (e) {
console.log("challenge failed to render");
window.ReactNativeWebView.postMessage("error");
console.log("challenge failed to render:", e);
window.ReactNativeWebView.postMessage(e.name);
}
try {
console.log("showing challenge");
hcaptcha.execute(getExecuteOpts());
} catch (e) {
console.log("failed to show challenge");
window.ReactNativeWebView.postMessage("error");
console.log("failed to show challenge:", e);
window.ReactNativeWebView.postMessage(e.name);
}
};
var onDataCallback = function(response) {
window.ReactNativeWebView.postMessage(response);
};
var onCancel = function() {
window.ReactNativeWebView.postMessage("cancel");
window.ReactNativeWebView.postMessage("challenge-closed");
};
var onOpen = function() {
document.body.style.backgroundColor = 'rgba(0.1, 0.1, 0.1, 0.4)';
window.ReactNativeWebView.postMessage("open");
console.log("challenge opened");
};
var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage(error); };
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage(error); };
var onDataErrorCallback = function(error) {
console.log("challenge error callback fired");
window.ReactNativeWebView.postMessage("error");
console.warn("challenge error callback fired");
window.ReactNativeWebView.postMessage(error);
};
const getRenderConfig = function(siteKey, theme, size) {
var config = {
Expand Down Expand Up @@ -261,33 +261,33 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with minimum pro
// have loaded by this point; render is sync.
console.log("challenge render complete");
} catch (e) {
console.log("challenge failed to render");
window.ReactNativeWebView.postMessage("error");
console.log("challenge failed to render:", e);
window.ReactNativeWebView.postMessage(e.name);
}
try {
console.log("showing challenge");
hcaptcha.execute(getExecuteOpts());
} catch (e) {
console.log("failed to show challenge");
window.ReactNativeWebView.postMessage("error");
console.log("failed to show challenge:", e);
window.ReactNativeWebView.postMessage(e.name);
}
};
var onDataCallback = function(response) {
window.ReactNativeWebView.postMessage(response);
};
var onCancel = function() {
window.ReactNativeWebView.postMessage("cancel");
window.ReactNativeWebView.postMessage("challenge-closed");
};
var onOpen = function() {
document.body.style.backgroundColor = 'rgba(0, 0, 0, 0.3)';
window.ReactNativeWebView.postMessage("open");
console.log("challenge opened");
};
var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage(error); };
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage(error); };
var onDataErrorCallback = function(error) {
console.log("challenge error callback fired");
window.ReactNativeWebView.postMessage("error");
console.warn("challenge error callback fired");
window.ReactNativeWebView.postMessage(error);
};
const getRenderConfig = function(siteKey, theme, size) {
var config = {
Expand Down
Loading
Loading