Display an order review page before capturing payment.
The continue flow is a two-step checkout experience that allows customers to review their order details after PayPal approval but before final payment submission. This flow is ideal for e-commerce checkouts requiring shipping confirmation, complex orders needing customer review, or compliance requirements mandating order confirmation.
To enable the continue flow, you must set userAction = UserAction.Continue in the component configuration. Without this setting, the button defaults to the pay now flow.
The PayPal continue flow consists of six key steps with approval and capture separation.
The customer taps the PayPal button, which triggers the payment flow. The SDK validates the PayPal configuration and transaction data before proceeding to order creation. If validation fails, onError is triggered.
The SDK creates a PayPal order using the provided transaction details. This involves sending the payment amount, currency, merchant information, shipping options, and any additional order details to PayPal's API. If order creation fails, onError is triggered.
The customer is redirected to PayPal checkout where they can log in, select their payment method, and optionally choose a shipping address.
When the customer taps the PayPal button (with userAction set to Continue), the PayPal authentication flow opens and the customer enters their credentials.
After login, the customer is requested to review the order. The text shown in the PayPal user interface may vary; sometimes it's displayed as "Continue to Review Order".
This step has five associated callbacks:
onApprove: Retrieves order details and navigates to the review page if the customer successfully approves the order.onCancel: Cancels the transaction if the customer cancels in PayPal.onError: Receives error data if any error occurs during the approval process.onShippingAddressChange: Validates shipping addresses as the customer changes them in PayPal.onShippingOptionsChange: Handles shipping option changes made by the customer.
PayPal handles all authentication and payment method selection within their secure environment.
After PayPal approval, the customer is returned to your app with the approved order. The customer can review their order details including contact information, shipping address, and order summary before final submission.
On the review screen, the customer can see:
- Contact information: Name, email, and phone number from PayPal
- Shipping address: Full delivery address selected in PayPal
- Order summary: Itemised breakdown of subtotal, shipping, tax, and total
The customer can either submit the order to complete their payment or cancel and return to the checkout screen.
Once the customer confirms their order on the review screen, you capture the payment. This is when the funds are actually transferred from the customer's account.
Your capture API call will return a success or failure response, which you handle in your backend code.
To use PayPal continue flow in your Android application:
- Ensure you have a valid PayPal Business account with API credentials.
- Configure your PayPal merchant account to accept the currencies you need.
- Set up your merchant configuration in the Unity Portal (API credentials, payment methods, risk settings).
- Implement a review screen to display order details.
- Create a backend endpoint to capture payments after customer confirmation.
Set up your PxpCheckout instance with transaction information for PayPal payments.
val transactionData = TransactionData(
amount = 2500.0,
currency = "USD",
merchant = "your-merchant-id",
entryType = EntryType.Ecom,
intent = TransactionIntentData(
paypal = IntentType.Authorisation
),
merchantTransactionId = "order-${System.currentTimeMillis()}",
merchantTransactionDate = { Clock.System.now().toString() },
cardAcceptorName = "Your Store Name"
)
val sdkConfig = PxpSdkConfig(
environment = Environment.TEST, // or Environment.LIVE for production
session = SessionConfig(
sessionId = "your-session-id",
hmacKey = "your-hmac-key",
data = "your-session-data",
encryptionKey = "your-encryption-key",
locale = "en-US"
),
transactionData = transactionData,
clientId = "your-client-id",
ownerId = "your-owner-id",
ownerType = "MerchantGroup",
onGetShopper = {
// Return current shopper data dynamically
Shopper(
id = "shopper-123"
)
},
onGetShippingAddress = {
// Return current shipping address when needed
ShippingAddress(
addressLine1 = "123 Main Street",
addressLine2 = "Apt 1",
city = "New York",
county = "NY",
postalCode = "10001",
countryCode = "US"
)
}
)
val pxpCheckout = PxpCheckout.builder()
.withConfig(sdkConfig)
.withContext(context)
.build()Create the PayPal button with the continue flow configuration. The userAction = UserAction.Continue setting is required to enable the continue flow.
Android PayPal callbacks receive JSON strings, not typed objects. You'll need to parse the JSON data in your callbacks. The onSuccess callback receives a JSON string containing the order approval data.
import com.pxp.checkout.components.paypal.types.UserAction
import com.pxp.checkout.components.paypal.types.ShippingPreference
import org.json.JSONObject
val paypalConfig = PayPalComponentConfig().apply {
// Required PayPal configuration
renderType = "setOfButtons"
payeeEmailAddress = "merchant@example.com"
paymentDescription = "Order Payment"
// REQUIRED: Enable continue flow (two-step checkout)
userAction = UserAction.Continue.toString()
// Shipping configuration
shippingPreference = ShippingPreference.GetFromFile.toString()
shippingOptions = listOf(
ShippingOption(
id = "1",
label = "Standard Shipping",
selected = true,
amounts = ShippingAmounts(
currencyCode = "USD",
shipping = 10.00
),
type = "Shipping"
),
ShippingOption(
id = "2",
label = "Express Shipping",
selected = false,
amounts = ShippingAmounts(
currencyCode = "USD",
shipping = 15.00
),
type = "Shipping"
)
)
// Display options
locale = "en-US"
fundingSources = listOf("paypal")
// Styling
style = PayPalButtonStyle(
layout = "vertical",
color = "gold",
shape = "rect",
label = "paypal",
tagline = false,
height = 55
)
// REQUIRED: Handle order approval
onSuccess = { dataJson ->
Log.d("PayPal", "PayPal order approved: $dataJson")
try {
// Parse the JSON response and navigate to review screen
val orderData = parsePayPalResponse(dataJson)
navigateToReviewScreen(orderData)
} catch (e: Exception) {
Log.e("PayPal", "Error parsing order details", e)
showError("Unable to retrieve order details. Please try again.")
}
}
// REQUIRED: Handle payment errors
onError = { error ->
Log.e("PayPal", "PayPal payment error", error)
showError("Payment failed. Please try again.")
}
// OPTIONAL: Handle payment cancellation
onCancel = {
Log.d("PayPal", "PayPal payment cancelled")
showMessage("Payment was cancelled. You can try again anytime.")
}
// OPTIONAL: Validate shipping address changes
onShippingAddressChange = { dataJson ->
Log.d("PayPal", "Shipping address changed: $dataJson")
try {
// Parse the JSON data to check country code
val jsonObject = JSONObject(dataJson)
val shippingAddress = jsonObject.optJSONObject("shippingAddress")
if (shippingAddress != null) {
val countryCode = shippingAddress.optString("countryCode", "")
// Validate country
if (countryCode != "US") {
// Return reject action with error code
return@onShippingAddressChange "reject:COUNTRY_ERROR"
}
}
} catch (e: Exception) {
Log.e("PayPal", "Error parsing shipping address", e)
}
// Return null to allow the change
null
}
// OPTIONAL: Handle shipping option changes
onShippingOptionsChange = { dataJson ->
Log.d("PayPal", "Shipping option changed: $dataJson")
null
}
}
// Create the component
val paypalComponent = pxpCheckout.createComponent<PayPalComponent, PayPalComponentConfig>(
type = ComponentType.PAYPAL,
config = paypalConfig
)Create a review screen composable to display order details after PayPal approval.
@Composable
fun ReviewOrderScreen(
orderData: OrderData,
onSubmit: (OrderData) -> Unit,
onCancel: () -> Unit
) {
val payer = orderData.payer
val purchaseUnit = orderData.purchaseUnits.first()
val shipping = purchaseUnit.shipping
val amount = purchaseUnit.amount
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
Text(
text = "Review your order",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
Text(
text = "Please review your order details before completing the purchase",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 24.dp)
)
// Contact information
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Contact information",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
Text("Name: ${payer.name.givenName} ${payer.name.surname}")
Text("Email: ${payer.emailAddress}")
payer.phone?.phoneNumber?.nationalNumber?.let {
Text("Phone: $it")
}
}
}
// Shipping address
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Delivering to",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
Text(
text = shipping.name.fullName,
fontWeight = FontWeight.Bold
)
Text(shipping.address.addressLine1)
shipping.address.addressLine2?.let { Text(it) }
Text("${shipping.address.adminArea2}, ${shipping.address.adminArea1} ${shipping.address.postalCode}")
Text(shipping.address.countryCode)
}
}
// Order summary
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Order summary",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Subtotal:")
Text("${amount.currencyCode} ${amount.breakdown.itemTotal.value}")
}
amount.breakdown.shipping?.value?.let {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Delivery:")
Text("${amount.currencyCode} $it")
}
}
amount.breakdown.taxTotal?.value?.let {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Tax:")
Text("${amount.currencyCode} $it")
}
}
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Total:",
fontWeight = FontWeight.Bold
)
Text(
text = "${amount.currencyCode} ${amount.value}",
fontWeight = FontWeight.Bold
)
}
}
}
// Actions
Text(
text = "Order ID: ${orderData.orderId}",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(bottom = 16.dp)
)
Button(
onClick = { onSubmit(orderData) },
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp)
) {
Text("Submit Order & Pay Now")
}
OutlinedButton(
onClick = onCancel,
modifier = Modifier.fillMaxWidth()
) {
Text("Cancel and go back")
}
}
}Implement the backend capture logic when the customer submits the order from the review screen.
// In your ViewModel or Repository
suspend fun handleSubmitOrder(orderData: OrderData): Result<CaptureResponse> {
return withContext(Dispatchers.IO) {
try {
// Call backend to capture the PayPal order
val response = apiService.completeOrder(
orderId = orderData.orderId,
sessionId = sessionData.sessionId,
merchantTransactionId = transactionData.merchantTransactionId
)
if (response.isSuccessful) {
Log.d("PayPal", "Payment captured successfully")
Result.success(response.body()!!)
} else {
Result.failure(Exception(response.errorBody()?.string() ?: "Payment capture failed"))
}
} catch (e: Exception) {
Log.e("PayPal", "Order submission error", e)
Result.failure(e)
}
}
}
// Backend endpoint (example using Ktor)
suspend fun completePaypalOrder(call: ApplicationCall) {
val request = call.receive<CompleteOrderRequest>()
try {
// Call the PXP API to capture the PayPal order
val response = pxpApiClient.post("api/v1/paypal/orders/${request.orderId}/capture") {
contentType(ContentType.Application.Json)
setBody(
mapOf(
"sessionId" to request.sessionId,
"merchantTransactionId" to request.merchantTransactionId
)
)
}
call.respond(
HttpStatusCode.OK,
mapOf(
"success" to true,
"data" to response.body<Map<String, Any>>(),
"systemTransactionId" to response.body<Map<String, Any>>()["systemTransactionId"],
"providerTransactionId" to response.body<Map<String, Any>>()["providerTransactionId"]
)
)
} catch (e: Exception) {
call.respond(
HttpStatusCode.InternalServerError,
mapOf(
"success" to false,
"error" to "Failed to complete order",
"message" to e.message
)
)
}
}Validate shipping addresses as customers change them in PayPal.
val paypalConfig = PayPalComponentConfig().apply {
userAction = UserAction.Continue.toString()
onShippingAddressChange = { dataJson ->
Log.d("PayPal", "Shipping address changed: $dataJson")
try {
// Parse the JSON data to check shipping address
val jsonObject = JSONObject(dataJson)
val shippingAddress = jsonObject.optJSONObject("shippingAddress")
if (shippingAddress != null) {
val countryCode = shippingAddress.optString("countryCode", "")
val state = shippingAddress.optString("state", "")
val postalCode = shippingAddress.optString("postalCode", "")
// Validate country
val allowedCountries = listOf("US", "CA")
if (!allowedCountries.contains(countryCode)) {
return@onShippingAddressChange "reject:COUNTRY_ERROR"
}
// Validate state for US addresses
if (countryCode == "US") {
val restrictedStates = listOf("AK", "HI") // Alaska, Hawaii
if (restrictedStates.contains(state)) {
return@onShippingAddressChange "reject:STATE_ERROR"
}
}
// Validate postal code format
if (countryCode == "US") {
val postalCodeRegex = Regex("""^\d{5}(-\d{4})?$""")
if (!postalCodeRegex.matches(postalCode)) {
return@onShippingAddressChange "reject:POSTAL_CODE_ERROR"
}
}
}
} catch (e: Exception) {
Log.e("PayPal", "Error parsing shipping address", e)
}
// Return null to allow the change
null
}
}Handle when customers cancel from the review screen.
fun handleCancelReview(navController: NavController) {
Log.d("PayPal", "Customer cancelled from review screen")
// Log analytics event
analytics.track("review_page_cancelled", mapOf(
"order_id" to orderData.orderId,
"stage" to "review"
))
// Clear order data
viewModel.clearOrderData()
// Return to checkout screen
navController.popBackStack()
// Show message
Toast.makeText(context, "Order cancelled. You can start a new checkout anytime.", Toast.LENGTH_SHORT).show()
}Implement comprehensive error handling for PayPal continue flow.
val paypalConfig = PayPalComponentConfig().apply {
userAction = UserAction.Continue.toString()
onError = { errorJson ->
Log.e("PayPal", "PayPal error: $errorJson")
// Parse error from JSON string
val error = parsePayPalError(errorJson)
// Handle specific PayPal error types
val errorMessage = when (error.name) {
"VALIDATION_ERROR" -> "Payment details validation failed. Please try again."
"INSTRUMENT_DECLINED" -> "Your PayPal payment method was declined. Please try a different method."
"PAYER_ACTION_REQUIRED" -> "Additional action required in PayPal. Please complete the payment process."
"UNPROCESSABLE_ENTITY" -> "Payment cannot be processed. Please contact support."
else -> "Payment failed. Please try again or contact support."
}
showError(errorMessage)
}
onSuccess = { dataJson ->
try {
val orderData = parsePayPalResponse(dataJson)
navigateToReviewScreen(orderData)
} catch (e: Exception) {
Log.e("PayPal", "Failed to retrieve order details", e)
val errorMessage = when {
e is HttpException && e.code() == 404 -> "Order not found. Please start a new checkout."
e is HttpException && e.code() == 422 -> "Order details could not be retrieved. Please try again."
e is HttpException && e.code() >= 500 -> "PayPal service temporarily unavailable. Please try again later."
else -> "Unable to retrieve order details. Please try again."
}
showError(errorMessage)
}
}
}
// Handle order submission errors
suspend fun handleSubmitOrder(orderData: OrderData) {
try {
val result = capturePayment(orderData)
// Handle success
} catch (e: Exception) {
Log.e("PayPal", "Payment capture error", e)
val errorMessage = when {
e is HttpException && e.code() == 422 -> "Payment details could not be verified. Please try again."
e is HttpException && e.code() == 409 -> "This payment has already been processed."
e is HttpException && e.code() >= 500 -> "Payment system temporarily unavailable. Please try again later."
else -> "Payment processing failed. Please try again."
}
showError(errorMessage)
}
}Unity sends PayPal transaction approved webhooks when PayPal orders are approved, providing complete payer details and shipping information.
{
"eventCategory": "transaction",
"eventDate": "2026-03-17T09:50:05.3525590Z",
"eventData": {
"state": "Approved",
"amounts": {
"transaction": 10,
"currencyCode": "USD"
},
"merchant": "merchant-1",
"site": "site-1",
"transactionMethod": {
"intent": "Create",
"fundingType": "Paypal",
"entryType": "Ecom"
},
"shippingAddress": {
"addressLine1": "123 Main St",
"postalCode": "95131",
"countryCode": "US",
"city": "San Francisco",
"county": "CA"
},
"merchantTransactionDate": "2026-03-06T02:53:12",
"merchantTransactionId": "99ECC73F-A9A9-44E2-9821-4F4797B3145D",
"systemTransactionId": "c3898060-431d-4829-bcde-049edcee70ae",
"providerTransactionId": "0DC51351HB1980105",
"fundingData": {
"paypal": {
"payeeEmailAddress": "merchant@example.com",
"paymentSource": "Paypal"
}
},
"payerDetails": {
"emailAddress": "john.doe@example.com",
"givenName": "John",
"surName": "Doe",
"phoneNumber": "4087098069"
},
"eventCode": "TXNPP_APP"
},
"eventOwner": {
"merchantGroup": "MerchantGroup",
"merchant": "merchant-1",
"site": "site-1"
}
}| Parameter | Description |
|---|---|
eventCategorystring | The category of the webhook event (e.g., "transaction"). |
eventDatestring | The date and time when the event occurred, in ISO 8601 format. |
eventCodestring | The event code. TXNPP_APP indicates PayPal order approved. |
payerDetails.emailAddressstring | The customer's email address from PayPal. |
payerDetails.givenNamestring | The customer's first name from PayPal. |
payerDetails.surNamestring | The customer's last name from PayPal. |
payerDetails.phoneNumberstring | The customer's phone number from PayPal (if provided). |
shippingAddressobject | Complete shipping address if customer selected one in PayPal. |
systemTransactionIdstring | Unity's internal transaction ID. |
providerTransactionIdstring | PayPal's transaction ID. |
The payer details (email, name, phone) are also displayed in the Unity Portal's reporting system for PayPal transaction details.