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
- API level 21+, Kotlin 1.8+
- Google Pay enabled in the Google Pay & Wallet Console
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 setup | query selection + optional mutation addItem |
onPaymentDataChanged(SHIPPING_ADDRESS) | onShippingAddressChanged() | mutation paymentInstructions(paymentInitiateOnly: true) |
onPaymentDataChanged(SHIPPING_OPTION) | onShippingOptionChanged() | mutation setShippingMethod |
onAuthorized | Payment data in result | Validate addresses locally |
onSubmit | onSubmit() callback | mutation 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. MonitoruserErrorsin every Centra response. Useadb logcatto inspect SDK logs. - Token persistence: store the Centra session token in
EncryptedSharedPreferences. Update it fromextensions.tokenon 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 handleWidgetEventwith{ 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.