Skip to content

3DS transactions

Learn about 3D Secure authentication with Google Pay for enhanced transaction security.

Overview

By implementing 3DS authentication with Google Pay on Android, you benefit from:

  • Enhanced security: Additional authentication layer protects against fraudulent transactions.
  • Liability shift: Shifts liability from merchant to card issuer for authenticated transactions.
  • Reduced chargebacks: Significantly decreases chargeback rates through strong authentication.
  • Regulatory compliance: Meets Strong Customer Authentication (SCA) requirements under PSD2 and similar regulations.
  • Higher success rate: Banks are more likely to approve 3DS-authenticated transactions.
  • Fraud prevention: Real-time risk assessment and challenge when necessary.

However, the 3DS payment flow is longer than the non-3DS one due to the additional authentication steps. It may also require active customer participation if a challenge is presented.

3D Secure authentication is strongly recommended for high-value transactions, subscription services, and any scenario where enhanced security is required. It significantly reduces fraud risk and chargebacks whilst providing liability shift benefits.

Payment flow

The 3DS Google Pay payment flow consists of nine key steps.

Step 1: Button interaction

The customer taps the Google Pay button in your Android app, triggering the payment sheet to open. Google Pay handles device authentication (biometric or PIN) internally.

Step 2: Payment method selection

Within the Google Pay payment sheet, the customer selects their preferred payment method. Google Pay tokenises the payment data using its secure tokenisation system.

Step 3: Payment sheet completion

The customer confirms the payment in the Google Pay sheet. The SDK receives the encrypted payment token from Google Pay.

Step 4: Pre-initiation

This is the initial step in the 3DS authentication flow. It establishes the authentication session by sending transaction and card details to the payment processor.

This step has two associated callbacks:

  • onPreInitiateAuthentication: Returns configuration for the authentication setup.
  • onPostInitiateAuthentication: Receives the result of the pre-initiation call.

Step 5: Fingerprinting

During this step, device information and characteristics are collected by the fingerprinting component. The SDK gathers device data and submits transaction details to the issuer's fingerprint URL, enabling risk assessment based on the user's device profile.

Step 6: Authentication

The 3DS server evaluates the transaction risk and determines the authentication path:

  • Frictionless flow: If the transaction is low-risk, authentication completes automatically without customer interaction
  • Challenge flow: If additional verification is needed, the customer completes the 3DS authentication challenge in a native Android dialog or WebView (PIN entry, SMS code, biometric verification, etc.)

The authentication step has two associated callbacks:

  • onPreAuthentication: Configures the main authentication parameters.
  • onPostAuthentication: Receives authentication results and challenge data.

Step 7: Authentication result

The SDK receives the 3DS authentication result indicating whether authentication was successful, failed, or requires additional action.

Step 8: Authorisation

This is the final processing step where the SDK sends the authorisation request to the payment gateway, including the 3DS authentication data. You receive the transaction data along with the 3DS authentication results and decide whether to proceed.

The authorisation step has two associated callbacks:

  • onPreAuthorisation: Provides final transaction data, including 3DS authentication results. This is your last chance to modify the transaction before authorisation.
  • onPostAuthorisation: Receives the final transaction result from the payment gateway. The transaction is either approved or declined and final transaction details are available, along with 3DS authentication confirmation.

Step 9: Authorisation result

You receive the final authorisation response from the payment gateway. The transaction is either approved or declined and final transaction details are available, along with 3DS authentication confirmation.

Implementation

Before you start

To use 3DS with Google Pay payments on Android:

  1. Ensure 3D Secure is enabled in the Unity Portal for your merchant group.
  2. Configure Google Pay in the Unity Portal with your gateway merchant ID.
  3. Register your Android app package name and certificate in the Unity Portal.
  4. Get your 3DS provider ID from your payment processor.
  5. Ensure your app has the required permissions in AndroidManifest.xml.

Step 1: Initialise the SDK

Initialise the PXP Checkout SDK with your merchant credentials:

import com.pxp.PxpCheckout
import com.pxp.checkout.models.*
import java.text.SimpleDateFormat
import java.util.*

class PaymentActivity : ComponentActivity() {
    
    private lateinit var pxpCheckout: PxpCheckout
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Fetch session from your secure backend
        val sessionConfig = fetchSessionFromBackend()
        
        // Initialise SDK
        pxpCheckout = PxpCheckout.builder()
            .withConfig(
                PxpSdkConfig(
                    environment = Environment.TEST,
                    session = sessionConfig,
                    transactionData = TransactionData(
                        currency = "GBP",
                        amount = 299.99,
                        merchant = "YOUR_MERCHANT_ID",
                        merchantTransactionId = UUID.randomUUID().toString(),
                        merchantTransactionDate = {
                            SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
                                timeZone = TimeZone.getTimeZone("UTC")
                            }.format(Date())
                        },
                        entryType = EntryType.Ecom,
                        intent = TransactionIntentData(
                            card = IntentType.Authorisation
                        )
                    ),
                    ownerType = "MerchantGroup",
                    ownerId = "your-owner-id"
                )
            )
            .withContext(this)
            .build()
    }
}

Step 2: Implement callbacks

Configure the Google Pay button with the required callbacks for 3DS authentication:

import com.pxp.checkout.components.googlepay.*
import com.pxp.checkout.components.googlepay.types.*
import com.pxp.checkout.components.threeds.ChallengeWindowSize
import com.pxp.checkout.models.*
import com.pxp.checkout.services.kount.*
import com.pxp.checkout.services.models.authentication.*

val googlePayConfig = GooglePayButtonComponentConfig().apply {
    
    // Payment request configuration
    paymentDataRequest = PaymentDataRequest(
        allowedPaymentMethods = listOf(
            PaymentMethodSpecification(
                parameters = PaymentMethodParameters(
                    allowedCardNetworks = listOf(
                        CardNetwork.VISA,
                        CardNetwork.MASTERCARD,
                        CardNetwork.AMEX
                    ),
                    // Prefer 3DS cryptogram authentication
                    allowedAuthMethods = listOf(
                        CardAuthMethod.CRYPTOGRAM_3DS
                    )
                )
            )
        ),
        transactionInfo = TransactionInfo(
            currencyCode = "GBP",
            totalPriceStatus = TotalPriceStatus.FINAL,
            totalPrice = "299.99"
        )
    )
    
    // RECOMMENDED: Use Unity authentication strategy
    useUnityAuthenticationStrategy = true
    
    // REQUIRED: Provide 3DS pre-initiation configuration
    onPreInitiateAuthentication = {
        PreInitiateIntegratedAuthenticationData(
            providerId = "your_3ds_provider_id", // Optional
            requestorAuthenticationIndicator = RequestorAuthenticationIndicatorType.PAYMENT_TRANSACTION,
            timeout = 300 // 5 minutes in seconds
        )
    }
    
    // OPTIONAL: Handle the pre-initiation result
    onPostInitiateAuthentication = { result ->
        when (result) {
            is FailedAuthenticationResult -> {
                Log.e("GooglePay3DS", "Pre-initiation failed: ${result.errorReason}")
            }
            is MerchantAuthenticationResult -> {
                Log.d("GooglePay3DS", "Pre-initiation complete: ${result.authenticationId}")
                Log.d("GooglePay3DS", "3DS supported: ${result.threeDSecureSupported}")
            }
        }
    }
    
    // REQUIRED: Configure main authentication
    onPreAuthentication = suspend {
        // This is a suspend function - no need for lifecycleScope.async
        // Get authentication decision from backend
        val decision = getAuthenticationDecision()
        
        if (decision == null) {
            // Not proceeding with authentication
            null
        } else {
            InitiateIntegratedAuthenticationData(
                merchantCountryNumericCode = "826", // United Kingdom
                merchantLegalName = "Your Store Limited",
                challengeWindowSize = ChallengeWindowSize.FULL_SCREEN,
                requestorChallengeIndicator = decision.challengeIndicator 
                    ?: RequestorChallengeIndicatorType.NO_PREFERENCE,
                timeout = decision.timeout ?: 300
            )
        }
    }
    
    // OPTIONAL: Handle the authentication result
    onPostAuthentication = { data ->
        Log.d("GooglePay3DS", "Authentication complete: ${data.authenticationId}")
        
        // Note: This callback is not suspend, so use coroutine scope if needed
        // Use lifecycleScope from your Activity or ViewModel context
    }
    
    // REQUIRED: Final transaction review before authorisation
    onPreAuthorisation = suspend { data ->
        // This is a suspend function - no need for lifecycleScope.async
        Log.d("GooglePay3DS", "Processing payment. Token: ${data?.gatewayTokenId}")
        
        // Get authorisation decision from backend
        val transactionDecision = getAuthorisationDecision(data?.gatewayTokenId)
        
        if (transactionDecision == null) {
            null
        } else {
            // Return transaction data with risk screening
            GooglePayTransactionInitData(
                riskScreeningData = RiskScreeningData(
                    performRiskScreening = true,
                    excludeDeviceData = false,
                    userIp = "192.168.1.100",
                    account = RiskScreeningAccount(
                        id = "user_12345678",
                        creationDateTime = "2024-01-15T10:30:00.000Z"
                    ),
                    items = listOf(
                        RiskScreeningItem(
                            price = 299.99,
                            quantity = 1,
                            category = "Electronics",
                            sku = "GPAY-PROD-001"
                        )
                    ),
                    fulfillments = listOf(
                        RiskScreeningFulfillment(
                            type = FulfillmentType.SHIPPED,
                            shipping = RiskScreeningShipping(
                                shippingMethod = ShippingMethod.EXPRESS
                            ),
                            recipientPerson = RiskScreeningRecipientPerson(
                                phoneNumber = "+1234567890",
                                email = "customer@example.com"
                            )
                        )
                    )
                )
            )
        }
    }
    
    // REQUIRED: Handle the final result
    onPostAuthorisation = { result, paymentData ->
        when (result) {
            is MerchantSubmitResult -> {
                // Success
                Log.d("GooglePay3DS", "Payment successful!")
                Log.d("GooglePay3DS", "Merchant Txn: ${result.merchantTransactionId}")
                Log.d("GooglePay3DS", "System Txn: ${result.systemTransactionId}")
                
                // Navigate to success screen
                navigateToSuccess(result.merchantTransactionId)
            }
            is FailedSubmitResult -> {
                // Failure
                Log.e("GooglePay3DS", "Payment failed: ${result.errorReason}")
                showError(result.errorReason)
            }
        }
    }
    
    // OPTIONAL: Handle errors
    onError = { error ->
        Log.e("GooglePay3DS", "Error: ${error.message}", error)
        showError("Payment error. Please try again.")
    }
    
    // OPTIONAL: Handle cancellation
    onCancel = {
        Log.d("GooglePay3DS", "Payment cancelled by user")
        showMessage("Payment cancelled")
    }
}

Step 3: Handle common scenarios

Conditional 3DS

Only trigger 3DS authentication above a certain amount:

val googlePayConfig = GooglePayButtonComponentConfig().apply {
    // ... basic configuration
    
    onPreInitiateAuthentication = {
        val amount = getTransactionAmount()
        
        if (amount > 100.0) {
            PreInitiateIntegratedAuthenticationData(
                providerId = "your_provider",
                requestorAuthenticationIndicator = RequestorAuthenticationIndicatorType.PAYMENT_TRANSACTION,
                timeout = 300
            )
        } else {
            // Return null to skip 3DS for low-value transactions
            null
        }
    }
}

Challenge indicator selection

Select appropriate challenge indicators based on transaction risk:

fun selectChallengeIndicator(transaction: Transaction): RequestorChallengeIndicatorType {
    return when {
        // High-value transactions
        transaction.amount > 500.0 -> {
            RequestorChallengeIndicatorType.CHALLENGE_REQUESTED_MANDATE
        }
        
        // Subscription first payment
        transaction.type == TransactionType.SUBSCRIPTION && transaction.isFirstPayment -> {
            RequestorChallengeIndicatorType.CHALLENGE_REQUESTED_3DS_REQUESTOR_PREFERENCE
        }
        
        // Trusted returning customers
        transaction.customerTrust == CustomerTrust.HIGH -> {
            RequestorChallengeIndicatorType.NO_CHALLENGE_REQUESTED
        }
        
        // Default
        else -> RequestorChallengeIndicatorType.NO_PREFERENCE
    }
}

val googlePayConfig = GooglePayButtonComponentConfig().apply {
    // ... basic configuration
    
    onPreAuthentication = suspend {
        val decision = getAuthenticationDecision()
        
        if (decision == null) {
            null
        } else {
            val transaction = getCurrentTransaction()
            val challengeIndicator = selectChallengeIndicator(transaction)
            
            InitiateIntegratedAuthenticationData(
                merchantCountryNumericCode = "826",
                merchantLegalName = "Your Store Ltd",
                challengeWindowSize = ChallengeWindowSize.FULL_SCREEN,
                requestorChallengeIndicator = challengeIndicator
            )
        }
    }
}

Soft decline retry

Handle soft declines by retrying with forced 3DS challenge:

val googlePayConfig = GooglePayButtonComponentConfig().apply {
    // ... other configuration
    
    onPreRetrySoftDecline = { result ->
        Log.d("GooglePay3DS", "Soft decline detected, retrying with 3DS challenge")
        
        // Access result details based on result type
        when (result) {
            is RefusedSubmitResult -> {
                // Track soft decline
                trackEvent("payment-soft-decline", mapOf(
                    "stateCode" to result.stateData.code,
                    "stateMessage" to result.stateData.message,
                    "providerCode" to (result.providerResponse?.code ?: "")
                ))
            }
            else -> {
                Log.w("GooglePay3DS", "Unexpected result type for soft decline: ${result::class.simpleName}")
            }
        }
        
        // Retry with forced 3DS challenge
        GooglePayButtonComponentConfig.RetryConfiguration(
            retry = true,
            updatedConfigs = GooglePayButtonComponentConfig.RetryConfiguration.UpdatedConfigs(
                onPreInitiateAuthentication = {
                    PreInitiateIntegratedAuthenticationData(
                        providerId = "your_3ds_provider_id",
                        requestorAuthenticationIndicator = RequestorAuthenticationIndicatorType.PAYMENT_TRANSACTION,
                        timeout = 300
                    )
                },
                onPreAuthentication = suspend {
                    InitiateIntegratedAuthenticationData(
                        merchantCountryNumericCode = "826",
                        merchantLegalName = "Your Store Ltd",
                        challengeWindowSize = ChallengeWindowSize.FULL_SCREEN,
                        // Force challenge on retry
                        requestorChallengeIndicator = RequestorChallengeIndicatorType.CHALLENGE_REQUESTED_MANDATE
                    )
                }
            )
        )
    }
}

Step 4: Handle errors

Implement comprehensive error handling for 3DS payments:

val googlePayConfig = GooglePayButtonComponentConfig().apply {
    // ... basic configuration
    
    onPostAuthentication = { data ->
        // Note: This is a non-suspend callback
        // Use lifecycleScope from your Activity or ViewModel context if you need to call suspend functions
        
        Log.d("GooglePay3DS", "Authentication complete: ${data.authenticationId}")
        // Process authentication result or trigger backend calls
    }
    
    onError = { error ->
        Log.e("GooglePay3DS", "Payment error", error)
        
        // Handle error
        val errorMessage = "Payment failed. Please try again or use a different payment method."
        showError(errorMessage)
    }
    
    onPostAuthorisation = { result, paymentData ->
        when (result) {
            is MerchantSubmitResult -> {
                handlePaymentSuccess(result, paymentData)
            }
            is FailedSubmitResult -> {
                handlePaymentFailure(result)
            }
        }
    }
}

fun handlePaymentSuccess(result: MerchantSubmitResult, paymentData: AuthorisationPaymentData) {
    Log.d("GooglePay3DS", "✅ Payment successful with 3DS")
    Log.d("GooglePay3DS", "Transaction ID: ${result.merchantTransactionId}")
    Log.d("GooglePay3DS", "System Transaction ID: ${result.systemTransactionId}")
    
    // Store transaction details
    storeTransactionRecord(
        transactionId = result.merchantTransactionId,
        systemTransactionId = result.systemTransactionId,
        processingType = "3ds",
        paymentMethod = "google-pay",
        timestamp = System.currentTimeMillis()
    )
    
    // Navigate to success page
    navigateToSuccess(result.merchantTransactionId)
}

fun handlePaymentFailure(result: FailedSubmitResult) {
    Log.e("GooglePay3DS", "❌ Payment failed")
    Log.e("GooglePay3DS", "Error code: ${result.errorCode}")
    Log.e("GooglePay3DS", "Error reason: ${result.errorReason}")
    Log.e("GooglePay3DS", "Correlation ID: ${result.correlationId}")
    
    showError("Payment failed: ${result.errorReason}")
    
    // Log for analysis
    logPaymentDecline(
        errorCode = result.errorCode,
        errorReason = result.errorReason,
        correlationId = result.correlationId,
        httpStatusCode = result.httpStatusCode,
        timestamp = System.currentTimeMillis()
    )
    
    // Offer alternative payment methods
    showAlternativePaymentOptions()
}

Complete example

The following example shows a complete 3DS Google Pay implementation with Jetpack Compose:

import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.pxp.PxpCheckout
import com.pxp.checkout.components.googlepay.*
import com.pxp.checkout.components.googlepay.types.*
import com.pxp.checkout.components.threeds.ChallengeWindowSize
import com.pxp.checkout.models.*
import com.pxp.checkout.services.kount.*
import com.pxp.checkout.services.models.authentication.*
import com.pxp.checkout.types.ComponentType
import kotlinx.coroutines.launch
import java.util.*

@Composable
fun GooglePay3DSCheckout(
    pxpCheckout: PxpCheckout,
    amount: String,
    onSuccess: (String) -> Unit,
    onError: (String) -> Unit
) {
    var isProcessing by remember { mutableStateOf(false) }
    var errorMessage by remember { mutableStateOf<String?>(null) }
    val coroutineScope = rememberCoroutineScope()
    
    // Create Google Pay button component with 3DS
    val googlePayComponent = remember {
        pxpCheckout.createComponent<
            GooglePayButtonComponent,
            GooglePayButtonComponentConfig
        >(
            type = ComponentType.GOOGLE_PAY_BUTTON,
            config = GooglePayButtonComponentConfig().apply {
                
                paymentDataRequest = PaymentDataRequest(
                    allowedPaymentMethods = listOf(
                        PaymentMethodSpecification(
                            parameters = PaymentMethodParameters(
                                allowedCardNetworks = listOf(
                                    CardNetwork.VISA,
                                    CardNetwork.MASTERCARD,
                                    CardNetwork.AMEX
                                ),
                                allowedAuthMethods = listOf(
                                    CardAuthMethod.CRYPTOGRAM_3DS
                                )
                            )
                        )
                    ),
                    transactionInfo = TransactionInfo(
                        currencyCode = "GBP",
                        countryCode = "GB",
                        totalPriceStatus = TotalPriceStatus.FINAL,
                        totalPrice = amount
                    )
                )
                
                // Use Unity authentication strategy
                useUnityAuthenticationStrategy = true
                
                // Step 1: Set up 3DS
                onPreInitiateAuthentication = {
                    Log.d("GooglePay3DS", "🔐 Initiating 3DS authentication")
                    PreInitiateIntegratedAuthenticationData(
                        providerId = BuildConfig.THREEDS_PROVIDER_ID,
                        requestorAuthenticationIndicator = RequestorAuthenticationIndicatorType.PAYMENT_TRANSACTION,
                        timeout = 300
                    )
                }
                
                // Step 2: Handle pre-initiation result
                onPostInitiateAuthentication = { result ->
                    when (result) {
                        is FailedAuthenticationResult -> {
                            Log.e("GooglePay3DS", "Pre-initiation failed: ${result.errorReason}")
                        }
                        is MerchantAuthenticationResult -> {
                            Log.d("GooglePay3DS", "Pre-authentication check: ${result.authenticationId}")
                            Log.d("GooglePay3DS", "3DS supported: ${result.threeDSecureSupported}")

                            val authId = result.authenticationId ?: return@onPostInitiateAuthentication

                            coroutineScope.launch {
                                val authResult = fetchAuthenticationResultFromBackend(authId)
                                evaluateAuthenticationAndUpdateSession(authResult)

                                if (authResult.scaMandated) {
                                    Log.d("GooglePay3DS", "SCA mandated - full authentication required")
                                }
                            }
                        }
                    }
                }
                
                // Step 3: Configure authentication
                onPreAuthentication = suspend {
                    Log.d("GooglePay3DS", "🔍 Configuring full 3DS authentication")
                    
                    val decision = getAuthenticationDecision()
                    
                    if (decision == null) {
                        null // Skip authentication
                    } else {
                        // Assess transaction risk
                        val transactionAmount = amount.toDoubleOrNull() ?: 0.0
                        val customerHistory = getCustomerHistory()
                        val isHighRisk = transactionAmount > 500.0 || 
                            customerHistory.chargebacks > 0
                        
                        val challengeIndicator = when {
                            isHighRisk -> RequestorChallengeIndicatorType.CHALLENGE_REQUESTED_MANDATE
                            transactionAmount < 30.0 && 
                                customerHistory.successfulTransactions > 10 -> {
                                RequestorChallengeIndicatorType.NO_CHALLENGE_REQUESTED
                            }
                            else -> RequestorChallengeIndicatorType.NO_PREFERENCE
                        }
                        
                        InitiateIntegratedAuthenticationData(
                            merchantCountryNumericCode = "826",
                            merchantLegalName = "Your Store Limited",
                            challengeWindowSize = ChallengeWindowSize.FULL_SCREEN,
                            requestorChallengeIndicator = challengeIndicator
                        )
                    }
                }
                
                // Step 4: Handle authentication result
                onPostAuthentication = { data ->
                    Log.d("GooglePay3DS", "✅ 3DS authentication completed: ${data.authenticationId}")
                    
                    coroutineScope.launch {
                        val authResult = fetchAuthenticationResultFromBackend(data.authenticationId)
                        
                        if (authResult.state == AuthenticationState.AUTHENTICATION_FAILED) {
                            errorMessage = "Card verification failed. Please try again."
                            return@launch
                        }
                        
                        evaluateAuthenticationAndUpdateAuthorization(authResult)
                    }
                }
                
                // Step 5: Final authorisation
                onPreAuthorisation = suspend { data ->
                    Log.d("GooglePay3DS", "💳 Processing payment with 3DS data")
                    
                    val transactionDecision = getAuthorisationDecision(data?.gatewayTokenId)
                    
                    if (transactionDecision == null) {
                        null
                    } else {
                        GooglePayTransactionInitData(
                            riskScreeningData = RiskScreeningData(
                                performRiskScreening = true,
                                excludeDeviceData = false,
                                userIp = "192.168.1.100",
                                account = RiskScreeningAccount(
                                    id = "user_12345678",
                                    creationDateTime = "2024-01-15T10:30:00.000Z"
                                ),
                                items = listOf(
                                    RiskScreeningItem(
                                        price = 299.99,
                                        quantity = 1,
                                        category = "Electronics",
                                        sku = "GPAY-PROD-001"
                                    )
                                ),
                                fulfillments = listOf(
                                    RiskScreeningFulfillment(
                                        type = FulfillmentType.SHIPPED,
                                        shipping = RiskScreeningShipping(
                                            shippingMethod = ShippingMethod.EXPRESS
                                        ),
                                        recipientPerson = RiskScreeningRecipientPerson(
                                            phoneNumber = "+1234567890",
                                            email = "customer@example.com"
                                        )
                                    )
                                )
                            )
                        )
                    }
                }
                
                // Step 6: Handle success/failure
                onPostAuthorisation = { result, _ ->
                    isProcessing = false
                    
                    when (result) {
                        is MerchantSubmitResult -> {
                            Log.d("GooglePay3DS", "✅ Payment authorised with 3DS")
                            onSuccess(result.merchantTransactionId)
                        }
                        is FailedSubmitResult -> {
                            Log.e("GooglePay3DS", "❌ Payment declined: ${result.errorReason}")
                            errorMessage = "Payment declined. Please try another payment method."
                        }
                    }
                }
                
                // Step 7: Error handling
                onError = { error ->
                    Log.e("GooglePay3DS", "Error during 3DS flow", error)
                    isProcessing = false
                    errorMessage = "An error occurred. Please try again."
                }
                
                onCancel = {
                    Log.d("GooglePay3DS", "Payment cancelled by user")
                    isProcessing = false
                    errorMessage = null
                }
            }
        )
    }
    
    // UI
    Surface(
        modifier = Modifier.fillMaxSize(),
        color = MaterialTheme.colorScheme.background
    ) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp)
        ) {
            // Order summary
            Card(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(bottom = 24.dp)
            ) {
                Column(modifier = Modifier.padding(16.dp)) {
                    Text(
                        text = "Order summary",
                        style = MaterialTheme.typography.titleLarge
                    )
                    
                    Spacer(modifier = Modifier.height(16.dp))
                    
                    Row(
                        modifier = Modifier.fillMaxWidth(),
                        horizontalArrangement = Arrangement.SpaceBetween
                    ) {
                        Text("Premium Product")
                        Text($amount")
                    }
                    
                    Divider(modifier = Modifier.padding(vertical = 8.dp))
                    
                    Row(
                        modifier = Modifier.fillMaxWidth(),
                        horizontalArrangement = Arrangement.SpaceBetween
                    ) {
                        Text(
                            text = "Total",
                            style = MaterialTheme.typography.titleMedium
                        )
                        Text(
                            text =$amount",
                            style = MaterialTheme.typography.titleMedium
                        )
                    }
                }
            }
            
            // Payment section
            Card(modifier = Modifier.fillMaxWidth()) {
                Column(modifier = Modifier.padding(16.dp)) {
                    Text(
                        text = "Pay with Google Pay (3DS Secure)",
                        style = MaterialTheme.typography.titleLarge
                    )
                    
                    Spacer(modifier = Modifier.height(16.dp))
                    
                    // Error message
                    errorMessage?.let { error ->
                        Card(
                            modifier = Modifier
                                .fillMaxWidth()
                                .padding(bottom = 16.dp),
                            colors = CardDefaults.cardColors(
                                containerColor = MaterialTheme.colorScheme.errorContainer
                            )
                        ) {
                            Text(
                                text = error,
                                modifier = Modifier.padding(12.dp),
                                color = MaterialTheme.colorScheme.onErrorContainer
                            )
                        }
                    }
                    
                    // Google Pay button
                    googlePayComponent?.Content(
                        modifier = Modifier
                            .fillMaxWidth()
                            .height(56.dp)
                    )
                    
                    // Processing indicator
                    if (isProcessing) {
                        LinearProgressIndicator(
                            modifier = Modifier
                                .fillMaxWidth()
                                .padding(top = 16.dp)
                        )
                        
                        Text(
                            text = "Processing secure payment...",
                            modifier = Modifier.padding(top = 8.dp),
                            style = MaterialTheme.typography.bodyMedium,
                            color = MaterialTheme.colorScheme.onSurfaceVariant
                        )
                    }
                    
                    Spacer(modifier = Modifier.height(16.dp))
                    
                    // Security info
                    Column(
                        modifier = Modifier.padding(vertical = 8.dp),
                        verticalArrangement = Arrangement.spacedBy(4.dp)
                    ) {
                        Text(
                            text = "✓ Protected by 3D Secure authentication",
                            style = MaterialTheme.typography.bodySmall,
                            color = MaterialTheme.colorScheme.onSurfaceVariant
                        )
                        Text(
                            text = "✓ Fast and secure checkout with Google Pay",
                            style = MaterialTheme.typography.bodySmall,
                            color = MaterialTheme.colorScheme.onSurfaceVariant
                        )
                        Text(
                            text = "✓ Additional verification for your safety",
                            style = MaterialTheme.typography.bodySmall,
                            color = MaterialTheme.colorScheme.onSurfaceVariant
                        )
                    }
                }
            }
        }
    }
}

Callback data

This section describes the data received by the different callbacks as part of the 3DS Google Pay flow on Android.

The onPreInitiateAuthentication callback doesn't receive any parameters. Instead, it returns your 3DS configuration in the PreInitiateIntegratedAuthenticationData object.

onPostInitiateAuthentication

The onPostInitiateAuthentication callback receives a MerchantAuthenticationResult on success, or a FailedAuthenticationResult on failure. On success, the SDK exposes two fields: authenticationId and threeDSecureSupported. Use the ID to retrieve full pre-initiate details from your backend.

When both authenticationId is absent and threeDSecureSupported is false, the SDK skips the remaining 3DS flow and doesn't invoke onPreAuthentication. This early-exit check applies when useUnityAuthenticationStrategy is false. When useUnityAuthenticationStrategy is true, the SDK uses Unity's authentication strategy API instead.

Event data

ParameterDescription
data
AuthenticationResult
required
Authentication initiation result. Success is MerchantAuthenticationResult; failure is FailedAuthenticationResult.
data.authenticationId
String?
The unique identifier for this 3DS session. May be null when 3DS isn't required. Use this ID to retrieve the full PreInitiateAuthentication result from the Unity backend.
data.threeDSecureSupported
Boolean
Whether 3DS authentication is supported for this transaction.

Use the authenticationId with the Get 3DS pre-initiate authentication details API to retrieve pre-authentication results including SCA mandates, exemptions, and state.

Example implementation

// Within an Activity
onPostInitiateAuthentication = { data ->
    when (data) {
        is FailedAuthenticationResult -> {
            Log.e("GooglePay3DS", "3DS pre-initiation failed: ${data.errorReason}")
            return@onPostInitiateAuthentication
        }
        is MerchantAuthenticationResult -> {
            Log.d("GooglePay3DS", "Authentication ID: ${data.authenticationId}")
            Log.d("GooglePay3DS", "3DS supported: ${data.threeDSecureSupported}")

            if (data.authenticationId.isNullOrEmpty() && !data.threeDSecureSupported) {
                Log.d("GooglePay3DS", "3DS not supported — SDK skips authentication")
                return@onPostInitiateAuthentication
            }

            val authId = data.authenticationId ?: return@onPostInitiateAuthentication

            lifecycleScope.launch {
                val authResult = fetchAuthenticationResultFromBackend(authId)

                when (authResult.state) {
                    "PendingClientData" -> Log.d("GooglePay3DS", "Waiting for client data collection")
                    else -> Log.d("GooglePay3DS", "Authentication state: ${authResult.state}")
                }

                if (authResult.scaMandated) {
                    Log.d("GooglePay3DS", "SCA is required - 3DS must complete successfully")
                    showMessage("Additional verification required")
                }

                when (authResult.applicableExemptions) {
                    "LVP" -> Log.d("GooglePay3DS", "Low value exemption available")
                    "TRA" -> Log.d("GooglePay3DS", "Transaction risk analysis exemption available")
                }

                evaluateAuthenticationAndUpdateSession(authResult)
            }
        }
    }
}

onPreAuthentication

The onPreAuthentication callback receives no parameters. Return authentication configuration or null to skip.

Example implementation

onPreAuthentication = suspend {
    // Get authentication decision evaluated after onPostInitiateAuthentication
    val decision = getAuthenticationDecision()
    
    if (decision == null) {
        // Not proceeding with authentication
        Log.d("GooglePay3DS", "Skipping authentication based on backend decision")
        null
    } else {
        Log.d("GooglePay3DS", "Proceeding with authentication")
        
        // Check if SCA is mandated to adjust challenge preference
        val challengeIndicator = when {
            decision.scaMandated -> {
                Log.d("GooglePay3DS", "SCA mandated - requesting challenge")
                RequestorChallengeIndicatorType.CHALLENGE_REQUESTED_MANDATE
            }
            decision.applicableExemptions == "LVP" -> {
                Log.d("GooglePay3DS", "Low value exemption - requesting no challenge")
                RequestorChallengeIndicatorType.NO_CHALLENGE_REQUESTED_LOW_VALUE_EXEMPTION
            }
            decision.applicableExemptions == "TRA" -> {
                Log.d("GooglePay3DS", "TRA exemption - requesting no challenge")
                RequestorChallengeIndicatorType.NO_CHALLENGE_REQUESTED_TRANSACTION_RISK_ANALYSIS
            }
            else -> RequestorChallengeIndicatorType.NO_PREFERENCE
        }
        
        InitiateIntegratedAuthenticationData(
            merchantCountryNumericCode = "826",
            merchantLegalName = "Your Company Ltd",
            challengeWindowSize = ChallengeWindowSize.FULL_SCREEN,
            requestorChallengeIndicator = challengeIndicator,
            timeout = if (decision.scaMandated) 600 else 300
        )
    }
}

Challenge window sizes

ValueDimensionsDescription
FULL_SCREENFullscreenFull-screen window (recommended for mobile)
IFRAME_250x400250x400 pxCompact window
IFRAME_390x400390x400 pxStandard window
IFRAME_500x600500x600 pxMedium window
IFRAME_600x400600x400 pxWide window

Challenge indicators

ValueDescriptionWhen to use
NO_PREFERENCE (01)No preferenceStandard transactions
NO_CHALLENGE_REQUESTED (02)No challenge requestedLow-risk transactions
CHALLENGE_REQUESTED_3DS_REQUESTOR_PREFERENCE (03)Challenge requested: Your preferenceWhen additional security desired
CHALLENGE_REQUESTED_MANDATE (04)Challenge requested: MandateHigh-value or high-risk transactions
NO_CHALLENGE_REQUESTED_TRA_PERFORMED (05)No challenge requested (transaction risk analysis performed)When you have performed risk analysis
NO_CHALLENGE_REQUESTED_LOW_VALUE_EXEMPTION (10)No challenge requested (Low value exemption)Low-value transactions under exemption thresholds

onPostAuthentication

The onPostAuthentication callback receives only the authentication identifier.

Event data

ParameterDescription
data
AuthenticationResult
required
Authentication result containing authentication details.
data.authenticationId
String
The unique identifier for the authentication attempt. Use to retrieve full details from the Unity backend.

Example implementation

// Within an Activity
onPostAuthentication = { data ->
    Log.d("GooglePay3DS", "3DS authentication completed: ${data.authenticationId}")
    
    // Use lifecycleScope in Activity context
    lifecycleScope.launch {
        // Retrieve full authentication result from backend
        val authResult = fetchAuthenticationResultFromBackend(data.authenticationId)
        
        Log.d("GooglePay3DS", "Authentication result: $authResult")
        
        // Check transaction status
        when (authResult.transactionStatus) {
            "Y" -> {
                Log.d("GooglePay3DS", "Authentication successful - no challenge needed")
                showMessage("Card verified successfully")
            }
            "C" -> {
                Log.d("GooglePay3DS", "Challenge completed - checking result...")
                if (authResult.state == "AuthenticationSuccessful") {
                    Log.d("GooglePay3DS", "Challenge completed successfully")
                    showMessage("Verification completed")
                } else {
                    Log.e("GooglePay3DS", "Challenge failed")
                    showError("Verification failed. Please try again.")
                    return@launch // Stop payment
                }
            }
            "N" -> {
                Log.e("GooglePay3DS", "Authentication failed")
                showError("Card verification failed")
                return@launch // Stop payment
            }
            "R" -> {
                Log.e("GooglePay3DS", "Authentication rejected")
                showError("Payment was rejected by your bank")
                return@launch // Stop payment
            }
        }
        
        // Evaluate authentication result to update authorisation decision
        evaluateAuthenticationAndUpdateAuthorization(authResult)
        
        Log.d("GooglePay3DS", "Proceeding to final authorisation...")
    }
}

onPreAuthorisation

The onPreAuthorisation callback receives only the gateway token ID.

Event data

ParameterDescription
data
PreAuthorisationData?
Pre-authorisation data containing token identifiers.
data.gatewayTokenId
String?
The gateway token identifier for the payment. Use this ID to retrieve full token details from the Unity backend.

Example implementation

onPreAuthorisation = suspend { data ->
    Log.d("GooglePay3DS", "Pre-authorisation for token: ${data?.gatewayTokenId}")
    
    // Retrieve token details and make authorisation decision
    val transactionDecision = getAuthorisationDecision(data?.gatewayTokenId)
    
    if (transactionDecision == null) {
        Log.d("GooglePay3DS", "Not proceeding with authorisation")
        null
    } else {
        // Perform pre-payment validation
        val isHighRisk = checkCustomerRiskProfile()
        val customerTier = getCustomerTier()
        
        GooglePayTransactionInitData(
            riskScreeningData = RiskScreeningData(
                performRiskScreening = true,
                excludeDeviceData = false,
                userIp = "192.168.1.100",
                account = RiskScreeningAccount(
                    id = "user_12345678",
                    creationDateTime = "2024-01-15T10:30:00.000Z"
                ),
                items = listOf(
                    RiskScreeningItem(
                        price = 299.99,
                        quantity = 1,
                        category = "Electronics",
                        sku = "GPAY-PROD-001"
                    )
                ),
                fulfillments = listOf(
                    RiskScreeningFulfillment(
                        type = FulfillmentType.SHIPPED,
                        shipping = RiskScreeningShipping(
                            shippingMethod = ShippingMethod.EXPRESS
                        ),
                        recipientPerson = RiskScreeningRecipientPerson(
                            phoneNumber = "+1234567890",
                            email = "customer@example.com"
                        )
                    )
                )
            ),
            psd2Data = GooglePayTransactionInitData.Psd2Data(
                scaExemption = if (cartTotal < 30.0) {
                    ScaExemptionType.LOW_VALUE
                } else {
                    ScaExemptionType.TRANSACTION_RISK_ANALYSIS
                }
            )
        )
    }
}

onPostAuthorisation

The onPostAuthorisation callback receives the authorisation result and original payment data.

Event data

ParameterDescription
result
SubmitResult?
Transaction result - either MerchantSubmitResult (success) or FailedSubmitResult (failure).
paymentData
AuthorisationPaymentData
required
Google Pay payment data collected during checkout (email, shipping address, shipping option).

Success result

data class MerchantSubmitResult(
    val merchantTransactionId: String,
    val systemTransactionId: String
)

Failure result

data class FailedSubmitResult(
    val errorCode: String,
    val errorReason: String,
    val correlationId: String,
    val httpStatusCode: Int
)

Example implementation

onPostAuthorisation = { result, paymentData ->
    Log.d("GooglePay3DS", "3DS Google Pay payment result")
    
    when (result) {
        is MerchantSubmitResult -> {
            // Success
            Log.d("GooglePay3DS", "Payment successful with 3DS!")
            Log.d("GooglePay3DS", "Merchant Txn: ${result.merchantTransactionId}")
            Log.d("GooglePay3DS", "System Txn: ${result.systemTransactionId}")
            
            // Store transaction details
            storeTransactionRecord(
                merchantTransactionId = result.merchantTransactionId,
                systemTransactionId = result.systemTransactionId,
                paymentMethod = "google-pay",
                processingType = "3ds",
                liabilityShift = true,
                timestamp = System.currentTimeMillis()
            )
            
            // Navigate to success page
            navigateToSuccess(result.merchantTransactionId)
        }
        is FailedSubmitResult -> {
            // Failure
            Log.e("GooglePay3DS", "Payment failed")
            Log.e("GooglePay3DS", "Error code: ${result.errorCode}")
            Log.e("GooglePay3DS", "Error reason: ${result.errorReason}")
            Log.e("GooglePay3DS", "Correlation ID: ${result.correlationId}")
            
            // Handle specific error types
            handlePaymentFailure(result)
        }
    }
}