diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ca9f077f2a..a4cf8fa174 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -485,6 +485,7 @@ dependencies { implementation(project(":core-ui")) implementation(libs.androidx.fragment.ktx) + implementation(libs.androidx.fragment.compose) implementation(libs.androidx.appcompat) { version { strictly("1.6.1") @@ -609,6 +610,8 @@ dependencies { testImplementation(testFixtures(project(":libsignal-service"))) testImplementation(testLibs.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) androidTestImplementation(testLibs.androidx.test.ext.junit) androidTestImplementation(testLibs.espresso.core) androidTestImplementation(testLibs.androidx.test.core) diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutActivityTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutActivityTest.kt new file mode 100644 index 0000000000..85484442c0 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutActivityTest.kt @@ -0,0 +1,118 @@ +package org.thoughtcrime.securesms.backup.v2.ui.subscription + +import android.content.ClipboardManager +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createEmptyComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToNode +import androidx.core.content.ContextCompat +import androidx.test.core.app.ActivityScenario +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isNull +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockkStatic +import kotlinx.coroutines.flow.MutableSharedFlow +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.signal.core.util.billing.BillingProduct +import org.signal.core.util.billing.BillingPurchaseResult +import org.signal.core.util.money.FiatMoney +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.backup.v2.MessageBackupTier +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.testing.InAppPaymentsRule +import org.thoughtcrime.securesms.testing.SignalActivityRule +import org.thoughtcrime.securesms.util.RemoteConfig +import java.math.BigDecimal +import java.util.Currency + +@RunWith(AndroidJUnit4::class) +class MessageBackupsCheckoutActivityTest { + + @get:Rule val activityRule = SignalActivityRule() + + @get:Rule val iapRule = InAppPaymentsRule() + + @get:Rule val composeTestRule = createEmptyComposeRule() + + private val purchaseResults = MutableSharedFlow() + + @Before + fun setUp() { + every { AppDependencies.billingApi.getBillingPurchaseResults() } returns purchaseResults + coEvery { AppDependencies.billingApi.queryProduct() } returns BillingProduct(price = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD"))) + + mockkStatic(RemoteConfig::class) + every { RemoteConfig.messageBackups } returns true + } + + @Test + fun e2e_happy_path() { + val scenario = launchCheckoutFlow() + val context = InstrumentationRegistry.getInstrumentation().targetContext + + assertThat(SignalStore.backup.backupTier).isNull() + + // Backup education screen + composeTestRule.onNodeWithText(context.getString(R.string.RemoteBackupsSettingsFragment__signal_backups)).assertIsDisplayed() + composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsEducationScreen__enable_backups)).performClick() + + // Key education screen + composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyEducationScreen__your_backup_key)).assertIsDisplayed() + composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__next)).performClick() + + // Key record screen + composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__record_your_backup_key)).assertIsDisplayed() + composeTestRule.onNodeWithTag("message-backups-key-record-screen-lazy-column") + .performScrollToNode(hasText(context.getString(R.string.MessageBackupsKeyRecordScreen__copy_to_clipboard))) + composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__copy_to_clipboard)).performClick() + + scenario.onActivity { + val backupKeyString = SignalStore.account.accountEntropyPool.value.chunked(4).joinToString(" ") + val clipboardManager = ContextCompat.getSystemService(context, ClipboardManager::class.java) + assertThat(clipboardManager?.primaryClip?.getItemAt(0)?.coerceToText(context)).isEqualTo(backupKeyString) + } + + composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__next)).assertIsDisplayed() + composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__next)).performClick() + + // Key record bottom sheet + composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__keep_your_key_safe)).assertIsDisplayed() + composeTestRule.onNodeWithTag("message-backups-key-record-screen-sheet-content") + .performScrollToNode(hasText(context.getString(R.string.MessageBackupsKeyRecordScreen__continue))) + composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__continue)).assertIsNotEnabled() + composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__ive_recorded_my_key)).performClick() + composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__continue)).assertIsEnabled() + composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__continue)).performClick() + + // Type selection screen + composeTestRule.onNodeWithText(context.getString(R.string.MessagesBackupsTypeSelectionScreen__choose_your_backup_plan)).assertIsDisplayed() + composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsTypeSelectionScreen__next)).assertIsNotEnabled() + composeTestRule.onNodeWithTag("message-backups-type-selection-screen-lazy-column") + .performScrollToNode(hasText(context.getString(R.string.MessageBackupsTypeSelectionScreen__free))) + composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsTypeSelectionScreen__free)).performClick() + composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsTypeSelectionScreen__next)).assertIsEnabled() + composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsTypeSelectionScreen__next)).performClick() + composeTestRule.waitForIdle() + + assertThat(SignalStore.backup.backupTier).isEqualTo(MessageBackupTier.FREE) + } + + private fun launchCheckoutFlow(tier: MessageBackupTier? = null): ActivityScenario { + return ActivityScenario.launch( + MessageBackupsCheckoutActivity.Contract().createIntent(InstrumentationRegistry.getInstrumentation().targetContext, tier) + ) + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__RecurringDonations.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__RecurringDonations.kt index 6ceb6dd17d..77f38ba631 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__RecurringDonations.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__RecurringDonations.kt @@ -24,13 +24,12 @@ import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDepende import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.testing.Delete import org.thoughtcrime.securesms.testing.Get +import org.thoughtcrime.securesms.testing.InAppPaymentsRule import org.thoughtcrime.securesms.testing.SignalActivityRule import org.thoughtcrime.securesms.testing.actions.RecyclerViewScrollToBottomAction import org.thoughtcrime.securesms.testing.success -import org.thoughtcrime.securesms.util.JsonUtils import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription import org.whispersystems.signalservice.api.subscriptions.SubscriberId -import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration import java.math.BigDecimal import java.util.Currency import kotlin.time.Duration.Companion.days @@ -42,6 +41,9 @@ class CheckoutFlowActivityTest__RecurringDonations { @get:Rule val harness = SignalActivityRule(othersCount = 10) + @get:Rule + val iapRule = InAppPaymentsRule() + private val intent = CheckoutFlowActivity.createIntent(InstrumentationRegistry.getInstrumentation().targetContext, InAppPaymentType.RECURRING_DONATION) @Test @@ -59,7 +61,6 @@ class CheckoutFlowActivityTest__RecurringDonations { @Test fun givenACurrentDonation_whenILoadScreen_thenIExpectUpgradeButton() { - initialiseConfigurationResponse() initialiseActiveSubscription() ActivityScenario.launch(intent) @@ -71,7 +72,6 @@ class CheckoutFlowActivityTest__RecurringDonations { @Test fun givenACurrentDonation_whenIPressCancel_thenIExpectCancellationDialog() { - initialiseConfigurationResponse() initialiseActiveSubscription() ActivityScenario.launch(intent) @@ -85,7 +85,6 @@ class CheckoutFlowActivityTest__RecurringDonations { @Test fun givenAPendingRecurringDonation_whenILoadScreen_thenIExpectDisabledUpgradeButton() { - initialiseConfigurationResponse() initialisePendingSubscription() ActivityScenario.launch(intent) @@ -94,17 +93,6 @@ class CheckoutFlowActivityTest__RecurringDonations { onView(withText(R.string.SubscribeFragment__update_subscription)).check(matches(isNotEnabled())) } - private fun initialiseConfigurationResponse() { - InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( - Get("/v1/subscription/configuration") { - val assets = InstrumentationRegistry.getInstrumentation().context.resources.assets - assets.open("inAppPaymentsTests/configuration.json").use { stream -> - MockResponse().success(JsonUtils.fromJson(stream, SubscriptionsConfiguration::class.java)) - } - } - ) - } - private fun initialiseActiveSubscription() { val currency = Currency.getInstance("USD") val subscriber = InAppPaymentSubscriberRecord( diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/InAppPaymentsRule.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/InAppPaymentsRule.kt new file mode 100644 index 0000000000..ad0b325936 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/InAppPaymentsRule.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.testing + +import androidx.test.platform.app.InstrumentationRegistry +import okhttp3.mockwebserver.MockResponse +import org.junit.rules.ExternalResource +import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider +import org.thoughtcrime.securesms.util.JsonUtils +import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration + +/** + * Sets up some common infrastructure for on-device InAppPayment testing + */ +class InAppPaymentsRule : ExternalResource() { + override fun before() { + initialiseConfigurationResponse() + } + + private fun initialiseConfigurationResponse() { + InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( + Get("/v1/subscription/configuration") { + val assets = InstrumentationRegistry.getInstrumentation().context.resources.assets + assets.open("inAppPaymentsTests/configuration.json").use { stream -> + MockResponse().success(JsonUtils.fromJson(stream, SubscriptionsConfiguration::class.java)) + } + } + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsEducationScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsEducationScreen.kt index f534fac315..abdd0157fa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsEducationScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsEducationScreen.kt @@ -46,6 +46,7 @@ fun MessageBackupsEducationScreen( ) { Scaffolds.Settings( onNavigationClick = onNavigationClick, + navigationContentDescription = stringResource(android.R.string.cancel), navigationIconPainter = painterResource(id = R.drawable.symbol_x_24), title = "" ) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt index d29cede52d..42f40036f9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt @@ -9,6 +9,7 @@ import android.app.Activity import android.os.Bundle import android.view.View import androidx.activity.OnBackPressedCallback +import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -20,12 +21,14 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import kotlinx.coroutines.rx3.asFlowable import org.signal.core.util.getSerializableCompat +import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentCheckoutDelegate import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.compose.Nav import org.thoughtcrime.securesms.database.InAppPaymentTable import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.viewModel @@ -36,7 +39,8 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega companion object { - private const val TIER = "tier" + @VisibleForTesting + const val TIER = "tier" fun create(messageBackupTier: MessageBackupTier?): MessageBackupsFlowFragment { return MessageBackupsFlowFragment().apply { @@ -88,7 +92,9 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega MessageBackupsEducationScreen( onNavigationClick = viewModel::goToPreviousStage, onEnableBackups = viewModel::goToNextStage, - onLearnMore = {} + onLearnMore = { + CommunicationActions.openBrowserLink(requireContext(), getString(R.string.backup_support_url)) + } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt index 1c79475757..0238d9df10 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3Api @@ -33,6 +34,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -70,6 +72,10 @@ fun MessageBackupsKeyRecordScreen( skipPartiallyExpanded = true ) + val backupKeyString = remember(backupKey) { + backupKey.chunked(4).joinToString(" ") + } + Scaffolds.Settings( title = "", navigationIconPainter = painterResource(R.drawable.symbol_arrow_left_24), @@ -82,67 +88,79 @@ fun MessageBackupsKeyRecordScreen( .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally ) { - Image( - painter = painterResource(R.drawable.image_signal_backups_lock), - contentDescription = null, + LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier - .padding(top = 24.dp) - .size(80.dp) - ) - - Text( - text = stringResource(R.string.MessageBackupsKeyRecordScreen__record_your_backup_key), - style = MaterialTheme.typography.headlineMedium, - modifier = Modifier.padding(top = 16.dp) - ) - - Text( - text = stringResource(R.string.MessageBackupsKeyRecordScreen__this_key_is_required_to_recover), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = 12.dp) - ) - - val backupKeyString = remember(backupKey) { - backupKey.chunked(4).joinToString(" ") - } - - Box( - modifier = Modifier - .padding(top = 24.dp, bottom = 16.dp) - .background( - color = SignalTheme.colors.colorSurface1, - shape = RoundedCornerShape(10.dp) + .weight(1f) + .testTag("message-backups-key-record-screen-lazy-column") + ) { + item { + Image( + painter = painterResource(R.drawable.image_signal_backups_lock), + contentDescription = null, + modifier = Modifier + .padding(top = 24.dp) + .size(80.dp) ) - .padding(24.dp) - ) { - Text( - text = backupKeyString, - style = MaterialTheme.typography.bodyLarge - .copy( - fontSize = 18.sp, - fontWeight = FontWeight(400), - letterSpacing = 1.44.sp, - lineHeight = 36.sp, - textAlign = TextAlign.Center, - fontFamily = FontFamily.Monospace - ) - ) - } + } - Buttons.Small( - onClick = { onCopyToClipboardClick(backupKeyString) } - ) { - Text( - text = stringResource(R.string.MessageBackupsKeyRecordScreen__copy_to_clipboard) - ) + item { + Text( + text = stringResource(R.string.MessageBackupsKeyRecordScreen__record_your_backup_key), + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(top = 16.dp) + ) + } + + item { + Text( + text = stringResource(R.string.MessageBackupsKeyRecordScreen__this_key_is_required_to_recover), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 12.dp) + ) + } + + item { + Box( + modifier = Modifier + .padding(top = 24.dp, bottom = 16.dp) + .background( + color = SignalTheme.colors.colorSurface1, + shape = RoundedCornerShape(10.dp) + ) + .padding(24.dp) + ) { + Text( + text = backupKeyString, + style = MaterialTheme.typography.bodyLarge + .copy( + fontSize = 18.sp, + fontWeight = FontWeight(400), + letterSpacing = 1.44.sp, + lineHeight = 36.sp, + textAlign = TextAlign.Center, + fontFamily = FontFamily.Monospace + ) + ) + } + } + + item { + Buttons.Small( + onClick = { onCopyToClipboardClick(backupKeyString) } + ) { + Text( + text = stringResource(R.string.MessageBackupsKeyRecordScreen__copy_to_clipboard) + ) + } + } } Box( modifier = Modifier .fillMaxWidth() - .weight(1f) .padding(bottom = 24.dp) ) { Buttons.LargeTonal( @@ -189,66 +207,80 @@ private fun BottomSheetContent( ) { var checked by remember { mutableStateOf(false) } - Column( + LazyColumn( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .fillMaxWidth() .padding(horizontal = dimensionResource(CoreUiR.dimen.gutter)) + .testTag("message-backups-key-record-screen-sheet-content") ) { - BottomSheets.Handle() - Text( - text = stringResource(R.string.MessageBackupsKeyRecordScreen__keep_your_key_safe), - style = MaterialTheme.typography.titleLarge, - textAlign = TextAlign.Center, - modifier = Modifier.padding(top = 30.dp) - ) - - Text( - text = stringResource(R.string.MessageBackupsKeyRecordScreen__signal_will_not), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - modifier = Modifier.padding(top = 12.dp) - ) - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(vertical = 24.dp) - .defaultMinSize(minWidth = 220.dp) - .clip(shape = RoundedCornerShape(percent = 50)) - .clickable(onClick = { checked = !checked }) - ) { - Checkbox( - checked = checked, - onCheckedChange = { checked = it } - ) + item { + BottomSheets.Handle() + } + item { Text( - text = stringResource(R.string.MessageBackupsKeyRecordScreen__ive_recorded_my_key), - style = MaterialTheme.typography.bodyLarge + text = stringResource(R.string.MessageBackupsKeyRecordScreen__keep_your_key_safe), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 30.dp) ) } - Buttons.LargeTonal( - enabled = checked, - onClick = onContinueClick, - modifier = Modifier - .padding(bottom = 16.dp) - .defaultMinSize(minWidth = 220.dp) - ) { - Text(text = stringResource(R.string.MessageBackupsKeyRecordScreen__continue)) + item { + Text( + text = stringResource(R.string.MessageBackupsKeyRecordScreen__signal_will_not), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 12.dp) + ) } - TextButton( - onClick = onSeeKeyAgainClick, - modifier = Modifier - .padding(bottom = 24.dp) - .defaultMinSize(minWidth = 220.dp) - ) { - Text( - text = stringResource(R.string.MessageBackupsKeyRecordScreen__see_key_again) - ) + item { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(vertical = 24.dp) + .defaultMinSize(minWidth = 220.dp) + .clip(shape = RoundedCornerShape(percent = 50)) + .clickable(onClick = { checked = !checked }) + ) { + Checkbox( + checked = checked, + onCheckedChange = { checked = it } + ) + + Text( + text = stringResource(R.string.MessageBackupsKeyRecordScreen__ive_recorded_my_key), + style = MaterialTheme.typography.bodyLarge + ) + } + } + + item { + Buttons.LargeTonal( + enabled = checked, + onClick = onContinueClick, + modifier = Modifier + .padding(bottom = 16.dp) + .defaultMinSize(minWidth = 220.dp) + ) { + Text(text = stringResource(R.string.MessageBackupsKeyRecordScreen__continue)) + } + } + + item { + TextButton( + onClick = onSeeKeyAgainClick, + modifier = Modifier + .padding(bottom = 24.dp) + .defaultMinSize(minWidth = 220.dp) + ) { + Text( + text = stringResource(R.string.MessageBackupsKeyRecordScreen__see_key_again) + ) + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt index 312cd72a07..9f8687f288 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource @@ -93,6 +94,7 @@ fun MessageBackupsTypeSelectionScreen( modifier = Modifier .fillMaxWidth() .weight(1f) + .testTag("message-backups-type-selection-screen-lazy-column") ) { item { Image( diff --git a/app/src/main/java/org/thoughtcrime/securesms/compose/ComposeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/compose/ComposeFragment.kt index ea368d5e90..9b742676df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/compose/ComposeFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/compose/ComposeFragment.kt @@ -5,9 +5,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.compose.content import org.signal.core.ui.theme.SignalTheme import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.util.DynamicTheme @@ -16,16 +15,11 @@ import org.thoughtcrime.securesms.util.DynamicTheme * Generic ComposeFragment which can be subclassed to build UI with compose. */ abstract class ComposeFragment : LoggingFragment() { - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - SignalTheme( - isDarkMode = DynamicTheme.isDarkTheme(LocalContext.current) - ) { - FragmentContent() - } - } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = content { + SignalTheme( + isDarkMode = DynamicTheme.isDarkTheme(LocalContext.current) + ) { + FragmentContent() } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dd2b0fd238..66cd33e277 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,7 @@ androidx-compose-material3 = { module = "androidx.compose.material3:material3" } androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } androidx-compose-ui-tooling-core = { module = "androidx.compose.ui:ui-tooling" } androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } +androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" } androidx-compose-rxjava3 = "androidx.compose.runtime:runtime-rxjava3:1.7.6" @@ -71,6 +72,7 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "a androidx-core-ktx = "androidx.core:core-ktx:1.15.0" androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "androidx-fragment" } androidx-fragment-testing = { module = "androidx.fragment:fragment-testing", version.ref = "androidx-fragment" } +androidx-fragment-compose = { module = "androidx.fragment:fragment-compose", version.ref = "androidx-fragment" } androidx-annotation = "androidx.annotation:annotation:1.9.1" androidx-constraintlayout = "androidx.constraintlayout:constraintlayout:2.2.0" androidx-window-window = { module = "androidx.window:window", version.ref = "androidx-window" } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index f74824a0a0..0e53bee0f7 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -703,6 +703,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -777,6 +782,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -873,6 +883,38 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1086,6 +1128,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -1115,6 +1162,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -1297,6 +1349,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2527,11 +2587,21 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + @@ -6146,6 +6216,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -6154,6 +6232,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + +