diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 88edb628..00000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules/ -lib/ diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 4c21c44d..00000000 --- a/.eslintrc +++ /dev/null @@ -1,74 +0,0 @@ -{ - "root": true, - "extends": [ - "universe/native", - "universe/node", - "universe/web", - "plugin:react-hooks/recommended" - ], - "plugins": ["unused-imports", "react-native"], - "parserOptions": { - "project": "./tsconfig.json" - }, - "rules": { - // Forbid cyclical dependencies between modules - "import/no-cycle": "error", - // Automatically sort imports for consistency - "import/order": [ - "warn", - { - "groups": [ - "builtin", - "external", - ["parent", "sibling", "index"], - "object" - ], - "pathGroups": [ - { - "group": "object", - "pattern": "*.@(jpg|png|svg)", - "patternOptions": { "matchBase": true } - } - ], - "pathGroupsExcludedImportTypes": [], - "newlines-between": "always", - "alphabetize": { "order": "asc", "caseInsensitive": true } - } - ], - // Makes sure that we are not deploying console.log's to production - "no-console": "warn", - // Remove any unused imports - "unused-imports/no-unused-imports": "warn", - // Error if an element that likely requires a key - "react/jsx-key": ["error", { "checkFragmentShorthand": true }], - "react-native/no-unused-styles": 2, - // The one of the most common app crash issue are white spaces outside of Text elements - "no-irregular-whitespace": "error", - "no-nested-ternary": "warn", - // Ignore no-unused-vars on vars prepended with _ - "@typescript-eslint/no-unused-vars": [ - "warn", - { - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_", - "caughtErrorsIgnorePattern": "^_" - } - ], - "no-else-return": "warn" - }, - "overrides": [ - { - "files": ["**/*.*(ts|tsx)"], - "rules": { - "@typescript-eslint/no-unnecessary-condition": "warn", - "@typescript-eslint/dot-notation": "error", - "@typescript-eslint/await-thenable": "error" - } - }, - { - "files": ["**/*.*(spec|test).*(ts|tsx)"], - "plugins": ["jest"], - "extends": ["plugin:jest/recommended", "plugin:jest/style"] - } - ] -} diff --git a/.github/renovate.json b/.github/renovate.json index 6cd98774..156aa33d 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,10 +1,15 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:base", ":dependencyDashboard", ":semanticCommits"], + "extends": [ + "config:base", + ":dependencyDashboard", + ":semanticCommits", + "helpers:pinGitHubActionDigests" + ], "schedule": "every weekend", "timezone": "Europe/Lisbon", "labels": ["dependencies"], - "postUpdateOptions": ["yarnDedupeHighest"], + "postUpdateOptions": ["pnpmDedupe"], "lockFileMaintenance": { "enabled": false }, diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 66ddd721..c7f8317d 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -17,34 +17,33 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v7 - - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Setup mise + uses: jdx/mise-action@b44d12e7d0d8ee7461bb893e1b1993500433185b # v5 + - name: Setup pnpm + uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6 with: - node-version-file: ./.tool-versions + cache: true - name: Setup JDK - uses: actions/setup-java@v6 + uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6 with: distribution: temurin java-version: "17" - name: Setup Android SDK - uses: android-actions/setup-android@v4 + uses: android-actions/setup-android@be39fa834029ff78f1a44aa3bb0819b8fc2bd8fd # v4 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6 + uses: gradle/actions/setup-gradle@8da46b539cd8fe6ab27ca75128919a18d757604c # v6 - name: Install dependencies - run: yarn install --frozen-lockfile - - name: Install example dependencies - run: yarn install --frozen-lockfile - working-directory: example + run: pnpm install --frozen-lockfile - name: Prebuild example app - run: npx expo prebuild --platform android --no-install + run: pnpm exec expo prebuild --platform android --no-install working-directory: example # Compiling proves the module builds, not that the app loads it: a # podspec Expo could not find still compiled here, but was never # registered, so requireNativeModule would have thrown at runtime. - name: Check the module is autolinked run: | - npx expo-modules-autolinking resolve --platform android --json \ + pnpm exec expo-modules-autolinking resolve --platform android --json \ | node -e 'const m = JSON.parse(require("fs").readFileSync(0, "utf8")).modules; if (!m.some((x) => x.packageName === "@bounceapp/react-native-paypal")) { console.error("@bounceapp/react-native-paypal is not autolinked for android"); @@ -62,15 +61,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Setup JDK - uses: actions/setup-java@v6 + uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6 with: distribution: temurin java-version: "17" # Keyed on the script, which pins the detekt version and its checksum. - name: Cache detekt - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ~/.cache/react-native-paypal key: detekt-${{ hashFiles('scripts/detekt.sh') }} diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 54054816..c7bd0e80 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -9,22 +9,18 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v7 - - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Setup mise + uses: jdx/mise-action@b44d12e7d0d8ee7461bb893e1b1993500433185b # v5 + - name: Setup pnpm + uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6 with: - node-version-file: ./.tool-versions - - name: Setup Yarn - uses: actions/cache@v6 - with: - path: node_modules - key: yarn-cache-${{ hashFiles('**/yarn.lock', '.yarnrc') }} - restore-keys: yarn-cache- - - run: yarn install --frozen-lockfile + cache: true + - run: pnpm install --frozen-lockfile - name: Build docs - run: yarn docs + run: pnpm run docs - name: Deploy - uses: peaceiris/actions-gh-pages@v4 + uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./docs diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 6361d7a8..c87d00b1 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -19,25 +19,24 @@ jobs: runs-on: macos-latest steps: - name: Checkout - uses: actions/checkout@v7 - - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Setup mise + uses: jdx/mise-action@b44d12e7d0d8ee7461bb893e1b1993500433185b # v5 + - name: Setup pnpm + uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6 with: - node-version-file: ./.tool-versions + cache: true - name: Install dependencies - run: yarn install --frozen-lockfile - - name: Install example dependencies - run: yarn install --frozen-lockfile - working-directory: example + run: pnpm install --frozen-lockfile - name: Prebuild example app - run: npx expo prebuild --platform ios --no-install + run: pnpm exec expo prebuild --platform ios --no-install working-directory: example # Compiling proves the module builds, not that the app loads it: a # podspec Expo could not find still compiled here, but was never # registered, so requireNativeModule would have thrown at runtime. - name: Check the module is autolinked run: | - npx expo-modules-autolinking resolve --platform apple --json \ + pnpm exec expo-modules-autolinking resolve --platform apple --json \ | node -e 'const m = JSON.parse(require("fs").readFileSync(0, "utf8")).modules; if (!m.some((x) => x.packageName === "@bounceapp/react-native-paypal")) { console.error("@bounceapp/react-native-paypal is not autolinked for apple"); diff --git a/.github/workflows/quality-assurance.yml b/.github/workflows/quality-assurance.yml index 98a4d961..4f3786c6 100644 --- a/.github/workflows/quality-assurance.yml +++ b/.github/workflows/quality-assurance.yml @@ -15,25 +15,21 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: # commitlint walks the range between the PR base and head, so the # full history has to be present -- a shallow clone breaks it. fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v7 + - name: Setup mise + uses: jdx/mise-action@b44d12e7d0d8ee7461bb893e1b1993500433185b # v5 + - name: Setup pnpm + uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6 with: - node-version-file: ./.tool-versions - - name: Setup Yarn - uses: actions/cache@v6 - with: - path: node_modules - key: yarn-cache-${{ hashFiles('**/yarn.lock', '.yarnrc') }} - restore-keys: yarn-cache- - - run: yarn install --frozen-lockfile + cache: true + - run: pnpm install --frozen-lockfile - name: Validate all commits if: github.event_name == 'pull_request' - run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose - - run: yarn typecheck - - run: yarn test - - run: yarn lint + run: pnpm exec commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose + - run: pnpm typecheck + - run: pnpm test + - run: pnpm lint diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index 50e3bac7..cf0a5bf9 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -20,16 +20,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v7 - - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Setup mise + uses: jdx/mise-action@b44d12e7d0d8ee7461bb893e1b1993500433185b # v5 + - name: Setup pnpm + uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6 with: - node-version-file: ./.tool-versions + cache: true - name: Install dependencies - run: yarn install --frozen-lockfile - - name: Install example dependencies - run: yarn install --frozen-lockfile - working-directory: example + run: pnpm install --frozen-lockfile - name: Export web bundle - run: npx expo export --platform web --output-dir ../.web-build + run: pnpm exec expo export --platform web --output-dir ../.web-build working-directory: example diff --git a/.gitignore b/.gitignore index 3ea7e2f1..700476be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -.eslintcache # OSX # @@ -55,8 +54,6 @@ example/vendor/ # node_modules/ npm-debug.log -yarn-debug.log -yarn-error.log # BUCK buck-out/ diff --git a/.husky/.gitignore b/.husky/.gitignore deleted file mode 100644 index 31354ec1..00000000 --- a/.husky/.gitignore +++ /dev/null @@ -1 +0,0 @@ -_ diff --git a/.husky/commit-msg b/.husky/commit-msg deleted file mode 100755 index 3142333f..00000000 --- a/.husky/commit-msg +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npx commitlint --edit diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index 7acbe845..00000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -yarn lint-staged -yarn typecheck -yarn test diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 9d951f55..00000000 --- a/.npmignore +++ /dev/null @@ -1,26 +0,0 @@ -example/ -e2e/ -docs/ -scripts/ -.tmp/ -.github/ -**/__tests__ -**/__fixtures__ -**/__mocks__ -.eslintcache - -android/src/androidTest/ -android/src/test/ -android/build/ - -wdio.conf.js -yarn-error.log -wdio.ios.js -wdio.android.js -run-appium-tests.js -CODEOWNERS -CONTRIBUTING.md -appium.log -babel.config.js -tsconfig.json -yarn.lock diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 00000000..6bb16d2b --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,32 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 80, + "semi": false, + "singleQuote": false, + "trailingComma": "all", + "arrowParens": "avoid", + "bracketSameLine": true, + "sortPackageJson": false, + "sortImports": { + "groups": ["builtin", "external", ["parent", "sibling", "index"]], + "newlinesBetween": true, + "order": "asc", + "ignoreCase": true + }, + "ignorePatterns": [ + "node_modules", + "lib", + "docs", + "example/android", + "example/ios", + "pnpm-lock.yaml" + ], + "overrides": [ + { + "files": ["**/*.md"], + "options": { + "sortImports": false + } + } + ] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 00000000..ace55bb6 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,231 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "import", "react", "node"], + "categories": { + "correctness": "off" + }, + "options": { + "typeAware": true + }, + "env": { + "browser": true, + "commonjs": true, + "es2022": true, + "jest": true, + "node": true + }, + "globals": { + "__DEV__": "readonly" + }, + "ignorePatterns": [ + "node_modules", + "lib", + "docs", + "example/android", + "example/ios" + ], + "rules": { + "import/no-cycle": "error", + "no-console": "warn", + "react/jsx-key": [ + "error", + { + "checkFragmentShorthand": true, + "checkKeyMustBeforeSpread": false, + "warnOnDuplicates": false + } + ], + "no-irregular-whitespace": "error", + "no-nested-ternary": "warn", + "no-else-return": "warn", + "react/rules-of-hooks": "error", + "react/exhaustive-deps": "warn", + "react/jsx-boolean-value": ["warn", "never"], + "react/jsx-curly-brace-presence": ["warn", "never"], + "react/jsx-fragments": ["warn", "syntax"], + "react/jsx-no-duplicate-props": "error", + "react/jsx-no-undef": "error", + "react/no-did-mount-set-state": "warn", + "react/no-did-update-set-state": "warn", + "react/no-direct-mutation-state": "warn", + "react/no-redundant-should-component-update": "warn", + "react/no-this-in-sfc": "warn", + "react/no-unknown-property": "warn", + "react/no-will-update-set-state": "warn", + "react/require-render-return": "warn", + "react/self-closing-comp": "warn", + "constructor-super": "warn", + "eqeqeq": ["warn", "smart"], + "getter-return": "warn", + "no-array-constructor": "warn", + "no-caller": "warn", + "no-case-declarations": "warn", + "no-compare-neg-zero": "warn", + "no-cond-assign": "warn", + "no-const-assign": "error", + "no-constant-condition": [ + "warn", + { + "checkLoops": false + } + ], + "no-debugger": "warn", + "no-delete-var": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-empty-character-class": "warn", + "no-empty-pattern": "warn", + "no-eval": "warn", + "no-ex-assign": "warn", + "no-extend-native": "warn", + "no-extra-bind": "warn", + "no-extra-boolean-cast": "warn", + "no-fallthrough": "warn", + "no-func-assign": "error", + "no-global-assign": "warn", + "no-implied-eval": "warn", + "no-inner-declarations": "warn", + "no-invalid-regexp": "error", + "no-iterator": "warn", + "no-label-var": "warn", + "no-labels": [ + "warn", + { + "allowLoop": true, + "allowSwitch": true + } + ], + "no-lone-blocks": "warn", + "no-multi-assign": "warn", + "no-new": "warn", + "no-new-func": "warn", + "no-object-constructor": "warn", + "no-new-native-nonconstructor": "error", + "no-obj-calls": "warn", + "no-proto": "warn", + "no-return-assign": "warn", + "no-script-url": "warn", + "no-self-assign": "warn", + "no-self-compare": "warn", + "no-sequences": "warn", + "no-shadow-restricted-names": "warn", + "no-sparse-arrays": "warn", + "no-this-before-super": "warn", + "no-throw-literal": "warn", + "no-unneeded-ternary": "warn", + "no-unreachable": "warn", + "no-unsafe-negation": "warn", + "no-unused-labels": "warn", + "no-useless-computed-key": "warn", + "no-useless-concat": "warn", + "no-useless-escape": "warn", + "no-useless-rename": "warn", + "no-useless-return": "warn", + "no-var": "warn", + "no-void": "warn", + "no-with": "warn", + "object-shorthand": "warn", + "prefer-const": [ + "warn", + { + "destructuring": "all", + "ignoreReadBeforeAssign": true + } + ], + "prefer-promise-reject-errors": "warn", + "prefer-rest-params": "warn", + "prefer-spread": "warn", + "radix": "warn", + "unicode-bom": ["warn", "never"], + "use-isnan": "error", + "valid-typeof": "error", + "yoda": [ + "warn", + "never", + { + "exceptRange": true, + "onlyEquality": false + } + ], + "import/export": "error", + "import/first": "warn", + "import/namespace": [ + "error", + { + "allowComputed": true + } + ], + "import/no-duplicates": "error", + "node/handle-callback-err": ["warn", "^(e|err|error|.+Error)$"], + "node/no-new-require": "warn", + "node/no-path-concat": "warn", + "no-redeclare": "warn", + "no-unused-expressions": [ + "warn", + { + "allowShortCircuit": true, + "enforceForJSX": true, + "allowTernary": false, + "allowTaggedTemplates": false + } + ], + "no-useless-constructor": "warn" + }, + "overrides": [ + { + "files": ["**/*.ts", "**/*.tsx"], + "rules": { + "typescript/no-unnecessary-condition": "warn", + "typescript/dot-notation": "error", + "typescript/await-thenable": "error", + "no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ], + "typescript/array-type": [ + "warn", + { + "default": "array" + } + ], + "typescript/no-empty-object-type": [ + "warn", + { + "allowInterfaces": "with-single-extends" + } + ], + "typescript/no-wrapper-object-types": "warn", + "typescript/consistent-type-assertions": [ + "warn", + { + "assertionStyle": "as", + "objectLiteralTypeAssertions": "allow" + } + ], + "typescript/no-extra-non-null-assertion": "warn", + "no-dupe-class-members": "warn" + } + }, + { + "files": ["**/*.js"], + "rules": { + "no-dupe-class-members": "error", + "no-undef": "error", + "no-unused-vars": [ + "warn", + { + "vars": "all", + "args": "none", + "ignoreRestSiblings": true, + "caughtErrors": "all", + "caughtErrorsIgnorePattern": "^_" + } + ] + } + } + ] +} diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index faf9daca..00000000 --- a/.prettierignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules -lib - -# Generated by `expo prebuild` when running the native build workflows locally. -example/android -example/ios diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 56698334..00000000 --- a/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "arrowParens": "avoid", - "bracketSameLine": true, - "semi": false, - "trailingComma": "all" -} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 5be01f64..ab3c9c41 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,3 @@ { - "recommendations": [ - "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode", - "streetsidesoftware.code-spell-checker" - ] + "recommendations": ["oxc.oxc-vscode", "streetsidesoftware.code-spell-checker"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index b216ec55..255a9163 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,32 +1,32 @@ { "[json]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "oxc.oxc-vscode" }, "[jsonc]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "oxc.oxc-vscode" }, "[markdown]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "oxc.oxc-vscode" }, "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.defaultFormatter": "oxc.oxc-vscode", "editor.formatOnSave": true }, "[javascriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.defaultFormatter": "oxc.oxc-vscode", "editor.formatOnSave": true }, "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.defaultFormatter": "oxc.oxc-vscode", "editor.formatOnSave": true }, "[typescriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.defaultFormatter": "oxc.oxc-vscode", "editor.formatOnSave": true }, "editor.codeActionsOnSave": { "source.organizeImports": "explicit", - "source.fixAll.eslint": "explicit" + "source.fixAll.oxc": "explicit" }, "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/.yarnrc b/.yarnrc deleted file mode 100644 index fedc0f11..00000000 --- a/.yarnrc +++ /dev/null @@ -1,3 +0,0 @@ -# Override Yarn command so we can automatically setup the repo on running `yarn` - -yarn-path "scripts/bootstrap.js" diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..a7abdcf8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,30 @@ +# CLAUDE.md + +`@bounceapp/react-native-paypal`: an Expo module that bridges the PayPal (Braintree) iOS and Android SDKs. A Swift module (`ios/`), a Kotlin module (`android/`), a TypeScript wrapper (`src/`) and an Expo config plugin (`plugin/`), with an Expo example app in `example/`, a pnpm workspace package. + +## Commands (pnpm only) + +Node comes from `.tool-versions` (`mise install`); run `corepack enable` once so `pnpm` is the pinned version. + +- `pnpm install`: install dependencies and build `lib/`. +- `pnpm typecheck`: `tsc`. +- `pnpm lint`: oxlint (type-aware) and `oxfmt --check`. `pnpm lint:fix` autofixes and formats. +- `pnpm lint:kotlin`: detekt on `android/src`. Needs a JDK on the path or `JAVA_HOME`. +- `pnpm test`: Jest (`--passWithNoTests`). One file: `pnpm test plugin/__tests__/withPaypal.test.js`; one test: `pnpm test -t ""`. +- `pnpm run docs`: typedoc into `docs/`. Spell out `run`: `pnpm docs` is a built-in pnpm command. +- `pnpm example start|ios|android|web`: run the example app (`start` is `expo start --dev-client`; `ios` and `android` are `expo run:ios` and `expo run:android`). + +## Testing changes + +- Jest covers only the config plugin (`plugin/__tests__/withPaypal.test.js`). The Swift and Kotlin code has no automated tests. +- The example app is the check for iOS and Android. It needs a development build, because the native module is not in Expo Go: `pnpm example ios` or `pnpm example android` builds and installs one, and `pnpm example start` serves JS to it. Metro bundles the library from `src/`, so edits there reload without a rebuild; native changes need a new `ios` or `android` run. +- PayPal does not work on web. `pnpm example web` only checks that importing the package does not break a web bundle (see the comment in `.github/workflows/web.yml`). + +## Notes + +- `src/Paypal.ts` is the native implementation, `src/Paypal.web.ts` is a web stub, and `src/types.ts` holds the shared types. `src/index.ts`, `src/types.ts`, `plugin/` and `app.plugin.js` are public API: do not change them without discussing it. +- `lib/` is build output (`bob build`) and `docs/` is typedoc output. Both are git-ignored; do not edit them by hand. `example/ios` and `example/android` are generated by `expo run:*` and git-ignored too. +- Leave `LICENSE` untouched. +- The Braintree SDK versions live in `react-native-paypal.podspec` (iOS) and `android/build.gradle` (Android). +- Git hooks come from `lefthook.yml`: pre-commit runs oxlint and oxfmt on staged files, then `tsc` and Jest; commit-msg runs commitlint. +- Commits follow Conventional Commits (`@commitlint/config-conventional`). Allowed types: `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `style`, `test`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 145bd083..bd2e1724 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,56 +4,66 @@ We want this community to be friendly and respectful to each other. Please follo ## Development workflow -To get started with the project, run `yarn` in the root directory to install the required dependencies for each package: +To get started with the project, install the tools the repo pins, then the dependencies: ```sh -yarn +mise install +corepack enable +pnpm install ``` -> While it's possible to use [`npm`](https://github.com/npm/cli), the tooling is built around [`yarn`](https://classic.yarnpkg.com/), so you'll have an easier time if you use `yarn` for development. +[`mise`](https://mise.jdx.dev/) installs the Node version from `.tool-versions`, and `corepack enable` makes `pnpm` resolve to the version in `package.json`. `pnpm install` installs the dependencies of the library and of the example app (a pnpm workspace package), builds `lib/` and installs the git hooks. -While developing, you can run the [example app](/example/) to test your changes. Any changes you make in your library's JavaScript code will be reflected in the example app without a rebuild. If you change any native code, then you'll need to rebuild the example app. +While developing, you can run the [example app](/example/) to test your changes. It needs a development build, because the library has native code that Expo Go does not include: `pnpm example ios` and `pnpm example android` build one and install it on a simulator or emulator. Any changes you make in your library's JavaScript code are reflected in the example app without a rebuild, because Metro bundles the library from `src/`. If you change any native code, then you'll need to rebuild the example app. -To start the packager: +To start the packager for a development build that is already installed: ```sh -yarn example start +pnpm example start ``` To run the example app on Android: ```sh -yarn example android +pnpm example android ``` To run the example app on iOS: ```sh -yarn example ios +pnpm example ios ``` -Make sure your code passes TypeScript and ESLint. Run the following to verify: +PayPal does not work on web, so `pnpm example web` is only useful to check that importing the library does not break a web bundle. + +Make sure your code passes TypeScript, [oxlint](https://oxc.rs/docs/guide/usage/linter) and [oxfmt](https://oxc.rs/docs/guide/usage/formatter). Run the following to verify: + +```sh +pnpm typecheck +pnpm lint +``` + +To fix lint and formatting errors, run the following: ```sh -yarn typecheck -yarn lint +pnpm lint:fix ``` -To fix formatting errors, run the following: +To lint the Kotlin files with [detekt](https://detekt.dev/), run the following (it needs a JDK): ```sh -yarn lint --fix +pnpm lint:kotlin ``` Remember to add tests for your change if possible. Run the unit tests by: ```sh -yarn test +pnpm test ``` -To edit the Objective-C files, open `example/ios/PaypalExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > react-native-paypal`. +To edit the Swift files, run `pnpm example ios` once to generate the native project, then open `example/ios/reactnativepaypalexample.xcworkspace` in Xcode and find the source files at `Pods > Development Pods > react-native-paypal`. -To edit the Kotlin files, open `example/android` in Android studio and find the source files at `reactnativepaypal` under `Android`. +To edit the Kotlin files, run `pnpm example android` once to generate the native project, then open `example/android` in Android Studio and find the source files at `reactnativepaypal` under `Android`. ### Commit message convention @@ -66,15 +76,13 @@ We follow the [conventional commits specification](https://www.conventionalcommi - `test`: adding or updating tests, e.g. add integration tests using detox. - `chore`: tooling changes, e.g. change CI config. -Our pre-commit hooks verify that your commit message matches this format when committing. +Our commit-msg hook (commitlint) verifies that your commit message matches this format when committing. ### Linting and tests -[ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) - -We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. +We use [TypeScript](https://www.typescriptlang.org/) for type checking, [oxlint](https://oxc.rs/docs/guide/usage/linter) for linting, [oxfmt](https://oxc.rs/docs/guide/usage/formatter) for formatting the code, [detekt](https://detekt.dev/) for linting the Kotlin code, and [Jest](https://jestjs.io/) for testing. -Our pre-commit hooks verify that the linter and tests pass when committing. +[lefthook](https://lefthook.dev/) runs the git hooks, configured in `lefthook.yml`. Our pre-commit hook runs oxlint and oxfmt on the staged files, then verifies that type checking and the tests pass. The Kotlin lint is not part of the hooks; run it yourself when you change Android code. ### Publishing to npm @@ -83,20 +91,23 @@ We use [release-it](https://github.com/release-it/release-it) to make it easier To publish new versions, run the following: ```sh -yarn release +pnpm release ``` ### Scripts The `package.json` file contains various scripts for common tasks: -- `yarn bootstrap`: setup project by installing all dependencies and pods. -- `yarn typescript`: type-check files with TypeScript. -- `yarn lint`: lint files with ESLint. -- `yarn test`: run unit tests with Jest. -- `yarn example start`: start the Metro server for the example app. -- `yarn example android`: run the example app on Android. -- `yarn example ios`: run the example app on iOS. +- `pnpm install` (or `pnpm bootstrap`): install all dependencies and build `lib/`. +- `pnpm typecheck`: type-check files with TypeScript. +- `pnpm lint`: lint files with oxlint and check formatting with oxfmt. +- `pnpm lint:fix`: fix lint and formatting errors. +- `pnpm lint:kotlin`: lint the Kotlin files with detekt. +- `pnpm test`: run unit tests with Jest. +- `pnpm run docs`: generate the API reference with typedoc. +- `pnpm example start`: start the Metro server for the example app. +- `pnpm example android`: run the example app on Android. +- `pnpm example ios`: run the example app on iOS. ### Sending a pull request diff --git a/README.md b/README.md index d1a334f1..45a2e364 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ error, such as an option of the wrong type, so there is no need for a ## 👏 Contributing -See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. +See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow, including how to run the example app (it needs a development build; Expo Go does not include the native module). ## License diff --git a/commitlint.config.js b/commitlint.config.js deleted file mode 100644 index 5717d257..00000000 --- a/commitlint.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - extends: ["@commitlint/config-conventional"], -} diff --git a/commitlint.config.ts b/commitlint.config.ts new file mode 100644 index 00000000..fe089f36 --- /dev/null +++ b/commitlint.config.ts @@ -0,0 +1,7 @@ +import type { UserConfig } from "@commitlint/types" + +const config: UserConfig = { + extends: ["@commitlint/config-conventional"], +} + +export default config diff --git a/example/.gitignore b/example/.gitignore index d914c328..45e0b96a 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -23,8 +23,6 @@ expo-env.d.ts # debug npm-debug.* -yarn-debug.* -yarn-error.* # macOS .DS_Store diff --git a/example/App.tsx b/example/App.tsx index 4b570067..7f358962 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -1,27 +1,49 @@ import { requestBillingAgreement, + RequestBillingAgreementOptions, RequestBillingAgreementResponse, } from "@bounceapp/react-native-paypal" import { useState } from "react" import { Button, ScrollView, StyleSheet, Text, View } from "react-native" import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context" +// Placeholder values: with a real client token and an App Link served by your +// domain, the PayPal flow would complete. +const options: RequestBillingAgreementOptions = { + clientToken: "CLIENT_TOKEN", + appLinkReturnUrl: "https://example.com/paypal", + hasUserLocationConsent: false, + billingAgreementDescription: "Example billing agreement", + localeCode: "en_US", + shippingAddressRequired: false, + displayName: "Example app", +} + export default function App() { const [loading, setLoading] = useState(false) - const [payload, setPayload] = - useState() + const [logs, setLogs] = useState([]) + + const log = (message: string) => + setLogs(previous => [ + `${new Date().toLocaleTimeString()} ${message}`, + ...previous, + ]) const onPress = async () => { setLoading(true) + log("requestBillingAgreement called") try { - const res = await requestBillingAgreement({ - clientToken: "CLIENT_TOKEN", - appLinkReturnUrl: "https://example.com/paypal", - }) - setPayload(res) - } catch (_error) { - setPayload(null) + const res: RequestBillingAgreementResponse = + await requestBillingAgreement(options) + + if (res.error) { + log(`error result: ${JSON.stringify(res.error, null, 4)}`) + } else { + log(`payload: ${JSON.stringify(res.payload, null, 4)}`) + } + } catch (error) { + log(`threw: ${String(error)}`) } setLoading(false) @@ -31,11 +53,11 @@ export default function App() { - {payload && ( - - {JSON.stringify(payload, null, 4)} + {logs.map((entry, index) => ( + + {entry} - )} + ))}