Compare commits

..

19 Commits

Author SHA1 Message Date
Zane Schepke 6d8953bf4e fmt 2025-04-29 07:30:37 -04:00
Zane Schepke 375cc978c5 add permissions 2025-04-29 07:25:50 -04:00
Zane Schepke 0f4399bb62 remove push 2025-04-29 07:20:36 -04:00
Zane Schepke 00fb8c7d29 fix: publish 2025-04-29 07:20:11 -04:00
Zane Schepke ab271225de fix: checksum 2025-04-29 07:15:25 -04:00
Zane Schepke 9180344043 fix: file pattern 2025-04-29 07:01:02 -04:00
Zane Schepke c5a2f6e311 fix: upload 2025-04-29 06:40:49 -04:00
Zane Schepke 4780c9bd6f test 2025-04-29 05:43:50 -04:00
Zane Schepke 6350ab6752 dispatch 2025-04-29 05:42:15 -04:00
Zane Schepke f6cf33b996 ci: remove nightly push 2025-04-29 05:40:11 -04:00
Zane Schepke 9872f4da2b ci: fix build upload 2025-04-29 05:39:05 -04:00
Zane Schepke b6fd93eae5 ci: refactor and fix ci bugs 2025-04-29 05:35:16 -04:00
Zane Schepke 85316bec3f build: change flavor name to improve clarity 2025-04-29 04:59:22 -04:00
Zane Schepke 1935653309 chore(deps): bump compose, datastore 2025-04-29 04:45:11 -04:00
Zane Schepke e3e24b4a06 fix: cleanup logs on update 2025-04-29 04:37:32 -04:00
Zane Schepke 7af53dcc18 fix: skip ping job for static configured tunnels
#741
2025-04-28 17:35:31 -04:00
Zane Schepke 2eb0ab0f19 fix: vpn permission bug
closes #754
2025-04-28 16:07:01 -04:00
Zane Schepke 07857a53c2 fix: regenerate icon to also trigger auth
closes #757
2025-04-28 15:17:55 -04:00
Zane Schepke 25fd31e252 fix: tunnel lock (#765)
fix: start up logger bug
refactor: switch to bound services
refactor: expose resolved peer endpoint
2025-04-28 15:06:43 -04:00
16 changed files with 224 additions and 105 deletions
+6 -7
View File
@@ -1,4 +1,6 @@
name: Build
name: build
permissions:
contents: read
on:
workflow_dispatch:
@@ -20,7 +22,7 @@ on:
default: fdroid
options:
- fdroid
- full
- standalone
secrets:
SIGNING_KEY_ALIAS:
required: false
@@ -94,10 +96,7 @@ jobs:
run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
- name: Create service_account.json
if: ${{ inputs.build_type != 'debug' }}
id: createServiceAccount
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
- name: Build APK
run: |
flavor=${{ inputs.flavor }}
@@ -123,6 +122,6 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: android_artifacts_${{ inputs.flavor }}
path: app/build/outputs/apk/${{ inputs.flavor }}/release/wgtunnel-${{ inputs.flavor }}-release-*.apk
path: app/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/wgtunnel-${{ inputs.flavor }}${{ inputs.flavor == 'fdroid' && '-release' || '' }}-*.apk
retention-days: 1
if-no-files-found: warn
+127
View File
@@ -0,0 +1,127 @@
name: nightly
permissions:
contents: write
packages: write
on:
workflow_dispatch:
schedule:
- cron: "4 3 * * *"
jobs:
check_commits:
name: Check for New Commits
runs-on: ubuntu-latest
outputs:
has_new_commits: ${{ steps.check.outputs.new_commits }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Check for new commits
id: check
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }})
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
build-standalone-nightly:
uses: ./.github/workflows/build.yml
secrets: inherit
with:
build_type: "nightly"
flavor: standalone
publish:
needs:
- check_commits
- build-standalone-nightly
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
name: publish-nightly
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install system dependencies
run: |
sudo apt update && sudo apt install -y gh apksigner
- name: Set latest tag
uses: rickstaa/action-create-tag@v1
id: tag_creation
with:
tag: "latest"
message: "Automated tag for HEAD commit"
force_push_tag: true
github_token: ${{ secrets.GITHUB_TOKEN }}
tag_exists_error: false
- name: Generate Changelog
id: changelog
uses: requarks/changelog-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
toTag: "nightly"
fromTag: "latest"
writeToFile: false
- name: Make download dir
run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts
uses: actions/download-artifact@v4
with:
pattern: android_artifacts_*
path: ${{ github.workspace }}/temp
- name: Set release notes
run: |
echo "RELEASE_NOTES=Nightly build for the latest development version of the app." >> $GITHUB_ENV
- name: Delete previous nightly version
uses: ClementTsang/delete-tag-and-release@v0.4.0
with:
tag_name: "nightly"
delete_release: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get checksum
id: checksum
run: |
file_path=$(find ${{ github.workspace }}/temp -type f -iname "*.apk" | head -n 1)
if [ -z "$file_path" ]; then
echo "No APK file found"
exit 1
fi
checksum=$(apksigner verify --print-certs "$file_path" | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")
echo "checksum=$checksum" >> $GITHUB_OUTPUT
- name: Create nightly release
id: create_release
uses: softprops/action-gh-release@v2
with:
body: |
${{ env.RELEASE_NOTES }}
SHA-256 fingerprints for the 4096-bit signing certificate:
```sh
${{ steps.checksum.outputs.checksum }}
```
To verify fingerprint:
```sh
apksigner verify --print-certs [path to APK file] | grep SHA-256
```
### Changelog
${{ steps.changelog.outputs.changes }}
tag_name: nightly
name: nightly
draft: false
prerelease: true
make_latest: false
files: |
${{ github.workspace }}/temp/**/*.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+2
View File
@@ -1,4 +1,6 @@
name: on-pr
permissions:
contents: read
on:
workflow_dispatch:
+33 -77
View File
@@ -1,8 +1,9 @@
name: publish
permissions:
contents: write
packages: write
on:
schedule:
- cron: "4 3 * * *"
workflow_dispatch:
inputs:
track:
@@ -22,95 +23,60 @@ on:
options:
- none
- prerelease
- nightly
- release
default: release
required: true
tag_name:
description: "Tag name for release"
required: false
default: nightly
default: 1.1.1
flavor:
type: choice
description: "Product flavor"
required: true
default: full
default: standalone
options:
- fdroid
- full
- standalone
workflow_call:
inputs:
flavor:
type: string
description: "Product flavor"
required: false
default: full
env:
UPLOAD_DIR_ANDROID: android_artifacts
permissions:
contents: write
packages: write
default: standalone
jobs:
check_commits:
name: Check for New Commits
runs-on: ubuntu-latest
outputs:
has_new_commits: ${{ steps.check.outputs.new_commits }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for new commits
id: check
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }})
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
build-fdroid:
if: ${{ inputs.release_type == 'release' || inputs.flavor == 'fdroid' }}
uses: ./.github/workflows/build.yml
secrets: inherit
with:
build_type: ${{ inputs.release_type == '' && 'nightly' || inputs.release_type }}
build_type: ${{ inputs.release_type }}
flavor: fdroid
build-full:
if: ${{ inputs.release_type == 'release' || inputs.release_type == 'nightly' || inputs.release_type == 'prerelease' || inputs.flavor == 'full' }}
build-standalone:
if: ${{ inputs.release_type == 'release' || inputs.release_type == 'prerelease' || inputs.flavor == 'standalone' }}
uses: ./.github/workflows/build.yml
secrets: inherit
with:
build_type: ${{ inputs.release_type == '' && 'nightly' || inputs.release_type }}
flavor: full
build_type: ${{ inputs.release_type }}
flavor: standalone
publish:
needs:
- check_commits
- build-full
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
- build-standalone
name: publish-github
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Install system dependencies
run: |
sudo apt update && sudo apt install -y gh apksigner
- name: Set TAG_NAME
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV
elif [ "${{ github.event_name }}" = "schedule" ]; then
echo "TAG_NAME=nightly" >> $GITHUB_ENV
echo "RELEASE_TYPE=nightly" >> $GITHUB_ENV
fi
- name: Set latest tag
uses: rickstaa/action-create-tag@v1
id: tag_creation
@@ -126,22 +92,26 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
latest: true
- name: Generate Changelog
id: changelog
uses: requarks/changelog-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
toTag: ${{ github.event_name == 'schedule' && 'nightly' || steps.latest_release.outputs.tag_name }}
toTag: ${{ steps.latest_release.outputs.tag_name }}
fromTag: "latest"
writeToFile: false
- name: Make download dir
run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts
uses: actions/download-artifact@v4
with:
pattern: android_artifacts_*
path: ${{ github.workspace }}/temp
merge-multiple: true
- name: Set version release notes
if: ${{ inputs.release_type == 'release' }}
run: |
@@ -151,36 +121,22 @@ jobs:
echo "$RELEASE_NOTES" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: On nightly release notes
if: ${{ contains(env.TAG_NAME, 'nightly') }}
run: |
echo "RELEASE_NOTES=Nightly build for the latest development version of the app." >> $GITHUB_ENV
- name: On prerelease release notes
if: ${{ inputs.release_type == 'prerelease' }}
run: |
echo "RELEASE_NOTES=Testing version of app for specific feature." >> $GITHUB_ENV
- name: Delete previous release
if: ${{ contains(env.TAG_NAME, 'nightly') || inputs.release_type == 'prerelease' }}
uses: ClementTsang/delete-tag-and-release@v0.4.0
with:
tag_name: ${{ env.TAG_NAME }}
delete_release: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get checksums
- name: Get checksum
id: checksum
run: |
checksums=""
for file_path in $(find ${{ github.workspace }}/temp -type f -iname "*.apk"); do
checksum=$(apksigner verify -print-certs $file_path | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")
checksums="$checksums\n$file_path: $checksum"
done
echo "checksum<<EOF" >> $GITHUB_OUTPUT
echo -e "$checksums" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
file_path=$(find ${{ github.workspace }}/temp -type f -iname "*.apk" | head -n 1)
if [ -z "$file_path" ]; then
echo "No APK file found"
exit 1
fi
checksum=$(apksigner verify --print-certs "$file_path" | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")
echo "checksum=$checksum" >> $GITHUB_OUTPUT
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v2
@@ -200,13 +156,13 @@ jobs:
### Changelog
${{ steps.changelog.outputs.changes }}
tag_name: ${{ env.TAG_NAME }}
name: ${{ env.TAG_NAME }}
tag_name: ${{ github.event.inputs.tag_name }}
name: ${{ github.event.inputs.tag_name }}
draft: false
prerelease: ${{ inputs.release_type == 'prerelease' || inputs.release_type == '' || inputs.release_type == 'nightly' }}
prerelease: ${{ inputs.release_type == 'prerelease' }}
make_latest: ${{ inputs.release_type == 'release' }}
files: |
${{ github.workspace }}/temp/*
${{ github.workspace }}/temp/**/*.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -224,7 +180,7 @@ jobs:
event-type: fdroid-update
publish-play:
if: ${{ inputs.track != 'none' && inputs.track != '' }}
if: ${{ inputs.track != 'none' }}
name: Publish to Google Play
runs-on: ubuntu-latest
+4 -1
View File
@@ -103,7 +103,10 @@ android {
dimension = "type"
buildConfigField("String", "FLAVOR", "\"google\"")
}
create("full") { dimension = "type" }
create("standalone") {
dimension = "type"
buildConfigField("String", "FLAVOR", "\"standalone\"")
}
}
compileOptions {
-1
View File
@@ -168,7 +168,6 @@
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.USER_PRESENT" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
@@ -134,6 +134,7 @@ class MainActivity : AppCompatActivity() {
vpnPermissionDenied = true
} else {
vpnPermissionDenied = false
showVpnPermissionDialog = false
}
},
)
@@ -208,7 +209,10 @@ class MainActivity : AppCompatActivity() {
WireguardAutoTunnelTheme(theme = appUiState.appState.theme) {
VpnDeniedDialog(
showVpnPermissionDialog,
onDismiss = { showVpnPermissionDialog = false },
onDismiss = {
showVpnPermissionDialog = false
vpnPermissionDenied = false
},
)
Scaffold(
@@ -3,12 +3,12 @@ package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -26,13 +26,12 @@ class RestartReceiver : BroadcastReceiver() {
@Inject lateinit var tunnelManager: TunnelManager
@Inject lateinit var logReader: LogReader
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
override fun onReceive(context: Context, intent: Intent) {
Timber.d("RestartReceiver triggered with action: ${intent.action}")
// screen on for Android TV only to help with sleep shutdowns
val isTv = context.isRunningOnTv()
if (intent.action == Intent.ACTION_USER_PRESENT && !isTv) return
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
applicationScope.launch(ioDispatcher) {
@@ -50,6 +49,7 @@ class RestartReceiver : BroadcastReceiver() {
} else {
Timber.d("Restore on boot disabled, skipping")
}
if (intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) logReader.deleteAndClearLogs()
}
}
}
@@ -165,8 +165,12 @@ class TunnelForegroundService : LifecycleService() {
} else {
pingJobs[tun]?.cancel() // Cancel any stale job
if (tun.isPingEnabled) {
pingJobs[tun] = startPingJob(tun)
Timber.d("Started ping job for ${tun.tunName}")
if (tun.isStaticallyConfigured()) {
Timber.d("Skipping ping for statically configured tunnel")
} else {
pingJobs[tun] = startPingJob(tun)
Timber.d("Started ping job for ${tun.tunName}")
}
}
}
}
@@ -60,6 +60,10 @@ data class TunnelConf(
return result
}
fun isStaticallyConfigured(): Boolean {
return toAmConfig().peers.all { it.endpoint.get().host.isValidIpv4orIpv6Address() }
}
fun copyWithCallback(
id: Int = this.id,
tunName: String = this.tunName,
@@ -52,8 +52,9 @@ fun InterfaceFields(
if (isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
IconButton(
enabled = isAuthenticated,
enabled = true,
onClick = {
if (!isAuthenticated) return@IconButton showAuthPrompt()
val keypair = com.wireguard.crypto.KeyPair()
onInterfaceChange(
interfaceState.copy(
@@ -23,6 +23,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.ContactSupportOptions
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.GeneralSupportOptions
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.UpdateSection
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.canInstallPackages
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.requestInstallPackagesPermission
@@ -54,7 +55,7 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), appViewModel: A
InfoDialog(
onDismiss = { viewModel.handleUpdateShown() },
onAttest = {
if (BuildConfig.FLAVOR != "full") {
if (BuildConfig.FLAVOR != Constants.STANDALONE_FLAVOR) {
uiState.appUpdate?.apkUrl?.let { context.openWebUrl(it) }
return@InfoDialog
}
@@ -86,7 +87,8 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), appViewModel: A
},
confirmText = {
Text(
if (BuildConfig.FLAVOR != "full") stringResource(R.string.download)
if (BuildConfig.FLAVOR != Constants.STANDALONE_FLAVOR)
stringResource(R.string.download)
else stringResource(R.string.download_and_install)
)
},
@@ -124,7 +126,7 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), appViewModel: A
if (
BuildConfig.DEBUG ||
BuildConfig.VERSION_NAME.contains("beta") ||
BuildConfig.FLAVOR == "google"
BuildConfig.FLAVOR == Constants.GOOGLE_PLAY_FLAVOR
)
return@UpdateSection context.showToast(R.string.update_check_unsupported)
context.showToast(R.string.checking_for_update)
@@ -85,7 +85,7 @@ fun ContactSupportOptions(context: android.content.Context) {
),
)
)
if (BuildConfig.FLAVOR == Constants.FDROID_FLAVOR) {
if (BuildConfig.FLAVOR != Constants.GOOGLE_PLAY_FLAVOR) {
add(
SelectionItem(
leadingIcon = Icons.Filled.Favorite,
@@ -35,5 +35,7 @@ object Constants {
const val QR_CODE_NAME_PROPERTY = "# Name ="
const val FDROID_FLAVOR = "fdroid"
const val GOOGLE_PLAY_FLAVOR = "google"
const val STANDALONE_FLAVOR = "standalone"
const val RELEASE = "release"
}
@@ -1,14 +1,29 @@
package com.zaneschepke.wireguardautotunnel.util.extensions
import java.util.regex.Pattern
import timber.log.Timber
val hasNumberInParentheses = """^(.+?)\((\d+)\)$""".toRegex()
fun String.isValidIpv4orIpv6Address(): Boolean {
val ipv4Pattern = Pattern.compile("^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\$")
val ipv6Pattern = Pattern.compile("^([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}\$")
return ipv4Pattern.matcher(this).matches() || ipv6Pattern.matcher(this).matches()
val sanitized = removeSurrounding("[", "]")
val ipv6Pattern =
Regex(
"(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:)" +
"{1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]" +
"{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:" +
"[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4})" +
"{1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}" +
":((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]" +
"{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}" +
"[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:)" +
"{1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"
)
val ipv4Pattern =
Regex(
"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}" +
"(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
)
return ipv4Pattern.matches(sanitized) || ipv6Pattern.matches(sanitized)
}
fun String.hasNumberInParentheses(): Boolean {
+4 -3
View File
@@ -6,7 +6,7 @@ androidx-junit = "1.2.1"
appcompat = "1.7.0"
biometricKtx = "1.2.0-alpha05"
coreKtx = "1.16.0"
datastorePreferences = "1.1.4"
datastorePreferences = "1.2.0-alpha01"
desugar_jdk_libs = "2.1.5"
espressoCore = "3.6.1"
hiltAndroid = "2.56.2"
@@ -28,7 +28,8 @@ androidGradlePlugin = "8.9.2"
kotlin = "2.1.20"
ksp = "2.1.20-2.0.0"
composeBom = "2025.04.01"
compose = "1.7.8"
compose = "1.8.0"
icons = "1.7.8"
workRuntimeKtxVersion = "2.10.1"
zxingAndroidEmbedded = "4.3.0"
coreSplashscreen = "1.0.1"
@@ -95,7 +96,7 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorCli
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktorClientCore" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktorClientCore" }
lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle-runtime-compose" }
material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" }
material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "icons" }
pin-lock-compose = { module = "com.zaneschepke:pin_lock_compose", version.ref = "pinLockCompose" }
qrcode-kotlin = { module = "io.github.g0dkar:qrcode-kotlin", version.ref = "qrcodeKotlin" }