Skip to main content
Version: 4.0

Configuration

Tailor the Halo UI SDK to your application's needs through the HDConfig class and custom theming.

Initialization

Build one HDConfig and hand it to both HaloSdkUi.prepare and HaloSdkUi.init. The activity passed to HDConfig must be a ComponentActivity (or subclass such as AppCompatActivity), since the SDK renders its UI with Jetpack Compose.

HDConfig Parameters

ParameterTypeDefaultDescription
activityComponentActivity-The activity that will host the SDK UI.
onTokenRequestsuspend () -> String-Suspending callback that must return a valid Halo SDK token.
presentationHDPresentationFULL_SCREENHow inbound payments are presented — see Presentation.
themeHDThemeHDTheme()Colors, shapes, and logo configuration.
themeModeHaloThemeModeSYSTEMForce light, dark, or follow the system theme.
schemeLogosHDSchemeLogosHDSchemeLogos()Which card scheme logos are shown.
showTransactionResultBooleantrueShow the SDK's own result/receipt screen after a charge. Set false to return to your app as soon as the transaction completes.
languageHDLanguage?nullPins the SDK language — see Languages.
schemeString"halo"The custom URL scheme this brand's payment links arrive on — see Inbound Payments.
kernelString?nullThe Halo kernel short App Links are resolved against, e.g. kernelserver.qa.haloplus.io.
kernelPinsSet<String>emptySet()SHA-256 SPKI fingerprints for kernel's TLS certificate, as sha256/<base64>.
receivePushBooleanfalseWhether this device registers itself to receive Push to Terminal payments — see Push to Terminal.
note

The merchant's name and details are not configured here — they come from the Halo backend record behind your token, and are displayed by the SDK from there.

Example

import za.co.synthesis.halo.sdk_ui.HaloSdkUi
import za.co.synthesis.halo.sdk_ui.models.HDConfig

val config = HDConfig(
activity = this,
onTokenRequest = {
// Return your Halo SDK token here (e.g., fetch from your backend)
"YOUR_SDK_TOKEN"
},
)

HaloSdkUi.prepare(config) // at your splash
val result = HaloSdkUi.init(config) // suspending, once you have a token
note

init returns the SDK's HaloInitializationResult — its resultType and errorCode carry why when bring-up failed — or null when a step failed before the SDK could report at all. It is idempotent: a second init on a live SDK answers with what it already has.

Bring-up: attach at onCreate, prepare at your splash, init once you hold a token, launch per charge.

What Each Call Does

attach(activity, savedInstanceState) hands the SDK its Android context: the payment kernel, overlay protection, entropy. It is the single most expensive step of bringing the SDK up, and it needs nothing from you but the activity — so run it first, immediately after super.onCreate, and everything your app does afterwards runs against a payment stack that is already loading. Anywhere later and the cost lands on whoever is waiting next. It also wires the Android lifecycle through to the SDK — you do not forward onStart/onResume/onPause/onStop yourself. Pass savedInstanceState straight through so the SDK can restore its own state across a process death. Cheap to call more than once.

prepare(config) is not a suspend function, returns immediately, and cannot fail in a way you have to handle. It caches the appearance half of your config — theme, language, scheme logos, scheme, kernel — which is what lets an inbound payment, which brings the SDK up natively with no host in the process, still show your branding and speak your language. It also loads the translations and warms the tap screen's artwork in the background.

init(config) is a suspend function — call it from a coroutine. It registers the device (re-attesting only when the token's identity has changed since last time), brings the SDK up, and suspends until the SDK reports its real outcome: registered, attested, initialized, kernel settled. Runtime permissions (Permissions) are requested alongside it, not before it — a denial doesn't block bring-up. Call it the moment your login succeeds rather than from your payment screen: the merchant is almost certainly still navigating, which is free time this can run in.


Presentation

HDPresentation controls how the SDK puts its screens on top of your app.

ValueDescription
HDPresentation.FULL_SCREENThe SDK as its own app: an opaque activity that replaces the host for the length of a charge.
HDPresentation.SHEETThe charge as a bottom sheet with your app dimmed behind it — reads as your app asking for a card rather than another app taking over.

HDConfig.presentation is the answer for inbound payments only (an app-to-app intent or a payment link, which arrive with no launch call to say). A charge you start says it per call on launch — see Usage — so consecutive charges can differ: a quick tap as a sheet over the screen the merchant was on, the next one full-screen, and back again.

HaloSdkUi.launch(amount, merchantRef, currency, presentation = HDPresentation.SHEET)

The SDK ships the translucent window SHEET needs — there is nothing to add to your manifest.

What a Sheet Covers

A sheet applies to the charge itself — bring-up, tap, the card-scheme animation, the currency choice, the result and the error screen — so nothing switches surface mid-card-read. The keypad and the detailed transaction breakdown stay full-screen either way: one is a screen's worth of controls, the other a screen's worth of rows, and a sheet is a shape as well as a position.

It is the same shell either way — every page declares a heading, a body and an optional footer, and the shell arranges them: stacked, or two panes side by side once the window is wider than it is tall. There is no separate sheet layout to fall out of sync with the full-screen one.

Two things belong to the sheet alone:

  • A bar of its own at the top — your logo on the left, and the buttons the full-screen style puts at the foot of the page (Cancel, Share receipt, Charge) as small actions on the right, since two button heights out of a few hundred dp of sheet is a real cost. Your logo sits leading rather than centred here, because the bar is only a few hundred dp across and a centred mark would shift every time the actions change width.
  • One sheet for the whole charge. The pages change inside it rather than each raising a sheet of its own — bring-up handing over to tap is not a surface closing and another opening over your app.

Swiping the sheet down, tapping the dimmed area, and pressing back all raise a cancel confirmation rather than closing outright — on bring-up as well as on tap — so an accidental swipe can't abort a live card read.

"Powered by Halo Dot" sits at the foot of the sheet, not on every screen: inside your app, the payment surface has to say whose it is, and full-screen the top bar has already said so.

What SHEET costs

A translucent window is no longer stopped behind the payment surface — your app keeps rendering for the whole transaction, next to a card read — and your pixels are visible behind a card-present screen, which is the shape overlay protection exists to catch (the SDK reports HaloErrorCode.OverlayDetected if it sees one). Both are reasonable trade-offs for a brand that has decided a sheet is the right experience, but know it before choosing it.

Inbound payment links follow HDConfig.presentation, since they arrive with no launch call to ask — a brand that presents its own charges as a sheet looks the same however the charge arrived.


SDK Version & Device Installation ID

HaloSdkUi.sdkVersion returns the underlying Halo SDK's own version string, for a host that displays it (e.g. beside a link to the PCI listing the SDK is certified under). It's a property of the library, so it's readable before attach.

val version = HaloSdkUi.sdkVersion

HaloSdkUi.deviceInstallationId(context) returns the Halo installation id for this app on this device — what the kernel knows the install by, and what you'd present to register the device for Push to Terminal. It is null until the device has registered at least once, since the SDK is only handed the id inside init's own device registration; from then on it survives launches and process deaths.

val installation = HaloSdkUi.deviceInstallationId(context)

Theming

Tailor the SDK's look and feel to match your app's brand.

Theme Mode

Control whether the SDK follows the system theme or forces a specific mode. You can set this in HDConfig or update it at runtime.

ModeDescription
HaloThemeMode.LIGHTForces light theme
HaloThemeMode.DARKForces dark theme
HaloThemeMode.SYSTEMFollows system theme (default)
import za.co.synthesis.halo.sdk_ui.models.HaloThemeMode

// Update at runtime
HaloSdkUi.setThemeMode(HaloThemeMode.SYSTEM)

Custom Theme

Define your brand's colors, shapes, and logo via HDTheme and pass it to your HDConfig.

HDTheme Properties

PropertyTypeDefaultDescription
lightHDColorSchemeHDColorScheme.default()Color scheme for light mode
darkHDColorSchemeHDColorScheme.defaultDark()Color scheme for dark mode
shapeDp16.dpCorner radius for buttons and containers
paddingHorizontalDp24.dpHorizontal padding inside containers
paddingVerticalDp12.dpVertical padding inside containers
logoHDCompanyLogoHDCompanyLogo()Company logo configuration
textHDTextSizesHDTextSizes()Type scale — see Text Sizes

HDColorScheme Properties

PropertyTypeDescription
primaryColorMain brand color (buttons, highlights)
secondaryColorSecondary accent color
surfaceColorBackground color
onSurfaceColorText/icon color on surface
onPrimaryColorText/icon color on primary background
outlineColorBorder and divider color
errorColorError/decline color

Start from HDColorScheme.default() / HDColorScheme.defaultDark() and copy() what you need — every property is required, so there is no partial constructor.

HDCompanyLogo Properties

PropertyTypeDefaultDescription
assetString?nullYour logo — a URL, or a path to an asset bundled in your app. null uses the bundled Halo logo.
iconString?nullYour square app mark — the one on your launcher. Used where the SDK has one glyph's worth of room rather than a bar: today, the small icon on a pushed payment's notification. null falls back to your launcher icon.

asset is a self-theming template SVG: one file serves both light and dark mode. The SDK substitutes the active colour scheme into shared placeholder tokens ({{PRIMARY}}, {{SECONDARY}}, {{ERROR}}, {{SURFACE}}, {{ONSURFACE}}, {{OUTLINE}}) before rendering, so you never ship separate light/dark variants; any token left unreplaced renders as an invisible fill.

import za.co.synthesis.halo.sdk_ui.models.HDTheme
import za.co.synthesis.halo.sdk_ui.models.HDColorScheme
import za.co.synthesis.halo.sdk_ui.models.HDCompanyLogo
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

val myCustomTheme = HDTheme(
light = HDColorScheme.default().copy(
primary = Color(0xFF6200EE),
onPrimary = Color.White,
),
dark = HDColorScheme.defaultDark().copy(
primary = Color(0xFFBB86FC),
),
shape = 12.dp,
logo = HDCompanyLogo(asset = "my-brand-logo.svg", icon = "my-brand-icon.svg"),
)

val config = HDConfig(
// ... other props
theme = myCustomTheme
)

Text Sizes

HDTextSizes is the SDK's whole type scale — one size per text role, in sp, honouring the device's font-size accessibility setting. The defaults are the Material 3 scale, so a host that says nothing gets what the SDK has always drawn. Raise them for an unattended terminal, or lower them to fit more receipt on a short screen; line spacing scales with the size.

PropertyDefaultUsed for
display45.spThe amount, and nothing else
titleLarge22.spA page's headline
title16.spSection headings and button labels
body14.spRunning text
bold16.spEmphasis within running text
label12.spThe left side of a detail row
value14.spThe right side of a detail row
import za.co.synthesis.halo.sdk_ui.models.HDTextSizes

// The one-knob version: the same UI, 20% bigger.
val theme = HDTheme(text = HDTextSizes.scaled(1.2f))

// Or set individual roles.
val custom = HDTheme(text = HDTextSizes(display = 52.sp, label = 13.sp))

Bridging from Flutter or React Native

Hosts that build their theme outside Kotlin can avoid Compose types entirely: HDTheme.build takes raw dp floats and an asset path, and HDColorScheme.fromArgb takes 0xAARRGGBB values. HDTextSizes.fromSp does the same for type — any size left null keeps its default.

val theme = HDTheme.build(
light = HDColorScheme.fromArgb(
primary = 0xFF6200EE, secondary = 0xFFF2B500, surface = 0xFFFFFFFF,
onSurface = 0xFF000000, onPrimary = 0xFFFFFFFF, outline = 0xFF666666,
error = 0xFFFF647C,
),
shapeDp = 12f,
logo = "my-brand-logo.svg",
icon = "my-brand-icon.svg",
text = HDTextSizes.fromSp(display = 52f),
)

Languages

The SDK UI ships translations for English (en), Afrikaans (af), Zulu (zu), French (fr), German (de), Spanish (es), and Portuguese (pt). By default it follows the device language, falling back to English for unsupported locales. To pin a specific language:

import za.co.synthesis.halo.sdk_ui.core.HDLanguage

val config = HDConfig(
// ... other props
language = HDLanguage.AFRIKAANS,
)
HaloSdkUi.setLanguage(HDLanguage.ENGLISH)

Scheme Logos

Control which payment scheme logos are displayed in the SDK UI using HDSchemeLogos. By default, all supported logos are enabled.

PropertyTypeDefaultDescription
nfcBooleantrueShow the NFC/Contactless indicator
visaBooleantrueShow the Visa logo
mastercardBooleantrueShow the Mastercard logo
amexBooleantrueShow the American Express logo
discoverBooleantrueShow the Discover logo
eloBooleantrueShow the Elo logo
import za.co.synthesis.halo.sdk_ui.models.HDSchemeLogos

val myLogos = HDSchemeLogos(
amex = false,
discover = false
)

val config = HDConfig(
// ... other props
schemeLogos = myLogos
)
note

The DebiCheck mark is not listed here. It is not a brand's choice of what it accepts — a mandate is a DebiCheck — so the SDK shows it on a mandate and only on a mandate.