diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b20835f..e6e50ea 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -9,11 +9,6 @@ on: schedule: - cron: '0 0 * * 0' # https://crontab.guru/every-week -env: - TEST_APP_DEPS: react-native-modal react-native-webview - TEST_APP_DEV_DEPS: typescript @babel/preset-env react-shallow-renderer - RN_BUNDLE_ARGS: --entry-file index.js --platform android --dev false --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res - jobs: build: runs-on: ubuntu-latest @@ -21,11 +16,29 @@ jobs: - uses: actions/checkout@v4 - run: npm install - run: npm test - test-yarn: + + test: needs: build - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} outputs: rn-version: ${{ steps.rn-version.outputs.value }} + concurrency: + group: '${{ github.workflow }}-${{ matrix.platform }}-${{ matrix.pm }}-${{ github.head_ref || github.ref_name }}' + cancel-in-progress: true + strategy: + fail-fast: false + max-parallel: 5 + matrix: + os: [ ubuntu-latest ] + platform: [ android ] + pm: [ npm, yarn ] + include: + - os: macos-latest + platform: ios + pm: npm + - os: macos-latest + platform: ios + pm: yarn steps: - uses: actions/checkout@v4 with: @@ -34,85 +47,32 @@ jobs: with: java-version: 17 distribution: adopt - - run: npx react-native init rnexample + - run: | + # npm install -g ${{ matrix.pm }} react-native + npm run example -- --pm ${{ matrix.pm }} + working-directory: react-native-hcaptcha - id: rn-version - working-directory: rnexample + working-directory: react-native-hcaptcha-example run: | RN_VERSION=$(cat package.json | jq ".dependencies.\"react-native\"" -r) echo "value=${RN_VERSION}" >> $GITHUB_OUTPUT - - name: Run yarn add ... - working-directory: rnexample - run: | - yarn add @hcaptcha/react-native-hcaptcha@file:../react-native-hcaptcha - yarn add --dev ${{ env.TEST_APP_DEV_DEPS }} - yarn add ${{ env.TEST_APP_DEPS }} - cp ../react-native-hcaptcha/Example.App.js App.js - cp ../react-native-hcaptcha/Example.jest.config.js jest.config.js - - run: | - mkdir -p android/app/src/main/assets - mkdir -p android/app/src/main/res - yarn react-native bundle ${{ env.RN_BUNDLE_ARGS }} - working-directory: rnexample - run: cat package.json - working-directory: rnexample + working-directory: react-native-hcaptcha-example - run: yarn test --config ./jest.config.js - working-directory: rnexample + working-directory: react-native-hcaptcha-example + - run: npx react-native build-${{ matrix.platform }} + working-directory: react-native-hcaptcha-example - run: npx --yes check-peer-dependencies --yarn --runOnlyOnRootDependencies - working-directory: rnexample - test-npm: - needs: build - runs-on: ubuntu-latest - outputs: - rn-version: ${{ steps.rn-version.outputs.value }} - steps: - - uses: actions/setup-java@v4 - with: - java-version: 17 - distribution: adopt - - run: npx react-native init rnexample - - id: rn-version - working-directory: rnexample - run: | - RN_VERSION=$(cat package.json | jq ".dependencies.\"react-native\"" -r) - echo "value=${RN_VERSION}" >> $GITHUB_OUTPUT - - uses: actions/checkout@v4 - with: - path: rnexample/react-native-hcaptcha - - name: Run npm install ... - working-directory: rnexample - run: | - npm i --save file:./react-native-hcaptcha - npm i --save --include=dev ${{ env.TEST_APP_DEV_DEPS }} - npm i --save ${{ env.TEST_APP_DEPS }} - cp ./react-native-hcaptcha/Example.App.js App.js - cp ./react-native-hcaptcha/Example.jest.config.js jest.config.js - - run: | - mkdir -p android/app/src/main/assets - mkdir -p android/app/src/main/res - npx react-native bundle ${{ env.RN_BUNDLE_ARGS }} - working-directory: rnexample - - run: cat package.json - working-directory: rnexample - - run: npm run test -- --config ./jest.config.js - working-directory: rnexample - - run: npx --yes check-peer-dependencies --npm --runOnlyOnRootDependencies - working-directory: rnexample - - run: | - echo "org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m" >> gradle.properties - ./gradlew assemble - working-directory: rnexample/android - - uses: actions/upload-artifact@v4 - with: - name: apks - path: rnexample/android/app/build/outputs/apk/release/app-release.apk + working-directory: react-native-hcaptcha-example + create-an-issue: runs-on: ubuntu-latest - needs: [test-npm, test-yarn] - if: always() && github.event == 'schedule' && (needs.test-npm.result == 'failure' || needs.test-yarn.result == 'failure') + needs: test + if: always() && github.event == 'schedule' && needs.test.result == 'failure' steps: - uses: actions/checkout@v4 - run: | - RN_VERSION="${{ needs.test-npm.outputs.rn-version || needs.test-yarn.outputs.rn-version }}" + RN_VERSION="${{ needs.test.outputs.rn-version }}" GHA_RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" echo "RN_VERSION=${RN_VERSION}" >> $GITHUB_ENV echo "GHA_RUN_URL=${GHA_RUN_URL}" >> $GITHUB_ENV @@ -123,9 +83,10 @@ jobs: assignees: CAMOBAP update_existing: true filename: .github/examples-issue-template.md + release: if: github.event_name == 'release' - needs: [test-npm, test-yarn] + needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/Example.jest.config.js b/Example.jest.config.js index eb12417..364abb8 100644 --- a/Example.jest.config.js +++ b/Example.jest.config.js @@ -12,4 +12,5 @@ module.exports = { + "|react-native-animatable" + ")/)", ], + setupFiles: ['./jest.setup.js'], } diff --git a/Example.jest.setup.js b/Example.jest.setup.js new file mode 100644 index 0000000..4290416 --- /dev/null +++ b/Example.jest.setup.js @@ -0,0 +1,6 @@ +jest.mock('react-native-webview', () => { + return { + WebView: () => 'WebView', + }; +}); + diff --git a/MAINTAINER.md b/MAINTAINER.md index a941f09..b3168b6 100644 --- a/MAINTAINER.md +++ b/MAINTAINER.md @@ -23,33 +23,20 @@ PATCH: bugfix only. For `expo` test app -- `cd ..` -- `expo init expo-example -t blank` -- `cd expo-example` -- `yarn add file:../react-native-hcaptcha` -- `yarn add react-native-modal react-native-webview` -- `cp ../react-native-hcaptcha/Example.App.js App.js` -- `yarn android` +- `cd react-native-hcaptcha` +- `yarn example --expo +- `yarn android` or `npm run android` For `react-native` test app -- `cd ..` -- `react-native init rnexample` or `react-native init rnexample --version 0.63.4` for specific version -- `cd rnexample` -- `yarn add file:../react-native-hcaptcha` -- `yarn add react-native-modal react-native-webview` -- `cp ../react-native-hcaptcha/Example.App.js App.js` -- `yarn android` +- `cd react-native-hcaptcha` +- `yarn example` +- `yarn android` or `npm run android` For iOS instead the last step do: -- `pushd ios; pod install; popd` -- `yarn ios` - -To quickly update `react-native-hcaptcha` locally just run: - -`yarn upgrade file:../react-native-hcaptcha` - +- `pushd ios; env USE_HERMES=0 pod install; popd` +- `yarn ios` or `npm run ios` ### Known issues @@ -145,4 +132,27 @@ ERROR Invariant Violation: No callback found with cbID xxxxx and callID yyyyy Solution: delete `node_modules` in `react-native-hcaptcha`. -This issue is related to mismatched `react-native` versions in the test app vs. `react-native-hcaptcha` \ No newline at end of file +This issue is related to mismatched `react-native` versions in the test app vs. `react-native-hcaptcha` + +--- + +Problem: + +Xcode failed to build with: + +> ... +> Framework 'hermes' not found + +Solution: `env USE_HERMES=0 pod install` or add `:hermes_enabled => false` into `use_react_native!` call in `ios/Podfile` + +--- + +Problem: +``` +> yarn add file:../react-native-hcaptcha +Usage Error: The file:../react-native-hcaptcha string didn't match the required format (package-name@range). Did you perhaps forget to explicitly reference the package name? +``` + +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 diff --git a/__scripts__/generate-example.js b/__scripts__/generate-example.js new file mode 100644 index 0000000..7f4cb17 --- /dev/null +++ b/__scripts__/generate-example.js @@ -0,0 +1,187 @@ +#!/usr/bin/env node + +const { execSync, spawnSync } = require('child_process'); +const { platform } = require('os'); +const fs = require('fs'); +const path = require('path'); + +// Function to display help message +function showHelp() { + console.log(` +Usage: ${path.basename(process.argv[1])} [options] + +Options: + --expo Use expo-cli (default: false) + --template Specify the project template + --name Specify the project name (required) + --path Specify the project path (default: ../) + --pm Specify the package manager to use (default: yarn) + --verbose Enable verbose logging + -h, --help Show this help message +`); + process.exit(1); +} + +// Simple argument parser function +function parseArgs(args) { + const options = { + cliName: 'react-native', + projectName: 'react_native_hcaptcha_example', + projectRelativeProjectPath: '../react-native-hcaptcha-example', + packageManager: 'yarn', + verbose: false, + projectTemplate: undefined, + frameworkVersion: undefined, + }; + + const argHandlers = { + '--expo': () => options.cliName = 'expo', + '--template': (value) => options.projectTemplate = value, + '--name': (value) => { + options.projectName = value.replace(/[^a-zA-Z0-9]/g, '_'); + options.projectRelativeProjectPath = path.join('..', value); + }, + '--path': (value) => { + options.projectRelativeProjectPath = value; + }, + '--pm': (value) => options.packageManager = value, + '--verbose': () => options.verbose = true, + '-h': showHelp, + '--help': showHelp + }; + + for (let i = 2; i < args.length; i++) { + const arg = args[i]; + + if (argHandlers[arg]) { + const handler = argHandlers[arg]; + if (typeof handler === 'function') { + const nextArg = args[i + 1]; + // Check if the next argument is not another flag + if (typeof handler === 'function' && (nextArg && !nextArg.startsWith('--'))) { + handler(nextArg); + i++; // Skip next argument as it is consumed + } else if (handler === showHelp) { + handler(); + } else { + console.error(`Error: ${arg} requires a value.`); + showHelp(); + } + } + } else { + console.error(`Unknown option: ${arg}`); + showHelp(); + } + } + + if (!options.projectName) { + console.error('Error: --name is required.'); + showHelp(); + } + + return options; +} + +function cleanPathEnv() { + return process.env.PATH.split(':') + .filter(dir => !dir.includes('node_modules/.bin')) + .join(':'); +} + +// Function to build the create command +function buildCreateCommand({ cliName, projectRelativeProjectPath, projectName, projectTemplate, packageManager, frameworkVersion, verbose }) { + let createCommand = ['npx', cliName, 'init', projectName, '--directory', projectRelativeProjectPath]; + + if (projectTemplate) { + createCommand.push('--template', projectTemplate); + } + + if (cliName === 'expo') { + createCommand.push(`--${packageManager}`); + } else if (cliName.includes('react-native')) { + createCommand.push('--pm', packageManager); + + if (frameworkVersion) { + createCommand.push('--version', frameworkVersion); + } + } else { + console.error('Error: unsupporte cliName'); + showHelp(); + } + + if (verbose) { + createCommand.push('--verbose'); + } + + return createCommand; +} + +function checkHcaptchaLinked() { + return false; // https://stackoverflow.com/a/47403470/902217 +} + +// Main function that takes parsed arguments and runs the necessary setup +function main({ cliName, projectRelativeProjectPath, projectName, projectTemplate, packageManager, frameworkVersion, verbose }) { + console.warn(`Warning! Example project will be generated in '${path.dirname(process.cwd())}'`); + + // Build the command to initialize the project + const createCommand = buildCreateCommand({ cliName, projectRelativeProjectPath, projectName, projectTemplate, packageManager, frameworkVersion }); + + // Run the project initialization command + console.log(`Running command: ${createCommand}`); + execSync(createCommand.join(' '), { stdio: 'inherit', shell: true, env: { ...process.env, PATH: cleanPathEnv(), USE_HERMES: 0 } }); + + const projectPath = path.join(process.cwd(), projectRelativeProjectPath); + const packageManagerOptions = { stdio: 'inherit', cwd: projectPath }; + + // Copy App.js + fs.unlinkSync(path.join(projectPath, 'App.tsx')) + fs.copyFileSync('Example.App.js', path.join(projectPath, 'App.js')); + fs.copyFileSync('Example.jest.config.js', path.join(projectPath, 'jest.config.js')); + fs.copyFileSync('Example.jest.setup.js', path.join(projectPath, 'jest.setup.js')); + + // Install dependencies + const isHcaptchaLinked = checkHcaptchaLinked(); + const mainPackage = '@hcaptcha/react-native-hcaptcha' + const mainPackagePath = `${path.dirname(projectRelativeProjectPath)}/react-native-hcaptcha` + const peerPackages = 'react-native-modal react-native-webview'; + const devPackages = 'typescript @babel/preset-env react-shallow-renderer'; + + console.warn('Installing dependencies...'); + if (packageManager === 'yarn') { + execSync(`yarn add @hcaptcha/react-native-hcaptcha@file:${mainPackagePath}`, 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}`); + execSync(`npm i --save file:./react-native-hcaptcha`, packageManagerOptions); + execSync(`npm i --save --dev ${devPackages}`, packageManagerOptions); + execSync(`npm i --save ${peerPackages}`, packageManagerOptions); + } + + if (isHcaptchaLinked) { + execSync(`${packageManager} link ${mainPackage}`, packageManagerOptions); + } + + // iOS: pod install + if (platform() === 'darwin') { + const podOptions = { stdio: 'inherit', cwd: path.join(projectPath, 'ios'), env: { ...process.env, USE_HERMES: 0 } }; + 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); + + ['assets', 'res'].map(dir => fs.mkdirSync(path.join(projectPath, 'android/app/src/main', dir), { recursive: true })); + + console.log('Setup complete.'); +} + +main(parseArgs(process.argv)); \ No newline at end of file diff --git a/package.json b/package.json index 2b9a7ff..b6e59d4 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "hCaptcha Library for React Native (both Android and iOS)", "main": "index.js", "scripts": { - "test": "jest" + "test": "jest", + "example": "node __scripts__/generate-example.js" }, "jest": { "preset": "react-native", @@ -60,4 +61,4 @@ "babel-jest": "^27.2.2", "metro-react-native-babel-preset": "^0.77.0" } -} +} \ No newline at end of file