In-Car Payments
In-Car Wallets is a feature which is currently in private beta. Please contact your account representative to get access to the SDK and the Wallet infrastructure.
Connected commerce around cars is a growing business area. The car is becoming a platform for services and applications, which are not only related to driving. The car is becoming a hub for services, which are related to the driver and passengers. This includes services like entertainment, navigation, and also payments. The car is becoming a wallet on wheels.
Starfish provides Software Development Kits (SDK) which turn cars into mobile wallets and enable providers of in-car commerce experiences, with a convenient approach to implement their solutions.
High-Level Overview
In order to understand the integration of an In-Car Wallet, it is important to understand the context in which it is used. The following illustration shows the main building blocks and their primary interactions.
Car Context
The car context revolves around the In-Car Wallet and the In-Car Applications that utilize it. Starfish provides two SDKs to simplify the development of these components.
In-Car Application
In-Car Applications enable commerce-related use cases such as parking, fueling, and food delivery. Typically offered by third-party providers, these apps are independent of the car manufacturer’s core offerings. To streamline implementation and ensure compatibility across providers, Starfish offers the Wallet Consumer SDK. This lightweight library provides a simple interface for integrating with the In-Car Wallet.
In-Car Wallet
The In-Car Wallet is a core component provided by car manufacturers. It securely stores payment methods, manages transactions, and offers a user interface for wallet interactions. While the wallet experience is tailored to the car manufacturer’s brand, Starfish’s Wallet SDK abstracts payment processing, enabling rapid development of fully certified in-car payment solutions.
Backends
Supporting the car context are backend services that handle business logic and third-party integrations.
Appication Backends
These services power In-Car Applications by implementing business logic and managing connections to third-party services, such as payment gateways and providers. It is important to understand, that the Wallet per se only handles the authentication of the shopper for a payment. The actual payment processing is done by the application backend.
Wallet Service
The Wallet Service, provided by Starfish, manages wallet assets and interfaces with the payment ecosystem to ensure secure, seamless transactions.
Payment Tokens
During a transaction, the In-Car Application prompts the driver or passenger to authenticate themselves to authorize the payment. Upon successful authentication, the In-Car Wallet generates a payment token. This token can be used by the app provider with their payment processing system to request the required authorization.
Getting Started with App Development
For the sake of simplicity we are not focussing at this place on the implementation of In-Car Wallets, but on their use for In-Car Applications.
If you want to dive deeper into the implementation of the Wallet, please reach out to you account representative.
Prerequisites
Ok, so we sort of exaggerated when we said we only need your app to get started. We need a couple of extra things but they are easy to grab, then we move focus on the app.
Your Hellgate Account
First thing first, go to Hellgate.io and register for a sandbox account if you don't have one yet. Once you're in, you can head over to the Wallet-Developer-Settings section on the left menu bar and you should see a your wallet credentials to get access to the app dependencies. It might take a second since the credentials are created for you just in time. Keep them around, you will need them for later, or come back to the page if you need them again.
The Wallet Simulator
Cars are notoriously hard to keep around in the office, so they don't make good test instruments. Because of this we have developed a wallet simulator that works just like your car wallet would, with some extra functionality for testing purposes, and the obvious branding difference. But is all Hellgate wallet behind the scenes, so you can develop with confidence.
To Install the simulator:
- If you have a test device, head to the Simulator Play Store page and install it as usual.
- If you are using the emulator, you can download the latest APK and install it by dragging the apk file into your running emulator screen.
Once you have in installed open the app and follow the quick instructions to configure it with your hellgate wallet account id that you got in the previous step. Ignore the webhook field for the moment. And that is it, close the app, there is no save button, and you are ready to go. Now it's time to move over to your own app and start a payment. We will follow up with some more screenshots of the simulator in action, to give you an idea what to expect.
When you start a payment from your app you will be prompted with an outcome, this is how you simulate the various possibilities, from a successful payment to an authentication failure.
Your app
Finaly we get to do some code, but not a whole lot.
If you have a running app, choose where you want to place your "pay now" button and follow along.
If you do not have an app, you can start a template project from Android Studio. Go to File / New / New Project
and select a template, the empty activity one is a good place to start.
Setting up the SDK Dependency
There are several ways to declare an additional maven repository and dependencies, here is one via settings.gradle.kts
and app/build.gradle.kts
.
Grab the maven credentials from your Hellgate dashboard, and include it in your gradle files.
// in settings.gradle.kts
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
// other repos
maven {
url = URI.create("https://maven.hellgate.io/repository/sf-maven")
credentials {
username = "QUP6VG965A" // username from your dashboard
password = "************" // password from your dashboard
}
}
}
}
// in app/build.gradle.kts
dependencies {
// other dependencies
implementation("io.hellgate:hw.wallet-consumer-sdk:1.0.0")
}
The Payment Authentication Code
Payments are initiated using everyday android intents. We fill a io.hellgate.hw.consumer.PaymentData
object and using standard intent launch plumbing we launch the intent. In essence, this is the whole function we need to implement:
fun makePayment(
scope: CoroutineScope,
launcher: ManagedActivityResultLauncher<PaymentData, PaymentAuthenticationResult>,
) {
scope.launch {
launcher.launch(
PaymentData(
merchantRefId = "1234",
signature = "TEST",
amount = 10000,
currencyCode = "USD",
creditor = "Simple Store",
subject = "Simple payment",
),
)
}
}
Now we also need to do something with the result. Our intent result is a io.hellgate.hw.consumer.PaymentAuthenticationResult
, which can either be Success
or Failure
, and in case of a success, give us our payment token. Again, this does not mean the payment was completed, it just means it was authenticated, the actual payment should be done in your backend, but we will get there in a moment. For now lets just add a quick function to handle our result, in this case we will just print it out:
fun receiveResult(result: PaymentAuthenticationResult) {
when (result) {
is PaymentAuthenticationResult.Success ->
Log.d("MainActivity", "Payment authentication successful, got the token: ${result.paymentToken}")
is PaymentAuthenticationResult.Failure ->
Log.d("MainActivity", "Received authentication failed")
}
}
The UI code
From here on, it's all standard Android development shenanigans. Let's create some very standard button that calls our makePayment
function:
@Composable
fun PaymentButton(
scope: CoroutineScope,
launcher: ManagedActivityResultLauncher<PaymentData, PaymentAuthenticationResult>,
) {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Button(onClick = { makePayment(scope, launcher) }) {
Text("Make a Payment")
}
}
}
}
There is a lot of android plumbing code going on, the only relevant piece is the onClick
handler invoking our makePayment
function,
Button(onClick = { makePayment(scope, launcher) })
So now all is left is to add our button to the content of some activity:
setContent {
val scope = rememberCoroutineScope()
val launcher = rememberLauncherForActivityResult(PaymentIntentContract) { result ->
receiveResult(result)
}
MyApplicationTheme {
PaymentButton(scope, launcher)
}
}
The important pieces here are the intent launcher initialization with our receiver function:
val launcher = rememberLauncherForActivityResult(PaymentIntentContract) { result ->
receiveResult(result)
}
And displaying our button:
PaymentButton(scope, launcher)
Putting it all Together
If you started using the empty activity an Android Studio template, the end result could look something like the code listing below. Of course in your own application, or if you used a different starter template, it will look different, but the important thing here is the pattern.
package team.starfish.myapplication
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.hellgate.hw.consumer.PaymentAuthenticationResult
import io.hellgate.hw.consumer.PaymentData
import io.hellgate.hw.consumer.PaymentIntentContract
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import team.starfish.myapplication.ui.theme.MyApplicationTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
val scope = rememberCoroutineScope()
val launcher = rememberLauncherForActivityResult(PaymentIntentContract) { result ->
receiveResult(result)
}
MyApplicationTheme {
PaymentButton(scope, launcher)
}
}
}
}
@Composable
fun PaymentButton(
scope: CoroutineScope,
launcher: ManagedActivityResultLauncher<PaymentData, PaymentAuthenticationResult>,
) {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Button(onClick = { makePayment(scope, launcher) }) {
Text("Make a Payment")
}
}
}
}
fun makePayment(
scope: CoroutineScope,
launcher: ManagedActivityResultLauncher<PaymentData, PaymentAuthenticationResult>,
) {
scope.launch {
launcher.launch(
PaymentData(
merchantRefId = "1234",
signature = "TEST",
amount = 10000,
currencyCode = "USD",
creditor = "Simple Store",
subject = "Simple payment",
),
)
}
}
fun receiveResult(result: PaymentAuthenticationResult) {
when (result) {
is PaymentAuthenticationResult.Success ->
Log.d("MainActivity", "Payment authentication successful, got the token: ${result.paymentToken}")
is PaymentAuthenticationResult.Failure ->
Log.d("MainActivity", "Received authentication failed")
}
}
You should now be able to simulate a payment authorization, and get the token printed out. If you started from the Android Studio template, this is how the app should look like:
More on PaymentData
The PaymentData
data class contains the minimum information required to authenticate the payment. is all you need to start payment, plus a few extra properties to assist the actual wallet app with the information display.
data class PaymentData(
val merchantRefId: String, // unique transaction id, will not be displayed to the user
val signature: String, // signature of the payment request for simulation purposes this should be "TEST"
val amount: Int, // amount in minor currency unit
val currencyCode: String, // currency code according to ISO 4217
val creditor: String, // For display purpose only in the simulation, in a real world scenario this would be verified with the signature
val subject: String, // For display purposes, description of the payment being made
)
Doing something with the result
Right now we are just printing out our result. But what we really want is to Make a Payment, and there are two possible course of actions here.
- You receive the payment token in your app and you send it back to your own backend for further processing
- Preferred, you configure your account with a webhook that receives the payment token directly from Hellgate, and you fetch the status from your frontend
When you created your Hellgate account we also configured a simple backend simulator for you, which basically receives the token and simulates a payment. You can then use said backend server to fetch the payment status and update your app UI and behavior according to the new purchase that was just made. So this is what we will use in this guide.`
Because polling for a result is such a common action to take, the Wallet Consumer SDK provides a helper function named simplePolling
, that takes a URL and polls until it receives a successful result. SO we will leverage that function, punch in our backend simulator url, and that will get us a result that honours what we selected as the outcome in the wallet simulator. We then replace the Success
case in our receiveResult
function, with our polling function:
simplePolling("https://icp-wallet-backendsimulator.fly.dev/merchant/$merchantId/payment/${result.paymentId}").onSuccess {
Log.d("MainActivity", "Received payment status: $it")
}
The whole thing, with a few extra logging and making our function suspend, now looks like this:
suspend fun receiveResult(result: PaymentAuthenticationResult) {
Log.d("MainActivity", "Received result: $result")
val merchantId = "hardwareStore123"
when (result) {
is PaymentAuthenticationResult.Success -> {
Log.d("MainActivity", "Authentication: SUCCESS, Payment status: PENDING")
Log.d("MainActivity", "Fetching payment status from merchant server")
simplePolling("https://icp-wallet-backendsimulator.fly.dev/merchant/$merchantId/payment/${result.paymentId}").onSuccess {
Log.d("MainActivity", "Received payment status: $it")
}
}
is PaymentAuthenticationResult.Failure ->
Log.d("MainActivity", "Received authentication failed")
}
}
This particular piece requires internet access. If you are trying this out in yoru existing app is very likely that particular permission is already set, but if you started with the Android Studio Template, don't forget to add it to AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
One final piece, we made our receiveResult
suspend, because we are going out to the internet. So we need to update our launcher
configuration to send that result through our coroutine scope. Becoming:
val launcher = rememberLauncherForActivityResult(PaymentIntentContract) { result ->
scope.launch { receiveResult(result) }
}
Final result
With the recent changes, our demo code leveraging our simulators should look similar to this:
package team.starfish.myapplication
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import io.hellgate.hw.consumer.PaymentAuthenticationResult
import io.hellgate.hw.consumer.PaymentData
import io.hellgate.hw.consumer.PaymentIntentContract
import io.hellgate.hw.consumer.polling.simplePolling
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import team.starfish.myapplication.ui.theme.MyApplicationTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
val scope = rememberCoroutineScope()
val launcher = rememberLauncherForActivityResult(PaymentIntentContract) { result ->
scope.launch { receiveResult(result) }
}
MyApplicationTheme {
PaymentButton(scope, launcher)
}
}
}
}
@Composable
fun PaymentButton(
scope: CoroutineScope,
launcher: ManagedActivityResultLauncher<PaymentData, PaymentAuthenticationResult>,
) {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Button(onClick = { makePayment(scope, launcher) }) {
Text("Make a Payment")
}
}
}
}
fun makePayment(
scope: CoroutineScope,
launcher: ManagedActivityResultLauncher<PaymentData, PaymentAuthenticationResult>,
) {
scope.launch {
launcher.launch(
PaymentData(
merchantRefId = "1234",
signature = "TEST",
amount = 10000,
currencyCode = "USD",
creditor = "Simple Store",
subject = "Simple payment",
),
)
}
}
suspend fun receiveResult(result: PaymentAuthenticationResult) {
Log.d("MainActivity", "Received result: $result")
val merchantId = "hardwareStore123"
when (result) {
is PaymentAuthenticationResult.Success -> {
Log.d("MainActivity", "Authentication: SUCCESS, Payment status: PENDING")
Log.d("MainActivity", "Fetching payment status from merchant server")
simplePolling("https://icp-wallet-backendsimulator.fly.dev/merchant/$merchantId/payment/${result.paymentId}").onSuccess {
Log.d("MainActivity", "Received payment status: $it")
}
}
is PaymentAuthenticationResult.Failure ->
Log.d("MainActivity", "Received authentication failed")
}
}