mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
Add Recovery Phrase tab
This commit is contained in:
parent
7a1b5749aa
commit
ab62d8f333
@ -363,6 +363,7 @@ dependencies {
|
||||
implementation 'androidx.compose.animation:animation:1.6.2'
|
||||
implementation 'androidx.compose.ui:ui-tooling:1.6.2'
|
||||
implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.33.1-alpha"
|
||||
implementation "com.google.accompanist:accompanist-pager:0.33.1-alpha"
|
||||
implementation "com.google.accompanist:accompanist-pager-indicators:0.33.1-alpha"
|
||||
implementation "androidx.compose.runtime:runtime-livedata:1.6.2"
|
||||
|
||||
|
@ -3,154 +3,170 @@ package org.thoughtcrime.securesms.onboarding
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.LocalContentAlpha
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Tab
|
||||
import androidx.compose.material.TabRow
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityLinkDeviceBinding
|
||||
import network.loki.messenger.databinding.FragmentRecoveryPhraseBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.crypto.MnemonicCodec
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
import org.thoughtcrime.securesms.ui.AppTheme
|
||||
import org.thoughtcrime.securesms.ui.OutlineButton
|
||||
import org.thoughtcrime.securesms.ui.baseBold
|
||||
import org.thoughtcrime.securesms.ui.colorDestructive
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
class LinkDeviceActivity : BaseActionBarActivity() {
|
||||
|
||||
private lateinit var binding: ActivityLinkDeviceBinding
|
||||
@Inject
|
||||
lateinit var prefs: TextSecurePreferences
|
||||
|
||||
private val adapter = LinkDeviceActivityAdapter(this)
|
||||
val viewModel: LinkDeviceViewModel by viewModels()
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setUpActionBarSessionLogo()
|
||||
TextSecurePreferences.apply {
|
||||
setHasViewedSeed(this@LinkDeviceActivity, true)
|
||||
setConfigurationMessageSynced(this@LinkDeviceActivity, false)
|
||||
setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
|
||||
setLastProfileUpdateTime(this@LinkDeviceActivity, 0)
|
||||
}
|
||||
binding = ActivityLinkDeviceBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||
}
|
||||
// endregion
|
||||
supportActionBar?.title = "Load Account"
|
||||
prefs.setHasViewedSeed(true)
|
||||
prefs.setConfigurationMessageSynced(false)
|
||||
prefs.setRestorationTime(System.currentTimeMillis())
|
||||
prefs.setLastProfileUpdateTime(0)
|
||||
|
||||
// region Interaction
|
||||
override fun handleQRCodeScanned(mnemonic: String) {
|
||||
try {
|
||||
val seed = Hex.fromStringCondensed(mnemonic)
|
||||
continueWithSeed(seed)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki","Error getting seed from QR code", e)
|
||||
Toast.makeText(this, "An error occurred.", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun continueWithMnemonic(mnemonic: String) {
|
||||
val loadFileContents: (String) -> String = { fileName ->
|
||||
MnemonicUtilities.loadFileContents(this, fileName)
|
||||
}
|
||||
try {
|
||||
val hexEncodedSeed = MnemonicCodec(loadFileContents).decode(mnemonic)
|
||||
val seed = Hex.fromStringCondensed(hexEncodedSeed)
|
||||
continueWithSeed(seed)
|
||||
} catch (error: Exception) {
|
||||
val message = if (error is MnemonicCodec.DecodingError) {
|
||||
error.description
|
||||
} else {
|
||||
"An error occurred."
|
||||
lifecycleScope.launch {
|
||||
viewModel.eventFlow.collect {
|
||||
startLoadingActivity(it.mnemonic)
|
||||
}
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun continueWithSeed(seed: ByteArray) {
|
||||
startLoadingActivity(seed)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
// region Adapter
|
||||
private class LinkDeviceActivityAdapter(private val activity: LinkDeviceActivity) : FragmentPagerAdapter(activity.supportFragmentManager) {
|
||||
val recoveryPhraseFragment = RecoveryPhraseFragment()
|
||||
|
||||
override fun getCount(): Int {
|
||||
return 2
|
||||
}
|
||||
|
||||
override fun getItem(index: Int): Fragment {
|
||||
return when (index) {
|
||||
0 -> recoveryPhraseFragment
|
||||
1 -> {
|
||||
val result = ScanQRCodeWrapperFragment()
|
||||
result.delegate = activity
|
||||
result.message = activity.getString(R.string.activity_link_device_qr_message)
|
||||
result
|
||||
}
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPageTitle(index: Int): CharSequence {
|
||||
return when (index) {
|
||||
0 -> activity.getString(R.string.activity_link_device_recovery_phrase)
|
||||
1 -> activity.getString(R.string.activity_link_device_scan_qr_code)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Recovery Phrase Fragment
|
||||
class RecoveryPhraseFragment : Fragment() {
|
||||
private lateinit var binding: FragmentRecoveryPhraseBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentRecoveryPhraseBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
with(binding) {
|
||||
mnemonicEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
||||
mnemonicEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
||||
mnemonicEditText.setOnEditorActionListener { v, actionID, _ ->
|
||||
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
||||
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
||||
handleContinueButtonTapped()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
ComposeView(this).apply {
|
||||
setContent {
|
||||
val state by viewModel.stateFlow.collectAsState()
|
||||
AppTheme {
|
||||
LoadAccountScreen(state, viewModel::onChange, viewModel::onRecoveryPhrase)
|
||||
}
|
||||
}
|
||||
continueButton.setOnClickListener { handleContinueButtonTapped() }
|
||||
}
|
||||
}.let(::setContentView)
|
||||
}
|
||||
|
||||
private fun handleContinueButtonTapped() {
|
||||
val mnemonic = binding.mnemonicEditText.text?.trim().toString()
|
||||
(requireActivity() as LinkDeviceActivity).continueWithMnemonic(mnemonic)
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun LoadAccountScreen(state: LinkDeviceState, onChange: (String) -> Unit = {}, onContinue: () -> Unit = {}) {
|
||||
val tabs = listOf(R.string.activity_recovery_password, R.string.activity_link_device_scan_qr_code)
|
||||
val pagerState = rememberPagerState { tabs.size }
|
||||
|
||||
Column {
|
||||
TabRow(
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
modifier = Modifier.height(48.dp)
|
||||
) {
|
||||
tabs.forEachIndexed { i, it ->
|
||||
Tab(i == pagerState.currentPage, onClick = { pagerState.targetPage }) {
|
||||
Text(stringResource(id = it))
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.weight(1f)
|
||||
) { i ->
|
||||
when(tabs[i]) {
|
||||
R.string.activity_recovery_password -> RecoveryPassword(state, onChange, onContinue)
|
||||
R.string.activity_link_device_scan_qr_code -> ScanQrCode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
@Composable
|
||||
fun RecoveryPassword(state: LinkDeviceState, onChange: (String) -> Unit = {}, onContinue: () -> Unit = {}) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 60.dp)
|
||||
) {
|
||||
Spacer(Modifier.weight(1f))
|
||||
Row {
|
||||
Text("Recovery Password", style = MaterialTheme.typography.h4)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_recovery_phrase),
|
||||
contentDescription = "",
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.size(28.dp))
|
||||
Text("Enter your recovery password to load your account. If you haven't saved it, you can find it in your app settings.")
|
||||
Spacer(Modifier.size(24.dp))
|
||||
OutlinedTextField(
|
||||
value = state.recoveryPhrase,
|
||||
onValueChange = { onChange(it) },
|
||||
placeholder = { Text("Enter your recovery password") },
|
||||
colors = TextFieldDefaults.outlinedTextFieldColors(
|
||||
textColor = state.error?.let { colorDestructive } ?: LocalContentColor.current.copy(LocalContentAlpha.current),
|
||||
focusedBorderColor = Color(0xff414141),
|
||||
unfocusedBorderColor = Color(0xff414141),
|
||||
cursorColor = LocalContentColor.current,
|
||||
placeholderColor = state.error?.let { colorDestructive } ?: MaterialTheme.colors.onSurface.copy(ContentAlpha.medium)
|
||||
),
|
||||
singleLine = true,
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { onContinue() },
|
||||
onGo = { onContinue() },
|
||||
onSearch = { onContinue() },
|
||||
onSend = { onContinue() },
|
||||
),
|
||||
isError = state.error != null,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
Spacer(Modifier.size(12.dp))
|
||||
state.error?.let {
|
||||
Text(it, style = MaterialTheme.typography.baseBold, color = MaterialTheme.colors.error)
|
||||
}
|
||||
Spacer(Modifier.weight(2f))
|
||||
OutlineButton(
|
||||
text = stringResource(id = R.string.continue_2),
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally).padding(horizontal = 64.dp, vertical = 20.dp).width(200.dp)
|
||||
) { onContinue() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScanQrCode() {
|
||||
|
||||
}
|
||||
|
||||
fun Context.startLinkDeviceActivity() {
|
||||
Intent(this, LinkDeviceActivity::class.java).let(::startActivity)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
package org.thoughtcrime.securesms.onboarding
|
||||
|
||||
data class LinkDeviceState(
|
||||
val recoveryPhrase: String = "",
|
||||
val error: String? = null
|
||||
)
|
@ -0,0 +1,77 @@
|
||||
package org.thoughtcrime.securesms.onboarding
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.session.libsignal.crypto.MnemonicCodec
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
|
||||
import javax.inject.Inject
|
||||
|
||||
class LinkDeviceEvent(val mnemonic: ByteArray)
|
||||
|
||||
@HiltViewModel
|
||||
class LinkDeviceViewModel @Inject constructor(
|
||||
application: Application
|
||||
): AndroidViewModel(application) {
|
||||
private val state = MutableStateFlow(LinkDeviceState())
|
||||
val stateFlow = state.asStateFlow()
|
||||
|
||||
private val event = Channel<LinkDeviceEvent>()
|
||||
val eventFlow = event.receiveAsFlow()
|
||||
|
||||
fun onRecoveryPhrase() {
|
||||
val mnemonic = state.value.recoveryPhrase
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
MnemonicCodec { MnemonicUtilities.loadFileContents(getApplication(), it) }
|
||||
.decode(mnemonic)
|
||||
.let(Hex::fromStringCondensed)
|
||||
.let(::LinkDeviceEvent)
|
||||
.let { event.send(it) }
|
||||
} catch (exception: Exception) {
|
||||
when (exception) {
|
||||
is MnemonicCodec.DecodingError -> exception.description
|
||||
else -> "An error occurred."
|
||||
}.let { error -> state.update { it.copy(error = error) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// override fun handleQRCodeScanned(mnemonic: String) {
|
||||
// try {
|
||||
// val seed = Hex.fromStringCondensed(mnemonic)
|
||||
// continueWithSeed(seed)
|
||||
// } catch (e: Exception) {
|
||||
// Log.e("Loki","Error getting seed from QR code", e)
|
||||
// Toast.makeText(this, "An error occurred.", Toast.LENGTH_LONG).show()
|
||||
// }
|
||||
// }
|
||||
|
||||
// fun continueWithMnemonic(mnemonic: String) {
|
||||
// val loadFileContents: (String) -> String = { fileName ->
|
||||
// MnemonicUtilities.loadFileContents(this, fileName)
|
||||
// }
|
||||
// try {
|
||||
// val hexEncodedSeed = MnemonicCodec(loadFileContents).decode(mnemonic)
|
||||
// val seed = Hex.fromStringCondensed(hexEncodedSeed)
|
||||
// continueWithSeed(seed)
|
||||
// } catch (error: Exception) {
|
||||
// val message = if (error is MnemonicCodec.DecodingError) {
|
||||
// error.description
|
||||
// } else {
|
||||
// "An error occurred."
|
||||
|
||||
fun onChange(recoveryPhrase: String) {
|
||||
state.value = LinkDeviceState(recoveryPhrase)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user