From a4a8e702bf54b9a6a12066c7ed2bc740e1653181 Mon Sep 17 00:00:00 2001 From: koalasat Date: Fri, 18 Jul 2025 15:22:32 +0200 Subject: [PATCH 01/18] New movile release workflow --- .github/workflows/android-build.yml | 286 ++++++++++++++-------------- 1 file changed, 140 insertions(+), 146 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index ff16e8d8..9b65264c 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -30,165 +30,159 @@ jobs: - name: 'Checkout' uses: actions/checkout@v4 - # - name: 'Download Android Web.bundle Artifact (built frontend)' - # if: inputs.semver == '' # Only if workflow fired from frontend-build.yml - # uses: dawidd6/action-download-artifact@v11 - # with: - # workflow: frontend-build.yml - # workflow_conclusion: success - # name: mobile-web.bundle - # path: mobile/html/Web.bundle + - name: 'Download Android Web.bundle Artifact (built frontend)' + if: inputs.semver == '' # Only if workflow fired from frontend-build.yml + uses: dawidd6/action-download-artifact@v11 + with: + workflow: frontend-build.yml + workflow_conclusion: success + name: mobile-web.bundle + path: android/app/src/main/assets - # - name: 'Download main.js Artifact for a release' - # if: inputs.semver != '' # Only if fired as job in release.yml - # uses: actions/download-artifact@v4 - # with: - # name: mobile-web.bundle - # path: mobile/html/Web.bundle + - name: 'Download main.js Artifact for a release' + if: inputs.semver != '' # Only if fired as job in release.yml + uses: actions/download-artifact@v4 + with: + name: mobile-web.bundle + path: android/app/src/main/assets - # - name: 'Install npm Dependencies' - # run: | - # cd mobile - # npm install + - name: Cache gradle + uses: actions/cache@v4 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + restore-keys: | + ${{ runner.os }}-gradle- - # - name: 'Patch modules' # react-native-tor and react-native-encrypted-storage rely on deprecated jcenter repositories. We patch the modules temporarily - # run: | - # cd mobile - # cp -r patch_modules/* node_modules/ - # - uses: actions/setup-java@v4 - # with: - # distribution: temurin - # java-version: 11 + - name: Build APK + run: ./gradlew assembleRelease --stacktrace - # - name: Setup Gradle - # uses: gradle/gradle-build-action@v3 + - name: 'Check for non-FOSS libraries' + run: | + wget https://github.com/iBotPeaches/Apktool/releases/download/v2.7.0/apktool_2.7.0.jar + wget https://github.com/iBotPeaches/Apktool/raw/master/scripts/linux/apktool + # clone the repo + git clone https://gitlab.com/IzzyOnDroid/repo.git + # create a directory for Apktool and move the apktool* files there + mkdir -p repo/lib/radar/tool + mv apktool* repo/lib/radar/tool + # create an alias for ease of use + chmod u+x repo/lib/radar/tool/apktool + mv repo/lib/radar/tool/apktool_2.7.0.jar repo/lib/radar/tool/apktool.jar + repo/bin/scanapk.php app/build/outputs/apk/release/app-universal-release-unsigned.apk - # - name: Decode Keystore - # id: decode_keystore - # uses: timheuer/base64-to-file@v1.2 - # with: - # fileName: 'keystore.jks' - # fileDir: './' - # encodedString: ${{ secrets.KEYSTORE }} + - name: Sign APK + uses: r0adkll/sign-android-release@v1 + with: + releaseDirectory: app/build/outputs/apk/release + signingKeyBase64: ${{ secrets.KEYSTORE }} + alias: ${{ secrets.KEY_ALIAS }} + keyStorePassword: ${{ secrets.KEY_STORE_PASS }} + keyPassword: ${{ secrets.KEY_PASS }} + env: + BUILD_TOOLS_VERSION: "34.0.0" - # - name: 'Build Android Release' - # run: | - # cd mobile/android - # ./gradlew assembleRelease - # env: - # KEY_ALIAS: ${{ secrets.KEY_ALIAS }} - # KEY_PASS: ${{ secrets.KEY_PASS }} - # KEY_STORE_PASS: ${{ secrets.KEY_STORE_PASS }} + - uses: kaisugi/action-regex-match@v1.0.1 + id: regex-match + with: + text: ${{ github.ref }} + regex: '(v*-pre*)' + flags: gm + - name: 'Get Commit Hash' + id: commit + uses: pr-mpt/actions-commit-hash@v3 - # - name: 'Check for non-FOSS libraries' - # run: | - # wget https://github.com/iBotPeaches/Apktool/releases/download/v2.7.0/apktool_2.7.0.jar - # wget https://github.com/iBotPeaches/Apktool/raw/master/scripts/linux/apktool - # # clone the repo - # git clone https://gitlab.com/IzzyOnDroid/repo.git - # # create a directory for Apktool and move the apktool* files there - # mkdir -p repo/lib/radar/tool - # mv apktool* repo/lib/radar/tool - # # create an alias for ease of use - # chmod u+x repo/lib/radar/tool/apktool - # mv repo/lib/radar/tool/apktool_2.7.0.jar repo/lib/radar/tool/apktool.jar - # repo/bin/scanapk.php mobile/android/app/build/outputs/apk/release/app-universal-release.apk + # Create artifacts (only for Release) + # Create app-universal-release APK artifact asset for Release + - name: 'Upload universal .apk Release Artifact (for Release)' + uses: actions/upload-artifact@v4 + if: inputs.semver != '' # If this workflow is called from release.yml + with: + name: robosats-${{ inputs.semver }}-universal.apk + path: android/app/build/outputs/apk/release/app-universal-release.apk - # - name: 'Get Commit Hash' - # id: commit - # uses: pr-mpt/actions-commit-hash@v3 + # Create app-arm64-v8a-release APK artifact asset for Release + - name: 'Upload arm64-v8a .apk Release Artifact (for Release)' + uses: actions/upload-artifact@v4 + if: inputs.semver != '' # If this workflow is called from release.yml + with: + name: robosats-${{ inputs.semver }}-arm64-v8a.apk + path: android/app/build/outputs/apk/release/app-arm64-v8a-release.apk - # # Create artifacts (only for Release) - # # Create app-universal-release APK artifact asset for Release - # - name: 'Upload universal .apk Release Artifact (for Release)' - # uses: actions/upload-artifact@v4 - # if: inputs.semver != '' # If this workflow is called from release.yml - # with: - # name: robosats-${{ inputs.semver }}-universal.apk - # path: mobile/android/app/build/outputs/apk/release/app-universal-release.apk + # Create app-armeabi-v7a-release APK artifact asset for Release + - name: 'Upload armeabi-v7a .apk Release Artifact (for Release)' + uses: actions/upload-artifact@v4 + if: inputs.semver != '' # If this workflow is called from release.yml + with: + name: robosats-${{ inputs.semver }}-armeabi-v7a.apk + path: android/app/build/outputs/apk/release/app-armeabi-v7a-release.apk - # # Create app-arm64-v8a-release APK artifact asset for Release - # - name: 'Upload arm64-v8a .apk Release Artifact (for Release)' - # uses: actions/upload-artifact@v4 - # if: inputs.semver != '' # If this workflow is called from release.yml - # with: - # name: robosats-${{ inputs.semver }}-arm64-v8a.apk - # path: mobile/android/app/build/outputs/apk/release/app-arm64-v8a-release.apk + # Create app-x86_64-release APK artifact asset for Release + - name: 'Upload x86_64 .apk Release Artifact (for Release)' + uses: actions/upload-artifact@v4 + if: inputs.semver != '' # If this workflow is called from release.yml + with: + name: robosats-${{ inputs.semver }}-x86_64.apk + path: android/app/build/outputs/apk/release/app-x86_64-release.apk - # # Create app-armeabi-v7a-release APK artifact asset for Release - # - name: 'Upload armeabi-v7a .apk Release Artifact (for Release)' - # uses: actions/upload-artifact@v4 - # if: inputs.semver != '' # If this workflow is called from release.yml - # with: - # name: robosats-${{ inputs.semver }}-armeabi-v7a.apk - # path: mobile/android/app/build/outputs/apk/release/app-armeabi-v7a-release.apk + - name: 'Create Pre-release' + id: create_release + if: inputs.semver == '' + uses: ncipollo/release-action@v1.18.0 + with: + tag: android-${{ steps.commit.outputs.short }} + name: robosats-android-${{ steps.commit.outputs.short }} + prerelease: true - # # Create app-x86_64-release APK artifact asset for Release - # - name: 'Upload x86_64 .apk Release Artifact (for Release)' - # uses: actions/upload-artifact@v4 - # if: inputs.semver != '' # If this workflow is called from release.yml - # with: - # name: robosats-${{ inputs.semver }}-x86_64.apk - # path: mobile/android/app/build/outputs/apk/release/app-x86_64-release.apk + # Upload universal APK to pre-release + - name: 'Upload universal Pre-release APK Asset' + id: upload-release-universal-apk-asset + if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release) + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./android/app/build/outputs/apk/release/app-universal-release.apk + asset_name: robosats-${{ steps.commit.outputs.short }}-universal.apk + asset_content_type: application/apk - # - name: 'Create Pre-release' - # id: create_release - # if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release) - # uses: ncipollo/release-action@v1.18.0 - # with: - # tag: android-${{ steps.commit.outputs.short }} - # name: robosats-android-${{ steps.commit.outputs.short }} - # prerelease: true + # Upload arm64-v8a APK to pre-release + - name: 'Upload arm64-v8a Pre-release APK Asset' + id: upload-release-arm64-v8a-apk-asset + if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release) + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./android/app/build/outputs/apk/release/app-arm64-v8a-release.apk + asset_name: robosats-${{ steps.commit.outputs.short }}-arm64-v8a.apk + asset_content_type: application/apk - # # Upload universal APK to pre-release - # - name: 'Upload universal Pre-release APK Asset' - # id: upload-release-universal-apk-asset - # if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release) - # uses: actions/upload-release-asset@v1 - # env: - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # with: - # upload_url: ${{ steps.create_release.outputs.upload_url }} - # asset_path: ./mobile/android/app/build/outputs/apk/release/app-universal-release.apk - # asset_name: robosats-${{ steps.commit.outputs.short }}-universal.apk - # asset_content_type: application/apk + # Upload armeabi-v7a APK to pre-release + - name: 'Upload armeabi-v7a Pre-release APK Asset' + id: upload-release-armeabi-v7a-apk-asset + if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release) + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./android/app/build/outputs/apk/release/app-armeabi-v7a-release.apk + asset_name: robosats-${{ steps.commit.outputs.short }}-armeabi-v7a.apk + asset_content_type: application/apk - # # Upload arm64-v8a APK to pre-release - # - name: 'Upload arm64-v8a Pre-release APK Asset' - # id: upload-release-arm64-v8a-apk-asset - # if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release) - # uses: actions/upload-release-asset@v1 - # env: - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # with: - # upload_url: ${{ steps.create_release.outputs.upload_url }} - # asset_path: ./mobile/android/app/build/outputs/apk/release/app-arm64-v8a-release.apk - # asset_name: robosats-${{ steps.commit.outputs.short }}-arm64-v8a.apk - # asset_content_type: application/apk - - # # Upload armeabi-v7a APK to pre-release - # - name: 'Upload armeabi-v7a Pre-release APK Asset' - # id: upload-release-armeabi-v7a-apk-asset - # if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release) - # uses: actions/upload-release-asset@v1 - # env: - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # with: - # upload_url: ${{ steps.create_release.outputs.upload_url }} - # asset_path: ./mobile/android/app/build/outputs/apk/release/app-armeabi-v7a-release.apk - # asset_name: robosats-${{ steps.commit.outputs.short }}-armeabi-v7a.apk - # asset_content_type: application/apk - - # # Upload x86_64 APK to pre-release - # - name: 'Upload x86_64 Pre-release APK Asset' - # id: upload-release-x86_64-apk-asset - # if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release) - # uses: actions/upload-release-asset@v1 - # env: - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # with: - # upload_url: ${{ steps.create_release.outputs.upload_url }} - # asset_path: ./mobile/android/app/build/outputs/apk/release/app-x86_64-release.apk - # asset_name: robosats-${{ steps.commit.outputs.short }}-x86_64.apk - # asset_content_type: application/apk + # Upload x86_64 APK to pre-release + - name: 'Upload x86_64 Pre-release APK Asset' + id: upload-release-x86_64-apk-asset + if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release) + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./android/app/build/outputs/apk/release/app-x86_64-release.apk + asset_name: robosats-${{ steps.commit.outputs.short }}-x86_64.apk + asset_content_type: application/apk From 3fa54b40fd2676898eb716590da206c98dedc37f Mon Sep 17 00:00:00 2001 From: koalasat Date: Fri, 18 Jul 2025 15:25:28 +0200 Subject: [PATCH 02/18] New movile version --- android/app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 262fd95f..e647da3b 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "com.robosats" minSdk = 24 targetSdk = 36 - versionCode = 1 - versionName = "1.0" + versionCode = 15 + versionName = "0.8.1-alpha" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } From d46656b758cfac32aec4b66f4d5fddfe65b73af8 Mon Sep 17 00:00:00 2001 From: koalasat Date: Fri, 18 Jul 2025 15:29:56 +0200 Subject: [PATCH 03/18] Fix workflow --- .github/workflows/android-build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 9b65264c..30179d20 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -55,7 +55,7 @@ jobs: ${{ runner.os }}-gradle- - name: Build APK - run: ./gradlew assembleRelease --stacktrace + run: android/gradlew assembleRelease --stacktrace - name: 'Check for non-FOSS libraries' run: | @@ -69,7 +69,7 @@ jobs: # create an alias for ease of use chmod u+x repo/lib/radar/tool/apktool mv repo/lib/radar/tool/apktool_2.7.0.jar repo/lib/radar/tool/apktool.jar - repo/bin/scanapk.php app/build/outputs/apk/release/app-universal-release-unsigned.apk + repo/bin/scanapk.php android/app/build/outputs/apk/release/app-universal-release-unsigned.apk - name: Sign APK uses: r0adkll/sign-android-release@v1 @@ -144,7 +144,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./android/app/build/outputs/apk/release/app-universal-release.apk + asset_path: android/app/build/outputs/apk/release/app-universal-release.apk asset_name: robosats-${{ steps.commit.outputs.short }}-universal.apk asset_content_type: application/apk @@ -157,7 +157,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./android/app/build/outputs/apk/release/app-arm64-v8a-release.apk + asset_path: android/app/build/outputs/apk/release/app-arm64-v8a-release.apk asset_name: robosats-${{ steps.commit.outputs.short }}-arm64-v8a.apk asset_content_type: application/apk @@ -170,7 +170,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./android/app/build/outputs/apk/release/app-armeabi-v7a-release.apk + asset_path: android/app/build/outputs/apk/release/app-armeabi-v7a-release.apk asset_name: robosats-${{ steps.commit.outputs.short }}-armeabi-v7a.apk asset_content_type: application/apk @@ -183,6 +183,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./android/app/build/outputs/apk/release/app-x86_64-release.apk + asset_path: android/app/build/outputs/apk/release/app-x86_64-release.apk asset_name: robosats-${{ steps.commit.outputs.short }}-x86_64.apk asset_content_type: application/apk From 4594b355ba7eae598a367439c79fb52a7bcba218 Mon Sep 17 00:00:00 2001 From: koalasat Date: Fri, 18 Jul 2025 15:33:15 +0200 Subject: [PATCH 04/18] Fix workflow --- .github/workflows/frontend-build.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/frontend-build.yml b/.github/workflows/frontend-build.yml index a3008d85..ee38a7d1 100644 --- a/.github/workflows/frontend-build.yml +++ b/.github/workflows/frontend-build.yml @@ -73,11 +73,11 @@ jobs: path: | web/static web/*.html - # - name: 'Archive Mobile Build Results' - # uses: actions/upload-artifact@v4 - # with: - # name: mobile-web.bundle - # path: mobile/html/Web.bundle + - name: 'Archive Mobile Build Results' + uses: actions/upload-artifact@v4 + with: + name: mobile-web.bundle + path: android/app/src/main/assets # Invoke pre-release image build if this was not a tag push # Docker images tagged only with short commit hash From 5d8f2643a0ec687cdb28731e25cdb4c812b68544 Mon Sep 17 00:00:00 2001 From: koalasat Date: Fri, 18 Jul 2025 15:41:14 +0200 Subject: [PATCH 05/18] Fix workflow --- .github/workflows/android-build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 30179d20..bcc638da 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -55,7 +55,9 @@ jobs: ${{ runner.os }}-gradle- - name: Build APK - run: android/gradlew assembleRelease --stacktrace + run: | + cd android + ./gradlew assembleRelease --stacktrace - name: 'Check for non-FOSS libraries' run: | From 72d024786f2b4402da06bf02306959dadac4cb36 Mon Sep 17 00:00:00 2001 From: koalasat Date: Fri, 18 Jul 2025 15:50:13 +0200 Subject: [PATCH 06/18] Fix workflow --- .github/workflows/android-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index bcc638da..35741dc0 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -76,7 +76,7 @@ jobs: - name: Sign APK uses: r0adkll/sign-android-release@v1 with: - releaseDirectory: app/build/outputs/apk/release + releaseDirectory: android/app/build/outputs/apk/release signingKeyBase64: ${{ secrets.KEYSTORE }} alias: ${{ secrets.KEY_ALIAS }} keyStorePassword: ${{ secrets.KEY_STORE_PASS }} From bf9c821cdf4e872cf8e8d94c9ae8f8919b19a633 Mon Sep 17 00:00:00 2001 From: koalasat Date: Fri, 18 Jul 2025 16:02:07 +0200 Subject: [PATCH 07/18] Fix workflow --- .github/workflows/android-build.yml | 77 +++-------------------------- 1 file changed, 8 insertions(+), 69 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 35741dc0..49fccb21 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -97,94 +97,33 @@ jobs: # Create artifacts (only for Release) # Create app-universal-release APK artifact asset for Release - - name: 'Upload universal .apk Release Artifact (for Release)' + - name: 'Upload universal .apk Artifact' uses: actions/upload-artifact@v4 if: inputs.semver != '' # If this workflow is called from release.yml with: name: robosats-${{ inputs.semver }}-universal.apk - path: android/app/build/outputs/apk/release/app-universal-release.apk + path: android/app/build/outputs/apk/release/app-universal-release-unsigned-signed.apk # Create app-arm64-v8a-release APK artifact asset for Release - - name: 'Upload arm64-v8a .apk Release Artifact (for Release)' + - name: 'Upload arm64-v8a .apk Artifact' uses: actions/upload-artifact@v4 if: inputs.semver != '' # If this workflow is called from release.yml with: name: robosats-${{ inputs.semver }}-arm64-v8a.apk - path: android/app/build/outputs/apk/release/app-arm64-v8a-release.apk + path: android/app/build/outputs/apk/release/app-arm64-v8a-release-unsigned-signed.apk # Create app-armeabi-v7a-release APK artifact asset for Release - - name: 'Upload armeabi-v7a .apk Release Artifact (for Release)' + - name: 'Upload armeabi-v7a .apk Artifact' uses: actions/upload-artifact@v4 if: inputs.semver != '' # If this workflow is called from release.yml with: name: robosats-${{ inputs.semver }}-armeabi-v7a.apk - path: android/app/build/outputs/apk/release/app-armeabi-v7a-release.apk + path: android/app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned-signed.apk # Create app-x86_64-release APK artifact asset for Release - - name: 'Upload x86_64 .apk Release Artifact (for Release)' + - name: 'Upload x86_64 .apk Artifact' uses: actions/upload-artifact@v4 if: inputs.semver != '' # If this workflow is called from release.yml with: name: robosats-${{ inputs.semver }}-x86_64.apk - path: android/app/build/outputs/apk/release/app-x86_64-release.apk - - - name: 'Create Pre-release' - id: create_release - if: inputs.semver == '' - uses: ncipollo/release-action@v1.18.0 - with: - tag: android-${{ steps.commit.outputs.short }} - name: robosats-android-${{ steps.commit.outputs.short }} - prerelease: true - - # Upload universal APK to pre-release - - name: 'Upload universal Pre-release APK Asset' - id: upload-release-universal-apk-asset - if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release) - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: android/app/build/outputs/apk/release/app-universal-release.apk - asset_name: robosats-${{ steps.commit.outputs.short }}-universal.apk - asset_content_type: application/apk - - # Upload arm64-v8a APK to pre-release - - name: 'Upload arm64-v8a Pre-release APK Asset' - id: upload-release-arm64-v8a-apk-asset - if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release) - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: android/app/build/outputs/apk/release/app-arm64-v8a-release.apk - asset_name: robosats-${{ steps.commit.outputs.short }}-arm64-v8a.apk - asset_content_type: application/apk - - # Upload armeabi-v7a APK to pre-release - - name: 'Upload armeabi-v7a Pre-release APK Asset' - id: upload-release-armeabi-v7a-apk-asset - if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release) - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: android/app/build/outputs/apk/release/app-armeabi-v7a-release.apk - asset_name: robosats-${{ steps.commit.outputs.short }}-armeabi-v7a.apk - asset_content_type: application/apk - - # Upload x86_64 APK to pre-release - - name: 'Upload x86_64 Pre-release APK Asset' - id: upload-release-x86_64-apk-asset - if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release) - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: android/app/build/outputs/apk/release/app-x86_64-release.apk - asset_name: robosats-${{ steps.commit.outputs.short }}-x86_64.apk - asset_content_type: application/apk + path: android/app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk \ No newline at end of file From 394281df749271318ecbd2df8b98e8e6478493b2 Mon Sep 17 00:00:00 2001 From: koalasat Date: Fri, 18 Jul 2025 16:07:56 +0200 Subject: [PATCH 08/18] Fix workflow --- .github/workflows/android-build.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 49fccb21..6173f7df 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -99,7 +99,6 @@ jobs: # Create app-universal-release APK artifact asset for Release - name: 'Upload universal .apk Artifact' uses: actions/upload-artifact@v4 - if: inputs.semver != '' # If this workflow is called from release.yml with: name: robosats-${{ inputs.semver }}-universal.apk path: android/app/build/outputs/apk/release/app-universal-release-unsigned-signed.apk @@ -107,7 +106,6 @@ jobs: # Create app-arm64-v8a-release APK artifact asset for Release - name: 'Upload arm64-v8a .apk Artifact' uses: actions/upload-artifact@v4 - if: inputs.semver != '' # If this workflow is called from release.yml with: name: robosats-${{ inputs.semver }}-arm64-v8a.apk path: android/app/build/outputs/apk/release/app-arm64-v8a-release-unsigned-signed.apk @@ -115,7 +113,6 @@ jobs: # Create app-armeabi-v7a-release APK artifact asset for Release - name: 'Upload armeabi-v7a .apk Artifact' uses: actions/upload-artifact@v4 - if: inputs.semver != '' # If this workflow is called from release.yml with: name: robosats-${{ inputs.semver }}-armeabi-v7a.apk path: android/app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned-signed.apk @@ -123,7 +120,6 @@ jobs: # Create app-x86_64-release APK artifact asset for Release - name: 'Upload x86_64 .apk Artifact' uses: actions/upload-artifact@v4 - if: inputs.semver != '' # If this workflow is called from release.yml with: name: robosats-${{ inputs.semver }}-x86_64.apk path: android/app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk \ No newline at end of file From f25df4e98d7d11479b82a1e5a32abe348e1bb46d Mon Sep 17 00:00:00 2001 From: koalasat Date: Fri, 18 Jul 2025 16:27:12 +0200 Subject: [PATCH 09/18] Fix workflow --- .github/workflows/android-build.yml | 37 +++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 6173f7df..49341864 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -95,9 +95,9 @@ jobs: id: commit uses: pr-mpt/actions-commit-hash@v3 - # Create artifacts (only for Release) # Create app-universal-release APK artifact asset for Release - name: 'Upload universal .apk Artifact' + if: inputs.semver != '' uses: actions/upload-artifact@v4 with: name: robosats-${{ inputs.semver }}-universal.apk @@ -105,6 +105,7 @@ jobs: # Create app-arm64-v8a-release APK artifact asset for Release - name: 'Upload arm64-v8a .apk Artifact' + if: inputs.semver != '' uses: actions/upload-artifact@v4 with: name: robosats-${{ inputs.semver }}-arm64-v8a.apk @@ -112,6 +113,7 @@ jobs: # Create app-armeabi-v7a-release APK artifact asset for Release - name: 'Upload armeabi-v7a .apk Artifact' + if: inputs.semver != '' uses: actions/upload-artifact@v4 with: name: robosats-${{ inputs.semver }}-armeabi-v7a.apk @@ -119,7 +121,38 @@ jobs: # Create app-x86_64-release APK artifact asset for Release - name: 'Upload x86_64 .apk Artifact' + if: inputs.semver != '' uses: actions/upload-artifact@v4 with: name: robosats-${{ inputs.semver }}-x86_64.apk - path: android/app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk \ No newline at end of file + path: android/app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk + + - name: Create Pre-Release + id: create_release + if: inputs.semver == '' # only if this workflow is not called from a push to tag (a Release) + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + draft: true + + - name: Set asset_name + id: set_asset_name + if: inputs.semver == '' + run: | + TAG_NAME=${GITHUB_REF#refs/tags/} + echo "::set-output name=asset_tag::$TAG_NAME" + + - name: Upload APK Universal Asset + id: upload-release-asset-universal-apk + if: inputs.semver == '' + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: app/build/outputs/apk/release/app-universal-release-unsigned-signed.apk + asset_name: pokey-universal-${{ steps.set_asset_name.outputs.asset_tag }}.apk + asset_content_type: application/zip From c59a197a6ca323bcf35b1ff9124887cc6c68292b Mon Sep 17 00:00:00 2001 From: koalasat Date: Fri, 18 Jul 2025 16:34:18 +0200 Subject: [PATCH 10/18] Fix workflow --- .github/workflows/android-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 49341864..2883b4ec 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -153,6 +153,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: app/build/outputs/apk/release/app-universal-release-unsigned-signed.apk - asset_name: pokey-universal-${{ steps.set_asset_name.outputs.asset_tag }}.apk + asset_path: android/app/build/outputs/apk/release/app-universal-release-unsigned-signed.apk + asset_name: robosats-universal-${{ steps.set_asset_name.outputs.asset_tag }}.apk asset_content_type: application/zip From 7a0d072d10213cc78d9fb7b947f290eceea60525 Mon Sep 17 00:00:00 2001 From: koalasat Date: Fri, 18 Jul 2025 16:36:53 +0200 Subject: [PATCH 11/18] Fix workflow --- .github/workflows/android-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 2883b4ec..0b9190e0 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -150,7 +150,7 @@ jobs: if: inputs.semver == '' uses: actions/upload-release-asset@v1 env: - GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: android/app/build/outputs/apk/release/app-universal-release-unsigned-signed.apk From 3dd151e3193e50e0f3fcb66b589e087770aa5e27 Mon Sep 17 00:00:00 2001 From: koalasat Date: Fri, 18 Jul 2025 18:05:11 +0200 Subject: [PATCH 12/18] Simplfy Tor connection on Android --- .github/workflows/android-build.yml | 9 +- .../main/java/com/robosats/MainActivity.kt | 372 +----------------- .../main/java/com/robosats/WebAppInterface.kt | 82 +++- .../java/com/robosats/tor/TorKmpManager.kt | 9 +- android/app/src/main/res/values/strings.xml | 2 +- frontend/src/services/Android/index.ts | 7 + frontend/src/services/Native/index.d.ts | 57 --- frontend/src/services/Native/index.ts | 70 ---- .../src/services/Roboidentities/Native.ts | 4 - .../RoboidentitiesNativeClient/index.ts | 42 -- .../System/SystemNativeClient/index.ts | 64 --- frontend/src/services/System/index.ts | 7 +- .../Websocket/WebsocketNativeClient/index.ts | 91 ----- frontend/src/services/Websocket/index.ts | 5 - .../index.ts | 58 +-- frontend/src/services/api/index.ts | 7 +- frontend/templates/frontend/index.ejs | 56 +-- frontend/webpack.config.ts | 8 + 18 files changed, 170 insertions(+), 780 deletions(-) delete mode 100644 frontend/src/services/Native/index.d.ts delete mode 100644 frontend/src/services/Native/index.ts delete mode 100644 frontend/src/services/Roboidentities/Native.ts delete mode 100644 frontend/src/services/Roboidentities/RoboidentitiesNativeClient/index.ts delete mode 100644 frontend/src/services/System/SystemNativeClient/index.ts delete mode 100644 frontend/src/services/Websocket/WebsocketNativeClient/index.ts rename frontend/src/services/api/{ApiNativeClient => ApiAndroidClient}/index.ts (61%) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 0b9190e0..a62ce816 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -138,13 +138,6 @@ jobs: release_name: ${{ github.ref }} draft: true - - name: Set asset_name - id: set_asset_name - if: inputs.semver == '' - run: | - TAG_NAME=${GITHUB_REF#refs/tags/} - echo "::set-output name=asset_tag::$TAG_NAME" - - name: Upload APK Universal Asset id: upload-release-asset-universal-apk if: inputs.semver == '' @@ -154,5 +147,5 @@ jobs: with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: android/app/build/outputs/apk/release/app-universal-release-unsigned-signed.apk - asset_name: robosats-universal-${{ steps.set_asset_name.outputs.asset_tag }}.apk + asset_name: robosats-universal-${{ github.ref }}.apk asset_content_type: application/zip diff --git a/android/app/src/main/java/com/robosats/MainActivity.kt b/android/app/src/main/java/com/robosats/MainActivity.kt index ed8cba8f..20a48b7b 100644 --- a/android/app/src/main/java/com/robosats/MainActivity.kt +++ b/android/app/src/main/java/com/robosats/MainActivity.kt @@ -3,7 +3,6 @@ package com.robosats import android.annotation.SuppressLint import android.app.Application import android.content.Context -import android.graphics.Bitmap import android.os.Build import android.os.Bundle import android.util.Log @@ -14,7 +13,6 @@ import android.webkit.GeolocationPermissions import android.webkit.PermissionRequest import android.webkit.ServiceWorkerController import android.webkit.WebChromeClient -import android.webkit.WebResourceError import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebSettings @@ -26,11 +24,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout import com.robosats.tor.TorKmp import com.robosats.tor.TorKmpManager -import java.io.ByteArrayInputStream -import java.net.HttpURLConnection -import java.net.InetSocketAddress -import java.net.Proxy -import java.net.URL class MainActivity : AppCompatActivity() { private lateinit var webView: WebView @@ -38,13 +31,6 @@ class MainActivity : AppCompatActivity() { private lateinit var loadingContainer: ConstraintLayout private lateinit var statusTextView: TextView - // Security constants - private val ALLOWED_DOMAINS = arrayOf(".onion") - private val CONTENT_SECURITY_POLICY = "default-src 'self'; connect-src 'self' https://*.onion http://*.onion; " + - "script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; " + - "img-src 'self' data:; font-src 'self' data:; object-src 'none'; " + - "media-src 'none'; frame-src 'none'; worker-src 'self';" - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -158,7 +144,7 @@ class MainActivity : AppCompatActivity() { return } - // IMMEDIATELY set a blocking WebViewClient to prevent ANY network access + // Set a blocking WebViewClient to prevent ANY network access webView.webViewClient = object : WebViewClient() { override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { // Block ALL requests until we're sure Tor proxy is correctly set up @@ -166,7 +152,7 @@ class MainActivity : AppCompatActivity() { } override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { - // Block ALL URL loading attempts until proxy is properly configured + // Block ALL URL loading attempts return true } } @@ -187,24 +173,12 @@ class MainActivity : AppCompatActivity() { throw SecurityException("Tor disconnected during proxy setup") } - // Try to set up the proxy - setupProxyForWebView() - // If we get here, proxy setup was successful // Perform one final Tor connection check if (!torKmp.isConnected()) { throw SecurityException("Tor disconnected after proxy setup") } - // Now get the proxy information that we previously verified in setupProxyForWebView - // Use system properties that we've already set up and verified - val proxyHost = System.getProperty("http.proxyHost") - ?: throw SecurityException("Missing proxy host in system properties") - val proxyPort = System.getProperty("http.proxyPort")?.toIntOrNull() - ?: throw SecurityException("Missing or invalid proxy port in system properties") - - Log.d("WebViewProxy", "Using proxy settings: $proxyHost:$proxyPort") - // Success - now configure WebViewClient and load URL on UI thread runOnUiThread { updateStatus("Secure connection established. Loading app...") @@ -233,237 +207,6 @@ class MainActivity : AppCompatActivity() { } } - // Create a custom WebViewClient that forces all traffic through Tor - webView.webViewClient = object : WebViewClient() { - override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { - // Verify Tor is connected before allowing any resource request - if (!torKmp.isConnected()) { - Log.e("SecurityError", "Tor disconnected during resource request") - return WebResourceResponse("text/plain", "UTF-8", null) - } - - val urlString = request.url.toString() - Log.d("WebViewProxy", "Intercepting request: $urlString") - - // Block all external requests that aren't to .onion domains or local files - if (!isAllowedRequest(urlString)) { - Log.e("SecurityPolicy", "Blocked forbidden request to: $urlString") - return WebResourceResponse("text/plain", "UTF-8", null) - } - - try { - // Special handling for .onion domains - val isOnionDomain = urlString.contains(".onion") - - // Only proceed if it's an onion domain or local file - if (!isOnionDomain && !urlString.startsWith("file://")) { - Log.e("SecurityPolicy", "Blocked non-onion external request: $urlString") - return WebResourceResponse("text/plain", "UTF-8", null) - } - - // For .onion domains, we must use SOCKS proxy type - val proxyType = if (isOnionDomain) - Proxy.Type.SOCKS - else - Proxy.Type.HTTP - - // Create a proxy instance for Tor with the appropriate type - val torProxy = Proxy( - proxyType, - InetSocketAddress(proxyHost, proxyPort) - ) - - if (isOnionDomain) { - Log.d("WebViewProxy", "Handling .onion domain with SOCKS proxy: $urlString") - } - - // If it's a local file, return it directly - if (urlString.startsWith("file://")) { - // Let the system handle local files - return super.shouldInterceptRequest(view, request) - } - - // Create connection with proxy already configured - val url = URL(urlString) - val connection = url.openConnection(torProxy) - - // Configure basic connection properties - connection.connectTimeout = 60000 // Longer timeout for onion domains - connection.readTimeout = 60000 - - if (connection is HttpURLConnection) { - // Ensure no connection reuse to prevent proxy leaks - connection.setRequestProperty("Connection", "close") - - // Add security headers - connection.setRequestProperty("Sec-Fetch-Site", "same-origin") - connection.setRequestProperty("Sec-Fetch-Mode", "cors") - connection.setRequestProperty("DNT", "1") // Do Not Track - - // Copy request headers - request.requestHeaders.forEach { (key, value) -> - connection.setRequestProperty(key, value) - } - - // Set the request method - connection.requestMethod = request.method - - // Special handling for OPTIONS (CORS preflight) requests - if (request.method == "OPTIONS") { - // For OPTIONS, we'll create a custom response without making a network request - // This is the most reliable way to handle CORS preflight - Log.d("CORS", "Handling OPTIONS preflight request for: $urlString") - - // Create CORS headers map - val preflightHeaders = HashMap() - preflightHeaders["Access-Control-Allow-Origin"] = "*" - preflightHeaders["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT, DELETE, HEAD" - preflightHeaders["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Authorization" - preflightHeaders["Access-Control-Max-Age"] = "86400" // Cache preflight for 24 hours - preflightHeaders["Access-Control-Allow-Credentials"] = "true" - preflightHeaders["Content-Type"] = "text/plain" - - // Log CORS headers for debugging - Log.d("CORS", "Preflight response with CORS headers: $preflightHeaders") - - // Return a custom preflight response without actually connecting - return WebResourceResponse( - "text/plain", - "UTF-8", - 200, - "OK", - preflightHeaders, - ByteArrayInputStream("".toByteArray()) - ) - } - - // Try to connect - connection.connect() - val responseCode = connection.responseCode - - // Get content type - val mimeType = connection.contentType ?: "text/plain" - val encoding = connection.contentEncoding ?: "UTF-8" - - Log.d("WebViewProxy", "Successfully proxied request to $url (HTTP ${connection.responseCode})") - - // Get the correct input stream based on response code - val inputStream = if (responseCode >= 400) { - connection.errorStream ?: ByteArrayInputStream(byteArrayOf()) - } else { - connection.inputStream - } - - // Create response headers map with security headers - val responseHeaders = HashMap() - - // First copy original response headers, but carefully handle CORS headers - for (i in 0 until connection.headerFields.size) { - val key = connection.headerFields.keys.elementAtOrNull(i) - if (key != null && key.isNotEmpty()) { - // Skip any CORS headers from the original response - we'll add our own - if (!key.startsWith("Access-Control-")) { - val value = connection.getHeaderField(key) - if (value != null) { - responseHeaders[key] = value - } - } else { - // Log any CORS headers we're skipping from the original response - Log.d("CORS", "Skipping original CORS header: $key: ${connection.getHeaderField(key)}") - } - } - } - - // Add our own CORS headers - responseHeaders["Access-Control-Allow-Origin"] = "*" - responseHeaders["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT, DELETE, HEAD" - responseHeaders["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Authorization" - responseHeaders["Access-Control-Allow-Credentials"] = "true" - if (!responseHeaders.containsKey("Content-Security-Policy")) { - responseHeaders["Content-Security-Policy"] = CONTENT_SECURITY_POLICY - } - if (!responseHeaders.containsKey("X-Content-Type-Options")) { - responseHeaders["X-Content-Type-Options"] = "nosniff" - } - if (!responseHeaders.containsKey("X-Frame-Options")) { - responseHeaders["X-Frame-Options"] = "DENY" - } - if (!responseHeaders.containsKey("Referrer-Policy")) { - responseHeaders["Referrer-Policy"] = "no-referrer" - } - - // Log the CORS headers for debugging - responseHeaders["Access-Control-Allow-Origin"]?.let { - Log.d("CORS", "Access-Control-Allow-Origin: $it") - } - - // Return proxied response with security headers - return WebResourceResponse( - mimeType, - encoding, - responseCode, - "OK", - responseHeaders, - inputStream - ) - } else { - // For non-HTTP connections (rare) - val inputStream = connection.getInputStream() - Log.d("WebViewProxy", "Successfully established non-HTTP connection to $url") - return WebResourceResponse( - "application/octet-stream", - "UTF-8", - inputStream - ) - } - } catch (e: Exception) { - Log.e("WebViewProxy", "Error proxying request: $urlString - ${e.message}", e) - - // For security, block the request rather than falling back to system handling - return WebResourceResponse("text/plain", "UTF-8", null) - } - } - - // We're not handling SSL, so we don't need the onReceivedSslError method - - override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { - // Verify Tor is still connected before allowing any request - if (!torKmp.isConnected()) { - Log.e("SecurityError", "Tor disconnected during navigation") - return true // Block the request - } - return false // Let our proxied client handle it - } - - override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) { - Log.e("WebViewError", "Error loading resource: ${error.description}") - super.onReceivedError(view, request, error) - } - - override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { - // Verify Tor is connected when page starts loading - if (!torKmp.isConnected()) { - Log.e("SecurityError", "Tor disconnected as page started loading") - view.stopLoading() - return - } - super.onPageStarted(view, url, favicon) - } - - override fun onPageFinished(view: WebView, url: String) { - // Verify Tor is still connected when page finishes loading - if (!torKmp.isConnected()) { - Log.e("SecurityError", "Tor disconnected after page loaded") - return - } - - // No JavaScript injection - just log page load completion - Log.d("WebView", "Page finished loading: $url") - - super.onPageFinished(view, url) - } - } - webView.settings.userAgentString = "AndroidRobosats" // Add the JavaScript interface @@ -488,46 +231,6 @@ class MainActivity : AppCompatActivity() { }.start() } - private fun setupProxyForWebView() { - // Triple-check Tor is connected - if (!torKmp.isConnected()) { - throw SecurityException("Cannot set up proxy - Tor is not connected") - } - - try { - // Get the proxy from TorKmpManager, handling possible exceptions - val proxy = TorKmpManager.getTorKmpObject().proxy ?: - throw SecurityException("Tor proxy is null despite Tor being connected") - - val inetSocketAddress = proxy.address() as InetSocketAddress - val host = inetSocketAddress.hostName - val port = inetSocketAddress.port - - if (host.isBlank() || port <= 0) { - throw SecurityException("Invalid Tor proxy address: $host:$port") - } - - Log.d("WebViewProxy", "Setting up Tor proxy: $host:$port") - - // Set up the proxy - setWebViewProxy(applicationContext, host, port) - - // Verify proxy was set correctly - if (System.getProperty("http.proxyHost") != host || - System.getProperty("http.proxyPort") != port.toString()) { - throw SecurityException("Proxy verification failed - system properties don't match expected values") - } - - Log.d("WebViewProxy", "Proxy setup completed successfully") - } catch (e: Exception) { - Log.e("WebViewProxy", "Error setting up proxy: ${e.message}", e) - throw SecurityException("Failed to set up Tor proxy: ${e.message}", e) - } - } - - /** - * Sets the proxy for WebView using the most direct approach that's known to work with Tor - */ /** * Configure WebView settings with a security-first approach */ @@ -597,24 +300,6 @@ class MainActivity : AppCompatActivity() { webSettings.textZoom = 100 } - /** - * Check if a URL request is allowed based on security policy - */ - private fun isAllowedRequest(url: String): Boolean { - // Always allow local file requests - if (url.startsWith("file:///android_asset/") || url.startsWith("file:///data/")) { - return true - } - - // Allow onion domains - if (ALLOWED_DOMAINS.any { url.contains(it) }) { - return true - } - - // Block everything else - return false - } - // SSL error description method removed as we're not using SSL /** @@ -638,57 +323,4 @@ class MainActivity : AppCompatActivity() { super.onDestroy() } - - private fun setWebViewProxy(context: Context, proxyHost: String, proxyPort: Int) { - try { - // First set system properties (required as a foundation) - System.setProperty("http.proxyHost", proxyHost) - System.setProperty("http.proxyPort", proxyPort.toString()) - System.setProperty("https.proxyHost", proxyHost) - System.setProperty("https.proxyPort", proxyPort.toString()) - System.setProperty("proxy.host", proxyHost) - System.setProperty("proxy.port", proxyPort.toString()) - - Log.d("WebViewProxy", "Set system proxy properties") - - // Create and apply a proxy at the application level - val proxyClass = Class.forName("android.net.ProxyInfo") - val proxyConstructor = proxyClass.getConstructor(String::class.java, Int::class.javaPrimitiveType, String::class.java) - val proxyInfo = proxyConstructor.newInstance(proxyHost, proxyPort, null) - - try { - // Try to set global proxy through ConnectivityManager - val connectivityManager = context.getSystemService(CONNECTIVITY_SERVICE) - val setDefaultProxyMethod = connectivityManager.javaClass.getDeclaredMethod("setDefaultProxy", proxyClass) - setDefaultProxyMethod.isAccessible = true - setDefaultProxyMethod.invoke(connectivityManager, proxyInfo) - Log.d("WebViewProxy", "Set proxy via ConnectivityManager") - } catch (e: Exception) { - Log.w("WebViewProxy", "Could not set proxy via ConnectivityManager: ${e.message}") - } - - // WebView operations must be run on the UI thread - runOnUiThread { - try { - // Force WebView to use proxy via direct settings (must be on UI thread) - webView.settings.javaClass.getDeclaredMethod("setHttpProxy", String::class.java, Int::class.javaPrimitiveType) - ?.apply { isAccessible = true } - ?.invoke(webView.settings, proxyHost, proxyPort) - Log.d("WebViewProxy", "Applied proxy directly to WebView settings") - } catch (e: Exception) { - Log.w("WebViewProxy", "Could not set proxy directly on WebView settings: ${e.message}") - // Continue - we'll rely on system properties and connection-level proxying - } - } - - // Wait to ensure UI thread operations complete - // This prevents race conditions with WebView operations - Thread.sleep(500) - - Log.d("WebViewProxy", "Proxy setup completed") - } catch (e: Exception) { - Log.e("WebViewProxy", "Error setting WebView proxy", e) - throw SecurityException("Failed to set WebView proxy: ${e.message}", e) - } - } } diff --git a/android/app/src/main/java/com/robosats/WebAppInterface.kt b/android/app/src/main/java/com/robosats/WebAppInterface.kt index 177cb812..bd32caf3 100644 --- a/android/app/src/main/java/com/robosats/WebAppInterface.kt +++ b/android/app/src/main/java/com/robosats/WebAppInterface.kt @@ -7,18 +7,24 @@ import android.webkit.JavascriptInterface import android.webkit.WebView import android.widget.Toast import com.robosats.tor.TorKmpManager.getTorKmpObject +import okhttp3.Call +import okhttp3.Callback +import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.OkHttpClient.Builder import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener import okio.ByteString -import java.util.Objects +import org.json.JSONObject +import java.io.IOException import java.util.concurrent.TimeUnit import java.util.regex.Pattern import okhttp3.Request.Builder as RequestBuilder + /** * Provides a secure bridge between JavaScript and native Android code. * This class is designed with security in mind, implementing input validation, @@ -238,6 +244,80 @@ class WebAppInterface(private val context: Context, private val webView: WebView } } + @JavascriptInterface + fun sendRequest(uuid: String, action: String, url: String, headers: String, body: String) { + // Validate inputs + if (!isValidUuid(uuid)) { + Log.e(TAG, "Invalid UUID for sendRequest: $uuid") + rejectPromise(uuid, "Invalid UUID") + return + } + + try { + // Create OkHttpClient with Tor proxy + val client = Builder() + .connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout + .readTimeout(30, TimeUnit.SECONDS) // Set read timeout + .proxy(getTorKmpObject().proxy) + .build() + + // Build request with URL + val requestBuilder = RequestBuilder().url(url) + + // Add headers from JSON + val headersObject = JSONObject(headers) + val keys = headersObject.keys() + while (keys.hasNext()) { + val key = keys.next() + val value = headersObject.optString(key) + requestBuilder.addHeader(key, value) + } + + // Set request method and body + when (action) { + "DELETE" -> requestBuilder.delete() + "POST" -> { + val mediaType = "application/json; charset=utf-8".toMediaType() + val requestBody = body.toRequestBody(mediaType) + requestBuilder.post(requestBody) + } + else -> requestBuilder.get() + } + + // Build and execute request + val request = requestBuilder.build() + client.newCall(request).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + Log.d("RobosatsError", e.toString()) + rejectPromise(uuid, "Request failed: ${e.message}") + } + + override fun onResponse(call: Call, response: Response) { + try { + // Get response body + val responseBody = response.body.string() + + // Create JSON object with headers + val headersJson = JSONObject() + response.headers.names().forEach { name -> + headersJson.put(name, response.header(name)) + } + + // Return response as JSON string + val result = "{\"json\":$responseBody, \"headers\": $headersJson}" + resolvePromise(uuid, result) + } catch (e: Exception) { + Log.e(TAG, "Error processing response", e) + rejectPromise(uuid, "Error processing response: ${e.message}") + } + } + }) + } catch (e: Exception) { + Log.e(TAG, "Error in sendRequest", e) + rejectPromise(uuid, "Error sending request: ${e.message}") + } + } + private fun onWsMessage(path: String?, message: String?) { safeEvaluateJavascript("javascript:window.AndroidRobosats.onWSMessage('$path', '$message')") } diff --git a/android/app/src/main/java/com/robosats/tor/TorKmpManager.kt b/android/app/src/main/java/com/robosats/tor/TorKmpManager.kt index 9accff0c..3c3b1164 100644 --- a/android/app/src/main/java/com/robosats/tor/TorKmpManager.kt +++ b/android/app/src/main/java/com/robosats/tor/TorKmpManager.kt @@ -23,11 +23,9 @@ import io.matthewnelson.kmp.tor.manager.common.state.isOff import io.matthewnelson.kmp.tor.manager.common.state.isOn import io.matthewnelson.kmp.tor.manager.common.state.isStarting import io.matthewnelson.kmp.tor.manager.common.state.isStopping -import io.matthewnelson.kmp.tor.manager.R import kotlinx.coroutines.* import java.net.InetSocketAddress import java.net.Proxy -import java.util.concurrent.ExecutionException class TorKmp(application : Application) { @@ -234,14 +232,11 @@ class TorKmp(application : Application) { if (seconds == null) { it } else { - appContext.getString( - R.string.kmp_tor_newnym_rate_limited, - seconds - ) + TorControlSignal.NEW_NYM_RATE_LIMITED } } it == TorControlSignal.NEW_NYM_SUCCESS -> { - appContext.getString(R.string.kmp_tor_newnym_success) + TorControlSignal.NEW_NYM_SUCCESS } else -> { null diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index b1a28134..d9f62954 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ Robosats - \ No newline at end of file + diff --git a/frontend/src/services/Android/index.ts b/frontend/src/services/Android/index.ts index a6a080c1..dc526ded 100644 --- a/frontend/src/services/Android/index.ts +++ b/frontend/src/services/Android/index.ts @@ -13,6 +13,13 @@ interface AndroidAppRobosats { getTorStatus: (uuid: string) => void; openWS: (uuid: string, path: string) => void; sendWsMessage: (uuid: string, path: string, message: string) => void; + sendRequest: ( + uuid: string, + action: 'GET' | 'POST' | 'DELETE', + url: string, + headers: string, + body: string, + ) => void; } class AndroidRobosats { diff --git a/frontend/src/services/Native/index.d.ts b/frontend/src/services/Native/index.d.ts deleted file mode 100644 index cec946a7..00000000 --- a/frontend/src/services/Native/index.d.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type NativeRobosats from './index'; - -declare global { - interface Window { - ReactNativeWebView?: ReactNativeWebView; - NativeRobosats?: NativeRobosats; - RobosatsSettings: 'web-basic' | 'web-pro' | 'selfhosted-basic' | 'selfhosted-pro'; - } -} - -export interface ReactNativeWebView { - postMessage: (message: string) => Promise>; -} - -export interface NativeWebViewMessageHttp { - id?: number; - category: 'http'; - type: 'post' | 'get' | 'put' | 'delete'; - path: string; - baseUrl: string; - headers?: object; - body?: object; -} - -export interface NativeWebViewMessageSystem { - id?: number; - category: 'system'; - type: - | 'init' - | 'torStatus' - | 'WsMessage' - | 'copyToClipboardString' - | 'setCookie' - | 'deleteCookie' - | 'navigateToPage'; - key?: string; - detail?: string; -} - -export interface NativeWebViewMessageRoboidentities { - id?: number; - category: 'roboidentities'; - type: 'roboname' | 'robohash'; - string?: string; - size?: string; -} - -export declare type NativeWebViewMessage = - | NativeWebViewMessageHttp - | NativeWebViewMessageSystem - | NativeWebViewMessageRoboidentities - | NA; - -export interface NativeRobosatsPromise { - resolve: (value: object | PromiseLike) => void; - reject: (reason?: string) => void; -} diff --git a/frontend/src/services/Native/index.ts b/frontend/src/services/Native/index.ts deleted file mode 100644 index 1d5119e6..00000000 --- a/frontend/src/services/Native/index.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - type NativeRobosatsPromise, - type NativeWebViewMessage, - type NativeWebViewMessageSystem, -} from './index.d'; - -class NativeRobosats { - public torDaemonStatus = 'NOTINIT'; - - private messageCounter: number = 0; - - private readonly pendingMessages = new Map(); - - public cookies: Record = {}; - - public loadCookie = (cookie: { key: string; value: string }): void => { - this.cookies[cookie.key] = cookie.value; - }; - - public onMessageResolve: (messageId: number, response?: object) => void = ( - messageId, - response = {}, - ) => { - if (this.pendingMessages.has(messageId)) { - this.pendingMessages.get(messageId)?.resolve(response); - this.pendingMessages.delete(messageId); - } - }; - - public onMessageReject: (messageId: number, response?: object) => void = ( - messageId, - response = {}, - ) => { - if (this.pendingMessages.has(messageId)) { - this.pendingMessages.get(messageId)?.reject(response); - this.pendingMessages.delete(messageId); - } - }; - - public onMessage: (message: NativeWebViewMessageSystem) => void = (message) => { - if (message.type === 'torStatus') { - this.torDaemonStatus = message.detail ?? 'ERROR'; - window.dispatchEvent(new CustomEvent('torStatus', { detail: this.torDaemonStatus })); - } else if (message.type === 'setCookie') { - if (message.key !== undefined) { - this.cookies[message.key] = String(message.detail); - } - } else { - window.dispatchEvent(new CustomEvent(message.type, { detail: message?.detail })); - } - }; - - public postMessage: (message: NativeWebViewMessage) => Promise = async (message) => { - this.messageCounter += 1; - message.id = this.messageCounter; - const json = JSON.stringify(message); - void window.ReactNativeWebView?.postMessage(json); - - return await new Promise((resolve, reject) => { - if (message.id !== undefined) { - this.pendingMessages.set(message.id, { - resolve, - reject, - }); - } - }); - }; -} - -export default NativeRobosats; diff --git a/frontend/src/services/Roboidentities/Native.ts b/frontend/src/services/Roboidentities/Native.ts deleted file mode 100644 index 1533a585..00000000 --- a/frontend/src/services/Roboidentities/Native.ts +++ /dev/null @@ -1,4 +0,0 @@ -import RoboidentitiesClientNativeClient from './RoboidentitiesNativeClient'; -import { type RoboidentitiesClient } from './type'; - -export const roboidentitiesClient: RoboidentitiesClient = new RoboidentitiesClientNativeClient(); diff --git a/frontend/src/services/Roboidentities/RoboidentitiesNativeClient/index.ts b/frontend/src/services/Roboidentities/RoboidentitiesNativeClient/index.ts deleted file mode 100644 index 0b67b4c2..00000000 --- a/frontend/src/services/Roboidentities/RoboidentitiesNativeClient/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { type RoboidentitiesClient } from '../type'; - -class RoboidentitiesNativeClient implements RoboidentitiesClient { - private robonames: Record = {}; - private robohashes: Record = {}; - - public generateRoboname: (initialString: string) => Promise = async (initialString) => { - if (this.robonames[initialString]) { - return this.robonames[initialString]; - } else { - const response = await window.NativeRobosats?.postMessage({ - category: 'roboidentities', - type: 'roboname', - detail: initialString, - }); - const result = response ? Object.values(response)[0] : ''; - this.robonames[initialString] = result; - return result; - } - }; - - public generateRobohash: (initialString: string, size: 'small' | 'large') => Promise = - async (initialString, size) => { - const key = `${initialString};${size === 'small' ? 80 : 256}`; - - if (this.robohashes[key]) { - return this.robohashes[key]; - } else { - const response = await window.NativeRobosats?.postMessage({ - category: 'roboidentities', - type: 'robohash', - detail: key, - }); - const result: string = response ? Object.values(response)[0] : ''; - const image: string = `data:image/png;base64,${result}`; - this.robohashes[key] = image; - return image; - } - }; -} - -export default RoboidentitiesNativeClient; diff --git a/frontend/src/services/System/SystemNativeClient/index.ts b/frontend/src/services/System/SystemNativeClient/index.ts deleted file mode 100644 index 9f554c3a..00000000 --- a/frontend/src/services/System/SystemNativeClient/index.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { type SystemClient } from '..'; -import NativeRobosats from '../../Native'; - -class SystemNativeClient implements SystemClient { - constructor() { - window.NativeRobosats = new NativeRobosats(); - void window.NativeRobosats.postMessage({ - category: 'system', - type: 'init', - }).then(() => { - this.loading = false; - }); - } - - public loading = true; - - public copyToClipboard: (value: string) => void = async (value) => { - return await window.NativeRobosats?.postMessage({ - category: 'system', - type: 'copyToClipboardString', - detail: value, - }); - }; - - public getCookie: (key: string) => string = (key) => { - const cookie = window.NativeRobosats?.cookies[key]; - return cookie === null || cookie === undefined ? '' : cookie; - }; - - public setCookie: (key: string, value: string) => void = (key, value) => { - window.NativeRobosats?.loadCookie({ key, value }); - void window.NativeRobosats?.postMessage({ - category: 'system', - type: 'setCookie', - key, - detail: value, - }); - }; - - public deleteCookie: (key: string) => void = (key) => { - delete window.NativeRobosats?.cookies[key]; - - void window.NativeRobosats?.postMessage({ - category: 'system', - type: 'deleteCookie', - key, - }); - }; - - // Emulate storage as emulated cookies (....to improve) - public getItem: (key: string) => string = (key) => { - return this.getCookie(key); - }; - - public setItem: (key: string, value: string) => void = (key, value) => { - this.setCookie(key, value); - }; - - public deleteItem: (key: string) => void = (key) => { - this.deleteCookie(key); - }; -} - -export default SystemNativeClient; diff --git a/frontend/src/services/System/index.ts b/frontend/src/services/System/index.ts index 4d31d34b..9caf6489 100644 --- a/frontend/src/services/System/index.ts +++ b/frontend/src/services/System/index.ts @@ -1,4 +1,3 @@ -import SystemNativeClient from './SystemNativeClient'; import SystemWebClient from './SystemWebClient'; import SystemDesktopClient from './SystemDesktopClient'; import SystemAndroidClient from './SystemAndroidClient'; @@ -15,11 +14,7 @@ export interface SystemClient { } function getSystemClient(): SystemClient { - if (window.navigator.userAgent.includes('robosats')) { - // If userAgent has "RoboSats", we assume the app is running inside of the - // react-native-web view of the RoboSats Android app. - return new SystemNativeClient(); - } else if (window.navigator.userAgent.includes('Electron')) { + if (window.navigator.userAgent.includes('Electron')) { // If userAgent has "Electron", we assume the app is running inside of an Electron app. return new SystemDesktopClient(); } else if (window.navigator.userAgent.includes('AndroidRobosats')) { diff --git a/frontend/src/services/Websocket/WebsocketNativeClient/index.ts b/frontend/src/services/Websocket/WebsocketNativeClient/index.ts deleted file mode 100644 index 358557a3..00000000 --- a/frontend/src/services/Websocket/WebsocketNativeClient/index.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { WebsocketState, type WebsocketClient, type WebsocketConnection } from '..'; -import WebsocketWebClient from '../WebsocketWebClient'; - -class WebsocketConnectionNative implements WebsocketConnection { - constructor(path: string) { - this.path = path; - window.addEventListener('wsMessage', (event) => { - const path: string = event?.detail?.path; - const message: string = event?.detail?.message; - if (path && message && path === this.path) { - this.wsMessagePromises.forEach((fn) => { - fn({ data: message }); - }); - } - }); - } - - private readonly path: string; - - private readonly wsMessagePromises: Array<(message: object) => void> = []; - private readonly wsClosePromises: Array<() => void> = []; - - public send: (message: string) => void = (message: string) => { - void window.NativeRobosats?.postMessage({ - category: 'ws', - type: 'send', - path: this.path, - message, - }); - }; - - public close: () => void = () => { - void window.NativeRobosats?.postMessage({ - category: 'ws', - type: 'close', - path: this.path, - }).then((response) => { - if (response.connection) { - this.wsClosePromises.forEach((fn) => { - fn(); - }); - } else { - Error('Failed to close websocket connection.'); - } - }); - }; - - public onMessage: (event: (message: object) => void) => void = (event) => { - this.wsMessagePromises.push(event); - }; - - public onClose: (event: () => void) => void = (event) => { - this.wsClosePromises.push(event); - }; - - public onError: (event: (error: object) => void) => void = (_event) => { - // Not implemented - }; - - public getReadyState: () => number = () => WebsocketState.OPEN; -} - -class WebsocketNativeClient implements WebsocketClient { - public useProxy = true; - - private readonly webClient: WebsocketWebClient = new WebsocketWebClient(); - - public open: (path: string) => Promise = async (path) => { - if (!this.useProxy) return await this.webClient.open(path); - - return await new Promise((resolve, reject) => { - window.NativeRobosats?.postMessage({ - category: 'ws', - type: 'open', - path, - }) - .then((response) => { - if (response.connection) { - resolve(new WebsocketConnectionNative(path)); - } else { - reject(new Error('Failed to establish a websocket connection.')); - } - }) - .catch(() => { - reject(new Error('Failed to establish a websocket connection.')); - }); - }); - }; -} - -export default WebsocketNativeClient; diff --git a/frontend/src/services/Websocket/index.ts b/frontend/src/services/Websocket/index.ts index 5a6ad5ce..171ac15d 100644 --- a/frontend/src/services/Websocket/index.ts +++ b/frontend/src/services/Websocket/index.ts @@ -1,5 +1,4 @@ import WebsocketAndroidClient from './WebsocketAndroidClient'; -import WebsocketNativeClient from './WebsocketNativeClient'; import WebsocketWebClient from './WebsocketWebClient'; export const WebsocketState = { @@ -28,10 +27,6 @@ function getWebsocketClient(): WebsocketClient { // If userAgent has "AndroidRobosats", we assume the app is running inside of the // WebView of the Kotlin RoboSats Android app. return new WebsocketAndroidClient(); - } else if (window.navigator.userAgent.includes('robosats')) { - // If userAgent has "RoboSats", we assume the app is running inside of the - // react-native-web view of the RoboSats Android app. - return new WebsocketNativeClient(); } else { // Otherwise, we assume the app is running in a web browser. return new WebsocketWebClient(); diff --git a/frontend/src/services/api/ApiNativeClient/index.ts b/frontend/src/services/api/ApiAndroidClient/index.ts similarity index 61% rename from frontend/src/services/api/ApiNativeClient/index.ts rename to frontend/src/services/api/ApiAndroidClient/index.ts index 76ee2457..7b2ca816 100644 --- a/frontend/src/services/api/ApiNativeClient/index.ts +++ b/frontend/src/services/api/ApiAndroidClient/index.ts @@ -1,8 +1,9 @@ import { type ApiClient, type Auth } from '..'; import { systemClient } from '../../System'; import ApiWebClient from '../ApiWebClient'; +import { v4 as uuidv4 } from 'uuid'; -class ApiNativeClient implements ApiClient { +class ApiAndroidClient implements ApiClient { public useProxy = true; private readonly webClient: ApiClient = new ApiWebClient(); @@ -54,13 +55,16 @@ class ApiNativeClient implements ApiClient { public delete: (baseUrl: string, path: string, auth?: Auth) => Promise = async (baseUrl, path, auth) => { if (!this.useProxy) return await this.webClient.delete(baseUrl, path, auth); - return await window.NativeRobosats?.postMessage({ - category: 'http', - type: 'delete', - baseUrl, - path, - headers: this.getHeaders(auth), - }).then(this.parseResponse); + + const jsonHeaders = JSON.stringify(this.getHeaders(auth)); + + const result = await new Promise((resolve, reject) => { + const uuid: string = uuidv4(); + window.AndroidAppRobosats?.sendRequest(uuid, 'DELETE', baseUrl + path, jsonHeaders, ''); + window.AndroidRobosats?.storePromise(uuid, resolve, reject); + }); + + return this.parseResponse(JSON.parse(result)); }; public post: ( @@ -70,14 +74,17 @@ class ApiNativeClient implements ApiClient { auth?: Auth, ) => Promise = async (baseUrl, path, body, auth) => { if (!this.useProxy) return await this.webClient.post(baseUrl, path, body, auth); - return await window.NativeRobosats?.postMessage({ - category: 'http', - type: 'post', - baseUrl, - path, - body, - headers: this.getHeaders(auth), - }).then(this.parseResponse); + + const jsonHeaders = JSON.stringify(this.getHeaders(auth)); + const jsonBody = JSON.stringify(body); + + const result = await new Promise((resolve, reject) => { + const uuid: string = uuidv4(); + window.AndroidAppRobosats?.sendRequest(uuid, 'POST', baseUrl + path, jsonHeaders, jsonBody); + window.AndroidRobosats?.storePromise(uuid, resolve, reject); + }); + + return this.parseResponse(JSON.parse(result)); }; public get: (baseUrl: string, path: string, auth?: Auth) => Promise = async ( @@ -86,14 +93,17 @@ class ApiNativeClient implements ApiClient { auth, ) => { if (!this.useProxy) return await this.webClient.get(baseUrl, path, auth); - return await window.NativeRobosats?.postMessage({ - category: 'http', - type: 'get', - baseUrl, - path, - headers: this.getHeaders(auth), - }).then(this.parseResponse); + + const jsonHeaders = JSON.stringify(this.getHeaders(auth)); + + const result = await new Promise((resolve, reject) => { + const uuid: string = uuidv4(); + window.AndroidAppRobosats?.sendRequest(uuid, 'GET', baseUrl + path, jsonHeaders, ''); + window.AndroidRobosats?.storePromise(uuid, resolve, reject); + }); + + return this.parseResponse(JSON.parse(result)); }; } -export default ApiNativeClient; +export default ApiAndroidClient; diff --git a/frontend/src/services/api/index.ts b/frontend/src/services/api/index.ts index a6fcc421..adf7eb92 100644 --- a/frontend/src/services/api/index.ts +++ b/frontend/src/services/api/index.ts @@ -1,5 +1,5 @@ import ApiWebClient from './ApiWebClient'; -import ApiNativeClient from './ApiNativeClient'; +import ApiAndroidClient from './ApiAndroidClient'; export interface Auth { tokenSHA256: string; @@ -15,5 +15,6 @@ export interface ApiClient { delete: (baseUrl: string, path: string, auth?: Auth) => Promise; } -export const apiClient: ApiClient = - window.ReactNativeWebView != null ? new ApiNativeClient() : new ApiWebClient(); +export const apiClient: ApiClient = window.navigator.userAgent.includes('AndroidRobosats') + ? new ApiAndroidClient() + : new ApiWebClient(); diff --git a/frontend/templates/frontend/index.ejs b/frontend/templates/frontend/index.ejs index 1ae6d640..d645e805 100644 --- a/frontend/templates/frontend/index.ejs +++ b/frontend/templates/frontend/index.ejs @@ -6,11 +6,11 @@ - + - + <% if (pro) { %> RoboSats PRO - Simple and Private Bitcoin Exchange @@ -24,7 +24,7 @@ - +