Skip to content
Open
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
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
# PayPal Android SDK Release Notes

## 2.3.0 (2025-11-03)
## Unreleased

* Add overloaded functions with callbacks for `start` and `vault` methods in
`PayPalWebCheckoutClient`
Comment on lines +5 to +6
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this CHANGELOG entry a duplicate? I see a similar one listed in version 2.3.0 for "Add PayPalWebCheckoutClient.start(activity, request, callback) method with asynchronous callback support."

* Modify `PayPalWebCheckoutRequest` to include `appSwitchWhenEligible` property to control app
switch behavior
* Breaking Changes
* Make start and vault functions in `PayPalWebCheckoutClient` suspend functions

## 2.3.0 (2025-11-03)
* PayPalWebPayments
* Add `PayPalWebCheckoutClient.start(activity, request, callback)` method with asynchronous
callback support
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ internal class DataVaultPaymentMethodTokensAPI internal constructor(
)

val graphQLRequest = GraphQLRequest(query, variables, "UpdateVaultSetupToken")
val graphQLResponse =
graphQLClient.send<UpdateSetupTokenResponse, UpdateSetupTokenVariables>(graphQLRequest)
val graphQLResponse = graphQLClient.send<
UpdateSetupTokenResponse,
UpdateSetupTokenVariables>(graphQLRequest)
return when (graphQLResponse) {
is GraphQLResult.Success -> {
val response = graphQLResponse.response
Expand Down
118 changes: 118 additions & 0 deletions CorePayments/api/CorePayments.api
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@ public final class com/paypal/android/corepayments/UpdateClientConfigVariables$C
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class com/paypal/android/corepayments/api/PatchCCOWithAppSwitchEligibility$Companion {
}

public final class com/paypal/android/corepayments/common/DeviceInspector$Companion {
}

public final class com/paypal/android/corepayments/graphql/GraphQLClient$Companion {
}

Expand Down Expand Up @@ -286,3 +292,115 @@ public final class com/paypal/android/corepayments/graphql/GraphQLResult$Success
public fun toString ()Ljava/lang/String;
}

public final class com/paypal/android/corepayments/model/APIResult$Failure : com/paypal/android/corepayments/model/APIResult {
public fun <init> (Lcom/paypal/android/corepayments/PayPalSDKError;)V
public final fun component1 ()Lcom/paypal/android/corepayments/PayPalSDKError;
public final fun copy (Lcom/paypal/android/corepayments/PayPalSDKError;)Lcom/paypal/android/corepayments/model/APIResult$Failure;
public static synthetic fun copy$default (Lcom/paypal/android/corepayments/model/APIResult$Failure;Lcom/paypal/android/corepayments/PayPalSDKError;ILjava/lang/Object;)Lcom/paypal/android/corepayments/model/APIResult$Failure;
public fun equals (Ljava/lang/Object;)Z
public final fun getError ()Lcom/paypal/android/corepayments/PayPalSDKError;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/paypal/android/corepayments/model/APIResult$Success : com/paypal/android/corepayments/model/APIResult {
public fun <init> (Ljava/lang/Object;)V
public final fun component1 ()Ljava/lang/Object;
public final fun copy (Ljava/lang/Object;)Lcom/paypal/android/corepayments/model/APIResult$Success;
public static synthetic fun copy$default (Lcom/paypal/android/corepayments/model/APIResult$Success;Ljava/lang/Object;ILjava/lang/Object;)Lcom/paypal/android/corepayments/model/APIResult$Success;
public fun equals (Ljava/lang/Object;)Z
public final fun getData ()Ljava/lang/Object;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/paypal/android/corepayments/model/AppSwitchEligibilityData$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lcom/paypal/android/corepayments/model/AppSwitchEligibilityData$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/paypal/android/corepayments/model/AppSwitchEligibilityData;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/paypal/android/corepayments/model/AppSwitchEligibilityData;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}

public final class com/paypal/android/corepayments/model/AppSwitchEligibilityData$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class com/paypal/android/corepayments/model/ExperimentationContext$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lcom/paypal/android/corepayments/model/ExperimentationContext$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/paypal/android/corepayments/model/ExperimentationContext;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/paypal/android/corepayments/model/ExperimentationContext;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}

public final class com/paypal/android/corepayments/model/ExperimentationContext$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class com/paypal/android/corepayments/model/ExternalData$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lcom/paypal/android/corepayments/model/ExternalData$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/paypal/android/corepayments/model/ExternalData;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/paypal/android/corepayments/model/ExternalData;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}

public final class com/paypal/android/corepayments/model/ExternalData$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class com/paypal/android/corepayments/model/PatchCcoData$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lcom/paypal/android/corepayments/model/PatchCcoData$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/paypal/android/corepayments/model/PatchCcoData;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/paypal/android/corepayments/model/PatchCcoData;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}

public final class com/paypal/android/corepayments/model/PatchCcoData$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class com/paypal/android/corepayments/model/PatchCcoWithAppSwitchEligibilityResponse$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lcom/paypal/android/corepayments/model/PatchCcoWithAppSwitchEligibilityResponse$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/paypal/android/corepayments/model/PatchCcoWithAppSwitchEligibilityResponse;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/paypal/android/corepayments/model/PatchCcoWithAppSwitchEligibilityResponse;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}

public final class com/paypal/android/corepayments/model/PatchCcoWithAppSwitchEligibilityResponse$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class com/paypal/android/corepayments/model/PatchCcoWithAppSwitchEligibilityVariables$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lcom/paypal/android/corepayments/model/PatchCcoWithAppSwitchEligibilityVariables$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/paypal/android/corepayments/model/PatchCcoWithAppSwitchEligibilityVariables;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/paypal/android/corepayments/model/PatchCcoWithAppSwitchEligibilityVariables;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}

public final class com/paypal/android/corepayments/model/PatchCcoWithAppSwitchEligibilityVariables$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

4 changes: 4 additions & 0 deletions CorePayments/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<queries>
<package android:name="com.paypal.android.p2pmobile" />
</queries>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import androidx.annotation.RestrictTo
object APIClientError {

// 0. An unknown error occurred.
fun unknownError(correlationId: String? = null) = PayPalSDKError(
fun unknownError(correlationId: String? = null, throwable: Throwable? = null) = PayPalSDKError(
code = PayPalSDKErrorCode.UNKNOWN.ordinal,
errorDescription = "An unknown error occurred. Contact developer.paypal.com/support.",
reason = throwable,
correlationId = correlationId
)

Expand Down Expand Up @@ -91,4 +92,9 @@ object APIClientError {
)
return error
}

fun graphQLRequestLoadError() = PayPalSDKError(
code = PayPalSDKErrorCode.GRAPHQL_CREATE_REQUEST_ERROR.ordinal,
errorDescription = "Error creating GraphQL request"
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ import androidx.annotation.RestrictTo
* @suppress
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
data class APIRequest(val path: String, val method: HttpMethod, val body: String? = null)
data class APIRequest(
val path: String,
val method: HttpMethod,
val body: String? = null,
val headers: Map<String, String>? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ enum class PayPalSDKErrorCode {
SERVER_RESPONSE_ERROR,
CHECKOUT_ERROR,
NATIVE_CHECKOUT_ERROR,
GRAPHQL_JSON_INVALID_ERROR
GRAPHQL_JSON_INVALID_ERROR,
NO_ACCESS_TOKEN_ERROR,
GRAPHQL_CREATE_REQUEST_ERROR,
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ class UpdateClientConfigAPI(

@Serializable
@OptIn(InternalSerializationApi::class)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
data class UpdateClientConfigVariables(
val token: String,
val fundingSource: String,
Expand All @@ -107,10 +108,12 @@ data class UpdateClientConfigVariables(

@Serializable
@OptIn(InternalSerializationApi::class)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
data class UpdateClientConfigResponse(
val updateClientConfig: String
)

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
sealed class UpdateClientConfigResult {
data object Success : UpdateClientConfigResult()
data class Failure(val error: PayPalSDKError) : UpdateClientConfigResult()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.paypal.android.corepayments.api

import android.content.Context
import androidx.annotation.RestrictTo
import com.paypal.android.corepayments.APIClientError
import com.paypal.android.corepayments.CoreConfig
import com.paypal.android.corepayments.LoadRawResourceResult
import com.paypal.android.corepayments.R
import com.paypal.android.corepayments.ResourceLoader
import com.paypal.android.corepayments.UpdateClientConfigAPI
import com.paypal.android.corepayments.graphql.GraphQLClient
import com.paypal.android.corepayments.graphql.GraphQLRequest
import com.paypal.android.corepayments.graphql.GraphQLResult
import com.paypal.android.corepayments.model.APIResult
import com.paypal.android.corepayments.model.AppSwitchEligibility
import com.paypal.android.corepayments.model.ExperimentationContext
import com.paypal.android.corepayments.model.PatchCcoWithAppSwitchEligibilityResponse
import com.paypal.android.corepayments.model.PatchCcoWithAppSwitchEligibilityVariables
import com.paypal.android.corepayments.model.TokenType
import kotlinx.serialization.InternalSerializationApi

@OptIn(InternalSerializationApi::class)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class PatchCCOWithAppSwitchEligibility internal constructor(
private val graphQLClient: GraphQLClient,
private val resourceLoader: ResourceLoader,
) {

constructor(coreConfig: CoreConfig) : this(
graphQLClient = GraphQLClient(coreConfig),
resourceLoader = ResourceLoader()
)

suspend operator fun invoke(
context: Context,
orderId: String,
tokenType: TokenType,
merchantOptInForAppSwitch: Boolean,
paypalNativeAppInstalled: Boolean
): APIResult<AppSwitchEligibility> {

val graphQLRequest = createGraphQLRequest(
context = context,
tokenType = tokenType,
orderId = orderId,
merchantOptInForAppSwitch = merchantOptInForAppSwitch,
paypalNativeAppInstalled = paypalNativeAppInstalled
) ?: return APIResult.Failure(
APIClientError.dataParsingError(correlationId = null)
)

val graphQLResult = graphQLClient.send<
PatchCcoWithAppSwitchEligibilityResponse,
PatchCcoWithAppSwitchEligibilityVariables>(graphQLRequest)
return when (graphQLResult) {
is GraphQLResult.Success -> {
graphQLResult.response.data?.let { responseData ->
parseResponse(responseData)?.let { appSwitchEligibility ->
APIResult.Success(data = appSwitchEligibility)
} ?: APIResult.Failure(
APIClientError.dataParsingError(graphQLResult.correlationId)
)
} ?: APIResult.Failure(
APIClientError.noResponseData(graphQLResult.correlationId)
)
}

is GraphQLResult.Failure -> APIResult.Failure(graphQLResult.error)
}
}

private suspend fun createGraphQLRequest(
context: Context,
tokenType: TokenType,
orderId: String,
merchantOptInForAppSwitch: Boolean,
paypalNativeAppInstalled: Boolean
): GraphQLRequest<PatchCcoWithAppSwitchEligibilityVariables>? {
val resourceResult = resourceLoader.loadRawResource(
context,
R.raw.graphql_query_patch_cco_app_switch_eligibility
)

val query = when (resourceResult) {
is LoadRawResourceResult.Success -> resourceResult.value
is LoadRawResourceResult.Failure -> return null
}

val variables = PatchCcoWithAppSwitchEligibilityVariables(
tokenType = tokenType.name,
contextId = orderId,
token = orderId,
merchantOptInForAppSwitch = merchantOptInForAppSwitch,
experimentationContext = ExperimentationContext(
integrationChannel = INTEGRATION_CHANNEL,
),
integrationArtifact = UpdateClientConfigAPI.Defaults.INTEGRATION_ARTIFACT,
userExperienceFlow = UpdateClientConfigAPI.Defaults.USER_EXPERIENCE_FLOW,
osType = OS_TYPE,
paypalNativeAppInstalled = paypalNativeAppInstalled
)

return GraphQLRequest(
query = query,
variables = variables,
operationName = "PatchCcoWithAppSwitchEligibility"
)
}

private fun parseResponse(response: PatchCcoWithAppSwitchEligibilityResponse): AppSwitchEligibility? {
val appSwitchEligibilityData = response.external
?.patchCcoWithAppSwitchEligibility
?.appSwitchEligibility

return appSwitchEligibilityData?.let {
AppSwitchEligibility(
appSwitchEligible = it.appSwitchEligible,
launchUrl = it.redirectURL,
ineligibleReason = it.ineligibleReason
)
}
}

companion object {
const val INTEGRATION_CHANNEL = "PPCP_NATIVE_SDK"
const val OS_TYPE = "ANDROID"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.paypal.android.corepayments.common

import android.content.Context
import androidx.annotation.RestrictTo

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class DeviceInspector(private val context: Context) {

val isPayPalInstalled: Boolean
get() = isAppInstalled(PAYPAL_APP_PACKAGE)

private fun isAppInstalled(packageName: String): Boolean = runCatching {
context.packageManager.getApplicationInfo(packageName, 0)
true
}.getOrDefault(false)

companion object {
const val PAYPAL_APP_PACKAGE = "com.paypal.android.p2pmobile"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.paypal.android.corepayments.common

internal object Headers {
const val PAYPAL_DEBUG_ID = "paypal-debug-id"
const val AUTHORIZATION = "Authorization"
const val CONTENT_TYPE = "Content-Type"
}
Loading
Loading