diff --git a/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherActivity.kt b/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherActivity.kt index 099e3fc5171..e940ccbb5e2 100644 --- a/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherActivity.kt +++ b/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherActivity.kt @@ -18,7 +18,7 @@ import com.stripe.android.StripePaymentController.Companion.SETUP_REQUEST_CODE import com.stripe.android.model.PaymentMethodCreateParams import com.stripe.android.model.StripeIntent import com.stripe.android.payments.core.analytics.ErrorReporter -import com.stripe.android.utils.fadeOut +import com.stripe.android.uicore.utils.fadeOut import com.stripe.android.view.AuthActivityStarterHost import kotlinx.coroutines.launch import org.json.JSONObject diff --git a/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncherActivity.kt b/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncherActivity.kt index 96e0ecbdf35..27e6b68be60 100644 --- a/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncherActivity.kt +++ b/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayPaymentMethodLauncherActivity.kt @@ -13,7 +13,7 @@ import com.google.android.gms.wallet.contract.ApiTaskResult import com.google.android.gms.wallet.contract.TaskResultContracts.GetPaymentDataResult import com.stripe.android.model.PaymentMethod import com.stripe.android.payments.core.analytics.ErrorReporter -import com.stripe.android.utils.fadeOut +import com.stripe.android.uicore.utils.fadeOut import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext diff --git a/payments-core/src/main/java/com/stripe/android/payments/paymentlauncher/PaymentLauncherConfirmationActivity.kt b/payments-core/src/main/java/com/stripe/android/payments/paymentlauncher/PaymentLauncherConfirmationActivity.kt index aac9b5ef243..42555d74367 100644 --- a/payments-core/src/main/java/com/stripe/android/payments/paymentlauncher/PaymentLauncherConfirmationActivity.kt +++ b/payments-core/src/main/java/com/stripe/android/payments/paymentlauncher/PaymentLauncherConfirmationActivity.kt @@ -11,7 +11,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import com.stripe.android.core.exception.StripeException import com.stripe.android.payments.core.analytics.ErrorReporter -import com.stripe.android.utils.fadeOut +import com.stripe.android.uicore.utils.fadeOut import com.stripe.android.view.AuthActivityStarterHost import kotlinx.coroutines.launch diff --git a/payments-core/src/main/java/com/stripe/android/utils/AnimationConstants.kt b/payments-core/src/main/java/com/stripe/android/utils/AnimationConstants.kt deleted file mode 100644 index 49cd2d4adf4..00000000000 --- a/payments-core/src/main/java/com/stripe/android/utils/AnimationConstants.kt +++ /dev/null @@ -1,31 +0,0 @@ -@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - -package com.stripe.android.utils - -import android.app.Activity -import android.app.Activity.OVERRIDE_TRANSITION_CLOSE -import android.os.Build -import androidx.annotation.AnimRes -import androidx.annotation.RestrictTo -import com.stripe.android.R -import com.stripe.android.utils.AnimationConstants.FADE_IN -import com.stripe.android.utils.AnimationConstants.FADE_OUT - -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -object AnimationConstants { - @AnimRes - val FADE_IN = R.anim.stripe_paymentsheet_transition_fade_in - - @AnimRes - val FADE_OUT = R.anim.stripe_paymentsheet_transition_fade_out -} - -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -fun Activity.fadeOut() { - if (Build.VERSION.SDK_INT >= 34) { - overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, FADE_IN, FADE_OUT) - } else { - @Suppress("DEPRECATION") - overridePendingTransition(FADE_IN, FADE_OUT) - } -} diff --git a/paymentsheet/api/paymentsheet.api b/paymentsheet/api/paymentsheet.api index 2f681155001..48bf55dd768 100644 --- a/paymentsheet/api/paymentsheet.api +++ b/paymentsheet/api/paymentsheet.api @@ -1,13 +1,6 @@ public abstract interface annotation class com/stripe/android/ExperimentalAllowsRemovalOfLastSavedPaymentMethodApi : java/lang/annotation/Annotation { } -public final class com/stripe/android/common/ui/ComposableSingletons$BottomSheetKt { - public static final field INSTANCE Lcom/stripe/android/common/ui/ComposableSingletons$BottomSheetKt; - public static field lambda-1 Lkotlin/jvm/functions/Function2; - public fun ()V - public final fun getLambda-1$paymentsheet_release ()Lkotlin/jvm/functions/Function2; -} - public abstract interface class com/stripe/android/customersheet/CustomerAdapter { public static final field Companion Lcom/stripe/android/customersheet/CustomerAdapter$Companion; public abstract fun attachPaymentMethod (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/paymentsheet/detekt-baseline.xml b/paymentsheet/detekt-baseline.xml index 638e23e084f..783225cd0aa 100644 --- a/paymentsheet/detekt-baseline.xml +++ b/paymentsheet/detekt-baseline.xml @@ -39,7 +39,6 @@ LongMethod:USBankAccountForm.kt$@Composable private fun AccountDetailsForm( showCheckbox: Boolean, isProcessing: Boolean, bankName: String?, last4: String?, saveForFutureUseElement: SaveForFutureUseElement, onRemoveAccount: () -> Unit, ) MagicNumber:AutocompleteScreen.kt$0.07f MagicNumber:BaseSheetActivity.kt$BaseSheetActivity$30 - MagicNumber:BottomSheet.kt$BottomSheetState$10 MagicNumber:PaymentMethodsUI.kt$.3f MagicNumber:PaymentMethodsUI.kt$.4f MagicNumber:PaymentMethodsUI.kt$.5f diff --git a/paymentsheet/src/main/java/com/stripe/android/common/ui/BottomSheet.kt b/paymentsheet/src/main/java/com/stripe/android/common/ui/BottomSheet.kt deleted file mode 100644 index 0403cfe1eb0..00000000000 --- a/paymentsheet/src/main/java/com/stripe/android/common/ui/BottomSheet.kt +++ /dev/null @@ -1,207 +0,0 @@ -package com.stripe.android.common.ui - -import android.os.Build -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.MaterialTheme -import androidx.compose.material.ModalBottomSheetDefaults -import androidx.compose.material.ModalBottomSheetLayout -import androidx.compose.material.ModalBottomSheetState -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.ModalBottomSheetValue.Expanded -import androidx.compose.material.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.unit.dp -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.stripe.android.paymentsheet.BuildConfig -import com.stripe.android.uicore.stripeShapes -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.flow.first - -internal const val BottomSheetContentTestTag = "BottomSheetContentTestTag" - -@OptIn(ExperimentalMaterialApi::class) -internal class BottomSheetState( - val modalBottomSheetState: ModalBottomSheetState, - val keyboardHandler: BottomSheetKeyboardHandler, - val sheetGesturesEnabled: Boolean, -) { - - private var dismissalType: DismissalType? = null - - suspend fun show() { - repeatUntilSucceededOrLimit(10) { - // Showing the bottom sheet can be interrupted. - // We keep trying until it's fully displayed. - modalBottomSheetState.show() - } - - // Ensure that isVisible is being updated correctly inside ModalBottomSheetState - snapshotFlow { modalBottomSheetState.isVisible }.first { isVisible -> isVisible } - } - - suspend fun awaitDismissal(): DismissalType { - snapshotFlow { modalBottomSheetState.isVisible }.first { isVisible -> !isVisible } - return dismissalType ?: DismissalType.SwipedDownByUser - } - - suspend fun hide() { - if (skipHideAnimation) { - return - } - - dismissalType = DismissalType.Programmatically - // We dismiss the keyboard before we dismiss the sheet. This looks cleaner and prevents - // a CancellationException. - keyboardHandler.dismiss() - if (modalBottomSheetState.isVisible) { - repeatUntilSucceededOrLimit(10) { - // Hiding the bottom sheet can be interrupted. - // We keep trying until it's fully hidden. - modalBottomSheetState.hide() - } - } - } - - internal enum class DismissalType { - Programmatically, - SwipedDownByUser, - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -internal fun rememberBottomSheetState( - confirmValueChange: (ModalBottomSheetValue) -> Boolean = { true }, -): BottomSheetState { - val modalBottomSheetState = rememberModalBottomSheetState( - initialValue = ModalBottomSheetValue.Hidden, - confirmValueChange = confirmValueChange, - skipHalfExpanded = true, - animationSpec = tween(), - ) - - val keyboardHandler = rememberBottomSheetKeyboardHandler() - - return remember { - BottomSheetState( - modalBottomSheetState = modalBottomSheetState, - keyboardHandler = keyboardHandler, - sheetGesturesEnabled = false, - ) - } -} - -/** - * Renders the provided [sheetContent] in a modal bottom sheet. - * - * @param state The [BottomSheetState] that controls the visibility of the bottom sheet. - * navigate to a specific screen. - * @param onDismissed Called when the user dismisses the bottom sheet by swiping down. You should - * inform your view model about this change. - */ -@OptIn(ExperimentalMaterialApi::class) -@Composable -internal fun BottomSheet( - state: BottomSheetState, - modifier: Modifier = Modifier, - onDismissed: () -> Unit, - sheetContent: @Composable () -> Unit, -) { - val systemUiController = rememberSystemUiController() - val scrimColor = ModalBottomSheetDefaults.scrimColor - - val isExpanded = state.modalBottomSheetState.targetValue == Expanded - - val statusBarColorAlpha by animateFloatAsState( - targetValue = if (isExpanded) scrimColor.alpha else 0f, - animationSpec = tween(), - label = "StatusBarColorAlpha", - ) - - LaunchedEffect(Unit) { - state.show() - - val dismissalType = state.awaitDismissal() - if (dismissalType == BottomSheetState.DismissalType.SwipedDownByUser) { - onDismissed() - } - } - - LaunchedEffect(systemUiController, statusBarColorAlpha) { - systemUiController.setStatusBarColor( - color = scrimColor.copy(statusBarColorAlpha), - darkIcons = false, - ) - } - - LaunchedEffect(systemUiController) { - systemUiController.setNavigationBarColor( - color = Color.Transparent, - darkIcons = false, - ) - } - - ModalBottomSheetLayout( - modifier = modifier - .statusBarsPadding() - .imePadding(), - sheetState = state.modalBottomSheetState, - sheetShape = RoundedCornerShape( - topStart = MaterialTheme.stripeShapes.cornerRadius.dp, - topEnd = MaterialTheme.stripeShapes.cornerRadius.dp, - ), - sheetGesturesEnabled = state.sheetGesturesEnabled, - sheetElevation = 0.dp, - sheetContent = { - Box(modifier = Modifier.testTag(BottomSheetContentTestTag)) { - sheetContent() - } - }, - content = {}, - ) -} - -private suspend fun repeatUntilSucceededOrLimit( - limit: Int, - block: suspend () -> Unit -) { - var counter = 0 - while (counter < limit) { - try { - block() - break - } catch (ignored: CancellationException) { - counter += 1 - } - } -} - -private val skipHideAnimation: Boolean - get() = BuildConfig.DEBUG && (isRunningUnitTest || isRunningUiTest) - -private val isRunningUnitTest: Boolean - get() { - return runCatching { - Build.FINGERPRINT.lowercase() == "robolectric" - }.getOrDefault(false) - } - -private val isRunningUiTest: Boolean - get() { - return runCatching { - Class.forName("androidx.test.InstrumentationRegistry") - }.isSuccess - } diff --git a/paymentsheet/src/main/java/com/stripe/android/common/ui/BottomSheetKeyboardHandler.kt b/paymentsheet/src/main/java/com/stripe/android/common/ui/BottomSheetKeyboardHandler.kt index d20cdf2d014..eacc52cf8cc 100644 --- a/paymentsheet/src/main/java/com/stripe/android/common/ui/BottomSheetKeyboardHandler.kt +++ b/paymentsheet/src/main/java/com/stripe/android/common/ui/BottomSheetKeyboardHandler.kt @@ -1,5 +1,7 @@ package com.stripe.android.common.ui +import android.os.Build +import androidx.annotation.RestrictTo import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.ime import androidx.compose.runtime.Composable @@ -9,14 +11,23 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalTextInputService import androidx.compose.ui.text.input.TextInputService +import com.stripe.android.uicore.BuildConfig +import com.stripe.android.uicore.elements.bottomsheet.StripeBottomSheetDismissalInterceptor import kotlinx.coroutines.flow.first -internal class BottomSheetKeyboardHandler( +@RestrictTo(RestrictTo.Scope.LIBRARY) +class BottomSheetKeyboardHandler internal constructor( private val textInputService: TextInputService?, private val isKeyboardVisible: State, -) { +) : StripeBottomSheetDismissalInterceptor { - suspend fun dismiss() { + override suspend fun onBottomSheetDismissal() { + if (skipHideAnimation) { + return + } + + // We dismiss the keyboard before we dismiss the sheet. This looks cleaner and prevents + // a CancellationException. if (isKeyboardVisible.value) { @Suppress("DEPRECATION") textInputService?.hideSoftwareKeyboard() @@ -29,10 +40,28 @@ internal class BottomSheetKeyboardHandler( } } +@RestrictTo(RestrictTo.Scope.LIBRARY) @Composable -internal fun rememberBottomSheetKeyboardHandler(): BottomSheetKeyboardHandler { +fun rememberBottomSheetKeyboardHandler(): BottomSheetKeyboardHandler { val imeHeight = WindowInsets.ime.getBottom(LocalDensity.current) val isImeVisibleState = rememberUpdatedState(newValue = imeHeight > 0) val textInputService = LocalTextInputService.current return BottomSheetKeyboardHandler(textInputService, isImeVisibleState) } + +private val skipHideAnimation: Boolean + get() = BuildConfig.DEBUG && (isRunningUnitTest || isRunningUiTest) + +private val isRunningUnitTest: Boolean + get() { + return runCatching { + Build.FINGERPRINT.lowercase() == "robolectric" + }.getOrDefault(false) + } + +private val isRunningUiTest: Boolean + get() { + return runCatching { + Class.forName("androidx.test.InstrumentationRegistry") + }.isSuccess + } diff --git a/paymentsheet/src/main/java/com/stripe/android/common/ui/ElementsBottomSheetLayout.kt b/paymentsheet/src/main/java/com/stripe/android/common/ui/ElementsBottomSheetLayout.kt new file mode 100644 index 00000000000..2437968b2aa --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/common/ui/ElementsBottomSheetLayout.kt @@ -0,0 +1,47 @@ +package com.stripe.android.common.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.stripe.android.uicore.elements.bottomsheet.StripeBottomSheetLayout +import com.stripe.android.uicore.elements.bottomsheet.StripeBottomSheetState +import com.stripe.android.uicore.elements.bottomsheet.rememberStripeBottomSheetLayoutInfo + +@Composable +internal fun ElementsBottomSheetLayout( + state: StripeBottomSheetState, + modifier: Modifier = Modifier, + onDismissed: () -> Unit, + content: @Composable () -> Unit, +) { + val systemUiController = rememberSystemUiController() + val keyboardHandler = rememberBottomSheetKeyboardHandler() + val layoutInfo = rememberStripeBottomSheetLayoutInfo() + + LaunchedEffect(Unit) { + state.addInterceptor(keyboardHandler) + } + + LaunchedEffect(systemUiController) { + systemUiController.setNavigationBarColor( + color = Color.Transparent, + darkIcons = false, + ) + } + + StripeBottomSheetLayout( + state = state, + layoutInfo = layoutInfo, + modifier = modifier, + onUpdateStatusBarColor = { color -> + systemUiController.setStatusBarColor( + color = color, + darkIcons = false, + ) + }, + onDismissed = onDismissed, + sheetContent = content, + ) +} diff --git a/paymentsheet/src/main/java/com/stripe/android/customersheet/CustomerSheet.kt b/paymentsheet/src/main/java/com/stripe/android/customersheet/CustomerSheet.kt index 2da780933d5..8b7c306a87a 100644 --- a/paymentsheet/src/main/java/com/stripe/android/customersheet/CustomerSheet.kt +++ b/paymentsheet/src/main/java/com/stripe/android/customersheet/CustomerSheet.kt @@ -16,7 +16,7 @@ import com.stripe.android.paymentsheet.PaymentSheet import com.stripe.android.paymentsheet.model.PaymentOptionFactory import com.stripe.android.paymentsheet.model.PaymentSelection import com.stripe.android.uicore.image.StripeImageLoader -import com.stripe.android.utils.AnimationConstants +import com.stripe.android.uicore.utils.AnimationConstants import dev.drewhamilton.poko.Poko import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope diff --git a/paymentsheet/src/main/java/com/stripe/android/customersheet/CustomerSheetActivity.kt b/paymentsheet/src/main/java/com/stripe/android/customersheet/CustomerSheetActivity.kt index 23dcab0f9a9..c2dd0f5070d 100644 --- a/paymentsheet/src/main/java/com/stripe/android/customersheet/CustomerSheetActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/customersheet/CustomerSheetActivity.kt @@ -14,11 +14,12 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.core.view.WindowCompat import androidx.lifecycle.ViewModelProvider -import com.stripe.android.common.ui.BottomSheet -import com.stripe.android.common.ui.rememberBottomSheetState +import com.stripe.android.common.ui.ElementsBottomSheetLayout +import com.stripe.android.customersheet.CustomerSheetViewAction.OnDismissed import com.stripe.android.customersheet.ui.CustomerSheetScreen import com.stripe.android.uicore.StripeTheme -import com.stripe.android.utils.fadeOut +import com.stripe.android.uicore.elements.bottomsheet.rememberStripeBottomSheetState +import com.stripe.android.uicore.utils.fadeOut internal class CustomerSheetActivity : AppCompatActivity() { @@ -66,7 +67,7 @@ internal class CustomerSheetActivity : AppCompatActivity() { setContent { StripeTheme { - val bottomSheetState = rememberBottomSheetState( + val bottomSheetState = rememberStripeBottomSheetState( confirmValueChange = { if (it == ModalBottomSheetValue.Hidden) { viewModel.bottomSheetConfirmStateChange() @@ -90,9 +91,9 @@ internal class CustomerSheetActivity : AppCompatActivity() { viewModel.handleViewAction(CustomerSheetViewAction.OnBackPressed) } - BottomSheet( + ElementsBottomSheetLayout( state = bottomSheetState, - onDismissed = { viewModel.handleViewAction(CustomerSheetViewAction.OnDismissed) }, + onDismissed = { viewModel.handleViewAction(OnDismissed) }, ) { CustomerSheetScreen( viewState = viewState, diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/DefaultPaymentSheetLauncher.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/DefaultPaymentSheetLauncher.kt index 1acba43c888..5b2d8e721b1 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/DefaultPaymentSheetLauncher.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/DefaultPaymentSheetLauncher.kt @@ -9,7 +9,7 @@ import androidx.core.app.ActivityOptionsCompat import androidx.fragment.app.Fragment import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner -import com.stripe.android.utils.AnimationConstants +import com.stripe.android.uicore.utils.AnimationConstants import org.jetbrains.annotations.TestOnly /** diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsActivity.kt index a2e0b643752..2d6027a5892 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsActivity.kt @@ -10,13 +10,13 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.lifecycle.ViewModelProvider -import com.stripe.android.common.ui.BottomSheet -import com.stripe.android.common.ui.rememberBottomSheetState +import com.stripe.android.common.ui.ElementsBottomSheetLayout import com.stripe.android.paymentsheet.ui.BaseSheetActivity import com.stripe.android.paymentsheet.ui.PaymentSheetFlowType.Custom import com.stripe.android.paymentsheet.ui.PaymentSheetScreen import com.stripe.android.paymentsheet.utils.applicationIsTaskOwner import com.stripe.android.uicore.StripeTheme +import com.stripe.android.uicore.elements.bottomsheet.rememberStripeBottomSheetState import kotlinx.coroutines.flow.filterNotNull /** @@ -53,7 +53,7 @@ internal class PaymentOptionsActivity : BaseSheetActivity() StripeTheme { val isProcessing by viewModel.processing.collectAsState() - val bottomSheetState = rememberBottomSheetState( + val bottomSheetState = rememberStripeBottomSheetState( confirmValueChange = { !isProcessing }, ) @@ -65,7 +65,7 @@ internal class PaymentOptionsActivity : BaseSheetActivity() } } - BottomSheet( + ElementsBottomSheetLayout( state = bottomSheetState, onDismissed = viewModel::onUserCancel, ) { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetActivity.kt index c8092c29d9d..b1eb1d7032a 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetActivity.kt @@ -12,14 +12,14 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope -import com.stripe.android.common.ui.BottomSheet -import com.stripe.android.common.ui.rememberBottomSheetState +import com.stripe.android.common.ui.ElementsBottomSheetLayout import com.stripe.android.googlepaylauncher.GooglePayPaymentMethodLauncherContractV2 import com.stripe.android.paymentsheet.ui.BaseSheetActivity import com.stripe.android.paymentsheet.ui.PaymentSheetFlowType.Complete import com.stripe.android.paymentsheet.ui.PaymentSheetScreen import com.stripe.android.paymentsheet.utils.applicationIsTaskOwner import com.stripe.android.uicore.StripeTheme +import com.stripe.android.uicore.elements.bottomsheet.rememberStripeBottomSheetState import kotlinx.coroutines.flow.filterNotNull internal class PaymentSheetActivity : BaseSheetActivity() { @@ -67,7 +67,7 @@ internal class PaymentSheetActivity : BaseSheetActivity() { StripeTheme { val isProcessing by viewModel.processing.collectAsState() - val bottomSheetState = rememberBottomSheetState( + val bottomSheetState = rememberStripeBottomSheetState( confirmValueChange = { !isProcessing }, ) @@ -79,7 +79,7 @@ internal class PaymentSheetActivity : BaseSheetActivity() { } } - BottomSheet( + ElementsBottomSheetLayout( state = bottomSheetState, onDismissed = viewModel::onUserCancel, ) { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivity.kt index a6e519597fb..66be1731ab3 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivity.kt @@ -19,11 +19,11 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument -import com.stripe.android.common.ui.BottomSheet -import com.stripe.android.common.ui.rememberBottomSheetState +import com.stripe.android.common.ui.ElementsBottomSheetLayout import com.stripe.android.paymentsheet.parseAppearance import com.stripe.android.uicore.StripeTheme -import com.stripe.android.utils.fadeOut +import com.stripe.android.uicore.elements.bottomsheet.rememberStripeBottomSheetState +import com.stripe.android.uicore.utils.fadeOut import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialApi::class) @@ -50,10 +50,11 @@ internal class AddressElementActivity : ComponentActivity() { setContent { val coroutineScope = rememberCoroutineScope() + val navController = rememberNavController() viewModel.navigator.navigationController = navController - val bottomSheetState = rememberBottomSheetState() + val bottomSheetState = rememberStripeBottomSheetState() BackHandler { viewModel.navigator.onBack() @@ -68,7 +69,7 @@ internal class AddressElementActivity : ComponentActivity() { } StripeTheme { - BottomSheet( + ElementsBottomSheetLayout( state = bottomSheetState, onDismissed = viewModel.navigator::dismiss, ) { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressLauncher.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressLauncher.kt index e1a73132000..17b17acbd5c 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressLauncher.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressLauncher.kt @@ -12,7 +12,7 @@ import androidx.core.app.ActivityOptionsCompat import androidx.fragment.app.Fragment import com.stripe.android.paymentsheet.PaymentSheet import com.stripe.android.paymentsheet.addresselement.AddressLauncher.AdditionalFieldsConfiguration.FieldConfiguration -import com.stripe.android.utils.AnimationConstants +import com.stripe.android.uicore.utils.AnimationConstants import kotlinx.parcelize.Parcelize /** diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowController.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowController.kt index 516921bc760..027ce102708 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowController.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowController.kt @@ -64,7 +64,7 @@ import com.stripe.android.paymentsheet.state.PaymentSheetState import com.stripe.android.paymentsheet.ui.SepaMandateContract import com.stripe.android.paymentsheet.ui.SepaMandateResult import com.stripe.android.paymentsheet.utils.canSave -import com.stripe.android.utils.AnimationConstants +import com.stripe.android.uicore.utils.AnimationConstants import dagger.Lazy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/bacs/BacsMandateConfirmationActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/bacs/BacsMandateConfirmationActivity.kt index 498872c8f3d..e5a77a252ba 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/bacs/BacsMandateConfirmationActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/bacs/BacsMandateConfirmationActivity.kt @@ -10,15 +10,16 @@ import androidx.appcompat.app.AppCompatActivity import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.runtime.LaunchedEffect import androidx.core.view.WindowCompat -import com.stripe.android.common.ui.BottomSheet -import com.stripe.android.common.ui.rememberBottomSheetState +import com.stripe.android.common.ui.ElementsBottomSheetLayout import com.stripe.android.paymentsheet.R import com.stripe.android.paymentsheet.parseAppearance +import com.stripe.android.paymentsheet.paymentdatacollection.bacs.BacsMandateConfirmationViewAction.OnBackPressed import com.stripe.android.paymentsheet.ui.PaymentSheetScaffold import com.stripe.android.paymentsheet.ui.PaymentSheetTopBar import com.stripe.android.paymentsheet.ui.PaymentSheetTopBarState import com.stripe.android.uicore.StripeTheme -import com.stripe.android.utils.fadeOut +import com.stripe.android.uicore.elements.bottomsheet.rememberStripeBottomSheetState +import com.stripe.android.uicore.utils.fadeOut import kotlinx.coroutines.flow.collectLatest import com.stripe.android.R as StripeR import com.stripe.android.ui.core.R as StripeUiCoreR @@ -41,14 +42,14 @@ internal class BacsMandateConfirmationActivity : AppCompatActivity() { renderEdgeToEdge() onBackPressedDispatcher.addCallback { - viewModel.handleViewAction(BacsMandateConfirmationViewAction.OnBackPressed) + viewModel.handleViewAction(OnBackPressed) } starterArgs.appearance.parseAppearance() setContent { StripeTheme { - val bottomSheetState = rememberBottomSheetState() + val bottomSheetState = rememberStripeBottomSheetState() LaunchedEffect(bottomSheetState) { viewModel.result.collectLatest { result -> @@ -61,11 +62,9 @@ internal class BacsMandateConfirmationActivity : AppCompatActivity() { } } - BottomSheet( + ElementsBottomSheetLayout( state = bottomSheetState, - onDismissed = { - viewModel.handleViewAction(BacsMandateConfirmationViewAction.OnBackPressed) - } + onDismissed = { viewModel.handleViewAction(OnBackPressed) }, ) { PaymentSheetScaffold( topBar = { @@ -79,7 +78,7 @@ internal class BacsMandateConfirmationActivity : AppCompatActivity() { editMenuLabel = StripeR.string.stripe_edit ), handleBackPressed = { - viewModel.handleViewAction(BacsMandateConfirmationViewAction.OnBackPressed) + viewModel.handleViewAction(OnBackPressed) }, toggleEditing = {}, ) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/polling/PollingActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/polling/PollingActivity.kt index dd580cef470..6be47ad0ea5 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/polling/PollingActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/polling/PollingActivity.kt @@ -14,11 +14,11 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.core.view.WindowCompat import androidx.lifecycle.ViewModelProvider -import com.stripe.android.common.ui.BottomSheet -import com.stripe.android.common.ui.rememberBottomSheetState +import com.stripe.android.common.ui.ElementsBottomSheetLayout import com.stripe.android.payments.PaymentFlowResult import com.stripe.android.uicore.StripeTheme -import com.stripe.android.utils.fadeOut +import com.stripe.android.uicore.elements.bottomsheet.rememberStripeBottomSheetState +import com.stripe.android.uicore.utils.fadeOut import kotlin.time.Duration.Companion.seconds internal class PollingActivity : AppCompatActivity() { @@ -49,7 +49,7 @@ internal class PollingActivity : AppCompatActivity() { StripeTheme { val uiState by viewModel.uiState.collectAsState() - val state = rememberBottomSheetState( + val state = rememberStripeBottomSheetState( confirmValueChange = { proposedValue -> if (proposedValue == ModalBottomSheetValue.Hidden) { uiState.pollingState != PollingState.Active @@ -75,7 +75,7 @@ internal class PollingActivity : AppCompatActivity() { } } - BottomSheet( + ElementsBottomSheetLayout( state = state, onDismissed = { /* Not applicable here */ }, ) { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/polling/PollingAuthenticator.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/polling/PollingAuthenticator.kt index 7a42fc3d737..72dc80d11cc 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/polling/PollingAuthenticator.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/polling/PollingAuthenticator.kt @@ -11,7 +11,7 @@ import com.stripe.android.payments.PaymentFlowResult import com.stripe.android.payments.core.analytics.ErrorReporter import com.stripe.android.payments.core.authentication.PaymentAuthenticator import com.stripe.android.paymentsheet.R -import com.stripe.android.utils.AnimationConstants +import com.stripe.android.uicore.utils.AnimationConstants import com.stripe.android.view.AuthActivityStarterHost private const val UPI_TIME_LIMIT_IN_SECONDS = 5 * 60 diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/BaseSheetActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/BaseSheetActivity.kt index 993c6bc20f0..23b3d1a428a 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/BaseSheetActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/BaseSheetActivity.kt @@ -7,7 +7,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat import com.stripe.android.paymentsheet.LinkHandler import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel -import com.stripe.android.utils.fadeOut +import com.stripe.android.uicore.utils.fadeOut internal abstract class BaseSheetActivity : AppCompatActivity() { abstract val viewModel: BaseSheetViewModel diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/PrimaryButtonAnimator.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/PrimaryButtonAnimator.kt index faeb5bc3187..42b89ed14b9 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/PrimaryButtonAnimator.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/PrimaryButtonAnimator.kt @@ -8,8 +8,7 @@ import android.view.animation.AnimationUtils import androidx.core.animation.doOnEnd import androidx.core.view.isInvisible import androidx.core.view.isVisible -import com.stripe.android.paymentsheet.R -import com.stripe.android.R as StripeR +import com.stripe.android.uicore.R as StripeUiCoreR internal class PrimaryButtonAnimator( private val context: Context @@ -26,7 +25,7 @@ internal class PrimaryButtonAnimator( view.startAnimation( AnimationUtils.loadAnimation( context, - StripeR.anim.stripe_paymentsheet_transition_fade_in + StripeUiCoreR.anim.stripe_transition_fade_in ).also { animation -> animation.setAnimationListener( object : Animation.AnimationListener { @@ -92,7 +91,7 @@ internal class PrimaryButtonAnimator( view.startAnimation( AnimationUtils.loadAnimation( context, - StripeR.anim.stripe_paymentsheet_transition_fade_out + StripeUiCoreR.anim.stripe_transition_fade_out ).also { animation -> animation.setAnimationListener( object : Animation.AnimationListener { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/SepaMandateActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/SepaMandateActivity.kt index c308ec0c4bf..0eca0c3b4dd 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/SepaMandateActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/SepaMandateActivity.kt @@ -22,11 +22,11 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat -import com.stripe.android.common.ui.BottomSheet -import com.stripe.android.common.ui.rememberBottomSheetState +import com.stripe.android.common.ui.ElementsBottomSheetLayout import com.stripe.android.paymentsheet.R import com.stripe.android.ui.core.elements.H4Text import com.stripe.android.uicore.StripeTheme +import com.stripe.android.uicore.elements.bottomsheet.rememberStripeBottomSheetState import com.stripe.android.uicore.stripeColors import com.stripe.android.ui.core.R as StripeUiCoreR @@ -50,8 +50,12 @@ internal class SepaMandateActivity : AppCompatActivity() { setContent { StripeTheme { - val bottomSheetState = rememberBottomSheetState() - BottomSheet(state = bottomSheetState, onDismissed = { finish() }) { + val bottomSheetState = rememberStripeBottomSheetState() + + ElementsBottomSheetLayout( + state = bottomSheetState, + onDismissed = this::finish, + ) { SepaMandateScreen( merchantName = merchantName, acknowledgedCallback = { diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentOptionsActivityTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentOptionsActivityTest.kt index 267ce5cf255..37d31af81dc 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentOptionsActivityTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentOptionsActivityTest.kt @@ -23,7 +23,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.stripe.android.ApiKeyFixtures import com.stripe.android.PaymentConfiguration -import com.stripe.android.common.ui.BottomSheetContentTestTag import com.stripe.android.core.Logger import com.stripe.android.core.injection.WeakMapInjectorRegistry import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFactory @@ -38,6 +37,7 @@ import com.stripe.android.paymentsheet.model.PaymentSelection import com.stripe.android.paymentsheet.ui.PAYMENT_SHEET_PRIMARY_BUTTON_TEST_TAG import com.stripe.android.paymentsheet.ui.PrimaryButton import com.stripe.android.paymentsheet.ui.getLabel +import com.stripe.android.uicore.elements.bottomsheet.BottomSheetContentTestTag import com.stripe.android.utils.FakeCustomerRepository import com.stripe.android.utils.FakeLinkConfigurationCoordinator import com.stripe.android.utils.InjectableActivityScenario diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetActivityTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetActivityTest.kt index 5954cfe5c06..a99f726d6b2 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetActivityTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetActivityTest.kt @@ -22,7 +22,6 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import com.stripe.android.ApiKeyFixtures import com.stripe.android.PaymentConfiguration -import com.stripe.android.common.ui.BottomSheetContentTestTag import com.stripe.android.core.Logger import com.stripe.android.core.injection.WeakMapInjectorRegistry import com.stripe.android.googlepaylauncher.GooglePayPaymentMethodLauncher @@ -56,6 +55,7 @@ import com.stripe.android.paymentsheet.ui.GOOGLE_PAY_BUTTON_TEST_TAG import com.stripe.android.paymentsheet.ui.PAYMENT_SHEET_PRIMARY_BUTTON_TEST_TAG import com.stripe.android.paymentsheet.ui.PrimaryButton import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel +import com.stripe.android.uicore.elements.bottomsheet.BottomSheetContentTestTag import com.stripe.android.uicore.utils.stateFlowOf import com.stripe.android.utils.FakeCustomerRepository import com.stripe.android.utils.FakeIntentConfirmationInterceptor diff --git a/stripe-ui-core/api/stripe-ui-core.api b/stripe-ui-core/api/stripe-ui-core.api index 4852edee52d..0abf91639da 100644 --- a/stripe-ui-core/api/stripe-ui-core.api +++ b/stripe-ui-core/api/stripe-ui-core.api @@ -246,6 +246,17 @@ public final class com/stripe/android/uicore/elements/TextFieldUIKt { public static final fun TextFieldSection-vbMXUkU (Landroidx/compose/ui/Modifier;Lcom/stripe/android/uicore/elements/TextFieldController;IZZLjava/lang/Integer;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } +public final class com/stripe/android/uicore/elements/bottomsheet/ComposableSingletons$StripeBottomSheetLayoutKt { + public static final field INSTANCE Lcom/stripe/android/uicore/elements/bottomsheet/ComposableSingletons$StripeBottomSheetLayoutKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$stripe_ui_core_release ()Lkotlin/jvm/functions/Function2; +} + +public final class com/stripe/android/uicore/elements/bottomsheet/StripeBottomSheetLayoutInfoKt { + public static final fun rememberStripeBottomSheetLayoutInfo-Hde_KZM (FJJLandroidx/compose/runtime/Composer;II)Lcom/stripe/android/uicore/elements/bottomsheet/StripeBottomSheetLayoutInfo; +} + public final class com/stripe/android/uicore/image/ComposableSingletons$StripeImageKt { public static final field INSTANCE Lcom/stripe/android/uicore/image/ComposableSingletons$StripeImageKt; public static field lambda-1 Lkotlin/jvm/functions/Function3; diff --git a/stripe-ui-core/detekt-baseline.xml b/stripe-ui-core/detekt-baseline.xml index d29bb90ca6a..56d5d88f36e 100644 --- a/stripe-ui-core/detekt-baseline.xml +++ b/stripe-ui-core/detekt-baseline.xml @@ -12,6 +12,7 @@ FunctionParameterNaming:IdentifierSpec.kt$IdentifierSpec.Companion$_value: String LongMethod:Html.kt$@Composable @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) fun annotatedStringResource( text: String, imageGetter: Map<String, EmbeddableImage> = emptyMap(), urlSpanStyle: SpanStyle = SpanStyle(textDecoration = TextDecoration.Underline) ): AnnotatedString LongMethod:OTPElementUI.kt$@OptIn(ExperimentalComposeUiApi::class) @Composable @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) fun OTPElementUI( enabled: Boolean, element: OTPElement, modifier: Modifier = Modifier, boxShape: Shape = MaterialTheme.shapes.medium, boxTextStyle: TextStyle = OTPElementUI.defaultTextStyle(), boxSpacing: Dp = 8.dp, middleSpacing: Dp = 20.dp, otpInputPlaceholder: String = "●", colors: OTPElementColors = OTPElementColors( selectedBorder = MaterialTheme.colors.primary, placeholder = MaterialTheme.stripeColors.placeholderText ), focusRequester: FocusRequester = remember { FocusRequester() } ) + MagicNumber:AnimationConstants.kt$34 MagicNumber:DateConfig.kt$DateConfig$4 MagicNumber:DateConfig.kt$DateConfig.Companion$100 MagicNumber:DateConfig.kt$DateConfig.Companion$12 @@ -29,6 +30,7 @@ MagicNumber:StateFlows.kt$3 MagicNumber:StateFlows.kt$4 MagicNumber:StateFlows.kt$5 + MagicNumber:StripeBottomSheetState.kt$StripeBottomSheetState$10 MagicNumber:StripeTheme.kt$0.15 MagicNumber:StripeTheme.kt$0.32 MagicNumber:StripeTheme.kt$3 diff --git a/payments-core/res/anim/stripe_paymentsheet_transition_fade_in.xml b/stripe-ui-core/res/anim/stripe_transition_fade_in.xml similarity index 90% rename from payments-core/res/anim/stripe_paymentsheet_transition_fade_in.xml rename to stripe-ui-core/res/anim/stripe_transition_fade_in.xml index 64a46de2853..a4888f8c12c 100644 --- a/payments-core/res/anim/stripe_paymentsheet_transition_fade_in.xml +++ b/stripe-ui-core/res/anim/stripe_transition_fade_in.xml @@ -3,4 +3,4 @@ android:duration="@android:integer/config_shortAnimTime" android:interpolator="@android:anim/decelerate_interpolator" android:fromAlpha="0" - android:toAlpha="1" /> \ No newline at end of file + android:toAlpha="1" /> diff --git a/payments-core/res/anim/stripe_paymentsheet_transition_fade_out.xml b/stripe-ui-core/res/anim/stripe_transition_fade_out.xml similarity index 90% rename from payments-core/res/anim/stripe_paymentsheet_transition_fade_out.xml rename to stripe-ui-core/res/anim/stripe_transition_fade_out.xml index afab1996f96..00c73ef26cc 100644 --- a/payments-core/res/anim/stripe_paymentsheet_transition_fade_out.xml +++ b/stripe-ui-core/res/anim/stripe_transition_fade_out.xml @@ -3,4 +3,4 @@ android:duration="@android:integer/config_shortAnimTime" android:interpolator="@android:anim/decelerate_interpolator" android:fromAlpha="1" - android:toAlpha="0" /> \ No newline at end of file + android:toAlpha="0" /> diff --git a/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/bottomsheet/StripeBottomSheetLayout.kt b/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/bottomsheet/StripeBottomSheetLayout.kt new file mode 100644 index 00000000000..8633c317c9b --- /dev/null +++ b/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/bottomsheet/StripeBottomSheetLayout.kt @@ -0,0 +1,86 @@ +package com.stripe.android.uicore.elements.bottomsheet + +import androidx.annotation.RestrictTo +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetDefaults +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetValue.Expanded +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import com.stripe.android.uicore.elements.bottomsheet.StripeBottomSheetState.DismissalType + +@RestrictTo(RestrictTo.Scope.LIBRARY) +const val BottomSheetContentTestTag = "BottomSheetContentTestTag" + +/** + * Renders the provided [sheetContent] in a modal bottom sheet. + * + * @param state The [StripeBottomSheetState] that controls the visibility of the bottom sheet. + * navigate to a specific screen. + * @param onUpdateStatusBarColor Called when the status bar color needs to be updated. This is based + * on the expansion state of the sheet. + * @param onDismissed Called when the user dismisses the bottom sheet by swiping down. You should + * inform your view model about this change. + * @param sheetContent The content to render in the sheet + */ +@OptIn(ExperimentalMaterialApi::class) +@RestrictTo(RestrictTo.Scope.LIBRARY) +@Composable +fun StripeBottomSheetLayout( + state: StripeBottomSheetState, + layoutInfo: StripeBottomSheetLayoutInfo, + modifier: Modifier = Modifier, + onUpdateStatusBarColor: (Color) -> Unit = {}, + onDismissed: () -> Unit, + sheetContent: @Composable () -> Unit, +) { + val scrimColor = ModalBottomSheetDefaults.scrimColor + val isExpanded = state.modalBottomSheetState.targetValue == Expanded + + val statusBarColorAlpha by animateFloatAsState( + targetValue = if (isExpanded) scrimColor.alpha else 0f, + animationSpec = tween(), + label = "StatusBarColorAlpha", + ) + + LaunchedEffect(statusBarColorAlpha) { + onUpdateStatusBarColor(scrimColor.copy(statusBarColorAlpha)) + } + + LaunchedEffect(Unit) { + state.show() + + val dismissalType = state.awaitDismissal() + if (dismissalType == DismissalType.SwipedDownByUser) { + onDismissed() + } + } + + ModalBottomSheetLayout( + modifier = modifier + .statusBarsPadding() + .imePadding(), + scrimColor = layoutInfo.scrimColor, + sheetBackgroundColor = layoutInfo.sheetBackgroundColor, + sheetElevation = 0.dp, + sheetGesturesEnabled = false, + sheetShape = layoutInfo.sheetShape, + sheetState = state.modalBottomSheetState, + sheetContent = { + Box(modifier = Modifier.testTag(BottomSheetContentTestTag)) { + sheetContent() + } + }, + content = {}, + ) +} diff --git a/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/bottomsheet/StripeBottomSheetLayoutInfo.kt b/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/bottomsheet/StripeBottomSheetLayoutInfo.kt new file mode 100644 index 00000000000..91b28bf1344 --- /dev/null +++ b/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/bottomsheet/StripeBottomSheetLayoutInfo.kt @@ -0,0 +1,38 @@ +package com.stripe.android.uicore.elements.bottomsheet + +import androidx.annotation.RestrictTo +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ModalBottomSheetDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.stripe.android.uicore.stripeShapes + +@RestrictTo(RestrictTo.Scope.LIBRARY) +data class StripeBottomSheetLayoutInfo( + val sheetShape: Shape, + val sheetBackgroundColor: Color, + val scrimColor: Color, +) + +@Composable +fun rememberStripeBottomSheetLayoutInfo( + cornerRadius: Dp = MaterialTheme.stripeShapes.cornerRadius.dp, + sheetBackgroundColor: Color = MaterialTheme.colors.background, + scrimColor: Color = ModalBottomSheetDefaults.scrimColor, +): StripeBottomSheetLayoutInfo { + return remember { + StripeBottomSheetLayoutInfo( + sheetShape = RoundedCornerShape( + topStart = cornerRadius, + topEnd = cornerRadius, + ), + sheetBackgroundColor = sheetBackgroundColor, + scrimColor = scrimColor, + ) + } +} diff --git a/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/bottomsheet/StripeBottomSheetState.kt b/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/bottomsheet/StripeBottomSheetState.kt new file mode 100644 index 00000000000..3741ac957f6 --- /dev/null +++ b/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/bottomsheet/StripeBottomSheetState.kt @@ -0,0 +1,107 @@ +package com.stripe.android.uicore.elements.bottomsheet + +import androidx.annotation.RestrictTo +import androidx.compose.animation.core.tween +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow +import kotlinx.coroutines.flow.first +import kotlin.coroutines.cancellation.CancellationException + +@OptIn(ExperimentalMaterialApi::class) +@RestrictTo(RestrictTo.Scope.LIBRARY) +@Composable +fun rememberStripeBottomSheetState( + initialValue: ModalBottomSheetValue = ModalBottomSheetValue.Hidden, + confirmValueChange: (ModalBottomSheetValue) -> Boolean = { true }, +): StripeBottomSheetState { + val modalBottomSheetState = rememberModalBottomSheetState( + initialValue = initialValue, + confirmValueChange = confirmValueChange, + skipHalfExpanded = true, + animationSpec = tween(), + ) + + return remember { + StripeBottomSheetState( + modalBottomSheetState = modalBottomSheetState, + ) + } +} + +@RestrictTo(RestrictTo.Scope.LIBRARY) +interface StripeBottomSheetDismissalInterceptor { + suspend fun onBottomSheetDismissal() +} + +@OptIn(ExperimentalMaterialApi::class) +@RestrictTo(RestrictTo.Scope.LIBRARY) +class StripeBottomSheetState internal constructor( + val modalBottomSheetState: ModalBottomSheetState, +) { + + private var dismissalType: DismissalType? = null + + private val dismissalInterceptors = mutableListOf() + + fun addInterceptor(interceptor: StripeBottomSheetDismissalInterceptor) { + dismissalInterceptors += interceptor + } + + suspend fun show() { + repeatUntilSucceededOrLimit(10) { + // Showing the bottom sheet can be interrupted. + // We keep trying until it's fully displayed. + modalBottomSheetState.show() + } + + // Ensure that isVisible is being updated correctly inside ModalBottomSheetState + snapshotFlow { modalBottomSheetState.isVisible }.first { isVisible -> isVisible } + } + + suspend fun awaitDismissal(): DismissalType { + snapshotFlow { modalBottomSheetState.isVisible }.first { isVisible -> !isVisible } + return dismissalType ?: DismissalType.SwipedDownByUser + } + + suspend fun hide() { + dismissalType = DismissalType.Programmatically + + for (interceptor in dismissalInterceptors) { + interceptor.onBottomSheetDismissal() + } + + if (modalBottomSheetState.isVisible) { + repeatUntilSucceededOrLimit(10) { + // Hiding the bottom sheet can be interrupted. + // We keep trying until it's fully hidden. + modalBottomSheetState.hide() + } + } + } + + @RestrictTo(RestrictTo.Scope.LIBRARY) + enum class DismissalType { + Programmatically, + SwipedDownByUser, + } +} + +private suspend fun repeatUntilSucceededOrLimit( + limit: Int, + block: suspend () -> Unit +) { + var counter = 0 + while (counter < limit) { + try { + block() + break + } catch (ignored: CancellationException) { + counter += 1 + } + } +} diff --git a/stripe-ui-core/src/main/java/com/stripe/android/uicore/utils/AnimationConstants.kt b/stripe-ui-core/src/main/java/com/stripe/android/uicore/utils/AnimationConstants.kt new file mode 100644 index 00000000000..03aefdce273 --- /dev/null +++ b/stripe-ui-core/src/main/java/com/stripe/android/uicore/utils/AnimationConstants.kt @@ -0,0 +1,28 @@ +package com.stripe.android.uicore.utils + +import android.app.Activity +import android.os.Build +import androidx.annotation.AnimRes +import androidx.annotation.RestrictTo +import com.stripe.android.uicore.R +import com.stripe.android.uicore.utils.AnimationConstants.FADE_IN +import com.stripe.android.uicore.utils.AnimationConstants.FADE_OUT + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +object AnimationConstants { + @AnimRes + val FADE_IN = R.anim.stripe_transition_fade_in + + @AnimRes + val FADE_OUT = R.anim.stripe_transition_fade_out +} + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +fun Activity.fadeOut() { + if (Build.VERSION.SDK_INT >= 34) { + overrideActivityTransition(Activity.OVERRIDE_TRANSITION_CLOSE, FADE_IN, FADE_OUT) + } else { + @Suppress("DEPRECATION") + overridePendingTransition(FADE_IN, FADE_OUT) + } +}