Convert AdvancedPinPreferencesFragment to compose.

This commit is contained in:
Alex Hart
2025-06-13 14:37:55 -03:00
committed by Michelle Tang
parent e62f14b6d3
commit 76e8695000
5 changed files with 312 additions and 128 deletions

View File

@@ -1,105 +0,0 @@
package org.thoughtcrime.securesms.preferences;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity;
import org.thoughtcrime.securesms.payments.backup.PaymentsRecoveryStartFragmentArgs;
import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity;
import org.thoughtcrime.securesms.pin.PinOptOutDialog;
public class AdvancedPinPreferenceFragment extends ListSummaryPreferenceFragment {
private static final String PREF_ENABLE = "pref_pin_enable";
private static final String PREF_DISABLE = "pref_pin_disable";
@Override
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
}
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences_advanced_pin);
}
@Override
public void onResume() {
super.onResume();
updatePreferenceState();
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == CreateSvrPinActivity.REQUEST_NEW_PIN && resultCode == CreateSvrPinActivity.RESULT_OK) {
Snackbar.make(requireView(), R.string.ApplicationPreferencesActivity_pin_created, Snackbar.LENGTH_LONG).show();
}
}
private void updatePreferenceState() {
Preference enable = this.findPreference(PREF_ENABLE);
Preference disable = this.findPreference(PREF_DISABLE);
if (SignalStore.svr().hasOptedOut()) {
enable.setVisible(true);
disable.setVisible(false);
enable.setOnPreferenceClickListener(preference -> {
onPreferenceChanged(true);
return true;
});
} else {
enable.setVisible(false);
disable.setVisible(true);
disable.setOnPreferenceClickListener(preference -> {
onPreferenceChanged(false);
return true;
});
}
}
private void onPreferenceChanged(boolean enabled) {
boolean hasRegistrationLock = SignalStore.svr().isRegistrationLockEnabled();
if (!enabled && hasRegistrationLock) {
new MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.ApplicationPreferencesActivity_pins_are_required_for_registration_lock)
.setCancelable(true)
.setPositiveButton(android.R.string.ok, (d, which) -> d.dismiss())
.show();
} else if (!enabled && SignalStore.payments().mobileCoinPaymentsEnabled() && !SignalStore.payments().getUserConfirmedMnemonic()) {
new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.ApplicationPreferencesActivity_record_payments_recovery_phrase)
.setMessage(R.string.ApplicationPreferencesActivity_before_you_can_disable_your_pin)
.setPositiveButton(R.string.ApplicationPreferencesActivity_record_phrase, (dialog, which) -> {
Intent intent = new Intent(requireContext(), PaymentsActivity.class);
intent.putExtra(PaymentsActivity.EXTRA_PAYMENTS_STARTING_ACTION, R.id.action_directly_to_paymentsBackup);
intent.putExtra(PaymentsActivity.EXTRA_STARTING_ARGUMENTS, new PaymentsRecoveryStartFragmentArgs.Builder().setFinishOnConfirm(true).build().toBundle());
startActivity(intent);
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
.setCancelable(true)
.show();
} else if (!enabled) {
PinOptOutDialog.show(requireContext(),
() -> {
updatePreferenceState();
Snackbar.make(requireView(), R.string.ApplicationPreferencesActivity_pin_disabled, Snackbar.LENGTH_SHORT).show();
});
} else {
startActivityForResult(CreateSvrPinActivity.getIntentForPinCreate(requireContext()), CreateSvrPinActivity.REQUEST_NEW_PIN);
}
}
}

View File

@@ -0,0 +1,228 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.preferences
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Rows
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.Snackbars
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
import org.thoughtcrime.securesms.payments.backup.PaymentsRecoveryStartFragmentArgs.Builder
import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity
import org.thoughtcrime.securesms.pin.PinOptOutDialog
/**
* Fragment which allows user to enable or disable their PIN
*/
class AdvancedPinSettingsFragment : ComposeFragment() {
private val viewModel: AdvancedPinSettingsViewModel by viewModels()
override fun onResume() {
super.onResume()
viewModel.refresh()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.event.collectLatest {
when (it) {
AdvancedPinSettingsViewModel.Event.SHOW_OPT_OUT_DIALOG -> PinOptOutDialog.show(requireContext()) {
viewModel.onPinOptOutSuccess()
displayOptOutSnackbar()
}
AdvancedPinSettingsViewModel.Event.LAUNCH_PIN_CREATION_FLOW -> {
startActivityForResult(CreateSvrPinActivity.getIntentForPinCreate(requireContext()), CreateSvrPinActivity.REQUEST_NEW_PIN)
}
AdvancedPinSettingsViewModel.Event.LAUNCH_RECOVERY_PHRASE_HANDLING -> {
val intent = Intent(requireContext(), PaymentsActivity::class.java)
intent.putExtra(PaymentsActivity.EXTRA_PAYMENTS_STARTING_ACTION, R.id.action_directly_to_paymentsBackup)
intent.putExtra(PaymentsActivity.EXTRA_STARTING_ARGUMENTS, Builder().setFinishOnConfirm(true).build().toBundle())
startActivity(intent)
}
}
}
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == CreateSvrPinActivity.REQUEST_NEW_PIN && resultCode == CreateSvrPinActivity.RESULT_OK) {
Snackbar.make(requireView(), R.string.ApplicationPreferencesActivity_pin_created, Snackbar.LENGTH_LONG).show()
}
}
@Composable
override fun FragmentContent() {
val hasOptedOutOfPin: Boolean by viewModel.hasOptedOutOfPin.collectAsStateWithLifecycle()
AdvancedPinSettingsFragmentContent(
hasOptedOutOfPin = hasOptedOutOfPin,
onNavigationClick = { requireActivity().onBackPressedDispatcher.onBackPressed() },
snackbarHostState = viewModel.snackbarHostState,
onOptionClick = viewModel::setOptOut
)
val dialog: AdvancedPinSettingsViewModel.Dialog by viewModel.dialog.collectAsStateWithLifecycle()
when (dialog) {
AdvancedPinSettingsViewModel.Dialog.REGISTRATION_LOCK -> PinsAreRequiredForRegistrationLockDialog(
onDismiss = {
viewModel.dismissDialog()
}
)
AdvancedPinSettingsViewModel.Dialog.RECORD_PAYMENTS_RECOVERY_PHRASE -> RecordPaymentsRecoveryPhraseDialog(
onConfirm = {
viewModel.launchRecoveryPhraseHandling()
},
onDismiss = {
viewModel.dismissDialog()
}
)
else -> Unit
}
}
private fun displayOptOutSnackbar() {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.snackbarHostState.showSnackbar(
message = getString(R.string.ApplicationPreferencesActivity_pin_disabled),
duration = SnackbarDuration.Long
)
}
}
}
@Composable
private fun AdvancedPinSettingsFragmentContent(
hasOptedOutOfPin: Boolean,
onNavigationClick: () -> Unit = {},
onOptionClick: (Boolean) -> Unit = {},
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
) {
Scaffolds.Settings(
title = stringResource(R.string.preferences__advanced_pin_settings_title),
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24),
navigationContentDescription = stringResource(R.string.CallScreenTopBar__go_back),
onNavigationClick = onNavigationClick,
snackbarHost = {
Snackbars.Host(
snackbarHostState = snackbarHostState
)
}
) {
val listener: () -> Unit = remember(onOptionClick, hasOptedOutOfPin) {
{
onOptionClick(hasOptedOutOfPin)
}
}
if (hasOptedOutOfPin) {
Rows.TextRow(
text = stringResource(R.string.preferences__enable_pin),
label = stringResource(R.string.preferences__pins_keep_information_stored_with_signal_encrypted_so_only_you_can_access_it),
onClick = listener,
modifier = Modifier.padding(it)
)
} else {
Rows.TextRow(
text = stringResource(R.string.preferences__disable_pin),
label = stringResource(R.string.preferences__if_you_disable_the_pin_you_will_lose_all_data),
onClick = listener,
modifier = Modifier.padding(it)
)
}
}
}
@Composable
private fun PinsAreRequiredForRegistrationLockDialog(
onDismiss: () -> Unit = {}
) {
Dialogs.SimpleMessageDialog(
message = stringResource(R.string.ApplicationPreferencesActivity_pins_are_required_for_registration_lock),
dismiss = stringResource(android.R.string.ok),
onDismiss = onDismiss
)
}
@Composable
private fun RecordPaymentsRecoveryPhraseDialog(
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.ApplicationPreferencesActivity_record_payments_recovery_phrase),
body = stringResource(R.string.ApplicationPreferencesActivity_before_you_can_disable_your_pin),
confirm = stringResource(R.string.ApplicationPreferencesActivity_record_phrase),
onConfirm = onConfirm,
dismiss = stringResource(android.R.string.cancel),
onDismiss = onDismiss
)
}
@SignalPreview
@Composable
private fun AdvancedPinSettingsFragmentContentEnabledPreview() {
Previews.Preview {
AdvancedPinSettingsFragmentContent(
hasOptedOutOfPin = false
)
}
}
@SignalPreview
@Composable
private fun AdvancedPinSettingsFragmentContentDisabledPreview() {
Previews.Preview {
AdvancedPinSettingsFragmentContent(
hasOptedOutOfPin = true
)
}
}
@SignalPreview
@Composable
private fun PinsAreRequiredForRegistrationLockDialogPreview() {
Previews.Preview {
PinsAreRequiredForRegistrationLockDialog()
}
}
@SignalPreview
@Composable
private fun RecordPaymentsRecoveryPhraseDialogPreview() {
Previews.Preview {
RecordPaymentsRecoveryPhraseDialog({}, {})
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.preferences
import androidx.compose.material3.SnackbarHostState
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.thoughtcrime.securesms.keyvalue.SignalStore
class AdvancedPinSettingsViewModel : ViewModel() {
enum class Dialog {
NONE,
REGISTRATION_LOCK,
RECORD_PAYMENTS_RECOVERY_PHRASE
}
enum class Event {
SHOW_OPT_OUT_DIALOG,
LAUNCH_PIN_CREATION_FLOW,
LAUNCH_RECOVERY_PHRASE_HANDLING
}
private val internalDialog = MutableStateFlow(Dialog.NONE)
private val internalEvent = MutableSharedFlow<Event>()
private val internalHasOptedOutOfPin = MutableStateFlow(SignalStore.svr.hasOptedOut())
val dialog: StateFlow<Dialog> = internalDialog
val event: SharedFlow<Event> = internalEvent
val hasOptedOutOfPin: StateFlow<Boolean> = internalHasOptedOutOfPin
val snackbarHostState = SnackbarHostState()
fun refresh() {
internalHasOptedOutOfPin.value = SignalStore.svr.hasOptedOut()
}
fun setOptOut(enabled: Boolean) {
val hasRegistrationLock = SignalStore.svr.isRegistrationLockEnabled
when {
!enabled && hasRegistrationLock -> {
internalDialog.value = Dialog.REGISTRATION_LOCK
}
!enabled && SignalStore.payments.mobileCoinPaymentsEnabled() && !SignalStore.payments.userConfirmedMnemonic -> {
internalDialog.value = Dialog.RECORD_PAYMENTS_RECOVERY_PHRASE
}
!enabled -> {
dismissDialog()
emitEvent(Event.SHOW_OPT_OUT_DIALOG)
}
else -> {
dismissDialog()
emitEvent(Event.LAUNCH_PIN_CREATION_FLOW)
}
}
}
fun launchRecoveryPhraseHandling() {
emitEvent(Event.LAUNCH_RECOVERY_PHRASE_HANDLING)
}
fun onPinOptOutSuccess() {
internalHasOptedOutOfPin.value = SignalStore.svr.hasOptedOut()
}
fun dismissDialog() {
internalDialog.value = Dialog.NONE
}
private fun emitEvent(event: Event) {
viewModelScope.launch {
internalEvent.emit(event)
}
}
}