Skip to content

Commit

Permalink
Merge pull request #1256 from braintree/shopper-insights-rp2-feature
Browse files Browse the repository at this point in the history
Shopper insights feature
  • Loading branch information
jaxdesmarais authored Feb 4, 2025
2 parents 39d43af + 5b96e6f commit e496096
Show file tree
Hide file tree
Showing 23 changed files with 541 additions and 129 deletions.
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
)
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Braintree Android SDK Release Notes

## unreleased

* ShopperInsights (BETA)
* Add `isPayPalAppInstalled` and `isVenmoAppInstalled` methods
* Add `shopperSessionId` parameter to `ShopperInsightsClient`
* PayPal
* Add `shopperSessionId` to `PayPalCheckoutRequest` and `PayPalVaultRequest`
* Replace `sendPayPalPresentedEvent()` and `sendVenmoPresentedEvent()` with `sendPresentedEvent()`
* Replace `sendPayPalSelectedEvent()` and `sendVenmoSelectedEvent()` with `sendSelectedEvent()`

## 5.5.0 (2025-01-23)

* PayPal
Expand All @@ -14,7 +24,7 @@
* Add `shippingCallbackUrl` to `PayPalCheckoutRequest`
* 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 @@ -155,7 +155,8 @@ private void launchPayPal(
activity,
buyerEmailAddress,
buyerPhoneCountryCode,
buyerPhoneNationalNumber
buyerPhoneNationalNumber,
"fake-session-id"
);
} else {
payPalRequest = createPayPalCheckoutRequest(
Expand All @@ -164,7 +165,8 @@ private void launchPayPal(
buyerEmailAddress,
buyerPhoneCountryCode,
buyerPhoneNationalNumber,
isContactInformationEnabled
isContactInformationEnabled,
null
);
}
payPalClient.createPaymentAuthRequest(requireContext(), payPalRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,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 @@ -114,16 +124,25 @@ public static PayPalCheckoutRequest createPayPalCheckoutRequest(
String buyerEmailAddress,
String buyerPhoneCountryCode,
String buyerPhoneNationalNumber,
Boolean isContactInformationEnabled
Boolean isContactInformationEnabled,
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,18 +65,21 @@ 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(
requireContext(),
super.getAuthStringArg(),
Uri.parse("https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments"),
"com.braintreepayments.demo.braintree"
)

return inflater.inflate(R.layout.fragment_shopping_insights, container, false)
Expand Down Expand Up @@ -192,17 +200,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 +238,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 +278,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 @@ -5,6 +5,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 @@ -100,6 +101,7 @@ class PayPalCheckoutRequest @JvmOverloads constructor(
lineItems = lineItems
) {

@OptIn(ExperimentalBetaApi::class)
@Throws(JSONException::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
override fun createRequestBody(
Expand Down Expand Up @@ -144,6 +146,8 @@ class PayPalCheckoutRequest @JvmOverloads constructor(
info.recipentPhoneNumber?.let { parameters.put(RECIPIENT_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 @@ -135,5 +141,6 @@ abstract class PayPalRequest internal constructor(
internal const val SHIPPING_CALLBACK_URL_KEY: String = "shipping_callback_url"
internal const val RECIPIENT_EMAIL_KEY: String = "recipient_email"
internal const val RECIPIENT_PHONE_NUMBER_KEY: String = "international_phone"
internal const val SHOPPER_SESSION_ID: String = "shopper_session_id"
}
}
Loading

0 comments on commit e496096

Please sign in to comment.