Skip to content

Commit 897f4bf

Browse files
authored
UJ support (#102)
* feat(journeys): add core runtime, init hook, and setData bridge Phase 1 implements the journey runtime and the first shippable automatic capture path. Scope of work: - add a shared journey runtime with bounded buffering, navigation registration, and consumer lifecycle gating - install automatic app wrapping through AppRegistry.setWrapperComponentProvider using an internal wrapper component - capture initial automatic touch events (tap and basic drag) from the wrapper and screen transitions from a registered navigation container - migrate the captcha bridge from execute(opts) payload injection to Android-aligned setData(widgetId, payload) followed by execute(widgetId) - add verifyParams support with precedence over deprecated rqdata/phone fields - wire ConfirmHcaptcha and Hcaptcha into journey consumer lifecycle management - add focused runtime and captcha integration tests and update affected snapshots Review notes applied before commit: - removed render-time mutation by switching payload reads to peek semantics - fixed re-entry behavior so the current screen is emitted again when collection restarts on the same route - kept the public surface limited to init/register plus verifyParams and userJourney for this phase * feat(journeys): refine automatic touch classification and identifiers Phase 2 improves the quality of automatic touch capture without changing the public journey surface. Scope of work: - classify scroll-like motion during touch move so axis-dominant gestures emit ScrollView start/end events instead of being inferred only at gesture end - keep the event volume intentionally sparse by emitting only start/end transitions, with no continuous drag or scroll stream - upgrade identifier resolution so semantic metadata from nativeID, testID, or accessibilityLabel can replace numeric target tags when richer information becomes available mid-gesture - expand journey runtime tests to cover scroll classification, identifier upgrades, and semantic-over-numeric precedence Review notes applied before commit: - kept phase scope limited to runtime and wrapper behavior only - avoided introducing any new public API or diagnostics surface in this commit - verified that the resulting wire shape still uses the existing Android-compatible event schema * feat(journeys): add runtime diagnostics and stopEvents coverage Phase 3 adds the remaining runtime ergonomics and diagnostics around automatic journey collection. Scope of work: - allow initJourneyTracking to enable optional journey debug logging without affecting default production behavior - add an optional runtime stats callback that reports buffer size, consumer count, capture state, wrapper installation state, and current route snapshot for diagnostics and tests - export JourneyTrackingOptions and JourneyRuntimeStats typings for the public init surface - add test coverage for diagnostics behavior and for the existing ConfirmHcaptcha stopEvents() lifecycle path Review notes applied before commit: - kept diagnostics opt-in and side-effect free unless explicitly enabled - preserved the existing capture lifecycle semantics while exposing observability around them - limited the public API expansion to typed init options rather than adding another control surface * fix(journeys): inject fresh payloads at verify time and keep capture alive This change fixes the timing and lifecycle issues in the automatic journey implementation. Scope of work: - remove mount-time verifyData baking from the WebView HTML config and switch the initial verification path to a ready-handshake model - emit an internal ready signal after hcaptcha.render(...) and inject a fresh setData(widgetId, payload) + execute(widgetId) script from React Native when the widget is ready - keep the reset path data-driven while avoiding an unnecessary widget reset on the initial verify path - change the journey runtime so capture continues after init even when the active consumer count falls back to zero, preserving app-level history between captcha mounts - tighten current-route re-emission so enable/disable churn does not accumulate duplicate appear events when the current screen is already buffered - make ConfirmHcaptcha.stopEvents() clear the current journey buffer without shutting down global post-init capture - add tests for ring-buffer overflow, peek vs drain semantics, post-consumer buffering, ready-time payload injection, and error/cancel buffer preservation - update snapshots to reflect the new internal ready handshake and removal of baked verifyData from HTML Review notes applied before commit: - the initial verify path now matches the plan and Android semantics by building payloads at verification time rather than render time - runtime capture semantics now consistently follow the once-at-init model described in V3 - tests cover the previously missing lifecycle and buffering cases that could regress silently * cleanup and docs * better param tests * fix(journeys): resolve semantic ids, round coords, and harden debug info Align the journey runtime with the behavior validated in the example app manual runs. This change fixes three concrete issues in the shipping package: - resolve semantic identifiers from the full synthetic React Native event before falling back to native numeric targets, so nativeID/testID/accessibilityLabel survive real touch capture on iOS and Android - round captured touch coordinates to integers before emitting journey events - stop assuming ReactNativeVersion.version exists when building debug info; read the RN version defensively from Platform.constants.reactNativeVersion and ignore invalid shapes instead of throwing It also restores and expands test coverage around the affected paths: - add pure unit coverage for buildVerifyData and buildDebugInfo - add runtime tests for integer coordinates and synthetic-event identifier resolution - add verify-payload tests that prove legacy rqdata/phone fields still reach the final setData payload - refresh snapshots for the updated debug-info output Validated with: - npx jest __tests__/journey.test.js __tests__/buildVerifyData.test.js __tests__/Hcaptcha.test.js __tests__/ConfirmHcaptcha.test.js --runInBand - npx eslint Hcaptcha.js journey/runtime.js journey/wrapper.js __tests__/journey.test.js __tests__/buildVerifyData.test.js __tests__/Hcaptcha.test.js __tests__/ConfirmHcaptcha.test.js * add UJ example * fix(debug): preserve rnver fallback * fix(journey): ignore stationary long presses * feat(journey): add touch capture toggle * fix(journey): preserve wrapper composition across touch toggle * docs(journey): clarify touch capture toggle caveat
1 parent a18b8bf commit 897f4bf

17 files changed

Lines changed: 2205 additions & 117 deletions

Example.App.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import React, { useState, useRef } from 'react';
22
import { Text, View, StyleSheet, TouchableOpacity } from 'react-native';
33
import ConfirmHcaptcha from '@hcaptcha/react-native-hcaptcha';
4+
// import ConfirmHcaptcha, { initJourneyTracking } from '@hcaptcha/react-native-hcaptcha';
45

56
// demo sitekey
67
const siteKey = '00000000-0000-0000-0000-000000000000';
78
const baseUrl = 'https://hcaptcha.com';
89

10+
// Uncomment to enable automatic User Journeys collection for this example app.
11+
// initJourneyTracking();
12+
913
const App = () => {
1014
const [code, setCode] = useState(null);
1115
const captchaForm = useRef(null);
@@ -38,6 +42,8 @@ const App = () => {
3842
baseUrl={baseUrl}
3943
languageCode="en"
4044
onMessage={onMessage}
45+
// Uncomment to attach the buffered User Journey to each verification request.
46+
// userJourney={true}
4147
/>
4248
<TouchableOpacity
4349
onPress={() => {

Example.UserJourneys.App.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import React, { useRef, useState } from 'react';
2+
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
3+
import ConfirmHcaptcha, { initJourneyTracking } from '@hcaptcha/react-native-hcaptcha';
4+
5+
// demo sitekey
6+
const siteKey = '00000000-0000-0000-0000-000000000000';
7+
const baseUrl = 'https://hcaptcha.com';
8+
9+
initJourneyTracking({
10+
debug: true,
11+
onStats: (stats) => {
12+
console.log('[journey-stats]', JSON.stringify(stats));
13+
},
14+
});
15+
16+
const App = () => {
17+
const [code, setCode] = useState(null);
18+
const [warmups, setWarmups] = useState(0);
19+
const captchaForm = useRef(null);
20+
21+
const onMessage = (event) => {
22+
if (!event?.nativeEvent?.data) {
23+
return;
24+
}
25+
26+
if (event.nativeEvent.data === 'open') {
27+
console.log('Visual challenge opened');
28+
return;
29+
}
30+
31+
if (event.success) {
32+
setCode(event.nativeEvent.data);
33+
captchaForm.current.hide();
34+
event.markUsed();
35+
console.log('Verified code from hCaptcha', event.nativeEvent.data);
36+
return;
37+
}
38+
39+
if (event.nativeEvent.data === 'challenge-expired') {
40+
event.reset();
41+
console.log('Visual challenge expired, reset...', event.nativeEvent.data);
42+
return;
43+
}
44+
45+
setCode(event.nativeEvent.data);
46+
captchaForm.current.hide();
47+
console.log('Verification failed', event.nativeEvent.data);
48+
};
49+
50+
return (
51+
<View style={styles.container}>
52+
<ConfirmHcaptcha
53+
ref={captchaForm}
54+
baseUrl={baseUrl}
55+
languageCode="en"
56+
onMessage={onMessage}
57+
siteKey={siteKey}
58+
userJourney={true}
59+
/>
60+
<TouchableOpacity
61+
nativeID="warmup-touch"
62+
onPress={() => {
63+
setWarmups((value) => value + 1);
64+
}}
65+
testID="warmup-touch"
66+
>
67+
<Text style={styles.paragraph}>Preverify: {warmups} taps here</Text>
68+
</TouchableOpacity>
69+
<TouchableOpacity
70+
nativeID="launch-captcha"
71+
onPress={() => {
72+
captchaForm.current.show();
73+
}}
74+
testID="launch-captcha"
75+
>
76+
<Text style={styles.paragraph}>Tap to launch</Text>
77+
</TouchableOpacity>
78+
{code && (
79+
<Text style={styles.codeContainer}>
80+
{'passcode or status: '}
81+
<Text style={styles.codeText}>
82+
{code}
83+
</Text>
84+
</Text>
85+
)}
86+
</View>
87+
);
88+
};
89+
90+
const styles = StyleSheet.create({
91+
codeContainer: {
92+
alignSelf: 'center',
93+
},
94+
codeText: {
95+
color: 'darkviolet',
96+
fontSize: 6,
97+
fontWeight: 'bold',
98+
},
99+
container: {
100+
backgroundColor: '#ecf0f1',
101+
flex: 1,
102+
justifyContent: 'center',
103+
padding: 8,
104+
},
105+
paragraph: {
106+
fontSize: 18,
107+
fontWeight: 'bold',
108+
margin: 24,
109+
textAlign: 'center',
110+
},
111+
});
112+
113+
export default App;

Hcaptcha.d.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@ import React from 'react';
22
import { StyleProp, ViewStyle } from 'react-native';
33
import { WebViewMessageEvent } from 'react-native-webview';
44

5-
type HcaptchaProps = {
5+
export type HCaptchaVerifyParams = {
6+
rqdata?: string;
7+
phonePrefix?: string;
8+
phoneNumber?: string;
9+
};
10+
11+
export type HcaptchaProps = {
612
/**
713
* The callback function that runs after receiving a response, error, or when user cancels.
814
*/
9-
onMessage?: (event: CustomWebViewMessageEvent) => void;
15+
onMessage: (event: CustomWebViewMessageEvent) => void;
1016
/**
1117
* The size of the checkbox.
1218
*/
@@ -52,6 +58,10 @@ type HcaptchaProps = {
5258
* Hcaptcha execution options (see Enterprise docs)
5359
*/
5460
rqdata?: string;
61+
/**
62+
* Verification payload overrides. Values here take precedence over deprecated top-level fields.
63+
*/
64+
verifyParams?: HCaptchaVerifyParams;
5565
/**
5666
* Enable / Disable sentry error reporting.
5767
*/
@@ -99,6 +109,10 @@ type HcaptchaProps = {
99109
* Optional full phone number in E.164 format ("+44123..."), for use in MFA.
100110
*/
101111
phoneNumber?: string;
112+
/**
113+
* Enable automatic user journey injection.
114+
*/
115+
userJourney?: boolean;
102116
}
103117

104118
interface CustomWebViewMessageEvent extends WebViewMessageEvent {

0 commit comments

Comments
 (0)