diff --git a/.detoxrc.js b/.detoxrc.js index aeb24fa563..bb2e754737 100644 --- a/.detoxrc.js +++ b/.detoxrc.js @@ -2,146 +2,145 @@ module.exports = { testRunner: { args: { - '$0': 'jest', + $0: 'jest', config: 'e2e/e2e-utils/jest.config.js', - "testTimeout": 1800000, + testTimeout: 1800000, }, jest: { - setupTimeout: 1800000 - } + setupTimeout: 1800000, + }, }, apps: { 'ios.debug': { type: 'ios.app', binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/Zingo.app', - build: "xcodebuild -workspace ios/Zingo.xcworkspace -scheme Zingo -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.5'" + build: + "xcodebuild -workspace ios/Zingo.xcworkspace -scheme Zingo -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.5'", }, 'ios.release': { type: 'ios.app', binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/Zingo.app', - build: "xcodebuild -workspace ios/Zingo.xcworkspace -scheme Zingo -configuration Release -sdk iphonesimulator -derivedDataPath ios/build -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.5'" + build: + "xcodebuild -workspace ios/Zingo.xcworkspace -scheme Zingo -configuration Release -sdk iphonesimulator -derivedDataPath ios/build -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.5'", }, 'android.debug': { type: 'android.apk', binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', - build: 'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -', - reversePorts: [ - 8081 - ] + build: + 'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -', + reversePorts: [8081], }, 'android.debug.x86_64': { type: 'android.apk', binaryPath: 'android/app/build/outputs/apk/debug/app-x86_64-debug.apk', - build: 'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -', - reversePorts: [ - 8081 - ] + build: + 'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -', + reversePorts: [8081], }, 'android.debug.x86': { type: 'android.apk', binaryPath: 'android/app/build/outputs/apk/debug/app-x86-debug.apk', - build: 'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -', - reversePorts: [ - 8081 - ] + build: + 'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -', + reversePorts: [8081], }, 'android.debug.arm64-v8a': { type: 'android.apk', binaryPath: 'android/app/build/outputs/apk/debug/app-arm64-v8a-debug.apk', - build: 'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -', - reversePorts: [ - 8081 - ] + build: + 'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -', + reversePorts: [8081], }, 'android.debug.armeabi-v7a': { type: 'android.apk', - binaryPath: 'android/app/build/outputs/apk/debug/app-armeabi-v7a-debug.apk', - build: 'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -', - reversePorts: [ - 8081 - ] + binaryPath: + 'android/app/build/outputs/apk/debug/app-armeabi-v7a-debug.apk', + build: + 'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -', + reversePorts: [8081], }, 'android.release': { type: 'android.apk', binaryPath: 'android/app/build/outputs/apk/release/app-release.apk', - build: 'cd android ; ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release ; cd -' - } + build: + 'cd android ; ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release ; cd -', + }, }, devices: { simulator: { type: 'ios.simulator', device: { type: 'iPhone 16', - os: 'iOS 18.5' - } + os: 'iOS 18.5', + }, }, attached: { type: 'android.attached', device: { - adbName: '.*' - } + adbName: '.*', + }, }, emulator_x86_64: { type: 'android.emulator', device: { - avdName: 'android-30_google_apis_playstore_x86_64' - } + avdName: 'android-30_google_apis_playstore_x86_64', + }, }, emulator_x86: { type: 'android.emulator', device: { - avdName: 'android-30_google_apis_playstore_x86' - } - } + avdName: 'android-30_google_apis_playstore_x86', + }, + }, }, configurations: { 'ios.sim.debug': { device: 'simulator', - app: 'ios.debug' + app: 'ios.debug', }, 'ios.sim.release': { device: 'simulator', - app: 'ios.release' + app: 'ios.release', }, 'android.att.debug': { device: 'attached', - app: 'android.debug' + app: 'android.debug', }, 'android.att.debug.x86_64': { device: 'attached', - app: 'android.debug.x86_64' + app: 'android.debug.x86_64', }, 'android.att.debug.x86': { device: 'attached', - app: 'android.debug.x86' + app: 'android.debug.x86', }, 'android.att.debug.arm64-v8a': { device: 'attached', - app: 'android.debug.arm64-v8a' + app: 'android.debug.arm64-v8a', }, 'android.att.debug.armeabi-v7a': { device: 'attached', - app: 'android.debug.armeabi-v7a' + app: 'android.debug.armeabi-v7a', }, 'android.att.release': { device: 'attached', - app: 'android.release' + app: 'android.release', }, 'android.emu.debug': { device: 'emulator_x86', - app: 'android.debug' + app: 'android.debug', }, 'android.emu.release': { device: 'emulator_x86', - app: 'android.release' + app: 'android.release', }, 'android.emu.x86_64': { device: 'emulator_x86_64', - app: 'android.debug' + app: 'android.debug', }, 'android.emu.x86': { device: 'emulator_x86', - app: 'android.debug' + app: 'android.debug', }, - } -}; \ No newline at end of file + }, +}; diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index e59934adb5..0000000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -e2e/* \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 9ce7b8a138..0000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - root: true, - extends: ['@react-native'], - plugins: ['@react-native'], -}; diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..5e433ad525 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + ignore: + # Major RN bumps require manual migration + - dependency-name: "react-native" + update-types: ["version-update:semver-major"] + - dependency-name: "react" + update-types: ["version-update:semver-major"] + open-pull-requests-limit: 10 + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 diff --git a/.github/workflows/android-build.yaml b/.github/workflows/android-build.yaml index d175f711bb..9ddb6363c7 100644 --- a/.github/workflows/android-build.yaml +++ b/.github/workflows/android-build.yaml @@ -28,13 +28,13 @@ jobs: run: echo "REPO-OWNER=zingolabs" >> $GITHUB_ENV - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v7 with: repository: ${{ env.REPO-OWNER }}/zingo-mobile - name: Check if native rust cache exists id: native-check-build-cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v6 with: path: android/app/src/main/jniLibs/${{ env.ARCH }} key: native-android-uniffi-${{ env.ARCH }}-${{ env.CACHE-KEY }} @@ -46,7 +46,7 @@ jobs: - name: Check if kotlin uniffi cache exists id: kotlin-check-build-cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v6 with: path: android/app/build/generated/source/uniffi/release/java/uniffi/zingo key: kotlin-android-uniffi-${{ env.ARCH }}-${{ env.CACHE-KEY }} @@ -62,7 +62,7 @@ jobs: if: ${{ needs.android-check-build-cache.outputs.native-cache-found != 'true' || needs.android-check-build-cache.outputs.kotlin-cache-found != 'true' }} runs-on: ubuntu-24.04 container: - image: zingodevops/android_builder:017 + image: zingodevops/android_builder:018 env: RUSTUP_HOME: ${{ github.workspace }}/.rustup CARGO_HOME: ${{ github.workspace }}/.cargo @@ -86,7 +86,7 @@ jobs: run: echo "REPO-OWNER=zingolabs" >> $GITHUB_ENV - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v7 with: repository: ${{ env.REPO-OWNER }}/zingo-mobile @@ -129,7 +129,7 @@ jobs: --out-dir ../../android/app/build/generated/source/uniffi/release/java - name: Upload kotlin uniffi - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: kotlin-android-uniffi-${{ env.ARCH }}-${{ env.CACHE-KEY }} path: android/app/build/generated/source/uniffi/release/java/uniffi/zingo/zingo.kt @@ -179,11 +179,11 @@ jobs: AR: llvm-ar LD: ld RANLIB: llvm-ranlib - CC: ${{ env.CC }}24-clang + CC: ${{ env.CC }}26-clang CARGO_FEATURE_STD: ${{ env.CARGO_FEATURE_STD }} LIBCLANG_PATH: /usr/lib/llvm-18/lib - CARGO_NDK_PLATFORM: 24 - CARGO_NDK_ANDROID_PLATFORM: 24 + CARGO_NDK_PLATFORM: 26 + CARGO_NDK_ANDROID_PLATFORM: 26 - name: LLVM Strip working-directory: ./rust/target @@ -198,7 +198,7 @@ jobs: run: mv ./libzingo.so ./libuniffi_zingo.so - name: Upload native rust - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: native-android-uniffi-${{ env.ARCH }}-${{ env.CACHE-KEY }} path: rust/target/${{ env.TARGET }}/release/libuniffi_zingo.so @@ -213,7 +213,7 @@ jobs: run: echo "REPO-OWNER=zingolabs" >> $GITHUB_ENV - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v7 with: repository: ${{ env.REPO-OWNER }}/zingo-mobile @@ -224,7 +224,7 @@ jobs: path: android/app/src/main/jniLibs/${{ env.ARCH }} - name: Cache native rust - uses: actions/cache/save@v4 + uses: actions/cache/save@v6 with: path: android/app/src/main/jniLibs/${{ env.ARCH }} key: native-android-uniffi-${{ env.ARCH }}-${{ env.CACHE-KEY }} @@ -236,7 +236,7 @@ jobs: path: android/app/build/generated/source/uniffi/release/java/uniffi/zingo - name: Cache kotlin uniffi - uses: actions/cache/save@v4 + uses: actions/cache/save@v6 with: path: android/app/build/generated/source/uniffi/release/java/uniffi/zingo key: kotlin-android-uniffi-${{ env.ARCH }}-${{ env.CACHE-KEY }} diff --git a/.github/workflows/android-release.yaml b/.github/workflows/android-release.yaml new file mode 100644 index 0000000000..bf839a2bec --- /dev/null +++ b/.github/workflows/android-release.yaml @@ -0,0 +1,330 @@ +name: Android Release + +# Builds the 4 ABI APKs + the universal APK on a `zingo-*` tag and attaches +# them to a fresh GitHub Release. +# +# Reliability note: this workflow does NOT read from or write to any +# cross-run `actions/cache`. Rust libraries and uniffi kotlin bindings are +# built from source on the tagged commit and passed between jobs via +# `upload-artifact` (which is run-scoped and disappears after the workflow +# finishes). The gradle `setup-gradle` step keeps Maven dependency caching +# enabled because Maven artifacts are content-addressed and signed — that +# cache cannot affect what code ends up in the APK. + +on: + push: + tags: + - 'zingo-*' + +permissions: + contents: write + +jobs: + uniffi-kotlin: + name: Generate uniffi kotlin bindings + runs-on: ubuntu-24.04 + container: + image: zingodevops/android_builder:018 + env: + RUSTUP_HOME: ${{ github.workspace }}/.rustup + CARGO_HOME: ${{ github.workspace }}/.cargo + steps: + - name: Install libraries + run: | + apt update \ + && apt upgrade -y \ + && apt install -y --no-install-recommends --no-install-suggests \ + build-essential cmake golang clang-18 libclang-18-dev protobuf-compiler gcc g++ + + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Cargo default stable + run: | + rustup update + rustup upgrade + rustup toolchain install stable --profile minimal --component rust-src + rustup default stable + + - name: AWS libcrypto rust building + run: cargo install --force --locked bindgen-cli + + - name: Cargo uniffi kotlin + working-directory: ./rust/lib + run: | + cargo run --release --features=uniffi/cli --bin uniffi-bindgen \ + generate ./src/zingo.udl --language kotlin \ + --out-dir ../../android/app/build/generated/source/uniffi/release/java + + - name: Upload kotlin uniffi + uses: actions/upload-artifact@v7 + with: + name: uniffi-kotlin + path: android/app/build/generated/source/uniffi/release/java/uniffi/zingo/zingo.kt + + native-rust: + name: Build native rust (${{ matrix.arch }}) + runs-on: ubuntu-24.04 + container: + image: zingodevops/android_builder:018 + env: + RUSTUP_HOME: ${{ github.workspace }}/.rustup + CARGO_HOME: ${{ github.workspace }}/.cargo + strategy: + fail-fast: true + matrix: + arch: [x86_64, x86, arm64-v8a, armeabi-v7a] + steps: + - name: Install libraries + run: | + apt update \ + && apt upgrade -y \ + && apt install -y --no-install-recommends --no-install-suggests \ + build-essential cmake golang clang-18 libclang-18-dev protobuf-compiler gcc g++ + + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Cargo default stable + run: | + rustup update + rustup upgrade + rustup toolchain install stable --profile minimal --component rust-src + rustup default stable + + - name: AWS libcrypto rust building + run: cargo install --force --locked bindgen-cli + + - name: Set envs for x86_64 + if: ${{ matrix.arch == 'x86_64' }} + run: | + rustup target add x86_64-linux-android + echo "TARGET=x86_64-linux-android" >> $GITHUB_ENV + echo "CC=x86_64-linux-android" >> $GITHUB_ENV + echo "CARGO_FEATURE_STD=false" >> $GITHUB_ENV + + - name: Set envs for x86 + if: ${{ matrix.arch == 'x86' }} + run: | + rustup target add i686-linux-android + echo "TARGET=i686-linux-android" >> $GITHUB_ENV + echo "CC=i686-linux-android" >> $GITHUB_ENV + echo "CARGO_FEATURE_STD=false" >> $GITHUB_ENV + + - name: Set envs for arm64-v8a + if: ${{ matrix.arch == 'arm64-v8a' }} + run: | + rustup target add aarch64-linux-android + echo "TARGET=aarch64-linux-android" >> $GITHUB_ENV + echo "CC=aarch64-linux-android" >> $GITHUB_ENV + echo "CARGO_FEATURE_STD=true" >> $GITHUB_ENV + + - name: Set envs for armeabi-v7a + if: ${{ matrix.arch == 'armeabi-v7a' }} + run: | + rustup target add armv7-linux-androideabi + echo "TARGET=armv7-linux-androideabi" >> $GITHUB_ENV + echo "CC=armv7a-linux-androideabi" >> $GITHUB_ENV + echo "CARGO_FEATURE_STD=false" >> $GITHUB_ENV + + - name: Cargo NDK + run: cargo install --version 4.0.1 cargo-ndk + + - name: Cargo build + working-directory: ./rust/lib + run: | + cargo ndk \ + --target ${{ env.TARGET }} build \ + --release + env: + AR: llvm-ar + LD: ld + RANLIB: llvm-ranlib + CC: ${{ env.CC }}26-clang + CARGO_FEATURE_STD: ${{ env.CARGO_FEATURE_STD }} + LIBCLANG_PATH: /usr/lib/llvm-18/lib + CARGO_NDK_PLATFORM: 26 + CARGO_NDK_ANDROID_PLATFORM: 26 + + - name: LLVM Strip + working-directory: ./rust/target + run: llvm-strip --strip-all ./${{ env.TARGET }}/release/libzingo.so + + - name: LLVM Obj Copy + working-directory: ./rust/target + run: llvm-objcopy --remove-section .comment ./${{ env.TARGET }}/release/libzingo.so + + - name: Rename the rust lib for uniffi compatibility + working-directory: ./rust/target/${{ env.TARGET }}/release + run: mv ./libzingo.so ./libuniffi_zingo.so + + - name: Upload native rust + uses: actions/upload-artifact@v7 + with: + name: native-rust-${{ matrix.arch }} + path: rust/target/${{ env.TARGET }}/release/libuniffi_zingo.so + + android-release: + name: Assemble APKs and create GitHub Release + runs-on: ubuntu-24.04 + needs: [uniffi-kotlin, native-rust] + steps: + - name: Derive channel + version + build from tag + id: channel + run: | + set -eu + TAG="${GITHUB_REF_NAME}" + + # Tag formats: + # prod: zingo-X.Y.Z-BUILD + # beta: zingo-beta-X.Y.Z-BUILD + BUILD="${TAG##*-}" # last segment + WITHOUT_BUILD="${TAG%-*}" # strip -BUILD + VERSION="${WITHOUT_BUILD##*-}" # next segment + PREFIX="${WITHOUT_BUILD%-*}" # the rest: zingo or zingo-beta + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "build=${BUILD}" >> "$GITHUB_OUTPUT" + echo "prefix=${PREFIX}" >> "$GITHUB_OUTPUT" + + if [[ "$TAG" == zingo-beta-* ]]; then + echo "channel=beta" >> "$GITHUB_OUTPUT" + echo "flavor=beta" >> "$GITHUB_OUTPUT" + echo "gradle_task=assembleBetaRelease" >> "$GITHUB_OUTPUT" + echo "apk_prefix=app-beta" >> "$GITHUB_OUTPUT" + echo "label=Zingo Beta" >> "$GITHUB_OUTPUT" + else + echo "channel=prod" >> "$GITHUB_OUTPUT" + echo "flavor=prod" >> "$GITHUB_OUTPUT" + echo "gradle_task=assembleProdRelease" >> "$GITHUB_OUTPUT" + echo "apk_prefix=app-prod" >> "$GITHUB_OUTPUT" + echo "label=Zingo" >> "$GITHUB_OUTPUT" + fi + + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Free up disk space + run: | + set -eux + df -h + sudo rm -rf /usr/local/graalvm/ \ + /usr/local/share/powershell \ + /usr/local/share/chromium \ + /usr/local/.ghcup \ + /opt/hostedtoolcache/CodeQL \ + /usr/local/lib/android/sdk/ndk \ + /usr/share/dotnet \ + /opt/ghc \ + /usr/local/share/boost + df -h + + - name: Setup Java + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '17' + + # Maven dependency caching only — content-addressed, immutable, signed + # by upstream. No build outputs are cached cross-run. + - name: Gradle setup (Maven dep cache only) + uses: gradle/actions/setup-gradle@v6 + with: + gradle-version: wrapper + cache-read-only: true + + - name: Download native rust (x86_64) + uses: actions/download-artifact@v4 + with: + name: native-rust-x86_64 + path: android/app/src/main/jniLibs/x86_64 + + - name: Download native rust (x86) + uses: actions/download-artifact@v4 + with: + name: native-rust-x86 + path: android/app/src/main/jniLibs/x86 + + - name: Download native rust (arm64-v8a) + uses: actions/download-artifact@v4 + with: + name: native-rust-arm64-v8a + path: android/app/src/main/jniLibs/arm64-v8a + + - name: Download native rust (armeabi-v7a) + uses: actions/download-artifact@v4 + with: + name: native-rust-armeabi-v7a + path: android/app/src/main/jniLibs/armeabi-v7a + + - name: Download kotlin uniffi + uses: actions/download-artifact@v4 + with: + name: uniffi-kotlin + path: android/app/build/generated/source/uniffi/release/java/uniffi/zingo + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.18.0 + cache: yarn + + - name: Yarn install + run: yarn install --frozen-lockfile + + - name: Assemble APKs (4 ABI splits + universal) + working-directory: android + run: ./gradlew ${{ steps.channel.outputs.gradle_task }} -PsplitApk=true -PincludeUniversalApk=true + + - name: Collect APKs + run: | + set -eu + SRC="android/app/build/outputs/apk/${{ steps.channel.outputs.flavor }}/release" + DST="release-artifacts" + mkdir -p "$DST" + IN_PREFIX="${{ steps.channel.outputs.apk_prefix }}" + OUT_PREFIX="${{ steps.channel.outputs.prefix }}" + VERSION="${{ steps.channel.outputs.version }}" + BUILD="${{ steps.channel.outputs.build }}" + for abi in armeabi-v7a arm64-v8a x86 x86_64 universal; do + src_file="$SRC/${IN_PREFIX}-${abi}-release.apk" + dst_file="$DST/${OUT_PREFIX}-${abi}-${VERSION}-${BUILD}.apk" + if [[ ! -f "$src_file" ]]; then + echo "missing expected APK: $src_file" >&2 + ls -la "$SRC" >&2 || true + exit 1 + fi + cp "$src_file" "$dst_file" + echo " $dst_file" + done + ls -la "$DST" + + - name: Generate release notes + id: notes + uses: actions/github-script@v9 + with: + script: | + const tag = context.ref.replace('refs/tags/', ''); + const { data } = await github.rest.repos.generateReleaseNotes({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: tag, + }); + return data.body; + result-encoding: string + + - name: Create GitHub Release + uses: softprops/action-gh-release@v3 + with: + name: ${{ steps.channel.outputs.label }} ${{ github.ref_name }} + body: | + ${{ steps.notes.outputs.result }} + + ## Zingolabs + * @Oscar-Pepper @fluidvanadium @AloeareV @idky137 @dorianvp @nachog00 @karentaponte and @zancas making the magic behind the scenes. + + ## Manual Testing + * Android & IOS by @edicksonjga & @juanky201271. + files: release-artifacts/*.apk + draft: false + prerelease: ${{ steps.channel.outputs.channel == 'beta' }} diff --git a/.github/workflows/android-ubuntu-integration-test-ci.yaml b/.github/workflows/android-ubuntu-integration-test-ci.yaml index 846a9dd8c9..e365fc2f40 100644 --- a/.github/workflows/android-ubuntu-integration-test-ci.yaml +++ b/.github/workflows/android-ubuntu-integration-test-ci.yaml @@ -38,7 +38,7 @@ jobs: run: echo "REPO-OWNER=zingolabs" >> $GITHUB_ENV - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v7 with: repository: ${{ env.REPO-OWNER }}/zingo-mobile @@ -69,20 +69,20 @@ jobs: sudo udevadm trigger --name-match=kvm - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' - name: Gradle setup cache (write-only) - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v6 with: gradle-version: wrapper cache-write-only: true cache-read-only: false - name: Check AVD cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v6 id: avd-cache with: path: | @@ -99,13 +99,13 @@ jobs: arch: ${{ env.AVD-ARCH }} target: ${{ env.TARGET }} force-avd-creation: true - emulator-options: -no-snapshot-load -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + emulator-options: -no-snapshot-load -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-metrics disable-animations: false script: echo "Generated AVD snapshot for caching." - name: Save AVD cache if: steps.avd-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 + uses: actions/cache/save@v6 id: avd-cache-saving with: path: | @@ -126,31 +126,31 @@ jobs: run: echo "REPO-OWNER=zingolabs" >> $GITHUB_ENV - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v7 with: repository: ${{ env.REPO-OWNER }}/zingo-mobile - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' - name: Gradle setup cache (read/write) - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v6 with: gradle-version: wrapper cache-read-only: false - name: Native rust cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v6 with: path: android/app/src/main/jniLibs/${{ env.ABI }} key: native-android-uniffi-${{ env.ABI }}-${{ env.CACHE-KEY }} fail-on-cache-miss: true - name: Kotlin uniffi cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v6 with: path: android/app/build/generated/source/uniffi/release/java/uniffi/zingo key: kotlin-android-uniffi-${{ env.ABI }}-${{ env.CACHE-KEY }} @@ -182,21 +182,27 @@ jobs: sudo rm -rf /opt/ghc sudo rm -rf /usr/local/share/boost - sudo docker image prune --all --force || true - sudo docker builder prune -a || true + podman image prune --all --force || true + podman system prune --all --force || true echo "Disk space after cleanup:" df -h + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.18.0 + cache: yarn + - name: Yarn install run: yarn - name: Run Build working-directory: android - run: ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release -PsplitApk=true + run: ./gradlew assembleProdRelease assembleProdReleaseAndroidTest -DtestBuildType=release -PsplitApk=true - name: Upload Android build artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: android-build-outputs-${{ env.ABI }}-${{ env.CACHE-KEY }} path: | @@ -231,7 +237,7 @@ jobs: run: echo "REPO-OWNER=zingolabs" >> $GITHUB_ENV - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v7 with: repository: ${{ env.REPO-OWNER }}/zingo-mobile @@ -261,7 +267,6 @@ jobs: - name: Grant permissions run: | - sudo chmod 777 /var/run/docker.sock sudo chmod 777 ./scripts/ci/android_integration_tests_ci.sh - name: Enable KVM @@ -287,20 +292,20 @@ jobs: tool: nextest - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' - name: Gradle setup cache (read-only) - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v6 with: gradle-version: wrapper cache-write-only: false cache-read-only: true - name: Restore AVD cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v6 with: path: | ~/.android/avd/* @@ -334,12 +339,18 @@ jobs: sudo rm -rf /opt/ghc sudo rm -rf /usr/local/share/boost - sudo docker image prune --all --force || true - sudo docker builder prune -a || true + podman image prune --all --force || true + podman system prune --all --force || true echo "Disk space after cleanup:" df -h + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.18.0 + cache: yarn + - name: Yarn install run: yarn @@ -349,21 +360,63 @@ jobs: name: android-build-outputs-${{ env.ABI }}-${{ env.CACHE-KEY }} path: android/app/build/outputs - #- name: Pull docker image - # run: docker pull zingodevops/regchest:013 + # Resolve the zingolib commit the same way android-build does: the + # GITHUB_SHA under zingolib CI, otherwise the pinned rev in + # rust/Cargo.toml. The ci-build image tag is content-addressed from + # that commit's .env.testing-artifacts + rust-toolchain.toml + + # docker-ci/, so deriving it here keeps the zainod/zebrad binaries in + # lockstep with the zingolib we build against (a fixed tag like 011 + # drifts out of sync). + - name: Resolve zingolib rev + run: | + set -euo pipefail + if [[ "${{ github.repository }}" == *zingolib* ]]; then + REV="${GITHUB_SHA}" + else + line="$(grep -E '^zingolib[[:space:]]*=' rust/Cargo.toml)" + # Prefer an explicit rev pin if present. + REV="$(echo "$line" \ + | grep -oE 'rev[[:space:]]*=[[:space:]]*"[0-9a-f]+"' \ + | grep -oE '[0-9a-f]{7,40}' | head -1 || true)" + # Otherwise the dep is pinned by branch/tag: resolve it to a SHA. + if [[ -z "$REV" ]]; then + url="$(echo "$line" | grep -oE 'git[[:space:]]*=[[:space:]]*"[^"]+"' | sed -E 's/.*"([^"]+)".*/\1/')" + ref="$(echo "$line" | grep -oE '(branch|tag)[[:space:]]*=[[:space:]]*"[^"]+"' | sed -E 's/.*"([^"]+)".*/\1/')" + echo "Resolving zingolib ${ref} from ${url} via git ls-remote" + REV="$(git ls-remote "$url" "$ref" | awk 'NR==1{print $1}')" + fi + fi + test -n "$REV" + echo "ZINGOLIB_REV=$REV" >> "$GITHUB_ENV" + echo "Resolved zingolib rev: $REV" + + - name: Install cargo-make + uses: taiki-e/install-action@v2 + with: + tool: cargo-make + + - name: Compute ci-build image tag from that zingolib commit + run: | + set -euo pipefail + git clone --filter=blob:none https://github.com/zingolabs/zingolib zingolib-src + git -C zingolib-src checkout "$ZINGOLIB_REV" + TAG="$(cd zingolib-src && cargo make compute-image-tag | grep -E '^[0-9a-f]{14}$')" + test -n "$TAG" + echo "CI_BUILD_TAG=$TAG" >> "$GITHUB_ENV" + echo "Computed ci-build tag: $TAG" - - name: Pull ci-build docker image - run: docker pull zingodevops/ci-build:011 + - name: Pull ci-build image + run: podman pull docker.io/zingodevops/ci-build:${CI_BUILD_TAG} - name: Extract binaries & params from image run: | set -euxo pipefail - cid=$(docker create zingodevops/ci-build:011) + cid=$(podman create docker.io/zingodevops/ci-build:${CI_BUILD_TAG}) mkdir -p rust/test_binaries/bins - docker cp "$cid":/usr/bin/lightwalletd rust/test_binaries/bins/ - docker cp "$cid":/usr/bin/zcashd rust/test_binaries/bins/ - docker cp "$cid":/usr/bin/zcash-cli rust/test_binaries/bins/ - docker rm "$cid" + podman cp "$cid":/usr/bin/lightwalletd rust/test_binaries/bins/ + podman cp "$cid":/usr/bin/zainod rust/test_binaries/bins/ + podman cp "$cid":/usr/bin/zebrad rust/test_binaries/bins/ + podman rm "$cid" - name: binaries env var run: | @@ -377,7 +430,7 @@ jobs: arch: ${{ env.AVD-ARCH }} target: ${{ env.TARGET }} force-avd-creation: true - emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-metrics disable-animations: true script: cargo nextest run android_integration::${{ env.NEXTEST-ABI }}::${{ matrix.test }} --features "ci" --verbose --release working-directory: ./rust @@ -386,7 +439,7 @@ jobs: - name: Upload test reports if: ${{ always() && !cancelled() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: test-android-integration-reports-${{ env.ABI }}-${{ matrix.test }}-${{ env.TIMESTAMP }} path: android/app/build/outputs/integration_test_reports diff --git a/.github/workflows/ci-nightly.yaml b/.github/workflows/ci-nightly.yaml index 368be02cb8..85b3b8eda8 100644 --- a/.github/workflows/ci-nightly.yaml +++ b/.github/workflows/ci-nightly.yaml @@ -8,6 +8,7 @@ on: jobs: jest-test: uses: ./.github/workflows/jest-test.yaml + secrets: inherit create-timestamp: uses: ./.github/workflows/create-timestamp.yaml @@ -35,6 +36,7 @@ jobs: fail-fast: false uses: ./.github/workflows/android-ubuntu-integration-test-ci.yaml needs: [create-timestamp, create-cache-key, android-build] + secrets: inherit with: timestamp: ${{ needs.create-timestamp.outputs.timestamp }} cache-key: ${{ needs.create-cache-key.outputs.cache-key }} @@ -55,5 +57,6 @@ jobs: fail-fast: false uses: ./.github/workflows/ios-integration-test.yaml needs: [ create-cache-key, ios-build ] + secrets: inherit with: cache-key: ${{ needs.create-cache-key.outputs.cache-key }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f9ad997d0d..a9c76dd031 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,6 +9,7 @@ on: jobs: jest-test: uses: ./.github/workflows/jest-test.yaml + secrets: inherit create-timestamp: uses: ./.github/workflows/create-timestamp.yaml @@ -34,6 +35,7 @@ jobs: - { abi: x86, api-level: 29, target: default } uses: ./.github/workflows/android-ubuntu-integration-test-ci.yaml needs: [create-timestamp, create-cache-key, android-build] + secrets: inherit with: timestamp: ${{ needs.create-timestamp.outputs.timestamp }} cache-key: ${{ needs.create-cache-key.outputs.cache-key }} @@ -50,5 +52,6 @@ jobs: ios-integration-test: uses: ./.github/workflows/ios-integration-test.yaml needs: [create-cache-key, ios-build] + secrets: inherit with: cache-key: ${{ needs.create-cache-key.outputs.cache-key }} diff --git a/.github/workflows/create-cache-key.yaml b/.github/workflows/create-cache-key.yaml index b3854f48c1..2f4b839fe1 100644 --- a/.github/workflows/create-cache-key.yaml +++ b/.github/workflows/create-cache-key.yaml @@ -22,7 +22,7 @@ jobs: run: echo "REPO-OWNER=zingolabs" >> $GITHUB_ENV - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v7 with: repository: ${{ env.REPO-OWNER }}/zingo-mobile diff --git a/.github/workflows/inactive/android-macos-integration-test.yaml b/.github/workflows/inactive/android-macos-integration-test.yaml deleted file mode 100644 index 2ab3b665b7..0000000000 --- a/.github/workflows/inactive/android-macos-integration-test.yaml +++ /dev/null @@ -1,187 +0,0 @@ -name: Android Integration test - -on: - workflow_call: - inputs: - timestamp: - required: true - type: string - cache-key: - required: true - type: string - abi: - required: true - type: string - -env: - CACHE-KEY: ${{ inputs.cache-key }} - TIMESTAMP: ${{ inputs.timestamp }} - ABI: ${{ inputs.abi }} - REPO-OWNER: ${{ github.repository_owner }} - RUSTFLAGS: -D warnings - -jobs: - android-build-test-artifacts: - name: Android Build test artifacts - runs-on: macos-15 - steps: - - name: Set envs for zingolib CI - if: ${{ contains(github.repository, 'zingolib') }} - run: echo "REPO-OWNER=zingolabs" >> $GITHUB_ENV - - - name: Checkout repository - uses: actions/checkout@v5 - with: - repository: ${{ env.REPO-OWNER }}/zingo-mobile - - - name: Install protoc - run: brew install protobuf - - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: stable - cache-workspaces: rust -> rust/target - - - name: Install nextest - uses: taiki-e/install-action@nextest - - - name: Build and archive tests - working-directory: ./rust - run: cargo nextest archive --verbose --features "ci" --archive-file nextest-archive-${{ env.ABI }}.tar.zst - - - name: Upload archive - uses: actions/upload-artifact@v4 - with: - name: nextest-archive-${{ env.ABI }} - path: ./rust/nextest-archive-${{ env.ABI }}.tar.zst - - android-integration-test: - name: Android Integration test - runs-on: macos-15 - needs: android-build-test-artifacts - strategy: - fail-fast: false - matrix: - partition: [1, 2, 3, 4, 5, 6] - steps: - - name: Set envs for zingolib CI - if: ${{ contains(github.repository, 'zingolib') }} - run: echo "REPO-OWNER=zingolabs" >> $GITHUB_ENV - - - name: Checkout repository - uses: actions/checkout@v5 - with: - repository: ${{ env.REPO-OWNER }}/zingo-mobile - - - name: Set envs for x86_64 - if: ${{ env.ABI == 'x86_64' }} - run: | - echo "AVD-ARCH=x86_64" >> $GITHUB_ENV - echo "NEXTEST-ABI=x86_64" >> $GITHUB_ENV - - - name: Set envs for x86 - if: ${{ env.ABI == 'x86' }} - run: | - echo "AVD-ARCH=x86" >> $GITHUB_ENV - echo "NEXTEST-ABI=x86_32" >> $GITHUB_ENV - - - name: Set envs for arm64-v8a - if: ${{ env.ABI == 'arm64-v8a' }} - run: | - echo "AVD-ARCH=x86_64" >> $GITHUB_ENV - echo "NEXTEST-ABI=arm64" >> $GITHUB_ENV - - - name: Set envs for armeabi-v7a - if: ${{ env.ABI == 'armeabi-v7a' }} - run: | - echo "AVD-ARCH=x86" >> $GITHUB_ENV - echo "NEXTEST-ABI=arm32" >> $GITHUB_ENV - - - name: Install docker - run: | - brew install --head colima - brew install --formula docker - colima start - - - name: Colima status - run: colima status - - - name: Pull regchest docker image - run: docker pull zingodevops/regchest:013 - - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: stable - cache-workspaces: rust -> rust/target - - - name: Install nextest - uses: taiki-e/install-action@nextest - - - name: Native rust cache - uses: actions/cache/restore@v4 - with: - path: android/app/src/main/jniLibs/${{ env.ABI }} - key: native-android-uniffi-${{ env.ABI }}-${{ env.CACHE-KEY }} - fail-on-cache-miss: true - - - name: Kotlin uniffi cache - uses: actions/cache/restore@v4 - with: - path: android/app/build/generated/source/uniffi/release/java/uniffi/zingo - key: kotlin-android-uniffi-${{ env.ABI }}-${{ env.CACHE-KEY }} - fail-on-cache-miss: true - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - - - name: Setup Android SDK - uses: android-actions/setup-android@v3 - - - name: Add emulator to PATH - run: echo "${ANDROID_SDK_ROOT}/emulator" >> $GITHUB_PATH - - - name: Gradle setup - uses: gradle/actions/setup-gradle@v4 - with: - gradle-version: wrapper - cache-disabled: true - - - name: AVD cache - uses: actions/cache@v4 - id: avd-cache - with: - path: | - ~/.android/avd/* - ~/.android/adb* - key: avd-${{ env.AVD-ARCH }}-integ - - - name: Create AVD and generate snapshot for caching - if: steps.avd-cache.outputs.cache-hit != 'true' - uses: nick-fields/retry@v2 - with: - timeout_minutes: 60 - max_attempts: 3 - command: ./scripts/android_integration_tests.sh -a ${{ env.AVD-ARCH }} -s - - - name: Download archive - uses: actions/download-artifact@v4 - with: - name: nextest-archive-${{ env.ABI }} - path: ./rust - - - name: Run integration tests - working-directory: ./rust - run: | - cargo nextest run android_integration::${{ env.NEXTEST-ABI }} --verbose --profile ci \ - --archive-file nextest-archive-${{ env.ABI }}.tar.zst \ - --partition count:${{ matrix.partition}}/6 - - - name: Upload test reports - if: ${{ always() && !cancelled() }} - uses: actions/upload-artifact@v4 - with: - name: test-android-reports-${{ env.ABI }}-${{ matrix.partition}}-${{ env.TIMESTAMP }} - path: android/app/build/outputs/integration_test_reports \ No newline at end of file diff --git a/.github/workflows/inactive/android-ubuntu-e2e-test-ci.yaml b/.github/workflows/inactive/android-ubuntu-e2e-test-ci.yaml deleted file mode 100644 index d6ecd9beb7..0000000000 --- a/.github/workflows/inactive/android-ubuntu-e2e-test-ci.yaml +++ /dev/null @@ -1,300 +0,0 @@ -name: Android Ubuntu e2e tests - -on: - workflow_call: - inputs: - timestamp: - required: true - type: string - cache-key: - required: true - type: string - abi: - required: true - type: string - api-level: - required: true - type: string - target: - required: true - type: string - -env: - REPO-OWNER: ${{ github.repository_owner }} - ABI: ${{ inputs.abi }} - API-LEVEL: ${{ inputs.api-level }} - TARGET: ${{ inputs.target }} - TIMESTAMP: ${{ inputs.timestamp }} - -jobs: - android-ubuntu-e2e-test-ci-avd-cache: - name: Avd Cache e2e - runs-on: ubuntu-24.04 - env: - CACHE-KEY: ${{ inputs.cache-key }} - steps: - - name: Set envs for zingolib CI - if: ${{ contains(github.repository, 'zingolib') }} - run: echo "REPO-OWNER=zingolabs" >> $GITHUB_ENV - - - name: Checkout repository - uses: actions/checkout@v5 - with: - repository: ${{ env.REPO-OWNER }}/zingo-mobile - - - name: Set envs for x86_64 - if: ${{ env.ABI == 'x86_64' }} - run: | - echo "AVD-ARCH=x86_64" >> $GITHUB_ENV - - - name: Set envs for x86 - if: ${{ env.ABI == 'x86' }} - run: | - echo "AVD-ARCH=x86" >> $GITHUB_ENV - - - name: Set envs for arm64-v8a - if: ${{ env.ABI == 'arm64-v8a' }} - run: | - echo "AVD-ARCH=arm64-v8a" >> $GITHUB_ENV - - - name: Set envs for armeabi-v7a - if: ${{ env.ABI == 'armeabi-v7a' }} - run: | - echo "AVD-ARCH=arm64-v8a" >> $GITHUB_ENV - - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - name: JVM Heap Size - run: export _JAVA_OPTIONS="-Xmx4g" - - - name: Monitor memory usage - run: top -b -n1 | head -n 20 - - - name: Check AVD cache - uses: actions/cache/restore@v4 - id: avd-cache - with: - path: | - ~/.android/avd/* - ~/.android/adb* - key: avd-${{ env.ABI }}-api-${{ env.API-LEVEL }}-e2e - lookup-only: true - - - name: Create AVD and generate snapshot for caching - if: steps.avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ env.API-LEVEL }} - arch: ${{ env.AVD-ARCH }} - target: ${{ env.TARGET }} - force-avd-creation: true - emulator-options: -no-snapshot-load -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - disable-animations: false - script: echo "Generated AVD snapshot for caching." - ram-size: 4096M - heap-size: 512M - disk-size: 4096M - pre-emulator-launch-script: top -b -n1 | head -n 20 - - - name: Save AVD cache - if: steps.avd-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 - id: avd-cache-saving - with: - path: | - ~/.android/avd/* - ~/.android/adb* - key: avd-${{ env.ABI }}-api-${{ env.API-LEVEL }}-e2e - - android-ubuntu-e2e-test-ci: - name: Android Ubuntu e2e test - needs: android-ubuntu-e2e-test-ci-avd-cache - runs-on: ubuntu-24.04 - strategy: - fail-fast: false - matrix: - test: [ - "parse_invalid_address", - "tex_send_address", - "reload_while_tx_pending", - "change_custom_server", - "change_custom_regtest_server", - "change_custom_testnet_server", - "change_server_from_list", - "new_wallet", - "screen_awake", - "send", - "sync_report", - "transaction_history", - "shielding" - ] - env: - CACHE-KEY: ${{ inputs.cache-key }} - steps: - - name: Set envs for zingolib CI - if: ${{ contains(github.repository, 'zingolib') }} - run: echo "REPO-OWNER=zingolabs" >> $GITHUB_ENV - - - name: Checkout repository - uses: actions/checkout@v5 - with: - repository: ${{ env.REPO-OWNER }}/zingo-mobile - - - name: Set envs for x86_64 - if: ${{ env.ABI == 'x86_64' }} - run: | - echo "AVD-ARCH=x86_64" >> $GITHUB_ENV - echo "NEXTEST-ABI=x86_64" >> $GITHUB_ENV - - - name: Set envs for x86 - if: ${{ env.ABI == 'x86' }} - run: | - echo "AVD-ARCH=x86" >> $GITHUB_ENV - echo "NEXTEST-ABI=x86_32" >> $GITHUB_ENV - - - name: Set envs for arm64-v8a - if: ${{ env.ABI == 'arm64-v8a' }} - run: | - echo "AVD-ARCH=arm64-v8a" >> $GITHUB_ENV - echo "NEXTEST-ABI=arm64" >> $GITHUB_ENV - - - name: Set envs for armeabi-v7a - if: ${{ env.ABI == 'armeabi-v7a' }} - run: | - echo "AVD-ARCH=arm64-v8a" >> $GITHUB_ENV - echo "NEXTEST-ABI=arm32" >> $GITHUB_ENV - - - name: Remove unnecessary directories to free up space - run: | - echo "Disk space before post-build cleanup:" - df -h - sudo docker builder prune -f - sudo docker system prune -a -f - sudo rm -rf /usr/local/.ghcup - sudo rm -rf /opt/hostedtoolcache/CodeQL - sudo rm -rf /usr/local/lib/android/sdk/ndk - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf /usr/local/share/boost - sudo apt-get clean - echo "Disk space after post-build cleanup:" - df -h - - - name: Grant permissions - run: | - sudo chmod 777 /var/run/docker.sock - sudo chmod 777 ./scripts/ci/e2e_tests_ci.sh - - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - name: Install protoc - run: | - sudo apt update - sudo apt install -y protobuf-compiler - - - name: Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: stable - cache-workspaces: rust -> rust/target - - - name: Install nextest - uses: taiki-e/install-action@v2 - with: - tool: nextest - - - name: Pull regchest docker image - run: docker pull zingodevops/regchest:013 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - - - name: Gradle setup - uses: gradle/actions/setup-gradle@v4 - with: - gradle-version: wrapper - cache-disabled: true - - - name: Native rust cache - uses: actions/cache/restore@v4 - with: - path: android/app/src/main/jniLibs/${{ env.ABI }} - key: native-android-uniffi-${{ env.ABI }}-${{ env.CACHE-KEY }} - fail-on-cache-miss: true - - - name: Kotlin uniffi cache - uses: actions/cache/restore@v4 - with: - path: android/app/build/generated/source/uniffi/release/java/uniffi/zingo - key: kotlin-android-uniffi-${{ env.ABI }}-${{ env.CACHE-KEY }} - fail-on-cache-miss: true - - - name: Restore AVD cache - uses: actions/cache/restore@v4 - with: - path: | - ~/.android/avd/* - ~/.android/adb* - key: avd-${{ env.ABI }}-api-${{ env.API-LEVEL }}-e2e - fail-on-cache-miss: true - - - name: Yarn install - run: yarn - - - name: Run Build - working-directory: android - run: ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release -PsplitApk=true - - - name: Remove unnecessary directories to free up space after build - run: | - echo "Disk space before post-build cleanup:" - df -h - sudo rm -rf /usr/local/.ghcup - sudo rm -rf /opt/hostedtoolcache/CodeQL - sudo rm -rf /usr/local/lib/android/sdk/ndk - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf /usr/local/share/boost - sudo apt-get clean - echo "Disk space after post-build cleanup:" - df -h - - - name: JVM Heap Size - run: export _JAVA_OPTIONS="-Xmx4g" - - - name: Monitor memory usage - run: top -b -n1 | head -n 20 - - - name: Run e2e Test - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ env.API-LEVEL }} - arch: ${{ env.AVD-ARCH }} - target: ${{ env.TARGET }} - force-avd-creation: true - emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - disable-animations: true - script: cargo nextest run e2e::${{ env.NEXTEST-ABI }}::${{ matrix.test }} --features "ci regchest" --verbose --release - working-directory: ./rust - ram-size: 4096M - heap-size: 512M - disk-size: 4096M - pre-emulator-launch-script: top -b -n1 | head -n 20 - - - name: Upload test reports - if: ${{ always() && !cancelled() }} - uses: actions/upload-artifact@v4 - with: - name: test-android-e2e-reports-${{ env.ABI }}-${{ matrix.test }}-${{ env.TIMESTAMP }} - path: android/app/build/outputs/e2e_test_reports diff --git a/.github/workflows/inactive/android-ubuntu-foss-artifact.yaml b/.github/workflows/inactive/android-ubuntu-foss-artifact.yaml deleted file mode 100644 index bd0cf1511f..0000000000 --- a/.github/workflows/inactive/android-ubuntu-foss-artifact.yaml +++ /dev/null @@ -1,94 +0,0 @@ -name: Android Ubuntu FOSS Artifact - -on: - workflow_call: - inputs: - cache-key: - required: true - type: string - -env: - REPO-OWNER: ${{ github.repository_owner }} - ANDROID_SDK_ROOT: /usr/local/lib/android/sdk - -jobs: - android-ubuntu-foss-artifact: - name: Android Ubuntu Foss Artifact - runs-on: ubuntu-24.04 - env: - CACHE-KEY: ${{ inputs.cache-key }} - steps: - - name: Set envs for zingolib CI - if: ${{ contains(github.repository, 'zingolib') }} - run: echo "REPO-OWNER=zingolabs" >> $GITHUB_ENV - - - name: Checkout repository - uses: actions/checkout@v5 - with: - repository: ${{ env.REPO-OWNER }}/zingo-mobile - - - name: Gradle setup - uses: gradle/actions/setup-gradle@v4 - with: - gradle-version: wrapper - cache-disabled: true - - - name: Native rust cache arm64-v8a - uses: actions/cache/restore@v4 - with: - path: android/app/src/main/jniLibs/arm64-v8a - key: native-android-uniffi-arm64-v8a-${{ env.CACHE-KEY }} - fail-on-cache-miss: true - - - name: Native rust cache armeabi-v7a - uses: actions/cache/restore@v4 - with: - path: android/app/src/main/jniLibs/armeabi-v7a - key: native-android-uniffi-armeabi-v7a-${{ env.CACHE-KEY }} - fail-on-cache-miss: true - - - name: Native rust cache x86 - uses: actions/cache/restore@v4 - with: - path: android/app/src/main/jniLibs/x86 - key: native-android-uniffi-x86-${{ env.CACHE-KEY }} - fail-on-cache-miss: true - - - name: Native rust cache x86_64 - uses: actions/cache/restore@v4 - with: - path: android/app/src/main/jniLibs/x86_64 - key: native-android-uniffi-x86_64-${{ env.CACHE-KEY }} - fail-on-cache-miss: true - - - name: Kotlin uniffi cache - uses: actions/cache/restore@v4 - with: - path: android/app/build/generated/source/uniffi/release/java/uniffi/zingo - key: kotlin-android-uniffi-x86_64-${{ env.CACHE-KEY }} - fail-on-cache-miss: true - - - name: Yarn install - run: yarn - - - name: Yarn remove vision-camera library - run: yarn remove react-native-vision-camera || true - - - name: Restore AVD cache - uses: actions/cache/restore@v4 - with: - path: | - ~/.android/avd/* - ~/.android/adb* - key: avd-x86_64-api-34-integ - fail-on-cache-miss: true - - - name: Run Build the artifact - working-directory: ./android - run: ./gradlew assembleRelease -DtestBuildType=release -PsplitApk=false - - - name: Upload the Artifact - uses: actions/upload-artifact@v4 - with: - name: app-release.apk - path: android/app/build/outputs/apk/release/app-release.apk diff --git a/.github/workflows/inactive/android-ubuntu-foss-script-artifact.yaml b/.github/workflows/inactive/android-ubuntu-foss-script-artifact.yaml deleted file mode 100644 index 3bd2304c1a..0000000000 --- a/.github/workflows/inactive/android-ubuntu-foss-script-artifact.yaml +++ /dev/null @@ -1,107 +0,0 @@ -name: Android Ubuntu FOSS Script Artifact - -on: - workflow_call: - inputs: - -env: - REPO-OWNER: ${{ github.repository_owner }} - ZINGO_JKS_BASE64: ${{ secrets.ZINGO_JKS_BASE64 }} - KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} - KEY_ALIAS: ${{ secrets.KEY_ALIAS }} - KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} - -jobs: - android-ubuntu-foss-script-artifact: - name: Android Ubuntu Foss Script Artifact - runs-on: ubuntu-24.04 - container: - image: registry.gitlab.com/fdroid/fdroidserver:buildserver-bookworm - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - repository: ${{ env.REPO-OWNER }}/zingo-mobile - - - name: init - run: | - sudo chown -R $(whoami) . - sudo apt-get update - sudo apt-get dist-upgrade - sudo sdkmanager "platforms;android-31" - sudo sdkmanager "platform-tools" "build-tools;31.0.0" - sudo apt-get install -y openjdk-17-jdk-headless - sudo update-alternatives --set java /usr/lib/jvm/java-17-openjdk-amd64/bin/java - ls -la /opt/android-sdk/platforms - rm -rf /opt/android-sdk/platforms/android-30 - - - name: Node JS & build script - working-directory: rust/lib - run: | - sudo apt-get update && apt-get install pkg-config build-essential apt-utils libpixman-1-dev \ - libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev librsvg2-dev - sudo curl -Lo node.tar.gz https://nodejs.org/dist/v22.18.0/node-v22.18.0-linux-x64.tar.gz - sudo echo "eb5b031bdd728871c3b9a82655dbfa533bc262c0b6da1d09a86842430cef07d4 node.tar.gz" | sha256sum -c - - sudo tar xzf node.tar.gz --strip-components=1 -C /usr/local/ - sudo npm install --global yarn - sudo bash ../build_fdroid.sh - - - name: Yarn install - run: npm_config_build_from_source=true sudo yarn install - - - name: Yarn remove vision-camera library - run: sudo yarn remove react-native-vision-camera || true - - - name: local.properties Android SDK path - working-directory: android - run: sudo echo "sdk.dir=/opt/android-sdk" > local.properties - - - name: Gradle clean - working-directory: android - run: sudo ./gradlew clean - - - name: Copy native & kotlin libs - working-directory: rust/lib - run: | - sudo mkdir -p ../../android/app/src/main/jniLibs/arm64-v8a - sudo mkdir -p ../../android/app/src/main/jniLibs/armeabi-v7a - sudo mkdir -p ../../android/app/src/main/jniLibs/x86 - sudo mkdir -p ../../android/app/src/main/jniLibs/x86_64 - sudo mkdir -p ../../android/app/build/generated/source/uniffi/debug/java/uniffi/zingo - sudo mkdir -p ../../android/app/build/generated/source/uniffi/release/java/uniffi/zingo - sudo cp /opt/jniLibs/x86_64/libuniffi_zingo.so ../../android/app/src/main/jniLibs/x86_64/libuniffi_zingo.so - sudo cp /opt/jniLibs/x86/libuniffi_zingo.so ../../android/app/src/main/jniLibs/x86/libuniffi_zingo.so - sudo cp /opt/jniLibs/armeabi-v7a/libuniffi_zingo.so ../../android/app/src/main/jniLibs/armeabi-v7a/libuniffi_zingo.so - sudo cp /opt/jniLibs/arm64-v8a/libuniffi_zingo.so ../../android/app/src/main/jniLibs/arm64-v8a/libuniffi_zingo.so - sudo cp /opt/jniLibs/kotlin/zingo.kt ../../android/app/build/generated/source/uniffi/debug/java/uniffi/zingo/zingo.kt - sudo cp /opt/jniLibs/kotlin/zingo.kt ../../android/app/build/generated/source/uniffi/release/java/uniffi/zingo/zingo.kt - - - name: Decoding Keystore File & Save - working-directory: android/app - run: sudo echo "${{ env.ZINGO_JKS_BASE64 }}" | base64 --decode > Zingo.jks - - - name: Set up Keystore properties - working-directory: android - run: | - sudo echo "KEYSTORE_PASSWORD=${{ env.KEYSTORE_PASSWORD }}" > local.zingo.jks.properties - sudo echo "KEY_ALIAS=${{ env.KEY_ALIAS }}" >> local.zingo.jks.properties - sudo echo "KEY_PASSWORD=${{ env.KEY_PASSWORD }}" >> local.zingo.jks.properties - - - name: Build release Signed APK Artifact - working-directory: android - run: | - export SOURCE_DATE_EPOCH=1710000000 - export ZERO_AR_DATE=1 - sudo ./gradlew assembleRelease - - - name: Strip Non-determinstic Metadata - working-directory: android/app/build/outputs/apk/release - run: | - sudo apt-get install strip-nondeterminism - sudo strip-nondeterminism app-release.apk - - - name: Upload the Artifact - uses: actions/upload-artifact@v4 - with: - name: app-release.apk - path: android/app/build/outputs/apk/release/app-release.apk diff --git a/.github/workflows/inactive/ios-e2e-test.yaml b/.github/workflows/inactive/ios-e2e-test.yaml deleted file mode 100644 index 38ceb0b70f..0000000000 --- a/.github/workflows/inactive/ios-e2e-test.yaml +++ /dev/null @@ -1,123 +0,0 @@ -name: IOS e2e test - -on: - workflow_call: - inputs: - cache-key: - required: true - type: string - -env: - CACHE-KEY: ${{ inputs.cache-key }} - REPO-OWNER: ${{ github.repository_owner }} - -jobs: - ios-e2e-test: - name: IOS e2e test - runs-on: macos-15 - strategy: - fail-fast: false - matrix: - test: [ - "parse_invalid_address", - "tex_send_address", - "reload_while_tx_pending", - "change_custom_server", - "change_custom_regtest_server", - "change_custom_testnet_server", - "change_server_from_list", - "new_wallet", - "screen_awake", - "send", - "sync_report", - "transaction_history", - "shielding" - ] - steps: - - name: Set envs for zingolib CI - if: ${{ contains(github.repository, 'zingolib') }} - run: echo "REPO-OWNER=zingolabs" >> $GITHUB_ENV - - - name: Checkout repository - uses: actions/checkout@v5 - with: - repository: ${{ env.REPO-OWNER }}/zingo-mobile - - - name: Install nvm with node 22.18.0 - run: | - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm - [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion - nvm install 22.18.0 - nvm use 22.18.0 - - - name: Native rust cache - uses: actions/cache/restore@v4 - with: - path: rust/lib/Generated - key: native-ios-uniffi-universal-${{ env.CACHE-KEY }} - fail-on-cache-miss: true - - - name: Copy libs to ios folder - working-directory: rust/lib/Generated - run: cp ./*.* ../../../ios - - - name: Install dependencies - run: | - brew tap wix/brew - brew install applesimutils - brew install watchman - - - name: Yarn - run: yarn - - - name: Pod Install - working-directory: ios - run: pod install - - - name: Run IOS e2e build - uses: nick-fields/retry@v3 - with: - timeout_minutes: 60 - max_attempts: 3 - command: yarn detox build --configuration ios.sim.debug - - - name: Boot Simulator - uses: nick-fields/retry@v3 - with: - timeout_minutes: 30 - max_attempts: 3 - command: | - SIMULATOR_UID=$(xcrun simctl list devices | grep -m 1 "iPhone 16" | awk -F '[()]' '{print $2}' | tr -d '[:space:]') - xcrun simctl boot $SIMULATOR_UID - # xcrun simctl bootstatus $SIMULATOR_UID - # SIMULATOR_STATUS=$(xcrun simctl list | grep "$SIMULATOR_UID" | grep "Booted") - # if [ -z "$SIMULATOR_STATUS" ]; then - # exit 1 - # fi - - #- name: Start Simulator Logs - # run: xcrun simctl spawn booted log stream --level debug --style compact --predicate 'process == "Zingo"' > simulator.log & - - - name: Run IOS e2e test - uses: nick-fields/retry@v3 - with: - timeout_minutes: 60 - max_attempts: 1 - command: | - yarn detox test --configuration ios.sim.debug ${{ matrix.test }} --no-relaunch --reuse --record-logs all --headless - # success_status=$? - # if [ $success_status -ne 0 ]; then - # exit 1 - # fi - - - name: Upload Detox Logs as artifact - if: ${{ always() && !cancelled() }} - uses: actions/upload-artifact@v4 - with: - name: ios-e2e-detox-logs-${{ matrix.test }} - path: | - ./detox/runner.log - ./detox/artifacts - # ./simulator.log diff --git a/.github/workflows/ios-build.yaml b/.github/workflows/ios-build.yaml index 092c4c519f..97bc0e24a1 100644 --- a/.github/workflows/ios-build.yaml +++ b/.github/workflows/ios-build.yaml @@ -23,16 +23,18 @@ jobs: run: echo "REPO-OWNER=zingolabs" >> $GITHUB_ENV - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v7 with: repository: ${{ env.REPO-OWNER }}/zingo-mobile - - name: Check if native rust cache exists + - name: Check if iOS xcframework cache exists id: ios-check-build-cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v6 with: - path: rust/lib/Generated - key: native-ios-uniffi-universal-${{ env.CACHE-KEY }} + path: | + ios/Zingolib.xcframework + ios/zingo.swift + key: native-ios-xcframework-${{ env.CACHE-KEY }} lookup-only: true - name: Set cache-found @@ -40,7 +42,7 @@ jobs: run: echo "ios-cache-found=${{ steps.ios-check-build-cache.outputs.cache-hit }}" >> $GITHUB_OUTPUT ios-build: - name: Build native rust & swift uniffi + name: Build iOS Rust xcframework needs: ios-check-build-cache if: ${{ needs.ios-check-build-cache.outputs.ios-cache-found != 'true' }} runs-on: macos-15 @@ -53,7 +55,7 @@ jobs: run: echo "REPO-OWNER=zingolabs" >> $GITHUB_ENV - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v7 with: repository: ${{ env.REPO-OWNER }}/zingo-mobile @@ -79,33 +81,23 @@ jobs: echo "$first_line" | grep -vqE '\b(branch|tag)[[:space:]]*=' cd rust - sudo rustup default stable - sudo cargo update -p zingolib -p regchest_utils --aggressive + rustup default stable + cargo update -p zingolib -p regchest_utils --aggressive - name: Cargo default stable run: | - sudo rustup update - sudo rustup upgrade - sudo rustup default stable + rustup update + rustup upgrade + rustup default stable - - name: AWS libcrypto rust building - run: sudo cargo install --force --locked bindgen-cli + - name: Install bindgen-cli (needed by aws-lc-sys, ring etc.) + run: cargo install --force --locked bindgen-cli - name: Install protoc run: brew install protobuf - - name: Cargo uniffi swift - working-directory: rust/lib - run: | - sudo cargo run --release --bin uniffi-bindgen \ - generate ./src/zingo.udl --language swift \ - --out-dir ./Generated - - - name: Install Cargo Lipo - run: sudo cargo install cargo-lipo - - - name: Install rust Target - run: sudo rustup target add aarch64-apple-ios-sim x86_64-apple-ios + - name: Install iOS Rust targets (device + simulator) + run: rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios # this issue is solved in bindgen 0.70 # But we have a dependency that are using a incompatible version 0.69. @@ -121,24 +113,24 @@ jobs: CXXFLAGS_aarch64_apple_ios_sim = "--target=arm64-apple-ios-simulator -isysroot $SDKROOT" EOF - - name: Cargo lipo universal library - working-directory: rust/lib - run: | - sudo cargo lipo --release --targets aarch64-apple-ios-sim x86_64-apple-ios + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.18.0 - - name: Copy libraries to ios folder - working-directory: rust/lib - run: | - sudo cp ../target/universal/release/libzingo.a ./Generated/libuniffi_zingo.a + - name: Build iOS Rust xcframework + run: node rust/ios/build_ios.mjs - - name: Upload native rust - uses: actions/upload-artifact@v4 + - name: Upload xcframework artifact + uses: actions/upload-artifact@v7 with: - name: native-ios-uniffi-universal-${{ env.CACHE-KEY }} - path: rust/lib/Generated + name: native-ios-xcframework-${{ env.CACHE-KEY }} + path: | + ios/Zingolib.xcframework + ios/zingo.swift cache-native-ios-uniffi: - name: Cache native rust + name: Cache iOS xcframework needs: ios-build runs-on: macos-15 steps: @@ -147,19 +139,20 @@ jobs: run: echo "REPO-OWNER=zingolabs" >> $GITHUB_ENV - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v7 with: repository: ${{ env.REPO-OWNER }}/zingo-mobile - - name: Download native rust + - name: Download xcframework artifact uses: actions/download-artifact@v4 with: - name: native-ios-uniffi-universal-${{ env.CACHE-KEY }} - path: rust/lib/Generated + name: native-ios-xcframework-${{ env.CACHE-KEY }} + path: ios - - name: Cache native rust - uses: actions/cache/save@v4 + - name: Cache xcframework + uses: actions/cache/save@v6 with: - path: rust/lib/Generated - key: native-ios-uniffi-universal-${{ env.CACHE-KEY }} - + path: | + ios/Zingolib.xcframework + ios/zingo.swift + key: native-ios-xcframework-${{ env.CACHE-KEY }} diff --git a/.github/workflows/ios-integration-test.yaml b/.github/workflows/ios-integration-test.yaml index dece61468f..3c46f6caf0 100644 --- a/.github/workflows/ios-integration-test.yaml +++ b/.github/workflows/ios-integration-test.yaml @@ -29,36 +29,35 @@ jobs: run: echo "REPO-OWNER=zingolabs" >> $GITHUB_ENV - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v7 with: repository: ${{ env.REPO-OWNER }}/zingo-mobile - - name: Native rust cache - uses: actions/cache/restore@v4 + - name: Restore iOS xcframework cache + uses: actions/cache/restore@v6 with: - path: rust/lib/Generated - key: native-ios-uniffi-universal-${{ env.CACHE-KEY }} + path: | + ios/Zingolib.xcframework + ios/zingo.swift + key: native-ios-xcframework-${{ env.CACHE-KEY }} fail-on-cache-miss: true - - name: Copy libs to ios folder - working-directory: rust/lib/Generated - run: cp ./*.* ../../../ios - - - name: Install nvm with node 22.18.0 - run: | - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm - [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion - nvm install 22.18.0 - nvm use 22.18.0 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.18.0 + cache: yarn - name: Yarn run: yarn - name: Pod Install - working-directory: ios - run: pod install + uses: nick-fields/retry@v4 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + command: cd ios && pod install - name: Info run: | @@ -81,21 +80,21 @@ jobs: -quiet - name: Upload Build Result - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: xctestrun-and-products path: ios/build/DerivedData/Build/Products - name: Upload IOS build reports (.xcresult) if: ${{ always() && !cancelled() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: test-ios-integration-build-reports.xcresult path: ios/build/reports/Zingo-BuildForTesting.xcresult - name: Upload IOS build DerivedData logs (optional) if: ${{ always() && !cancelled() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: test-ios-integration-build-derived-logs path: | @@ -147,7 +146,7 @@ jobs: # run: echo "REPO-OWNER=zingolabs" >> $GITHUB_ENV # - name: Checkout repository - # uses: actions/checkout@v5 + # uses: actions/checkout@v7 # with: # repository: ${{ env.REPO-OWNER }}/zingo-mobile @@ -232,14 +231,14 @@ jobs: # - name: Upload IOS test reports (.xcresult) # if: ${{ always() && !cancelled() }} - # uses: actions/upload-artifact@v4 + # uses: actions/upload-artifact@v7 # with: # name: test-ios-integration-test-reports-${{ matrix.test }}.xcresult # path: ios/build/reports/Zingo-Test.xcresult # - name: Upload IOS DerivedData logs (optional) # if: ${{ always() && !cancelled() }} - # uses: actions/upload-artifact@v4 + # uses: actions/upload-artifact@v7 # with: # name: test-ios-integration-test-derived-logs-${{ matrix.test }} # path: | diff --git a/.github/workflows/jest-test.yaml b/.github/workflows/jest-test.yaml index f3a74ac3b6..5f2af08e12 100644 --- a/.github/workflows/jest-test.yaml +++ b/.github/workflows/jest-test.yaml @@ -9,9 +9,26 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.18.0 + cache: yarn - run: yarn - - run: yarn test + - name: Audit dependencies + run: | + yarn audit --level critical || audit_code=$? + [ $((${audit_code:-0} & 16)) -eq 0 ] + + - run: yarn coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v7 + with: + files: ./coverage/lcov.info + fail_ci_if_error: false diff --git a/.github/workflows/maestro-nightly.yaml b/.github/workflows/maestro-nightly.yaml new file mode 100644 index 0000000000..034df8a5f4 --- /dev/null +++ b/.github/workflows/maestro-nightly.yaml @@ -0,0 +1,294 @@ +name: Maestro E2E (Nightly) + +on: + schedule: + - cron: '0 3 * * *' + workflow_dispatch: + +jobs: + create-cache-key: + uses: ./.github/workflows/create-cache-key.yaml + + android-build: + uses: ./.github/workflows/android-build.yaml + needs: create-cache-key + with: + cache-key: ${{ needs.create-cache-key.outputs.cache-key }} + # x86_64 so the artifact runs on GitHub Actions' Linux Android emulator + # (the Linux runners are x86_64; an arm64-v8a APK would not install). + arch: x86_64 + + ios-build: + strategy: + fail-fast: false + uses: ./.github/workflows/ios-build.yaml + needs: create-cache-key + with: + cache-key: ${{ needs.create-cache-key.outputs.cache-key }} + + maestro-android: + name: Maestro — Android + runs-on: ubuntu-24.04 + needs: [create-cache-key, android-build] + env: + ABI: x86_64 + CACHE-KEY: ${{ needs.create-cache-key.outputs.cache-key }} + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Setup Java + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '17' + + - name: Gradle setup cache + uses: gradle/actions/setup-gradle@v6 + with: + gradle-version: wrapper + cache-read-only: true + + - name: Native rust cache + uses: actions/cache/restore@v6 + with: + path: android/app/src/main/jniLibs/${{ env.ABI }} + key: native-android-uniffi-${{ env.ABI }}-${{ env.CACHE-KEY }} + fail-on-cache-miss: true + + - name: Kotlin uniffi cache + uses: actions/cache/restore@v6 + with: + path: android/app/build/generated/source/uniffi/release/java/uniffi/zingo + key: kotlin-android-uniffi-${{ env.ABI }}-${{ env.CACHE-KEY }} + fail-on-cache-miss: true + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.18.0 + cache: yarn + + - name: Yarn install + run: yarn + + - name: Build APK + working-directory: android + run: ./gradlew assembleProdRelease -PsplitApk=true + + - name: Install Maestro CLI + # Pin to 2.5.1 — Maestro 2.6.0 (released the week between our last + # green nightly on May 26 and the first red on May 27) broke the + # iOS driver startup; pinning here on Android too so both jobs + # always run on the same known-good CLI release. + env: + MAESTRO_VERSION: '2.5.1' + run: | + curl -Ls "https://get.maestro.mobile.dev" | bash + echo "$HOME/.maestro/bin" >> $GITHUB_PATH + + - name: Enable KVM (Linux Android emulator acceleration) + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \ + | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Run Maestro tests on Android emulator + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 33 + target: google_apis + arch: x86_64 + profile: pixel_6 + disable-animations: true + script: | + adb install -r android/app/build/outputs/apk/prod/release/app-prod-x86_64-release.apk + # Start a background logcat capture so we have device-side logs in + # case Maestro times out waiting on a UI element. The maestro run + # itself decides exit code. + adb logcat -c + adb logcat > /tmp/maestro-logcat.log 2>&1 & + LOGCAT_PID=$! + set +e + maestro test .maestro/ + EXIT=$? + kill $LOGCAT_PID 2>/dev/null || true + exit $EXIT + + - name: Upload Maestro report on failure + if: failure() + uses: actions/upload-artifact@v7 + with: + name: maestro-android-report + path: | + ~/.maestro/tests/** + /tmp/maestro-logcat.log + + maestro-ios: + name: Maestro — iOS + runs-on: macos-15 + needs: [create-cache-key, ios-build] + env: + CACHE-KEY: ${{ needs.create-cache-key.outputs.cache-key }} + + steps: + - name: Xcode 16.4 + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '16.4' + + - name: Checkout repository + uses: actions/checkout@v7 + + # Restore the xcframework produced by .github/workflows/ios-build.yaml. + # That reusable workflow saves both artefacts directly under `ios/` so the + # xcodebuild step below can reference them without any copying. + - name: Native iOS xcframework cache + uses: actions/cache/restore@v6 + with: + path: | + ios/Zingolib.xcframework + ios/zingo.swift + key: native-ios-xcframework-${{ env.CACHE-KEY }} + fail-on-cache-miss: true + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.18.0 + cache: yarn + + - name: Yarn install + run: yarn + + - name: Pod install + uses: nick-fields/retry@v4 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + command: cd ios && pod install + + - name: Inspect Hermes dSYM tree + run: find ios/vendor/hermes-dsyms -maxdepth 6 2>/dev/null || echo "ios/vendor/hermes-dsyms not found" + + # Build in Release configuration. Maestro exercises the running RN app on + # the simulator — Debug builds expect a Metro packager on localhost:8081 + # to load the JS bundle at runtime, which CI does not run; Release + # embeds the bundle in the app. The Hermes dSYM swap that previously + # failed here is now satisfied: Podfile.lock is pinned to hermes-engine + # matching the react-native version in package.json, so pod install's + # post_install hook downloads the matching dSYMs. + - name: Build app + # OS=latest lets xcodebuild pick whatever iOS runtime is paired with + # the active Xcode — pinning OS=18.5 broke when macos-15 started + # shipping multiple runtimes (18.5/18.6/26.x) that aren't all + # registered as xcodebuild destinations. + working-directory: ios + run: | + set -euxo pipefail + mkdir -p build/DerivedData + xcodebuild build \ + -workspace Zingo.xcworkspace \ + -scheme Zingo \ + -sdk iphonesimulator \ + -configuration Release \ + -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \ + -derivedDataPath "build/DerivedData" \ + -quiet + + - name: Debug — Xcode + simulator inventory + # Started failing on May 27, 2026 with the same IOSDriverTimeout + # across Maestro 2.5.1 and 2.6.0 — strongly points to a GitHub + # macos-15 runner image change (Xcode/simulator catalog) that + # leaves us booting something other than what we built for. Dump + # the actual state so we can see what changed. + run: | + echo "--- Xcode version ---" + xcodebuild -version + echo "--- Available iOS runtimes ---" + xcrun simctl list runtimes | grep -i ios || true + echo "--- iPhone 16 devices ---" + xcrun simctl list devices "iPhone 16" || true + echo "--- Devicetypes containing 'iPhone' ---" + xcrun simctl list devicetypes | grep -i iphone | head -20 || true + + - name: Boot iOS simulator + run: | + # macos-15 now ships 5 iPhone 16 devices (one per iOS runtime: + # 18.5, 18.6, 26.0, 26.1, 26.2). `simctl boot "iPhone 16"` + # selects the highest-iOS match (iOS 26.2), which mismatches the + # iOS 18.5 build target above. Pick the iPhone 16 from iOS 18.5 + # explicitly by UDID. Also shutdown anything else booted on the + # runner first so Maestro can't accidentally pick a stale device. + xcrun simctl shutdown all || true + UDID=$(xcrun simctl list devices "iPhone 16" --json | \ + jq -r '.devices["com.apple.CoreSimulator.SimRuntime.iOS-18-5"][] | select(.name=="iPhone 16") | .udid') + if [ -z "$UDID" ]; then + echo "::error::No iPhone 16 device found for iOS 18.5 runtime" + exit 1 + fi + echo "Booting iPhone 16 (iOS 18.5) — UDID=$UDID" + echo "SIM_UDID=$UDID" >> $GITHUB_ENV + xcrun simctl boot "$UDID" || true + xcrun simctl bootstatus "$UDID" -b + echo "--- Booted devices after our boot ---" + xcrun simctl list devices booted + + - name: Install Maestro CLI + # Pinned — see comment in maestro-android job. 2.6.0 broke iOS. + env: + MAESTRO_VERSION: '2.5.1' + run: | + curl -Ls "https://get.maestro.mobile.dev" | bash + echo "$HOME/.maestro/bin" >> $GITHUB_PATH + + - name: Install app on simulator + run: | + # Use the explicit UDID exported above instead of "booted" — + # there may be other iPhone 16 devices on the runner (across + # different iOS runtimes) and we want this app on exactly the + # iOS 18.5 one it was built for. + APP=ios/build/DerivedData/Build/Products/Release-iphonesimulator/Zingo.app + xcrun simctl install "$SIM_UDID" "$APP" + + # Verify FrontBoard has registered the bundle before handing off + # to Maestro. Without this, the first launch after install can + # race FrontBoard and fail with FBSOpenApplicationServiceErrorDomain + # ("unknown to FrontBoard"). Poll listapps until the bundle ID + # shows up, max ~30s. + BUNDLE_ID=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP/Info.plist") + echo "Waiting for $BUNDLE_ID to register with FrontBoard…" + for i in $(seq 1 30); do + if xcrun simctl listapps "$SIM_UDID" | grep -q "\"$BUNDLE_ID\""; then + echo "App registered (after ${i}s)." + exit 0 + fi + sleep 1 + done + echo "::error::App $BUNDLE_ID never registered with FrontBoard" + xcrun simctl listapps "$SIM_UDID" | head -40 + exit 1 + + - name: Run Maestro tests + # GitHub macOS-15 runners now ship many extra iOS runtimes; first-time + # boot of the iOS 18.5 sim runs data migration plugins (~55s) and the + # XCTest driver install often eats the rest of the 3 min budget. + # Bumping to 5 min absorbs the worst-case cold-runner timing. + # Pass --device explicitly so Maestro never auto-picks the wrong + # simulator when the runner has any other booted device. + env: + MAESTRO_DRIVER_STARTUP_TIMEOUT: '300000' + run: | + echo "--- Devices booted right before maestro test ---" + xcrun simctl list devices booted + maestro --device "$SIM_UDID" test .maestro/ + + - name: Upload Maestro report on failure + if: failure() + uses: actions/upload-artifact@v7 + with: + name: maestro-ios-report + path: ~/.maestro/tests/** diff --git a/.gitignore b/.gitignore index a29f40a58f..4f54ec2f71 100644 --- a/.gitignore +++ b/.gitignore @@ -26,10 +26,11 @@ ios/vendor/ # Exclude compiled libs android/app/src/main/jniLibs android/app/release/ -ios/libuniffi_zingo.a +android/app/beta/release/ +*.aab +*.apk +ios/Zingolib.xcframework/ ios/zingo.swift -ios/zingoFFI.h -ios/zingoFFI.modulemap rust/lib/Generated/zingo.swift rust/lib/Generated/zingoFFI.h rust/lib/Generated/zingoFFI.modulemap @@ -54,7 +55,6 @@ local.properties node_modules/ npm-debug.log yarn-error.log -yarn.lock nohup.out @@ -95,4 +95,9 @@ buck-out/ # Keystore /android/app/Zingo.jks -/android/local.zingo.jks.properties \ No newline at end of file +/android/local.zingo.jks.properties + +# Claude Code +.claude/ +# Jest coverage output +coverage/ diff --git a/.maestro/01_basic_new_wallet.yaml b/.maestro/01_basic_new_wallet.yaml new file mode 100644 index 0000000000..7ab0f1e7b7 --- /dev/null +++ b/.maestro/01_basic_new_wallet.yaml @@ -0,0 +1,14 @@ +appId: org.ZingoLabs.Zingo +--- +- launchApp: + clearState: true + +# In basic mode the wallet is created automatically. +# The app lands on the Receive screen. +- extendedWaitUntil: + visible: + id: "receive.title" + timeout: 120000 + +- assertVisible: + id: "receive.unified-address" diff --git a/.maestro/02_basic_drawer_menu.yaml b/.maestro/02_basic_drawer_menu.yaml new file mode 100644 index 0000000000..1a483426ef --- /dev/null +++ b/.maestro/02_basic_drawer_menu.yaml @@ -0,0 +1,39 @@ +appId: org.ZingoLabs.Zingo +--- +- launchApp + +- extendedWaitUntil: + visible: + id: "receive.title" + timeout: 120000 + +# Open options panel and verify basic-mode items are present +- tapOn: + id: "header.drawmenu" +- assertVisible: + id: "menu.addressbook" +- assertVisible: + id: "menu.loadwalletfromseed" +- back + +# Server info now lives inline inside Settings (see settings.*-server rows); +# About remains as its own navigation row. +- tapOn: + id: "header.settings" +- extendedWaitUntil: + visible: + id: "settings.scroll-view" + timeout: 10000 +- assertVisible: + id: "settings.about" + +# Navigate to About and verify it loads +- tapOn: + id: "settings.about" +- extendedWaitUntil: + visible: + id: "about.scroll-view" + timeout: 10000 + +- back +- back diff --git a/.maestro/03_basic_settings.yaml b/.maestro/03_basic_settings.yaml new file mode 100644 index 0000000000..8d7b3e025f --- /dev/null +++ b/.maestro/03_basic_settings.yaml @@ -0,0 +1,25 @@ +appId: org.ZingoLabs.Zingo +--- +- launchApp + +- extendedWaitUntil: + visible: + id: "receive.title" + timeout: 120000 + +# Navigate to Settings via gear icon +- tapOn: + id: "header.settings" +- extendedWaitUntil: + visible: + id: "settings.scroll-view" + timeout: 10000 + +# Scroll to bottom to verify the full Settings screen loaded +- scrollUntilVisible: + element: + id: "settings.button.save" +- assertVisible: + id: "settings.button.save" + +- back diff --git a/.maestro/04_basic_address_book.yaml b/.maestro/04_basic_address_book.yaml new file mode 100644 index 0000000000..82767add5c --- /dev/null +++ b/.maestro/04_basic_address_book.yaml @@ -0,0 +1,24 @@ +appId: org.ZingoLabs.Zingo +--- +- launchApp + +- extendedWaitUntil: + visible: + id: "receive.title" + timeout: 120000 + +# Open drawer and navigate to Address Book +- tapOn: + id: "header.drawmenu" +- tapOn: + id: "menu.addressbook" +- extendedWaitUntil: + visible: + id: "addressbook.scroll-view" + timeout: 10000 + +# Verify the add-new-contact button is present +- assertVisible: + id: "addressbook.button.new" + +- back diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..91d5f6ff8e --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.18.0 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..df70134298 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +rust/target +android/build +node_modules diff --git a/App.tsx b/App.tsx index 46c71e3067..5db5b0a821 100644 --- a/App.tsx +++ b/App.tsx @@ -1,20 +1,31 @@ /* eslint-disable react-native/no-inline-styles */ import React, { useEffect, useState } from 'react'; import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; -import { createNavigationContainerRef, DefaultTheme, NavigationContainer } from '@react-navigation/native'; +import { + createNavigationContainerRef, + DefaultTheme, + NavigationContainer, +} from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import { LoadedApp } from './app/LoadedApp'; import { LoadingApp } from './app/LoadingApp'; +import ScannerAddress from './components/Send/components/ScannerAddress'; +import ScannerUfvk from './app/LoadingApp/components/ScannerUfvk'; import { ThemeType, AppStackParamList } from './app/types'; import { ModeEnum, RouteEnum } from './app/AppState'; import { BackHandler, LogBox, StatusBar } from 'react-native'; +import { KeyboardProvider } from 'react-native-keyboard-controller'; +import AppErrorBoundary from './components/ErrorBoundary/AppErrorBoundary'; +import BiometricBlankingOverlay from './app/BiometricBlankingOverlay'; -LogBox.ignoreLogs(['[Reanimated] Reduced motion setting is enabled on this device.']); +LogBox.ignoreLogs([ + '[Reanimated] Reduced motion setting is enabled on this device.', +]); const advancePalette: string[] = [ - '#06172d', + '#010A17', '#dadfe1', '#43a637', '#23692f', @@ -28,7 +39,7 @@ const advancePalette: string[] = [ ]; const basicPalette: string[] = [ - '#07182e', + '#010A17', '#dadfe1', '#15576f', '#4fa254', @@ -41,7 +52,7 @@ const basicPalette: string[] = [ '#040C17', ]; -const advancedTheme: ThemeType = { +export const advancedTheme: ThemeType = { ...DefaultTheme, dark: true, colors: { @@ -61,6 +72,8 @@ const advancedTheme: ThemeType = { syncing: '#ebff5a', // yellow notification: '', sideMenuBackground: advancePalette[10], + bottomSheetBackground: '#031124', + bottomSheetBorder: '#05234C', warning: { background: '#262527', border: '#65491C', @@ -79,7 +92,7 @@ const advancedTheme: ThemeType = { }, }; -const basicTheme: ThemeType = { +export const basicTheme: ThemeType = { ...DefaultTheme, dark: true, colors: { @@ -96,6 +109,8 @@ const basicTheme: ThemeType = { syncing: '#ebff5a', // yellow notification: '', sideMenuBackground: basicPalette[10], + bottomSheetBackground: '#031124', + bottomSheetBorder: '#05234C', warning: { background: '#262527', border: '#65491C', @@ -139,27 +154,61 @@ const App: React.FunctionComponent = () => { //console.log('render App - 1'); return ( - - - - - - - {props => } - - - {props => } - - - - - + + + + + + + + + + {props => } + + + {props => } + + {/* ScannerAddress lives at the root Stack (above LoadedApp, + therefore above the BottomSheetModalProvider portal). Without + this, any open BottomSheetModal renders ON TOP of the camera, + hiding the preview. + presentation: 'transparentModal' avoids the iOS UIKit modal + freeze on the underlying view — with 'modal' or default + 'card' the BottomSheetModal backdrop below stays + unresponsive for several seconds after the camera is + dismissed because iOS pauses the underlying scene during + the native modal presentation. */} + + + + + + + + ); }; diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..7503aa8e44 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,13 @@ +## Agent skills + +### Issue tracker + +Issues live in GitHub Issues (`zingolabs/zingo-mobile`). See `docs/agents/issue-tracker.md`. + +### Triage labels + +Using default label vocabulary (needs-triage, needs-info, ready-for-agent, ready-for-human, wontfix). See `docs/agents/triage-labels.md`. + +### Domain docs + +Single-context repo — one `CONTEXT.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`. diff --git a/LICENSE b/LICENSE index 9608852cbd..cee0035bee 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 The Zingoista Movement +Copyright (c) 2026 The Zingoista Movement Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ea2a10a53f..55872c70e8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Zingo Android and iOS apps + +Zingo Mobile is a shielded Zcash light-client wallet for Android and iOS, built with React Native and powered by the [Zingolib](https://github.com/zingolabs/zingolib) Rust SDK. + App Store: [https://apps.apple.com/app/zingo/id1668209531](https://apps.apple.com/app/zingo/id1668209531) + Google Play: [https://play.google.com/store/apps/details?id=org.ZingoLabs.Zingo](https://play.google.com/store/apps/details?id=org.ZingoLabs.Zingo) # Security Vulnerability Disclosure @@ -12,6 +16,29 @@ zingodisclosure@proton.me Please see the platform specific [iOS](./docs/ios_developer_quickstart.md) and [Android](./docs/android_developer_quickstart.md) "quickstart" documentation. +## Releasing + +Zingo ships as two parallel apps from this repo: + +- **Production** (`org.ZingoLabs.Zingo`) — App Store + Play Production. +- **Beta** (`org.ZingoLabs.Zingo.Beta`) — TestFlight External + Play Open Testing. + +Both share the same JS bundle and Rust libs; only the native shell differs +(bundle ID, display name, icon with `BETA` band). Version/build bumps are +scoped per channel via: + +```bash +yarn release:prod:prep +yarn release:beta:prep +``` + +Pushing a `zingo--` or `zingo-beta--` tag +triggers a CI workflow that builds the 4 ABI APKs + a universal APK from +source on the tagged commit and publishes them to a fresh GitHub Release. + +Full step-by-step for both stores, signing setup, and the underlying iOS/Android +flavor architecture: see [docs/release_quickstart.md](./docs/release_quickstart.md). + ## Testing ### Prerequisites Integration tests and end-to-end tests require a regtest server. On linux hosts, these may be run @@ -31,6 +58,15 @@ network in a docker container. Before running tests, pull the latest Regchest im `yarn test` ### Integration Tests +These exercise the Rust ↔ Kotlin/Swift FFI boundary against a regtest network. + +The Android suite (`rust/android/tests/integration_tests.rs`) runs on every PR +via the `android-ubuntu-integration-test-ci` workflow. The iOS suite +(`rust/ios/tests/integration_tests.rs`) exists but its `cargo nextest run` +invocation is currently commented out in `ios-integration-test.yaml`, so it +does **not** gate PRs — you can still run it locally with the same nextest +commands. + 1. Create quick-boot snapshots to speed up AVD launch times. From the root directory, run:
`./scripts/android_integration_tests.sh -a x86_64 -s`
`./scripts/android_integration_tests.sh -a x86 -s`
@@ -57,7 +93,10 @@ For more information on running integration tests on non-default AVDs, run:
`cargo nextest run e2e::x86_64::test_name` +### End-to-End Tests (Maestro UI flows) +[Maestro](https://maestro.mobile.dev/) drives the released app from the +outside, asserting on the rendered UI. Flows live in `.maestro/` as YAML +(`01_basic_new_wallet.yaml`, etc.). Runs nightly in CI via +`.github/workflows/maestro-nightly.yaml` against both Android and iOS; +this is the e2e suite that PR reviewers and releases lean on going +forward. + +To run locally: + +1. Install the Maestro CLI:
+ `curl -Ls "https://get.maestro.mobile.dev" | bash` (puts the binary in `~/.maestro/bin`) + +2. Boot an emulator/simulator and install the app you want to test against + (debug or release). Maestro talks to whatever device is currently + selected by `adb` / `xcrun simctl`. + +3. From the repo root:
+ `maestro test .maestro/` + Or run a single flow:
+ `maestro test .maestro/01_basic_new_wallet.yaml` + +The legacy Detox suite under `e2e/*.test.js` is no longer wired to CI or +to any `yarn` script. It is being phased out in favour of Maestro and +should not be relied on; new e2e coverage should land as Maestro flows. + # Troubleshooting For notes on known issues and problems, see the [trouble-shooting notes](./TROUBLESHOOTING.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..2cdae0bae5 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,51 @@ +# Security Policy + +## Supported Versions + +| Platform | Store | Supported | +| -------- | ------------------------------------------------------------------------------------------------------- | --------- | +| iOS | [App Store](https://apps.apple.com/app/zingo/id1668209531) | ✓ | +| Android | [Google Play](https://play.google.com/store/apps/details?id=org.ZingoLabs.Zingo) | ✓ | + +Only the latest released version on each platform receives security fixes. Older versions are not backported. + +## Reporting a Vulnerability + +If you believe you have found a security vulnerability in Zingo, please **do not open a public GitHub issue**. Instead, report it privately: + +**Email:** zingodisclosure@proton.me + +Please include as much of the following as possible: + +- A clear description of the vulnerability and its potential impact +- Steps to reproduce or a proof-of-concept +- Affected version(s) and platform(s) +- Any suggested mitigations + +## What to Expect + +- **Acknowledgement** within 72 hours of your report. +- **Status update** within 7 days with an initial assessment. +- **Coordinated disclosure** — we will work with you to agree on a disclosure timeline before any public announcement. +- Credit in the release notes if you wish to be acknowledged. + +## Scope + +Issues considered in scope: + +- Private key or seed phrase exposure +- Unauthorized fund transfer or transaction signing +- Authentication or authorization bypasses +- Cryptographic weaknesses in wallet or shielded transaction handling +- Remote code execution or data exfiltration affecting app users + +Out of scope: + +- Denial of service against the lightwalletd server +- Issues in third-party dependencies not directly introduced by this project +- Social engineering or phishing attacks +- Reports already publicly known + +## Disclosure Policy + +We follow a **90-day coordinated disclosure** timeline. If a fix cannot be delivered within that window we will communicate the reasons and agree on an extension with the reporter. diff --git a/__mocks__/@gorhom/bottom-sheet.js b/__mocks__/@gorhom/bottom-sheet.js index 0ebf5ffa86..b20c8d97bf 100644 --- a/__mocks__/@gorhom/bottom-sheet.js +++ b/__mocks__/@gorhom/bottom-sheet.js @@ -1,7 +1,25 @@ import React from 'react'; const BottomSheet = ({ children }) => <>{children}; +const BottomSheetModal = ({ children }) => <>{children}; const BottomSheetView = ({ children }) => <>{children}; +const BottomSheetScrollView = ({ children }) => <>{children}; +const BottomSheetBackdrop = ({ children }) => <>{children}; +const BottomSheetFooter = ({ children }) => <>{children}; +const BottomSheetModalProvider = ({ children }) => <>{children}; + +const useBottomSheetModal = () => ({ + dismiss: () => false, + dismissAll: () => {}, +}); export default BottomSheet; -export { BottomSheetView }; +export { + BottomSheetModal, + BottomSheetView, + BottomSheetScrollView, + BottomSheetBackdrop, + BottomSheetFooter, + BottomSheetModalProvider, + useBottomSheetModal, +}; diff --git a/__mocks__/@react-native-clipboard/clipboard.js b/__mocks__/@react-native-clipboard/clipboard.js index d56718fa13..4f82060bf0 100644 --- a/__mocks__/@react-native-clipboard/clipboard.js +++ b/__mocks__/@react-native-clipboard/clipboard.js @@ -1,2 +1,6 @@ -export const getString = jest.fn(() => Promise.resolve('mocked clipboard content')); +export const getString = jest.fn(() => + Promise.resolve('mocked clipboard content'), +); export const setString = jest.fn(); + +export default { getString, setString }; diff --git a/__mocks__/@react-native-community/netinfo.js b/__mocks__/@react-native-community/netinfo.js index f833bee3a8..9afed6b1bd 100644 --- a/__mocks__/@react-native-community/netinfo.js +++ b/__mocks__/@react-native-community/netinfo.js @@ -1,28 +1,28 @@ const RN = jest.requireActual('react-native'); export const NetInfoStateType = { - unknown: 'unknown', - none: 'none', - cellular: 'cellular', - wifi: 'wifi', - bluetooth: 'bluetooth', - ethernet: 'ethernet', - wimax: 'wimax', - vpn: 'vpn', - other: 'other', - }; + unknown: 'unknown', + none: 'none', + cellular: 'cellular', + wifi: 'wifi', + bluetooth: 'bluetooth', + ethernet: 'ethernet', + wimax: 'wimax', + vpn: 'vpn', + other: 'other', +}; RN.NativeModules.RNCNetInfo = { - execute: jest.fn(() => '{}'), + execute: jest.fn(() => '{}'), }; export default { - RNCNetInfo: RN, - addEventListener: jest.fn(), - fetch: jest.fn().mockImplementation(() => - Promise.resolve({ - isConnected: true, - isInternetReachable: true, - }), - ), + RNCNetInfo: RN, + addEventListener: jest.fn(), + fetch: jest.fn().mockImplementation(() => + Promise.resolve({ + isConnected: true, + isInternetReachable: true, + }), + ), }; diff --git a/__mocks__/@react-native-community/netinfo/src/index.js b/__mocks__/@react-native-community/netinfo/src/index.js index f833bee3a8..9afed6b1bd 100644 --- a/__mocks__/@react-native-community/netinfo/src/index.js +++ b/__mocks__/@react-native-community/netinfo/src/index.js @@ -1,28 +1,28 @@ const RN = jest.requireActual('react-native'); export const NetInfoStateType = { - unknown: 'unknown', - none: 'none', - cellular: 'cellular', - wifi: 'wifi', - bluetooth: 'bluetooth', - ethernet: 'ethernet', - wimax: 'wimax', - vpn: 'vpn', - other: 'other', - }; + unknown: 'unknown', + none: 'none', + cellular: 'cellular', + wifi: 'wifi', + bluetooth: 'bluetooth', + ethernet: 'ethernet', + wimax: 'wimax', + vpn: 'vpn', + other: 'other', +}; RN.NativeModules.RNCNetInfo = { - execute: jest.fn(() => '{}'), + execute: jest.fn(() => '{}'), }; export default { - RNCNetInfo: RN, - addEventListener: jest.fn(), - fetch: jest.fn().mockImplementation(() => - Promise.resolve({ - isConnected: true, - isInternetReachable: true, - }), - ), + RNCNetInfo: RN, + addEventListener: jest.fn(), + fetch: jest.fn().mockImplementation(() => + Promise.resolve({ + isConnected: true, + isInternetReachable: true, + }), + ), }; diff --git a/__mocks__/@react-navigation/drawer.js b/__mocks__/@react-navigation/drawer.js deleted file mode 100644 index bf02f46cc1..0000000000 --- a/__mocks__/@react-navigation/drawer.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; - -const mockNavigation = { - navigate: jest.fn(), - openDrawer: jest.fn(), - closeDrawer: jest.fn(), - dispatch: jest.fn(), - goBack: jest.fn(), - addListener: jest.fn(), - removeListener: jest.fn(), - canGoBack: jest.fn(), - isFocused: jest.fn(), - getParent: jest.fn(), - getState: jest.fn(), - getId: jest.fn(), - setOptions: jest.fn(), - reset: jest.fn(), - replace: jest.fn(), - push: jest.fn(), - pop: jest.fn(), - popToTop: jest.fn(), - setParams: jest.fn(), - jumpTo: jest.fn(), - emit: jest.fn(), - dangerouslyGetParent: jest.fn(), - dangerouslyGetState: jest.fn(), -}; -const MockNavigator = ({ children }) => children; -const MockScreen = ({ _name, children, component }) => { - if (typeof children === 'function') { - return children({ navigation: mockNavigation }); - } - - const Component = component; - return Component ? : <>{children}; -}; - -export const createDrawerNavigator = jest.fn(() => ({ - Navigator: MockNavigator, - Screen: MockScreen, -})); diff --git a/__mocks__/@react-navigation/elements.js b/__mocks__/@react-navigation/elements.js index 8ec3deaa8e..b040e7173b 100644 --- a/__mocks__/@react-navigation/elements.js +++ b/__mocks__/@react-navigation/elements.js @@ -1 +1,3 @@ -export const PlatformPressable = jest.fn().mockImplementation(({ children }) => children); +export const PlatformPressable = jest + .fn() + .mockImplementation(({ children }) => children); diff --git a/__mocks__/@react-navigation/native.js b/__mocks__/@react-navigation/native.js index 4ad79b30f0..65ac1a7711 100644 --- a/__mocks__/@react-navigation/native.js +++ b/__mocks__/@react-navigation/native.js @@ -1,6 +1,6 @@ import { mockTheme } from '../dataMocks/mockTheme'; -export const useTheme = () => (mockTheme); +export const useTheme = () => mockTheme; export const useScrollToTop = jest.fn(); export const useIsFocused = jest.fn(); export const useNavigation = jest.fn(); @@ -37,4 +37,4 @@ export const createNavigationContainerRef = jest.fn(() => { refImpl.current = refImpl; return refImpl; -}); \ No newline at end of file +}); diff --git a/__mocks__/dataMocks/mockAddressBook.ts b/__mocks__/dataMocks/mockAddressBook.ts index 65d3f7e2a2..221cd6fab1 100644 --- a/__mocks__/dataMocks/mockAddressBook.ts +++ b/__mocks__/dataMocks/mockAddressBook.ts @@ -1,4 +1,4 @@ -import { AddressBookFileClass } from '../../app/AppState'; +import { AddressBookFileClass, ChainNameEnum } from '../../app/AppState'; export const mockAddressBook: AddressBookFileClass[] = [ { @@ -6,11 +6,15 @@ export const mockAddressBook: AddressBookFileClass[] = [ address: 'u1234567890_____________', color: '#000000', own: true, + chain: ChainNameEnum.mainChainName, + swapChain: 'ZEC', }, { label: 'lolo', address: 'u0987654321_____________', color: '#FFFFFF', own: false, + chain: ChainNameEnum.mainChainName, + swapChain: 'ZEC', }, ]; diff --git a/__mocks__/dataMocks/mockAddresses.ts b/__mocks__/dataMocks/mockAddresses.ts index 4b9ee7fdeb..2705110517 100644 --- a/__mocks__/dataMocks/mockAddresses.ts +++ b/__mocks__/dataMocks/mockAddresses.ts @@ -1,41 +1,46 @@ -import { UnifiedAddressClass, AddressKindEnum, TransparentAddressClass } from '../../app/AppState'; -import { RPCAddressScopeEnum } from '../../app/rpc/enums/RPCAddressScopeEnum'; +import { + UnifiedAddressClass, + AddressKindEnum, + TransparentAddressClass, +} from '../../app/AppState'; +import { RPCAddressScopeEnum } from '../../app/walletBackend/enums/RPCAddressScopeEnum'; -export const mockAddresses: (UnifiedAddressClass | TransparentAddressClass)[] = [ - { - index: 0, - address: 'UA-orchard-sapling12345678901234567890', - addressKind: AddressKindEnum.u, - has_orchard: true, - has_sapling: true, - has_transparent: false, - }, - { - index: 1, - address: 'UA-sapling-12345678901234567890', - addressKind: AddressKindEnum.u, - has_orchard: false, - has_sapling: true, - has_transparent: false, - }, - { - index: 2, - address: 'UA-orchard-12345678901234567890', - addressKind: AddressKindEnum.u, - has_orchard: true, - has_sapling: false, - has_transparent: false, - }, - { - index: 0, - address: 'transparent-1-12345678901234567890', - addressKind: AddressKindEnum.t, - scope: RPCAddressScopeEnum.external, - }, - { - index: 1, - address: 'transparent-2-12345678901234567890', - addressKind: AddressKindEnum.t, - scope: RPCAddressScopeEnum.external, - }, -]; +export const mockAddresses: (UnifiedAddressClass | TransparentAddressClass)[] = + [ + { + index: 0, + address: 'UA-orchard-sapling12345678901234567890', + addressKind: AddressKindEnum.u, + has_orchard: true, + has_sapling: true, + has_transparent: false, + }, + { + index: 1, + address: 'UA-sapling-12345678901234567890', + addressKind: AddressKindEnum.u, + has_orchard: false, + has_sapling: true, + has_transparent: false, + }, + { + index: 2, + address: 'UA-orchard-12345678901234567890', + addressKind: AddressKindEnum.u, + has_orchard: true, + has_sapling: false, + has_transparent: false, + }, + { + index: 0, + address: 'transparent-1-12345678901234567890', + addressKind: AddressKindEnum.t, + scope: RPCAddressScopeEnum.external, + }, + { + index: 1, + address: 'transparent-2-12345678901234567890', + addressKind: AddressKindEnum.t, + scope: RPCAddressScopeEnum.external, + }, + ]; diff --git a/__mocks__/dataMocks/mockLoadedAppNavigation.ts b/__mocks__/dataMocks/mockLoadedAppNavigation.ts index 083c49f3e2..6df8dac0e4 100644 --- a/__mocks__/dataMocks/mockLoadedAppNavigation.ts +++ b/__mocks__/dataMocks/mockLoadedAppNavigation.ts @@ -2,7 +2,10 @@ import { StackNavigationProp } from '@react-navigation/stack'; import { AppStackParamList } from '../../app/types'; import { RouteEnum } from '../../app/AppState'; -export const mockLoadedAppNavigation: StackNavigationProp = { +export const mockLoadedAppNavigation: StackNavigationProp< + AppStackParamList, + RouteEnum.LoadedApp +> = { navigate: jest.fn(), goBack: jest.fn(), dispatch: jest.fn(), diff --git a/__mocks__/dataMocks/mockLoadedAppRoute.ts b/__mocks__/dataMocks/mockLoadedAppRoute.ts index 952a5d76fd..0392d495e6 100644 --- a/__mocks__/dataMocks/mockLoadedAppRoute.ts +++ b/__mocks__/dataMocks/mockLoadedAppRoute.ts @@ -1,8 +1,15 @@ import { RouteProp } from '@react-navigation/native'; import { AppStackParamList } from '../../app/types'; -import { LaunchingModeEnum, RouteEnum } from '../../app/AppState'; +import { + ChainNameEnum, + LaunchingModeEnum, + RouteEnum, +} from '../../app/AppState'; -export const mockLoadedAppRoute: RouteProp = { +export const mockLoadedAppRoute: RouteProp< + AppStackParamList, + RouteEnum.LoadedApp +> = { key: 'LoadedApp-test', name: RouteEnum.LoadedApp, params: { @@ -12,5 +19,6 @@ export const mockLoadedAppRoute: RouteProp = { +export const mockLoadingAppNavigation: StackNavigationProp< + AppStackParamList, + RouteEnum.LoadingApp +> = { navigate: jest.fn(), goBack: jest.fn(), dispatch: jest.fn(), diff --git a/__mocks__/dataMocks/mockLoadingAppRoute.ts b/__mocks__/dataMocks/mockLoadingAppRoute.ts index 4be399e06b..3cb98566bd 100644 --- a/__mocks__/dataMocks/mockLoadingAppRoute.ts +++ b/__mocks__/dataMocks/mockLoadingAppRoute.ts @@ -2,11 +2,14 @@ import { RouteProp } from '@react-navigation/native'; import { AppStackParamList } from '../../app/types'; import { RouteEnum } from '../../app/AppState'; -export const mockLoadingAppRoute: RouteProp = { +export const mockLoadingAppRoute: RouteProp< + AppStackParamList, + RouteEnum.LoadingApp +> = { key: 'LoadingApp-test', name: RouteEnum.LoadingApp, params: { - screen: 0, + screen: RouteEnum.Launching, startingApp: true, biometricsFailed: false, newWallet: false, diff --git a/__mocks__/dataMocks/mockNavigation.ts b/__mocks__/dataMocks/mockNavigation.ts index e1e39da62c..b96a855b64 100644 --- a/__mocks__/dataMocks/mockNavigation.ts +++ b/__mocks__/dataMocks/mockNavigation.ts @@ -20,4 +20,5 @@ export default { preload: jest.fn(), navigateDeprecated: jest.fn(), replaceParams: jest.fn(), + // as any: mock is shared across all screen types; no single NavigationProp type satisfies all } as any; diff --git a/__mocks__/dataMocks/mockSendPageState.ts b/__mocks__/dataMocks/mockSendPageState.ts index a3084b0d3f..4b277aa8d2 100644 --- a/__mocks__/dataMocks/mockSendPageState.ts +++ b/__mocks__/dataMocks/mockSendPageState.ts @@ -1,11 +1,14 @@ import { SendPageStateClass, ToAddrClass } from '../../app/AppState'; -const mockSendPageState: SendPageStateClass = new SendPageStateClass(new ToAddrClass(0)); +const mockSendPageState: SendPageStateClass = new SendPageStateClass( + new ToAddrClass(0), +); mockSendPageState.toaddr.id = 1234567890; mockSendPageState.toaddr.to = 'UA-12345678901234567890'; mockSendPageState.toaddr.amount = '1.12345678'; mockSendPageState.toaddr.amountCurrency = '50.22'; -mockSendPageState.toaddr.memo = 'memo1\nmemo2\nmemo3\nmemo4\nmemo5\nmemo6\nmemo7\nmemo8\nmemo9\nmemo10'; +mockSendPageState.toaddr.memo = + 'memo1\nmemo2\nmemo3\nmemo4\nmemo5\nmemo6\nmemo7\nmemo8\nmemo9\nmemo10'; export default mockSendPageState; diff --git a/__mocks__/dataMocks/mockSnackbars.ts b/__mocks__/dataMocks/mockSnackbars.ts deleted file mode 100644 index 9a8af0b481..0000000000 --- a/__mocks__/dataMocks/mockSnackbars.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ScreenEnum, SnackbarType } from '../../app/AppState'; - -export const mockSnackbars: SnackbarType[] = [ - { - message: 'snackbar 1', - screenName: [ScreenEnum.About], - }, - { - message: 'snackbar 2', - screenName: [ScreenEnum.LoadedApp], - }, - { - message: 'snackbar 3', - screenName: [ScreenEnum.About], - }, -]; diff --git a/__mocks__/dataMocks/mockSyncingStatus.ts b/__mocks__/dataMocks/mockSyncingStatus.ts index d20713a196..6630fb09ce 100644 --- a/__mocks__/dataMocks/mockSyncingStatus.ts +++ b/__mocks__/dataMocks/mockSyncingStatus.ts @@ -1,5 +1,5 @@ -import { RPCSyncScanRangePriorityStatusEnum } from '../../app/rpc/enums/RPCSyncScanRangePriorityStatusEnum'; -import { RPCSyncStatusType } from '../../app/rpc/types/RPCSyncStatusType'; +import { RPCSyncScanRangePriorityStatusEnum } from '../../app/walletBackend/enums/RPCSyncScanRangePriorityStatusEnum'; +import { RPCSyncStatusType } from '../../app/walletBackend/types/RPCSyncStatusType'; export const mockSyncingStatus: RPCSyncStatusType = {} as RPCSyncStatusType; diff --git a/__mocks__/dataMocks/mockTheme.ts b/__mocks__/dataMocks/mockTheme.ts index dd32d31f47..e7175e03b5 100644 --- a/__mocks__/dataMocks/mockTheme.ts +++ b/__mocks__/dataMocks/mockTheme.ts @@ -18,6 +18,8 @@ export const mockTheme: ThemeType = { syncing: '#ebff5a', notification: '', sideMenuBackground: '#040C17', + bottomSheetBackground: '#031124', + bottomSheetBorder: '#05234C', warning: { background: '#262527', border: '#65491C', diff --git a/__mocks__/dataMocks/mockTranslate.ts b/__mocks__/dataMocks/mockTranslate.ts index 60bcd6f1f0..528e50dcc3 100644 --- a/__mocks__/dataMocks/mockTranslate.ts +++ b/__mocks__/dataMocks/mockTranslate.ts @@ -1,5 +1,9 @@ -import { CurrencyEnum, LanguageEnum, BlockExplorerEnum } from '../../app/AppState'; -import { RPCPerformanceLevelEnum } from '../../app/rpc/enums/RPCPerformanceLevelEnum'; +import { + CurrencyEnum, + LanguageEnum, + BlockExplorerEnum, +} from '../../app/AppState'; +import { RPCPerformanceLevelEnum } from '../../app/walletBackend/enums/RPCPerformanceLevelEnum'; export const mockTranslate = (p: string) => { if (p === 'about.copyright') { @@ -29,10 +33,6 @@ export const mockTranslate = (p: string) => { value: CurrencyEnum.USDCurrency, text: 'text USD', }, - { - value: CurrencyEnum.USDTORCurrency, - text: 'text USD TOR', - }, ]; } else if (p === 'settings.languages') { return [ @@ -131,8 +131,12 @@ export const mockTranslate = (p: string) => { text: 'text Zcashexplorer', }, { - value: BlockExplorerEnum.Zypherscan, - text: 'text Zypherscan', + value: BlockExplorerEnum.Zexplorer, + text: 'text Zexplorer', + }, + { + value: BlockExplorerEnum.None, + text: 'text None', }, ]; } else { diff --git a/__mocks__/dataMocks/mockValueTransfers.ts b/__mocks__/dataMocks/mockValueTransfers.ts index d709f48cc6..00387941d9 100644 --- a/__mocks__/dataMocks/mockValueTransfers.ts +++ b/__mocks__/dataMocks/mockValueTransfers.ts @@ -1,5 +1,9 @@ -import { PoolEnum, ValueTransferType, ValueTransferKindEnum } from '../../app/AppState'; -import { RPCValueTransfersStatusEnum } from '../../app/rpc/enums/RPCValueTransfersStatusEnum'; +import { + PoolEnum, + ValueTransferType, + ValueTransferKindEnum, +} from '../../app/AppState'; +import { RPCValueTransfersStatusEnum } from '../../app/walletBackend/enums/RPCValueTransfersStatusEnum'; export const mockValueTransfers: ValueTransferType[] = [ { diff --git a/__mocks__/i18n-js.js b/__mocks__/i18n-js.js index c0411e8a20..5a466e2ee6 100644 --- a/__mocks__/i18n-js.js +++ b/__mocks__/i18n-js.js @@ -1,12 +1,12 @@ // i18n-js.js -const I18nCtor = jest.fn().mockImplementation((_file) => ({ - t: jest.fn((k) => k), +const I18nCtor = jest.fn().mockImplementation(_file => ({ + t: jest.fn(k => k), locale: 'en', })); module.exports = { __esModule: true, - I18n: I18nCtor, // soporte import nombrado - default: I18nCtor // soporte import por defecto + I18n: I18nCtor, // soporte import nombrado + default: I18nCtor, // soporte import por defecto }; diff --git a/__mocks__/moment.js b/__mocks__/moment.js index 253b719894..27a8a1111e 100644 --- a/__mocks__/moment.js +++ b/__mocks__/moment.js @@ -1,5 +1,5 @@ const mMoment = { - format: (p) => { + format: p => { if (p === 'MMM YYYY') { return 'Dec 2022'; } else if (p === 'YYYY MMM D h:mm a') { diff --git a/__mocks__/moment/locale/es.js b/__mocks__/moment/locale/es.js index 24ae07ecd3..540950b133 100644 --- a/__mocks__/moment/locale/es.js +++ b/__mocks__/moment/locale/es.js @@ -1,3 +1,3 @@ export default () => () => ({ - defineLocale: jest.fn(), + defineLocale: jest.fn(), }); diff --git a/__mocks__/moment/locale/pt.js b/__mocks__/moment/locale/pt.js index 24ae07ecd3..540950b133 100644 --- a/__mocks__/moment/locale/pt.js +++ b/__mocks__/moment/locale/pt.js @@ -1,3 +1,3 @@ export default () => () => ({ - defineLocale: jest.fn(), + defineLocale: jest.fn(), }); diff --git a/__mocks__/moment/locale/ru.js b/__mocks__/moment/locale/ru.js index 24ae07ecd3..540950b133 100644 --- a/__mocks__/moment/locale/ru.js +++ b/__mocks__/moment/locale/ru.js @@ -1,3 +1,3 @@ export default () => () => ({ - defineLocale: jest.fn(), + defineLocale: jest.fn(), }); diff --git a/__mocks__/react-native-context-menu-view.js b/__mocks__/react-native-context-menu-view.js deleted file mode 100644 index f30e939bba..0000000000 --- a/__mocks__/react-native-context-menu-view.js +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; - -const ContextMenu = ({ children }) => <>{children}; - -export default ContextMenu; -export const ContextMenuOnPressNativeEvent = {}; diff --git a/__mocks__/react-native-device-info.js b/__mocks__/react-native-device-info.js index 470408a9c2..22b7e80f6a 100644 --- a/__mocks__/react-native-device-info.js +++ b/__mocks__/react-native-device-info.js @@ -2,3 +2,6 @@ export const getSystemName = jest.fn(() => 'Mocked System Name'); export const getSystemVersion = jest.fn(() => 'Mocked System Version'); export const getManufacturer = jest.fn(() => 'Mocked Manufacturer'); export const getModel = jest.fn(() => 'Mocked Model'); +export const getVersion = jest.fn(() => '2.0.19'); +export const getBuildNumber = jest.fn(() => '307'); +export const getApplicationName = jest.fn(() => 'Zingo'); diff --git a/__mocks__/react-native-easy-biometrics.js b/__mocks__/react-native-easy-biometrics.js index 334be98024..7e40fbe192 100644 --- a/__mocks__/react-native-easy-biometrics.js +++ b/__mocks__/react-native-easy-biometrics.js @@ -1,3 +1,3 @@ export const RNBiometrics = { - requestBioAuth: jest.fn(() => Promise.resolve(true)), + requestBioAuth: jest.fn(() => Promise.resolve(true)), }; diff --git a/__mocks__/react-native-encrypted-storage.js b/__mocks__/react-native-encrypted-storage.js new file mode 100644 index 0000000000..2eda59e149 --- /dev/null +++ b/__mocks__/react-native-encrypted-storage.js @@ -0,0 +1,17 @@ +let store = {}; + +export default { + setItem: jest.fn((key, value) => { + store[key] = value; + return Promise.resolve(); + }), + getItem: jest.fn(key => Promise.resolve(store[key] ?? null)), + removeItem: jest.fn(key => { + delete store[key]; + return Promise.resolve(); + }), + clear: jest.fn(() => { + store = {}; + return Promise.resolve(); + }), +}; diff --git a/__mocks__/react-native-gesture-handler.js b/__mocks__/react-native-gesture-handler.js index 5eeb5070d2..cd536cf3e5 100644 --- a/__mocks__/react-native-gesture-handler.js +++ b/__mocks__/react-native-gesture-handler.js @@ -1,16 +1,16 @@ export const GestureHandlerRootView = ({ children }) => children; export default { - Swipeable: jest.fn().mockImplementation(({ children }) => children), - RNGestureHandlerModule: { - attachGestureHandler: jest.fn(), - createGestureHandler: jest.fn(), - dropGestureHandler: jest.fn(), - updateGestureHandler: jest.fn(), - forceTouchAvailable: jest.fn(), - hasGenericPassword: jest.fn(), - getSupportedBiometryType: jest.fn(), - State: {}, - Directions: {}, - }, + Swipeable: jest.fn().mockImplementation(({ children }) => children), + RNGestureHandlerModule: { + attachGestureHandler: jest.fn(), + createGestureHandler: jest.fn(), + dropGestureHandler: jest.fn(), + updateGestureHandler: jest.fn(), + forceTouchAvailable: jest.fn(), + hasGenericPassword: jest.fn(), + getSupportedBiometryType: jest.fn(), + State: {}, + Directions: {}, + }, }; diff --git a/__mocks__/react-native-keyboard-controller.js b/__mocks__/react-native-keyboard-controller.js new file mode 100644 index 0000000000..7138844a60 --- /dev/null +++ b/__mocks__/react-native-keyboard-controller.js @@ -0,0 +1,69 @@ +// Minimal mock for react-native-keyboard-controller. +// Tests run in a JSDOM-style environment with no native keyboard module — +// we just stub the provider as a passthrough and the hooks return safe +// defaults (no keyboard visible, height 0). +const React = require('react'); + +const passthrough = ({ children }) => children; + +const defaultState = { + isVisible: false, + height: 0, + duration: 0, + timestamp: 0, + target: -1, + type: 0, + appearance: 'light', +}; + +const useKeyboardState = selector => + selector ? selector(defaultState) : defaultState; + +const useKeyboardAnimation = () => ({ + progress: { value: 0 }, + height: { value: 0 }, +}); + +const useReanimatedKeyboardAnimation = () => ({ + progress: { value: 0 }, + height: { value: 0 }, +}); + +const noop = () => {}; + +module.exports = { + KeyboardProvider: passthrough, + KeyboardAvoidingView: passthrough, + KeyboardStickyView: passthrough, + KeyboardAwareScrollView: passthrough, + KeyboardToolbar: passthrough, + OverKeyboardView: passthrough, + KeyboardExtender: passthrough, + KeyboardChatScrollView: passthrough, + useKeyboardState, + useKeyboardAnimation, + useReanimatedKeyboardAnimation, + useKeyboardHandler: noop, + useGenericKeyboardHandler: noop, + useResizeMode: noop, + useKeyboardController: () => ({ + setEnabled: noop, + enabled: true, + }), + useFocusedInputHandler: noop, + useReanimatedFocusedInput: () => ({ input: { value: null } }), + KeyboardEvents: { + addListener: () => ({ remove: noop }), + }, + KeyboardController: { + dismiss: noop, + setInputMode: noop, + setDefaultMode: noop, + isVisible: () => false, + }, +}; +// Keep React import referenced so bundlers don't strip it in environments +// that need it for createElement (the passthroughs above are React +// children, not elements, so this is belt-and-suspenders). + +React; diff --git a/__mocks__/react-native-keychain.js b/__mocks__/react-native-keychain.js index 80d5c3d87d..8ee897d85b 100644 --- a/__mocks__/react-native-keychain.js +++ b/__mocks__/react-native-keychain.js @@ -1,21 +1,21 @@ export const ACCESS_CONTROL = { - BIOMETRY_CURRENT_SET: 'biometryCurrentSet', + BIOMETRY_CURRENT_SET: 'biometryCurrentSet', }; export const ACCESSIBLE = { - WHEN_UNLOCKED_THIS_DEVICE_ONLY: 'whenUnlockedThisDeviceOnly', + WHEN_UNLOCKED_THIS_DEVICE_ONLY: 'whenUnlockedThisDeviceOnly', }; export const AUTHENTICATION_TYPE = { - BIOMETRICS: 'biometrics', + BIOMETRICS: 'biometrics', }; export const SECURITY_LEVEL = { - SECURE_SOFTWARE: 'secureSoftware', + SECURE_SOFTWARE: 'secureSoftware', }; export const SECURITY_RULES = { - NONE: 'none', + NONE: 'none', }; export const STORAGE_TYPE = { - RSA : 'RSA', - AES_GCM_NO_AUTH: 'AES_GCM_NO_AUTH', + RSA: 'RSA', + AES_GCM_NO_AUTH: 'AES_GCM_NO_AUTH', }; export const setGenericPassword = jest.fn(); export const getGenericPassword = jest.fn(); diff --git a/__mocks__/react-native-linear-gradient.js b/__mocks__/react-native-linear-gradient.js new file mode 100644 index 0000000000..476be851ec --- /dev/null +++ b/__mocks__/react-native-linear-gradient.js @@ -0,0 +1,6 @@ +import React from 'react'; +import { View } from 'react-native'; + +const LinearGradient = props => ; + +export default LinearGradient; diff --git a/__mocks__/react-native-localize.js b/__mocks__/react-native-localize.js index 7f0481d1e9..33833bfa62 100644 --- a/__mocks__/react-native-localize.js +++ b/__mocks__/react-native-localize.js @@ -1,6 +1,6 @@ export const getNumberFormatSettings = () => { - return { - decimalSeparator: '.', // us - groupingSeparator: ',', // us - }; + return { + decimalSeparator: '.', // us + groupingSeparator: ',', // us }; +}; diff --git a/__mocks__/react-native-picker-select.js b/__mocks__/react-native-picker-select.js deleted file mode 100644 index 9793945cf9..0000000000 --- a/__mocks__/react-native-picker-select.js +++ /dev/null @@ -1 +0,0 @@ -export default () => 'RNPickerSelect'; diff --git a/__mocks__/react-native-reanimated.js b/__mocks__/react-native-reanimated.js index 015d8007af..b4759bb462 100644 --- a/__mocks__/react-native-reanimated.js +++ b/__mocks__/react-native-reanimated.js @@ -5,10 +5,20 @@ const Animated = class Animated { }; const useAnimatedStyle = jest.fn(() => ({})); -const useSharedValue = jest.fn((v) => ({ value: v })); +const useSharedValue = jest.fn(v => ({ value: v })); const useAnimatedGestureHandler = jest.fn(() => ({})); -const withTiming = jest.fn((v) => v); -const withSpring = jest.fn((v) => v); +const useAnimatedReaction = jest.fn(); +const withTiming = jest.fn(v => v); +const withSpring = jest.fn(v => v); +const runOnJS = jest.fn(fn => fn); +const Easing = { + linear: v => v, + ease: v => v, + cubic: v => v, + out: () => v => v, + in: () => v => v, + inOut: () => v => v, +}; export default Animated; @@ -16,6 +26,9 @@ export { useAnimatedStyle, useSharedValue, useAnimatedGestureHandler, + useAnimatedReaction, withTiming, withSpring, + runOnJS, + Easing, }; diff --git a/__mocks__/react-native-safe-area-context.js b/__mocks__/react-native-safe-area-context.js index f4f2a64e9f..86bff7a349 100644 --- a/__mocks__/react-native-safe-area-context.js +++ b/__mocks__/react-native-safe-area-context.js @@ -1,2 +1,9 @@ export const SafeAreaProvider = ({ children }) => children; export const SafeAreaView = ({ children }) => children; +export const useSafeAreaInsets = () => ({ + top: 0, + right: 0, + bottom: 0, + left: 0, +}); +export const useSafeAreaFrame = () => ({ x: 0, y: 0, width: 0, height: 0 }); diff --git a/__mocks__/react-native-svg.js b/__mocks__/react-native-svg.js new file mode 100644 index 0000000000..bf3a97ac0c --- /dev/null +++ b/__mocks__/react-native-svg.js @@ -0,0 +1,9 @@ +const mock = () => null; +export default mock; +export const SvgXml = () => null; +export const Svg = () => null; +export const Path = () => null; +export const Polygon = () => null; +export const Rect = () => null; +export const G = () => null; +export const Circle = () => null; diff --git a/__mocks__/react-native-tab-view.js b/__mocks__/react-native-tab-view.js deleted file mode 100644 index a892d9ad2a..0000000000 --- a/__mocks__/react-native-tab-view.js +++ /dev/null @@ -1,2 +0,0 @@ -export const TabView = ''; -export const TabBar = ''; diff --git a/__mocks__/react-native-toast-message.js b/__mocks__/react-native-toast-message.js new file mode 100644 index 0000000000..9eed679f46 --- /dev/null +++ b/__mocks__/react-native-toast-message.js @@ -0,0 +1,6 @@ +const Toast = { + show: jest.fn(), + hide: jest.fn(), +}; + +export default Toast; diff --git a/__mocks__/react-native.js b/__mocks__/react-native.js index 341aff3443..adaa817bc8 100644 --- a/__mocks__/react-native.js +++ b/__mocks__/react-native.js @@ -5,9 +5,16 @@ jest.mock('react-native', () => { getLatestBlockServerInfo: jest.fn(() => '{}'), getLatestBlockWalletInfo: jest.fn(() => '{}'), walletExists: jest.fn(() => 'false'), - getValueTransfersList: jest.fn(() => '{ "value_transfers": [], "total": 0 }'), + walletBackupExists: jest.fn(() => 'false'), + doSaveBackup: jest.fn(() => 'true'), + restoreExistingWalletBackup: jest.fn(() => '{}'), + getValueTransfersList: jest.fn( + () => '{ "value_transfers": [], "total": 0 }', + ), setCryptoDefaultProvider: jest.fn(() => 'true'), - createNewWallet: jest.fn(() => '{ "seed": "seed phrase test", "birthday": 0 }'), + createNewWallet: jest.fn( + () => '{ "seed": "seed phrase test", "birthday": 0 }', + ), doSave: jest.fn(), pollSyncInfo: jest.fn(() => '{}'), runSyncProcess: jest.fn(() => '{}'), @@ -33,7 +40,6 @@ jest.mock('react-native', () => { getSpendableBalanceTotalInfo: jest.fn(() => '{}'), getOptionWalletInfo: jest.fn(() => '{}'), setOptionWalletProcess: jest.fn(() => '{}'), - createTorClientProcess: jest.fn(() => '{}'), getUnifiedAddressesInfo: jest.fn(() => '{}'), getTransparentAddressesInfo: jest.fn(() => '{}'), createNewUnifiedAddressProcess: jest.fn(() => '{}'), @@ -49,6 +55,7 @@ jest.mock('react-native', () => { getZenniesDonationAddress: jest.fn(() => '{}'), }; RN.View = jest.fn(); + RN.RefreshControl = jest.fn(() => null); return RN; }); diff --git a/__mocks__/svgMock.js b/__mocks__/svgMock.js new file mode 100644 index 0000000000..71ee2012f0 --- /dev/null +++ b/__mocks__/svgMock.js @@ -0,0 +1,6 @@ +import React from 'react'; +import { View } from 'react-native'; + +const SvgMock = props => ; + +export default SvgMock; diff --git a/__tests__/AbSummaryLine.snapshot.tsx b/__tests__/AbSummaryLine.snapshot.tsx new file mode 100644 index 0000000000..e4ba0efb83 --- /dev/null +++ b/__tests__/AbSummaryLine.snapshot.tsx @@ -0,0 +1,98 @@ +/** + * @format + */ + +import 'react-native'; +import React from 'react'; +import { render } from '@testing-library/react-native'; +import { + ContextAppLoadedProvider, + defaultAppContextLoaded, +} from '../app/context'; + +import AbSummaryLine from '../components/AddressBook/components/AbSummaryLine'; +import { AddressBookActionEnum, ModeEnum } from '../app/AppState'; +import { mockTranslate } from '../__mocks__/dataMocks/mockTranslate'; +import { mockAddressBook } from '../__mocks__/dataMocks/mockAddressBook'; +import { mockTotalBalance } from '../__mocks__/dataMocks/mockTotalBalance'; + +describe('AbSummaryLine - snapshots', () => { + const state = { ...defaultAppContextLoaded }; + state.translate = mockTranslate; + state.addressBook = mockAddressBook; + state.totalBalance = mockTotalBalance; + state.mode = ModeEnum.advanced; + + const onFn = jest.fn(); + + test('AbSummaryLine normal contact', () => { + expect( + render( + + void + } + /> + , + ).toJSON(), + ).toMatchSnapshot(); + }); + + test('AbSummaryLine protected address', () => { + expect( + render( + + void + } + addressProtected + /> + , + ).toJSON(), + ).toMatchSnapshot(); + }); + + test('AbSummaryLine readOnly mode', () => { + const roState = { ...state, readOnly: true }; + expect( + render( + + void + } + /> + , + ).toJSON(), + ).toMatchSnapshot(); + }); +}); diff --git a/__tests__/About.snapshot.tsx b/__tests__/About.snapshot.tsx index fb0b519e2a..b1783c92c9 100644 --- a/__tests__/About.snapshot.tsx +++ b/__tests__/About.snapshot.tsx @@ -7,16 +7,22 @@ import React from 'react'; import { render } from '@testing-library/react-native'; import About from '../components/About'; -import { ContextAppLoadedProvider, defaultAppContextLoaded } from '../app/context'; +import { + ContextAppLoadedProvider, + defaultAppContextLoaded, +} from '../app/context'; import { mockInfo } from '../__mocks__/dataMocks/mockInfo'; import { mockTotalBalance } from '../__mocks__/dataMocks/mockTotalBalance'; import { mockTranslate } from '../__mocks__/dataMocks/mockTranslate'; -import { DrawerScreenProps } from '@react-navigation/drawer'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { AppDrawerParamList } from '../app/types'; import { RouteEnum } from '../app/AppState'; import mockNavigation from '../__mocks__/dataMocks/mockNavigation'; -function makeDrawerProps(): DrawerScreenProps { +function makeDrawerProps(): NativeStackScreenProps< + AppDrawerParamList, + RouteEnum.About +> { return { navigation: mockNavigation, route: { @@ -30,7 +36,7 @@ function makeDrawerProps(): DrawerScreenProps { //snapshot test test('About - snapshot', () => { - const state = defaultAppContextLoaded; + const state = { ...defaultAppContextLoaded }; state.translate = mockTranslate; state.info = mockInfo; state.totalBalance = mockTotalBalance; diff --git a/__tests__/AddressBook.AbDetail.snapshot.tsx b/__tests__/AddressBook.AbDetail.snapshot.tsx index 01184b2d40..88cebe9e71 100644 --- a/__tests__/AddressBook.AbDetail.snapshot.tsx +++ b/__tests__/AddressBook.AbDetail.snapshot.tsx @@ -6,16 +6,24 @@ import 'react-native'; import React from 'react'; import { render, RenderResult } from '@testing-library/react-native'; -import { ContextAppLoadedProvider, defaultAppContextLoaded } from '../app/context'; +import { + ContextAppLoadedProvider, + defaultAppContextLoaded, +} from '../app/context'; import AbDetail from '../components/AddressBook/components/AbDetail'; -import { AddressBookActionEnum, AddressBookFileClass, RouteEnum, ScreenEnum } from '../app/AppState'; +import { + AddressBookActionEnum, + AddressBookFileClass, + RouteEnum, + ScreenEnum, +} from '../app/AppState'; import { mockTranslate } from '../__mocks__/dataMocks/mockTranslate'; import { mockAddressBook } from '../__mocks__/dataMocks/mockAddressBook'; // test suite describe('Component Address Book Details - test', () => { //snapshot test - const state = defaultAppContextLoaded; + const state = { ...defaultAppContextLoaded }; state.addressBook = mockAddressBook; state.translate = mockTranslate; const onCancel = jest.fn(); @@ -30,7 +38,8 @@ describe('Component Address Book Details - test', () => { action={AddressBookActionEnum.Add} doAction={onAction} screenName={ScreenEnum.AddressBook} - routeStack={RouteEnum.AddressBookStack} + routeStack={RouteEnum.AddressBook} + navigation={{ navigate: onCancel } as never} /> , ); @@ -46,7 +55,8 @@ describe('Component Address Book Details - test', () => { action={AddressBookActionEnum.Modify} doAction={onAction} screenName={ScreenEnum.AddressBook} - routeStack={RouteEnum.ConfirmStack} + routeStack={RouteEnum.AddressBook} + navigation={{ navigate: onCancel } as never} /> , ); @@ -62,7 +72,8 @@ describe('Component Address Book Details - test', () => { action={AddressBookActionEnum.Delete} doAction={onAction} screenName={ScreenEnum.AddressBook} - routeStack={RouteEnum.ValueTransferDetailStack} + routeStack={RouteEnum.AddressBook} + navigation={{ navigate: onCancel } as never} /> , ); diff --git a/__tests__/AddressBook.snapshot.tsx b/__tests__/AddressBook.snapshot.tsx index d187e8075f..82e598750a 100644 --- a/__tests__/AddressBook.snapshot.tsx +++ b/__tests__/AddressBook.snapshot.tsx @@ -6,16 +6,22 @@ import 'react-native'; import React from 'react'; import { render, RenderResult } from '@testing-library/react-native'; -import { ContextAppLoadedProvider, defaultAppContextLoaded } from '../app/context'; +import { + ContextAppLoadedProvider, + defaultAppContextLoaded, +} from '../app/context'; import { AddressBook } from '../components/AddressBook'; import { mockAddressBook } from '../__mocks__/dataMocks/mockAddressBook'; import { mockTranslate } from '../__mocks__/dataMocks/mockTranslate'; -import { DrawerScreenProps } from '@react-navigation/drawer'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { AppDrawerParamList } from '../app/types'; import { RouteEnum } from '../app/AppState'; import mockNavigation from '../__mocks__/dataMocks/mockNavigation'; -function makeDrawerProps(): DrawerScreenProps { +function makeDrawerProps(): NativeStackScreenProps< + AppDrawerParamList, + RouteEnum.AddressBook +> { return { navigation: mockNavigation, route: { @@ -29,7 +35,7 @@ function makeDrawerProps(): DrawerScreenProps { //snapshot test test('Address Book - snapshot', () => { - const state = defaultAppContextLoaded; + const state = { ...defaultAppContextLoaded }; state.addressBook = mockAddressBook; state.translate = mockTranslate; const onSet = jest.fn(); diff --git a/__tests__/AddressItem.snapshot.tsx b/__tests__/AddressItem.snapshot.tsx new file mode 100644 index 0000000000..7691c440c3 --- /dev/null +++ b/__tests__/AddressItem.snapshot.tsx @@ -0,0 +1,81 @@ +/** + * @format + */ + +import 'react-native'; +import React from 'react'; +import { render } from '@testing-library/react-native'; +import { + ContextAppLoadedProvider, + defaultAppContextLoaded, +} from '../app/context'; + +import AddressItem from '../components/Components/AddressItem'; +import { ModeEnum, ScreenEnum } from '../app/AppState'; +import { mockTranslate } from '../__mocks__/dataMocks/mockTranslate'; +import { mockAddressBook } from '../__mocks__/dataMocks/mockAddressBook'; +import { mockTotalBalance } from '../__mocks__/dataMocks/mockTotalBalance'; + +describe('AddressItem - snapshots', () => { + const state = { ...defaultAppContextLoaded }; + state.translate = mockTranslate; + state.addressBook = mockAddressBook; + state.totalBalance = mockTotalBalance; + state.mode = ModeEnum.advanced; + + test('AddressItem unknown address, no contact', () => { + expect( + render( + + + , + ).toJSON(), + ).toMatchSnapshot(); + }); + + test('AddressItem known contact from address book with icons', () => { + expect( + render( + + + , + ).toJSON(), + ).toMatchSnapshot(); + }); + + test('AddressItem oneLine mode', () => { + expect( + render( + + + , + ).toJSON(), + ).toMatchSnapshot(); + }); + + test('AddressItem privacy mode', () => { + const privState = { ...state, privacy: true }; + expect( + render( + + + , + ).toJSON(), + ).toMatchSnapshot(); + }); +}); diff --git a/__tests__/AddressList.snapshot.tsx b/__tests__/AddressList.snapshot.tsx index 90d2a709f8..da0eb3c4ea 100644 --- a/__tests__/AddressList.snapshot.tsx +++ b/__tests__/AddressList.snapshot.tsx @@ -6,16 +6,21 @@ import 'react-native'; import React from 'react'; import { render } from '@testing-library/react-native'; -import { ContextAppLoadedProvider, defaultAppContextLoaded } from '../app/context'; +import { + ContextAppLoadedProvider, + defaultAppContextLoaded, +} from '../app/context'; import { AddressList } from '../components/AddressList'; import { mockTranslate } from '../__mocks__/dataMocks/mockTranslate'; import { mockAddresses } from '../__mocks__/dataMocks/mockAddresses'; import { AddressKindEnum, RouteEnum } from '../app/AppState'; -import { DrawerScreenProps } from '@react-navigation/drawer'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { AppDrawerParamList } from '../app/types'; import mockNavigation from '../__mocks__/dataMocks/mockNavigation'; -function makeDrawerProps(ak: AddressKindEnum): DrawerScreenProps { +function makeDrawerProps( + ak: AddressKindEnum, +): NativeStackScreenProps { return { navigation: mockNavigation, route: { @@ -32,11 +37,11 @@ function makeDrawerProps(ak: AddressKindEnum): DrawerScreenProps { //snapshot test test('Address Unified List - snapshot', () => { - const state = defaultAppContextLoaded; + const state = { ...defaultAppContextLoaded }; state.addresses = mockAddresses; state.translate = mockTranslate; const props = makeDrawerProps(AddressKindEnum.u); - const al: any = render( + const al = render( , @@ -45,11 +50,11 @@ describe('Component Unified Address List - test', () => { }); test('Address Transparent List - snapshot', () => { - const state = defaultAppContextLoaded; + const state = { ...defaultAppContextLoaded }; state.addresses = mockAddresses; state.translate = mockTranslate; const props = makeDrawerProps(AddressKindEnum.t); - const al: any = render( + const al = render( , diff --git a/__tests__/AlSummaryLine.snapshot.tsx b/__tests__/AlSummaryLine.snapshot.tsx new file mode 100644 index 0000000000..eb5c33acdd --- /dev/null +++ b/__tests__/AlSummaryLine.snapshot.tsx @@ -0,0 +1,81 @@ +/** + * @format + */ + +import 'react-native'; +import React from 'react'; +import { render } from '@testing-library/react-native'; +import { + ContextAppLoadedProvider, + defaultAppContextLoaded, +} from '../app/context'; + +import { ChainNameEnum } from '../app/AppState'; +import AlSummaryLine from '../components/AddressList/components/AlSummaryLine'; +import { mockTranslate } from '../__mocks__/dataMocks/mockTranslate'; +import { mockAddresses } from '../__mocks__/dataMocks/mockAddresses'; +import { mockAddressBook } from '../__mocks__/dataMocks/mockAddressBook'; + +describe('AlSummaryLine - snapshots', () => { + const state = { ...defaultAppContextLoaded }; + state.translate = mockTranslate; + state.addressBook = mockAddressBook; + + const onFn = jest.fn(); + + test('AlSummaryLine unified address', () => { + expect( + render( + + + , + ).toJSON(), + ).toMatchSnapshot(); + }); + + test('AlSummaryLine transparent address', () => { + expect( + render( + + + , + ).toJSON(), + ).toMatchSnapshot(); + }); + + test('AlSummaryLine address with contact label', () => { + const stateWithContact = { ...state }; + stateWithContact.addressBook = [ + { + label: 'pepe', + address: mockAddresses[0].address, + color: '#000', + own: false, + chain: ChainNameEnum.mainChainName, + swapChain: 'ZEC', + }, + ]; + expect( + render( + + + , + ).toJSON(), + ).toMatchSnapshot(); + }); +}); diff --git a/__tests__/Button.snapshot.tsx b/__tests__/Button.snapshot.tsx index a2bdcd4fe6..97db9913aa 100644 --- a/__tests__/Button.snapshot.tsx +++ b/__tests__/Button.snapshot.tsx @@ -15,7 +15,13 @@ describe('Component Button - test', () => { const onPress = jest.fn(); test('Button Primary - snapshot', () => { const button = render( -