MPoC SDK
API Reference
UI Customization

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 your AndroidManifest.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.

customer-view

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(),
    )
  }
 
}
partial customization
full customization