Initiate In-App payment

The main classes and interfaces involved here are:

  • SamsungPay– Class for a merchant app to get Samsung Pay SDK information and the status of Samsung Pay on the device.

  • PaymentManager – Class to provide payment/transaction functionality.

  • CardInfoListener – Interface for requestCardInfo result callbacks from Samsung Wallet.

  • CustomSheetTransactionInfoListener – Interface for transaction success/failure callbacks from Samsung Wallet; payment information is provided with a success callback and must be used by the merchant app for processing the payment.

The flow pictured next captures the essential online payment API process between merchant apps integrated with the Samsung Pay SDK and Samsung Wallet and the merchant’s payment gateway (PG).

API flow for In-App payments

Reflected in the diagram above are the following operations:

  1. Check the ready status of Samsung Pay.
  2. Start the Payment Manager to establish the service binding and verify the merchant app.
  3. Get payment card information and the payment amount, including updates.
  4. Get/update the user’s billing and shipping addresses, including an updated payment amount if shipping charges will be incurred.
  5. Authenticate the user.
  6. Submit payment information to PG.
  7. Verify transaction success or failure.

Token Modes (Network vs. Gateway)

To complete the payment, the merchant’s designated payment gateway (PG) handles one of two types of tokens: gateway tokens (indirect) or network tokens (direct). The Samsung Pay SDK supports both types.

The essential difference between the two types is who decrypts the token information. Network tokens require that the merchant app handles decryption of the token bundle or work with the PG to handle decryption, whereas Gateway token decryption is handled by the PG via the Samsung-PG Interface Server.

Network Token Mode(Direct)
Gateway Token Mode(Indirect)

Check with your PG to determine its specific requirements for payment processing.

Regardless of the PG model employed, direct or indirect, the goal is to offer Samsung Pay as a secure payment method within your merchant app. The most common use case involves the following general steps:

  1. To make a purchase, the user selects to “Buy” or got to checkout after adding items to a shopping cart.
  2. Now in checkout, the user selects a payment option; for example, either the merchant’s “standard” method or Samsung Pay.
  3. Upon selecting Samsung Pay, the user is presented with a payment sheet that allows for card selection and shipping address confirmation with the option to add/modify information for this order, whereupon the user:

    * Makes payment card selection from the list of enrolled cards.

    * Chooses to change or add the delivery address.

    * Enters required address information in the form presented and saves it.

    * Authenticates the payment method, amount, and delivery with a biometric verification (fingerprint, iris…) or PIN.

Checking registered/enrolled card information

Before displaying the Samsung Pay button, a partner app can query card brand information for the user’s currently enrolled payment cards in Samsung Wallet to determine if payment is supported with the enrolled card. For example, if a merchant app accepts one card brand exclusively but the user has not registered any cards matching this brand in Samsung Wallet, the merchant app needs to determine whether or not to display the Samsung Pay button for this purchase checkout.

To query the card brand, use the requestCardInfo() API method of the PaymentManager class. The requestFilter is optional bundle data reserved for future use. The merchant app does not need to set a value for it now. However, before calling this method, CardInfoListener must be registered so its listener can provide the following events:

  • onResult - called when the Samsung Pay SDK returns card information from Samsung Wallet; returns information about enrolled cards or is empty if no card is registered.
  • onFailure - called when the query fails; for example, if SDK service in the Samsung Wallet app ends abnormally.

The following snippet shows how to retrieve the list of supported card brands from Samsung Pay:

val serviceId = "partner_app_service_id"
val bundle = Bundle()
bundle.putString(SamsungPay.PARTNER_SERVICE_TYPE, SpaySdk.ServiceType.INAPP_PAYMENT.toString())
val partnerInfo = PartnerInfo(serviceId, bundle)
val paymentManager = PaymentManager(context, partnerInfo)
paymentManager.requestCardInfo(Bundle(), cardInfoListener) // get Card Brand List
//CardInfoListener is for listening requestCardInfo() callback events

val cardInfoListener: CardInfoListener = object : CardInfoListener {
    // This callback is received when the card information is received successfully
    override fun onResult(cardResponse: List<CardInfo>) {
        var visaCount = 0
        var mcCount = 0
        var amexCount = 0
        var dsCount = 0
        var brandStrings = "Card Info : "
        var brand: SpaySdk.Brand?
        for (i in cardResponse.indices) {
            brand = cardResponse[i].brand
            when (brand) {
                SpaySdk.Brand.AMERICANEXPRESS -> amexCount++
                SpaySdk.Brand.MASTERCARD -> mcCount++
                SpaySdk.Brand.VISA -> visaCount++
                SpaySdk.Brand.DISCOVER -> dsCount++
                else -> { /* Other card brands */
                }
            }
        }
        brandStrings += "  VI = $visaCount,  MC = $mcCount,  AX = $amexCount,  DS = $dsCount"
        Log.d(TAG, "cardInfoListener onResult  : $brandStrings")
        Toast.makeText(context, "cardInfoListener onResult" + brandStrings, Toast.LENGTH_LONG).show()
    }
    /*
     * This callback is received when the card information cannot be retrieved.
     * For example, when SDK service in the Samsung Wallet app dies abnormally.
     */
    override fun onFailure(errorCode: Int, errorData: Bundle) {
        //Called when an error occurs during In-App cryptogram generation
        Toast.makeText(context, "cardInfoListener onFailure : " + errorCode,
            Toast.LENGTH_LONG).show()
    }
}

Creating a transaction request

Upon successful initialization of the SamsungPay class, the merchant app needs to create a transaction request with payment information

Using the custom payment sheet

To initiate a payment transaction with Samsung Pay’s custom payment sheet, your merchant app must populate the following mandatory fields in CustomSheetPaymentInfo:

  • Merchant Name - as it will appear in Samsung Pay’s payment sheet, as well as the user's card account statement.

  • Amount - the constituent transaction properties (currency, item price, shipping price, tax, total price) which together determine the total amount the user is agreeing to pay the merchant.

Optionally, the following fields can be added to the payment information:

  • Merchant ID- can be used for the merchant’s own designated purpose at its discretion unless the merchant uses an indirect PG like Stripe or Braintree. If an indirect PG is used, this field must be set to the merchant’s Payment Gateway ID fetched from the Samsung Pay Developers portal.

  • Order Number - usually created by the merchant app via interaction with a PG. This number is required for refunds and chargebacks. In the case of Visa cards, the value is mandatory. The allowed characters are [A-Z][a-z][0-9,-] and the length of the value can be up to 36 characters.

  • Address - the user’s billing and/or shipping address (see Applying an AddressControl for details).

  • Allowed Card Brands - specifies card brands accepted by the merchant. If no brand is specified, all brands are accepted by default. If at least one brand is specified, all other card brands not specified are set to "card not supported’ on the payment sheet.

Here’s the 'CustomSheetPaymentInfo' structure:

class CustomSheetPaymentInfo : Parcelable {
    private val version: String? = null
    private val merchantId: String? = null
    private val merchantName: String? = null
    private val orderNumber: String? = null
    private val addressInPaymentSheet: AddressInPaymentSheet = AddressInPaymentSheet.DO_NOT_SHOW
    private val allowedCardBrand: List<SpaySdk.Brand>? = null
    private val cardInfo: CardInfo? = null
    private val isCardHolderNameRequired = false
    private val isRecurring = false
    private val merchantCountryCode: String? = null
    private val customSheet: CustomSheet? = null
    private val extraPaymentInfo: Bundle? = null
}

Your merchant app sends this CustomSheetPaymentInfo to Samsung Wallet via the applicable Samsung Pay SDK API methods. Upon successful user authentication in direct mode, Samsung Wallet returns the above "Payment Info" structure and a result string. The result string is forwarded to the PG by your merchant app to complete the transaction. It will vary based on the PG you’re using.

The following example demonstrates how to populate customSheet in the CustomSheetPaymentInfo class. See Sample merchant app using custom payment sheet below for example usage of each CustomSheet control.

/*
 * Make user's transaction details.
 * The merchant app should send CustomSheetPaymentInfo to Samsung Wallet via 
 * the applicable Samsung Pay SDK API method for the operation being invoked.
 */
private fun makeCustomSheetPaymentInfo(): CustomSheetPaymentInfo {
    val brandList = ArrayList<SpaySdk.Brand>()
    // If the supported brand is not specified, all card brands in Samsung Wallet are 
    // listed in the Payment Sheet.
    brandList.add(PaymentManager.Brand.VISA)
    brandList.add(PaymentManager.Brand.MASTERCARD)
    brandList.add(PaymentManager.Brand.AMERICANEXPRESS)
    /*
     * Make the SheetControls you want and add them to custom sheet.
     * Place each control in sequence with AmountBoxControl listed last.
     */
    val customSheet = CustomSheet()
    customSheet.addControl(makeBillingAddressControl())
    customSheet.addControl(makeShippingAddressControl())
    customSheet.addControl(makePlainTextControl())
    customSheet.addControl(makeShippingMethodSpinnerControl())
    customSheet.addControl(makeAmountControl())
    val customSheetPaymentInfo = CustomSheetPaymentInfo.Builder()
        .setMerchantId("123456")
        .setMerchantName("Sample Merchant") 
        // Merchant requires billing address from Samsung Wallet and 
        // sends the shipping address to Samsung Wallet.
        // Show both billing and shipping address on the payment sheet.
        .setAddressInPaymentSheet(CustomSheetPaymentInfo.AddressInPaymentSheet.NEED_BILLING_SEND_SHIPPING)
        .setAllowedCardBrands(brandList)
        .setCardHolderNameEnabled(true)
        .setRecurringEnabled(false)
        .setCustomSheet(customSheet)
        .build()
    return customSheetPaymentInfo
}

Requesting payment with a custom payment sheet

The startInAppPayWithCustomSheet() method of the PaymentManager class is applied to request payment using a custom payment sheet in Samsung Wallet. The two methods are defined as follows:

  • startInAppPayWithCustomSheet() - initiates the payment request with a custom payment sheet. The payment sheet persist for 5 minutes after the API is called. If the time limit expires, the transaction fails.

  • updateSheet() - must be called to update current payment sheet. As of API level 1.5, a merchant app can update the custom sheet with a custom error message. Refer to Updating sheet with custom error message.

When you call the startInAppPayWithCustomSheet() method, a custom payment sheet is displayed on the merchant app screen. From it, the user can select a registered card for payment and change the billing and shipping addresses, as necessary.

The result is delivered to CustomSheetTransactionInfoListener, which provides the following events:

  • onSuccess() - called when Samsung Pay confirms payment. It provides the CustomSheetPaymentInfo object and the paymentCredential JSON string.

  • CustomSheetPaymentInfo is used for the current transaction. It contains amount, shippingAddress, merchantId, merchantName, orderNumber. API methods exclusively available in the onSuccess() callback comprise:

    • getPaymentCardLast4DPAN() – returns the last 4 digits of the user's digitized personal/primary identification number (DPAN)
    • getPaymentCardLast4FPAN() – returns the last 4 digits of the user's funding personal/primary identification number (FPAN)
    • getPaymentCardBrand() – returns the brand of the card used for the transaction
    • getPaymentCurrencyCode() – returns the ISO currency code in which the transaction is valued
    • getPaymentShippingAddress() – returns the shipping/delivery address for the transaction
    • getPaymentShippingMethod() – returns the shipping method for the transaction.
      For PGs using the direct model (network tokens), the paymentCredential is a JSON object containing encrypted cryptogram which can be passed to the PG. PGs using the indirect model (gateway tokens) like Stripe, it is a JSON object containing reference (card reference – a token ID generated by the PG) and status (i.e., AUTHORIZED, PENDING, CHARGED, or REFUNDED). Refer to Payment credential sample for details.
  • onCardInfoUpdated() - called when the user changes the payment card. In this callback, updateSheet() method must be called to update current payment sheet.

  • onFailure() - called when the transaction fails; returns the error code and errorData bundle for the failure

Here’s how to call the startInAppPayWithCustomSheet() method of the PaymentManager class:

/*
 * CustomSheetTransactionInfoListener is for listening callback events of In-App custom sheet payment.
 * This is invoked when card is changed by the user on the custom payment sheet,
 * and also with the success or failure of online (In-App) payment.
 */
private val transactionListener  = object :  CustomSheetTransactionInfoListener {
    // This callback is received when the user changes card on the custom payment sheet in Samsung Pay
    override fun onCardInfoUpdated(selectedCardInfo: CardInfo, customSheet: CustomSheet) {
       /*
        * Called when the user changes card in Samsung Wallet.
        * Newly selected cardInfo is passed so merchant app can update transaction amount
        * based on different card (if needed),
        */
        val amountBoxControl =
            customSheet.getSheetControl(AMOUNT_CONTROL_ID) as AmountBoxControl
        amountBoxControl.updateValue(PRODUCT_ITEM_ID, 1000.0) //item price
        amountBoxControl.updateValue(PRODUCT_TAX_ID, 50.0) // sales tax
        amountBoxControl.updateValue(PRODUCT_SHIPPING_ID, 10.0) // Shipping fee
        amountBoxControl.updateValue(PRODUCT_FUEL_ID, 0.0, "Pending") // additional item status
        amountBoxControl.setAmountTotal(
            1060.0,
            AmountConstants.FORMAT_TOTAL_PRICE_ONLY
        ) // grand total
        customSheet.updateControl(amountBoxControl)

        // Call updateSheet() with AmountBoxControl; mandatory.
        try {
            paymentManager.updateSheet(customSheet)
        } catch (e: java.lang.IllegalStateException) {
            e.printStackTrace()
        } catch (e: java.lang.NullPointerException) {
            e.printStackTrace()
        }
    }
   /*
    * This callback is received when the payment is approved by the user and the transaction payload
    * is generated. Payload can be an encrypted cryptogram (network token mode) or the PG's token
    * reference ID (gateway token mode).
    */
    override fun onSuccess(response: CustomSheetPaymentInfo, paymentCredential: String, extraPaymentData: Bundle) {
       /*
        * Called when Samsung Pay creates the transaction cryptogram, which merchant app then sends
        * to merchant server or PG to complete in-app payment.
        */
        try {
            val DPAN = response.cardInfo.cardMetaData.getString(SpaySdk.EXTRA_LAST4_DPAN, "")
            val FPAN = response.cardInfo.cardMetaData.getString(SpaySdk.EXTRA_LAST4_FPAN, "")
            Toast.makeText(context, "DPAN: " + DPAN + "FPAN: " + FPAN, Toast.LENGTH_LONG).show()
        } catch (e: java.lang.NullPointerException) {
            e.printStackTrace()
        }
        Toast.makeText(context, "Transaction : onSuccess", Toast.LENGTH_LONG).show()
    }

    override fun onFailure(errorCode: Int, errorData: Bundle) {
        // Called when an error occurs during cryptogram generation
        Toast.makeText(context, "Transaction : onFailure : $errorCode", Toast.LENGTH_LONG)
            .show()
    }
}
private fun startInAppPayWithCustomSheet() {
    // Show custom payment sheet
    try {
        val bundle = Bundle()
        bundle.putString(
            SamsungPay.PARTNER_SERVICE_TYPE,
            SpaySdk.ServiceType.INAPP_PAYMENT.toString()
        )
        val partnerInfo = PartnerInfo(serviceId, bundle)
        paymentManager = PaymentManager(context, partnerInfo)
        // Request payment using Samsung Wallet
        paymentManager.startInAppPayWithCustomSheet(
            makeCustomSheetPaymentInfo(),
            transactionListener
        )
    } catch (e: IllegalStateException) {
        e.printStackTrace()
    } catch (e: NumberFormatException) {
        e.printStackTrace()
    } catch (e: NullPointerException) {
        e.printStackTrace()
    } catch (e: IllegalArgumentException) {
        e.printStackTrace()
    }
}

When an address is provided by Samsung Wallet, onAddressUpdated()is called whenever address information is updated in the custom payment sheet. You can use the updateSheet() method to update the shipping fee or any other relevant information in the payment sheet. Set the errorCode to determine if the address provided by Samsung Wallet app is invalid, out of delivery, or does not exist. For example, when the:

  • merchant does not support the product delivery to the designated location
  • billing address from Samsung Wallet is not valid for tax recalculation

For all such cases, the merchant app should call updateSheet() with one of the following error codes:

  • ERROR_SHIPPING_ADDRESS_INVALID
  • ERROR_SHIPPING_ADDRESS_UNABLE_TO_SHIP
  • ERROR_SHIPPING_ADDRESS_NOT_EXIST
  • ERROR_BILLING_ADDRESS_INVALID
  • ERROR_BILLING_ADDRESS_NOT_EXIST

The sample code included below under Applying the Address Control demonstrates how to use the updateSheet() method for 'addressControl' in the payment sheet.


5.5. Payment credential sample

The paymentCredential is the resulting output of the startInAppPayWithCustomSheet() method. The structure varies depending on the PG you’re using and the integration model—direct or indirect). The following paymentCredential is for a Visa card.

For PG using direct (network token) mode – e.g. First Data, Adyen, CYBS

Sample paymentCredential JSON output (using JWE-only)

{
"billing_address":{"city":"BillingCity","country":"USA","state_province":"CA","street":"BillingAddr1","zip_postal_code":"123456"},
"card_last4digits":"1122",
"3DS":{"data":"eyJhbGciOiJSU0ExXzUiLCJraWQiOiJCak91a1h2aFV4WU5wOFIwVGs2Y25OaCtZWWFqZXhIeHRVZ0VFdHlhYy9NPSIsInR5cCI6IkpPU0UiLCJjaGFubmVsU2VjdXJpdHlDb250ZXh0IjoiUlNBX1BLSSIsImVuYyI6IkExMjhHQ00ifQ.Fg2OOUvHdGKkIVyBa2S5KtUrPWUeujKZEyxz7n6kALhQahszv3P5JaBaOJ-RoKcznFjDg3qierzjktU7zXST9gwv4Oclahpfdw64w0X6TtAxeYJiIVkJUG-edXXTWaJeyeIkgC68wEhF1CltSqG4zLWi6upVCAywdPpBN0Hl0C5WcF5Az4WABYtV_Fda5aHGuyPnE70kEQRTWdlacW9MzEJx2Xth7Msd9OHoulR8LUQ-7gha17jHoOBwgMoQ9q0hAoCNm0LjWiuhKoRyyu-Njulnbkk8FZus_AIuMgdv2YN9ygFqIlMculb0VWuF0YeKX6IsgAxi0ZQhLiUsJkCZ_w.AuZZxoG46lnrtk3Q.QE2llwS30VzH-ZduuE8b045CnfRm2p-RjZGBnZcHELS3v26N64cFg1AV5mtP5f-fSwbJ3ntP5x4V1NK8FmdY0uSPxzeMfvl5badGAC7w9FrXt6X5xV1Fqu6-q-ZkbxcB9bYgownt983BcKOE1bd5djxFBOdLrc4j68ikDjc5M3LEBDx6hV0aQzKmilCH-JeVL3AwQyKBny4Vj7m3Fizw7u1PRLI2ZfWUkXDfS4Vwv3bPm4QUDEMVnHXJ.qTYmdmn4ne93juljNmWkJg","type":"S","version":"100"},
"merchant_ref":"MerchantId",
"method":"3DS",
"recurring_payment":false
}

Decrypt using the merchant’s private key. Below is sample private key.

 -----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA4LZYjQR+dqd/XLEOXct9jwTJXHD2PTJke9djtMIjKi0h2Oc2GHoW4uJHHY/1jvFt2+zCnjTOXuVLp+76/DWA3bCwFRj+fPP6x5KKYlPb+dJDYo1TTumltNqCWymJB3u7jBC+xR4vKfRzqjxkE7xhN/SBb82uE8c3sMzVKYnUJi<…>
-----END RSA PRIVATE KEY-----

The decrypted output will be similar to this:

{
   "amount":"1000",
   "currency_code":"USD",
   "utc":"1490266732173",
   "eci_indicator":"5",
   "tokenPAN":"1234567890123456",
   "tokenPanExpiration":"0420",
   "cryptogram":"AK+zkbPMCORcABCD3AGRAoACFA=="
}

Processing the payload

Depending on the structure of the payment processing API provided by your PG, your merchant app can send either of these directly to the PG:

  • Entire paymentCredential output
  • Extracted “3DS” part only

Consult your PG documentation for specific guidance.

When using indirect model (e.g. Stripe)

In indirect (gateway token) mode, paymentCredential is the PG’s token reference ID and its status. Here’s a sample of the JSON output.

{
    "reference":"tok_18rje5E6SzUi23f2mEFAkeP7",
   "status":"AUTHORIZED"
}

For Stripe, your merchant app should be able to pass this token object directly to Charge or another appropriate payment processing API provided by the PG.