Skip to content

Commit

Permalink
Create ErrorMessageTranslator and default implementation (#807)
Browse files Browse the repository at this point in the history
**Summary**
ErrorMessageTranslator is an interface that a user can
implement to specify custom translations for
server-provided errors.

**Motivation**
Stripe error messages are in English. In a few places,
we show these messages on screen in dialogs. An app
developer may wish to provide their own translation of
these messages so that are legibile to their customers.

Fixes #680
  • Loading branch information
mshafrir-stripe authored Feb 13, 2019
1 parent 8251321 commit ea40ee8
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public void onCustomerRetrieved(@NonNull Customer customer) {
}

@Override
public void onError(int errorCode, @Nullable String errorMessage,
public void onError(int httpCode, @Nullable String errorMessage,
@Nullable StripeError stripeError) {
mSelectSourceButton.setEnabled(false);
mErrorDialogHandler.showError(errorMessage);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public void onCustomerRetrieved(@NonNull Customer customer) {
}

@Override
public void onError(int errorCode, @Nullable String errorMessage,
public void onError(int httpCode, @Nullable String errorMessage,
@Nullable StripeError stripeError) {
mCustomer = null;
mSelectPaymentButton.setEnabled(false);
Expand Down Expand Up @@ -188,7 +188,7 @@ public void onCustomerRetrieved(@NonNull Customer customer) {
}

@Override
public void onError(int errorCode, @Nullable String errorMessage,
public void onError(int httpCode, @Nullable String errorMessage,
@Nullable StripeError stripeError) {
mProgressBar.setVisibility(View.INVISIBLE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ public void onCustomerRetrieved(@NonNull Customer customer) {
}

@Override
public void onError(int errorCode, @Nullable String errorMessage,
public void onError(int httpCode, @Nullable String errorMessage,
@Nullable StripeError stripeError) {
displayError("Error getting payment method");
}
Expand Down Expand Up @@ -402,7 +402,7 @@ public void onCustomerRetrieved(@NonNull Customer customer) {
}

@Override
public void onError(int errorCode, @Nullable String errorMessage,
public void onError(int httpCode, @Nullable String errorMessage,
@Nullable StripeError stripeError) {
displayError(errorMessage);
}
Expand Down
8 changes: 4 additions & 4 deletions stripe/src/main/java/com/stripe/android/CustomerSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -548,16 +548,16 @@ public void onKeyUpdate(
}

@Override
public void onKeyError(int errorCode, @Nullable String errorMessage) {
public void onKeyError(int httpCode, @Nullable String errorMessage) {
// Any error eliminates all listeners

if (mCustomerRetrievalListener != null) {
mCustomerRetrievalListener.onError(errorCode, errorMessage, null);
mCustomerRetrievalListener.onError(httpCode, errorMessage, null);
mCustomerRetrievalListener = null;
}

if (mSourceRetrievalListener != null) {
mSourceRetrievalListener.onError(errorCode, errorMessage, null);
mSourceRetrievalListener.onError(httpCode, errorMessage, null);
mSourceRetrievalListener = null;
}
}
Expand Down Expand Up @@ -806,7 +806,7 @@ private static void sendErrorIntent(@NonNull WeakReference<Context> contextRef,
public interface CustomerRetrievalListener {
void onCustomerRetrieved(@NonNull Customer customer);

void onError(int errorCode, @Nullable String errorMessage,
void onError(int httpCode, @Nullable String errorMessage,
@Nullable StripeError stripeError);
}

Expand Down
4 changes: 2 additions & 2 deletions stripe/src/main/java/com/stripe/android/PaymentSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,10 @@ public void onCustomerRetrieved(@NonNull Customer customer) {
}

@Override
public void onError(int errorCode, @Nullable String errorMessage,
public void onError(int httpCode, @Nullable String errorMessage,
@Nullable StripeError stripeError) {
if (mPaymentSessionListener != null) {
mPaymentSessionListener.onError(errorCode, errorMessage);
mPaymentSessionListener.onError(httpCode, errorMessage);
mPaymentSessionListener.onCommunicatingStateChanged(false);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.stripe.android.StripeError;
import com.stripe.android.model.Customer;
import com.stripe.android.model.CustomerSource;
import com.stripe.android.view.i18n.TranslatorManager;

import java.util.List;

Expand All @@ -51,6 +52,7 @@ public class PaymentMethodsActivity extends AppCompatActivity {
private boolean mRecyclerViewUpdated;
private boolean mStartedFromPaymentSession;

@NonNull
public static Intent newIntent(Context context) {
return new Intent(context, PaymentMethodsActivity.class);
}
Expand Down Expand Up @@ -100,17 +102,19 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_ADD_CARD && resultCode == RESULT_OK) {
setCommunicatingProgress(true);
initLoggingTokens();
CustomerSession.CustomerRetrievalListener listener =
final CustomerSession.CustomerRetrievalListener listener =
new CustomerSession.CustomerRetrievalListener() {
@Override
public void onCustomerRetrieved(@NonNull Customer customer) {
updateCustomerAndSetDefaultSourceIfNecessary(customer);
}

@Override
public void onError(int errorCode, @Nullable String errorMessage,
public void onError(int httpCode, @Nullable String errorMessage,
@Nullable StripeError stripeError) {
String displayedError = errorMessage == null ? "" : errorMessage;
final String displayedError = TranslatorManager
.getErrorMessageTranslator()
.translate(httpCode, errorMessage, stripeError);
showError(displayedError);
setCommunicatingProgress(false);
}
Expand All @@ -125,8 +129,8 @@ public void onError(int errorCode, @Nullable String errorMessage,

@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem saveItem = menu.findItem(R.id.action_save);
Drawable compatIcon =
final MenuItem saveItem = menu.findItem(R.id.action_save);
final Drawable compatIcon =
ViewUtils.getTintedIconWithAttribute(
this,
getTheme(),
Expand Down Expand Up @@ -160,7 +164,7 @@ public boolean onOptionsItemSelected(MenuItem item) {

@VisibleForTesting
void initializeCustomerSourceData() {
Customer cachedCustomer = mCustomerSessionProxy == null
final Customer cachedCustomer = mCustomerSessionProxy == null
? CustomerSession.getInstance().getCachedCustomer()
: mCustomerSessionProxy.getCachedCustomer();

Expand Down Expand Up @@ -215,13 +219,14 @@ public void onCustomerRetrieved(@NonNull Customer customer) {
}

@Override
public void onError(int errorCode, @Nullable String errorMessage,
public void onError(int httpCode, @Nullable String errorMessage,
@Nullable StripeError stripeError) {
// Note: if this Activity is changed to subclass StripeActivity,
// this code will make the error message show twice, since StripeActivity
// will listen to the broadcast version of the error
// coming from CustomerSession
String displayedError = errorMessage == null ? "" : errorMessage;
final String displayedError = TranslatorManager.getErrorMessageTranslator()
.translate(httpCode, errorMessage, stripeError);
showError(displayedError);
setCommunicatingProgress(false);
}
Expand Down Expand Up @@ -258,22 +263,20 @@ private void createListFromCustomerSources() {
return;
}

List<CustomerSource> customerSourceList = mCustomer.getSources();

final List<CustomerSource> customerSourceList = mCustomer.getSources();
if (!mRecyclerViewUpdated) {
mMaskedCardAdapter = new MaskedCardAdapter(customerSourceList);
// init the RecyclerView
RecyclerView.LayoutManager linearLayoutManager = new LinearLayoutManager(this);
mRecyclerView.setHasFixedSize(false);
mRecyclerView.setLayoutManager(linearLayoutManager);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.setAdapter(mMaskedCardAdapter);
mRecyclerViewUpdated = true;
} else {
mMaskedCardAdapter.setCustomerSourceList(customerSourceList);
}

String defaultSource = mCustomer.getDefaultSource();
if (!TextUtils.isEmpty(defaultSource)) {
final String defaultSource = mCustomer.getDefaultSource();
if (defaultSource != null && !TextUtils.isEmpty(defaultSource)) {
mMaskedCardAdapter.setSelectedSource(defaultSource);
}
mMaskedCardAdapter.notifyDataSetChanged();
Expand All @@ -298,7 +301,7 @@ private void finishWithSelection(String selectedSourceId) {
}

private void getCustomerFromSession() {
CustomerSession.CustomerRetrievalListener listener =
final CustomerSession.CustomerRetrievalListener listener =
new CustomerSession.CustomerRetrievalListener() {
@Override
public void onCustomerRetrieved(@NonNull Customer customer) {
Expand All @@ -307,7 +310,7 @@ public void onCustomerRetrieved(@NonNull Customer customer) {
}

@Override
public void onError(int errorCode, @Nullable String errorMessage,
public void onError(int httpCode, @Nullable String errorMessage,
@Nullable StripeError stripeError) {
setCommunicatingProgress(false);
}
Expand Down Expand Up @@ -348,9 +351,10 @@ public void onCustomerRetrieved(@NonNull Customer customer) {
}

@Override
public void onError(int errorCode, @Nullable String errorMessage,
public void onError(int httpCode, @Nullable String errorMessage,
@Nullable StripeError stripeError) {
String displayedError = errorMessage == null ? "" : errorMessage;
final String displayedError = TranslatorManager.getErrorMessageTranslator()
.translate(httpCode, errorMessage, stripeError);
showError(displayedError);
setCommunicatingProgress(false);
}
Expand All @@ -371,7 +375,7 @@ public void onError(int errorCode, @Nullable String errorMessage,
}

private void showError(@NonNull String error) {
AlertDialog alertDialog = new AlertDialog.Builder(this)
new AlertDialog.Builder(this)
.setMessage(error)
.setCancelable(true)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
Expand All @@ -380,8 +384,8 @@ public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
}
})
.create();
alertDialog.show();
.create()
.show();
}

private void updateAdapterWithCustomer(@NonNull Customer customer) {
Expand All @@ -396,7 +400,7 @@ private void updateAdapterWithCustomer(@NonNull Customer customer) {
}

interface CustomerSessionProxy {
void addProductUsageTokenIfValid(String token);
void addProductUsageTokenIfValid(@NonNull String token);

@Nullable
Customer getCachedCustomer();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.stripe.android.view.i18n;

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

import com.stripe.android.StripeError;

public interface ErrorMessageTranslator {
/**
* See https://stripe.com/docs/api/errors for a list of error codes and associated messages
*
* @param httpCode The HTTP code associated with the error response.
* @param errorMessage A human-readable message providing more details about the error.
* For card errors, these messages can be shown to your users.
* @param stripeError The {@link StripeError} that represents detailed information about the
* error. Specifically, {@link StripeError#code} is useful for understanding
* the underlying error (e.g. "payment_method_unactivated").
*
* @return a non-null error message
*/
@NonNull
String translate(int httpCode, @Nullable String errorMessage,
@Nullable StripeError stripeError);

class Default implements ErrorMessageTranslator {
@NonNull
@Override
public String translate(int httpCode, @Nullable String errorMessage,
@Nullable StripeError stripeError) {
return errorMessage == null ? "" : errorMessage;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.stripe.android.view.i18n;

import android.app.Application;

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

/**
* A class that provides a {@link ErrorMessageTranslator} for translating server-provided error
* messages, as defined in https://stripe.com/docs/api/errors.
*
* See {@link com.stripe.android.view.PaymentMethodsActivity} for example usage.
*
* To use a custom {@link ErrorMessageTranslator} in your app,
* override {@link Application#onCreate()} in your app's Application subclass and call
* {@link #setErrorMessageTranslator(ErrorMessageTranslator)}.
*
* <pre>
* <code>
* public class MyApp extends Application {
* public void onCreate() {
* super.onCreate();
* TranslatorManager.setErrorMessageTranslator(new MyErrorMessageTranslator());
* }
* }
* </code>
* </pre>
*/
public class TranslatorManager {
@NonNull
private static final ErrorMessageTranslator DEFAULT_ERROR_MESSAGE_TRANSLATOR =
new ErrorMessageTranslator.Default();

@Nullable
private static ErrorMessageTranslator mErrorMessageTranslator;

private TranslatorManager() {
}

@NonNull
public static ErrorMessageTranslator getErrorMessageTranslator() {
return mErrorMessageTranslator != null ?
mErrorMessageTranslator : DEFAULT_ERROR_MESSAGE_TRANSLATOR;
}

public static void setErrorMessageTranslator(
@Nullable ErrorMessageTranslator errorMessageTranslator) {
mErrorMessageTranslator = errorMessageTranslator;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.stripe.android.view.i18n;

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

import com.stripe.android.StripeError;
import com.stripe.android.StripeErrorFixtures;

import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class TranslatorManagerTest {
private static final StripeError STRIPE_ERROR = StripeErrorFixtures.INVALID_REQUEST_ERROR;

@Before
public void setup() {
TranslatorManager.setErrorMessageTranslator(null);
}

@Test
public void testDefaultErrorMessageTranslator() {
assertEquals("error!",
TranslatorManager.getErrorMessageTranslator()
.translate(0, "error!", STRIPE_ERROR));
}

@Test
public void testCustomErrorMessageTranslator() {
TranslatorManager.setErrorMessageTranslator(new ErrorMessageTranslator() {
@NonNull
@Override
public String translate(int httpCode, @Nullable String errorMessage,
@Nullable StripeError stripeError) {
return "custom message";
}
});
assertEquals("custom message", TranslatorManager.getErrorMessageTranslator()
.translate(0, "original message", STRIPE_ERROR));
}
}

0 comments on commit ea40ee8

Please sign in to comment.