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

[v7 Beta] Create DropInLauncher #468

Open
wants to merge 20 commits into
base: v7_beta
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7118268
Rename DropInIntentData to DropInLaunchIntent.
sshropshire Mar 27, 2024
257d30e
Remove deprecated DropInClient methods.
sshropshire Mar 27, 2024
e6a98ac
Rename DropInLifecycleObserver to DropInLauncher.
sshropshire Mar 27, 2024
7a70517
Extract DropInLauncher success and error handling logic from DropInCl…
sshropshire Mar 27, 2024
7ca4634
Remove invalidateClientToken() method.
sshropshire Mar 27, 2024
7ad19eb
Remove DropInClient#getAuthorization() internal call.
sshropshire Mar 27, 2024
3247387
Fix DropInLauncherUnitTest.
sshropshire Mar 27, 2024
7978016
Migrate DropInClient to using context.
sshropshire Mar 27, 2024
4ae9532
Migrate fetch method to a RecentPaymentMethodsClient.
sshropshire Mar 27, 2024
4f85b19
Remove DropInClient and fix broken imports.
sshropshire Mar 27, 2024
9d00464
Fix unit tests.
sshropshire Mar 27, 2024
6457aa5
Remove DropInClientParams class.
sshropshire Mar 27, 2024
6810ac7
Add class and constructor documentation to RecentPaymentMethodsClient.
sshropshire Mar 27, 2024
1a842ce
Provide documentation for DropInLauncherCallback.
sshropshire Mar 27, 2024
8220458
Rename DropInLaunchIntent to DropInLaunchInput and make it package pr…
sshropshire Mar 27, 2024
e0658c3
Add place authorization string in DropInRequest.
sshropshire Apr 3, 2024
7bdd2ac
Revert "Add place authorization string in DropInRequest."
sshropshire Apr 3, 2024
618d3f8
Add TODO.
sshropshire Apr 3, 2024
821b249
Update CHANGELOG.
sshropshire Apr 3, 2024
1977a95
Rename launchDropIn to start on DropInLauncher.
sshropshire Apr 8, 2024
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
Prev Previous commit
Next Next commit
Migrate fetch method to a RecentPaymentMethodsClient.
sshropshire committed Mar 27, 2024
commit 4ae9532310e53c97281516405581747dd241ee7d
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.braintreepayments.api;

import android.content.Context;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.FragmentActivity;

public class RecentPaymentMethodsClient {

private final BraintreeClient braintreeClient;

private final GooglePayClient googlePayClient;

private final PaymentMethodClient paymentMethodClient;
private final DropInSharedPreferences sharedPreferences;

RecentPaymentMethodsClient(@NonNull Context context, @NonNull String authorization) {
this(context, new BraintreeClient(context, authorization));
}

private RecentPaymentMethodsClient(
@NonNull Context context,
@NonNull BraintreeClient braintreeClient
) {
this(
context,
braintreeClient,
new GooglePayClient(braintreeClient),
new PaymentMethodClient(braintreeClient),
DropInSharedPreferences.getInstance(context)
);
}

@VisibleForTesting
RecentPaymentMethodsClient(
@NonNull Context context,
@NonNull BraintreeClient braintreeClient,
@NonNull GooglePayClient googlePayClient,
@NonNull PaymentMethodClient paymentMethodClient,
@NonNull DropInSharedPreferences sharedPreferences
) {
this.braintreeClient = braintreeClient;
this.googlePayClient = new GooglePayClient(braintreeClient);
this.paymentMethodClient = new PaymentMethodClient(braintreeClient);
this.sharedPreferences = sharedPreferences;
}

/**
* Called to get a user's existing payment method, if any.
* The payment method returned is not guaranteed to be the most recently added payment method.
* If your user already has an existing payment method, you may not need to show Drop-In.
* <p>
* Note: a client token must be used and will only return a payment method if it contains a
* customer id.
*
* @param activity the current {@link FragmentActivity}
* @param callback callback for handling result
*/
// NEXT_MAJOR_VERSION: - update this function name to more accurately represent the behavior of the function
public void fetchMostRecentPaymentMethod(FragmentActivity activity, final FetchMostRecentPaymentMethodCallback callback) {
braintreeClient.getAuthorization((authorization, authError) -> {
if (authorization != null) {

boolean isClientToken = (authorization instanceof ClientToken);
if (!isClientToken) {
InvalidArgumentException clientTokenRequiredError =
new InvalidArgumentException("DropInClient#fetchMostRecentPaymentMethods() must " +
"be called with a client token");
callback.onResult(null, clientTokenRequiredError);
return;
}

DropInPaymentMethod lastUsedPaymentMethod =
sharedPreferences.getLastUsedPaymentMethod();

if (lastUsedPaymentMethod == DropInPaymentMethod.GOOGLE_PAY) {
googlePayClient.isReadyToPay(activity, (isReadyToPay, isReadyToPayError) -> {
if (isReadyToPay) {
DropInResult result = new DropInResult();
result.setPaymentMethodType(DropInPaymentMethod.GOOGLE_PAY);
callback.onResult(result, null);
} else {
getPaymentMethodNonces(callback);
}
});
} else {
getPaymentMethodNonces(callback);
}
} else {
callback.onResult(null, authError);
}
});
}

private void getPaymentMethodNonces(final FetchMostRecentPaymentMethodCallback callback) {
paymentMethodClient.getPaymentMethodNonces((paymentMethodNonceList, error) -> {
if (paymentMethodNonceList != null) {
DropInResult result = new DropInResult();
if (paymentMethodNonceList.size() > 0) {
PaymentMethodNonce paymentMethod = paymentMethodNonceList.get(0);
result.setPaymentMethodNonce(paymentMethod);
}
callback.onResult(result, null);
} else if (error != null) {
callback.onResult(null, error);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
package com.braintreepayments.api;

import static junit.framework.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;

import androidx.fragment.app.FragmentActivity;
import androidx.test.core.app.ApplicationProvider;
import androidx.work.testing.WorkManagerTestInitHelper;

import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.android.controller.ActivityController;

import java.util.ArrayList;

@RunWith(RobolectricTestRunner.class)
public class RecentPaymentMethodsClientUnitTest {

private FragmentActivity activity;

private Context context;
private DropInSharedPreferences dropInSharedPreferences;

@Before
public void beforeEach() {
context = ApplicationProvider.getApplicationContext();
dropInSharedPreferences = mock(DropInSharedPreferences.class);

ActivityController<FragmentActivity> activityController =
Robolectric.buildActivity(FragmentActivity.class);
activity = activityController.get();

// This suppresses errors from WorkManager initialization within BraintreeClient initialization (AnalyticsClient)
WorkManagerTestInitHelper.initializeTestWorkManager(context);
}

@Test
public void fetchMostRecentPaymentMethod_forwardsAuthorizationFetchErrors() {
Exception authError = new Exception("auth error");
BraintreeClient braintreeClient = new MockBraintreeClientBuilder()
.authorizationError(authError)
.build();

RecentPaymentMethodsClient sut = new RecentPaymentMethodsClient(
context,
braintreeClient,
new GooglePayClient(braintreeClient),
new PaymentMethodClient(braintreeClient),
dropInSharedPreferences
);

FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class);
sut.fetchMostRecentPaymentMethod(activity, callback);

verify(callback).onResult(null, authError);
}

@Test
public void fetchMostRecentPaymentMethod_callsBackWithErrorIfInvalidClientTokenWasUsed() {
BraintreeClient braintreeClient = new MockBraintreeClientBuilder()
.authorizationSuccess(Authorization.fromString(Fixtures.TOKENIZATION_KEY))
.build();

RecentPaymentMethodsClient sut = new RecentPaymentMethodsClient(
context,
braintreeClient,
new GooglePayClient(braintreeClient),
new PaymentMethodClient(braintreeClient),
dropInSharedPreferences
);

FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class);
sut.fetchMostRecentPaymentMethod(activity, callback);

ArgumentCaptor<InvalidArgumentException> captor = ArgumentCaptor.forClass(InvalidArgumentException.class);
verify(callback).onResult((DropInResult) isNull(), captor.capture());

InvalidArgumentException exception = captor.getValue();
assertEquals("DropInClient#fetchMostRecentPaymentMethods() must be called with a client token", exception.getMessage());
}

@Test
public void fetchMostRecentPaymentMethod_callsBackWithResultIfLastUsedPaymentMethodTypeWasPayWithGoogle() throws JSONException, JSONException {

GooglePayClient googlePayClient = new MockGooglePayClientBuilder()
.isReadyToPaySuccess(true)
.build();
BraintreeClient braintreeClient = new MockBraintreeClientBuilder()
.authorizationSuccess(Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN))
.configuration(Configuration.fromJson(Fixtures.CONFIGURATION_WITH_GOOGLE_PAY))
.build();

RecentPaymentMethodsClient sut = new RecentPaymentMethodsClient(
context,
braintreeClient,
googlePayClient,
new PaymentMethodClient(braintreeClient),
dropInSharedPreferences
);

when(
dropInSharedPreferences.getLastUsedPaymentMethod()
).thenReturn(DropInPaymentMethod.GOOGLE_PAY);

FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class);
sut.fetchMostRecentPaymentMethod(activity, callback);

ArgumentCaptor<DropInResult> captor = ArgumentCaptor.forClass(DropInResult.class);
verify(callback).onResult(captor.capture(), (Exception) isNull());

DropInResult result = captor.getValue();
assertEquals(DropInPaymentMethod.GOOGLE_PAY, result.getPaymentMethodType());
assertNull(result.getPaymentMethodNonce());
}

@Test
public void fetchMostRecentPaymentMethod_doesNotCallBackWithPayWithGoogleIfPayWithGoogleIsNotAvailable() throws JSONException {

GooglePayClient googlePayClient = new MockGooglePayClientBuilder()
.isReadyToPaySuccess(false)
.build();
BraintreeClient braintreeClient = new MockBraintreeClientBuilder()
.authorizationSuccess(Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN))
.configuration(Configuration.fromJson(Fixtures.CONFIGURATION_WITH_GOOGLE_PAY))
.build();

ArrayList<PaymentMethodNonce> paymentMethods = new ArrayList<>();
paymentMethods.add(CardNonce.fromJSON(new JSONObject(Fixtures.VISA_CREDIT_CARD_RESPONSE)));
paymentMethods.add(GooglePayCardNonce.fromJSON(new JSONObject(Fixtures.GOOGLE_PAY_NETWORK_TOKENIZED_RESPONSE)));
PaymentMethodClient paymentMethodClient = new MockPaymentMethodClientBuilder()
.getPaymentMethodNoncesSuccess(paymentMethods)
.build();

when(
dropInSharedPreferences.getLastUsedPaymentMethod()
).thenReturn(DropInPaymentMethod.GOOGLE_PAY);

RecentPaymentMethodsClient sut = new RecentPaymentMethodsClient(
context,
braintreeClient,
googlePayClient,
paymentMethodClient,
dropInSharedPreferences
);

FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class);
sut.fetchMostRecentPaymentMethod(activity, callback);

ArgumentCaptor<DropInResult> captor = ArgumentCaptor.forClass(DropInResult.class);
verify(callback).onResult(captor.capture(), (Exception) isNull());

DropInResult result = captor.getValue();
assertEquals(DropInPaymentMethod.VISA, result.getPaymentMethodType());
assertNotNull(result.getPaymentMethodNonce());
assertEquals("11", ((CardNonce) result.getPaymentMethodNonce()).getLastTwo());
}

@Test
public void fetchMostRecentPaymentMethod_callsBackWithErrorOnGetPaymentMethodsError() {
BraintreeClient braintreeClient = new MockBraintreeClientBuilder()
.authorizationSuccess(Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN))
.build();

PaymentMethodClient paymentMethodClient = new MockPaymentMethodClientBuilder()
.getPaymentMethodNoncesError(new BraintreeException("Error occurred"))
.build();

RecentPaymentMethodsClient sut = new RecentPaymentMethodsClient(
context,
braintreeClient,
new GooglePayClient(braintreeClient),
paymentMethodClient,
dropInSharedPreferences
);

FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class);
sut.fetchMostRecentPaymentMethod(activity, callback);

ArgumentCaptor<BraintreeException> captor = ArgumentCaptor.forClass(BraintreeException.class);
verify(callback).onResult((DropInResult) isNull(), captor.capture());

BraintreeException exception = captor.getValue();
assertEquals("Error occurred", exception.getMessage());
}

@Test
public void fetchMostRecentPaymentMethod_callsBackWithResultWhenThereIsAPaymentMethod()
throws JSONException {
BraintreeClient braintreeClient = new MockBraintreeClientBuilder()
.authorizationSuccess(Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN))
.configuration(Configuration.fromJson(Fixtures.CONFIGURATION_WITH_GOOGLE_PAY))
.build();

ArrayList<PaymentMethodNonce> paymentMethods = new ArrayList<>();
paymentMethods.add(CardNonce.fromJSON(new JSONObject(Fixtures.VISA_CREDIT_CARD_RESPONSE)));

PaymentMethodClient paymentMethodClient = new MockPaymentMethodClientBuilder()
.getPaymentMethodNoncesSuccess(paymentMethods)
.build();

RecentPaymentMethodsClient sut = new RecentPaymentMethodsClient(
context,
braintreeClient,
new GooglePayClient(braintreeClient),
paymentMethodClient,
dropInSharedPreferences
);

FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class);
sut.fetchMostRecentPaymentMethod(activity, callback);

ArgumentCaptor<DropInResult> captor = ArgumentCaptor.forClass(DropInResult.class);
verify(callback).onResult(captor.capture(), (Exception) isNull());

DropInResult result = captor.getValue();
assertEquals(DropInPaymentMethod.VISA, result.getPaymentMethodType());
assertNotNull(result.getPaymentMethodNonce());
assertEquals("11", ((CardNonce) result.getPaymentMethodNonce()).getLastTwo());
}

@Test
public void fetchMostRecentPaymentMethod_callsBackWithNullResultWhenThereAreNoPaymentMethods() throws JSONException {
BraintreeClient braintreeClient = new MockBraintreeClientBuilder()
.authorizationSuccess(Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN))
.configuration(Configuration.fromJson(Fixtures.CONFIGURATION_WITH_GOOGLE_PAY))
.build();

ArrayList<PaymentMethodNonce> paymentMethods = new ArrayList<>();

PaymentMethodClient paymentMethodClient = new MockPaymentMethodClientBuilder()
.getPaymentMethodNoncesSuccess(paymentMethods)
.build();

RecentPaymentMethodsClient sut = new RecentPaymentMethodsClient(
context,
braintreeClient,
new GooglePayClient(braintreeClient),
paymentMethodClient,
dropInSharedPreferences
);

FetchMostRecentPaymentMethodCallback callback = mock(FetchMostRecentPaymentMethodCallback.class);
sut.fetchMostRecentPaymentMethod(activity, callback);

ArgumentCaptor<DropInResult> captor = ArgumentCaptor.forClass(DropInResult.class);
verify(callback).onResult(captor.capture(), (Exception) isNull());

DropInResult result = captor.getValue();
assertNull(result.getPaymentMethodType());
assertNull(result.getPaymentMethodNonce());
}
}