diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt index fd2409a725..b895d2579b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt @@ -116,6 +116,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.GooglePay import org.thoughtcrime.securesms.components.snackbars.LocalSnackbarStateConsumerRegistry import org.thoughtcrime.securesms.components.snackbars.SnackbarHostKey import org.thoughtcrime.securesms.components.snackbars.SnackbarState +import org.thoughtcrime.securesms.components.verificationrequested.VerificationCodeRequestedBottomSheet import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner import org.thoughtcrime.securesms.conversation.ConversationIntents @@ -195,6 +196,7 @@ import org.thoughtcrime.securesms.window.AppScaffoldNavigator import org.thoughtcrime.securesms.window.NavigationType import org.thoughtcrime.securesms.window.rememberThreePaneScaffoldNavigatorDelegate import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState +import kotlin.time.Duration.Companion.minutes import org.signal.core.ui.R as CoreUiR class MainActivity : @@ -357,6 +359,25 @@ class MainActivity : } } } + + launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + SignalStore + .account + .verificationCodeRequestedAtMsFlow + .filter { it > 0L } + .collect { requestedAt -> + val notificationThreshold = requestedAt + 10.minutes.inWholeMilliseconds + if (System.currentTimeMillis() < notificationThreshold) { + VerificationCodeRequestedBottomSheet.show(supportFragmentManager, requestedAt) + } else { + Log.i(TAG, "Verification code requested but is older than 10 minutes, not showing sheet") + } + + SignalStore.account.verificationCodeRequestedAtMs = 0L + } + } + } } supportFragmentManager.setFragmentResultListener( diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt index fc6dbc0b58..29608912a8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt @@ -86,6 +86,7 @@ class AppSettingsActivity : DSLSettingsActivity(), GooglePayComponent { is AppSettingsRoute.BackupsRoute.Backups -> AppSettingsFragmentDirections.actionDirectToBackupsSettingsFragment().setLaunchCheckoutFlow(appSettingsRoute.launchCheckoutFlow) AppSettingsRoute.Invite -> AppSettingsFragmentDirections.actionDirectToInviteFragment() AppSettingsRoute.DataAndStorageRoute.DataAndStorage -> AppSettingsFragmentDirections.actionDirectToStoragePreferenceFragment() + AppSettingsRoute.AccountRoute.Account -> AppSettingsFragmentDirections.actionDirectToAccountSettingsFragment() else -> error("Unsupported start location: ${appSettingsRoute?.javaClass?.name}") } } @@ -177,6 +178,9 @@ class AppSettingsActivity : DSLSettingsActivity(), GooglePayComponent { @JvmStatic fun changeNumber(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.ChangeNumberRoute.Start) + @JvmStatic + fun account(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.AccountRoute.Account) + @JvmStatic fun subscriptions(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.DonationsRoute.Donations(directToCheckoutType = InAppPaymentType.RECURRING_DONATION)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/verificationrequested/VerificationCodeRequestedBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/components/verificationrequested/VerificationCodeRequestedBottomSheet.kt new file mode 100644 index 0000000000..4985452fd3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/verificationrequested/VerificationCodeRequestedBottomSheet.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.verificationrequested + +import android.os.Bundle +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalResources +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.fragment.app.FragmentManager +import org.signal.core.ui.BottomSheetUtil +import org.signal.core.ui.compose.BottomSheets +import org.signal.core.ui.compose.Buttons +import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment +import org.signal.core.ui.compose.DayNightPreviews +import org.signal.core.ui.compose.Previews +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.util.DateUtils +import java.util.Locale + +/** + * Sheet shown when the server has pushed a notification telling us a verification code was + * requested for the user's account. + */ +class VerificationCodeRequestedBottomSheet : ComposeBottomSheetDialogFragment() { + + override val peekHeightPercentage: Float = 1f + + companion object { + private const val ARG_REQUESTED_AT = "requested_at" + + @JvmStatic + fun show(fragmentManager: FragmentManager, requestedAtMs: Long) { + VerificationCodeRequestedBottomSheet().apply { + arguments = Bundle().apply { putLong(ARG_REQUESTED_AT, requestedAtMs) } + }.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) + } + } + + @Composable + override fun SheetContent() { + val context = LocalContext.current + val resources = LocalResources.current + val requestedAt = requireArguments().getLong(ARG_REQUESTED_AT) + val formattedTime = remember(requestedAt) { + val time = DateUtils.getOnlyTimeString(context, requestedAt) + val day = DateUtils.getDayPrecisionTimeString(context, Locale.getDefault(), requestedAt) + resources.getString(R.string.VerificationCodeRequestedBottomSheet__time_with_day, time, day) + } + val nestedScrollInterop = rememberNestedScrollInteropConnection() + val scrollModifier = Modifier.nestedScroll(nestedScrollInterop) + + VerificationCodeRequestedContent( + formattedTime = formattedTime, + onSafetyTipsClicked = { + val fragmentManager = parentFragmentManager + dismissAllowingStateLoss() + VerificationCodeRequestedSafetyTipsBottomSheet.show(fragmentManager) + }, + onOkClicked = { dismissAllowingStateLoss() }, + modifier = scrollModifier + ) + } +} + +@Composable +private fun VerificationCodeRequestedContent( + formattedTime: String, + onSafetyTipsClicked: () -> Unit, + onOkClicked: () -> Unit, + modifier: Modifier = Modifier +) { + val scrollState = rememberScrollState() + + Column( + modifier = Modifier.fillMaxWidth() + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth() + ) { + BottomSheets.Handle() + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .weight(weight = 1f, fill = false) + .verticalScroll(state = scrollState) + .padding(horizontal = 36.dp) + .padding(bottom = 36.dp) + ) { + Spacer(modifier = Modifier.height(26.dp)) + + Image( + painter = painterResource(id = R.drawable.verificationcode_alert_96), + contentDescription = null, + modifier = Modifier.size(96.dp) + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = stringResource(id = R.string.VerificationCodeRequestedBottomSheet__title), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = formattedTime, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Text( + text = stringResource(id = R.string.VerificationCodeRequestedBottomSheet__body_1), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = stringResource(id = R.string.VerificationCodeRequestedBottomSheet__body_2), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Buttons.LargeTonal( + onClick = onSafetyTipsClicked, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + ) { + Text(text = stringResource(id = R.string.VerificationCodeRequestedBottomSheet__safety_tips)) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Buttons.LargeTonal( + onClick = onOkClicked, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + ) { + Text(text = stringResource(id = R.string.VerificationCodeRequestedBottomSheet__ok)) + } + } + } +} + +@DayNightPreviews +@Composable +private fun VerificationCodeRequestedContentPreview() { + Previews.BottomSheetContentPreview { + VerificationCodeRequestedContent( + formattedTime = "3:25 PM Today", + onSafetyTipsClicked = {}, + onOkClicked = {} + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/verificationrequested/VerificationCodeRequestedSafetyTipsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/components/verificationrequested/VerificationCodeRequestedSafetyTipsBottomSheet.kt new file mode 100644 index 0000000000..3a1d9e451a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/verificationrequested/VerificationCodeRequestedSafetyTipsBottomSheet.kt @@ -0,0 +1,182 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.verificationrequested + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.fragment.app.FragmentManager +import org.signal.core.ui.BottomSheetUtil +import org.signal.core.ui.compose.BottomSheets +import org.signal.core.ui.compose.Buttons +import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment +import org.signal.core.ui.compose.DayNightPreviews +import org.signal.core.ui.compose.Previews +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity + +/** + * Sheet showing safety tips related to a verification code alert. + */ +class VerificationCodeRequestedSafetyTipsBottomSheet : ComposeBottomSheetDialogFragment() { + + override val peekHeightPercentage: Float = 1f + + companion object { + @JvmStatic + fun show(fragmentManager: FragmentManager) { + VerificationCodeRequestedSafetyTipsBottomSheet().show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) + } + } + + @Composable + override fun SheetContent() { + val nestedScrollInterop = rememberNestedScrollInteropConnection() + val scrollModifier = Modifier.nestedScroll(nestedScrollInterop) + + SafetyTipsContent( + onOpenAccountSettings = { + startActivity(AppSettingsActivity.account(requireContext())) + dismissAllowingStateLoss() + }, + modifier = scrollModifier + ) + } +} + +@Composable +private fun SafetyTipsContent( + onOpenAccountSettings: () -> Unit, + modifier: Modifier = Modifier +) { + val scrollState = rememberScrollState() + + Column( + modifier = Modifier.fillMaxWidth() + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth() + ) { + BottomSheets.Handle() + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .weight(weight = 1f, fill = false) + .verticalScroll(state = scrollState) + .padding(horizontal = 36.dp) + .padding(bottom = 36.dp) + ) { + Spacer(modifier = Modifier.height(26.dp)) + + Text( + text = stringResource(id = R.string.SafetyTipsBottomSheet__title), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(36.dp)) + + SafetyTipRow( + iconRes = R.drawable.safetytip_48_message, + titleRes = R.string.SafetyTipsBottomSheet__tip_1_title, + bodyRes = R.string.SafetyTipsBottomSheet__tip_1_body + ) + + Spacer(modifier = Modifier.height(40.dp)) + + SafetyTipRow( + iconRes = R.drawable.safetytip_48_pin, + titleRes = R.string.SafetyTipsBottomSheet__tip_2_title, + bodyRes = R.string.SafetyTipsBottomSheet__tip_2_body + ) + + Spacer(modifier = Modifier.height(40.dp)) + + SafetyTipRow( + iconRes = R.drawable.safetytip_48_lock, + titleRes = R.string.SafetyTipsBottomSheet__tip_3_title, + bodyRes = R.string.SafetyTipsBottomSheet__tip_3_body + ) + + Spacer(modifier = Modifier.height(40.dp)) + + Buttons.LargeTonal( + onClick = onOpenAccountSettings, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + ) { + Text(text = stringResource(id = R.string.SafetyTipsBottomSheet__open_account_settings)) + } + } + } +} + +@Composable +private fun SafetyTipRow( + iconRes: Int, + titleRes: Int, + bodyRes: Int +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(24.dp) + ) { + Image( + painter = painterResource(id = iconRes), + contentDescription = null, + modifier = Modifier.size(48.dp) + ) + + Column(modifier = Modifier.fillMaxWidth()) { + Text( + text = stringResource(id = titleRes), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = stringResource(id = bodyRes), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +@DayNightPreviews +@Composable +private fun SafetyTipsContentPreview() { + Previews.BottomSheetContentPreview { + SafetyTipsContent(onOpenAccountSettings = {}) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmReceiveService.java b/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmReceiveService.java index e406a0a0b5..8d6d40eef0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmReceiveService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmReceiveService.java @@ -35,13 +35,16 @@ public class FcmReceiveService extends FirebaseMessagingService { remoteMessage.getOriginalPriority(), NetworkUtil.getNetworkStatus(this))); - String registrationChallenge = remoteMessage.getData().get("challenge"); - String rateLimitChallenge = remoteMessage.getData().get("rateLimitChallenge"); + String registrationChallenge = remoteMessage.getData().get("challenge"); + String rateLimitChallenge = remoteMessage.getData().get("rateLimitChallenge"); + String verificationCodeRequest = remoteMessage.getData().get("verificationCodeRequested"); if (registrationChallenge != null) { handleRegistrationPushChallenge(registrationChallenge); } else if (rateLimitChallenge != null) { handleRateLimitPushChallenge(rateLimitChallenge); + } else if (verificationCodeRequest != null && SignalStore.account().isPrimaryDevice()) { + handleVerificationCodeRequested(verificationCodeRequest, remoteMessage.getSentTime()); } else { handleReceivedNotification(AppDependencies.getApplication(), remoteMessage); } @@ -102,4 +105,20 @@ public class FcmReceiveService extends FirebaseMessagingService { Log.d(TAG, "Got a rate limit push challenge."); AppDependencies.getJobManager().add(new SubmitRateLimitPushChallengeJob(challenge)); } + + private static void handleVerificationCodeRequested(String verificationCodeRequestJson, long sentTime) { + Log.i(TAG, "Got a verification code requested push."); + + VerificationCodeRequestedPush verificationRequestedPush = VerificationCodeRequestedPush.fromJson(verificationCodeRequestJson); + + long requestedAt; + if (verificationRequestedPush != null && verificationRequestedPush.getTimestamp() != null) { + requestedAt = verificationRequestedPush.getTimestamp(); + } else { + Log.w(TAG, "Unable to parse requested at timestamp from server, using sent time instead"); + requestedAt = sentTime; + } + + SignalStore.account().setVerificationCodeRequestedAtMs(requestedAt); + } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/gcm/VerificationCodeRequestedPush.kt b/app/src/main/java/org/thoughtcrime/securesms/gcm/VerificationCodeRequestedPush.kt new file mode 100644 index 0000000000..24332d28cb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/gcm/VerificationCodeRequestedPush.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.gcm + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import org.signal.core.util.logging.Log + +@Serializable +data class VerificationCodeRequestedPush(val timestamp: Long?) { + companion object { + + private val TAG = Log.tag(VerificationCodeRequestedPush::class) + + private val json = Json { ignoreUnknownKeys = true } + + @JvmStatic + fun fromJson(jsonString: String): VerificationCodeRequestedPush? { + return try { + json.decodeFromString(jsonString) + } catch (e: Throwable) { + Log.w(TAG, "Unable to parse verification code request", e) + null + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt index 5b829b475b..e6e2d519e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences import android.preference.PreferenceManager +import kotlinx.coroutines.flow.Flow import org.signal.core.models.AccountEntropyPool import org.signal.core.models.ServiceId.ACI import org.signal.core.models.ServiceId.PNI @@ -86,6 +87,8 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context) private const val KEY_HAS_LINKED_DEVICES = "account.has_linked_devices" private const val KEY_HAS_INACTIVE_PRIMARY_DEVICE_ALERT = "account.has_inactive_primary_device_alert" + private const val KEY_VERIFICATION_CODE_REQUESTED_AT = "account.verification_code_requested_at" + private const val KEY_ACCOUNT_ENTROPY_POOL = "account.account_entropy_pool" private const val KEY_RESTORED_ACCOUNT_ENTROPY_KEY = "account.restored_account_entropy_pool" private const val KEY_RESTORED_ACCOUNT_ENTROPY_KEY_FROM_PRIMARY = "account.restore_account_entropy_pool_primary" @@ -562,6 +565,11 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context) @get:JvmName("isMultiDevice") var isMultiDevice by booleanValue(KEY_HAS_LINKED_DEVICES, false) + /** Server has indicated a verification code was requested for the account at this timestamp (ms since epoch) */ + private val verificationCodeRequestedAtMsValue = longValue(KEY_VERIFICATION_CODE_REQUESTED_AT, 0) + var verificationCodeRequestedAtMs: Long by verificationCodeRequestedAtMsValue + val verificationCodeRequestedAtMsFlow: Flow by lazy { verificationCodeRequestedAtMsValue.toFlow() } + /** Do not alter. If you need to migrate more stuff, create a new method. */ private fun migrateFromSharedPrefsV1(context: Context) { Log.i(TAG, "[V1] Migrating account values from shared prefs.") diff --git a/app/src/main/res/drawable/safetytip_48_lock.xml b/app/src/main/res/drawable/safetytip_48_lock.xml new file mode 100644 index 0000000000..cf49572183 --- /dev/null +++ b/app/src/main/res/drawable/safetytip_48_lock.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/drawable/safetytip_48_message.xml b/app/src/main/res/drawable/safetytip_48_message.xml new file mode 100644 index 0000000000..e3d818ae95 --- /dev/null +++ b/app/src/main/res/drawable/safetytip_48_message.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/drawable/safetytip_48_pin.xml b/app/src/main/res/drawable/safetytip_48_pin.xml new file mode 100644 index 0000000000..43bc31749c --- /dev/null +++ b/app/src/main/res/drawable/safetytip_48_pin.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/verificationcode_alert_96.xml b/app/src/main/res/drawable/verificationcode_alert_96.xml new file mode 100644 index 0000000000..2abad8ad9b --- /dev/null +++ b/app/src/main/res/drawable/verificationcode_alert_96.xml @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/app/src/main/res/navigation/app_settings_with_change_number.xml b/app/src/main/res/navigation/app_settings_with_change_number.xml index decf073511..7f9f33970d 100644 --- a/app/src/main/res/navigation/app_settings_with_change_number.xml +++ b/app/src/main/res/navigation/app_settings_with_change_number.xml @@ -845,6 +845,16 @@ app:popUpTo="@id/app_settings" app:popUpToInclusive="true" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 95f3833863..932ccb6e8c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9902,5 +9902,35 @@ To try again, uninstall and re-install Signal on this device, and choose \"Restore or transfer\". + + A verification code was requested + + Do not give your verification code to anyone. Signal will never message you for it. If you received a message from someone pretending to be Signal, it is a scam. + + You can safely ignore this message if you requested the code yourself. + + Safety tips + + OK + + %1$s %2$s + + + Safety Tips + + Don\'t respond to chats from Signal + + Signal will never message you for your registration code, PIN, or recovery key. Never respond to a chat pretending to be Signal. + + Keep your verification code safe + + If you received a verification code you didn\'t request, someone may be attempting to access your account. Do not share your code. + + Turn on registration lock in account settings + + Protect your account by requiring your Signal PIN, in addition to your verification code, when registering with Signal. + + Open account settings +