Composables
Docs Login
Pairel

Membership

package com.composables.pairel

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.composables.compose.ripple.rememberRippleIndication
import com.composables.pairel.components.BottomBar
import com.composables.pairel.components.BottomBarHeight
import com.composables.pairel.components.Button
import com.composables.pairel.components.Card
import com.composables.pairel.components.CenteredAppBar
import com.composables.pairel.components.Icon
import com.composables.pairel.components.IconButton
import com.composables.pairel.components.ScreenScaffold
import com.composables.pairel.components.Text
import com.composables.pairel.theme.ArrowLeft
import com.composables.pairel.theme.Check
import com.composables.pairel.theme.Icons
import com.composables.pairel.theme.Theme

data class MembershipPlan(
    val months: Int,
    val totalPrice: String,
    val monthlyPrice: String,
    val badge: String? = null,
)

private val membershipPlans =
    listOf(
        MembershipPlan(months = 1, totalPrice = "$24.99", monthlyPrice = "$24.99"),
        MembershipPlan(
            months = 3,
            totalPrice = "$49.99",
            monthlyPrice = "$16.66",
            badge = "Most popular",
        ),
        MembershipPlan(
            months = 6,
            totalPrice = "$74.99",
            monthlyPrice = "$12.50",
            badge = "Best value",
        ),
    )

private val plusBenefits =
    listOf(
        "See everyone who likes you" to "No more guessing who's interested.",
        "Rematch when a conversation expires" to "Get a second chance to say hello.",
        "Set more preferences" to "Narrow Discover to what matters to you.",
    )

@Composable
fun MembershipScreen(
    navigateBack: () -> Unit,
    navigateToCheckout: (MembershipPlan) -> Unit,
    modifier: Modifier = Modifier,
) {
    var selectedPlan by remember { mutableStateOf(membershipPlans[1]) }

    val bottomPadding =
        BottomBarHeight + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()

    ScreenScaffold(
        modifier = modifier,
        topBar = {
            CenteredAppBar(
                title = { Text("Membership") },
                navigation = {
                    IconButton(onClick = navigateBack) {
                        Icon(
                            Icons.ArrowLeft,
                            contentDescription = "Back",
                            modifier = Modifier.size(24.dp),
                        )
                    }
                },
            )
        },
        bottomBar = {
            BottomBar {
                Button(
                    onClick = { navigateToCheckout(selectedPlan) },
                    modifier =
                        Modifier.widthIn(max = 560.dp).fillMaxWidth().padding(vertical = 4.dp),
                ) {
                    Text(
                        "Continue with ${planLength(selectedPlan.months)} · ${selectedPlan.totalPrice}"
                    )
                }
            }
        },
    ) {
        Column(
            Modifier.fillMaxSize()
                .verticalScroll(rememberScrollState())
                .padding(top = 8.dp, bottom = bottomPadding + 24.dp)
                .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal))
                .padding(horizontal = 20.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Column(
                Modifier.widthIn(max = 560.dp).fillMaxWidth(),
                verticalArrangement = Arrangement.spacedBy(28.dp),
            ) {
                Card(Modifier.fillMaxWidth()) {
                    Text(
                        "Your plan",
                        style = Theme.typography.caption,
                        color = Theme.colors.onBackgroundMuted,
                    )
                    Text(
                        "Free",
                        style = Theme.typography.title.copy(fontSize = 24.sp, lineHeight = 30.sp),
                        modifier = Modifier.padding(top = 4.dp),
                    )
                    Text(
                        "Swipe, match, and chat for free. Upgrade any time for more.",
                        color = Theme.colors.onBackgroundMuted,
                        modifier = Modifier.padding(top = 4.dp),
                    )
                }

                Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
                    Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
                        Text(
                            "Pairel Plus".uppercase(),
                            style = Theme.typography.caption.copy(letterSpacing = 1.sp),
                            color = Theme.colors.accent,
                        )
                        Text("Make every connection count", style = Theme.typography.quote)
                    }
                    for ((title, description) in plusBenefits) {
                        BenefitRow(title = title, description = description)
                    }
                }

                Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
                    Text("Choose a plan", style = Theme.typography.title)
                    Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
                        for (plan in membershipPlans) {
                            PlanOption(
                                plan = plan,
                                selected = plan == selectedPlan,
                                onSelect = { selectedPlan = plan },
                                modifier = Modifier.weight(1f),
                            )
                        }
                    }
                    Text(
                        "Renews automatically at the same price. Cancel any time in your app store settings.",
                        style = Theme.typography.caption.copy(fontWeight = null),
                        color = Theme.colors.onBackgroundMuted,
                    )
                }
            }
        }
    }
}

private fun planLength(months: Int) = if (months == 1) "1 month" else "$months months"

@Composable
private fun BenefitRow(title: String, description: String, modifier: Modifier = Modifier) {
    Row(modifier, horizontalArrangement = Arrangement.spacedBy(14.dp)) {
        Box(
            Modifier.size(28.dp).clip(CircleShape).background(Theme.colors.accent),
            contentAlignment = Alignment.Center,
        ) {
            Icon(
                Icons.Check,
                contentDescription = null,
                tint = Theme.colors.onAccent,
                modifier = Modifier.size(16.dp),
            )
        }
        Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
            Text(title, style = Theme.typography.label)
            Text(description, color = Theme.colors.onBackgroundMuted)
        }
    }
}

@Composable
private fun PlanOption(
    plan: MembershipPlan,
    selected: Boolean,
    onSelect: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier =
            modifier
                .clip(Theme.shapes.small)
                .background(Theme.colors.surface)
                .border(
                    width = if (selected) 2.dp else 1.dp,
                    color = if (selected) Theme.colors.accent else Theme.colors.border,
                    shape = Theme.shapes.small,
                )
                .selectable(
                    selected = selected,
                    onClick = onSelect,
                    role = Role.RadioButton,
                    interactionSource = remember { MutableInteractionSource() },
                    indication = rememberRippleIndication(color = Theme.colors.onSurface),
                )
                .padding(horizontal = 12.dp, vertical = 14.dp),
        verticalArrangement = Arrangement.spacedBy(4.dp),
    ) {
        Text(
            plan.badge ?: "",
            style = Theme.typography.caption,
            color = if (plan.badge != null) Theme.colors.accent else Color.Transparent,
            maxLines = 1,
        )
        Text(
            planLength(plan.months),
            style = Theme.typography.label,
            color = Theme.colors.onSurface,
        )
        Text(
            "${plan.monthlyPrice}/mo",
            style = Theme.typography.title.copy(fontSize = 16.sp, lineHeight = 22.sp),
            maxLines = 1,
            color = Theme.colors.onSurface,
        )
        Box(Modifier.height(2.dp))
        Text(
            "${plan.totalPrice} total",
            style = Theme.typography.caption.copy(fontWeight = null),
            color = Theme.colors.onBackgroundMuted,
        )
    }
}

More screens from Pairel

by Alex Styl

Search screens and components

Screen flows

Screens

UI Elements