Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ internal data class HttpRequest(
val url: URL,
val method: HttpMethod = HttpMethod.GET,
val body: String? = null,
val headers: MutableMap<String, String> = mutableMapOf(),
val headers: Map<String, String> = emptyMap(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,22 @@ class GraphQLClient internal constructor(
private val graphQLEndpoint = coreConfig.environment.graphQLEndpoint
private val graphQLURL = "$graphQLEndpoint/graphql"

private val httpRequestHeaders = mutableMapOf(
private val httpRequestHeaders = mapOf(
"Content-Type" to "application/json",
"Accept" to "application/json",
"x-app-name" to "nativecheckout",
"Origin" to coreConfig.environment.graphQLEndpoint
)

suspend fun send(graphQLRequestBody: JSONObject, queryName: String? = null): GraphQLResult {
suspend fun send(
graphQLRequestBody: JSONObject,
queryName: String? = null,
additionalHeaders: Map<String, String> = emptyMap()
): GraphQLResult {
val body = graphQLRequestBody.toString()
val urlString = if (queryName != null) "$graphQLURL?$queryName" else graphQLURL
val httpRequest = HttpRequest(URL(urlString), HttpMethod.POST, body, httpRequestHeaders)
val allHeaders = httpRequestHeaders + additionalHeaders
val httpRequest = HttpRequest(URL(urlString), HttpMethod.POST, body, allHeaders)

val httpResponse = http.send(httpRequest)
val correlationId: String? = httpResponse.headers[PAYPAL_DEBUG_ID]
Expand All @@ -51,7 +56,10 @@ class GraphQLClient internal constructor(
} else {
try {
val responseAsJSON = JSONObject(httpResponse.body)
GraphQLResult.Success(responseAsJSON.getJSONObject("data"), correlationId = correlationId)
GraphQLResult.Success(
responseAsJSON.getJSONObject("data"),
correlationId = correlationId
)
} catch (jsonParseError: JSONException) {
val error = APIClientError.graphQLJSONParseError(correlationId, jsonParseError)
GraphQLResult.Failure(error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ class HttpUnitTest {

@Test
fun `send sets request headers on url connection`() = runTest {
val httpRequest = HttpRequest(url, HttpMethod.GET)
httpRequest.headers["Sample-Header"] = "sample-value"
val headers = mapOf("Sample-Header" to "sample-value")
val httpRequest = HttpRequest(url, HttpMethod.GET, headers = headers)

val sut = createHttp(testScheduler)
sut.send(httpRequest)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.paypal.android.uishared.components.ActionButtonColumn
import com.paypal.android.uishared.components.ActionPaymentButtonColumn
import com.paypal.android.uishared.components.CreateOrderForm
import com.paypal.android.uishared.components.ErrorView
import com.paypal.android.uishared.components.OrderView
Expand Down Expand Up @@ -106,9 +107,8 @@ private fun Step2_StartPayPalWebCheckout(uiState: PayPalWebUiState, viewModel: P
fundingSource = uiState.fundingSource,
onFundingSourceChange = { value -> viewModel.fundingSource = value },
)
ActionButtonColumn(
defaultTitle = "START CHECKOUT",
successTitle = "CHECKOUT COMPLETE",
ActionPaymentButtonColumn(
fundingSource = uiState.fundingSource,
state = uiState.payPalWebCheckoutState,
onClick = { context.getActivityOrNull()?.let { viewModel.startWebCheckout(it) } },
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.paypal.android.uishared.components

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.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.paypal.android.paymentbuttons.CardButton
import com.paypal.android.paymentbuttons.CardButtonLabel
import com.paypal.android.paymentbuttons.PayPalButton
import com.paypal.android.paymentbuttons.PayPalButtonColor
import com.paypal.android.paymentbuttons.PayPalButtonLabel
import com.paypal.android.paymentbuttons.PaymentButtonSize
import com.paypal.android.paypalwebpayments.PayPalWebCheckoutFundingSource
import com.paypal.android.uishared.state.ActionState
import com.paypal.android.uishared.state.CompletedActionState
import com.paypal.android.utils.UIConstants

@Composable
fun <S, E> ActionPaymentButtonColumn(
fundingSource: PayPalWebCheckoutFundingSource,
state: ActionState<S, E>,
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable (CompletedActionState<S, E>) -> Unit = {},
) {
Card(
modifier = modifier
) {
DemoPaymentButton(
fundingSource = fundingSource,
onClick = {
if (state is ActionState.Idle) {
onClick()
}
},
modifier = Modifier.fillMaxWidth()
)

// optional content
val completedState = when (state) {
is ActionState.Success -> CompletedActionState.Success(state.value)
is ActionState.Failure -> CompletedActionState.Failure(state.value)
else -> null
}
completedState?.let {
content(completedState)
}
}
}

@Composable
fun DemoPaymentButton(
fundingSource: PayPalWebCheckoutFundingSource,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
when (fundingSource) {

PayPalWebCheckoutFundingSource.CARD -> AndroidView(
factory = { context ->
CardButton(context).apply { setOnClickListener { onClick() } }
},
update = { button ->
button.label = CardButtonLabel.PAY
button.size = PaymentButtonSize.LARGE
},
modifier = modifier
)

else -> AndroidView(
factory = { context ->
PayPalButton(context).apply { setOnClickListener { onClick() } }
},
update = { button ->
button.color = PayPalButtonColor.BLUE
button.label = PayPalButtonLabel.PAY
button.size = PaymentButtonSize.LARGE
},
modifier = modifier
)
}
}

@Preview
@Composable
fun StatefulActionPaymentButtonPreview() {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
Column {
ActionPaymentButtonColumn(
fundingSource = PayPalWebCheckoutFundingSource.CARD,
state = ActionState.Idle,
onClick = {},
modifier = Modifier
.fillMaxWidth()
.padding(UIConstants.paddingMedium)
) { state ->
Text(text = "Sample Text", modifier = Modifier.padding(64.dp))
}
}
}
}
}
1 change: 1 addition & 0 deletions Demo/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<item>PAYPAL_CREDIT</item>
<item>PAY_LATER</item>
<item>PAYPAL</item>
<item>CARD</item>
</string-array>

</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import com.paypal.android.corepayments.analytics.AnalyticsService
import com.paypal.android.paypalwebpayments.analytics.CheckoutEvent
import com.paypal.android.paypalwebpayments.analytics.PayPalWebAnalytics
import com.paypal.android.paypalwebpayments.analytics.VaultEvent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

// NEXT MAJOR VERSION: consider renaming this module to PayPalWebClient since
// it now offers both checkout and vaulting
Expand All @@ -17,7 +21,9 @@ import com.paypal.android.paypalwebpayments.analytics.VaultEvent
*/
class PayPalWebCheckoutClient internal constructor(
private val analytics: PayPalWebAnalytics,
private val payPalWebLauncher: PayPalWebLauncher
private val updateClientConfigAPI: UpdateClientConfigAPI,
private val payPalWebLauncher: PayPalWebLauncher,
private val dispatcher: CoroutineDispatcher
) {

// for analytics tracking
Expand All @@ -33,7 +39,9 @@ class PayPalWebCheckoutClient internal constructor(
*/
constructor(context: Context, configuration: CoreConfig, urlScheme: String) : this(
PayPalWebAnalytics(AnalyticsService(context.applicationContext, configuration)),
UpdateClientConfigAPI(context, configuration),
PayPalWebLauncher(urlScheme, configuration),
dispatcher = Dispatchers.Main
)

/**
Expand All @@ -48,6 +56,10 @@ class PayPalWebCheckoutClient internal constructor(
checkoutOrderId = request.orderId
analytics.notify(CheckoutEvent.STARTED, checkoutOrderId)

if (request.fundingSource == PayPalWebCheckoutFundingSource.CARD) {
// TODO: consider returning an error immediately
}

val result = payPalWebLauncher.launchPayPalWebCheckout(activity, request)
when (result) {
is PayPalPresentAuthChallengeResult.Success -> analytics.notify(
Expand All @@ -61,6 +73,53 @@ class PayPalWebCheckoutClient internal constructor(
return result
}

/**
* Confirm PayPal payment source for an order.
*
* @param request [PayPalWebCheckoutRequest] for requesting an order approval
*/
fun start(
activity: ComponentActivity,
request: PayPalWebCheckoutRequest,
callback: PayPalWebStartCallback
) {
CoroutineScope(dispatcher).launch {
checkoutOrderId = request.orderId
analytics.notify(CheckoutEvent.STARTED, checkoutOrderId)

if (request.fundingSource == PayPalWebCheckoutFundingSource.CARD) {
val updateConfigResult = request.run {
updateClientConfigAPI.updateClientConfig(
orderId = orderId,
fundingSource = fundingSource
)
}
if (updateConfigResult is UpdateClientConfigResult.Failure) {
// notify failure
callback.onPayPalWebStartResult(
PayPalPresentAuthChallengeResult.Failure(updateConfigResult.error)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Have we decided to return failure upon failure on this GraphQL call?
I know JS SDK does not do that atm and Ravi had mentioned that this is probably what should be done.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think we've made a decision on this. I'd be curious to know how failure affects the overall transaction before making the final call.

)
return@launch
}
}

val result = payPalWebLauncher.launchPayPalWebCheckout(activity, request)
when (result) {
is PayPalPresentAuthChallengeResult.Success -> analytics.notify(
CheckoutEvent.AUTH_CHALLENGE_PRESENTATION_SUCCEEDED,
checkoutOrderId
)

is PayPalPresentAuthChallengeResult.Failure ->
analytics.notify(
CheckoutEvent.AUTH_CHALLENGE_PRESENTATION_FAILED,
checkoutOrderId
)
}
callback.onPayPalWebStartResult(result)
}
}

/**
* Vault PayPal as a payment method.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,10 @@ enum class PayPalWebCheckoutFundingSource(val value: String) {
/**
* PAYPAL will launch the web checkout for a one-time PayPal Checkout flow
*/
PAYPAL("paypal")
PAYPAL("paypal"),

/**
* TODO: add docstring
*/
CARD("card")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.paypal.android.paypalwebpayments

import androidx.annotation.MainThread

fun interface PayPalWebStartCallback {

/**
* Called when the result of a PayPal web launch is available.
*/
@MainThread
fun onPayPalWebStartResult(result: PayPalPresentAuthChallengeResult)
}
Loading