mirror of
https://github.com/twofas/2fas-android.git
synced 2025-01-05 14:05:30 +01:00
Migrate to Compose Material3
wip
This commit is contained in:
parent
59374b5309
commit
3488c3bc3d
@ -1,26 +0,0 @@
|
||||
@Suppress("DSL_SCOPE_VIOLATION")
|
||||
plugins {
|
||||
alias(libs.plugins.twofasAndroidLibrary)
|
||||
alias(libs.plugins.twofasCompose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.twofasapp.about"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":base"))
|
||||
implementation(project(":di"))
|
||||
implementation(project(":resources"))
|
||||
implementation(project(":extensions"))
|
||||
implementation(project(":design"))
|
||||
implementation(project(":environment"))
|
||||
implementation(project(":featuretoggle"))
|
||||
implementation(project(":prefs"))
|
||||
|
||||
implementation(libs.bundles.appCompat)
|
||||
implementation(libs.bundles.compose)
|
||||
implementation(libs.bundles.playReview)
|
||||
implementation(libs.webkit)
|
||||
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".ui.AboutActivity"
|
||||
android:label="About" />
|
||||
</application>
|
||||
</manifest>
|
@ -1,204 +0,0 @@
|
||||
package com.twofasapp.about.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment.Companion.Bottom
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.webkit.WebSettingsCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import com.google.android.play.core.review.ReviewManagerFactory
|
||||
import com.twofasapp.resources.R
|
||||
import com.twofasapp.design.compose.HeaderEntry
|
||||
import com.twofasapp.design.compose.SimpleEntry
|
||||
import com.twofasapp.design.compose.Toolbar
|
||||
import com.twofasapp.design.theme.AppThemeLegacy
|
||||
import com.twofasapp.design.theme.divider
|
||||
import com.twofasapp.design.theme.isNight
|
||||
import com.twofasapp.design.theme.textSecondary
|
||||
import com.twofasapp.extensions.openBrowserApp
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class AboutActivity : ComponentActivity() {
|
||||
|
||||
private val viewModel: AboutViewModel by viewModel()
|
||||
|
||||
companion object Screens {
|
||||
private const val About = "about"
|
||||
private const val Licenses = "licenses"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
AppThemeLegacy {
|
||||
Surface {
|
||||
val navController = rememberNavController()
|
||||
|
||||
NavHost(navController = navController, startDestination = About) {
|
||||
composable(About) { About(navController) }
|
||||
composable(Licenses) { Licenses() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun About(navController: NavHostController) {
|
||||
val uiState = viewModel.uiState.collectAsState()
|
||||
val context = LocalContext.current
|
||||
|
||||
Scaffold(
|
||||
topBar = { Toolbar(title = stringResource(id = R.string.settings__about)) { onBackPressed() } }
|
||||
) { padding ->
|
||||
Column(modifier = Modifier.padding(padding)) {
|
||||
|
||||
LazyColumn(modifier = Modifier.weight(1f)) {
|
||||
item { HeaderEntry(text = stringResource(id = R.string.settings__general)) }
|
||||
item {
|
||||
SimpleEntry(
|
||||
title = stringResource(id = R.string.settings__write_a_review),
|
||||
icon = painterResource(id = R.drawable.ic_about_write_review),
|
||||
click = {
|
||||
val manager = ReviewManagerFactory.create(this@AboutActivity)
|
||||
val request = manager.requestReviewFlow()
|
||||
request.addOnCompleteListener { task ->
|
||||
if (task.isSuccessful) {
|
||||
val flow = manager.launchReviewFlow(
|
||||
this@AboutActivity,
|
||||
task.result
|
||||
)
|
||||
flow.addOnCompleteListener {
|
||||
viewModel.reviewDone()
|
||||
}
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SimpleEntry(
|
||||
title = stringResource(id = R.string.settings__privacy_policy),
|
||||
icon = painterResource(id = R.drawable.ic_about_privacy_policy),
|
||||
click = { openBrowserApp(url = "https://2fas.com/privacy-policy/") }
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SimpleEntry(
|
||||
title = stringResource(id = R.string.settings__terms_of_service),
|
||||
icon = painterResource(id = R.drawable.ic_about_terms),
|
||||
click = { openBrowserApp(url = "https://2fas.com/terms-of-service/") }
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SimpleEntry(
|
||||
title = stringResource(id = R.string.about_licenses),
|
||||
icon = painterResource(id = R.drawable.ic_about_licenses),
|
||||
click = { navController.navigate(Licenses) }
|
||||
)
|
||||
}
|
||||
|
||||
item { Divider(color = MaterialTheme.colors.divider) }
|
||||
|
||||
item { HeaderEntry(text = stringResource(id = R.string.settings__share_app)) }
|
||||
item {
|
||||
SimpleEntry(
|
||||
title = stringResource(id = R.string.settings__tell_a_friend),
|
||||
icon = painterResource(id = R.drawable.ic_about_share),
|
||||
click = {
|
||||
ShareCompat.IntentBuilder(context)
|
||||
.setType("text/plain")
|
||||
.setChooserTitle("Share 2FAS")
|
||||
.setText(getString(R.string.settings__recommendation))
|
||||
.startChooser()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "App version ${uiState.value.versionName}",
|
||||
color = MaterialTheme.colors.textSecondary,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Bottom)
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.logo_2fas),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(36.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Licenses() {
|
||||
val isNight = isNight()
|
||||
|
||||
Scaffold(
|
||||
topBar = { Toolbar(title = stringResource(id = R.string.about_licenses)) { onBackPressed() } }
|
||||
) { padding ->
|
||||
AndroidView(factory = {
|
||||
WebView(this).apply {
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK) && isNight) {
|
||||
WebSettingsCompat.setForceDark(settings, WebSettingsCompat.FORCE_DARK_ON);
|
||||
}
|
||||
|
||||
webViewClient = WebViewClient()
|
||||
try {
|
||||
loadUrl("file:///android_asset/open_source_licenses.html")
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"There is no WebView installed. Can not display licenses.",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package com.twofasapp.about.ui
|
||||
|
||||
internal data class AboutUiState(
|
||||
val versionName: String = "",
|
||||
)
|
@ -1,41 +0,0 @@
|
||||
package com.twofasapp.about.ui
|
||||
|
||||
import com.twofasapp.base.BaseViewModel
|
||||
import com.twofasapp.base.dispatcher.Dispatchers
|
||||
import com.twofasapp.environment.AppConfig
|
||||
import com.twofasapp.environment.BuildVariant
|
||||
import com.twofasapp.prefs.usecase.RateAppStatusPreference
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.time.Instant
|
||||
|
||||
internal class AboutViewModel(
|
||||
private val dispatchers: Dispatchers,
|
||||
private val appConfig: AppConfig,
|
||||
private val rateAppStatusPreference: RateAppStatusPreference
|
||||
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(AboutUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
_uiState.update {
|
||||
it.copy(versionName = generateVersionName())
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateVersionName(): String =
|
||||
if (appConfig.buildVariant != BuildVariant.Release) {
|
||||
"${appConfig.versionName} (${appConfig.buildVariant.name})"
|
||||
} else {
|
||||
appConfig.versionName
|
||||
}
|
||||
|
||||
fun reviewDone() {
|
||||
rateAppStatusPreference.put(
|
||||
rateAppStatusPreference.get().copy(counterStarted = Instant.now(), counterReached = Instant.now())
|
||||
)
|
||||
}
|
||||
}
|
@ -15,10 +15,10 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(project(":base"))
|
||||
implementation(project(":di"))
|
||||
implementation(project(":core:di"))
|
||||
implementation(project(":extensions"))
|
||||
implementation(project(":prefs"))
|
||||
implementation(project(":environment"))
|
||||
|
||||
implementation(project(":serialization"))
|
||||
implementation(project(":time"))
|
||||
implementation(project(":spanner"))
|
||||
@ -29,17 +29,13 @@ dependencies {
|
||||
implementation(project(":resources"))
|
||||
implementation(project(":design"))
|
||||
implementation(project(":permissions"))
|
||||
implementation(project(":network"))
|
||||
implementation(project(":push"))
|
||||
implementation(project(":persistence"))
|
||||
implementation(project(":qrscanner"))
|
||||
implementation(project(":about"))
|
||||
implementation(project(":settings"))
|
||||
implementation(project(":widgets"))
|
||||
implementation(project(":services"))
|
||||
implementation(project(":services:domain"))
|
||||
implementation(project(":widgets:domain"))
|
||||
implementation(project(":notifications"))
|
||||
implementation(project(":navigation"))
|
||||
implementation(project(":backup"))
|
||||
implementation(project(":core"))
|
||||
@ -51,15 +47,22 @@ dependencies {
|
||||
implementation(project(":start"))
|
||||
implementation(project(":start:domain"))
|
||||
implementation(project(":time:domain"))
|
||||
implementation(project(":externalimport"))
|
||||
implementation(project(":browserextension:domain"))
|
||||
|
||||
implementation(project(":core:common"))
|
||||
implementation(project(":core:designsystem"))
|
||||
implementation(project(":core:storage"))
|
||||
implementation(project(":core:network"))
|
||||
implementation(project(":data:notifications"))
|
||||
implementation(project(":data:session"))
|
||||
implementation(project(":data:services"))
|
||||
implementation(project(":data:browserext"))
|
||||
implementation(project(":feature:startup"))
|
||||
implementation(project(":feature:home"))
|
||||
implementation(project(":feature:trash"))
|
||||
implementation(project(":feature:about"))
|
||||
implementation(project(":feature:externalimport"))
|
||||
implementation(project(":feature:browserext"))
|
||||
implementation(project(":feature:appsettings"))
|
||||
|
||||
implementation(libs.bundles.appCompat)
|
||||
implementation(libs.bundles.rxJava)
|
||||
@ -71,6 +74,7 @@ dependencies {
|
||||
implementation(libs.bundles.room)
|
||||
implementation(libs.bundles.compose)
|
||||
implementation(libs.bundles.viewModel)
|
||||
implementation(libs.bundles.accompanist)
|
||||
implementation(libs.bundles.playReview)
|
||||
implementation(libs.bundles.playUpdate)
|
||||
implementation(libs.timber)
|
||||
|
@ -92,10 +92,10 @@
|
||||
<!-- android:name=".ui.main.MainActivity"-->
|
||||
|
||||
<activity
|
||||
android:name=".start.ui.start.StartActivity"
|
||||
android:name=".ui.main.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:noHistory="true"
|
||||
android:noHistory="false"
|
||||
android:screenOrientation="locked"
|
||||
android:theme="@style/Theme.App">
|
||||
<intent-filter>
|
||||
@ -164,26 +164,10 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".start.ui.onboarding.OnboardingActivity"
|
||||
android:label="Onboarding" />
|
||||
|
||||
<activity
|
||||
android:name=".security.ui.security.SecurityActivity"
|
||||
android:label="Security" />
|
||||
|
||||
<activity
|
||||
android:name=".features.trash.TrashActivity"
|
||||
android:label="Trash" />
|
||||
|
||||
<activity
|
||||
android:name=".features.trash.delete.DisposeServiceActivity"
|
||||
android:label="Delete From Trash" />
|
||||
|
||||
<activity
|
||||
android:name=".notifications.ui.NotificationsActivity"
|
||||
android:label="Notifications" />
|
||||
|
||||
<activity
|
||||
android:name=".widgets.configure.WidgetSettingsActivity"
|
||||
android:exported="true"
|
||||
|
@ -14,17 +14,11 @@ import com.twofasapp.features.backup.import.ImportBackupActivity
|
||||
import com.twofasapp.features.backup.import.ImportBackupContract
|
||||
import com.twofasapp.features.backup.import.ImportBackupPresenter
|
||||
import com.twofasapp.features.main.DrawerPresenter
|
||||
import com.twofasapp.features.main.MainServicesActivity
|
||||
import com.twofasapp.features.main.MainContract
|
||||
import com.twofasapp.features.main.MainPresenter
|
||||
import com.twofasapp.features.main.MainServicesActivity
|
||||
import com.twofasapp.features.main.ToolbarPresenter
|
||||
import com.twofasapp.features.navigator.ActivityScopedNavigator
|
||||
import com.twofasapp.features.trash.TrashActivity
|
||||
import com.twofasapp.features.trash.TrashContract
|
||||
import com.twofasapp.features.trash.TrashPresenter
|
||||
import com.twofasapp.features.trash.delete.DisposeServiceActivity
|
||||
import com.twofasapp.features.trash.delete.DisposeServiceContract
|
||||
import com.twofasapp.features.trash.delete.DisposeServicePresenter
|
||||
import com.twofasapp.prefs.ScopedNavigator
|
||||
import com.twofasapp.widgets.configure.WidgetSettingsActivity
|
||||
import com.twofasapp.widgets.configure.WidgetSettingsContract
|
||||
@ -85,9 +79,6 @@ val activityScopeModule = module {
|
||||
activityScope<BackupActivity> {
|
||||
scopedOf(::BackupPresenter) { bind<BackupContract.Presenter>() }
|
||||
}
|
||||
activityScope<DisposeServiceActivity> {
|
||||
scopedOf(::DisposeServicePresenter) { bind<DisposeServiceContract.Presenter>() }
|
||||
}
|
||||
activityScope<ExportBackupActivity> {
|
||||
scopedOf(::ExportBackupPresenter) { bind<ExportBackupContract.Presenter>() }
|
||||
}
|
||||
@ -97,7 +88,4 @@ val activityScopeModule = module {
|
||||
activityScope<WidgetSettingsActivity> {
|
||||
scopedOf(::WidgetSettingsPresenter) { bind<WidgetSettingsContract.Presenter>() }
|
||||
}
|
||||
activityScope<TrashActivity> {
|
||||
scopedOf(::TrashPresenter) { bind<TrashContract.Presenter>() }
|
||||
}
|
||||
}
|
@ -7,7 +7,6 @@ import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.multidex.MultiDex
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import com.twofasapp.about.AboutModule
|
||||
import com.twofasapp.backup.BackupModule
|
||||
import com.twofasapp.backup.domain.SyncBackupTrigger
|
||||
import com.twofasapp.backup.domain.SyncBackupWorkDispatcher
|
||||
@ -17,11 +16,8 @@ import com.twofasapp.browserextension.BrowserExtensionModule
|
||||
import com.twofasapp.core.log.FileLogger
|
||||
import com.twofasapp.developer.DeveloperModule
|
||||
import com.twofasapp.di.Modules
|
||||
import com.twofasapp.externalimport.ExternalImportModule
|
||||
import com.twofasapp.featuretoggle.FeatureToggleModule
|
||||
import com.twofasapp.featuretoggle.domain.FetchRemoteConfigCase
|
||||
import com.twofasapp.network.NetworkModule
|
||||
import com.twofasapp.notifications.NotificationsModule
|
||||
import com.twofasapp.parsers.ParsersModule
|
||||
import com.twofasapp.permissions.PermissionsModule
|
||||
import com.twofasapp.persistence.PersistenceModule
|
||||
@ -35,7 +31,6 @@ import com.twofasapp.services.ServicesModule
|
||||
import com.twofasapp.services.backup.remoteBackupModule
|
||||
import com.twofasapp.services.backupcipher.backupCipherModule
|
||||
import com.twofasapp.services.googleauth.googleAuthModule
|
||||
import com.twofasapp.settings.SettingsModule
|
||||
import com.twofasapp.start.StartModule
|
||||
import com.twofasapp.time.TimeModule
|
||||
import com.twofasapp.usecases.services.PinOptionsUseCase
|
||||
@ -80,20 +75,15 @@ class App : MultiDexApplication() {
|
||||
PermissionsModule(),
|
||||
PreferencesPlainModule(),
|
||||
PreferencesEncryptedModule(),
|
||||
NetworkModule(),
|
||||
BrowserExtensionModule(),
|
||||
PushModule(),
|
||||
PersistenceModule(),
|
||||
QrScannerModule(),
|
||||
AboutModule(),
|
||||
SettingsModule(),
|
||||
ServicesModule(),
|
||||
NotificationsModule(),
|
||||
BackupModule(),
|
||||
FeatureToggleModule(),
|
||||
DeveloperModule(),
|
||||
SecurityModule(),
|
||||
ExternalImportModule(),
|
||||
)
|
||||
.map { it.provide() }
|
||||
.plus(Modules.provide())
|
||||
|
@ -1,24 +0,0 @@
|
||||
package com.twofasapp
|
||||
|
||||
import com.twofasapp.environment.AppConfig
|
||||
import com.twofasapp.environment.BuildVariant
|
||||
|
||||
class AppConfigImpl : AppConfig {
|
||||
|
||||
override val id: String = BuildConfig.APPLICATION_ID
|
||||
|
||||
override val isDebug: Boolean = BuildConfig.DEBUG
|
||||
|
||||
override val versionName: String = BuildConfig.VERSION_NAME
|
||||
|
||||
override val versionCode: Int = BuildConfig.VERSION_CODE
|
||||
|
||||
override val buildVariant: BuildVariant = when (BuildConfig.BUILD_TYPE) {
|
||||
"debug" -> BuildVariant.Debug
|
||||
"releaseLocal" -> BuildVariant.ReleaseLocal
|
||||
"release" -> BuildVariant.Release
|
||||
else -> throw RuntimeException("Unknown build variant!")
|
||||
}
|
||||
|
||||
override val deviceName: String = android.os.Build.MANUFACTURER + " " + android.os.Build.MODEL
|
||||
}
|
@ -7,8 +7,6 @@ import com.twofasapp.base.dispatcher.AppDispatchers
|
||||
import com.twofasapp.base.dispatcher.Dispatchers
|
||||
import com.twofasapp.core.cipher.CipherService
|
||||
import com.twofasapp.core.cipher.CipherServiceImpl
|
||||
import com.twofasapp.environment.AppConfig
|
||||
import com.twofasapp.externalimport.domain.GoogleAuthenticatorImporter
|
||||
import com.twofasapp.permissions.CameraPermissionRequest
|
||||
import com.twofasapp.permissions.CameraPermissionRequestFlow
|
||||
import com.twofasapp.services.analytics.AnalyticsServiceFirebase
|
||||
@ -31,8 +29,6 @@ val applicationModule = module {
|
||||
|
||||
single<com.twofasapp.core.analytics.AnalyticsService> { AnalyticsServiceFirebase().apply { init(androidContext()) } }
|
||||
|
||||
single<AppConfig> { AppConfigImpl() }
|
||||
|
||||
single<CipherService> { CipherServiceImpl() }
|
||||
|
||||
single<com.twofasapp.backup.data.FilesProvider> { com.twofasapp.backup.data.FilesProviderImpl(androidContext()) }
|
||||
|
@ -1,7 +1,12 @@
|
||||
package com.twofasapp
|
||||
|
||||
import com.twofasapp.di.KoinModule
|
||||
import com.twofasapp.navigation.*
|
||||
import com.twofasapp.navigation.SecurityRouter
|
||||
import com.twofasapp.navigation.SecurityRouterImpl
|
||||
import com.twofasapp.navigation.ServiceRouter
|
||||
import com.twofasapp.navigation.ServiceRouterImpl
|
||||
import com.twofasapp.navigation.StartRouter
|
||||
import com.twofasapp.navigation.StartRouterImpl
|
||||
import org.koin.core.module.dsl.bind
|
||||
import org.koin.core.module.dsl.factoryOf
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
@ -11,9 +16,7 @@ class NavigationModule : KoinModule {
|
||||
|
||||
override fun provide() = module {
|
||||
factoryOf(::StartRouterImpl) { bind<StartRouter>() }
|
||||
singleOf(::SettingsRouterImpl) { bind<SettingsRouter>() }
|
||||
singleOf(::ServiceRouterImpl) { bind<ServiceRouter>() }
|
||||
singleOf(::SecurityRouterImpl) { bind<SecurityRouter>() }
|
||||
singleOf(::ExternalImportRouterImpl) { bind<ExternalImportRouter>() }
|
||||
}
|
||||
}
|
@ -32,7 +32,6 @@ import com.twofasapp.usecases.services.EditStateObserver
|
||||
import com.twofasapp.usecases.services.GetService
|
||||
import com.twofasapp.usecases.services.GetServices
|
||||
import com.twofasapp.usecases.services.GetServicesIncludingTrashed
|
||||
import com.twofasapp.usecases.services.GetTrashedServices
|
||||
import com.twofasapp.usecases.services.PinOptionsUseCase
|
||||
import com.twofasapp.usecases.services.RestoreService
|
||||
import com.twofasapp.usecases.services.SearchStateObserver
|
||||
@ -74,7 +73,6 @@ val useCaseModule = module {
|
||||
single<GetServicesUseCase> { GetServices(get()) }
|
||||
single { GetServices(get()) }
|
||||
single { GetServicesIncludingTrashed(get()) }
|
||||
single { GetTrashedServices(get()) }
|
||||
single { ServicesObserver(get()) }
|
||||
single { GetService(get()) }
|
||||
single { TrashService(get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||
|
@ -2,12 +2,27 @@ package com.twofasapp.di
|
||||
|
||||
import com.twofasapp.analytics.AnalyticsFirebase
|
||||
import com.twofasapp.common.analytics.Analytics
|
||||
import com.twofasapp.common.environment.AppBuild
|
||||
import com.twofasapp.common.time.TimeProvider
|
||||
import com.twofasapp.environment.AppBuildImpl
|
||||
import com.twofasapp.time.TimeProviderImpl
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.koin.core.module.dsl.bind
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
class AppModule : KoinModule {
|
||||
override fun provide() = module {
|
||||
single {
|
||||
Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
explicitNulls = false
|
||||
coerceInputValues = true
|
||||
}
|
||||
}
|
||||
singleOf(::AnalyticsFirebase) { bind<Analytics>() }
|
||||
singleOf(::AppBuildImpl) { bind<AppBuild>() }
|
||||
singleOf(::TimeProviderImpl) { bind<TimeProvider>() }
|
||||
}
|
||||
}
|
@ -1,27 +1,43 @@
|
||||
package com.twofasapp.di
|
||||
|
||||
import com.twofasapp.common.di.CommonModule
|
||||
import com.twofasapp.data.browserext.di.DataBrowserExtModule
|
||||
import com.twofasapp.data.notifications.di.DataNotificationsModule
|
||||
import com.twofasapp.data.services.di.DataServicesModule
|
||||
import com.twofasapp.data.session.di.DataSessionModule
|
||||
import com.twofasapp.feature.about.di.AboutModule
|
||||
import com.twofasapp.feature.appsettings.di.AppSettingsModule
|
||||
import com.twofasapp.feature.externalimport.di.ExternalImportModule
|
||||
import com.twofasapp.feature.home.di.HomeModule
|
||||
import com.twofasapp.feature.startup.di.StartupModule
|
||||
import com.twofasapp.storage.di.PreferencesModule
|
||||
import com.twofasapp.feature.trash.di.TrashModule
|
||||
import com.twofasapp.network.di.NetworkModule
|
||||
import com.twofasapp.storage.di.StorageModule
|
||||
import org.koin.core.module.Module
|
||||
|
||||
object Modules {
|
||||
private val app = listOf(
|
||||
AppModule(),
|
||||
CommonModule(),
|
||||
PreferencesModule(),
|
||||
NetworkModule(),
|
||||
StorageModule(),
|
||||
)
|
||||
|
||||
private val data = listOf(
|
||||
DataNotificationsModule(),
|
||||
DataSessionModule(),
|
||||
DataServicesModule(),
|
||||
DataBrowserExtModule(),
|
||||
)
|
||||
|
||||
private val feature = listOf(
|
||||
MainModule(),
|
||||
StartupModule(),
|
||||
HomeModule(),
|
||||
ExternalImportModule(),
|
||||
AppSettingsModule(),
|
||||
TrashModule(),
|
||||
AboutModule(),
|
||||
)
|
||||
|
||||
fun provide(): List<Module> =
|
||||
|
27
app/src/main/java/com/twofasapp/environment/AppBuildImpl.kt
Normal file
27
app/src/main/java/com/twofasapp/environment/AppBuildImpl.kt
Normal file
@ -0,0 +1,27 @@
|
||||
package com.twofasapp.environment
|
||||
|
||||
import com.twofasapp.BuildConfig
|
||||
import com.twofasapp.common.environment.AppBuild
|
||||
import com.twofasapp.common.environment.BuildVariant
|
||||
import com.twofasapp.common.environment.BuildVariant.Debug
|
||||
import com.twofasapp.common.environment.BuildVariant.Release
|
||||
import com.twofasapp.common.environment.BuildVariant.ReleaseLocal
|
||||
|
||||
class AppBuildImpl : AppBuild {
|
||||
override val id: String = BuildConfig.APPLICATION_ID
|
||||
|
||||
override val isDebuggable: Boolean = BuildConfig.DEBUG
|
||||
|
||||
override val versionName: String = BuildConfig.VERSION_NAME
|
||||
|
||||
override val versionCode: Int = BuildConfig.VERSION_CODE
|
||||
|
||||
override val buildVariant: BuildVariant = when (BuildConfig.BUILD_TYPE) {
|
||||
"debug" -> Debug
|
||||
"releaseLocal" -> ReleaseLocal
|
||||
"release" -> Release
|
||||
else -> throw RuntimeException("Unknown build variant!")
|
||||
}
|
||||
|
||||
override val deviceName: String = android.os.Build.MANUFACTURER + " " + android.os.Build.MODEL
|
||||
}
|
@ -3,19 +3,19 @@ package com.twofasapp.features.addserviceqr
|
||||
import android.net.Uri
|
||||
import com.twofasapp.backup.domain.SyncBackupTrigger
|
||||
import com.twofasapp.backup.domain.SyncBackupWorkDispatcher
|
||||
import com.twofasapp.environment.AppConfig
|
||||
import com.twofasapp.common.environment.AppBuild
|
||||
import com.twofasapp.extensions.removeWhiteCharacters
|
||||
import com.twofasapp.externalimport.domain.ExternalImport
|
||||
import com.twofasapp.externalimport.domain.GoogleAuthenticatorImporter
|
||||
import com.twofasapp.feature.externalimport.domain.ExternalImport
|
||||
import com.twofasapp.feature.externalimport.domain.GoogleAuthenticatorImporter
|
||||
import com.twofasapp.prefs.ScopedNavigator
|
||||
import com.twofasapp.prefs.model.OtpAuthLink
|
||||
import com.twofasapp.prefs.model.ServiceDto
|
||||
import com.twofasapp.prefs.usecase.LastScannedQrPreference
|
||||
import com.twofasapp.qrscanner.domain.ReadQrFromImageRx
|
||||
import com.twofasapp.qrscanner.domain.ScanQr
|
||||
import com.twofasapp.services.domain.ConvertOtpLinkToService
|
||||
import com.twofasapp.usecases.services.AddService
|
||||
import com.twofasapp.usecases.services.CheckServiceExists
|
||||
import com.twofasapp.services.domain.ConvertOtpLinkToService
|
||||
import com.twofasapp.usecases.services.GetService
|
||||
import com.twofasapp.usecases.totp.ParseOtpAuthLink
|
||||
import io.reactivex.rxkotlin.toFlowable
|
||||
@ -33,7 +33,7 @@ class AddServiceQrPresenter(
|
||||
private val getService: GetService,
|
||||
private val readQrFromImageRx: ReadQrFromImageRx,
|
||||
private val googleAuthenticatorImporter: GoogleAuthenticatorImporter,
|
||||
private val appConfig: AppConfig,
|
||||
private val appBuild: AppBuild,
|
||||
private val lastScannedQrPreference: LastScannedQrPreference,
|
||||
) : AddServiceQrContract.Presenter() {
|
||||
|
||||
@ -69,7 +69,7 @@ class AddServiceQrPresenter(
|
||||
isGoogleAuthenticatorLink(content) -> importFromGoogleAuthenticator(isFromGallery, content)
|
||||
isMarketLink(content) -> view.showIncorrectQrStoreLink { resetScanner() }
|
||||
else -> {
|
||||
if (appConfig.isDebug) {
|
||||
if (appBuild.isDebuggable) {
|
||||
lastScannedQrPreference.put(content)
|
||||
}
|
||||
|
||||
@ -125,6 +125,7 @@ class AddServiceQrPresenter(
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
is ExternalImport.ParsingError -> onSaveFailed(isFromGallery, result.reason)
|
||||
ExternalImport.UnsupportedError -> onSaveFailed(isFromGallery, null)
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
package com.twofasapp.features.main
|
||||
|
||||
import com.twofasapp.resources.R
|
||||
import com.twofasapp.backup.domain.SyncBackupTrigger
|
||||
import com.twofasapp.backup.domain.SyncBackupWorkDispatcher
|
||||
import com.twofasapp.common.environment.AppBuild
|
||||
import com.twofasapp.core.analytics.AnalyticsService
|
||||
import com.twofasapp.environment.AppConfig
|
||||
import com.twofasapp.extensions.doNothing
|
||||
import com.twofasapp.parsers.ServiceIcons
|
||||
import com.twofasapp.permissions.CameraPermissionRequest
|
||||
@ -14,6 +13,7 @@ import com.twofasapp.prefs.model.RemoteBackupStatus
|
||||
import com.twofasapp.prefs.model.ServiceDto
|
||||
import com.twofasapp.prefs.usecase.RemoteBackupStatusPreference
|
||||
import com.twofasapp.prefs.usecase.StoreGroups
|
||||
import com.twofasapp.resources.R
|
||||
import com.twofasapp.services.domain.ConvertOtpLinkToService
|
||||
import com.twofasapp.services.domain.StoreHotpServices
|
||||
import com.twofasapp.start.domain.DeeplinkHandler
|
||||
@ -42,7 +42,7 @@ class MainPresenter(
|
||||
private val syncSyncBackupDispatcher: SyncBackupWorkDispatcher,
|
||||
private val servicesRefreshTrigger: ServicesRefreshTrigger,
|
||||
private val addService: AddService,
|
||||
private val appConfig: AppConfig,
|
||||
private val appBuild: AppBuild,
|
||||
private val parseOtpAuthLink: ParseOtpAuthLink,
|
||||
private val convertOtpLinkToService: ConvertOtpLinkToService,
|
||||
private val deeplinkHandler: DeeplinkHandler,
|
||||
@ -109,7 +109,7 @@ class MainPresenter(
|
||||
}
|
||||
|
||||
override fun markAppUpdateDisplayed() {
|
||||
appUpdateLastCheckVersionPreference.put(appConfig.versionCode.toLong())
|
||||
appUpdateLastCheckVersionPreference.put(appBuild.versionCode.toLong())
|
||||
}
|
||||
|
||||
override fun updateUnreadNotifications(hasUnreadNotifications: Boolean) {
|
||||
@ -219,7 +219,7 @@ class MainPresenter(
|
||||
}
|
||||
|
||||
override fun canDisplayAppUpdate(): Boolean {
|
||||
return appConfig.versionCode.toLong() != appUpdateLastCheckVersionPreference.get()
|
||||
return appBuild.versionCode.toLong() != appUpdateLastCheckVersionPreference.get()
|
||||
&& rateAppCondition.execute().not()
|
||||
}
|
||||
}
|
||||
|
@ -27,15 +27,8 @@ import com.google.android.play.core.install.model.InstallStatus
|
||||
import com.google.android.play.core.install.model.UpdateAvailability
|
||||
import com.google.android.play.core.review.ReviewManagerFactory
|
||||
import com.jakewharton.rxbinding3.appcompat.queryTextChanges
|
||||
import com.twofasapp.resources.R
|
||||
import com.twofasapp.base.BaseActivityPresenter
|
||||
import com.twofasapp.base.lifecycle.AuthAware
|
||||
import com.twofasapp.browserextension.domain.FetchTokenRequestsCase
|
||||
import com.twofasapp.browserextension.domain.ObserveMobileDeviceCase
|
||||
import com.twofasapp.browserextension.notification.BrowserExtensionRequestPayload
|
||||
import com.twofasapp.browserextension.notification.BrowserExtensionRequestReceiver
|
||||
import com.twofasapp.browserextension.notification.DomainMatcher
|
||||
import com.twofasapp.browserextension.ui.request.BrowserExtensionRequestActivity
|
||||
import com.twofasapp.databinding.ActivityMainBinding
|
||||
import com.twofasapp.design.dialogs.CancelAction
|
||||
import com.twofasapp.design.dialogs.ConfirmAction
|
||||
@ -53,18 +46,15 @@ import com.twofasapp.features.addserviceqr.AddServiceQrActivity
|
||||
import com.twofasapp.features.addserviceqr.ScanInfoDialog
|
||||
import com.twofasapp.features.backup.BackupActivity
|
||||
import com.twofasapp.features.services.addedservice.AddedServiceBottomSheet
|
||||
import com.twofasapp.notifications.domain.FetchNotificationsCase
|
||||
import com.twofasapp.notifications.domain.HasUnreadNotificationsCase
|
||||
import com.twofasapp.permissions.RationaleDialog
|
||||
import com.twofasapp.prefs.model.ServiceDto
|
||||
import com.twofasapp.resources.R
|
||||
import com.twofasapp.security.ui.security.SecurityActivity
|
||||
import com.twofasapp.services.domain.GetServicesCase
|
||||
import com.twofasapp.services.ui.ServiceActivity
|
||||
import com.twofasapp.usecases.services.EditStateObserver
|
||||
import com.twofasapp.usecases.services.SearchStateObserver
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
import lt.neworld.spanner.Spanner
|
||||
import lt.neworld.spanner.Spans
|
||||
@ -80,10 +70,9 @@ class MainServicesActivity : BaseActivityPresenter<ActivityMainBinding>(), MainC
|
||||
private val presenter: MainContract.Presenter by injectThis()
|
||||
private val editStateObserver: EditStateObserver by inject()
|
||||
private val searchStateObserver: SearchStateObserver by inject()
|
||||
private val hasUnreadNotificationsCase: HasUnreadNotificationsCase by inject()
|
||||
private val fetchNotificationsCase: FetchNotificationsCase by inject()
|
||||
private val fetchTokenRequestsCase: FetchTokenRequestsCase by inject()
|
||||
private val observeMobileDeviceCase: ObserveMobileDeviceCase by inject()
|
||||
|
||||
// private val fetchTokenRequestsCase: FetchTokenRequestsCase by inject()
|
||||
// private val observeMobileDeviceCase: ObserveMobileDeviceCase by inject()
|
||||
private val getServicesCase: GetServicesCase by inject()
|
||||
private val authenticationDialogs = mutableMapOf<String, MaterialDialog>()
|
||||
private val fabMenuDelegate: FabMenuDelegate by lazy { FabMenuDelegate(this, viewBinding) }
|
||||
@ -134,115 +123,104 @@ class MainServicesActivity : BaseActivityPresenter<ActivityMainBinding>(), MainC
|
||||
startActivity<BackupActivity>()
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
launch {
|
||||
hasUnreadNotificationsCase().flowOn(Dispatchers.IO).collect {
|
||||
presenter.updateUnreadNotifications(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
launch(Dispatchers.IO) { fetchNotificationsCase() }
|
||||
launch(Dispatchers.IO) {
|
||||
launch(Dispatchers.IO) {
|
||||
|
||||
try {
|
||||
val mobileDevice = observeMobileDeviceCase.invoke().first()
|
||||
val tokenRequests = fetchTokenRequestsCase(mobileDevice.id)
|
||||
|
||||
|
||||
tokenRequests.forEach { tokenRequest ->
|
||||
val domain = DomainMatcher.extractDomain(tokenRequest.domain)
|
||||
val matchedServices = DomainMatcher.findServicesMatchingDomain(
|
||||
getServicesCase(),
|
||||
domain
|
||||
)
|
||||
|
||||
runOnUiThread {
|
||||
if (authenticationDialogs.containsKey(tokenRequest.requestId)
|
||||
.not()
|
||||
) {
|
||||
authenticationDialogs.put(
|
||||
tokenRequest.requestId,
|
||||
MaterialDialog(this@MainServicesActivity)
|
||||
.title(text = "2FA token request")
|
||||
.message(text = "Do you want to share the 2FA token to ${tokenRequest.domain}?")
|
||||
.cancelable(false)
|
||||
.positiveButton(text = "Approve") {
|
||||
val isOneDomainMatched =
|
||||
matchedServices.size == 1
|
||||
val serviceId =
|
||||
if (matchedServices.size == 1) matchedServices.first().id else null
|
||||
|
||||
if (isOneDomainMatched) {
|
||||
val payload =
|
||||
BrowserExtensionRequestPayload(
|
||||
action = BrowserExtensionRequestPayload.Action.Approve,
|
||||
notificationId = -1,
|
||||
extensionId = tokenRequest.extensionId,
|
||||
requestId = tokenRequest.requestId,
|
||||
serviceId = serviceId ?: -1,
|
||||
domain = domain,
|
||||
)
|
||||
sendBroadcast(
|
||||
BrowserExtensionRequestReceiver.createIntent(
|
||||
this@MainServicesActivity,
|
||||
payload
|
||||
)
|
||||
)
|
||||
} else {
|
||||
|
||||
val contentIntent = Intent(
|
||||
this@MainServicesActivity,
|
||||
BrowserExtensionRequestActivity::class.java
|
||||
).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
|
||||
putExtra(
|
||||
BrowserExtensionRequestPayload.Key,
|
||||
BrowserExtensionRequestPayload(
|
||||
action = BrowserExtensionRequestPayload.Action.Approve,
|
||||
notificationId = -1,
|
||||
extensionId = tokenRequest.extensionId,
|
||||
requestId = tokenRequest.requestId,
|
||||
serviceId = serviceId ?: -1,
|
||||
domain = domain,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
startActivity(contentIntent)
|
||||
}
|
||||
}
|
||||
.negativeButton(text = "Deny") {
|
||||
val payload = BrowserExtensionRequestPayload(
|
||||
action = BrowserExtensionRequestPayload.Action.Deny,
|
||||
notificationId = -1,
|
||||
extensionId = tokenRequest.extensionId,
|
||||
requestId = tokenRequest.requestId,
|
||||
serviceId = -1,
|
||||
domain = domain,
|
||||
)
|
||||
sendBroadcast(
|
||||
BrowserExtensionRequestReceiver.createIntent(
|
||||
this@MainServicesActivity,
|
||||
payload
|
||||
)
|
||||
)
|
||||
}
|
||||
.show { }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
// launch(Dispatchers.IO) {
|
||||
//
|
||||
// try {
|
||||
// val mobileDevice = observeMobileDeviceCase.invoke().first()
|
||||
// val tokenRequests = fetchTokenRequestsCase(mobileDevice.id)
|
||||
//
|
||||
//
|
||||
// tokenRequests.forEach { tokenRequest ->
|
||||
// val domain = DomainMatcher.extractDomain(tokenRequest.domain)
|
||||
// val matchedServices = DomainMatcher.findServicesMatchingDomain(
|
||||
// getServicesCase(),
|
||||
// domain
|
||||
// )
|
||||
//
|
||||
// runOnUiThread {
|
||||
// if (authenticationDialogs.containsKey(tokenRequest.requestId)
|
||||
// .not()
|
||||
// ) {
|
||||
// authenticationDialogs.put(
|
||||
// tokenRequest.requestId,
|
||||
// MaterialDialog(this@MainServicesActivity)
|
||||
// .title(text = "2FA token request")
|
||||
// .message(text = "Do you want to share the 2FA token to ${tokenRequest.domain}?")
|
||||
// .cancelable(false)
|
||||
// .positiveButton(text = "Approve") {
|
||||
// val isOneDomainMatched =
|
||||
// matchedServices.size == 1
|
||||
// val serviceId =
|
||||
// if (matchedServices.size == 1) matchedServices.first().id else null
|
||||
//
|
||||
// if (isOneDomainMatched) {
|
||||
// val payload =
|
||||
// BrowserExtensionRequestPayload(
|
||||
// action = BrowserExtensionRequestPayload.Action.Approve,
|
||||
// notificationId = -1,
|
||||
// extensionId = tokenRequest.extensionId,
|
||||
// requestId = tokenRequest.requestId,
|
||||
// serviceId = serviceId ?: -1,
|
||||
// domain = domain,
|
||||
// )
|
||||
// sendBroadcast(
|
||||
// BrowserExtensionRequestReceiver.createIntent(
|
||||
// this@MainServicesActivity,
|
||||
// payload
|
||||
// )
|
||||
// )
|
||||
// } else {
|
||||
//
|
||||
// val contentIntent = Intent(
|
||||
// this@MainServicesActivity,
|
||||
// BrowserExtensionRequestActivity::class.java
|
||||
// ).apply {
|
||||
// flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
//
|
||||
// putExtra(
|
||||
// BrowserExtensionRequestPayload.Key,
|
||||
// BrowserExtensionRequestPayload(
|
||||
// action = BrowserExtensionRequestPayload.Action.Approve,
|
||||
// notificationId = -1,
|
||||
// extensionId = tokenRequest.extensionId,
|
||||
// requestId = tokenRequest.requestId,
|
||||
// serviceId = serviceId ?: -1,
|
||||
// domain = domain,
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// startActivity(contentIntent)
|
||||
// }
|
||||
// }
|
||||
// .negativeButton(text = "Deny") {
|
||||
// val payload = BrowserExtensionRequestPayload(
|
||||
// action = BrowserExtensionRequestPayload.Action.Deny,
|
||||
// notificationId = -1,
|
||||
// extensionId = tokenRequest.extensionId,
|
||||
// requestId = tokenRequest.requestId,
|
||||
// serviceId = -1,
|
||||
// domain = domain,
|
||||
// )
|
||||
// sendBroadcast(
|
||||
// BrowserExtensionRequestReceiver.createIntent(
|
||||
// this@MainServicesActivity,
|
||||
// payload
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// .show { }
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch (e: Exception) {
|
||||
// e.printStackTrace()
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,6 @@ import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.net.Uri
|
||||
import androidx.core.os.bundleOf
|
||||
import com.twofasapp.resources.R
|
||||
import com.twofasapp.about.ui.AboutActivity
|
||||
import com.twofasapp.backup.ui.export.ExportBackupActivity
|
||||
import com.twofasapp.core.RequestCodes
|
||||
import com.twofasapp.developer.ui.DeveloperActivity
|
||||
@ -15,22 +13,18 @@ import com.twofasapp.extensions.openBrowserApp
|
||||
import com.twofasapp.extensions.startActivity
|
||||
import com.twofasapp.extensions.startActivityForResult
|
||||
import com.twofasapp.extensions.toastLong
|
||||
import com.twofasapp.externalimport.ui.ExternalImportActivity
|
||||
import com.twofasapp.features.addserviceqr.AddServiceQrActivity
|
||||
import com.twofasapp.features.backup.BackupActivity
|
||||
import com.twofasapp.features.backup.import.ImportBackupActivity
|
||||
import com.twofasapp.features.main.MainServicesActivity
|
||||
import com.twofasapp.features.trash.TrashActivity
|
||||
import com.twofasapp.features.trash.delete.DisposeServiceActivity
|
||||
import com.twofasapp.notifications.ui.NotificationsActivity
|
||||
import com.twofasapp.prefs.ScopedNavigator
|
||||
import com.twofasapp.prefs.model.CheckLockStatus
|
||||
import com.twofasapp.prefs.model.LockMethodEntity
|
||||
import com.twofasapp.prefs.model.ServiceDto
|
||||
import com.twofasapp.resources.R
|
||||
import com.twofasapp.security.ui.lock.LockActivity
|
||||
import com.twofasapp.security.ui.security.SecurityActivity
|
||||
import com.twofasapp.services.ui.ServiceActivity
|
||||
import com.twofasapp.settings.ui.SettingsActivity
|
||||
import com.twofasapp.start.ui.start.StartActivity
|
||||
|
||||
|
||||
@ -95,7 +89,6 @@ class ActivityScopedNavigator(
|
||||
}
|
||||
|
||||
override fun openDisposeService(service: ServiceDto) {
|
||||
activity.startActivity<DisposeServiceActivity>(DisposeServiceActivity.ARG_SERVICE to service)
|
||||
}
|
||||
|
||||
override fun openSecurity() {
|
||||
@ -110,23 +103,21 @@ class ActivityScopedNavigator(
|
||||
}
|
||||
|
||||
override fun openSettings() {
|
||||
activity.startActivity<SettingsActivity>()
|
||||
// activity.startActivity<SettingsActivity>()
|
||||
}
|
||||
|
||||
override fun openExternalImport() {
|
||||
activity.startActivity<ExternalImportActivity>()
|
||||
// activity.startActivity<ExternalImportActivity>()
|
||||
}
|
||||
|
||||
override fun openTrash() {
|
||||
activity.startActivity<TrashActivity>()
|
||||
}
|
||||
|
||||
override fun openAuthenticate(canGoBack: Boolean, requestCode: Int?) {
|
||||
when (checkLockStatus.execute()) {
|
||||
LockMethodEntity.NO_LOCK -> Unit
|
||||
else -> activity.startActivityForResult<LockActivity>(
|
||||
requestCode
|
||||
?: RequestCodes.AUTH_REQUEST_CODE, "canGoBack" to canGoBack
|
||||
requestCode ?: RequestCodes.AUTH_REQUEST_CODE, "canGoBack" to canGoBack
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -144,7 +135,7 @@ class ActivityScopedNavigator(
|
||||
}
|
||||
|
||||
override fun openAbout() {
|
||||
activity.startActivity<AboutActivity>()
|
||||
// activity.startActivity<AboutActivity>()
|
||||
}
|
||||
|
||||
override fun openDeveloperOptions() {
|
||||
@ -163,7 +154,7 @@ class ActivityScopedNavigator(
|
||||
}
|
||||
|
||||
override fun openNotifications() {
|
||||
activity.startActivity<NotificationsActivity>()
|
||||
// TO BE REMOVED
|
||||
}
|
||||
|
||||
private fun resolveIntent(intent: Intent, action: () -> Unit) {
|
||||
|
@ -3,10 +3,8 @@ package com.twofasapp.features.services
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import com.mikepenz.fastadapter.binding.AbstractBindingItem
|
||||
import com.twofasapp.resources.R
|
||||
import com.twofasapp.databinding.ItemNoServicesBinding
|
||||
import com.twofasapp.extensions.startActivity
|
||||
import com.twofasapp.externalimport.ui.ExternalImportActivity
|
||||
import com.twofasapp.resources.R
|
||||
|
||||
class NoServicesItem : AbstractBindingItem<ItemNoServicesBinding>() {
|
||||
|
||||
@ -19,9 +17,5 @@ class NoServicesItem : AbstractBindingItem<ItemNoServicesBinding>() {
|
||||
|
||||
override fun bindView(binding: ItemNoServicesBinding, payloads: List<Any>) {
|
||||
super.bindView(binding, payloads)
|
||||
|
||||
binding.externalImport.setOnClickListener {
|
||||
binding.root.context.startActivity<ExternalImportActivity>()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package com.twofasapp.features.trash
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import com.mikepenz.fastadapter.binding.AbstractBindingItem
|
||||
import com.twofasapp.resources.R
|
||||
import com.twofasapp.databinding.ItemEmptyTrashBinding
|
||||
|
||||
class EmptyTrashItem : AbstractBindingItem<ItemEmptyTrashBinding>() {
|
||||
|
||||
override var identifier = R.id.item_empty_trash.toLong()
|
||||
override val type = R.id.item_empty_trash
|
||||
override var isSelectable = false
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater, parent: ViewGroup?) =
|
||||
ItemEmptyTrashBinding.inflate(inflater, parent, false)
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package com.twofasapp.features.trash
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import com.mikepenz.fastadapter.IItem
|
||||
import com.mikepenz.fastadapter.adapters.FastItemAdapter
|
||||
import com.mikepenz.fastadapter.diff.FastAdapterDiffUtil
|
||||
import com.twofasapp.base.BaseActivityPresenter
|
||||
import com.twofasapp.extensions.navigationClicksThrottled
|
||||
import com.twofasapp.databinding.ActivityTrashBinding
|
||||
import com.twofasapp.views.ModelDiffUtilCallback
|
||||
|
||||
class TrashActivity : BaseActivityPresenter<ActivityTrashBinding>(), TrashContract.View {
|
||||
|
||||
private val presenter: TrashContract.Presenter by injectThis()
|
||||
private val adapter = FastItemAdapter<IItem<*>>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityTrashBinding::inflate)
|
||||
setPresenter(presenter)
|
||||
viewBinding.recycler.adapter = adapter
|
||||
viewBinding.recycler.itemAnimator = DefaultItemAnimator()
|
||||
}
|
||||
|
||||
override fun toolbarBackClicks() = viewBinding.toolbar.navigationClicksThrottled()
|
||||
|
||||
override fun setItems(items: List<IItem<*>>) {
|
||||
FastAdapterDiffUtil.set(adapter.itemAdapter, items, ModelDiffUtilCallback())
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package com.twofasapp.features.trash
|
||||
|
||||
import com.mikepenz.fastadapter.IItem
|
||||
import com.twofasapp.base.BasePresenter
|
||||
import io.reactivex.Flowable
|
||||
|
||||
interface TrashContract {
|
||||
|
||||
interface View {
|
||||
fun toolbarBackClicks(): Flowable<Unit>
|
||||
|
||||
fun setItems(items: List<IItem<*>>)
|
||||
}
|
||||
|
||||
abstract class Presenter : com.twofasapp.base.BasePresenter() {
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package com.twofasapp.features.trash
|
||||
|
||||
import com.twofasapp.prefs.ScopedNavigator
|
||||
import com.twofasapp.usecases.services.GetTrashedServices
|
||||
import com.twofasapp.usecases.services.RestoreService
|
||||
|
||||
class TrashPresenter(
|
||||
private val view: TrashContract.View,
|
||||
private val navigator: ScopedNavigator,
|
||||
private val getTrashedServices: GetTrashedServices,
|
||||
private val restoreService: RestoreService,
|
||||
) : TrashContract.Presenter() {
|
||||
|
||||
override fun onViewAttached() {
|
||||
view.toolbarBackClicks().safelySubscribe { navigator.navigateBack() }
|
||||
refreshItems()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
refreshItems()
|
||||
}
|
||||
|
||||
private fun refreshItems() {
|
||||
getTrashedServices.execute()
|
||||
.safelySubscribe { list ->
|
||||
|
||||
if (list.isEmpty()) {
|
||||
view.setItems(listOf(EmptyTrashItem()))
|
||||
return@safelySubscribe
|
||||
}
|
||||
|
||||
val items = list
|
||||
.sortedByDescending { it.updatedAt }
|
||||
.map { service ->
|
||||
TrashedServiceItem(
|
||||
model = TrashedService(service),
|
||||
onRestoreClick = { onRestoreClick(it) },
|
||||
onDeleteClick = { onDeleteClick(it) },
|
||||
)
|
||||
}
|
||||
|
||||
view.setItems(items)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRestoreClick(model: TrashedService) {
|
||||
restoreService.execute(model.service)
|
||||
.safelySubscribe { refreshItems() }
|
||||
}
|
||||
|
||||
private fun onDeleteClick(model: TrashedService) {
|
||||
navigator.openDisposeService(model.service)
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package com.twofasapp.features.trash
|
||||
|
||||
import com.twofasapp.prefs.model.ServiceDto
|
||||
|
||||
data class TrashedService(
|
||||
val service: ServiceDto,
|
||||
)
|
@ -1,45 +0,0 @@
|
||||
package com.twofasapp.features.trash
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.ViewGroup
|
||||
import android.widget.PopupMenu
|
||||
import androidx.core.view.isVisible
|
||||
import com.mikepenz.fastadapter.binding.ModelAbstractBindingItem
|
||||
import com.twofasapp.resources.R
|
||||
import com.twofasapp.databinding.ItemTrashedServiceBinding
|
||||
|
||||
class TrashedServiceItem(
|
||||
model: TrashedService,
|
||||
private val onRestoreClick: (TrashedService) -> Unit,
|
||||
private val onDeleteClick: (TrashedService) -> Unit,
|
||||
) : ModelAbstractBindingItem<TrashedService, ItemTrashedServiceBinding>(model) {
|
||||
|
||||
override var identifier = model.service.id
|
||||
override val type = R.id.item_trashed_service
|
||||
override var isSelectable = false
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater, parent: ViewGroup?) =
|
||||
ItemTrashedServiceBinding.inflate(inflater, parent, false)
|
||||
|
||||
override fun bindView(binding: ItemTrashedServiceBinding, payloads: List<Any>) {
|
||||
binding.name.text = model.service.name
|
||||
binding.info.text = model.service.otpAccount
|
||||
binding.info.isVisible = model.service.otpAccount.isNullOrBlank().not()
|
||||
|
||||
binding.iconLayout.updateIcon(model.service)
|
||||
|
||||
binding.actionMore.setOnClickListener {
|
||||
val popup = PopupMenu(it.context!!, it)
|
||||
popup.menuInflater.inflate(com.twofasapp.R.menu.menu_trashed_service, popup.menu)
|
||||
popup.setOnMenuItemClickListener { menuItem: MenuItem ->
|
||||
when (menuItem.itemId) {
|
||||
com.twofasapp.R.id.menu_restore -> onRestoreClick.invoke(model)
|
||||
com.twofasapp. R.id.menu_delete -> onDeleteClick.invoke(model)
|
||||
}
|
||||
true
|
||||
}
|
||||
popup.show()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package com.twofasapp.features.trash.delete
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.TextView
|
||||
import com.jakewharton.rxbinding3.widget.checkedChanges
|
||||
import com.twofasapp.base.BaseActivityPresenter
|
||||
import com.twofasapp.extensions.clicksThrottled
|
||||
import com.twofasapp.databinding.ActivityDisposeServiceBinding
|
||||
import com.twofasapp.prefs.model.ServiceDto
|
||||
import com.twofasapp.services.ui.ServiceActivity
|
||||
import io.reactivex.BackpressureStrategy
|
||||
import io.reactivex.Flowable
|
||||
|
||||
class DisposeServiceActivity : BaseActivityPresenter<ActivityDisposeServiceBinding>(), DisposeServiceContract.View {
|
||||
|
||||
companion object {
|
||||
const val ARG_SERVICE = "service"
|
||||
}
|
||||
|
||||
private val presenter: DisposeServiceContract.Presenter by injectThis()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityDisposeServiceBinding::inflate)
|
||||
setPresenter(presenter)
|
||||
}
|
||||
|
||||
override fun deleteClicks() = viewBinding.delete.clicksThrottled()
|
||||
|
||||
override fun cancelClicks() = viewBinding.cancel.clicksThrottled()
|
||||
|
||||
override fun closeClicks() = viewBinding.close.clicksThrottled()
|
||||
|
||||
override fun deleteSwitchChanges(): Flowable<Boolean> =
|
||||
viewBinding.deleteSwitch.checkedChanges().toFlowable(BackpressureStrategy.LATEST)
|
||||
|
||||
override fun setDeleteEnabled(isEnabled: Boolean) {
|
||||
viewBinding.delete.isEnabled = isEnabled
|
||||
}
|
||||
|
||||
override fun getServiceExtra() =
|
||||
intent.extras!!.getParcelable<ServiceDto>(ServiceActivity.ARG_SERVICE)!!
|
||||
|
||||
override fun setHeader(serviceName: String?) {
|
||||
viewBinding.header.text = serviceName
|
||||
}
|
||||
|
||||
override fun setNote(resId: Int?, serviceName: String?) =
|
||||
formatText(viewBinding.note, resId, serviceName)
|
||||
|
||||
private fun formatText(textView: TextView, resId: Int?, serviceName: String?) {
|
||||
if (resId != null) {
|
||||
textView.text = getString(resId, serviceName, serviceName)
|
||||
} else {
|
||||
textView.text = serviceName
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package com.twofasapp.features.trash.delete
|
||||
|
||||
import com.twofasapp.prefs.model.ServiceDto
|
||||
import io.reactivex.Flowable
|
||||
|
||||
interface DisposeServiceContract {
|
||||
|
||||
interface View {
|
||||
fun deleteClicks(): Flowable<Unit>
|
||||
fun cancelClicks(): Flowable<Unit>
|
||||
fun closeClicks(): Flowable<Unit>
|
||||
fun deleteSwitchChanges(): Flowable<Boolean>
|
||||
|
||||
fun getServiceExtra(): ServiceDto
|
||||
fun setHeader(serviceName: String?)
|
||||
fun setNote(resId: Int?, serviceName: String?)
|
||||
fun setDeleteEnabled(isEnabled: Boolean)
|
||||
}
|
||||
|
||||
abstract class Presenter : com.twofasapp.base.BasePresenter()
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package com.twofasapp.features.trash.delete
|
||||
|
||||
import com.twofasapp.resources.R
|
||||
import com.twofasapp.prefs.ScopedNavigator
|
||||
import com.twofasapp.services.domain.DeleteServiceUseCase
|
||||
|
||||
class DisposeServicePresenter(
|
||||
private val view: DisposeServiceContract.View,
|
||||
private val navigator: ScopedNavigator,
|
||||
private val deleteServiceUseCase: DeleteServiceUseCase,
|
||||
) : DisposeServiceContract.Presenter() {
|
||||
|
||||
override fun onViewAttached() {
|
||||
view.getServiceExtra().let {
|
||||
view.setHeader(it.name)
|
||||
view.setNote(R.string.tokens__you_will_not_be_able_to_sign_in_to_your, it.name)
|
||||
}
|
||||
|
||||
view.closeClicks().safelySubscribe { navigator.navigateBack() }
|
||||
view.cancelClicks().safelySubscribe { navigator.navigateBack() }
|
||||
view.deleteSwitchChanges().safelySubscribe { view.setDeleteEnabled(it) }
|
||||
|
||||
view.deleteClicks()
|
||||
.flatMapSingle { deleteServiceUseCase.execute(view.getServiceExtra()).toSingle { } }
|
||||
.safelySubscribe {
|
||||
navigator.finish()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
package com.twofasapp.navigation
|
||||
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.composable
|
||||
import com.twofasapp.externalimport.ui.aegis.AegisScreenFactory
|
||||
import com.twofasapp.externalimport.ui.googleauthenticator.GoogleAuthenticatorScreenFactory
|
||||
import com.twofasapp.externalimport.ui.main.ExternalImportScreenFactory
|
||||
import com.twofasapp.externalimport.ui.raivo.RaivoScreenFactory
|
||||
import com.twofasapp.externalimport.ui.result.ImportResultScreenFactory
|
||||
import com.twofasapp.externalimport.ui.scan.ImportScanScreenFactory
|
||||
import timber.log.Timber
|
||||
|
||||
class ExternalImportRouterImpl(
|
||||
private val externalImportScreenFactory: ExternalImportScreenFactory,
|
||||
private val importScanScreenFactory: ImportScanScreenFactory,
|
||||
private val importResultScreenFactory: ImportResultScreenFactory,
|
||||
private val googleAuthenticatorScreenFactory: GoogleAuthenticatorScreenFactory,
|
||||
private val aegisScreenFactory: AegisScreenFactory,
|
||||
private val raivoScreenFactory: RaivoScreenFactory,
|
||||
) : ExternalImportRouter() {
|
||||
|
||||
companion object {
|
||||
private const val ARG_CONTENT = "ARG_CONTENT"
|
||||
private const val ARG_TYPE = "ARG_TYPE"
|
||||
private const val ARG_START_GALLERY = "ARG_START_GALLERY"
|
||||
|
||||
private const val MAIN = "external_import"
|
||||
private const val IMPORT_SCAN = "import_scan/{$ARG_START_GALLERY}"
|
||||
private const val IMPORT_RESULT = "import_result/{$ARG_TYPE}/{$ARG_CONTENT}"
|
||||
private const val GOOGLE_AUTH = "import_google_authenticator"
|
||||
private const val AEGIS = "import_aegis"
|
||||
private const val RAIVO = "import_raivo"
|
||||
}
|
||||
|
||||
override fun buildNavGraph(builder: NavGraphBuilder, viewModelStoreOwner: ViewModelStoreOwner?) {
|
||||
builder.composable(route = MAIN, content = { externalImportScreenFactory.create() })
|
||||
builder.composable(
|
||||
route = IMPORT_SCAN,
|
||||
content = { importScanScreenFactory.create(it.arguments?.getString(ARG_START_GALLERY).toBoolean()) })
|
||||
builder.composable(
|
||||
route = IMPORT_RESULT,
|
||||
content = {
|
||||
importResultScreenFactory.create(
|
||||
type = it.arguments?.getString(ARG_TYPE).orEmpty(),
|
||||
content = it.arguments?.getString(ARG_CONTENT).orEmpty()
|
||||
)
|
||||
}
|
||||
)
|
||||
builder.composable(route = GOOGLE_AUTH, content = { googleAuthenticatorScreenFactory.create() })
|
||||
builder.composable(route = AEGIS, content = { aegisScreenFactory.create() })
|
||||
builder.composable(route = RAIVO, content = { raivoScreenFactory.create() })
|
||||
}
|
||||
|
||||
override fun navigate(navController: NavHostController, direction: ExternalImportDirections) {
|
||||
Timber.d("$direction")
|
||||
|
||||
when (direction) {
|
||||
ExternalImportDirections.GoBack -> navController.popBackStack()
|
||||
ExternalImportDirections.Main -> navController.navigate(MAIN)
|
||||
is ExternalImportDirections.ImportScan -> {
|
||||
navController.navigate(
|
||||
IMPORT_SCAN.replace("{${ARG_START_GALLERY}}", direction.startWithGallery.toString())
|
||||
)
|
||||
}
|
||||
is ExternalImportDirections.ImportResult -> {
|
||||
navController.navigate(
|
||||
IMPORT_RESULT
|
||||
.replace("{${ARG_TYPE}}", direction.type.name)
|
||||
.replace("{${ARG_CONTENT}}", direction.content)
|
||||
) { popUpTo(MAIN) }
|
||||
}
|
||||
ExternalImportDirections.GoogleAuthenticator -> navController.navigate(GOOGLE_AUTH)
|
||||
ExternalImportDirections.Aegis -> navController.navigate(AEGIS)
|
||||
ExternalImportDirections.Raivo -> navController.navigate(RAIVO)
|
||||
}
|
||||
}
|
||||
|
||||
override fun navigateBack() {
|
||||
navigate(ExternalImportDirections.GoBack)
|
||||
}
|
||||
|
||||
override fun startDirection(): String = MAIN
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
package com.twofasapp.navigation
|
||||
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.composable
|
||||
import com.twofasapp.browserextension.ui.browser.BrowserDetailsScreenFactory
|
||||
import com.twofasapp.browserextension.ui.main.BrowserExtensionScreenFactory
|
||||
import com.twofasapp.browserextension.ui.pairing.progress.PairingProgressScreenFactory
|
||||
import com.twofasapp.browserextension.ui.pairing.scan.PairingScanScreenFactory
|
||||
import com.twofasapp.settings.ui.main.SettingsMainScreenFactory
|
||||
import com.twofasapp.settings.ui.theme.ThemeScreenFactory
|
||||
import timber.log.Timber
|
||||
|
||||
class SettingsRouterImpl(
|
||||
private val settingsMainScreenFactory: SettingsMainScreenFactory,
|
||||
private val themeScreenFactory: ThemeScreenFactory,
|
||||
private val browserExtensionScreenFactory: BrowserExtensionScreenFactory,
|
||||
private val pairingProgressScreenFactory: PairingProgressScreenFactory,
|
||||
private val pairingScanScreenFactory: PairingScanScreenFactory,
|
||||
private val browserDetailsScreenFactory: BrowserDetailsScreenFactory,
|
||||
) : SettingsRouter() {
|
||||
|
||||
companion object {
|
||||
private const val ARG_EXTENSION_ID = "extensionId"
|
||||
|
||||
private const val MAIN = "settings_main"
|
||||
private const val THEME = "theme"
|
||||
private const val BROWSER_EXTENSION = "browser_extension"
|
||||
private const val BROWSER_DETAILS = "browser_details/{$ARG_EXTENSION_ID}"
|
||||
private const val PAIRING_SCAN = "pairing_scan"
|
||||
private const val PAIRING_PROGRESS = "pairing_progress/{$ARG_EXTENSION_ID}"
|
||||
}
|
||||
|
||||
override fun buildNavGraph(builder: NavGraphBuilder, viewModelStoreOwner: ViewModelStoreOwner?) {
|
||||
builder.composable(route = MAIN, content = { settingsMainScreenFactory.create() })
|
||||
builder.composable(route = THEME, content = { themeScreenFactory.create() })
|
||||
builder.composable(route = BROWSER_EXTENSION, content = { browserExtensionScreenFactory.create() })
|
||||
builder.composable(route = BROWSER_DETAILS, content = {
|
||||
browserDetailsScreenFactory.create(it.arguments?.getString(ARG_EXTENSION_ID).orEmpty())
|
||||
})
|
||||
builder.composable(route = PAIRING_SCAN, content = { pairingScanScreenFactory.create() })
|
||||
builder.composable(route = PAIRING_PROGRESS, content = {
|
||||
pairingProgressScreenFactory.create(it.arguments?.getString(ARG_EXTENSION_ID).orEmpty())
|
||||
})
|
||||
}
|
||||
|
||||
override fun startDirection(): String = MAIN
|
||||
|
||||
override fun navigate(
|
||||
navController: NavHostController,
|
||||
direction: SettingsDirections,
|
||||
) {
|
||||
Timber.d("$direction")
|
||||
|
||||
when (direction) {
|
||||
SettingsDirections.GoBack -> navController.popBackStack()
|
||||
SettingsDirections.Main -> navController.navigate(MAIN)
|
||||
SettingsDirections.Theme -> navController.navigate(THEME)
|
||||
SettingsDirections.BrowserExtension -> navController.navigate(BROWSER_EXTENSION)
|
||||
SettingsDirections.PairingScan -> navController.navigate(PAIRING_SCAN) { popUpTo(BROWSER_EXTENSION) }
|
||||
|
||||
is SettingsDirections.PairingProgress -> navController.navigate(
|
||||
PAIRING_PROGRESS.replace("{$ARG_EXTENSION_ID}", direction.extensionId)
|
||||
) { popUpTo(BROWSER_EXTENSION) }
|
||||
|
||||
is SettingsDirections.BrowserDetails -> navController.navigate(
|
||||
BROWSER_DETAILS.replace("{$ARG_EXTENSION_ID}", direction.extensionId)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun navigateBack() {
|
||||
navigate(SettingsDirections.GoBack)
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ package com.twofasapp.navigation
|
||||
import android.app.Activity
|
||||
import com.twofasapp.extensions.startActivity
|
||||
import com.twofasapp.features.main.MainServicesActivity
|
||||
import com.twofasapp.start.ui.onboarding.OnboardingActivity
|
||||
|
||||
class StartRouterImpl(
|
||||
private val activity: Activity
|
||||
@ -14,7 +13,6 @@ class StartRouterImpl(
|
||||
override fun navigate(direction: StartDirections) {
|
||||
when (direction) {
|
||||
is StartDirections.Main -> activity.startActivity<MainServicesActivity>()
|
||||
is StartDirections.Onboarding -> activity.startActivity<OnboardingActivity>()
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ import com.twofasapp.services.googledrive.models.UpdateGoogleDriveFileResult
|
||||
import com.twofasapp.services.googledrive.models.mapToRemoteBackupErrorType
|
||||
import com.twofasapp.prefs.model.isSet
|
||||
import com.twofasapp.base.usecase.UseCaseParameterized
|
||||
import com.twofasapp.environment.AppConfig
|
||||
import com.twofasapp.common.environment.AppBuild
|
||||
import com.twofasapp.usecases.services.GetServices
|
||||
import com.twofasapp.prefs.usecase.StoreGroups
|
||||
import com.twofasapp.services.domain.StoreServicesOrder
|
||||
@ -24,7 +24,7 @@ class UpdateRemoteBackup(
|
||||
private val remoteBackupKeyPreference: com.twofasapp.prefs.usecase.RemoteBackupKeyPreference,
|
||||
private val jsonSerializer: com.twofasapp.serialization.JsonSerializer,
|
||||
private val encryptBackup: EncryptBackup,
|
||||
private val appConfig: AppConfig,
|
||||
private val appBuild: AppBuild,
|
||||
) : UseCaseParameterized<UpdateRemoteBackup.Params, Single<UpdateRemoteBackupResult>> {
|
||||
|
||||
data class Params(
|
||||
@ -50,8 +50,8 @@ class UpdateRemoteBackup(
|
||||
|
||||
com.twofasapp.prefs.model.RemoteBackup(
|
||||
updatedAt = params.updatedAt,
|
||||
appVersionCode = appConfig.versionCode,
|
||||
appVersionName = appConfig.versionName,
|
||||
appVersionCode = appBuild.versionCode,
|
||||
appVersionName = appBuild.versionName,
|
||||
groups = groups.filter { it.id != null }.map { it.toRemote() },
|
||||
services = servicesOrdered,
|
||||
account = remoteBackupStatusPreference.get().account,
|
||||
|
21
app/src/main/java/com/twofasapp/time/TimeProviderImpl.kt
Normal file
21
app/src/main/java/com/twofasapp/time/TimeProviderImpl.kt
Normal file
@ -0,0 +1,21 @@
|
||||
package com.twofasapp.time
|
||||
|
||||
import android.os.SystemClock
|
||||
import com.twofasapp.common.time.TimeProvider
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class TimeProviderImpl : TimeProvider {
|
||||
|
||||
override fun currentDateTimeUtc(): OffsetDateTime {
|
||||
return OffsetDateTime.now(ZoneOffset.UTC)
|
||||
}
|
||||
|
||||
override fun systemCurrentTime(): Long {
|
||||
return System.currentTimeMillis()
|
||||
}
|
||||
|
||||
override fun systemElapsedTime(): Long {
|
||||
return SystemClock.elapsedRealtime()
|
||||
}
|
||||
}
|
@ -1,12 +1,30 @@
|
||||
package com.twofasapp.ui.main
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import com.twofasapp.common.ktx.clearGraphBackStack
|
||||
import com.twofasapp.extensions.startActivity
|
||||
import com.twofasapp.extensions.startActivityForResult
|
||||
import com.twofasapp.feature.about.navigation.AboutGraph
|
||||
import com.twofasapp.feature.about.navigation.aboutNavigation
|
||||
import com.twofasapp.feature.appsettings.navigation.AppSettingsGraph
|
||||
import com.twofasapp.feature.appsettings.navigation.appSettingsNavigation
|
||||
import com.twofasapp.feature.browserext.notification.BrowserExtGraph
|
||||
import com.twofasapp.feature.browserext.notification.browserExtNavigation
|
||||
import com.twofasapp.feature.externalimport.navigation.ExternalImportGraph
|
||||
import com.twofasapp.feature.externalimport.navigation.externalImportNavigation
|
||||
import com.twofasapp.feature.home.navigation.HomeGraph
|
||||
import com.twofasapp.feature.home.navigation.HomeNavigationListener
|
||||
import com.twofasapp.feature.home.navigation.homeNavigation
|
||||
import com.twofasapp.feature.startup.navigation.startupNavigation
|
||||
|
||||
import com.twofasapp.feature.trash.navigation.TrashGraph
|
||||
import com.twofasapp.feature.trash.navigation.trashNavigation
|
||||
import com.twofasapp.features.addserviceqr.AddServiceQrActivity
|
||||
import com.twofasapp.features.backup.BackupActivity
|
||||
import com.twofasapp.security.ui.security.SecurityActivity
|
||||
import com.twofasapp.services.ui.ServiceActivity
|
||||
|
||||
@Composable
|
||||
fun MainNavHost(
|
||||
@ -14,10 +32,67 @@ fun MainNavHost(
|
||||
startDestination: String,
|
||||
) {
|
||||
NavHost(navController = navController, startDestination = startDestination) {
|
||||
|
||||
startupNavigation(
|
||||
onFinish = { navController.navigate(HomeGraph.route) { popUpTo(0) } }
|
||||
openHome = { navController.navigate(HomeGraph.route) { popUpTo(0) } }
|
||||
)
|
||||
|
||||
homeNavigation()
|
||||
homeNavigation(
|
||||
navController = navController,
|
||||
listener = object : HomeNavigationListener {
|
||||
override fun openAddManuallyService(activity: Activity) {
|
||||
activity.startActivity<ServiceActivity>()
|
||||
}
|
||||
|
||||
override fun openAddQrService(activity: Activity) {
|
||||
activity.startActivity<AddServiceQrActivity>()
|
||||
}
|
||||
|
||||
override fun openService(activity: Activity, serviceId: Long) {
|
||||
activity.startActivityForResult<ServiceActivity>(
|
||||
ServiceActivity.REQUEST_KEY_ADD_SERVICE,
|
||||
ServiceActivity.ARG_SERVICE_ID to serviceId,
|
||||
)
|
||||
}
|
||||
|
||||
override fun openExternalImport() {
|
||||
navController.navigate(ExternalImportGraph.route)
|
||||
}
|
||||
|
||||
override fun openBrowserExt() {
|
||||
navController.navigate(BrowserExtGraph.route)
|
||||
}
|
||||
|
||||
override fun openSecurity(activity: Activity) {
|
||||
activity.startActivity<SecurityActivity>()
|
||||
}
|
||||
|
||||
override fun openBackup(activity: Activity) {
|
||||
activity.startActivity<BackupActivity>()
|
||||
}
|
||||
|
||||
override fun openAppSettings() {
|
||||
navController.navigate(AppSettingsGraph.route)
|
||||
}
|
||||
|
||||
override fun openTrash() {
|
||||
navController.navigate(TrashGraph.route)
|
||||
}
|
||||
|
||||
override fun openAbout() {
|
||||
navController.navigate(AboutGraph.route)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
externalImportNavigation(
|
||||
navController = navController,
|
||||
onFinish = { navController.clearGraphBackStack() }
|
||||
)
|
||||
|
||||
appSettingsNavigation()
|
||||
trashNavigation(navController = navController)
|
||||
aboutNavigation(navController = navController)
|
||||
browserExtNavigation(navController = navController)
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.twofasapp.designsystem.MainAppTheme
|
||||
import com.twofasapp.designsystem.TwsTheme
|
||||
import com.twofasapp.designsystem.TwTheme
|
||||
import com.twofasapp.feature.home.navigation.HomeGraph
|
||||
import com.twofasapp.feature.startup.navigation.StartupGraph
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
@ -24,7 +24,7 @@ fun MainScreen(
|
||||
MainAppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = TwsTheme.color.background,
|
||||
color = TwTheme.color.background,
|
||||
) {
|
||||
uiState?.let {
|
||||
val startDestination = when (it) {
|
||||
|
@ -3,6 +3,8 @@ package com.twofasapp.ui.main
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.twofasapp.common.coroutines.Dispatchers
|
||||
import com.twofasapp.common.ktx.runSafely
|
||||
import com.twofasapp.data.notifications.NotificationsRepository
|
||||
import com.twofasapp.data.session.SessionRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
@ -11,6 +13,7 @@ import kotlinx.coroutines.launch
|
||||
class MainViewModel(
|
||||
private val dispatchers: Dispatchers,
|
||||
private val sessionRepository: SessionRepository,
|
||||
private val notificationsRepository: NotificationsRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
val uiState: MutableStateFlow<MainUiState?> = MutableStateFlow(null)
|
||||
@ -24,5 +27,9 @@ class MainViewModel(
|
||||
|
||||
uiState.update { state }
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
runSafely { notificationsRepository.fetchNotifications() }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,12 @@
|
||||
package com.twofasapp.usecases.backup
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.twofasapp.BuildConfig
|
||||
import com.twofasapp.backup.domain.SyncBackupTrigger
|
||||
import com.twofasapp.base.usecase.UseCaseParameterized
|
||||
import com.twofasapp.common.environment.AppBuild
|
||||
import com.twofasapp.core.analytics.AnalyticsEvent
|
||||
import com.twofasapp.core.analytics.AnalyticsParam
|
||||
import com.twofasapp.entity.SyncBackupResult
|
||||
import com.twofasapp.environment.AppConfig
|
||||
import com.twofasapp.extensions.doNothing
|
||||
import com.twofasapp.parsers.LegacyTypeToId
|
||||
import com.twofasapp.parsers.ServiceIcons
|
||||
@ -53,7 +52,7 @@ class SyncBackupServices(
|
||||
private val analyticsService: com.twofasapp.core.analytics.AnalyticsService,
|
||||
private val storeRecentlyDeleted: StoreRecentlyDeleted,
|
||||
private val observeSyncStatus: ObserveSyncStatus,
|
||||
private val appConfig: AppConfig,
|
||||
private val appBuild: AppBuild,
|
||||
) : UseCaseParameterized<SyncBackupServices.Params, Single<SyncBackupResult>> {
|
||||
|
||||
data class Params(
|
||||
@ -192,7 +191,7 @@ class SyncBackupServices(
|
||||
val localSchemaVersion = backupStatus.schemaVersion
|
||||
val remoteSchemaVersion = remoteStatus.schemaVersion
|
||||
|
||||
val localAppVersionCode = appConfig.versionCode
|
||||
val localAppVersionCode = appBuild.versionCode
|
||||
val remoteAppVersionCode = remoteStatus.appVersionCode
|
||||
|
||||
// Sync matching groups
|
||||
|
@ -1,17 +0,0 @@
|
||||
package com.twofasapp.usecases.services
|
||||
|
||||
import com.twofasapp.prefs.model.ServiceDto
|
||||
import com.twofasapp.services.data.ServicesRepository
|
||||
import com.twofasapp.base.usecase.UseCase
|
||||
import io.reactivex.Scheduler
|
||||
import io.reactivex.Single
|
||||
|
||||
class GetTrashedServices(private val servicesRepository: ServicesRepository) : UseCase<Single<List<ServiceDto>>> {
|
||||
|
||||
override fun execute(subscribeScheduler: Scheduler, observeScheduler: Scheduler): Single<List<ServiceDto>> {
|
||||
return servicesRepository.select()
|
||||
.map { list -> list.filter { it.isDeleted == true } }
|
||||
.subscribeOn(subscribeScheduler)
|
||||
.observeOn(observeScheduler)
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/coordinator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
style="@style/Toolbar.Back"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/settings__trash" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/windowBackground"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -11,7 +11,7 @@
|
||||
android:id="@+id/orbits"
|
||||
android:layout_width="110dp"
|
||||
android:layout_height="110dp"
|
||||
android:src="@drawable/services_empty" />
|
||||
android:src="@drawable/img_services_empty" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyStateTitle"
|
||||
|
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/menu_restore"
|
||||
android:title="@string/settings__restore" />
|
||||
<item
|
||||
android:id="@+id/menu_delete"
|
||||
android:title="@string/tokens__remove_forever" />
|
||||
</menu>
|
@ -12,16 +12,15 @@ android {
|
||||
dependencies {
|
||||
implementation(project(":base"))
|
||||
implementation(project(":core"))
|
||||
implementation(project(":di"))
|
||||
implementation(project(":core:common"))
|
||||
implementation(project(":core:di"))
|
||||
implementation(project(":design"))
|
||||
implementation(project(":extensions"))
|
||||
implementation(project(":permissions"))
|
||||
implementation(project(":prefs"))
|
||||
implementation(project(":persistence"))
|
||||
implementation(project(":network"))
|
||||
implementation(project(":push"))
|
||||
implementation(project(":resources"))
|
||||
implementation(project(":environment"))
|
||||
implementation(project(":navigation"))
|
||||
implementation(project(":services:domain"))
|
||||
implementation(project(":serialization"))
|
||||
|
@ -9,6 +9,6 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(project(":base"))
|
||||
implementation(project(":di"))
|
||||
implementation(project(":core:di"))
|
||||
implementation(project(":extensions"))
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import android.net.Uri
|
||||
import com.twofasapp.backup.EncryptBackup
|
||||
import com.twofasapp.backup.domain.converter.toRemoteGroup
|
||||
import com.twofasapp.backup.domain.converter.toRemoteService
|
||||
import com.twofasapp.environment.AppConfig
|
||||
import com.twofasapp.common.environment.AppBuild
|
||||
import com.twofasapp.prefs.model.RemoteBackup
|
||||
import com.twofasapp.prefs.model.RemoteService
|
||||
import com.twofasapp.serialization.JsonSerializer
|
||||
@ -17,7 +17,7 @@ import com.twofasapp.time.domain.TimeProvider
|
||||
class ExportBackupSuspended(
|
||||
private val context: Context,
|
||||
private val timeProvider: TimeProvider,
|
||||
private val appConfig: AppConfig,
|
||||
private val appBuild: AppBuild,
|
||||
private val servicesRepository: ServicesRepository,
|
||||
private val getServicesCase: GetServicesCase,
|
||||
private val getGroupsCase: GetGroupsCase,
|
||||
@ -46,8 +46,8 @@ class ExportBackupSuspended(
|
||||
.copy(order = RemoteService.Order(position = servicesOrder.ids.indexOf(it.id)))
|
||||
},
|
||||
updatedAt = timeProvider.systemCurrentTime(),
|
||||
appVersionCode = appConfig.versionCode,
|
||||
appVersionName = appConfig.versionName,
|
||||
appVersionCode = appBuild.versionCode,
|
||||
appVersionName = appBuild.versionName,
|
||||
groups = groups.list.filter { group -> group.id != null }.map { group -> group.toRemoteGroup() },
|
||||
account = null,
|
||||
)
|
||||
@ -72,6 +72,7 @@ class ExportBackupSuspended(
|
||||
|
||||
Result.Success(json)
|
||||
}
|
||||
|
||||
is EncryptBackup.Result.Error -> Result.Error(result.throwable)
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.twofasapp.backup.EncryptBackup
|
||||
import com.twofasapp.backup.ui.export.ExportBackup
|
||||
import com.twofasapp.environment.AppConfig
|
||||
import com.twofasapp.common.environment.AppBuild
|
||||
import com.twofasapp.prefs.usecase.ServicesOrderPreference
|
||||
import com.twofasapp.prefs.usecase.StoreGroups
|
||||
import com.twofasapp.services.domain.GetServicesUseCase
|
||||
@ -16,7 +16,7 @@ import io.reactivex.schedulers.Schedulers
|
||||
|
||||
class ExportBackupToDisk(
|
||||
private val context: Context,
|
||||
private val appConfig: AppConfig,
|
||||
private val appBuild: AppBuild,
|
||||
private val getServices: GetServicesUseCase,
|
||||
private val timeProvider: TimeProvider,
|
||||
private val servicesOrderPreference: ServicesOrderPreference,
|
||||
@ -36,8 +36,8 @@ class ExportBackupToDisk(
|
||||
it.mapToRemote()
|
||||
.copy(order = com.twofasapp.prefs.model.RemoteService.Order(position = servicesOrder.ids.indexOf(it.id)))
|
||||
},
|
||||
appVersionCode = appConfig.versionCode,
|
||||
appVersionName = appConfig.versionName,
|
||||
appVersionCode = appBuild.versionCode,
|
||||
appVersionName = appBuild.versionName,
|
||||
groups = storeGroups.all().list.filter { group -> group.id != null }.map { group -> group.toRemote() },
|
||||
account = null,
|
||||
)
|
||||
@ -60,6 +60,7 @@ class ExportBackupToDisk(
|
||||
Single.just(ExportBackup.Result.Success(jsonSerializer.serializePretty(result.encryptedRemoteBackup)))
|
||||
}
|
||||
}
|
||||
|
||||
is EncryptBackup.Result.Error -> Single.just(ExportBackup.Result.UnknownError)
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,9 @@ import android.os.Bundle
|
||||
import androidx.core.content.FileProvider
|
||||
import com.twofasapp.backup.databinding.ActivityExportBackupBinding
|
||||
import com.twofasapp.base.BaseActivityPresenter
|
||||
import com.twofasapp.common.environment.AppBuild
|
||||
import com.twofasapp.core.RequestCodes
|
||||
import com.twofasapp.design.dialogs.InfoDialog
|
||||
import com.twofasapp.environment.AppConfig
|
||||
import com.twofasapp.extensions.clicksThrottled
|
||||
import com.twofasapp.extensions.navigationClicksThrottled
|
||||
import com.twofasapp.extensions.toastLong
|
||||
@ -89,7 +89,7 @@ class ExportBackupActivity : BaseActivityPresenter<ActivityExportBackupBinding>(
|
||||
outputStream.write(content.toByteArray())
|
||||
outputStream.close()
|
||||
|
||||
val uri = FileProvider.getUriForFile(this, get<AppConfig>().id, file)
|
||||
val uri = FileProvider.getUriForFile(this, get<AppBuild>().id, file)
|
||||
|
||||
val shareIntent = Intent().apply {
|
||||
type = "*/*"
|
||||
|
@ -9,7 +9,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":di"))
|
||||
implementation(project(":core:di"))
|
||||
implementation(project(":prefs"))
|
||||
implementation(project(":resources"))
|
||||
|
||||
|
@ -13,24 +13,25 @@ android {
|
||||
dependencies {
|
||||
implementation(project(":base"))
|
||||
implementation(project(":core"))
|
||||
implementation(project(":di"))
|
||||
implementation(project(":core:common"))
|
||||
implementation(project(":core:di"))
|
||||
implementation(project(":design"))
|
||||
implementation(project(":extensions"))
|
||||
implementation(project(":permissions"))
|
||||
implementation(project(":prefs"))
|
||||
implementation(project(":persistence"))
|
||||
implementation(project(":network"))
|
||||
implementation(project(":push"))
|
||||
implementation(project(":qrscanner"))
|
||||
implementation(project(":environment"))
|
||||
|
||||
implementation(project(":navigation"))
|
||||
implementation(project(":services:domain"))
|
||||
implementation(project(":time:domain"))
|
||||
implementation(project(":serialization"))
|
||||
implementation(project(":resources"))
|
||||
implementation(project(":core:designsystem"))
|
||||
implementation(project(":core:locale"))
|
||||
implementation(project(":security:domain"))
|
||||
implementation(project(":browserextension:domain"))
|
||||
|
||||
implementation(project(":data:browserext"))
|
||||
implementation(libs.bundles.fastAdapter)
|
||||
implementation(libs.bundles.rxJava)
|
||||
implementation(libs.bundles.appCompat)
|
||||
|
@ -1,17 +0,0 @@
|
||||
@Suppress("DSL_SCOPE_VIOLATION")
|
||||
plugins {
|
||||
alias(libs.plugins.twofasAndroidLibrary)
|
||||
alias(libs.plugins.kotlinParcelize)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.twofasapp.browserextension.domain"
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation(project(":base"))
|
||||
implementation(project(":time:domain"))
|
||||
implementation(libs.bundles.rxJava)
|
||||
implementation(libs.kotlinCoroutines)
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.twofasapp.browserextension.domain">
|
||||
|
||||
</manifest>
|
@ -1,7 +0,0 @@
|
||||
package com.twofasapp.browserextension.domain
|
||||
|
||||
import com.twofasapp.browserextension.domain.model.TokenRequest
|
||||
|
||||
interface FetchTokenRequestsCase {
|
||||
suspend operator fun invoke(extensionId: String): List<TokenRequest>
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package com.twofasapp.browserextension.domain
|
||||
|
||||
import com.twofasapp.browserextension.domain.model.MobileDevice
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ObserveMobileDeviceCase {
|
||||
operator fun invoke(): Flow<MobileDevice>
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package com.twofasapp.browserextension.domain
|
||||
|
||||
import com.twofasapp.browserextension.domain.model.PairedBrowser
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ObservePairedBrowsersCase {
|
||||
operator fun invoke(): Flow<List<PairedBrowser>>
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package com.twofasapp.browserextension.domain.model
|
||||
|
||||
import com.twofasapp.time.domain.formatter.TimeFormatter
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
|
||||
data class PairedBrowser(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val pairedAt: Instant,
|
||||
val extensionPublicKey: String,
|
||||
) {
|
||||
fun formatPairedAt(): String {
|
||||
return pairedAt.atOffset(ZoneOffset.UTC).format(TimeFormatter.fullDate)
|
||||
}
|
||||
}
|
@ -1,20 +1,20 @@
|
||||
package com.twofasapp.browserextension
|
||||
|
||||
import com.twofasapp.browserextension.data.BrowserExtensionLocalData
|
||||
import com.twofasapp.browserextension.data.BrowserExtensionLocalDataImpl
|
||||
import com.twofasapp.browserextension.data.BrowserExtensionRemoteData
|
||||
import com.twofasapp.browserextension.data.BrowserExtensionRemoteDataImpl
|
||||
import com.twofasapp.browserextension.domain.*
|
||||
import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository
|
||||
import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepositoryImpl
|
||||
import com.twofasapp.browserextension.domain.ApproveLoginRequestCase
|
||||
import com.twofasapp.browserextension.domain.DeletePairedBrowserCase
|
||||
import com.twofasapp.browserextension.domain.DenyLoginRequestCase
|
||||
import com.twofasapp.browserextension.domain.EncryptCodeCase
|
||||
import com.twofasapp.browserextension.domain.FetchPairedBrowsersCase
|
||||
import com.twofasapp.browserextension.domain.FetchTokenRequestsCase
|
||||
import com.twofasapp.browserextension.domain.ObserveMobileDeviceCase
|
||||
import com.twofasapp.browserextension.domain.ObservePairedBrowsersCase
|
||||
import com.twofasapp.browserextension.domain.PairBrowserCase
|
||||
import com.twofasapp.browserextension.domain.RegisterMobileDeviceCase
|
||||
import com.twofasapp.browserextension.domain.UpdateMobileDeviceCase
|
||||
import com.twofasapp.browserextension.notification.ShowBrowserExtensionRequestNotificationCaseImpl
|
||||
import com.twofasapp.browserextension.ui.browser.BrowserDetailsScreenFactory
|
||||
import com.twofasapp.browserextension.ui.browser.BrowserDetailsViewModel
|
||||
import com.twofasapp.browserextension.ui.main.BrowserExtensionScreenFactory
|
||||
import com.twofasapp.browserextension.ui.main.BrowserExtensionViewModel
|
||||
import com.twofasapp.browserextension.ui.pairing.progress.PairingProgressScreenFactory
|
||||
import com.twofasapp.browserextension.ui.pairing.progress.PairingProgressViewModel
|
||||
import com.twofasapp.browserextension.ui.pairing.scan.PairingScanScreenFactory
|
||||
import com.twofasapp.browserextension.ui.pairing.scan.PairingScanViewModel
|
||||
import com.twofasapp.browserextension.ui.request.BrowserExtensionRequestViewModel
|
||||
import com.twofasapp.di.KoinModule
|
||||
@ -27,19 +27,15 @@ import org.koin.dsl.module
|
||||
class BrowserExtensionModule : KoinModule {
|
||||
|
||||
override fun provide() = module {
|
||||
singleOf(::BrowserExtensionLocalDataImpl) { bind<BrowserExtensionLocalData>() }
|
||||
singleOf(::BrowserExtensionRemoteDataImpl) { bind<BrowserExtensionRemoteData>() }
|
||||
singleOf(::BrowserExtensionRepositoryImpl) { bind<BrowserExtensionRepository>() }
|
||||
|
||||
singleOf(::ShowBrowserExtensionRequestNotificationCaseImpl) { bind<ShowBrowserExtensionRequestNotificationCase>() }
|
||||
|
||||
singleOf(::RegisterMobileDeviceCase)
|
||||
singleOf(::PairBrowserCase)
|
||||
singleOf(::ObserveMobileDeviceCaseImpl) { bind<ObserveMobileDeviceCase>() }
|
||||
singleOf(::ObserveMobileDeviceCase)
|
||||
singleOf(::UpdateMobileDeviceCase)
|
||||
singleOf(::ObservePairedBrowsersCaseImpl) { bind<ObservePairedBrowsersCase>() }
|
||||
singleOf(::ObservePairedBrowsersCase)
|
||||
singleOf(::FetchPairedBrowsersCase)
|
||||
singleOf(::FetchTokenRequestsCaseImpl) { bind<FetchTokenRequestsCase>() }
|
||||
singleOf(::FetchTokenRequestsCase)
|
||||
singleOf(::ApproveLoginRequestCase)
|
||||
singleOf(::DenyLoginRequestCase)
|
||||
singleOf(::EncryptCodeCase)
|
||||
@ -50,10 +46,5 @@ class BrowserExtensionModule : KoinModule {
|
||||
viewModelOf(::PairingProgressViewModel)
|
||||
viewModelOf(::BrowserExtensionRequestViewModel)
|
||||
viewModelOf(::BrowserDetailsViewModel)
|
||||
|
||||
singleOf(::BrowserExtensionScreenFactory)
|
||||
singleOf(::PairingProgressScreenFactory)
|
||||
singleOf(::PairingScanScreenFactory)
|
||||
singleOf(::BrowserDetailsScreenFactory)
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package com.twofasapp.browserextension.data
|
||||
|
||||
import com.twofasapp.browserextension.domain.model.MobileDevice
|
||||
import com.twofasapp.browserextension.domain.model.PairedBrowser
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal interface BrowserExtensionLocalData {
|
||||
fun observeMobileDevice(): Flow<MobileDevice>
|
||||
fun observePairedBrowsers(): Flow<List<PairedBrowser>>
|
||||
suspend fun saveMobileDevice(mobileDevice: MobileDevice)
|
||||
suspend fun savePairedBrowser(pairedBrowser: PairedBrowser)
|
||||
suspend fun updatePairedBrowsers(pairedBrowsers: List<PairedBrowser>)
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
package com.twofasapp.browserextension.data
|
||||
|
||||
import com.twofasapp.browserextension.domain.model.MobileDevice
|
||||
import com.twofasapp.browserextension.domain.model.PairedBrowser
|
||||
import com.twofasapp.persistence.dao.PairedBrowserDao
|
||||
import com.twofasapp.persistence.model.PairedBrowserEntity
|
||||
import com.twofasapp.prefs.model.MobileDeviceEntity
|
||||
import com.twofasapp.prefs.usecase.MobileDevicePreference
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.time.Instant
|
||||
|
||||
internal class BrowserExtensionLocalDataImpl(
|
||||
private val mobileDevicePreference: MobileDevicePreference,
|
||||
private val dao: PairedBrowserDao,
|
||||
) : BrowserExtensionLocalData {
|
||||
|
||||
override fun observeMobileDevice(): Flow<MobileDevice> {
|
||||
return mobileDevicePreference.flow().map { it.toDomain() }
|
||||
}
|
||||
|
||||
override fun observePairedBrowsers(): Flow<List<PairedBrowser>> {
|
||||
return dao.observe()
|
||||
.map { list -> list.map { it.toDomain() } }
|
||||
}
|
||||
|
||||
override suspend fun saveMobileDevice(mobileDevice: MobileDevice) {
|
||||
mobileDevicePreference.put(mobileDevice.toEntity())
|
||||
}
|
||||
|
||||
override suspend fun savePairedBrowser(pairedBrowser: PairedBrowser) {
|
||||
dao.insertOrUpdate(pairedBrowser.toEntity())
|
||||
}
|
||||
|
||||
override suspend fun updatePairedBrowsers(pairedBrowsers: List<PairedBrowser>) {
|
||||
dao.updateAll(pairedBrowsers.map { it.toEntity() })
|
||||
}
|
||||
|
||||
private fun MobileDevice.toEntity() =
|
||||
MobileDeviceEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
fcmToken = fcmToken,
|
||||
platform = platform,
|
||||
publicKey = publicKey,
|
||||
)
|
||||
|
||||
private fun MobileDeviceEntity.toDomain() =
|
||||
MobileDevice(
|
||||
id = id,
|
||||
name = name,
|
||||
fcmToken = fcmToken,
|
||||
platform = platform,
|
||||
publicKey = publicKey,
|
||||
)
|
||||
|
||||
private fun PairedBrowser.toEntity() =
|
||||
PairedBrowserEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
extensionPublicKey = extensionPublicKey,
|
||||
pairedAt = pairedAt.toEpochMilli(),
|
||||
)
|
||||
|
||||
private fun PairedBrowserEntity.toDomain() =
|
||||
PairedBrowser(
|
||||
id = id,
|
||||
name = name,
|
||||
pairedAt = Instant.ofEpochMilli(pairedAt),
|
||||
extensionPublicKey = extensionPublicKey,
|
||||
)
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package com.twofasapp.browserextension.data
|
||||
|
||||
import com.twofasapp.browserextension.domain.model.MobileDevice
|
||||
import com.twofasapp.browserextension.domain.model.PairedBrowser
|
||||
import com.twofasapp.browserextension.domain.model.TokenRequest
|
||||
|
||||
internal interface BrowserExtensionRemoteData {
|
||||
suspend fun registerMobileDevice(
|
||||
deviceName: String,
|
||||
devicePublicKey: String,
|
||||
fcmToken: String,
|
||||
platform: String,
|
||||
): MobileDevice
|
||||
|
||||
suspend fun updateMobileDevice(
|
||||
deviceId: String,
|
||||
newName: String
|
||||
)
|
||||
|
||||
suspend fun pairBrowser(
|
||||
deviceId: String,
|
||||
extensionId: String,
|
||||
deviceName: String,
|
||||
devicePublicKey: String,
|
||||
): PairedBrowser
|
||||
|
||||
suspend fun updatePairedBrowser(
|
||||
extensionId: String,
|
||||
newName: String
|
||||
)
|
||||
|
||||
suspend fun deletePairedBrowser(
|
||||
deviceId: String,
|
||||
extensionId: String
|
||||
)
|
||||
|
||||
suspend fun getBrowsers(
|
||||
deviceId: String,
|
||||
): List<PairedBrowser>
|
||||
|
||||
suspend fun acceptLoginRequest(
|
||||
deviceId: String,
|
||||
extensionId: String,
|
||||
requestId: String,
|
||||
code: String
|
||||
)
|
||||
|
||||
suspend fun denyLoginRequest(
|
||||
extensionId: String,
|
||||
requestId: String,
|
||||
)
|
||||
|
||||
suspend fun fetchTokenRequests(
|
||||
extensionId: String,
|
||||
): List<TokenRequest>
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
package com.twofasapp.browserextension.data
|
||||
|
||||
import com.twofasapp.browserextension.domain.model.MobileDevice
|
||||
import com.twofasapp.browserextension.domain.model.PairedBrowser
|
||||
import com.twofasapp.browserextension.domain.model.TokenRequest
|
||||
import com.twofasapp.network.api.BrowserExtensionApi
|
||||
import com.twofasapp.network.body.ApproveLoginRequestBody
|
||||
import com.twofasapp.network.body.DeviceRegisterBody
|
||||
import com.twofasapp.network.body.PairBrowserBody
|
||||
import java.time.Instant
|
||||
|
||||
internal class BrowserExtensionRemoteDataImpl(
|
||||
private val api: BrowserExtensionApi,
|
||||
) : BrowserExtensionRemoteData {
|
||||
|
||||
override suspend fun registerMobileDevice(deviceName: String, devicePublicKey: String, fcmToken: String, platform: String): MobileDevice {
|
||||
val response = api.registerMobileDevice(
|
||||
DeviceRegisterBody(
|
||||
name = deviceName,
|
||||
fcm_token = fcmToken,
|
||||
platform = platform,
|
||||
)
|
||||
)
|
||||
|
||||
return MobileDevice(
|
||||
id = response.id,
|
||||
name = response.name,
|
||||
fcmToken = fcmToken,
|
||||
platform = response.platform,
|
||||
publicKey = devicePublicKey,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun updateMobileDevice(deviceId: String, newName: String) {
|
||||
api.updateMobileDevice(deviceId, newName)
|
||||
}
|
||||
|
||||
override suspend fun pairBrowser(deviceId: String, extensionId: String, deviceName: String, devicePublicKey: String): PairedBrowser {
|
||||
val pairResponse = api.pairBrowser(
|
||||
deviceId = deviceId,
|
||||
body = PairBrowserBody(
|
||||
extension_id = extensionId,
|
||||
device_name = deviceName,
|
||||
device_public_key = devicePublicKey,
|
||||
)
|
||||
)
|
||||
|
||||
val browserResponse = api.getBrowser(deviceId = deviceId, extensionId = extensionId)
|
||||
|
||||
return PairedBrowser(
|
||||
id = browserResponse.id,
|
||||
name = browserResponse.name,
|
||||
pairedAt = Instant.parse(browserResponse.paired_at),
|
||||
extensionPublicKey = pairResponse.extension_public_key
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun updatePairedBrowser(extensionId: String, newName: String) {
|
||||
api.updateBrowserName(extensionId, newName)
|
||||
}
|
||||
|
||||
override suspend fun deletePairedBrowser(deviceId: String, extensionId: String) {
|
||||
api.deletePairedBrowser(deviceId, extensionId)
|
||||
}
|
||||
|
||||
override suspend fun getBrowsers(deviceId: String): List<PairedBrowser> {
|
||||
return api.getBrowsers(deviceId).map {
|
||||
PairedBrowser(
|
||||
id = it.id,
|
||||
name = it.name,
|
||||
pairedAt = Instant.parse(it.paired_at),
|
||||
extensionPublicKey = "",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun acceptLoginRequest(deviceId: String, extensionId: String, requestId: String, code: String) {
|
||||
api.acceptLoginRequest(
|
||||
deviceId = deviceId,
|
||||
body = ApproveLoginRequestBody(
|
||||
extension_id = extensionId,
|
||||
token_request_id = requestId,
|
||||
token = code,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun denyLoginRequest(extensionId: String, requestId: String) {
|
||||
api.denyLoginRequest(
|
||||
extensionId = extensionId,
|
||||
tokenRequestId = requestId,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun fetchTokenRequests(deviceId: String): List<TokenRequest> {
|
||||
return api.fetchTokenRequests(deviceId)
|
||||
.filter { it.status.equals("pending", true) }
|
||||
.map {
|
||||
TokenRequest(
|
||||
domain = it.domain,
|
||||
requestId = it.token_request_id,
|
||||
extensionId = it.extension_id,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
package com.twofasapp.browserextension.domain
|
||||
|
||||
import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository
|
||||
import com.twofasapp.data.browserext.BrowserExtRepository
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
internal class ApproveLoginRequestCase(
|
||||
private val browserExtensionRepository: BrowserExtensionRepository,
|
||||
class ApproveLoginRequestCase(
|
||||
private val browserExtensionRepository: BrowserExtRepository,
|
||||
private val observeMobileDeviceCase: ObserveMobileDeviceCase,
|
||||
private val observePairedBrowsersCase: ObservePairedBrowsersCase,
|
||||
private val encryptCodeCase: EncryptCodeCase,
|
||||
|
@ -1,11 +1,11 @@
|
||||
package com.twofasapp.browserextension.domain
|
||||
|
||||
import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository
|
||||
import com.twofasapp.data.browserext.BrowserExtRepository
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
internal class DeletePairedBrowserCase(
|
||||
class DeletePairedBrowserCase(
|
||||
private val observeMobileDeviceCase: ObserveMobileDeviceCase,
|
||||
private val browserExtensionRepository: BrowserExtensionRepository,
|
||||
private val browserExtensionRepository: BrowserExtRepository,
|
||||
) {
|
||||
suspend operator fun invoke(extensionId: String) {
|
||||
return browserExtensionRepository.deletePairedBrowser(
|
||||
|
@ -1,9 +1,9 @@
|
||||
package com.twofasapp.browserextension.domain
|
||||
|
||||
import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository
|
||||
import com.twofasapp.data.browserext.BrowserExtRepository
|
||||
|
||||
internal class DenyLoginRequestCase(
|
||||
private val browserExtensionRepository: BrowserExtensionRepository,
|
||||
class DenyLoginRequestCase(
|
||||
private val browserExtensionRepository: BrowserExtRepository,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
extensionId: String,
|
||||
|
@ -1,9 +1,9 @@
|
||||
package com.twofasapp.browserextension.domain
|
||||
|
||||
import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository
|
||||
import com.twofasapp.data.browserext.BrowserExtRepository
|
||||
|
||||
internal class FetchPairedBrowsersCase(
|
||||
private val browserExtensionRepository: BrowserExtensionRepository
|
||||
class FetchPairedBrowsersCase(
|
||||
private val browserExtensionRepository: BrowserExtRepository
|
||||
) {
|
||||
|
||||
suspend operator fun invoke() {
|
||||
|
@ -0,0 +1,15 @@
|
||||
package com.twofasapp.browserextension.domain
|
||||
|
||||
import com.twofasapp.data.browserext.BrowserExtRepository
|
||||
import com.twofasapp.data.browserext.domain.TokenRequest
|
||||
|
||||
class FetchTokenRequestsCase(
|
||||
private val browserExtensionRepository: BrowserExtRepository
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(deviceId: String): List<TokenRequest> {
|
||||
if (deviceId.isBlank()) return emptyList()
|
||||
|
||||
return browserExtensionRepository.fetchTokenRequests(deviceId)
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package com.twofasapp.browserextension.domain
|
||||
|
||||
import com.twofasapp.browserextension.domain.model.TokenRequest
|
||||
import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository
|
||||
|
||||
internal class FetchTokenRequestsCaseImpl(
|
||||
private val browserExtensionRepository: BrowserExtensionRepository
|
||||
) : FetchTokenRequestsCase {
|
||||
|
||||
override suspend operator fun invoke(deviceId: String): List<TokenRequest> {
|
||||
if(deviceId.isBlank()) return emptyList()
|
||||
|
||||
return browserExtensionRepository.fetchTokenRequests(deviceId)
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.twofasapp.browserextension.domain
|
||||
|
||||
import com.twofasapp.data.browserext.BrowserExtRepository
|
||||
import com.twofasapp.data.browserext.domain.MobileDevice
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class ObserveMobileDeviceCase(
|
||||
private val browserExtensionRepository: BrowserExtRepository
|
||||
) {
|
||||
|
||||
operator fun invoke(): Flow<MobileDevice> {
|
||||
return browserExtensionRepository.observeMobileDevice()
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package com.twofasapp.browserextension.domain
|
||||
|
||||
import com.twofasapp.browserextension.domain.model.MobileDevice
|
||||
import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal class ObserveMobileDeviceCaseImpl(
|
||||
private val browserExtensionRepository: BrowserExtensionRepository
|
||||
) : ObserveMobileDeviceCase {
|
||||
|
||||
override operator fun invoke(): Flow<MobileDevice> {
|
||||
return browserExtensionRepository.observeMobileDevice()
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.twofasapp.browserextension.domain
|
||||
|
||||
import com.twofasapp.data.browserext.BrowserExtRepository
|
||||
import com.twofasapp.data.browserext.domain.PairedBrowser
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class ObservePairedBrowsersCase(
|
||||
private val browserExtensionRepository: BrowserExtRepository
|
||||
) {
|
||||
|
||||
operator fun invoke(): Flow<List<PairedBrowser>> {
|
||||
return browserExtensionRepository.observePairedBrowsers()
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package com.twofasapp.browserextension.domain
|
||||
|
||||
import com.twofasapp.browserextension.domain.model.PairedBrowser
|
||||
import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal class ObservePairedBrowsersCaseImpl(
|
||||
private val browserExtensionRepository: BrowserExtensionRepository
|
||||
) : ObservePairedBrowsersCase {
|
||||
|
||||
override operator fun invoke(): Flow<List<PairedBrowser>> {
|
||||
return browserExtensionRepository.observePairedBrowsers()
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
package com.twofasapp.browserextension.domain
|
||||
|
||||
import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository
|
||||
import com.twofasapp.data.browserext.BrowserExtRepository
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
internal class PairBrowserCase(
|
||||
private val browserExtensionRepository: BrowserExtensionRepository,
|
||||
class PairBrowserCase(
|
||||
private val browserExtensionRepository: BrowserExtRepository,
|
||||
private val observeMobileDeviceCase: ObserveMobileDeviceCase,
|
||||
) {
|
||||
|
||||
|
@ -2,21 +2,21 @@ package com.twofasapp.browserextension.domain
|
||||
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import com.twofasapp.browserextension.domain.model.MobileDevice
|
||||
import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository
|
||||
import com.twofasapp.common.environment.AppBuild
|
||||
import com.twofasapp.core.encoding.encodeBase64ToString
|
||||
import com.twofasapp.environment.AppConfig
|
||||
import com.twofasapp.data.browserext.BrowserExtRepository
|
||||
import com.twofasapp.data.browserext.domain.MobileDevice
|
||||
import com.twofasapp.push.domain.GetFcmTokenCase
|
||||
import kotlinx.coroutines.flow.first
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
|
||||
internal class RegisterMobileDeviceCase(
|
||||
private val browserExtensionRepository: BrowserExtensionRepository,
|
||||
class RegisterMobileDeviceCase(
|
||||
private val browserExtensionRepository: BrowserExtRepository,
|
||||
private val observeMobileDeviceCase: ObserveMobileDeviceCase,
|
||||
private val getFcmTokenCase: GetFcmTokenCase,
|
||||
private val appConfig: AppConfig,
|
||||
private val appBuild: AppBuild,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@ -31,7 +31,7 @@ internal class RegisterMobileDeviceCase(
|
||||
mobileDevice
|
||||
} else {
|
||||
browserExtensionRepository.registerMobileDevice(
|
||||
deviceName = mobileDevice.name.ifBlank { appConfig.deviceName },
|
||||
deviceName = mobileDevice.name.ifBlank { appBuild.deviceName },
|
||||
devicePublicKey = createDevicePublicKey(),
|
||||
fcmToken = fcmToken,
|
||||
)
|
||||
|
@ -1,10 +1,10 @@
|
||||
package com.twofasapp.browserextension.domain
|
||||
|
||||
import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository
|
||||
import com.twofasapp.data.browserext.BrowserExtRepository
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
internal class UpdateMobileDeviceCase(
|
||||
private val browserExtensionRepository: BrowserExtensionRepository
|
||||
class UpdateMobileDeviceCase(
|
||||
private val browserExtensionRepository: BrowserExtRepository
|
||||
) {
|
||||
|
||||
data class Params(
|
||||
|
@ -1,84 +0,0 @@
|
||||
package com.twofasapp.browserextension.domain.repository
|
||||
|
||||
import com.twofasapp.browserextension.data.BrowserExtensionLocalData
|
||||
import com.twofasapp.browserextension.data.BrowserExtensionRemoteData
|
||||
import com.twofasapp.browserextension.domain.model.MobileDevice
|
||||
import com.twofasapp.browserextension.domain.model.PairedBrowser
|
||||
import com.twofasapp.browserextension.domain.model.TokenRequest
|
||||
import com.twofasapp.extensions.ifNotBlank
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
internal class BrowserExtensionRepositoryImpl(
|
||||
private val localData: BrowserExtensionLocalData,
|
||||
private val remoteData: BrowserExtensionRemoteData,
|
||||
) : BrowserExtensionRepository {
|
||||
|
||||
companion object {
|
||||
private const val PLATFORM = "android"
|
||||
}
|
||||
|
||||
override fun observeMobileDevice(): Flow<MobileDevice> {
|
||||
return localData.observeMobileDevice()
|
||||
}
|
||||
|
||||
override fun observePairedBrowsers(): Flow<List<PairedBrowser>> {
|
||||
return localData.observePairedBrowsers()
|
||||
}
|
||||
|
||||
override suspend fun updateMobileDevice(mobileDevice: MobileDevice) {
|
||||
remoteData.updateMobileDevice(mobileDevice.id, mobileDevice.name)
|
||||
localData.saveMobileDevice(mobileDevice)
|
||||
}
|
||||
|
||||
override suspend fun registerMobileDevice(deviceName: String, devicePublicKey: String, fcmToken: String): MobileDevice {
|
||||
val mobileDevice = remoteData.registerMobileDevice(
|
||||
deviceName = deviceName,
|
||||
devicePublicKey = devicePublicKey,
|
||||
fcmToken = fcmToken,
|
||||
platform = PLATFORM,
|
||||
)
|
||||
|
||||
localData.saveMobileDevice(mobileDevice)
|
||||
return mobileDevice
|
||||
}
|
||||
|
||||
override suspend fun pairBrowser(deviceId: String, extensionId: String, deviceName: String, devicePublicKey: String): PairedBrowser {
|
||||
val browser = remoteData.pairBrowser(
|
||||
deviceId = deviceId,
|
||||
extensionId = extensionId,
|
||||
deviceName = deviceName,
|
||||
devicePublicKey = devicePublicKey
|
||||
)
|
||||
localData.savePairedBrowser(browser)
|
||||
return browser
|
||||
}
|
||||
|
||||
override suspend fun updatePairedBrowser(extensionId: String, newName: String) {
|
||||
remoteData.updatePairedBrowser(extensionId = extensionId, newName = newName)
|
||||
fetchPairedBrowsers()
|
||||
}
|
||||
|
||||
override suspend fun fetchPairedBrowsers() {
|
||||
localData.observeMobileDevice().first().id.ifNotBlank { id ->
|
||||
localData.updatePairedBrowsers(remoteData.getBrowsers(id))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun fetchTokenRequests(deviceId: String): List<TokenRequest> {
|
||||
return remoteData.fetchTokenRequests(deviceId)
|
||||
}
|
||||
|
||||
override suspend fun deletePairedBrowser(deviceId: String, extensionId: String) {
|
||||
remoteData.deletePairedBrowser(deviceId, extensionId)
|
||||
fetchPairedBrowsers()
|
||||
}
|
||||
|
||||
override suspend fun acceptLoginRequest(deviceId: String, extensionId: String, requestId: String, code: String) {
|
||||
return remoteData.acceptLoginRequest(deviceId, extensionId, requestId, code)
|
||||
}
|
||||
|
||||
override suspend fun denyLoginRequest(extensionId: String, requestId: String) {
|
||||
return remoteData.denyLoginRequest(extensionId, requestId)
|
||||
}
|
||||
}
|
@ -4,42 +4,39 @@ import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.twofasapp.resources.R
|
||||
import com.twofasapp.design.compose.SimpleEntry
|
||||
import com.twofasapp.design.compose.Toolbar
|
||||
import com.twofasapp.design.compose.dialogs.ConfirmDialog
|
||||
import com.twofasapp.design.compose.dialogs.InputDialog
|
||||
import com.twofasapp.design.theme.divider
|
||||
import com.twofasapp.navigation.SettingsDirections
|
||||
import com.twofasapp.navigation.SettingsRouter
|
||||
import com.twofasapp.designsystem.TwTheme
|
||||
import com.twofasapp.designsystem.common.TwTopAppBar
|
||||
import com.twofasapp.resources.R
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.get
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@Composable
|
||||
internal fun BrowserDetailsScreen(
|
||||
fun BrowserDetailsScreen(
|
||||
onFinish: () -> Unit,
|
||||
extensionId: String,
|
||||
viewModel: BrowserDetailsViewModel = get(),
|
||||
router: SettingsRouter = get(),
|
||||
viewModel: BrowserDetailsViewModel = koinViewModel(),
|
||||
) {
|
||||
viewModel.init(extensionId)
|
||||
val uiState = viewModel.uiState.collectAsState().value
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
val scope = rememberCoroutineScope()
|
||||
viewModel.onFinish = onFinish
|
||||
|
||||
Scaffold(
|
||||
scaffoldState = scaffoldState,
|
||||
// scaffoldState = scaffoldState,
|
||||
topBar = {
|
||||
Toolbar(title = stringResource(id = R.string.browser__browser_extension)) {
|
||||
router.navigate(SettingsDirections.GoBack)
|
||||
}
|
||||
TwTopAppBar(titleText = stringResource(id = R.string.browser__browser_extension))
|
||||
}
|
||||
) { padding ->
|
||||
LazyColumn(modifier = Modifier.padding(padding)) {
|
||||
@ -56,13 +53,13 @@ internal fun BrowserDetailsScreen(
|
||||
subtitle = uiState.browserPairedAt,
|
||||
)
|
||||
}
|
||||
item { Divider(color = MaterialTheme.colors.divider, modifier = Modifier.padding(vertical = 8.dp)) }
|
||||
item { Divider(color = TwTheme.color.divider, modifier = Modifier.padding(vertical = 8.dp)) }
|
||||
item {
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.showConfirmForget() },
|
||||
shape = CircleShape,
|
||||
modifier = Modifier.padding(start = 72.dp, top = 6.dp, bottom = 2.dp),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colors.primary)
|
||||
border = BorderStroke(1.dp, TwTheme.color.primary)
|
||||
) {
|
||||
Text(text = "Forget this web browser")
|
||||
}
|
||||
@ -86,8 +83,8 @@ internal fun BrowserDetailsScreen(
|
||||
when (it) {
|
||||
is BrowserDetailsUiState.Event.ShowSnackbarError -> {
|
||||
scope.launch {
|
||||
scaffoldState.snackbarHostState.currentSnackbarData?.dismiss()
|
||||
scaffoldState.snackbarHostState.showSnackbar(it.message)
|
||||
// scaffoldState.snackbarHostState.currentSnackbarData?.dismiss()
|
||||
// scaffoldState.snackbarHostState.showSnackbar(it.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
package com.twofasapp.browserextension.ui.browser
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.twofasapp.browserextension.ui.pairing.scan.PairingScanScreen
|
||||
|
||||
class BrowserDetailsScreenFactory {
|
||||
|
||||
@Composable
|
||||
fun create(extensionId: String) {
|
||||
BrowserDetailsScreen(extensionId)
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ package com.twofasapp.browserextension.ui.browser
|
||||
import com.twofasapp.base.UiEvent
|
||||
import com.twofasapp.base.UiState
|
||||
|
||||
internal data class BrowserDetailsUiState(
|
||||
data class BrowserDetailsUiState(
|
||||
val extensionId: String = "",
|
||||
val browserName: String = "",
|
||||
val browserPairedAt: String = "",
|
||||
|
@ -5,26 +5,26 @@ import com.twofasapp.base.BaseViewModel
|
||||
import com.twofasapp.base.dispatcher.Dispatchers
|
||||
import com.twofasapp.browserextension.domain.DeletePairedBrowserCase
|
||||
import com.twofasapp.browserextension.domain.ObservePairedBrowsersCase
|
||||
import com.twofasapp.navigation.SettingsDirections
|
||||
import com.twofasapp.navigation.SettingsRouter
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class BrowserDetailsViewModel(
|
||||
class BrowserDetailsViewModel(
|
||||
private val dispatchers: Dispatchers,
|
||||
private val observePairedBrowsersCase: ObservePairedBrowsersCase,
|
||||
private val deletePairedBrowserCase: DeletePairedBrowserCase,
|
||||
private val settingsRouter: SettingsRouter,
|
||||
) : BaseViewModel() {
|
||||
|
||||
var onFinish: () -> Unit = {}
|
||||
|
||||
private val _uiState = MutableStateFlow(BrowserDetailsUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
fun init(extensionId: String) {
|
||||
viewModelScope.launch {
|
||||
viewModelScope.launch(dispatchers.io()) {
|
||||
observePairedBrowsersCase().flowOn(dispatchers.io()).collect { list ->
|
||||
val browser = list.find { it.id == extensionId }
|
||||
|
||||
@ -33,7 +33,7 @@ internal class BrowserDetailsViewModel(
|
||||
it.copy(
|
||||
extensionId = extensionId,
|
||||
browserName = browser.name,
|
||||
browserPairedAt = browser.formatPairedAt(),
|
||||
// browserPairedAt = browser.formatPairedAt(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -54,7 +54,7 @@ internal class BrowserDetailsViewModel(
|
||||
viewModelScope.launch(dispatchers.io()) {
|
||||
runSafely(catch = { postError() }) {
|
||||
deletePairedBrowserCase(uiState.value.extensionId)
|
||||
settingsRouter.navigate(SettingsDirections.GoBack)
|
||||
onFinish() // TODO: FIX
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,21 @@
|
||||
package com.twofasapp.browserextension.ui.main
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
@ -22,39 +23,51 @@ import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import com.twofasapp.resources.R
|
||||
import com.twofasapp.design.compose.*
|
||||
import com.twofasapp.design.compose.SimpleEntry
|
||||
import com.twofasapp.design.compose.dialogs.InputDialog
|
||||
import com.twofasapp.design.compose.dialogs.RationaleDialog
|
||||
import com.twofasapp.extensions.openBrowserApp
|
||||
import com.twofasapp.navigation.SettingsDirections
|
||||
import com.twofasapp.navigation.SettingsRouter
|
||||
import com.twofasapp.designsystem.TwTheme
|
||||
import com.twofasapp.designsystem.common.TwButton
|
||||
import com.twofasapp.designsystem.common.TwTopAppBar
|
||||
import com.twofasapp.designsystem.screen.CommonContent
|
||||
import com.twofasapp.designsystem.settings.SettingsHeader
|
||||
import com.twofasapp.designsystem.settings.SettingsLink
|
||||
import com.twofasapp.locale.TwLocale
|
||||
import com.twofasapp.resources.R
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.get
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@Composable
|
||||
internal fun BrowserExtensionScreen(
|
||||
viewModel: BrowserExtensionViewModel = get(),
|
||||
router: SettingsRouter = get(),
|
||||
fun BrowserExtensionScreen(
|
||||
openPairingScan: () -> Unit,
|
||||
openBrowserDetails: (String) -> Unit,
|
||||
viewModel: BrowserExtensionViewModel = koinViewModel(),
|
||||
) {
|
||||
val uiState = viewModel.uiState.collectAsState().value
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
viewModel.onPairClick = { openPairingScan() }
|
||||
Scaffold(
|
||||
scaffoldState = scaffoldState,
|
||||
topBar = { Toolbar(title = stringResource(id = R.string.browser__browser_extension)) { router.navigate(SettingsDirections.GoBack) } },
|
||||
topBar = { TwTopAppBar(titleText = TwLocale.strings.browserExtTitle) },
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
) { padding ->
|
||||
if (uiState.isLoading) return@Scaffold
|
||||
|
||||
if (uiState.pairedBrowsers.isEmpty()) {
|
||||
EmptyScreen(viewModel, padding)
|
||||
EmptyScreen(
|
||||
onPairBrowserClick = openPairingScan,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(16.dp)
|
||||
)
|
||||
} else {
|
||||
ContentScreen(
|
||||
onBrowserClick = openBrowserDetails,
|
||||
onPairBrowserClick = openPairingScan,
|
||||
viewModel = viewModel,
|
||||
uiState = uiState,
|
||||
router = router,
|
||||
padding = padding
|
||||
)
|
||||
}
|
||||
@ -64,8 +77,8 @@ internal fun BrowserExtensionScreen(
|
||||
when (it) {
|
||||
is BrowserExtensionUiState.Event.ShowSnackbarError -> {
|
||||
scope.launch {
|
||||
scaffoldState.snackbarHostState.currentSnackbarData?.dismiss()
|
||||
scaffoldState.snackbarHostState.showSnackbar(it.message)
|
||||
snackbarHostState.currentSnackbarData?.dismiss()
|
||||
snackbarHostState.showSnackbar(it.message)
|
||||
viewModel.eventHandled(it.id)
|
||||
}
|
||||
}
|
||||
@ -74,44 +87,37 @@ internal fun BrowserExtensionScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ContentScreen(
|
||||
private fun ContentScreen(
|
||||
onBrowserClick: (String) -> Unit,
|
||||
onPairBrowserClick: () -> Unit,
|
||||
viewModel: BrowserExtensionViewModel,
|
||||
uiState: BrowserExtensionUiState,
|
||||
router: SettingsRouter,
|
||||
padding: PaddingValues,
|
||||
) {
|
||||
LazyColumn(modifier = Modifier.padding(padding)) {
|
||||
item {
|
||||
HeaderEntry(text = stringResource(id = R.string.browser__paired_devices_browser_title))
|
||||
}
|
||||
item { SettingsHeader(TwLocale.strings.browserExtPairedDevices) }
|
||||
|
||||
items(uiState.pairedBrowsers, key = { it.id }) {
|
||||
SimpleEntry(
|
||||
title = it.name,
|
||||
iconVisibleWhenNotSet = true,
|
||||
subtitle = it.formatPairedAt(),
|
||||
click = { router.navigate(SettingsDirections.BrowserDetails(extensionId = it.id)) }
|
||||
SettingsLink(it.name, onClick = { onBrowserClick(it.id) })
|
||||
// subtitle = it.formatPairedAt(),
|
||||
}
|
||||
|
||||
item {
|
||||
TwButton(
|
||||
text = "+ ${TwLocale.strings.browserExtAddNew}",
|
||||
onClick = onPairBrowserClick,
|
||||
height = TwTheme.dimen.buttonHeightSmall,
|
||||
modifier = Modifier.padding(start = 72.dp, top = 6.dp, bottom = 2.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Button(
|
||||
onClick = { viewModel.onPairBrowserClick() },
|
||||
shape = ButtonShape(),
|
||||
modifier = Modifier.padding(start = 72.dp, top = 6.dp, bottom = 2.dp)
|
||||
) {
|
||||
Text(text = "+ Add new".uppercase(), color = ButtonTextColor())
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
HeaderEntry(text = stringResource(id = R.string.browser__this_device_name))
|
||||
}
|
||||
item { SettingsHeader(TwLocale.strings.browserExtDeviceName) }
|
||||
|
||||
// TODO
|
||||
item {
|
||||
SimpleEntry(
|
||||
title = uiState.mobileDevice?.name.orEmpty(),
|
||||
subtitle = stringResource(id = R.string.browser__this_device_footer),
|
||||
subtitle = TwLocale.strings.browserExtDeviceNameSubtitle,
|
||||
iconEnd = painterResource(id = R.drawable.ic_toolbar_edit),
|
||||
iconEndClick = { viewModel.onEditDeviceClick() },
|
||||
)
|
||||
@ -139,74 +145,34 @@ internal fun ContentScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun EmptyScreen(
|
||||
viewModel: BrowserExtensionViewModel,
|
||||
padding: PaddingValues,
|
||||
private fun EmptyScreen(
|
||||
onPairBrowserClick: () -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val activity = (LocalContext.current as? Activity)
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
ConstraintLayout(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)) {
|
||||
val (content, pair) = createRefs()
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.constrainAs(content) {
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(pair.top)
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(parent.end)
|
||||
}
|
||||
.padding(vertical = 16.dp)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.browser_extension_start_image),
|
||||
contentDescription = null,
|
||||
CommonContent(
|
||||
image = painterResource(id = R.drawable.browser_extension_start_image),
|
||||
titleText = TwLocale.strings.browserExtHeader,
|
||||
descriptionText = "${TwLocale.strings.browserExtBody1}\n${TwLocale.strings.browserExtBody2}",
|
||||
ctaPrimaryText = TwLocale.strings.browserExtCta,
|
||||
ctaPrimaryClick = onPairBrowserClick,
|
||||
description = {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
append("${TwLocale.strings.browserExtMore1} ")
|
||||
withStyle(style = SpanStyle(TwTheme.color.primary)) {
|
||||
append(TwLocale.strings.browserExtMore2)
|
||||
}
|
||||
},
|
||||
style = TwTheme.typo.body2,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.height(130.dp)
|
||||
.offset(y = (-16).dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 16.dp)
|
||||
.clickable { uriHandler.openUri(TwLocale.links.browserExt) },
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "2FAS Web Browser extension",
|
||||
style = MaterialTheme.typography.h6,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "1. Install the 2FAS browser extension on your desktop computer.\n2. Pair it with your 2FAS app.",
|
||||
style = MaterialTheme.typography.body1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
|
||||
Text(text = buildAnnotatedString {
|
||||
append("More info: ")
|
||||
withStyle(style = SpanStyle(MaterialTheme.colors.primary)) {
|
||||
append("2fas.com/be")
|
||||
}
|
||||
}, style = MaterialTheme.typography.body2, modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.align(CenterHorizontally)
|
||||
.clickable {
|
||||
activity?.openBrowserApp(url = "https://2fas.com/be")
|
||||
}, textAlign = TextAlign.Center)
|
||||
}
|
||||
|
||||
Button(onClick = { viewModel.onPairBrowserClick() },
|
||||
shape = ButtonShape(),
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.constrainAs(pair) {
|
||||
bottom.linkTo(parent.bottom, margin = 16.dp)
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(parent.end)
|
||||
}) {
|
||||
Text(text = "Pair with web browser".uppercase(), color = ButtonTextColor())
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package com.twofasapp.browserextension.ui.main
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
class BrowserExtensionScreenFactory {
|
||||
|
||||
@Composable
|
||||
fun create() {
|
||||
BrowserExtensionScreen()
|
||||
}
|
||||
}
|
@ -2,10 +2,10 @@ package com.twofasapp.browserextension.ui.main
|
||||
|
||||
import com.twofasapp.base.UiEvent
|
||||
import com.twofasapp.base.UiState
|
||||
import com.twofasapp.browserextension.domain.model.MobileDevice
|
||||
import com.twofasapp.browserextension.domain.model.PairedBrowser
|
||||
import com.twofasapp.data.browserext.domain.MobileDevice
|
||||
import com.twofasapp.data.browserext.domain.PairedBrowser
|
||||
|
||||
internal data class BrowserExtensionUiState(
|
||||
data class BrowserExtensionUiState(
|
||||
val isLoading: Boolean = true,
|
||||
val pairedBrowsers: List<PairedBrowser> = emptyList(),
|
||||
val mobileDevice: MobileDevice? = null,
|
||||
|
@ -7,16 +7,20 @@ import com.twofasapp.browserextension.domain.FetchPairedBrowsersCase
|
||||
import com.twofasapp.browserextension.domain.ObserveMobileDeviceCase
|
||||
import com.twofasapp.browserextension.domain.ObservePairedBrowsersCase
|
||||
import com.twofasapp.browserextension.domain.UpdateMobileDeviceCase
|
||||
import com.twofasapp.navigation.SettingsDirections
|
||||
import com.twofasapp.navigation.SettingsRouter
|
||||
import com.twofasapp.permissions.CameraPermissionRequestFlow
|
||||
import com.twofasapp.permissions.PermissionStatus
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class BrowserExtensionViewModel(
|
||||
class BrowserExtensionViewModel(
|
||||
private val dispatchers: Dispatchers,
|
||||
private val settingsRouter: SettingsRouter,
|
||||
private val cameraPermissionRequest: CameraPermissionRequestFlow,
|
||||
private val observeMobileDeviceCase: ObserveMobileDeviceCase,
|
||||
private val observePairedBrowsersCase: ObservePairedBrowsersCase,
|
||||
@ -27,6 +31,8 @@ internal class BrowserExtensionViewModel(
|
||||
private val _uiState = MutableStateFlow(BrowserExtensionUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
var onPairClick: () -> Unit = {}
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
|
||||
@ -56,7 +62,10 @@ internal class BrowserExtensionViewModel(
|
||||
.take(1)
|
||||
.onEach {
|
||||
when (it) {
|
||||
PermissionStatus.GRANTED -> settingsRouter.navigate(SettingsDirections.PairingScan)
|
||||
PermissionStatus.GRANTED -> {
|
||||
onPairClick()
|
||||
}
|
||||
|
||||
PermissionStatus.DENIED -> Unit
|
||||
PermissionStatus.DENIED_NEVER_ASK -> _uiState.update { state ->
|
||||
state.copy(showRationaleDialog = true)
|
||||
|
@ -1,11 +1,14 @@
|
||||
package com.twofasapp.browserextension.ui.pairing.progress
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -16,33 +19,46 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import com.airbnb.lottie.compose.*
|
||||
import com.airbnb.lottie.compose.LottieAnimation
|
||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
||||
import com.airbnb.lottie.compose.LottieConstants
|
||||
import com.airbnb.lottie.compose.animateLottieCompositionAsState
|
||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
||||
import com.twofasapp.design.compose.AnimatedContent
|
||||
import com.twofasapp.design.compose.ButtonHeight
|
||||
import com.twofasapp.design.compose.ButtonShape
|
||||
import com.twofasapp.design.compose.ButtonTextColor
|
||||
import com.twofasapp.designsystem.common.TwTopAppBar
|
||||
import com.twofasapp.resources.R
|
||||
import com.twofasapp.design.compose.*
|
||||
import com.twofasapp.navigation.SettingsDirections
|
||||
import com.twofasapp.navigation.SettingsRouter
|
||||
import org.koin.androidx.compose.get
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@Composable
|
||||
internal fun PairingProgressScreen(
|
||||
fun PairingProgressScreen(
|
||||
openMain: () -> Unit,
|
||||
openPairingScan: () -> Unit,
|
||||
extensionId: String,
|
||||
viewModel: PairingProgressViewModel = get(),
|
||||
router: SettingsRouter = get(),
|
||||
viewModel: PairingProgressViewModel = koinViewModel(),
|
||||
) {
|
||||
val uiState = viewModel.uiState.collectAsState()
|
||||
viewModel.pairBrowser(extensionId)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Toolbar(title = stringResource(id = if (uiState.value.isPairing) R.string.browser__pairing_with_browser else R.string.settings__browser_extension_result_toolbar_title)) {
|
||||
router.navigate(SettingsDirections.GoBack)
|
||||
}
|
||||
TwTopAppBar(titleText = stringResource(id = if (uiState.value.isPairing) R.string.browser__pairing_with_browser else R.string.settings__browser_extension_result_toolbar_title))
|
||||
}
|
||||
) { padding ->
|
||||
AnimatedContent(
|
||||
condition = uiState.value.isPairing,
|
||||
contentWhenTrue = { ProgressContent() },
|
||||
contentWhenFalse = { ResultContent(uiState.value.isPairingSuccess, uiState.value.code, router) }
|
||||
contentWhenFalse = {
|
||||
ResultContent(
|
||||
onContinueClick = { openMain() },
|
||||
onScanAgainClick = { openPairingScan() },
|
||||
uiState.value.isPairingSuccess,
|
||||
uiState.value.code,
|
||||
padding
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -57,9 +73,11 @@ internal fun ProgressContent() {
|
||||
|
||||
@Composable
|
||||
internal fun ResultContent(
|
||||
onContinueClick: () -> Unit = {},
|
||||
onScanAgainClick: () -> Unit = {},
|
||||
isSuccess: Boolean,
|
||||
code: Int? = null,
|
||||
router: SettingsRouter,
|
||||
padding: PaddingValues,
|
||||
) {
|
||||
val image = if (isSuccess) R.drawable.browser_extension_success_image else R.drawable.browser_extension_error_image
|
||||
|
||||
@ -77,15 +95,19 @@ internal fun ResultContent(
|
||||
|
||||
val cta = if (isSuccess) R.string.commons__continue else R.string.browser__result_error_cta
|
||||
|
||||
val ctaAction: () -> Unit = if (isSuccess) {
|
||||
{ router.navigate(SettingsDirections.GoBack) }
|
||||
} else {
|
||||
{ router.navigate(SettingsDirections.PairingScan) }
|
||||
val ctaAction: () -> Unit = {
|
||||
if (isSuccess) {
|
||||
onContinueClick()
|
||||
// { router.navigate(SettingsDirections.GoBack) }
|
||||
} else {
|
||||
onScanAgainClick()
|
||||
// { router.navigate(SettingsDirections.PairingScan) }
|
||||
}
|
||||
}
|
||||
|
||||
ConstraintLayout(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(padding)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
val (content, pair) = createRefs()
|
||||
@ -112,14 +134,14 @@ internal fun ResultContent(
|
||||
|
||||
Text(
|
||||
text = stringResource(id = title),
|
||||
style = MaterialTheme.typography.h6,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = description),
|
||||
style = MaterialTheme.typography.body1,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
)
|
||||
|
@ -1,11 +0,0 @@
|
||||
package com.twofasapp.browserextension.ui.pairing.progress
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
class PairingProgressScreenFactory {
|
||||
|
||||
@Composable
|
||||
fun create(extensionId: String) {
|
||||
PairingProgressScreen(extensionId)
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package com.twofasapp.browserextension.ui.pairing.progress
|
||||
|
||||
internal data class PairingProgressUiState(
|
||||
data class PairingProgressUiState(
|
||||
val isPairing: Boolean = true,
|
||||
val isPairingSuccess: Boolean = false,
|
||||
val code: Int? = null
|
||||
|
@ -5,14 +5,14 @@ import com.twofasapp.base.BaseViewModel
|
||||
import com.twofasapp.base.dispatcher.Dispatchers
|
||||
import com.twofasapp.browserextension.domain.PairBrowserCase
|
||||
import com.twofasapp.browserextension.domain.RegisterMobileDeviceCase
|
||||
import com.twofasapp.network.exception.BrowserAlreadyPairedException
|
||||
import com.twofasapp.data.browserext.remote.exception.BrowserAlreadyPairedException
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class PairingProgressViewModel(
|
||||
class PairingProgressViewModel(
|
||||
private val dispatchers: Dispatchers,
|
||||
private val registerMobileDeviceCase: RegisterMobileDeviceCase,
|
||||
private val pairBrowserCase: PairBrowserCase,
|
||||
|
@ -2,40 +2,41 @@ package com.twofasapp.browserextension.ui.pairing.scan
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.twofasapp.resources.R
|
||||
import com.twofasapp.design.compose.Toolbar
|
||||
import com.twofasapp.design.dialogs.InfoDialog
|
||||
import com.twofasapp.navigation.SettingsDirections
|
||||
import com.twofasapp.navigation.SettingsRouter
|
||||
import com.twofasapp.designsystem.common.TwTopAppBar
|
||||
import com.twofasapp.qrscanner.ui.QrScannerScreen
|
||||
import org.koin.androidx.compose.get
|
||||
import com.twofasapp.resources.R
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@Composable
|
||||
internal fun PairingScanScreen(
|
||||
viewModel: PairingScanViewModel = get(),
|
||||
router: SettingsRouter = get(),
|
||||
fun PairingScanScreen(
|
||||
openPairingProgress: (String) -> Unit,
|
||||
viewModel: PairingScanViewModel = koinViewModel(),
|
||||
) {
|
||||
val uiState = viewModel.uiState.collectAsState()
|
||||
val activity = (LocalContext.current as? Activity)
|
||||
var openSuccess by remember { mutableStateOf(true) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Toolbar(
|
||||
title = stringResource(id = R.string.commons__scan_qr_code)) {
|
||||
router.navigate(SettingsDirections.GoBack)
|
||||
}
|
||||
TwTopAppBar(titleText = stringResource(id = R.string.commons__scan_qr_code), modifier = Modifier.clickable { viewModel.pairMockedBrowser() }, showBackButton = true)
|
||||
}
|
||||
) { padding ->
|
||||
QrScannerScreen()
|
||||
|
||||
if (uiState.value.isSuccess) {
|
||||
router.navigate(SettingsDirections.PairingProgress(uiState.value.extensionId))
|
||||
if (uiState.value.isSuccess && openSuccess) {
|
||||
openSuccess = false
|
||||
openPairingProgress(uiState.value.extensionId)
|
||||
}
|
||||
|
||||
if (uiState.value.showErrorDialog) {
|
||||
|
@ -1,11 +0,0 @@
|
||||
package com.twofasapp.browserextension.ui.pairing.scan
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
class PairingScanScreenFactory {
|
||||
|
||||
@Composable
|
||||
fun create() {
|
||||
PairingScanScreen()
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package com.twofasapp.browserextension.ui.pairing.scan
|
||||
|
||||
internal data class PairingScanUiState(
|
||||
data class PairingScanUiState(
|
||||
val isSuccess: Boolean = false,
|
||||
val extensionId: String = "",
|
||||
val showErrorDialog: Boolean = false,
|
||||
|
@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class PairingScanViewModel(
|
||||
class PairingScanViewModel(
|
||||
private val scanQr: ScanQr,
|
||||
) : BaseViewModel() {
|
||||
|
||||
@ -59,4 +59,14 @@ internal class PairingScanViewModel(
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun pairMockedBrowser() {
|
||||
viewModelScope.launch {
|
||||
scanQr.publishResult(
|
||||
ScanQr.Result(
|
||||
"twofas_c://662699c0-dab0-4c3e-93a8-81dc31e24747"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20")
|
||||
classpath("org.jetbrains.kotlin:kotlin-serialization:1.7.20")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${libs.versions.kotlin.get()}")
|
||||
classpath("org.jetbrains.kotlin:kotlin-serialization:${libs.versions.kotlin.get()}")
|
||||
classpath("com.google.gms:google-services:4.3.14")
|
||||
classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.2")
|
||||
}
|
||||
|
@ -15,8 +15,8 @@ java {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("com.android.tools.build:gradle:8.0.0-alpha11")
|
||||
compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20")
|
||||
compileOnly("com.android.tools.build:gradle:${libs.versions.agp.get()}")
|
||||
compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:${libs.versions.kotlin.get()}")
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
|
@ -47,6 +47,8 @@ internal fun Project.applyKotlinAndroid(
|
||||
"-opt-in=kotlin.Experimental",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.lifecycle.compose.ExperimentalLifecycleComposeApi",
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":di"))
|
||||
implementation(project(":core:di"))
|
||||
implementation(libs.kotlinCoroutines)
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
package com.twofasapp.environment
|
||||
package com.twofasapp.common.environment
|
||||
|
||||
interface AppConfig {
|
||||
interface AppBuild {
|
||||
val id: String
|
||||
val isDebug: Boolean
|
||||
val isDebuggable: Boolean
|
||||
val versionName: String
|
||||
val versionCode: Int
|
||||
val buildVariant: BuildVariant
|
||||
val deviceName: String
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.twofasapp.common.environment
|
||||
|
||||
enum class BuildVariant {
|
||||
Release,
|
||||
ReleaseLocal,
|
||||
Debug,
|
||||
}
|
79
core/common/src/main/java/com/twofasapp/common/ktx/Base64.kt
Normal file
79
core/common/src/main/java/com/twofasapp/common/ktx/Base64.kt
Normal file
@ -0,0 +1,79 @@
|
||||
package com.twofasapp.common.ktx
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
fun String.encodeBase64ToString(): String = String(this.toByteArray().encodeBase64())
|
||||
fun String.encodeBase64ToByteArray(): ByteArray = this.toByteArray().encodeBase64()
|
||||
fun ByteArray.encodeBase64ToString(): String = String(this.encodeBase64())
|
||||
|
||||
fun String.decodeBase64(): String = String(this.toByteArray().decodeBase64())
|
||||
fun String.decodeBase64ToByteArray(): ByteArray = this.toByteArray().decodeBase64()
|
||||
fun ByteArray.decodeBase64ToString(): String = String(this.decodeBase64())
|
||||
|
||||
private fun ByteArray.encodeBase64(): ByteArray {
|
||||
val table = (CharRange('A', 'Z') + CharRange('a', 'z') + CharRange('0', '9') + '+' + '/').toCharArray()
|
||||
val output = ByteArrayOutputStream()
|
||||
var padding = 0
|
||||
var position = 0
|
||||
while (position < this.size) {
|
||||
var b = this[position].toInt() and 0xFF shl 16 and 0xFFFFFF
|
||||
if (position + 1 < this.size) b = b or (this[position + 1].toInt() and 0xFF shl 8) else padding++
|
||||
if (position + 2 < this.size) b = b or (this[position + 2].toInt() and 0xFF) else padding++
|
||||
for (i in 0 until 4 - padding) {
|
||||
val c = b and 0xFC0000 shr 18
|
||||
output.write(table[c].code)
|
||||
b = b shl 6
|
||||
}
|
||||
position += 3
|
||||
}
|
||||
for (i in 0 until padding) {
|
||||
output.write('='.code)
|
||||
}
|
||||
return output.toByteArray()
|
||||
}
|
||||
|
||||
private fun ByteArray.decodeBase64(): ByteArray {
|
||||
val table = intArrayOf(
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1,
|
||||
-1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
|
||||
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1
|
||||
)
|
||||
|
||||
val output = ByteArrayOutputStream()
|
||||
var position = 0
|
||||
while (position < this.size) {
|
||||
var b: Int
|
||||
if (table[this[position].toInt()] != -1) {
|
||||
b = table[this[position].toInt()] and 0xFF shl 18
|
||||
} else {
|
||||
position++
|
||||
continue
|
||||
}
|
||||
var count = 0
|
||||
if (position + 1 < this.size && table[this[position + 1].toInt()] != -1) {
|
||||
b = b or (table[this[position + 1].toInt()] and 0xFF shl 12)
|
||||
count++
|
||||
}
|
||||
if (position + 2 < this.size && table[this[position + 2].toInt()] != -1) {
|
||||
b = b or (table[this[position + 2].toInt()] and 0xFF shl 6)
|
||||
count++
|
||||
}
|
||||
if (position + 3 < this.size && table[this[position + 3].toInt()] != -1) {
|
||||
b = b or (table[this[position + 3].toInt()] and 0xFF)
|
||||
count++
|
||||
}
|
||||
while (count > 0) {
|
||||
val c = b and 0xFF0000 shr 16
|
||||
output.write(c.toChar().toInt())
|
||||
b = b shl 8
|
||||
count--
|
||||
}
|
||||
position += 4
|
||||
}
|
||||
return output.toByteArray()
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.twofasapp.common.ktx
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
inline fun <T> runSafely(block: () -> T): Result<T> =
|
||||
try {
|
||||
Result.success(block())
|
||||
} catch (cancellationException: CancellationException) {
|
||||
throw cancellationException
|
||||
} catch (exception: Exception) {
|
||||
Result.failure(exception)
|
||||
}
|
||||
|
||||
fun tickerFlow(period: Long, initialDelay: Long = 0) = flow {
|
||||
delay(initialDelay)
|
||||
while (true) {
|
||||
emit(Unit)
|
||||
delay(period)
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.twofasapp.common.ktx
|
||||
|
||||
import androidx.navigation.NavController
|
||||
|
||||
fun NavController.clearGraphBackStack() {
|
||||
currentBackStackEntry?.destination?.parent?.route?.let { currentGraphRoute ->
|
||||
popBackStack(currentGraphRoute, inclusive = true)
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user