diff --git a/CorePayments/src/main/java/com/paypal/android/corepayments/HttpRequest.kt b/CorePayments/src/main/java/com/paypal/android/corepayments/HttpRequest.kt index 6f8d991ad..0737b0295 100644 --- a/CorePayments/src/main/java/com/paypal/android/corepayments/HttpRequest.kt +++ b/CorePayments/src/main/java/com/paypal/android/corepayments/HttpRequest.kt @@ -6,5 +6,5 @@ internal data class HttpRequest( val url: URL, val method: HttpMethod = HttpMethod.GET, val body: String? = null, - val headers: MutableMap = mutableMapOf(), + val headers: Map = emptyMap(), ) diff --git a/CorePayments/src/main/java/com/paypal/android/corepayments/graphql/GraphQLClient.kt b/CorePayments/src/main/java/com/paypal/android/corepayments/graphql/GraphQLClient.kt index d5ad6c970..fea7642b8 100644 --- a/CorePayments/src/main/java/com/paypal/android/corepayments/graphql/GraphQLClient.kt +++ b/CorePayments/src/main/java/com/paypal/android/corepayments/graphql/GraphQLClient.kt @@ -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 = 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] @@ -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) diff --git a/CorePayments/src/test/java/com/paypal/android/corepayments/HttpUnitTest.kt b/CorePayments/src/test/java/com/paypal/android/corepayments/HttpUnitTest.kt index 1e89cd842..75859dee2 100644 --- a/CorePayments/src/test/java/com/paypal/android/corepayments/HttpUnitTest.kt +++ b/CorePayments/src/test/java/com/paypal/android/corepayments/HttpUnitTest.kt @@ -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) diff --git a/Demo/src/main/java/com/paypal/android/ui/paypalweb/PayPalWebView.kt b/Demo/src/main/java/com/paypal/android/ui/paypalweb/PayPalWebView.kt index e805facfe..872bacc1a 100644 --- a/Demo/src/main/java/com/paypal/android/ui/paypalweb/PayPalWebView.kt +++ b/Demo/src/main/java/com/paypal/android/ui/paypalweb/PayPalWebView.kt @@ -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 @@ -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 diff --git a/Demo/src/main/java/com/paypal/android/uishared/components/ActionPaymentButtonColumn.kt b/Demo/src/main/java/com/paypal/android/uishared/components/ActionPaymentButtonColumn.kt new file mode 100644 index 000000000..e1a8ad215 --- /dev/null +++ b/Demo/src/main/java/com/paypal/android/uishared/components/ActionPaymentButtonColumn.kt @@ -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 ActionPaymentButtonColumn( + fundingSource: PayPalWebCheckoutFundingSource, + state: ActionState, + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable (CompletedActionState) -> 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)) + } + } + } + } +} diff --git a/Demo/src/main/res/values/strings.xml b/Demo/src/main/res/values/strings.xml index 884afaa07..574747473 100644 --- a/Demo/src/main/res/values/strings.xml +++ b/Demo/src/main/res/values/strings.xml @@ -45,6 +45,7 @@ PAYPAL_CREDIT PAY_LATER PAYPAL + CARD diff --git a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClient.kt b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClient.kt index adaa99798..9441c7af4 100644 --- a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClient.kt +++ b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClient.kt @@ -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 @@ -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 @@ -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 ) /** @@ -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( @@ -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) + ) + 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. * diff --git a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutFundingSource.kt b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutFundingSource.kt index 783150165..ea8451953 100644 --- a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutFundingSource.kt +++ b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutFundingSource.kt @@ -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") } diff --git a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebStartCallback.kt b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebStartCallback.kt new file mode 100644 index 000000000..76df40d5a --- /dev/null +++ b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebStartCallback.kt @@ -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) +} diff --git a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/UpdateClientConfigAPI.kt b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/UpdateClientConfigAPI.kt new file mode 100644 index 000000000..d33de6b3f --- /dev/null +++ b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/UpdateClientConfigAPI.kt @@ -0,0 +1,101 @@ +package com.paypal.android.paypalwebpayments + +import android.content.Context +import androidx.annotation.RawRes +import com.paypal.android.corepayments.CoreConfig +import com.paypal.android.corepayments.LoadRawResourceResult +import com.paypal.android.corepayments.PayPalSDKError +import com.paypal.android.corepayments.ResourceLoader +import com.paypal.android.corepayments.graphql.GraphQLClient +import com.paypal.android.corepayments.graphql.GraphQLResult +import org.json.JSONException +import org.json.JSONObject + +internal class UpdateClientConfigAPI( + private val coreConfig: CoreConfig, + private val applicationContext: Context, + private val graphQLClient: GraphQLClient, + private val resourceLoader: ResourceLoader +) { + + constructor(context: Context, coreConfig: CoreConfig) : this( + coreConfig, + context.applicationContext, + GraphQLClient(coreConfig), + ResourceLoader() + ) + + suspend fun updateClientConfig( + orderId: String, + fundingSource: PayPalWebCheckoutFundingSource + ): UpdateClientConfigResult { + @RawRes val resId = R.raw.graphql_query_update_client_config + return when (val result = resourceLoader.loadRawResource(applicationContext, resId)) { + is LoadRawResourceResult.Success -> + sendUpdateClientConfigGraphQLRequest( + query = result.value, + orderId = orderId, + fundingSource = fundingSource + ) + + is LoadRawResourceResult.Failure -> UpdateClientConfigResult.Failure( + PayPalSDKError(0, "TODO: implement") + ) + } + } + + private suspend fun sendUpdateClientConfigGraphQLRequest( + query: String, + orderId: String, + fundingSource: PayPalWebCheckoutFundingSource + ): UpdateClientConfigResult { + val variables = JSONObject() + .put("orderID", orderId) + .put("fundingSource", fundingSource.value) + .put("integrationArtifact", "PAYPAL_JS_SDK") + .put("userExperienceFlow", "INCONTEXT") + .put("productFlow", "SMART_PAYMENT_BUTTONS") + .put("buttonSessionId", JSONObject.NULL) + + val graphQLRequest = JSONObject() + .put("query", query) + .put("variables", variables) + + val clientId = coreConfig.clientId + val graphQLResponse = graphQLClient.send( + graphQLRequestBody = graphQLRequest, + queryName = "UpdateClientConfig", + additionalHeaders = mapOf("paypal-client-context" to clientId) + ) + return when (graphQLResponse) { + is GraphQLResult.Success -> { + val responseJSON = graphQLResponse.data + if (responseJSON == null) { + val error = graphQLResponse.run { + val errorDescription = "Error updating client config: $errors" + PayPalSDKError(0, errorDescription, correlationId) + } + UpdateClientConfigResult.Failure(error) + } else { + parseSuccessfulUpdateSuccessJSON(responseJSON, graphQLResponse.correlationId) + } + } + + is GraphQLResult.Failure -> UpdateClientConfigResult.Failure(graphQLResponse.error) + } + } + + private fun parseSuccessfulUpdateSuccessJSON( + responseBody: JSONObject, + correlationId: String? + ): UpdateClientConfigResult { + return try { + val clientConfig = responseBody.optString("updateClientConfig", "") + UpdateClientConfigResult.Success(clientConfig) + } catch (jsonError: JSONException) { + val message = "Update Client Config Failed: GraphQL JSON body was invalid." + val error = PayPalSDKError(0, message, correlationId, reason = jsonError) + UpdateClientConfigResult.Failure(error) + } + } +} diff --git a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/UpdateClientConfigResult.kt b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/UpdateClientConfigResult.kt new file mode 100644 index 000000000..521110fb6 --- /dev/null +++ b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/UpdateClientConfigResult.kt @@ -0,0 +1,9 @@ +package com.paypal.android.paypalwebpayments + +import com.paypal.android.corepayments.PayPalSDKError + +sealed class UpdateClientConfigResult { + + data class Success(val clientConfig: String) : UpdateClientConfigResult() + data class Failure(val error: PayPalSDKError) : UpdateClientConfigResult() +} diff --git a/PayPalWebPayments/src/main/res/raw/graphql_query_update_client_config.graphql b/PayPalWebPayments/src/main/res/raw/graphql_query_update_client_config.graphql new file mode 100644 index 000000000..5c3de8736 --- /dev/null +++ b/PayPalWebPayments/src/main/res/raw/graphql_query_update_client_config.graphql @@ -0,0 +1,17 @@ +mutation UpdateClientConfig( + $orderID: String!, + $fundingSource: ButtonFundingSourceType!, + $integrationArtifact: IntegrationArtifactType!, + $userExperienceFlow: UserExperienceFlowType!, + $productFlow: ProductFlowType!, + $buttonSessionID: String +) { + updateClientConfig( + token: $orderID + fundingSource: $fundingSource + integrationArtifact: $integrationArtifact, + userExperienceFlow: $userExperienceFlow, + productFlow: $productFlow, + buttonSessionID: $buttonSessionID + ) +} diff --git a/PayPalWebPayments/src/test/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClientUnitTest.kt b/PayPalWebPayments/src/test/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClientUnitTest.kt index 6157f79a9..56effe635 100644 --- a/PayPalWebPayments/src/test/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClientUnitTest.kt +++ b/PayPalWebPayments/src/test/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClientUnitTest.kt @@ -9,6 +9,7 @@ import io.mockk.mockk import io.mockk.verify import junit.framework.TestCase.assertSame import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Before import org.junit.Test @@ -21,6 +22,7 @@ class PayPalWebCheckoutClientUnitTest { private val activity: FragmentActivity = mockk(relaxed = true) private val analytics = mockk(relaxed = true) + private val updateClientConfigAPI = mockk(relaxed = true) private val intent = Intent() @@ -30,7 +32,12 @@ class PayPalWebCheckoutClientUnitTest { @Before fun beforeEach() { payPalWebLauncher = mockk(relaxed = true) - sut = PayPalWebCheckoutClient(analytics, payPalWebLauncher) + sut = PayPalWebCheckoutClient( + analytics = analytics, + updateClientConfigAPI = updateClientConfigAPI, + payPalWebLauncher = payPalWebLauncher, + dispatcher = Dispatchers.Main + ) } @Test diff --git a/PaymentButtons/src/main/java/com/paypal/android/paymentbuttons/CardButton.kt b/PaymentButtons/src/main/java/com/paypal/android/paymentbuttons/CardButton.kt new file mode 100644 index 000000000..5818cf803 --- /dev/null +++ b/PaymentButtons/src/main/java/com/paypal/android/paymentbuttons/CardButton.kt @@ -0,0 +1,235 @@ +package com.paypal.android.paymentbuttons + +import android.content.Context +import android.content.res.TypedArray +import android.util.AttributeSet +import android.view.View +import androidx.core.content.res.use +import com.paypal.android.paymentbuttons.error.createFormattedIllegalArgumentException +import com.paypal.android.ui.R + +/** + * PayPalButton provides a PayPal button with the ability to modify the [color], [label], [shape], + * and [size]. + * + * Setting up PayPalButton within an XML layout: + * ``` + * + * ``` + * + * Optionally you can provide the following attributes: `paypal_color`, `paypal_label`, + * `payment_button_shape`, and `payment_button_size`. + * + */ +open class CardButton @JvmOverloads constructor( + context: Context, + attributeSet: AttributeSet? = null, + defStyleAttr: Int = 0 +) : PaymentButton(context, attributeSet, defStyleAttr) { + + /** + * Updates the color of the Payment Button with the provided [CardButtonColor]. + * + * This may update the PayPal wordmark to aid with visibility as well. When updated to GOLD or + * WHITE it will be updated to the traditional wordmark. When updated to BLUE or BLACK it will + * be updated to the monochrome wordmark. + */ + override var color: PaymentButtonColor = CardButtonColor.BLACK + set(value) { + field = value + updateShapeDrawableFillColor(field) + } + + /** + * Updates the label for Payment Button with the provided [CardButtonLabel]. + * + * This will default to [PAYPAL] if one is not provided which omits a label and only displays + * the wordmark. Note: this does not support [PAY_LATER], if you require a button with that + * label then use the specialized [PayLaterButton]. + */ + open var label: CardButtonLabel = CardButtonLabel.CHECKOUT + set(value) { + if (value != CardButtonLabel.PAY_LATER) { + field = value + updateLabel(field) + } + } + + override val wordmarkDarkLuminanceResId: Int = R.drawable.card_white + + override val wordmarkLightLuminanceResId: Int = R.drawable.card_black + + override val fundingType: PaymentButtonFundingType = PaymentButtonFundingType.PAYPAL + + init { + contentDescription = context.getString(R.string.paypal_payment_button_description) + analyticsService.sendAnalyticsEvent( + "payment-button:initialized", + orderId = null, + buttonType = PaymentButtonFundingType.PAYPAL.buttonType + ) + } + + private fun updateColorFrom(typedArray: TypedArray) { + val paypalColorAttribute = typedArray.getInt( + R.styleable.PayPalButton_paypal_color, + CardButtonColor.GOLD.value + ) + color = CardButtonColor(paypalColorAttribute) + } + + private fun updateLabelFrom(typedArray: TypedArray) { + val paypalLabelAttribute = typedArray.getInt( + R.styleable.PayPalButton_paypal_label, 0 + ) + label = CardButtonLabel(paypalLabelAttribute) + } + + protected fun updateLabel(updatedLabel: CardButtonLabel) { + // simulate payment label at the end + prefixTextVisibility = View.GONE + suffixTextVisibility = View.VISIBLE + suffixText = "Pay with Card" + } +} + +/** + * Defines the colors available for PayPal buttons. + * + * @see GOLD is the default color if one is not provided and is the recommended choice as research + * has shown it results in the best conversion. + * @see BLUE is the preferred alternative color if gold does not work for your experience. Research + * has shown that people know it is our brand color, which provides a halo of trust and security to + * your experience. + * @see WHITE is one of our secondary alternatives. This color is less capable of drawing people's + * attention. + * @see BLACK is one of our secondary alternatives. This color is less capable of drawing people's + * attention. + * @see SILVER is one of our secondary alternatives. This color is less capable of drawing people's + * attention. + */ +enum class CardButtonColor( + val value: Int, + override val colorResId: Int, + override val hasOutline: Boolean = false, + override val luminance: PaymentButtonColorLuminance +) : PaymentButtonColor { + GOLD(value = 0, colorResId = R.color.paypal_gold, luminance = PaymentButtonColorLuminance.LIGHT), + BLUE(value = 1, colorResId = R.color.paypal_blue, luminance = PaymentButtonColorLuminance.DARK), + WHITE( + value = 2, + colorResId = R.color.paypal_white, + hasOutline = true, + luminance = PaymentButtonColorLuminance.LIGHT + ), + BLACK(value = 3, colorResId = R.color.paypal_black, luminance = PaymentButtonColorLuminance.DARK), + SILVER(value = 4, colorResId = R.color.paypal_silver, luminance = PaymentButtonColorLuminance.LIGHT); + + companion object { + /** + * Given an [attributeIndex] this will provide the correct [CardButtonColor]. If an + * invalid [attributeIndex] is provided then it will throw an [IllegalStateException]. + * + * @throws [IllegalArgumentException] when an invalid index is provided. + */ + operator fun invoke(attributeIndex: Int): CardButtonColor { + return when (attributeIndex) { + GOLD.value -> GOLD + BLUE.value -> BLUE + WHITE.value -> WHITE + BLACK.value -> BLACK + SILVER.value -> SILVER + else -> throw createFormattedIllegalArgumentException("CardButtonColor", values().size) + } + } + } +} + +/** + * Defines the labels available for payment buttons. If no label is provided then it will + * default to [PAYPAL] which will not display a label and will only display the PayPal wordmark. For + * other labels they will have the label value itself along with a position either at the start or + * the end of the button. + * + */ +enum class CardButtonLabel( + val value: Int, + val position: Position? = null, + private val stringResId: Int? = null +) { + /** + * Label for PayPal text + */ + PAYPAL(value = 0), + + /** + * Label for Checkout text + */ + CHECKOUT( + value = 1, + position = Position.END, + stringResId = R.string.paypal_checkout_smart_payment_button_label_checkout + ), + + /** + * Label for Buy Now text + */ + BUY_NOW( + value = 2, + position = Position.END, + stringResId = R.string.paypal_checkout_smart_payment_button_label_buy_now + ), + + /** + * Label for Pay text + */ + PAY( + value = 3, + position = Position.START, + stringResId = R.string.paypal_checkout_smart_payment_button_label_pay + ), + + /** + * Label for Pay Later text + */ + PAY_LATER( + value = 4, + position = Position.END, + stringResId = R.string.paypal_checkout_smart_payment_button_label_pay_later + ); + + fun retrieveLabel(context: Context): String? { + return stringResId?.let { context.getString(it) } + } + + /** + * Defines at what position a label is displayed. A label can either be positioned at the start + * or end of a button. + */ + enum class Position { + START, + END; + } + + companion object { + /** + * Given an [attributeIndex] this will provide the correct [CardButtonLabel]. + * If an invalid [attributeIndex] is provided then it will throw an [IllegalArgumentException]. + * + * @throws [IllegalArgumentException] when an invalid index is provided. + */ + operator fun invoke(attributeIndex: Int): CardButtonLabel { + return when (attributeIndex) { + PAYPAL.value -> PAYPAL + CHECKOUT.value -> CHECKOUT + BUY_NOW.value -> BUY_NOW + PAY.value -> PAY + PAY_LATER.value -> PAY_LATER + else -> throw createFormattedIllegalArgumentException("PaymentButtonLabel", values().size) + } + } + } +} diff --git a/PaymentButtons/src/main/res/drawable/card_black.xml b/PaymentButtons/src/main/res/drawable/card_black.xml new file mode 100644 index 000000000..1ad9f4e5d --- /dev/null +++ b/PaymentButtons/src/main/res/drawable/card_black.xml @@ -0,0 +1,12 @@ + + + diff --git a/PaymentButtons/src/main/res/drawable/card_white.xml b/PaymentButtons/src/main/res/drawable/card_white.xml new file mode 100644 index 000000000..120c97881 --- /dev/null +++ b/PaymentButtons/src/main/res/drawable/card_white.xml @@ -0,0 +1,12 @@ + + +