Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
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<String>? = null,
buyerCountry: String? = null,
currency: String? = null
): APIResult<FundingEligibility> {
val graphQLRequest = createGraphQLRequest(
context = context
) ?: return APIResult.Failure(APIClientError.dataParsingError(correlationId = null))
return sendGraphQLRequestWithLSATAuthentication(graphQLRequest)
}

private suspend fun createGraphQLRequest(
context: Context
): GraphQLRequest<GetFundingEligibilityVariables>? {
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(
merchantID = listOf("V9YP27HFNG2LW"),
enableFunding = listOf("VENMO"),
)

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

private fun parseResponse(response: GetFundingEligibilityResponse): FundingEligibility? {
val fundingEligibilityData = response.fundingEligibility

return fundingEligibilityData?.let {
FundingEligibility(
venmoEligible = it.venmo?.eligible ?: false
)
}
}

private suspend fun sendGraphQLRequestWithLSATAuthentication(
graphQLRequest: GraphQLRequest<GetFundingEligibilityVariables>
): APIResult<FundingEligibility> {
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)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.paypal.android.corepayments.model

import androidx.annotation.RestrictTo

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
data class FundingEligibility(
val venmoEligible: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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 merchantID: List<String>,
val enableFunding: List<String>
)

/**
* 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 venmo: VenmoEligibilityData? = null
)

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@OptIn(InternalSerializationApi::class)
@Serializable
data class VenmoEligibilityData(
val eligible: Boolean = false
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
query GetFundingEligibility(
$merchantID: [String],
$enableFunding: [SupportedPaymentMethodsType]
) {
fundingEligibility(
merchantId: $merchantID,
enableFunding: $enableFunding
) {
venmo {
eligible
reasons
}
}
}
4 changes: 4 additions & 0 deletions Demo/src/main/java/com/paypal/android/ui/DemoApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -125,6 +126,9 @@ fun DemoApp() {
navController.popBackStack()
})
}
composable(DemoAppDestinations.PAY_WITH_VENMO) {
PayWithVenmoView()
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ private val payPalWebFeatures = listOf(
Feature.PAYPAL_WEB_VAULT
)

private val venmoFeatures = listOf(Feature.PAY_WITH_VENMO)

@ExperimentalFoundationApi
@Composable
fun FeaturesView(
Expand All @@ -64,6 +66,12 @@ fun FeaturesView(
item {
FeatureOptions(payPalWebFeatures, onSelectedFeatureChange = onSelectedFeatureChange)
}
stickyHeader {
FeatureGroupHeader("Venmo")
}
item {
FeatureOptions(venmoFeatures, onSelectedFeatureChange = onSelectedFeatureChange)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Order, Exception> = ActionState.Idle,
val payWithVenmoState: ActionState<*, Exception> = ActionState.Idle,
) {
val isCreateOrderSuccessful: Boolean
get() = createOrderState is ActionState.Success
}

102 changes: 102 additions & 0 deletions Demo/src/main/java/com/paypal/android/ui/venmo/PayWithVenmoView.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
}
Loading
Loading