diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 90a3793..fb8f48b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -93,24 +93,25 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: path: react-native-hcaptcha - - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + - if: matrix.platform == 'android' + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: adopt - - if: contains(matrix.os, 'macos') + - if: matrix.platform == 'ios' run: sudo xcode-select -s /Applications/Xcode_16.4.app - - run: | - npm run example -- --pm ${{ matrix.pm }} + - run: npm run example -- --pm ${{ matrix.pm }} working-directory: react-native-hcaptcha env: YARN_ENABLE_IMMUTABLE_INSTALLS: false - id: rn-version working-directory: react-native-hcaptcha-example run: | - RN_VERSION=$(cat package.json | jq ".dependencies.\"react-native\"" -r) + RN_VERSION=$(jq -r ".dependencies.\"react-native\"" package.json) echo "value=${RN_VERSION}" >> $GITHUB_OUTPUT - - run: cat package.json - working-directory: react-native-hcaptcha-example - run: yarn test --config ./jest.config.js working-directory: react-native-hcaptcha-example - run: npx react-native build-${{ matrix.platform }} @@ -118,6 +119,64 @@ jobs: - run: npx --yes check-peer-dependencies --yarn --runOnlyOnRootDependencies working-directory: react-native-hcaptcha-example + e2e: + needs: build + permissions: + contents: read + runs-on: ${{ matrix.os }} + concurrency: + group: 'e2e-${{ matrix.platform }}-${{ github.head_ref || github.ref_name }}' + cancel-in-progress: true + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + platform: android + - os: macos-latest + platform: ios + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + - if: matrix.platform == 'android' + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + with: + java-version: 17 + distribution: adopt + - run: npm install + - name: Generate E2E host app + run: npm run test:e2e:setup -- --pm npm --platform ${{ matrix.platform }} + + - if: matrix.platform == 'android' + name: Run Android E2E tests + uses: hCaptcha/hcaptcha-android-sdk/.github/actions/android-emulator-run@dc2e0fc978424322c977b68ae06f8dd54e571e22 + with: + target: google_apis + api-level: '34' + profile: pixel_7 + script: npm run test:e2e:android + + - if: matrix.platform == 'ios' + name: Boot iOS simulator + run: | + UDID=$(xcrun simctl list devices available --json | \ + jq -r '.devices["com.apple.CoreSimulator.SimRuntime.iOS-18-5"][] | select(.name=="iPhone 16") | .udid') + echo "Booting iPhone 16 (iOS 18.5): $UDID" + xcrun simctl bootstatus "$UDID" -b + + - if: matrix.platform == 'ios' + name: Run iOS E2E tests + run: npm run test:e2e:ios + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + if: always() + with: + name: e2e-results-${{ matrix.platform }} + path: __e2e__/__image_snapshots__/ + retention-days: 14 + create-an-issue: permissions: contents: read diff --git a/.gitignore b/.gitignore index f78c883..534900d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ node_modules/* yarn.lock +.DS_Store # Reassure performance testing files .reassure/ +output/ + +# Generated E2E host app +__e2e__/host/ diff --git a/Hcaptcha.js b/Hcaptcha.js index 1e1cbfa..d480fca 100644 --- a/Hcaptcha.js +++ b/Hcaptcha.js @@ -20,6 +20,42 @@ const patchPostMessageJsCode = `(${String(function () { window.ReactNativeWebView.postMessage = patchedPostMessage; })})();`; +const serializeForInlineScript = (value) => + JSON.stringify(value) + .replace(//g, '\\u003e') + .replace(/&/g, '\\u0026') + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029'); + +const normalizeTheme = (value) => { + if (value == null) { + return null; + } + + if (typeof value === 'object') { + return value; + } + + if (typeof value === 'string') { + try { + return JSON.parse(value); + } catch (_) { + return value; + } + } + + return value; +}; + +const normalizeSize = (value) => { + if (value == null) { + return 'invisible'; + } + + return value === 'checkbox' ? 'normal' : value; +}; + const buildHcaptchaApiUrl = (jsSrc, siteKey, hl, theme, host, sentry, endpoint, assethost, imghost, reportapi, orientation) => { var url = `${jsSrc || 'https://hcaptcha.com/1/api.js'}?render=explicit&onload=onloadCallback`; @@ -43,7 +79,7 @@ const buildHcaptchaApiUrl = (jsSrc, siteKey, hl, theme, host, sentry, endpoint, * * @param {*} onMessage: callback after receiving response, error, or when user cancels * @param {*} siteKey: your hCaptcha sitekey - * @param {string} size: The size of the checkbox, can be 'invisible', 'compact' or 'checkbox', Default: 'invisible' + * @param {string} size: The size of the widget, can be 'invisible', 'compact' or 'normal'. 'checkbox' is kept as a legacy alias for 'normal'. Default: 'invisible' * @param {*} style: custom style * @param {*} url: base url * @param {*} languageCode: can be found at https://docs.hcaptcha.com/languages @@ -90,26 +126,15 @@ const Hcaptcha = ({ phonePrefix, phoneNumber, }) => { - const apiUrl = buildHcaptchaApiUrl(jsSrc, siteKey, languageCode, theme, host, sentry, endpoint, assethost, imghost, reportapi, orientation); const tokenTimeout = 120000; const loadingTimeout = 15000; const [isLoading, setIsLoading] = useState(true); - - if (theme && typeof theme === 'string') { - try { - JSON.parse(theme); - } catch (_) { - theme = `"${theme}"`; - } - } - - if (theme && typeof theme === 'object') { - theme = `${JSON.stringify(theme)}`; - } - - if (rqdata && typeof rqdata === 'string') { - rqdata = `"${rqdata}"`; - } + const normalizedTheme = useMemo(() => normalizeTheme(theme), [theme]); + const normalizedSize = useMemo(() => normalizeSize(size), [size]); + const apiUrl = useMemo( + () => buildHcaptchaApiUrl(jsSrc, siteKey, languageCode, normalizedTheme, host, sentry, endpoint, assethost, imghost, reportapi, orientation), + [jsSrc, siteKey, languageCode, normalizedTheme, host, sentry, endpoint, assethost, imghost, reportapi, orientation] + ); const debugInfo = useMemo( () => { @@ -128,6 +153,21 @@ const Hcaptcha = ({ [debug] ); + const serializedWebViewConfig = useMemo( + () => serializeForInlineScript({ + apiUrl, + backgroundColor: backgroundColor ?? '', + debugInfo, + phoneNumber: phoneNumber ?? null, + phonePrefix: phonePrefix ?? null, + rqdata: rqdata ?? null, + siteKey: siteKey || '', + size: normalizedSize, + theme: normalizedTheme, + }), + [apiUrl, backgroundColor, debugInfo, normalizedSize, normalizedTheme, phoneNumber, phonePrefix, rqdata, siteKey] + ); + const generateTheWebViewContent = useMemo( () => ` @@ -137,14 +177,21 @@ const Hcaptcha = ({ -
`, - [debugInfo, apiUrl, siteKey, theme, size, backgroundColor, rqdata, phonePrefix, phoneNumber] + [serializedWebViewConfig] ); useEffect(() => { @@ -261,7 +309,7 @@ const Hcaptcha = ({ data: 'sms-open-failed', description: err.message, }, - success: false + success: false, }); }); return false; diff --git a/MAINTAINER.md b/MAINTAINER.md index b3168b6..932b73c 100644 --- a/MAINTAINER.md +++ b/MAINTAINER.md @@ -12,13 +12,33 @@ PATCH: bugfix only. - bump [`package.json's`](./package.json) version - run `npm i` to update `package-lock.json` +- update [`MAINTAINER.md`](./MAINTAINER.md) if release or verification steps changed +- verify: + - `npm test` + - `npm run lint` + - `CI=1 npm run test:e2e` to confirm CI skips local-only device E2E cleanly + - `npm run test:e2e:android-local` if you want the full Android emulator verification locally - commit `package.json` and `package-lock.json` +- commit any maintainer or release-documentation updates in the same PR - open the PR for review - once the PR is approved and merged to master: - - set the tag on master matching your version: git tag `vM.M.P` + - set the tag on master matching your version: `git tag vM.m.p` + - push the tag: `git push origin vM.m.p` - draft a new release https://github.com/hCaptcha/react-native-hcaptcha/releases + - summarize functional changes in the release notes, including: + - safer WebView config handling for HTML-facing props + - `ConfirmHcaptcha` forwarding of `phonePrefix` and `phoneNumber` + - `checkbox` size alias normalization to `normal` + - expanded unit coverage and local Android theme E2E coverage - once the release is created, CI will release the new version to https://www.npmjs.com/package/@hcaptcha/react-native-hcaptcha?activeTab=versions +3. Post-release verification: + +- confirm the GitHub release published successfully +- confirm npm lists the new version: + - `npm view @hcaptcha/react-native-hcaptcha version` +- smoke-test install in a disposable app if the release changed packaging or generated assets + ### Generate test app For `expo` test app @@ -33,6 +53,13 @@ For `react-native` test app - `yarn example` - `yarn android` or `npm run android` +For the local Android emulator regression E2E added in this repo: + +- `cd react-native-hcaptcha` +- ensure Android SDK, emulator, and an AVD are installed +- run `npm run test:e2e:android-local` +- inspect artifacts in [`output/android-e2e`](./output/android-e2e) if the run fails + For iOS instead the last step do: - `pushd ios; env USE_HERMES=0 pod install; popd` @@ -155,4 +182,4 @@ Usage Error: The file:../react-native-hcaptcha string didn't match the required Solution: `yarn add @hcaptcha/react-native-hcaptcha@file:../react-native-hcaptcha` -Yarn 2.10.x and above doesn't require `file:` scheme prefix https://stackoverflow.com/questions/40102686/how-to-install-package-with-local-path-by-yarn-it-couldnt-find-package \ No newline at end of file +Yarn 2.10.x and above doesn't require `file:` scheme prefix https://stackoverflow.com/questions/40102686/how-to-install-package-with-local-path-by-yarn-it-couldnt-find-package diff --git a/README.md b/README.md index d58d62c..41357a3 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ The SDK supports phone prefix and phone number parameters for MFA (Multi-Factor | **Name** | **Type** | **Description** | |:---|:---|:---| | siteKey _(required)_ | string | The hCaptcha siteKey | -| size | string | The size of the checkbox, can be 'invisible', 'compact' or 'checkbox', Default: 'invisible' | +| size | string | The size of the widget, can be 'invisible', 'compact' or 'normal'. `checkbox` is also accepted as a legacy alias for `normal`. Default: 'invisible' | | onMessage | Function (see [here](https://github.com/react-native-webview/react-native-webview/blob/master/src/WebViewTypes.ts#L299)) | The callback function that runs after receiving a response, error, or when user cancels. | | languageCode | string | Default language for hCaptcha; overrides phone defaults. A complete list of supported languages and their codes can be found [here](https://docs.hcaptcha.com/languages/) | | showLoading | boolean | Whether to show a loading indicator while the hCaptcha web content loads | diff --git a/__e2e__/__image_snapshots__/android/hcaptcha-dark.png b/__e2e__/__image_snapshots__/android/hcaptcha-dark.png new file mode 100644 index 0000000..e9ee695 Binary files /dev/null and b/__e2e__/__image_snapshots__/android/hcaptcha-dark.png differ diff --git a/__e2e__/__image_snapshots__/android/hcaptcha-light.png b/__e2e__/__image_snapshots__/android/hcaptcha-light.png new file mode 100644 index 0000000..058f682 Binary files /dev/null and b/__e2e__/__image_snapshots__/android/hcaptcha-light.png differ diff --git a/__e2e__/__image_snapshots__/ios/hcaptcha-dark.png b/__e2e__/__image_snapshots__/ios/hcaptcha-dark.png new file mode 100644 index 0000000..6f59821 Binary files /dev/null and b/__e2e__/__image_snapshots__/ios/hcaptcha-dark.png differ diff --git a/__e2e__/__image_snapshots__/ios/hcaptcha-light.png b/__e2e__/__image_snapshots__/ios/hcaptcha-light.png new file mode 100644 index 0000000..21fc5cb Binary files /dev/null and b/__e2e__/__image_snapshots__/ios/hcaptcha-light.png differ diff --git a/__e2e__/darkTheme.harness.js b/__e2e__/darkTheme.harness.js new file mode 100644 index 0000000..1c74f56 --- /dev/null +++ b/__e2e__/darkTheme.harness.js @@ -0,0 +1,76 @@ +import { describe, test, render, expect } from 'react-native-harness'; +import { screen } from '@react-native-harness/ui'; +import React from 'react'; +import { StyleSheet, View } from 'react-native'; + +import Hcaptcha from '@hcaptcha/react-native-hcaptcha/Hcaptcha.js'; + +const siteKey = '10000000-ffff-ffff-ffff-000000000001'; +const baseUrl = 'https://hcaptcha.com'; + +const WEBVIEW_LOAD_MS = 10000; + +const WidgetFixture = ({ theme }) => ( + + + {}} + /> + + +); + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#eef2ff', + }, + widgetFrame: { + width: 360, + height: 118, + overflow: 'hidden', + borderRadius: 18, + borderWidth: 1, + borderColor: '#cbd5e1', + backgroundColor: '#ffffff', + }, + widget: { + flex: 1, + height: '100%', + }, +}); + +describe('hCaptcha theme rendering', () => { + test('light widget matches baseline', async () => { + await render(, { timeout: WEBVIEW_LOAD_MS }); + await new Promise((r) => setTimeout(r, WEBVIEW_LOAD_MS)); + + const screenshot = await screen.screenshot(); + await expect(screenshot).toMatchImageSnapshot({ + name: 'hcaptcha-light', + threshold: 0.15, + failureThreshold: 5, + failureThresholdType: 'percent', + }); + }); + + test('dark widget matches baseline', async () => { + await render(, { timeout: WEBVIEW_LOAD_MS }); + await new Promise((r) => setTimeout(r, WEBVIEW_LOAD_MS)); + + const screenshot = await screen.screenshot(); + await expect(screenshot).toMatchImageSnapshot({ + name: 'hcaptcha-dark', + threshold: 0.15, + failureThreshold: 5, + failureThresholdType: 'percent', + }); + }); +}); diff --git a/__e2e__/jest.harness.config.mjs b/__e2e__/jest.harness.config.mjs new file mode 100644 index 0000000..031efa1 --- /dev/null +++ b/__e2e__/jest.harness.config.mjs @@ -0,0 +1,4 @@ +export default { + preset: 'react-native-harness', + testMatch: ['/__e2e__/**/*.harness.{js,ts,tsx}'], +}; diff --git a/__e2e__/rn-harness.config.mjs b/__e2e__/rn-harness.config.mjs new file mode 100644 index 0000000..048d317 --- /dev/null +++ b/__e2e__/rn-harness.config.mjs @@ -0,0 +1,37 @@ +import { + androidPlatform, + androidEmulator, +} from '@react-native-harness/platform-android'; +import { + applePlatform, + appleSimulator, +} from '@react-native-harness/platform-apple'; + +const config = { + entryPoint: './index.js', + appRegistryComponentName: 'react_native_hcaptcha_example', + + disableViewFlattening: true, + bridgeTimeout: 120000, + forwardClientLogs: true, + + runners: [ + androidPlatform({ + name: 'android', + device: androidEmulator(process.env.HARNESS_AVD_NAME || 'test'), + bundleId: 'com.react_native_hcaptcha_example', + }), + applePlatform({ + name: 'ios', + device: appleSimulator( + process.env.HARNESS_IOS_DEVICE || 'iPhone 16', + process.env.HARNESS_IOS_VERSION || '18.5', + ), + bundleId: 'org.reactjs.native.example.react-native-hcaptcha-example', + }), + ], + + defaultRunner: 'android', +}; + +export default config; diff --git a/__mocks__/react-native-webview.js b/__mocks__/react-native-webview.js index 8451896..5c7671f 100644 --- a/__mocks__/react-native-webview.js +++ b/__mocks__/react-native-webview.js @@ -2,13 +2,28 @@ import React from 'react'; let messageDataToSend = null; +let lastInjectJavaScriptMock = null; export const setWebViewMessageData = (data) => { messageDataToSend = data; }; +export const getLastInjectJavaScriptMock = () => lastInjectJavaScriptMock; + +export const resetWebViewMockState = () => { + messageDataToSend = null; + lastInjectJavaScriptMock = null; +}; + const WebView = React.forwardRef((props, ref) => { const { onMessage } = props; + const injectJavaScript = React.useMemo(() => jest.fn(), []); + + lastInjectJavaScriptMock = injectJavaScript; + + React.useImperativeHandle(ref, () => ({ + injectJavaScript, + }), [injectJavaScript]); React.useEffect(() => { if (messageDataToSend && onMessage) { diff --git a/__scripts__/generate-example.js b/__scripts__/generate-example.js index 26575c4..44c53a3 100644 --- a/__scripts__/generate-example.js +++ b/__scripts__/generate-example.js @@ -16,6 +16,7 @@ Options: --name Specify the project name (required) --path Specify the project path (default: ../) --pm Specify the package manager to use (default: yarn) + --skip-build Skip native builds (pod install, gradlew assemble) --verbose Enable verbose logging -h, --help Show this help message `); @@ -30,6 +31,7 @@ function parseArgs(args) { projectRelativeProjectPath: '../react-native-hcaptcha-example', packageManager: 'yarn', verbose: false, + skipBuild: false, projectTemplate: undefined, frameworkVersion: undefined, }; @@ -45,6 +47,7 @@ function parseArgs(args) { options.projectRelativeProjectPath = value; }, '--pm': (value) => { options.packageManager = value; }, + '--skip-build': () => { options.skipBuild = true; }, '--verbose': () => { options.verbose = true; }, '-h': showHelp, '--help': showHelp, @@ -55,14 +58,13 @@ function parseArgs(args) { if (argHandlers[arg]) { const handler = argHandlers[arg]; - if (typeof handler === 'function') { + if (handler.length === 0) { + handler(); + } else { const nextArg = args[i + 1]; - // Check if the next argument is not another flag - if (typeof handler === 'function' && (nextArg && !nextArg.startsWith('--'))) { + if (nextArg && !nextArg.startsWith('--')) { handler(nextArg); - i++; // Skip next argument as it is consumed - } else if (handler === showHelp) { - handler(); + i++; } else { console.error(`Error: ${arg} requires a value.`); showHelp(); @@ -99,7 +101,7 @@ function buildCreateCommand({ cliName, projectRelativeProjectPath, projectName, if (cliName === 'expo') { createCommand.push(`--${packageManager}`); } else if (cliName.includes('react-native')) { - createCommand.push('--pm', packageManager); + createCommand.push('--pm', packageManager, '--install-pods', 'false'); if (frameworkVersion) { createCommand.push('--version', frameworkVersion); @@ -121,7 +123,7 @@ function checkHcaptchaLinked() { } // Main function that takes parsed arguments and runs the necessary setup -function main({ cliName, projectRelativeProjectPath, projectName, projectTemplate, packageManager, frameworkVersion, verbose }) { +function main({ cliName, projectRelativeProjectPath, projectName, projectTemplate, packageManager, frameworkVersion, verbose, skipBuild }) { console.warn(`Warning! Example project will be generated in '${path.dirname(process.cwd())}'`); // Build the command to initialize the project @@ -143,19 +145,21 @@ function main({ cliName, projectRelativeProjectPath, projectName, projectTemplat // Install dependencies const isHcaptchaLinked = checkHcaptchaLinked(); const mainPackage = '@hcaptcha/react-native-hcaptcha'; - const mainPackagePath = path.join(path.dirname(projectRelativeProjectPath), path.basename(process.cwd())); + const libRoot = process.cwd(); + const libPathFromProject = path.relative(projectPath, libRoot); const peerPackages = 'react-native-modal react-native-webview'; const devPackages = 'typescript @babel/preset-env'; console.warn('Installing dependencies...'); if (packageManager === 'yarn') { - execSync(`yarn add @hcaptcha/react-native-hcaptcha@file:${mainPackagePath}`, packageManagerOptions); + execSync(`yarn add @hcaptcha/react-native-hcaptcha@file:${libPathFromProject}`, packageManagerOptions); execSync(`yarn add --dev ${devPackages}`, packageManagerOptions); execSync(`yarn add ${peerPackages}`, packageManagerOptions); } else { // https://github.com/facebook/react-native/issues/29977 - react-native doesn't work with symlinks so `cp` instead - // fs.symlinkSync(mainPackagePath, path.join(projectPath, 'react-native-hcaptcha'), 'dir'); - execSync(`cp -r ${mainPackagePath} ${projectPath}`); + const destLibDir = path.join(projectPath, 'react-native-hcaptcha'); + const excludes = ['__e2e__/host', '__tests__', '__mocks__', 'node_modules', '.git', 'output', '.reassure'].map(e => `--exclude=${e}`).join(' '); + execSync(`rsync -a ${excludes} ${libRoot}/ ${destLibDir}/`, { stdio: 'inherit' }); execSync('npm i --save file:./react-native-hcaptcha', packageManagerOptions); execSync(`npm i --save --dev ${devPackages}`, packageManagerOptions); execSync(`npm i --save ${peerPackages}`, packageManagerOptions); @@ -165,21 +169,25 @@ function main({ cliName, projectRelativeProjectPath, projectName, projectTemplat execSync(`${packageManager} link ${mainPackage}`, packageManagerOptions); } - // iOS: pod install - if (platform() === 'darwin') { - const podOptions = { stdio: 'inherit', cwd: path.join(projectPath, 'ios') }; - execSync('bundle install', podOptions); - execSync('bundle exec pod install', podOptions); - } + if (!skipBuild) { + // iOS: pod install + if (platform() === 'darwin') { + const podOptions = { stdio: 'inherit', cwd: path.join(projectPath, 'ios') }; + execSync('bundle install', podOptions); + execSync('bundle exec pod install', podOptions); + } - // Android - const gradleOptions = { - stdio: 'inherit', cwd: path.join(projectPath, 'android'), - env: { ...process.env, GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx4096m -XX:MaxMetaspaceSize=1024m"' }, - }; - execSync('./gradlew assemble', gradleOptions); + // Android + const gradleOptions = { + stdio: 'inherit', cwd: path.join(projectPath, 'android'), + env: { ...process.env, GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx4096m -XX:MaxMetaspaceSize=1024m"' }, + }; + execSync('./gradlew assemble', gradleOptions); - ['assets', 'res'].map(dir => fs.mkdirSync(path.join(projectPath, 'android/app/src/main', dir), { recursive: true })); + ['assets', 'res'].map(dir => fs.mkdirSync(path.join(projectPath, 'android/app/src/main', dir), { recursive: true })); + } else { + console.log('Skipping native builds (--skip-build).'); + } console.log('Setup complete.'); } diff --git a/__scripts__/setup-e2e-host.js b/__scripts__/setup-e2e-host.js new file mode 100644 index 0000000..f45f77e --- /dev/null +++ b/__scripts__/setup-e2e-host.js @@ -0,0 +1,121 @@ +#!/usr/bin/env node + +const { execSync } = require('child_process'); +const { platform: osPlatform } = require('os'); +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '..'); +const HOST_DIR = path.join(ROOT, '__e2e__', 'host'); +const E2E_DIR = path.join(ROOT, '__e2e__'); + +const pm = process.argv.includes('--pm') ? process.argv[process.argv.indexOf('--pm') + 1] : 'npm'; +const targetPlatform = process.argv.includes('--platform') + ? process.argv[process.argv.indexOf('--platform') + 1] + : null; + +if (fs.existsSync(HOST_DIR)) { + console.log('Cleaning previous host app...'); + fs.rmSync(HOST_DIR, { recursive: true, force: true }); +} + +// 1. Scaffold the host app WITHOUT building native code +console.log('Generating host app at __e2e__/host/ ...'); +execSync( + `node __scripts__/generate-example.js --path __e2e__/host --pm ${pm} --skip-build`, + { stdio: 'inherit', cwd: ROOT }, +); + +const hostTestsDir = path.join(HOST_DIR, '__tests__'); +if (fs.existsSync(hostTestsDir)) { + fs.rmSync(hostTestsDir, { recursive: true, force: true }); +} + +// 2. Install react-native-harness (must happen BEFORE native build so +// HarnessUI TurboModule is compiled into the binary) +console.log('Installing react-native-harness in host app...'); +const harnessPkgs = [ + 'react-native-harness', + '@react-native-harness/platform-android', + '@react-native-harness/platform-apple', + '@react-native-harness/ui', +].join(' '); + +if (pm === 'yarn') { + execSync(`yarn add --dev ${harnessPkgs}`, { stdio: 'inherit', cwd: HOST_DIR }); +} else { + execSync(`npm i --save-dev ${harnessPkgs}`, { stdio: 'inherit', cwd: HOST_DIR }); +} + +// Patch @react-native-harness/ui codegenConfig: the published package is +// missing ios.modulesProvider, so RN's codegen never registers HarnessUI in +// RCTModuleProviders.mm and the TurboModule can't be found at runtime on iOS. +const harnessUiPkgPath = path.join(HOST_DIR, 'node_modules', '@react-native-harness', 'ui', 'package.json'); +if (fs.existsSync(harnessUiPkgPath)) { + const harnessUiPkg = JSON.parse(fs.readFileSync(harnessUiPkgPath, 'utf8')); + if (harnessUiPkg.codegenConfig && !harnessUiPkg.codegenConfig.ios) { + harnessUiPkg.codegenConfig.ios = { + modulesProvider: { HarnessUI: 'HarnessUI' }, + }; + fs.writeFileSync(harnessUiPkgPath, JSON.stringify(harnessUiPkg, null, 2) + '\n'); + console.log('Patched @react-native-harness/ui codegenConfig with ios.modulesProvider'); + } +} + +// 3. Build native code for the target platform only +const buildIos = (!targetPlatform || targetPlatform === 'ios') && osPlatform() === 'darwin'; +const buildAndroid = !targetPlatform || targetPlatform === 'android'; + +if (buildIos) { + const gemfile = path.join(HOST_DIR, 'Gemfile'); + if (fs.existsSync(gemfile)) { + const content = fs.readFileSync(gemfile, 'utf8'); + if (!content.includes("gem 'nkf'")) { + fs.appendFileSync(gemfile, "\ngem 'nkf'\n"); + } + } + const podDir = path.join(HOST_DIR, 'ios'); + console.log('Running pod install...'); + execSync('bundle install', { stdio: 'inherit', cwd: podDir }); + execSync('bundle exec pod install', { stdio: 'inherit', cwd: podDir }); + + console.log('Building iOS...'); + execSync('npx react-native build-ios --buildFolder build', { stdio: 'inherit', cwd: HOST_DIR }); +} + +if (buildAndroid) { + const gradleDir = path.join(HOST_DIR, 'android'); + console.log('Building Android...'); + execSync('./gradlew assembleDebug', { + stdio: 'inherit', + cwd: gradleDir, + env: { ...process.env, GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx4096m -XX:MaxMetaspaceSize=1024m"' }, + }); + + ['assets', 'res'].forEach(dir => + fs.mkdirSync(path.join(HOST_DIR, 'android/app/src/main', dir), { recursive: true }), + ); +} + +// 4. Copy harness configs and test files into host app +console.log('Copying harness configs and test files into host app...'); +const filesToCopy = [ + ['rn-harness.config.mjs', 'rn-harness.config.mjs'], + ['jest.harness.config.mjs', 'jest.harness.config.mjs'], +]; +for (const [src, dest] of filesToCopy) { + fs.copyFileSync(path.join(E2E_DIR, src), path.join(HOST_DIR, dest)); +} + +const hostE2eDir = path.join(HOST_DIR, '__e2e__'); +fs.mkdirSync(hostE2eDir, { recursive: true }); +const testFiles = fs.readdirSync(E2E_DIR).filter(f => f.endsWith('.harness.js') || f.endsWith('.harness.ts') || f.endsWith('.harness.tsx')); +for (const file of testFiles) { + fs.copyFileSync(path.join(E2E_DIR, file), path.join(hostE2eDir, file)); +} + +const snapshotsDir = path.join(E2E_DIR, '__image_snapshots__'); +fs.mkdirSync(snapshotsDir, { recursive: true }); +fs.symlinkSync(snapshotsDir, path.join(hostE2eDir, '__image_snapshots__')); + +console.log('E2E host app setup complete.'); diff --git a/__tests__/ConfirmHcaptcha.test.js b/__tests__/ConfirmHcaptcha.test.js index 6dcea5e..5df1a1e 100644 --- a/__tests__/ConfirmHcaptcha.test.js +++ b/__tests__/ConfirmHcaptcha.test.js @@ -1,8 +1,20 @@ import React from 'react'; -import { render } from '@testing-library/react-native'; +import { act, render } from '@testing-library/react-native'; +import { SafeAreaView } from 'react-native'; + +import Hcaptcha from '../Hcaptcha'; import ConfirmHcaptcha from '../index'; -describe('ConfirmHcaptcha snapshot tests', () => { +describe('ConfirmHcaptcha', () => { + const getModal = (component) => component.UNSAFE_getByType('Modal'); + const getHcaptchaChild = (component) => component.UNSAFE_getByType(Hcaptcha); + const getInstance = (component) => component.UNSAFE_getByType(ConfirmHcaptcha).instance; + + beforeEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + it('renders ConfirmHcaptcha with minimum props', () => { const component = render( { languageCode="en" /> ); + expect(component).toMatchSnapshot(); }); - it('renders ConfirmHcaptcha with all props', () => { + it('forwards every shared prop to the embedded Hcaptcha component', () => { + const onMessage = jest.fn(); + const debug = { customDebug: true }; const component = render( { assethost="https://all.props/assethost" imghost="https://all.props/imghost" host="all-props-host" + debug={debug} + phonePrefix="44" + phoneNumber="+44123456789" /> ); - expect(component).toMatchSnapshot(); + + expect(getHcaptchaChild(component).props).toMatchObject({ + size: 'compact', + siteKey: '00000000-0000-0000-0000-000000000000', + url: 'https://hcaptcha.com', + languageCode: 'en', + orientation: 'landscape', + onMessage, + showLoading: true, + closableLoading: true, + backgroundColor: 'rgba(0.1, 0.1, 0.1, 0.4)', + loadingIndicatorColor: '#999999', + theme: 'light', + rqdata: '{"some":"data"}', + sentry: true, + jsSrc: 'https://all.props/api-endpoint', + endpoint: 'https://all.props/endpoint', + reportapi: 'https://all.props/reportapi', + assethost: 'https://all.props/assethost', + imghost: 'https://all.props/imghost', + host: 'all-props-host', + debug, + phonePrefix: '44', + phoneNumber: '+44123456789', + }); }); - it('renders ConfirmHcaptcha without safe area view', () => { + it('applies wrapper-only props to the modal and backdrop container', () => { const component = render( + ); + const modal = getModal(component); + const safeAreaView = component.UNSAFE_getByType(SafeAreaView); + + expect(modal.props.useNativeDriver).toBe(true); + expect(modal.props.hideModalContentWhileAnimating).toBe(true); + expect(modal.props.isVisible).toBe(false); + expect(modal.props.hasBackdrop).toBe(true); + expect(modal.props.coverScreen).toBe(true); + expect(modal.props.animationIn).toBe('fadeIn'); + expect(modal.props.animationOut).toBe('fadeOut'); + expect(safeAreaView.props.style).toEqual([ + expect.objectContaining({ + flex: 1, + justifyContent: 'center', + overflow: 'hidden', + }), + { backgroundColor: 'rgba(0.1, 0.1, 0.1, 0.4)' }, + ]); + }); + + it('disables modal backdrop/screen coverage when passiveSiteKey is enabled and omits wrapper backdrop color when hasBackdrop is false', () => { + const component = render( + + ); + const modal = getModal(component); + const safeAreaView = component.UNSAFE_getByType(SafeAreaView); + + expect(modal.props.style).toEqual([ + expect.objectContaining({ margin: 0, display: 'none' }), + { display: 'none' }, + ]); + expect(modal.props.hasBackdrop).toBe(false); + expect(modal.props.coverScreen).toBe(false); + expect(safeAreaView.props.style).toEqual([ + expect.objectContaining({ + flex: 1, + justifyContent: 'center', + overflow: 'hidden', + }), + {}, + ]); + }); + + it('uses SafeAreaView by default and a plain View wrapper when useSafeAreaView is false', () => { + const defaultWrapper = render( + + ); + + expect(defaultWrapper.UNSAFE_queryByType(SafeAreaView)).not.toBeNull(); + + const plainViewWrapper = render( + ); - expect(component).toMatchSnapshot(); + + expect(plainViewWrapper.UNSAFE_queryByType(SafeAreaView)).toBeNull(); + }); + + it('show() and hide() toggle modal visibility, and hide(source) emits cancel', () => { + const onMessage = jest.fn(); + const component = render( + + ); + const instance = getInstance(component); + + act(() => { + instance.show(); + }); + + expect(getModal(component).props.isVisible).toBe(true); + + act(() => { + instance.hide(); + }); + + expect(getModal(component).props.isVisible).toBe(false); + expect(onMessage).not.toHaveBeenCalled(); + + act(() => { + instance.show(); + }); + + act(() => { + instance.hide('backdrop'); + }); + + expect(getModal(component).props.isVisible).toBe(false); + expect(onMessage).toHaveBeenCalledWith({ nativeEvent: { data: 'cancel' } }); + }); + + it('backdrop and back-button handlers call hide(source) and emit cancel events', () => { + const onMessage = jest.fn(); + const component = render( + + ); + const instance = getInstance(component); + + act(() => { + instance.show(); + }); + + act(() => { + getModal(component).props.onBackdropPress(); + }); + + expect(onMessage).toHaveBeenCalledWith({ nativeEvent: { data: 'cancel' } }); + expect(getModal(component).props.isVisible).toBe(false); + + onMessage.mockClear(); + + act(() => { + instance.show(); + }); + + act(() => { + getModal(component).props.onBackButtonPress(); + }); + + expect(onMessage).toHaveBeenCalledWith({ nativeEvent: { data: 'cancel' } }); + expect(getModal(component).props.isVisible).toBe(false); }); }); diff --git a/__tests__/Hcaptcha.test.js b/__tests__/Hcaptcha.test.js index b237daa..61927b9 100644 --- a/__tests__/Hcaptcha.test.js +++ b/__tests__/Hcaptcha.test.js @@ -1,171 +1,568 @@ import React from 'react'; -import { render, waitFor } from '@testing-library/react-native'; +import vm from 'vm'; +import { act, render, waitFor } from '@testing-library/react-native'; +import { ActivityIndicator, Linking, TouchableWithoutFeedback } from 'react-native'; + import Hcaptcha from '../Hcaptcha'; -import { setWebViewMessageData } from 'react-native-webview'; +import { + getLastInjectJavaScriptMock, + resetWebViewMockState, + setWebViewMessageData, +} from 'react-native-webview'; + +const LONG_TOKEN = '10000000-aaaa-bbbb-cccc-000000000001'; + +describe('Hcaptcha', () => { + const getWebView = (component) => component.UNSAFE_getByType('WebView'); + const getWebViewHtml = (component) => getWebView(component).props.source.html; + const getSerializedConfig = (component) => { + const match = getWebViewHtml(component).match(/var hcaptchaConfig = (.*?);\n\s*Object\.entries/s); + + expect(match).not.toBeNull(); + + return JSON.parse(match[1]); + }; + const getApiQueryParams = (component) => + Object.fromEntries(new URL(getSerializedConfig(component).apiUrl).searchParams.entries()); + const getInlineScripts = (component) => + [...getWebViewHtml(component).matchAll(/', }, }; + const component = render( + '} + url="https://hcaptcha.com" + languageCode={'en"'} + backgroundColor={'red\';window.ReactNativeWebView.postMessage("bg");//'} + theme={theme} + rqdata={'";window.ReactNativeWebView.postMessage("rqdata");//'} + sentry={true} + jsSrc={'https://example.com/api.js?x='} + endpoint={'https://example.com/endpoint?'} + reportapi={'https://example.com/reportapi?'} + assethost={'https://example.com/assethost?'} + imghost={'https://example.com/imghost?'} + host={'host"'} + debug={{ + '': '', + }} + orientation={'landscape"'} + phonePrefix={'44";window.ReactNativeWebView.postMessage("prefix");//'} + phoneNumber={'+44123\');window.ReactNativeWebView.postMessage("phone");//'} + /> + ); - [ - { - data: theme, - }, - { - data: JSON.stringify(theme), - }, - { - data: undefined, + const html = getWebViewHtml(component); + const config = getSerializedConfig(component); + const query = getApiQueryParams(component); + + expect(html).toContain('var hcaptchaConfig = '); + expect(html).toContain('const rqdata = hcaptchaConfig.rqdata;'); + expect(html).toContain('const phonePrefix = hcaptchaConfig.phonePrefix;'); + expect(html).toContain('const phoneNumber = hcaptchaConfig.phoneNumber;'); + expect(html).not.toContain(''); + expect(html).not.toContain('const rqdata = ";window.ReactNativeWebView.postMessage("rqdata")'); + expect(html).toContain('\\u003c/script\\u003e\\u003cscript\\u003ealert(\\"site\\")\\u003c/script\\u003e'); + expect(html).toContain('\\u003c/script\\u003e\\u003cscript\\u003ealert(\\"debug\\")\\u003c/script\\u003e'); + + expect(config.siteKey).toBe('site"'); + expect(config.backgroundColor).toBe('red\';window.ReactNativeWebView.postMessage("bg");//'); + expect(config.rqdata).toBe('";window.ReactNativeWebView.postMessage("rqdata");//'); + expect(config.phonePrefix).toBe('44";window.ReactNativeWebView.postMessage("prefix");//'); + expect(config.phoneNumber).toBe('+44123\');window.ReactNativeWebView.postMessage("phone");//'); + expect(config.theme).toEqual(theme); + expect(config.debugInfo['']).toBe(''); + + expect(query.hl).toBe('en"'); + expect(query.host).toBe(encodeURIComponent('host"')); + expect(query.endpoint).toBe('https://example.com/endpoint?'); + expect(query.reportapi).toBe('https://example.com/reportapi?'); + expect(query.assethost).toBe('https://example.com/assethost?'); + expect(query.imghost).toBe('https://example.com/imghost?'); + expect(query.orientation).toBe('landscape"'); + expect(query.sentry).toBe('true'); + expect(query.custom).toBe('true'); + }); + + it('does not render a loading overlay when showLoading is false', () => { + const component = render( + + ); + + expect(component.UNSAFE_queryByType(TouchableWithoutFeedback)).toBeNull(); + expect(component.UNSAFE_queryByType(ActivityIndicator)).toBeNull(); + }); + + it('only allows dismissing the loading overlay when closableLoading is true', () => { + const onMessage = jest.fn(); + const nonClosable = render( + + ); + const nonClosableTouchTarget = nonClosable.UNSAFE_getByType(TouchableWithoutFeedback); + + act(() => { + nonClosableTouchTarget.props.onPress(); + }); + + expect(onMessage).not.toHaveBeenCalled(); + + const closable = render( + + ); + const closableTouchTarget = closable.UNSAFE_getByType(TouchableWithoutFeedback); + + act(() => { + closableTouchTarget.props.onPress(); + }); + + expect(onMessage).toHaveBeenCalledWith({ nativeEvent: { data: 'cancel' } }); + }); + + it('emits a loading timeout while the challenge is still loading', () => { + jest.useFakeTimers(); + const onMessage = jest.fn(); + + render( + + ); + + act(() => { + jest.advanceTimersByTime(15000); + }); + + expect(onMessage).toHaveBeenCalledWith({ + nativeEvent: { + data: 'error', + description: 'loading timeout', }, - ].forEach(({ data }) => { - it(`test ${typeof data}`, async () => { - const component = render( - - ); - expect(component).toMatchSnapshot(); + }); + }); + + it('forwards open messages, marks them as successful, and hides the loading overlay', async () => { + const onMessage = jest.fn(); + setWebViewMessageData('open'); + const component = render( + + ); + + await waitFor(() => { + expect(onMessage).toHaveBeenCalledWith(expect.objectContaining({ + success: true, + reset: expect.any(Function), + nativeEvent: expect.objectContaining({ data: 'open' }), + })); + }); + + expect(component.UNSAFE_queryByType(TouchableWithoutFeedback)).toBeNull(); + }); + + it('forwards token messages with reset and markUsed hooks', async () => { + jest.useFakeTimers(); + const onMessage = jest.fn(); + setWebViewMessageData(LONG_TOKEN); + + const component = render( + + ); + + await waitFor(() => { + expect(onMessage).toHaveBeenCalledWith(expect.objectContaining({ + success: true, + reset: expect.any(Function), + markUsed: expect.any(Function), + nativeEvent: expect.objectContaining({ data: LONG_TOKEN }), + })); + }); + + const [{ reset, markUsed }] = onMessage.mock.calls[0]; + + reset(); + expect(getLastInjectJavaScriptMock()).toHaveBeenCalledWith('onloadCallback();'); + + act(() => { + getWebView(component).props.onMessage({ nativeEvent: { data: 'open' } }); + }); + + markUsed(); + act(() => { + jest.advanceTimersByTime(120000); + }); + + expect(onMessage).toHaveBeenCalledTimes(2); + expect(onMessage).toHaveBeenNthCalledWith(2, expect.objectContaining({ + success: true, + nativeEvent: expect.objectContaining({ data: 'open' }), + })); + }); + + it('emits an expired message when a forwarded token is not marked used', async () => { + jest.useFakeTimers(); + const onMessage = jest.fn(); + setWebViewMessageData(LONG_TOKEN); + + const component = render( + + ); + + await waitFor(() => { + expect(onMessage).toHaveBeenCalledTimes(1); + }); + + act(() => { + getWebView(component).props.onMessage({ nativeEvent: { data: 'open' } }); + }); + + act(() => { + jest.advanceTimersByTime(120000); + }); + + expect(onMessage).toHaveBeenNthCalledWith(3, { + nativeEvent: { data: 'expired' }, + success: false, + reset: expect.any(Function), + }); + }); + + it('marks short non-open messages as errors', async () => { + const onMessage = jest.fn(); + setWebViewMessageData('webview-error'); + + render( + + ); + + await waitFor(() => { + expect(onMessage).toHaveBeenCalledWith(expect.objectContaining({ + success: false, + reset: expect.any(Function), + nativeEvent: expect.objectContaining({ data: 'webview-error' }), + })); + }); + }); + + it('opens hcaptcha links externally and blocks navigation in the WebView', () => { + const openURL = jest.spyOn(Linking, 'openURL').mockResolvedValue(true); + const component = render( + + ); + + const shouldStart = getWebView(component).props.onShouldStartLoadWithRequest({ + url: 'https://www.hcaptcha.com/privacy', + }); + + expect(shouldStart).toBe(false); + expect(openURL).toHaveBeenCalledWith('https://www.hcaptcha.com/privacy'); + }); + + it('opens sms links externally and reports failures back through onMessage', async () => { + const openURL = jest.spyOn(Linking, 'openURL'); + const onMessage = jest.fn(); + const component = render( + + ); + + openURL.mockResolvedValueOnce(true); + const successfulSms = getWebView(component).props.onShouldStartLoadWithRequest({ + url: 'sms:+15551234567', + }); + + expect(successfulSms).toBe(false); + expect(openURL).toHaveBeenCalledWith('sms:+15551234567'); + + openURL.mockRejectedValueOnce(new Error('sms unavailable')); + const failedSms = getWebView(component).props.onShouldStartLoadWithRequest({ + url: 'sms:+15557654321', + }); + + expect(failedSms).toBe(false); + + await waitFor(() => { + expect(onMessage).toHaveBeenCalledWith({ + nativeEvent: { + data: 'sms-open-failed', + description: 'sms unavailable', + }, + success: false, }); }); }); + + it('allows non-hcaptcha, non-sms navigations to continue inside the WebView', () => { + const openURL = jest.spyOn(Linking, 'openURL').mockResolvedValue(true); + const component = render( + + ); + + const shouldStart = getWebView(component).props.onShouldStartLoadWithRequest({ + url: 'https://example.com/path', + }); + + expect(shouldStart).toBe(true); + expect(openURL).not.toHaveBeenCalled(); + }); }); diff --git a/__tests__/__snapshots__/ConfirmHcaptcha.test.js.snap b/__tests__/__snapshots__/ConfirmHcaptcha.test.js.snap index 89321ce..6d25ddb 100644 --- a/__tests__/__snapshots__/ConfirmHcaptcha.test.js.snap +++ b/__tests__/__snapshots__/ConfirmHcaptcha.test.js.snap @@ -1,183 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with all props 1`] = ` - - - - - - - - - - - - - - -
- - ", - } - } - style={ - [ - { - "backgroundColor": "transparent", - "width": "100%", - }, - undefined, - ] - } - /> -
-
-
-`; - -exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with minimum props 1`] = ` +exports[`ConfirmHcaptcha renders ConfirmHcaptcha with minimum props 1`] = ` - - - - - - - - - - -
- - ", - } - } - style={ - [ - { - "backgroundColor": "transparent", - "width": "100%", + "current": { + "injectJavaScript": [MockFunction], }, - undefined, - ] - } - /> - - -
-`; - -exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha without safe area view 1`] = ` - - - - - @@ -525,6 +183,6 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha without safe are } /> - + `; diff --git a/__tests__/__snapshots__/Hcaptcha.test.js.snap b/__tests__/__snapshots__/Hcaptcha.test.js.snap index b8a1f48..041f970 100644 --- a/__tests__/__snapshots__/Hcaptcha.test.js.snap +++ b/__tests__/__snapshots__/Hcaptcha.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Hcaptcha snapshot tests Theme test object 1`] = ` +exports[`Hcaptcha renders Hcaptcha with minimum props 1`] = ` - - - - - - - - - - -
- - ", - } - } - style={ - [ - { - "backgroundColor": "transparent", - "width": "100%", - }, - undefined, - ] - } - /> -
-`; - -exports[`Hcaptcha snapshot tests Theme test string 1`] = ` - - - - - - - - - - - - -
- - ", - } - } - style={ - [ - { - "backgroundColor": "transparent", - "width": "100%", - }, - undefined, - ] - } - /> -
-`; - -exports[`Hcaptcha snapshot tests Theme test undefined 1`] = ` - - - - - - - - - - - - -
- - ", - } - } - style={ - [ - { - "backgroundColor": "transparent", - "width": "100%", - }, - undefined, - ] - } - /> -
-`; - -exports[`Hcaptcha snapshot tests renders Hcaptcha with all props 1`] = ` - - - - - - - - - - - - -
- - ", - } - } - style={ - [ - { - "backgroundColor": "transparent", - "width": "100%", + "current": { + "injectJavaScript": [MockFunction], }, - undefined, - ] - } - /> - - - -
-`; - -exports[`Hcaptcha snapshot tests renders Hcaptcha with debug 1`] = ` - - - - - -
- - ", - } - } - style={ - [ - { - "backgroundColor": "transparent", - "width": "100%", - }, - undefined, - ] - } - /> -
-`; - -exports[`Hcaptcha snapshot tests renders Hcaptcha with minimum props 1`] = ` - - - - - - - - - - diff --git a/index.js b/index.js index e77a66b..3de78e3 100644 --- a/index.js +++ b/index.js @@ -46,6 +46,8 @@ class ConfirmHcaptcha extends PureComponent { hasBackdrop, debug, useSafeAreaView, + phonePrefix, + phoneNumber, } = this.props; const WrapperComponent = useSafeAreaView === false ? View : SafeAreaView; @@ -87,6 +89,8 @@ class ConfirmHcaptcha extends PureComponent { host={host} orientation={orientation} debug={debug} + phonePrefix={phonePrefix} + phoneNumber={phoneNumber} /> @@ -133,6 +137,8 @@ ConfirmHcaptcha.propTypes = { host: PropTypes.string, hasBackdrop: PropTypes.bool, debug: PropTypes.object, + phonePrefix: PropTypes.string, + phoneNumber: PropTypes.string, }; ConfirmHcaptcha.defaultProps = { @@ -154,6 +160,8 @@ ConfirmHcaptcha.defaultProps = { host: undefined, hasBackdrop: true, debug: {}, + phonePrefix: null, + phoneNumber: null, }; export default ConfirmHcaptcha; diff --git a/package-lock.json b/package-lock.json index 6110bfd..e5bf5d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@hcaptcha/react-native-hcaptcha", - "version": "2.1.0", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@hcaptcha/react-native-hcaptcha", - "version": "2.1.0", + "version": "2.2.0", "license": "MIT", "dependencies": { "@babel/core": "^7.26.5", @@ -15,15 +15,20 @@ "@react-native/babel-preset": "*" }, "devDependencies": { + "@react-native-harness/platform-android": "^1.0.0", + "@react-native-harness/platform-apple": "^1.0.0", + "@react-native-harness/ui": "^1.0.0", "@react-native/eslint-config": "^0.78.0", "@testing-library/react-native": "^13.2.0", "eslint": "^8.19.0", "eslint-plugin-react-native": "^5.0.0", "husky": "^9.1.7", "jest": "^29.7.0", + "pngjs": "^7.0.0", "prettier": "^3.6.2", "react": "*", "react-native": "*", + "react-native-harness": "^1.0.0", "react-native-modal": "*", "react-native-webview": "*", "reassure": "^1.4.0" @@ -49,14 +54,14 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -131,15 +136,15 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", - "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.10", - "@babel/types": "^7.26.10", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -147,12 +152,12 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" @@ -175,17 +180,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz", - "integrity": "sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.26.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.26.9", + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "engines": { @@ -228,14 +233,23 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -272,21 +286,21 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -310,14 +324,14 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", - "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.26.5" + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -327,31 +341,31 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -394,12 +408,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", - "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "license": "MIT", "dependencies": { - "@babel/types": "^7.26.10" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -917,13 +931,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", - "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1933,32 +1947,32 @@ } }, "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz", - "integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" @@ -1994,23 +2008,14 @@ "node": ">=4" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", - "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -2085,6 +2090,29 @@ "react": ">=18.0.0" } }, + "node_modules/@clack/core": { + "version": "1.0.0-alpha.7", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.0-alpha.7.tgz", + "integrity": "sha512-3vdh6Ar09D14rVxJZIm3VQJkU+ZOKKT5I5cC0cOVazy70CNyYYjiwRj9unwalhESndgxx6bGc/m6Hhs4EKF5XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "1.0.0-alpha.9", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.0.0-alpha.9.tgz", + "integrity": "sha512-sKs0UjiHFWvry4SiRfBi5Qnj0C/6AYx8aKkFPZQSuUZXgAram25ZDmhQmP7vj1aFyLpfHWtLQjWvOvcat0TOLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/core": "1.0.0-alpha.7", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.0.tgz", @@ -2431,6 +2459,30 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/reporters": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", @@ -2611,17 +2663,13 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -2633,15 +2681,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/source-map": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", @@ -2660,9 +2699,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2734,6 +2773,472 @@ "node": ">= 8" } }, + "node_modules/@react-native-harness/babel-preset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@react-native-harness/babel-preset/-/babel-preset-1.0.0.tgz", + "integrity": "sha512-YaTLNxEuJitzEea1v/rIabIXPc+BpXBBo1AosMor/vlR5C4PdIcAXRLzyEL9I2lDdPK+qHInG9KWpvohk9uW6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-class-static-block": "^7.27.1", + "babel-plugin-istanbul": "^7.0.1" + }, + "peerDependencies": { + "@babel/core": "^7.22.0", + "@babel/plugin-transform-react-jsx": "*" + } + }, + "node_modules/@react-native-harness/babel-preset/node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@react-native-harness/babel-preset/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@react-native-harness/babel-preset/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@react-native-harness/bridge": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@react-native-harness/bridge/-/bridge-1.0.0.tgz", + "integrity": "sha512-S3tPNumuFO98aJWtVMB01O56EXXhDedYTbMBWBaE6fTn7YftK6KCgGDMDPL1SzsXNQrOIO4ypti6IwP63QWlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-harness/platforms": "1.0.0", + "@react-native-harness/tools": "1.0.0", + "birpc": "^2.4.0", + "pixelmatch": "^7.1.0", + "pngjs": "^7.0.0", + "ssim.js": "^3.5.0", + "tslib": "^2.3.0", + "ws": "^8.18.2" + } + }, + "node_modules/@react-native-harness/bundler-metro": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@react-native-harness/bundler-metro/-/bundler-metro-1.0.0.tgz", + "integrity": "sha512-Aj+hA8MY4zfnrijSOXRU3AL0cjPOX+tV6q/kSrHUw7E07KxSQ7xSSqmmhjtLrjU8OvJaoYu3e2Vprf38E61i1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-harness/config": "1.0.0", + "@react-native-harness/metro": "1.0.0", + "@react-native-harness/tools": "1.0.0", + "connect": "^3.7.0", + "nocache": "^4.0.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "metro": "*", + "metro-config": "*" + } + }, + "node_modules/@react-native-harness/cli": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@react-native-harness/cli/-/cli-1.0.0.tgz", + "integrity": "sha512-EgmYIRGXdREIdKQiaBnoDqFvlUtvnevslsLedjBZawxPmMVFv5ABRdNbYUvSG5f6k6waK70bAyuTjoQ7oCBrew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-harness/bridge": "1.0.0", + "@react-native-harness/config": "1.0.0", + "@react-native-harness/platforms": "1.0.0", + "@react-native-harness/tools": "1.0.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "jest-cli": "*" + } + }, + "node_modules/@react-native-harness/config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@react-native-harness/config/-/config-1.0.0.tgz", + "integrity": "sha512-xLGs/b4pzUW2MjmD0J7SaE1V87CSEXtH6HfPjOmilbgnRFBz/Bg69GajkpMfLRCuH5d31NXnJQFfhX3KjkwmZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-harness/tools": "1.0.0", + "tslib": "^2.3.0", + "zod": "^3.25.67" + } + }, + "node_modules/@react-native-harness/jest": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@react-native-harness/jest/-/jest-1.0.0.tgz", + "integrity": "sha512-AsDguF6RAM//Wygnf4BlX4nzZybzfk7TnXELTwreM4FYgRHPwXLbbQZC9BIfjKabj8cvnxOoGXiGZ9hU9jHcmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^30.2.0", + "@react-native-harness/bridge": "1.0.0", + "@react-native-harness/bundler-metro": "1.0.0", + "@react-native-harness/config": "1.0.0", + "@react-native-harness/platforms": "1.0.0", + "@react-native-harness/tools": "1.0.0", + "chalk": "^4.1.2", + "jest-message-util": "^30.2.0", + "jest-util": "^30.2.0", + "p-limit": "^7.1.1", + "tslib": "^2.3.0", + "yargs": "^17.7.2" + } + }, + "node_modules/@react-native-harness/jest/node_modules/@jest/console": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", + "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@react-native-harness/jest/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@react-native-harness/jest/node_modules/@jest/test-result": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", + "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/types": "30.3.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@react-native-harness/jest/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@react-native-harness/jest/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@react-native-harness/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@react-native-harness/jest/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-native-harness/jest/node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@react-native-harness/jest/node_modules/jest-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@react-native-harness/jest/node_modules/p-limit": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", + "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-harness/jest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@react-native-harness/jest/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@react-native-harness/jest/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native-harness/metro": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@react-native-harness/metro/-/metro-1.0.0.tgz", + "integrity": "sha512-+J3GtdKKOAMtseV9YL0WRzSVbS2Y+wck3n9OoBErH9iBlFev22FtalUamG5KWWy/y4jRfQjWtYK+B91UoZlUGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-harness/babel-preset": "1.0.0", + "@react-native-harness/config": "1.0.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@react-native-harness/runtime": "1.0.0", + "metro": "*" + } + }, + "node_modules/@react-native-harness/platform-android": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@react-native-harness/platform-android/-/platform-android-1.0.0.tgz", + "integrity": "sha512-tIZVdFwtRoPp8XN9drINxbxqmPti6j+ULbf73d/UYTAGdP0AXxzziKIFMWhWYut6jw0EFF0tY9DxlIifgpOLRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-harness/config": "1.0.0", + "@react-native-harness/platforms": "1.0.0", + "@react-native-harness/tools": "1.0.0", + "tslib": "^2.3.0", + "zod": "^3.25.67" + } + }, + "node_modules/@react-native-harness/platform-apple": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@react-native-harness/platform-apple/-/platform-apple-1.0.0.tgz", + "integrity": "sha512-TKfraoHrrgLu2KzU+iJjKvZlU95Gt20P7nhb9GHbs5iGxJ+bzFWsFq4m0LV6BHpspIx8NprHU59vGMQkqjzg1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-harness/platforms": "1.0.0", + "@react-native-harness/tools": "1.0.0", + "tslib": "^2.3.0", + "zod": "^3.25.67" + } + }, + "node_modules/@react-native-harness/platforms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@react-native-harness/platforms/-/platforms-1.0.0.tgz", + "integrity": "sha512-z3V4J/TkhGYL239nOiqFuW+A7bNlINc4EtVrkp2rpHChJf7JXH1chzY5rVuIDLVxo8/MsGrF7AkJRgK1HsVrQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + } + }, + "node_modules/@react-native-harness/runtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@react-native-harness/runtime/-/runtime-1.0.0.tgz", + "integrity": "sha512-3KFkaHoskIl35U/12O3Ut0MwvU/LXNfsnJuKxiJGOXFEuvuCgNqc0Cm+NXNvEvCkh2nXZJjvsRBh8Icv0gt/Fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-harness/bridge": "1.0.0", + "@vitest/expect": "4.0.16", + "@vitest/spy": "4.0.16", + "chai": "^6.2.2", + "event-target-shim": "^6.0.2", + "use-sync-external-store": "^1.6.0", + "zustand": "^5.0.5" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/@react-native-harness/runtime/node_modules/event-target-shim": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz", + "integrity": "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/@react-native-harness/tools": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@react-native-harness/tools/-/tools-1.0.0.tgz", + "integrity": "sha512-xesxIQkTAc69Of2mozXC/wnzhzBQSkOvx7CEidRMD8jok1RfkYuj5JbwWHgBh8vaScixsqP2WDuBF2MiLwOuCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/prompts": "1.0.0-alpha.9", + "is-unicode-supported": "^0.1.0", + "nano-spawn": "^1.0.2", + "picocolors": "^1.1.1", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react-native": "*" + } + }, + "node_modules/@react-native-harness/ui": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@react-native-harness/ui/-/ui-1.0.0.tgz", + "integrity": "sha512-jVrcALCZCuEiw9tIDQsARHac9sxTk7Db6FJmVjhWAeZGhGyBPrJMfpogyEElZ0VeVS1imZ3ryIWU0PN/7OKhfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-harness/runtime": "1.0.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react-native": "*" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.78.0", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.78.0.tgz", @@ -3115,6 +3620,13 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@testing-library/react-native": { "version": "13.2.0", "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-13.2.0.tgz", @@ -3187,6 +3699,24 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -3534,6 +4064,61 @@ "dev": true, "license": "ISC" }, + "node_modules/@vitest/expect": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", + "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", + "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz", + "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz", + "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.16", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -3825,6 +4410,16 @@ "dev": true, "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", @@ -4053,6 +4648,16 @@ ], "license": "MIT" }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4249,6 +4854,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -7084,6 +7699,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -8753,6 +9381,19 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nano-spawn": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.3.tgz", + "integrity": "sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -8776,6 +9417,16 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, + "node_modules/nocache": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nocache/-/nocache-4.0.0.tgz", + "integrity": "sha512-AntnTbmKZvNYIsTVPPwv7dfZdAfo/6H/2ZlZACK66NAOQtIApxkB/6pf/c+s+ACW8vemGJzUCyVTssrzNUK6yQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -9217,6 +9868,19 @@ "node": ">= 6" } }, + "node_modules/pixelmatch": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz", + "integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==", + "dev": true, + "license": "ISC", + "dependencies": { + "pngjs": "^7.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -9230,6 +9894,16 @@ "node": ">=8" } }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -9525,6 +10199,25 @@ "prop-types": "^15.8.1" } }, + "node_modules/react-native-harness": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/react-native-harness/-/react-native-harness-1.0.0.tgz", + "integrity": "sha512-7D0luL7D5yDrV1ehQo9DOIwu8I3Ft1udfvdT5uepFxGBZZpF0c1aLr1zLTVLtg3/YKlJp2/RtJJLwlDcxiaALg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-harness/babel-preset": "1.0.0", + "@react-native-harness/cli": "1.0.0", + "@react-native-harness/jest": "1.0.0", + "@react-native-harness/metro": "1.0.0", + "@react-native-harness/runtime": "1.0.0", + "tslib": "^2.3.0" + }, + "bin": { + "harness": "bin.js", + "react-native-harness": "bin.js" + } + }, "node_modules/react-native-modal": { "version": "14.0.0-rc.0", "resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-14.0.0-rc.0.tgz", @@ -10349,6 +11042,13 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/ssim.js": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ssim.js/-/ssim.js-3.5.0.tgz", + "integrity": "sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g==", + "dev": true, + "license": "MIT" + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -10707,6 +11407,16 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -11062,6 +11772,16 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -11263,6 +11983,28 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -11327,6 +12069,36 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 6b59846..8a0e4c1 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,16 @@ { "name": "@hcaptcha/react-native-hcaptcha", - "version": "2.1.0", + "version": "2.2.0", "description": "hCaptcha Library for React Native (both Android and iOS)", "main": "index.js", "scripts": { "prepare": "husky", "test": "jest --testPathIgnorePatterns=\\.perf-test\\.js$", + "test:e2e:setup": "node __scripts__/setup-e2e-host.js", + "test:e2e:install:android": "adb install -r __e2e__/host/android/app/build/outputs/apk/debug/app-debug.apk", + "test:e2e:install:ios": "xcrun simctl install booted __e2e__/host/ios/build/Build/Products/Debug-iphonesimulator/react_native_hcaptcha_example.app", + "test:e2e:android": "npm run test:e2e:install:android && cd __e2e__/host && npx react-native-harness --harnessRunner android", + "test:e2e:ios": "npm run test:e2e:install:ios && cd __e2e__/host && npx react-native-harness --harnessRunner ios", "lint": "eslint .", "example": "node __scripts__/generate-example.js", "perf:baseline": "reassure --baseline", @@ -16,6 +21,10 @@ "verbose": true, "setupFiles": [ "/__mocks__/global.js" + ], + "testPathIgnorePatterns": [ + "/node_modules/", + "/__e2e__/" ] }, "repository": { @@ -56,6 +65,7 @@ "eslint-plugin-react-native": "^5.0.0", "husky": "^9.1.7", "jest": "^29.7.0", + "pngjs": "^7.0.0", "prettier": "^3.6.2", "react": "*", "react-native": "*",