Card Reading UI Customization
Overview
MineHades
MPoC SDK includes a secure card reading process which can't be changed or updated by developers. However,
the SDK
supports developers to customize the UI of the card reading process. There are two ways to customize the UI Activity
MineSecPaymentActivity
- Partial customization allows you to simply customize the UI by changing part of UI components, or theme or logo colors etc.
- Full customization allows you to completely design and control the screens, themes, and behaviors during the payment process.
Be careful!!
Tip: If you choose to use a custom
Activity
, you need to specify it in yourAndroidManifest.xml
as:<activity android:name=".YourCustomPaymentActivity" android:process="com.theminesec.MineHades.MPoCSdk" />
Quick Customize (Partial Customization)
UI can be quickly updated by override and set experimentalScreenProvider
to false. This allows you to implement
partial UI customizations,
such as changing themes and specific screens. By default, the SDK provides predefined screens and themes, but you can
take partial control by providing your own
implementation for certain elements of the UI.
To achieve this, you can override two main components:
Custom Theme
: You can define your own colors and theme for the UI to match your app’s design.Custom UI Provider
: You can provide custom UI for different states like the preparation screen or awaiting card screen.
A general description of the UI partial customization part as below.
class CustomPaymentActivity : MineSecPaymentActivity() {
// you do not need to implement anything if you just want to choose the SDK’s default UI
// Provide Theme or not,primary color
override fun provideTheme() = CustomThemeProvider
// You have to set value experimentalScreenProvider as false
override val experimentalScreenProvider = false
// Provide your own UI
override fun provideUi(): UiProvider {
return CustomUiProvider(
customSupportedPayments = listOf(PaymentMethod.VISA, PaymentMethod.MASTERCARD),
customShowWallet = true
)
}
}
object CustomThemeProvider : ThemeProvider() {
override fun provideColors(darkTheme: Boolean) =
if (darkTheme) MPoCColorsDark().copy(
primary = Color(RED).toArgb(),
primaryForeground = Color(0xFFE4F8EE).toArgb(),
)
else MPoCColorsLight().copy(
primary = Color(RED).toArgb(),
primaryForeground = Color(0xFFFFFFFF).toArgb(),
)
}
class CustomUiProvider(
private val customSupportedPayments: List<PaymentMethod>,
private val customShowWallet: Boolean
) : UiProvider(
amountView = CustomAmountView,
awaitCardIndicatorView = CustomAwaitCardIndicatorView,
progressIndicatorView = CustomProgressIndicatorView
) {
//customSupportedPayments-you can specify your own supported payment methods.
//customShowWallet-you can enable/disable the wallet icon display(Apple pay,Huawei pay,Sumsung pay.)
//Amount display customization
object CustomAmountView : AmountView {
override fun createAmountView(context: Context, amount: Amount, description: String?): View {
}
}
//Await card indicator view customization
//Note:you also can put your own animation(anim_await_card_day.mp4 or anim_await_card_night.mp4) to your application folder(res/raw)
object CustomAwaitCardIndicatorView : AwaitCardIndicatorView {
override fun createAwaitCardIndicatorView(context: Context): View {
}
}
//Progress indicator view customization
object CustomProgressIndicatorView : ProgressIndicatorView {
override fun createProgressIndicatorView(context: Context): View {
}
}
}
Full Customize
If your app requires full customization of the card reading UI, you must override the screenProvider. This will allow you to completely design and control the screens, themes, and behaviors during the payment process. By default, the SDK provides predefined screens and themes, but you can take full control by providing your own implementation. To achieve this, you can override two main components:
- Custom Theme: You can define your own colors and theme for the UI to match your app’s design.
- Custom Screen Provider: You can provide custom screens for different states like the preparation screen or awaiting card screen.
class CustomPaymentActivity : MineSecPaymentActivity() {
// you do not need to implement anything if you just want to choose the SDK’s default UI
// Provide Theme,primary color.
override fun provideTheme() = CustomThemeProvider
// Provide your own screen.
override val screenProvider = MsaScreenProvider
}
object CustomThemeProvider : ThemeProvider() {
override fun provideColors(darkTheme: Boolean) =
if (darkTheme) MPoCColorsDark().copy(
primary = Color(RED).toArgb(),
primaryForeground = Color(0xFFE4F8EE).toArgb(),
)
else MPoCColorsLight().copy(
primary = Color(RED).toArgb(),
primaryForeground = Color(0xFFFFFFFF).toArgb(),
)
}
object YourOwnScreenProvider : ScreenProvider() {
override fun PreparationScreen(
request: MhdEmvTransactionDto,
preparingFlow: Flow<UiState.Preparing>,
countdownFlow: StateFlow<Int>
) {
//request-with transaction data,txnAmount,txnCurrencyText,descriptions,so you can implement amount display by here.
//preparingFlow-you can display the UI state text by here,title,description.
//val uiState by preparingFlow.collectAsStateWithLifecycle(UiState.Preparing.Idle)
//countdownFlow-you can display the timeout counter by here .
//val countdownSec by countdownFlow.collectAsStateWithLifecycle(),and also can add fallback to process timeout.
}
override fun AwaitingCardScreen(
request: MhdEmvTransactionDto,
awaitingFlow: Flow<UiState.Awaiting>,
supportedMethods: List<PaymentMethod>,
countdownFlow: StateFlow<Int>,
onAbort: () -> Unit
) {
//request-with transaction data,txnAmount,txnCurrencyText,descriptions,so you can implement amount display by here.
//preparingFlow-you can display the UI state text by here,title,description.
//val uiState by awaitingFlow.collectAsStateWithLifecycle(UiState.Preparing.Idle)
//countdownFlow-you can display the timeout counter by here .
//onAbort-the abort(cancel) icon customization
}
}
Appendix
Reference - partial customization example
class CustomPaymentActivity : MineSecPaymentActivity() {
override val experimentalScreenProvider = false
override fun provideUi(): UiProvider {
return CustomUiProvider(
customSupportedPayments = listOf(PaymentMethod.VISA, PaymentMethod.MASTERCARD),
customShowWallet = true
)
}
}
object CustomThemeProvider : ThemeProvider() {
override fun provideColors(darkTheme: Boolean) =
if (darkTheme) MPoCColorsDark().copy(
primary = Color(RED).toArgb(),
primaryForeground = Color(0xFFE4F8EE).toArgb(),
)
else MPoCColorsLight().copy(
primary = Color(RED).toArgb(),
primaryForeground = Color(0xFFFFFFFF).toArgb(),
)
}
class CustomUiProvider(
private val customSupportedPayments: List<PaymentMethod>,
private val customShowWallet: Boolean
) : UiProvider(
amountView = CustomAmountView,
awaitCardIndicatorView = CustomAwaitCardIndicatorView,
progressIndicatorView = CustomProgressIndicatorView
) {
object CustomAmountView : AmountView {
override fun createAmountView(
context: Context,
amount: Amount,
description: String?
): View {
return TextView(context).apply {
text = "${amount.toDisplayString()} - ${description ?: "No Description"}"
textSize = 18f
setTextColor(Color.Black.toArgb())
}
}
}
object CustomAwaitCardIndicatorView : AwaitCardIndicatorView {
override fun createAwaitCardIndicatorView(context: Context): View {
return TextView(context).apply {
text = "Please insert or tap your card..."
textSize = 16f
setTextColor(Color.Blue.toArgb())
}
}
}
object CustomProgressIndicatorView : ProgressIndicatorView {
override fun createProgressIndicatorView(context: Context): View {
return ProgressBar(context).apply {
isIndeterminate = true
}
}
}
object CustomUiStateDisplayView : UiStateDisplayView {
override fun createUiStateDisplayView(context: Context, uiState: UiState): View {
return TextView(context).apply {
text = when (uiState) {
is UiState.Preparing -> "Loading..."
is UiState.Awaiting -> "Transaction Successful"
// is UiState.Processing -> "Transaction Failed: "
else -> "Awaiting Transaction..."
}
textSize = 18f
setTextColor(
when (uiState) {
is UiState.Preparing -> Color.Blue.toArgb()
is UiState.Awaiting -> Color.Green.toArgb()
// is UiState.Processing -> Color.Red.toArgb()
else -> Color.Gray.toArgb()
}
)
gravity = Gravity.CENTER
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
setMargins(16, 16, 16, 16)
}
}
}
}
@Composable
override fun AcceptanceMarkDisplay(
supportedPayments: List<PaymentMethod>,
showWallet: Boolean
) {
//Also using the default payment methods display
super.AcceptanceMarkDisplay(customSupportedPayments, customShowWallet)
}
}
Reference - full customize example
class CustomPaymentActivity : MineSecPaymentActivity() {
override fun provideTheme() = CustomThemeProvider
override val screenProvider = MsaScreenProvider
}
object MsaScreenProvider : ScreenProvider() {
private const val customerFontFeature = "ss01,ss04,cv10"
private val shellPadding
@Composable
get() = PaddingValues(SampleTheme.spacing.md)
@Composable
override fun PreparationScreen(
request: MhdEmvTransactionDto,
preparingFlow: Flow<UiState.Preparing>,
countdownFlow: StateFlow<Int>
) {
val uiState by preparingFlow.collectAsStateWithLifecycle(UiState.Preparing.Idle)
Box(
modifier = Modifier
.fillMaxSize()
.padding(shellPadding),
) {
// render animation first to lay it in the back
Column(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(modifier = Modifier.size(280.dp), contentAlignment = Alignment.Center) {
SampleProcessingIndicator()
}
Spacer(Modifier.height(SampleTheme.spacing.lg))
CustomStateDisplay(uiState)
}
Column(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopCenter),
) {
CustomTopBar(countdownFlow)
CustomAmountDisplay(request)
}
Column(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
) {
CustomCopyright()
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
override fun AwaitingCardScreen(
request: MhdEmvTransactionDto,
awaitingFlow: Flow<UiState.Awaiting>,
supportedMethods: List<PaymentMethod>,
countdownFlow: StateFlow<Int>,
onAbort: () -> Unit
) {
val uiState by awaitingFlow.collectAsStateWithLifecycle(UiState.Preparing.Idle)
Box(
modifier = Modifier
.fillMaxSize()
.padding(shellPadding),
) {
// render animation first to lay it in the back
Column(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SampleAwaitCardIndicator()
Spacer(Modifier.height(SampleTheme.spacing.lg))
CustomStateDisplay(uiState)
}
Column(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopCenter),
) {
CustomTopBar(countdownFlow, onAbort)
CustomAmountDisplay(request)
}
Column(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
) {
FlowRow(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(SampleTheme.spacing.xs2),
horizontalArrangement = Arrangement.spacedBy(
SampleTheme.spacing.xs,
Alignment.CenterHorizontally
),
) {
supportedMethods.filter { it == PaymentMethod.VISA || it == PaymentMethod.MASTERCARD }.forEach { it.Icon() }
}
Spacer(modifier = Modifier.size(SampleTheme.spacing.xs2))
FlowRow(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(SampleTheme.spacing.xs2),
horizontalArrangement = Arrangement.spacedBy(
SampleTheme.spacing.xs,
Alignment.CenterHorizontally
),
) { WalletType.entries.forEach { it.Icon() } }
Spacer(modifier = Modifier.size(SampleTheme.spacing.lg))
CustomCopyright()
}
}
}
@Composable
fun CustomTopBar(
countdownFlow: StateFlow<Int>,
onAbort: (() -> Unit)? = null,
) {
val countdownSec by countdownFlow.collectAsStateWithLifecycle()
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
verticalAlignment = Alignment.CenterVertically,
) {
onAbort?.let {
IconButton(
modifier = Modifier
.size(56.dp)
.requiredSize(56.dp)
.offset(x = (-16).dp),
colors = IconButtonDefaults.iconButtonColors(
contentColor = SampleTheme.mpocColors.mutedForeground.toComposeColor(),
),
onClick = it,
) {
Icon(
painter = painterResource(id = com.theminesec.MineHades.R.drawable.ico_close),
contentDescription = stringResource(com.theminesec.MineHades.R.string.action_abort)
)
}
}
Spacer(Modifier.weight(1f, true))
Text(
text = stringResource(com.theminesec.MineHades.R.string.var_countdown_second, countdownSec),
style = SampleTheme.typography.bodyLarge,
color = SampleTheme.mpocColors.primary.toComposeColor()
)
}
}
@Composable
fun CustomAmountDisplay(request: MhdEmvTransactionDto) {
val amount = Amount(
BigDecimal.valueOf(request.txnAmount)
.movePointLeft(Currency.getInstance(request.txnCurrencyText).defaultFractionDigits),
Currency.getInstance(request.txnCurrencyText)
)
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(Modifier.height(SampleTheme.spacing.sm))
Text(
text = when (request.txnType) {
TranType.SALE -> "Sale"
TranType.REFUND -> "Refund"
},
style = SampleTheme.typography.titleSmall,
color = SampleTheme.mpocColors.mutedForeground.toComposeColor(),
)
Text(
text = amount.toDisplayString(),
style = SampleTheme.typography.headlineLarge.copy(fontFeatureSettings = "$customerFontFeature,tnum"),
)
request.description?.let {
Text(it)
}
}
}
@Composable
fun CustomStateDisplay(uiState: UiState) {
Text(
text = uiState.getTitle(),
textAlign = TextAlign.Center,
style = SampleTheme.typography.titleLarge,
)
Spacer(Modifier.size(SampleTheme.spacing.xs2))
Text(
text = uiState.getDescription(),
textAlign = TextAlign.Center,
style = SampleTheme.typography.bodyMedium,
color = SampleTheme.mpocColors.accentForeground.toComposeColor()
)
}
@Composable
fun CustomCopyright() {
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
text = stringResource(R.string.var_copyright, LocalDate.now().year),
style = SampleTheme.typography.bodySmall.copy(fontFeatureSettings = "$baseFontFeature,tnum"),
color = SampleTheme.mpocColors.mutedForeground.toComposeColor(),
)
}
}

