Fix bottom sheet behavior and design.

main
Nicholas 2023-02-23 11:37:51 -05:00 zatwierdzone przez Cody Henthorne
rodzic 2ffc576387
commit f3922c4156
5 zmienionych plików z 156 dodań i 76 usunięć

Wyświetl plik

@ -14,6 +14,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.navigation.Navigation; import androidx.navigation.Navigation;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
@ -28,9 +29,7 @@ import org.thoughtcrime.securesms.components.registration.VerificationPinKeyboar
import org.thoughtcrime.securesms.registration.ReceivedSmsEvent; import org.thoughtcrime.securesms.registration.ReceivedSmsEvent;
import org.thoughtcrime.securesms.registration.VerifyAccountRepository; import org.thoughtcrime.securesms.registration.VerifyAccountRepository;
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel; import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.LifecycleDisposable; import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.SupportEmailUtil;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener; import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer; import org.thoughtcrime.securesms.util.dualsim.MccMncProducer;
@ -62,7 +61,8 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
private VerificationPinKeyboard keyboard; private VerificationPinKeyboard keyboard;
private ActionCountDownButton callMeCountDown; private ActionCountDownButton callMeCountDown;
private ActionCountDownButton resendSmsCountDown; private ActionCountDownButton resendSmsCountDown;
private View wrongNumber; private MaterialButton wrongNumber;
private MaterialButton bottomSheetButton;
private boolean autoCompleting; private boolean autoCompleting;
private ViewModel viewModel; private ViewModel viewModel;
@ -87,6 +87,7 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
callMeCountDown = view.findViewById(R.id.call_me_count_down); callMeCountDown = view.findViewById(R.id.call_me_count_down);
resendSmsCountDown = view.findViewById(R.id.resend_sms_count_down); resendSmsCountDown = view.findViewById(R.id.resend_sms_count_down);
wrongNumber = view.findViewById(R.id.wrong_number); wrongNumber = view.findViewById(R.id.wrong_number);
bottomSheetButton = view.findViewById(R.id.having_trouble_button);
new SignalStrengthPhoneStateListener(this, this); new SignalStrengthPhoneStateListener(this, this);
@ -96,6 +97,7 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
setOnCodeFullyEnteredListener(verificationCodeView); setOnCodeFullyEnteredListener(verificationCodeView);
wrongNumber.setOnClickListener(v -> returnToPhoneEntryScreen()); wrongNumber.setOnClickListener(v -> returnToPhoneEntryScreen());
bottomSheetButton.setOnClickListener( v -> showBottomSheet());
callMeCountDown.setTextResources(R.string.RegistrationActivity_call, R.string.RegistrationActivity_call_me_instead_available_in); callMeCountDown.setTextResources(R.string.RegistrationActivity_call, R.string.RegistrationActivity_call_me_instead_available_in);
resendSmsCountDown.setTextResources(R.string.RegistrationActivity_resend_code, R.string.RegistrationActivity_resend_sms_available_in); resendSmsCountDown.setTextResources(R.string.RegistrationActivity_resend_code, R.string.RegistrationActivity_resend_sms_available_in);
@ -120,9 +122,9 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
disposables.bindTo(getViewLifecycleOwner().getLifecycle()); disposables.bindTo(getViewLifecycleOwner().getLifecycle());
viewModel = getViewModel(); viewModel = getViewModel();
viewModel.getSuccessfulCodeRequestAttempts().observe(getViewLifecycleOwner(), (attempts) -> { viewModel.getIncorrectCodeAttempts().observe(getViewLifecycleOwner(), (attempts) -> {
if (attempts >= 3) { if (attempts >= 3) {
new ContactSupportBottomSheetFragment(this::openTroubleshootingSteps, this::sendEmailToSupport).show(getChildFragmentManager(), "support_bottom_sheet"); bottomSheetButton.setVisibility(View.VISIBLE);
} }
}); });
@ -229,6 +231,8 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
} }
protected void handleIncorrectCodeError() { protected void handleIncorrectCodeError() {
viewModel.incrementIncorrectCodeAttempts();
Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_code, Toast.LENGTH_LONG).show(); Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_code, Toast.LENGTH_LONG).show();
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() { keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
@Override @Override
@ -416,19 +420,10 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
}); });
} }
private void openTroubleshootingSteps() {
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.support_center_url));
}
private void sendEmailToSupport() { private void showBottomSheet() {
String body = SupportEmailUtil.generateSupportEmailBody(requireContext(), ContactSupportBottomSheetFragment bottomSheet = new ContactSupportBottomSheetFragment();
R.string.RegistrationActivity_code_support_subject, bottomSheet.show(getChildFragmentManager(), "support_bottom_sheet");
null,
null);
CommunicationActions.openEmail(requireContext(),
SupportEmailUtil.getSupportEmailAddress(requireContext()),
getString(R.string.RegistrationActivity_code_support_subject),
body);
} }
@Override @Override

Wyświetl plik

@ -6,31 +6,98 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.SupportEmailUtil
/** /**
* Helpful bottom sheet dialog displayed during registration when the user enters the wrong verification code too many times. * Helpful bottom sheet dialog displayed during registration when the user enters the wrong verification code too many times.
*/ */
class ContactSupportBottomSheetFragment(private val troubleshootingStepsListener: Runnable, private val contactSupportListener: Runnable) : ComposeBottomSheetDialogFragment() { class ContactSupportBottomSheetFragment : ComposeBottomSheetDialogFragment() {
@Preview
@Composable @Composable
override fun SheetContent() { override fun SheetContent() {
val annotatedText = buildAnnotatedString { val annotatedText = buildClickableString()
withStyle(SpanStyle(fontSize = 28.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface)) {
return Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(Alignment.Center)
.padding(16.dp)
) {
Handle()
Text(
text = buildAnnotatedString {
withStyle(SpanStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface)) {
append(stringResource(R.string.RegistrationActivity_support_bottom_sheet_title)) append(stringResource(R.string.RegistrationActivity_support_bottom_sheet_title))
} }
append(stringResource(R.string.RegistrationActivity_support_bottom_sheet_body_part_1)) },
modifier = Modifier.padding(8.dp)
)
Text(
text = stringResource(R.string.RegistrationActivity_support_bottom_sheet_body_suggestions),
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(8.dp)
)
ClickableText(
text = annotatedText,
onClick = { offset ->
annotatedText.getStringAnnotations(
tag = "URL",
start = offset,
end = offset
)
.firstOrNull()?.let { annotation ->
when (annotation.item) {
TROUBLESHOOTING_STEPS_KEY -> openTroubleshootingSteps()
CONTACT_SUPPORT_KEY -> sendEmailToSupport()
}
}
},
modifier = Modifier.padding(8.dp)
)
}
}
@Composable
private fun buildClickableString(): AnnotatedString {
val troubleshootingStepsString = stringResource(R.string.RegistrationActivity_support_bottom_sheet_cta_troubleshooting_steps_substring)
val contactSupportString = stringResource(R.string.RegistrationActivity_support_bottom_sheet_cta_contact_support_substring)
val completeString = stringResource(R.string.RegistrationActivity_support_bottom_sheet_body_call_to_action)
val troubleshootingStartIndex = completeString.indexOf(troubleshootingStepsString)
val troubleshootingEndIndex = troubleshootingStartIndex + troubleshootingStepsString.length
val contactSupportStartIndex = completeString.indexOf(contactSupportString)
val contactSupportEndIndex = contactSupportStartIndex + contactSupportString.length
val doesStringEndWithContactSupport = contactSupportEndIndex >= completeString.lastIndex
return buildAnnotatedString {
withStyle(
style = SpanStyle(
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.Normal
)
) {
append(completeString.substring(0, troubleshootingStartIndex))
}
pushStringAnnotation( pushStringAnnotation(
tag = "URL", tag = "URL",
annotation = TROUBLESHOOTING_STEPS_KEY annotation = TROUBLESHOOTING_STEPS_KEY
@ -41,10 +108,17 @@ class ContactSupportBottomSheetFragment(private val troubleshootingStepsListener
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
) { ) {
append(stringResource(R.string.RegistrationActivity_support_bottom_sheet_body_part_2)) append(troubleshootingStepsString)
} }
pop() pop()
append(stringResource(R.string.RegistrationActivity_support_bottom_sheet_body_part_3)) withStyle(
style = SpanStyle(
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.Normal
)
) {
append(completeString.substring(troubleshootingEndIndex, contactSupportStartIndex))
}
pushStringAnnotation( pushStringAnnotation(
tag = "URL", tag = "URL",
annotation = CONTACT_SUPPORT_KEY annotation = CONTACT_SUPPORT_KEY
@ -55,38 +129,34 @@ class ContactSupportBottomSheetFragment(private val troubleshootingStepsListener
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
) { ) {
append(stringResource(R.string.RegistrationActivity_support_bottom_sheet_body_part_4)) append(contactSupportString)
} }
pop() pop()
if (!doesStringEndWithContactSupport) {
append(completeString.substring(contactSupportEndIndex, completeString.lastIndex))
}
}
} }
return Column( private fun openTroubleshootingSteps() {
modifier = Modifier CommunicationActions.openBrowserLink(requireContext(), getString(R.string.support_center_url))
.fillMaxWidth() }
.wrapContentSize(Alignment.Center)
) { private fun sendEmailToSupport() {
Handle() val body = SupportEmailUtil.generateSupportEmailBody(
ClickableText( requireContext(),
text = annotatedText, R.string.RegistrationActivity_code_support_subject,
onClick = { offset -> null,
// We check if there is an *URL* annotation attached to the text null
// at the clicked position
annotatedText.getStringAnnotations(
tag = "URL",
start = offset,
end = offset
) )
.firstOrNull()?.let { annotation -> CommunicationActions.openEmail(
when (annotation.item) { requireContext(),
TROUBLESHOOTING_STEPS_KEY -> troubleshootingStepsListener.run() SupportEmailUtil.getSupportEmailAddress(requireContext()),
CONTACT_SUPPORT_KEY -> contactSupportListener.run() getString(R.string.RegistrationActivity_code_support_subject),
} body
}
},
modifier = Modifier.padding(16.dp)
) )
} }
}
companion object { companion object {
private const val TROUBLESHOOTING_STEPS_KEY = "troubleshooting" private const val TROUBLESHOOTING_STEPS_KEY = "troubleshooting"
private const val CONTACT_SUPPORT_KEY = "contact_support" private const val CONTACT_SUPPORT_KEY = "contact_support"

Wyświetl plik

@ -40,7 +40,7 @@ public abstract class BaseRegistrationViewModel extends ViewModel {
private static final String STATE_REGISTRATION_SECRET = "REGISTRATION_SECRET"; private static final String STATE_REGISTRATION_SECRET = "REGISTRATION_SECRET";
private static final String STATE_VERIFICATION_CODE = "TEXT_CODE_ENTERED"; private static final String STATE_VERIFICATION_CODE = "TEXT_CODE_ENTERED";
private static final String STATE_CAPTCHA = "CAPTCHA"; private static final String STATE_CAPTCHA = "CAPTCHA";
private static final String STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS = "SUCCESSFUL_CODE_REQUEST_ATTEMPTS"; private static final String STATE_INCORRECT_CODE_ATTEMPTS = "STATE_INCORRECT_CODE_ATTEMPTS";
private static final String STATE_REQUEST_RATE_LIMITER = "REQUEST_RATE_LIMITER"; private static final String STATE_REQUEST_RATE_LIMITER = "REQUEST_RATE_LIMITER";
private static final String STATE_KBS_TOKEN = "KBS_TOKEN"; private static final String STATE_KBS_TOKEN = "KBS_TOKEN";
private static final String STATE_TIME_REMAINING = "TIME_REMAINING"; private static final String STATE_TIME_REMAINING = "TIME_REMAINING";
@ -65,7 +65,7 @@ public abstract class BaseRegistrationViewModel extends ViewModel {
setInitialDefaultValue(STATE_NUMBER, NumberViewState.INITIAL); setInitialDefaultValue(STATE_NUMBER, NumberViewState.INITIAL);
setInitialDefaultValue(STATE_REGISTRATION_SECRET, password); setInitialDefaultValue(STATE_REGISTRATION_SECRET, password);
setInitialDefaultValue(STATE_VERIFICATION_CODE, ""); setInitialDefaultValue(STATE_VERIFICATION_CODE, "");
setInitialDefaultValue(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, 0); setInitialDefaultValue(STATE_INCORRECT_CODE_ATTEMPTS, 0);
setInitialDefaultValue(STATE_REQUEST_RATE_LIMITER, new LocalCodeRequestRateLimiter(60_000)); setInitialDefaultValue(STATE_REQUEST_RATE_LIMITER, new LocalCodeRequestRateLimiter(60_000));
setInitialDefaultValue(STATE_RECOVERY_PASSWORD, SignalStore.kbsValues().getRecoveryPassword()); setInitialDefaultValue(STATE_RECOVERY_PASSWORD, SignalStore.kbsValues().getRecoveryPassword());
} }
@ -160,13 +160,13 @@ public abstract class BaseRegistrationViewModel extends ViewModel {
savedState.set(STATE_VERIFICATION_CODE, code); savedState.set(STATE_VERIFICATION_CODE, code);
} }
public void markASuccessfulAttempt() { public void incrementIncorrectCodeAttempts() {
//noinspection ConstantConditions //noinspection ConstantConditions
savedState.set(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, (Integer) savedState.get(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS) + 1); savedState.set(STATE_INCORRECT_CODE_ATTEMPTS, (Integer) savedState.get(STATE_INCORRECT_CODE_ATTEMPTS) + 1);
} }
public LiveData<Integer> getSuccessfulCodeRequestAttempts() { public LiveData<Integer> getIncorrectCodeAttempts() {
return savedState.getLiveData(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, 0); return savedState.getLiveData(STATE_INCORRECT_CODE_ATTEMPTS, 0);
} }
public @Nullable TokenData getKeyBackupCurrentToken() { public @Nullable TokenData getKeyBackupCurrentToken() {
@ -244,10 +244,6 @@ public abstract class BaseRegistrationViewModel extends ViewModel {
}) })
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnSuccess((RegistrationSessionProcessor processor) -> { .doOnSuccess((RegistrationSessionProcessor processor) -> {
if (processor.hasResult()) {
markASuccessfulAttempt();
}
if (processor.hasResult() && processor.isAllowedToRequestCode()) { if (processor.hasResult() && processor.isAllowedToRequestCode()) {
setCanSmsAtTime(processor.getNextCodeViaSmsAttempt()); setCanSmsAtTime(processor.getNextCodeViaSmsAttempt());
setCanCallAtTime(processor.getNextCodeViaCallAttempt()); setCanCallAtTime(processor.getNextCodeViaCallAttempt());

Wyświetl plik

@ -101,6 +101,20 @@
app:layout_constraintVertical_bias="1.0" app:layout_constraintVertical_bias="1.0"
tools:text="@string/RegistrationActivity_resend_code" /> tools:text="@string/RegistrationActivity_resend_code" />
<com.google.android.material.button.MaterialButton
android:id="@+id/having_trouble_button"
style="@style/Signal.Widget.Button.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/RegistrationActivity_support_bottom_sheet_title"
android:textColor="@color/signal_colorPrimary"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/code" />
<org.thoughtcrime.securesms.components.registration.VerificationCodeView <org.thoughtcrime.securesms.components.registration.VerificationCodeView
android:id="@+id/code" android:id="@+id/code"
android:layout_width="0dp" android:layout_width="0dp"

Wyświetl plik

@ -1824,11 +1824,16 @@
<string name="RegistrationActivity_call">Call</string> <string name="RegistrationActivity_call">Call</string>
<string name="RegistrationActivity_verification_code">Verification Code</string> <string name="RegistrationActivity_verification_code">Verification Code</string>
<string name="RegistrationActivity_resend_code">Resend Code</string> <string name="RegistrationActivity_resend_code">Resend Code</string>
<!-- A title for a bottom sheet dialog offering to help a user having trouble entering their verification code.-->
<string name="RegistrationActivity_support_bottom_sheet_title">Having trouble registering?</string> <string name="RegistrationActivity_support_bottom_sheet_title">Having trouble registering?</string>
<string name="RegistrationActivity_support_bottom_sheet_body_part_1">• Make sure your phone has a cellular signal to receive your SMS or call\n • Confirm you can receive a phone call to the number\n • Check that you have entered your phone number correctly.\nFor more information, please follow </string> <!-- A list of suggestions to try for a user having trouble entering their verification code.-->
<string name="RegistrationActivity_support_bottom_sheet_body_part_2">these troubleshooting steps</string> <string name="RegistrationActivity_support_bottom_sheet_body_suggestions">• Make sure your phone has a cellular signal to receive your SMS or call\n • Confirm you can receive a phone call to the number\n • Check that you have entered your phone number correctly.</string>
<string name="RegistrationActivity_support_bottom_sheet_body_part_3"> or </string> <!-- A call to action for a user having trouble entering the verification to seek further help. -->
<string name="RegistrationActivity_support_bottom_sheet_body_part_4">Contact Support</string> <string name="RegistrationActivity_support_bottom_sheet_body_call_to_action">For more information, please follow these troubleshooting steps or Contact Support</string>
<!-- A clickable piece of text that will take the user to our website with additional suggestions.-->
<string name="RegistrationActivity_support_bottom_sheet_cta_troubleshooting_steps_substring">these troubleshooting steps</string>
<!-- A clickable piece of text that will pre-fill a request for support email in the user's email app.-->
<string name="RegistrationActivity_support_bottom_sheet_cta_contact_support_substring">Contact Support</string>
<!-- RegistrationLockV2Dialog --> <!-- RegistrationLockV2Dialog -->
<string name="RegistrationLockV2Dialog_turn_on_registration_lock">Turn on Registration Lock?</string> <string name="RegistrationLockV2Dialog_turn_on_registration_lock">Turn on Registration Lock?</string>