Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shopper insights rp2 feature #1256

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
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
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,11 @@ class AnalyticsClient internal constructor(
endTime = analyticsEventParams.endTime,
endpoint = analyticsEventParams.endpoint,
experiment = analyticsEventParams.experiment,
paymentMethodsDisplayed = analyticsEventParams.paymentMethodsDisplayed,
appSwitchUrl = analyticsEventParams.appSwitchUrl
appSwitchUrl = analyticsEventParams.appSwitchUrl,
shopperSessionId = analyticsEventParams.shopperSessionId,
buttonType = analyticsEventParams.buttonType,
buttonOrder = analyticsEventParams.buttonOrder,
pageType = analyticsEventParams.pageType
)
configurationLoader.loadConfiguration { result ->
if (result is ConfigurationLoaderResult.Success) {
Expand Down Expand Up @@ -239,9 +242,11 @@ class AnalyticsClient internal constructor(
.putOpt(FPTI_KEY_END_TIME, event.endTime)
.putOpt(FPTI_KEY_ENDPOINT, event.endpoint)
.putOpt(FPTI_KEY_MERCHANT_EXPERIMENT, event.experiment)
.putOpt(FPTI_KEY_MERCHANT_PAYMENT_METHODS_DISPLAYED,
event.paymentMethodsDisplayed.ifEmpty { null })
.putOpt(FPTI_KEY_URL, event.appSwitchUrl)
.putOpt(FPTI_KEY_SHOPPER_SESSION_ID, event.shopperSessionId)
.putOpt(FPTI_KEY_BUTTON_TYPE, event.buttonType)
.putOpt(FPTI_KEY_BUTTON_POSITION, event.buttonOrder)
.putOpt(FPTI_KEY_PAGE_TYPE, event.pageType)
return json.toString()
}

Expand Down Expand Up @@ -295,6 +300,10 @@ class AnalyticsClient internal constructor(
private const val FPTI_KEY_MERCHANT_EXPERIMENT = "experiment"
private const val FPTI_KEY_MERCHANT_PAYMENT_METHODS_DISPLAYED = "payment_methods_displayed"
private const val FPTI_KEY_URL = "url"
private const val FPTI_KEY_SHOPPER_SESSION_ID = "shopper_session_id"
private const val FPTI_KEY_BUTTON_TYPE = "button_type"
private const val FPTI_KEY_BUTTON_POSITION = "button_position"
private const val FPTI_KEY_PAGE_TYPE = "page_type"

private const val FPTI_BATCH_KEY_VENMO_INSTALLED = "venmo_installed"
private const val FPTI_BATCH_KEY_PAYPAL_INSTALLED = "paypal_installed"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ internal data class AnalyticsEvent(
val endTime: Long? = null,
val endpoint: String? = null,
val experiment: String? = null,
val paymentMethodsDisplayed: List<String> = emptyList(),
val appSwitchUrl: String? = null
val appSwitchUrl: String? = null,
val shopperSessionId: String? = null,
val buttonType: String? = null,
val buttonOrder: String? = null,
val pageType: String? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ import androidx.annotation.RestrictTo
* @property endpoint The endpoint being called.
* @property experiment Currently a ShopperInsights module specific event that indicates
* the experiment, as a JSON string, that the merchant sent to the us.
* @property paymentMethodsDisplayed A ShopperInsights module specific event that indicates the
* order of payment methods displayed to the shopper by the merchant.
* @property shopperSessionId The Shopper Insights customer session ID created by a merchant's
* server SDK or graphQL integration.
* @property buttonType buttonType Represents the tapped button type.
* @property buttonOrder The order or ranking in which payment buttons appear.
* @property pageType The page or view that a button is displayed on.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
data class AnalyticsEventParams @JvmOverloads constructor(
Expand All @@ -27,6 +30,9 @@ data class AnalyticsEventParams @JvmOverloads constructor(
val endTime: Long? = null,
val endpoint: String? = null,
val experiment: String? = null,
val paymentMethodsDisplayed: List<String> = emptyList(),
val appSwitchUrl: String? = null
val appSwitchUrl: String? = null,
val shopperSessionId: String? = null,
val buttonType: String? = null,
val buttonOrder: String? = null,
val pageType: String? = null
)
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
# Braintree Android SDK Release Notes

## unreleased

* PayPal
* Fix bug to ensure that `PayPalVaultRequest.userAuthenticationEmail` is not sent as an empty string
* ShopperInsights (BETA)
* Add `isPayPalAppInstalled` and `isVenmoAppInstalled` methods
* Add `shopperSessionId` parameter to `ShopperInsightsClient`
* BraintreePayPal
* Add `shopperSessionId` to `PayPalCheckoutRequest` and `PayPalVaultRequest`
* Replace `sendPayPalPresentedEvent()` and `sendVenmoPresentedEvent()` with `sendPresentedEvent()`
* Replace `sendPayPalSelectedEvent()` and `sendVenmoSelectedEvent()` with `sendSelectedEvent()`
* ThreeDSecure
* Return error if no `dfReferenceId` is returned in the 3D Secure flow

## 5.3.0 (2024-12-11)

* PayPal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,15 +149,17 @@ private void launchPayPal(
activity,
buyerEmailAddress,
buyerPhoneCountryCode,
buyerPhoneNationalNumber
buyerPhoneNationalNumber,
null
);
} else {
payPalRequest = createPayPalCheckoutRequest(
activity,
amount,
buyerEmailAddress,
buyerPhoneCountryCode,
buyerPhoneNationalNumber
buyerPhoneNationalNumber,
null
);
}
payPalClient.createPaymentAuthRequest(requireContext(), payPalRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,27 @@ public static PayPalVaultRequest createPayPalVaultRequest(
Context context,
String buyerEmailAddress,
String buyerPhoneCountryCode,
String buyerPhoneNationalNumber
String buyerPhoneNationalNumber,
String shopperInsightsSessionId
) {

PayPalVaultRequest request = new PayPalVaultRequest(true);

if (!buyerEmailAddress.isEmpty()) {
if (buyerEmailAddress != null && !buyerEmailAddress.isEmpty()) {
request.setUserAuthenticationEmail(buyerEmailAddress);
}

if (!buyerPhoneCountryCode.isEmpty() && !buyerPhoneNationalNumber.isEmpty()) {
request.setUserPhoneNumber(new PayPalPhoneNumber(buyerPhoneCountryCode, buyerPhoneNationalNumber));
if ((buyerPhoneCountryCode != null && !buyerPhoneCountryCode.isEmpty())
&& (buyerPhoneNationalNumber != null && !buyerPhoneNationalNumber.isEmpty())) {

request.setUserPhoneNumber(new PayPalPhoneNumber(
buyerPhoneCountryCode,
buyerPhoneNationalNumber)
);
}

if (shopperInsightsSessionId != null && !shopperInsightsSessionId.isEmpty()) {
request.setShopperSessionId(shopperInsightsSessionId);
}

if (Settings.isPayPalAppSwithEnabled(context)) {
Expand Down Expand Up @@ -112,16 +122,25 @@ public static PayPalCheckoutRequest createPayPalCheckoutRequest(
String amount,
String buyerEmailAddress,
String buyerPhoneCountryCode,
String buyerPhoneNationalNumber
String buyerPhoneNationalNumber,
String shopperInsightsSessionId
) {
PayPalCheckoutRequest request = new PayPalCheckoutRequest(amount, true);

if (!buyerEmailAddress.isEmpty()) {
if (buyerEmailAddress != null && !buyerEmailAddress.isEmpty()) {
request.setUserAuthenticationEmail(buyerEmailAddress);
}

if (!buyerPhoneCountryCode.isEmpty() && !buyerPhoneNationalNumber.isEmpty()) {
request.setUserPhoneNumber(new PayPalPhoneNumber(buyerPhoneCountryCode, buyerPhoneNationalNumber));
if ((buyerPhoneCountryCode != null && !buyerPhoneCountryCode.isEmpty())
&& (buyerPhoneNationalNumber != null && !buyerPhoneNationalNumber.isEmpty())) {
request.setUserPhoneNumber(new PayPalPhoneNumber(
buyerPhoneCountryCode,
buyerPhoneNationalNumber)
);
}

if (shopperInsightsSessionId != null && !shopperInsightsSessionId.isEmpty()) {
request.setShopperSessionId(shopperInsightsSessionId);
}

request.setDisplayName(Settings.getPayPalDisplayName(context));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import com.braintreepayments.api.paypal.PayPalPaymentAuthRequest
import com.braintreepayments.api.paypal.PayPalPaymentAuthResult
import com.braintreepayments.api.paypal.PayPalPendingRequest
import com.braintreepayments.api.paypal.PayPalResult
import com.braintreepayments.api.shopperinsights.ButtonOrder
import com.braintreepayments.api.shopperinsights.ButtonType
import com.braintreepayments.api.shopperinsights.ExperimentType
import com.braintreepayments.api.shopperinsights.PageType
import com.braintreepayments.api.shopperinsights.PresentmentDetails
import com.braintreepayments.api.shopperinsights.ShopperInsightsBuyerPhone
import com.braintreepayments.api.shopperinsights.ShopperInsightsClient
import com.braintreepayments.api.shopperinsights.ShopperInsightsRequest
Expand Down Expand Up @@ -60,12 +65,14 @@ class ShopperInsightsFragment : BaseFragment() {
private lateinit var venmoStartedPendingRequest: VenmoPendingRequest.Started
private lateinit var paypalStartedPendingRequest: PayPalPendingRequest.Started

private var shopperSessionId: String = "test-shopper-session-id"

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
shopperInsightsClient = ShopperInsightsClient(requireContext(), authStringArg)
shopperInsightsClient = ShopperInsightsClient(requireContext(), authStringArg, shopperSessionId)

venmoClient = VenmoClient(requireContext(), super.getAuthStringArg(), null)
payPalClient = PayPalClient(
Expand Down Expand Up @@ -192,17 +199,25 @@ class ShopperInsightsFragment : BaseFragment() {
is ShopperInsightsResult.Success -> {
if (result.response.isPayPalRecommended) {
payPalVaultButton.isEnabled = true
shopperInsightsClient.sendPayPalPresentedEvent(
"""{"exp_name":"PaymentReady","treatment_name":"control"}""",
listOf("PayPal", "Venmo", "other")
shopperInsightsClient.sendPresentedEvent(
ButtonType.PAYPAL,
PresentmentDetails(
ExperimentType.TEST,
ButtonOrder.FIRST,
PageType.HOMEPAGE
)
)
}

if (result.response.isVenmoRecommended) {
venmoButton.isEnabled = true
shopperInsightsClient.sendVenmoPresentedEvent(
"""{"exp_name":"PaymentReady","treatment_name":"test"}""",
listOf("Venmo", "PayPal", "other")
shopperInsightsClient.sendPresentedEvent(
ButtonType.VENMO,
PresentmentDetails(
ExperimentType.TEST,
ButtonOrder.OTHER,
PageType.HOMEPAGE
)
)
}

Expand All @@ -222,15 +237,19 @@ class ShopperInsightsFragment : BaseFragment() {
}

private fun launchPayPalVault() {
shopperInsightsClient.sendPayPalSelectedEvent()
shopperInsightsClient.sendSelectedEvent(
ButtonType.PAYPAL
)

payPalClient.createPaymentAuthRequest(
requireContext(),
PayPalRequestFactory.createPayPalVaultRequest(
activity,
emailInput.editText?.text.toString(),
countryCodeInput.editText?.text.toString(),
nationalNumberInput.editText?.text.toString()
nationalNumberInput.editText?.text.toString(),
shopperSessionId

)
) { authRequest ->
when (authRequest) {
Expand Down Expand Up @@ -258,7 +277,9 @@ class ShopperInsightsFragment : BaseFragment() {
}

private fun launchVenmo() {
shopperInsightsClient.sendVenmoSelectedEvent()
shopperInsightsClient.sendSelectedEvent(
ButtonType.VENMO
)

val venmoRequest = VenmoRequest(VenmoPaymentMethodUsage.SINGLE_USE)
venmoRequest.profileId = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.text.TextUtils
import com.braintreepayments.api.core.Authorization
import com.braintreepayments.api.core.ClientToken
import com.braintreepayments.api.core.Configuration
import com.braintreepayments.api.core.ExperimentalBetaApi
import com.braintreepayments.api.core.PostalAddress
import com.braintreepayments.api.core.PostalAddressParser
import kotlinx.parcelize.Parcelize
Expand Down Expand Up @@ -91,6 +92,7 @@ class PayPalCheckoutRequest @JvmOverloads constructor(
lineItems = lineItems
) {

@OptIn(ExperimentalBetaApi::class)
@Throws(JSONException::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
override fun createRequestBody(
Expand Down Expand Up @@ -126,6 +128,8 @@ class PayPalCheckoutRequest @JvmOverloads constructor(

userPhoneNumber?.let { parameters.put(PHONE_NUMBER_KEY, it.toJson()) }

parameters.putOpt(SHOPPER_SESSION_ID, shopperSessionId)

if (currencyCode == null) {
currencyCode = configuration?.payPalCurrencyIsoCode
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.braintreepayments.api.core.BraintreeClient
import com.braintreepayments.api.core.BraintreeException
import com.braintreepayments.api.core.BraintreeRequestCodes
import com.braintreepayments.api.core.Configuration
import com.braintreepayments.api.core.ExperimentalBetaApi
import com.braintreepayments.api.core.GetReturnLinkUseCase
import com.braintreepayments.api.core.LinkType
import com.braintreepayments.api.core.MerchantRepository
Expand All @@ -27,7 +28,6 @@ class PayPalClient internal constructor(
private val merchantRepository: MerchantRepository = MerchantRepository.instance,
private val getReturnLinkUseCase: GetReturnLinkUseCase = GetReturnLinkUseCase(merchantRepository)
) {

/**
* Used for linking events from the client to server side request
* In the PayPal flow this will be either an EC token or a Billing Agreement token
Expand All @@ -44,6 +44,11 @@ class PayPalClient internal constructor(
*/
private var isVaultRequest = false

/**
* Used for sending Shopper Insights session ID provided by merchant to FPTI
*/
private var shopperSessionId: String? = null

/**
* The final URL string used to open the app switch flow
*/
Expand Down Expand Up @@ -82,11 +87,13 @@ class PayPalClient internal constructor(
* @param payPalRequest a [PayPalRequest] used to customize the request.
* @param callback [PayPalPaymentAuthCallback]
*/
@OptIn(ExperimentalBetaApi::class)
fun createPaymentAuthRequest(
context: Context,
payPalRequest: PayPalRequest,
callback: PayPalPaymentAuthCallback
) {
shopperSessionId = payPalRequest.shopperSessionId
isVaultRequest = payPalRequest is PayPalVaultRequest

braintreeClient.sendAnalyticsEvent(PayPalAnalytics.TOKENIZATION_STARTED, analyticsParams)
Expand Down Expand Up @@ -363,7 +370,8 @@ class PayPalClient internal constructor(
return AnalyticsEventParams(
payPalContextId = payPalContextId,
linkType = linkType?.stringValue,
isVaultRequest = isVaultRequest
isVaultRequest = isVaultRequest,
shopperSessionId = shopperSessionId
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.os.Parcelable
import androidx.annotation.RestrictTo
import com.braintreepayments.api.core.Authorization
import com.braintreepayments.api.core.Configuration
import com.braintreepayments.api.core.ExperimentalBetaApi
import com.braintreepayments.api.core.PostalAddress
import org.json.JSONException

Expand Down Expand Up @@ -72,6 +73,8 @@ import org.json.JSONException
* where the user has a PayPal Account with the same email.
* @property userPhoneNumber User phone number used to initiate a quicker authentication flow in
* cases where the user has a PayPal Account with the phone number.
* @property shopperSessionId the shopper session ID returned from your shopper insights server SDK
* integration
* @property lineItems The line items for this transaction. It can include up to 249 line items.
*/
abstract class PayPalRequest internal constructor(
Expand All @@ -87,6 +90,9 @@ abstract class PayPalRequest internal constructor(
open var riskCorrelationId: String? = null,
open var userAuthenticationEmail: String? = null,
open var userPhoneNumber: PayPalPhoneNumber? = null,

@property:ExperimentalBetaApi
open var shopperSessionId: String? = null,
open var lineItems: List<PayPalLineItem> = emptyList()
) : Parcelable {

Expand Down Expand Up @@ -132,5 +138,6 @@ abstract class PayPalRequest internal constructor(
internal const val PLAN_TYPE_KEY: String = "plan_type"
internal const val PLAN_METADATA_KEY: String = "plan_metadata"
internal const val PHONE_NUMBER_KEY: String = "phone_number"
internal const val SHOPPER_SESSION_ID: String = "shopper_session_id"
}
}
Loading
Loading