diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2593c579..eaa3d566 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -20,9 +20,6 @@
-
diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPActivity.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPActivity.kt
index b83a40c3..acfaa6bc 100644
--- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPActivity.kt
+++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPActivity.kt
@@ -1,142 +1,202 @@
package com.yogeshpaliyal.keypass.ui.addTOTP
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import android.view.Menu
-import android.view.MenuItem
-import androidx.activity.viewModels
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.view.isVisible
-import androidx.lifecycle.Observer
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.snackbar.Snackbar
-import com.google.zxing.integration.android.IntentIntegrator
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.ArrowBackIosNew
+import androidx.compose.material.icons.rounded.Delete
+import androidx.compose.material.icons.rounded.Done
+import androidx.compose.material3.BottomAppBar
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.yogeshpaliyal.common.data.AccountModel
import com.yogeshpaliyal.common.utils.TOTPHelper
import com.yogeshpaliyal.keypass.R
-import com.yogeshpaliyal.keypass.databinding.ActivityAddTotpactivityBinding
-import dagger.hilt.android.AndroidEntryPoint
+import com.yogeshpaliyal.keypass.ui.detail.DeleteConfirmation
+import com.yogeshpaliyal.keypass.ui.detail.KeyPassInputField
+import com.yogeshpaliyal.keypass.ui.detail.QRScanner
+import com.yogeshpaliyal.keypass.ui.redux.actions.Action
+import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction
+import com.yogeshpaliyal.keypass.ui.redux.actions.ToastAction
+import org.reduxkotlin.compose.rememberTypedDispatcher
-@AndroidEntryPoint
-class AddTOTPActivity : AppCompatActivity() {
+@Composable
+fun TOTPScreen(id: String? = null, viewModel: AddTOTPViewModel = hiltViewModel()) {
+ val dispatchAction = rememberTypedDispatcher()
- companion object {
-
- private const val ARG_ACCOUNT_ID = "account_id"
-
- @JvmStatic
- fun start(context: Context?, accountId: String? = null) {
- val starter = Intent(context, AddTOTPActivity::class.java)
- starter.putExtra(ARG_ACCOUNT_ID, accountId)
- context?.startActivity(starter)
- }
- }
-
- private lateinit var binding: ActivityAddTotpactivityBinding
-
- private val mViewModel by viewModels()
-
- private val accountId by lazy {
- intent.extras?.getString(ARG_ACCOUNT_ID)
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityAddTotpactivityBinding.inflate(layoutInflater)
- binding.mViewModel = mViewModel
- binding.lifecycleOwner = this
- setContentView(binding.root)
-
- setSupportActionBar(binding.toolbar)
-
- binding.tilSecretKey.isVisible = accountId == null
- mViewModel.loadOldAccount(accountId)
-
- binding.toolbar.setNavigationOnClickListener {
- onBackPressed()
- }
-
- binding.tilSecretKey.setEndIconOnClickListener {
- // ScannerActivity.start(this)
- IntentIntegrator(this).setPrompt("").initiateScan()
- }
-
- mViewModel.error.observe(
- this,
- Observer {
- it?.getContentIfNotHandled()?.let {
- Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT).show()
- }
- }
+ // task value state
+ val (accountModel, setAccountModel) = remember {
+ mutableStateOf(
+ AccountModel()
)
+ }
- mViewModel.goBack.observe(
- this,
- Observer {
- it.getContentIfNotHandled()?.let {
- onBackPressed()
- }
- }
- )
-
- binding.btnSave.setOnClickListener {
- mViewModel.saveAccount(accountId)
+ val launcher = rememberLauncherForActivityResult(QRScanner()) {
+ it?.let {
+ val totp = TOTPHelper(it)
+ setAccountModel(
+ accountModel.copy(
+ password = totp.secret,
+ title = totp.label,
+ username = totp.issuer
+ )
+ )
}
}
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- if (accountId != null) {
- menuInflater.inflate(R.menu.menu_delete, menu)
- }
- return super.onCreateOptionsMenu(menu)
+ val goBack: () -> Unit = {
+ dispatchAction(GoBackAction)
}
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- if (item.itemId == R.id.action_delete) {
- deleteAccount()
+ // Set initial object
+ LaunchedEffect(key1 = Unit) {
+ viewModel.loadOldAccount(id) {
+ setAccountModel(it.copy())
}
- return super.onOptionsItemSelected(item)
}
- private fun deleteAccount() {
- MaterialAlertDialogBuilder(this)
- .setTitle(getString(R.string.delete_account_title))
- .setMessage(getString(R.string.delete_account_msg))
- .setPositiveButton(
- getString(R.string.delete)
- ) { dialog, which ->
- dialog?.dismiss()
-
- if (accountId != null) {
- mViewModel.deleteAccount(accountId!!) {
- onBackPressed()
+ Scaffold(bottomBar = {
+ BottomBar(
+ backPressed = goBack,
+ showDeleteButton = accountModel.uniqueId != null,
+ onDeletePressed = {
+ accountModel.uniqueId?.let {
+ viewModel.deleteAccount(it) {
+ goBack()
}
}
}
- .setNegativeButton(getString(R.string.cancel)) { dialog, which ->
- dialog.dismiss()
- }.show()
- }
-
- @Deprecated("Deprecated in Java")
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
- val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
-
- if (result != null) {
- if (result.contents != null) {
- try {
- val totp = TOTPHelper(result.contents)
- totp.secret?.let {
- mViewModel.setSecretKey(it)
- }
- mViewModel.setAccountName(totp.label)
- } catch (e: Exception) {
- e.printStackTrace()
- }
+ ) {
+ if (accountModel.password.isNullOrEmpty()) {
+ dispatchAction(ToastAction(R.string.alert_black_secret_key))
+ return@BottomBar
+ }
+
+ if (accountModel.title.isNullOrEmpty()) {
+ dispatchAction(ToastAction(R.string.alert_black_account_name))
+ return@BottomBar
+ }
+
+ viewModel.saveAccount(accountModel, goBack)
+ }
+ }) { paddingValues ->
+ Surface(modifier = Modifier.padding(paddingValues)) {
+ Fields(accountModel = accountModel, updateAccountModel = { newAccountModel ->
+ setAccountModel(newAccountModel)
+ }) {
+ launcher.launch(null)
}
- } else {
- super.onActivityResult(requestCode, resultCode, data)
}
}
}
+
+@Composable
+fun Fields(
+ modifier: Modifier = Modifier,
+ accountModel: AccountModel,
+ updateAccountModel: (newAccountModel: AccountModel) -> Unit,
+ scanClicked: () -> Unit
+) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(16.dp)
+ .verticalScroll(rememberScrollState()),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ if (accountModel.uniqueId == null) {
+ KeyPassInputField(
+ modifier = Modifier.testTag("secretKey"),
+ placeholder = R.string.secret_key,
+ value = accountModel.password,
+ setValue = {
+ updateAccountModel(accountModel.copy(password = it))
+ },
+ trailingIcon = {
+ IconButton(onClick = {
+ scanClicked()
+ }) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_twotone_qr_code_scanner_24),
+ contentDescription = ""
+ )
+ }
+ }
+ )
+ }
+
+ KeyPassInputField(
+ modifier = Modifier.testTag("accountName"),
+ placeholder = R.string.account_name,
+ value = accountModel.title,
+ setValue = {
+ updateAccountModel(accountModel.copy(title = it))
+ }
+ )
+ }
+}
+
+@Composable
+fun BottomBar(
+ backPressed: () -> Unit,
+ showDeleteButton: Boolean,
+ onDeletePressed: () -> Unit,
+ onSaveClicked: () -> Unit
+) {
+ val (openDialog, setOpenDialog) = remember { mutableStateOf(false) }
+
+ BottomAppBar(actions = {
+ IconButton(onClick = backPressed) {
+ Icon(
+ painter = rememberVectorPainter(image = Icons.Rounded.ArrowBackIosNew),
+ contentDescription = "Go Back",
+ tint = MaterialTheme.colorScheme.onSurface
+ )
+ }
+ if (showDeleteButton) {
+ IconButton(onClick = {
+ setOpenDialog(true)
+ }) {
+ Icon(
+ painter = rememberVectorPainter(image = Icons.Rounded.Delete),
+ contentDescription = "Delete",
+ tint = MaterialTheme.colorScheme.onSurface
+ )
+ }
+ }
+ }, floatingActionButton = {
+ FloatingActionButton(modifier = Modifier.testTag("save"), onClick = onSaveClicked) {
+ Icon(
+ painter = rememberVectorPainter(image = Icons.Rounded.Done),
+ contentDescription = "Save Changes"
+ )
+ }
+ })
+
+ DeleteConfirmation(
+ openDialog,
+ updateDialogVisibility = {
+ setOpenDialog(it)
+ },
+ onDeletePressed
+ )
+}
diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPViewModel.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPViewModel.kt
index b2177311..02235968 100644
--- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPViewModel.kt
+++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPViewModel.kt
@@ -1,14 +1,11 @@
package com.yogeshpaliyal.keypass.ui.addTOTP
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.yogeshpaliyal.common.AppDatabase
import com.yogeshpaliyal.common.constants.AccountType
import com.yogeshpaliyal.common.data.AccountModel
-import com.yogeshpaliyal.common.utils.Event
-import com.yogeshpaliyal.keypass.R
+import com.yogeshpaliyal.common.utils.getRandomString
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -18,64 +15,32 @@ import javax.inject.Inject
class AddTOTPViewModel @Inject constructor(private val appDatabase: AppDatabase) :
ViewModel() {
- private val _goBack = MutableLiveData>()
- val goBack: LiveData> = _goBack
-
- private val _error = MutableLiveData>()
- val error: LiveData> = _error
-
- val secretKey = MutableLiveData("")
-
- val accountName = MutableLiveData("")
-
- fun loadOldAccount(accountId: String?) {
+ fun loadOldAccount(accountId: String?, loadAccount: (accountModel: AccountModel) -> Unit) {
accountId ?: return
viewModelScope.launch(Dispatchers.IO) {
appDatabase.getDao().getAccount(accountId)?.let { accountModel ->
- accountName.postValue(accountModel.title ?: "")
+ loadAccount(accountModel)
}
}
}
- fun saveAccount(accountId: String?) {
+ fun saveAccount(accountModel: AccountModel, onComplete: () -> Unit) {
viewModelScope.launch {
- val secretKey = secretKey.value
- val accountName = accountName.value
-
- if (accountId == null) {
- if (secretKey.isNullOrEmpty()) {
- _error.postValue(Event(R.string.alert_black_secret_key))
- return@launch
- }
- }
-
- if (accountName.isNullOrEmpty()) {
- _error.postValue(Event(R.string.alert_black_account_name))
- return@launch
- }
-
- val accountModel = if (accountId == null) {
- AccountModel(password = secretKey, title = accountName, type = AccountType.TOTP)
+ val accountModelDb = if (accountModel.uniqueId == null) {
+ AccountModel(uniqueId = getRandomString(), password = accountModel.password, title = accountModel.title, type = AccountType.TOTP)
} else {
- appDatabase.getDao().getAccount(accountId)?.also {
- it.title = accountName
+ appDatabase.getDao().getAccount(accountModel.uniqueId)?.also {
+ it.title = accountModel.title
+ it.password = accountModel.password
}
}
- accountModel?.let { appDatabase.getDao().insertOrUpdateAccount(it) }
- _goBack.postValue(Event(Unit))
+ accountModelDb?.let { appDatabase.getDao().insertOrUpdateAccount(it) }
+ onComplete()
}
}
- fun setSecretKey(secretKey: String) {
- this.secretKey.value = secretKey
- }
-
- fun setAccountName(accountName: String) {
- this.accountName.value = accountName
- }
-
fun deleteAccount(accountId: String, onDeleted: () -> Unit) {
viewModelScope.launch {
appDatabase.getDao().deleteAccount(accountId)
diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/AccountsList.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/AccountsList.kt
index 08d401d2..dfff57ee 100644
--- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/AccountsList.kt
+++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/AccountsList.kt
@@ -51,9 +51,9 @@ import com.yogeshpaliyal.common.constants.AccountType
import com.yogeshpaliyal.common.data.AccountModel
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.ui.redux.actions.CopyToClipboard
-import com.yogeshpaliyal.keypass.ui.redux.actions.IntentNavigation
import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction
import com.yogeshpaliyal.keypass.ui.redux.states.AccountDetailState
+import com.yogeshpaliyal.keypass.ui.redux.states.TotpDetailState
import kotlinx.coroutines.delay
import org.reduxkotlin.compose.rememberDispatcher
import kotlin.time.Duration.Companion.seconds
@@ -77,7 +77,7 @@ fun AccountsList(accounts: List? = null) {
account,
onClick = {
if (it.type == AccountType.TOTP) {
- dispatch(IntentNavigation.AddTOTP(it.uniqueId))
+ dispatch(NavigationAction(TotpDetailState(it.uniqueId)))
} else {
dispatch(NavigationAction(AccountDetailState(it.id)))
}
diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt
index ea8cf817..e290b8b3 100644
--- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt
+++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt
@@ -49,6 +49,7 @@ import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import com.yogeshpaliyal.keypass.BuildConfig
+import com.yogeshpaliyal.keypass.ui.addTOTP.TOTPScreen
import com.yogeshpaliyal.keypass.ui.auth.AuthScreen
import com.yogeshpaliyal.keypass.ui.backup.BackupScreen
import com.yogeshpaliyal.keypass.ui.changeDefaultPasswordLength.ChangeDefaultPasswordLengthScreen
@@ -171,6 +172,7 @@ fun CurrentPage() {
}
is TotpDetailState -> {
+ TOTPScreen(it.accountId)
}
}
}
diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/NavigationModel.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/NavigationModel.kt
index 6d7c3b01..8126462d 100644
--- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/NavigationModel.kt
+++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/NavigationModel.kt
@@ -4,6 +4,7 @@ import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.ui.redux.actions.IntentNavigation
import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction
import com.yogeshpaliyal.keypass.ui.redux.states.HomeState
+import com.yogeshpaliyal.keypass.ui.redux.states.TotpDetailState
object NavigationModel {
@@ -31,7 +32,7 @@ object NavigationModel {
icon = R.drawable.ic_twotone_totp,
titleRes = R.string.add_totp,
checked = false,
- action = IntentNavigation.AddTOTP()
+ action = NavigationAction(TotpDetailState())
)
)
}
diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/IntentNavigation.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/IntentNavigation.kt
index 4d30a674..42a65037 100644
--- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/IntentNavigation.kt
+++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/IntentNavigation.kt
@@ -4,5 +4,4 @@ sealed interface IntentNavigation : Action {
object GeneratePassword : IntentNavigation
object BackupActivity : IntentNavigation
object ShareApp : IntentNavigation
- data class AddTOTP(val accountId: String? = null) : IntentNavigation
}
diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/middlewares/IntentNavigationMiddleware.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/middlewares/IntentNavigationMiddleware.kt
index c72c5b3e..99530576 100644
--- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/middlewares/IntentNavigationMiddleware.kt
+++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/middlewares/IntentNavigationMiddleware.kt
@@ -3,7 +3,6 @@ package com.yogeshpaliyal.keypass.ui.redux.middlewares
import android.content.Intent
import com.yogeshpaliyal.keypass.BuildConfig
import com.yogeshpaliyal.keypass.R
-import com.yogeshpaliyal.keypass.ui.addTOTP.AddTOTPActivity
import com.yogeshpaliyal.keypass.ui.generate.GeneratePasswordActivity
import com.yogeshpaliyal.keypass.ui.redux.actions.IntentNavigation
import com.yogeshpaliyal.keypass.ui.redux.states.KeyPassState
@@ -18,10 +17,6 @@ val intentNavigationMiddleware = middleware { store, next, action
state.context?.startActivity(intent)
}
- is IntentNavigation.AddTOTP -> {
- AddTOTPActivity.start(state.context, action.accountId)
- }
-
is IntentNavigation.BackupActivity -> {
// BackupActivity.start(state.context)
}
diff --git a/app/src/main/res/layout/activity_add_totpactivity.xml b/app/src/main/res/layout/activity_add_totpactivity.xml
deleted file mode 100644
index 26e8923b..00000000
--- a/app/src/main/res/layout/activity_add_totpactivity.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5b893afd..1b8cd5f2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -107,5 +107,6 @@
Authentication Error %s
Change password length
Default password length
+ Secret Key
diff --git a/common/src/main/java/com/yogeshpaliyal/common/data/AccountModel.kt b/common/src/main/java/com/yogeshpaliyal/common/data/AccountModel.kt
index a959a95b..fb82ec0d 100644
--- a/common/src/main/java/com/yogeshpaliyal/common/data/AccountModel.kt
+++ b/common/src/main/java/com/yogeshpaliyal/common/data/AccountModel.kt
@@ -7,7 +7,6 @@ import androidx.room.PrimaryKey
import com.google.gson.annotations.SerializedName
import com.yogeshpaliyal.common.constants.AccountType
import com.yogeshpaliyal.common.utils.TOTPHelper
-import com.yogeshpaliyal.common.utils.getRandomString
/*
* @author Yogesh Paliyal
@@ -29,7 +28,7 @@ data class AccountModel(
@ColumnInfo(name = "unique_id")
@SerializedName("unique_id")
- var uniqueId: String? = getRandomString(),
+ var uniqueId: String? = null,
@ColumnInfo(name = "username")
@SerializedName("username")