Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# PayPal Android SDK Release Notes

## Unreleased

* Migrates to kotlin serialization for all data serialization

## 2.2.0 (2025-10-01)
* CardPayments
* Migrate `CardClient.presentAuthChallenge()` method to take a plain `Activity` reference instead of a `ComponentActivity` reference
Expand Down
2 changes: 2 additions & 0 deletions CardPayments/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
alias libs.plugins.android.library
alias libs.plugins.kotlinAndroid
alias libs.plugins.kotlinParcelize
alias libs.plugins.kotlinSerialization
alias libs.plugins.binaryCompatibilityValidator
}

Expand Down Expand Up @@ -53,6 +54,7 @@ dependencies {
implementation libs.androidx.coreKtx
implementation libs.androidx.appcompat
implementation libs.kotlinx.coroutinesAndroid
implementation libs.kotlinx.serializationJson
implementation libs.braintree.browserSwitch
implementation libs.lifecycle.commonJava8
implementation libs.lifecycle.runtimeKtx
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class CardClient internal constructor(
val result = response.run {
CardApproveOrderResult.Success(
orderId = orderId,
status = status?.name
status = status.name
)
}
callback.onCardApproveOrderResult(result)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.paypal.android.cardpayments

import com.paypal.android.corepayments.PayPalSDKError
import com.paypal.android.corepayments.graphql.GraphQLError
import kotlinx.serialization.InternalSerializationApi

internal object CardError {

Expand Down Expand Up @@ -29,6 +30,7 @@ internal object CardError {
errorDescription = cause.message ?: "Unable to Browser Switch"
)

@OptIn(InternalSerializationApi::class)
fun updateSetupTokenResponseBodyMissing(errors: List<GraphQLError>?, correlationId: String?) = PayPalSDKError(
0,
"Error updating setup token: $errors",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,54 +1,72 @@
package com.paypal.android.cardpayments

import com.paypal.android.cardpayments.api.ApplicationContext
import com.paypal.android.cardpayments.api.BillingAddress
import com.paypal.android.cardpayments.api.CardAttributes
import com.paypal.android.cardpayments.api.CardPaymentSource
import com.paypal.android.cardpayments.api.ConfirmPaymentSourceRequest
import com.paypal.android.cardpayments.api.PaymentSource
import com.paypal.android.cardpayments.api.Verification
import com.paypal.android.cardpayments.api.VerificationMethod
import com.paypal.android.cardpayments.threedsecure.SCA
import com.paypal.android.corepayments.APIRequest
import com.paypal.android.corepayments.HttpMethod
import org.json.JSONObject
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

internal class CardRequestFactory {

@OptIn(InternalSerializationApi::class)
fun createConfirmPaymentSourceRequest(cardRequest: CardRequest): APIRequest {
val card = cardRequest.card
val cardNumber = card.number.replace("\\s".toRegex(), "")
val cardExpiry = "${card.expirationYear}-${card.expirationMonth}"

val cardJSON = JSONObject()
.put("number", cardNumber)
.put("expiry", cardExpiry)

card.cardholderName?.let { cardJSON.put("name", it) }
cardJSON.put("security_code", card.securityCode)

card.billingAddress?.apply {
val billingAddressJSON = JSONObject()
.put("address_line_1", streetAddress)
.put("address_line_2", extendedAddress)
.put("admin_area_1", region)
.put("admin_area_2", locality)
.put("postal_code", postalCode)
.put("country_code", countryCode)
cardJSON.put("billing_address", billingAddressJSON)
val billingAddress = card.billingAddress?.let { address ->
BillingAddress(
addressLine1 = address.streetAddress,
addressLine2 = address.extendedAddress,
adminArea2 = address.locality,
adminArea1 = address.region,
postalCode = address.postalCode,
countryCode = address.countryCode
)
}

val verification = when (cardRequest.sca) {
SCA.SCA_WHEN_REQUIRED -> Verification(method = VerificationMethod.SCA_WHEN_REQUIRED)
SCA.SCA_ALWAYS -> Verification(method = VerificationMethod.SCA_ALWAYS)
}

val bodyJSON = JSONObject()
val verificationJSON = JSONObject()
.put("method", cardRequest.sca.name)
val attributesJSON = JSONObject()
.put("verification", verificationJSON)
val cardAttributes = CardAttributes(verification = verification)

val cardPaymentSource = CardPaymentSource(
name = card.cardholderName,
number = cardNumber,
expiry = cardExpiry,
securityCode = card.securityCode,
billingAddress = billingAddress,
attributes = cardAttributes
)

cardJSON.put("attributes", attributesJSON)
val paymentSource = PaymentSource(card = cardPaymentSource)

val returnUrl = cardRequest.returnUrl
val returnURLJSON = JSONObject()
.put("return_url", returnUrl)
.put("cancel_url", returnUrl) // we can set the same url
bodyJSON.put("application_context", returnURLJSON)
val applicationContext = ApplicationContext(
returnUrl = cardRequest.returnUrl,
cancelUrl = cardRequest.returnUrl // we can set the same url
)

val paymentSourceJSON = JSONObject().put("card", cardJSON)
bodyJSON.put("payment_source", paymentSourceJSON)
val confirmPaymentSourceRequest = ConfirmPaymentSourceRequest(
paymentSource = paymentSource,
applicationContext = applicationContext
)

val body = bodyJSON.toString().replace("\\/", "/")
val body = Json.encodeToString(confirmPaymentSourceRequest)

val path = "v2/checkout/orders/${cardRequest.orderId}/confirm-payment-source"
return APIRequest(path, HttpMethod.POST, body)
return APIRequest(
path = "v2/checkout/orders/${cardRequest.orderId}/confirm-payment-source",
method = HttpMethod.POST,
body = body
)
}
}
Original file line number Diff line number Diff line change
@@ -1,79 +1,79 @@
package com.paypal.android.cardpayments

import com.paypal.android.cardpayments.api.ConfirmPaymentSourceResult
import com.paypal.android.cardpayments.model.PaymentSource
import com.paypal.android.cardpayments.model.PurchaseUnit
import com.paypal.android.cardpayments.model.ConfirmPaymentSourceResponse
import com.paypal.android.cardpayments.model.ErrorResponse
import com.paypal.android.corepayments.APIClientError
import com.paypal.android.corepayments.HttpResponse
import com.paypal.android.corepayments.OrderErrorDetail
import com.paypal.android.corepayments.OrderStatus
import com.paypal.android.corepayments.PayPalSDKError
import com.paypal.android.corepayments.PaymentsJSON
import org.json.JSONException
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json

internal class CardResponseParser {

fun parseConfirmPaymentSourceResponse(httpResponse: HttpResponse): ConfirmPaymentSourceResult =
try {
val bodyResponse = httpResponse.body!!

val json = PaymentsJSON(bodyResponse)
val status = json.getString("status")
val id = json.getString("id")

// this section is for 3DS
val payerActionHref = json.getLinkHref("payer-action")
ConfirmPaymentSourceResult.Success(
id,
OrderStatus.valueOf(status),
payerActionHref,
json.optMapObject("payment_source.card") { PaymentSource(it) },
json.optMapObjectArray("purchase_units") { PurchaseUnit(it) }
)
} catch (ignored: JSONException) {
val correlationId = httpResponse.headers["Paypal-Debug-Id"]
val error = APIClientError.dataParsingError(correlationId)
ConfirmPaymentSourceResult.Failure(error)
}

fun parseError(httpResponse: HttpResponse): PayPalSDKError? {
val result: PayPalSDKError?
if (httpResponse.isSuccessful) {
result = null
} else {
private val json = Json {
ignoreUnknownKeys = true
coerceInputValues = true
}

val correlationId = httpResponse.headers["Paypal-Debug-Id"]
val bodyResponse = httpResponse.body
if (bodyResponse.isNullOrBlank()) {
result = APIClientError.noResponseData(correlationId)
} else {
result = when (val status = httpResponse.status) {
HttpResponse.STATUS_UNKNOWN_HOST -> {
APIClientError.unknownHost(correlationId)
}
HttpResponse.STATUS_UNDETERMINED -> {
APIClientError.unknownError(correlationId)
}
HttpResponse.SERVER_ERROR -> {
APIClientError.serverResponseError(correlationId)
}
else -> {
val json = PaymentsJSON(bodyResponse)
val message = json.getString("message")
@OptIn(InternalSerializationApi::class)
fun parseConfirmPaymentSourceResponse(httpResponse: HttpResponse): ConfirmPaymentSourceResult {
val correlationId = httpResponse.headers["Paypal-Debug-Id"]
val error = parseError(httpResponse)
val responseBody = httpResponse.body?.takeIf { it.isNotBlank() }
?: return ConfirmPaymentSourceResult.Failure(APIClientError.noResponseData(correlationId))

val errorDetails = json.optMapObjectArray("details") {
val issue = it.getString("issue")
val description = it.getString("description")
OrderErrorDetail(issue, description)
}
return when {
error != null -> ConfirmPaymentSourceResult.Failure(error)

val description = "$message -> $errorDetails"
APIClientError.httpURLConnectionError(status, description, correlationId)
}
else -> {
try {
val response =
json.decodeFromString<ConfirmPaymentSourceResponse>(responseBody)
ConfirmPaymentSourceResult.Success(
orderId = response.id,
status = response.status,
payerActionHref = response.links?.firstOrNull { it.rel == "payer-action" }?.href,
paymentSource = response.paymentSource?.card,
purchaseUnits = response.purchaseUnits ?: emptyList()
)
} catch (_: SerializationException) {
ConfirmPaymentSourceResult.Failure(APIClientError.dataParsingError(correlationId))
}
}
}
}

fun parseError(httpResponse: HttpResponse): PayPalSDKError? {
val correlationId = httpResponse.headers["Paypal-Debug-Id"]
val bodyResponse = httpResponse.body
val status = httpResponse.status

return result
return when {
httpResponse.isSuccessful -> null
bodyResponse.isNullOrBlank() -> APIClientError.noResponseData(correlationId)
status == HttpResponse.STATUS_UNKNOWN_HOST -> APIClientError.unknownHost(correlationId)
status == HttpResponse.STATUS_UNDETERMINED -> APIClientError.unknownError(correlationId)
status == HttpResponse.SERVER_ERROR -> APIClientError.serverResponseError(correlationId)
else -> parseDetailedError(bodyResponse, status, correlationId)
}
}

@OptIn(InternalSerializationApi::class)
private fun parseDetailedError(
bodyResponse: String,
status: Int,
correlationId: String?
): PayPalSDKError =
try {
val errorResponse = json.decodeFromString<ErrorResponse>(bodyResponse)
val errorDetails =
errorResponse.details?.map { OrderErrorDetail(it.issue, it.description) }
val description = "${errorResponse.message} -> $errorDetails"
APIClientError.httpURLConnectionError(status, description, correlationId)
} catch (_: SerializationException) {
APIClientError.dataParsingError(correlationId)
}
}
Loading