Learn about 3D Secure authentication with Google Pay for enhanced transaction security.
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.
The 3DS Google Pay payment flow consists of nine key steps.
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.
Within the Google Pay payment sheet, the customer selects their preferred payment method. Google Pay tokenises the payment data using its secure tokenisation system.
The customer confirms the payment in the Google Pay sheet. The SDK receives the encrypted payment token from Google Pay.
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.
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.
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.
The SDK receives the 3DS authentication result indicating whether authentication was successful, failed, or requires additional action.
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.
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.
To use 3DS with Google Pay payments on Android:
- Ensure 3D Secure is enabled in the Unity Portal for your merchant group.
- Configure Google Pay in the Unity Portal with your gateway merchant ID.
- Register your Android app package name and certificate in the Unity Portal.
- Get your 3DS provider ID from your payment processor.
- Ensure your app has the required permissions in
AndroidManifest.xml.
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()
}
}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")
}
}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
}
}
}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
)
}
}
}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
)
}
)
)
}
}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()
}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
)
}
}
}
}
}
}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.
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.
| Parameter | Description |
|---|---|
dataAuthenticationResult required | Authentication initiation result. Success is MerchantAuthenticationResult; failure is FailedAuthenticationResult. |
data.authenticationIdString? | 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.threeDSecureSupportedBoolean | 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.
// 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)
}
}
}
}The onPreAuthentication callback receives no parameters. Return authentication configuration or null to skip.
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
)
}
}| Value | Dimensions | Description |
|---|---|---|
FULL_SCREEN | Fullscreen | Full-screen window (recommended for mobile) |
IFRAME_250x400 | 250x400 px | Compact window |
IFRAME_390x400 | 390x400 px | Standard window |
IFRAME_500x600 | 500x600 px | Medium window |
IFRAME_600x400 | 600x400 px | Wide window |
| Value | Description | When to use |
|---|---|---|
NO_PREFERENCE (01) | No preference | Standard transactions |
NO_CHALLENGE_REQUESTED (02) | No challenge requested | Low-risk transactions |
CHALLENGE_REQUESTED_3DS_REQUESTOR_PREFERENCE (03) | Challenge requested: Your preference | When additional security desired |
CHALLENGE_REQUESTED_MANDATE (04) | Challenge requested: Mandate | High-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 |
The onPostAuthentication callback receives only the authentication identifier.
| Parameter | Description |
|---|---|
dataAuthenticationResult required | Authentication result containing authentication details. |
data.authenticationIdString | The unique identifier for the authentication attempt. Use to retrieve full details from the Unity backend. |
// 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...")
}
}The onPreAuthorisation callback receives only the gateway token ID.
| Parameter | Description |
|---|---|
dataPreAuthorisationData? | Pre-authorisation data containing token identifiers. |
data.gatewayTokenIdString? | The gateway token identifier for the payment. Use this ID to retrieve full token details from the Unity backend. |
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
}
)
)
}
}The onPostAuthorisation callback receives the authorisation result and original payment data.
| Parameter | Description |
|---|---|
resultSubmitResult? | Transaction result - either MerchantSubmitResult (success) or FailedSubmitResult (failure). |
paymentDataAuthorisationPaymentData required | Google Pay payment data collected during checkout (email, shipping address, shipping option). |
data class MerchantSubmitResult(
val merchantTransactionId: String,
val systemTransactionId: String
)data class FailedSubmitResult(
val errorCode: String,
val errorReason: String,
val correlationId: String,
val httpStatusCode: Int
)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)
}
}
}