package jp.webpay.android.token.ui; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.os.Bundle; import android.support.annotation.StringRes; import android.support.v4.app.DialogFragment; import android.view.ContextThemeWrapper; import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import jp.webpay.android.token.ErrorResponseException; import jp.webpay.android.token.R; import jp.webpay.android.token.WebPay; import jp.webpay.android.token.WebPayListener; import jp.webpay.android.token.model.CardType; import jp.webpay.android.token.model.ErrorResponse; import jp.webpay.android.token.model.RawCard; import jp.webpay.android.token.model.Token; import jp.webpay.android.token.ui.field.BaseCardField; import jp.webpay.android.token.ui.field.CvcField; import jp.webpay.android.token.ui.field.NameField; import jp.webpay.android.token.ui.field.NumberField; /** * This class is to create tokens from users input. WebPayTokenFragment is recommended for most users, but * you can use this for better UX. This dialog is responsible for accept card information, validate it, * creating token and handling errors. * <p> * Activities that contain this fragment (and not contain {@link WebPayTokenFragment}) * must implement the {@link WebPayTokenCompleteListener} interface to handle results. */ public class CardDialogFragment extends DialogFragment implements NumberField.OnCardTypeChangeListener { private static final String ARG_PUBLISHABLE_KEY = "publishableKey"; private static final String ARG_SUPPORTED_CARD_TYPES = "supportedCardTypes"; private static final String TAG = "webpay:CardDialogFragment"; private static final Map<CardType, Integer> CARD_TYPE_TO_DRAWABLE = new HashMap<CardType, Integer>() {{ put(CardType.VISA, R.drawable.card_visa); put(CardType.AMERICAN_EXPRESS, R.drawable.card_amex); put(CardType.MASTERCARD, R.drawable.card_master); put(CardType.JCB, R.drawable.card_jcb); put(CardType.DINERS_CLUB, R.drawable.card_diners); }}; private WebPay mWebPay; private WebPayTokenCompleteListener mListener; private Throwable mLastException; private ArrayList<CardType> mSupportedCardTypes; private @StringRes int mSendButtonTitle = R.string.card_send; /** * Use this factory method to create a new instance of this fragment * using the provided parameters. * <p> * {@link WebPayTokenFragment} will give an idea * about how to use this class. * * @param publishableKey WebPay publishable key to generate token * @param supportedCardTypes supported card types retrieved from availability API. Use * {@link WebPay#retrieveAvailability(jp.webpay.android.token.WebPayListener)}. * Pass null if you do not need to show and check supported card types. * @return A new instance of dialog fragment */ public static CardDialogFragment newInstance(String publishableKey, List<CardType> supportedCardTypes) { CardDialogFragment fragment = new CardDialogFragment(); Bundle args = new Bundle(); args.putString(ARG_PUBLISHABLE_KEY, publishableKey); if (supportedCardTypes != null) { String typeNames[] = new String[supportedCardTypes.size()]; for (int i = 0; i < supportedCardTypes.size(); i++) { typeNames[i] = supportedCardTypes.get(i).toString(); } args.putStringArray(ARG_SUPPORTED_CARD_TYPES, typeNames); } fragment.setArguments(args); return fragment; } public CardDialogFragment() { } /** * Set send button title string resource id. * Default is {@code jp.webpay.android.R.string.card_send}, which is "Pay with card". * This method works before and after dialog is created. * * @param sendButtonTitle send button title res id */ public void setSendButtonTitle(@StringRes int sendButtonTitle) { this.mSendButtonTitle = sendButtonTitle; Dialog dialog = getDialog(); if (dialog == null) return; Button sendButton = (Button) dialog.findViewById(R.id.button_submit); if (sendButton == null) return; sendButton.setText(sendButtonTitle); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Bundle arguments = getArguments(); String publishableKey = arguments.getString(ARG_PUBLISHABLE_KEY); mWebPay = new WebPay(publishableKey); String typeNames[] = arguments.getStringArray(ARG_SUPPORTED_CARD_TYPES); if (typeNames == null) { mSupportedCardTypes = null; } else { mSupportedCardTypes = new ArrayList<CardType>(); for (String name : typeNames) { mSupportedCardTypes.add(CardType.valueOf(name)); } } } // using "null" for inflate is correct according to // http://developer.android.com/guide/topics/ui/dialogs.html#CustomLayout @SuppressLint("InflateParams") @Override public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(getActivity(), R.style.WebPayDialogTheme)); builder.setView(getActivity().getLayoutInflater().inflate(R.layout.dialog_card, null)); return builder.create(); } @Override public void onStart() { super.onStart(); final AlertDialog dialog = (AlertDialog) getDialog(); if (dialog == null) return; Button sendButton = (Button) dialog.findViewById(R.id.button_submit); sendButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendCardInfoToWebPay(); } }); sendButton.setText(mSendButtonTitle); Button cancelButton = (Button) dialog.findViewById(R.id.button_cancel); cancelButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { dialog.dismiss(); mListener.onCancelled(mLastException); } }); NumberField numberField = (NumberField) dialog.findViewById(R.id.cardNumberField); numberField.setOnCardTypeChangeListener(this); // allow all brands if no specification if (mSupportedCardTypes == null) numberField.setCardTypesSupported(Arrays.asList(CardType.values())); else numberField.setCardTypesSupported(mSupportedCardTypes); if (numberField.getText().toString().equals("")) { onCardTypeChange(null); // initialize } NameField nameField = (NameField) dialog.findViewById(R.id.cardNameField); nameField.setOnEditorActionListener(new EditText.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (actionId == EditorInfo.IME_ACTION_SEND) { sendCardInfoToWebPay(); return true; } return false; } }); showAvailableCardTypes(); } @Override public void onAttach(Activity activity) { super.onAttach(activity); try { mListener = (WebPayTokenCompleteListener) getParentFragment(); } catch (ClassCastException ignored) { mListener = null; } if (mListener == null) { try { mListener = (WebPayTokenCompleteListener) activity; } catch (ClassCastException ignored) { mListener = null; } } if (mListener == null) { throw new IllegalStateException("Activity or parent fragment must implement WebPayTokenCompleteListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; } private void hideSoftKeyboard() { View currentFocus = getDialog().getCurrentFocus(); if(currentFocus !=null) { InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); inputMethodManager.hideSoftInputFromWindow(currentFocus.getWindowToken(), 0); } } private void showAvailableCardTypes() { View label = getDialog().findViewById(R.id.cardTypeLabel); LinearLayout iconList = (LinearLayout)getDialog().findViewById(R.id.cardTypeIconList); iconList.removeAllViews(); if (mSupportedCardTypes == null) { label.setVisibility(View.GONE); iconList.setVisibility(View.GONE); } else { label.setVisibility(View.VISIBLE); iconList.setVisibility(View.VISIBLE); for (CardType cardType : mSupportedCardTypes) { ImageView view = new ImageView(getActivity()); view.setImageDrawable(getResources().getDrawable(CARD_TYPE_TO_DRAWABLE.get(cardType))); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); lp.setMargins(0, 0, 5, 0); view.setLayoutParams(lp); iconList.addView(view); } } } private void sendCardInfoToWebPay() { RawCard card = createValidCardFromForm(); if (card == null) { return; } hideSoftKeyboard(); switchIndicatorVisibility(true); updateRequestLanguage(); mWebPay.createToken(card, new WebPayListener<Token>() { @Override public void onCreate(Token result) { switchIndicatorVisibility(false); mListener.onTokenCreated(result); getDialog().dismiss(); } @Override public void onException(Throwable cause) { switchIndicatorVisibility(false); mLastException = cause; showWebPayErrorAlert(cause); } }); } private void switchIndicatorVisibility(boolean visible) { if (visible) { getDialog().findViewById(R.id.progress).setVisibility(View.VISIBLE); getDialog().findViewById(R.id.buttons).setVisibility(View.GONE); } else { getDialog().findViewById(R.id.progress).setVisibility(View.GONE); getDialog().findViewById(R.id.buttons).setVisibility(View.VISIBLE); } } private void updateRequestLanguage() { if (getResources().getConfiguration().locale.getISO3Language().equals("jpn")) { mWebPay.setLanguage("ja"); } else { mWebPay.setLanguage("en"); } } /** * Collect values on the form and return card * @return card that contains input information, null if one of fields is invalid */ private RawCard createValidCardFromForm() { Dialog dialog = getDialog(); RawCard card = new RawCard(); int fieldIds[] = new int[]{R.id.cardCvcField, R.id.cardExpiryField, R.id.cardNameField, R.id.cardNumberField}; for (int fieldId : fieldIds) { BaseCardField field = (BaseCardField) dialog.findViewById(fieldId); if (!field.validate()) return null; field.updateCard(card); } return card; } private void showWebPayErrorAlert(Throwable cause) { String message = null; if (cause instanceof ErrorResponseException) { ErrorResponse response = ((ErrorResponseException) cause).getResponse(); if (response.causedBy.equals("buyer")) { message = response.message; } } if (message == null) { message = getString(R.string.tokenize_error_message); } new AlertDialog.Builder(getActivity()) .setMessage(message) .setPositiveButton(android.R.string.yes, null) .show(); } @Override public void onCardTypeChange(CardType cardType) { NumberField numberFiled = (NumberField) getDialog().findViewById(R.id.cardNumberField); int iconDrawableId = (cardType == null) ? 0 : CARD_TYPE_TO_DRAWABLE.get(cardType).intValue(); numberFiled.setCompoundDrawablesWithIntrinsicBounds(0, 0, iconDrawableId, 0); CvcField cvcField = (CvcField) getDialog().findViewById(R.id.cardCvcField); int drawableId = CardType.AMERICAN_EXPRESS.equals(cardType) ? R.drawable.cvc_amex : R.drawable.cvc; cvcField.setOnHelpIconClickListener(cvcHelpListener(drawableId)); } @SuppressLint("InflateParams") // using "null" for inflate is correct private View.OnClickListener cvcHelpListener(final int drawableId) { return new View.OnClickListener() { @Override public void onClick(View v) { View view = getActivity().getLayoutInflater().inflate(R.layout.dialog_cvc_help, null); ((ImageView) view.findViewById(R.id.cvc_help)).setImageDrawable(getResources().getDrawable(drawableId)); new AlertDialog.Builder(new ContextThemeWrapper(getActivity(), R.style.WebPayDialogTheme)) .setView(view) .show(); } }; } }