Migrate to Compose Material3

wip
This commit is contained in:
Rafał Kobyłko 2023-01-24 17:45:28 +01:00
parent 59374b5309
commit 3488c3bc3d
476 changed files with 9086 additions and 4438 deletions

View File

@ -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)
}

View File

@ -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>

View File

@ -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()
}
}
})
}
}
}

View File

@ -1,5 +0,0 @@
package com.twofasapp.about.ui
internal data class AboutUiState(
val versionName: String = "",
)

View File

@ -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())
)
}
}

View File

@ -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)

View File

@ -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"

View File

@ -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>() }
}
}

View File

@ -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())

View File

@ -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
}

View File

@ -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()) }

View File

@ -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>() }
}
}

View File

@ -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()) }

View File

@ -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>() }
}
}

View File

@ -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> =

View 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
}

View File

@ -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)
}

View File

@ -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()
}
}

View File

@ -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()
// }
// }
}
}
}

View File

@ -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) {

View File

@ -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>()
}
}
}

View File

@ -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)
}

View File

@ -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())
}
}

View File

@ -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() {
}
}

View File

@ -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)
}
}

View File

@ -1,7 +0,0 @@
package com.twofasapp.features.trash
import com.twofasapp.prefs.model.ServiceDto
data class TrashedService(
val service: ServiceDto,
)

View File

@ -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()
}
}
}

View File

@ -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
}
}
}

View File

@ -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()
}

View File

@ -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()
}
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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>()
}
}
}

View File

@ -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,

View 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()
}
}

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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() }
}
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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"))

View File

@ -9,6 +9,6 @@ android {
dependencies {
implementation(project(":base"))
implementation(project(":di"))
implementation(project(":core:di"))
implementation(project(":extensions"))
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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 = "*/*"

View File

@ -9,7 +9,7 @@ android {
}
dependencies {
implementation(project(":di"))
implementation(project(":core:di"))
implementation(project(":prefs"))
implementation(project(":resources"))

View File

@ -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)

View File

@ -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)
}

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.twofasapp.browserextension.domain">
</manifest>

View File

@ -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>
}

View File

@ -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>
}

View File

@ -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>>
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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>)
}

View File

@ -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,
)
}

View File

@ -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>
}

View File

@ -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,
)
}
}
}

View File

@ -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,

View File

@ -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(

View File

@ -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,

View File

@ -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() {

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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,
) {

View File

@ -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,
)

View File

@ -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(

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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 = "",

View File

@ -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
}
}
}

View File

@ -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,
)
}

View File

@ -1,11 +0,0 @@
package com.twofasapp.browserextension.ui.main
import androidx.compose.runtime.Composable
class BrowserExtensionScreenFactory {
@Composable
fun create() {
BrowserExtensionScreen()
}
}

View File

@ -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,

View File

@ -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)

View File

@ -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
)

View File

@ -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)
}
}

View File

@ -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

View File

@ -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,

View File

@ -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) {

View File

@ -1,11 +0,0 @@
package com.twofasapp.browserextension.ui.pairing.scan
import androidx.compose.runtime.Composable
class PairingScanScreenFactory {
@Composable
fun create() {
PairingScanScreen()
}
}

View File

@ -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,

View File

@ -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"
)
)
}
}
}

View File

@ -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")
}

View File

@ -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 {

View File

@ -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",
)
}

View File

@ -8,6 +8,6 @@ android {
}
dependencies {
implementation(project(":di"))
implementation(project(":core:di"))
implementation(libs.kotlinCoroutines)
}

View File

@ -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
}
}

View File

@ -0,0 +1,7 @@
package com.twofasapp.common.environment
enum class BuildVariant {
Release,
ReleaseLocal,
Debug,
}

View 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()
}

View File

@ -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)
}
}

View File

@ -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