Skip to content

Commit

Permalink
Merge branch 'main' into contact-information-feature
Browse files Browse the repository at this point in the history
# Conflicts:
#	CHANGELOG.md
  • Loading branch information
jaxdesmarais committed Dec 16, 2024
2 parents cdda934 + 8c181f1 commit acb1d2e
Show file tree
Hide file tree
Showing 1,221 changed files with 6,870 additions and 1,795 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import androidx.annotation.RestrictTo
import com.braintreepayments.api.sharedutils.HttpResponseCallback
import com.braintreepayments.api.sharedutils.HttpResponseTiming
import com.braintreepayments.api.sharedutils.ManifestValidator
import com.braintreepayments.api.sharedutils.Time
import org.json.JSONException
import org.json.JSONObject

Expand All @@ -22,12 +21,12 @@ class BraintreeClient internal constructor(
authorization: Authorization,
returnUrlScheme: String,
appLinkReturnUri: Uri?,
deepLinkFallbackUrlScheme: String? = null,
sdkComponent: SdkComponent = SdkComponent.create(applicationContext),
private val httpClient: BraintreeHttpClient = BraintreeHttpClient(),
private val graphQLClient: BraintreeGraphQLClient = BraintreeGraphQLClient(),
private val configurationLoader: ConfigurationLoader = ConfigurationLoader.instance,
private val manifestValidator: ManifestValidator = ManifestValidator(),
private val time: Time = Time(),
private val merchantRepository: MerchantRepository = MerchantRepository.instance,
private val analyticsClient: AnalyticsClient = AnalyticsClient(),
) {
Expand All @@ -47,13 +46,15 @@ class BraintreeClient internal constructor(
returnUrlScheme: String? = null,
appLinkReturnUri: Uri? = null,
integrationType: IntegrationType? = null,
deepLinkFallbackUrlScheme: String? = null,
) : this(
applicationContext = context.applicationContext,
authorization = Authorization.fromString(authorization),
returnUrlScheme = returnUrlScheme
?: "${getAppPackageNameWithoutUnderscores(context.applicationContext)}.braintree",
appLinkReturnUri = appLinkReturnUri,
integrationType = integrationType ?: IntegrationType.CUSTOM,
deepLinkFallbackUrlScheme = deepLinkFallbackUrlScheme
)

init {
Expand All @@ -73,6 +74,9 @@ class BraintreeClient internal constructor(
if (appLinkReturnUri != null) {
it.appLinkReturnUri = appLinkReturnUri
}
if (deepLinkFallbackUrlScheme != null) {
it.deepLinkFallbackUrlScheme = deepLinkFallbackUrlScheme
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.braintreepayments.api.core

import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.annotation.RestrictTo
import com.braintreepayments.api.core.GetReturnLinkUseCase.ReturnLinkResult

/**
* Use case that returns a return link that should be used for navigating from App Switch / CCT back into the merchant
* app. It handles both App Links and Deep Links.
*
* If a user unchecks the "Open supported links" checkbox in the Android OS settings for the merchant's app. If this
* setting is unchecked, this use case will return [ReturnLinkResult.DeepLink], otherwise [ReturnLinkResult.AppLink]
* will be returned.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class GetReturnLinkUseCase(private val merchantRepository: MerchantRepository) {

sealed class ReturnLinkResult {
data class AppLink(val appLinkReturnUri: Uri) : ReturnLinkResult()

data class DeepLink(val deepLinkFallbackUrlScheme: String) : ReturnLinkResult()

data class Failure(val exception: Exception) : ReturnLinkResult()
}

operator fun invoke(): ReturnLinkResult {
val context = merchantRepository.applicationContext
val intent = Intent(Intent.ACTION_VIEW, merchantRepository.appLinkReturnUri).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
}
val resolvedActivity = context.packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
return if (resolvedActivity?.activityInfo?.packageName == context.packageName) {
merchantRepository.appLinkReturnUri?.let {
ReturnLinkResult.AppLink(it)
} ?: run {
ReturnLinkResult.Failure(BraintreeException("App Link Return Uri is null"))
}
} else {
merchantRepository.deepLinkFallbackUrlScheme?.let {
ReturnLinkResult.DeepLink(it)
} ?: run {
ReturnLinkResult.Failure(BraintreeException("Deep Link fallback return url is null"))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ class MerchantRepository {
lateinit var returnUrlScheme: String
var appLinkReturnUri: Uri? = null

var deepLinkFallbackUrlScheme: String? = null

companion object {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import com.braintreepayments.api.BrowserSwitchClient
import com.braintreepayments.api.sharedutils.HttpResponseCallback
import com.braintreepayments.api.sharedutils.ManifestValidator
import com.braintreepayments.api.sharedutils.NetworkResponseCallback
import com.braintreepayments.api.sharedutils.Time
import com.braintreepayments.api.testutils.Fixtures
import io.mockk.*
import org.json.JSONException
Expand Down Expand Up @@ -319,10 +318,7 @@ class BraintreeClientUnitTest {
.configuration(configuration)
.build()

val time: Time = mockk()
every { time.currentTime } returns 123

val sut = createBraintreeClient(configurationLoader, time)
val sut = createBraintreeClient(configurationLoader)
sut.sendAnalyticsEvent("event.started")

verify {
Expand Down Expand Up @@ -432,7 +428,6 @@ class BraintreeClientUnitTest {

private fun createBraintreeClient(
configurationLoader: ConfigurationLoader = mockk(),
time: Time = Time(),
appLinkReturnUri: Uri? = Uri.parse("https://example.com"),
merchantRepository: MerchantRepository = MerchantRepository.instance
) = BraintreeClient(
Expand All @@ -446,7 +441,6 @@ class BraintreeClientUnitTest {
analyticsClient = analyticsClient,
manifestValidator = manifestValidator,
configurationLoader = configurationLoader,
time = time,
merchantRepository = merchantRepository,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.braintreepayments.api.core

import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.ResolveInfo
import android.net.Uri
import io.mockk.every
import io.mockk.mockk
import org.junit.Before
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

@RunWith(RobolectricTestRunner::class)
class GetReturnLinkUseCaseUnitTest {

private val merchantRepository: MerchantRepository = mockk(relaxed = true)
private val context: Context = mockk(relaxed = true)
private val resolveInfo = ResolveInfo()
private val activityInfo = ActivityInfo()
private val contextPackageName = "context.package.name"
private val appLinkReturnUri = Uri.parse("https://example.com")
private val deepLinkFallbackUrlScheme = "com.braintreepayments.demo"

lateinit var subject: GetReturnLinkUseCase

@Before
fun setUp() {
every { merchantRepository.applicationContext } returns context
every { merchantRepository.appLinkReturnUri } returns appLinkReturnUri
every { merchantRepository.deepLinkFallbackUrlScheme } returns deepLinkFallbackUrlScheme
every { context.packageName } returns contextPackageName
resolveInfo.activityInfo = activityInfo
every { context.packageManager.resolveActivity(any<Intent>(), any<Int>()) } returns resolveInfo

subject = GetReturnLinkUseCase(merchantRepository)
}

@Test
fun `when invoke is called and app link is available, APP_LINK is returned`() {
activityInfo.packageName = "context.package.name"

val result = subject()

assertEquals(GetReturnLinkUseCase.ReturnLinkResult.AppLink(appLinkReturnUri), result)
}

@Test
fun `when invoke is called and app link is not available, DEEP_LINK is returned`() {
activityInfo.packageName = "different.package.name"

val result = subject()

assertEquals(GetReturnLinkUseCase.ReturnLinkResult.DeepLink(deepLinkFallbackUrlScheme), result)
}

@Test
fun `when invoke is called and deep link is available but null, Failure is returned`() {
activityInfo.packageName = "different.package.name"
every { merchantRepository.deepLinkFallbackUrlScheme } returns null

val result = subject()

assertTrue { result is GetReturnLinkUseCase.ReturnLinkResult.Failure }
assertEquals(
"Deep Link fallback return url is null",
(result as GetReturnLinkUseCase.ReturnLinkResult.Failure).exception.message
)
}

@Test
fun `when invoke is called and app link is available but null, Failure is returned`() {
activityInfo.packageName = "context.package.name"
every { merchantRepository.appLinkReturnUri } returns null

val result = subject()

assertTrue { result is GetReturnLinkUseCase.ReturnLinkResult.Failure }
assertEquals(
"App Link Return Uri is null",
(result as GetReturnLinkUseCase.ReturnLinkResult.Failure).exception.message
)
}
}
17 changes: 14 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
# Braintree Android SDK Release Notes

## unreleased

* LocalPayment
* Make LocalPaymentAuthRequestParams public (fixes #1207)
* PayPal
* Add `PayPalContactInformation` request object
* Add `PayPalCheckoutRequest.contactInformation` optional property

## 5.3.0 (2024-12-11)

* PayPal
* Add `deepLinkFallbackUrlScheme` to `PayPalClient` constructor params for supporting deep link fallback
* Send `url` in `event_params` for App Switch events to PayPal's analytics service (FPTI)
* LocalPayment
* Make LocalPaymentAuthRequestParams public (fixes #1207)
* ThreeDSecure
* Add `ThreeDSecureRequest.requestorAppUrl`
* Venmo
* Add `VenmoClient` constructor with `appLinkReturnUri` argument to use App Links when redirecting back from the Venmo flow
* Add `deepLinkFallbackUrlScheme` to `VenmoClient` constructor params for supporting deep link fallback
* Deprecate `VenmoClient` constructor with `returnUrlScheme` argument

## 5.2.0 (2024-10-30)

* GooglePay
Expand Down
1 change: 1 addition & 0 deletions Demo/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

<data android:scheme="https" />
<data android:host="mobile-sdk-demo-site-838cead5d3ab.herokuapp.com" />
<data android:pathPrefix="/braintree-payments" />
</intent-filter>
</activity>
</application>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,10 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
});

payPalClient = new PayPalClient(
requireContext(),
super.getAuthStringArg(),
Uri.parse("https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments")
requireContext(),
super.getAuthStringArg(),
Uri.parse("https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments"),
"com.braintreepayments.demo.braintree"
);
payPalLauncher = new PayPalLauncher();

Expand Down
4 changes: 4 additions & 0 deletions Demo/src/main/java/com/braintreepayments/demo/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ public static boolean vaultVenmo(Context context) {
return getPreferences(context).getBoolean("vault_venmo", true);
}

public static boolean useAppLinkReturn(Context context) {
return getPreferences(context).getBoolean("use_app_link_return", true);
}

public static boolean isAmexRewardsBalanceEnabled(Context context) {
return getPreferences(context).getBoolean("amex_rewards_balance", false);
}
Expand Down
31 changes: 21 additions & 10 deletions Demo/src/main/java/com/braintreepayments/demo/VenmoFragment.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.braintreepayments.demo;

import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
Expand All @@ -13,6 +14,7 @@
import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment;

import com.braintreepayments.api.core.UserCanceledException;
import com.braintreepayments.api.venmo.VenmoAccountNonce;
import com.braintreepayments.api.venmo.VenmoClient;
import com.braintreepayments.api.venmo.VenmoLauncher;
Expand All @@ -24,7 +26,6 @@
import com.braintreepayments.api.venmo.VenmoPendingRequest;
import com.braintreepayments.api.venmo.VenmoRequest;
import com.braintreepayments.api.venmo.VenmoResult;
import com.braintreepayments.api.core.UserCanceledException;

import java.util.ArrayList;

Expand All @@ -41,6 +42,18 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
venmoButton = view.findViewById(R.id.venmo_button);
venmoButton.setOnClickListener(this::launchVenmo);

if (venmoClient == null) {
if (Settings.useAppLinkReturn(requireContext())) {
venmoClient = new VenmoClient(
requireContext(),
super.getAuthStringArg(),
Uri.parse("https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments"),
"com.braintreepayments.demo.braintree"
);
} else {
venmoClient = new VenmoClient(requireContext(), super.getAuthStringArg());
}
}
venmoLauncher = new VenmoLauncher();

return view;
Expand Down Expand Up @@ -70,27 +83,24 @@ private void handleVenmoResult(VenmoResult result) {
handleError(new UserCanceledException("User canceled Venmo"));
}
}

private void handleVenmoAccountNonce(VenmoAccountNonce venmoAccountNonce) {
super.onPaymentMethodNonceCreated(venmoAccountNonce);

NavDirections action =
VenmoFragmentDirections.actionVenmoFragmentToDisplayNonceFragment(venmoAccountNonce);
NavDirections action = VenmoFragmentDirections.actionVenmoFragmentToDisplayNonceFragment(venmoAccountNonce);
NavHostFragment.findNavController(this).navigate(action);
}

public void launchVenmo(View v) {
getActivity().setProgressBarIndeterminateVisibility(true);
if (venmoClient == null) {
venmoClient = new VenmoClient(requireContext(), super.getAuthStringArg(), null);
}

FragmentActivity activity = getActivity();

getActivity().setProgressBarIndeterminateVisibility(true);

boolean shouldVault =
Settings.vaultVenmo(activity) && !TextUtils.isEmpty(Settings.getCustomerId(activity));
Settings.vaultVenmo(activity) && !TextUtils.isEmpty(Settings.getCustomerId(activity));

VenmoPaymentMethodUsage venmoPaymentMethodUsage = shouldVault ?
VenmoPaymentMethodUsage.MULTI_USE : VenmoPaymentMethodUsage.SINGLE_USE;
VenmoPaymentMethodUsage.MULTI_USE : VenmoPaymentMethodUsage.SINGLE_USE;
VenmoRequest venmoRequest = new VenmoRequest(venmoPaymentMethodUsage);
venmoRequest.setProfileId(null);
venmoRequest.setShouldVault(shouldVault);
Expand Down Expand Up @@ -130,6 +140,7 @@ private void completeVenmoFlow(VenmoPaymentAuthResult.Success paymentAuthResult)
private void storePendingRequest(VenmoPendingRequest.Started request) {
PendingRequestStore.getInstance().putVenmoPendingRequest(requireContext(), request);
}

private VenmoPendingRequest.Started getPendingRequest() {
return PendingRequestStore.getInstance().getVenmoPendingRequest(requireContext());
}
Expand Down
2 changes: 2 additions & 0 deletions Demo/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@
<string name="venmo">Venmo</string>
<string name="vault_venmo">Vault Venmo</string>
<string name="vault_venmo_summary">Vault Venmo payment methods on creation. Requires a customer id to be set.</string>
<string name="use_app_links_return">Use App Link</string>
<string name="use_app_links_return_summary">Use merchant App Link, instead of deeplink, when redirecting back from Venmo flow.</string>
<string name="amex">Amex</string>
<string name="amex_rewards_balance">Get Rewards Balance</string>
<string name="amex_rewards_balance_summary">Fetch Amex Rewards Balance on Card Tokenization. Requires a Client Token and relevant configurations.</string>
Expand Down
6 changes: 6 additions & 0 deletions Demo/src/main/res/xml/settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,12 @@
android:summary="@string/vault_venmo_summary"
android:defaultValue="true" />

<CheckBoxPreference
android:key="use_app_links_return"
android:title="@string/use_app_links_return"
android:summary="@string/use_app_links_return_summary"
android:defaultValue="true" />

</PreferenceCategory>

<PreferenceCategory
Expand Down
Loading

0 comments on commit acb1d2e

Please sign in to comment.