Skip to main content

Adyen Android native (Google Pay)

This guide covers integrating Google Pay checkout in a native Android app using the Adyen Android SDK and the Centra Storefront API. It assumes you are familiar with the express checkout flow overview and the web express checkout guide.

The Centra Storefront API is fully native-compatible — all API calls are GraphQL over HTTPS with token-based auth. The expressCheckoutWidgets query returns raw JSON config that you pass directly to the Adyen Android SDK.

Prerequisites

Centra

  • Adyen Drop-in configured as a payment method in your Centra store
  • Google Pay enabled in the Adyen Drop-in plugin settings

Android project

SDK installation (Gradle)

dependencies {
implementation("com.adyen.checkout:google-pay:5.7.0")
implementation("com.adyen.checkout:3ds2:5.7.0")
// For Jetpack Compose:
// implementation("com.adyen.checkout:google-pay-compose:5.7.0")
}

Session management

Centra uses token-based sessions. Every Storefront API response includes an extensions.token field — persist this in EncryptedSharedPreferences and send it as Authorization: Bearer {token} on every request. See Authorization & sessions for details.

Integration steps

1. Fetch express checkout configuration

Request expressCheckoutWidgets from your backend. This call requires the X-Shared-Secret header, so it must go through a server-side proxy — never expose the shared secret in the Android app.

Your backend calls:

query expressCheckoutWidgets($input: ExpressCheckoutWidgetsInput!) {
expressCheckoutWidgets(input: $input) {
type
... on AdyenExpressCheckoutWidget {
clientKey
paymentMethodsResponse
country
context
languageCode
paymentAmount {
amount
currency
}
}
}
}

Parse the response into a Kotlin data class:

@Serializable
data class AdyenExpressConfig(
val clientKey: String,
val paymentMethodsResponse: JsonObject,
val country: String,
val context: String,
val languageCode: String,
val paymentAmount: PaymentAmount
) {
@Serializable
data class PaymentAmount(
val amount: Int,
val currency: String
)
}

Fetch from your backend endpoint (not directly from Centra — your backend holds the shared secret):

suspend fun fetchExpressConfig(): AdyenExpressConfig {
val response = httpClient.get("https://your-backend.com/api/express-config") {
header("Authorization", "Bearer $centraToken")
}
return response.body()
}

2. Initialize Adyen checkout

Use the Advanced flow — Centra manages the server-side payment session, not Adyen Sessions.

import com.adyen.checkout.core.Environment
import com.adyen.checkout.core.AdyenLogLevel

val environment = if (config.context == "live") {
Environment.EUROPE // or Environment.AUSTRALIA, Environment.UNITED_STATES
} else {
Environment.TEST
}

val amount = Amount(
currency = config.paymentAmount.currency,
value = config.paymentAmount.amount
)

Parse the paymentMethodsResponse from the config:

import com.adyen.checkout.components.core.PaymentMethodTypes
import com.adyen.checkout.core.internal.data.model.ModelUtils

val paymentMethodsJson = config.paymentMethodsResponse.toString()
val paymentMethodsApiResponse = PaymentMethodsApiResponse.SERIALIZER
.deserialize(JSONObject(paymentMethodsJson))

3. Set up Google Pay component

Configure GooglePayComponent for express checkout:

import com.adyen.checkout.googlepay.GooglePayComponent
import com.adyen.checkout.googlepay.GooglePayConfiguration

val googlePayConfig = GooglePayConfiguration.Builder(
shopperLocale = Locale.forLanguageTag(config.languageCode),
environment = environment,
clientKey = config.clientKey
)
.setAmount(amount)
.setCountryCode(config.country)
.setMerchantAccount("YourAdyenMerchantAccount")
.setGooglePayEnvironment(
if (config.context == "live") WalletConstants.ENVIRONMENT_PRODUCTION
else WalletConstants.ENVIRONMENT_TEST
)
.setIsExpressCheckout(true)
.setShippingAddressRequired(true)
.setShippingAddressParameters(
ShippingAddressParameters.newBuilder()
.setPhoneNumberRequired(true)
.build()
)
.build()

Check availability before showing the button:

GooglePayComponent.isAvailable(
application = application,
paymentMethod = googlePayPaymentMethod,
configuration = googlePayConfig
) { isAvailable ->
if (isAvailable) {
showGooglePayButton()
}
}

Create the component and attach it to your Activity or Fragment:

val googlePayComponent = GooglePayComponent.PROVIDER.get(
activity = this,
paymentMethod = googlePayPaymentMethod,
configuration = googlePayConfig,
callback = googlePayCallback
)

For Jetpack Compose, use the Compose variant:

GooglePayButton(
modifier = Modifier.fillMaxWidth(),
component = googlePayComponent,
onClick = { googlePayComponent.startGooglePayScreen(activity, REQUEST_CODE) }
)

4. Handle Google Pay callbacks

Implement the callback interface. Each callback maps to a Centra API call:

Web (@adyen/adyen-web)Android (adyen-android)Centra API call
onPaymentDataChanged(INITIALIZE)Pre-presentation setupquery selection + optional mutation addItem
onPaymentDataChanged(SHIPPING_ADDRESS)onShippingAddressChanged()mutation paymentInstructions(paymentInitiateOnly: true)
onPaymentDataChanged(SHIPPING_OPTION)onShippingOptionChanged()mutation setShippingMethod
onAuthorizedPayment data in resultValidate addresses locally
onSubmitonSubmit() callbackmutation paymentInstructions (final)

Before presenting (item preparation)

Before starting the Google Pay flow, ensure the selection is ready. If this is a PDP express checkout, add the item:

// Before starting Google Pay
val selection = centraClient.query(SelectionQuery())

pendingExpressItem?.let { item ->
val addResult = centraClient.mutate(
AddItemMutation(item = item.itemId, quantity = 1)
)
addedLineId = addResult.addItem?.selection?.lines?.firstOrNull()?.id
}

Shipping address changed

When the shopper provides or changes their shipping address in the Google Pay sheet:

override fun onShippingAddressChanged(
shippingAddress: GooglePayShippingAddress,
resolve: (GooglePayShippingAddressResult) -> Unit,
reject: (String) -> Unit
) {
scope.launch {
try {
val address = mapGoogleAddressToCentra(shippingAddress)

val result = centraClient.mutate(
PaymentInstructionsMutation(
input = PaymentInstructionsInput(
paymentMethod = adyenPaymentMethodId,
shippingAddress = address,
paymentInitiateOnly = true,
paymentReturnPage = returnUrl,
paymentFailedPage = failedUrl,
termsAndConditions = true
)
)
)

val selection = result.paymentInstructions?.selection
val shippingOptions = buildGooglePayShippingOptions(selection)
val updatedAmount = buildGooglePayTransactionInfo(selection)

resolve(GooglePayShippingAddressResult(
shippingOptions = shippingOptions,
updatedTransactionInfo = updatedAmount
))
} catch (e: Exception) {
reject("Failed to calculate shipping")
}
}
}

Shipping option changed

When the shopper selects a different shipping option:

override fun onShippingOptionChanged(
shippingOption: GooglePayShippingOption,
resolve: (GooglePayShippingOptionResult) -> Unit,
reject: (String) -> Unit
) {
scope.launch {
try {
val result = centraClient.mutate(
SetShippingMethodMutation(id = shippingOption.id)
)

val selection = result.setShippingMethod?.selection
val updatedAmount = buildGooglePayTransactionInfo(selection)

resolve(GooglePayShippingOptionResult(
updatedTransactionInfo = updatedAmount
))
} catch (e: Exception) {
reject("Failed to set shipping method")
}
}
}

Payment authorized and address mapping

After authorization, map Google Pay addresses to Centra's format:

fun mapGoogleAddressToCentra(
address: GooglePayAddress
): CentraAddressInput {
return CentraAddressInput(
firstName = address.name?.split(" ")?.firstOrNull(),
lastName = address.name?.split(" ")?.drop(1)?.joinToString(" "),
address1 = address.address1,
address2 = address.address2,
city = address.locality,
state = address.administrativeArea,
zipCode = address.postalCode,
country = address.countryCode,
phoneNumber = address.phoneNumber
)
}

Final payment submission (onSubmit)

When Adyen submits the final Google Pay payment data:

override fun onSubmit(state: GooglePayComponentState) {
scope.launch {
val paymentFields = state.data.paymentMethod?.let { pm ->
JSONObject(PaymentMethodDetails.SERIALIZER.serialize(pm).toString())
.toMap()
} ?: emptyMap()

val result = centraClient.mutate(
PaymentInstructionsMutation(
input = PaymentInstructionsInput(
paymentMethod = adyenPaymentMethodId,
shippingAddress = currentShippingAddress,
separateBillingAddress = currentBillingAddress,
paymentMethodSpecificFields = paymentFields,
paymentReturnPage = returnUrl,
paymentFailedPage = failedUrl,
termsAndConditions = true
)
)
)

handlePaymentResponse(result)
}
}

5. Handle payment response and 3D Secure

After paymentInstructions, inspect the response action:

fun handlePaymentResponse(result: PaymentInstructionsPayload) {
val action = result.paymentInstructions?.action ?: run {
val errors = result.paymentInstructions?.userErrors
if (!errors.isNullOrEmpty()) {
showError(errors)
}
return
}

when (action.__typename) {
"JavascriptPaymentAction" -> {
// 3DS — extract formFields, build Adyen Action, handle natively
val formFields = action.formFields
if (formFields != null) {
val actionJson = JSONObject(formFields.toString())
val adyenAction = Action.SERIALIZER.deserialize(actionJson)
googlePayComponent.handleAction(adyenAction, activity)
}
}

"RedirectPaymentAction" -> {
// Redirect-based 3DS or payment method
action.url?.let { url ->
val intent = CustomTabsIntent.Builder().build()
intent.launchUrl(activity, Uri.parse(url))
}
}

"SuccessPaymentAction" -> {
// Payment completed
navigateToOrderConfirmation()
}
}
}

3DS completion (onAdditionalDetails)

After the Adyen SDK completes 3DS, it provides the result. Send it to Centra:

override fun onAdditionalDetails(actionComponentData: ActionComponentData) {
scope.launch {
val fields = actionComponentData.details?.let { details ->
JSONObject(details.toString()).toMap()
} ?: emptyMap()

val result = centraClient.mutate(
PaymentResultMutation(paymentMethodFields = fields)
)

if (result.paymentResult?.type == "success") {
navigateToOrderConfirmation()
} else {
showError(result.paymentResult?.userErrors)
}
}
}

Return URL handling

For redirect-based 3DS, register a deep link intent filter in AndroidManifest.xml:

<activity
android:name=".CheckoutActivity"
android:launchMode="singleTop"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="yourapp"
android:host="checkout" />
</intent-filter>
</activity>

Handle the return in your Activity:

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)

intent.data?.let { uri ->
val params = uri.queryParameterNames.associateWith { name ->
uri.getQueryParameter(name).orEmpty()
}

scope.launch {
val result = centraClient.mutate(
PaymentResultMutation(paymentMethodFields = params)
)
// Handle result...
}
}
}

6. Payment completion

On success: query the placed order and navigate to your confirmation screen:

val order = centraClient.query(LatestOrderQuery())
navigateToConfirmation(order)

On failure: show the error and allow retry. The shopper can tap the Google Pay button again.

On cancellation: if you added an item for PDP express checkout, roll it back:

addedLineId?.let { lineId ->
centraClient.mutate(
UpdateLineMutation(id = lineId, quantity = 0)
)
addedLineId = null
}

Operational hints

  • Availability: always check GooglePayComponent.isAvailable() before rendering the button. Silently hide it on unsupported devices.
  • Production approval: Google Pay requires approval in the Google Pay & Wallet Console before going live. Test mode works without approval.
  • Debugging: set AdyenLogger.setLogLevel(Log.VERBOSE) during development. Monitor userErrors in every Centra response. Use adb logcat to inspect SDK logs.
  • Token persistence: store the Centra session token in EncryptedSharedPreferences. Update it from extensions.token on every API response.
  • Emulator testing: the Android emulator supports Google Pay in test mode with Adyen sandbox test cards.
  • ProGuard / R8: Adyen SDK includes its own ProGuard rules automatically — no manual configuration needed.

Validation checklist

Before going live, verify:

  • Google Pay button appears on supported devices and is hidden otherwise
  • Payment sheet shows the correct amount and line items
  • Shipping address change updates totals and available shipping methods
  • Shipping method change updates the total
  • Authorization completes and the order is placed in Centra
  • 3DS challenge can be completed (both native and redirect flows)
  • Cancellation removes any temporarily added line items
  • Confirmation screen shows correct order data from query order
  • Google Pay & Wallet Console production approval is obtained

Shipping: Ingrid is not supported natively

Centra's Ingrid integration is a hard-coded HTML widget that requires a browser DOM. There is no API-only mode for Ingrid — it cannot be rendered in a native app without embedding a web view.

The same constraints and workaround described in the iOS guide's Ingrid section apply here:

  • If Ingrid is NOT enabled: use Centra's standard shipping methods via mutation setShippingMethod. These are fully native-compatible.
  • If Ingrid IS enabled: call mutation handleWidgetEvent with { expressCheckout: true } during initialization, then use standard Centra shipping methods only. The order will be placed outside of Ingrid.
  • If Ingrid delivery options are a hard requirement: the Ingrid widget must be rendered in a WebView. This is outside the scope of this guide.

See the web express checkout guide's Ingrid section for the full handleWidgetEvent mutation and variables.