Implementing "Add to Wallet" in an Android Application

Mobassir Ahsan

Engineer, Samsung Developer Program

Samsung Wallet is a fast and secure digital wallet application bundled with millions of Samsung Galaxy devices worldwide. Its streamlined functionality allows users to add and store various passes, tickets, and credentials all in one place. You can design custom Wallet cards, such as for boarding passes, tickets, coupons, gift cards and loyalty cards, for your service and issue them to users who can add them to the Samsung Wallet application on their device.

Samsung Wallet cards are signed with the RS256 asymmetric algorithm. The RS256 signing algorithm requires a private key, which is a personal credential that must never be shared in the application. Consequently, a separate server application is needed to store the key and sign the Wallet card data (CData).

When the user taps the "Add to Samsung Wallet" button in the application, the server application creates and signs the Wallet card data, then returns a JWT token that is used to add the Wallet card to the user's Samsung Wallet application.

undefined
undefined
undefined
undefined

Figure 1: "Add to Wallet" flow

undefined

This tutorial uses Kotlin to demonstrate how to implement the "Add to Wallet" feature in an Android mobile application that adds movie tickets to Samsung Wallet. It also shows how to generate the Wallet cards using a Spring Boot server. You can follow along with the tutorial by downloading the sample code files.

Develop the mobile application

To implement the "Add to Wallet" button in the mobile application, you must configure the application, implement the application UI, and define the application logic for the button.

Configuring the mobile application project

Create an application project and configure it to connect to and communicate with the server through REST API requests:

  1. In Android Studio, create a new project.
  2. To implement REST API support in the application, add the following Retrofit library dependencies to the application's "build.gradle" file:
    'com.squareup.retrofit2:retrofit:2.11.0'
    'com.squareup.retrofit2:converter-gson: 2.11.0'
    
  3. To enable communication with the server, add the following permissions to the "AndroidManifest.xml" file:
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    
  4. To enable testing with a local non-HTTPS server, add the following attribute to the "application" element in the "AndroidManifest.xml" file:
    android:usesCleartextTraffic="true"
    

Implementing the application UI

The application UI consists of two screens: the movie tickets list and a ticket detail page.

  1. In the application code, define a Movie data class that contains details for the movie tickets that can be added to Samsung Wallet.

    data class Movie(
        val name:String,
        val studio:String,
        val ticketNumber:String,
    )
    
  2. Create a RecyclerView that displays the list of movie tickets as buttons on the main screen. Each button has a listener that opens the detail page for the ticket. You can study the implementation details in the sample code.

  3. To check whether the device supports Samsung Wallet, send an HTTP GET request to the following endpoint, where Build.MODEL is the model of the device:

    https://api-us3.mpay.samsung.com/wallet/cmn/v2.0/device/available?serviceType=WALLET&modelName=${Build.MODEL}
    
  4. Create the ticket detail page layout in the "activity_movie_detail.xml" file. The "Add to Wallet" button is implemented on this page.

    undefined
    undefined
    undefined
    undefined

    Figure 2: Detail page layout

    undefined

    Implement the ticket detail page functionality in the MovieDetailActivity activity class in the application code.

    For this demonstration, the movie ticket data is predefined in the application code. In a real application, the data is usually retrieved in real time from an external database.

    val movieLists = listOf<Movie>(
        Movie("The Wallet", "Samsung Studios", "A-01"),
        Movie("Crying Sea", "Laplace Studio","H-07"),
        Movie("Canoe", "Terra Productions", "R-03")
    )
    val position:Int = intent.getIntExtra("moviePosition", 0)
    val movie:Movie = movieLists[position]
    
    binding.mvNameText.text = movie.name
    binding.mvStudioText.text = movie.studio
    binding.mvTicketNumber.text = "Ticket: ${movie.ticketNumber}"
    
    binding.addToWalletButton.setOnClickListener {
        // Request server to generate card data
        // Retrieve signed card data from server
        // Add card to Samsung Wallet application
    }
    
  5. When the user taps the "Add to Wallet" button, OnClickListener() is triggered. Its functionality is defined later in this tutorial.

Connecting to the server

To communicate with the server:

  1. In the TokenResponse class, define the structure of the JSON response to be received from the server. The status field indicates whether the token generation request was successful, and the jwt field contains the generated CData in the form of a JWT token.

    data class TokenResponse(
        val status: String,
        val jwt:String
    )
    
  2. In the "ApiClient.kt" file, define a RetrofitClient object that is used to establish the connection with the server.

  3. Define the ApiInterface interface, which defines the API request and response:

    • The API endpoint URL is BASE_URL/movie/{ID}, where {ID} is the movie ticket ID to be added

    • The expected response from the endpoint is a TokenResponse object.

  4. Define an ApiClient object that extends ApiInterface and creates a RetrofitClient instance to establish the server connection.

    object RetrofitClient {
        private const val BASE_URL = "http://192.xxx.xxx.xxx:8080" // Define your server URL
    
        val retrofit: Retrofit by lazy {
            Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
        }
    }
    interface ApiInterface {
        @GET("/movie/{id}")
        suspend fun getMovie(@Path("id") movieId:Int): Response<TokenResponse>
    }
    
    object ApiClient {
        val apiService: ApiInterface by lazy {
            RetrofitClient.retrofit.create(ApiInterface::class.java)
        }
    }
    

Adding card data to Samsung Wallet

To request CData generation from the server and add the Wallet card to Samsung Wallet:

  1. In the addToWalletButton.setOnClickListener() method within the MovieDetailActivity class, send a request to the server to generate CData for the selected movie ticket.

  2. If CData generation is successful, to add the movie ticket to Samsung Wallet, send an HTTP request containing the CData token to the following endpoint URL: https://a.swallet.link/atw/v1/{Card Id}#Clip?cdata={CData token}

  3. For more information about this endpoint, see Data Transmit Link.

binding.addToWalletButton.setOnClickListener {
    CoroutineScope(Dispatchers.Main).launch {
        val response = ApiClient.apiService.getMovie(position)
        if(response.isSuccessful && response.body()!=null){
            startActivity(Intent(
                Intent.ACTION_VIEW,
                Uri.parse("http://a.swallet.link/atw/v1/3aabbccddee00#Clip?cdata=${response.body()!!.jwt}")))
// Replace '3aabbccddee00' part with your card ID 
        }
    }
}

Generate signed Wallet card data

The server application must be configured to receive the card data request from the mobile application and return a signed JWT token. This part of the tutorial uses the Spring Boot framework.

Configuring the server project

To create and configure a server application to generate and sign Wallet card data:

  1. In the Spring Initializr tool or any supported Java IDE, create a Spring Boot project and open the sample code.
  2. To configure the server to receive REST API requests from the mobile application, add the "Spring Web" dependency to the project.
  3. Define a Token data class. Make sure it has the same attributes as the TokenResponse data class defined in the mobile application.

    data class Token(val status:String, val jwt:String
    
  4. Initialize a TokenController class that receives the incoming requests and returns a Token object in response.

    @RestController
    @RequestMapping("movie")
    class TokenController {
        @GetMapping(path = ["/{movieId}"])
        fun getMovie(@PathVariable movieId:Int): Token {
            return Token("success", "{DUMMY_CDATA}")
            // CData generation logic
        }
    }
    

    The CData generation and signing logic is described in the next section.

Implementing card data signing logic

For easier understanding, this section describes a simplified implementation of the CData Generation Sample Code.

  1. In the server application project, copy the following credential files to the "sample/securities/" directory.
    • Samsung public key from the Samsung certificate ("Samsung.crt")
    • Partner public key from your partner certificate ("Partner.crt")
    • Partner private key from the private key file ("Partner.key")
  2. To handle the certificate files and signing algorithms, add the following dependencies to the server application's "build.gradle" file:
    implementation 'com.nimbusds:nimbus-jose-jwt:9.37.3'
    implementation 'org.bouncycastle:bcprov-jdk18on:1.77'
    
  3. In a new "JwtGen.kt" file, define a readCertificate() method that reads the public keys from the certificates and a readPrivateKey() method that reads the private key from the key file.

    private val PARTNER_ID = "4048012345678912345" // Replace with your partner ID
    
    private val samsungPublicKey = 
        readCertificate(getStringFromFile("sample/securities/Samsung.crt"))
    private val partnerPublicKey = 
        readCertificate(getStringFromFile("sample/securities/Partner.crt"))
    private val partnerPrivateKey = 
        readPrivateKey(getStringFromFile("sample/securities/Partner.key"))
    
    fun readPrivateKey(key: String): PrivateKey {
        val keyByte = readKeyByte(key)
        lateinit var privateKey: PrivateKey
        val pkcs8Spec = PKCS8EncodedKeySpec(keyByte)
        try {
            val kf = KeyFactory.getInstance("RSA")
            privateKey = kf.generatePrivate(pkcs8Spec)
        } catch (e: InvalidKeySpecException) {
            e.printStackTrace()
        } catch (e: NoSuchAlgorithmException) {
            e.printStackTrace()
        }
        return privateKey
    }
    
    fun readCertificate(cert: String): PublicKey {
        lateinit var certificate: Certificate
        val keyByte = readKeyByte(cert)
        val `is`: InputStream = ByteArrayInputStream(keyByte)
        try {
            val cf = CertificateFactory.getInstance("X.509")
            certificate = cf.generateCertificate(`is`)
        } catch (e: CertificateException) {
            e.printStackTrace()
        }
        return certificate.publicKey
    }
    
    private fun readKeyByte(key: String): ByteArray {
        val keyByte: ByteArray
        val bais = ByteArrayInputStream(key.toByteArray(StandardCharsets.UTF_8))
        val reader: Reader = InputStreamReader(bais, StandardCharsets.UTF_8)
        val pemReader = PemReader(reader)
        var pemObject: PemObject? = null
        try {
            pemObject = pemReader.readPemObject()
        } catch (e: IOException) {
            e.printStackTrace()
        }
        keyByte = if (pemObject == null) {
            Base64.getDecoder().decode(key)
        } else {
            pemObject.content
        }
        return keyByte
    }
    
    fun getStringFromFile(path: String?): String {
        try {
            val file =
                File(Objects.requireNonNull(ClassLoader.getSystemClassLoader().getResource(path)).file)
            return String(Files.readAllBytes(file.toPath()))
        } catch (e: IOException) {
            throw RuntimeException(e)
        }
    }
    

Generating card data

CData token generation is implemented in the "JwtGen.kt" file:

  1. Read the file containing raw JSON data that defines the ticket data structure.

    For this demonstration, use the "Ticket.json" file in the "sample/payload/" directory of the CData generation sample code.

  2. Generate or fill in the required ticket details. For example, the "{title}" and "{seatNumber}" fields are replaced with the movie title and seat number.

    For information about the complete JSON structure, see Wallet Cards.

  3. Convert the JSON data to a JWE object.
  4. Encrypt the JWE object with the Samsung public key.
  5. Build the custom JWS header for Samsung Wallet cards.
  6. Sign and validate the complete JWS object with your partner private and public key using the RS256 asymmetric algorithm. This is the CData token.
private val currentTimeMillis = System.currentTimeMillis()

private val plainData:String = getStringFromFile("sample/payload/Ticket.json")
    .replace("{refId}", UUID.randomUUID().toString())
    .replace("{language}", "en")
    .replace("{createdAt}", currentTimeMillis.toString())
    .replace("{updatedAt}", currentTimeMillis.toString())
    .replace("{issueDate}", currentTimeMillis.toString())
    .replace("{startDate}", (currentTimeMillis + TimeUnit.DAYS.toMillis(1)).toString())
    .replace("{endDate}", (currentTimeMillis + TimeUnit.DAYS.toMillis(1) + +TimeUnit.HOURS.toMillis(2)).toString())

fun generateCdata(movieName: String, movieTicktNo:String): String{
    // Modify data as needed
    val data = plainData.replace("{title}", "\"$movieName\"")
        .replace("{seatNumber}","\"$movieTicktNo\"")

    //print(data)

    return generate(PARTNER_ID, samsungPublicKey, partnerPublicKey, partnerPrivateKey, data)
}

private fun generate(partnerId: String, samsungPublicKey: PublicKey, partnerPublicKey: PublicKey,
                     partnerPrivateKey: PrivateKey, data: String): String {
    val jweEnc = EncryptionMethod.A128GCM
    val jweAlg = JWEAlgorithm.RSA1_5
    val jweHeader = JWEHeader.Builder(jweAlg, jweEnc).build()
    val encryptor = RSAEncrypter(samsungPublicKey as RSAPublicKey)
    val jwe = JWEObject(jweHeader, Payload(data))
    try {
        jwe.encrypt(encryptor)
    } catch (e: JOSEException) {
        e.printStackTrace()
    }
    val payload = jwe.serialize()

    val jwsAlg = JWSAlgorithm.RS256
    val utc = System.currentTimeMillis()

    val jwsHeader = JWSHeader.Builder(jwsAlg)
        .contentType("CARD")
        .customParam("partnerId", partnerId)
        .customParam("ver", "2")
        .customParam("utc", utc)
        .build()

    val jwsObj = JWSObject(jwsHeader, Payload(payload))

    val rsaJWK = RSAKey.Builder(partnerPublicKey as RSAPublicKey)
        .privateKey(partnerPrivateKey)
        .build()

    val signer: JWSSigner
    try {
        signer = RSASSASigner(rsaJWK)
        jwsObj.sign(signer)
    } catch (e: JOSEException) {
        e.printStackTrace()
    }
    return jwsObj.serialize()
}

Returning the signed token

In the server application code, when the server receives a request at the movie/{movieID} endpoint, the TokenController class calls the JwtGen.generateCdata() method with the movie ID, which generates and returns the CData JWT token in the API response.

In this tutorial, since the movie ticket list was predefined in the mobile application project, make sure the same Movie data class and list are defined here too.

@RestController
@RequestMapping("movie")
class TokenController {
    @GetMapping(path = ["/{movieId}"])
    fun getMovie(@PathVariable movieId:Int): Token {
        val movieLists = listOf<Movie>(
            Movie("The Wallet", "Samsung Studios", "A-01"),
            Movie("Crying Sea", "Laplace Studio","H-07"),
            Movie("Canoe", "Terra Productions", "R-03")
        )
        
        if( movieId>2){
            // Implement your verification logic
            return Token("failure", "")
        }
        else{
            val cdata = JwtGen.generateCdata(movieLists[movieId].name, movieLists[movieId].ticketNumber)
            return Token("success", cdata)
        }
    }
}

Testing the application

To test your "Add to Wallet" integration:

  1. Connect the server and the mobile device to the same network.
  2. Launch the server and mobile applications.
  3. In the mobile application, tap a movie ticket in the list. Its detail page opens.
  4. Tap Add to Samsung Wallet. The server generates and returns the CData token.
  5. The Samsung Wallet application launches on the device and the movie ticket information is added to it.
undefined
undefined
undefined
undefined

Figure 3: Ticket added to Samsung Wallet

undefined

Summary

Implementing the "Add to Wallet" feature enables your users to add your digital content, such as tickets, passes, and loyalty cards, to the Samsung Wallet application on their mobile device as Wallet cards. In addition to implementing the "Add to Samsung Wallet" button in your mobile application, you must also create a server application that securely generates and signs the Wallet card data and returns it to the mobile application for transmitting to Samsung Wallet.

For more information about adding "Add to Wallet" to your application, see Implementing ATW button. You can also study the extended sample application (clicking this link downloads the sample code) and the API reference.

If you have questions about or need help with the information presented in this article, you can share your queries on the Samsung Developers Forum. You can also contact us directly for more specialized support through the Samsung Developer Support Portal.

Resources

Click the links below to download the sample code.

  1. Android App Sample Code
  2. Extended Android App Sample Code
  3. CData Generation Server Sample Code