MineSec Pay 7
UI Customization
Theming & Branding

Theming & Branding

msa-uikit ships with a complete design system, and every part of it can be changed from one place: the MsaBrand form. You fill in only what you want to change. Everything you leave out keeps the MineSec default, so you also pick up later fixes to the defaults.

val MyBrandTheme = MsaBrand(
    primaryLight = Color(0xFFFF6600),
    primaryDark = Color(0xFFFF6600),
    onBrandLight = Color(0xFF1A0A00),  // dark ink: white on #FF6600 is 2.94:1 and fails WCAG AA
    onBrandDark = Color(0xFF1A0A00),
    fontFamily = FontFamily(Font(Res.font.mybrand_regular), Font(Res.font.mybrand_bold)),
    logoOnLight = Res.drawable.mybrand_logo_dark_ink,
    logoOnDark = Res.drawable.mybrand_logo_light_ink,
).toTheme()

This page covers:


Two ways to fill the form

BuilderUse it when
MsaBrand.fromBrandColor(...)You have one brand colour (Level 1 style). The palette is derived from it, and the text colour on it is picked by WCAG contrast.
MsaBrand(...)You set individual tokens, fonts, sizes or shapes.

Both end with .toTheme(), which returns an MsaCustomTheme.

import com.theminesec.multiplatform.msa.theme.MsaBrand
 
val MyBrandTheme = MsaBrand.fromBrandColor(
    brandColor = Color(0xFF0066CC),
    brandColorDark = Color(0xFF0066CC),   // optional, defaults to brandColor
    gradient = Color(0xFF3388DD),         // optional second stop of the gradient button
    logoOnLight = Res.drawable.mybrand_logo_dark_ink,
    logoOnDark = Res.drawable.mybrand_logo_light_ink,
    firstScreenBg = Res.drawable.mybrand_landing,
).toTheme()

If you leave gradient out, the gradient button is a flat brandColor.


Colours

Colours are defined by the MsaColors interface. The defaults are MsaColorsLight and MsaColorsDark. Each token is named for what it is used for, and comes in surface and content pairs: a surface is painted behind things, and its content token is what stays readable on it.

In MsaBrand, each token has a light and a dark parameter: primaryLight / primaryDark, onBrandLight / onBrandDark, and so on.

Brand

TokenLightDarkUse
primary#01B856#01B856Brand colour: filled buttons, links, approved badges
primaryGradient#2BD659#2BD659Second stop of the gradient button. Set it whenever you set primary.
secondary#0A307A#0A307ASecondary brand, used by Void
onBrand#FFFFFF#FFFFFFText and icons on primary or secondary

Surfaces

TokenLightDarkUse
background#FFFFFF#0A101DThe app background
foreground#101A2D#FFFFFFPrimary text
surface#F3F5FA#334155Panels, result cards, the QR card
onSurface#3E4B65#CBD5E1Text on surface
paymentScreenBgfollows primary#0A101DSale, amount entry, QR and the payment header
paymentScreenForeground#FFFFFF#FFFFFFText on paymentScreenBg

Status

TokenLightDarkUse
error#F62851#F62851Declined transactions and destructive actions
warning#F5BC15#F5BC15In progress, attention
onStatus#FFFFFF#FFFFFFText on a status colour
approval#22D375#22D375Approval accent: the picker tick and affirmative text actions

Approved badges use primary.

Controls

TokenLightDarkUse
muted#E1E7F4#334155Disabled surface
mutedForeground#67748E#CBD5E1Disabled and secondary text
input#8699C1#8699C1Input borders
ring#C9D2E3#C9D2E3Focus rings and dividers

Standalone

TokenLightDarkUse
scrim#1D1C22#1D1C22Wash behind a modal
imageForeground#FFFFFF#FFFFFFText on the landing screen photograph

Derived tokens

paymentScreenBg follows primary in light mode, so changing primary also moves the payment screens. Set paymentScreenBgLight / paymentScreenBgDark only to break that link on purpose.

Material's ColorScheme is derived from MsaColors, so Material components follow your palette without a second mapping.

Several tokens are white on purpose

onBrand, onStatus, paymentScreenForeground and imageForeground are all white by default, but they are not interchangeable. Each one keeps text readable on a different surface. With a light brand colour such as amber or lime, onBrand must become dark while imageForeground stays white on the photograph. Set each one for the surface it sits on.

Check contrast

A mid-range brand colour usually needs dark text on it. Before you ship, check onBrand against primary:

ContrastWCAG AA (normal text)
#FFFFFF on #FF66002.94:1Fails
#1A0A00 on #FF66006.58:1Passes

MsaBrand.fromBrandColor does this check for you. MsaBrand.contrastRatio(a, b) and MsaBrand.readableOn(color) are available if you build the palette yourself.

Card-scheme and wallet marks

Visa, Mastercard, UnionPay and the other scheme and wallet marks keep their own colours. Each scheme's brand rules forbid recolouring them, so they have no token.


Typography

Typography uses Material 3's Typography with 13 styles. The default typeface is Inter.

StyleSize / line heightWeightTypical use
labelSmall14 / 20 spNormalCaptions
labelMedium14 / 20 spMediumLabels
labelLarge14 / 24 spSemiBoldEmphasised labels
bodySmall16 / 20 spNormalBody text
bodyMedium16 / 24 spMediumBody text
bodyLarge16 / 20 spSemiBoldProminent body text, buttons
titleSmall18 / 24 spSemiBoldSubsection titles
titleMedium20 / 24 spSemiBoldSection headers
titleLarge24 / 24 spSemiBoldPage titles
headlineSmall24 / 36 spSemiBoldHeadings
headlineMedium28 / 36 spSemiBoldQR headings
headlineLarge32 / 40 spSemiBoldLarge headings
displaySmall36 / 44 spBoldThe transaction amount

Change the typeface

Pass a FontFamily to fontFamily. It can come from Compose Multiplatform resources, as below, or from Android res/font/ with Font(R.font.mybrand_regular, FontWeight.Normal):

import org.jetbrains.compose.resources.Font
 
MsaBrand(
    fontFamily = FontFamily(
        Font(Res.font.mybrand_regular, FontWeight.Normal),
        Font(Res.font.mybrand_medium, FontWeight.Medium),
        Font(Res.font.mybrand_semibold, FontWeight.SemiBold),
        Font(Res.font.mybrand_bold, FontWeight.Bold),
    ),
)

This replaces Inter in all 13 styles and keeps the sizes, weights and line heights. Set typography instead only when the sizes themselves must change.

The receipt keeps a monospace face so that its columns line up. Change it with receiptFont only if your font is also fixed-width.


Sizes

Sizes come in five groups. They are fixed at the 360 dp design baseline and do not scale with the window width.

GroupTokens
Spacingxs2 4 · xs 8 · sm 12 · md 16 · lg 24 · xl 32 · xl2 48 · xl4 80
IconSizexs 16 · sm 20 · md 24 · lg 28 · xl 32 · xl2 36 · xl3 48 · xl4 64
MinTouchSizesm 40 · md 56 · lg 60 · xl3 100
Radiusmd 6 · lg 8
AnimationSizesm 160

All values are in dp. To change a group, implement its interface and pass it to MsaBrand:

MsaBrand(
    radius = object : Radius {
        override val md = 2.dp
        override val lg = 4.dp
    },
)

Read sizes from MsaTheme in your screens rather than writing .dp values inline, so that they follow the theme:

Column(modifier = Modifier.padding(MsaTheme.spacing.md)) {
    Icon(
        imageVector = Icons.Default.CheckCircle,
        contentDescription = null,
        modifier = Modifier.size(MsaTheme.iconSize.lg),
    )
}

Shapes

Shapes set the rounded corners of buttons, cards and text fields. Pass a Material Shapes to shapes:

MsaBrand(
    shapes = Shapes(
        extraSmall = RoundedCornerShape(2.dp),
        small = RoundedCornerShape(4.dp),
        medium = RoundedCornerShape(8.dp),
        large = RoundedCornerShape(12.dp),
        extraLarge = RoundedCornerShape(20.dp),
    ),
)

Read them in your UI with MsaTheme.shapes:

Box(
    modifier = Modifier
        .clip(MsaTheme.shapes.large)
        .background(MsaTheme.colors.primary)
)

Logos and artwork

msa-uikit keeps its images in Compose Multiplatform resources, not in Android res/. Android's resource merging does not apply to them: a drawable with the same name in your app does not replace the uikit one.

Pass your artwork to MsaBrand as values instead. The parameters take a Compose DrawableResource, so apply the Compose Multiplatform plugin to your app module, add the files to its composeResources/drawable/ folder, and reference them through your module's generated Res:

MsaBrand parameterWhere it appears
logoOnLightLogo on light surfaces, for example the printed receipt (use your dark-ink logo)
logoOnDarkLogo on dark or brand-coloured surfaces, for example the launch screen (use your light-ink logo)
firstScreenBgFull-screen artwork behind the landing screen

Use vector XML or PNG/WebP for drawables. Each slot you leave out keeps the MineSec artwork.

The launcher icon (ic_launcher) is an ordinary Android resource in your app module and is set as in any Android app. The merchant's own logo and name are not part of the theme: they come from the backend for each merchant.


Applying your theme

MineSec Pay 7 draws UI in several separate activities: the main activity, the App2App activity and the card-reading activity. Each one composes on its own, so apply the same theme value in two places.

1. Wrap your app in MsaAppTheme

class MainActivity : BaseMsaActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MsaAppTheme(MyBrandTheme) {
                App(screenProviders = myScreenProviders)
            }
        }
    }
}

Use MsaAppTheme rather than AppTheme. AppTheme applies the colours, type and sizes but not the logos.

2. Pass the theme to your screen providers

The App2App and card-reading activities compose through ScreenProviders.themeWrapper:

class MyScreenProviders : DefaultScreenProviders(MyBrandTheme) {
    override val themeWrapper: @Composable (content: @Composable () -> Unit) -> Unit =
        { content -> MsaAppTheme(MyBrandTheme) { content() } }
}

Without this, a re-branded build shows MineSec branding on App2App payments and on the card prompt. Check your re-brand on all of these screens, not only the main activity: the card screens are the ones the cardholder sees.

3. Use the theme in your screens

Anywhere inside the theme, read tokens from MsaTheme:

Text(
    text = "Welcome",
    color = MsaTheme.colors.foreground,
    style = MsaTheme.typography.headlineSmall,
    modifier = Modifier.padding(MsaTheme.spacing.md),
)

MsaTheme exposes colors, typography, receiptFontFamily, spacing, iconSize, minTouchSize, radius, animationSize, shapes and isDark.

Advanced: subclass ThemeProvider

MsaBrand covers every value MineSec Pay 7 lets you change. If you need logic it cannot express, for example a colour that depends on a runtime value, subclass ThemeProvider and override its hooks: provideMsaColors, provideFontFamily, provideTypography, provideShapes, provideSpacing, provideIconSize, provideRadius, provideMinTouchSize, provideAnimationSize and provideReceiptFontFamily. MsaCustomTheme is itself a ThemeProvider.


Next Steps

  • Screen Providers Reference: every screen with its State and Action classes
  • Example project (provided to Level 2 customers): see branding/AppThemes.kt for a worked theme