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

Card Single Result Object #841

Merged
merged 9 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from 8 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@
* Card
* Remove `threeDSecureInfo` from `CardNonce`
* Move `ThreeDSecureInfo` to `three-d-secure` module
* Add `CardResult` object
* Change `CardTokenizeCallback` parameters
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we feel like it'd be helpful to merchants to know what the change is to?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are pretty much changing the parameters of every public method and callback throughout the SDK for this version, I've just been saying "change parameters" to keep the CHANGELOG less verbose. The detailed changes are in the migration guide though. So I don't know because yes it might be helpful to see the actual parameter changes, but also for every method that is changing the CHANGELOG will get very long and verbose. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to leave the generic parameters CHANGELOG notes for now but will reconsider how detailed we are for all of the changes in the CHANGELOG before the first beta

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think we were pretty verbose in the iOS changelog, it's quite long for the betas. No idea if it's helpful or not though.

* SEPA Direct Debit
* Remove `SEPADirectDebitLifecycleObserver` and `SEPADirectDebitListener`
* Add `SEPADirectDebitLauncher`, `SEPADirectDebitLauncherCallback`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,21 +122,20 @@ public void tokenize_failsWithTokenizationKeyAndValidateTrue() throws Exception

final CountDownLatch countDownLatch = new CountDownLatch(1);
CardClient sut = setupCardClient(TOKENIZATION_KEY);
sut.tokenize(card, new CardTokenizeCallback() {
@Override
public void onResult(CardNonce cardNonce, Exception error) {
assertTrue(error instanceof AuthorizationException);

if (requestProtocol.equals(GRAPHQL)) {
assertEquals("You are unauthorized to perform input validation with the provided authentication credentials.",
error.getMessage());
} else {
assertEquals("Tokenization key authorization not allowed for this endpoint. Please use an " +
"authentication method with upgraded permissions", error.getMessage());
}

countDownLatch.countDown();
sut.tokenize(card, cardResult -> {
assertTrue(cardResult instanceof CardResult.Failure);
Exception error = ((CardResult.Failure) cardResult).getError();
assertTrue(error instanceof AuthorizationException);

if (requestProtocol.equals(GRAPHQL)) {
assertEquals("You are unauthorized to perform input validation with the provided authentication credentials.",
error.getMessage());
} else {
assertEquals("Tokenization key authorization not allowed for this endpoint. Please use an " +
"authentication method with upgraded permissions", error.getMessage());
}

countDownLatch.countDown();
});

countDownLatch.await();
Expand Down Expand Up @@ -165,19 +164,18 @@ public void tokenize_tokenizesCvvOnly() throws Exception {
card.setCvv("123");

CardClient sut = setupCardClient(TOKENIZATION_KEY);
sut.tokenize(card, new CardTokenizeCallback() {
@Override
public void onResult(CardNonce cardNonce, Exception error) {

assertNotNull(cardNonce.getBinData());
assertEquals("Unknown", cardNonce.getCardType());
assertEquals("", cardNonce.getLastFour());
assertEquals("", cardNonce.getLastTwo());
assertFalse(cardNonce.isDefault());
assertNotNull(cardNonce.getString());

countDownLatch.countDown();
}
sut.tokenize(card, cardResult -> {
assertTrue(cardResult instanceof CardResult.Success);
CardNonce cardNonce = ((CardResult.Success) cardResult).getNonce();

assertNotNull(cardNonce.getBinData());
assertEquals("Unknown", cardNonce.getCardType());
assertEquals("", cardNonce.getLastFour());
assertEquals("", cardNonce.getLastTwo());
assertFalse(cardNonce.isDefault());
assertNotNull(cardNonce.getString());

countDownLatch.countDown();
});

countDownLatch.await();
Expand All @@ -196,7 +194,9 @@ public void tokenize_callsErrorCallbackForInvalidCvv() throws Exception {

final CountDownLatch countDownLatch = new CountDownLatch(1);
CardClient sut = setupCardClient(authorization);
sut.tokenize(card, (cardNonce, error) -> {
sut.tokenize(card, (cardResult) -> {
assertTrue(cardResult instanceof CardResult.Failure);
Exception error = ((CardResult.Failure) cardResult).getError();
assertEquals("CVV verification failed",
((ErrorWithResponse) error).errorFor("creditCard").getFieldErrors().get(0).getMessage());
countDownLatch.countDown();
Expand Down Expand Up @@ -231,7 +231,9 @@ public void tokenize_callsErrorCallbackForInvalidPostalCode() throws Exception {

final CountDownLatch countDownLatch = new CountDownLatch(1);
CardClient sut = setupCardClient(authorization);
sut.tokenize(card, (cardNonce, error) -> {
sut.tokenize(card, (cardResult) -> {
assertTrue(cardResult instanceof CardResult.Failure);
Exception error = ((CardResult.Failure) cardResult).getError();
assertEquals("Postal code verification failed",
((ErrorWithResponse) error).errorFor("creditCard").errorFor("billingAddress")
.getFieldErrors().get(0).getMessage());
Expand All @@ -254,7 +256,9 @@ public void tokenize_whenInvalidCountryCode_callsErrorCallbackWithDetailedError(

final CountDownLatch countDownLatch = new CountDownLatch(1);
CardClient sut = setupCardClient(authorization);
sut.tokenize(card, (cardNonce, error) -> {
sut.tokenize(card, (cardResult) -> {
assertTrue(cardResult instanceof CardResult.Failure);
Exception error = ((CardResult.Failure) cardResult).getError();
assertEquals("Country code (alpha3) is not an accepted country",
((ErrorWithResponse) error).errorFor("creditCard").errorFor("billingAddress")
.getFieldErrors().get(0).getMessage());
Expand Down Expand Up @@ -288,12 +292,14 @@ public void tokenize_tokenizesACardWithACompleteBillingAddress() throws Exceptio
}

private void assertTokenizationSuccessful(String authorization, Card card) throws Exception {
BraintreeClient braintreeClient = new BraintreeClient(new ClientParams(ApplicationProvider.getApplicationContext(), authorization));
BraintreeClient braintreeClient = new BraintreeClient(ApplicationProvider.getApplicationContext(), authorization);
CardClient sut = new CardClient(braintreeClient);

final CountDownLatch countDownLatch = new CountDownLatch(1);
sut.tokenize(card, (cardNonce, error) -> {
sut.tokenize(card, (cardResult) -> {

assertTrue(cardResult instanceof CardResult.Success);
CardNonce cardNonce = ((CardResult.Success) cardResult).getNonce();
assertNotNull(cardNonce.getString());
assertEquals("Visa", cardNonce.getCardType());
assertEquals("1111", cardNonce.getLastFour());
Expand All @@ -317,7 +323,7 @@ private void assertTokenizationSuccessful(String authorization, Card card) throw
}

private CardClient setupCardClient(String authorization) {
BraintreeClient braintreeClient = new BraintreeClient(new ClientParams(ApplicationProvider.getApplicationContext(), authorization));
BraintreeClient braintreeClient = new BraintreeClient(ApplicationProvider.getApplicationContext(), authorization);
return new CardClient(braintreeClient);
}

Expand Down
26 changes: 13 additions & 13 deletions Card/src/main/java/com/braintreepayments/api/CardClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import android.content.Context;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.json.JSONException;
Expand Down Expand Up @@ -46,25 +45,26 @@ public CardClient(@NonNull Context context, @NonNull String authorization) {
* The tokenization result is returned via a {@link CardTokenizeCallback} callback.
*
* <p>
* On success, the {@link CardTokenizeCallback#onResult(CardNonce, Exception)} method will be
* invoked with a nonce.
* On success, the {@link CardTokenizeCallback#onCardResult(CardResult)} method will be
* invoked with a {@link CardResult.Success} including a nonce.
*
* <p>
* If creation fails validation, the {@link CardTokenizeCallback#onResult(CardNonce, Exception)}
* method will be invoked with an {@link ErrorWithResponse} exception.
* If creation fails validation, the {@link CardTokenizeCallback#onCardResult(CardResult)}
* method will be invoked with a {@link CardResult.Failure} including an
* {@link ErrorWithResponse} exception.
*
* <p>
* If an error not due to validation (server error, network issue, etc.) occurs, the
* {@link CardTokenizeCallback#onResult(CardNonce, Exception)} method will be invoked with an
* {@link Exception} describing the error.
* {@link CardTokenizeCallback#onCardResult(CardResult)} method will be invoked with a
* {@link CardResult.Failure} with an {@link Exception} describing the error.
*
* @param card {@link Card}
* @param callback {@link CardTokenizeCallback}
*/
public void tokenize(@NonNull final Card card, @NonNull final CardTokenizeCallback callback) {
braintreeClient.getConfiguration((configuration, error) -> {
if (error != null) {
callback.onResult(null, error);
callback.onCardResult(new CardResult.Failure(error));
return;
}

Expand All @@ -80,7 +80,7 @@ public void tokenize(@NonNull final Card card, @NonNull final CardTokenizeCallba
(tokenizationResponse, exception) -> handleTokenizeResponse(
tokenizationResponse, exception, callback));
} catch (BraintreeException | JSONException e) {
callback.onResult(null, e);
callback.onCardResult(new CardResult.Failure(e));
}
} else {
apiClient.tokenizeREST(card,
Expand All @@ -96,15 +96,15 @@ private void handleTokenizeResponse(JSONObject tokenizationResponse, Exception e
try {
CardNonce cardNonce = CardNonce.fromJSON(tokenizationResponse);

callback.onResult(cardNonce, null);
callback.onCardResult(new CardResult.Success(cardNonce));
braintreeClient.sendAnalyticsEvent("card.nonce-received");

} catch (JSONException e) {
callback.onResult(null, e);
callback.onCardResult(new CardResult.Failure(e));
braintreeClient.sendAnalyticsEvent("card.nonce-failed");
}
} else {
callback.onResult(null, exception);
} else if (exception != null) {
callback.onCardResult(new CardResult.Failure(exception));
braintreeClient.sendAnalyticsEvent("card.nonce-failed");
}
}
Expand Down
17 changes: 17 additions & 0 deletions Card/src/main/java/com/braintreepayments/api/CardResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.braintreepayments.api

/**
* Result of tokenizing a [Card]
*/
sealed class CardResult {

/**
* The card tokenization completed successfully. This [nonce] should be sent to your server.
*/
class Success(val nonce: CardNonce) : CardResult()

/**
* There was an [error] during card tokenization.
*/
class Failure(val error: Exception) : CardResult()
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package com.braintreepayments.api;

import androidx.annotation.Nullable;

/**
* Callback for receiving result of {@link CardClient#tokenize(Card, CardTokenizeCallback)}.
*/
public interface CardTokenizeCallback {

/**
* @param cardNonce {@link CardNonce}
* @param error an exception that occurred while tokenizing card
* @param cardResult a {@link CardResult} containing a {@link CardNonce} or {@link Exception}
*/
void onResult(@Nullable CardNonce cardNonce, @Nullable Exception error);
void onCardResult(CardResult cardResult);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.braintreepayments.api;

import static junit.framework.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
Expand Down Expand Up @@ -75,10 +76,12 @@ public void tokenize_whenGraphQLEnabled_tokenizesWithGraphQL() throws JSONExcept

sut.tokenize(card, cardTokenizeCallback);

ArgumentCaptor<CardNonce> captor = ArgumentCaptor.forClass(CardNonce.class);
verify(cardTokenizeCallback).onResult(captor.capture(), isNull());
ArgumentCaptor<CardResult> captor = ArgumentCaptor.forClass(CardResult.class);
verify(cardTokenizeCallback).onCardResult(captor.capture());

CardNonce cardNonce = captor.getValue();
CardResult result = captor.getValue();
assertTrue(result instanceof CardResult.Success);
CardNonce cardNonce = ((CardResult.Success) result).getNonce();
assertEquals("3744a73e-b1ab-0dbd-85f0-c12a0a4bd3d1", cardNonce.getString());
}

Expand All @@ -96,10 +99,12 @@ public void tokenize_whenGraphQLDisabled_tokenizesWithREST() throws JSONExceptio

sut.tokenize(card, cardTokenizeCallback);

ArgumentCaptor<CardNonce> captor = ArgumentCaptor.forClass(CardNonce.class);
verify(cardTokenizeCallback).onResult(captor.capture(), isNull());
ArgumentCaptor<CardResult> captor = ArgumentCaptor.forClass(CardResult.class);
verify(cardTokenizeCallback).onCardResult(captor.capture());

CardNonce cardNonce = captor.getValue();
CardResult result = captor.getValue();
assertTrue(result instanceof CardResult.Success);
CardNonce cardNonce = ((CardResult.Success) result).getNonce();
assertEquals("123456-12345-12345-a-adfa", cardNonce.getString());
}

Expand Down Expand Up @@ -149,7 +154,13 @@ public void tokenize_whenGraphQLEnabled_callsListenerWithErrorOnFailure() {
CardClient sut = new CardClient(braintreeClient, apiClient);
sut.tokenize(card, cardTokenizeCallback);

verify(cardTokenizeCallback).onResult(null, error);
ArgumentCaptor<CardResult> captor = ArgumentCaptor.forClass(CardResult.class);
verify(cardTokenizeCallback).onCardResult(captor.capture());

CardResult result = captor.getValue();
assertTrue(result instanceof CardResult.Failure);
Exception actualError = ((CardResult.Failure) result).getError();
assertEquals(error, actualError);
}

@Test
Expand All @@ -166,7 +177,13 @@ public void tokenize_whenGraphQLDisabled_callsListenerWithErrorOnFailure() {
CardClient sut = new CardClient(braintreeClient, apiClient);
sut.tokenize(card, cardTokenizeCallback);

verify(cardTokenizeCallback).onResult(null, error);
ArgumentCaptor<CardResult> captor = ArgumentCaptor.forClass(CardResult.class);
verify(cardTokenizeCallback).onCardResult(captor.capture());

CardResult result = captor.getValue();
assertTrue(result instanceof CardResult.Failure);
Exception actualError = ((CardResult.Failure) result).getError();
assertEquals(error, actualError);
}

@Test
Expand Down Expand Up @@ -213,6 +230,12 @@ public void tokenize_propagatesConfigurationFetchError() {
CardClient sut = new CardClient(braintreeClient, apiClient);
sut.tokenize(card, cardTokenizeCallback);

verify(cardTokenizeCallback).onResult(null, configError);
ArgumentCaptor<CardResult> captor = ArgumentCaptor.forClass(CardResult.class);
verify(cardTokenizeCallback).onCardResult(captor.capture());

CardResult result = captor.getValue();
assertTrue(result instanceof CardResult.Failure);
Exception actualError = ((CardResult.Failure) result).getError();
assertEquals(configError, actualError);
}
}
11 changes: 6 additions & 5 deletions Demo/src/main/java/com/braintreepayments/demo/CardFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.braintreepayments.api.Card;
import com.braintreepayments.api.CardClient;
import com.braintreepayments.api.CardNonce;
import com.braintreepayments.api.CardResult;
import com.braintreepayments.api.DataCollector;
import com.braintreepayments.api.PaymentMethodNonce;
import com.braintreepayments.api.ThreeDSecureAdditionalInformation;
Expand Down Expand Up @@ -182,11 +183,11 @@ public void onPurchase(View v) {
card.setShouldValidate(false);
card.setPostalCode(cardForm.getPostalCode());

cardClient.tokenize(card, (cardNonce, tokenizeError) -> {
if (cardNonce != null) {
handlePaymentMethodNonceCreated(cardNonce);
} else {
handleError(tokenizeError);
cardClient.tokenize(card, (cardResult) -> {
if (cardResult instanceof CardResult.Success) {
handlePaymentMethodNonceCreated(((CardResult.Success) cardResult).getNonce());
} else if (cardResult instanceof CardResult.Failure) {
handleError(((CardResult.Failure) cardResult).getError());
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class LocalPaymentClientTest {
@Before
public void setUp() {
countDownLatch = new CountDownLatch(1);
braintreeClient = new BraintreeClient(new ClientParams(ApplicationProvider.getApplicationContext(), "sandbox_f252zhq7_hh4cpc39zq4rgjcg"));
braintreeClient = new BraintreeClient(ApplicationProvider.getApplicationContext(), "sandbox_f252zhq7_hh4cpc39zq4rgjcg");
}

@Test(timeout = 10000)
Expand Down
16 changes: 16 additions & 0 deletions v5_MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ basics for updating your Braintree integration from v4 to v5.
1. [Android API](#android-api)
1. [Braintree Client](#braintree-client)
1. [Data Collector](#data-collector)
1. [Card](#card)
1. [Union Pay](#union-pay)
1. [Venmo](#venmo)
1. [Google Pay](#google-pay)
Expand Down Expand Up @@ -48,6 +49,21 @@ dataCollector.collectDeviceData(context) { deviceData, error ->
}
```

## Card

The card tokenization integration has been updated to simplify instantiation and result handling.

```kotlin
val cardClient = CardClient(context, "TOKENIZATION_KEY_OR_CLIENT_TOKEN")

cardClient.tokenize(card) { cardResult ->
when (cardResult) {
is CardResult.Success -> { /* handle cardResult.nonce */ }
is CardResult.Failure -> { /* handle cardResult.error */ }
}
}
```

## Union Pay

The `union-pay` module, and all containing classes, was removed in v5. UnionPay cards can now be
Expand Down
Loading