diff --git a/.github/workflows/android_alpha.yml b/.github/workflows/android_alpha.yml new file mode 100644 index 000000000..fa21ac720 --- /dev/null +++ b/.github/workflows/android_alpha.yml @@ -0,0 +1,114 @@ +name: Build and deploy Android alpha +on: + push: + tags: + - 'android-alpha-*' + branches: + - 'ci/android-alpha-*' + # - ci/* + workflow_call: + secrets: + RABBY_MOBILE_ANDROID_KEY_STORE: + required: true + RABBY_MOBILE_ANDROID_KEY_PASSWORD: + required: true + RABBY_MOBILE_ANDROID_KEY_ALIAS: + required: true + RABBY_MOBILE_KR_PWD: + required: true + +# defaults: +# run: +# shell: bash -ieo pipefail {0} +env: + BUILD_TARGET_PLATFORM: android + +jobs: + alpha-build: + name: android-alpha-build + runs-on: + - self-hosted + - mobile + - macOS + steps: + - uses: actions/checkout@v3 + - name: Get Actions Variables + id: actionvars + shell: bash + run: | + trigger_date=$(date +'%Y%m%d%H%M%S') + echo "trigger_date=$trigger_date" >> "$GITHUB_OUTPUT" + + flatten_refname=${GITHUB_REF_NAME//\//'_'} + flatten_refname=${flatten_refname//\./'_'} + echo "flatten_refname=$flatten_refname" >> "$GITHUB_OUTPUT" + # env: + # GITHUB_REF_NAME: ${{ github.ref_name }} + + # - name: Setup Nodejs and Yarn + # uses: actions/setup-node@v3 + # with: + # node-version: 18 + + # - name: Setup Ruby & Fastlane + # uses: ruby/setup-ruby@v1 + # with: + # ruby-version: '2.7' + # bundler-cache: true # runs 'bundle install' and caches installed gems automatically + + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: 'zulu' # See 'Supported distributions' for available options + java-version: '17' + + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + + - name: Setup & Install dependencies + run: | + yarn --version; + # yarn setup && yarn postinstall; + yarn install; + cd apps/mobile; + bundle install; + env: + FLATTEN_REFNAME: ${{ steps.actionvars.outputs.flatten_refname }} + TRIGGER_DATE: ${{ steps.actionvars.outputs.trigger_date }} + + # - name: Prepare JSBundle for sourcemap + # run: | + # cd apps/mobile; + # SENTRY_BUNDLE_STAGE=bundle bash ./android/bundle_sourcemap.sh + + - name: Build app + env: + RABBY_MOBILE_BUILD_BUCKET: ${{ secrets.RABBY_MOBILE_BUILD_BUCKET }} + RABBY_MOBILE_KR_PWD: ${{ secrets.RABBY_DESKTOP_KR_PWD }} + RABBY_MOBILE_ANDROID_KEY_STORE: ${{ secrets.RABBY_MOBILE_ANDROID_KEY_STORE }} + RABBY_MOBILE_ANDROID_KEY_PASSWORD: ${{ secrets.RABBY_MOBILE_ANDROID_KEY_PASSWORD }} + RABBY_MOBILE_ANDROID_KEY_ALIAS: ${{ secrets.RABBY_MOBILE_ANDROID_KEY_ALIAS }} + RABBY_MOBILE_SENTRY_AUTH_TOKEN: ${{ secrets.RABBY_MOBILE_SENTRY_AUTH_TOKEN }} + SENTRY_AUTH_TOKEN: ${{ secrets.RABBY_MOBILE_SENTRY_AUTH_TOKEN }} + LARK_CHAT_URL: ${{ secrets.LARK_CHAT_URL }} + LARK_CHAT_SECRET: ${{ secrets.LARK_CHAT_SECRET }} + # see more details on https://docs.github.com/en/actions/learn-github-actions/contexts#github-context + GIT_ACTIONS_JOB_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GIT_COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }} + GIT_REF_NAME: ${{ github.ref_name }} + GIT_REF_URL: ${{ github.server_url }}/${{ github.repository }}/tree/${{ github.ref_name }} + GITHUB_REF: ${{ github.ref }} + run: | + cd apps/mobile; + bundle exec fastlane android alpha; + + # - name: Upload app to aws bucket + # run: aws s3 cp ./android/app/build/outputs/apk/release/app-release.apk s3://${RABBY_MOBILE_BUILD_BUCKET}/downloads/debank-app/${APK_NAME}.apk --acl public-read + + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: rabbymobile-${{ steps.actionvars.outputs.flatten_refname }}-${{ steps.actionvars.outputs.trigger_date }} + path: | + ./apps/mobile/android/app/build/outputs/apk/release/app-release.apk + retention-days: 90 diff --git a/.github/workflows/ios_alpha.yml b/.github/workflows/ios_alpha.yml new file mode 100644 index 000000000..ef8987a80 --- /dev/null +++ b/.github/workflows/ios_alpha.yml @@ -0,0 +1,104 @@ +name: Build and deploy iOS alpha +on: + push: + tags: + - 'ios-alpha-*' + branches: + - 'ci/ios-alpha-*' + - ci/* + workflow_dispatch: + workflow_call: + secrets: + MATCH_GIT_PRIVATE_KEY: + required: true + MATCH_PASSWORD: + required: true + NPM_AUTH_TOKEN: + required: true + +# defaults: +# run: +# shell: bash -ieo pipefail {0} + +env: + BUILD_TARGET_PLATFORM: ios + +jobs: + setup: + name: iOS-alpha-build + runs-on: [self-hosted, mobile, macOS] + # defaults: + # run: + # shell: bash -ieo pipefail {0} + steps: + - uses: actions/checkout@v4 + - name: Get Actions Variables + id: actionvars + shell: bash + run: | + trigger_date=$(date +'%Y%m%d%H%M%S') + echo "trigger_date=$trigger_date" >> "$GITHUB_OUTPUT" + + flatten_refname=${GITHUB_REF_NAME//\//'_'} + flatten_refname=${flatten_refname//\./'_'} + echo "flatten_refname=$flatten_refname" >> "$GITHUB_OUTPUT" + + # - name: Setup Nodejs and Yarn + # uses: actions/setup-node@v3 + # with: + # node-version: 18 + + # - name: Setup Ruby & Fastlane + # uses: ruby/setup-ruby@v1 + # with: + # ruby-version: '2.7' + # bundler-cache: true # runs 'bundle install' and caches installed gems automatically + + - uses: maxim-lobanov/setup-xcode@v1 + id: setup-xcode + with: + # use specific version rather than 'latest-stable' if multiple Xcodes installed + xcode-version: '15.3' + + - name: Install dependencies + run: | + yarn --version; + cd ./apps/mobile; + yarn install; + + - name: Build app + env: + KEYCHAIN_PASS: ${{ secrets.KEYCHAIN_PASS }} + RABBY_MOBILE_BUILD_BUCKET: ${{ secrets.RABBY_MOBILE_BUILD_BUCKET }} + RABBY_MOBILE_KR_PWD: ${{ secrets.RABBY_DESKTOP_KR_PWD }} + RABBY_MOBILE_SENTRY_AUTH_TOKEN: ${{ secrets.RABBY_MOBILE_SENTRY_AUTH_TOKEN }} + SENTRY_AUTH_TOKEN: ${{ secrets.RABBY_MOBILE_SENTRY_AUTH_TOKEN }} + CONFIGURATION: release + TYPE: adhoc + BUILD_ENV: alpha + LARK_CHAT_URL: ${{ secrets.LARK_CHAT_URL }} + LARK_CHAT_SECRET: ${{ secrets.LARK_CHAT_SECRET }} + RABBY_BOT_LARK_ACCESS_TOKEN: ${{ secrets.RABBY_BOT_LARK_ACCESS_TOKEN }} + # see more details on https://docs.github.com/en/actions/learn-github-actions/contexts#github-context + GIT_ACTIONS_JOB_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} + GIT_COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }} + GIT_REF_NAME: ${{ github.ref_name }} + GIT_REF_URL: ${{ github.server_url }}/${{ github.repository }}/tree/${{ github.ref_name }} + GITHUB_REF: ${{ github.ref }} + run: | + cd apps/mobile; + # security unlock-keychain -p ${{ secrets.KEYCHAIN_PASS }} ~/Library/Keychains/login.keychain-db + security unlock-keychain -p ${{ secrets.KEYCHAIN_PASS }} login.keychain + cd ios && bundle install && bundle exec pod install; + cd ../; + REALLY_UPLOAD=true ./scripts/deploy-ios-adhoc.sh + + # - name: copy dest + # run: | + # cp ./ios/Package/rabbymobile.ipa ./ios/outputs/RabbyMobile.ipa + # cp ./ios/Package/manifest.plist ./ios/outputs/manifest.plist + + # - name: Upload app + # run: aws s3 cp ./ios/outputs s3://${BUILD_BUCKET}/downloads/debank-app/ --recursive --acl public-read diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..b36799f69 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,67 @@ +name: Test + +on: + push: + branches: + - dev + - feat* + # - ci/* + +defaults: + run: + shell: bash -ieo pipefail {0} + +jobs: + setup: + name: Build + strategy: + matrix: + host: [macOS] + runs-on: + - self-hosted + - mobile + - ${{ matrix.host }} + env: + NODE_OPTIONS: '--max_old_space_size=4096' + + steps: + - name: Checkout git repo + uses: actions/checkout@v3 + + - name: Env Test + id: env-test + shell: bash + run: | + echo "whoami $(whoami)" + echo "shell is $(echo $0)" + echo "HOME is $HOME" + echo "which node $(which node)" + + - name: Install and build + shell: bash + run: | + npm i -g @debank/cli@latest; + cd apps/mobile; + yarn install; + yarn apply-patch; + yarn prepare-archive; + + security unlock-keychain -p ${{ secrets.KEYCHAIN_PASS }} ~/Library/Keychains/login.keychain-db + + env: + KEYCHAIN_PASS: ${{ secrets.KEYCHAIN_PASS }} + RABBY_MOBILE_BUILD_BUCKET: ${{ secrets.RABBY_MOBILE_BUILD_BUCKET }} + RABBY_MOBILE_KR_PWD: ${{ secrets.RABBY_DESKTOP_KR_PWD }} + RABBY_MOBILE_ANDROID_KEY_STORE: ${{ secrets.RABBY_MOBILE_ANDROID_KEY_STORE }} + RABBY_MOBILE_ANDROID_KEY_PASSWORD: ${{ secrets.RABBY_MOBILE_ANDROID_KEY_PASSWORD }} + RABBY_MOBILE_ANDROID_KEY_ALIAS: ${{ secrets.RABBY_MOBILE_ANDROID_KEY_ALIAS }} + RABBY_MOBILE_SENTRY_AUTH_TOKEN: ${{ secrets.RABBY_MOBILE_SENTRY_AUTH_TOKEN }} + LARK_CHAT_URL: ${{ secrets.LARK_CHAT_URL }} + LARK_CHAT_SECRET: ${{ secrets.LARK_CHAT_SECRET }} + RABBY_REALLY_COPY: ${{ vars.RABBY_REALLY_COPY }} + # see more details on https://docs.github.com/en/actions/learn-github-actions/contexts#github-context + GIT_ACTIONS_JOB_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GIT_COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }} + GIT_REF_NAME: ${{ github.ref_name }} + GIT_REF_URL: ${{ github.server_url }}/${{ github.repository }}/tree/${{ github.ref_name }} + GITHUB_REF: ${{ github.ref }} diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore index 0cb8ee46f..91ef64b24 100644 --- a/apps/mobile/.gitignore +++ b/apps/mobile/.gitignore @@ -25,6 +25,7 @@ ios/.xcode.env.local # Android/IntelliJ # build/ +build4sentry/ .idea .gradle local.properties @@ -72,13 +73,17 @@ tmp.iconset ios/Package -scripts/deployments/ios/manifest.plist +/*/deployments/tmp scripts/deployments/**/version.json scripts/deployments/**/*.md -scripts/deployments/**/*.ipa -scripts/deployments/**/*.apk -scripts/deployments/**/*.aab + +scripts/deployments/ios/manifest.plist +scripts/deployments/ios/*.ipa +scripts/deployments/android/*.apk +scripts/deployments/android/*.aab + +scripts/tmp **/InpageBridgeWeb3.js /android/app/src/main/assets/custom/vconsole.min.js diff --git a/apps/mobile/Gemfile b/apps/mobile/Gemfile index 738244026..67986cce0 100644 --- a/apps/mobile/Gemfile +++ b/apps/mobile/Gemfile @@ -6,3 +6,6 @@ ruby ">= 2.6.10" gem 'cocoapods', '~> 1.11', '>= 1.11.2' gem 'activesupport', '>= 6.1.7.3', '< 7.1.0' gem 'fastlane' + +plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/apps/mobile/Gemfile.lock b/apps/mobile/Gemfile.lock index 3915665f4..db4b37aa9 100644 --- a/apps/mobile/Gemfile.lock +++ b/apps/mobile/Gemfile.lock @@ -156,6 +156,9 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) + fastlane-plugin-sentry (1.20.0) + os (~> 1.1, >= 1.1.4) + fastlane-plugin-versioning_android (0.1.1) ffi (1.16.3) fourflusher (2.3.1) fuzzy_match (2.0.4) @@ -279,6 +282,8 @@ DEPENDENCIES activesupport (>= 6.1.7.3, < 7.1.0) cocoapods (~> 1.11, >= 1.11.2) fastlane + fastlane-plugin-sentry + fastlane-plugin-versioning_android RUBY VERSION ruby 2.6.10p210 diff --git a/apps/mobile/android/bundle_sourcemap.sh b/apps/mobile/android/bundle_sourcemap.sh new file mode 100644 index 000000000..56049d00c --- /dev/null +++ b/apps/mobile/android/bundle_sourcemap.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +set -e + +script_dir="$( cd "$( dirname "$0" )" && pwd )" +android_dir=$script_dir +project_dir=$(dirname $script_dir) + +. $project_dir/scripts/fns.sentry.sh --source-only + +# https://docs.sentry.io/platforms/react-native/manual-setup/hermes/ +# script would be also executed on ../fastlane/ + +sentryfn_setup; +if [[ ! -f $HERMES_BIN ]]; then + echo "[bundle_sourcemap] Hermes compiler not found. Make sure you have hermesc installed correctly." + exit 1 +fi + +if [[ $SENTRY_BUNDLE_STAGE == "bundle" ]]; then + echo "[bundle_sourcemap::bundle] Start to bundle." + + sentryfn_build_sourcemap; +elif [[ $SENTRY_BUNDLE_STAGE == "postbundle" ]]; then + echo "[bundle_sourcemap::postbundle] Start to output & compose sourcemap." + + sentryfn_build_sourcemap; + sentryfn_build_hbc; + sentryfn_compose_sourcemap; +else + echo "[bundle_sourcemap] Unknown SENTRY_BUNDLE_STAGE: $SENTRY_BUNDLE_STAGE" + exit 1 +fi + +echo "[bundle_sourcemap] Done." diff --git a/apps/mobile/android/upload_sentry_source.sh b/apps/mobile/android/upload_sentry_source.sh deleted file mode 100644 index 0c4141d07..000000000 --- a/apps/mobile/android/upload_sentry_source.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/sh - -set -e - -script_dir="$( cd "$( dirname "$0" )" && pwd )" -project_dir=$(dirname $script_dir) - -# https://docs.sentry.io/platforms/react-native/manual-setup/hermes/ - -os_name=$(uname -s) - -if [[ "$os_name" = "Darwin" ]]; then - HERMES_OS_BIN="osx-bin" -elif [[ "$os_name" = "Linux" ]]; then - HERMES_OS_BIN="linux64-bin" -else - HERMES_OS_BIN="win64-bin" -fi - -if [[ -f "$project_dir/node_modules/react-native/sdks/hermesc/${HERMES_OS_BIN}/hermesc" ]]; then - # react-native v0.69 or higher (keep only this condition when version is reached) - HERMES_BIN="$project_dir/node_modules/react-native/sdks/hermesc/${HERMES_OS_BIN}/hermesc" -elif [[ -f "$project_dir/node_modules/hermes-engine/${HERMES_OS_BIN}/hermesc" ]]; then - # react-native v0.68 (current) - HERMES_BIN="$project_dir/node_modules/hermes-engine/${HERMES_OS_BIN}/hermesc" -else - echo "Hermes compiler not found." - exit 1; -fi - -JSBUNDLE_NAME="../android/app/src/main/assets/index.android.bundle" - -"${HERMES_BIN}" \ - -O \ - -emit-binary \ - -output-source-map \ - -out="${JSBUNDLE_NAME}.hbc" \ - "${JSBUNDLE_NAME}" - -rm -f "${JSBUNDLE_NAME}" -mv "${JSBUNDLE_NAME}.hbc" "${JSBUNDLE_NAME}" - -node ../node_modules/react-native/scripts/compose-source-maps.js \ - "${JSBUNDLE_NAME}.packager.map" \ - "${JSBUNDLE_NAME}.hbc.map" \ - -o "${JSBUNDLE_NAME}.map" diff --git a/apps/mobile/fastlane/Fastfile b/apps/mobile/fastlane/Fastfile new file mode 100644 index 000000000..cb5a6136d --- /dev/null +++ b/apps/mobile/fastlane/Fastfile @@ -0,0 +1,146 @@ +require 'base64' + +platform :ios do + private_lane :prepare_apple_sign do |opts| + # setup_ci + + # enforce configuration + update_code_signing_settings( + use_automatic_signing: false, + sdk: "iphoneos*", + path: './ios/RabbyMobile.xcodeproj', + profile_name: opts[:profile_name], + bundle_identifier: "com.debank.rabby-mobile", + ) + + ENV['CONFIGURATION'] = 'release' + match( + type: opts[:type], + readonly: true, + shallow_clone: true, + verbose: false, + force_for_new_devices: true, + clone_branch_directly: true) + end + + desc "Release for the iOS adhoc" + lane :adhoc do + prepare_apple_sign(type: 'adhoc', profile_name: "RabbyMobileAdHoc") + + gym( + scheme: 'RabbyMobile', + workspace: './ios/RabbyMobile.xcworkspace', + configuration: 'Release', + output_directory: "./ios/Package/latest", + build_path: "./ios/Package/latest", + destination: "generic/platform=iOS", + clean: true, + ## if you wanna output log to stdout, uncomment below 2 lines + # xcodebuild_formatter: '', + # suppress_xcode_output: false, + skip_package_pkg: true, + export_method: 'ad-hoc', + # export_method: 'release-testing', + export_options: { + method: 'ad-hoc', + signingStyle: 'manual', + # provisioningProfiles: { + # "com.debank.rabby-mobile": "RabbyMobileAdHoc" + # } + manifest: { + appURL: "https://download.rabby.io/downloads/wallet-mobile/ios/rabbymobile.ipa", + displayImageURL: "https://download.rabby.io/downloads/wallet-mobile/ios/icon_57x57@57w.png", + fullSizeImageURL: "https://download.rabby.io/downloads/wallet-mobile/ios/icon_512x512@512w.png", + } + }) + + # # shell executeb on ./fastlane directory, add `sh "pwd"` below to check it + # sh "/usr/libexec/PlistBuddy -c \"Set:items:0:metadata:title Rabby Wallet\" ../ios/Package/latest/manifest.plist" + end + + desc "Release for the iOS production" + lane :release do + prepare_apple_sign(type: 'appstore', profile_name: "RabbyMobileAppStore") + gym( + scheme: 'RabbyMobile', + workspace: './ios/RabbyMobile.xcworkspace', + configuration: 'Release', + output_directory: "./ios/Package", + build_path: "./ios/Package", + destination: "generic/platform=iOS", + skip_profile_detection: true, + clean: true, + skip_package_pkg: true, + export_method: "app-store", + verbose: true, + export_options: { + method: "app-store", + signingStyle: 'manual', + }) + pilot( + apple_id: '6474381673', + username: ENV["RABBY_MOBILE_PILOT_USERNAME"], + ipa: "./ios/Package/RabbyMobile.ipa", + skip_waiting_for_build_processing: true, + ) + end +end + +platform :android do + private_lane :write_key_store do + # keystore is stored as base64 from environment variable + if !ENV["RABBY_MOBILE_ANDROID_KEY_STORE"].empty? + File.open("../android/app/app-upload-key.keystore", 'w') do |file| + file.write(Base64.decode64(ENV["RABBY_MOBILE_ANDROID_KEY_STORE"])) + end + end + end + + private_lane :build_rabbymobile_app do |opts| + # @see https://github.com/facebook/react-native/issues/35583 + # https://docs.sentry.io/platforms/react-native/manual-setup/hermes/ + # sh "SENTRY_BUNDLE_STAGE=postbundle bash ../android/bundle_sourcemap.sh" + + gradle(tasks: opts[:tasks], project_dir: './android', properties: { + "MYAPP_UPLOAD_STORE_FILE" => "app-upload-key.keystore", + "MYAPP_UPLOAD_STORE_PASSWORD" => ENV['RABBY_MOBILE_ANDROID_KEY_PASSWORD'], + "MYAPP_UPLOAD_KEY_ALIAS" => ENV['RABBY_MOBILE_ANDROID_KEY_ALIAS'], + "MYAPP_UPLOAD_KEY_PASSWORD" => ENV['RABBY_MOBILE_ANDROID_KEY_PASSWORD'] + # if you wanna customize sentry upload, exclude app:bundleReleaseJsAndAssets use js bundle generated by previous step + # }, flags: "-x bundleReleaseJsAndAssets") + }) + + android_version_code = android_get_version_code(gradle_file: "./android/app/build.gradle") + android_version_name = android_get_version_name(gradle_file: "./android/app/build.gradle") + + # sentry_upload_sourcemap( + # auth_token: ENV["RABBY_MOBILE_SENTRY_AUTH_TOKEN"], + # org_slug: 'debank', + # project_slug: 'rabby-mobile', + # version: android_version_name, + # app_identifier: 'com.debank.rabbymobile', + # build: android_version_code, + # dist: android_version_code, + # url_prefix: "app:///", # https://github.com/getsentry/sentry-cli/issues/311 fuck sentry's docs + # sourcemap: [ + # './android/app/build4sentry/generated/assets/index.android.bundle', + # './android/app/build4sentry/generated/assets/index.android.bundle.map' + # ], + # rewrite: true + # ) + end + + desc "Release for the Android alpha" + lane :alpha do + write_key_store + gradle(task: 'clean', project_dir: './android') + build_rabbymobile_app(tasks: ['assembleRelease']) + end + + desc "Release for the Android production" + lane :release do + write_key_store + gradle(task: 'clean', project_dir: './android') + build_rabbymobile_app(tasks: ['assembleRelease', 'bundleRelease']) + end +end diff --git a/apps/mobile/fastlane/Pluginfile b/apps/mobile/fastlane/Pluginfile new file mode 100644 index 000000000..7c4e0c617 --- /dev/null +++ b/apps/mobile/fastlane/Pluginfile @@ -0,0 +1,6 @@ +# Autogenerated by fastlane +# +# Ensure this file is checked in to source control! + +gem 'fastlane-plugin-versioning_android' +gem 'fastlane-plugin-sentry' diff --git a/apps/mobile/fastlane/README.md b/apps/mobile/fastlane/README.md new file mode 100644 index 000000000..f2cd9e007 --- /dev/null +++ b/apps/mobile/fastlane/README.md @@ -0,0 +1,59 @@ +## fastlane documentation + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## iOS + +### ios adhoc + +```sh +[bundle exec] fastlane ios adhoc +``` + +Release for the iOS adhoc + +### ios release + +```sh +[bundle exec] fastlane ios release +``` + +Release for the iOS production + +--- + +## Android + +### android alpha + +```sh +[bundle exec] fastlane android alpha +``` + +Release for the Android alpha + +### android release + +```sh +[bundle exec] fastlane android release +``` + +Release for the Android production + +--- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/apps/mobile/ios/build.sh b/apps/mobile/ios/build.sh index be0117716..5bcf7a360 100755 --- a/apps/mobile/ios/build.sh +++ b/apps/mobile/ios/build.sh @@ -1,5 +1,7 @@ #!/bin/sh +## script deprecated, use fastlane instead + script_dir="$( cd "$( dirname "$0" )" && pwd )" project_dir=$script_dir diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 6ec5f497e..b3d5e44bf 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -20,6 +20,7 @@ "syncrnversion:incb": "./node_modules/.bin/react-native-version --never-amend", "syncrnversion": "./node_modules/.bin/react-native-version --never-amend --never-increment-build", "start": "yarn ensure-git-hooks && yarn build:deps && yarn build-inpage && react-native start", + "prepare-archive": "yarn build:deps && yarn build-inpage", "test": "jest", "build-inpage": "sh ./scripts/postinstall.sh", "create-patch": "sh ./scripts/create-patch.sh", @@ -164,6 +165,7 @@ "@tsconfig/react-native": "^3.0.0", "@types/koa-compose": "^3.2.8", "@types/lodash": "^4.14.202", + "@types/qrcode": "^1.5.5", "@types/react": "^18.0.24", "@types/react-is": "^18.2.4", "@types/react-native-version-check": "^3.4.8", @@ -180,6 +182,7 @@ "patch-package": "^8.0.0", "postinstall-postinstall": "^2.1.0", "prettier": "^2.7.1", + "qrcode": "^1.5.3", "react-devtools": "^4.28.5", "react-native-asset": "^2.1.1", "react-native-svg-transformer": "^1.1.0", diff --git a/apps/mobile/scripts/deploy-ios-adhoc.sh b/apps/mobile/scripts/deploy-ios-adhoc.sh index 618176cea..b5be9e22b 100755 --- a/apps/mobile/scripts/deploy-ios-adhoc.sh +++ b/apps/mobile/scripts/deploy-ios-adhoc.sh @@ -16,48 +16,65 @@ proj_version=$(node --eval="process.stdout.write(require('./package.json').versi app_display_name=$(node --eval="process.stdout.write(require('./app.json').displayName)"); cd $script_dir; -unix_replace_variables $script_dir/tpl/ios/manifest.plist $script_dir/deployments/ios/manifest.plist \ - --var-IPA_DOWNLOAD_URL="$cdn_deployment_urlbase/ios/rabbymobile.ipa" \ - --var-URL_DISPLAY_IMAGE="$cdn_deployment_urlbase/ios/icon_57x57@57w.png" \ - --var-URL_FULL_SIZE_IMAGE="$cdn_deployment_urlbase/ios/icon_512x512@512w.png" \ - --var-APP_VERSION="$proj_version" \ - --var-APP_DISPLAY_NAME="$app_display_name" +ipa_name="RabbyMobile" +ouput_dir=$project_dir/ios/Package/latest +deployment_local_dir="$script_dir/deployments" + +tmp_dir="$script_dir/deployments/tmp" +mkdir -p $tmp_dir + +xcodebuild -project $project_dir/ios/RabbyMobile.xcodeproj -target "RabbyMobile" -showBuildSettings -json | plutil -convert xml1 - -o $tmp_dir/RabbyMobileAdHoc.plist + +ios_version_name=$(/usr/libexec/PlistBuddy -c "Print:CFBundleShortVersionString" $project_dir/ios/RabbyMobile/Info.plist) +ios_version_code=$(/usr/libexec/PlistBuddy -c "Print:0:buildSettings:CURRENT_PROJECT_VERSION" $tmp_dir/RabbyMobileAdHoc.plist) +cd $project_dir/ios; unix_replace_variables $script_dir/tpl/ios/version.json $script_dir/deployments/ios/version.json \ --var-DOWNLOAD_URL=$cdn_deployment_urlbase/ios/ \ - --var-APP_VER_CODE=100 \ + --var-APP_VER_CODE=$ios_version_code \ --var-APP_VER="$proj_version" -if [ ! -z $REALLY_BUILD ]; then - BUILD_DATE=`date '+%Y%m%d_%H%M%S'` - export build_export_path="$script_dir/deployments/ios" - +if [[ -z $SKIP_BUILD || ! -f $ouput_dir/RabbyMobile.ipa ]]; then cd $project_dir/ios; bundle install && bundle exec pod install; - sh $project_dir/ios/build.sh ad-hoc - - rm -rf $export_path + cd $project_dir; + bundle exec fastlane ios adhoc; fi +file_date=$(date -r $ouput_dir/RabbyMobile.ipa '+%Y%m%d_%H%M%S') +version_bundle_name="${ios_version_name}.${ios_version_code}-$file_date" +deployment_s3_dir=$S3_IOS_PUB_DEPLOYMENT/ios-$version_bundle_name +deployment_cdn_baseurl=$cdn_deployment_urlbase/ios-$version_bundle_name +manifest_plist_url="itms-services://?action=download-manifest&url=$deployment_cdn_baseurl/manifest.plist" + +cp $ouput_dir/RabbyMobile.ipa $deployment_local_dir/ios/rabbymobile.ipa +cp $ouput_dir/manifest.plist $deployment_local_dir/ios/manifest.plist + +/usr/libexec/PlistBuddy -c "Set:items:0:metadata:title Rabby Wallet" $deployment_local_dir/ios/manifest.plist +/usr/libexec/PlistBuddy -c "Set:items:0:assets:0:url $deployment_cdn_baseurl/rabbymobile.ipa" $deployment_local_dir/ios/manifest.plist # appURL +/usr/libexec/PlistBuddy -c "Set:items:0:assets:1:url $deployment_cdn_baseurl/icon_57x57@57w.png" $deployment_local_dir/ios/manifest.plist # displayImageURL +/usr/libexec/PlistBuddy -c "Set:items:0:assets:2:url $deployment_cdn_baseurl/icon_512x512@512w.png" $deployment_local_dir/ios/manifest.plist # fullSizeImageURL + +echo "[deploy-ios-adhoc] will upload to $deployment_s3_dir" +echo "[deploy-ios-adhoc] will be served at $deployment_cdn_baseurl" + echo "" + if [ ! -z $REALLY_UPLOAD ]; then - if [ ! -z $ipa_name ]; then - echo "[deploy-ios-adhoc] backup..." - aws s3 cp $export_ipa_path/$ipa_name.ipa $IOS_BAK_DEPLOYMENT/$ipa_name.ipa --acl public-read --profile debankbuild - fi - - echo "[deploy-ios-adhoc] start sync..." - aws s3 sync $script_dir/deployments/ios $IOS_PUB_DEPLOYMENT/ios/ --exclude '*' --include "*.plist" --acl public-read --profile debankbuild --content-type application/x-plist - aws s3 sync $script_dir/deployments/ios $IOS_PUB_DEPLOYMENT/ios/ --exclude '*' --include "*.png" --acl public-read --profile debankbuild --content-type image/png - aws s3 sync $script_dir/deployments/ios $IOS_PUB_DEPLOYMENT/ios/ --exclude '*' --include "*.json" --acl public-read --profile debankbuild --content-type application/json - aws s3 sync $script_dir/deployments/ios $IOS_PUB_DEPLOYMENT/ios/ --exclude '*' --include "*.ipa" --acl public-read --profile debankbuild --content-type application/octet-stream + echo "[deploy-ios-adhoc] start sync to $deployment_s3_dir..." + aws s3 sync $script_dir/deployments/ios $deployment_s3_dir/ --exclude '*' --include "*.ipa" --acl public-read --content-type application/octet-stream + aws s3 sync $script_dir/deployments/ios $deployment_s3_dir/ --exclude '*' --include "*.plist" --acl public-read --content-type application/x-plist + aws s3 sync $script_dir/deployments/ios $deployment_s3_dir/ --exclude '*' --include "*.png" --acl public-read --content-type image/png + aws s3 sync $script_dir/deployments/ios $deployment_s3_dir/ --exclude '*' --include "*.json" --acl public-read --content-type application/json fi +node $script_dir/notify-lark.js "$manifest_plist_url" ios + [ -z $RABBY_MOBILE_CDN_FRONTEND_ID ] && RABBY_MOBILE_CDN_FRONTEND_ID="" if [ -z $CI ]; then echo "[deploy-ios-adhoc] force fresh CDN:" - echo "[deploy-ios-adhoc] \`aws cloudfront create-invalidation --distribution-id $RABBY_MOBILE_CDN_FRONTEND_ID --paths '/$s3_upload_prefix/ios/*'\` --profile debankbuild" + echo "[deploy-ios-adhoc] \`aws cloudfront create-invalidation --distribution-id $RABBY_MOBILE_CDN_FRONTEND_ID --paths '/$s3_upload_prefix/ios/*'\`" echo "" fi diff --git a/apps/mobile/scripts/fns.sentry.sh b/apps/mobile/scripts/fns.sentry.sh new file mode 100644 index 000000000..cc85a2350 --- /dev/null +++ b/apps/mobile/scripts/fns.sentry.sh @@ -0,0 +1,98 @@ +RM_OS_NAME=$(uname -s); + +sentryfn_setup() { + if [ -z $project_dir ]; then + echo "project_dir is not set" + exit 1 + fi + + if [[ "$RM_OS_NAME" = "Darwin" ]]; then + export HERMES_OS_BIN="osx-bin" + [ -z $BUILD_TARGET_PLATFORM ] && export BUILD_TARGET_PLATFORM="ios"; + elif [[ "$RM_OS_NAME" = "Linux" ]]; then + export HERMES_OS_BIN="linux64-bin" + [ -z $BUILD_TARGET_PLATFORM ] && export BUILD_TARGET_PLATFORM="android"; + else + export HERMES_OS_BIN="win64-bin" + [ -z $BUILD_TARGET_PLATFORM ] && export BUILD_TARGET_PLATFORM="android"; + fi + + if [[ "$BUILD_TARGET_PLATFORM" == "android" ]]; then + export RMRN_BUNDLE_FILENAME="index.android.bundle" + else + export RMRN_BUNDLE_FILENAME="main.jsbundle" + fi + + if [[ -f "$project_dir/node_modules/hermes-engine/${HERMES_OS_BIN}/hermesc" ]]; then + # react-native v0.68 or lower + export HERMES_BIN="$project_dir/node_modules/hermes-engine/${HERMES_OS_BIN}/hermesc" + else + # react-native v0.69 or higher (keep only this condition when version is reached) + export HERMES_BIN="$project_dir/node_modules/react-native/sdks/hermesc/${HERMES_OS_BIN}/hermesc" + fi + + # the default bundle output directory + export RMRN_BUNDLE_DIR="$project_dir/$BUILD_TARGET_PLATFORM/app/build4sentry/generated" + # You can change it by parameter of `react-native bundle --bundle-output`, but make sure it's parent directory exists + export RMRN_JSBUNDLE_NAME="$RMRN_BUNDLE_DIR/assets/$RMRN_BUNDLE_FILENAME" + export RMRN_OJSBUNDLE_NAME="$RMRN_BUNDLE_DIR/asset-pieces/$RMRN_BUNDLE_FILENAME" + # export RMRN_JSBUNDLE_NAME=$RMRN_BUNDLE_FILENAME + + dirs=("$RMRN_JSBUNDLE_NAME" "$RMRN_OJSBUNDLE_NAME") + for filename in "${dirs[@]}"; do + dir=$(dirname $filename) + rm -rf $dir; + mkdir -p $dir; + done + + # list ALL important variables + echo "HERMES_OS_BIN: $HERMES_OS_BIN" + echo "HERMES_BIN: $HERMES_BIN" + echo "RMRN_BUNDLE_DIR: $RMRN_BUNDLE_DIR" + echo "RMRN_JSBUNDLE_NAME: $RMRN_JSBUNDLE_NAME" + echo "" +} + +sentryfn_build_sourcemap() { + echo "[fns_sentry] Building sourcemap..." + + mkdir -p $RMRN_BUNDLE_DIR; + + $project_dir/node_modules/.bin/react-native bundle \ + --platform $BUILD_TARGET_PLATFORM \ + --reset-cache \ + --dev false \ + --minify false \ + --entry-file index.js \ + --bundle-output ${RMRN_JSBUNDLE_NAME} \ + --assets-dest ${RMRN_BUNDLE_DIR}/res \ + --sourcemap-output ${RMRN_JSBUNDLE_NAME}.packager.map + + cp ${RMRN_JSBUNDLE_NAME} ${RMRN_OJSBUNDLE_NAME} + cp ${RMRN_JSBUNDLE_NAME}.packager.map ${RMRN_OJSBUNDLE_NAME}.packager.map +} + +sentryfn_build_hbc() { + echo "[fns_sentry] Building Hermes Bytecode (HBC)..." + + "${HERMES_BIN}" \ + -O -emit-binary -output-source-map \ + -out="${RMRN_JSBUNDLE_NAME}.hbc" \ + "${RMRN_JSBUNDLE_NAME}" + + rm "${RMRN_JSBUNDLE_NAME}" + mv "${RMRN_JSBUNDLE_NAME}.hbc" "${RMRN_JSBUNDLE_NAME}" +} + +sentryfn_compose_sourcemap() { + echo "[fns_sentry] composing sourcemap..." + + node $project_dir/node_modules/react-native/scripts/compose-source-maps.js \ + "${RMRN_JSBUNDLE_NAME}.packager.map" \ + "${RMRN_JSBUNDLE_NAME}.hbc.map" \ + -o "${RMRN_JSBUNDLE_NAME}.map" + + # node $project_dir/node_modules/@sentry/react-native/scripts/copy-debugid.js \ + # ${RMRN_JSBUNDLE_NAME}.packager.map ${RMRN_JSBUNDLE_NAME}.map + # rm -f ${RMRN_JSBUNDLE_NAME}.packager.map +} diff --git a/apps/mobile/scripts/fns.sh b/apps/mobile/scripts/fns.sh index fabb96132..3e5ac7c59 100755 --- a/apps/mobile/scripts/fns.sh +++ b/apps/mobile/scripts/fns.sh @@ -29,8 +29,8 @@ checkout_s3_pub_deployment_params() { "selfhost-reg") ANDROID_PUB_DEPLOYMENT=$RABBY_MOBILE_REG_PUB_DEPLOYMENT export ANDROID_BAK_DEPLOYMENT=$RABBY_MOBILE_REG_BAK_DEPLOYMENT - IOS_PUB_DEPLOYMENT=$RABBY_MOBILE_PUB_DEPLOYMENT - export IOS_BAK_DEPLOYMENT=$RABBY_MOBILE_BAK_DEPLOYMENT + S3_IOS_PUB_DEPLOYMENT=$RABBY_MOBILE_PUB_DEPLOYMENT + export S3_IOS_BAK_DEPLOYMENT=$RABBY_MOBILE_BAK_DEPLOYMENT ;; *) echo "Invalid buildchannel: $buildchannel" @@ -39,17 +39,17 @@ checkout_s3_pub_deployment_params() { esac if [ $BUILD_TARGET_PLATFORM == 'ios' ]; then - if [ -z $IOS_PUB_DEPLOYMENT ]; then - echo "[buildchannel:$buildchannel] IOS_PUB_DEPLOYMENT is not set" + if [ -z $S3_IOS_PUB_DEPLOYMENT ]; then + echo "[buildchannel:$buildchannel] S3_IOS_PUB_DEPLOYMENT is not set" exit 1; fi - if [ -z $IOS_BAK_DEPLOYMENT ]; then - echo "[buildchannel:$buildchannel] IOS_BAK_DEPLOYMENT is not set" + if [ -z $S3_IOS_BAK_DEPLOYMENT ]; then + echo "[buildchannel:$buildchannel] S3_IOS_BAK_DEPLOYMENT is not set" exit 1; fi - export s3_upload_prefix=$(echo "$IOS_PUB_DEPLOYMENT" | sed "s#s3://${RABBY_MOBILE_BUILD_BUCKET}/##g" | cut -d'/' -f2-) + export s3_upload_prefix=$(echo "$S3_IOS_PUB_DEPLOYMENT" | sed "s#s3://${RABBY_MOBILE_BUILD_BUCKET}/##g" | cut -d'/' -f2-) # echo "[debug] s3_upload_prefix is $s3_upload_prefix" export cdn_deployment_urlbase="https://download.rabby.io/$s3_upload_prefix" elif [ $BUILD_TARGET_PLATFORM == 'android' ]; then diff --git a/apps/mobile/scripts/notify-lark.js b/apps/mobile/scripts/notify-lark.js new file mode 100644 index 000000000..5003a4cde --- /dev/null +++ b/apps/mobile/scripts/notify-lark.js @@ -0,0 +1,176 @@ +#!/usr/bin/env node + +const { createHmac } = require('crypto'); +// @see https://www.npmjs.com/package/qrcode +const QRCode = require('qrcode'); +const FormData = require('form-data'); // npm install --save form-data + +const Axios = require('axios'); + +function makeSign(secret) { + const timestamp = Date.now(); + const timeSec = Math.floor(timestamp / 1000); + const stringToSign = `${timeSec}\n${secret}`; + const hash = createHmac('sha256', stringToSign).digest(); + + const Signature = hash.toString('base64'); + + return { + timeSec, + Signature, + }; +} + +const chatURL = process.env.LARK_CHAT_URL; +if (!chatURL) { + throw new Error('LARK_CHAT_URL is not set'); +} +const secret = process.env.LARK_CHAT_SECRET; +if (!secret) { + throw new Error('LARK_CHAT_SECRET is not set'); +} +const accessToken = process.env.RABBY_BOT_LARK_ACCESS_TOKEN; +if (!accessToken) { + throw new Error('RABBY_BOT_LARK_ACCESS_TOKEN is not set'); +} + +async function generateQRCodeImageBuffer(text) { + return new Promise((resolve, reject) => { + QRCode.toBuffer(text, (err, buffer) => { + if (err) { + reject(err); + } else { + resolve(buffer); + } + }); + }); +} + +/** + * @sample + * + curl --location --request POST 'https://open.larksuite.com/open-apis/im/v1/images' \ + --header 'Content-Type: multipart/form-data' \ + --header 'Authorization: Bearer $RABBY_BOT_LARK_ACCESS_TOKEN' \ + --form 'image_type="message"' \ + --form 'image=@file_path' + + response: {"code":0,"data":{"image_key":"key"},"msg":"success"} + */ +async function uploadImageToLark(imageBuffer) { + const headers = { + 'Content-Type': 'multipart/form-data', + Authorization: `Bearer ${process.env.RABBY_BOT_LARK_ACCESS_TOKEN}`, + }; + + const form = new FormData(); + form.append('image_type', 'message'); + form.append('image', imageBuffer); + + const res = await Axios.post( + 'https://open.larksuite.com/open-apis/im/v1/images', + form, + { headers }, + ); + + if (res.data.code !== 0) { + throw new Error('upload image to lark failed'); + } + + return res.data.data.image_key; +} + +// sendMessage with axios +async function sendMessage({ + platform = 'android', + downloadURL = '', + actionsJobUrl = '', + gitCommitURL = '', + gitRefURL = '', + triggers = [], +}) { + const { timeSec, Signature } = makeSign(secret); + + // dedupe + triggers = [...new Set(triggers)]; + + const headers = { + 'Content-Type': 'application/json', + Signature: Signature, + }; + + const platformName = platform + .replace('android', 'Android') + .replace('ios', 'iOS'); + + const qrcodeImgBuf = await generateQRCodeImageBuffer(downloadURL); + const image_key = await uploadImageToLark(qrcodeImgBuf); + + const body = { + timestamp: timeSec, + sign: Signature, + // msg_type: 'text', + // content: { + // text: message, + // }, + msg_type: 'post', + content: { + post: { + zh_cn: { + title: `📱 [${platformName}] Rabby Mobile 预览包已生成 🚀 `, + content: [ + platform !== 'ios' && [ + { tag: 'text', text: `下载链接: ` }, + { tag: 'a', href: downloadURL, text: downloadURL }, + ], + [ + { tag: 'text', text: `二维码,拿 📱 扫一下 🔽` }, + { tag: 'img', image_key }, + ], + // [ + // { tag: 'img', image_key: 'img_1' }, + // ] + [{ tag: 'text', text: `---------` }], + [ + { tag: 'text', text: `Actions Job: ` }, + { tag: 'a', href: actionsJobUrl, text: actionsJobUrl }, + ], + [ + { tag: 'text', text: `Git Commit: ` }, + { tag: 'a', href: gitCommitURL, text: gitCommitURL }, + ], + gitRefURL && [ + { tag: 'text', text: `Git Ref: ` }, + { tag: 'text', text: gitRefURL }, + ], + triggers.length && [ + { tag: 'text', text: `Triggers: ` }, + { tag: 'text', text: triggers.join(', ') }, + ], + ].filter(Boolean), + }, + }, + }, + }; + + const res = await Axios.post(chatURL, body, { headers }); + console.log(res.data); +} + +const args = process.argv.slice(2); + +if (args[0]) { + sendMessage({ + downloadURL: args[0], + platform: args[1], + actionsJobUrl: process.env.GIT_ACTIONS_JOB_URL, + gitCommitURL: process.env.GIT_COMMIT_URL, + gitRefURL: process.env.GIT_REF_URL, + triggers: [ + process.env.GITHUB_TRIGGERING_ACTOR, + process.env.GITHUB_ACTOR, + ].filter(Boolean), + }); +} else { + console.log('[notify-lark] no message'); +} diff --git a/yarn.lock b/yarn.lock index a51af3561..5da017412 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7754,6 +7754,15 @@ __metadata: languageName: node linkType: hard +"@types/qrcode@npm:^1.5.5": + version: 1.5.5 + resolution: "@types/qrcode@npm:1.5.5" + dependencies: + "@types/node": "*" + checksum: d92c1d3e77406bf13a03ec521b2ffb1ac99b2e6ea3a17cad670f2610f62e1293554c57e4074bb2fd4e9369f475f863b69e0ae8c543cb049c4a3c1b0c2d92522a + languageName: node + linkType: hard + "@types/qs@npm:*": version: 6.9.11 resolution: "@types/qs@npm:6.9.11::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40types%2Fqs%2F-%2Fqs-6.9.11.tgz" @@ -25747,6 +25756,20 @@ __metadata: languageName: node linkType: hard +"qrcode@npm:^1.5.3": + version: 1.5.3 + resolution: "qrcode@npm:1.5.3" + dependencies: + dijkstrajs: ^1.0.1 + encode-utf8: ^1.0.3 + pngjs: ^5.0.0 + yargs: ^15.3.1 + bin: + qrcode: bin/qrcode + checksum: 9a8a20a0a9cb1d15de8e7b3ffa214e8b6d2a8b07655f25bd1b1d77f4681488f84d7bae569870c0652872d829d5f8ac4922c27a6bd14c13f0e197bf07b28dead7 + languageName: node + linkType: hard + "qs@npm:6.11.0": version: 6.11.0 resolution: "qs@npm:6.11.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fqs%2F-%2Fqs-6.11.0.tgz" @@ -25911,6 +25934,7 @@ __metadata: "@types/lodash": ^4.14.202 "@types/lodash.debounce": ^4.0.9 "@types/pump": ^1.1.3 + "@types/qrcode": ^1.5.5 "@types/react": ^18.0.24 "@types/react-is": ^18.2.4 "@types/react-native-version-check": ^3.4.8 @@ -25955,6 +25979,7 @@ __metadata: postinstall-postinstall: ^2.1.0 prettier: ^2.7.1 pump: ^3.0.0 + qrcode: ^1.5.3 react: 18.2.0 react-content-loader: ^6.2.1 react-devtools: ^4.28.5