coderef

PayPal

PayPal is the elder statesman of fintech rendered as a friendly, recognizable consumer-finance utility. The canvas is white, the brand is blue — specifically the deep navy PayPal Blue (#003087) that's been the company's identity since the…
DownloadRaw
PayPal

Build with the PayPal look.

Palette, type and components from this DESIGN.md, applied to a sample hero.

Palette

At a glance

Theme
dark
Typefaces
PayPal Sans Small, PayPal Sans Big, Inter
Sections
8
Length
4,249 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · PayPal

Light and dark previews, tokens and a ready-to-use package, generated from this DESIGN.md. No AI involved: the same file always produces the same output.

Generating design system…
DownloadRaw

PayPal (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports PayPal’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables, the dual-blue P-P wordmark, motion, and haptics.

Why a Compose guide for an iOS-referenced app? The DESIGN.md tokens are platform-neutral. This file keeps the visual identity (PayPal’s calm white canvas, the iconic dual-blue P-P overlap, rounded PayPal Sans, card-wallet elevation) while making everything idiomatic Android — NavigationBar instead of a UITabBar, ModalBottomSheet instead of a sheet presentation, animateIntAsState instead of .contentTransition(.numericText()), sp/dp instead of pt.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ for card-art and recipient avatars. No color extraction — PayPal’s palette is a fixed dual-blue system, so Palette is not needed. PayPal is light-first with a cobalt-tinted dark mode (since 2020); both schemes are provided.

1. Color Tokens

// ui/theme/PayPalColors.kt
import androidx.compose.ui.graphics.Color

object PayPalColors {
    // Brand
    val Blue       = Color(0xFF003087) // primary CTA, active tab, wordmark front
    val Sky        = Color(0xFF0070BA) // secondary accent, link, wordmark back
    val Cobalt     = Color(0xFF001C64) // splash / illustration bg only — keep reserved
    val BlueDark   = Color(0xFF3B82F6) // dark-mode shifted PayPal Blue

    // Canvas & Surfaces (light)
    val Canvas       = Color(0xFFFFFFFF)
    val SurfaceGray  = Color(0xFFF5F7FA)
    val SurfaceGray2 = Color(0xFFEEF1F4)
    val Divider      = Color(0xFFE5E8ED)

    // Text (light)
    val TextPrimary   = Color(0xFF001435) // almost-black, slight blue tint — NOT pure black
    val TextSecondary = Color(0xFF2C2E2F)
    val TextMuted     = Color(0xFF687173)
    val TextTertiary  = Color(0xFF9DA3A6)

    // Semantic
    val Success   = Color(0xFF1C8B43)
    val SuccessBg = Color(0xFFE4F5EA)
    val Error     = Color(0xFFD20021)
    val ErrorBg   = Color(0xFFFCE5E8)
    val Warning   = Color(0xFFFFB81C)
    val WarningBg = Color(0xFFFFF6E0)
    val PendingFg = Color(0xFFA06B00)

    // Activity icon colors
    val IconSent     = Blue
    val IconReceived = Success
    val IconCard     = Sky
    val IconReward   = Warning

    // Dark mode
    val DarkCanvas   = Color(0xFF0A0E1A) // slight blue tint — NOT pure black
    val DarkSurface1 = Color(0xFF141A2A)
    val DarkSurface2 = Color(0xFF1F2740)
    val DarkDivider  = Color(0xFF2A3142)
    val DarkTextPri  = Color(0xFFFFFFFF)
    val DarkTextSec  = Color(0xFFA8AEC4)
}

Wire it into both schemes. PayPal is light-first; the dark scheme keeps a cobalt-tinted near-black so the blue identity survives, and PayPal Blue brightens to #3B82F6 for contrast.

// ui/theme/Theme.kt
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.MaterialTheme

private val PayPalLight = lightColorScheme(
    primary        = PayPalColors.Blue,
    onPrimary      = PayPalColors.Canvas,
    secondary      = PayPalColors.Sky,
    background     = PayPalColors.Canvas,
    onBackground   = PayPalColors.TextPrimary,
    surface        = PayPalColors.Canvas,
    onSurface      = PayPalColors.TextPrimary,
    surfaceVariant = PayPalColors.SurfaceGray,
    outline        = PayPalColors.Divider,
    error          = PayPalColors.Error,
)

private val PayPalDark = darkColorScheme(
    primary        = PayPalColors.BlueDark,
    onPrimary      = PayPalColors.Canvas,
    secondary      = PayPalColors.Sky,
    background     = PayPalColors.DarkCanvas,
    onBackground   = PayPalColors.DarkTextPri,
    surface        = PayPalColors.DarkSurface1,
    onSurface      = PayPalColors.DarkTextPri,
    surfaceVariant = PayPalColors.DarkSurface2,
    outline        = PayPalColors.DarkDivider,
    error          = PayPalColors.Error,
)

@Composable
fun PayPalTheme(
    dark: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit,
) = MaterialTheme(
    colorScheme = if (dark) PayPalDark else PayPalLight,
    typography = PayPalTypography,
    content = content,
)

2. Typography

PayPal Sans Big (display) and PayPal Sans Small (body) are proprietary (Monotype, ~2018). Drop the TTFs in res/font/ (lowercase, snake_case) and build two families. Fall back to Inter (closest free match) — its humanist roundness is the nearest substitute on Android. Tabular figures on every amount: fontFeatureSettings = "tnum".

// ui/theme/PayPalType.kt
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

val PayPalSansBig = FontFamily(
    Font(R.font.paypal_sans_big_regular, FontWeight.Normal),
    Font(R.font.paypal_sans_big_medium,  FontWeight.Medium),
    Font(R.font.paypal_sans_big_bold,    FontWeight.Bold),
)
val PayPalSansSmall = FontFamily(
    Font(R.font.paypal_sans_small_regular, FontWeight.Normal),
    Font(R.font.paypal_sans_small_medium,  FontWeight.Medium),
    Font(R.font.paypal_sans_small_bold,    FontWeight.Bold),
)

private const val TNUM = "tnum" // tabular — the Activity column must align

// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1)
object PayPalText {
    val BalanceHero    = TextStyle(PayPalSansBig,   fontWeight = FontWeight.Bold,   fontSize = 36.sp, lineHeight = 36.sp, letterSpacing = (-0.5).sp, fontFeatureSettings = TNUM)
    val SendAmountHero = TextStyle(PayPalSansBig,   fontWeight = FontWeight.Bold,   fontSize = 56.sp, lineHeight = 56.sp, letterSpacing = (-1.0).sp, fontFeatureSettings = TNUM)
    val ScreenTitle    = TextStyle(PayPalSansBig,   fontWeight = FontWeight.Bold,   fontSize = 24.sp, lineHeight = 28.sp, letterSpacing = (-0.2).sp)
    val SectionHeader  = TextStyle(PayPalSansBig,   fontWeight = FontWeight.Bold,   fontSize = 18.sp, lineHeight = 22.sp, letterSpacing = (-0.1).sp)
    val CardTitle      = TextStyle(PayPalSansBig,   fontWeight = FontWeight.Medium, fontSize = 16.sp, lineHeight = 21.sp)
    val ActivityAmount = TextStyle(PayPalSansBig,   fontWeight = FontWeight.Medium, fontSize = 16.sp, lineHeight = 19.sp, fontFeatureSettings = TNUM)
    val Body           = TextStyle(PayPalSansSmall, fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 23.sp)
    val BodySmall      = TextStyle(PayPalSansSmall, fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 20.sp)
    val ActivityTitle  = TextStyle(PayPalSansSmall, fontWeight = FontWeight.Medium, fontSize = 15.sp, lineHeight = 20.sp)
    val ActivitySub    = TextStyle(PayPalSansSmall, fontWeight = FontWeight.Normal, fontSize = 13.sp, lineHeight = 17.sp)
    val Meta           = TextStyle(PayPalSansSmall, fontWeight = FontWeight.Normal, fontSize = 13.sp, lineHeight = 17.sp)
    val Link           = TextStyle(PayPalSansSmall, fontWeight = FontWeight.Medium, fontSize = 15.sp, lineHeight = 21.sp)
    val Tab            = TextStyle(PayPalSansSmall, fontWeight = FontWeight.Medium, fontSize = 11.sp, lineHeight = 11.sp, letterSpacing = 0.1.sp)
    val Chip           = TextStyle(PayPalSansSmall, fontWeight = FontWeight.Medium, fontSize = 13.sp, lineHeight = 13.sp)
    val Button         = TextStyle(PayPalSansBig,   fontWeight = FontWeight.Bold,   fontSize = 17.sp, lineHeight = 17.sp)
    val ButtonSmall    = TextStyle(PayPalSansSmall, fontWeight = FontWeight.Medium, fontSize = 15.sp, lineHeight = 15.sp)
}

// Map onto Material 3 slots so stock components inherit the brand
val PayPalTypography = Typography(
    headlineLarge = PayPalText.ScreenTitle,
    titleLarge    = PayPalText.SectionHeader,
    titleMedium   = PayPalText.CardTitle,
    bodyMedium    = PayPalText.Body,
    labelSmall    = PayPalText.Tab,
)

3. Signature Components

Balance Card

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import java.text.NumberFormat
import java.util.Locale

@Composable
fun BalanceCard(
    balance: Double,
    onAddMoney: () -> Unit,
    onTransfer: () -> Unit,
    modifier: Modifier = Modifier,
) {
    val usd = remember { NumberFormat.getCurrencyInstance(Locale.US) }
    Column(
        modifier
            .fillMaxWidth()
            .padding(horizontal = 16.dp)
            .shadow(8.dp, RoundedCornerShape(16.dp), spotColor = Color.Black.copy(alpha = 0.06f)) // gentle card-wallet lift
            .clip(RoundedCornerShape(16.dp))
            .background(PayPalColors.Canvas)
            .border(1.dp, PayPalColors.Divider, RoundedCornerShape(16.dp))
            .padding(20.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp),
    ) {
        Text("PayPal balance", style = PayPalText.ActivitySub, color = PayPalColors.TextMuted)
        AnimatedBalance(balance, usd) // numeric roll — see §4
        Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
            PayPalPrimaryButton("Add Money", onAddMoney, Modifier.weight(1f), height = 44.dp)
            PayPalSecondaryButton("Transfer", onTransfer, Modifier.weight(1f))
        }
    }
}

Activity Row

import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.Icon
import androidx.compose.ui.graphics.vector.ImageVector

enum class ActivityKind { Sent, Received, Card, Reward }

@Composable
fun ActivityRow(
    name: String,
    subtitle: String,
    amount: Double,
    kind: ActivityKind,
    received: Boolean,
    modifier: Modifier = Modifier,
) {
    val usd = remember { NumberFormat.getCurrencyInstance(Locale.US) }
    val (iconColor, glyph) = when (kind) {
        ActivityKind.Sent     -> PayPalColors.IconSent to Icons.Filled.ArrowOutward
        ActivityKind.Received -> PayPalColors.IconReceived to Icons.Filled.SouthWest
        ActivityKind.Card     -> PayPalColors.IconCard to Icons.Filled.CreditCard
        ActivityKind.Reward   -> PayPalColors.IconReward to Icons.Filled.Redeem
    }
    Box(modifier.fillMaxWidth().background(PayPalColors.Canvas)) {
        Row(
            Modifier.fillMaxWidth().heightIn(min = 68.dp).padding(horizontal = 16.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(12.dp),
        ) {
            Box(
                Modifier.size(40.dp).clip(CircleShape).background(iconColor),
                contentAlignment = Alignment.Center,
            ) {
                // shape carries direction — never rely on color alone
                Icon(glyph, contentDescription = null, tint = Color.White, modifier = Modifier.size(18.dp))
            }
            Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
                Text(name, style = PayPalText.ActivityTitle, color = PayPalColors.TextPrimary)
                Text(subtitle, style = PayPalText.ActivitySub, color = PayPalColors.TextMuted)
            }
            Text(
                (if (received) "+" else "") + usd.format(amount),
                style = PayPalText.ActivityAmount,
                color = if (received) PayPalColors.Success else PayPalColors.TextPrimary,
            )
        }
        Box(Modifier.align(Alignment.BottomCenter).fillMaxWidth().height(0.5.dp).background(PayPalColors.Divider))
    }
}

Primary / Secondary CTA (soft pill, radius = half-height)

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.ui.draw.scale
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback

@Composable
fun PayPalPrimaryButton(
    label: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    height: Dp = 48.dp,
) {
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    val scale by animateFloatAsState(
        targetValue = if (pressed) 0.98f else 1f,
        animationSpec = spring(dampingRatio = 0.7f, stiffness = 700f),
        label = "ctaScale",
    )
    val haptics = LocalHapticFeedback.current
    Box(
        modifier
            .fillMaxWidth()
            .height(height)
            .scale(scale)
            .clip(RoundedCornerShape(percent = 50)) // radius = half-height → soft pill
            .background(if (pressed) PayPalColors.Cobalt else PayPalColors.Blue)
            .clickable(interaction, indication = null) {
                haptics.performHapticFeedback(HapticFeedbackType.LongPress) // ~iOS medium impact
                onClick()
            },
        contentAlignment = Alignment.Center,
    ) {
        Text(label, style = PayPalText.Button, color = Color.White)
    }
}

@Composable
fun PayPalSecondaryButton(
    label: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    height: Dp = 48.dp,
) {
    Box(
        modifier
            .fillMaxWidth()
            .height(height)
            .clip(RoundedCornerShape(percent = 50))
            .border(1.5.dp, PayPalColors.Blue, RoundedCornerShape(percent = 50))
            .clickable(onClick = onClick),
        contentAlignment = Alignment.Center,
    ) {
        Text(label, style = PayPalText.ButtonSmall, color = PayPalColors.Blue)
    }
}

Activity Filter Chip & Status Pill

@Composable
fun ActivityFilterChip(label: String, selected: Boolean, onClick: () -> Unit) {
    Box(
        Modifier
            .clip(RoundedCornerShape(16.dp))
            .background(if (selected) PayPalColors.Blue else PayPalColors.SurfaceGray2)
            .clickable(onClick = onClick)
            .padding(horizontal = 16.dp, vertical = 8.dp),
    ) {
        Text(label, style = PayPalText.Chip, color = if (selected) Color.White else PayPalColors.TextMuted)
    }
}

enum class PayPalStatus { Completed, Pending, Failed, Refunded }

@Composable
fun StatusPill(status: PayPalStatus) {
    val (bg, fg, label) = when (status) {
        PayPalStatus.Completed -> Triple(PayPalColors.SuccessBg, PayPalColors.Success, "Completed")
        PayPalStatus.Pending   -> Triple(PayPalColors.WarningBg, PayPalColors.PendingFg, "Pending")
        PayPalStatus.Failed    -> Triple(PayPalColors.ErrorBg, PayPalColors.Error, "Failed")
        PayPalStatus.Refunded  -> Triple(PayPalColors.SurfaceGray2, PayPalColors.TextMuted, "Refunded")
    }
    Box(
        Modifier.clip(RoundedCornerShape(4.dp)).background(bg).padding(horizontal = 10.dp, vertical = 4.dp),
    ) {
        Text(label, style = PayPalText.Chip.copy(fontSize = 12.sp), color = fg)
    }
}

4. Dual-Blue P-P Wordmark + Live Balance Roll

PayPal’s two brand-load-bearing dynamics: the dual-blue P-P logomark (never a single color) and the balance number that rolls to its new value after a Send. iOS uses .contentTransition(.numericText()); Android’s analog is animateIntAsState on the cents (rendered with AnimatedContent per character for the roll feel).

P-P Wordmark

import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.TextUnit

@Composable
fun PayPalWordmark(size: TextUnit = 32.sp, modifier: Modifier = Modifier) {
    Box(modifier, contentAlignment = Alignment.Center) {
        // Back P — PayPal Sky, italic, shifted left
        Text(
            "P",
            style = PayPalText.Button.copy(fontFamily = PayPalSansBig, fontSize = size, fontWeight = FontWeight.Bold),
            color = PayPalColors.Sky,
            modifier = Modifier.graphicsLayer { translationX = -size.toPx() * 0.18f; rotationZ = -8f },
        )
        // Front P — PayPal Blue, italic, shifted right; the overlap darkens to a blue blend
        Text(
            "P",
            style = PayPalText.Button.copy(fontFamily = PayPalSansBig, fontSize = size, fontWeight = FontWeight.Bold),
            color = PayPalColors.Blue,
            modifier = Modifier.graphicsLayer { translationX = size.toPx() * 0.10f; rotationZ = -8f },
        )
    }
}

NEVER render the wordmark in a single color — the Sky-behind / Blue-front overlap with the darker blended middle zone IS the PayPal brand. For pixel-exact parity ship the official two-tone mark as a vector drawable and load via ImageVector.vectorResource(R.drawable.paypal_pp).

Live balance roll

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith

@Composable
fun AnimatedBalance(balance: Double, formatter: NumberFormat) {
    val target = formatter.format(balance)
    AnimatedContent(
        targetState = target,
        transitionSpec = {
            (slideInVertically(tween(400)) { it } togetherWith slideOutVertically(tween(400)) { -it })
        },
        label = "balanceRoll",
    ) { value ->
        Text(value, style = PayPalText.BalanceHero, color = PayPalColors.TextPrimary)
    }
}

The same BalanceHero/SendAmountHero styles drive the Send Money fullscreen: a calm centered $ (muted) + 56.sp tabular amount, a “USD” Meta chip below, and a live PayPalPrimaryButton("Send ${'$'}X.XX") whose label recomposes with the entered amount.

5. Navigation

PayPal has a 5-tab bottom bar (Home / Send / Wallet / Activity / Finances). Use Material 3 NavigationBar; the iOS bar is an opaque white surface with a 0.5pt hairline — no blur involved, so the opaque Canvas surface is exact. Active is PayPal Blue (icon + label), inactive #687173. There is no Material tint pill.

@Composable
fun PayPalBottomBar(selected: Int, onSelect: (Int) -> Unit) {
    val haptics = LocalHapticFeedback.current
    NavigationBar(
        containerColor = PayPalColors.Canvas,
        tonalElevation = 0.dp,
    ) {
        val items = listOf(
            "Home"     to Icons.Filled.Home,
            "Send"     to Icons.Filled.Send,
            "Wallet"   to Icons.Filled.AccountBalanceWallet,
            "Activity" to Icons.Filled.Schedule,
            "Finances" to Icons.Filled.ShowChart,
        )
        items.forEachIndexed { i, (label, icon) ->
            NavigationBarItem(
                selected = selected == i,
                onClick = {
                    haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove) // .selection
                    onSelect(i)
                },
                icon = { Icon(icon, contentDescription = label, modifier = Modifier.size(24.dp)) },
                label = { Text(label, style = PayPalText.Tab) },
                colors = NavigationBarItemDefaults.colors(
                    selectedIconColor = PayPalColors.Blue,
                    selectedTextColor = PayPalColors.Blue,
                    unselectedIconColor = PayPalColors.TextMuted,
                    unselectedTextColor = PayPalColors.TextMuted,
                    indicatorColor = Color.Transparent, // no Material pill — PayPal has none
                ),
            )
        }
    }
}

The top nav carries the PayPalWordmark() (24–32.sp) leading, with a notification bell + help “?” trailing, over an opaque Canvas surface with a 0.5dp Divider bottom hairline. Confirm-Send / Filters use ModalBottomSheet (16.dp top corners, 4×32.dp drag handle, Color.Black.copy(alpha = 0.6f) scrim).

6. Motion

PayPal motion is restrained and reassuring — gentle springs, no confetti (it’s institutional).

Moment Compose recipe
Send tap scale 1 → 0.97 → 1 spring(dampingRatio = 0.7f, stiffness = 700f) + HapticFeedbackType.LongPress (medium)
Send success success screen slideInVertically(tween(350)); checkmark Animatable scale 0.5 → 1 with 100ms start delay + HapticFeedbackType.Confirm
Activity row press background animateColorAsState to #F5F7FA tween(150), no scale
Tab switch icon outline→filled cross-fade tween(200), label color animateColorAsState #687173 → #003087, .selection haptic
Balance update AnimatedContent slideIn/OutVertically(tween(400)) per §4
Modal sheet ModalBottomSheet velocity-based drag-to-dismiss (Material default)
// Send success checkmark — scale-in with a short delay
@Composable
fun SendSuccessCheck(visible: Boolean) {
    val scale = remember { Animatable(0.5f) }
    val haptics = LocalHapticFeedback.current
    LaunchedEffect(visible) {
        if (visible) {
            haptics.performHapticFeedback(HapticFeedbackType.Confirm) // .success
            kotlinx.coroutines.delay(100)
            scale.animateTo(1f, spring(dampingRatio = 0.7f, stiffness = 500f))
        }
    }
    Box(
        Modifier.size(80.dp).scale(scale.value).clip(CircleShape).background(PayPalColors.Blue),
        contentAlignment = Alignment.Center,
    ) {
        Icon(Icons.Filled.Check, contentDescription = "Sent", tint = Color.White, modifier = Modifier.size(40.dp))
    }
}

Haptics: prefer LocalHapticFeedback. For the medium Send impact use LocalView.current.performHapticFeedback(HapticFeedbackConstants.CONFIRM) (API 30+) or a Vibrator VibrationEffect.createOneShot(15, VibrationEffect.DEFAULT_AMPLITUDE).

7. Icons

PayPal ships custom glyphs; the closest first-party set is androidx.compose.material:material-icons-extended. The dual-blue P-P wordmark is NOT an icon — compose two text Ps (§4) or ship the official two-tone vector drawable.

Purpose SF Symbol (iOS) Material Icon (Compose)
Home (tab) house / house.fill Icons.Filled.Home
Send (tab) paperplane / .fill Icons.Filled.Send
Wallet (tab) wallet.pass / .fill Icons.Filled.AccountBalanceWallet
Activity (tab) clock / .fill Icons.Filled.Schedule
Finances (tab) chart.line.uptrend.xyaxis Icons.Filled.ShowChart
Activity: Sent arrow.up.right Icons.Filled.ArrowOutward
Activity: Received arrow.down.left Icons.Filled.SouthWest
Activity: Card creditcard.fill Icons.Filled.CreditCard
Activity: Reward gift.fill Icons.Filled.Redeem
Notifications bell / bell.fill Icons.Filled.Notifications
Help questionmark.circle Icons.Filled.HelpOutline
Search magnifyingglass Icons.Filled.Search
Back chevron.left Icons.Filled.ArrowBack
Success checkmark.circle.fill Icons.Filled.CheckCircle
Failed xmark.circle.fill Icons.Filled.Cancel

8. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; AnimatedContent + modern motion are comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Edge-to-edge: call enableEdgeToEdge(); the white canvas wants dark-content system bars (light-content in dark mode). The top nav respects the camera cutout; the bottom bar pins above the gesture nav via Scaffold/windowInsetsPadding; ModalBottomSheet respects the bottom inset automatically.
  • Tabular digits: keep fontFeatureSettings = "tnum" on balance, activity amounts, and percentages so the Activity feed’s $-column aligns perfectly.
  • Font scaling: sp honors the user’s font scale — scale body, activity subtitles, secondary labels. Clamp the balance hero (36sp → 48sp max) and Send amount (56sp → 72sp max); pin 11sp tab labels and 13sp filter chips by deriving from dp or wrapping in CompositionLocalProvider(LocalDensity provides Density(density, fontScale = 1f)).
  • TalkBack: merge each activity row with Modifier.semantics(mergeDescendants = true) and label it “Sent to Sarah Kim, $24.50, yesterday at 3:42 PM, completed”; label the balance card “PayPal balance, $1,247 and 92 cents”. The directional icon shape (out-arrow vs SouthWest) carries meaning for color-blind users — never rely on color alone.
  • Touch targets: Material guidance is 48.dp minimum. The 48.dp primary CTA and 68.dp activity rows clear it; give 32.dp filter chips a 48.dp hit width and icon-only nav buttons full IconButton padding.
  • Contrast: #001435 on #FFFFFF exceeds WCAG AA at all sizes; #687173 on white meets AA at 14sp+; PayPal Blue #003087 on white is AAA. Validate the 11sp tab labels and tinted status pills (e.g. #A06B00 on #FFF6E0) with a contrast checker.
  • Reduce motion: when Settings.Global.ANIMATOR_DURATION_SCALE == 0, skip the modal-sheet bounce and the checkmark scale-in (cross-fade instead); render the balance change instantly without the roll.
  • Dark mode: switch to #0A0E1A (cobalt-tinted, NOT pure black) and brighten PayPal Blue to #3B82F6 for contrast — the blue identity must survive in dark. Do not enable Material You dynamicColorScheme() — PayPal’s dual-blue brand is fixed and trust-load-bearing; it must not retint to the user’s wallpaper. Keep Cobalt #001C64 reserved for splash/illustration only.
All mobile designs →

Brand names describe the visual style only; coderef is not affiliated with or endorsed by any brand listed. Licenses