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
- Colours: the 20 colour tokens
- Typography: typeface and type scale
- Sizes: spacing, icons, radius and touch targets
- Shapes: corner shapes for surfaces
- Logos and artwork
- Applying your theme
Two ways to fill the form
| Builder | Use 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
| Token | Light | Dark | Use |
|---|---|---|---|
primary | #01B856 | #01B856 | Brand colour: filled buttons, links, approved badges |
primaryGradient | #2BD659 | #2BD659 | Second stop of the gradient button. Set it whenever you set primary. |
secondary | #0A307A | #0A307A | Secondary brand, used by Void |
onBrand | #FFFFFF | #FFFFFF | Text and icons on primary or secondary |
Surfaces
| Token | Light | Dark | Use |
|---|---|---|---|
background | #FFFFFF | #0A101D | The app background |
foreground | #101A2D | #FFFFFF | Primary text |
surface | #F3F5FA | #334155 | Panels, result cards, the QR card |
onSurface | #3E4B65 | #CBD5E1 | Text on surface |
paymentScreenBg | follows primary | #0A101D | Sale, amount entry, QR and the payment header |
paymentScreenForeground | #FFFFFF | #FFFFFF | Text on paymentScreenBg |
Status
| Token | Light | Dark | Use |
|---|---|---|---|
error | #F62851 | #F62851 | Declined transactions and destructive actions |
warning | #F5BC15 | #F5BC15 | In progress, attention |
onStatus | #FFFFFF | #FFFFFF | Text on a status colour |
approval | #22D375 | #22D375 | Approval accent: the picker tick and affirmative text actions |
Approved badges use primary.
Controls
| Token | Light | Dark | Use |
|---|---|---|---|
muted | #E1E7F4 | #334155 | Disabled surface |
mutedForeground | #67748E | #CBD5E1 | Disabled and secondary text |
input | #8699C1 | #8699C1 | Input borders |
ring | #C9D2E3 | #C9D2E3 | Focus rings and dividers |
Standalone
| Token | Light | Dark | Use |
|---|---|---|---|
scrim | #1D1C22 | #1D1C22 | Wash behind a modal |
imageForeground | #FFFFFF | #FFFFFF | Text 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:
| Contrast | WCAG AA (normal text) | |
|---|---|---|
#FFFFFF on #FF6600 | 2.94:1 | Fails |
#1A0A00 on #FF6600 | 6.58:1 | Passes |
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.
| Style | Size / line height | Weight | Typical use |
|---|---|---|---|
labelSmall | 14 / 20 sp | Normal | Captions |
labelMedium | 14 / 20 sp | Medium | Labels |
labelLarge | 14 / 24 sp | SemiBold | Emphasised labels |
bodySmall | 16 / 20 sp | Normal | Body text |
bodyMedium | 16 / 24 sp | Medium | Body text |
bodyLarge | 16 / 20 sp | SemiBold | Prominent body text, buttons |
titleSmall | 18 / 24 sp | SemiBold | Subsection titles |
titleMedium | 20 / 24 sp | SemiBold | Section headers |
titleLarge | 24 / 24 sp | SemiBold | Page titles |
headlineSmall | 24 / 36 sp | SemiBold | Headings |
headlineMedium | 28 / 36 sp | SemiBold | QR headings |
headlineLarge | 32 / 40 sp | SemiBold | Large headings |
displaySmall | 36 / 44 sp | Bold | The 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.
| Group | Tokens |
|---|---|
Spacing | xs2 4 · xs 8 · sm 12 · md 16 · lg 24 · xl 32 · xl2 48 · xl4 80 |
IconSize | xs 16 · sm 20 · md 24 · lg 28 · xl 32 · xl2 36 · xl3 48 · xl4 64 |
MinTouchSize | sm 40 · md 56 · lg 60 · xl3 100 |
Radius | md 6 · lg 8 |
AnimationSize | sm 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 parameter | Where it appears |
|---|---|
logoOnLight | Logo on light surfaces, for example the printed receipt (use your dark-ink logo) |
logoOnDark | Logo on dark or brand-coloured surfaces, for example the launch screen (use your light-ink logo) |
firstScreenBg | Full-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.ktfor a worked theme