refactor(errors): унифицировал доменные ошибки и добавил failed-статус задач
This commit is contained in:
@@ -6,6 +6,7 @@ import androidx.activity.result.ActivityResultLauncher
|
|||||||
import com.github.nullptroma.wallenc.domain.vault.vaults.yandex.YandexRegistration
|
import com.github.nullptroma.wallenc.domain.vault.vaults.yandex.YandexRegistration
|
||||||
import com.github.nullptroma.wallenc.vault.contract.CloudBrand
|
import com.github.nullptroma.wallenc.vault.contract.CloudBrand
|
||||||
import com.github.nullptroma.wallenc.vault.contract.RemoteVaultAuthenticator
|
import com.github.nullptroma.wallenc.vault.contract.RemoteVaultAuthenticator
|
||||||
|
import com.github.nullptroma.wallenc.vault.contract.VaultLinkFailure
|
||||||
import com.github.nullptroma.wallenc.vault.contract.VaultLinkOutcome
|
import com.github.nullptroma.wallenc.vault.contract.VaultLinkOutcome
|
||||||
import com.yandex.authsdk.YandexAuthLoginOptions
|
import com.yandex.authsdk.YandexAuthLoginOptions
|
||||||
import com.yandex.authsdk.YandexAuthOptions
|
import com.yandex.authsdk.YandexAuthOptions
|
||||||
@@ -70,16 +71,12 @@ class YandexSignInService @Inject constructor(
|
|||||||
|
|
||||||
override fun beginLink(brand: CloudBrand, onResult: (VaultLinkOutcome) -> Unit) {
|
override fun beginLink(brand: CloudBrand, onResult: (VaultLinkOutcome) -> Unit) {
|
||||||
if (brand != CloudBrand.YANDEX) {
|
if (brand != CloudBrand.YANDEX) {
|
||||||
onResult(VaultLinkOutcome.Failed("Brand $brand is not supported by YandexSignInService"))
|
onResult(VaultLinkOutcome.Failed(VaultLinkFailure.UnsupportedBrand))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val l = launcher
|
val l = launcher
|
||||||
if (l == null) {
|
if (l == null) {
|
||||||
onResult(
|
onResult(VaultLinkOutcome.Failed(VaultLinkFailure.NotRegistered))
|
||||||
VaultLinkOutcome.Failed(
|
|
||||||
"YandexSignInService: call registerWith(activity) from MainActivity.onCreate first",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
synchronized(this) { pending = onResult }
|
synchronized(this) { pending = onResult }
|
||||||
@@ -90,9 +87,7 @@ class YandexSignInService @Inject constructor(
|
|||||||
is YandexAuthResult.Success ->
|
is YandexAuthResult.Success ->
|
||||||
VaultLinkOutcome.Success(YandexRegistration(result.token.value))
|
VaultLinkOutcome.Success(YandexRegistration(result.token.value))
|
||||||
is YandexAuthResult.Failure ->
|
is YandexAuthResult.Failure ->
|
||||||
VaultLinkOutcome.Failed(
|
VaultLinkOutcome.Failed(VaultLinkFailure.AuthError)
|
||||||
result.exception.message ?: result.exception.toString(),
|
|
||||||
)
|
|
||||||
YandexAuthResult.Cancelled -> VaultLinkOutcome.Cancelled
|
YandexAuthResult.Cancelled -> VaultLinkOutcome.Cancelled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.github.nullptroma.wallenc.domain.vault.errors
|
||||||
|
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.toWallencException
|
||||||
|
import com.github.nullptroma.wallenc.domain.vault.network.yandexdisk.YandexDiskAuthException
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
fun Throwable.toVaultWallencException(): WallencException = when (this) {
|
||||||
|
is WallencException -> this
|
||||||
|
is YandexDiskAuthException -> WallencException.Auth.Failed
|
||||||
|
is HttpException -> WallencException.Network.HttpFailed(
|
||||||
|
operation = "http",
|
||||||
|
statusCode = code(),
|
||||||
|
cause = this,
|
||||||
|
)
|
||||||
|
is FileNotFoundException -> WallencException.Storage.FileNotFound
|
||||||
|
is IOException -> mapVaultIo(this)
|
||||||
|
is IllegalStateException -> mapIllegalState(this)
|
||||||
|
is IllegalArgumentException -> mapIllegalArgument(this)
|
||||||
|
else -> toWallencException()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapVaultIo(e: IOException): WallencException {
|
||||||
|
val msg = e.message.orEmpty()
|
||||||
|
return when {
|
||||||
|
msg.contains("OAuth token is missing", ignoreCase = true) ->
|
||||||
|
WallencException.Auth.TokenMissing
|
||||||
|
msg.contains("HTTP 423", ignoreCase = true) || msg.contains("423 after retries", ignoreCase = true) ->
|
||||||
|
WallencException.Network.ResourceLocked
|
||||||
|
msg.contains("async operation timed out", ignoreCase = true) ->
|
||||||
|
WallencException.Network.OperationTimedOut
|
||||||
|
msg.contains("async operation failed", ignoreCase = true) ->
|
||||||
|
WallencException.Network.OperationFailed
|
||||||
|
else -> WallencException.Network.IoFailed(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapIllegalState(e: IllegalStateException): WallencException = when {
|
||||||
|
e.message == "Not a file" -> WallencException.Storage.NotAFile
|
||||||
|
e.message == "Not a directory" -> WallencException.Storage.NotADirectory
|
||||||
|
e.message?.startsWith("Path segment is a file:") == true -> WallencException.Storage.PathIsFile
|
||||||
|
e.message?.startsWith("Cannot openWrite over directory:") == true ->
|
||||||
|
WallencException.Storage.CannotWriteOverDirectory
|
||||||
|
e.message?.startsWith("Expected file after upload:") == true ->
|
||||||
|
WallencException.Storage.UnexpectedState
|
||||||
|
else -> WallencException.Storage.UnexpectedState
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapIllegalArgument(e: IllegalArgumentException): WallencException = when {
|
||||||
|
e.message == "Deleting root path is forbidden" -> WallencException.Storage.DeleteRootForbidden
|
||||||
|
else -> WallencException.Unknown(e)
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.vault.storages
|
package com.github.nullptroma.wallenc.domain.vault.storages
|
||||||
|
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||||
import com.github.nullptroma.wallenc.domain.vault.model.StorageKeyMap
|
import com.github.nullptroma.wallenc.domain.vault.model.StorageKeyMap
|
||||||
import com.github.nullptroma.wallenc.domain.vault.ports.StorageKeyMapStore
|
import com.github.nullptroma.wallenc.domain.vault.ports.StorageKeyMapStore
|
||||||
import com.github.nullptroma.wallenc.domain.vault.storages.encrypt.EncryptedStorage
|
import com.github.nullptroma.wallenc.domain.vault.storages.encrypt.EncryptedStorage
|
||||||
@@ -109,9 +110,9 @@ class UnlockManager(
|
|||||||
rememberPassword: Boolean
|
rememberPassword: Boolean
|
||||||
): EncryptedStorage = withContext(ioDispatcher) {
|
): EncryptedStorage = withContext(ioDispatcher) {
|
||||||
return@withContext mutex.withLock {
|
return@withContext mutex.withLock {
|
||||||
val encInfo = storage.metaInfo.value.encInfo ?: throw Exception("EncInfo is null") // TODO
|
val encInfo = storage.metaInfo.value.encInfo ?: throw WallencException.Storage.EncInfoMissing
|
||||||
if (!Encryptor.checkKey(key, encInfo))
|
if (!Encryptor.checkKey(key, encInfo))
|
||||||
throw Exception("Incorrect Key")
|
throw WallencException.Storage.IncorrectKey
|
||||||
|
|
||||||
val opened = _openedStorages.value.toMutableMap()
|
val opened = _openedStorages.value.toMutableMap()
|
||||||
val cur = opened[storage.uuid]
|
val cur = opened[storage.uuid]
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.vault.storages.encrypt
|
package com.github.nullptroma.wallenc.domain.vault.storages.encrypt
|
||||||
|
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||||
|
|
||||||
import com.github.nullptroma.wallenc.domain.vault.storages.common.BaseStorage
|
import com.github.nullptroma.wallenc.domain.vault.storages.common.BaseStorage
|
||||||
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
|
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
|
||||||
import com.github.nullptroma.wallenc.domain.encrypt.Encryptor
|
import com.github.nullptroma.wallenc.domain.encrypt.Encryptor
|
||||||
@@ -27,7 +29,7 @@ class EncryptedStorage private constructor(
|
|||||||
private val scope = CoroutineScope(ioDispatcher + job)
|
private val scope = CoroutineScope(ioDispatcher + job)
|
||||||
|
|
||||||
private val encInfo =
|
private val encInfo =
|
||||||
source.metaInfo.value.encInfo ?: throw Exception("Storage is not encrypted") // TODO
|
source.metaInfo.value.encInfo ?: throw WallencException.Storage.NotEncrypted
|
||||||
|
|
||||||
override val isVirtualStorage: Boolean = true
|
override val isVirtualStorage: Boolean = true
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@ class EncryptedStorage private constructor(
|
|||||||
|
|
||||||
private fun checkKey() {
|
private fun checkKey() {
|
||||||
if (!Encryptor.checkKey(key, encInfo))
|
if (!Encryptor.checkKey(key, encInfo))
|
||||||
throw Exception("Incorrect key") // TODO
|
throw WallencException.Storage.IncorrectKey
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getKey(): EncryptKey = EncryptKey(key.bytes)
|
fun getKey(): EncryptKey = EncryptKey(key.bytes)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.vault.storages.local
|
package com.github.nullptroma.wallenc.domain.vault.storages.local
|
||||||
|
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JacksonException
|
import com.fasterxml.jackson.core.JacksonException
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
@@ -230,7 +232,7 @@ class LocalStorageAccessor(
|
|||||||
dirCallback: (suspend (File, CommonDirectory) -> Unit)? = null
|
dirCallback: (suspend (File, CommonDirectory) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
if (!checkAvailable())
|
if (!checkAvailable())
|
||||||
throw Exception("Not available")
|
throw WallencException.Storage.NotAvailable
|
||||||
val basePath = Path(_filesystemBasePath.pathString, baseStoragePath)
|
val basePath = Path(_filesystemBasePath.pathString, baseStoragePath)
|
||||||
val workedFiles = mutableSetOf<String>()
|
val workedFiles = mutableSetOf<String>()
|
||||||
val workedMetaFiles = mutableSetOf<String>()
|
val workedMetaFiles = mutableSetOf<String>()
|
||||||
@@ -396,7 +398,7 @@ class LocalStorageAccessor(
|
|||||||
|
|
||||||
override suspend fun getFileInfo(path: String): IFile {
|
override suspend fun getFileInfo(path: String): IFile {
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
||||||
?: throw Exception("Что то пошло не так") // TODO
|
?: throw WallencException.Storage.UnexpectedState
|
||||||
return CommonFile(
|
return CommonFile(
|
||||||
metaInfo = pair.meta,
|
metaInfo = pair.meta,
|
||||||
)
|
)
|
||||||
@@ -404,7 +406,7 @@ class LocalStorageAccessor(
|
|||||||
|
|
||||||
override suspend fun getDirInfo(path: String): IDirectory {
|
override suspend fun getDirInfo(path: String): IDirectory {
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
||||||
?: throw Exception("Что то пошло не так") // TODO
|
?: throw WallencException.Storage.UnexpectedState
|
||||||
return CommonDirectory(
|
return CommonDirectory(
|
||||||
metaInfo = pair.meta,
|
metaInfo = pair.meta,
|
||||||
elementsCount = null
|
elementsCount = null
|
||||||
@@ -413,7 +415,7 @@ class LocalStorageAccessor(
|
|||||||
|
|
||||||
override suspend fun setHidden(path: String, hidden: Boolean) {
|
override suspend fun setHidden(path: String, hidden: Boolean) {
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
||||||
?: throw Exception("Что то пошло не так") // TODO
|
?: throw WallencException.Storage.UnexpectedState
|
||||||
if (pair.meta.isHidden == hidden)
|
if (pair.meta.isHidden == hidden)
|
||||||
return
|
return
|
||||||
val newMeta = pair.meta.copy(isHidden = hidden)
|
val newMeta = pair.meta.copy(isHidden = hidden)
|
||||||
@@ -440,7 +442,7 @@ class LocalStorageAccessor(
|
|||||||
val path = Path(_filesystemBasePath.pathString, storagePath)
|
val path = Path(_filesystemBasePath.pathString, storagePath)
|
||||||
val file = path.toFile()
|
val file = path.toFile()
|
||||||
if (file.exists() && file.isDirectory) {
|
if (file.exists() && file.isDirectory) {
|
||||||
throw Exception("Что то пошло не так") // TODO
|
throw WallencException.Storage.UnexpectedState
|
||||||
} else if(!file.exists()) {
|
} else if(!file.exists()) {
|
||||||
val parent = Path(storagePath).parent
|
val parent = Path(storagePath).parent
|
||||||
createDir(parent.pathString)
|
createDir(parent.pathString)
|
||||||
@@ -451,7 +453,7 @@ class LocalStorageAccessor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, file)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, file)
|
||||||
?: throw Exception("Что то пошло не так") // TODO
|
?: throw WallencException.Storage.UnexpectedState
|
||||||
val newMeta = pair.meta.copy(lastModified = Clock.systemUTC().instant(), size = Files.size(pair.file.toPath()))
|
val newMeta = pair.meta.copy(lastModified = Clock.systemUTC().instant(), size = Files.size(pair.file.toPath()))
|
||||||
writeMeta(pair.metaFile, newMeta)
|
writeMeta(pair.metaFile, newMeta)
|
||||||
_filesUpdates.emit(
|
_filesUpdates.emit(
|
||||||
@@ -468,13 +470,13 @@ class LocalStorageAccessor(
|
|||||||
val path = Path(_filesystemBasePath.pathString, storagePath)
|
val path = Path(_filesystemBasePath.pathString, storagePath)
|
||||||
val file = path.toFile()
|
val file = path.toFile()
|
||||||
if (file.exists() && !file.isDirectory) {
|
if (file.exists() && !file.isDirectory) {
|
||||||
throw Exception("Что то пошло не так") // TODO
|
throw WallencException.Storage.UnexpectedState
|
||||||
} else if(!file.exists()) {
|
} else if(!file.exists()) {
|
||||||
Files.createDirectories(path)
|
Files.createDirectories(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, file)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, file)
|
||||||
?: throw Exception("Что то пошло не так") // TODO
|
?: throw WallencException.Storage.UnexpectedState
|
||||||
val newMeta = pair.meta.copy(lastModified = Clock.systemUTC().instant())
|
val newMeta = pair.meta.copy(lastModified = Clock.systemUTC().instant())
|
||||||
writeMeta(pair.metaFile, newMeta)
|
writeMeta(pair.metaFile, newMeta)
|
||||||
_dirsUpdates.emit(
|
_dirsUpdates.emit(
|
||||||
@@ -512,7 +514,7 @@ class LocalStorageAccessor(
|
|||||||
|
|
||||||
override suspend fun delete(path: String) = withContext(ioDispatcher) {
|
override suspend fun delete(path: String) = withContext(ioDispatcher) {
|
||||||
if (path == "/" || path.isBlank()) {
|
if (path == "/" || path.isBlank()) {
|
||||||
throw IllegalArgumentException("Deleting root path is forbidden")
|
throw WallencException.Storage.DeleteRootForbidden
|
||||||
}
|
}
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
||||||
if (pair != null) {
|
if (pair != null) {
|
||||||
@@ -527,7 +529,7 @@ class LocalStorageAccessor(
|
|||||||
override suspend fun openWrite(path: String): OutputStream = withContext(ioDispatcher) {
|
override suspend fun openWrite(path: String): OutputStream = withContext(ioDispatcher) {
|
||||||
touchFileInternal(path, recordJournal = false)
|
touchFileInternal(path, recordJournal = false)
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
||||||
?: throw Exception("Файла нет") // TODO
|
?: throw WallencException.Storage.FileNotFound
|
||||||
return@withContext pair.file.outputStream().onClosed {
|
return@withContext pair.file.outputStream().onClosed {
|
||||||
CoroutineScope(ioDispatcher).launch {
|
CoroutineScope(ioDispatcher).launch {
|
||||||
touchFileInternal(path, recordJournal = false)
|
touchFileInternal(path, recordJournal = false)
|
||||||
@@ -539,13 +541,13 @@ class LocalStorageAccessor(
|
|||||||
|
|
||||||
override suspend fun openRead(path: String): InputStream = withContext(ioDispatcher) {
|
override suspend fun openRead(path: String): InputStream = withContext(ioDispatcher) {
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
||||||
?: throw Exception("Файла нет") // TODO
|
?: throw WallencException.Storage.FileNotFound
|
||||||
return@withContext pair.file.inputStream()
|
return@withContext pair.file.inputStream()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun moveToTrash(path: String) = withContext(ioDispatcher) {
|
override suspend fun moveToTrash(path: String) = withContext(ioDispatcher) {
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
||||||
?: throw Exception("Файла нет") // TODO
|
?: throw WallencException.Storage.FileNotFound
|
||||||
val newMeta = pair.meta.copy(isDeleted = true)
|
val newMeta = pair.meta.copy(isDeleted = true)
|
||||||
writeMeta(pair.metaFile, newMeta)
|
writeMeta(pair.metaFile, newMeta)
|
||||||
appendSyncEntry(path = path, operation = StorageSyncOperation.DELETE)
|
appendSyncEntry(path = path, operation = StorageSyncOperation.DELETE)
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.vault.storages.yandex
|
package com.github.nullptroma.wallenc.domain.vault.storages.yandex
|
||||||
|
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||||
|
import com.github.nullptroma.wallenc.domain.vault.errors.toVaultWallencException
|
||||||
|
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.github.nullptroma.wallenc.domain.vault.network.yandexdisk.YandexDiskAuthException
|
import com.github.nullptroma.wallenc.domain.vault.network.yandexdisk.YandexDiskAuthException
|
||||||
import com.github.nullptroma.wallenc.domain.vault.network.yandexdisk.dto.ResourceDto
|
import com.github.nullptroma.wallenc.domain.vault.network.yandexdisk.dto.ResourceDto
|
||||||
@@ -104,19 +107,23 @@ class YandexStorageAccessor(
|
|||||||
} catch (e: YandexDiskAuthException) {
|
} catch (e: YandexDiskAuthException) {
|
||||||
reportAuthFailure()
|
reportAuthFailure()
|
||||||
_storageReady.value = false
|
_storageReady.value = false
|
||||||
throw e
|
throw WallencException.Auth.Failed
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
_storageReady.value = false
|
_storageReady.value = false
|
||||||
throw Exception("Yandex storage init failed", e)
|
throw e.toVaultWallencException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun <T> guard(block: () -> T): T {
|
private inline fun <T> guard(block: () -> T): T {
|
||||||
try {
|
try {
|
||||||
return block()
|
return block()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: YandexDiskAuthException) {
|
} catch (e: YandexDiskAuthException) {
|
||||||
reportAuthFailure()
|
reportAuthFailure()
|
||||||
throw e
|
throw WallencException.Auth.Failed
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
throw e.toVaultWallencException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,13 +452,13 @@ class YandexStorageAccessor(
|
|||||||
|
|
||||||
override suspend fun getFileInfo(path: String): IFile = withContext(ioDispatcher) {
|
override suspend fun getFileInfo(path: String): IFile = withContext(ioDispatcher) {
|
||||||
val r = guard { repo.get(toDiskPath(path)) }
|
val r = guard { repo.get(toDiskPath(path)) }
|
||||||
if (r.type != "file") throw IllegalStateException("Not a file")
|
if (r.type != "file") throw WallencException.Storage.NotAFile
|
||||||
r.toCommonFile(path)
|
r.toCommonFile(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getDirInfo(path: String): IDirectory = withContext(ioDispatcher) {
|
override suspend fun getDirInfo(path: String): IDirectory = withContext(ioDispatcher) {
|
||||||
val r = guard { repo.get(toDiskPath(path)) }
|
val r = guard { repo.get(toDiskPath(path)) }
|
||||||
if (r.type != "dir") throw IllegalStateException("Not a directory")
|
if (r.type != "dir") throw WallencException.Storage.NotADirectory
|
||||||
r.toCommonDir(path)
|
r.toCommonDir(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,7 +497,7 @@ class YandexStorageAccessor(
|
|||||||
val diskPath = toDiskPath(acc)
|
val diskPath = toDiskPath(acc)
|
||||||
when (guard { repo.getOrNull(diskPath) }?.type) {
|
when (guard { repo.getOrNull(diskPath) }?.type) {
|
||||||
"dir" -> continue
|
"dir" -> continue
|
||||||
"file" -> throw IllegalStateException("Path segment is a file: $acc")
|
"file" -> throw WallencException.Storage.PathIsFile
|
||||||
else -> guard { repo.createFolder(diskPath) }
|
else -> guard { repo.createFolder(diskPath) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -498,7 +505,7 @@ class YandexStorageAccessor(
|
|||||||
|
|
||||||
override suspend fun delete(path: String) = withContext(ioDispatcher) {
|
override suspend fun delete(path: String) = withContext(ioDispatcher) {
|
||||||
if (path == "/" || path.isBlank()) {
|
if (path == "/" || path.isBlank()) {
|
||||||
throw IllegalArgumentException("Deleting root path is forbidden")
|
throw WallencException.Storage.DeleteRootForbidden
|
||||||
}
|
}
|
||||||
val diskPath = toDiskPath(path)
|
val diskPath = toDiskPath(path)
|
||||||
val prior = guard { repo.getOrNull(diskPath) }
|
val prior = guard { repo.getOrNull(diskPath) }
|
||||||
@@ -531,14 +538,14 @@ class YandexStorageAccessor(
|
|||||||
val diskPath = toDiskPath(path)
|
val diskPath = toDiskPath(path)
|
||||||
val prior = guard { repo.getOrNull(diskPath) }
|
val prior = guard { repo.getOrNull(diskPath) }
|
||||||
if (prior?.type == "dir") {
|
if (prior?.type == "dir") {
|
||||||
throw IllegalStateException("Cannot openWrite over directory: $path")
|
throw WallencException.Storage.CannotWriteOverDirectory
|
||||||
}
|
}
|
||||||
val hadFile = prior?.type == "file"
|
val hadFile = prior?.type == "file"
|
||||||
val priorSize = if (prior?.type == "file") prior.size ?: 0L else 0L
|
val priorSize = if (prior?.type == "file") prior.size ?: 0L else 0L
|
||||||
guard { repo.uploadFile(diskPath, tmp, overwrite = true) }
|
guard { repo.uploadFile(diskPath, tmp, overwrite = true) }
|
||||||
val after = guard { getMetadataAfterWrite(diskPath) }
|
val after = guard { getMetadataAfterWrite(diskPath) }
|
||||||
if (after.type != "file") {
|
if (after.type != "file") {
|
||||||
throw IllegalStateException("Expected file after upload: $path")
|
throw WallencException.Storage.UnexpectedState
|
||||||
}
|
}
|
||||||
val newSize = after.size ?: 0L
|
val newSize = after.size ?: 0L
|
||||||
_size.value = ((_size.value ?: 0L) + newSize - priorSize).coerceAtLeast(0L)
|
_size.value = ((_size.value ?: 0L) + newSize - priorSize).coerceAtLeast(0L)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.vault.vaults.local
|
package com.github.nullptroma.wallenc.domain.vault.vaults.local
|
||||||
|
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||||
|
|
||||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||||
import com.github.nullptroma.wallenc.domain.vault.storages.local.LocalStorage
|
import com.github.nullptroma.wallenc.domain.vault.storages.local.LocalStorage
|
||||||
@@ -64,7 +66,7 @@ class LocalVault(
|
|||||||
private suspend fun readStorages() {
|
private suspend fun readStorages() {
|
||||||
val path = path.value
|
val path = path.value
|
||||||
if (path == null || !_isAvailable.value) {
|
if (path == null || !_isAvailable.value) {
|
||||||
throw Exception("Not available")
|
throw WallencException.Storage.NotAvailable
|
||||||
}
|
}
|
||||||
|
|
||||||
val dirs = path.listFiles()?.filter { it.isDirectory }
|
val dirs = path.listFiles()?.filter { it.isDirectory }
|
||||||
@@ -79,7 +81,7 @@ class LocalVault(
|
|||||||
override suspend fun createStorage(): LocalStorage = withContext(ioDispatcher) {
|
override suspend fun createStorage(): LocalStorage = withContext(ioDispatcher) {
|
||||||
val path = path.value
|
val path = path.value
|
||||||
if (path == null || !_isAvailable.value) {
|
if (path == null || !_isAvailable.value) {
|
||||||
throw Exception("Not available")
|
throw WallencException.Storage.NotAvailable
|
||||||
}
|
}
|
||||||
|
|
||||||
val storageUuid = UUID.randomUUID()
|
val storageUuid = UUID.randomUUID()
|
||||||
@@ -104,7 +106,7 @@ class LocalVault(
|
|||||||
override suspend fun remove(storage: IStorage) = withContext(ioDispatcher) {
|
override suspend fun remove(storage: IStorage) = withContext(ioDispatcher) {
|
||||||
val path = path.value
|
val path = path.value
|
||||||
if (path == null || !_isAvailable.value) {
|
if (path == null || !_isAvailable.value) {
|
||||||
throw Exception("Not available")
|
throw WallencException.Storage.NotAvailable
|
||||||
}
|
}
|
||||||
|
|
||||||
val curStorages = _storages.value.toMutableList()
|
val curStorages = _storages.value.toMutableList()
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.github.nullptroma.wallenc.domain.errors
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Единая иерархия сбоев Wallenc. Ожидаемые бизнес-отказы (CanEncryptResult и т.п.)
|
||||||
|
* остаются отдельными sealed-типами; сюда попадают только операционные ошибки.
|
||||||
|
*/
|
||||||
|
sealed class WallencException(
|
||||||
|
message: String? = null,
|
||||||
|
cause: Throwable? = null,
|
||||||
|
) : Exception(message, cause) {
|
||||||
|
|
||||||
|
sealed class Feature : WallencException() {
|
||||||
|
data object StorageNotFound : Feature()
|
||||||
|
data object NeedsDecryptedView : Feature()
|
||||||
|
data object SecretNotFound : Feature()
|
||||||
|
data object StorageNotWritable : Feature()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Storage(cause: Throwable? = null) : WallencException(cause = cause) {
|
||||||
|
data object NotAvailable : Storage()
|
||||||
|
data object FileNotFound : Storage()
|
||||||
|
data object IncorrectKey : Storage()
|
||||||
|
data class IoFailed(override val cause: Throwable) : Storage(cause)
|
||||||
|
data object EncInfoMissing : Storage()
|
||||||
|
data object NotEncrypted : Storage()
|
||||||
|
data object NotWritable : Storage()
|
||||||
|
data object NotAFile : Storage()
|
||||||
|
data object NotADirectory : Storage()
|
||||||
|
data object PathIsFile : Storage()
|
||||||
|
data object CannotWriteOverDirectory : Storage()
|
||||||
|
data object DeleteRootForbidden : Storage()
|
||||||
|
data object UnexpectedState : Storage()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ошибки аутентификации (OAuth, токен), без привязки к провайдеру. */
|
||||||
|
sealed class Auth : WallencException() {
|
||||||
|
data object Failed : Auth()
|
||||||
|
data object TokenMissing : Auth()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Сетевые и удалённые операции (HTTP, блокировки, таймауты). */
|
||||||
|
sealed class Network(cause: Throwable? = null) : WallencException(cause = cause) {
|
||||||
|
data class HttpFailed(
|
||||||
|
val operation: String,
|
||||||
|
val statusCode: Int,
|
||||||
|
override val cause: Throwable? = null,
|
||||||
|
) : Network(cause)
|
||||||
|
data class IoFailed(override val cause: Throwable) : Network(cause)
|
||||||
|
data object ResourceLocked : Network()
|
||||||
|
data object OperationFailed : Network()
|
||||||
|
data object OperationTimedOut : Network()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Unknown(override val cause: Throwable?) : WallencException(cause = cause)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.github.nullptroma.wallenc.domain.errors
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
fun Throwable.toWallencException(): WallencException = when (this) {
|
||||||
|
is WallencException -> this
|
||||||
|
is FileNotFoundException -> WallencException.Storage.FileNotFound
|
||||||
|
is IOException -> WallencException.Network.IoFailed(this)
|
||||||
|
else -> WallencException.Unknown(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Throwable.rethrowAsWallencException(): Nothing = throw toWallencException()
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.tasks
|
package com.github.nullptroma.wallenc.domain.tasks
|
||||||
|
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||||
|
|
||||||
interface TaskContext {
|
interface TaskContext {
|
||||||
val taskId: TaskId
|
val taskId: TaskId
|
||||||
|
|
||||||
@@ -8,4 +10,6 @@ interface TaskContext {
|
|||||||
suspend fun reportProgress(progress: TaskProgress) = reportProgress(progress.fraction, progress.label)
|
suspend fun reportProgress(progress: TaskProgress) = reportProgress(progress.fraction, progress.label)
|
||||||
|
|
||||||
fun log(level: TaskLogLevel, message: String)
|
fun log(level: TaskLogLevel, message: String)
|
||||||
|
|
||||||
|
fun fail(error: WallencException): Nothing
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.tasks
|
package com.github.nullptroma.wallenc.domain.tasks
|
||||||
|
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||||
|
|
||||||
sealed class TaskRunState {
|
sealed class TaskRunState {
|
||||||
data object Queued : TaskRunState()
|
data object Queued : TaskRunState()
|
||||||
data class Running(val progress: TaskProgress?) : TaskRunState()
|
data class Running(val progress: TaskProgress?) : TaskRunState()
|
||||||
data object Completed : TaskRunState()
|
data object Completed : TaskRunState()
|
||||||
data object Cancelled : TaskRunState()
|
data object Cancelled : TaskRunState()
|
||||||
data class Failed(val message: String) : TaskRunState()
|
data class Failed(val error: WallencException) : TaskRunState()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.github.nullptroma.wallenc.domain.errors
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertSame
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class WallencExceptionMappingTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun preservesWallencException() {
|
||||||
|
val original = WallencException.Feature.StorageNotFound
|
||||||
|
assertSame(original, original.toWallencException())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mapsFileNotFoundException() {
|
||||||
|
val mapped = FileNotFoundException("missing").toWallencException()
|
||||||
|
assertEquals(WallencException.Storage.FileNotFound, mapped)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mapsIOExceptionToIoFailed() {
|
||||||
|
val cause = IOException("disk")
|
||||||
|
val mapped = cause.toWallencException()
|
||||||
|
assertTrue(mapped is WallencException.Network.IoFailed)
|
||||||
|
assertSame(cause, (mapped as WallencException.Network.IoFailed).cause)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mapsGenericExceptionToUnknown() {
|
||||||
|
val cause = Exception("boom")
|
||||||
|
val mapped = cause.toWallencException()
|
||||||
|
assertTrue(mapped is WallencException.Unknown)
|
||||||
|
assertSame(cause, (mapped as WallencException.Unknown).cause)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.github.nullptroma.wallenc.task.runtime
|
package com.github.nullptroma.wallenc.task.runtime
|
||||||
|
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.toWallencException
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.PipelineState
|
import com.github.nullptroma.wallenc.domain.tasks.PipelineState
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.PipelineTask
|
import com.github.nullptroma.wallenc.domain.tasks.PipelineTask
|
||||||
@@ -188,9 +190,13 @@ class TaskOrchestrator(
|
|||||||
replaceTask(taskId) { it.copy(state = TaskRunState.Completed) }
|
replaceTask(taskId) { it.copy(state = TaskRunState.Completed) }
|
||||||
} catch (_: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
replaceTask(taskId) { it.copy(state = TaskRunState.Cancelled) }
|
replaceTask(taskId) { it.copy(state = TaskRunState.Cancelled) }
|
||||||
|
} catch (e: TaskFailedException) {
|
||||||
|
replaceTask(taskId) {
|
||||||
|
it.copy(state = TaskRunState.Failed(e.error))
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
replaceTask(taskId) {
|
replaceTask(taskId) {
|
||||||
it.copy(state = TaskRunState.Failed(e.message ?: e.toString()))
|
it.copy(state = TaskRunState.Failed(e.toWallencException()))
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
cancelRequested.remove(taskId)
|
cancelRequested.remove(taskId)
|
||||||
@@ -216,7 +222,13 @@ class TaskOrchestrator(
|
|||||||
override fun log(level: TaskLogLevel, message: String) {
|
override fun log(level: TaskLogLevel, message: String) {
|
||||||
appendLog(level, message)
|
appendLog(level, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun fail(error: WallencException): Nothing {
|
||||||
|
throw TaskFailedException(error)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TaskFailedException(val error: WallencException) : RuntimeException()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val MAX_LOG_LINES = 500
|
private const val MAX_LOG_LINES = 500
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.github.nullptroma.wallenc.ui.resources
|
||||||
|
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||||
|
import com.github.nullptroma.wallenc.ui.R
|
||||||
|
import com.github.nullptroma.wallenc.usecases.AddStorageToSyncGroupResult
|
||||||
|
import com.github.nullptroma.wallenc.vault.contract.VaultLinkFailure
|
||||||
|
|
||||||
|
fun WallencException.toUserNotification(): UserNotification.TextRes = when (this) {
|
||||||
|
WallencException.Feature.StorageNotFound ->
|
||||||
|
UserNotification.TextRes(R.string.error_storage_not_found)
|
||||||
|
WallencException.Feature.NeedsDecryptedView ->
|
||||||
|
UserNotification.TextRes(R.string.error_storage_locked_view)
|
||||||
|
WallencException.Feature.SecretNotFound ->
|
||||||
|
UserNotification.TextRes(R.string.error_secret_not_found)
|
||||||
|
WallencException.Feature.StorageNotWritable ->
|
||||||
|
UserNotification.TextRes(R.string.error_storage_not_writable)
|
||||||
|
|
||||||
|
WallencException.Storage.NotAvailable,
|
||||||
|
WallencException.Storage.NotWritable,
|
||||||
|
->
|
||||||
|
UserNotification.TextRes(R.string.error_storage_not_writable)
|
||||||
|
WallencException.Storage.FileNotFound ->
|
||||||
|
UserNotification.TextRes(R.string.error_file_not_found)
|
||||||
|
WallencException.Storage.IncorrectKey ->
|
||||||
|
UserNotification.TextRes(R.string.error_incorrect_password)
|
||||||
|
WallencException.Storage.NotEncrypted ->
|
||||||
|
UserNotification.TextRes(R.string.error_storage_not_encrypted)
|
||||||
|
WallencException.Storage.EncInfoMissing ->
|
||||||
|
UserNotification.TextRes(R.string.error_enc_info_missing)
|
||||||
|
WallencException.Storage.DeleteRootForbidden ->
|
||||||
|
UserNotification.TextRes(R.string.error_delete_root_forbidden)
|
||||||
|
WallencException.Storage.NotAFile ->
|
||||||
|
UserNotification.TextRes(R.string.error_not_a_file)
|
||||||
|
WallencException.Storage.NotADirectory ->
|
||||||
|
UserNotification.TextRes(R.string.error_not_a_directory)
|
||||||
|
WallencException.Storage.PathIsFile ->
|
||||||
|
UserNotification.TextRes(R.string.error_path_is_file)
|
||||||
|
WallencException.Storage.CannotWriteOverDirectory ->
|
||||||
|
UserNotification.TextRes(R.string.error_cannot_write_over_directory)
|
||||||
|
WallencException.Storage.UnexpectedState ->
|
||||||
|
UserNotification.TextRes(R.string.error_unexpected_state)
|
||||||
|
is WallencException.Storage.IoFailed ->
|
||||||
|
UserNotification.TextRes(R.string.error_network)
|
||||||
|
|
||||||
|
WallencException.Auth.Failed,
|
||||||
|
WallencException.Auth.TokenMissing,
|
||||||
|
->
|
||||||
|
UserNotification.TextRes(R.string.vault_link_error_auth)
|
||||||
|
WallencException.Network.ResourceLocked ->
|
||||||
|
UserNotification.TextRes(R.string.error_disk_resource_locked)
|
||||||
|
WallencException.Network.OperationFailed,
|
||||||
|
WallencException.Network.OperationTimedOut,
|
||||||
|
is WallencException.Network.HttpFailed,
|
||||||
|
is WallencException.Network.IoFailed,
|
||||||
|
->
|
||||||
|
UserNotification.TextRes(R.string.error_network)
|
||||||
|
|
||||||
|
is WallencException.Unknown ->
|
||||||
|
UserNotification.TextRes(R.string.error_unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun VaultLinkFailure.toUserNotification(): UserNotification.TextRes = when (this) {
|
||||||
|
VaultLinkFailure.UnsupportedBrand ->
|
||||||
|
UserNotification.TextRes(R.string.vault_link_error_unsupported_brand)
|
||||||
|
VaultLinkFailure.NotRegistered ->
|
||||||
|
UserNotification.TextRes(R.string.vault_link_error_not_registered)
|
||||||
|
VaultLinkFailure.AuthError ->
|
||||||
|
UserNotification.TextRes(R.string.vault_link_error_auth)
|
||||||
|
VaultLinkFailure.Unknown ->
|
||||||
|
UserNotification.TextRes(R.string.vault_link_error_unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AddStorageToSyncGroupResult.toUserNotification(groupId: String): UserNotification.TextRes = when (this) {
|
||||||
|
AddStorageToSyncGroupResult.Added ->
|
||||||
|
UserNotification.TextRes(R.string.sync_msg_storage_added, listOf(groupId))
|
||||||
|
AddStorageToSyncGroupResult.GroupNotFound ->
|
||||||
|
UserNotification.TextRes(R.string.sync_error_group_not_found)
|
||||||
|
AddStorageToSyncGroupResult.AlreadyInGroup ->
|
||||||
|
UserNotification.TextRes(R.string.sync_msg_storage_already_added)
|
||||||
|
AddStorageToSyncGroupResult.EncryptedStorageNotAllowed ->
|
||||||
|
UserNotification.TextRes(R.string.sync_msg_only_plain_storage_allowed)
|
||||||
|
AddStorageToSyncGroupResult.MissingEncryptionSecret ->
|
||||||
|
UserNotification.TextRes(R.string.sync_msg_storage_encryption_key_required)
|
||||||
|
AddStorageToSyncGroupResult.IncompatibleEncryption ->
|
||||||
|
UserNotification.TextRes(R.string.sync_msg_storage_incompatible_encryption)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.github.nullptroma.wallenc.ui.resources
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UserNotification.resolveText(): String = when (this) {
|
||||||
|
is UserNotification.TextRes -> {
|
||||||
|
if (formatArgs.isEmpty()) {
|
||||||
|
stringResource(id)
|
||||||
|
} else {
|
||||||
|
stringResource(id, *formatArgs.toTypedArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is UserNotification.Plain -> message
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ import androidx.compose.ui.window.Dialog
|
|||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.github.nullptroma.wallenc.ui.R
|
import com.github.nullptroma.wallenc.ui.R
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
|
||||||
import com.github.nullptroma.wallenc.vault.contract.CloudBrand
|
import com.github.nullptroma.wallenc.vault.contract.CloudBrand
|
||||||
import com.github.nullptroma.wallenc.vault.contract.VaultLinkOutcome
|
import com.github.nullptroma.wallenc.vault.contract.VaultLinkOutcome
|
||||||
|
|
||||||
@@ -211,9 +212,18 @@ fun RemoteVaultsScreen(
|
|||||||
when (outcome) {
|
when (outcome) {
|
||||||
is VaultLinkOutcome.Success ->
|
is VaultLinkOutcome.Success ->
|
||||||
viewModel.onLinkSucceeded(outcome.registration)
|
viewModel.onLinkSucceeded(outcome.registration)
|
||||||
is VaultLinkOutcome.Failed ->
|
is VaultLinkOutcome.Failed -> {
|
||||||
Toast.makeText(context, outcome.message, Toast.LENGTH_LONG)
|
val notification = outcome.reason.toUserNotification()
|
||||||
.show()
|
val text = if (notification.formatArgs.isEmpty()) {
|
||||||
|
context.getString(notification.id)
|
||||||
|
} else {
|
||||||
|
context.getString(
|
||||||
|
notification.id,
|
||||||
|
*notification.formatArgs.toTypedArray(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Toast.makeText(context, text, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
VaultLinkOutcome.Cancelled -> { }
|
VaultLinkOutcome.Cancelled -> { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.github.nullptroma.wallenc.ui.R
|
import com.github.nullptroma.wallenc.ui.R
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.resolveText
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StorageHomeScreen(
|
fun StorageHomeScreen(
|
||||||
@@ -80,9 +81,9 @@ fun StorageHomeScreen(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
uiState.errorMessage?.let {
|
uiState.errorNotification?.let { notification ->
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = notification.resolveText(),
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.github.nullptroma.wallenc.ui.screens.main.screens.storage
|
package com.github.nullptroma.wallenc.ui.screens.main.screens.storage
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.UserNotification
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class StorageHomeScreenState(
|
data class StorageHomeScreenState(
|
||||||
@@ -13,5 +14,5 @@ data class StorageHomeScreenState(
|
|||||||
val twoFaCount: Int = 0,
|
val twoFaCount: Int = 0,
|
||||||
val textSecretsCount: Int = 0,
|
val textSecretsCount: Int = 0,
|
||||||
val canManageDomainData: Boolean = false,
|
val canManageDomainData: Boolean = false,
|
||||||
val errorMessage: String? = null,
|
val errorNotification: UserNotification.TextRes? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package com.github.nullptroma.wallenc.ui.screens.main.screens.storage
|
|||||||
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||||
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
|
||||||
import com.github.nullptroma.wallenc.usecases.FindStorageUseCase
|
import com.github.nullptroma.wallenc.usecases.FindStorageUseCase
|
||||||
import com.github.nullptroma.wallenc.usecases.ManageTextSecretsUseCase
|
import com.github.nullptroma.wallenc.usecases.ManageTextSecretsUseCase
|
||||||
import com.github.nullptroma.wallenc.usecases.ManageTwoFaTokensUseCase
|
import com.github.nullptroma.wallenc.usecases.ManageTwoFaTokensUseCase
|
||||||
@@ -33,7 +35,7 @@ class StorageHomeViewModel @Inject constructor(
|
|||||||
state.value.copy(
|
state.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
storageUuid = storageUuid.toString(),
|
storageUuid = storageUuid.toString(),
|
||||||
errorMessage = "Storage not found",
|
errorNotification = WallencException.Feature.StorageNotFound.toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -56,8 +58,8 @@ class StorageHomeViewModel @Inject constructor(
|
|||||||
twoFaCount = twoFa.size,
|
twoFaCount = twoFa.size,
|
||||||
textSecretsCount = secrets.size,
|
textSecretsCount = secrets.size,
|
||||||
canManageDomainData = canManageDomainData,
|
canManageDomainData = canManageDomainData,
|
||||||
errorMessage = if (isRawEncrypted) {
|
errorNotification = if (isRawEncrypted) {
|
||||||
"Откройте расшифрованное отображение storage для работы с 2FA и секретами"
|
WallencException.Feature.NeedsDecryptedView.toUserNotification()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.github.nullptroma.wallenc.ui.R
|
import com.github.nullptroma.wallenc.ui.R
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.resolveText
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -58,8 +59,11 @@ fun TextSecretDetailsScreen(
|
|||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
) {
|
) {
|
||||||
uiState.errorMessage?.let {
|
uiState.errorNotification?.let { notification ->
|
||||||
Text(it, color = MaterialTheme.colorScheme.error)
|
Text(
|
||||||
|
text = notification.resolveText(),
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (uiState.isMutating) {
|
if (uiState.isMutating) {
|
||||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.github.nullptroma.wallenc.ui.screens.main.screens.storage.secrets
|
package com.github.nullptroma.wallenc.ui.screens.main.screens.storage.secrets
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.UserNotification
|
||||||
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
|
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -9,5 +10,5 @@ data class TextSecretDetailsScreenState(
|
|||||||
val isAvailable: Boolean = false,
|
val isAvailable: Boolean = false,
|
||||||
val isMutating: Boolean = false,
|
val isMutating: Boolean = false,
|
||||||
val secret: TextSecretRecord? = null,
|
val secret: TextSecretRecord? = null,
|
||||||
val errorMessage: String? = null,
|
val errorNotification: UserNotification.TextRes? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
|||||||
import com.github.nullptroma.wallenc.domain.tasks.TaskId
|
import com.github.nullptroma.wallenc.domain.tasks.TaskId
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
||||||
import com.github.nullptroma.wallenc.ui.R
|
import com.github.nullptroma.wallenc.ui.R
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||||
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
|
||||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
||||||
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.requireStorageUuid
|
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.requireStorageUuid
|
||||||
import com.github.nullptroma.wallenc.usecases.FindStorageUseCase
|
import com.github.nullptroma.wallenc.usecases.FindStorageUseCase
|
||||||
@@ -44,7 +46,7 @@ class TextSecretDetailsViewModel @Inject constructor(
|
|||||||
updateState(
|
updateState(
|
||||||
state.value.copy(
|
state.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
errorMessage = "Storage not found",
|
errorNotification = WallencException.Feature.StorageNotFound.toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -54,7 +56,7 @@ class TextSecretDetailsViewModel @Inject constructor(
|
|||||||
state.value.copy(
|
state.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
isAvailable = false,
|
isAvailable = false,
|
||||||
errorMessage = "Откройте расшифрованное отображение storage для просмотра и редактирования секрета",
|
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -75,7 +77,11 @@ class TextSecretDetailsViewModel @Inject constructor(
|
|||||||
isAvailable = available,
|
isAvailable = available,
|
||||||
isMutating = isMutating,
|
isMutating = isMutating,
|
||||||
secret = secret,
|
secret = secret,
|
||||||
errorMessage = if (secret == null) "Secret not found" else null,
|
errorNotification = if (secret == null) {
|
||||||
|
WallencException.Feature.SecretNotFound.toUserNotification()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}.collect { ui ->
|
}.collect { ui ->
|
||||||
updateState(ui)
|
updateState(ui)
|
||||||
@@ -89,7 +95,7 @@ class TextSecretDetailsViewModel @Inject constructor(
|
|||||||
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
||||||
updateState(
|
updateState(
|
||||||
state.value.copy(
|
state.value.copy(
|
||||||
errorMessage = "Откройте расшифрованное отображение storage для просмотра и редактирования секрета",
|
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretEntryRecord
|
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretEntryRecord
|
||||||
import com.github.nullptroma.wallenc.ui.R
|
import com.github.nullptroma.wallenc.ui.R
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.resolveText
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TextSecretEditScreen(
|
fun TextSecretEditScreen(
|
||||||
@@ -42,7 +43,7 @@ fun TextSecretEditScreen(
|
|||||||
) {
|
) {
|
||||||
val uiState by viewModel.state.collectAsStateWithLifecycle()
|
val uiState by viewModel.state.collectAsStateWithLifecycle()
|
||||||
val currentOnSaved by rememberUpdatedState(onSaved)
|
val currentOnSaved by rememberUpdatedState(onSaved)
|
||||||
val inputEnabled = uiState.isAvailable && !uiState.isMutating && uiState.errorMessage == null
|
val inputEnabled = uiState.isAvailable && !uiState.isMutating && uiState.errorNotification == null
|
||||||
|
|
||||||
var title by remember(uiState.initialSecret) {
|
var title by remember(uiState.initialSecret) {
|
||||||
mutableStateOf(uiState.initialSecret?.title.orEmpty())
|
mutableStateOf(uiState.initialSecret?.title.orEmpty())
|
||||||
@@ -75,8 +76,8 @@ fun TextSecretEditScreen(
|
|||||||
stringResource(R.string.text_secret_edit)
|
stringResource(R.string.text_secret_edit)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
uiState.errorMessage?.let { err ->
|
uiState.errorNotification?.let { notification ->
|
||||||
Text(text = err)
|
Text(text = notification.resolveText())
|
||||||
}
|
}
|
||||||
if (uiState.isMutating) {
|
if (uiState.isMutating) {
|
||||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.github.nullptroma.wallenc.ui.screens.main.screens.storage.secrets
|
package com.github.nullptroma.wallenc.ui.screens.main.screens.storage.secrets
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.UserNotification
|
||||||
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
|
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -9,5 +10,5 @@ data class TextSecretEditScreenState(
|
|||||||
val isAvailable: Boolean = false,
|
val isAvailable: Boolean = false,
|
||||||
val isMutating: Boolean = false,
|
val isMutating: Boolean = false,
|
||||||
val initialSecret: TextSecretRecord? = null,
|
val initialSecret: TextSecretRecord? = null,
|
||||||
val errorMessage: String? = null,
|
val errorNotification: UserNotification.TextRes? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
|||||||
import com.github.nullptroma.wallenc.domain.tasks.TaskId
|
import com.github.nullptroma.wallenc.domain.tasks.TaskId
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
||||||
import com.github.nullptroma.wallenc.ui.R
|
import com.github.nullptroma.wallenc.ui.R
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||||
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
|
||||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
||||||
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.optionalSecretId
|
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.optionalSecretId
|
||||||
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.requireStorageUuid
|
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.requireStorageUuid
|
||||||
@@ -48,7 +50,7 @@ class TextSecretEditViewModel @Inject constructor(
|
|||||||
updateState(
|
updateState(
|
||||||
state.value.copy(
|
state.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
errorMessage = "Storage not found",
|
errorNotification = WallencException.Feature.StorageNotFound.toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -58,7 +60,7 @@ class TextSecretEditViewModel @Inject constructor(
|
|||||||
state.value.copy(
|
state.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
isAvailable = false,
|
isAvailable = false,
|
||||||
errorMessage = "Откройте расшифрованное отображение storage для редактирования секрета",
|
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -78,7 +80,7 @@ class TextSecretEditViewModel @Inject constructor(
|
|||||||
isAvailable = available,
|
isAvailable = available,
|
||||||
isMutating = isMutating,
|
isMutating = isMutating,
|
||||||
initialSecret = currentSecret,
|
initialSecret = currentSecret,
|
||||||
errorMessage = null,
|
errorNotification = null,
|
||||||
)
|
)
|
||||||
}.collect { ui ->
|
}.collect { ui ->
|
||||||
updateState(ui)
|
updateState(ui)
|
||||||
@@ -96,7 +98,7 @@ class TextSecretEditViewModel @Inject constructor(
|
|||||||
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
||||||
updateState(
|
updateState(
|
||||||
state.value.copy(
|
state.value.copy(
|
||||||
errorMessage = "Откройте расшифрованное отображение storage для редактирования секрета",
|
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
|
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
|
||||||
import com.github.nullptroma.wallenc.ui.R
|
import com.github.nullptroma.wallenc.ui.R
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.resolveText
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TextSecretsScreen(
|
fun TextSecretsScreen(
|
||||||
@@ -66,8 +67,11 @@ fun TextSecretsScreen(
|
|||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
uiState.errorMessage?.let {
|
uiState.errorNotification?.let { notification ->
|
||||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
Text(
|
||||||
|
text = notification.resolveText(),
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uiState.items.isEmpty()) {
|
if (uiState.items.isEmpty()) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.github.nullptroma.wallenc.ui.screens.main.screens.storage.secrets
|
package com.github.nullptroma.wallenc.ui.screens.main.screens.storage.secrets
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.UserNotification
|
||||||
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
|
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -8,5 +9,5 @@ data class TextSecretsScreenState(
|
|||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
val isAvailable: Boolean = false,
|
val isAvailable: Boolean = false,
|
||||||
val items: List<TextSecretRecord> = emptyList(),
|
val items: List<TextSecretRecord> = emptyList(),
|
||||||
val errorMessage: String? = null,
|
val errorNotification: UserNotification.TextRes? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package com.github.nullptroma.wallenc.ui.screens.main.screens.storage.secrets
|
|||||||
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||||
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
|
||||||
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.requireStorageUuid
|
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.requireStorageUuid
|
||||||
import com.github.nullptroma.wallenc.usecases.FindStorageUseCase
|
import com.github.nullptroma.wallenc.usecases.FindStorageUseCase
|
||||||
import com.github.nullptroma.wallenc.usecases.ManageTextSecretsUseCase
|
import com.github.nullptroma.wallenc.usecases.ManageTextSecretsUseCase
|
||||||
@@ -31,7 +33,7 @@ class TextSecretsViewModel @Inject constructor(
|
|||||||
updateState(
|
updateState(
|
||||||
state.value.copy(
|
state.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
errorMessage = "Storage not found",
|
errorNotification = WallencException.Feature.StorageNotFound.toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -41,7 +43,7 @@ class TextSecretsViewModel @Inject constructor(
|
|||||||
state.value.copy(
|
state.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
isAvailable = false,
|
isAvailable = false,
|
||||||
errorMessage = "Откройте расшифрованное отображение storage для работы с секретами",
|
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -54,7 +56,7 @@ class TextSecretsViewModel @Inject constructor(
|
|||||||
isLoading = false,
|
isLoading = false,
|
||||||
isAvailable = available,
|
isAvailable = available,
|
||||||
items = items,
|
items = items,
|
||||||
errorMessage = null,
|
errorNotification = null,
|
||||||
)
|
)
|
||||||
}.collect { ui ->
|
}.collect { ui ->
|
||||||
updateState(ui)
|
updateState(ui)
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
|
import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
|
||||||
import com.github.nullptroma.wallenc.ui.R
|
import com.github.nullptroma.wallenc.ui.R
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.resolveText
|
||||||
import com.github.nullptroma.wallenc.ui.elements.QrScannerDialog
|
import com.github.nullptroma.wallenc.ui.elements.QrScannerDialog
|
||||||
import com.github.nullptroma.wallenc.usecases.TwoFaCodeState
|
import com.github.nullptroma.wallenc.usecases.TwoFaCodeState
|
||||||
import com.github.nullptroma.wallenc.usecases.buildTwoFaCodeState
|
import com.github.nullptroma.wallenc.usecases.buildTwoFaCodeState
|
||||||
@@ -133,8 +134,11 @@ fun TwoFaTokensScreen(
|
|||||||
return@Column
|
return@Column
|
||||||
}
|
}
|
||||||
|
|
||||||
uiState.errorMessage?.let {
|
uiState.errorNotification?.let { notification ->
|
||||||
Text(it)
|
Text(
|
||||||
|
text = notification.resolveText(),
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uiState.items.isEmpty()) {
|
if (uiState.items.isEmpty()) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.github.nullptroma.wallenc.ui.screens.main.screens.storage.twofa
|
|||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
|
import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.UserNotification
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class TwoFaTokensScreenState(
|
data class TwoFaTokensScreenState(
|
||||||
@@ -9,5 +10,5 @@ data class TwoFaTokensScreenState(
|
|||||||
val isAvailable: Boolean = false,
|
val isAvailable: Boolean = false,
|
||||||
val isMutating: Boolean = false,
|
val isMutating: Boolean = false,
|
||||||
val items: List<TwoFaTokenRecord> = emptyList(),
|
val items: List<TwoFaTokenRecord> = emptyList(),
|
||||||
val errorMessage: String? = null,
|
val errorNotification: UserNotification.TextRes? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
|
|||||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
||||||
import com.github.nullptroma.wallenc.ui.R
|
import com.github.nullptroma.wallenc.ui.R
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||||
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
|
||||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
||||||
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.requireStorageUuid
|
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.requireStorageUuid
|
||||||
import com.github.nullptroma.wallenc.usecases.FindStorageUseCase
|
import com.github.nullptroma.wallenc.usecases.FindStorageUseCase
|
||||||
@@ -40,7 +42,7 @@ class TwoFaTokensViewModel @Inject constructor(
|
|||||||
updateState(
|
updateState(
|
||||||
state.value.copy(
|
state.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
errorMessage = "Storage not found",
|
errorNotification = WallencException.Feature.StorageNotFound.toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -50,7 +52,7 @@ class TwoFaTokensViewModel @Inject constructor(
|
|||||||
state.value.copy(
|
state.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
isAvailable = false,
|
isAvailable = false,
|
||||||
errorMessage = "Откройте расшифрованное отображение storage для работы с 2FA",
|
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -69,7 +71,7 @@ class TwoFaTokensViewModel @Inject constructor(
|
|||||||
isAvailable = available,
|
isAvailable = available,
|
||||||
isMutating = isMutating,
|
isMutating = isMutating,
|
||||||
items = items,
|
items = items,
|
||||||
errorMessage = null,
|
errorNotification = null,
|
||||||
)
|
)
|
||||||
}.collect { ui ->
|
}.collect { ui ->
|
||||||
updateState(ui)
|
updateState(ui)
|
||||||
@@ -92,7 +94,7 @@ class TwoFaTokensViewModel @Inject constructor(
|
|||||||
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
||||||
updateState(
|
updateState(
|
||||||
state.value.copy(
|
state.value.copy(
|
||||||
errorMessage = "Откройте расшифрованное отображение storage для работы с 2FA",
|
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -140,7 +142,7 @@ class TwoFaTokensViewModel @Inject constructor(
|
|||||||
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
||||||
updateState(
|
updateState(
|
||||||
state.value.copy(
|
state.value.copy(
|
||||||
errorMessage = "Откройте расшифрованное отображение storage для работы с 2FA",
|
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import com.github.nullptroma.wallenc.domain.tasks.PipelineTask
|
|||||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
||||||
import com.github.nullptroma.wallenc.ui.R
|
import com.github.nullptroma.wallenc.ui.R
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -191,7 +192,17 @@ private fun TaskRow(task: PipelineTask, isRunning: Boolean) {
|
|||||||
progressLabel ?: stringResource(R.string.task_state_running)
|
progressLabel ?: stringResource(R.string.task_state_running)
|
||||||
TaskRunState.Completed -> stringResource(R.string.task_state_completed)
|
TaskRunState.Completed -> stringResource(R.string.task_state_completed)
|
||||||
TaskRunState.Cancelled -> stringResource(R.string.task_state_cancelled)
|
TaskRunState.Cancelled -> stringResource(R.string.task_state_cancelled)
|
||||||
is TaskRunState.Failed -> stringResource(R.string.task_state_failed, s.message)
|
is TaskRunState.Failed -> {
|
||||||
|
val notification = s.error.toUserNotification()
|
||||||
|
stringResource(
|
||||||
|
R.string.task_state_failed,
|
||||||
|
if (notification.formatArgs.isEmpty()) {
|
||||||
|
stringResource(notification.id)
|
||||||
|
} else {
|
||||||
|
stringResource(notification.id, *notification.formatArgs.toTypedArray())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Text(stateLabel, style = MaterialTheme.typography.bodySmall)
|
Text(stateLabel, style = MaterialTheme.typography.bodySmall)
|
||||||
if (task.state is TaskRunState.Running) {
|
if (task.state is TaskRunState.Running) {
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ import com.github.nullptroma.wallenc.usecases.StorageFileManagementUseCase
|
|||||||
import com.github.nullptroma.wallenc.ui.R
|
import com.github.nullptroma.wallenc.ui.R
|
||||||
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
||||||
import com.github.nullptroma.wallenc.ui.extensions.toPrintable
|
import com.github.nullptroma.wallenc.ui.extensions.toPrintable
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.toWallencException
|
||||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
||||||
import com.github.nullptroma.wallenc.ui.resources.UserNotification
|
import com.github.nullptroma.wallenc.ui.resources.UserNotification
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
@@ -266,12 +268,7 @@ abstract class AbstractVaultBrowserViewModel(
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to enable encryption")
|
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to enable encryption")
|
||||||
_userNotifications.emit(
|
emitTaskError(e)
|
||||||
UserNotification.TextRes(
|
|
||||||
R.string.msg_failed_enable_encryption,
|
|
||||||
listOf(e.message ?: e.toString()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -296,12 +293,7 @@ abstract class AbstractVaultBrowserViewModel(
|
|||||||
ctx.log(TaskLogLevel.Info, "Storage opened")
|
ctx.log(TaskLogLevel.Info, "Storage opened")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to open encrypted storage")
|
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to open encrypted storage")
|
||||||
_userNotifications.emit(
|
emitTaskError(e)
|
||||||
UserNotification.TextRes(
|
|
||||||
R.string.msg_failed_open_storage,
|
|
||||||
listOf(e.message ?: e.toString()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -325,12 +317,7 @@ abstract class AbstractVaultBrowserViewModel(
|
|||||||
ctx.log(TaskLogLevel.Info, "Storage closed")
|
ctx.log(TaskLogLevel.Info, "Storage closed")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to close encrypted storage")
|
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to close encrypted storage")
|
||||||
_userNotifications.emit(
|
emitTaskError(e)
|
||||||
UserNotification.TextRes(
|
|
||||||
R.string.msg_failed_close_storage,
|
|
||||||
listOf(e.message ?: e.toString()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -359,12 +346,7 @@ abstract class AbstractVaultBrowserViewModel(
|
|||||||
_userNotifications.emit(UserNotification.TextRes(R.string.msg_encryption_disabled))
|
_userNotifications.emit(UserNotification.TextRes(R.string.msg_encryption_disabled))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ctx.log(TaskLogLevel.Error, e.message ?: "Failed")
|
ctx.log(TaskLogLevel.Error, e.message ?: "Failed")
|
||||||
_userNotifications.emit(
|
emitTaskError(e)
|
||||||
UserNotification.TextRes(
|
|
||||||
R.string.msg_failed_disable_encryption,
|
|
||||||
listOf(e.message ?: e.toString()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -464,17 +446,16 @@ abstract class AbstractVaultBrowserViewModel(
|
|||||||
_userNotifications.emit(UserNotification.TextRes(R.string.msg_sync_lock_cleared))
|
_userNotifications.emit(UserNotification.TextRes(R.string.msg_sync_lock_cleared))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ctx.log(TaskLogLevel.Error, e.message ?: "clear sync lock failed")
|
ctx.log(TaskLogLevel.Error, e.message ?: "clear sync lock failed")
|
||||||
_userNotifications.emit(
|
emitTaskError(e)
|
||||||
UserNotification.TextRes(
|
|
||||||
R.string.msg_sync_lock_clear_failed,
|
|
||||||
listOf(e.message ?: e.toString()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun emitTaskError(e: Exception) {
|
||||||
|
_userNotifications.emit(e.toWallencException().toUserNotification())
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
private const val TAG = "VaultBrowser"
|
private const val TAG = "VaultBrowser"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,6 +154,29 @@
|
|||||||
<string name="task_title_save_text_secret">Сохранение текстового секрета</string>
|
<string name="task_title_save_text_secret">Сохранение текстового секрета</string>
|
||||||
<string name="task_title_delete_text_secret">Удаление текстового секрета</string>
|
<string name="task_title_delete_text_secret">Удаление текстового секрета</string>
|
||||||
|
|
||||||
|
<string name="error_storage_not_found">Хранилище не найдено</string>
|
||||||
|
<string name="error_storage_locked_view">Откройте расшифрованное отображение storage для работы с этим разделом</string>
|
||||||
|
<string name="error_secret_not_found">Секрет не найден</string>
|
||||||
|
<string name="error_storage_not_writable">Хранилище недоступно для записи</string>
|
||||||
|
<string name="error_file_not_found">Файл не найден</string>
|
||||||
|
<string name="error_incorrect_password">Неверный пароль</string>
|
||||||
|
<string name="error_storage_not_encrypted">Хранилище не зашифровано</string>
|
||||||
|
<string name="error_enc_info_missing">Отсутствуют метаданные шифрования</string>
|
||||||
|
<string name="error_delete_root_forbidden">Нельзя удалить корень хранилища</string>
|
||||||
|
<string name="error_not_a_file">Ожидался файл</string>
|
||||||
|
<string name="error_not_a_directory">Ожидалась папка</string>
|
||||||
|
<string name="error_path_is_file">Путь указывает на файл, а не на папку</string>
|
||||||
|
<string name="error_cannot_write_over_directory">Нельзя записать поверх папки</string>
|
||||||
|
<string name="error_unexpected_state">Хранилище в неожиданном состоянии</string>
|
||||||
|
<string name="error_network">Ошибка сети или сервера</string>
|
||||||
|
<string name="error_disk_resource_locked">Ресурс временно заблокирован. Повторите позже.</string>
|
||||||
|
<string name="error_unknown">Что-то пошло не так</string>
|
||||||
|
<string name="sync_error_group_not_found">Группа синхронизации не найдена</string>
|
||||||
|
<string name="vault_link_error_auth">Не удалось войти</string>
|
||||||
|
<string name="vault_link_error_not_registered">Вход не готов. Перезапустите приложение.</string>
|
||||||
|
<string name="vault_link_error_unknown">Не удалось войти</string>
|
||||||
|
<string name="vault_link_error_unsupported_brand">Этот провайдер не поддерживается</string>
|
||||||
|
|
||||||
<string name="msg_encryption_enabled">Шифрование включено</string>
|
<string name="msg_encryption_enabled">Шифрование включено</string>
|
||||||
<string name="msg_storage_already_encrypted">Хранилище уже зашифровано</string>
|
<string name="msg_storage_already_encrypted">Хранилище уже зашифровано</string>
|
||||||
<string name="msg_storage_not_empty">Хранилище не пустое</string>
|
<string name="msg_storage_not_empty">Хранилище не пустое</string>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.github.nullptroma.wallenc.ui.resources
|
||||||
|
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||||
|
import com.github.nullptroma.wallenc.ui.R
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class WallencUserNotificationMappingTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mapsFeatureStorageNotFound() {
|
||||||
|
val notification = WallencException.Feature.StorageNotFound.toUserNotification()
|
||||||
|
assertEquals(R.string.error_storage_not_found, notification.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mapsStorageIncorrectKey() {
|
||||||
|
val notification = WallencException.Storage.IncorrectKey.toUserNotification()
|
||||||
|
assertEquals(R.string.error_incorrect_password, notification.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mapsUnknown() {
|
||||||
|
val notification = WallencException.Unknown(Exception("x")).toUserNotification()
|
||||||
|
assertEquals(R.string.error_unknown, notification.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.github.nullptroma.wallenc.usecases
|
package com.github.nullptroma.wallenc.usecases
|
||||||
|
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.toWallencException
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncEngine
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncEngine
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.TaskId
|
import com.github.nullptroma.wallenc.domain.tasks.TaskId
|
||||||
@@ -46,8 +47,10 @@ class RunStorageSyncUseCase(
|
|||||||
ctx.log(TaskLogLevel.Info, "Storage sync finished")
|
ctx.log(TaskLogLevel.Info, "Storage sync finished")
|
||||||
ctx.reportProgress(null, "Storage sync: completed")
|
ctx.reportProgress(null, "Storage sync: completed")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ctx.log(TaskLogLevel.Error, "Storage sync failed: ${e.message}")
|
val err = e.toWallencException()
|
||||||
ctx.reportProgress(null, "Storage sync: failed - ${e.message}")
|
ctx.log(TaskLogLevel.Error, "Storage sync failed: $err")
|
||||||
|
ctx.reportProgress(null, "Storage sync: failed - $err")
|
||||||
|
ctx.fail(err)
|
||||||
} finally {
|
} finally {
|
||||||
running.set(false)
|
running.set(false)
|
||||||
_syncRunning.value = false
|
_syncRunning.value = false
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.github.nullptroma.wallenc.vault.contract
|
||||||
|
|
||||||
|
enum class VaultLinkFailure {
|
||||||
|
UnsupportedBrand,
|
||||||
|
NotRegistered,
|
||||||
|
AuthError,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
@@ -13,6 +13,6 @@ sealed interface VaultLinkOutcome {
|
|||||||
/** Пользователь отменил вход. */
|
/** Пользователь отменил вход. */
|
||||||
data object Cancelled : VaultLinkOutcome
|
data object Cancelled : VaultLinkOutcome
|
||||||
|
|
||||||
/** Ошибка SDK/сети/сервера; [message] годится для показа пользователю. */
|
/** Ошибка SDK/сети/сервера; [reason] маппится в UI через [VaultLinkFailure]. */
|
||||||
data class Failed(val message: String) : VaultLinkOutcome
|
data class Failed(val reason: VaultLinkFailure) : VaultLinkOutcome
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user