Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(POM-451): (Card Tokenization) Adjustable bottom sheet height #256

Merged
merged 9 commits into from
Jan 31, 2025
6 changes: 3 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ buildscript {
kotlinVersion = '2.1.0'
kspVersion = '2.1.0-1.0.29'
dokkaVersion = '1.9.20'
androidxNavigationVersion = '2.8.5'
androidxNavigationVersion = '2.8.6'
nexusPublishPluginVersion = '2.0.0'
}
dependencies {
Expand Down Expand Up @@ -39,10 +39,10 @@ ext {
androidxCoreVersion = '1.15.0'
androidxAppCompatVersion = '1.7.0'
androidxConstraintLayoutVersion = '2.2.0'
androidxActivityVersion = '1.9.3'
androidxActivityVersion = '1.10.0'
androidxFragmentVersion = '1.8.5'
androidxLifecycleVersion = '2.8.7'
androidxRecyclerViewVersion = '1.3.2'
androidxRecyclerViewVersion = '1.4.0'
androidxSwipeRefreshLayoutVersion = '1.1.0'
androidxBrowserVersion = '1.8.0'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import com.processout.sdk.ui.shared.extension.screenSize

internal abstract class BaseBottomSheetDialogFragment<T : Parcelable> : BottomSheetDialogFragment() {

protected abstract val expandable: Boolean
protected abstract var expandable: Boolean
protected abstract val defaultViewHeight: Int
protected val screenHeight by lazy { requireContext().screenSize().height }
protected var animationDurationMillis: Long = 400
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.viewModels
Expand All @@ -27,17 +26,21 @@ import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.B
import com.processout.sdk.ui.core.theme.ProcessOutTheme
import com.processout.sdk.ui.shared.component.displayCutoutHeight
import com.processout.sdk.ui.shared.component.screenModeAsState
import com.processout.sdk.ui.shared.configuration.POBottomSheetConfiguration.Height.Fixed
import com.processout.sdk.ui.shared.configuration.POBottomSheetConfiguration.Height.WrapContent
import kotlin.math.roundToInt

internal class CardTokenizationBottomSheet : BaseBottomSheetDialogFragment<POCard>() {

companion object {
val tag: String = CardTokenizationBottomSheet::class.java.simpleName
}

override val expandable = false
override val defaultViewHeight by lazy { screenHeight }
override var expandable = false
override val defaultViewHeight by lazy { (screenHeight * 0.3).roundToInt() }

private var configuration: POCardTokenizationConfiguration? = null
private val viewHeightConfiguration by lazy { configuration?.bottomSheet?.height ?: WrapContent }

private val viewModel: CardTokenizationViewModel by viewModels {
CardTokenizationViewModel.Factory(
Expand Down Expand Up @@ -65,12 +68,20 @@ internal class CardTokenizationBottomSheet : BaseBottomSheetDialogFragment<POCar
with(viewModel.completion.collectAsStateWithLifecycle()) {
LaunchedEffect(value) { handle(value) }
}
with(screenModeAsState(viewHeight = defaultViewHeight + displayCutoutHeight())) {
var viewHeight by remember { mutableIntStateOf(defaultViewHeight) }
with(screenModeAsState(viewHeight = viewHeight)) {
LaunchedEffect(value) { apply(value) }
}
val displayCutoutHeight = displayCutoutHeight()
CardTokenizationScreen(
state = viewModel.state.collectAsStateWithLifecycle().value,
onEvent = remember { viewModel::onEvent },
onContentHeightChanged = { contentHeight ->
viewHeight = when (val height = viewHeightConfiguration) {
is Fixed -> (screenHeight * height.fraction + displayCutoutHeight).roundToInt()
WrapContent -> contentHeight
}
},
style = CardTokenizationScreen.style(custom = configuration?.style)
)
}
Expand All @@ -79,7 +90,10 @@ internal class CardTokenizationBottomSheet : BaseBottomSheetDialogFragment<POCar

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
configuration?.let { apply(it.cancellation) }
configuration?.let {
expandable = it.bottomSheet.expandable
apply(it.bottomSheet.cancellation)
}
}

private fun handle(completion: CardTokenizationCompletion) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ internal class CardTokenizationInteractor(
),
Field(
id = CardFieldId.CARDHOLDER,
shouldCollect = configuration.isCardholderNameFieldVisible
shouldCollect = configuration.cardholderNameRequired
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
Expand All @@ -16,6 +14,7 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.colorResource
Expand All @@ -33,25 +32,36 @@ import com.processout.sdk.ui.core.component.field.text.POTextField
import com.processout.sdk.ui.core.state.POActionState
import com.processout.sdk.ui.core.state.POImmutableList
import com.processout.sdk.ui.core.style.POAxis
import com.processout.sdk.ui.core.theme.ProcessOutTheme
import com.processout.sdk.ui.core.theme.ProcessOutTheme.colors
import com.processout.sdk.ui.core.theme.ProcessOutTheme.dimensions
import com.processout.sdk.ui.core.theme.ProcessOutTheme.shapes
import com.processout.sdk.ui.core.theme.ProcessOutTheme.spacing
import com.processout.sdk.ui.shared.component.DynamicFooter
import com.processout.sdk.ui.shared.component.rememberLifecycleEvent
import com.processout.sdk.ui.shared.extension.dpToPx
import com.processout.sdk.ui.shared.state.FieldState

@Composable
internal fun CardTokenizationScreen(
state: CardTokenizationViewModelState,
onEvent: (CardTokenizationEvent) -> Unit,
onContentHeightChanged: (Int) -> Unit,
style: CardTokenizationScreen.Style = CardTokenizationScreen.style()
) {
var topBarHeight by remember { mutableIntStateOf(0) }
var bottomBarHeight by remember { mutableIntStateOf(0) }
Scaffold(
modifier = Modifier
.nestedScroll(rememberNestedScrollInteropConnection())
.clip(shape = ProcessOutTheme.shapes.topRoundedCornersLarge),
.clip(shape = shapes.topRoundedCornersLarge),
containerColor = style.backgroundColor,
topBar = {
POHeader(
modifier = Modifier.verticalScroll(rememberScrollState()),
modifier = Modifier
.verticalScroll(rememberScrollState())
.onGloballyPositioned {
topBarHeight = it.size.height
},
title = state.title,
style = style.title,
dividerColor = style.dividerColor,
Expand All @@ -65,7 +75,10 @@ internal fun CardTokenizationScreen(
primary = state.primaryAction,
secondary = state.secondaryAction,
onEvent = onEvent,
style = style.actionsContainer
style = style.actionsContainer,
modifier = Modifier.onGloballyPositioned {
bottomBarHeight = it.size.height
}
)
}
}
Expand All @@ -75,13 +88,21 @@ internal fun CardTokenizationScreen(
.fillMaxSize()
.padding(scaffoldPadding)
.verticalScroll(rememberScrollState())
.padding(ProcessOutTheme.spacing.extraLarge)
.padding(spacing.extraLarge)
) {
Sections(
state = state,
onEvent = onEvent,
style = style
)
val verticalSpacingPx = (spacing.extraLarge * 4 + 10.dp).dpToPx()
Column(
modifier = Modifier.onGloballyPositioned {
val contentHeight = it.size.height + topBarHeight + bottomBarHeight + verticalSpacingPx
onContentHeightChanged(contentHeight)
}
) {
Sections(
state = state,
onEvent = onEvent,
style = style
)
}
}
}
}
Expand All @@ -98,14 +119,14 @@ private fun Sections(
val lifecycleEvent = rememberLifecycleEvent()
state.sections.elements.forEachIndexed { index, section ->
val padding = if (section.id == FUTURE_PAYMENTS) {
ProcessOutTheme.spacing.small
spacing.small
} else when (index) {
0 -> 0.dp
else -> ProcessOutTheme.spacing.extraLarge
else -> spacing.extraLarge
}
Spacer(Modifier.requiredHeight(padding))
Column(
verticalArrangement = Arrangement.spacedBy(ProcessOutTheme.spacing.small)
verticalArrangement = Arrangement.spacedBy(spacing.small)
) {
section.title?.let {
with(style.sectionTitle) {
Expand Down Expand Up @@ -133,7 +154,7 @@ private fun Sections(
style = style.errorMessage,
modifier = Modifier
.fillMaxWidth()
.padding(top = ProcessOutTheme.spacing.small)
.padding(top = spacing.small)
)
}
}
Expand Down Expand Up @@ -172,7 +193,7 @@ private fun Item(
modifier = modifier
)
is Item.Group -> Row(
horizontalArrangement = Arrangement.spacedBy(ProcessOutTheme.spacing.small)
horizontalArrangement = Arrangement.spacedBy(spacing.small)
) {
item.items.elements.forEach { groupItem ->
Item(
Expand Down Expand Up @@ -305,7 +326,7 @@ private fun AnimatedFieldIcon(@DrawableRes id: Int) {
POAnimatedImage(
id = id,
modifier = Modifier
.requiredHeight(ProcessOutTheme.dimensions.formComponentMinHeight)
.requiredHeight(dimensions.formComponentMinHeight)
.padding(POField.contentPadding),
contentScale = ContentScale.FillHeight
)
Expand All @@ -316,11 +337,13 @@ private fun Actions(
primary: POActionState,
secondary: POActionState?,
onEvent: (CardTokenizationEvent) -> Unit,
style: POActionsContainer.Style
style: POActionsContainer.Style,
modifier: Modifier = Modifier
) {
val actions = mutableListOf(primary)
secondary?.let { actions.add(it) }
POActionsContainer(
modifier = modifier,
actions = POImmutableList(
if (style.axis == POAxis.Horizontal) actions.reversed() else actions
),
Expand Down Expand Up @@ -370,12 +393,12 @@ internal object CardTokenizationScreen {
} ?: POActionsContainer.default,
backgroundColor = custom?.backgroundColorResId?.let {
colorResource(id = it)
} ?: ProcessOutTheme.colors.surface.default,
} ?: colors.surface.default,
dividerColor = custom?.dividerColorResId?.let {
colorResource(id = it)
} ?: ProcessOutTheme.colors.border.subtle,
} ?: colors.border.subtle,
dragHandleColor = custom?.dragHandleColorResId?.let {
colorResource(id = it)
} ?: ProcessOutTheme.colors.border.subtle
} ?: colors.border.subtle
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ internal class CardTokenizationViewModel private constructor(
}
)
},
draggable = cancellation.dragDown
draggable = bottomSheet.cancellation.dragDown || bottomSheet.expandable
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,39 @@ import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.B
import com.processout.sdk.ui.core.shared.image.PODrawableImage
import com.processout.sdk.ui.core.style.*
import com.processout.sdk.ui.shared.configuration.POActionConfirmationConfiguration
import com.processout.sdk.ui.shared.configuration.POBottomSheetConfiguration
import com.processout.sdk.ui.shared.configuration.POBottomSheetConfiguration.Height.Fixed
import com.processout.sdk.ui.shared.configuration.POBottomSheetConfiguration.Height.WrapContent
import com.processout.sdk.ui.shared.configuration.POCancellationConfiguration
import kotlinx.parcelize.Parcelize

/**
* Defines card tokenization configuration.
*
* @param[title] Custom title.
* @param[cvcRequired] Specifies whether the card CVC should be collected. Default value is _true_.
* @param[isCardholderNameFieldVisible] Specifies whether the cardholder name field should be displayed. Default value is _true_.
* @param[cvcRequired] Specifies whether the CVC field should be displayed. Default value is _true_.
* @param[cardholderNameRequired] Specifies whether the cardholder name field should be displayed. Default value is _true_.
* @param[billingAddress] Allows to customize the collection of billing address.
* @param[savingAllowed] Displays checkbox that allows to save the card details for future payments.
* @param[submitButton] Submit button configuration.
* @param[cancelButton] Cancel button configuration. Use _null_ to hide.
* @param[cancellation] Specifies cancellation behaviour.
* @param[bottomSheet] Specifies bottom sheet configuration. By default is [WrapContent] and non-expandable.
* @param[metadata] Metadata related to the card.
* @param[style] Allows to customize the look and feel.
*/
@Parcelize
data class POCardTokenizationConfiguration(
val title: String? = null,
val cvcRequired: Boolean = true,
val isCardholderNameFieldVisible: Boolean = true,
val cardholderNameRequired: Boolean = true,
val billingAddress: BillingAddressConfiguration = BillingAddressConfiguration(),
val savingAllowed: Boolean = false,
val submitButton: Button = Button(),
val cancelButton: CancelButton? = CancelButton(),
val cancellation: POCancellationConfiguration = POCancellationConfiguration(),
val bottomSheet: POBottomSheetConfiguration = POBottomSheetConfiguration(
height = WrapContent,
expandable = false
),
val metadata: Map<String, String>? = null,
val style: Style? = null
) : Parcelable {
Expand All @@ -42,7 +48,7 @@ data class POCardTokenizationConfiguration(
* Defines card tokenization configuration.
*
* @param[title] Custom title.
* @param[cvcRequired] Specifies whether the card CVC should be collected. Default value is _true_.
* @param[cvcRequired] Specifies whether the CVC field should be displayed. Default value is _true_.
* @param[isCardholderNameFieldVisible] Specifies whether the cardholder name field should be displayed. Default value is _true_.
* @param[billingAddress] Allows to customize the collection of billing address.
* @param[savingAllowed] Displays checkbox that allows to save the card details for future payments.
Expand All @@ -67,13 +73,17 @@ data class POCardTokenizationConfiguration(
) : this(
title = title,
cvcRequired = cvcRequired,
isCardholderNameFieldVisible = isCardholderNameFieldVisible,
cardholderNameRequired = isCardholderNameFieldVisible,
billingAddress = billingAddress,
savingAllowed = savingAllowed,
submitButton = Button(text = primaryActionText),
cancelButton = if (cancellation.secondaryAction)
CancelButton(text = secondaryActionText) else null,
cancellation = cancellation,
bottomSheet = POBottomSheetConfiguration(
height = Fixed(1f),
expandable = false,
cancellation = cancellation
),
metadata = metadata,
style = style
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ internal class CardUpdateBottomSheet : BaseBottomSheetDialogFragment<POCard>() {
val tag: String = CardUpdateBottomSheet::class.java.simpleName
}

override val expandable = false
override var expandable = false
override val defaultViewHeight by lazy { 440.dpToPx(requireContext()) }

private var configuration: POCardUpdateConfiguration? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ internal class DynamicCheckoutInteractor(
private fun POCardTokenizationConfiguration.apply(configuration: CardConfiguration) =
copy(
cvcRequired = configuration.cvcRequired,
isCardholderNameFieldVisible = configuration.cardholderNameRequired,
cardholderNameRequired = configuration.cardholderNameRequired,
billingAddress = billingAddress.copy(
mode = configuration.billingAddress.collectionMode.map(),
countryCodes = configuration.billingAddress.restrictToCountryCodes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ internal class NativeAlternativePaymentBottomSheet : BaseBottomSheetDialogFragme
val tag: String = NativeAlternativePaymentBottomSheet::class.java.simpleName
}

override val expandable = true
override var expandable = true
override val defaultViewHeight by lazy { 460.dpToPx(requireContext()) }
private val maxPeekHeight by lazy { (screenHeight * 0.8).roundToInt() }

Expand Down
Loading