From 90d88424e4c73b399ab216364e90a2e6752e5660 Mon Sep 17 00:00:00 2001 From: sshropshire Date: Fri, 13 Feb 2026 15:08:39 -0600 Subject: [PATCH 1/4] Create Venmo feature in demo app. Implement initial create order UI for PayWithVenmo. Wire up launch venmo button. Stub out VenmoClient. Clean up. --- .../java/com/paypal/android/ui/DemoApp.kt | 4 + .../paypal/android/ui/DemoAppDestinations.kt | 2 + .../com/paypal/android/ui/features/Feature.kt | 1 + .../android/ui/features/FeaturesView.kt | 8 ++ .../android/ui/venmo/PayWithVenmoUiState.kt | 13 +++ .../android/ui/venmo/PayWithVenmoView.kt | 102 ++++++++++++++++++ .../android/ui/venmo/PayWithVenmoViewModel.kt | 75 +++++++++++++ Demo/src/main/res/values/strings.xml | 3 + Venmo/build.gradle | 3 + .../com/paypal/android/venmo/VenmoClient.kt | 56 ++++++++++ 10 files changed, 267 insertions(+) create mode 100644 Demo/src/main/java/com/paypal/android/ui/venmo/PayWithVenmoUiState.kt create mode 100644 Demo/src/main/java/com/paypal/android/ui/venmo/PayWithVenmoView.kt create mode 100644 Demo/src/main/java/com/paypal/android/ui/venmo/PayWithVenmoViewModel.kt create mode 100644 Venmo/src/main/java/com/paypal/android/venmo/VenmoClient.kt diff --git a/Demo/src/main/java/com/paypal/android/ui/DemoApp.kt b/Demo/src/main/java/com/paypal/android/ui/DemoApp.kt index 952cb4301..5819fa789 100644 --- a/Demo/src/main/java/com/paypal/android/ui/DemoApp.kt +++ b/Demo/src/main/java/com/paypal/android/ui/DemoApp.kt @@ -32,6 +32,7 @@ import com.paypal.android.ui.paypalwebvault.PayPalVaultView import com.paypal.android.ui.selectcard.SelectCardView import com.paypal.android.ui.vaultcard.VaultCardView import com.paypal.android.ui.vaultcard.VaultCardViewModel +import com.paypal.android.ui.venmo.PayWithVenmoView import com.paypal.android.uishared.components.DemoAppTopBar import com.paypal.android.uishared.effects.NavDestinationChangeDisposableEffect import com.paypal.android.utils.UIConstants @@ -125,6 +126,9 @@ fun DemoApp() { navController.popBackStack() }) } + composable(DemoAppDestinations.PAY_WITH_VENMO) { + PayWithVenmoView() + } } } } diff --git a/Demo/src/main/java/com/paypal/android/ui/DemoAppDestinations.kt b/Demo/src/main/java/com/paypal/android/ui/DemoAppDestinations.kt index f7ec46c0a..a126fb492 100644 --- a/Demo/src/main/java/com/paypal/android/ui/DemoAppDestinations.kt +++ b/Demo/src/main/java/com/paypal/android/ui/DemoAppDestinations.kt @@ -9,6 +9,7 @@ object DemoAppDestinations { const val PAYPAL_BUTTONS = "paypal_buttons" const val PAYPAL_STATIC_BUTTONS = "paypal_static_buttons" const val SELECT_TEST_CARD = "select_test_card" + const val PAY_WITH_VENMO = "venmo" fun titleForDestination(destination: String?): String = when (destination) { CARD_APPROVE_ORDER -> "Card Approve Order" @@ -19,6 +20,7 @@ object DemoAppDestinations { PAYPAL_STATIC_BUTTONS -> "PayPal Static Buttons" SELECT_TEST_CARD -> "Select a Test Card" PAYPAL_WEB_VAULT -> "Paypal Vault" + PAY_WITH_VENMO -> "Venmo" else -> "Demo" } } diff --git a/Demo/src/main/java/com/paypal/android/ui/features/Feature.kt b/Demo/src/main/java/com/paypal/android/ui/features/Feature.kt index b2b9a95e0..175497a1a 100644 --- a/Demo/src/main/java/com/paypal/android/ui/features/Feature.kt +++ b/Demo/src/main/java/com/paypal/android/ui/features/Feature.kt @@ -14,4 +14,5 @@ enum class Feature(@StringRes val stringRes: Int, val routeName: String) { R.string.feature_paypal_static_buttons, DemoAppDestinations.PAYPAL_STATIC_BUTTONS ), + PAY_WITH_VENMO(R.string.feature_pay_with_venmo, DemoAppDestinations.PAY_WITH_VENMO) } diff --git a/Demo/src/main/java/com/paypal/android/ui/features/FeaturesView.kt b/Demo/src/main/java/com/paypal/android/ui/features/FeaturesView.kt index e6aef79d1..f8605cb66 100644 --- a/Demo/src/main/java/com/paypal/android/ui/features/FeaturesView.kt +++ b/Demo/src/main/java/com/paypal/android/ui/features/FeaturesView.kt @@ -41,6 +41,8 @@ private val payPalWebFeatures = listOf( Feature.PAYPAL_WEB_VAULT ) +private val venmoFeatures = listOf(Feature.PAY_WITH_VENMO) + @ExperimentalFoundationApi @Composable fun FeaturesView( @@ -64,6 +66,12 @@ fun FeaturesView( item { FeatureOptions(payPalWebFeatures, onSelectedFeatureChange = onSelectedFeatureChange) } + stickyHeader { + FeatureGroupHeader("Venmo") + } + item { + FeatureOptions(venmoFeatures, onSelectedFeatureChange = onSelectedFeatureChange) + } } } diff --git a/Demo/src/main/java/com/paypal/android/ui/venmo/PayWithVenmoUiState.kt b/Demo/src/main/java/com/paypal/android/ui/venmo/PayWithVenmoUiState.kt new file mode 100644 index 000000000..dd5847435 --- /dev/null +++ b/Demo/src/main/java/com/paypal/android/ui/venmo/PayWithVenmoUiState.kt @@ -0,0 +1,13 @@ +package com.paypal.android.ui.venmo + +import com.paypal.android.api.model.Order +import com.paypal.android.uishared.state.ActionState + +data class PayWithVenmoUiState( + val createOrderState: ActionState = ActionState.Idle, + val payWithVenmoState: ActionState<*, Exception> = ActionState.Idle, +) { + val isCreateOrderSuccessful: Boolean + get() = createOrderState is ActionState.Success +} + diff --git a/Demo/src/main/java/com/paypal/android/ui/venmo/PayWithVenmoView.kt b/Demo/src/main/java/com/paypal/android/ui/venmo/PayWithVenmoView.kt new file mode 100644 index 000000000..a6f0397ea --- /dev/null +++ b/Demo/src/main/java/com/paypal/android/ui/venmo/PayWithVenmoView.kt @@ -0,0 +1,102 @@ +package com.paypal.android.ui.venmo + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.paypal.android.R +import com.paypal.android.uishared.components.ActionButtonColumn +import com.paypal.android.uishared.components.ErrorView +import com.paypal.android.uishared.components.OrderView +import com.paypal.android.uishared.components.StepHeader +import com.paypal.android.uishared.state.CompletedActionState +import com.paypal.android.utils.UIConstants +import com.paypal.android.utils.getActivityOrNull + +@Composable +fun PayWithVenmoView( + viewModel: PayWithVenmoViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val scrollState = rememberScrollState() + LaunchedEffect(scrollState.maxValue) { + // continuously scroll to bottom of the list when event state is updated + scrollState.animateScrollTo(scrollState.maxValue) + } + + val contentPadding = UIConstants.paddingMedium + Column( + verticalArrangement = UIConstants.spacingLarge, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = contentPadding) + .verticalScroll(scrollState) + ) { + Step1_CreateOrder(uiState, viewModel) + if (uiState.isCreateOrderSuccessful) { + Step2_StartPayWithVenmo(uiState, viewModel) + } + } +} + +@Composable +private fun Step1_CreateOrder(uiState: PayWithVenmoUiState, viewModel: PayWithVenmoViewModel) { + Column( + verticalArrangement = UIConstants.spacingMedium, + ) { + StepHeader(stepNumber = 1, title = "Create an Order") + ActionButtonColumn( + defaultTitle = "CREATE ORDER", + successTitle = "ORDER CREATED", + state = uiState.createOrderState, + onClick = { viewModel.createOrder() }, + modifier = Modifier + .fillMaxWidth() + ) { state -> + when (state) { + is CompletedActionState.Failure -> ErrorView(error = state.value) + is CompletedActionState.Success -> OrderView(order = state.value) + } + } + } +} + +@Composable +private fun Step2_StartPayWithVenmo( + uiState: PayWithVenmoUiState, + viewModel: PayWithVenmoViewModel +) { + val context = LocalContext.current + Column( + verticalArrangement = UIConstants.spacingMedium, + ) { + StepHeader(stepNumber = 2, title = stringResource(R.string.launch_venmo)) + ActionButtonColumn( + defaultTitle = "START CHECKOUT", + successTitle = "CHECKOUT COMPLETE", + state = uiState.payWithVenmoState, + onClick = { context.getActivityOrNull()?.let { viewModel.startVenmo(it) } }, + modifier = Modifier + .fillMaxWidth() + ) { state -> + when (state) { + is CompletedActionState.Failure -> ErrorView(error = state.value) + is CompletedActionState.Success -> state.value.run { + Text("We did iiiit!") +// PayPalWebCheckoutResultView(orderId, payerId) + } + } + } + } +} diff --git a/Demo/src/main/java/com/paypal/android/ui/venmo/PayWithVenmoViewModel.kt b/Demo/src/main/java/com/paypal/android/ui/venmo/PayWithVenmoViewModel.kt new file mode 100644 index 000000000..718e4ab33 --- /dev/null +++ b/Demo/src/main/java/com/paypal/android/ui/venmo/PayWithVenmoViewModel.kt @@ -0,0 +1,75 @@ +package com.paypal.android.ui.venmo + +import android.content.Context +import androidx.activity.ComponentActivity +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.paypal.android.api.model.Order +import com.paypal.android.api.model.OrderIntent +import com.paypal.android.api.services.SDKSampleServerAPI +import com.paypal.android.corepayments.CoreConfig +import com.paypal.android.models.OrderRequest +import com.paypal.android.uishared.enums.DeepLinkStrategy +import com.paypal.android.uishared.state.ActionState +import com.paypal.android.usecase.CreateOrderUseCase +import com.paypal.android.venmo.VenmoClient +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + + +@HiltViewModel +class PayWithVenmoViewModel @Inject constructor( + @ApplicationContext val applicationContext: Context, + val createOrderUseCase: CreateOrderUseCase, +) : ViewModel() { + + private val _uiState = MutableStateFlow(PayWithVenmoUiState()) + val uiState = _uiState.asStateFlow() + + private val coreConfig = CoreConfig(SDKSampleServerAPI.clientId) + private val venmoClient = VenmoClient(applicationContext, coreConfig) + + private var createOrderState + get() = _uiState.value.createOrderState + set(value) { + _uiState.update { it.copy(createOrderState = value) } + } + + private var payWithVenmoState + get() = _uiState.value.payWithVenmoState + set(value) { + _uiState.update { it.copy(payWithVenmoState = value) } + } + + private val createdOrder: Order? + get() = (createOrderState as? ActionState.Success)?.value + + fun createOrder() { + viewModelScope.launch { + createOrderState = ActionState.Loading + val orderRequest = _uiState.value.run { + OrderRequest( + intent = OrderIntent.CAPTURE, + shouldVaultOnSuccess = false, + appSwitchWhenEligible = false, + deepLinkStrategy = DeepLinkStrategy.CUSTOM_URL_SCHEME + ) + } + createOrderState = createOrderUseCase(orderRequest).mapToActionState() + } + } + + fun startVenmo(activity: ComponentActivity) { + val orderId = createdOrder?.id + if (orderId == null) { + payWithVenmoState = ActionState.Failure(Exception("Create an order to continue.")) + } else { + venmoClient.startVenmo(activity, orderId) + } + } +} \ No newline at end of file diff --git a/Demo/src/main/res/values/strings.xml b/Demo/src/main/res/values/strings.xml index cd045025b..9e4d58363 100644 --- a/Demo/src/main/res/values/strings.xml +++ b/Demo/src/main/res/values/strings.xml @@ -8,6 +8,7 @@ Paypal Vault PayPal Buttons PayPal Buttons - XML + Pay with Venmo CARD NUMBER @@ -55,4 +56,6 @@ PAYPAL + Launch Venmo + diff --git a/Venmo/build.gradle b/Venmo/build.gradle index 7be820174..a62a8ad27 100644 --- a/Venmo/build.gradle +++ b/Venmo/build.gradle @@ -44,9 +44,12 @@ android { } dependencies { + api project(':CorePayments') + implementation libs.androidx.coreKtx implementation libs.androidx.appcompat implementation libs.android.material + implementation libs.kotlinx.coroutinesAndroid testImplementation libs.junit diff --git a/Venmo/src/main/java/com/paypal/android/venmo/VenmoClient.kt b/Venmo/src/main/java/com/paypal/android/venmo/VenmoClient.kt new file mode 100644 index 000000000..e53538ece --- /dev/null +++ b/Venmo/src/main/java/com/paypal/android/venmo/VenmoClient.kt @@ -0,0 +1,56 @@ +package com.paypal.android.venmo + +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.core.net.toUri +import com.paypal.android.corepayments.CoreConfig +import com.paypal.android.corepayments.UpdateClientConfigAPI +import com.paypal.android.corepayments.UpdateClientConfigResult +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch + +class VenmoClient( + private val ccoAPI: UpdateClientConfigAPI, + private val applicationScope: CoroutineScope = CoroutineScope(SupervisorJob()), +) { + + constructor(context: Context, config: CoreConfig) : this(UpdateClientConfigAPI(context, config)) + + fun startVenmo(activity: ComponentActivity, orderId: String) { + applicationScope.launch { + // we don't seem to need the result + val ccoUpdateResult = ccoAPI.updateClientConfig(tokenId = orderId, fundingSource = "venmo") + when (ccoUpdateResult) { + UpdateClientConfigResult.Success -> { + Log.d("venmo", "CCO Update Success") + } + is UpdateClientConfigResult.Failure -> { + Log.d("venmo", "CCO Update Failure") + } + } + + // FROM: VenmoAppSwitch + val localVenmoBaseUrl = "https://www.paypal.com/smart/checkout/venmo" + val sandboxVenmoBaseUrl = "https://www.sandbox.paypal.com/smart/checkout/venmo" + val appSwitchUri = localVenmoBaseUrl.toUri() + .buildUpon() + .appendQueryParameter("buyerCountry", "US") + .appendQueryParameter("channel", "mobile-web") + .appendQueryParameter("enableFunding", "venmo") + .appendQueryParameter("env", "sandbox") + .appendQueryParameter("facilitatorAccessToken", "") + .appendQueryParameter("fundingSource", "venmo") + .appendQueryParameter("orderId", orderId) + .build() + activity.startActivity(Intent(Intent.ACTION_VIEW, appSwitchUri)) + + // FROM: VenmoWebProductFlow.ts (Sandbox) + + // FROM: VenmoAppSwitchProductFlow.ts (Sandbox) + + } + } +} \ No newline at end of file From caf664cf5ea4ebcbe0568459ff7ad0ace415af1d Mon Sep 17 00:00:00 2001 From: Karthik Gangineni Date: Thu, 26 Feb 2026 16:02:12 -0600 Subject: [PATCH 2/4] * Venmo funding eligibility graphQL check --- .../corepayments/api/GetFundingEligibility.kt | 123 ++++++++++++++++++ .../corepayments/model/FundingEligibility.kt | 9 ++ .../model/GetFundingEligibilityRequest.kt | 57 ++++++++ ...phql_query_get_funding_eligibility.graphql | 44 +++++++ .../com/paypal/android/venmo/VenmoClient.kt | 43 +++++- 5 files changed, 273 insertions(+), 3 deletions(-) create mode 100644 CorePayments/src/main/java/com/paypal/android/corepayments/api/GetFundingEligibility.kt create mode 100644 CorePayments/src/main/java/com/paypal/android/corepayments/model/FundingEligibility.kt create mode 100644 CorePayments/src/main/java/com/paypal/android/corepayments/model/GetFundingEligibilityRequest.kt create mode 100644 CorePayments/src/main/res/raw/graphql_query_get_funding_eligibility.graphql diff --git a/CorePayments/src/main/java/com/paypal/android/corepayments/api/GetFundingEligibility.kt b/CorePayments/src/main/java/com/paypal/android/corepayments/api/GetFundingEligibility.kt new file mode 100644 index 000000000..b038da7b8 --- /dev/null +++ b/CorePayments/src/main/java/com/paypal/android/corepayments/api/GetFundingEligibility.kt @@ -0,0 +1,123 @@ +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.common.Headers +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.FundingEligibility +import com.paypal.android.corepayments.model.GetFundingEligibilityResponse +import com.paypal.android.corepayments.model.GetFundingEligibilityVariables +import kotlinx.serialization.InternalSerializationApi + +@OptIn(InternalSerializationApi::class) +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class GetFundingEligibility internal constructor( + private val graphQLClient: GraphQLClient, + private val resourceLoader: ResourceLoader, + private val authenticationSecureTokenServiceAPI: AuthenticationSecureTokenServiceAPI, +) { + + constructor(coreConfig: CoreConfig) : this( + graphQLClient = GraphQLClient(coreConfig), + resourceLoader = ResourceLoader(), + authenticationSecureTokenServiceAPI = AuthenticationSecureTokenServiceAPI(coreConfig), + ) + + suspend operator fun invoke( + context: Context, + clientId: String, + merchantId: List? = null, + buyerCountry: String? = null, + currency: String? = null + ): APIResult { + val graphQLRequest = createGraphQLRequest( + context = context, + clientId = clientId, + merchantId = merchantId, + buyerCountry = buyerCountry, + currency = currency + ) ?: return APIResult.Failure(APIClientError.dataParsingError(correlationId = null)) + return sendGraphQLRequestWithLSATAuthentication(graphQLRequest) + } + + private suspend fun createGraphQLRequest( + context: Context, + clientId: String, + merchantId: List?, + buyerCountry: String?, + currency: String? + ): GraphQLRequest? { + val resourceResult = resourceLoader.loadRawResource( + context, + R.raw.graphql_query_get_funding_eligibility + ) + + val query = when (resourceResult) { + is LoadRawResourceResult.Success -> resourceResult.value + is LoadRawResourceResult.Failure -> return null + } + + val variables = GetFundingEligibilityVariables( + clientID = clientId, + merchantID = "V9YP27HFNG2LW", + buyerCountry = buyerCountry, + currency = currency + ) + + return GraphQLRequest( + query = query, + variables = variables, + operationName = "GetFundingEligibility" + ) + } + + private fun parseResponse(response: GetFundingEligibilityResponse): FundingEligibility? { + val fundingEligibilityData = response.fundingEligibility + + return fundingEligibilityData?.let { + FundingEligibility( + cardEligible = it.card?.eligible ?: false, + venmoEligible = it.venmo?.eligible ?: false + ) + } + } + + private suspend fun sendGraphQLRequestWithLSATAuthentication( + graphQLRequest: GraphQLRequest + ): APIResult { + val tokenResult = authenticationSecureTokenServiceAPI.createLowScopedAccessToken() + if (tokenResult is APIResult.Failure) { + return APIResult.Failure(tokenResult.error) + } + val token = (tokenResult as APIResult.Success).data + val graphQLResult = graphQLClient.send< + GetFundingEligibilityResponse, + GetFundingEligibilityVariables>( + graphQLRequest, + additionalHeaders = mapOf(Headers.AUTHORIZATION to "Bearer $token") + ) + return when (graphQLResult) { + is GraphQLResult.Success -> { + graphQLResult.response.data?.let { responseData -> + parseResponse(responseData)?.let { fundingEligibility -> + APIResult.Success(data = fundingEligibility) + } ?: APIResult.Failure( + APIClientError.dataParsingError(graphQLResult.correlationId) + ) + } ?: APIResult.Failure( + APIClientError.noResponseData(graphQLResult.correlationId) + ) + } + + is GraphQLResult.Failure -> APIResult.Failure(graphQLResult.error) + } + } +} diff --git a/CorePayments/src/main/java/com/paypal/android/corepayments/model/FundingEligibility.kt b/CorePayments/src/main/java/com/paypal/android/corepayments/model/FundingEligibility.kt new file mode 100644 index 000000000..cfe8d212c --- /dev/null +++ b/CorePayments/src/main/java/com/paypal/android/corepayments/model/FundingEligibility.kt @@ -0,0 +1,9 @@ +package com.paypal.android.corepayments.model + +import androidx.annotation.RestrictTo + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class FundingEligibility( + val cardEligible: Boolean, + val venmoEligible: Boolean +) diff --git a/CorePayments/src/main/java/com/paypal/android/corepayments/model/GetFundingEligibilityRequest.kt b/CorePayments/src/main/java/com/paypal/android/corepayments/model/GetFundingEligibilityRequest.kt new file mode 100644 index 000000000..6d78cf364 --- /dev/null +++ b/CorePayments/src/main/java/com/paypal/android/corepayments/model/GetFundingEligibilityRequest.kt @@ -0,0 +1,57 @@ +package com.paypal.android.corepayments.model + +import androidx.annotation.RestrictTo +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.Serializable + +/** + * Request data class for GetFundingEligibility GraphQL operation. + * Uses Kotlin serialization for JSON handling. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +@OptIn(InternalSerializationApi::class) +@Serializable +data class GetFundingEligibilityVariables( + val clientID: String, + val merchantID: String, + val buyerCountry: String? = null, + val currency: String? = null, + val intent: String? = null, + val commit: Boolean? = null, + val vault: Boolean? = null, + val disableFunding: List? = null, + val disableCard: List? = null +) + +/** + * Response data class for GetFundingEligibility GraphQL operation + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +@OptIn(InternalSerializationApi::class) +@Serializable +data class GetFundingEligibilityResponse( + val fundingEligibility: FundingEligibilityData? = null +) + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +@OptIn(InternalSerializationApi::class) +@Serializable +data class FundingEligibilityData( + val card: CardEligibilityData? = null, + val venmo: VenmoEligibilityData? = null +) + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +@OptIn(InternalSerializationApi::class) +@Serializable +data class CardEligibilityData( + val eligible: Boolean = false, + val branded: Boolean? = null +) + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +@OptIn(InternalSerializationApi::class) +@Serializable +data class VenmoEligibilityData( + val eligible: Boolean = false +) diff --git a/CorePayments/src/main/res/raw/graphql_query_get_funding_eligibility.graphql b/CorePayments/src/main/res/raw/graphql_query_get_funding_eligibility.graphql new file mode 100644 index 000000000..280c22c0b --- /dev/null +++ b/CorePayments/src/main/res/raw/graphql_query_get_funding_eligibility.graphql @@ -0,0 +1,44 @@ +query GetFundingEligibility( + $clientID: String! + $merchantID: [String] + $buyerCountry: CountryCodes + $currency: SupportedCountryCurrencies + $intent: FundingEligibilityIntent + $commit: Boolean + $vault: Boolean + $disableFunding: [SupportedPaymentMethodsType] + $disableCard: [SupportedCardsType] +) { + fundingEligibility( + clientId: $clientID + buyerCountry: $buyerCountry + currency: $currency + intent: $intent + commit: $commit + vault: $vault + disableFunding: $disableFunding + disableCard: $disableCard + merchantId: $merchantID + ) { + card { + eligible + branded + } + credit { + eligible + reasons + } + venmo { + eligible + reasons + } + paypal { + eligible + reasons + } + paylater { + eligible + reasons + } +} +} diff --git a/Venmo/src/main/java/com/paypal/android/venmo/VenmoClient.kt b/Venmo/src/main/java/com/paypal/android/venmo/VenmoClient.kt index e53538ece..84318312f 100644 --- a/Venmo/src/main/java/com/paypal/android/venmo/VenmoClient.kt +++ b/Venmo/src/main/java/com/paypal/android/venmo/VenmoClient.kt @@ -8,20 +8,57 @@ import androidx.core.net.toUri import com.paypal.android.corepayments.CoreConfig import com.paypal.android.corepayments.UpdateClientConfigAPI import com.paypal.android.corepayments.UpdateClientConfigResult +import com.paypal.android.corepayments.api.GetFundingEligibility +import com.paypal.android.corepayments.model.APIResult import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch class VenmoClient( private val ccoAPI: UpdateClientConfigAPI, + private val getFundingEligibility: GetFundingEligibility, private val applicationScope: CoroutineScope = CoroutineScope(SupervisorJob()), ) { - constructor(context: Context, config: CoreConfig) : this(UpdateClientConfigAPI(context, config)) + constructor(context: Context, config: CoreConfig) : this( + UpdateClientConfigAPI(context, config), + GetFundingEligibility(config) + ) - fun startVenmo(activity: ComponentActivity, orderId: String) { + fun startVenmo( + activity: ComponentActivity, + orderId: String, + buyerCountry: String = "US", + currency: String = "USD" + ) { applicationScope.launch { - // we don't seem to need the result + // Check funding eligibility for Venmo + val eligibilityResult = getFundingEligibility( + context = activity, + clientId = activity.applicationContext.packageName, + buyerCountry = buyerCountry, + currency = currency + ) + + when (eligibilityResult) { + is APIResult.Success -> { + val fundingEligibility = eligibilityResult.data + if (!fundingEligibility.venmoEligible) { + Log.d("venmo", "Venmo is not eligible for this transaction") + return@launch + } + } + + is APIResult.Failure -> { + Log.d( + "venmo", + "Failed to check funding eligibility: ${eligibilityResult.error}" + ) + return@launch + } + } + + // Venmo is eligible, proceed with CCO update val ccoUpdateResult = ccoAPI.updateClientConfig(tokenId = orderId, fundingSource = "venmo") when (ccoUpdateResult) { UpdateClientConfigResult.Success -> { From e0038d24a0eaa2d126527e3e15183b156e1ce85a Mon Sep 17 00:00:00 2001 From: sshropshire Date: Thu, 26 Feb 2026 16:01:54 -0600 Subject: [PATCH 3/4] Tweak URL for Venmo. --- .../com/paypal/android/ui/venmo/PayWithVenmoViewModel.kt | 5 ++++- Venmo/src/main/java/com/paypal/android/venmo/VenmoClient.kt | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Demo/src/main/java/com/paypal/android/ui/venmo/PayWithVenmoViewModel.kt b/Demo/src/main/java/com/paypal/android/ui/venmo/PayWithVenmoViewModel.kt index 718e4ab33..72d07794b 100644 --- a/Demo/src/main/java/com/paypal/android/ui/venmo/PayWithVenmoViewModel.kt +++ b/Demo/src/main/java/com/paypal/android/ui/venmo/PayWithVenmoViewModel.kt @@ -12,6 +12,7 @@ import com.paypal.android.models.OrderRequest import com.paypal.android.uishared.enums.DeepLinkStrategy import com.paypal.android.uishared.state.ActionState import com.paypal.android.usecase.CreateOrderUseCase +import com.paypal.android.utils.ReturnUrlFactory import com.paypal.android.venmo.VenmoClient import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -69,7 +70,9 @@ class PayWithVenmoViewModel @Inject constructor( if (orderId == null) { payWithVenmoState = ActionState.Failure(Exception("Create an order to continue.")) } else { - venmoClient.startVenmo(activity, orderId) + val deepLinkStrategy = DeepLinkStrategy.CUSTOM_URL_SCHEME + val returnUrl = ReturnUrlFactory.createGenericReturnUrl(deepLinkStrategy) + venmoClient.startVenmo(activity, orderId, returnUrl) } } } \ No newline at end of file diff --git a/Venmo/src/main/java/com/paypal/android/venmo/VenmoClient.kt b/Venmo/src/main/java/com/paypal/android/venmo/VenmoClient.kt index 84318312f..8bfe5f999 100644 --- a/Venmo/src/main/java/com/paypal/android/venmo/VenmoClient.kt +++ b/Venmo/src/main/java/com/paypal/android/venmo/VenmoClient.kt @@ -28,6 +28,7 @@ class VenmoClient( fun startVenmo( activity: ComponentActivity, orderId: String, + returnUrl: String, buyerCountry: String = "US", currency: String = "USD" ) { @@ -80,7 +81,8 @@ class VenmoClient( .appendQueryParameter("env", "sandbox") .appendQueryParameter("facilitatorAccessToken", "") .appendQueryParameter("fundingSource", "venmo") - .appendQueryParameter("orderId", orderId) + .appendQueryParameter("orderID", orderId) + .appendQueryParameter("pageUrl", returnUrl) .build() activity.startActivity(Intent(Intent.ACTION_VIEW, appSwitchUri)) From 306f42f55bab43ed0201ada0034240bb60adfa5e Mon Sep 17 00:00:00 2001 From: sshropshire Date: Thu, 26 Feb 2026 16:25:06 -0600 Subject: [PATCH 4/4] Implement Venmo GraphQL eligibility fix. --- .../corepayments/api/GetFundingEligibility.kt | 19 ++----- .../corepayments/model/FundingEligibility.kt | 1 - .../model/GetFundingEligibilityRequest.kt | 20 +------- ...phql_query_get_funding_eligibility.graphql | 50 ++++--------------- .../com/paypal/android/venmo/VenmoClient.kt | 4 +- 5 files changed, 19 insertions(+), 75 deletions(-) diff --git a/CorePayments/src/main/java/com/paypal/android/corepayments/api/GetFundingEligibility.kt b/CorePayments/src/main/java/com/paypal/android/corepayments/api/GetFundingEligibility.kt index b038da7b8..d245136a3 100644 --- a/CorePayments/src/main/java/com/paypal/android/corepayments/api/GetFundingEligibility.kt +++ b/CorePayments/src/main/java/com/paypal/android/corepayments/api/GetFundingEligibility.kt @@ -39,21 +39,13 @@ class GetFundingEligibility internal constructor( currency: String? = null ): APIResult { val graphQLRequest = createGraphQLRequest( - context = context, - clientId = clientId, - merchantId = merchantId, - buyerCountry = buyerCountry, - currency = currency + context = context ) ?: return APIResult.Failure(APIClientError.dataParsingError(correlationId = null)) return sendGraphQLRequestWithLSATAuthentication(graphQLRequest) } private suspend fun createGraphQLRequest( - context: Context, - clientId: String, - merchantId: List?, - buyerCountry: String?, - currency: String? + context: Context ): GraphQLRequest? { val resourceResult = resourceLoader.loadRawResource( context, @@ -66,10 +58,8 @@ class GetFundingEligibility internal constructor( } val variables = GetFundingEligibilityVariables( - clientID = clientId, - merchantID = "V9YP27HFNG2LW", - buyerCountry = buyerCountry, - currency = currency + merchantID = listOf("V9YP27HFNG2LW"), + enableFunding = listOf("VENMO"), ) return GraphQLRequest( @@ -84,7 +74,6 @@ class GetFundingEligibility internal constructor( return fundingEligibilityData?.let { FundingEligibility( - cardEligible = it.card?.eligible ?: false, venmoEligible = it.venmo?.eligible ?: false ) } diff --git a/CorePayments/src/main/java/com/paypal/android/corepayments/model/FundingEligibility.kt b/CorePayments/src/main/java/com/paypal/android/corepayments/model/FundingEligibility.kt index cfe8d212c..08b16fbb3 100644 --- a/CorePayments/src/main/java/com/paypal/android/corepayments/model/FundingEligibility.kt +++ b/CorePayments/src/main/java/com/paypal/android/corepayments/model/FundingEligibility.kt @@ -4,6 +4,5 @@ import androidx.annotation.RestrictTo @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class FundingEligibility( - val cardEligible: Boolean, val venmoEligible: Boolean ) diff --git a/CorePayments/src/main/java/com/paypal/android/corepayments/model/GetFundingEligibilityRequest.kt b/CorePayments/src/main/java/com/paypal/android/corepayments/model/GetFundingEligibilityRequest.kt index 6d78cf364..0d2114492 100644 --- a/CorePayments/src/main/java/com/paypal/android/corepayments/model/GetFundingEligibilityRequest.kt +++ b/CorePayments/src/main/java/com/paypal/android/corepayments/model/GetFundingEligibilityRequest.kt @@ -12,15 +12,8 @@ import kotlinx.serialization.Serializable @OptIn(InternalSerializationApi::class) @Serializable data class GetFundingEligibilityVariables( - val clientID: String, - val merchantID: String, - val buyerCountry: String? = null, - val currency: String? = null, - val intent: String? = null, - val commit: Boolean? = null, - val vault: Boolean? = null, - val disableFunding: List? = null, - val disableCard: List? = null + val merchantID: List, + val enableFunding: List ) /** @@ -37,18 +30,9 @@ data class GetFundingEligibilityResponse( @OptIn(InternalSerializationApi::class) @Serializable data class FundingEligibilityData( - val card: CardEligibilityData? = null, val venmo: VenmoEligibilityData? = null ) -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -@OptIn(InternalSerializationApi::class) -@Serializable -data class CardEligibilityData( - val eligible: Boolean = false, - val branded: Boolean? = null -) - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @OptIn(InternalSerializationApi::class) @Serializable diff --git a/CorePayments/src/main/res/raw/graphql_query_get_funding_eligibility.graphql b/CorePayments/src/main/res/raw/graphql_query_get_funding_eligibility.graphql index 280c22c0b..535f9c9ea 100644 --- a/CorePayments/src/main/res/raw/graphql_query_get_funding_eligibility.graphql +++ b/CorePayments/src/main/res/raw/graphql_query_get_funding_eligibility.graphql @@ -1,44 +1,14 @@ query GetFundingEligibility( - $clientID: String! - $merchantID: [String] - $buyerCountry: CountryCodes - $currency: SupportedCountryCurrencies - $intent: FundingEligibilityIntent - $commit: Boolean - $vault: Boolean - $disableFunding: [SupportedPaymentMethodsType] - $disableCard: [SupportedCardsType] + $merchantID: [String], + $enableFunding: [SupportedPaymentMethodsType] ) { - fundingEligibility( - clientId: $clientID - buyerCountry: $buyerCountry - currency: $currency - intent: $intent - commit: $commit - vault: $vault - disableFunding: $disableFunding - disableCard: $disableCard - merchantId: $merchantID - ) { - card { - eligible - branded + fundingEligibility( + merchantId: $merchantID, + enableFunding: $enableFunding + ) { + venmo { + eligible + reasons + } } - credit { - eligible - reasons - } - venmo { - eligible - reasons - } - paypal { - eligible - reasons - } - paylater { - eligible - reasons - } -} } diff --git a/Venmo/src/main/java/com/paypal/android/venmo/VenmoClient.kt b/Venmo/src/main/java/com/paypal/android/venmo/VenmoClient.kt index 8bfe5f999..911caf9e5 100644 --- a/Venmo/src/main/java/com/paypal/android/venmo/VenmoClient.kt +++ b/Venmo/src/main/java/com/paypal/android/venmo/VenmoClient.kt @@ -15,12 +15,14 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch class VenmoClient( + private val coreConfig: CoreConfig, private val ccoAPI: UpdateClientConfigAPI, private val getFundingEligibility: GetFundingEligibility, private val applicationScope: CoroutineScope = CoroutineScope(SupervisorJob()), ) { constructor(context: Context, config: CoreConfig) : this( + coreConfig = config, UpdateClientConfigAPI(context, config), GetFundingEligibility(config) ) @@ -36,7 +38,7 @@ class VenmoClient( // Check funding eligibility for Venmo val eligibilityResult = getFundingEligibility( context = activity, - clientId = activity.applicationContext.packageName, + clientId = coreConfig.clientId, buyerCountry = buyerCountry, currency = currency )