Compare commits

..

47 Commits

Author SHA1 Message Date
spectrum fa72d8de43 build: make conan tvOS pipeline build
build: make tvOS awg prebuilt explicit

docs: make Apple TV build manual portable
2026-04-27 21:49:00 +03:00
spectrum e43973f8ad feat: add support for tvOS platform 2026-04-25 22:33:27 +03:00
spectrum 8d28beacd8 refactor: separate network extension sources for different protocols and platforms 2026-04-25 22:30:51 +03:00
spectrum 495d59da07 feat: add Apple TV support for network extension and refactor ios cmake scripts 2026-04-25 22:30:07 +03:00
spectrum 528015d17e feat: add support for Apple TV target settings for iOS/Xcode projects 2026-04-25 22:28:06 +03:00
Yaroslav Gurov 55237f951e feat: add conan to all mobile and NE builds 2026-04-21 01:59:18 +02:00
Yaroslav Gurov 1b4f3d5872 chore: remove MacOS-old from build targets 2026-04-21 01:57:47 +02:00
Yaroslav Gurov 8eefe7d26b fix: windows artifact upload 2026-04-20 21:33:59 +02:00
Yaroslav Gurov fd34bf08f1 fix: windows rework CI/CD 2026-04-20 20:40:40 +02:00
Yaroslav Gurov a7734d6093 fix: CI/CD correct QT paths 2026-04-16 16:11:02 +02:00
Yaroslav Gurov 526a7cad89 fix: libncap automake args 2026-04-16 15:31:28 +02:00
Yaroslav Gurov 13955dd36b feat: rework CI/CD for Linux 2026-04-16 14:31:31 +02:00
Yaroslav Gurov d81c1e5c1a feat: rework CI/CD for macos 2026-04-16 01:57:25 +02:00
Yaroslav Gurov 3ad6a7a46d feat: productbuild cpack support 2026-04-13 02:56:18 +02:00
Yaroslav Gurov de5e2635e3 feat: cpack windows full-featured build 2026-04-05 06:18:18 +02:00
Yaroslav Gurov b08154671e feat: linux cpack support 2026-04-03 00:44:35 +02:00
Yaroslav Gurov af2ade1d20 feat: linux initial packaging support 2026-04-02 12:15:41 +02:00
Yaroslav Gurov f456db5392 feat: linux <-> conan features 2026-04-02 12:12:51 +02:00
Yaroslav Gurov ea71e9b87d chore: remove redundant hint from conan find 2026-03-31 14:46:48 +02:00
Yaroslav Gurov 7f3a4aaa14 feat(conan): patch cloak to properly provide env for golang 2026-03-30 17:53:16 +02:00
Yaroslav Gurov 7c455591f6 chore: bump android deps for 16kb support 2026-03-29 03:53:58 +02:00
Yaroslav Gurov f8e229ff7b fix: provide flags for cloak plugin for openvpn-pt-android 2026-03-29 03:53:18 +02:00
Yaroslav Gurov b033fefa31 fix: install for apple systems 2026-03-28 13:41:09 +01:00
Yaroslav Gurov aab0927242 chore: exclude qtkeychain from the target build 2026-03-28 03:08:26 +01:00
Yaroslav Gurov ca8164fde4 feat: support full-featured cmake install 2026-03-28 02:41:49 +01:00
Yaroslav Gurov 7efb532bc9 feat: support of drivers for windows 2026-03-28 02:40:45 +01:00
Yaroslav Gurov 98915b3cf2 chore(conan): recipes cleanup 2026-03-27 00:22:09 +01:00
Yaroslav Gurov fa5076dff0 feat: 16kb support for Android 2026-03-26 23:44:21 +01:00
Yaroslav Gurov dbb918b68c feat: conan and platform bootstrap rework in cmake 2026-03-26 23:43:19 +01:00
Yaroslav Gurov 5078d1a2ca conan: fix version for v2ray-rules-dat 2026-03-26 13:56:53 +01:00
Yaroslav Gurov 6907886c44 conan: make awg-windows and wintun be interpret as exes 2026-03-23 23:37:01 +01:00
Yaroslav Gurov 2f01aac483 conan: windows initial support 2026-03-21 02:03:29 +01:00
Yaroslav Gurov b94cddcb1c conan: address lack of SONAME of libck-ovpn-plugin.so correctly 2026-03-17 21:58:03 +01:00
Yaroslav Gurov 2b5c2cb021 conan: use lib linking instead of QT_EXTRA_LIBS for OVPN 2026-03-17 20:16:36 +01:00
Yaroslav Gurov 729d6ee6d3 conan: add geosite.dat and geoip.dat 2026-03-17 15:22:22 +01:00
Yaroslav Gurov cd87253844 conan: beautify makefile-based recipes 2026-03-17 01:26:00 +01:00
Yaroslav Gurov 8cdaa0c2ee chore: get rid of manual deploy recursive copying 2026-03-17 01:24:24 +01:00
Yaroslav Gurov d563d66b28 chore: bump min macos version 2026-03-17 01:23:14 +01:00
Yaroslav Gurov 37327e30d3 conan: apple full-featured support 2026-03-16 13:24:02 +01:00
NickVs2015 e0af63ce1c fix: awg android connection establish 2026-03-10 00:43:10 +03:00
NickVs2015 041d0fcab5 feat: openvpn add support android 2026-03-09 22:55:05 +03:00
Yaroslav Gurov 2a6960a4fb conan: android additional recipes and fixes 2026-03-09 14:02:47 +01:00
Yaroslav Gurov 8481367fb6 fix: android libssh fixes 2026-03-06 13:29:53 +01:00
Yaroslav Gurov 70d6f9996d feat: conan android initial support 2026-03-06 12:39:46 +01:00
Yaroslav Gurov 616d58e901 feat: macos full feature conan build, except ss and cloak 2026-03-03 01:29:03 +01:00
Yaroslav Gurov 0bfbd82536 feat: add awg-go and awg-apple recipes 2026-02-24 11:51:18 +01:00
Yaroslav Gurov 544ad4ba6e feat: initial conan support 2026-02-23 00:55:11 +01:00
210 changed files with 15072 additions and 11057 deletions
+56
View File
@@ -0,0 +1,56 @@
# .github/actions/setup-keychain/action.yml
name: Setup apple keychain
description: Creates and configures a temporary build keychain
inputs:
keychain-path:
description: Name of the keychain
required: true
keychain-password:
description: Temporary keychain password
required: true
app-cert-base64:
description: Base64-encoded P12 app certificate
required: true
app-cert-password:
description: Application certificate password
required: true
installer-cert-base64:
description: Base64-encoded P12 installer certificate
required: true
installer-cert-password:
description: Installer certificate password
required: true
runs:
using: composite
steps:
- name: Create keychain
shell: bash
env:
KEYCHAIN_PATH: ${{ inputs.keychain-path }}
KEYCHAIN_PASSWORD: ${{ inputs.keychain-password }}
APP_CERT_BASE64: ${{ inputs.app-cert-base64 }}
APP_CERT_PASSWORD: ${{ inputs.app-cert-password }}
INSTALLER_CERT_BASE64: ${{ inputs.installer-cert-base64 }}
INSTALLER_CERT_PASSWORD: ${{ inputs.installer-cert-password }}
run: |
set -e
APP_CERT_PATH=$RUNNER_TEMP/DeveloperIdApplicationCertificate.p12
INSTALLER_CERT_PATH=$RUNNER_TEMP/DeveloperIdInstallerCertificate.p12
echo -n "$APP_CERT_BASE64" | base64 --decode -o "$APP_CERT_PATH"
echo -n "$INSTALLER_CERT_BASE64" | base64 --decode -o "$INSTALLER_CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security default-keychain -s "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "${{ github.action_path }}/DeveloperIDG2CA.cer" -k "$KEYCHAIN_PATH" -A
security import "$APP_CERT_PATH" -k "$KEYCHAIN_PATH" -P "$APP_CERT_PASSWORD" -A -t cert -f pkcs12
security import "$INSTALLER_CERT_PATH" -k "$KEYCHAIN_PATH" -P "$INSTALLER_CERT_PASSWORD" -A -t cert -f pkcs12
security set-key-partition-list -S apple-tool:,apple:,codesign: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
+95 -202
View File
@@ -38,7 +38,18 @@ jobs:
set-env: 'true' set-env: 'true'
aqtversion: '==3.3.0' aqtversion: '==3.3.0'
py7zrversion: '==0.22.*' py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}' extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Setup python'
uses: actions/setup-python@v6
with:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
- name: 'Install system packages'
run: sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev
- name: 'Get sources' - name: 'Get sources'
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -46,38 +57,17 @@ jobs:
submodules: 'true' submodules: 'true'
fetch-depth: 10 fetch-depth: 10
- name: 'Get version from CMakeLists.txt'
id: get_version
run: |
VERSION=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+.[0-9]+.[0-9]+.[0-9]+)\)/\1/')
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Version: $VERSION"
# - name: 'Setup ccache'
# uses: hendrikmuhs/ccache-action@v1.2
- name: 'Build project' - name: 'Build project'
run: | shell: bash
sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev env:
export QT_BIN_DIR=${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64/bin QT_INSTALL_DIR: ${{ runner.temp }}
export QIF_BIN_DIR=${{ runner.temp }}/Qt/Tools/QtInstallerFramework/${{ env.QIF_VERSION }}/bin run: ./deploy/build.sh
bash deploy/build_linux.sh
- name: 'Pack installer'
run: cd deploy && tar -cf AmneziaVPN_Linux_Installer.tar AmneziaVPN_Linux_Installer.bin && zip AmneziaVPN_${VERSION}_linux_x64.tar.zip AmneziaVPN_Linux_Installer.tar
- name: 'Upload installer artifact' - name: 'Upload installer artifact'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: AmneziaVPN_${{ env.VERSION }}_linux_x64.tar.zip path: deploy/build/AmneziaVPN-*-Linux.run
path: deploy/AmneziaVPN_${{ env.VERSION }}_linux_x64.tar.zip archive: false
retention-days: 7
- name: 'Upload unpacked artifact'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN_Linux_unpacked
path: deploy/AppDir
retention-days: 7 retention-days: 7
- name: 'Upload translations artifact' - name: 'Upload translations artifact'
@@ -95,7 +85,6 @@ jobs:
env: env:
QT_VERSION: 6.10.1 QT_VERSION: 6.10.1
QIF_VERSION: 4.7 QIF_VERSION: 4.7
BUILD_ARCH: 64
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
@@ -111,17 +100,6 @@ jobs:
submodules: 'true' submodules: 'true'
fetch-depth: 10 fetch-depth: 10
- name: 'Get version from CMakeLists.txt'
id: get_version
shell: bash
run: |
VERSION=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+.[0-9]+.[0-9]+.[0-9]+)\)/\1/')
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Version: $VERSION"
# - name: 'Setup ccache'
# uses: hendrikmuhs/ccache-action@v1.2
- name: 'Install Qt' - name: 'Install Qt'
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v3
with: with:
@@ -158,39 +136,34 @@ jobs:
$wixBinDir = Join-Path $env:USERPROFILE ".dotnet\tools" $wixBinDir = Join-Path $env:USERPROFILE ".dotnet\tools"
echo "WIX_BIN_DIR=$wixBinDir" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append echo "WIX_BIN_DIR=$wixBinDir" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: 'Setup python'
uses: actions/setup-python@v6
with:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
- name: 'Build project' - name: 'Build project'
shell: cmd shell: cmd
env:
QT_INSTALL_DIR: ${{ runner.temp }}
run: | run: |
set BUILD_ARCH=${{ env.BUILD_ARCH }} set WIX_ROOT_PATH="${{ env.USERPROFILE }}\.dotnet\tools"
set QT_BIN_DIR="${{ runner.temp }}\\Qt\\${{ env.QT_VERSION }}\\msvc2022_64\\bin" deploy\build.bat --installer all
set QIF_BIN_DIR="${{ runner.temp }}\\Qt\\Tools\\QtInstallerFramework\\${{ env.QIF_VERSION }}\\bin"
set WIX_BIN_DIR=%USERPROFILE%\.dotnet\tools
call deploy\\build_windows.bat
- name: 'Rename Windows installer' - name: 'Upload WIX installer artifact'
shell: cmd uses: actions/upload-artifact@v7
run: |
copy AmneziaVPN_x${{ env.BUILD_ARCH }}.exe AmneziaVPN_%VERSION%_x64.exe
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v4
with: with:
name: AmneziaVPN_${{ env.VERSION }}_x64.exe path: deploy/build/AmneziaVPN-*-win64.msi
path: AmneziaVPN_${{ env.VERSION }}_x64.exe archive: false
retention-days: 7 retention-days: 7
- name: 'Upload MSI installer artifact' - name: 'Upload IFW installer artifact'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: AmneziaVPN_Windows_MSI_installer path: deploy/build/AmneziaVPN-*-win64.exe
path: AmneziaVPN_x${{ env.BUILD_ARCH }}.msi archive: false
retention-days: 7
- name: 'Upload unpacked artifact'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN_Windows_unpacked
path: deploy\\build_${{ env.BUILD_ARCH }}\\client\\Release
retention-days: 7 retention-days: 7
# ------------------------------------------------------ # ------------------------------------------------------
@@ -258,11 +231,13 @@ jobs:
submodules: 'true' submodules: 'true'
fetch-depth: 10 fetch-depth: 10
# - name: 'Setup ccache' - name: 'Setup python'
# uses: hendrikmuhs/ccache-action@v1.2 uses: actions/setup-python@v6
with:
python-version: 3.14
- name: 'Install dependencies' - name: 'Install deps'
run: pip install jsonschema jinja2 run: pip install "conan==2.26.2" jsonschema jinja2
- name: 'Build project' - name: 'Build project'
run: | run: |
@@ -285,93 +260,6 @@ jobs:
IOS_APP_PROVISIONING_PROFILE: ${{ secrets.IOS_APP_PROVISIONING_PROFILE }} IOS_APP_PROVISIONING_PROFILE: ${{ secrets.IOS_APP_PROVISIONING_PROFILE }}
IOS_NE_PROVISIONING_PROFILE: ${{ secrets.IOS_NE_PROVISIONING_PROFILE }} IOS_NE_PROVISIONING_PROFILE: ${{ secrets.IOS_NE_PROVISIONING_PROFILE }}
# - name: 'Upload appstore .ipa and dSYMs to artifacts'
# uses: actions/upload-artifact@v4
# with:
# name: app-store ipa & dsyms
# path: |
# ${{ github.workspace }}/AmneziaVPN-iOS.ipa
# ${{ github.workspace }}/*.app.dSYM.zip
# retention-days: 7
# ------------------------------------------------------
Build-MacOS-old:
runs-on: macos-latest
env:
# Keep compat with MacOS 10.15 aka Catalina by Qt 6.4
QT_VERSION: 6.4.3
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
MAC_APP_CERT_CERT: ${{ secrets.MAC_APP_CERT_CERT }}
MAC_SIGNER_ID: ${{ secrets.MAC_SIGNER_ID }}
MAC_APP_CERT_PW: ${{ secrets.MAC_APP_CERT_PW }}
MAC_INSTALLER_SIGNER_CERT: ${{ secrets.MAC_INSTALLER_SIGNER_CERT }}
MAC_INSTALLER_SIGNER_ID: ${{ secrets.MAC_INSTALLER_SIGNER_ID }}
MAC_INSTALL_CERT_PW: ${{ secrets.MAC_INSTALL_CERT_PW }}
APPLE_DEV_EMAIL: ${{ secrets.APPLE_DEV_EMAIL }}
APPLE_DEV_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }}
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
FREE_V2_ENDPOINT: ${{ secrets.FREE_V2_ENDPOINT }}
PREM_V1_ENDPOINT: ${{ secrets.PREM_V1_ENDPOINT }}
steps:
- name: 'Setup xcode'
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.4.0'
- name: 'Install Qt'
uses: jurplel/install-qt-action@v3
with:
version: ${{ env.QT_VERSION }}
host: 'mac'
target: 'desktop'
arch: 'clang_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
dir: ${{ runner.temp }}
setup-python: 'true'
set-env: 'true'
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
- name: 'Get sources'
uses: actions/checkout@v4
with:
submodules: 'true'
fetch-depth: 10
# - name: 'Setup ccache'
# uses: hendrikmuhs/ccache-action@v1.2
- name: 'Build project'
run: |
export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos/bin"
bash deploy/build_macos.sh -n
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN_MacOS_old_installer
path: deploy/build/pkg/AmneziaVPN.pkg
retention-days: 7
- name: 'Upload unpacked artifact'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN_MacOS_old_unpacked
path: deploy/build/client/AmneziaVPN.app
retention-days: 7
# ------------------------------------------------------ # ------------------------------------------------------
Build-MacOS: Build-MacOS:
@@ -379,19 +267,8 @@ jobs:
env: env:
QT_VERSION: 6.10.1 QT_VERSION: 6.10.1
KEYCHAIN_NAME: "build.keychain"
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }} KEYCHAIN_PASSWORD: ""
MAC_APP_CERT_CERT: ${{ secrets.MAC_APP_CERT_CERT }}
MAC_SIGNER_ID: ${{ secrets.MAC_SIGNER_ID }}
MAC_APP_CERT_PW: ${{ secrets.MAC_APP_CERT_PW }}
MAC_INSTALLER_SIGNER_CERT: ${{ secrets.MAC_INSTALLER_SIGNER_CERT }}
MAC_INSTALLER_SIGNER_ID: ${{ secrets.MAC_INSTALLER_SIGNER_ID }}
MAC_INSTALL_CERT_PW: ${{ secrets.MAC_INSTALL_CERT_PW }}
APPLE_DEV_EMAIL: ${{ secrets.APPLE_DEV_EMAIL }}
APPLE_DEV_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }}
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
@@ -420,7 +297,15 @@ jobs:
set-env: 'true' set-env: 'true'
aqtversion: '==3.3.0' aqtversion: '==3.3.0'
py7zrversion: '==0.22.*' py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}' extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Setup python'
uses: actions/setup-python@v6
with:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
- name: 'Get sources' - name: 'Get sources'
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -428,39 +313,34 @@ jobs:
submodules: 'true' submodules: 'true'
fetch-depth: 10 fetch-depth: 10
- name: 'Get version from CMakeLists.txt' - name: 'Install certs'
id: get_version uses: ./.github/actions/setup-keychain
run: | with:
VERSION=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+.[0-9]+.[0-9]+.[0-9]+)\)/\1/') keychain-path: ${{ env.KEYCHAIN_NAME }}
echo "VERSION=$VERSION" >> $GITHUB_ENV keychain-password: ${{ env.KEYCHAIN_PASSWORD }}
echo "Version: $VERSION" app-cert-base64: ${{ secrets.MAC_APP_CERT_CERT }}
app-cert-password: ${{ secrets.MAC_APP_CERT_PW }}
# - name: 'Setup ccache' installer-cert-base64: ${{ secrets.MAC_INSTALLER_SIGNER_CERT }}
# uses: hendrikmuhs/ccache-action@v1.2 installer-cert-password: ${{ secrets.MAC_INSTALL_CERT_PW }}
- name: 'Build project' - name: 'Build project'
run: | env:
export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos/bin" QT_INSTALL_DIR: ${{ runner.temp }}
bash deploy/build_macos.sh -n CODESIGN_KEYCHAIN: ${{ env.KEYCHAIN_NAME }}
CODESIGN_SIGNATURE: ${{ secrets.MAC_SIGNER_ID }}
- name: 'Pack macOS installer' CODESIGN_INSTALLER_KEYCHAIN: ${{ env.KEYCHAIN_NAME }}
run: | CODESIGN_INSTALLER_SIGNATURE: ${{ secrets.MAC_INSTALLER_SIGNER_ID }}
cd deploy/build/pkg NOTARYTOOL_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
zip -r ../../AmneziaVPN_${VERSION}_macos.zip AmneziaVPN.pkg NOTARYTOOL_EMAIL: ${{ secrets.APPLE_DEV_EMAIL }}
cd ../../.. NOTARYTOOL_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }}
shell: bash
run: deploy/build.sh
- name: 'Upload installer artifact' - name: 'Upload installer artifact'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: AmneziaVPN_${{ env.VERSION }}_macos.zip path: deploy/build/AmneziaVPN-*-Darwin.pkg
path: deploy/AmneziaVPN_${{ env.VERSION }}_macos.zip archive: false
retention-days: 7
- name: 'Upload unpacked artifact'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN_MacOS_unpacked
path: deploy/build/client/AmneziaVPN.app
retention-days: 7 retention-days: 7
Build-MacOS-NE: Build-MacOS-NE:
@@ -519,8 +399,13 @@ jobs:
submodules: 'true' submodules: 'true'
fetch-depth: 10 fetch-depth: 10
# - name: 'Setup ccache' - name: 'Setup python'
# uses: hendrikmuhs/ccache-action@v1.2 uses: actions/setup-python@v6
with:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
- name: 'Build project' - name: 'Build project'
run: | run: |
@@ -652,6 +537,14 @@ jobs:
run: | run: |
echo $KEYSTORE_BASE64 | base64 --decode > android.keystore echo $KEYSTORE_BASE64 | base64 --decode > android.keystore
- name: 'Setup python'
uses: actions/setup-python@v6
with:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
- name: 'Build project' - name: 'Build project'
env: env:
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
+15 -40
View File
@@ -1,7 +1,14 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN) set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.15.0) set(AMNEZIAVPN_VERSION 4.8.13.1)
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
${CMAKE_SOURCE_DIR}/cmake/platform_settings.cmake
${CMAKE_SOURCE_DIR}/cmake/recipes_bootstrap.cmake
${CMAKE_SOURCE_DIR}/cmake/conan_provider.cmake
CACHE STRING "" FORCE)
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION} project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
DESCRIPTION "AmneziaVPN" DESCRIPTION "AmneziaVPN"
@@ -12,7 +19,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}") set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 2118) set(APP_ANDROID_VERSION_CODE 2107)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux") set(MZ_PLATFORM_NAME "linux")
@@ -24,6 +31,8 @@ elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Android")
set(MZ_PLATFORM_NAME "android") set(MZ_PLATFORM_NAME "android")
elseif(${CMAKE_SYSTEM_NAME} STREQUAL "iOS") elseif(${CMAKE_SYSTEM_NAME} STREQUAL "iOS")
set(MZ_PLATFORM_NAME "ios") set(MZ_PLATFORM_NAME "ios")
elseif(${CMAKE_SYSTEM_NAME} STREQUAL "tvOS")
set(MZ_PLATFORM_NAME "ios")
elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten") elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten")
set(MZ_PLATFORM_NAME "wasm") set(MZ_PLATFORM_NAME "wasm")
endif() endif()
@@ -33,7 +42,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(APPLE) if(APPLE)
if(IOS) if(IOS OR CMAKE_SYSTEM_NAME STREQUAL "tvOS")
set(CMAKE_OSX_ARCHITECTURES "arm64") set(CMAKE_OSX_ARCHITECTURES "arm64")
elseif(MACOS_NE) elseif(MACOS_NE)
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64") set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64")
@@ -44,44 +53,10 @@ endif()
add_subdirectory(client) add_subdirectory(client)
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE) if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS")
add_subdirectory(service) add_subdirectory(service)
include(${CMAKE_SOURCE_DIR}/deploy/installer/config.cmake)
endif() endif()
set(AMNEZIA_STAGE_DIR "${CMAKE_BINARY_DIR}/stage") if ((LINUX AND NOT ANDROID) OR (APPLE AND NOT IOS AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS") OR (WIN32))
include(${CMAKE_SOURCE_DIR}/cmake/CPack.cmake)
if(WIN32 AND NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
file(TO_CMAKE_PATH "${AMNEZIA_STAGE_DIR}" AMNEZIA_STAGE_DIR_CMAKE)
set(CPACK_GENERATOR "WIX")
set(CPACK_WIX_VERSION 4)
set(CPACK_PACKAGE_NAME "AmneziaVPN")
set(CPACK_PACKAGE_VENDOR "AmneziaVPN")
set(CPACK_PACKAGE_VERSION ${AMNEZIAVPN_VERSION})
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "AmneziaVPN client")
set(AMNEZIA_LICENSE_TXT "${CMAKE_BINARY_DIR}/LICENSE.txt")
configure_file("${CMAKE_SOURCE_DIR}/LICENSE" "${AMNEZIA_LICENSE_TXT}" COPYONLY)
set(CPACK_RESOURCE_FILE_LICENSE "${AMNEZIA_LICENSE_TXT}")
set(CPACK_PACKAGE_INSTALL_DIRECTORY "AmneziaVPN")
set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}")
set(CPACK_PACKAGE_EXECUTABLES "AmneziaVPN" "AmneziaVPN")
set(CPACK_WIX_UPGRADE_GUID "{2D55AC62-96D6-4692-8C05-0D85BBF95485}")
set(CPACK_WIX_PRODUCT_ICON "${CMAKE_SOURCE_DIR}/client/images/app.ico")
# WiX patches
set(_AMNEZIA_WIX_PATCH_SERVICE "${CMAKE_SOURCE_DIR}/deploy/installer/wix/service_install_patch.xml")
set(_AMNEZIA_WIX_PATCH_CLOSE_APP "${CMAKE_SOURCE_DIR}/deploy/installer/wix/close_client_patch.xml")
file(TO_CMAKE_PATH "${_AMNEZIA_WIX_PATCH_SERVICE}" _AMNEZIA_WIX_PATCH_SERVICE_CMAKE)
file(TO_CMAKE_PATH "${_AMNEZIA_WIX_PATCH_CLOSE_APP}" _AMNEZIA_WIX_PATCH_CLOSE_APP_CMAKE)
set(CPACK_WIX_PATCH_FILE "${_AMNEZIA_WIX_PATCH_SERVICE_CMAKE};${_AMNEZIA_WIX_PATCH_CLOSE_APP_CMAKE}")
# WiX v4 Util extension for CloseApplication + namespace for util
set(CPACK_WIX_EXTENSIONS "${CPACK_WIX_EXTENSIONS};WixToolset.Util.wixext")
set(CPACK_WIX_CUSTOM_XMLNS "util=http://wixtoolset.org/schemas/v4/wxs/util")
set(CPACK_INSTALLED_DIRECTORIES "${AMNEZIA_STAGE_DIR_CMAKE};/")
include(CPack)
endif() endif()
+1 -1
View File
@@ -179,7 +179,7 @@ You may face compiling issues in QT Creator after you've worked in Android Studi
## License ## License
This project is licensed under the GNU General Public License v3.0 (see LICENSE) and also includes third-party components distributed under their own terms (see THIRD_PARTY_LICENSES.md). GPL v3.0
## Donate ## Donate
-149
View File
@@ -1,149 +0,0 @@
# Third-Party Licenses
This project is licensed under the GNU General Public License v3.0.
This file lists third-party software components used by this repository.
Each component is distributed under its own license as linked below.
---
## QtKeychain
- Source: https://github.com/frankosterfeld/qtkeychain
- License: BSD License
- License Text: https://www.gnu.org/licenses/license-list.html#ModifiedBSD
---
## QSimpleCrypto
- Source: https://github.com/n1flh31mur/QSimpleCrypto
- License: Apache License 2.0
- License Text: https://github.com/n1flh31mur/QSimpleCrypto/blob/master/LICENSE
---
## SortFilterProxyModel
- Source: https://github.com/oKcerG/SortFilterProxyModel
- License: MIT License
- License Text: https://github.com/oKcerG/SortFilterProxyModel/blob/master/LICENSE
---
## QJsonStruct
- Source: https://github.com/Qv2ray/QJsonStruct
- License: MIT License
- License Text: https://github.com/Qv2ray/QJsonStruct/blob/master/LICENSE
---
## QR Code Generator (qrcodegen)
- Source: https://github.com/nayuki/QR-Code-generator
- License: MIT License
- License Text: https://www.nayuki.io/page/qr-code-generator-library
---
## Qt Gamepad
- Source: https://github.com/qt/qtgamepad
- License: GNU General Public License v3.0 (GPL-3.0)
- License Text: https://www.gnu.org/licenses/gpl-3.0.en.html
---
## AmneziaWG Apple (WireGuard)
- Source: https://github.com/amnezia-vpn/amneziawg-apple
- License: MIT License
- License Text: https://github.com/amnezia-vpn/amneziawg-apple/blob/master/COPYING
---
## AmneziaWG Android
- Source: https://github.com/amnezia-vpn/amneziawg-go
- License: MIT License
- License Text: https://github.com/amnezia-vpn/amneziawg-go/blob/master/LICENSE
---
## Xray Core
- Source: https://github.com/XTLS/Xray-core
- License: Mozilla Public License 2.0 (MPL-2.0)
- License Text: https://github.com/XTLS/Xray-core/blob/main/LICENSE
---
## Cloak
- Source: https://github.com/cbeuw/Cloak
- License: GNU General Public License v3.0 (GPL-3.0)
- License Text: https://github.com/cbeuw/Cloak/blob/master/LICENSE
---
## Shadowsocks
- Source: https://github.com/shadowsocks/shadowsocks-libev
- License: GPL-3.0-or-later
- License Text: http://www.gnu.org/licenses/
---
## OpenSSL
- Source: https://github.com/openssl/openssl
- License: Apache License 2.0
- License Text: https://www.openssl.org/source/license.html
---
## libssh
- Source: https://www.libssh.org/
- License: GNU Lesser General Public License (LGPL)
- License Text: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
---
## OpenVPNAdapter
- Source: https://github.com/ss-abramchuk/OpenVPNAdapter
- License: GNU Affero General Public License v3.0 (AGPL-3.0)
- License Text: https://github.com/ss-abramchuk/OpenVPNAdapter/blob/master/LICENSE
---
## Wintun
- Source: https://www.wintun.net/
- License: Prebuilt Binaries License
- License Text: https://github.com/WireGuard/wintun/blob/master/prebuilt-binaries-license.txt
---
## Mullvad Split Tunnel Driver
- Source: https://github.com/mullvad/win-split-tunnel
- License: GNU General Public License v3.0 (GPL-3.0) and Mozilla Public License Version 2.0
- License Text: https://github.com/mullvad/win-split-tunnel/blob/master/LICENSE-GPL.md https://github.com/mullvad/win-split-tunnel/blob/master/LICENSE-MPL.txt
---
## tun2socks
- Source: https://github.com/eycorsican/go-tun2socks
- License: MIT License
- License Text: https://github.com/eycorsican/go-tun2socks/blob/master/LICENSE
---
## TAP-Windows Driver
- Source: https://github.com/OpenVPN/tap-windows6
- License: tap-windows6 license
- License Text: https://github.com/OpenVPN/tap-windows6/blob/master/COPYING
+340
View File
@@ -0,0 +1,340 @@
# AmneziaVPN Apple TV Build
This document describes how to build the current branch for Apple TV from the repository root.
The pipeline is:
1. Use a separately built static Qt 6.9.2 for `tvOS`.
2. Let Conan build/provide C/C++ dependencies.
3. Generate an Xcode project with `qt-cmake`.
4. Build the `.app` and embedded Network Extension with `xcodebuild`.
Important:
- Run the project commands from the repository root.
- This is a device build for `appletvos`, not a simulator build.
- `xcodebuild build` produces `.app`.
- `.ipa` is produced later via `archive` and `-exportArchive`.
- The current tvOS Network Extension scope is WireGuard-only.
- The temporary tvOS WireGuard bridge prebuilt is opt-in. The Conan recipe does not contain machine-specific fallback paths.
- Do not initialize or update submodules just for this build. If a clean checkout has empty `client/3rd` folders, pass `AMNEZIA_THIRDPARTY_ROOT` to an already initialized read-only `client/3rd` tree.
## 1. Environment
Set these paths for your machine:
```bash
export REPO_ROOT="$PWD"
export QT_DESKTOP_PREFIX="$HOME/Qt/6.9.2/macos"
export QT_TVOS_SRC="$HOME/Qt_tv/qt-6.9.2-tvos-src"
export QT_TVOS_PREFIX="$HOME/Qt_tv/6.9.2/tvos-device"
export BUILD_DIR="$REPO_ROOT/build-tvos-device-conan"
```
If this checkout does not have initialized `client/3rd` sources, point CMake at an initialized tree:
```bash
export AMNEZIA_THIRDPARTY_ROOT="/path/to/initialized/amnezia/client/3rd"
```
If you are using a temporary prebuilt tvOS WireGuard bridge, point Conan at it explicitly:
```bash
export AMNEZIA_TVOS_AWG_PREBUILT_DIR="/path/to/WireGuardKitGo-appletvos"
export AMNEZIA_TVOS_AWG_VERSION_HEADER_DIR="/path/to/directory/with/wireguard-go-version.h"
```
`AMNEZIA_TVOS_AWG_PREBUILT_DIR` must contain `libwg-go.a`.
`AMNEZIA_TVOS_AWG_VERSION_HEADER_DIR` is optional when `wireguard-go-version.h` lives in the same directory as `libwg-go.a`.
If the env vars are not set, the recipe uses the normal source build path. Rebuilding and publishing the tvOS WireGuard bridge through the registry is a separate task.
## 2. Required Local Tools
Conan must be available:
```bash
uv tool install conan
export PATH="$HOME/.local/bin:$PATH"
conan --version
```
Validated version:
```text
Conan version 2.27.1
```
The build uses Xcode's AppleTVOS SDK:
```bash
xcrun --sdk appletvos --show-sdk-path
```
## 3. Prepare Qt Sources
Do not edit the installed Qt sources in place. Copy them into a separate tvOS fork:
```bash
mkdir -p "$HOME/Qt_tv"
rsync -a "$HOME/Qt/6.9.2/Src/" "$QT_TVOS_SRC/"
```
Recommended for reproducibility:
```bash
cd "$QT_TVOS_SRC"
git init
git add .
git commit -m "Qt 6.9.2 source snapshot"
```
## 4. Apply the Qt tvOS Patchset
Apply the local Qt tvOS patchset to `$QT_TVOS_SRC`.
If you need to recreate the patchset from a fresh copy, compare these files against `$HOME/Qt/6.9.2/Src` and reapply the same changes:
- `qtbase/cmake/QtBaseGlobalTargets.cmake`
- `qtbase/cmake/QtBaseHelpers.cmake`
- `qtbase/cmake/QtBuildPathsHelpers.cmake`
- `qtbase/cmake/QtMkspecHelpers.cmake`
- `qtbase/cmake/QtConfig.cmake.in`
- `qtbase/mkspecs/unsupported/macx-tvos-clang/qplatformdefs.h`
- `qtbase/src/corelib/CMakeLists.txt`
- `qtbase/src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm`
- `qtbase/src/gui/CMakeLists.txt`
- `qtbase/src/widgets/CMakeLists.txt`
- `qtbase/src/network/kernel/qnetworkproxy_darwin.cpp`
- `qtbase/src/testlib/qtestcrashhandler.cpp`
- `qtbase/src/plugins/platforms/ios/qiosapplicationdelegate.mm`
- `qtbase/src/plugins/platforms/ios/qiosscreen.mm`
- `qtbase/src/plugins/platforms/ios/qiostheme.mm`
- `qtbase/src/plugins/platforms/ios/quiview.mm`
- `qtbase/src/plugins/platforms/ios/qiosclipboard.mm`
Recommended after patching:
```bash
git -C "$QT_TVOS_SRC" diff > "$HOME/Qt_tv/qt-6.9.2-tvos.patch"
```
Do not use `QT_APPLE_SDK=appletvos`. The working path is `CMAKE_SYSTEM_NAME=tvOS` with `CMAKE_OSX_SYSROOT=appletvos`.
## 5. Build Qt 6.9.2 for Apple TV
Only the modules required by this project are built.
```bash
mkdir -p /private/tmp/qt6.9.2-tvos-device-build
cd /private/tmp/qt6.9.2-tvos-device-build
"$QT_TVOS_SRC/configure" \
-release -static -appstore-compliant \
-nomake tests -nomake examples \
-submodules qtbase,qtdeclarative,qtshadertools,qtremoteobjects,qtsvg,qt5compat,qttools \
-qt-host-path "$QT_DESKTOP_PREFIX" \
-prefix "$QT_TVOS_PREFIX" \
-- \
-G Ninja \
-DQT_QMAKE_TARGET_MKSPEC=macx-tvos-clang \
-DCMAKE_SYSTEM_NAME=tvOS \
-DCMAKE_OSX_SYSROOT=appletvos \
-DCMAKE_OSX_ARCHITECTURES=arm64 \
-DCMAKE_OSX_DEPLOYMENT_TARGET=17.0 \
-DBUILD_SHARED_LIBS=OFF \
-DQT_NO_APPLE_SDK_MAX_VERSION_CHECK=ON
cmake --build . --parallel 8
cmake --install .
```
Sanity checks:
```bash
"$QT_TVOS_PREFIX/bin/qt-cmake" --version
"$QT_TVOS_PREFIX/bin/qmake" -query QMAKE_XSPEC
```
Expected `QMAKE_XSPEC`:
```text
macx-tvos-clang
```
Return to the repository root after building Qt:
```bash
cd "$REPO_ROOT"
```
## 6. Conan Dependency Behavior
For `CMAKE_SYSTEM_NAME=tvOS`, the project-level Conan graph is intentionally reduced:
- included: `awg-apple/2.0.1`
- included: `libssh/0.11.3@amnezia`
- included: `openssl/3.6.1` with `no_apps=True`
- excluded: `openvpnadapter`
- excluded: `hev-socks5-tunnel`
This keeps the current Apple TV target in the same practical scope as before: app plus WireGuard-only Network Extension.
`libssh` is built with `WITH_EXEC=OFF` on tvOS because tvOS does not provide `fork()` or `execv()`.
## 7. Configure the Project
From the repository root:
```bash
cd "$REPO_ROOT"
"$QT_TVOS_PREFIX/bin/qt-cmake" \
-B"$BUILD_DIR" \
-GXcode \
-DQT_HOST_PATH="$QT_DESKTOP_PREFIX" \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_SYSTEM_NAME=tvOS \
-DCMAKE_OSX_SYSROOT=appletvos
```
If you need to provide an external initialized `client/3rd` tree:
```bash
"$QT_TVOS_PREFIX/bin/qt-cmake" \
-B"$BUILD_DIR" \
-GXcode \
-DQT_HOST_PATH="$QT_DESKTOP_PREFIX" \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_SYSTEM_NAME=tvOS \
-DCMAKE_OSX_SYSROOT=appletvos \
-DAMNEZIA_THIRDPARTY_ROOT="$AMNEZIA_THIRDPARTY_ROOT"
```
Expected non-fatal configure warnings:
```text
Warning: plug-in QIOSIntegrationPlugin is not known to the current Qt installation.
Warning: plug-in QJpegPlugin is not known to the current Qt installation.
...
```
In this repo those warnings are tolerated because `client/cmake/ios.cmake` also links the static plugin targets explicitly when available.
## 8. Build the Apple TV App
```bash
xcodebuild -quiet \
-project "$BUILD_DIR/AmneziaVPN.xcodeproj" \
-scheme AmneziaVPN \
-configuration RelWithDebInfo \
-sdk appletvos \
CODE_SIGNING_ALLOWED=NO \
build
```
Outputs:
- `$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app`
- `$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/PlugIns/AmneziaVPNNetworkExtension.appex`
Verification:
```bash
file "$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/AmneziaVPN"
file "$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/PlugIns/AmneziaVPNNetworkExtension.appex/AmneziaVPNNetworkExtension"
lipo -info "$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/AmneziaVPN"
lipo -info "$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/PlugIns/AmneziaVPNNetworkExtension.appex/AmneziaVPNNetworkExtension"
```
Expected:
```text
Mach-O 64-bit executable arm64
Non-fat file: ... is architecture: arm64
```
Useful plist checks:
```bash
plutil -p "$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/Info.plist" | rg 'CFBundleIdentifier|DTPlatformName|UIDeviceFamily|MinimumOSVersion' -C 1
plutil -p "$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/PlugIns/AmneziaVPNNetworkExtension.appex/Info.plist" | rg 'CFBundleIdentifier|NSExtension|DTPlatformName|MinimumOSVersion' -C 1
```
Expected:
- `DTPlatformName => appletvos`
- `UIDeviceFamily => 3`
- `MinimumOSVersion => 17.0`
- extension point `com.apple.networkextension.packet-tunnel`
## 9. `.app` vs `.ipa`
This is the normal sequence:
1. `xcodebuild build` -> `.app`
2. `xcodebuild archive` -> `.xcarchive`
3. `xcodebuild -exportArchive` -> `.ipa`
So seeing `.app` after a successful `build` is correct.
## 10. Optional Archive and Export
The commands below are the next step for packaging, but signing and provisioning must be configured first.
Archive:
```bash
xcodebuild \
-project "$BUILD_DIR/AmneziaVPN.xcodeproj" \
-scheme AmneziaVPN \
-configuration RelWithDebInfo \
-sdk appletvos \
-archivePath "$BUILD_DIR/AmneziaVPN-tvos.xcarchive" \
archive
```
Export:
```bash
xcodebuild -exportArchive \
-archivePath "$BUILD_DIR/AmneziaVPN-tvos.xcarchive" \
-exportPath "$BUILD_DIR/export-tvos" \
-exportOptionsPlist /absolute/path/to/ExportOptions.plist
```
The resulting `.ipa` should appear under:
```text
$BUILD_DIR/export-tvos
```
## 11. Known Non-Fatal Warnings
The validated `xcodebuild` still prints warnings that do not break the build:
- missing Swift search path under the active Xcode Metal toolchain
- `SDKROOT[sdk=...]` target-level warnings generated by Xcode project export
- Swift conditional compilation flag warnings such as `GROUP_ID="..."`
- asset catalog warnings because the current icon set is still iOS-shaped, not a full tvOS Top Shelf asset set
- Go/WireGuard umbrella-header warnings from the temporary local `libwg-go.a` bridge
- deprecated libssh SCP API warnings in existing app code
- `qt_import_plugins()` warnings shown during configure
If the static platform plugin is not linked correctly, the typical failure is:
- `_OBJC_CLASS_$_QIOSApplicationDelegate`
- `_qt_main_wrapper`
Those are cleanup tasks, not blockers for the current build proof.
## 12. Fast Rebuild Checklist
If everything is already built once:
1. Reuse `$QT_TVOS_PREFIX`
2. Reuse Conan cache under `$HOME/.conan2`
3. Reuse or pass an initialized `AMNEZIA_THIRDPARTY_ROOT`
4. Re-run `qt-cmake` into `$BUILD_DIR`
5. Re-run `xcodebuild -quiet ... build`
+36 -36
View File
@@ -3,6 +3,9 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN) set(PROJECT AmneziaVPN)
project(${PROJECT}) project(${PROJECT})
set(AMNEZIA_THIRDPARTY_ROOT "${CMAKE_CURRENT_LIST_DIR}/3rd" CACHE PATH "Path to Amnezia client/3rd sources")
get_filename_component(AMNEZIA_THIRDPARTY_CLIENT_ROOT "${AMNEZIA_THIRDPARTY_ROOT}/.." ABSOLUTE)
set_property(GLOBAL PROPERTY USE_FOLDERS ON) set_property(GLOBAL PROPERTY USE_FOLDERS ON)
set_property(GLOBAL PROPERTY AUTOGEN_TARGETS_FOLDER "Autogen") set_property(GLOBAL PROPERTY AUTOGEN_TARGETS_FOLDER "Autogen")
set_property(GLOBAL PROPERTY AUTOMOC_TARGETS_FOLDER "Autogen") set_property(GLOBAL PROPERTY AUTOMOC_TARGETS_FOLDER "Autogen")
@@ -33,7 +36,7 @@ add_definitions(-DDEV_S3_ENDPOINT="$ENV{DEV_S3_ENDPOINT}")
add_definitions(-DFREE_V2_ENDPOINT="$ENV{FREE_V2_ENDPOINT}") add_definitions(-DFREE_V2_ENDPOINT="$ENV{FREE_V2_ENDPOINT}")
add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}") add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}")
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) if(WIN32 OR (APPLE AND NOT IOS AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS") OR (LINUX AND NOT ANDROID))
set(PACKAGES ${PACKAGES} Widgets) set(PACKAGES ${PACKAGES} Widgets)
endif() endif()
@@ -46,7 +49,7 @@ set(LIBS ${LIBS}
Qt6::Core5Compat Qt6::Concurrent Qt6::Core5Compat Qt6::Concurrent
) )
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) if(WIN32 OR (APPLE AND NOT IOS AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS") OR (LINUX AND NOT ANDROID))
set(LIBS ${LIBS} Qt6::Widgets) set(LIBS ${LIBS} Qt6::Widgets)
endif() endif()
@@ -56,7 +59,7 @@ target_include_directories(${PROJECT} PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}> $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
) )
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID)) if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS") OR (LINUX AND NOT ANDROID))
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep) qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep)
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_interface.rep) qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_interface.rep)
endif() endif()
@@ -78,7 +81,6 @@ set(AMNEZIAVPN_TS_FILES
) )
file(GLOB_RECURSE AMNEZIAVPN_TS_SOURCES *.qrc *.cpp *.h *.ui) file(GLOB_RECURSE AMNEZIAVPN_TS_SOURCES *.qrc *.cpp *.h *.ui)
list(FILTER AMNEZIAVPN_TS_SOURCES EXCLUDE REGEX "qtgamepad/examples")
qt_create_translation(AMNEZIAVPN_QM_FILES ${AMNEZIAVPN_TS_SOURCES} ${AMNEZIAVPN_TS_FILES}) qt_create_translation(AMNEZIAVPN_QM_FILES ${AMNEZIAVPN_TS_SOURCES} ${AMNEZIAVPN_TS_FILES})
@@ -109,6 +111,7 @@ include_directories(
${CMAKE_CURRENT_LIST_DIR}/../ipc ${CMAKE_CURRENT_LIST_DIR}/../ipc
${CMAKE_CURRENT_LIST_DIR}/../common/logger ${CMAKE_CURRENT_LIST_DIR}/../common/logger
${CMAKE_CURRENT_LIST_DIR} ${CMAKE_CURRENT_LIST_DIR}
${AMNEZIA_THIRDPARTY_CLIENT_ROOT}
${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_BINARY_DIR}
) )
@@ -176,7 +179,7 @@ if(LINUX AND NOT ANDROID)
link_directories(${CMAKE_CURRENT_LIST_DIR}/platforms/linux) link_directories(${CMAKE_CURRENT_LIST_DIR}/platforms/linux)
endif() endif()
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID)) if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS") OR (LINUX AND NOT ANDROID))
add_compile_definitions(AMNEZIA_DESKTOP) add_compile_definitions(AMNEZIA_DESKTOP)
endif() endif()
@@ -184,7 +187,8 @@ if(ANDROID)
include(cmake/android.cmake) include(cmake/android.cmake)
endif() endif()
if(IOS) if(IOS OR CMAKE_SYSTEM_NAME STREQUAL "tvOS")
option(AMNEZIA_IOS_ENABLE_APPLETV_TARGET "Enable Apple TV target settings for iOS/Xcode projects" OFF)
include(cmake/ios.cmake) include(cmake/ios.cmake)
include(cmake/ios-arch-fixup.cmake) include(cmake/ios-arch-fixup.cmake)
elseif(APPLE AND MACOS_NE) elseif(APPLE AND MACOS_NE)
@@ -197,36 +201,6 @@ endif()
target_link_libraries(${PROJECT} PRIVATE ${LIBS}) target_link_libraries(${PROJECT} PRIVATE ${LIBS})
target_compile_definitions(${PROJECT} PRIVATE "MZ_$<UPPER_CASE:${MZ_PLATFORM_NAME}>") target_compile_definitions(${PROJECT} PRIVATE "MZ_$<UPPER_CASE:${MZ_PLATFORM_NAME}>")
# deploy artifacts required to run the application to the debug build folder
if(WIN32)
if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8")
set(DEPLOY_PLATFORM_PATH "windows/x64")
else()
set(DEPLOY_PLATFORM_PATH "windows/x32")
endif()
elseif(LINUX)
set(DEPLOY_PLATFORM_PATH "linux/client")
elseif(APPLE AND NOT IOS)
set(DEPLOY_PLATFORM_PATH "macos")
endif()
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
add_custom_command(
TARGET ${PROJECT} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E $<IF:$<CONFIG:Debug>,copy_directory,true>
${CMAKE_SOURCE_DIR}/deploy/data/${DEPLOY_PLATFORM_PATH}
$<TARGET_FILE_DIR:${PROJECT}>
COMMAND_EXPAND_LISTS
)
add_custom_command(
TARGET ${PROJECT} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E $<IF:$<CONFIG:Debug>,copy_directory,true>
${CMAKE_SOURCE_DIR}/client/3rd-prebuilt/deploy-prebuilt/${DEPLOY_PLATFORM_PATH}
$<TARGET_FILE_DIR:${PROJECT}>
COMMAND_EXPAND_LISTS
)
endif()
target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC}) target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC})
# Finalize the executable so Qt can gather/deploy QML modules and plugins correctly (Android needs this). # Finalize the executable so Qt can gather/deploy QML modules and plugins correctly (Android needs this).
@@ -238,3 +212,29 @@ if(COMMAND qt_finalize_executable)
else() else()
qt_finalize_target(${PROJECT}) qt_finalize_target(${PROJECT})
endif() endif()
if(NOT IOS AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS")
install(TARGETS ${PROJECT}
DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT AmneziaVPN
)
install(FILES $<TARGET_RUNTIME_DLLS:${PROJECT}>
DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT AmneziaVPN
)
set(deploy_tool_options "")
if(WIN32)
set(deploy_tool_options "--force-openssl --force")
endif()
qt_generate_deploy_qml_app_script(
TARGET ${PROJECT}
OUTPUT_SCRIPT QT_DEPLOY_SCRIPT
NO_UNSUPPORTED_PLATFORM_ERROR
DEPLOY_TOOL_OPTIONS ${deploy_tool_options}
)
install(SCRIPT ${QT_DEPLOY_SCRIPT}
COMPONENT AmneziaVPN
)
endif()
+2 -12
View File
@@ -109,16 +109,6 @@ void AmneziaApplication::init()
// install filter on main window // install filter on main window
if (auto win = qobject_cast<QQuickWindow*>(obj)) { if (auto win = qobject_cast<QQuickWindow*>(obj)) {
win->installEventFilter(this); win->installEventFilter(this);
#ifdef Q_OS_ANDROID
QObject::connect(win, &QQuickWindow::sceneGraphError,
[](QQuickWindow::SceneGraphError, const QString &msg) {
qWarning() << "Scene graph error (suppressed):" << msg;
});
// Keep graphics context alive across hide/show cycles to avoid
// eglSwapBuffers/makeCurrent being called on a context Android has reclaimed.
win->setPersistentSceneGraph(true);
win->setPersistentGraphics(true);
#endif
win->show(); win->show();
} }
}, },
@@ -260,7 +250,7 @@ bool AmneziaApplication::parseCommands()
return true; return true;
} }
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
void AmneziaApplication::startLocalServer() { void AmneziaApplication::startLocalServer() {
const QString serverName("AmneziaVPNInstance"); const QString serverName("AmneziaVPNInstance");
QLocalServer::removeServer(serverName); QLocalServer::removeServer(serverName);
@@ -281,7 +271,7 @@ void AmneziaApplication::startLocalServer() {
bool AmneziaApplication::eventFilter(QObject *watched, QEvent *event) bool AmneziaApplication::eventFilter(QObject *watched, QEvent *event)
{ {
if (event->type() == QEvent::Close) { if (event->type() == QEvent::Close) {
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS)
quit(); quit();
#else #else
if (m_forceQuit) { if (m_forceQuit) {
+3 -3
View File
@@ -6,7 +6,7 @@
#include <QQmlApplicationEngine> #include <QQmlApplicationEngine>
#include <QQmlContext> #include <QQmlContext>
#include <QThread> #include <QThread>
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS)
#include <QGuiApplication> #include <QGuiApplication>
#else #else
#include <QApplication> #include <QApplication>
@@ -19,7 +19,7 @@
#define amnApp (static_cast<AmneziaApplication *>(QCoreApplication::instance())) #define amnApp (static_cast<AmneziaApplication *>(QCoreApplication::instance()))
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS)
#define AMNEZIA_BASE_CLASS QGuiApplication #define AMNEZIA_BASE_CLASS QGuiApplication
#else #else
#define AMNEZIA_BASE_CLASS QApplication #define AMNEZIA_BASE_CLASS QApplication
@@ -37,7 +37,7 @@ public:
void loadFonts(); void loadFonts();
bool parseCommands(); bool parseCommands();
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
void startLocalServer(); void startLocalServer();
#endif #endif
+2 -2
View File
@@ -1,11 +1,11 @@
[versions] [versions]
agp = "8.5.2" agp = "8.6.1"
kotlin = "1.9.24" kotlin = "1.9.24"
androidx-core = "1.13.1" androidx-core = "1.13.1"
androidx-activity = "1.9.1" androidx-activity = "1.9.1"
androidx-annotation = "1.8.2" androidx-annotation = "1.8.2"
androidx-biometric = "1.2.0-alpha05" androidx-biometric = "1.2.0-alpha05"
androidx-camera = "1.3.4" androidx-camera = "1.5.3"
androidx-fragment = "1.8.2" androidx-fragment = "1.8.2"
androidx-security-crypto = "1.1.0-alpha06" androidx-security-crypto = "1.1.0-alpha06"
androidx-datastore = "1.1.1" androidx-datastore = "1.1.1"
-3
View File
@@ -24,8 +24,5 @@
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string> <string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
<string name="openNotificationSettings">Открыть настройки уведомлений</string> <string name="openNotificationSettings">Открыть настройки уведомлений</string>
<string name="vpnStateEventChannelName">Уведомления о VPN</string>
<string name="vpnStateEventChannelDescription">Краткие оповещения при подключении и отключении VPN</string>
<string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string> <string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string>
</resources> </resources>
-3
View File
@@ -24,8 +24,5 @@
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string> <string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string>
<string name="openNotificationSettings">Open notification settings</string> <string name="openNotificationSettings">Open notification settings</string>
<string name="vpnStateEventChannelName">VPN connection alerts</string>
<string name="vpnStateEventChannelDescription">Brief alerts when VPN connects or disconnects</string>
<string name="tvNoFileBrowser">Please install a file management utility to browse files</string> <string name="tvNoFileBrowser">Please install a file management utility to browse files</string>
</resources> </resources>
@@ -75,8 +75,6 @@ private const val OPEN_FILE_ACTION_CODE = 3
private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4 private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED" private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
private const val KEY_PENDING_OPEN_FILE_URI = "pending_open_file_uri"
class AmneziaActivity : QtActivity() { class AmneziaActivity : QtActivity() {
@@ -92,12 +90,10 @@ class AmneziaActivity : QtActivity() {
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>() private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>() private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
private var isActivityResumed = false private var isActivityResumed = false
private var hasWindowFocus = false private var hasWindowFocus = false
private val resumeHandler = Handler(Looper.getMainLooper()) private val resumeHandler = Handler(Looper.getMainLooper())
private var pendingOpenFileUri: String? = null
private var openFileDeliveryScheduled = false
private val vpnServiceEventHandler: Handler by lazy(NONE) { private val vpnServiceEventHandler: Handler by lazy(NONE) {
object : Handler(Looper.getMainLooper()) { object : Handler(Looper.getMainLooper()) {
@@ -200,24 +196,14 @@ class AmneziaActivity : QtActivity() {
doBindService() doBindService()
} }
) )
pendingOpenFileUri = savedInstanceState?.getString(KEY_PENDING_OPEN_FILE_URI)
openFileDeliveryScheduled = false
registerBroadcastReceivers() registerBroadcastReceivers()
intent?.let(::processIntent) intent?.let(::processIntent)
runBlocking { vpnProto = proto.await() } runBlocking { vpnProto = proto.await() }
} }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
pendingOpenFileUri?.let { outState.putString(KEY_PENDING_OPEN_FILE_URI, it) }
}
private fun loadLibs() { private fun loadLibs() {
listOf( listOf(
"rsapss", "rsapss"
"crypto_3",
"ssl_3",
"ssh"
).forEach { ).forEach {
loadSharedLibrary(this.applicationContext, it) loadSharedLibrary(this.applicationContext, it)
} }
@@ -281,7 +267,6 @@ class AmneziaActivity : QtActivity() {
hasWindowFocus = false hasWindowFocus = false
// Cancel all pending operations when activity stops // Cancel all pending operations when activity stops
resumeHandler.removeCallbacksAndMessages(null) resumeHandler.removeCallbacksAndMessages(null)
openFileDeliveryScheduled = false
Log.d(TAG, "Stop Amnezia activity") Log.d(TAG, "Stop Amnezia activity")
doUnbindService() doUnbindService()
mainScope.launch { mainScope.launch {
@@ -295,55 +280,43 @@ class AmneziaActivity : QtActivity() {
super.onWindowFocusChanged(hasFocus) super.onWindowFocusChanged(hasFocus)
hasWindowFocus = hasFocus hasWindowFocus = hasFocus
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus") Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
// Cancel pending operations if window loses focus
if (!hasFocus) { if (!hasFocus) {
// Cancel pending operations if window loses focus
resumeHandler.removeCallbacksAndMessages(null) resumeHandler.removeCallbacksAndMessages(null)
} else if (isActivityResumed && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
window.decorView.apply {
invalidate()
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(1f, 1f)
}
}, 50)
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(2f, 2f)
requestLayout()
invalidate()
}
}, 150)
}
} }
} }
override fun dispatchKeyEvent(event: KeyEvent): Boolean { override fun dispatchKeyEvent(event: KeyEvent): Boolean {
val deviceId = event.deviceId
val keyCode = event.keyCode val keyCode = event.keyCode
val pressed = event.action == KeyEvent.ACTION_DOWN val pressed = event.action == KeyEvent.ACTION_DOWN
val source = event.source
when (keyCode) { if (deviceId < 0 && pressed) {
KeyEvent.KEYCODE_BUTTON_A, when (keyCode) {
KeyEvent.KEYCODE_BUTTON_B, KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_BUTTON_X, KeyEvent.KEYCODE_BUTTON_B,
KeyEvent.KEYCODE_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_X,
KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_BUTTON_Y,
KeyEvent.KEYCODE_BUTTON_SELECT -> { KeyEvent.KEYCODE_BUTTON_START,
nativeGamepadKeyEvent(0, keyCode, pressed) KeyEvent.KEYCODE_BUTTON_SELECT,
KeyEvent.KEYCODE_DPAD_CENTER -> {
nativeGamepadKeyEvent(0, keyCode, true)
nativeGamepadKeyEvent(0, keyCode, false)
return true return true
}
} }
KeyEvent.KEYCODE_DPAD_CENTER, }
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_DPAD_DOWN, // Real gamepad events (deviceId >= 0)
KeyEvent.KEYCODE_DPAD_LEFT, if (deviceId >= 0) {
KeyEvent.KEYCODE_DPAD_RIGHT -> { val isGamepad = (source and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD
val syntheticKeyCode = if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) KeyEvent.KEYCODE_ENTER else keyCode val isJoystick = (source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
val synthetic = KeyEvent( val isDpad = (source and InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD
event.downTime, event.eventTime, event.action, syntheticKeyCode, if (isGamepad || isJoystick || isDpad) {
event.repeatCount, event.metaState, -1, event.scanCode, nativeGamepadKeyEvent(deviceId, keyCode, pressed)
event.flags, InputDevice.SOURCE_KEYBOARD return true
)
return super.dispatchKeyEvent(synthetic)
} }
} }
@@ -353,18 +326,10 @@ class AmneziaActivity : QtActivity() {
private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean) private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean)
override fun onPause() { override fun onPause() {
// Notify Qt to stop rendering BEFORE super.onPause() destroys the EGL surface.
// Using a coroutine here would be too late — the surface is gone by the time
// the coroutine runs. A direct synchronous call gives Qt's render thread the
// best chance to process visible=false before surface destruction.
if (qtInitialized.isCompleted) {
QtAndroidController.onActivityPaused()
}
super.onPause() super.onPause()
isActivityResumed = false isActivityResumed = false
// Cancel all pending operations when activity pauses // Cancel all pending operations when activity pauses
resumeHandler.removeCallbacksAndMessages(null) resumeHandler.removeCallbacksAndMessages(null)
openFileDeliveryScheduled = false
Log.d(TAG, "Pause Amnezia activity") Log.d(TAG, "Pause Amnezia activity")
} }
@@ -372,24 +337,6 @@ class AmneziaActivity : QtActivity() {
super.onResume() super.onResume()
isActivityResumed = true isActivityResumed = true
Log.d(TAG, "Resume Amnezia activity") Log.d(TAG, "Resume Amnezia activity")
if (qtInitialized.isCompleted) {
QtAndroidController.onActivityResumed()
}
if (pendingOpenFileUri != null && !openFileDeliveryScheduled) {
val uri = pendingOpenFileUri!!
openFileDeliveryScheduled = true
resumeHandler.postDelayed({
if (!isFinishing && !isDestroyed) {
pendingOpenFileUri = null
openFileDeliveryScheduled = false
mainScope.launch {
qtInitialized.await()
QtAndroidController.onFileOpened(uri)
}
}
}, OPEN_FILE_AFTER_RESUME_DELAY_MS)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
window.decorView.apply { window.decorView.apply {
@@ -401,13 +348,13 @@ class AmneziaActivity : QtActivity() {
sendTouch(1f, 1f) sendTouch(1f, 1f)
} }
}, 100) }, 100)
resumeHandler.postDelayed({ resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) { if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(2f, 2f) sendTouch(2f, 2f)
} }
}, 200) }, 200)
resumeHandler.postDelayed({ resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) { if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
requestLayout() requestLayout()
@@ -453,25 +400,25 @@ class AmneziaActivity : QtActivity() {
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, windowInsets -> ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, windowInsets ->
val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime()) val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
val imeVisible = windowInsets.isVisible(WindowInsetsCompat.Type.ime()) val imeVisible = windowInsets.isVisible(WindowInsetsCompat.Type.ime())
val imeHeight = if (imeVisible) imeInsets.bottom else 0 val imeHeight = if (imeVisible) imeInsets.bottom else 0
val density = resources.displayMetrics.density val density = resources.displayMetrics.density
val imeHeightDp = (imeHeight / density).toInt() val imeHeightDp = (imeHeight / density).toInt()
// Also track system bars (navigation bar, status bar) changes // Also track system bars (navigation bar, status bar) changes
val systemBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val systemBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val navBarHeight = systemBarsInsets.bottom val navBarHeight = systemBarsInsets.bottom
val navBarHeightDp = (navBarHeight / density).toInt() val navBarHeightDp = (navBarHeight / density).toInt()
val statusBarHeight = systemBarsInsets.top val statusBarHeight = systemBarsInsets.top
val statusBarHeightDp = (statusBarHeight / density).toInt() val statusBarHeightDp = (statusBarHeight / density).toInt()
mainScope.launch { mainScope.launch {
qtInitialized.await() qtInitialized.await()
QtAndroidController.onImeInsetsChanged(imeHeightDp) QtAndroidController.onImeInsetsChanged(imeHeightDp)
QtAndroidController.onSystemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp) QtAndroidController.onSystemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp)
} }
// Return windowInsets instead of CONSUMED to allow proper handling // Return windowInsets instead of CONSUMED to allow proper handling
windowInsets windowInsets
} }
@@ -807,13 +754,9 @@ class AmneziaActivity : QtActivity() {
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION) grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}?.toString() ?: "" }?.toString() ?: ""
Log.v(TAG, "Open file: $uri") Log.v(TAG, "Open file: $uri")
if (uri.isNotEmpty()) { mainScope.launch {
pendingOpenFileUri = uri qtInitialized.await()
} else { QtAndroidController.onFileOpened(uri)
mainScope.launch {
qtInitialized.await()
QtAndroidController.onFileOpened(uri)
}
} }
} }
)) ))
@@ -842,7 +785,7 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused") @Suppress("unused")
fun getFd(fileName: String): Int { fun getFd(fileName: String): Int {
Log.v(TAG, "Get fd for $fileName") Log.v(TAG, "Get fd for $fileName")
return blockingCall(Dispatchers.IO) { return blockingCall {
try { try {
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r") pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
pfd?.fd ?: -1 pfd?.fd ?: -1
@@ -1006,12 +949,6 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused") @Suppress("unused")
fun isNotificationPermissionGranted(): Boolean = applicationContext.isNotificationPermissionGranted() fun isNotificationPermissionGranted(): Boolean = applicationContext.isNotificationPermissionGranted()
/** Called from Qt (AndroidController) — CLI-570 VPN connect/disconnect heads-up. */
@Suppress("unused")
fun showVpnStateNotification(title: String, message: String) {
ServiceNotification.showVpnStateEvent(applicationContext, title, message)
}
@Suppress("unused") @Suppress("unused")
fun requestNotificationPermission() { fun requestNotificationPermission() {
val shouldShowPreRequest = shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) val shouldShowPreRequest = shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
@@ -26,9 +26,6 @@ private const val OLD_NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notific
private const val NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notifications" private const val NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notifications"
const val NOTIFICATION_ID = 1337 const val NOTIFICATION_ID = 1337
const val VPN_STATE_EVENT_NOTIFICATION_ID = 1338
private const val VPN_STATE_EVENT_CHANNEL_ID = "org.amnezia.vpn.vpn_state_events"
private const val GET_ACTIVITY_REQUEST_CODE = 0 private const val GET_ACTIVITY_REQUEST_CODE = 0
private const val CONNECT_REQUEST_CODE = 1 private const val CONNECT_REQUEST_CODE = 1
private const val DISCONNECT_REQUEST_CODE = 2 private const val DISCONNECT_REQUEST_CODE = 2
@@ -165,42 +162,8 @@ class ServiceNotification(private val context: Context) {
.setDescription(context.resources.getString(R.string.notificationChannelDescription)) .setDescription(context.resources.getString(R.string.notificationChannelDescription))
.build() .build()
) )
createNotificationChannel(
Builder(VPN_STATE_EVENT_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setShowBadge(false)
.setSound(null, null)
.setVibrationEnabled(false)
.setLightsEnabled(false)
.setName(context.getString(R.string.vpnStateEventChannelName))
.setDescription(context.getString(R.string.vpnStateEventChannelDescription))
.build()
)
} }
} }
/** Brief alert when VPN connects or disconnects (invoked from Qt via AmneziaActivity). */
fun showVpnStateEvent(context: Context, title: String, message: String) {
if (!context.isNotificationPermissionGranted()) return
val nm = NotificationManagerCompat.from(context)
val notification = NotificationCompat.Builder(context, VPN_STATE_EVENT_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_amnezia_round)
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setContentIntent(
PendingIntent.getActivity(
context,
GET_ACTIVITY_REQUEST_CODE,
Intent(context, AmneziaActivity::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
.build()
nm.notify(VPN_STATE_EVENT_NOTIFICATION_ID, notification)
}
} }
} }
@@ -33,10 +33,7 @@ class TvFilePicker : ComponentActivity() {
return intent return intent
} }
}) { }) {
setResult(RESULT_OK, Intent().apply { setResult(RESULT_OK, Intent().apply { data = it })
data = it
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
})
finish() finish()
} }
@@ -31,7 +31,4 @@ object QtAndroidController {
external fun onImeInsetsChanged(heightDp: Int) external fun onImeInsetsChanged(heightDp: Int)
external fun onSystemBarsInsetsChanged(navBarHeightDp: Int, statusBarHeightDp: Int) external fun onSystemBarsInsetsChanged(navBarHeightDp: Int, statusBarHeightDp: Int)
external fun onActivityPaused()
external fun onActivityResumed()
} }
+12 -80
View File
@@ -1,88 +1,19 @@
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..) set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
set(AMNEZIA_THIRDPARTY_ROOT "${CLIENT_ROOT_DIR}/3rd" CACHE PATH "Path to Amnezia client/3rd sources")
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/Modules;${CMAKE_MODULE_PATH}") set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/Modules;${CMAKE_MODULE_PATH}")
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel) add_subdirectory(${AMNEZIA_THIRDPARTY_ROOT}/SortFilterProxyModel ${CMAKE_CURRENT_BINARY_DIR}/3rd/SortFilterProxyModel)
set(LIBS ${LIBS} SortFilterProxyModel) set(LIBS ${LIBS} SortFilterProxyModel)
include(${CLIENT_ROOT_DIR}/cmake/QSimpleCrypto.cmake) include(${CLIENT_ROOT_DIR}/cmake/QSimpleCrypto.cmake)
include(${CLIENT_ROOT_DIR}/3rd/qrcodegen/qrcodegen.cmake) include(${AMNEZIA_THIRDPARTY_ROOT}/qrcodegen/qrcodegen.cmake)
set(LIBSSH_ROOT_DIR "${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/libssh/")
set(OPENSSL_ROOT_DIR "${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/openssl/")
set(OPENSSL_LIBRARIES_DIR "${OPENSSL_ROOT_DIR}/lib")
if(WIN32)
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/windows/include")
if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8")
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/windows/x86_64/ssh.lib")
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/windows/x86_64")
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/windows/win64/libssl.lib")
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/windows/win64/libcrypto.lib")
else()
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/windows/x86/ssh.lib")
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/windows/x86")
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/windows/win32/libssl.lib")
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/windows/win32/libcrypto.lib")
endif()
elseif(APPLE AND NOT IOS)
if(MACOS_NE)
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/macos/universal2/libssh.a")
set(ZLIB_LIB_PATH "${LIBSSH_ROOT_DIR}/macos/universal2/libz.a")
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/macos/universal2")
else()
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/macos/x86_64/libssh.a")
set(ZLIB_LIB_PATH "${LIBSSH_ROOT_DIR}/macos/x86_64/libz.a")
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/macos/x86_64")
endif()
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/macos/include")
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/macos/lib/libssl.a")
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/macos/lib/libcrypto.a")
elseif(IOS)
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/ios/arm64")
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/ios/arm64/libssh.a")
set(ZLIB_LIB_PATH "${LIBSSH_ROOT_DIR}/ios/arm64/libz.a")
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/ios/iphone/include")
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/ios/iphone/lib/libssl.a")
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/ios/iphone/lib/libcrypto.a")
elseif(ANDROID)
set(abi ${CMAKE_ANDROID_ARCH_ABI})
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/android/${abi}")
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/android/${abi}/libssh.so")
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/android/include")
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/android/${abi}/libssl.a")
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/android/${abi}/libcrypto.a")
set(OPENSSL_LIBRARIES_DIR "${OPENSSL_ROOT_DIR}/android/${abi}")
elseif(LINUX)
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/linux/x86_64")
set(ZLIB_LIB_PATH "${LIBSSH_ROOT_DIR}/linux/x86_64/libz.a")
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/linux/x86_64/libssh.a")
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/linux/include")
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/linux/x86_64/libssl.a")
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/linux/x86_64/libcrypto.a")
endif()
file(COPY ${OPENSSL_LIB_SSL_PATH} ${OPENSSL_LIB_CRYPTO_PATH}
DESTINATION ${OPENSSL_LIBRARIES_DIR})
set(OPENSSL_USE_STATIC_LIBS TRUE)
set(LIBS ${LIBS}
${LIBSSH_LIB_PATH}
${ZLIB_LIB_PATH}
)
set(LIBS ${LIBS}
${OPENSSL_LIB_SSL_PATH}
${OPENSSL_LIB_CRYPTO_PATH}
)
add_compile_definitions(_WINSOCKAPI_) add_compile_definitions(_WINSOCKAPI_)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(BUILD_WITH_QT6 ON) set(BUILD_WITH_QT6 ON)
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain) add_subdirectory(${AMNEZIA_THIRDPARTY_ROOT}/qtkeychain ${CMAKE_CURRENT_BINARY_DIR}/3rd/qtkeychain EXCLUDE_FROM_ALL)
if(ANDROID) if(ANDROID)
# Use qtgamepad from amnezia-vpn/qtgamepad repository # Use qtgamepad from amnezia-vpn/qtgamepad repository
@@ -106,12 +37,13 @@ endif()
set(LIBS ${LIBS} qt6keychain) set(LIBS ${LIBS} qt6keychain)
include_directories( include_directories(
${OPENSSL_INCLUDE_DIR} ${AMNEZIA_THIRDPARTY_ROOT}/QSimpleCrypto/src/include
${LIBSSH_INCLUDE_DIR}/include ${AMNEZIA_THIRDPARTY_ROOT}/qtkeychain/qtkeychain
${LIBSSH_ROOT_DIR}/include
${CLIENT_ROOT_DIR}/3rd/libssh/include
${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src/include
${CLIENT_ROOT_DIR}/3rd/qtkeychain/qtkeychain
${CMAKE_CURRENT_BINARY_DIR}/3rd/qtkeychain ${CMAKE_CURRENT_BINARY_DIR}/3rd/qtkeychain
${CMAKE_CURRENT_BINARY_DIR}/3rd/libssh/include
) )
find_package(OpenSSL REQUIRED)
list(APPEND LIBS OpenSSL::SSL OpenSSL::Crypto)
find_package(libssh REQUIRED)
list(APPEND LIBS ssh::ssh)
+2 -1
View File
@@ -1,5 +1,6 @@
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..) set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
set(QSIMPLECRYPTO_DIR ${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src) set(AMNEZIA_THIRDPARTY_ROOT "${CLIENT_ROOT_DIR}/3rd" CACHE PATH "Path to Amnezia client/3rd sources")
set(QSIMPLECRYPTO_DIR ${AMNEZIA_THIRDPARTY_ROOT}/QSimpleCrypto/src)
include_directories(${QSIMPLECRYPTO_DIR}) include_directories(${QSIMPLECRYPTO_DIR})
+10 -14
View File
@@ -42,18 +42,14 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.cpp ${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.cpp
) )
foreach(abi IN ITEMS ${QT_ANDROID_ABIS})
set_property(TARGET ${PROJECT} PROPERTY QT_ANDROID_EXTRA_LIBS
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/amneziawg/android/${abi}/libwg-go.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/libck-ovpn-plugin.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/libovpn3.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/libovpnutil.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/librsapss.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openssl/android/${abi}/libcrypto_3.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openssl/android/${abi}/libssl_3.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/libssh/android/${abi}/libssh.so
)
endforeach()
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/xray/android/libxray.aar find_package(awg-android REQUIRED)
DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray) set(LIBS ${LIBS} amnezia::awg-android)
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${AMNEZIA_ANDROID_LIBWG_PATH} ${AMNEZIA_ANDROID_LIBWG_QUICK_PATH})
find_package(amnezia-libxray REQUIRED)
file(COPY ${AMNEZIA_LIBXRAY_PATH} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray)
find_package(openvpn-pt-android REQUIRED)
set(LIBS ${LIBS} amnezia::openvpn-pt-android)
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH})
+3 -1
View File
@@ -39,5 +39,7 @@ while(IOS_TARGETS)
set_target_properties(${TARGET_NAME} PROPERTIES set_target_properties(${TARGET_NAME} PROPERTIES
XCODE_ATTRIBUTE_ARCHS[sdk=iphoneos*] "arm64" XCODE_ATTRIBUTE_ARCHS[sdk=iphoneos*] "arm64"
XCODE_ATTRIBUTE_ARCHS[sdk=iphonesimulator*] "x86_64" XCODE_ATTRIBUTE_ARCHS[sdk=iphonesimulator*] "x86_64"
XCODE_ATTRIBUTE_ARCHS[sdk=appletvos*] "arm64"
XCODE_ATTRIBUTE_ARCHS[sdk=appletvsimulator*] "arm64"
) )
endwhile() endwhile()
+136 -32
View File
@@ -1,7 +1,19 @@
message("Client iOS build") message("Client iOS build")
set(CMAKE_OSX_DEPLOYMENT_TARGET 13.0)
set(APPLE_PROJECT_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) set(APPLE_PROJECT_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(AMNEZIA_IOS_APPLETV ${AMNEZIA_IOS_ENABLE_APPLETV_TARGET})
if(AMNEZIA_IOS_APPLETV)
message("Apple TV target mode is ON")
set(CMAKE_OSX_DEPLOYMENT_TARGET 17.0)
set(QT_NO_SET_DEFAULT_IOS_LAUNCH_SCREEN TRUE)
set(QT_NO_ADD_IOS_LAUNCH_SCREEN_TO_BUNDLE TRUE)
set(IOS_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Info-tvOS.plist.in)
set(IOS_LAUNCHSCREEN_STORYBOARD ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/tvOS/AmneziaVPNLaunchScreen.storyboard)
else()
message("Apple TV target mode is OFF")
set(IOS_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Info.plist.in)
set(IOS_LAUNCHSCREEN_STORYBOARD ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/AmneziaVPNLaunchScreen.storyboard)
endif()
enable_language(OBJC) enable_language(OBJC)
enable_language(OBJCXX) enable_language(OBJCXX)
@@ -10,13 +22,23 @@ enable_language(Swift)
find_package(Qt6 REQUIRED COMPONENTS ShaderTools) find_package(Qt6 REQUIRED COMPONENTS ShaderTools)
set(LIBS ${LIBS} Qt6::ShaderTools) set(LIBS ${LIBS} Qt6::ShaderTools)
find_library(FW_AUTHENTICATIONSERVICES AuthenticationServices) if(AMNEZIA_IOS_APPLETV)
find_library(FW_UIKIT UIKit) # Use framework linker flags directly for tvOS to avoid iPhoneOS SDK absolute paths.
find_library(FW_AVFOUNDATION AVFoundation) set(FW_AUTHENTICATIONSERVICES "-framework AuthenticationServices")
find_library(FW_FOUNDATION Foundation) set(FW_UIKIT "-framework UIKit")
find_library(FW_STOREKIT StoreKit) set(FW_AVFOUNDATION "-framework AVFoundation")
find_library(FW_USERNOTIFICATIONS UserNotifications) set(FW_FOUNDATION "-framework Foundation")
find_library(FW_NETWORKEXTENSION NetworkExtension) set(FW_STOREKIT "-framework StoreKit")
set(FW_USERNOTIFICATIONS "-framework UserNotifications")
else()
find_library(FW_AUTHENTICATIONSERVICES AuthenticationServices)
find_library(FW_UIKIT UIKit)
find_library(FW_AVFOUNDATION AVFoundation)
find_library(FW_FOUNDATION Foundation)
find_library(FW_STOREKIT StoreKit)
find_library(FW_USERNOTIFICATIONS UserNotifications)
find_library(FW_NETWORKEXTENSION NetworkExtension)
endif()
set(LIBS ${LIBS} set(LIBS ${LIBS}
${FW_AUTHENTICATIONSERVICES} ${FW_AUTHENTICATIONSERVICES}
@@ -25,9 +47,12 @@ set(LIBS ${LIBS}
${FW_FOUNDATION} ${FW_FOUNDATION}
${FW_STOREKIT} ${FW_STOREKIT}
${FW_USERNOTIFICATIONS} ${FW_USERNOTIFICATIONS}
${FW_NETWORKEXTENSION}
) )
if(NOT AMNEZIA_IOS_APPLETV)
set(LIBS ${LIBS} ${FW_NETWORKEXTENSION})
endif()
set(HEADERS ${HEADERS} set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
@@ -57,7 +82,7 @@ target_include_directories(${PROJECT} PRIVATE ${Qt6Gui_PRIVATE_INCLUDE_DIRS})
set_target_properties(${PROJECT} PROPERTIES set_target_properties(${PROJECT} PROPERTIES
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Info.plist.in MACOSX_BUNDLE_INFO_PLIST ${IOS_INFO_PLIST}
MACOSX_BUNDLE_ICON_FILE "AppIcon" MACOSX_BUNDLE_ICON_FILE "AppIcon"
MACOSX_BUNDLE_INFO_STRING "AmneziaVPN" MACOSX_BUNDLE_INFO_STRING "AmneziaVPN"
MACOSX_BUNDLE_BUNDLE_NAME "AmneziaVPN" MACOSX_BUNDLE_BUNDLE_NAME "AmneziaVPN"
@@ -66,7 +91,6 @@ set_target_properties(${PROJECT} PROPERTIES
MACOSX_BUNDLE_LONG_VERSION_STRING "${APPLE_PROJECT_VERSION}-${CMAKE_PROJECT_VERSION_TWEAK}" MACOSX_BUNDLE_LONG_VERSION_STRING "${APPLE_PROJECT_VERSION}-${CMAKE_PROJECT_VERSION_TWEAK}"
MACOSX_BUNDLE_SHORT_VERSION_STRING "${APPLE_PROJECT_VERSION}" MACOSX_BUNDLE_SHORT_VERSION_STRING "${APPLE_PROJECT_VERSION}"
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}" XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}"
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/ios/app/main.entitlements"
XCODE_ATTRIBUTE_MARKETING_VERSION "${APPLE_PROJECT_VERSION}" XCODE_ATTRIBUTE_MARKETING_VERSION "${APPLE_PROJECT_VERSION}"
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}" XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPN" XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPN"
@@ -74,13 +98,36 @@ set_target_properties(${PROJECT} PROPERTIES
XCODE_GENERATE_SCHEME TRUE XCODE_GENERATE_SCHEME TRUE
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO" XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon" XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY ON XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY ON
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks" XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks"
XCODE_EMBED_APP_EXTENSIONS networkextension XCODE_EMBED_APP_EXTENSIONS networkextension
) )
if(AMNEZIA_IOS_APPLETV)
set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_SUPPORTED_PLATFORMS "appletvos appletvsimulator"
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "3"
XCODE_ATTRIBUTE_TVOS_DEPLOYMENT_TARGET "${CMAKE_OSX_DEPLOYMENT_TARGET}"
XCODE_ATTRIBUTE_SDKROOT "appletvos"
XCODE_ATTRIBUTE_SDKROOT[sdk=appletvos*] "appletvos"
XCODE_ATTRIBUTE_SDKROOT[sdk=appletvsimulator*] "appletvsimulator"
XCODE_ATTRIBUTE_LIBRARY_SEARCH_PATHS "$(inherited) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
XCODE_ATTRIBUTE_LIBRARY_SEARCH_PATHS[sdk=appletvos*] "$(inherited) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
XCODE_ATTRIBUTE_LIBRARY_SEARCH_PATHS[sdk=appletvsimulator*] "$(inherited) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
XCODE_ATTRIBUTE_EXCLUDED_LIBRARY_SEARCH_PATHS "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS*.sdk/usr/lib/swift"
XCODE_ATTRIBUTE_EXCLUDED_FRAMEWORK_SEARCH_PATHS "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS*.sdk/System/Library/Frameworks"
)
set_target_properties(${PROJECT} PROPERTIES
QT_IOS_PERMISSIONS ""
)
else()
set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/ios/app/main.entitlements"
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
)
endif()
if(DEFINED DEPLOY) if(DEFINED DEPLOY)
set_target_properties(${PROJECT} PROPERTIES set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution" XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
@@ -111,7 +158,61 @@ target_compile_options(${PROJECT} PRIVATE
-DVPN_NE_BUNDLEID=\"${BUILD_IOS_APP_IDENTIFIER}.network-extension\" -DVPN_NE_BUNDLEID=\"${BUILD_IOS_APP_IDENTIFIER}.network-extension\"
) )
set(WG_APPLE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/3rd/amneziawg-apple/Sources) if(AMNEZIA_IOS_APPLETV)
# qscnetworkreachability plugin links IOKit, which is unavailable on tvOS.
qt_import_plugins(${PROJECT}
NO_DEFAULT
INCLUDE
QIOSIntegrationPlugin
QJpegPlugin
QSvgPlugin
QGifPlugin
QICOPlugin
QSvgIconPlugin
QSecureTransportBackendPlugin
EXCLUDE
QSCNetworkReachabilityNetworkInformationPlugin
QDarwinCameraPermissionPlugin
)
# Static tvOS Qt build doesn't auto-link these plugin archives into the
# Xcode target, but the app entry point lives in QIOSIntegrationPlugin.
set(_amnezia_tvos_static_plugins
Qt6::QIOSIntegrationPlugin
Qt6::QIOSIntegrationPlugin_init
Qt6::QJpegPlugin
Qt6::QJpegPlugin_init
Qt6::QSvgPlugin
Qt6::QSvgPlugin_init
Qt6::QGifPlugin
Qt6::QGifPlugin_init
Qt6::QICOPlugin
Qt6::QICOPlugin_init
Qt6::QSvgIconPlugin
Qt6::QSvgIconPlugin_init
Qt6::QSecureTransportBackendPlugin
Qt6::QSecureTransportBackendPlugin_init
)
foreach(_amnezia_tvos_static_plugin IN LISTS _amnezia_tvos_static_plugins)
if(TARGET ${_amnezia_tvos_static_plugin})
target_link_libraries(${PROJECT} PRIVATE ${_amnezia_tvos_static_plugin})
endif()
endforeach()
unset(_amnezia_tvos_static_plugin)
unset(_amnezia_tvos_static_plugins)
# Qt 6.9.2 iOS package links IOKit via Qt6::Core interface, but tvOS SDK
# does not provide IOKit. Strip this single framework for Apple TV builds.
get_target_property(_qtcore_iface_libs Qt6::Core INTERFACE_LINK_LIBRARIES)
if(_qtcore_iface_libs)
string(REPLACE "-framework IOKit;" "" _qtcore_iface_libs "${_qtcore_iface_libs}")
string(REPLACE ";-framework IOKit" "" _qtcore_iface_libs "${_qtcore_iface_libs}")
set_property(TARGET Qt6::Core PROPERTY INTERFACE_LINK_LIBRARIES "${_qtcore_iface_libs}")
endif()
endif()
set(AMNEZIA_THIRDPARTY_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rd" CACHE PATH "Path to Amnezia client/3rd sources")
set(WG_APPLE_SOURCE_DIR ${AMNEZIA_THIRDPARTY_ROOT}/amneziawg-apple/Sources)
target_sources(${PROJECT} PRIVATE target_sources(${PROJECT} PRIVATE
# ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosvpnprotocol.swift # ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosvpnprotocol.swift
@@ -121,28 +222,31 @@ target_sources(${PROJECT} PRIVATE
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift ${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift ${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift ${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
) )
target_sources(${PROJECT} PRIVATE if(IOS_LAUNCHSCREEN_STORYBOARD)
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/AmneziaVPNLaunchScreen.storyboard target_sources(${PROJECT} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets ${IOS_LAUNCHSCREEN_STORYBOARD}
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
) ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
)
set_property(TARGET ${PROJECT} APPEND PROPERTY RESOURCE set_property(TARGET ${PROJECT} APPEND PROPERTY RESOURCE
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/AmneziaVPNLaunchScreen.storyboard ${IOS_LAUNCHSCREEN_STORYBOARD}
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
) )
else()
target_sources(${PROJECT} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
)
set_property(TARGET ${PROJECT} APPEND PROPERTY RESOURCE
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
)
endif()
add_subdirectory(ios/networkextension) add_subdirectory(ios/networkextension)
add_dependencies(${PROJECT} networkextension) add_dependencies(${PROJECT} networkextension)
set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS
"${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework"
)
set(CMAKE_XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/)
target_link_libraries("networkextension" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework")
-3
View File
@@ -23,9 +23,6 @@ set_target_properties(${PROJECT} PROPERTIES
MACOSX_BUNDLE_SHORT_VERSION_STRING "${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}" MACOSX_BUNDLE_SHORT_VERSION_STRING "${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}"
MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}" MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
) )
set(CMAKE_OSX_ARCHITECTURES "x86_64" CACHE INTERNAL "" FORCE)
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.15)
set(HEADERS ${HEADERS} set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/ui/macos_util.h ${CMAKE_CURRENT_SOURCE_DIR}/ui/macos_util.h
-17
View File
@@ -1,7 +1,6 @@
message("Client ==> MacOS NE build") message("Client ==> MacOS NE build")
set_target_properties(${PROJECT} PROPERTIES MACOSX_BUNDLE TRUE) set_target_properties(${PROJECT} PROPERTIES MACOSX_BUNDLE TRUE)
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.15)
set(APPLE_PROJECT_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) set(APPLE_PROJECT_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
@@ -32,7 +31,6 @@ set(LIBS ${LIBS}
set(HEADERS ${HEADERS} set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macos_ne_vpn_notification.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
@@ -44,7 +42,6 @@ set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_contro
set(SOURCES ${SOURCES} set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macos_ne_vpn_notification.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
@@ -133,7 +130,6 @@ target_sources(${PROJECT} PRIVATE
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift ${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift ${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift ${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
) )
target_sources(${PROJECT} PRIVATE target_sources(${PROJECT} PRIVATE
@@ -155,19 +151,6 @@ message(${QtCore_location})
get_filename_component(QT_BIN_DIR_DETECTED "${QtCore_location}/../../../../../bin" ABSOLUTE) get_filename_component(QT_BIN_DIR_DETECTED "${QtCore_location}/../../../../../bin" ABSOLUTE)
set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS
"${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-macos/OpenVPNAdapter.framework"
)
set(CMAKE_XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-macos)
target_link_libraries("AmneziaVPNNetworkExtension" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-macos/OpenVPNAdapter.framework")
add_custom_command(TARGET ${PROJECT} POST_BUILD add_custom_command(TARGET ${PROJECT} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory
$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks
COMMAND /usr/bin/find "$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks/OpenVPNAdapter.framework" -name "*.sha256" -delete
COMMAND /usr/bin/codesign --force --sign "Apple Distribution: Privacy Technologies OU"
"$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks/OpenVPNAdapter.framework/Versions/Current/OpenVPNAdapter"
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Signing OpenVPNAdapter framework"
) )
+8 -14
View File
@@ -39,18 +39,15 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/mozilla/controllerimpl.h ${CLIENT_ROOT_DIR}/mozilla/controllerimpl.h
) )
if(NOT IOS AND NOT MACOS_NE) if(NOT IOS AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS")
set(HEADERS ${HEADERS} set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.h ${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.h
) )
endif() endif()
set(HEADERS ${HEADERS} if(NOT ANDROID)
${CLIENT_ROOT_DIR}/ui/notificationhandler.h
)
if(ANDROID)
set(HEADERS ${HEADERS} set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/platforms/android/android_notificationhandler.h ${CLIENT_ROOT_DIR}/ui/notificationhandler.h
) )
endif() endif()
@@ -92,14 +89,14 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/mozilla/shared/leakdetector.cpp ${CLIENT_ROOT_DIR}/mozilla/shared/leakdetector.cpp
) )
if(NOT IOS AND NOT MACOS_NE) if(NOT IOS AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS")
set(SOURCES ${SOURCES} set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.cpp ${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.cpp
) )
endif() endif()
# Include native macOS platform helpers (dock/status-item) # Include native macOS platform helpers (dock/status-item)
if(APPLE AND NOT IOS) if(APPLE AND NOT IOS AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS")
list(APPEND HEADERS list(APPEND HEADERS
${CLIENT_ROOT_DIR}/platforms/macos/macosutils.h ${CLIENT_ROOT_DIR}/platforms/macos/macosutils.h
${CLIENT_ROOT_DIR}/platforms/macos/macosstatusicon.h ${CLIENT_ROOT_DIR}/platforms/macos/macosstatusicon.h
@@ -112,12 +109,9 @@ if(APPLE AND NOT IOS)
) )
endif() endif()
set(SOURCES ${SOURCES} if(NOT ANDROID)
${CLIENT_ROOT_DIR}/ui/notificationhandler.cpp
)
if(ANDROID)
set(SOURCES ${SOURCES} set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/platforms/android/android_notificationhandler.cpp ${CLIENT_ROOT_DIR}/ui/notificationhandler.cpp
) )
endif() endif()
@@ -181,7 +175,7 @@ if(WIN32)
) )
endif() endif()
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID)) if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS") OR (LINUX AND NOT ANDROID))
message("Client desktop build") message("Client desktop build")
add_compile_definitions(AMNEZIA_DESKTOP) add_compile_definitions(AMNEZIA_DESKTOP)
@@ -1,17 +1,13 @@
#include "openvpn_configurator.h" #include "openvpn_configurator.h"
#include <QDebug> #include <QDebug>
#include <QCoreApplication>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QProcess> #include <QProcess>
#include <QString> #include <QString>
#include <QTemporaryDir> #include <QTemporaryDir>
#include <QTemporaryFile> #include <QTemporaryFile>
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
#include <QGuiApplication>
#else
#include <QApplication>
#endif
#include "core/networkUtilities.h" #include "core/networkUtilities.h"
#include "containers/containers_defs.h" #include "containers/containers_defs.h"
@@ -165,7 +161,7 @@ QString OpenVpnConfigurator::processConfigWithLocalSettings(const QPair<QString,
QString dnsConf = QString("\nscript-security 2\n" QString dnsConf = QString("\nscript-security 2\n"
"up %1/update-resolv-conf.sh\n" "up %1/update-resolv-conf.sh\n"
"down %1/update-resolv-conf.sh\n") "down %1/update-resolv-conf.sh\n")
.arg(qApp->applicationDirPath()); .arg(QCoreApplication::applicationDirPath());
config.append(dnsConf); config.append(dnsConf);
#endif #endif
+7 -11
View File
@@ -1,6 +1,7 @@
#include "ssh_configurator.h" #include "ssh_configurator.h"
#include <QDebug> #include <QDebug>
#include <QCoreApplication>
#include <QObject> #include <QObject>
#include <QProcess> #include <QProcess>
#include <QString> #include <QString>
@@ -8,11 +9,6 @@
#include <QTemporaryFile> #include <QTemporaryFile>
#include <QThread> #include <QThread>
#include <qtimer.h> #include <qtimer.h>
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(MACOS_NE)
#include <QGuiApplication>
#else
#include <QApplication>
#endif
#include "core/server_defs.h" #include "core/server_defs.h"
#include "utilities.h" #include "utilities.h"
@@ -24,7 +20,7 @@ SshConfigurator::SshConfigurator(std::shared_ptr<Settings> settings, const QShar
QString SshConfigurator::convertOpenSShKey(const QString &key) QString SshConfigurator::convertOpenSShKey(const QString &key)
{ {
#if !defined(Q_OS_IOS) && !defined(MACOS_NE) #if !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
QProcess p; QProcess p;
p.setProcessChannelMode(QProcess::MergedChannels); p.setProcessChannelMode(QProcess::MergedChannels);
@@ -70,13 +66,13 @@ QString SshConfigurator::convertOpenSShKey(const QString &key)
// DEAD CODE. // DEAD CODE.
void SshConfigurator::openSshTerminal(const ServerCredentials &credentials) void SshConfigurator::openSshTerminal(const ServerCredentials &credentials)
{ {
#if !defined(Q_OS_IOS) && !defined(MACOS_NE) #if !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
QProcess *p = new QProcess(); QProcess *p = new QProcess();
p->setProcessChannelMode(QProcess::SeparateChannels); p->setProcessChannelMode(QProcess::SeparateChannels);
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
p->setProcessEnvironment(prepareEnv()); p->setProcessEnvironment(prepareEnv());
p->setProgram(qApp->applicationDirPath() + "\\cygwin\\putty.exe"); p->setProgram(QCoreApplication::applicationDirPath() + "\\cygwin\\putty.exe");
if (credentials.secretData.contains("PRIVATE KEY")) { if (credentials.secretData.contains("PRIVATE KEY")) {
// todo: connect by key // todo: connect by key
@@ -100,10 +96,10 @@ QProcessEnvironment SshConfigurator::prepareEnv()
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
pathEnvVar.clear(); pathEnvVar.clear();
pathEnvVar.prepend(QDir::toNativeSeparators(QApplication::applicationDirPath()) + "\\cygwin;"); pathEnvVar.prepend(QDir::toNativeSeparators(QCoreApplication::applicationDirPath()) + "\\cygwin;");
pathEnvVar.prepend(QDir::toNativeSeparators(QApplication::applicationDirPath()) + "\\openvpn;"); pathEnvVar.prepend(QDir::toNativeSeparators(QCoreApplication::applicationDirPath()) + "\\openvpn;");
#elif defined(Q_OS_MACX) && !defined(MACOS_NE) #elif defined(Q_OS_MACX) && !defined(MACOS_NE)
pathEnvVar.prepend(QDir::toNativeSeparators(QApplication::applicationDirPath()) + "/Contents/MacOS"); pathEnvVar.prepend(QDir::toNativeSeparators(QCoreApplication::applicationDirPath()) + "/Contents/MacOS");
#endif #endif
env.insert("PATH", pathEnvVar); env.insert("PATH", pathEnvVar);
+1 -10
View File
@@ -10,10 +10,8 @@ namespace apiDefs
AmneziaFreeV3, AmneziaFreeV3,
AmneziaPremiumV1, AmneziaPremiumV1,
AmneziaPremiumV2, AmneziaPremiumV2,
AmneziaTrialV2,
SelfHosted, SelfHosted,
ExternalPremium, ExternalPremium
ExternalTrial
}; };
enum ConfigSource { enum ConfigSource {
@@ -34,7 +32,6 @@ namespace apiDefs
constexpr QLatin1String stackType("stack_type"); constexpr QLatin1String stackType("stack_type");
constexpr QLatin1String serviceType("service_type"); constexpr QLatin1String serviceType("service_type");
constexpr QLatin1String cliVersion("cli_version"); constexpr QLatin1String cliVersion("cli_version");
constexpr QLatin1String cliName("cli_name");
constexpr QLatin1String supportedProtocols("supported_protocols"); constexpr QLatin1String supportedProtocols("supported_protocols");
constexpr QLatin1String vpnKey("vpn_key"); constexpr QLatin1String vpnKey("vpn_key");
@@ -56,13 +53,8 @@ namespace apiDefs
constexpr QLatin1String activeDeviceCount("active_device_count"); constexpr QLatin1String activeDeviceCount("active_device_count");
constexpr QLatin1String maxDeviceCount("max_device_count"); constexpr QLatin1String maxDeviceCount("max_device_count");
constexpr QLatin1String subscriptionEndDate("subscription_end_date"); constexpr QLatin1String subscriptionEndDate("subscription_end_date");
constexpr QLatin1String subscriptionExpiredByServer("subscription_expired_by_server");
constexpr QLatin1String subscription("subscription");
constexpr QLatin1String endDate("end_date");
constexpr QLatin1String issuedConfigs("issued_configs"); constexpr QLatin1String issuedConfigs("issued_configs");
constexpr QLatin1String subscriptionDescription("subscription_description"); constexpr QLatin1String subscriptionDescription("subscription_description");
constexpr QLatin1String termsOfUseUrl("terms_of_use_url");
constexpr QLatin1String privacyPolicyUrl("privacy_policy_url");
constexpr QLatin1String supportInfo("support_info"); constexpr QLatin1String supportInfo("support_info");
constexpr QLatin1String email("email"); constexpr QLatin1String email("email");
@@ -77,7 +69,6 @@ namespace apiDefs
constexpr QLatin1String transactionId("transaction_id"); constexpr QLatin1String transactionId("transaction_id");
constexpr QLatin1String isTestPurchase("is_test_purchase"); constexpr QLatin1String isTestPurchase("is_test_purchase");
constexpr QLatin1String isInAppPurchase("is_in_app_purchase");
constexpr QLatin1String userCountryCode("user_country_code"); constexpr QLatin1String userCountryCode("user_country_code");
+22 -87
View File
@@ -3,32 +3,11 @@
#include <QDateTime> #include <QDateTime>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonValue>
namespace namespace
{ {
const QByteArray AMNEZIA_CONFIG_SIGNATURE = QByteArray::fromHex("000000ff"); const QByteArray AMNEZIA_CONFIG_SIGNATURE = QByteArray::fromHex("000000ff");
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
QDateTime subscriptionEndUtcFromString(const QString &subscriptionEndDate)
{
if (subscriptionEndDate.isEmpty()) {
return {};
}
QDateTime endDate = QDateTime::fromString(subscriptionEndDate, Qt::ISODateWithMs).toUTC();
if (!endDate.isValid()) {
endDate = QDateTime::fromString(subscriptionEndDate, Qt::ISODate).toUTC();
}
return endDate;
}
QString apiErrorMessageFromJson(const QJsonObject &jsonObj)
{
const QJsonValue value = jsonObj.value(QStringLiteral("message"));
return value.isString() ? value.toString().trimmed() : QString();
}
QString escapeUnicode(const QString &input) QString escapeUnicode(const QString &input)
{ {
QString output; QString output;
@@ -45,30 +24,9 @@ namespace
bool apiUtils::isSubscriptionExpired(const QString &subscriptionEndDate) bool apiUtils::isSubscriptionExpired(const QString &subscriptionEndDate)
{ {
if (subscriptionEndDate.isEmpty()) { QDateTime now = QDateTime::currentDateTimeUtc();
return false; QDateTime endDate = QDateTime::fromString(subscriptionEndDate, Qt::ISODateWithMs);
} return endDate < now;
const QDateTime endDate = subscriptionEndUtcFromString(subscriptionEndDate);
if (!endDate.isValid()) {
return false;
}
return endDate <= QDateTime::currentDateTimeUtc();
}
bool apiUtils::isSubscriptionExpiringSoon(const QString &subscriptionEndDate, int withinDays)
{
if (subscriptionEndDate.isEmpty()) {
return false;
}
const QDateTime endDate = subscriptionEndUtcFromString(subscriptionEndDate);
if (!endDate.isValid()) {
return false;
}
const QDateTime nowUtc = QDateTime::currentDateTimeUtc();
if (endDate <= nowUtc) {
return false;
}
return endDate <= nowUtc.addDays(withinDays);
} }
bool apiUtils::isServerFromApi(const QJsonObject &serverConfigObject) bool apiUtils::isServerFromApi(const QJsonObject &serverConfigObject)
@@ -100,24 +58,18 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
}; };
case apiDefs::ConfigSource::AmneziaGateway: { case apiDefs::ConfigSource::AmneziaGateway: {
constexpr QLatin1String servicePremium("amnezia-premium"); constexpr QLatin1String servicePremium("amnezia-premium");
constexpr QLatin1String serviceTrial("amnezia-trial");
constexpr QLatin1String serviceFree("amnezia-free"); constexpr QLatin1String serviceFree("amnezia-free");
constexpr QLatin1String serviceExternalPremium("external-premium"); constexpr QLatin1String serviceExternalPremium("external-premium");
constexpr QLatin1String serviceExternalTrial("external-trial");
auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject(); auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString(); auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString();
if (serviceType == servicePremium) { if (serviceType == servicePremium) {
return apiDefs::ConfigType::AmneziaPremiumV2; return apiDefs::ConfigType::AmneziaPremiumV2;
} else if (serviceType == serviceTrial) {
return apiDefs::ConfigType::AmneziaTrialV2;
} else if (serviceType == serviceFree) { } else if (serviceType == serviceFree) {
return apiDefs::ConfigType::AmneziaFreeV3; return apiDefs::ConfigType::AmneziaFreeV3;
} else if (serviceType == serviceExternalPremium) { } else if (serviceType == serviceExternalPremium) {
return apiDefs::ConfigType::ExternalPremium; return apiDefs::ConfigType::ExternalPremium;
} else if (serviceType == serviceExternalTrial) {
return apiDefs::ConfigType::ExternalTrial;
} }
} }
default: { default: {
@@ -138,53 +90,39 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
const int httpStatusCodeConflict = 409; const int httpStatusCodeConflict = 409;
const int httpStatusCodeNotFound = 404; const int httpStatusCodeNotFound = 404;
const int httpStatusCodeNotImplemented = 501; const int httpStatusCodeNotImplemented = 501;
const int httpStatusCodePaymentRequired = 402;
const int httpStatusCodeUnprocessableEntity = 422;
if (!sslErrors.empty()) { if (!sslErrors.empty()) {
qDebug().noquote() << sslErrors; qDebug().noquote() << sslErrors;
return amnezia::ErrorCode::ApiConfigSslError; return amnezia::ErrorCode::ApiConfigSslError;
} } else if (replyError == QNetworkReply::NoError) {
if (replyError == QNetworkReply::NoError) {
return amnezia::ErrorCode::NoError; return amnezia::ErrorCode::NoError;
} } else if (replyError == QNetworkReply::NetworkError::OperationCanceledError
if (replyError == QNetworkReply::NetworkError::OperationCanceledError || replyError == QNetworkReply::NetworkError::TimeoutError) {
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << replyError; qDebug() << replyError;
return amnezia::ErrorCode::ApiConfigTimeoutError; return amnezia::ErrorCode::ApiConfigTimeoutError;
} } else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
qDebug() << replyError; qDebug() << replyError;
return amnezia::ErrorCode::ApiUpdateRequestError; return amnezia::ErrorCode::ApiUpdateRequestError;
} } else {
qDebug() << QString::fromUtf8(responseBody);
qDebug() << replyError;
qDebug() << replyErrorString;
qDebug() << httpStatusCode;
qDebug() << QString::fromUtf8(responseBody); int httpStatusFromBody = -1;
qDebug() << replyError; QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
qDebug() << replyErrorString; if (jsonDoc.isObject()) {
qDebug() << httpStatusCode; QJsonObject jsonObj = jsonDoc.object();
httpStatusFromBody = jsonObj.value("http_status").toInt(-1);
}
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object();
const int httpStatusFromBody = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
if (httpStatusFromBody == httpStatusCodeConflict) { if (httpStatusFromBody == httpStatusCodeConflict) {
return amnezia::ErrorCode::ApiConfigLimitError; return amnezia::ErrorCode::ApiConfigLimitError;
} } else if (httpStatusFromBody == httpStatusCodeNotFound) {
if (httpStatusFromBody == httpStatusCodeNotFound) {
return amnezia::ErrorCode::ApiNotFoundError; return amnezia::ErrorCode::ApiNotFoundError;
} } else if (httpStatusFromBody == httpStatusCodeNotImplemented) {
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
return amnezia::ErrorCode::ApiUpdateRequestError; return amnezia::ErrorCode::ApiUpdateRequestError;
} }
if (httpStatusFromBody == httpStatusCodeUnprocessableEntity) {
if (apiErrorMessageFromJson(jsonObj) == unprocessableSubscriptionMessage) {
return amnezia::ErrorCode::ApiSubscriptionExpiredError;
}
return amnezia::ErrorCode::ApiConfigDownloadError;
}
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
}
return amnezia::ErrorCode::ApiConfigDownloadError; return amnezia::ErrorCode::ApiConfigDownloadError;
} }
@@ -195,8 +133,7 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject) bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
{ {
static const QSet<apiDefs::ConfigType> premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2, static const QSet<apiDefs::ConfigType> premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2,
apiDefs::ConfigType::AmneziaTrialV2, apiDefs::ConfigType::ExternalPremium, apiDefs::ConfigType::ExternalPremium };
apiDefs::ConfigType::ExternalTrial };
return premiumTypes.contains(getConfigType(serverConfigObject)); return premiumTypes.contains(getConfigType(serverConfigObject));
} }
@@ -240,9 +177,7 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject) QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
{ {
auto configType = apiUtils::getConfigType(serverConfigObject); if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) {
if (configType != apiDefs::ConfigType::AmneziaPremiumV2 && configType != apiDefs::ConfigType::AmneziaTrialV2
&& configType != apiDefs::ConfigType::ExternalPremium && configType != apiDefs::ConfigType::ExternalTrial) {
return {}; return {};
} }
-2
View File
@@ -13,8 +13,6 @@ namespace apiUtils
bool isSubscriptionExpired(const QString &subscriptionEndDate); bool isSubscriptionExpired(const QString &subscriptionEndDate);
bool isSubscriptionExpiringSoon(const QString &subscriptionEndDate, int withinDays = 10);
bool isPremiumServer(const QJsonObject &serverConfigObject); bool isPremiumServer(const QJsonObject &serverConfigObject);
apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject); apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject);
+8 -27
View File
@@ -8,12 +8,7 @@
#include "platforms/android/android_controller.h" #include "platforms/android/android_controller.h"
#endif #endif
// SystemTrayNotificationHandler exists only on desktop + macOS NE builds (see client/cmake/sources.cmake). #if defined(Q_OS_IOS) || defined(Q_OS_TVOS)
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "ui/systemtray_notificationhandler.h"
#endif
#if defined(Q_OS_IOS)
#include "platforms/ios/ios_controller.h" #include "platforms/ios/ios_controller.h"
#include <AmneziaVPN-Swift.h> #include <AmneziaVPN-Swift.h>
#endif #endif
@@ -96,12 +91,6 @@ void CoreController::initModels()
m_apiServicesModel.reset(new ApiServicesModel(this)); m_apiServicesModel.reset(new ApiServicesModel(this));
m_engine->rootContext()->setContextProperty("ApiServicesModel", m_apiServicesModel.get()); m_engine->rootContext()->setContextProperty("ApiServicesModel", m_apiServicesModel.get());
m_apiSubscriptionPlansModel.reset(new ApiSubscriptionPlansModel(this));
m_engine->rootContext()->setContextProperty("ApiSubscriptionPlansModel", m_apiSubscriptionPlansModel.get());
m_apiBenefitsModel.reset(new ApiBenefitsModel(this));
m_engine->rootContext()->setContextProperty("ApiBenefitsModel", m_apiBenefitsModel.get());
m_apiCountryModel.reset(new ApiCountryModel(this)); m_apiCountryModel.reset(new ApiCountryModel(this));
m_engine->rootContext()->setContextProperty("ApiCountryModel", m_apiCountryModel.get()); m_engine->rootContext()->setContextProperty("ApiCountryModel", m_apiCountryModel.get());
@@ -146,7 +135,7 @@ void CoreController::initControllers()
new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_sitesModel, m_appSplitTunnelingModel, m_settings)); new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_sitesModel, m_appSplitTunnelingModel, m_settings));
m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get()); m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get());
m_sitesController.reset(new SitesController(m_settings, m_sitesModel)); m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel));
m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get()); m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get());
m_allowedDnsController.reset(new AllowedDnsController(m_settings, m_allowedDnsModel)); m_allowedDnsController.reset(new AllowedDnsController(m_settings, m_allowedDnsModel));
@@ -162,11 +151,8 @@ void CoreController::initControllers()
new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_apiCountryModel, m_apiDevicesModel, m_settings)); new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_apiCountryModel, m_apiDevicesModel, m_settings));
m_engine->rootContext()->setContextProperty("ApiSettingsController", m_apiSettingsController.get()); m_engine->rootContext()->setContextProperty("ApiSettingsController", m_apiSettingsController.get());
m_apiConfigsController.reset( m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings));
new ApiConfigsController(m_serversModel, m_apiServicesModel, m_apiSubscriptionPlansModel, m_apiBenefitsModel, m_settings));
m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get()); m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get());
connect(m_apiConfigsController.get(), &ApiConfigsController::subscriptionRefreshNeeded,
this, [this]() { m_apiSettingsController->getAccountInfo(false); });
m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this)); m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this));
m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get()); m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get());
@@ -210,7 +196,7 @@ void CoreController::initAndroidController()
void CoreController::initAppleController() void CoreController::initAppleController()
{ {
#ifdef Q_OS_IOS #if defined(Q_OS_IOS) || defined(Q_OS_TVOS)
IosController::Instance()->initialize(); IosController::Instance()->initialize();
connect(IosController::Instance(), &IosController::importConfigFromOutside, this, [this](QString data) { connect(IosController::Instance(), &IosController::importConfigFromOutside, this, [this](QString data) {
emit m_pageController->goToPageHome(); emit m_pageController->goToPageHome();
@@ -247,6 +233,7 @@ void CoreController::initSignalHandlers()
void CoreController::initNotificationHandler() void CoreController::initNotificationHandler()
{ {
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS)
m_notificationHandler.reset(NotificationHandler::create(nullptr)); m_notificationHandler.reset(NotificationHandler::create(nullptr));
connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(), connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(),
@@ -259,10 +246,8 @@ void CoreController::initNotificationHandler()
&ConnectionController::closeConnection); &ConnectionController::closeConnection);
connect(this, &CoreController::translationsUpdated, m_notificationHandler.get(), &NotificationHandler::onTranslationsUpdated); connect(this, &CoreController::translationsUpdated, m_notificationHandler.get(), &NotificationHandler::onTranslationsUpdated);
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) auto* trayHandler = qobject_cast<SystemTrayNotificationHandler*>(m_notificationHandler.get());
if (auto *trayHandler = qobject_cast<SystemTrayNotificationHandler *>(m_notificationHandler.get())) { connect(this, &CoreController::websiteUrlChanged, trayHandler, &SystemTrayNotificationHandler::updateWebsiteUrl);
connect(this, &CoreController::websiteUrlChanged, trayHandler, &SystemTrayNotificationHandler::updateWebsiteUrl);
}
#endif #endif
} }
@@ -383,11 +368,7 @@ void CoreController::initPrepareConfigHandler()
return; return;
} }
m_installController->validateConfig(); if (!m_installController->isConfigValid()) {
});
connect(m_installController.get(), &InstallController::configValidated, this, [this](bool isValid) {
if (!isValid) {
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
return; return;
} }
+9 -5
View File
@@ -5,7 +5,9 @@
#include <QQmlContext> #include <QQmlContext>
#include <QThread> #include <QThread>
#include "ui/notificationhandler.h" #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS)
#include "ui/systemtray_notificationhandler.h"
#endif
#include "ui/controllers/api/apiConfigsController.h" #include "ui/controllers/api/apiConfigsController.h"
#include "ui/controllers/api/apiSettingsController.h" #include "ui/controllers/api/apiSettingsController.h"
@@ -30,11 +32,9 @@
#include "ui/models/protocols/ikev2ConfigModel.h" #include "ui/models/protocols/ikev2ConfigModel.h"
#endif #endif
#include "ui/models/api/apiAccountInfoModel.h" #include "ui/models/api/apiAccountInfoModel.h"
#include "ui/models/api/apiBenefitsModel.h"
#include "ui/models/api/apiCountryModel.h" #include "ui/models/api/apiCountryModel.h"
#include "ui/models/api/apiDevicesModel.h" #include "ui/models/api/apiDevicesModel.h"
#include "ui/models/api/apiServicesModel.h" #include "ui/models/api/apiServicesModel.h"
#include "ui/models/api/apiSubscriptionPlansModel.h"
#include "ui/models/appSplitTunnelingModel.h" #include "ui/models/appSplitTunnelingModel.h"
#include "ui/models/clientManagementModel.h" #include "ui/models/clientManagementModel.h"
#include "ui/models/protocols/awgConfigModel.h" #include "ui/models/protocols/awgConfigModel.h"
@@ -49,6 +49,10 @@
#include "ui/models/sites_model.h" #include "ui/models/sites_model.h"
#include "ui/models/newsModel.h" #include "ui/models/newsModel.h"
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS)
#include "ui/notificationhandler.h"
#endif
class CoreController : public QObject class CoreController : public QObject
{ {
Q_OBJECT Q_OBJECT
@@ -95,7 +99,9 @@ private:
QSharedPointer<VpnConnection> m_vpnConnection; QSharedPointer<VpnConnection> m_vpnConnection;
QSharedPointer<QTranslator> m_translator; QSharedPointer<QTranslator> m_translator;
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS)
QScopedPointer<NotificationHandler> m_notificationHandler; QScopedPointer<NotificationHandler> m_notificationHandler;
#endif
QMetaObject::Connection m_reloadConfigErrorOccurredConnection; QMetaObject::Connection m_reloadConfigErrorOccurredConnection;
@@ -127,8 +133,6 @@ private:
QSharedPointer<ClientManagementModel> m_clientManagementModel; QSharedPointer<ClientManagementModel> m_clientManagementModel;
QSharedPointer<ApiServicesModel> m_apiServicesModel; QSharedPointer<ApiServicesModel> m_apiServicesModel;
QSharedPointer<ApiSubscriptionPlansModel> m_apiSubscriptionPlansModel;
QSharedPointer<ApiBenefitsModel> m_apiBenefitsModel;
QSharedPointer<ApiCountryModel> m_apiCountryModel; QSharedPointer<ApiCountryModel> m_apiCountryModel;
QSharedPointer<ApiAccountInfoModel> m_apiAccountInfoModel; QSharedPointer<ApiAccountInfoModel> m_apiAccountInfoModel;
QSharedPointer<ApiDevicesModel> m_apiDevicesModel; QSharedPointer<ApiDevicesModel> m_apiDevicesModel;
+10 -35
View File
@@ -44,11 +44,8 @@ namespace
constexpr int httpStatusCodeNotFound = 404; constexpr int httpStatusCodeNotFound = 404;
constexpr int httpStatusCodeConflict = 409; constexpr int httpStatusCodeConflict = 409;
constexpr int httpStatusCodeNotImplemented = 501;
constexpr int httpStatusCodePaymentRequired = 402;
constexpr int httpStatusCodeUnprocessableEntity = 422;
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?"); constexpr int httpStatusCodeNotImplemented = 501;
} }
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs, GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
@@ -336,20 +333,11 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
QStringList baseUrls; QStringList baseUrls;
if (m_isDevEnvironment) { if (m_isDevEnvironment) {
baseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts); baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
} else { } else {
baseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts); baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
} }
if (baseUrls.empty()) {
qDebug() << "empty storage endpoint list";
return {};
}
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(baseUrls.begin(), baseUrls.end(), generator);
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
QStringList proxyStorageUrls; QStringList proxyStorageUrls;
@@ -424,14 +412,12 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
{ {
const QByteArray &responseBody = decryptedResponseBody; const QByteArray &responseBody = decryptedResponseBody;
int apiHttpStatus = -1; int httpStatus = -1;
QString apiErrorMessage;
if (isDecryptionSuccessful) { if (isDecryptionSuccessful) {
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody); QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) { if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object(); QJsonObject jsonObj = jsonDoc.object();
apiHttpStatus = jsonObj.value("http_status").toInt(-1); httpStatus = jsonObj.value("http_status").toInt(-1);
apiErrorMessage = jsonObj.value(QStringLiteral("message")).toString().trimmed();
} }
} else { } else {
qDebug() << "failed to decrypt the data"; qDebug() << "failed to decrypt the data";
@@ -442,12 +428,10 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
qDebug() << "timeout occurred"; qDebug() << "timeout occurred";
qDebug() << replyError; qDebug() << replyError;
return true; return true;
} } else if (responseBody.contains("html")) {
if (responseBody.contains("html")) {
qDebug() << "the response contains an html tag"; qDebug() << "the response contains an html tag";
return true; return true;
} } else if (httpStatus == httpStatusCodeNotFound) {
if (apiHttpStatus == httpStatusCodeNotFound) {
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2) if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|| responseBody.contains(errorResponsePattern3)) { || responseBody.contains(errorResponsePattern3)) {
return false; return false;
@@ -455,25 +439,16 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
qDebug() << replyError; qDebug() << replyError;
return true; return true;
} }
} } else if (httpStatus == httpStatusCodeNotImplemented) {
if (apiHttpStatus == httpStatusCodeNotImplemented) {
if (responseBody.contains(updateRequestResponsePattern)) { if (responseBody.contains(updateRequestResponsePattern)) {
return false; return false;
} else { } else {
qDebug() << replyError; qDebug() << replyError;
return true; return true;
} }
} } else if (httpStatus == httpStatusCodeConflict) {
if (apiHttpStatus == httpStatusCodeConflict) {
return false; return false;
} } else if (replyError != QNetworkReply::NetworkError::NoError) {
if (apiHttpStatus == httpStatusCodePaymentRequired) {
return false;
}
if (apiHttpStatus == httpStatusCodeUnprocessableEntity) {
return apiErrorMessage != unprocessableSubscriptionMessage;
}
if (replyError != QNetworkReply::NetworkError::NoError) {
qDebug() << replyError; qDebug() << replyError;
return true; return true;
} }
-2
View File
@@ -123,8 +123,6 @@ namespace amnezia
ApiUpdateRequestError = 1111, ApiUpdateRequestError = 1111,
ApiSubscriptionExpiredError = 1112, ApiSubscriptionExpiredError = 1112,
ApiPurchaseError = 1113, ApiPurchaseError = 1113,
ApiSubscriptionNotActiveError = 1114,
ApiNoPurchasedSubscriptionsError = 1115,
// QFile errors // QFile errors
OpenError = 1200, OpenError = 1200,
-2
View File
@@ -80,8 +80,6 @@ QString errorString(ErrorCode code) {
case (ErrorCode::ApiUpdateRequestError): errorMessage = QObject::tr("Please update the application to use this feature"); break; case (ErrorCode::ApiUpdateRequestError): errorMessage = QObject::tr("Please update the application to use this feature"); break;
case (ErrorCode::ApiSubscriptionExpiredError): errorMessage = QObject::tr("Your Amnezia Premium subscription has expired.\n Please check your email for renewal instructions.\n If you haven't received an email, please contact our support."); break; case (ErrorCode::ApiSubscriptionExpiredError): errorMessage = QObject::tr("Your Amnezia Premium subscription has expired.\n Please check your email for renewal instructions.\n If you haven't received an email, please contact our support."); break;
case (ErrorCode::ApiPurchaseError): errorMessage = QObject::tr("Unable to process purchase"); break; case (ErrorCode::ApiPurchaseError): errorMessage = QObject::tr("Unable to process purchase"); break;
case (ErrorCode::ApiSubscriptionNotActiveError): errorMessage = QObject::tr("No active subscription found"); break;
case (ErrorCode::ApiNoPurchasedSubscriptionsError): errorMessage = QObject::tr("No purchased subscriptions found. Please purchase a subscription first"); break;
// QFile errors // QFile errors
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break; case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
+2 -2
View File
@@ -24,7 +24,7 @@
#include <sys/socket.h> #include <sys/socket.h>
#include <unistd.h> #include <unistd.h>
#endif #endif
#if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(MACOS_NE) #if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
#include <sys/param.h> #include <sys/param.h>
#include <sys/sysctl.h> #include <sys/sysctl.h>
#include <sys/socket.h> #include <sys/socket.h>
@@ -404,7 +404,7 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
close(sock); close(sock);
return { gateway_address, QNetworkInterface::interfaceFromName(interface) }; return { gateway_address, QNetworkInterface::interfaceFromName(interface) };
#endif #endif
#if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(MACOS_NE) #if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
QString gateway; QString gateway;
int index = -1; int index = -1;
-6
View File
@@ -1,6 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 21V17C15 16.4696 15.2107 15.9609 15.5858 15.5858C15.9609 15.2107 16.4696 15 17 15H21" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 4V6C7.21572 6.61347 7.62494 7.14024 8.16602 7.50096C8.7071 7.86168 9.35075 8.03682 10 8V8C10.5304 8 11.0391 8.21071 11.4142 8.58579C11.7893 8.96086 12 9.46957 12 10C12 10.5304 12.2107 11.0391 12.5858 11.4142C12.9609 11.7893 13.4696 12 14 12C14.5304 12 15.0391 11.7893 15.4142 11.4142C15.7893 11.0391 16 10.5304 16 10C16 9.46957 16.2107 8.96086 16.5858 8.58579C16.9609 8.21071 17.4696 8 18 8H21" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 11H5C5.53043 11 6.03914 11.2107 6.41421 11.5858C6.78929 11.9609 7 12.4696 7 13V14C7 14.5304 7.21071 15.0391 7.58579 15.4142C7.96086 15.7893 8.46957 16 9 16C9.53043 16 10.0391 16.2107 10.4142 16.5858C10.7893 16.9609 11 17.4696 11 18V22" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.1777 8C23.2737 8 23.2737 16 18.1777 16C13.0827 16 11.0447 8 5.43875 8C0.85375 8 0.85375 16 5.43875 16C11.0447 16 13.0828 8 18.1788 8H18.1777Z" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 342 B

-4
View File
@@ -1,4 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 2H7C5.89543 2 5 2.89543 5 4V20C5 21.1046 5.89543 22 7 22H17C18.1046 22 19 21.1046 19 20V4C19 2.89543 18.1046 2 17 2Z" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 18H12.01" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 423 B

+54
View File
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>${QT_INTERNAL_DOLLAR_VAR}{PRODUCT_NAME}</string>
<key>CFBundleExecutable</key>
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundleVersion</key>
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>NSHumanReadableCopyright</key>
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UIRequiredDeviceCapabilities</key>
<array/>
<key>UIRequiresFullScreen</key>
<true/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UILaunchStoryboardName</key>
<string>AmneziaVPNLaunchScreen</string>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>com.wireguard.ios.app_group_id</key>
<string>group.org.amnezia.AmneziaVPN</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder.AppleTV.Storyboard" version="3.0" toolsVersion="13122.16" targetRuntime="AppleTV" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="1920" height="1080"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="wu6-TO-1qx"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
+89 -32
View File
@@ -1,6 +1,14 @@
enable_language(Swift) enable_language(Swift)
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/../..) set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/../..)
set(AMNEZIA_THIRDPARTY_ROOT "${CLIENT_ROOT_DIR}/3rd" CACHE PATH "Path to Amnezia client/3rd sources")
set(AMNEZIA_IOS_APPLETV ${AMNEZIA_IOS_ENABLE_APPLETV_TARGET})
if(AMNEZIA_IOS_APPLETV)
message("Network Extension tvOS mode is ON")
else()
message("Network Extension tvOS mode is OFF")
endif()
add_executable(networkextension) add_executable(networkextension)
set_target_properties(networkextension PROPERTIES set_target_properties(networkextension PROPERTIES
@@ -28,6 +36,23 @@ set_target_properties(networkextension PROPERTIES
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../Frameworks" XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../Frameworks"
) )
if(AMNEZIA_IOS_APPLETV)
set_target_properties(networkextension PROPERTIES
XCODE_ATTRIBUTE_SUPPORTED_PLATFORMS "appletvos appletvsimulator"
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "3"
XCODE_ATTRIBUTE_TVOS_DEPLOYMENT_TARGET "${CMAKE_OSX_DEPLOYMENT_TARGET}"
XCODE_ATTRIBUTE_SDKROOT "appletvos"
XCODE_ATTRIBUTE_SDKROOT[sdk=appletvos*] "appletvos"
XCODE_ATTRIBUTE_SDKROOT[sdk=appletvsimulator*] "appletvsimulator"
XCODE_ATTRIBUTE_LIBRARY_SEARCH_PATHS "$(inherited) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
XCODE_ATTRIBUTE_LIBRARY_SEARCH_PATHS[sdk=appletvos*] "$(inherited) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
XCODE_ATTRIBUTE_LIBRARY_SEARCH_PATHS[sdk=appletvsimulator*] "$(inherited) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
XCODE_ATTRIBUTE_EXCLUDED_LIBRARY_SEARCH_PATHS "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS*.sdk/usr/lib/swift"
XCODE_ATTRIBUTE_EXCLUDED_FRAMEWORK_SEARCH_PATHS "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS*.sdk/System/Library/Frameworks"
LINKER_LANGUAGE Swift
)
endif()
if(DEPLOY) if(DEPLOY)
set_target_properties(networkextension PROPERTIES set_target_properties(networkextension PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution" XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
@@ -45,38 +70,49 @@ endif()
set_target_properties(networkextension PROPERTIES set_target_properties(networkextension PROPERTIES
XCODE_ATTRIBUTE_SWIFT_VERSION "5.0" XCODE_ATTRIBUTE_SWIFT_VERSION "5.0"
XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES "YES" XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES "YES"
XCODE_ATTRIBUTE_SWIFT_OBJC_BRIDGING_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/WireGuardNetworkExtension-Bridging-Header.h"
XCODE_ATTRIBUTE_SWIFT_OPTIMIZATION_LEVEL "-Onone" XCODE_ATTRIBUTE_SWIFT_OPTIMIZATION_LEVEL "-Onone"
XCODE_ATTRIBUTE_SWIFT_PRECOMPILE_BRIDGING_HEADER "NO" XCODE_ATTRIBUTE_SWIFT_PRECOMPILE_BRIDGING_HEADER "NO"
) )
set_target_properties(networkextension PROPERTIES
XCODE_ATTRIBUTE_SWIFT_OBJC_BRIDGING_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/WireGuardNetworkExtension-Bridging-Header.h"
)
set_target_properties("networkextension" PROPERTIES set_target_properties("networkextension" PROPERTIES
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "X7UJ388FXK" XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "X7UJ388FXK"
) )
find_library(FW_ASSETS_LIBRARY AssetsLibrary)
find_library(FW_MOBILE_CORE MobileCoreServices)
find_library(FW_UI_KIT UIKit) find_library(FW_UI_KIT UIKit)
find_library(FW_LIBRESOLV libresolv.9.tbd) find_library(FW_LIBRESOLV libresolv.9.tbd)
target_link_libraries(networkextension PRIVATE ${FW_ASSETS_LIBRARY}) if(NOT AMNEZIA_IOS_APPLETV)
target_link_libraries(networkextension PRIVATE ${FW_MOBILE_CORE}) target_link_libraries(networkextension PRIVATE ${FW_UI_KIT})
target_link_libraries(networkextension PRIVATE ${FW_UI_KIT}) target_link_libraries(networkextension PRIVATE ${FW_LIBRESOLV})
target_link_libraries(networkextension PRIVATE ${FW_LIBRESOLV}) else()
target_link_libraries(networkextension PRIVATE -lresolv)
endif()
target_compile_options(networkextension PRIVATE -DGROUP_ID=\"${BUILD_IOS_GROUP_IDENTIFIER}\") target_compile_options(networkextension PRIVATE -DGROUP_ID=\"${BUILD_IOS_GROUP_IDENTIFIER}\")
target_compile_options(networkextension PRIVATE -DNETWORK_EXTENSION=1) target_compile_options(networkextension PRIVATE -DNETWORK_EXTENSION=1)
set(WG_APPLE_SOURCE_DIR ${CLIENT_ROOT_DIR}/3rd/amneziawg-apple/Sources) set(WG_APPLE_SOURCE_DIR ${AMNEZIA_THIRDPARTY_ROOT}/amneziawg-apple/Sources)
target_sources(networkextension PRIVATE set(NE_COMMON_SOURCES
${CLIENT_ROOT_DIR}/platforms/ios/NELogController.swift
${CLIENT_ROOT_DIR}/platforms/ios/Log.swift
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider.swift
)
set(NE_WIREGUARD_SOURCES
${WG_APPLE_SOURCE_DIR}/WireGuardKit/WireGuardAdapter.swift ${WG_APPLE_SOURCE_DIR}/WireGuardKit/WireGuardAdapter.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKit/PacketTunnelSettingsGenerator.swift ${WG_APPLE_SOURCE_DIR}/WireGuardKit/PacketTunnelSettingsGenerator.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKit/DNSResolver.swift ${WG_APPLE_SOURCE_DIR}/WireGuardKit/DNSResolver.swift
${WG_APPLE_SOURCE_DIR}/WireGuardNetworkExtension/ErrorNotifier.swift ${WG_APPLE_SOURCE_DIR}/WireGuardNetworkExtension/ErrorNotifier.swift
${WG_APPLE_SOURCE_DIR}/Shared/Keychain.swift ${WG_APPLE_SOURCE_DIR}/Shared/Keychain.swift
${WG_APPLE_SOURCE_DIR}/Shared/Model/TunnelConfiguration+WgQuickConfig.swift ${WG_APPLE_SOURCE_DIR}/Shared/FileManager+Extension.swift
${WG_APPLE_SOURCE_DIR}/Shared/Model/NETunnelProviderProtocol+Extension.swift ${WG_APPLE_SOURCE_DIR}/Shared/Model/NETunnelProviderProtocol+Extension.swift
${WG_APPLE_SOURCE_DIR}/Shared/Model/TunnelConfiguration+WgQuickConfig.swift
${WG_APPLE_SOURCE_DIR}/Shared/Model/String+ArrayConversion.swift ${WG_APPLE_SOURCE_DIR}/Shared/Model/String+ArrayConversion.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKit/TunnelConfiguration.swift ${WG_APPLE_SOURCE_DIR}/WireGuardKit/TunnelConfiguration.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKit/IPAddressRange.swift ${WG_APPLE_SOURCE_DIR}/WireGuardKit/IPAddressRange.swift
@@ -84,24 +120,50 @@ target_sources(networkextension PRIVATE
${WG_APPLE_SOURCE_DIR}/WireGuardKit/DNSServer.swift ${WG_APPLE_SOURCE_DIR}/WireGuardKit/DNSServer.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKit/InterfaceConfiguration.swift ${WG_APPLE_SOURCE_DIR}/WireGuardKit/InterfaceConfiguration.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKit/PeerConfiguration.swift ${WG_APPLE_SOURCE_DIR}/WireGuardKit/PeerConfiguration.swift
${WG_APPLE_SOURCE_DIR}/Shared/FileManager+Extension.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKitC/x25519.c ${WG_APPLE_SOURCE_DIR}/WireGuardKitC/x25519.c
${WG_APPLE_SOURCE_DIR}/WireGuardKit/Array+ConcurrentMap.swift ${WG_APPLE_SOURCE_DIR}/WireGuardKit/Array+ConcurrentMap.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKit/IPAddress+AddrInfo.swift ${WG_APPLE_SOURCE_DIR}/WireGuardKit/IPAddress+AddrInfo.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKit/PrivateKey.swift ${WG_APPLE_SOURCE_DIR}/WireGuardKit/PrivateKey.swift
${CLIENT_ROOT_DIR}/platforms/ios/HevSocksTunnel.swift
${CLIENT_ROOT_DIR}/platforms/ios/NELogController.swift
${CLIENT_ROOT_DIR}/platforms/ios/Log.swift
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+WireGuard.swift ${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+WireGuard.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+OpenVPN.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+Xray.swift
${CLIENT_ROOT_DIR}/platforms/ios/WGConfig.swift ${CLIENT_ROOT_DIR}/platforms/ios/WGConfig.swift
)
set(NE_XRAY_SOURCES
${CLIENT_ROOT_DIR}/platforms/ios/HevSocksTunnel.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+Xray.swift
${CLIENT_ROOT_DIR}/platforms/ios/XrayConfig.swift ${CLIENT_ROOT_DIR}/platforms/ios/XrayConfig.swift
)
set(NE_OPENVPN_SOURCES
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+OpenVPN.swift
)
set(NE_APPLE_GLUE_SOURCES
${CLIENT_ROOT_DIR}/platforms/ios/iosglue.mm ${CLIENT_ROOT_DIR}/platforms/ios/iosglue.mm
) )
if(AMNEZIA_IOS_APPLETV)
list(APPEND NE_APPLE_GLUE_SOURCES
${CLIENT_ROOT_DIR}/platforms/ios/tvos_cgo_stubs.c
)
endif()
target_sources(networkextension PRIVATE ${NE_COMMON_SOURCES})
if(NOT AMNEZIA_IOS_APPLETV)
target_sources(networkextension PRIVATE
${NE_WIREGUARD_SOURCES}
${NE_OPENVPN_SOURCES}
${NE_XRAY_SOURCES}
${NE_APPLE_GLUE_SOURCES}
)
else()
target_sources(networkextension PRIVATE
${NE_WIREGUARD_SOURCES}
${NE_APPLE_GLUE_SOURCES}
)
endif()
target_sources(networkextension PRIVATE target_sources(networkextension PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/PrivacyInfo.xcprivacy ${CMAKE_CURRENT_SOURCE_DIR}/PrivacyInfo.xcprivacy
) )
@@ -110,21 +172,16 @@ set_property(TARGET networkextension APPEND PROPERTY RESOURCE
${CMAKE_CURRENT_SOURCE_DIR}/PrivacyInfo.xcprivacy ${CMAKE_CURRENT_SOURCE_DIR}/PrivacyInfo.xcprivacy
) )
## Build wireguard-go-version.h
execute_process(
COMMAND go list -m golang.zx2c4.com/wireguard
WORKING_DIRECTORY ${CLIENT_ROOT_DIR}/3rd/wireguard-apple/Sources/WireGuardKitGo
OUTPUT_VARIABLE WG_VERSION_FULL
)
string(REGEX REPLACE ".*v\([0-9.]*\).*" "\\1" WG_VERSION_STRING 1.1.1)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/wireguard-go-version.h.in
${CMAKE_CURRENT_BINARY_DIR}/wireguard-go-version.h)
target_sources(networkextension PRIVATE
${CMAKE_CURRENT_BINARY_DIR}/wireguard-go-version.h)
target_include_directories(networkextension PRIVATE ${CLIENT_ROOT_DIR}) target_include_directories(networkextension PRIVATE ${CLIENT_ROOT_DIR})
target_include_directories(networkextension PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) target_include_directories(networkextension PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(networkextension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/wireguard/ios/arm64/libwg-go.a) find_package(awg-apple REQUIRED)
target_link_libraries(networkextension PRIVATE amnezia::awg-apple)
target_link_libraries(networkextension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/xray/HevSocks5Tunnel.xcframework) if(NOT AMNEZIA_IOS_APPLETV)
find_package(openvpnadapter REQUIRED)
target_link_libraries(networkextension PRIVATE amnezia::openvpnadapter)
find_package(hev-socks5-tunnel REQUIRED)
target_link_libraries(networkextension PRIVATE heiher::hev-socks5-tunnel)
endif()
@@ -1,3 +0,0 @@
#ifndef WIREGUARD_GO_VERSION
#define WIREGUARD_GO_VERSION "@WG_VERSION_STRING@"
#endif // WIREGUARD_GO_VERSION
+6 -17
View File
@@ -114,25 +114,14 @@ set_property(TARGET AmneziaVPNNetworkExtension APPEND PROPERTY RESOURCE
${CMAKE_CURRENT_SOURCE_DIR}/PrivacyInfo.xcprivacy ${CMAKE_CURRENT_SOURCE_DIR}/PrivacyInfo.xcprivacy
) )
## Build wireguard-go-version.h
execute_process(
COMMAND go list -m golang.zx2c4.com/wireguard
WORKING_DIRECTORY ${CLIENT_ROOT_DIR}/3rd/wireguard-apple/Sources/WireGuardKitGo
OUTPUT_VARIABLE WG_VERSION_FULL
)
string(REGEX REPLACE ".*v\([0-9.]*\).*" "\\1" WG_VERSION_STRING 1.1.1)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/wireguard-go-version.h.in
${CMAKE_CURRENT_BINARY_DIR}/wireguard-go-version.h)
target_sources(AmneziaVPNNetworkExtension PRIVATE
${CMAKE_CURRENT_BINARY_DIR}/wireguard-go-version.h)
target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CLIENT_ROOT_DIR}) target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CLIENT_ROOT_DIR})
target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/wireguard/macos/universal2/libwg-go.a) find_package(openvpnadapter REQUIRED)
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE amnezia::openvpnadapter)
message(${CLIENT_ROOT_DIR}) find_package(awg-apple REQUIRED)
message(${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/xray/HevSocks5Tunnel.xcframework/macos-arm64_x86_64/libhev-socks5-tunnel.a) target_link_libraries(AmneziaVPNNetworkExtension PRIVATE amnezia::awg-apple)
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/xray/HevSocks5Tunnel.xcframework/macos-arm64_x86_64/libhev-socks5-tunnel.a)
target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/xray/HevSocks5Tunnel.xcframework/macos-arm64_x86_64/Headers) find_package(hev-socks5-tunnel REQUIRED)
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE heiher::hev-socks5-tunnel)
@@ -1,3 +0,0 @@
#ifndef WIREGUARD_GO_VERSION
#define WIREGUARD_GO_VERSION "@WG_VERSION_STRING@"
#endif // WIREGUARD_GO_VERSION
+3 -4
View File
@@ -12,11 +12,11 @@
#include "Windows.h" #include "Windows.h"
#endif #endif
#if defined(Q_OS_IOS) #if defined(Q_OS_IOS) || defined(Q_OS_TVOS)
#include "platforms/ios/QtAppDelegate-C-Interface.h" #include "platforms/ios/QtAppDelegate-C-Interface.h"
#endif #endif
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
bool isAnotherInstanceRunning() bool isAnotherInstanceRunning()
{ {
QLocalSocket socket; QLocalSocket socket;
@@ -47,7 +47,7 @@ int main(int argc, char *argv[])
AmneziaApplication app(argc, argv); AmneziaApplication app(argc, argv);
OsSignalHandler::setup(); OsSignalHandler::setup();
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
if (isAnotherInstanceRunning()) { if (isAnotherInstanceRunning()) {
QTimer::singleShot(1000, &app, [&]() { app.quit(); }); QTimer::singleShot(1000, &app, [&]() { app.quit(); });
return app.exec(); return app.exec();
@@ -75,7 +75,6 @@ int main(int argc, char *argv[])
qInfo().noquote() << QString("Started %1 version %2 %3").arg(APPLICATION_NAME, APP_VERSION, GIT_COMMIT_HASH); qInfo().noquote() << QString("Started %1 version %2 %3").arg(APPLICATION_NAME, APP_VERSION, GIT_COMMIT_HASH);
qInfo().noquote() << QString("%1 (%2)").arg(QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture()); qInfo().noquote() << QString("%1 (%2)").arg(QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture());
qInfo().noquote() << QString("SSL backend: %1").arg(QSslSocket::sslLibraryVersionString());
return app.exec(); return app.exec();
} }
@@ -101,9 +101,7 @@ bool AndroidController::initialize()
{"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)}, {"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)},
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)}, {"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)},
{"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)}, {"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)},
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)}, {"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)}
{"onActivityPaused", "()V", reinterpret_cast<void *>(onActivityPaused)},
{"onActivityResumed", "()V", reinterpret_cast<void *>(onActivityResumed)}
}; };
QJniEnvironment env; QJniEnvironment env;
@@ -307,16 +305,6 @@ void AndroidController::requestNotificationPermission()
callActivityMethod("requestNotificationPermission", "()V"); callActivityMethod("requestNotificationPermission", "()V");
} }
void AndroidController::showVpnStateNotification(const QString &title, const QString &message)
{
if (!isNotificationPermissionGranted()) {
return;
}
callActivityMethod("showVpnStateNotification", "(Ljava/lang/String;Ljava/lang/String;)V",
QJniObject::fromString(title).object<jstring>(),
QJniObject::fromString(message).object<jstring>());
}
bool AndroidController::requestAuthentication() bool AndroidController::requestAuthentication()
{ {
QEventLoop wait; QEventLoop wait;
@@ -570,22 +558,3 @@ void AndroidController::onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jin
emit AndroidController::instance()->systemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp); emit AndroidController::instance()->systemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp);
} }
// static
void AndroidController::onActivityPaused(JNIEnv *env, jobject thiz)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->activityPaused();
}
// static
void AndroidController::onActivityResumed(JNIEnv *env, jobject thiz)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->activityResumed();
}
@@ -53,7 +53,6 @@ public:
QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize); QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize);
bool isNotificationPermissionGranted(); bool isNotificationPermissionGranted();
void requestNotificationPermission(); void requestNotificationPermission();
void showVpnStateNotification(const QString &title, const QString &message);
bool requestAuthentication(); bool requestAuthentication();
void sendTouch(float x, float y); void sendTouch(float x, float y);
@@ -76,8 +75,6 @@ signals:
void authenticationResult(bool result); void authenticationResult(bool result);
void imeInsetsChanged(int heightDp); void imeInsetsChanged(int heightDp);
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp); void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
void activityPaused();
void activityResumed();
private: private:
bool isWaitingStatus = true; bool isWaitingStatus = true;
@@ -108,8 +105,6 @@ private:
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data); static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
static void onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp); static void onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp);
static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp); static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp);
static void onActivityPaused(JNIEnv *env, jobject thiz);
static void onActivityResumed(JNIEnv *env, jobject thiz);
template <typename Ret, typename ...Args> template <typename Ret, typename ...Args>
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args); static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);
@@ -1,19 +0,0 @@
#include "android_notificationhandler.h"
#include "android_controller.h"
AndroidNotificationHandler::AndroidNotificationHandler(QObject *parent)
: NotificationHandler(parent)
{
}
void AndroidNotificationHandler::notify(Message type, const QString &title, const QString &message, int timerMsec)
{
Q_UNUSED(type);
Q_UNUSED(timerMsec);
// Permission is checked on the Kotlin side as well; avoid JNI if already denied.
if (!AndroidController::instance()->isNotificationPermissionGranted()) {
return;
}
AndroidController::instance()->showVpnStateNotification(title, message);
}
@@ -1,16 +0,0 @@
#ifndef ANDROID_NOTIFICATIONHANDLER_H
#define ANDROID_NOTIFICATIONHANDLER_H
#include "ui/notificationhandler.h"
class AndroidNotificationHandler final : public NotificationHandler {
Q_OBJECT
public:
explicit AndroidNotificationHandler(QObject *parent = nullptr);
protected:
void notify(Message type, const QString &title, const QString &message, int timerMsec) override;
};
#endif // ANDROID_NOTIFICATIONHANDLER_H
@@ -126,7 +126,8 @@ extension PacketTunnelProvider {
} }
vpnReachability.startTracking { [weak self] status in vpnReachability.startTracking { [weak self] status in
self?.handleOpenVPNReachabilityChange(status) guard status == .reachableViaWiFi else { return }
self?.ovpnAdapter?.reconnect(afterTimeInterval: 5)
} }
startHandler = completionHandler startHandler = completionHandler
@@ -21,44 +21,6 @@ extension Constants {
} }
extension PacketTunnelProvider { extension PacketTunnelProvider {
private func applyXraySplitTunnel(_ xrayConfig: XrayConfig,
settings: NEPacketTunnelNetworkSettings) {
guard let splitTunnelType = xrayConfig.splitTunnelType else {
return
}
guard let splitTunnelSites = xrayConfig.splitTunnelSites else {
xrayLog(.error, message: "Split tunnel sites are not set")
return
}
if splitTunnelType == 1 {
var ipv4IncludedRoutes = [NEIPv4Route]()
for allowedIPString in splitTunnelSites {
if let allowedIP = IPAddressRange(from: allowedIPString) {
ipv4IncludedRoutes.append(NEIPv4Route(
destinationAddress: "\(allowedIP.address)",
subnetMask: "\(allowedIP.subnetMask())"))
}
}
settings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
} else if splitTunnelType == 2 {
var ipv4ExcludedRoutes = [NEIPv4Route]()
for excludedIPString in splitTunnelSites {
if let excludedIP = IPAddressRange(from: excludedIPString) {
ipv4ExcludedRoutes.append(NEIPv4Route(
destinationAddress: "\(excludedIP.address)",
subnetMask: "\(excludedIP.subnetMask())"))
}
}
settings.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
}
}
func startXray(completionHandler: @escaping (Error?) -> Void) { func startXray(completionHandler: @escaping (Error?) -> Void) {
// Xray configuration // Xray configuration
@@ -110,7 +72,6 @@ extension PacketTunnelProvider {
settings.dnsSettings = !dnsArray.isEmpty settings.dnsSettings = !dnsArray.isEmpty
? NEDNSSettings(servers: dnsArray) ? NEDNSSettings(servers: dnsArray)
: NEDNSSettings(servers: ["1.1.1.1"]) : NEDNSSettings(servers: ["1.1.1.1"])
applyXraySplitTunnel(xrayConfig, settings: settings)
let xrayConfigData = xrayConfig.config.data(using: .utf8) let xrayConfigData = xrayConfig.config.data(using: .utf8)
+50 -142
View File
@@ -3,7 +3,9 @@ import NetworkExtension
import Network import Network
import os import os
import Darwin import Darwin
#if !os(tvOS)
import OpenVPNAdapter import OpenVPNAdapter
#endif
enum TunnelProtoType: String { enum TunnelProtoType: String {
case wireguard, openvpn, xray case wireguard, openvpn, xray
@@ -38,23 +40,22 @@ struct Constants {
class PacketTunnelProvider: NEPacketTunnelProvider { class PacketTunnelProvider: NEPacketTunnelProvider {
var wgAdapter: WireGuardAdapter? var wgAdapter: WireGuardAdapter?
#if !os(tvOS)
var ovpnAdapter: OpenVPNAdapter? var ovpnAdapter: OpenVPNAdapter?
private lazy var openVPNPacketFlowAdapter = PacketTunnelFlowAdapter(flow: packetFlow) private lazy var openVPNPacketFlowAdapter = PacketTunnelFlowAdapter(flow: packetFlow)
#endif
private let pathMonitorQueue = DispatchQueue(label: Constants.processQueueName + ".path-monitor") private let pathMonitorQueue = DispatchQueue(label: Constants.processQueueName + ".path-monitor")
private let networkChangeQueue = DispatchQueue(label: Constants.processQueueName + ".network-change")
private let pathMonitor = NWPathMonitor() private let pathMonitor = NWPathMonitor()
private var didReceiveInitialPathUpdate = false private var didReceiveInitialPathUpdate = false
private var currentPath: Network.NWPath? private var currentPath: Network.NWPath?
private var currentPathSignature: String? private var currentPathSignature: String?
private var pendingOpenVPNReconnectWorkItem: DispatchWorkItem?
private var pendingNetworkChangeWorkItem: DispatchWorkItem?
private var isApplyingNetworkChange = false
private var lastOpenVPNReachabilityStatus: OpenVPNReachabilityStatus?
var splitTunnelType: Int? var splitTunnelType: Int?
var splitTunnelSites: [String]? var splitTunnelSites: [String]?
#if !os(tvOS)
let vpnReachability = OpenVPNReachability() let vpnReachability = OpenVPNReachability()
#endif
var startHandler: ((Error?) -> Void)? var startHandler: ((Error?) -> Void)?
var stopHandler: (() -> Void)? var stopHandler: (() -> Void)?
@@ -62,9 +63,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
var activeIfaceIdx: UInt32 = 0 var activeIfaceIdx: UInt32 = 0
#if !os(tvOS)
func openVPNPacketFlow() -> OpenVPNAdapterPacketFlow { func openVPNPacketFlow() -> OpenVPNAdapterPacketFlow {
openVPNPacketFlowAdapter openVPNPacketFlowAdapter
} }
#endif
override init() { override init() {
super.init() super.init()
@@ -83,22 +86,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
guard hasMeaningfulChange, let proto = self.protoType else { return } guard hasMeaningfulChange, let proto = self.protoType else { return }
// WireGuard/AWG manages network changes internally in its own adapter. // WireGuard/AWG manages network changes internally; avoid restarting the tunnel here.
if proto == .wireguard { if proto == .wireguard {
return return
} }
if proto == .openvpn { DispatchQueue.main.async {
self.scheduleOpenVPNReconnect(reason: "NWPath changed") self.handle(networkChange: path) { _ in }
return
} }
if self.isApplyingNetworkChange || self.reasserting {
xrayLog(.debug, message: "Ignoring path change while xray restart is in progress")
return
}
self.scheduleNetworkChangeHandling(for: proto, path: path)
} }
pathMonitor.start(queue: pathMonitorQueue) pathMonitor.start(queue: pathMonitorQueue)
@@ -210,8 +205,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
return return
} }
cancelPendingOpenVPNReconnect()
cancelPendingNetworkChangeHandling()
didReceiveInitialPathUpdate = false didReceiveInitialPathUpdate = false
updateActiveInterfaceIndexForCurrentPath() updateActiveInterfaceIndexForCurrentPath()
@@ -221,18 +214,27 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
errorNotifier: errorNotifier, errorNotifier: errorNotifier,
completionHandler: completionHandler) completionHandler: completionHandler)
case .openvpn: case .openvpn:
#if os(tvOS)
completionHandler(NSError(domain: "org.amnezia.ne",
code: -1002,
userInfo: [NSLocalizedDescriptionKey: "OpenVPN backend is not available for tvOS in this build"]))
#else
startOpenVPN(completionHandler: completionHandler) startOpenVPN(completionHandler: completionHandler)
#endif
case .xray: case .xray:
#if os(tvOS)
completionHandler(NSError(domain: "org.amnezia.ne",
code: -1003,
userInfo: [NSLocalizedDescriptionKey: "Xray backend is not available for tvOS in this build"]))
#else
startXray(completionHandler: completionHandler) startXray(completionHandler: completionHandler)
#endif
} }
} }
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
cancelPendingOpenVPNReconnect()
cancelPendingNetworkChangeHandling()
guard let protoType else { guard let protoType else {
completionHandler() completionHandler()
return return
@@ -243,10 +245,18 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
stopWireguard(with: reason, stopWireguard(with: reason,
completionHandler: completionHandler) completionHandler: completionHandler)
case .openvpn: case .openvpn:
#if os(tvOS)
completionHandler()
#else
stopOpenVPN(with: reason, stopOpenVPN(with: reason,
completionHandler: completionHandler) completionHandler: completionHandler)
#endif
case .xray: case .xray:
#if os(tvOS)
completionHandler()
#else
stopXray(completionHandler: completionHandler) stopXray(completionHandler: completionHandler)
#endif
} }
} }
@@ -260,7 +270,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
case .wireguard: case .wireguard:
handleWireguardStatusMessage(messageData, completionHandler: completionHandler) handleWireguardStatusMessage(messageData, completionHandler: completionHandler)
case .openvpn: case .openvpn:
#if !os(tvOS)
handleOpenVPNStatusMessage(messageData, completionHandler: completionHandler) handleOpenVPNStatusMessage(messageData, completionHandler: completionHandler)
#else
completionHandler?(nil)
#endif
case .xray: case .xray:
break; break;
} }
@@ -277,111 +291,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
} }
private func handle(networkChange changePath: Network.NWPath, completion: @escaping (Error?) -> Void) { private func handle(networkChange changePath: Network.NWPath, completion: @escaping (Error?) -> Void) {
guard protoType == .xray else {
updateActiveInterfaceIndex(for: changePath)
completion(nil)
return
}
updateActiveInterfaceIndex(for: changePath) updateActiveInterfaceIndex(for: changePath)
reasserting = true neLog(.info, message: "Tunnel restarted.")
xrayLog(.info, message: "Applying network change to xray tunnel") startTunnel(options: nil, completionHandler: completion)
stopXray { }
startXray { [weak self] error in
self?.reasserting = false
completion(error)
}
}
private func scheduleNetworkChangeHandling(for proto: TunnelProtoType, path: Network.NWPath) {
guard proto == .xray else { return }
pendingNetworkChangeWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
guard let self else { return }
self.pendingNetworkChangeWorkItem = nil
if self.isApplyingNetworkChange || self.reasserting {
xrayLog(.debug, message: "Skipping network change while restart is already in progress")
return
}
self.isApplyingNetworkChange = true
DispatchQueue.main.async {
self.handle(networkChange: path) { [weak self] _ in
self?.networkChangeQueue.async {
self?.isApplyingNetworkChange = false
}
}
}
}
pendingNetworkChangeWorkItem = workItem
networkChangeQueue.asyncAfter(deadline: .now() + 1.0, execute: workItem)
}
private func scheduleOpenVPNReconnect(reason: String) {
guard protoType == .openvpn else { return }
pendingOpenVPNReconnectWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
guard let self else { return }
self.pendingOpenVPNReconnectWorkItem = nil
guard self.protoType == .openvpn else { return }
if self.reasserting {
ovpnLog(.debug, message: "Skipping OpenVPN reconnect while session is already reasserting")
return
}
DispatchQueue.main.async { [weak self] in
guard let self else { return }
guard !self.reasserting else {
ovpnLog(.debug, message: "Skipping OpenVPN reconnect while session is already reasserting")
return
}
ovpnLog(.info, message: "\(reason), reconnecting OpenVPN session")
self.ovpnAdapter?.reconnect(afterTimeInterval: 1)
}
}
pendingOpenVPNReconnectWorkItem = workItem
networkChangeQueue.asyncAfter(deadline: .now() + 1.0, execute: workItem)
}
func handleOpenVPNReachabilityChange(_ status: OpenVPNReachabilityStatus) {
defer { lastOpenVPNReachabilityStatus = status }
guard let previousStatus = lastOpenVPNReachabilityStatus else {
return
}
guard previousStatus != status else {
return
}
switch status {
case .reachableViaWiFi, .reachableViaWWAN:
scheduleOpenVPNReconnect(reason: "Reachability changed")
default:
break
}
}
private func cancelPendingOpenVPNReconnect() {
pendingOpenVPNReconnectWorkItem?.cancel()
pendingOpenVPNReconnectWorkItem = nil
lastOpenVPNReachabilityStatus = nil
}
private func cancelPendingNetworkChangeHandling() {
pendingNetworkChangeWorkItem?.cancel()
pendingNetworkChangeWorkItem = nil
isApplyingNetworkChange = false
} }
} }
@@ -391,14 +303,8 @@ private extension PacketTunnelProvider {
signatureComponents.append(path.isExpensive ? "exp" : "noexp") signatureComponents.append(path.isExpensive ? "exp" : "noexp")
signatureComponents.append(path.isConstrained ? "con" : "nocon") signatureComponents.append(path.isConstrained ? "con" : "nocon")
// Ignore loopback and tunnel-style `.other` interfaces so Xray does not let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular, .loopback, .other]
// react to its own utun lifecycle as if the physical uplink changed. let sortedInterfaces = path.availableInterfaces.sorted { lhs, rhs in
let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular]
let externalInterfaces = path.availableInterfaces.filter { interface in
interface.type == .wiredEthernet || interface.type == .wifi || interface.type == .cellular
}
let sortedInterfaces = externalInterfaces.sorted { lhs, rhs in
if lhs.type == rhs.type { if lhs.type == rhs.type {
return lhs.index < rhs.index return lhs.index < rhs.index
} }
@@ -419,8 +325,8 @@ private extension PacketTunnelProvider {
case .wiredEthernet: typeName = "ethernet" case .wiredEthernet: typeName = "ethernet"
case .wifi: typeName = "wifi" case .wifi: typeName = "wifi"
case .cellular: typeName = "cellular" case .cellular: typeName = "cellular"
case .loopback, .other: case .loopback: typeName = "loopback"
continue case .other: typeName = "other"
@unknown default: typeName = "unknown" @unknown default: typeName = "unknown"
} }
signatureComponents.append("\(typeName):\(interface.index)") signatureComponents.append("\(typeName):\(interface.index)")
@@ -437,16 +343,17 @@ private extension PacketTunnelProvider {
} }
extension WireGuardLogLevel { extension WireGuardLogLevel {
var osLogLevel: OSLogType { var osLogLevel: OSLogType {
switch self { switch self {
case .verbose: case .verbose:
return .debug return .debug
case .error: case .error:
return .error return .error
}
} }
}
} }
#if !os(tvOS)
final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow { final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
private let flow: NEPacketTunnelFlow private let flow: NEPacketTunnelFlow
@@ -465,6 +372,7 @@ final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
flow.writePackets(packets, withProtocols: protocols) flow.writePackets(packets, withProtocols: protocols)
} }
} }
#endif
extension NEProviderStopReason { extension NEProviderStopReason {
var amneziaDescription: String { var amneziaDescription: String {
+1 -1
View File
@@ -1,4 +1,4 @@
#if !MACOS_NE #if !MACOS_NE && !TARGET_OS_TV
#include "QRCodeReaderBase.h" #include "QRCodeReaderBase.h"
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
-178
View File
@@ -1,178 +0,0 @@
import Foundation
import StoreKit
@available(iOS 15.0, macOS 12.0, *)
@objcMembers
public class StoreKit2Helper: NSObject {
public static let shared = StoreKit2Helper()
private static let errorDomain = "StoreKit2Helper"
private struct EntitlementInfo {
let transactionId: UInt64
let originalTransactionId: UInt64
let productId: String
let purchaseDate: Date
var dictionary: NSDictionary {
[
"transactionId": String(transactionId),
"originalTransactionId": String(originalTransactionId),
"productId": productId
]
}
}
public func fetchCurrentEntitlements(completion: @escaping (Bool, [NSDictionary]?, NSError?) -> Void) {
Task { @MainActor in
do {
try await AppStore.sync()
var entitlements: [EntitlementInfo] = []
for await result in Transaction.currentEntitlements {
switch result {
case .verified(let transaction):
entitlements.append(EntitlementInfo(transactionId: transaction.id,
originalTransactionId: transaction.originalID,
productId: transaction.productID,
purchaseDate: transaction.purchaseDate))
case .unverified(_, let error):
print("[IAP][StoreKit2] Unverified transaction skipped: \(error.localizedDescription)")
}
}
let sortedEntitlements = entitlements.sorted { lhs, rhs in
if lhs.purchaseDate != rhs.purchaseDate {
return lhs.purchaseDate > rhs.purchaseDate
}
return lhs.transactionId > rhs.transactionId
}.map { $0.dictionary }
completion(true, sortedEntitlements, nil)
} catch {
completion(false, nil, error as NSError)
}
}
}
public func purchaseProduct(productIdentifier: String, completion: @escaping (Bool, String?, String?, String?, NSError?) -> Void) {
Task {
do {
let products = try await Product.products(for: [productIdentifier])
guard let product = products.first else {
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
error: makeError(code: 0, description: "Product not found"))
return
}
let result = try await product.purchase()
switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
await transaction.finish()
completePurchase(completion: completion, success: true, transactionId: String(transaction.id),
productId: transaction.productID, originalTransactionId: String(transaction.originalID), error: nil)
case .unverified(_, let error):
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
error: error as NSError)
}
case .userCancelled:
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
error: makeError(code: 1, description: "Purchase cancelled"))
case .pending:
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
error: makeError(code: 2, description: "Purchase pending"))
@unknown default:
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
error: makeError(code: 3, description: "Unknown purchase result"))
}
} catch {
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
error: error as NSError)
}
}
}
private func storefrontCurrencyCode(for product: Product) -> String {
product.priceFormatStyle.locale.currencyCode ?? ""
}
private func subscriptionBillingMonths(_ period: Product.SubscriptionPeriod) -> Double {
let periodValue = Double(period.value)
switch period.unit {
case .day:
return periodValue / 30.0
case .week:
return periodValue * 7.0 / 30.0
case .month:
return periodValue
case .year:
return periodValue * 12.0
@unknown default:
return periodValue
}
}
public func fetchProducts(identifiers: Set<String>, completion: @escaping ([NSDictionary], [String], NSError?) -> Void) {
Task {
do {
let products = try await Product.products(for: identifiers)
let productDicts = products.map { product in productDictionary(for: product) }
let fetchedIds = Set(products.map { $0.id })
let invalidIdentifiers = identifiers.filter { !fetchedIds.contains($0) }
DispatchQueue.main.async { completion(productDicts, Array(invalidIdentifiers), nil) }
} catch {
DispatchQueue.main.async { completion([], Array(identifiers), error as NSError) }
}
}
}
private func makeError(code: Int, description: String) -> NSError {
NSError(domain: Self.errorDomain, code: code, userInfo: [NSLocalizedDescriptionKey: description])
}
private func completePurchase(completion: @escaping (Bool, String?, String?, String?, NSError?) -> Void,
success: Bool,
transactionId: String?,
productId: String?,
originalTransactionId: String?,
error: NSError?) {
DispatchQueue.main.async {
completion(success, transactionId, productId, originalTransactionId, error)
}
}
private func productDictionary(for product: Product) -> NSDictionary {
let currencyCode = storefrontCurrencyCode(for: product)
var productData: [String: Any] = [
"productId": product.id,
"title": product.displayName,
"description": product.description,
"price": "\(product.price)",
"displayPrice": product.displayPrice,
"currencyCode": currencyCode,
"priceAmount": NSDecimalNumber(decimal: product.price).doubleValue
]
if let subscription = product.subscription {
let billingMonths = subscriptionBillingMonths(subscription.subscriptionPeriod)
productData["subscriptionBillingMonths"] = billingMonths
if let perMonthPrice = displayPricePerMonth(for: product, billingMonths: billingMonths, currencyCode: currencyCode) {
productData["displayPricePerMonth"] = perMonthPrice
}
}
return productData as NSDictionary
}
private func displayPricePerMonth(for product: Product, billingMonths: Double, currencyCode: String) -> String? {
if billingMonths <= 1e-6 {
return nil
}
let perMonthPrice = product.price / Decimal(billingMonths)
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = product.priceFormatStyle.locale
if !currencyCode.isEmpty {
formatter.currencyCode = currencyCode
}
return formatter.string(from: NSDecimalNumber(decimal: perMonthPrice))
}
}
+206 -56
View File
@@ -4,20 +4,27 @@
#import "StoreKitController.h" #import "StoreKitController.h"
#import <StoreKit/StoreKit.h> #import <StoreKit/StoreKit.h>
#import <AmneziaVPN-Swift.h>
#include <QtCore/QDebug> #include <QtCore/QDebug>
#include <QtCore/QString> #include <QtCore/QString>
namespace
{
QString toQString(NSString *value)
{
return QString::fromUtf8((value ?: @"").UTF8String);
}
}
API_AVAILABLE(ios(15.0), macos(12.0)) API_AVAILABLE(ios(15.0), macos(12.0))
@interface StoreKitController () <SKProductsRequestDelegate, SKPaymentTransactionObserver>
@property (nonatomic, copy) void (^purchaseCompletion)(BOOL success,
NSString *_Nullable transactionId,
NSString *_Nullable productId,
NSString *_Nullable originalTransactionId,
NSError *_Nullable error);
@property (nonatomic, copy) void (^restoreCompletion)(BOOL success,
NSArray<NSDictionary *> *_Nullable restoredTransactions,
NSError *_Nullable error);
@property (nonatomic, copy) void (^productsFetchCompletion)(NSArray<NSDictionary *> *products,
NSArray<NSString *> *invalidIdentifiers,
NSError *_Nullable error);
@property (nonatomic, strong) SKProductsRequest *productsRequest;
@property (nonatomic, strong) NSMutableArray<NSDictionary *> *restoredTransactions;
@end
@implementation StoreKitController @implementation StoreKitController
+ (instancetype)sharedInstance + (instancetype)sharedInstance
@@ -35,9 +42,17 @@ API_AVAILABLE(ios(15.0), macos(12.0))
- (instancetype)init API_AVAILABLE(ios(15.0), macos(12.0)) - (instancetype)init API_AVAILABLE(ios(15.0), macos(12.0))
{ {
self = [super init]; self = [super init];
if (self) {
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self; return self;
} }
- (void)dealloc
{
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
- (void)purchaseProduct:(NSString *)productIdentifier - (void)purchaseProduct:(NSString *)productIdentifier
completion:(void (^)(BOOL success, completion:(void (^)(BOOL success,
NSString *_Nullable transactionId, NSString *_Nullable transactionId,
@@ -45,48 +60,41 @@ API_AVAILABLE(ios(15.0), macos(12.0))
NSString *_Nullable originalTransactionId, NSString *_Nullable originalTransactionId,
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0)) NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
{ {
qInfo().noquote() << "[IAP][StoreKit2] Starting purchase for" << QString::fromUtf8(productIdentifier.UTF8String); self.purchaseCompletion = completion;
[[StoreKit2Helper shared] purchaseProductWithProductIdentifier:productIdentifier
completion:^(BOOL success, qInfo().noquote() << "[IAP][StoreKit] Starting purchase for" << QString::fromUtf8(productIdentifier.UTF8String);
NSString *transactionId, dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *productId, [self performPurchaseAsync:productIdentifier];
NSString *originalTransactionId, });
NSError *error) { }
if (success) {
qInfo().noquote() << "[IAP][StoreKit2] Purchase success. transactionId =" << toQString(transactionId) - (void)performPurchaseAsync:(NSString *)productIdentifier API_AVAILABLE(ios(15.0), macos(12.0))
<< "originalTransactionId =" << toQString(originalTransactionId) << "productId =" << toQString(productId); {
} else if (error) { dispatch_async(dispatch_get_main_queue(), ^{
qWarning().noquote() << "[IAP][StoreKit2] Purchase failed:" << toQString(error.localizedDescription); @try {
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:productIdentifier]];
request.delegate = self;
[request start];
} @catch (NSException *exception) {
NSError *error = [NSError errorWithDomain:@"StoreKitController"
code:1
userInfo:@{ NSLocalizedDescriptionKey : exception.reason ?: @"Purchase failed" }];
if (self.purchaseCompletion) {
self.purchaseCompletion(NO, nil, nil, nil, error);
self.purchaseCompletion = nil;
}
} }
if (completion) { });
completion(success, transactionId, productId, originalTransactionId, error);
}
}];
} }
- (void)restorePurchasesWithCompletion:(void (^)(BOOL success, - (void)restorePurchasesWithCompletion:(void (^)(BOOL success,
NSArray<NSDictionary *> *_Nullable restoredTransactions, NSArray<NSDictionary *> *_Nullable restoredTransactions,
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0)) NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
{ {
[[StoreKit2Helper shared] fetchCurrentEntitlementsWithCompletion:^(BOOL success, self.restoreCompletion = completion;
NSArray<NSDictionary *> *entitlements, self.restoredTransactions = [NSMutableArray array];
NSError *error) { [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
if (success) {
qInfo().noquote() << "[IAP][StoreKit2] currentEntitlements returned"
<< (int)(entitlements ? entitlements.count : 0) << "active entitlements";
for (NSDictionary *entitlement in entitlements) {
qInfo().noquote() << "[IAP][StoreKit2] Active entitlement:"
<< "transactionId=" << toQString(entitlement[@"transactionId"])
<< "originalTransactionId=" << toQString(entitlement[@"originalTransactionId"])
<< "productId=" << toQString(entitlement[@"productId"]);
}
} else {
qWarning().noquote() << "[IAP][StoreKit2] fetchCurrentEntitlements failed:" << toQString(error.localizedDescription);
}
if (completion) {
completion(success, entitlements, error);
}
}];
} }
- (void)fetchProductsWithIdentifiers:(NSSet<NSString *> *)productIdentifiers - (void)fetchProductsWithIdentifiers:(NSSet<NSString *> *)productIdentifiers
@@ -94,21 +102,163 @@ API_AVAILABLE(ios(15.0), macos(12.0))
NSArray<NSString *> *invalidIdentifiers, NSArray<NSString *> *invalidIdentifiers,
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0)) NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
{ {
[[StoreKit2Helper shared] fetchProductsWithIdentifiers:productIdentifiers self.productsFetchCompletion = completion;
completion:^(NSArray<NSDictionary *> *products, self.productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
NSArray<NSString *> *invalidIdentifiers, self.productsRequest.delegate = self;
NSError *error) { [self.productsRequest start];
if (!error) { }
for (NSDictionary *productInfo in products) {
qInfo().noquote() << "[IAP][StoreKit2] Fetched product info" << toQString(productInfo[@"productId"]) #pragma mark - SKProductsRequestDelegate / SKRequestDelegate
<< "price=" << toQString(productInfo[@"price"])
<< "currency=" << toQString(productInfo[@"currencyCode"]); - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
if (self.purchaseCompletion) {
SKProduct *product = response.products.firstObject;
if (!product) {
NSError *error = [NSError errorWithDomain:@"StoreKitController"
code:0
userInfo:@{ NSLocalizedDescriptionKey : @"Product not found" }];
self.purchaseCompletion(NO, nil, nil, nil, error);
self.purchaseCompletion = nil;
self.productsRequest = nil;
return;
}
NSString *currencyCode = [product.priceLocale objectForKey:NSLocaleCurrencyCode] ?: @"";
NSString *priceString = [product.price stringValue] ?: @"";
qInfo().noquote() << "[IAP][StoreKit] Received product" << QString::fromUtf8(product.productIdentifier.UTF8String)
<< "price=" << QString::fromUtf8(priceString.UTF8String)
<< "currency=" << QString::fromUtf8(currencyCode.UTF8String);
SKPayment *payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
self.productsRequest = nil;
return;
}
if (self.productsFetchCompletion) {
NSMutableArray<NSDictionary *> *productDicts = [NSMutableArray array];
for (SKProduct *p in response.products) {
NSDictionary *productDict = @{
@"productId": p.productIdentifier,
@"title": p.localizedTitle,
@"description": p.localizedDescription,
@"price": p.price.stringValue,
@"currencyCode": [p.priceLocale objectForKey:NSLocaleCurrencyCode] ?: @""
};
[productDicts addObject:productDict];
NSString *productCurrency = [p.priceLocale objectForKey:NSLocaleCurrencyCode] ?: @"";
NSString *productPrice = [p.price stringValue] ?: @"";
qInfo().noquote() << "[IAP][StoreKit] Fetched product info" << QString::fromUtf8(p.productIdentifier.UTF8String)
<< "price=" << QString::fromUtf8(productPrice.UTF8String)
<< "currency=" << QString::fromUtf8(productCurrency.UTF8String);
}
self.productsFetchCompletion(productDicts, response.invalidProductIdentifiers, nil);
self.productsFetchCompletion = nil;
self.productsRequest = nil;
return;
}
}
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
{
if (self.purchaseCompletion) {
self.purchaseCompletion(NO, nil, nil, nil, error);
self.purchaseCompletion = nil;
}
if (self.productsFetchCompletion) {
self.productsFetchCompletion(@[], @[], error);
self.productsFetchCompletion = nil;
}
self.productsRequest = nil;
}
#pragma mark - SKPaymentTransactionObserver
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
{
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased: {
NSString *originalTransactionId = transaction.originalTransaction.transactionIdentifier ?: transaction.transactionIdentifier;
qInfo().noquote() << "[IAP][StoreKit] Transaction purchased" << QString::fromUtf8(transaction.transactionIdentifier.UTF8String)
<< "original=" << QString::fromUtf8((originalTransactionId ?: @"").UTF8String)
<< "product=" << QString::fromUtf8(transaction.payment.productIdentifier.UTF8String);
if (self.purchaseCompletion) {
self.purchaseCompletion(YES,
transaction.transactionIdentifier,
transaction.payment.productIdentifier,
originalTransactionId,
nil);
self.purchaseCompletion = nil;
} }
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
} }
if (completion) { case SKPaymentTransactionStateFailed:
completion(products ?: @[], invalidIdentifiers ?: @[], error); qInfo().noquote() << "[IAP][StoreKit] Transaction failed" << QString::fromUtf8(transaction.transactionIdentifier.UTF8String)
<< "product=" << QString::fromUtf8(transaction.payment.productIdentifier.UTF8String)
<< "error=" << QString::fromUtf8(transaction.error.localizedDescription.UTF8String);
if (self.purchaseCompletion) {
self.purchaseCompletion(NO,
transaction.transactionIdentifier,
transaction.payment.productIdentifier,
nil,
transaction.error);
self.purchaseCompletion = nil;
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateRestored: {
if (self.restoreCompletion) {
NSString *transactionId = transaction.transactionIdentifier ?: @"";
NSString *originalTransactionId = transaction.originalTransaction.transactionIdentifier ?: transactionId;
NSString *productId = transaction.payment.productIdentifier ?: @"";
qInfo().noquote() << "[IAP][StoreKit] Transaction restored"
<< QString::fromUtf8(transactionId.UTF8String)
<< "original="
<< QString::fromUtf8((originalTransactionId ?: @"").UTF8String)
<< "product="
<< QString::fromUtf8((productId ?: @"").UTF8String);
NSDictionary *info = @{
@"transactionId": transactionId,
@"originalTransactionId": originalTransactionId ?: @"",
@"productId": productId ?: @""
};
if (!self.restoredTransactions) {
self.restoredTransactions = [NSMutableArray array];
}
[self.restoredTransactions addObject:info];
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
} }
}]; case SKPaymentTransactionStatePurchasing:
case SKPaymentTransactionStateDeferred:
break;
}
}
}
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
if (self.restoreCompletion) {
NSArray<NSDictionary *> *transactions = [self.restoredTransactions copy];
self.restoreCompletion(YES, transactions, nil);
self.restoreCompletion = nil;
self.restoredTransactions = nil;
}
}
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
{
if (self.restoreCompletion) {
self.restoreCompletion(NO, nil, error);
self.restoreCompletion = nil;
self.restoredTransactions = nil;
}
} }
@end @end
-2
View File
@@ -3,7 +3,5 @@ import Foundation
struct XrayConfig: Decodable { struct XrayConfig: Decodable {
let dns1: String? let dns1: String?
let dns2: String? let dns2: String?
let splitTunnelType: Int?
let splitTunnelSites: [String]?
let config: String let config: String
} }
+52 -83
View File
@@ -179,9 +179,8 @@ bool IosController::initialize()
[NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray<NETunnelProviderManager *> * _Nullable managers, NSError * _Nullable error) { [NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray<NETunnelProviderManager *> * _Nullable managers, NSError * _Nullable error) {
@try { @try {
if (error) { if (error) {
qWarning() << "IosController::initialize : loadAllFromPreferences failed:" qDebug() << "IosController::initialize : Error:" << [error.localizedDescription UTF8String];
<< [error.localizedDescription UTF8String] emit connectionStateChanged(Vpn::ConnectionState::Error);
<< "domain:" << [error.domain UTF8String] << "code:" << error.code;
ok = false; ok = false;
return; return;
} }
@@ -398,14 +397,8 @@ void IosController::vpnStatusDidChange(void *pNotification)
{ {
NETunnelProviderSession *session = (NETunnelProviderSession *)pNotification; NETunnelProviderSession *session = (NETunnelProviderSession *)pNotification;
if (!session) { if (session /* && session == TunnelManager.session */ ) {
return; qDebug() << "IosController::vpnStatusDidChange" << iosStatusToState(session.status) << session;
}
if (!m_currentTunnel || (NETunnelProviderSession *)m_currentTunnel.connection != session) {
return;
}
qDebug() << "IosController::vpnStatusDidChange" << iosStatusToState(session.status) << session;
if (session.status == NEVPNStatusDisconnected) { if (session.status == NEVPNStatusDisconnected) {
if (@available(iOS 16.0, *)) { if (@available(iOS 16.0, *)) {
@@ -519,6 +512,7 @@ void IosController::vpnStatusDidChange(void *pNotification)
m_statusRequestInFlight = false; m_statusRequestInFlight = false;
} }
emitConnectionStateIfChanged(nextState); emitConnectionStateIfChanged(nextState);
}
} }
void IosController::vpnConfigurationDidChange(void *pNotification) void IosController::vpnConfigurationDidChange(void *pNotification)
@@ -690,15 +684,6 @@ bool IosController::setupXray()
QJsonObject finalConfig; QJsonObject finalConfig;
finalConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1].toString()); finalConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1].toString());
finalConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2].toString()); finalConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2].toString());
finalConfig.insert(config_key::splitTunnelType, m_rawConfig[config_key::splitTunnelType]);
QJsonArray splitTunnelSites = m_rawConfig[config_key::splitTunnelSites].toArray();
for(int index = 0; index < splitTunnelSites.count(); index++) {
splitTunnelSites[index] = splitTunnelSites[index].toString().remove(" ");
}
finalConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
finalConfig.insert(config_key::config, xrayConfigStr); finalConfig.insert(config_key::config, xrayConfigStr);
QJsonDocument finalConfigDoc(finalConfig); QJsonDocument finalConfigDoc(finalConfig);
@@ -850,49 +835,39 @@ void IosController::startTunnel()
m_rxBytes = 0; m_rxBytes = 0;
m_txBytes = 0; m_txBytes = 0;
NETunnelProviderManager *tunnel = m_currentTunnel; [m_currentTunnel setEnabled:YES];
[tunnel setEnabled:YES];
dispatch_async(dispatch_get_main_queue(), ^{ [m_currentTunnel saveToPreferencesWithCompletionHandler:^(NSError *saveError) {
[tunnel saveToPreferencesWithCompletionHandler:^(NSError *saveError) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
if (saveError) {
qDebug().nospace() << "IosController::startTunnel" << protocolName << ": Connect " << protocolName
<< " Tunnel Save Error" << saveError.localizedDescription.UTF8String << " domain:"
<< saveError.domain.UTF8String << " code:" << saveError.code;
emit connectionStateChanged(Vpn::ConnectionState::Error);
return;
}
[tunnel loadFromPreferencesWithCompletionHandler:^(NSError *loadError) { if (saveError) {
dispatch_async(dispatch_get_main_queue(), ^{ qDebug().nospace() << "IosController::startTunnel" << protocolName << ": Connect " << protocolName << " Tunnel Save Error" << saveError.localizedDescription.UTF8String;
if (loadError) { emit connectionStateChanged(Vpn::ConnectionState::Error);
qDebug().nospace() << "IosController::startTunnel :" << tunnel.localizedDescription << protocolName return;
<< ": Connect " << protocolName << " Tunnel Load Error" }
<< loadError.localizedDescription.UTF8String;
emit connectionStateChanged(Vpn::ConnectionState::Error);
return;
}
NSError *startError = nil; [m_currentTunnel loadFromPreferencesWithCompletionHandler:^(NSError *loadError) {
qDebug() << iosStatusToState(tunnel.connection.status); if (loadError) {
qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << ": Connect " << protocolName << " Tunnel Load Error" << loadError.localizedDescription.UTF8String;
emit connectionStateChanged(Vpn::ConnectionState::Error);
return;
}
BOOL started = [tunnel.connection startVPNTunnelWithOptions:nil andReturnError:&startError]; NSError *startError = nil;
qDebug() << iosStatusToState(m_currentTunnel.connection.status);
if (!started || startError) { BOOL started = [m_currentTunnel.connection startVPNTunnelWithOptions:nil andReturnError:&startError];
qDebug().nospace() << "IosController::startTunnel :" << tunnel.localizedDescription << protocolName
<< " : Connect " << protocolName << " Tunnel Start Error" if (!started || startError) {
<< (startError ? startError.localizedDescription.UTF8String : ""); qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << " : Connect " << protocolName << " Tunnel Start Error"
emit connectionStateChanged(Vpn::ConnectionState::Error); << (startError ? startError.localizedDescription.UTF8String : "");
} else { emit connectionStateChanged(Vpn::ConnectionState::Error);
qDebug().nospace() << "IosController::startTunnel :" << tunnel.localizedDescription << protocolName } else {
<< " : Starting the tunnel succeeded"; qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << " : Starting the tunnel succeeded";
} }
}); }];
}]; });
}); }];
}];
});
} }
bool IosController::isOurManager(NETunnelProviderManager* manager) { bool IosController::isOurManager(NETunnelProviderManager* manager) {
@@ -984,6 +959,10 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
} }
bool IosController::shareText(const QStringList& filesToSend) { bool IosController::shareText(const QStringList& filesToSend) {
#if defined(Q_OS_TVOS)
Q_UNUSED(filesToSend)
return false;
#else
NSMutableArray *sharingItems = [NSMutableArray new]; NSMutableArray *sharingItems = [NSMutableArray new];
for (int i = 0; i < filesToSend.size(); i++) { for (int i = 0; i < filesToSend.size(); i++) {
@@ -992,7 +971,7 @@ bool IosController::shareText(const QStringList& filesToSend) {
} }
#if !MACOS_NE #if !MACOS_NE
UIViewController *qtController = getViewController(); UIViewController *qtController = getViewController();
if (!qtController) return; if (!qtController) return false;
UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:sharingItems applicationActivities:nil]; UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:sharingItems applicationActivities:nil];
#endif #endif
@@ -1016,23 +995,25 @@ bool IosController::shareText(const QStringList& filesToSend) {
wait.exec(); wait.exec();
return isAccepted; return isAccepted;
#endif
} }
QString IosController::openFile() { QString IosController::openFile() {
#if !MACOS_NE #if defined(Q_OS_TVOS)
return QString();
#elif !MACOS_NE
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.item"] inMode:UIDocumentPickerModeOpen]; UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.item"] inMode:UIDocumentPickerModeOpen];
DocumentPickerDelegate *documentPickerDelegate = [[DocumentPickerDelegate alloc] init]; DocumentPickerDelegate *documentPickerDelegate = [[DocumentPickerDelegate alloc] init];
documentPicker.delegate = documentPickerDelegate; documentPicker.delegate = documentPickerDelegate;
UIViewController *qtController = getViewController(); UIViewController *qtController = getViewController();
if (!qtController) return; if (!qtController) return QString();
[qtController presentViewController:documentPicker animated:YES completion:nil]; [qtController presentViewController:documentPicker animated:YES completion:nil];
#endif #endif
__block QString filePath; __block QString filePath;
#if !MACOS_NE #if !MACOS_NE && !defined(Q_OS_TVOS)
documentPickerDelegate.documentPickerClosedCallback = ^(NSString *path) { documentPickerDelegate.documentPickerClosedCallback = ^(NSString *path) {
if (path) { if (path) {
filePath = QString::fromUtf8(path.UTF8String); filePath = QString::fromUtf8(path.UTF8String);
@@ -1147,26 +1128,14 @@ void IosController::fetchProducts(const QStringList &productIds,
NSArray<NSString *> * _Nonnull invalidIdentifiers, NSArray<NSString *> * _Nonnull invalidIdentifiers,
NSError * _Nullable error) { NSError * _Nullable error) {
QList<QVariantMap> outProducts; QList<QVariantMap> outProducts;
for (NSDictionary *productInfo in products) { for (NSDictionary *p in products) {
QVariantMap productData; QVariantMap m;
productData["productId"] = QString::fromUtf8([productInfo[@"productId"] UTF8String]); m["productId"] = QString::fromUtf8([p[@"productId"] UTF8String]);
productData["title"] = QString::fromUtf8([productInfo[@"title"] UTF8String]); m["title"] = QString::fromUtf8([p[@"title"] UTF8String]);
productData["description"] = QString::fromUtf8([productInfo[@"description"] UTF8String]); m["description"] = QString::fromUtf8([p[@"description"] UTF8String]);
productData["price"] = QString::fromUtf8([productInfo[@"price"] UTF8String]); m["price"] = QString::fromUtf8([p[@"price"] UTF8String]);
if (productInfo[@"displayPrice"]) { m["currencyCode"] = QString::fromUtf8([p[@"currencyCode"] UTF8String]);
productData["displayPrice"] = QString::fromUtf8([productInfo[@"displayPrice"] UTF8String]); outProducts.push_back(m);
}
productData["currencyCode"] = QString::fromUtf8([productInfo[@"currencyCode"] UTF8String]);
if (productInfo[@"priceAmount"]) {
productData["priceAmount"] = [productInfo[@"priceAmount"] doubleValue];
}
if (productInfo[@"subscriptionBillingMonths"]) {
productData["subscriptionBillingMonths"] = [productInfo[@"subscriptionBillingMonths"] doubleValue];
}
if (productInfo[@"displayPricePerMonth"]) {
productData["displayPricePerMonth"] = QString::fromUtf8([productInfo[@"displayPricePerMonth"] UTF8String]);
}
outProducts.push_back(productData);
} }
QStringList invalid; QStringList invalid;
@@ -1,6 +1,7 @@
#import <NetworkExtension/NetworkExtension.h> #import <NetworkExtension/NetworkExtension.h>
#import <NetworkExtension/NETunnelProviderSession.h> #import <NetworkExtension/NETunnelProviderSession.h>
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#include <TargetConditionals.h>
#if !MACOS_NE #if !MACOS_NE
#include <UIKit/UIKit.h> #include <UIKit/UIKit.h>
@@ -21,7 +22,7 @@ class IosController;
@end @end
typedef void (^DocumentPickerClosedCallback)(NSString *path); typedef void (^DocumentPickerClosedCallback)(NSString *path);
#if !MACOS_NE #if !MACOS_NE && !TARGET_OS_TV
@interface DocumentPickerDelegate : NSObject <UIDocumentPickerDelegate> @interface DocumentPickerDelegate : NSObject <UIDocumentPickerDelegate>
@property (nonatomic, copy) DocumentPickerClosedCallback documentPickerClosedCallback; @property (nonatomic, copy) DocumentPickerClosedCallback documentPickerClosedCallback;
@@ -26,7 +26,7 @@
@end @end
#if !MACOS_NE #if !MACOS_NE && !TARGET_OS_TV
@implementation DocumentPickerDelegate @implementation DocumentPickerDelegate
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls { - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
+26 -13
View File
@@ -7,6 +7,24 @@
#import <UserNotifications/UserNotifications.h> #import <UserNotifications/UserNotifications.h>
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#if defined(Q_OS_TVOS)
IOSNotificationHandler::IOSNotificationHandler(QObject* parent) : NotificationHandler(parent) {}
IOSNotificationHandler::~IOSNotificationHandler() {}
void IOSNotificationHandler::notify(NotificationHandler::Message type,
const QString& title,
const QString& message,
int timerMsec) {
Q_UNUSED(type)
Q_UNUSED(title)
Q_UNUSED(message)
Q_UNUSED(timerMsec)
}
#else
#if !MACOS_NE #if !MACOS_NE
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
@@ -61,8 +79,6 @@ IOSNotificationHandler::~IOSNotificationHandler() { }
void IOSNotificationHandler::notify(NotificationHandler::Message type, const QString& title, void IOSNotificationHandler::notify(NotificationHandler::Message type, const QString& title,
const QString& message, int timerMsec) { const QString& message, int timerMsec) {
Q_UNUSED(type); Q_UNUSED(type);
// timerMsec is tray display hint on Windows, not a schedule delay — was wrongly used as seconds (CLI-570).
Q_UNUSED(timerMsec);
if (!m_delegate) { if (!m_delegate) {
return; return;
@@ -73,13 +89,11 @@ void IOSNotificationHandler::notify(NotificationHandler::Message type, const QSt
content.body = message.toNSString(); content.body = message.toNSString();
content.sound = [UNNotificationSound defaultSound]; content.sound = [UNNotificationSound defaultSound];
NSTimeInterval delay = 0.1; int timerSec = timerMsec / 1000;
UNTimeIntervalNotificationTrigger* trigger = UNTimeIntervalNotificationTrigger* trigger =
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:delay repeats:NO]; [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:timerSec repeats:NO];
NSString* requestId = [NSString stringWithFormat:@"amneziavpn.vpnstate.%lld", UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"amneziavpn"
(long long)([[NSDate date] timeIntervalSince1970] * 1000.0)];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:requestId
content:content content:content
trigger:trigger]; trigger:trigger];
@@ -147,7 +161,6 @@ IOSNotificationHandler::~IOSNotificationHandler() { }
void IOSNotificationHandler::notify(NotificationHandler::Message type, const QString& title, void IOSNotificationHandler::notify(NotificationHandler::Message type, const QString& title,
const QString& message, int timerMsec) { const QString& message, int timerMsec) {
Q_UNUSED(type); Q_UNUSED(type);
Q_UNUSED(timerMsec);
if (!m_delegate) { if (!m_delegate) {
return; return;
@@ -158,13 +171,11 @@ void IOSNotificationHandler::notify(NotificationHandler::Message type, const QSt
content.body = message.toNSString(); content.body = message.toNSString();
content.sound = [UNNotificationSound defaultSound]; content.sound = [UNNotificationSound defaultSound];
NSTimeInterval delay = 0.1; int timerSec = timerMsec / 1000;
UNTimeIntervalNotificationTrigger* trigger = UNTimeIntervalNotificationTrigger* trigger =
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:delay repeats:NO]; [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:timerSec repeats:NO];
NSString* requestId = [NSString stringWithFormat:@"amneziavpn.vpnstate.%lld", UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"amneziavpn"
(long long)([[NSDate date] timeIntervalSince1970] * 1000.0)];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:requestId
content:content content:content
trigger:trigger]; trigger:trigger];
@@ -179,3 +190,5 @@ void IOSNotificationHandler::notify(NotificationHandler::Message type, const QSt
}]; }];
} }
#endif #endif
#endif // Q_OS_TVOS
+6
View File
@@ -0,0 +1,6 @@
/*
* tvOS does not export these iOS runtime helpers used by Go cgo archives.
* WireGuardKitGo references them indirectly; provide no-op stubs for tvOS.
*/
void darwin_arm_init_mach_exception_handler(void) {}
void darwin_arm_init_thread_exception_port(void) {}
@@ -80,7 +80,7 @@ bool WireguardUtilsLinux::addInterface(const InterfaceConfig& config) {
QDir appPath(QCoreApplication::applicationDirPath()); QDir appPath(QCoreApplication::applicationDirPath());
QStringList wgArgs = {"-f", "amn0"}; QStringList wgArgs = {"-f", "amn0"};
m_tunnel.start(appPath.filePath("../../client/bin/wireguard-go"), wgArgs); m_tunnel.start(appPath.filePath("amneziawg-go"), wgArgs);
if (!m_tunnel.waitForStarted(WG_TUN_PROC_TIMEOUT)) { if (!m_tunnel.waitForStarted(WG_TUN_PROC_TIMEOUT)) {
logger.error() << "Unable to start tunnel process due to timeout"; logger.error() << "Unable to start tunnel process due to timeout";
m_tunnel.kill(); m_tunnel.kill();
@@ -79,7 +79,7 @@ bool WireguardUtilsMacos::addInterface(const InterfaceConfig& config) {
QDir appPath(QCoreApplication::applicationDirPath()); QDir appPath(QCoreApplication::applicationDirPath());
QStringList wgArgs = {"-f", "utun"}; QStringList wgArgs = {"-f", "utun"};
m_tunnel.start(appPath.filePath("wireguard-go"), wgArgs); m_tunnel.start(appPath.filePath("amneziawg-go"), wgArgs);
if (!m_tunnel.waitForStarted(WG_TUN_PROC_TIMEOUT)) { if (!m_tunnel.waitForStarted(WG_TUN_PROC_TIMEOUT)) {
logger.error() << "Unable to start tunnel process due to timeout"; logger.error() << "Unable to start tunnel process due to timeout";
m_tunnel.kill(); m_tunnel.kill();
@@ -1,8 +0,0 @@
#ifndef MACOS_NE_VPN_NOTIFICATION_H
#define MACOS_NE_VPN_NOTIFICATION_H
class QString;
void macosNePostVpnStateNotification(const QString &title, const QString &message);
#endif
@@ -1,91 +0,0 @@
#include "macos_ne_vpn_notification.h"
#include <QtGlobal>
#include <QString>
#import <Foundation/Foundation.h>
#import <UserNotifications/UserNotifications.h>
namespace {
@interface MacosNeVpnNotificationDelegate : NSObject <UNUserNotificationCenterDelegate>
@end
@implementation MacosNeVpnNotificationDelegate
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
Q_UNUSED(center)
Q_UNUSED(notification)
completionHandler(UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner);
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)(void))completionHandler
{
Q_UNUSED(center)
Q_UNUSED(response)
completionHandler();
}
@end
MacosNeVpnNotificationDelegate *delegateInstance()
{
static MacosNeVpnNotificationDelegate *d;
static dispatch_once_t once;
dispatch_once(&once, ^{
d = [[MacosNeVpnNotificationDelegate alloc] init];
});
return d;
}
void ensureNotificationCenterSetup()
{
static dispatch_once_t once;
dispatch_once(&once, ^{
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert |
UNAuthorizationOptionBadge)
completionHandler:^(BOOL granted, NSError *_Nullable error) {
Q_UNUSED(granted);
if (!error) {
center.delegate = delegateInstance();
}
}];
});
}
} // namespace
void macosNePostVpnStateNotification(const QString &title, const QString &message)
{
ensureNotificationCenterSetup();
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = title.toNSString();
content.body = message.toNSString();
content.sound = nil;
NSTimeInterval delay = 0.1;
UNTimeIntervalNotificationTrigger *trigger =
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:delay repeats:NO];
NSString *identifier =
[NSString stringWithFormat:@"amneziavpn.vpnstate.%lld", (long long)([[NSDate date] timeIntervalSince1970] * 1000.0)];
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier
content:content
trigger:trigger];
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center addNotificationRequest:request
withCompletionHandler:^(NSError *_Nullable error) {
if (error) {
NSLog(@"macosNePostVpnStateNotification failed: %@", error);
}
}];
}
+2 -2
View File
@@ -190,7 +190,7 @@ namespace amnezia
constexpr char defaultPort[] = "51820"; constexpr char defaultPort[] = "51820";
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(MACOS_NE) #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS) || defined(MACOS_NE)
constexpr char defaultMtu[] = "1280"; constexpr char defaultMtu[] = "1280";
#else #else
constexpr char defaultMtu[] = "1376"; constexpr char defaultMtu[] = "1376";
@@ -210,7 +210,7 @@ namespace amnezia
namespace awg namespace awg
{ {
constexpr char defaultPort[] = "55424"; constexpr char defaultPort[] = "55424";
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(MACOS_NE) #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS) || defined(MACOS_NE)
constexpr char defaultMtu[] = "1280"; constexpr char defaultMtu[] = "1280";
#else #else
constexpr char defaultMtu[] = "1376"; constexpr char defaultMtu[] = "1376";
+2 -2
View File
@@ -4,7 +4,7 @@
#include "core/errorstrings.h" #include "core/errorstrings.h"
#include "vpnprotocol.h" #include "vpnprotocol.h"
#if defined(Q_OS_WINDOWS) || defined(Q_OS_MACX) and !defined MACOS_NE || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)) #if defined(Q_OS_WINDOWS) || (defined(Q_OS_MACX) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)) || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID))
#include "openvpnovercloakprotocol.h" #include "openvpnovercloakprotocol.h"
#include "openvpnprotocol.h" #include "openvpnprotocol.h"
#include "shadowsocksvpnprotocol.h" #include "shadowsocksvpnprotocol.h"
@@ -114,7 +114,7 @@ VpnProtocol *VpnProtocol::factory(DockerContainer container, const QJsonObject &
#if defined(Q_OS_WINDOWS) #if defined(Q_OS_WINDOWS)
case DockerContainer::Ipsec: return new Ikev2Protocol(configuration); case DockerContainer::Ipsec: return new Ikev2Protocol(configuration);
#endif #endif
#if defined(Q_OS_WINDOWS) || defined(Q_OS_MACX) and !defined MACOS_NE || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)) #if defined(Q_OS_WINDOWS) || (defined(Q_OS_MACX) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)) || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID))
case DockerContainer::OpenVpn: return new OpenVpnProtocol(configuration); case DockerContainer::OpenVpn: return new OpenVpnProtocol(configuration);
case DockerContainer::Cloak: return new OpenVpnOverCloakProtocol(configuration); case DockerContainer::Cloak: return new OpenVpnOverCloakProtocol(configuration);
case DockerContainer::ShadowSocks: return new ShadowSocksVpnProtocol(configuration); case DockerContainer::ShadowSocks: return new ShadowSocksVpnProtocol(configuration);
+4 -4
View File
@@ -124,14 +124,14 @@ ErrorCode XrayProtocol::startTun2Socks()
m_tun2socksProcess->setProgram(PermittedProcess::Tun2Socks); m_tun2socksProcess->setProgram(PermittedProcess::Tun2Socks);
m_tun2socksProcess->setArguments({"-device", QString("tun://%1").arg(tunName), "-proxy", "socks5://127.0.0.1:10808" }); m_tun2socksProcess->setArguments({"-device", QString("tun://%1").arg(tunName), "-proxy", "socks5://127.0.0.1:10808" });
connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, [this]() { connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardError, this, [this]() {
auto readAllStandardOutput = m_tun2socksProcess->readAllStandardOutput(); auto readAllStandardError = m_tun2socksProcess->readAllStandardError();
if (!readAllStandardOutput.waitForFinished()) { if (!readAllStandardError.waitForFinished()) {
qWarning() << "Failed to read output from tun2socks"; qWarning() << "Failed to read output from tun2socks";
return; return;
} }
const QString line = readAllStandardOutput.returnValue(); const QString line = readAllStandardError.returnValue();
if (!line.contains("[TCP]") && !line.contains("[UDP]")) if (!line.contains("[TCP]") && !line.contains("[UDP]"))
qDebug() << "[tun2socks]:" << line; qDebug() << "[tun2socks]:" << line;
+1 -12
View File
@@ -27,12 +27,10 @@
<file>images/controls/folder-open.svg</file> <file>images/controls/folder-open.svg</file>
<file>images/controls/folder-search-2.svg</file> <file>images/controls/folder-search-2.svg</file>
<file>images/controls/gauge.svg</file> <file>images/controls/gauge.svg</file>
<file>images/controls/globe-2.svg</file>
<file>images/controls/github.svg</file> <file>images/controls/github.svg</file>
<file>images/controls/help-circle.svg</file> <file>images/controls/help-circle.svg</file>
<file>images/controls/history.svg</file> <file>images/controls/history.svg</file>
<file>images/controls/home.svg</file> <file>images/controls/home.svg</file>
<file>images/controls/infinity.svg</file>
<file>images/controls/info.svg</file> <file>images/controls/info.svg</file>
<file>images/controls/mail.svg</file> <file>images/controls/mail.svg</file>
<file>images/controls/map-pin.svg</file> <file>images/controls/map-pin.svg</file>
@@ -57,7 +55,6 @@
<file>images/controls/settings-news.svg</file> <file>images/controls/settings-news.svg</file>
<file>images/controls/share-2.svg</file> <file>images/controls/share-2.svg</file>
<file>images/controls/split-tunneling.svg</file> <file>images/controls/split-tunneling.svg</file>
<file>images/controls/smartphone.svg</file>
<file>images/controls/tag.svg</file> <file>images/controls/tag.svg</file>
<file>images/controls/telegram.svg</file> <file>images/controls/telegram.svg</file>
<file>images/controls/text-cursor.svg</file> <file>images/controls/text-cursor.svg</file>
@@ -136,13 +133,8 @@
<file>ui/qml/Components/HomeContainersListView.qml</file> <file>ui/qml/Components/HomeContainersListView.qml</file>
<file>ui/qml/Components/HomeSplitTunnelingDrawer.qml</file> <file>ui/qml/Components/HomeSplitTunnelingDrawer.qml</file>
<file>ui/qml/Components/InstalledAppsDrawer.qml</file> <file>ui/qml/Components/InstalledAppsDrawer.qml</file>
<file>ui/qml/Components/BenefitRow.qml</file>
<file>ui/qml/Components/BenefitsPanel.qml</file>
<file>ui/qml/Components/SubscriptionPlanCard.qml</file>
<file>ui/qml/Components/TermsAndPrivacyText.qml</file>
<file>ui/qml/Components/QuestionDrawer.qml</file> <file>ui/qml/Components/QuestionDrawer.qml</file>
<file>ui/qml/Components/SelectLanguageDrawer.qml</file> <file>ui/qml/Components/SelectLanguageDrawer.qml</file>
<file>ui/qml/Components/SubscriptionExpiredDrawer.qml</file>
<file>ui/qml/Components/ServersListView.qml</file> <file>ui/qml/Components/ServersListView.qml</file>
<file>ui/qml/Components/SettingsContainersListView.qml</file> <file>ui/qml/Components/SettingsContainersListView.qml</file>
<file>ui/qml/Components/TransportProtoSelector.qml</file> <file>ui/qml/Components/TransportProtoSelector.qml</file>
@@ -188,7 +180,6 @@
<file>ui/qml/Controls2/TextTypes/LabelTextType.qml</file> <file>ui/qml/Controls2/TextTypes/LabelTextType.qml</file>
<file>ui/qml/Controls2/TextTypes/ListItemTitleType.qml</file> <file>ui/qml/Controls2/TextTypes/ListItemTitleType.qml</file>
<file>ui/qml/Controls2/TextTypes/ParagraphTextType.qml</file> <file>ui/qml/Controls2/TextTypes/ParagraphTextType.qml</file>
<file>ui/qml/Controls2/TextTypes/BadgeTextType.qml</file>
<file>ui/qml/Controls2/TextTypes/SmallTextType.qml</file> <file>ui/qml/Controls2/TextTypes/SmallTextType.qml</file>
<file>ui/qml/Controls2/TopCloseButtonType.qml</file> <file>ui/qml/Controls2/TopCloseButtonType.qml</file>
<file>ui/qml/Controls2/VerticalRadioButton.qml</file> <file>ui/qml/Controls2/VerticalRadioButton.qml</file>
@@ -234,9 +225,7 @@
<file>ui/qml/Pages2/PageSettingsNewsDetail.qml</file> <file>ui/qml/Pages2/PageSettingsNewsDetail.qml</file>
<file>ui/qml/Pages2/PageProtocolAwgClientSettings.qml</file> <file>ui/qml/Pages2/PageProtocolAwgClientSettings.qml</file>
<file>ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml</file> <file>ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml</file>
<file>ui/qml/Pages2/PageSetupWizardApiFreeInfo.qml</file> <file>ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml</file>
<file>ui/qml/Pages2/PageSetupWizardApiPremiumInfo.qml</file>
<file>ui/qml/Pages2/PageSetupWizardApiTrialEmail.qml</file>
<file>ui/qml/Pages2/PageSetupWizardApiServicesList.qml</file> <file>ui/qml/Pages2/PageSetupWizardApiServicesList.qml</file>
<file>ui/qml/Pages2/PageSetupWizardConfigSource.qml</file> <file>ui/qml/Pages2/PageSetupWizardConfigSource.qml</file>
<file>ui/qml/Pages2/PageSetupWizardCredentials.qml</file> <file>ui/qml/Pages2/PageSetupWizardCredentials.qml</file>
+21 -14
View File
@@ -35,12 +35,13 @@ SecureQSettings::SecureQSettings(const QString &organization, const QString &app
} }
} }
m_settings.setValue("Conf/encrypted", true); m_settings.setValue("Conf/encrypted", true);
m_settings.sync();
} }
} }
QVariant SecureQSettings::value(const QString &key, const QVariant &defaultValue) const QVariant SecureQSettings::value(const QString &key, const QVariant &defaultValue) const
{ {
QMutexLocker locker(&m_mutex); QMutexLocker locker(&mutex);
if (m_cache.contains(key)) { if (m_cache.contains(key)) {
return m_cache.value(key); return m_cache.value(key);
@@ -84,7 +85,7 @@ QVariant SecureQSettings::value(const QString &key, const QVariant &defaultValue
void SecureQSettings::setValue(const QString &key, const QVariant &value) void SecureQSettings::setValue(const QString &key, const QVariant &value)
{ {
QMutexLocker locker(&m_mutex); QMutexLocker locker(&mutex);
if (encryptionRequired() && encryptedKeys.contains(key)) { if (encryptionRequired() && encryptedKeys.contains(key)) {
if (!getEncKey().isEmpty() && !getEncIv().isEmpty()) { if (!getEncKey().isEmpty() && !getEncIv().isEmpty()) {
@@ -106,20 +107,26 @@ void SecureQSettings::setValue(const QString &key, const QVariant &value)
} }
m_cache.insert(key, value); m_cache.insert(key, value);
sync();
} }
void SecureQSettings::remove(const QString &key) void SecureQSettings::remove(const QString &key)
{ {
QMutexLocker locker(&m_mutex); QMutexLocker locker(&mutex);
m_settings.remove(key); m_settings.remove(key);
m_cache.remove(key); m_cache.remove(key);
sync();
}
void SecureQSettings::sync()
{
m_settings.sync();
} }
QByteArray SecureQSettings::backupAppConfig() const QByteArray SecureQSettings::backupAppConfig() const
{ {
QMutexLocker locker(&m_mutex);
QJsonObject cfg; QJsonObject cfg;
const auto needToBackup = [this](const auto &key) { const auto needToBackup = [this](const auto &key) {
@@ -154,8 +161,6 @@ QByteArray SecureQSettings::backupAppConfig() const
bool SecureQSettings::restoreAppConfig(const QByteArray &json) bool SecureQSettings::restoreAppConfig(const QByteArray &json)
{ {
QMutexLocker locker(&m_mutex);
QJsonObject cfg = QJsonDocument::fromJson(json).object(); QJsonObject cfg = QJsonDocument::fromJson(json).object();
if (cfg.isEmpty()) if (cfg.isEmpty())
return false; return false;
@@ -168,16 +173,10 @@ bool SecureQSettings::restoreAppConfig(const QByteArray &json)
setValue(key, cfg.value(key).toVariant()); setValue(key, cfg.value(key).toVariant());
} }
sync();
return true; return true;
} }
void SecureQSettings::clearSettings()
{
QMutexLocker locker(&m_mutex);
m_settings.clear();
m_cache.clear();
}
QByteArray SecureQSettings::encryptText(const QByteArray &value) const QByteArray SecureQSettings::encryptText(const QByteArray &value) const
{ {
QSimpleCrypto::QBlockCipher cipher; QSimpleCrypto::QBlockCipher cipher;
@@ -295,3 +294,11 @@ void SecureQSettings::setSecTag(const QString &tag, const QByteArray &data)
qCritical() << "SecureQSettings::setSecTag Error:" << job->errorString(); qCritical() << "SecureQSettings::setSecTag Error:" << job->errorString();
} }
} }
void SecureQSettings::clearSettings()
{
QMutexLocker locker(&mutex);
m_settings.clear();
m_cache.clear();
sync();
}
+7 -6
View File
@@ -16,16 +16,14 @@ public:
explicit SecureQSettings(const QString &organization, const QString &application = QString(), explicit SecureQSettings(const QString &organization, const QString &application = QString(),
QObject *parent = nullptr); QObject *parent = nullptr);
QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const; Q_INVOKABLE QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const;
void setValue(const QString &key, const QVariant &value); Q_INVOKABLE void setValue(const QString &key, const QVariant &value);
void remove(const QString &key); void remove(const QString &key);
void sync();
QByteArray backupAppConfig() const; QByteArray backupAppConfig() const;
bool restoreAppConfig(const QByteArray &json); bool restoreAppConfig(const QByteArray &json);
void clearSettings();
private:
QByteArray encryptText(const QByteArray &value) const; QByteArray encryptText(const QByteArray &value) const;
QByteArray decryptText(const QByteArray &ba) const; QByteArray decryptText(const QByteArray &ba) const;
@@ -37,6 +35,9 @@ private:
static QByteArray getSecTag(const QString &tag); static QByteArray getSecTag(const QString &tag);
static void setSecTag(const QString &tag, const QByteArray &data); static void setSecTag(const QString &tag, const QByteArray &data);
void clearSettings();
private:
QSettings m_settings; QSettings m_settings;
mutable QHash<QString, QVariant> m_cache; mutable QHash<QString, QVariant> m_cache;
@@ -52,7 +53,7 @@ private:
const QByteArray magicString { "EncData" }; // Magic keyword used for mark encrypted QByteArray const QByteArray magicString { "EncData" }; // Magic keyword used for mark encrypted QByteArray
mutable QRecursiveMutex m_mutex; mutable QMutex mutex;
}; };
#endif // SECUREQSETTINGS_H #endif // SECUREQSETTINGS_H
+57 -34
View File
@@ -21,10 +21,10 @@ Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_N
{ {
// Import old settings // Import old settings
if (serversCount() == 0) { if (serversCount() == 0) {
QString user = m_settings.value("Server/userName").toString(); QString user = value("Server/userName").toString();
QString password = m_settings.value("Server/password").toString(); QString password = value("Server/password").toString();
QString serverName = m_settings.value("Server/serverName").toString(); QString serverName = value("Server/serverName").toString();
int port = m_settings.value("Server/serverPort").toInt(); int port = value("Server/serverPort").toInt();
if (!user.isEmpty() && !password.isEmpty() && !serverName.isEmpty()) { if (!user.isEmpty() && !password.isEmpty() && !serverName.isEmpty()) {
QJsonObject server; QJsonObject server;
@@ -222,7 +222,7 @@ QString Settings::nextAvailableServerName() const
void Settings::setSaveLogs(bool enabled) void Settings::setSaveLogs(bool enabled)
{ {
m_settings.setValue("Conf/saveLogs", enabled); setValue("Conf/saveLogs", enabled);
#ifndef Q_OS_ANDROID #ifndef Q_OS_ANDROID
if (!isSaveLogs()) { if (!isSaveLogs()) {
Logger::deInit(); Logger::deInit();
@@ -242,12 +242,12 @@ void Settings::setSaveLogs(bool enabled)
QDateTime Settings::getLogEnableDate() QDateTime Settings::getLogEnableDate()
{ {
return m_settings.value("Conf/logEnableDate").toDateTime(); return value("Conf/logEnableDate").toDateTime();
} }
void Settings::setLogEnableDate(QDateTime date) void Settings::setLogEnableDate(QDateTime date)
{ {
m_settings.setValue("Conf/logEnableDate", date); setValue("Conf/logEnableDate", date);
} }
QString Settings::routeModeString(RouteMode mode) const QString Settings::routeModeString(RouteMode mode) const
@@ -261,17 +261,17 @@ QString Settings::routeModeString(RouteMode mode) const
Settings::RouteMode Settings::routeMode() const Settings::RouteMode Settings::routeMode() const
{ {
return static_cast<RouteMode>(m_settings.value("Conf/routeMode", 0).toInt()); return static_cast<RouteMode>(value("Conf/routeMode", 0).toInt());
} }
bool Settings::isSitesSplitTunnelingEnabled() const bool Settings::isSitesSplitTunnelingEnabled() const
{ {
return m_settings.value("Conf/sitesSplitTunnelingEnabled", false).toBool(); return value("Conf/sitesSplitTunnelingEnabled", false).toBool();
} }
void Settings::setSitesSplitTunnelingEnabled(bool enabled) void Settings::setSitesSplitTunnelingEnabled(bool enabled)
{ {
m_settings.setValue("Conf/sitesSplitTunnelingEnabled", enabled); setValue("Conf/sitesSplitTunnelingEnabled", enabled);
} }
bool Settings::addVpnSite(RouteMode mode, const QString &site, const QString &ip) bool Settings::addVpnSite(RouteMode mode, const QString &site, const QString &ip)
@@ -359,12 +359,12 @@ void Settings::removeAllVpnSites(RouteMode mode)
QString Settings::primaryDns() const QString Settings::primaryDns() const
{ {
return m_settings.value("Conf/primaryDns", cloudFlareNs1).toString(); return value("Conf/primaryDns", cloudFlareNs1).toString();
} }
QString Settings::secondaryDns() const QString Settings::secondaryDns() const
{ {
return m_settings.value("Conf/secondaryDns", cloudFlareNs2).toString(); return value("Conf/secondaryDns", cloudFlareNs2).toString();
} }
void Settings::clearSettings() void Settings::clearSettings()
@@ -386,18 +386,18 @@ QString Settings::appsRouteModeString(AppsRouteMode mode) const
Settings::AppsRouteMode Settings::getAppsRouteMode() const Settings::AppsRouteMode Settings::getAppsRouteMode() const
{ {
return static_cast<AppsRouteMode>(m_settings.value("Conf/appsRouteMode", 0).toInt()); return static_cast<AppsRouteMode>(value("Conf/appsRouteMode", 0).toInt());
} }
void Settings::setAppsRouteMode(AppsRouteMode mode) void Settings::setAppsRouteMode(AppsRouteMode mode)
{ {
m_settings.setValue("Conf/appsRouteMode", mode); setValue("Conf/appsRouteMode", mode);
} }
QVector<InstalledAppInfo> Settings::getVpnApps(AppsRouteMode mode) const QVector<InstalledAppInfo> Settings::getVpnApps(AppsRouteMode mode) const
{ {
QVector<InstalledAppInfo> apps; QVector<InstalledAppInfo> apps;
auto appsArray = m_settings.value("Conf/" + appsRouteModeString(mode)).toJsonArray(); auto appsArray = value("Conf/" + appsRouteModeString(mode)).toJsonArray();
for (const auto &app : appsArray) { for (const auto &app : appsArray) {
InstalledAppInfo appInfo; InstalledAppInfo appInfo;
appInfo.appName = app.toObject().value("appName").toString(); appInfo.appName = app.toObject().value("appName").toString();
@@ -419,42 +419,43 @@ void Settings::setVpnApps(AppsRouteMode mode, const QVector<InstalledAppInfo> &a
appInfo.insert("appPath", app.appPath); appInfo.insert("appPath", app.appPath);
appsArray.push_back(appInfo); appsArray.push_back(appInfo);
} }
m_settings.setValue("Conf/" + appsRouteModeString(mode), appsArray); setValue("Conf/" + appsRouteModeString(mode), appsArray);
m_settings.sync();
} }
bool Settings::isAppsSplitTunnelingEnabled() const bool Settings::isAppsSplitTunnelingEnabled() const
{ {
return m_settings.value("Conf/appsSplitTunnelingEnabled", false).toBool(); return value("Conf/appsSplitTunnelingEnabled", false).toBool();
} }
void Settings::setAppsSplitTunnelingEnabled(bool enabled) void Settings::setAppsSplitTunnelingEnabled(bool enabled)
{ {
m_settings.setValue("Conf/appsSplitTunnelingEnabled", enabled); setValue("Conf/appsSplitTunnelingEnabled", enabled);
} }
bool Settings::isKillSwitchEnabled() const bool Settings::isKillSwitchEnabled() const
{ {
return m_settings.value("Conf/killSwitchEnabled", true).toBool(); return value("Conf/killSwitchEnabled", true).toBool();
} }
void Settings::setKillSwitchEnabled(bool enabled) void Settings::setKillSwitchEnabled(bool enabled)
{ {
m_settings.setValue("Conf/killSwitchEnabled", enabled); setValue("Conf/killSwitchEnabled", enabled);
} }
bool Settings::isStrictKillSwitchEnabled() const bool Settings::isStrictKillSwitchEnabled() const
{ {
return m_settings.value("Conf/strictKillSwitchEnabled", false).toBool(); return value("Conf/strictKillSwitchEnabled", false).toBool();
} }
void Settings::setStrictKillSwitchEnabled(bool enabled) void Settings::setStrictKillSwitchEnabled(bool enabled)
{ {
m_settings.setValue("Conf/strictKillSwitchEnabled", enabled); setValue("Conf/strictKillSwitchEnabled", enabled);
} }
QString Settings::getInstallationUuid(const bool needCreate) QString Settings::getInstallationUuid(const bool needCreate)
{ {
auto uuid = m_settings.value("Conf/installationUuid", "").toString(); auto uuid = value("Conf/installationUuid", "").toString();
if (needCreate && uuid.isEmpty()) { if (needCreate && uuid.isEmpty()) {
uuid = QUuid::createUuid().toString(); uuid = QUuid::createUuid().toString();
@@ -475,7 +476,7 @@ QString Settings::getInstallationUuid(const bool needCreate)
void Settings::setInstallationUuid(const QString &uuid) void Settings::setInstallationUuid(const QString &uuid)
{ {
m_settings.setValue("Conf/installationUuid", uuid); setValue("Conf/installationUuid", uuid);
} }
ServerCredentials Settings::defaultServerCredentials() const ServerCredentials Settings::defaultServerCredentials() const
@@ -496,6 +497,28 @@ ServerCredentials Settings::serverCredentials(int index) const
return credentials; return credentials;
} }
QVariant Settings::value(const QString &key, const QVariant &defaultValue) const
{
QVariant returnValue;
if (QThread::currentThread() == QCoreApplication::instance()->thread()) {
returnValue = m_settings.value(key, defaultValue);
} else {
QMetaObject::invokeMethod(&m_settings, "value", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, returnValue),
Q_ARG(const QString &, key), Q_ARG(const QVariant &, defaultValue));
}
return returnValue;
}
void Settings::setValue(const QString &key, const QVariant &value)
{
if (QThread::currentThread() == QCoreApplication::instance()->thread()) {
m_settings.setValue(key, value);
} else {
QMetaObject::invokeMethod(&m_settings, "setValue", Qt::BlockingQueuedConnection, Q_ARG(const QString &, key),
Q_ARG(const QVariant &, value));
}
}
void Settings::resetGatewayEndpoint() void Settings::resetGatewayEndpoint()
{ {
m_gatewayEndpoint = gatewayEndpoint; m_gatewayEndpoint = gatewayEndpoint;
@@ -518,50 +541,50 @@ QString Settings::getGatewayEndpoint(bool isTestPurchase)
bool Settings::isDevGatewayEnv(bool isTestPurchase) bool Settings::isDevGatewayEnv(bool isTestPurchase)
{ {
return isTestPurchase ? true : m_settings.value("Conf/devGatewayEnv", false).toBool(); return isTestPurchase ? true : value("Conf/devGatewayEnv", false).toBool();
} }
void Settings::toggleDevGatewayEnv(bool enabled) void Settings::toggleDevGatewayEnv(bool enabled)
{ {
m_settings.setValue("Conf/devGatewayEnv", enabled); setValue("Conf/devGatewayEnv", enabled);
} }
bool Settings::isHomeAdLabelVisible() bool Settings::isHomeAdLabelVisible()
{ {
return m_settings.value("Conf/homeAdLabelVisible", true).toBool(); return value("Conf/homeAdLabelVisible", true).toBool();
} }
void Settings::disableHomeAdLabel() void Settings::disableHomeAdLabel()
{ {
m_settings.setValue("Conf/homeAdLabelVisible", false); setValue("Conf/homeAdLabelVisible", false);
} }
bool Settings::isPremV1MigrationReminderActive() bool Settings::isPremV1MigrationReminderActive()
{ {
return m_settings.value("Conf/premV1MigrationReminderActive", true).toBool(); return value("Conf/premV1MigrationReminderActive", true).toBool();
} }
void Settings::disablePremV1MigrationReminder() void Settings::disablePremV1MigrationReminder()
{ {
m_settings.setValue("Conf/premV1MigrationReminderActive", false); setValue("Conf/premV1MigrationReminderActive", false);
} }
QStringList Settings::allowedDnsServers() const QStringList Settings::allowedDnsServers() const
{ {
return m_settings.value("Conf/allowedDnsServers").toStringList(); return value("Conf/allowedDnsServers").toStringList();
} }
void Settings::setAllowedDnsServers(const QStringList &servers) void Settings::setAllowedDnsServers(const QStringList &servers)
{ {
m_settings.setValue("Conf/allowedDnsServers", servers); setValue("Conf/allowedDnsServers", servers);
} }
QStringList Settings::readNewsIds() const QStringList Settings::readNewsIds() const
{ {
return m_settings.value("News/readIds").toStringList(); return value("News/readIds").toStringList();
} }
void Settings::setReadNewsIds(const QStringList &ids) void Settings::setReadNewsIds(const QStringList &ids)
{ {
m_settings.setValue("News/readIds", ids); setValue("News/readIds", ids);
} }
+25 -21
View File
@@ -29,11 +29,11 @@ public:
QJsonArray serversArray() const QJsonArray serversArray() const
{ {
return QJsonDocument::fromJson(m_settings.value("Servers/serversList").toByteArray()).array(); return QJsonDocument::fromJson(value("Servers/serversList").toByteArray()).array();
} }
void setServersArray(const QJsonArray &servers) void setServersArray(const QJsonArray &servers)
{ {
m_settings.setValue("Servers/serversList", QJsonDocument(servers).toJson()); setValue("Servers/serversList", QJsonDocument(servers).toJson());
} }
// Servers section // Servers section
@@ -45,11 +45,11 @@ public:
int defaultServerIndex() const int defaultServerIndex() const
{ {
return m_settings.value("Servers/defaultServerIndex", 0).toInt(); return value("Servers/defaultServerIndex", 0).toInt();
} }
void setDefaultServer(int index) void setDefaultServer(int index)
{ {
m_settings.setValue("Servers/defaultServerIndex", index); setValue("Servers/defaultServerIndex", index);
} }
QJsonObject defaultServer() const QJsonObject defaultServer() const
{ {
@@ -78,34 +78,34 @@ public:
// App settings section // App settings section
bool isAutoConnect() const bool isAutoConnect() const
{ {
return m_settings.value("Conf/autoConnect", false).toBool(); return value("Conf/autoConnect", false).toBool();
} }
void setAutoConnect(bool enabled) void setAutoConnect(bool enabled)
{ {
m_settings.setValue("Conf/autoConnect", enabled); setValue("Conf/autoConnect", enabled);
} }
bool isStartMinimized() const bool isStartMinimized() const
{ {
return m_settings.value("Conf/startMinimized", false).toBool(); return value("Conf/startMinimized", false).toBool();
} }
void setStartMinimized(bool enabled) void setStartMinimized(bool enabled)
{ {
m_settings.setValue("Conf/startMinimized", enabled); setValue("Conf/startMinimized", enabled);
} }
bool isNewsNotifications() const bool isNewsNotifications() const
{ {
return m_settings.value("Conf/newsNotifications", true).toBool(); return value("Conf/newsNotifications", true).toBool();
} }
void setNewsNotifications(bool enabled) void setNewsNotifications(bool enabled)
{ {
m_settings.setValue("Conf/newsNotifications", enabled); setValue("Conf/newsNotifications", enabled);
} }
bool isSaveLogs() const bool isSaveLogs() const
{ {
return m_settings.value("Conf/saveLogs", false).toBool(); return value("Conf/saveLogs", false).toBool();
} }
void setSaveLogs(bool enabled); void setSaveLogs(bool enabled);
@@ -122,18 +122,19 @@ public:
QString routeModeString(RouteMode mode) const; QString routeModeString(RouteMode mode) const;
RouteMode routeMode() const; RouteMode routeMode() const;
void setRouteMode(RouteMode mode) { m_settings.setValue("Conf/routeMode", mode); } void setRouteMode(RouteMode mode) { setValue("Conf/routeMode", mode); }
bool isSitesSplitTunnelingEnabled() const; bool isSitesSplitTunnelingEnabled() const;
void setSitesSplitTunnelingEnabled(bool enabled); void setSitesSplitTunnelingEnabled(bool enabled);
QVariantMap vpnSites(RouteMode mode) const QVariantMap vpnSites(RouteMode mode) const
{ {
return m_settings.value("Conf/" + routeModeString(mode)).toMap(); return value("Conf/" + routeModeString(mode)).toMap();
} }
void setVpnSites(RouteMode mode, const QVariantMap &sites) void setVpnSites(RouteMode mode, const QVariantMap &sites)
{ {
m_settings.setValue("Conf/" + routeModeString(mode), sites); setValue("Conf/" + routeModeString(mode), sites);
m_settings.sync();
} }
bool addVpnSite(RouteMode mode, const QString &site, const QString &ip = ""); bool addVpnSite(RouteMode mode, const QString &site, const QString &ip = "");
void addVpnSites(RouteMode mode, const QMap<QString, QString> &sites); // map <site, ip> void addVpnSites(RouteMode mode, const QMap<QString, QString> &sites); // map <site, ip>
@@ -146,11 +147,11 @@ public:
bool useAmneziaDns() const bool useAmneziaDns() const
{ {
return m_settings.value("Conf/useAmneziaDns", true).toBool(); return value("Conf/useAmneziaDns", true).toBool();
} }
void setUseAmneziaDns(bool enabled) void setUseAmneziaDns(bool enabled)
{ {
m_settings.setValue("Conf/useAmneziaDns", enabled); setValue("Conf/useAmneziaDns", enabled);
} }
QString primaryDns() const; QString primaryDns() const;
@@ -159,13 +160,13 @@ public:
// QString primaryDns() const { return m_primaryDns; } // QString primaryDns() const { return m_primaryDns; }
void setPrimaryDns(const QString &primaryDns) void setPrimaryDns(const QString &primaryDns)
{ {
m_settings.setValue("Conf/primaryDns", primaryDns); setValue("Conf/primaryDns", primaryDns);
} }
// QString secondaryDns() const { return m_secondaryDns; } // QString secondaryDns() const { return m_secondaryDns; }
void setSecondaryDns(const QString &secondaryDns) void setSecondaryDns(const QString &secondaryDns)
{ {
m_settings.setValue("Conf/secondaryDns", secondaryDns); setValue("Conf/secondaryDns", secondaryDns);
} }
// static constexpr char openNicNs5[] = "94.103.153.176"; // static constexpr char openNicNs5[] = "94.103.153.176";
@@ -187,16 +188,16 @@ public:
}; };
void setAppLanguage(QLocale locale) void setAppLanguage(QLocale locale)
{ {
m_settings.setValue("Conf/appLanguage", locale.name()); setValue("Conf/appLanguage", locale.name());
}; };
bool isScreenshotsEnabled() const bool isScreenshotsEnabled() const
{ {
return m_settings.value("Conf/screenshotsEnabled", true).toBool(); return value("Conf/screenshotsEnabled", true).toBool();
} }
void setScreenshotsEnabled(bool enabled) void setScreenshotsEnabled(bool enabled)
{ {
m_settings.setValue("Conf/screenshotsEnabled", enabled); setValue("Conf/screenshotsEnabled", enabled);
emit screenshotsEnabledChanged(enabled); emit screenshotsEnabledChanged(enabled);
} }
@@ -254,6 +255,9 @@ signals:
void settingsCleared(); void settingsCleared();
private: private:
QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const;
void setValue(const QString &key, const QVariant &value);
void setInstallationUuid(const QString &uuid); void setInstallationUuid(const QString &uuid);
mutable SecureQSettings m_settings; mutable SecureQSettings m_settings;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+56 -56
View File
@@ -199,7 +199,7 @@
<message> <message>
<location filename="../ui/models/api/apiServicesModel.cpp" line="116"/> <location filename="../ui/models/api/apiServicesModel.cpp" line="116"/>
<source>%1 $</source> <source>%1 $</source>
<translation>%1 $</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/models/api/apiServicesModel.cpp" line="118"/> <location filename="../ui/models/api/apiServicesModel.cpp" line="118"/>
@@ -672,32 +672,32 @@ Thank you for staying with us!</source>
<translation>Порт</translation> <translation>Порт</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="422"/> <location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="418"/>
<source>Save</source> <source>Save</source>
<translation>Сохранить</translation> <translation>Сохранить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="431"/> <location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="427"/>
<source>Save settings?</source> <source>Save settings?</source>
<translation>Сохранить настройки?</translation> <translation>Сохранить настройки?</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="432"/> <location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="428"/>
<source>Only the settings for this device will be changed</source> <source>Only the settings for this device will be changed</source>
<translation>Будут изменены настройки только для этого устройства</translation> <translation>Будут изменены настройки только для этого устройства</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="433"/> <location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="429"/>
<source>Continue</source> <source>Continue</source>
<translation>Продолжить</translation> <translation>Продолжить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="434"/> <location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="430"/>
<source>Cancel</source> <source>Cancel</source>
<translation>Отменить</translation> <translation>Отменить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="438"/> <location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="434"/>
<source>Unable change settings while there is an active connection</source> <source>Unable change settings while there is an active connection</source>
<translation>Невозможно изменить настройки во время активного соединения</translation> <translation>Невозможно изменить настройки во время активного соединения</translation>
</message> </message>
@@ -1651,7 +1651,7 @@ Thank you for staying with us!</source>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="204"/> <location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="204"/>
<source>mailto:support@amnezia.org</source> <source>mailto:support@amnezia.org</source>
<translation>mailto:support@amnezia.org</translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="211"/> <location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="211"/>
@@ -1775,72 +1775,72 @@ Thank you for staying with us!</source>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="22"/> <location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="22"/>
<source>Windows</source> <source>Windows</source>
<translation>Windows</translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="29"/> <location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="29"/>
<source>macOS</source> <source>macOS</source>
<translation>macOS</translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="36"/> <location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="36"/>
<source>Android</source> <source>Android</source>
<translation>Android</translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="43"/> <location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="43"/>
<source>AndroidTV</source> <source>AndroidTV</source>
<translation>Android TV</translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="50"/> <location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="50"/>
<source>iOS</source> <source>iOS</source>
<translation>iOS</translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="57"/> <location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="57"/>
<source>Linux</source> <source>Linux</source>
<translation>Linux</translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="64"/> <location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="64"/>
<source>Routers</source> <source>Routers</source>
<translation>Маршрутизаторы</translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="23"/> <location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="23"/>
<source>documentation/instructions/connect-amnezia-premium#windows</source> <source>documentation/instructions/connect-amnezia-premium#windows</source>
<translation>documentation/instructions/connect-amnezia-premium#windows</translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="30"/> <location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="30"/>
<source>documentation/instructions/connect-amnezia-premium#macos</source> <source>documentation/instructions/connect-amnezia-premium#macos</source>
<translation>documentation/instructions/connect-amnezia-premium#macos</translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="37"/> <location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="37"/>
<source>documentation/instructions/connect-amnezia-premium#android</source> <source>documentation/instructions/connect-amnezia-premium#android</source>
<translation>documentation/instructions/connect-amnezia-premium#android</translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="44"/> <location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="44"/>
<source>documentation/instructions/android_tv_connect/</source> <source>documentation/instructions/android_tv_connect/</source>
<translation>documentation/instructions/android_tv_connect/</translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="51"/> <location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="51"/>
<source>documentation/instructions/connect-amnezia-premium#ios</source> <source>documentation/instructions/connect-amnezia-premium#ios</source>
<translation>documentation/instructions/connect-amnezia-premium#ios</translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="58"/> <location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="58"/>
<source>documentation/instructions/connect-amnezia-premium#linux</source> <source>documentation/instructions/connect-amnezia-premium#linux</source>
<translation>documentation/instructions/connect-amnezia-premium#linux</translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="65"/> <location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="65"/>
<source>documentation/instructions/connect-amnezia-premium#routers</source> <source>documentation/instructions/connect-amnezia-premium#routers</source>
<translation>documentation/instructions/connect-amnezia-premium#routers</translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="101"/> <location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="101"/>
@@ -2111,7 +2111,7 @@ Thank you for staying with us!</source>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiSupport.qml" line="22"/> <location filename="../ui/qml/Pages2/PageSettingsApiSupport.qml" line="22"/>
<source>Telegram</source> <source>Telegram</source>
<translation>Telegram</translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiSupport.qml" line="30"/> <location filename="../ui/qml/Pages2/PageSettingsApiSupport.qml" line="30"/>
@@ -2141,7 +2141,7 @@ Thank you for staying with us!</source>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiSupport.qml" line="110"/> <location filename="../ui/qml/Pages2/PageSettingsApiSupport.qml" line="110"/>
<source>Support tag</source> <source>Support tag</source>
<translation>Идентификатор поддержки</translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiSupport.qml" line="120"/> <location filename="../ui/qml/Pages2/PageSettingsApiSupport.qml" line="120"/>
@@ -2272,12 +2272,12 @@ Thank you for staying with us!</source>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApplication.qml" line="180"/> <location filename="../ui/qml/Pages2/PageSettingsApplication.qml" line="180"/>
<source>News Notification</source> <source>News Notification</source>
<translation>Уведомления о новостях</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApplication.qml" line="181"/> <location filename="../ui/qml/Pages2/PageSettingsApplication.qml" line="181"/>
<source>Show a notification icon for unread news</source> <source>Show notification icon when has unread news</source>
<translation>Показывать значок уведомления, если есть непрочитанные новости</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApplication.qml" line="221"/> <location filename="../ui/qml/Pages2/PageSettingsApplication.qml" line="221"/>
@@ -3115,17 +3115,17 @@ Thank you for staying with us!</source>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="113"/> <location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="113"/>
<source>Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.</source> <source>Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.</source>
<translation>Списание с Apple ID при подтверждении. Продление автоматическое, если автопродление не отключено минимум за 24 часа до окончания периода. Управление в настройках Apple ID.</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="125"/> <location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="125"/>
<source>Subscribe Now</source> <source>Subscribe Now</source>
<translation>Подписаться сейчас</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="158"/> <location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="158"/>
<source>By continuing, you agree to the &lt;a href=&quot;%1&quot; style=&quot;color: #FBB26A;&quot;&gt;Terms of Use&lt;/a&gt; and &lt;a href=&quot;%2&quot; style=&quot;color: #FBB26A;&quot;&gt;Privacy Policy&lt;/a&gt;</source> <source>By continuing, you agree to the &lt;a href=&quot;%1&quot; style=&quot;color: #FBB26A;&quot;&gt;Terms of Use&lt;/a&gt; and &lt;a href=&quot;%2&quot; style=&quot;color: #FBB26A;&quot;&gt;Privacy Policy&lt;/a&gt;</source>
<translation>Продолжая, вы соглашаетесь с &lt;a href=&quot;%1&quot; style=&quot;color: #FBB26A;&quot;&gt;Условиями использования&lt;/a&gt; и &lt;a href=&quot;%2&quot; style=&quot;color: #FBB26A;&quot;&gt;Политикой конфиденциальности&lt;/a&gt;</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="186"/> <location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="186"/>
@@ -3697,7 +3697,7 @@ Thank you for staying with us!</source>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="270"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="270"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="572"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="566"/>
<source>Users</source> <source>Users</source>
<translation>Пользователи</translation> <translation>Пользователи</translation>
</message> </message>
@@ -3707,72 +3707,72 @@ Thank you for staying with us!</source>
<translation>Имя пользователя</translation> <translation>Имя пользователя</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="588"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="582"/>
<source>Search</source> <source>Search</source>
<translation>Поиск</translation> <translation>Поиск</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="717"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="711"/>
<source>Creation date: %1</source> <source>Creation date: %1</source>
<translation>Дата создания: %1</translation> <translation>Дата создания: %1</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="729"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="723"/>
<source>Latest handshake: %1</source> <source>Latest handshake: %1</source>
<translation>Последнее рукопожатие: %1</translation> <translation>Последнее рукопожатие: %1</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="741"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="735"/>
<source>Data received: %1</source> <source>Data received: %1</source>
<translation>Получено данных: %1</translation> <translation>Получено данных: %1</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="753"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="747"/>
<source>Data sent: %1</source> <source>Data sent: %1</source>
<translation>Отправлено данных: %1</translation> <translation>Отправлено данных: %1</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="763"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="757"/>
<source>Allowed IPs: %1</source> <source>Allowed IPs: %1</source>
<translation>Разрешенные подсети: %1</translation> <translation>Разрешенные подсети: %1</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="778"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="772"/>
<source>Rename</source> <source>Rename</source>
<translation>Переименовать</translation> <translation>Переименовать</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="803"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="797"/>
<source>Client name</source> <source>Client name</source>
<translation>Имя клиента</translation> <translation>Имя клиента</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="814"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="808"/>
<source>Save</source> <source>Save</source>
<translation>Сохранить</translation> <translation>Сохранить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="850"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="844"/>
<source>Revoke</source> <source>Revoke</source>
<translation>Отозвать</translation> <translation>Отозвать</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="853"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="847"/>
<source>Revoke the config for a user - %1?</source> <source>Revoke the config for a user - %1?</source>
<translation>Отозвать конфигурацию для пользователя - %1?</translation> <translation>Отозвать конфигурацию для пользователя - %1?</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="854"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="848"/>
<source>The user will no longer be able to connect to your server.</source> <source>The user will no longer be able to connect to your server.</source>
<translation>Пользователь больше не сможет подключаться к вашему серверу.</translation> <translation>Пользователь больше не сможет подключаться к вашему серверу.</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="855"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="849"/>
<source>Continue</source> <source>Continue</source>
<translation>Продолжить</translation> <translation>Продолжить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="856"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="850"/>
<source>Cancel</source> <source>Cancel</source>
<translation>Отменить</translation> <translation>Отменить</translation>
</message> </message>
@@ -3795,7 +3795,7 @@ Thank you for staying with us!</source>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="220"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="220"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="554"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="548"/>
<source>Share</source> <source>Share</source>
<translation>Поделиться</translation> <translation>Поделиться</translation>
</message> </message>
@@ -4241,7 +4241,7 @@ Thank you for staying with us!</source>
<message> <message>
<location filename="../core/errorstrings.cpp" line="32"/> <location filename="../core/errorstrings.cpp" line="32"/>
<source>Server error: Linux kernel is too old</source> <source>Server error: Linux kernel is too old</source>
<translation>Ошибка сервера: ядро Linux слишком старое</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../core/errorstrings.cpp" line="35"/> <location filename="../core/errorstrings.cpp" line="35"/>
@@ -5020,47 +5020,47 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<context> <context>
<name>SitesController</name> <name>SitesController</name>
<message> <message>
<location filename="../ui/controllers/sitesController.cpp" line="22"/> <location filename="../ui/controllers/sitesController.cpp" line="24"/>
<source>Hostname not look like ip adress or domain name</source> <source>Hostname not look like ip adress or domain name</source>
<translation>Имя хоста не похоже на IP-адрес или доменное имя</translation> <translation>Имя хоста не похоже на IP-адрес или доменное имя</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/sitesController.cpp" line="52"/> <location filename="../ui/controllers/sitesController.cpp" line="66"/>
<source>New site added: %1</source> <source>New site added: %1</source>
<translation>Добавлен новый сайт: %1</translation> <translation>Добавлен новый сайт: %1</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/sitesController.cpp" line="61"/> <location filename="../ui/controllers/sitesController.cpp" line="78"/>
<source>Site removed: %1</source> <source>Site removed: %1</source>
<translation>Сайт удален: %1</translation> <translation>Сайт удален: %1</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/sitesController.cpp" line="68"/> <location filename="../ui/controllers/sitesController.cpp" line="85"/>
<source>Site list cleared!</source> <source>Site list cleared!</source>
<translation>Список сайтов очищен!</translation> <translation>Список сайтов очищен!</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/sitesController.cpp" line="75"/> <location filename="../ui/controllers/sitesController.cpp" line="92"/>
<source>Can&apos;t open file: %1</source> <source>Can&apos;t open file: %1</source>
<translation>Невозможно открыть файл: %1</translation> <translation>Невозможно открыть файл: %1</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/sitesController.cpp" line="81"/> <location filename="../ui/controllers/sitesController.cpp" line="98"/>
<source>Failed to parse JSON data from file: %1</source> <source>Failed to parse JSON data from file: %1</source>
<translation>Не удалось разобрать JSON-данные из файла: %1</translation> <translation>Не удалось разобрать JSON-данные из файла: %1</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/sitesController.cpp" line="86"/> <location filename="../ui/controllers/sitesController.cpp" line="103"/>
<source>The JSON data is not an array in file: %1</source> <source>The JSON data is not an array in file: %1</source>
<translation>JSON-данные не являются массивом в файле: %1</translation> <translation>JSON-данные не являются массивом в файле: %1</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/sitesController.cpp" line="114"/> <location filename="../ui/controllers/sitesController.cpp" line="133"/>
<source>Import completed</source> <source>Import completed</source>
<translation>Импорт завершен</translation> <translation>Импорт завершен</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/sitesController.cpp" line="133"/> <location filename="../ui/controllers/sitesController.cpp" line="152"/>
<source>Export completed</source> <source>Export completed</source>
<translation>Экспорт завершен</translation> <translation>Экспорт завершен</translation>
</message> </message>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+126 -407
View File
@@ -9,14 +9,9 @@
#include "ui/controllers/systemController.h" #include "ui/controllers/systemController.h"
#include "version.h" #include "version.h"
#include <QClipboard> #include <QClipboard>
#include <QCoreApplication>
#include <QDebug> #include <QDebug>
#include <QEventLoop> #include <QEventLoop>
#include <QHash>
#include <QJsonArray>
#include <QSet> #include <QSet>
#include <QVariantMap>
#include <limits>
#include "platforms/ios/ios_controller.h" #include "platforms/ios/ios_controller.h"
@@ -44,15 +39,6 @@ namespace
constexpr char serviceInfo[] = "service_info"; constexpr char serviceInfo[] = "service_info";
constexpr char serviceProtocol[] = "service_protocol"; constexpr char serviceProtocol[] = "service_protocol";
constexpr char services[] = "services";
constexpr char serviceDescription[] = "service_description";
constexpr char subscriptionPlans[] = "subscription_plans";
constexpr char storeProductId[] = "store_product_id";
constexpr char priceLabel[] = "price_label";
constexpr char subtitle[] = "subtitle";
constexpr char isTrial[] = "is_trial";
constexpr char minPriceLabel[] = "min_price_label";
constexpr char apiPayload[] = "api_payload"; constexpr char apiPayload[] = "api_payload";
constexpr char keyPayload[] = "key_payload"; constexpr char keyPayload[] = "key_payload";
@@ -61,6 +47,9 @@ namespace
constexpr char config[] = "config"; constexpr char config[] = "config";
constexpr char subscription[] = "subscription";
constexpr char endDate[] = "end_date";
constexpr char isConnectEvent[] = "is_connect_event"; constexpr char isConnectEvent[] = "is_connect_event";
} }
@@ -252,190 +241,13 @@ namespace
return ErrorCode::NoError; return ErrorCode::NoError;
} }
#if defined(Q_OS_IOS) || defined(MACOS_NE)
struct StoreKitPlanQuote {
QString displayPrice;
double priceAmount = 0.0;
double subscriptionBillingMonths = 0.0;
QString displayPricePerMonth;
};
constexpr double kOneMonthThreshold = 1.0 + 1e-6;
constexpr double kMonthsFallbackThreshold = 1e-6;
constexpr double kMonthlyPriceEpsilon = 1e-9;
QStringList collectPremiumStoreProductIds(const QJsonArray &services)
{
QStringList productIds;
QSet<QString> seenProductIds;
for (const QJsonValue &serviceValue : services) {
const QJsonObject serviceObject = serviceValue.toObject();
if (serviceObject.value(configKey::serviceType).toString() != serviceType::amneziaPremium) {
continue;
}
const QJsonArray subscriptionPlans =
serviceObject.value(configKey::serviceDescription).toObject().value(configKey::subscriptionPlans).toArray();
for (const QJsonValue &planValue : subscriptionPlans) {
if (!planValue.isObject()) {
continue;
}
const QString storeProductId = planValue.toObject().value(configKey::storeProductId).toString();
if (storeProductId.isEmpty() || seenProductIds.contains(storeProductId)) {
continue;
}
seenProductIds.insert(storeProductId);
productIds.append(storeProductId);
}
}
return productIds;
}
QHash<QString, StoreKitPlanQuote> buildStoreKitQuoteMap(const QList<QVariantMap> &fetchedProducts)
{
QHash<QString, StoreKitPlanQuote> quotesByProductId;
quotesByProductId.reserve(fetchedProducts.size());
for (const QVariantMap &productInfo : fetchedProducts) {
const QString productId = productInfo.value(QStringLiteral("productId")).toString();
if (productId.isEmpty()) {
continue;
}
QString displayPrice = productInfo.value(QStringLiteral("displayPrice")).toString();
if (displayPrice.isEmpty()) {
const QString price = productInfo.value(QStringLiteral("price")).toString();
const QString currencyCode = productInfo.value(QStringLiteral("currencyCode")).toString();
displayPrice = currencyCode.isEmpty() ? price : (price + QLatin1Char(' ') + currencyCode);
}
StoreKitPlanQuote quote;
quote.displayPrice = displayPrice;
quote.priceAmount = productInfo.value(QStringLiteral("priceAmount")).toDouble();
quote.subscriptionBillingMonths = productInfo.value(QStringLiteral("subscriptionBillingMonths")).toDouble();
quote.displayPricePerMonth = productInfo.value(QStringLiteral("displayPricePerMonth")).toString();
quotesByProductId.insert(productId, quote);
}
return quotesByProductId;
}
void mergeStoreKitPricesIntoPremiumPlans(QJsonObject &data)
{
QJsonArray services = data.value(configKey::services).toArray();
if (services.isEmpty()) {
return;
}
const QStringList productIds = collectPremiumStoreProductIds(services);
if (productIds.isEmpty()) {
qInfo().noquote() << "[IAP] No store_product_id in premium plans; skip StoreKit merge into services payload";
return;
}
QList<QVariantMap> fetchedProducts;
QEventLoop loop;
IosController::Instance()->fetchProducts(productIds,
[&](const QList<QVariantMap> &products, const QStringList &invalidIds,
const QString &errorString) {
if (!errorString.isEmpty()) {
qWarning().noquote() << "[IAP] StoreKit merge fetch:" << errorString;
}
if (!invalidIds.isEmpty()) {
qWarning().noquote() << "[IAP] Unknown App Store product ids:" << invalidIds;
}
fetchedProducts = products;
loop.quit();
});
loop.exec();
const QHash<QString, StoreKitPlanQuote> quotesByProductId = buildStoreKitQuoteMap(fetchedProducts);
for (int serviceIndex = 0; serviceIndex < services.size(); ++serviceIndex) {
QJsonObject serviceObject = services.at(serviceIndex).toObject();
if (serviceObject.value(configKey::serviceType).toString() != serviceType::amneziaPremium) {
continue;
}
QJsonObject descriptionObject = serviceObject.value(configKey::serviceDescription).toObject();
const QJsonArray sourcePlans = descriptionObject.value(configKey::subscriptionPlans).toArray();
QJsonArray mergedPlans;
double minMonthlyAmount = std::numeric_limits<double>::infinity();
QString minMonthlyDisplay;
for (const QJsonValue &planValue : sourcePlans) {
if (!planValue.isObject()) {
continue;
}
QJsonObject planObject = planValue.toObject();
const QString storeProductId = planObject.value(configKey::storeProductId).toString();
if (storeProductId.isEmpty()) {
continue;
}
const auto quoteIterator = quotesByProductId.constFind(storeProductId);
if (quoteIterator == quotesByProductId.cend()) {
continue;
}
const bool isTrialPlan = planObject.value(configKey::isTrial).toBool();
const StoreKitPlanQuote &quote = *quoteIterator;
planObject.insert(configKey::priceLabel, quote.displayPrice);
const double months = quote.subscriptionBillingMonths;
if (!isTrialPlan && months > kOneMonthThreshold && !quote.displayPricePerMonth.isEmpty()) {
planObject.insert(
configKey::subtitle,
QCoreApplication::translate("ApiConfigsController", "%1/mo", "IAP: price per month in plan subtitle")
.arg(quote.displayPricePerMonth));
}
if (!isTrialPlan && quote.priceAmount > 0.0) {
const double monthsForMin = months > kMonthsFallbackThreshold ? months : 1.0;
const double monthly = quote.priceAmount / monthsForMin;
if (monthly < minMonthlyAmount - kMonthlyPriceEpsilon) {
minMonthlyAmount = monthly;
minMonthlyDisplay = !quote.displayPricePerMonth.isEmpty() ? quote.displayPricePerMonth : quote.displayPrice;
}
}
mergedPlans.append(planObject);
}
descriptionObject.insert(configKey::subscriptionPlans, mergedPlans);
if (minMonthlyAmount < std::numeric_limits<double>::infinity() && !minMonthlyDisplay.isEmpty()) {
descriptionObject.insert(configKey::minPriceLabel,
QCoreApplication::translate("ApiConfigsController", "from %1 per month",
"IAP: card footer minimum monthly price from StoreKit")
.arg(minMonthlyDisplay));
}
serviceObject.insert(configKey::serviceDescription, descriptionObject);
services.replace(serviceIndex, serviceObject);
}
data.insert(configKey::services, services);
}
#endif
} }
ApiConfigsController::ApiConfigsController(const QSharedPointer<ServersModel> &serversModel, ApiConfigsController::ApiConfigsController(const QSharedPointer<ServersModel> &serversModel,
const QSharedPointer<ApiServicesModel> &apiServicesModel, const QSharedPointer<ApiServicesModel> &apiServicesModel,
const QSharedPointer<ApiSubscriptionPlansModel> &subscriptionPlansModel,
const QSharedPointer<ApiBenefitsModel> &benefitsModel,
const std::shared_ptr<Settings> &settings, QObject *parent) const std::shared_ptr<Settings> &settings, QObject *parent)
: QObject(parent) : QObject(parent), m_serversModel(serversModel), m_apiServicesModel(apiServicesModel), m_settings(settings)
, m_serversModel(serversModel)
, m_apiServicesModel(apiServicesModel)
, m_subscriptionPlansModel(subscriptionPlansModel)
, m_benefitsModel(benefitsModel)
, m_settings(settings)
{ {
connect(m_apiServicesModel.data(), &ApiServicesModel::serviceSelectionChanged, this, [this]() {
const ApiServicesModel::ApiServicesData serviceData = m_apiServicesModel->selectedServiceData();
m_subscriptionPlansModel->updateModel(serviceData.subscriptionPlansJson);
m_benefitsModel->updateModel(serviceData.benefits);
});
} }
bool ApiConfigsController::exportVpnKey(const QString &fileName) bool ApiConfigsController::exportVpnKey(const QString &fileName)
@@ -488,8 +300,6 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode,
return false; return false;
} }
qDebug() << responseBody;
QJsonObject jsonConfig = QJsonDocument::fromJson(responseBody).object(); QJsonObject jsonConfig = QJsonDocument::fromJson(responseBody).object();
QString nativeConfig = jsonConfig.value(configKey::config).toString(); QString nativeConfig = jsonConfig.value(configKey::config).toString();
nativeConfig.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", protocolData.wireGuardClientPrivKey); nativeConfig.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", protocolData.wireGuardClientPrivKey);
@@ -556,8 +366,6 @@ bool ApiConfigsController::fillAvailableServices()
{ {
QJsonObject apiPayload; QJsonObject apiPayload;
apiPayload[configKey::osVersion] = QSysInfo::productType(); apiPayload[configKey::osVersion] = QSysInfo::productType();
apiPayload[configKey::appVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::cliName] = QString(APPLICATION_NAME);
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first(); apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
QByteArray responseBody; QByteArray responseBody;
@@ -574,11 +382,51 @@ bool ApiConfigsController::fillAvailableServices()
} }
QJsonObject data = QJsonDocument::fromJson(responseBody).object(); QJsonObject data = QJsonDocument::fromJson(responseBody).object();
#if defined(Q_OS_IOS) || defined(MACOS_NE) #if defined(Q_OS_IOS) || defined(MACOS_NE)
mergeStoreKitPricesIntoPremiumPlans(data); QEventLoop waitProducts;
bool productsFetched = false;
QString productPrice;
QString productCurrency;
IosController::Instance()->fetchProducts(QStringList() << QStringLiteral("amnezia_premium_6_month"),
[&](const QList<QVariantMap> &products,
const QStringList &invalidIds,
const QString &errorString) {
if (!errorString.isEmpty() || products.isEmpty()) {
qWarning().noquote() << "[IAP] Failed to fetch product price:" << errorString;
} else {
const auto &product = products.first();
productPrice = product.value("price").toString();
productCurrency = product.value("currencyCode").toString();
productsFetched = true;
qInfo().noquote() << "[IAP] Fetched product price:" << productPrice << productCurrency;
}
waitProducts.quit();
});
waitProducts.exec();
if (productsFetched && !productPrice.isEmpty()) {
QJsonArray services = data.value("services").toArray();
for (int i = 0; i < services.size(); ++i) {
QJsonObject service = services[i].toObject();
if (service.value(configKey::serviceType).toString() == serviceType::amneziaPremium) {
QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject();
QString formattedPrice = productPrice;
if (!productCurrency.isEmpty()) {
formattedPrice += " " + productCurrency;
}
serviceInfo["price"] = formattedPrice;
service[configKey::serviceInfo] = serviceInfo;
services[i] = service;
data["services"] = services;
qInfo().noquote() << "[IAP] Updated premium service price in data:" << formattedPrice;
break;
}
}
}
#endif #endif
m_apiServicesModel->updateModel(data); m_apiServicesModel->updateModel(data);
if (m_apiServicesModel->rowCount() > 0) { if (m_apiServicesModel->rowCount() > 0) {
m_apiServicesModel->setServiceIndex(0); m_apiServicesModel->setServiceIndex(0);
@@ -589,42 +437,39 @@ bool ApiConfigsController::fillAvailableServices()
bool ApiConfigsController::importService() bool ApiConfigsController::importService()
{ {
#if defined(Q_OS_IOS) || defined(MACOS_NE) #if defined(Q_OS_IOS) || defined(MACOS_NE)
const bool isIosOrMacOsNe = true; bool isIosOrMacOsNe = true;
#else #else
const bool isIosOrMacOsNe = false; bool isIosOrMacOsNe = false;
#endif #endif
if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaPremium) { if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaPremium) {
if (isIosOrMacOsNe) { if (isIosOrMacOsNe) {
return importPremiumFromAppStore(QString()); importSerivceFromAppStore();
return true;
} }
} else if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaFree) { } else {
return importFreeFromGateway(); importServiceFromGateway();
return true;
} }
return false; return false;
} }
bool ApiConfigsController::importPremiumFromAppStore(const QString &storeProductId) bool ApiConfigsController::importSerivceFromAppStore()
{ {
#if defined(Q_OS_IOS) || defined(MACOS_NE) #if defined(Q_OS_IOS) || defined(MACOS_NE)
QString productId = storeProductId.trimmed();
if (productId.isEmpty()) {
productId = QStringLiteral("amnezia_premium_6_month");
}
bool purchaseOk = false; bool purchaseOk = false;
QString originalTransactionId; QString originalTransactionId;
QString storeTransactionId; QString storeTransactionId;
QString purchasedStoreProductId; QString storeProductId;
QString purchaseError; QString purchaseError;
QEventLoop waitPurchase; QEventLoop waitPurchase;
IosController::Instance()->purchaseProduct(productId, IosController::Instance()->purchaseProduct(QStringLiteral("amnezia_premium_6_month"),
[&](bool success, const QString &transactionId, const QString &purchasedProductId, [&](bool success, const QString &txId, const QString &purchasedProductId,
const QString &originalTransactionIdResponse, const QString &errorString) { const QString &originalTxId, const QString &errorString) {
purchaseOk = success; purchaseOk = success;
originalTransactionId = originalTransactionIdResponse; originalTransactionId = originalTxId;
storeTransactionId = transactionId; storeTransactionId = txId;
purchasedStoreProductId = purchasedProductId; storeProductId = purchasedProductId;
purchaseError = errorString; purchaseError = errorString;
waitPurchase.quit(); waitPurchase.quit();
}); });
@@ -636,7 +481,7 @@ bool ApiConfigsController::importPremiumFromAppStore(const QString &storeProduct
return false; return false;
} }
qInfo().noquote() << "[IAP] Purchase success. transactionId =" << storeTransactionId qInfo().noquote() << "[IAP] Purchase success. transactionId =" << storeTransactionId
<< "originalTransactionId =" << originalTransactionId << "productId =" << purchasedStoreProductId; << "originalTransactionId =" << originalTransactionId << "productId =" << storeProductId;
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
@@ -660,26 +505,18 @@ bool ApiConfigsController::importPremiumFromAppStore(const QString &storeProduct
return false; return false;
} }
int duplicateServerIndex = -1; errorCode = importServiceFromBilling(responseBody, isTestPurchase);
errorCode = importServiceFromBilling(responseBody, isTestPurchase, duplicateServerIndex);
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
emit installServerFromApiFinished(tr("This subscription is already in the app."), duplicateServerIndex);
return true;
}
if (errorCode != ErrorCode::NoError) { if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode); emit errorOccurred(errorCode);
return false; return false;
} }
emit installServerFromApiFinished(
tr("%1 was added to the app.").arg(m_apiServicesModel->getSelectedServiceName())); emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
return true;
#else
Q_UNUSED(storeProductId);
return false;
#endif #endif
return true;
} }
bool ApiConfigsController::restoreServiceFromAppStore() bool ApiConfigsController::restoreSerivceFromAppStore()
{ {
#if defined(Q_OS_IOS) || defined(MACOS_NE) #if defined(Q_OS_IOS) || defined(MACOS_NE)
const QString premiumServiceType = QStringLiteral("amnezia-premium"); const QString premiumServiceType = QStringLiteral("amnezia-premium");
@@ -695,12 +532,20 @@ bool ApiConfigsController::restoreServiceFromAppStore()
return false; return false;
} }
const int premiumServiceIndex = m_apiServicesModel->serviceIndexForType(premiumServiceType); // Ensure we have a valid premium selection for gateway requests
if (premiumServiceIndex < 0) { bool premiumSelected = false;
for (int i = 0; i < m_apiServicesModel->rowCount(); ++i) {
m_apiServicesModel->setServiceIndex(i);
if (m_apiServicesModel->getSelectedServiceType() == premiumServiceType) {
premiumSelected = true;
break;
}
}
if (!premiumSelected) {
emit errorOccurred(ErrorCode::ApiServicesMissingError); emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false; return false;
} }
m_apiServicesModel->setServiceIndex(premiumServiceIndex);
bool restoreSuccess = false; bool restoreSuccess = false;
QList<QVariantMap> restoredTransactions; QList<QVariantMap> restoredTransactions;
@@ -722,23 +567,15 @@ bool ApiConfigsController::restoreServiceFromAppStore()
} }
if (restoredTransactions.isEmpty()) { if (restoredTransactions.isEmpty()) {
qInfo().noquote() << "[IAP] Restore completed, but no active entitlements found"; qInfo().noquote() << "[IAP] Restore completed, but no transactions were returned";
emit errorOccurred(ErrorCode::ApiNoPurchasedSubscriptionsError); emit errorOccurred(ErrorCode::ApiPurchaseError);
return false; return false;
} }
const bool isTestPurchase = IosController::Instance()->isTestFlight();
const QString serviceType = m_apiServicesModel->getSelectedServiceType();
const QString serviceProtocol = m_apiServicesModel->getSelectedServiceProtocol();
const QString countryCode = m_apiServicesModel->getCountryCode();
const QString appLanguage = m_settings->getAppLanguage().name().split("_").first();
const QString installationUuid = m_settings->getInstallationUuid(true);
bool hasInstalledConfig = false; bool hasInstalledConfig = false;
bool duplicateConfigAlreadyPresent = false; bool duplicateConfigAlreadyPresent = false;
int duplicateServerIndex = -1; int duplicateCount = 0;
QSet<QString> processedOriginalTransactionIds; QSet<QString> processedTransactions;
for (const QVariantMap &transaction : restoredTransactions) { for (const QVariantMap &transaction : restoredTransactions) {
const QString originalTransactionId = transaction.value(QStringLiteral("originalTransactionId")).toString(); const QString originalTransactionId = transaction.value(QStringLiteral("originalTransactionId")).toString();
const QString transactionId = transaction.value(QStringLiteral("transactionId")).toString(); const QString transactionId = transaction.value(QStringLiteral("transactionId")).toString();
@@ -749,28 +586,28 @@ bool ApiConfigsController::restoreServiceFromAppStore()
continue; continue;
} }
if (processedOriginalTransactionIds.contains(originalTransactionId)) { if (processedTransactions.contains(originalTransactionId)) {
qInfo().noquote() << "[IAP] Skipping duplicate restored transaction" << originalTransactionId; duplicateCount++;
continue; continue;
} }
processedOriginalTransactionIds.insert(originalTransactionId); processedTransactions.insert(originalTransactionId);
qInfo().noquote() << "[IAP] Restoring subscription. transactionId =" << transactionId qInfo().noquote() << "[IAP] Restoring subscription. transactionId =" << transactionId
<< "originalTransactionId =" << originalTransactionId << "productId =" << productId; << "originalTransactionId =" << originalTransactionId << "productId =" << productId;
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
appLanguage, m_settings->getAppLanguage().name().split("_").first(),
installationUuid, m_settings->getInstallationUuid(true),
countryCode, m_apiServicesModel->getCountryCode(),
"", "",
serviceType, m_apiServicesModel->getSelectedServiceType(),
serviceProtocol, m_apiServicesModel->getSelectedServiceProtocol(),
QJsonObject() }; QJsonObject() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject(); QJsonObject apiPayload = gatewayRequestData.toJsonObject();
apiPayload[apiDefs::key::transactionId] = originalTransactionId; apiPayload[apiDefs::key::transactionId] = originalTransactionId;
auto isTestPurchase = IosController::Instance()->isTestFlight();
QByteArray responseBody; QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase); ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) { if (errorCode != ErrorCode::NoError) {
@@ -779,42 +616,34 @@ bool ApiConfigsController::restoreServiceFromAppStore()
continue; continue;
} }
int currentDuplicateServerIndex = -1; ErrorCode installError = importServiceFromBilling(responseBody, isTestPurchase);
errorCode = importServiceFromBilling(responseBody, isTestPurchase, currentDuplicateServerIndex);
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) { if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
duplicateConfigAlreadyPresent = true; duplicateConfigAlreadyPresent = true;
if (duplicateServerIndex < 0) { qInfo().noquote() << "[IAP] Skipping restored transaction" << originalTransactionId
duplicateServerIndex = currentDuplicateServerIndex; << "because subscription config with the same vpn_key already exists";
} } else if (errorCode != ErrorCode::NoError) {
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists" << originalTransactionId; qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction" << originalTransactionId;
continue; } else {
hasInstalledConfig = true;
} }
if (errorCode != ErrorCode::NoError) {
qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction" << originalTransactionId
<< "errorCode =" << static_cast<int>(errorCode);
continue;
}
hasInstalledConfig = true;
} }
if (!hasInstalledConfig) { if (!hasInstalledConfig) {
if (duplicateConfigAlreadyPresent) { const ErrorCode restoreError = duplicateConfigAlreadyPresent ? ErrorCode::ApiConfigAlreadyAdded : ErrorCode::ApiPurchaseError;
emit installServerFromApiFinished(tr("This subscription is already in the app."), duplicateServerIndex); emit errorOccurred(restoreError);
return true;
}
emit errorOccurred(ErrorCode::ApiPurchaseError);
return false; return false;
} }
emit installServerFromApiFinished(tr("Subscription restored successfully.")); emit installServerFromApiFinished(tr("Subscription restored successfully."));
if (duplicateCount > 0) {
qInfo().noquote() << "[IAP] Skipped" << duplicateCount
<< "duplicate restored transactions for original transaction IDs already processed";
}
#endif #endif
return true; return true;
} }
bool ApiConfigsController::importFreeFromGateway() bool ApiConfigsController::importServiceFromGateway()
{ {
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
@@ -866,72 +695,6 @@ bool ApiConfigsController::importFreeFromGateway()
} }
} }
bool ApiConfigsController::importTrialFromGateway(const QString &email)
{
const QString trimmedEmail = email.trimmed();
if (trimmedEmail.isEmpty()) {
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
m_apiServicesModel->getCountryCode(),
"",
m_apiServicesModel->getSelectedServiceType(),
m_apiServicesModel->getSelectedServiceProtocol(),
QJsonObject() };
if (m_serversModel->isServerFromApiAlreadyExists(gatewayRequestData.userCountryCode, gatewayRequestData.serviceType,
gatewayRequestData.serviceProtocol)) {
emit errorOccurred(ErrorCode::ApiConfigAlreadyAdded);
return false;
}
ProtocolData protocolData = generateProtocolData(gatewayRequestData.serviceProtocol);
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload);
apiPayload.insert(apiDefs::key::email, trimmedEmail);
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/trial"), apiPayload, responseBody);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return false;
}
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
QString key = responseObject.value(apiDefs::key::config).toString();
if (key.isEmpty()) {
qWarning().noquote() << "[Trial] trial response does not contain config field";
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
return false;
}
key.replace(QStringLiteral("vpn://"), QString());
QByteArray configBytes = QByteArray::fromBase64(key.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
QByteArray uncompressed = qUncompress(configBytes);
if (!uncompressed.isEmpty()) {
configBytes = uncompressed;
}
if (configBytes.isEmpty()) {
qWarning().noquote() << "[Trial] trial response config payload is empty";
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
return false;
}
QJsonObject configObject = QJsonDocument::fromJson(configBytes).object();
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
configObject.insert(config_key::crc, crc);
m_serversModel->addServer(configObject);
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
return true;
}
bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName, bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
bool reloadServiceConfig) bool reloadServiceConfig)
{ {
@@ -958,7 +721,6 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
} }
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false); bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
bool wasSubscriptionExpired = m_serversModel->data(serverIndex, ServersModel::IsSubscriptionExpiredRole).toBool();
QByteArray responseBody; QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase); ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
@@ -975,12 +737,6 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
newApiConfig.insert(configKey::serviceType, apiConfig.value(configKey::serviceType)); newApiConfig.insert(configKey::serviceType, apiConfig.value(configKey::serviceType));
newApiConfig.insert(configKey::serviceProtocol, apiConfig.value(configKey::serviceProtocol)); newApiConfig.insert(configKey::serviceProtocol, apiConfig.value(configKey::serviceProtocol));
newApiConfig.insert(apiDefs::key::vpnKey, apiConfig.value(apiDefs::key::vpnKey)); newApiConfig.insert(apiDefs::key::vpnKey, apiConfig.value(apiDefs::key::vpnKey));
if (apiConfig.contains(apiDefs::key::isInAppPurchase)) {
newApiConfig.insert(apiDefs::key::isInAppPurchase, apiConfig.value(apiDefs::key::isInAppPurchase));
}
if (apiConfig.contains(apiDefs::key::isTestPurchase)) {
newApiConfig.insert(apiDefs::key::isTestPurchase, apiConfig.value(apiDefs::key::isTestPurchase));
}
newServerConfig.insert(configKey::apiConfig, newApiConfig); newServerConfig.insert(configKey::apiConfig, newApiConfig);
newServerConfig.insert(configKey::authData, gatewayRequestData.authData); newServerConfig.insert(configKey::authData, gatewayRequestData.authData);
@@ -991,11 +747,6 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
newServerConfig.insert(config_key::nameOverriddenByUser, true); newServerConfig.insert(config_key::nameOverriddenByUser, true);
} }
m_serversModel->editServer(newServerConfig, serverIndex); m_serversModel->editServer(newServerConfig, serverIndex);
if (wasSubscriptionExpired) {
emit subscriptionRefreshNeeded();
}
if (reloadServiceConfig) { if (reloadServiceConfig) {
emit reloadServerFromApiFinished(tr("API config reloaded")); emit reloadServerFromApiFinished(tr("API config reloaded"));
} else if (newCountryName.isEmpty()) { } else if (newCountryName.isEmpty()) {
@@ -1005,18 +756,7 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
} }
return true; return true;
} else { } else {
if (errorCode == ErrorCode::ApiSubscriptionExpiredError) { emit errorOccurred(errorCode);
if (!apiConfig.value(apiDefs::key::isInAppPurchase).toBool(false)) {
apiConfig.insert(apiDefs::key::subscriptionExpiredByServer, true);
serverConfig.insert(configKey::apiConfig, apiConfig);
m_serversModel->editServer(serverConfig, serverIndex);
emit subscriptionExpiredOnServer();
} else {
emit errorOccurred(errorCode);
}
} else {
emit errorOccurred(errorCode);
}
return false; return false;
} }
} }
@@ -1202,63 +942,43 @@ QString ApiConfigsController::getVpnKey()
return m_vpnKey; return m_vpnKey;
} }
ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase, ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase)
int &duplicateServerIndex)
{ {
#if defined(Q_OS_IOS) || defined(MACOS_NE) #ifdef Q_OS_IOS
duplicateServerIndex = -1;
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object(); QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
const QString rawVpnKey = responseObject.value(QStringLiteral("key")).toString(); QString key = responseObject.value(QStringLiteral("key")).toString();
if (rawVpnKey.isEmpty()) { if (key.isEmpty()) {
qWarning().noquote() << "[IAP] Subscription response does not contain a key field"; qWarning().noquote() << "[IAP] Subscription response does not contain a key field";
return ErrorCode::ApiPurchaseError; return ErrorCode::ApiPurchaseError;
} }
QString normalizedVpnKey = rawVpnKey; if (m_serversModel->hasServerWithVpnKey(key)) {
normalizedVpnKey.replace(QStringLiteral("vpn://"), QString());
duplicateServerIndex = m_serversModel->indexOfServerWithVpnKey(normalizedVpnKey);
if (duplicateServerIndex >= 0) {
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists"; qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
return ErrorCode::ApiConfigAlreadyAdded; return ErrorCode::ApiConfigAlreadyAdded;
} }
QByteArray configPayload = QString normalizedKey = key;
QByteArray::fromBase64(normalizedVpnKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); normalizedKey.replace(QStringLiteral("vpn://"), QString());
QByteArray configUncompressed = qUncompress(configPayload);
const bool payloadWasCompressed = !configUncompressed.isEmpty(); QByteArray configString = QByteArray::fromBase64(normalizedKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
if (payloadWasCompressed) { QByteArray configUncompressed = qUncompress(configString);
configPayload = configUncompressed; if (!configUncompressed.isEmpty()) {
configString = configUncompressed;
} }
if (configPayload.isEmpty()) { if (configString.isEmpty()) {
qWarning().noquote() << "[IAP] Subscription response config payload is empty"; qWarning().noquote() << "[IAP] Subscription response config payload is empty";
return ErrorCode::ApiPurchaseError; return ErrorCode::ApiPurchaseError;
} }
QJsonObject configObject = QJsonDocument::fromJson(configPayload).object(); QJsonObject configObject = QJsonDocument::fromJson(configString).object();
auto apiConfig = configObject.value(apiDefs::key::apiConfig).toObject();
apiConfig.insert(apiDefs::key::isTestPurchase, isTestPurchase);
apiConfig.insert(apiDefs::key::isInAppPurchase, true);
configObject.insert(apiDefs::key::apiConfig, apiConfig);
configPayload = QJsonDocument(configObject).toJson();
if (payloadWasCompressed) {
configPayload = qCompress(configPayload, 8);
}
normalizedVpnKey = QString(configPayload.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
duplicateServerIndex = m_serversModel->indexOfServerWithVpnKey(normalizedVpnKey);
if (duplicateServerIndex >= 0) {
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
return ErrorCode::ApiConfigAlreadyAdded;
}
apiConfig.insert(apiDefs::key::vpnKey, normalizedVpnKey);
configObject.insert(apiDefs::key::apiConfig, apiConfig);
quint16 crc = qChecksum(QJsonDocument(configObject).toJson()); quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
auto apiConfig = configObject.value(apiDefs::key::apiConfig).toObject();
apiConfig[apiDefs::key::vpnKey] = normalizedKey;
apiConfig[apiDefs::key::isTestPurchase] = isTestPurchase;
configObject.insert(apiDefs::key::apiConfig, apiConfig);
configObject.insert(config_key::crc, crc); configObject.insert(config_key::crc, crc);
m_serversModel->addServer(configObject); m_serversModel->addServer(configObject);
@@ -1266,7 +986,6 @@ ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &respo
#else #else
Q_UNUSED(responseBody) Q_UNUSED(responseBody)
Q_UNUSED(isTestPurchase) Q_UNUSED(isTestPurchase)
duplicateServerIndex = -1;
return ErrorCode::NoError; return ErrorCode::NoError;
#endif #endif
} }
@@ -1,13 +1,10 @@
#ifndef APICONFIGSCONTROLLER_H #ifndef APICONFIGSCONTROLLER_H
#define APICONFIGSCONTROLLER_H #define APICONFIGSCONTROLLER_H
#include <QList>
#include <QObject> #include <QObject>
#include "configurators/openvpn_configurator.h" #include "configurators/openvpn_configurator.h"
#include "ui/models/api/apiBenefitsModel.h"
#include "ui/models/api/apiServicesModel.h" #include "ui/models/api/apiServicesModel.h"
#include "ui/models/api/apiSubscriptionPlansModel.h"
#include "ui/models/servers_model.h" #include "ui/models/servers_model.h"
class ApiConfigsController : public QObject class ApiConfigsController : public QObject
@@ -15,9 +12,7 @@ class ApiConfigsController : public QObject
Q_OBJECT Q_OBJECT
public: public:
ApiConfigsController(const QSharedPointer<ServersModel> &serversModel, const QSharedPointer<ApiServicesModel> &apiServicesModel, ApiConfigsController(const QSharedPointer<ServersModel> &serversModel, const QSharedPointer<ApiServicesModel> &apiServicesModel,
const QSharedPointer<ApiSubscriptionPlansModel> &subscriptionPlansModel, const std::shared_ptr<Settings> &settings, QObject *parent = nullptr);
const QSharedPointer<ApiBenefitsModel> &benefitsModel, const std::shared_ptr<Settings> &settings,
QObject *parent = nullptr);
Q_PROPERTY(QList<QString> qrCodes READ getQrCodes NOTIFY vpnKeyExportReady) Q_PROPERTY(QList<QString> qrCodes READ getQrCodes NOTIFY vpnKeyExportReady)
Q_PROPERTY(int qrCodesCount READ getQrCodesCount NOTIFY vpnKeyExportReady) Q_PROPERTY(int qrCodesCount READ getQrCodesCount NOTIFY vpnKeyExportReady)
@@ -32,10 +27,9 @@ public slots:
bool fillAvailableServices(); bool fillAvailableServices();
bool importService(); bool importService();
bool importPremiumFromAppStore(const QString &storeProductId); bool importSerivceFromAppStore();
bool restoreServiceFromAppStore(); bool restoreSerivceFromAppStore();
bool importFreeFromGateway(); bool importServiceFromGateway();
bool importTrialFromGateway(const QString &email);
bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName, bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
bool reloadServiceConfig = false); bool reloadServiceConfig = false);
bool updateServiceFromTelegram(const int serverIndex); bool updateServiceFromTelegram(const int serverIndex);
@@ -49,10 +43,8 @@ public slots:
signals: signals:
void errorOccurred(ErrorCode errorCode); void errorOccurred(ErrorCode errorCode);
void subscriptionExpiredOnServer();
void subscriptionRefreshNeeded();
void installServerFromApiFinished(const QString &message, int preferredDefaultServerIndex = -1); void installServerFromApiFinished(const QString &message);
void changeApiCountryFinished(const QString &message); void changeApiCountryFinished(const QString &message);
void reloadServerFromApiFinished(const QString &message); void reloadServerFromApiFinished(const QString &message);
void updateServerFromApiFinished(); void updateServerFromApiFinished();
@@ -65,7 +57,7 @@ private:
QString getVpnKey(); QString getVpnKey();
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false); ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
ErrorCode importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase, int &duplicateServerIndex); ErrorCode importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase);
QList<QString> m_qrCodes; QList<QString> m_qrCodes;
QString m_vpnKey; QString m_vpnKey;
@@ -73,9 +65,6 @@ private:
QSharedPointer<ServersModel> m_serversModel; QSharedPointer<ServersModel> m_serversModel;
QSharedPointer<ApiServicesModel> m_apiServicesModel; QSharedPointer<ApiServicesModel> m_apiServicesModel;
std::shared_ptr<Settings> m_settings; std::shared_ptr<Settings> m_settings;
QSharedPointer<ApiSubscriptionPlansModel> m_subscriptionPlansModel;
QSharedPointer<ApiBenefitsModel> m_benefitsModel;
}; };
#endif #endif // APICONFIGSCONTROLLER_H
@@ -1,7 +1,6 @@
#include "apiSettingsController.h" #include "apiSettingsController.h"
#include <QEventLoop> #include <QEventLoop>
#include <QJsonDocument>
#include <QTimer> #include <QTimer>
#include "core/api/apiUtils.h" #include "core/api/apiUtils.h"
@@ -86,42 +85,6 @@ bool ApiSettingsController::getAccountInfo(bool reload)
return true; return true;
} }
void ApiSettingsController::getRenewalLink()
{
auto processedIndex = m_serversModel->getProcessedServerIndex();
auto serverConfig = m_serversModel->getServerConfig(processedIndex);
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
auto authData = serverConfig.value(configKey::authData).toObject();
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(isTestPurchase),
m_settings->isDevGatewayEnv(isTestPurchase),
requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
QJsonObject apiPayload;
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
apiPayload[configKey::serviceType] = apiConfig.value(configKey::serviceType).toString();
apiPayload[configKey::authData] = authData;
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
auto future = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload);
future.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) {
auto [errorCode, responseBody] = result;
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return;
}
QJsonObject responseJson = QJsonDocument::fromJson(responseBody).object();
QString url = responseJson.value("renewal_url").toString();
if (!url.isEmpty()) {
emit renewalLinkReceived(url);
}
});
}
void ApiSettingsController::updateApiCountryModel() void ApiSettingsController::updateApiCountryModel()
{ {
m_apiCountryModel->updateModel(m_apiAccountInfoModel->getAvailableCountries(), ""); m_apiCountryModel->updateModel(m_apiAccountInfoModel->getAvailableCountries(), "");
@@ -21,11 +21,9 @@ public slots:
bool getAccountInfo(bool reload); bool getAccountInfo(bool reload);
void updateApiCountryModel(); void updateApiCountryModel();
void updateApiDevicesModel(); void updateApiDevicesModel();
void getRenewalLink();
signals: signals:
void errorOccurred(ErrorCode errorCode); void errorOccurred(ErrorCode errorCode);
void renewalLinkReceived(const QString &url);
private: private:
QSharedPointer<ServersModel> m_serversModel; QSharedPointer<ServersModel> m_serversModel;
+1 -10
View File
@@ -1,12 +1,5 @@
#include "connectionController.h" #include "connectionController.h"
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(MACOS_NE)
#include <QGuiApplication>
#else
#include <QApplication>
#endif
#include "amnezia_application.h"
#include "utilities.h" #include "utilities.h"
#include "core/controllers/vpnConfigurationController.h" #include "core/controllers/vpnConfigurationController.h"
#include "version.h" #include "version.h"
@@ -34,7 +27,7 @@ ConnectionController::ConnectionController(const QSharedPointer<ServersModel> &s
void ConnectionController::openConnection() void ConnectionController::openConnection()
{ {
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true)) if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true))
{ {
emit connectionErrorOccurred(ErrorCode::AmneziaServiceNotRunning); emit connectionErrorOccurred(ErrorCode::AmneziaServiceNotRunning);
@@ -82,8 +75,6 @@ void ConnectionController::onConnectionStateChanged(Vpn::ConnectionState state)
m_connectionStateText = tr("Connecting..."); m_connectionStateText = tr("Connecting...");
switch (state) { switch (state) {
case Vpn::ConnectionState::Connected: { case Vpn::ConnectionState::Connected: {
amnApp->networkManager()->clearConnectionCache();
m_isConnectionInProgress = false; m_isConnectionInProgress = false;
m_isConnected = true; m_isConnected = true;
m_connectionStateText = tr("Connected"); m_connectionStateText = tr("Connected");
+5 -2
View File
@@ -7,6 +7,7 @@
#include <QFileInfo> #include <QFileInfo>
#include <QImage> #include <QImage>
#include <QStandardPaths> #include <QStandardPaths>
#include "core/controllers/vpnConfigurationController.h" #include "core/controllers/vpnConfigurationController.h"
#include "core/qrCodeUtils.h" #include "core/qrCodeUtils.h"
#include "core/serialization/serialization.h" #include "core/serialization/serialization.h"
@@ -169,7 +170,8 @@ void ExportController::generateWireGuardConfig(const QString &clientName)
m_config.append(line + "\n"); m_config.append(line + "\n");
} }
m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(m_config.toUtf8()); auto qr = qrCodeUtils::generateQrCode(m_config.toUtf8());
m_qrCodes << qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
emit exportConfigChanged(); emit exportConfigChanged();
} }
@@ -189,7 +191,8 @@ void ExportController::generateAwgConfig(const QString &clientName)
m_config.append(line + "\n"); m_config.append(line + "\n");
} }
m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(m_config.toUtf8()); auto qr = qrCodeUtils::generateQrCode(m_config.toUtf8());
m_qrCodes << qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
emit exportConfigChanged(); emit exportConfigChanged();
} }
+3 -3
View File
@@ -19,7 +19,7 @@
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
#include "platforms/android/android_controller.h" #include "platforms/android/android_controller.h"
#endif #endif
#if defined(Q_OS_IOS) || defined(MACOS_NE) #if defined(Q_OS_IOS) || defined(Q_OS_TVOS) || defined(MACOS_NE)
#include <CoreFoundation/CoreFoundation.h> #include <CoreFoundation/CoreFoundation.h>
#endif #endif
@@ -604,14 +604,14 @@ bool ImportController::decodeQrCode(const QString &code)
} }
#endif #endif
#if defined Q_OS_ANDROID || defined Q_OS_IOS #if defined Q_OS_ANDROID || defined Q_OS_IOS || defined(Q_OS_TVOS)
void ImportController::startDecodingQr() void ImportController::startDecodingQr()
{ {
m_qrCodeChunks.clear(); m_qrCodeChunks.clear();
m_totalQrCodeChunksCount = 0; m_totalQrCodeChunksCount = 0;
m_receivedQrCodeChunksCount = 0; m_receivedQrCodeChunksCount = 0;
#if defined(Q_OS_IOS) || defined(MACOS_NE) #if defined(Q_OS_IOS) || defined(Q_OS_TVOS) || defined(MACOS_NE)
m_isQrCodeProcessed = true; m_isQrCodeProcessed = true;
#endif #endif
#if defined Q_OS_ANDROID #if defined Q_OS_ANDROID
+3 -3
View File
@@ -38,7 +38,7 @@ public slots:
QString getConfigFileName(); QString getConfigFileName();
QString getMaliciousWarningText(); QString getMaliciousWarningText();
#if defined Q_OS_ANDROID || defined Q_OS_IOS #if defined Q_OS_ANDROID || defined Q_OS_IOS || defined(Q_OS_TVOS)
void startDecodingQr(); void startDecodingQr();
bool parseQrCodeChunk(const QString &code); bool parseQrCodeChunk(const QString &code);
@@ -70,7 +70,7 @@ private:
void processAmneziaConfig(QJsonObject &config); void processAmneziaConfig(QJsonObject &config);
#if defined Q_OS_ANDROID || defined Q_OS_IOS #if defined Q_OS_ANDROID || defined Q_OS_IOS || defined(Q_OS_TVOS)
void stopDecodingQr(); void stopDecodingQr();
#endif #endif
@@ -83,7 +83,7 @@ private:
ConfigTypes m_configType; ConfigTypes m_configType;
QString m_maliciousWarningText; QString m_maliciousWarningText;
#if defined Q_OS_ANDROID || defined Q_OS_IOS #if defined Q_OS_ANDROID || defined Q_OS_IOS || defined(Q_OS_TVOS)
QMap<int, QByteArray> m_qrCodeChunks; QMap<int, QByteArray> m_qrCodeChunks;
bool m_isQrCodeProcessed; bool m_isQrCodeProcessed;
int m_totalQrCodeChunksCount; int m_totalQrCodeChunksCount;
+53 -68
View File
@@ -83,8 +83,8 @@ void InstallController::install(DockerContainer container, int port, TransportPr
int s1 = QRandomGenerator::global()->bounded(15, 150); int s1 = QRandomGenerator::global()->bounded(15, 150);
int s2 = QRandomGenerator::global()->bounded(15, 150); int s2 = QRandomGenerator::global()->bounded(15, 150);
int s3 = QRandomGenerator::global()->bounded(1, 64); int s3 = QRandomGenerator::global()->bounded(0, 64);
int s4 = QRandomGenerator::global()->bounded(1, 20); int s4 = QRandomGenerator::global()->bounded(0, 20);
// Ensure all values are unique and don't create equal packet sizes // Ensure all values are unique and don't create equal packet sizes
QSet<int> usedValues; QSet<int> usedValues;
@@ -97,12 +97,12 @@ void InstallController::install(DockerContainer container, int port, TransportPr
while (usedValues.contains(s3) || s1 + AwgConstant::messageInitiationSize == s3 + AwgConstant::messageCookieReplySize while (usedValues.contains(s3) || s1 + AwgConstant::messageInitiationSize == s3 + AwgConstant::messageCookieReplySize
|| s2 + AwgConstant::messageResponseSize == s3 + AwgConstant::messageCookieReplySize) { || s2 + AwgConstant::messageResponseSize == s3 + AwgConstant::messageCookieReplySize) {
s3 = QRandomGenerator::global()->bounded(1, 64); s3 = QRandomGenerator::global()->bounded(0, 64);
} }
usedValues.insert(s3); usedValues.insert(s3);
while (usedValues.contains(s4)) { while (usedValues.contains(s4)) {
s4 = QRandomGenerator::global()->bounded(1, 20); s4 = QRandomGenerator::global()->bounded(0, 20);
} }
QString initPacketJunkSize = QString::number(s1); QString initPacketJunkSize = QString::number(s1);
@@ -987,94 +987,79 @@ void InstallController::addEmptyServer()
emit installServerFinished(tr("Server added successfully")); emit installServerFinished(tr("Server added successfully"));
} }
void InstallController::validateConfig() bool InstallController::isConfigValid()
{ {
int serverIndex = m_serversModel->getDefaultServerIndex(); int serverIndex = m_serversModel->getDefaultServerIndex();
QJsonObject serverConfigObject = m_serversModel->getServerConfig(serverIndex); QJsonObject serverConfigObject = m_serversModel->getServerConfig(serverIndex);
if (apiUtils::isServerFromApi(serverConfigObject)) { if (apiUtils::isServerFromApi(serverConfigObject)) {
emit configValidated(true); return true;
return;
} }
if (!m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { if (!m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
emit noInstalledContainers(); emit noInstalledContainers();
emit configValidated(false); return false;
return;
} }
DockerContainer container = qvariant_cast<DockerContainer>(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole)); DockerContainer container = qvariant_cast<DockerContainer>(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole));
if (container == DockerContainer::None) { if (container == DockerContainer::None) {
emit installationErrorOccurred(ErrorCode::NoInstalledContainersError); emit installationErrorOccurred(ErrorCode::NoInstalledContainersError);
emit configValidated(false); return false;
return;
} }
QSharedPointer<ServerController> serverController(new ServerController(m_settings));
VpnConfigurationsController vpnConfigurationController(m_settings, serverController);
QJsonObject containerConfig = m_containersModel->getContainerConfig(container); QJsonObject containerConfig = m_containersModel->getContainerConfig(container);
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
QSharedPointer<ServerController> serverController(new ServerController(m_settings));
auto isProtocolConfigExists = [](const QJsonObject &containerConfig, const DockerContainer container) { QFutureWatcher<ErrorCode> watcher;
for (Proto protocol : ContainerProps::protocolsForContainer(container)) {
QString protocolConfig =
containerConfig.value(ProtocolProps::protoToString(protocol)).toObject().value(config_key::last_config).toString();
if (protocolConfig.isEmpty()) { QFuture<ErrorCode> future = QtConcurrent::run([this, container, &credentials, &containerConfig, &serverController]() {
return false; ErrorCode errorCode = ErrorCode::NoError;
auto isProtocolConfigExists = [](const QJsonObject &containerConfig, const DockerContainer container) {
for (Proto protocol : ContainerProps::protocolsForContainer(container)) {
QString protocolConfig =
containerConfig.value(ProtocolProps::protoToString(protocol)).toObject().value(config_key::last_config).toString();
if (protocolConfig.isEmpty()) {
return false;
}
}
return true;
};
if (!isProtocolConfigExists(containerConfig, container)) {
VpnConfigurationsController vpnConfigurationController(m_settings, serverController);
errorCode = vpnConfigurationController.createProtocolConfigForContainer(credentials, container, containerConfig);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
m_serversModel->updateContainerConfig(container, containerConfig);
errorCode = m_clientManagementModel->appendClient(container, credentials, containerConfig,
QString("Admin [%1]").arg(QSysInfo::prettyProductName()), serverController);
if (errorCode != ErrorCode::NoError) {
return errorCode;
} }
} }
return true; return errorCode;
}; });
if (isProtocolConfigExists(containerConfig, container)) { QEventLoop wait;
emit configValidated(true); connect(&watcher, &QFutureWatcher<ErrorCode>::finished, &wait, &QEventLoop::quit);
return; watcher.setFuture(future);
wait.exec();
ErrorCode errorCode = watcher.result();
if (errorCode != ErrorCode::NoError) {
emit installationErrorOccurred(errorCode);
return false;
} }
return true;
struct ValidationResult {
ErrorCode errorCode = ErrorCode::NoError;
QJsonObject containerConfig;
};
QFuture<ValidationResult> future =
QtConcurrent::run([settings = m_settings, serverController, credentials, containerConfig, container]() mutable {
ValidationResult result;
result.containerConfig = containerConfig;
VpnConfigurationsController vpnConfigurationController(settings, serverController);
result.errorCode = vpnConfigurationController.createProtocolConfigForContainer(credentials, container,
result.containerConfig);
return result;
});
auto *watcher = new QFutureWatcher<ValidationResult>(this);
connect(watcher, &QFutureWatcher<ValidationResult>::finished, this,
[this, watcher, container, credentials, serverController]() {
auto result = watcher->result();
watcher->deleteLater();
if (result.errorCode != ErrorCode::NoError) {
emit installationErrorOccurred(result.errorCode);
emit configValidated(false);
return;
}
m_serversModel->updateContainerConfig(container, result.containerConfig);
ErrorCode appendError = m_clientManagementModel->appendClient(
container, credentials, result.containerConfig,
QString("Admin [%1]").arg(QSysInfo::prettyProductName()), serverController);
if (appendError != ErrorCode::NoError) {
emit installationErrorOccurred(appendError);
emit configValidated(false);
return;
}
emit configValidated(true);
});
watcher->setFuture(future);
} }
bool InstallController::isUpdateDockerContainerRequired(const DockerContainer container, const QJsonObject &oldConfig, bool InstallController::isUpdateDockerContainerRequired(const DockerContainer container, const QJsonObject &oldConfig,
@@ -1085,7 +1070,7 @@ bool InstallController::isUpdateDockerContainerRequired(const DockerContainer co
const QJsonObject &oldProtoConfig = oldConfig.value(ProtocolProps::protoToString(mainProto)).toObject(); const QJsonObject &oldProtoConfig = oldConfig.value(ProtocolProps::protoToString(mainProto)).toObject();
const QJsonObject &newProtoConfig = newConfig.value(ProtocolProps::protoToString(mainProto)).toObject(); const QJsonObject &newProtoConfig = newConfig.value(ProtocolProps::protoToString(mainProto)).toObject();
if (ContainerProps::isAwgContainer(container)) { if (container == DockerContainer::Awg2) {
const AwgConfig oldConfig(oldProtoConfig); const AwgConfig oldConfig(oldProtoConfig);
const AwgConfig newConfig(newProtoConfig); const AwgConfig newConfig(newProtoConfig);
+5 -4
View File
@@ -2,7 +2,9 @@
#define INSTALLCONTROLLER_H #define INSTALLCONTROLLER_H
#include <QObject> #include <QObject>
#include <QProcess> #if !defined(Q_OS_IOS) && !defined(Q_OS_TVOS)
#include <QProcess>
#endif
#include "containers/containers_defs.h" #include "containers/containers_defs.h"
#include "core/defs.h" #include "core/defs.h"
@@ -50,10 +52,9 @@ public slots:
void addEmptyServer(); void addEmptyServer();
void validateConfig(); bool isConfigValid();
signals: signals:
void configValidated(bool isValid);
void installContainerFinished(const QString &finishMessage, bool isServiceInstall); void installContainerFinished(const QString &finishMessage, bool isServiceInstall);
void installServerFinished(const QString &finishMessage); void installServerFinished(const QString &finishMessage);
@@ -112,7 +113,7 @@ private:
QString m_privateKeyPassphrase; QString m_privateKeyPassphrase;
#ifndef Q_OS_IOS #if !defined(Q_OS_IOS) && !defined(Q_OS_TVOS)
QList<QSharedPointer<QProcess>> m_sftpMountProcesses; QList<QSharedPointer<QProcess>> m_sftpMountProcesses;
#endif #endif
}; };
+6 -5
View File
@@ -1,11 +1,12 @@
#include "pageController.h" #include "pageController.h"
#include "utils/converter.h" #include "utils/converter.h"
#include "core/errorstrings.h" #include "core/errorstrings.h"
#include <QCoreApplication>
#if defined(MACOS_NE) #if defined(MACOS_NE)
#include "platforms/ios/ios_controller.h" #include "platforms/ios/ios_controller.h"
#endif #endif
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(MACOS_NE) #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS) || defined(MACOS_NE)
#include <QGuiApplication> #include <QGuiApplication>
#else #else
#include <QApplication> #include <QApplication>
@@ -14,7 +15,7 @@
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
#include "platforms/android/android_controller.h" #include "platforms/android/android_controller.h"
#endif #endif
#if defined Q_OS_MAC #if defined(Q_OS_MACX) && !defined(Q_OS_TVOS)
#include "ui/macos_util.h" #include "ui/macos_util.h"
#endif #endif
@@ -27,7 +28,7 @@ PageController::PageController(const QSharedPointer<ServersModel> &serversModel,
AndroidController::instance()->setNavigationBarColor(initialPageNavigationBarColor); AndroidController::instance()->setNavigationBarColor(initialPageNavigationBarColor);
#endif #endif
#if defined Q_OS_MACX #if defined(Q_OS_MACX) && !defined(Q_OS_TVOS)
connect(this, &PageController::raiseMainWindow, []() { connect(this, &PageController::raiseMainWindow, []() {
setDockIconVisible(true); setDockIconVisible(true);
}); });
@@ -64,7 +65,7 @@ QString PageController::getPagePath(PageLoader::PageEnum page)
void PageController::closeWindow() void PageController::closeWindow()
{ {
// On mobile platforms, quit app on close; on desktop, just hide window // On mobile platforms, quit app on close; on desktop, just hide window
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS)
qApp->quit(); qApp->quit();
#else #else
emit hideMainWindow(); emit hideMainWindow();
@@ -118,7 +119,7 @@ void PageController::showOnStartup()
} else { } else {
#if defined(Q_OS_WIN) || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)) #if defined(Q_OS_WIN) || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID))
emit hideMainWindow(); emit hideMainWindow();
#elif defined(Q_OS_MACX) #elif defined(Q_OS_MACX) && !defined(Q_OS_TVOS)
setDockIconVisible(false); setDockIconVisible(false);
#endif #endif
} }
+1 -4
View File
@@ -59,7 +59,7 @@ namespace PageLoader
PageSetupWizardViewConfig, PageSetupWizardViewConfig,
PageSetupWizardQrReader, PageSetupWizardQrReader,
PageSetupWizardApiServicesList, PageSetupWizardApiServicesList,
PageSetupWizardApiFreeInfo, PageSetupWizardApiServiceInfo,
PageProtocolOpenVpnSettings, PageProtocolOpenVpnSettings,
PageProtocolShadowSocksSettings, PageProtocolShadowSocksSettings,
@@ -76,9 +76,6 @@ namespace PageLoader
PageShareFullAccess, PageShareFullAccess,
PageShareConnection, PageShareConnection,
PageSetupWizardApiPremiumInfo,
PageSetupWizardApiTrialEmail,
PageDevMenu PageDevMenu
}; };
Q_ENUM_NS(PageEnum) Q_ENUM_NS(PageEnum)
+13 -13
View File
@@ -12,7 +12,7 @@
#include "platforms/android/android_controller.h" #include "platforms/android/android_controller.h"
#endif #endif
#if defined(Q_OS_IOS) || defined(MACOS_NE) #if defined(Q_OS_IOS) || defined(Q_OS_TVOS) || defined(MACOS_NE)
#include <AmneziaVPN-Swift.h> #include <AmneziaVPN-Swift.h>
#endif #endif
@@ -45,8 +45,6 @@ SettingsController::SettingsController(const QSharedPointer<ServersModel> &serve
emit safeAreaBottomMarginChanged(); emit safeAreaBottomMarginChanged();
emit safeAreaTopMarginChanged(); emit safeAreaTopMarginChanged();
}); });
connect(AndroidController::instance(), &AndroidController::activityPaused, this, &SettingsController::activityPaused);
connect(AndroidController::instance(), &AndroidController::activityResumed, this, &SettingsController::activityResumed);
#endif #endif
m_isDevModeEnabled = m_settings->isDevGatewayEnv(); m_isDevModeEnabled = m_settings->isDevGatewayEnv();
@@ -59,6 +57,8 @@ QString getPlatformName()
return "Windows"; return "Windows";
#elif defined(Q_OS_ANDROID) #elif defined(Q_OS_ANDROID)
return "Android"; return "Android";
#elif defined(Q_OS_TVOS)
return "tvOS";
#elif defined(Q_OS_LINUX) #elif defined(Q_OS_LINUX)
return "Linux"; return "Linux";
#elif defined(Q_OS_MACX) #elif defined(Q_OS_MACX)
@@ -111,7 +111,7 @@ bool SettingsController::isLoggingEnabled()
void SettingsController::toggleLogging(bool enable) void SettingsController::toggleLogging(bool enable)
{ {
m_settings->setSaveLogs(enable); m_settings->setSaveLogs(enable);
#if defined(Q_OS_IOS) #if defined(Q_OS_IOS) || defined(Q_OS_TVOS)
AmneziaVPN::toggleLogging(enable); AmneziaVPN::toggleLogging(enable);
#endif #endif
if (enable == true) { if (enable == true) {
@@ -160,7 +160,6 @@ void SettingsController::clearLogs()
qInfo().noquote() << QString("Started %1 version %2 %3").arg(APPLICATION_NAME, APP_VERSION, GIT_COMMIT_HASH); qInfo().noquote() << QString("Started %1 version %2 %3").arg(APPLICATION_NAME, APP_VERSION, GIT_COMMIT_HASH);
qInfo().noquote() << QString("%1 (%2)").arg(QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture()); qInfo().noquote() << QString("%1 (%2)").arg(QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture());
qInfo().noquote() << QString("SSL backend: %1").arg(QSslSocket::sslLibraryVersionString());
} }
void SettingsController::backupAppConfig(const QString &fileName) void SettingsController::backupAppConfig(const QString &fileName)
@@ -180,11 +179,12 @@ void SettingsController::backupAppConfig(const QString &fileName)
void SettingsController::restoreAppConfig(const QString &fileName) void SettingsController::restoreAppConfig(const QString &fileName)
{ {
QByteArray data; QFile file(fileName);
if (!SystemController::readFile(fileName, data)) {
emit changeSettingsErrorOccurred(tr("Can't open file: %1").arg(fileName)); file.open(QIODevice::ReadOnly);
return;
} QByteArray data = file.readAll();
restoreAppConfigFromData(data); restoreAppConfigFromData(data);
} }
@@ -194,7 +194,7 @@ void SettingsController::restoreAppConfigFromData(const QByteArray &data)
if (ok) { if (ok) {
QJsonObject newConfigData = QJsonDocument::fromJson(data).object(); QJsonObject newConfigData = QJsonDocument::fromJson(data).object();
#if defined(Q_OS_WINDOWS) || defined(Q_OS_LINUX) || defined(Q_OS_MACX) #if defined(Q_OS_WINDOWS) || defined(Q_OS_LINUX) || (defined(Q_OS_MACX) && !defined(Q_OS_TVOS))
bool autoStart = false; bool autoStart = false;
if (newConfigData.contains("Conf/autoStart")) { if (newConfigData.contains("Conf/autoStart")) {
autoStart = newConfigData["Conf/autoStart"].toBool(); autoStart = newConfigData["Conf/autoStart"].toBool();
@@ -231,7 +231,7 @@ void SettingsController::restoreAppConfigFromData(const QByteArray &data)
m_sitesModel->setRouteMode(siteSplitTunnelingRouteMode); m_sitesModel->setRouteMode(siteSplitTunnelingRouteMode);
m_sitesModel->toggleSplitTunneling(siteSplittunnelingEnabled); m_sitesModel->toggleSplitTunneling(siteSplittunnelingEnabled);
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS)
m_settings->setAutoConnect(false); m_settings->setAutoConnect(false);
m_settings->setStartMinimized(false); m_settings->setStartMinimized(false);
m_settings->setKillSwitchEnabled(false); m_settings->setKillSwitchEnabled(false);
@@ -270,7 +270,7 @@ void SettingsController::clearSettings()
emit changeSettingsFinished(tr("All settings have been reset to default values")); emit changeSettingsFinished(tr("All settings have been reset to default values"));
#if defined(Q_OS_IOS) || defined(MACOS_NE) #if defined(Q_OS_IOS) || defined(Q_OS_TVOS) || defined(MACOS_NE)
AmneziaVPN::clearSettings(); AmneziaVPN::clearSettings();
#endif #endif
} }
@@ -141,9 +141,6 @@ signals:
void safeAreaTopMarginChanged(); void safeAreaTopMarginChanged();
void safeAreaBottomMarginChanged(); void safeAreaBottomMarginChanged();
void activityPaused();
void activityResumed();
void isHomeAdLabelVisibleChanged(bool visible); void isHomeAdLabelVisibleChanged(bool visible);
void startMinimizedChanged(); void startMinimizedChanged();
+25 -6
View File
@@ -7,8 +7,10 @@
#include "systemController.h" #include "systemController.h"
#include "core/networkUtilities.h" #include "core/networkUtilities.h"
SitesController::SitesController(const std::shared_ptr<Settings> &settings, const QSharedPointer<SitesModel> &sitesModel, QObject *parent) SitesController::SitesController(const std::shared_ptr<Settings> &settings,
: QObject(parent), m_settings(settings), m_sitesModel(sitesModel) const QSharedPointer<VpnConnection> &vpnConnection,
const QSharedPointer<SitesModel> &sitesModel, QObject *parent)
: QObject(parent), m_settings(settings), m_vpnConnection(vpnConnection), m_sitesModel(sitesModel)
{ {
} }
@@ -32,20 +34,32 @@ void SitesController::addSite(QString hostname)
hostname = hostname.split("/", Qt::SkipEmptyParts).first(); hostname = hostname.split("/", Qt::SkipEmptyParts).first();
} }
const auto &resolveCallback = [this](const QHostInfo &hostInfo) { const auto &processSite = [this](const QString &hostname, const QString &ip) {
m_sitesModel->addSite(hostname, ip);
if (!ip.isEmpty()) {
QMetaObject::invokeMethod(m_vpnConnection.get(), "addRoutes", Qt::QueuedConnection,
Q_ARG(QStringList, QStringList() << ip));
} else if (NetworkUtilities::ipAddressWithSubnetRegExp().exactMatch(hostname)) {
QMetaObject::invokeMethod(m_vpnConnection.get(), "addRoutes", Qt::QueuedConnection,
Q_ARG(QStringList, QStringList() << hostname));
}
};
const auto &resolveCallback = [this, processSite](const QHostInfo &hostInfo) {
const QList<QHostAddress> &addresses = hostInfo.addresses(); const QList<QHostAddress> &addresses = hostInfo.addresses();
for (const QHostAddress &addr : hostInfo.addresses()) { for (const QHostAddress &addr : hostInfo.addresses()) {
if (addr.protocol() == QAbstractSocket::NetworkLayerProtocol::IPv4Protocol) { if (addr.protocol() == QAbstractSocket::NetworkLayerProtocol::IPv4Protocol) {
m_sitesModel->addSite(hostInfo.hostName(), addr.toString()); processSite(hostInfo.hostName(), addr.toString());
break; break;
} }
} }
}; };
if (NetworkUtilities::ipAddressWithSubnetRegExp().exactMatch(hostname)) { if (NetworkUtilities::ipAddressWithSubnetRegExp().exactMatch(hostname)) {
m_sitesModel->addSite(hostname, ""); processSite(hostname, "");
} else { } else {
m_sitesModel->addSite(hostname, ""); processSite(hostname, "");
QHostInfo::lookupHost(hostname, this, resolveCallback); QHostInfo::lookupHost(hostname, this, resolveCallback);
} }
@@ -58,6 +72,9 @@ void SitesController::removeSite(int index)
auto hostname = m_sitesModel->data(modelIndex, SitesModel::Roles::UrlRole).toString(); auto hostname = m_sitesModel->data(modelIndex, SitesModel::Roles::UrlRole).toString();
m_sitesModel->removeSite(modelIndex); m_sitesModel->removeSite(modelIndex);
QMetaObject::invokeMethod(m_vpnConnection.get(), "deleteRoutes", Qt::QueuedConnection,
Q_ARG(QStringList, QStringList() << hostname));
emit finished(tr("Site removed: %1").arg(hostname)); emit finished(tr("Site removed: %1").arg(hostname));
} }
@@ -111,6 +128,8 @@ void SitesController::importSites(const QString &fileName, bool replaceExisting)
m_sitesModel->addSites(sites, replaceExisting); m_sitesModel->addSites(sites, replaceExisting);
QMetaObject::invokeMethod(m_vpnConnection.get(), "addRoutes", Qt::QueuedConnection, Q_ARG(QStringList, ips));
emit finished(tr("Import completed")); emit finished(tr("Import completed"));
} }
+5 -2
View File
@@ -11,8 +11,9 @@ class SitesController : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit SitesController(const std::shared_ptr<Settings> &settings, const QSharedPointer<SitesModel> &sitesModel, explicit SitesController(const std::shared_ptr<Settings> &settings,
QObject *parent = nullptr); const QSharedPointer<VpnConnection> &vpnConnection,
const QSharedPointer<SitesModel> &sitesModel, QObject *parent = nullptr);
public slots: public slots:
void addSite(QString hostname); void addSite(QString hostname);
@@ -30,6 +31,8 @@ signals:
private: private:
std::shared_ptr<Settings> m_settings; std::shared_ptr<Settings> m_settings;
QSharedPointer<VpnConnection> m_vpnConnection;
QSharedPointer<SitesModel> m_sitesModel; QSharedPointer<SitesModel> m_sitesModel;
}; };
+4 -4
View File
@@ -14,7 +14,7 @@
#include "platforms/android/android_controller.h" #include "platforms/android/android_controller.h"
#endif #endif
#if defined(Q_OS_IOS) || defined(MACOS_NE) #if defined(Q_OS_IOS) || defined(Q_OS_TVOS) || defined(MACOS_NE)
#include "platforms/ios/ios_controller.h" #include "platforms/ios/ios_controller.h"
#include <CoreFoundation/CoreFoundation.h> #include <CoreFoundation/CoreFoundation.h>
#endif #endif
@@ -31,7 +31,7 @@ void SystemController::saveFile(const QString &fileName, const QString &data)
return; return;
#endif #endif
#ifdef Q_OS_IOS #if defined(Q_OS_IOS) || defined(Q_OS_TVOS)
QUrl fileUrl = QDir::tempPath() + "/" + fileName; QUrl fileUrl = QDir::tempPath() + "/" + fileName;
QFile file(fileUrl.toString()); QFile file(fileUrl.toString());
#else #else
@@ -43,7 +43,7 @@ void SystemController::saveFile(const QString &fileName, const QString &data)
file.write(data.toUtf8()); file.write(data.toUtf8());
file.close(); file.close();
#ifdef Q_OS_IOS #if defined(Q_OS_IOS) || defined(Q_OS_TVOS)
QStringList filesToSend; QStringList filesToSend;
filesToSend.append(fileUrl.toString()); filesToSend.append(fileUrl.toString());
// todo check if save successful // todo check if save successful
@@ -98,7 +98,7 @@ QString SystemController::getFileName(const QString &acceptLabel, const QString
return AndroidController::instance()->openFile(nameFilter); return AndroidController::instance()->openFile(nameFilter);
#endif #endif
#ifdef Q_OS_IOS #if defined(Q_OS_IOS) || defined(Q_OS_TVOS)
fileName = IosController::Instance()->openFile(); fileName = IosController::Instance()->openFile();
if (fileName.isEmpty()) { if (fileName.isEmpty()) {

Some files were not shown because too many files have changed in this diff Show More