feat(auth): add web captcha handling

- replace manual captcha screen with WebView-based VK captcha flow
- handle captcha error 14 by showing the captcha overlay and retrying with success_token
- pass captcha redirect/result state through AppSettings
- remove old captcha ViewModel, navigation, validation, and DI
- add ACRA crash reporting
- add WIP message edit mode UI/state
- update Gradle wrapper, SDK config, and dependencies
This commit is contained in:
2026-05-03 05:49:16 +03:00
parent 97c59a85b6
commit df2c61d8d7
51 changed files with 776 additions and 689 deletions
+3
View File
@@ -79,6 +79,9 @@ android {
}
dependencies {
implementation(libs.acra.email)
implementation(libs.acra.dialog)
implementation(projects.feature.auth)
implementation(projects.feature.chatmaterials)
@@ -8,6 +8,10 @@ import com.skydoves.compose.stability.runtime.ComposeStabilityAnalyzer
import dev.meloda.fast.auth.BuildConfig
import dev.meloda.fast.common.di.applicationModule
import dev.meloda.fast.datastore.AppSettings
import org.acra.config.dialog
import org.acra.config.mailSender
import org.acra.data.StringFormat
import org.acra.ktx.initAcra
import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
@@ -20,12 +24,14 @@ class AppGlobal : Application(), ImageLoaderFactory {
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
AppSettings.init(preferences)
ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)
initKoin()
ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)
initAcra()
}
override fun newImageLoader(): ImageLoader = get()
private fun initKoin() {
startKoin {
androidLogger()
@@ -34,5 +40,21 @@ class AppGlobal : Application(), ImageLoaderFactory {
}
}
override fun newImageLoader(): ImageLoader = get()
private fun initAcra() {
initAcra {
buildConfigClass = BuildConfig::class.java
reportFormat = StringFormat.JSON
mailSender {
mailTo = "lischenkodev@gmail.com"
reportAsFile = true
reportFileName = "Crash.txt"
}
dialog {
text = "App crashed"
enabled = true
}
}
}
}
@@ -7,9 +7,7 @@ import androidx.preference.PreferenceManager
import coil.ImageLoader
import coil.annotation.ExperimentalCoilApi
import dev.meloda.fast.MainViewModelImpl
import dev.meloda.fast.auth.captcha.di.captchaModule
import dev.meloda.fast.auth.login.di.loginModule
import dev.meloda.fast.auth.validation.di.validationModule
import dev.meloda.fast.auth.authModule
import dev.meloda.fast.chatmaterials.di.chatMaterialsModule
import dev.meloda.fast.common.LongPollController
import dev.meloda.fast.common.LongPollControllerImpl
@@ -38,9 +36,7 @@ import org.koin.dsl.module
val applicationModule = module {
includes(domainModule)
includes(
loginModule,
validationModule,
captchaModule,
authModule,
convosModule,
settingsModule,
messagesHistoryModule,
@@ -8,9 +8,9 @@ import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.BottomNavigationItem
import dev.meloda.fast.presentation.MainScreen
import dev.meloda.fast.profile.navigation.Profile
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.util.ImmutableList
import kotlinx.serialization.Serializable
import dev.meloda.fast.ui.R
@Serializable
object MainGraph
@@ -41,6 +41,7 @@ import com.google.accompanist.permissions.rememberPermissionState
import dev.meloda.fast.MainViewModel
import dev.meloda.fast.MainViewModelImpl
import dev.meloda.fast.auth.authNavGraph
import dev.meloda.fast.auth.captcha.presentation.CaptchaScreen
import dev.meloda.fast.auth.navigateToAuth
import dev.meloda.fast.chatmaterials.navigation.chatMaterialsScreen
import dev.meloda.fast.chatmaterials.navigation.navigateToChatMaterials
@@ -48,6 +49,8 @@ import dev.meloda.fast.common.LongPollController
import dev.meloda.fast.common.model.LongPollState
import dev.meloda.fast.convos.navigation.createChatScreen
import dev.meloda.fast.convos.navigation.navigateToCreateChat
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.CaptchaTokenResult
import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.languagepicker.navigation.languagePickerScreen
import dev.meloda.fast.languagepicker.navigation.navigateToLanguagePicker
@@ -310,6 +313,9 @@ fun RootScreen(
mutableStateOf<Pair<List<String>, Int?>?>(null)
}
val captchaRedirectUri by AppSettings.getCaptchaRedirectUriFlow()
.collectAsStateWithLifecycle()
Box(modifier = Modifier.fillMaxSize()) {
NavHost(
navController = navController,
@@ -334,7 +340,7 @@ fun RootScreen(
photoViewerInfo = listOf(url) to null
},
onMessageClicked = navController::navigateToMessagesHistory,
onNavigateToCreateChat = navController::navigateToCreateChat
onNavigateToCreateChat = navController::navigateToCreateChat,
)
messagesHistoryScreen(
@@ -381,6 +387,18 @@ fun RootScreen(
},
onDismiss = { photoViewerInfo = null }
)
CaptchaScreen(
captchaRedirectUri = captchaRedirectUri,
onBack = {
AppSettings.setCaptchaResult(CaptchaTokenResult.Cancelled)
},
onResult = { result ->
AppSettings.setCaptchaResult(
CaptchaTokenResult.Success(result)
)
},
)
}
}
}
@@ -32,10 +32,10 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import org.koin.android.ext.android.inject
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.time.Duration.Companion.seconds
class LongPollingService : Service() {
@@ -204,7 +204,7 @@ class LongPollingService : Service() {
}
}
private suspend fun getServerInfo(): VkLongPollData? = suspendCoroutine {
private suspend fun getServerInfo(): VkLongPollData? = suspendCancellableCoroutine {
longPollUseCase.getLongPollServer(
needPts = true,
version = VkConstants.LP_VERSION
@@ -224,7 +224,7 @@ class LongPollingService : Service() {
private suspend fun getUpdatesResponse(
server: VkLongPollData
): LongPollUpdates? = suspendCoroutine {
): LongPollUpdates? = suspendCancellableCoroutine {
longPollUseCase.getLongPollUpdates(
serverUrl = "https://${server.server}",
key = server.key,