Skip to content

Commit

Permalink
Update PaymentMethod Activity UX (#1494)
Browse files Browse the repository at this point in the history
- Remove "save" button from top-right corner of `PaymentMethodActivity`
- Trigger selecting a PaymentMethod by clicking on one
- Make back button always set a result
- In samplestore's `PaymentActivity`, remove `lateinit` keyword on
  `paymentSession`. In some cases, `onActivityResult()` is called
  before `onCreate()`, so `paymentSession` is not always
  initialized before it's used.
  • Loading branch information
mshafrir-stripe authored Sep 11, 2019
1 parent caaffd5 commit 5af0dca
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 210 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@

import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
Expand Down Expand Up @@ -47,12 +45,13 @@ public class PaymentMethodsActivity extends AppCompatActivity {

public static final String TOKEN_PAYMENT_METHODS_ACTIVITY = "PaymentMethodsActivity";

private boolean mCommunicating;
private PaymentMethodsAdapter mAdapter;
private ProgressBar mProgressBar;
private boolean mStartedFromPaymentSession;
private CustomerSession mCustomerSession;

@Nullable private PaymentMethod mTappedPaymentMethod = null;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Expand All @@ -63,13 +62,39 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {

mProgressBar = findViewById(R.id.payment_methods_progress_bar);

final String initiallySelectedPaymentMethodId;
if (savedInstanceState != null &&
savedInstanceState.containsKey(STATE_SELECTED_PAYMENT_METHOD_ID)) {
initiallySelectedPaymentMethodId =
savedInstanceState.getString(STATE_SELECTED_PAYMENT_METHOD_ID);
} else {
initiallySelectedPaymentMethodId = args.initialPaymentMethodId;
}
final RecyclerView recyclerView = findViewById(R.id.payment_methods_recycler);
mAdapter = new PaymentMethodsAdapter();
mAdapter = new PaymentMethodsAdapter(initiallySelectedPaymentMethodId);
mAdapter.setListener(new PaymentMethodsAdapter.Listener() {
@Override
public void onClick(@NonNull PaymentMethod paymentMethod) {
mTappedPaymentMethod = paymentMethod;
}
});

// init the RecyclerView
recyclerView.setHasFixedSize(false);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(mAdapter);
recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerView.setItemAnimator(new DefaultItemAnimator() {
@Override
public void onAnimationFinished(@NonNull RecyclerView.ViewHolder viewHolder) {
super.onAnimationFinished(viewHolder);

// wait until post-tap animations are completed before finishing activity
if (mTappedPaymentMethod != null) {
setSelectionAndFinish(mTappedPaymentMethod);
mTappedPaymentMethod = null;
}
}
});

mCustomerSession = CustomerSession.getInstance();
mStartedFromPaymentSession = args.isPaymentSessionActive;
Expand All @@ -86,18 +111,10 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}

final String selectedPaymentMethodId;
if (savedInstanceState != null &&
savedInstanceState.containsKey(STATE_SELECTED_PAYMENT_METHOD_ID)) {
selectedPaymentMethodId =
savedInstanceState.getString(STATE_SELECTED_PAYMENT_METHOD_ID);
} else {
selectedPaymentMethodId = args.initialPaymentMethodId;
}

getCustomerPaymentMethods(selectedPaymentMethodId);
fetchCustomerPaymentMethods();

// This prevents the first click from being eaten by the focus.
addCardView.requestFocusFromTouch();
Expand All @@ -112,6 +129,12 @@ protected void onActivityResult(int requestCode, int resultCode, @Nullable Inten
}
}

@Override
public boolean onSupportNavigateUp() {
setSelectionAndFinish(mAdapter.getSelectedPaymentMethod());
return true;
}

private void onPaymentMethodCreated(@Nullable Intent data) {
initLoggingTokens();

Expand All @@ -128,59 +151,26 @@ private void onPaymentMethodCreated(@Nullable Intent data) {
finishWithPaymentMethod(paymentMethod);
} else {
// Refresh the list of Payment Methods with the new Payment Method.
getCustomerPaymentMethods(paymentMethod != null ? paymentMethod.id : null);
fetchCustomerPaymentMethods();
}
} else {
getCustomerPaymentMethods(null);
fetchCustomerPaymentMethods();
}
}

@Override
public boolean onPrepareOptionsMenu(Menu menu) {
final MenuItem saveItem = menu.findItem(R.id.action_save);
final Drawable compatIcon = ViewUtils.getTintedIconWithAttribute(
this,
getTheme(),
R.attr.titleTextColor,
R.drawable.ic_checkmark);
saveItem.setIcon(compatIcon);
return super.onPrepareOptionsMenu(menu);
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.add_payment_method, menu);
menu.findItem(R.id.action_save).setEnabled(!mCommunicating);
return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_save) {
setSelectionAndFinish();
return true;
} else {
final boolean handled = super.onOptionsItemSelected(item);
if (!handled) {
onBackPressed();
}
return handled;
}
public void onBackPressed() {
setSelectionAndFinish(mAdapter.getSelectedPaymentMethod());
}

private void getCustomerPaymentMethods(@Nullable String selectPaymentMethodId) {
private void fetchCustomerPaymentMethods() {
setCommunicatingProgress(true);
mCustomerSession.getPaymentMethods(PaymentMethod.Type.Card,
new PaymentMethodsRetrievalListener(this, selectPaymentMethodId));
new PaymentMethodsRetrievalListener(this));
}

private void updatePaymentMethods(@NonNull List<PaymentMethod> paymentMethods,
@Nullable String selectPaymentMethodId) {
private void updatePaymentMethods(@NonNull List<PaymentMethod> paymentMethods) {
mAdapter.setPaymentMethods(paymentMethods);
if (selectPaymentMethodId != null) {
mAdapter.setSelectedPaymentMethod(selectPaymentMethodId);
}
}

private void initLoggingTokens() {
Expand All @@ -196,7 +186,6 @@ private void cancelAndFinish() {
}

private void setCommunicatingProgress(boolean communicating) {
mCommunicating = communicating;
if (communicating) {
mProgressBar.setVisibility(View.VISIBLE);
} else {
Expand All @@ -205,8 +194,8 @@ private void setCommunicatingProgress(boolean communicating) {
supportInvalidateOptionsMenu();
}

private void setSelectionAndFinish() {
final PaymentMethod paymentMethod = mAdapter.getSelectedPaymentMethod();
@VisibleForTesting
void setSelectionAndFinish(@Nullable PaymentMethod paymentMethod) {
if (paymentMethod == null || paymentMethod.id == null) {
cancelAndFinish();
return;
Expand Down Expand Up @@ -248,12 +237,8 @@ protected void onSaveInstanceState(Bundle outState) {
private static final class PaymentMethodsRetrievalListener extends
CustomerSession.ActivityPaymentMethodsRetrievalListener<PaymentMethodsActivity> {

@Nullable final String mSelectPaymentMethodId;

private PaymentMethodsRetrievalListener(@NonNull PaymentMethodsActivity activity,
@Nullable String selectPaymentMethodId) {
private PaymentMethodsRetrievalListener(@NonNull PaymentMethodsActivity activity) {
super(activity);
mSelectPaymentMethodId = selectPaymentMethodId;
}

@Override
Expand All @@ -263,7 +248,7 @@ public void onPaymentMethodsRetrieved(@NonNull List<PaymentMethod> paymentMethod
return;
}

activity.updatePaymentMethods(paymentMethods, mSelectPaymentMethodId);
activity.updatePaymentMethods(paymentMethods);
activity.setCommunicatingProgress(false);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.stripe.android.view

import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
Expand All @@ -13,16 +15,20 @@ import java.util.ArrayList
* A [RecyclerView.Adapter] that holds a set of [MaskedCardView] items for a given set
* of [PaymentMethod] objects.
*/
internal class PaymentMethodsAdapter : androidx.recyclerview.widget.RecyclerView.Adapter<PaymentMethodsAdapter.ViewHolder>() {
internal class PaymentMethodsAdapter constructor(
private val initiallySelectedPaymentMethodId: String?
) : RecyclerView.Adapter<PaymentMethodsAdapter.ViewHolder>() {
private val paymentMethods = ArrayList<PaymentMethod>()
private var selectedIndex = NO_SELECTION
var listener: Listener? = null
private val handler = Handler(Looper.getMainLooper())

private val newestPaymentMethodIndex: Int
get() {
var index = NO_SELECTION
var created = 0L
for (i in paymentMethods.indices) {
val paymentMethod = paymentMethods[i]

paymentMethods.forEachIndexed { i, paymentMethod ->
if (paymentMethod.created != null && paymentMethod.created > created) {
created = paymentMethod.created
index = i
Expand All @@ -42,17 +48,12 @@ internal class PaymentMethodsAdapter : androidx.recyclerview.widget.RecyclerView
}

fun setPaymentMethods(paymentMethods: List<PaymentMethod>) {
val selectedPaymentMethod = selectedPaymentMethod
val selectedPaymentMethodId = selectedPaymentMethod?.id

this.paymentMethods.clear()
this.paymentMethods.addAll(paymentMethods)

// if there were no selected payment methods, or the previously selected payment method
// was not found and set selected, select the newest payment method
if (selectedPaymentMethodId == null || !setSelectedPaymentMethod(selectedPaymentMethodId)) {
setSelectedIndex(newestPaymentMethodIndex)
}
setSelectedPaymentMethod(initiallySelectedPaymentMethodId)

notifyDataSetChanged()
}
Expand All @@ -78,14 +79,20 @@ internal class PaymentMethodsAdapter : androidx.recyclerview.widget.RecyclerView
holder.setPaymentMethod(paymentMethods[position])
holder.setSelected(position == selectedIndex)
holder.itemView.setOnClickListener {
val currentPosition = holder.adapterPosition
if (currentPosition != selectedIndex) {
val prevSelectedIndex = selectedIndex
setSelectedIndex(currentPosition)
onPositionClicked(holder.adapterPosition)
}
}

notifyItemChanged(prevSelectedIndex)
notifyItemChanged(currentPosition)
}
private fun onPositionClicked(position: Int) {
if (selectedIndex != position) {
// selected a Payment Method that wasn't previously selected
notifyItemChanged(position)
notifyItemChanged(selectedIndex)
setSelectedIndex(position)
}

handler.post {
listener?.onClick(paymentMethods[position])
}
}

Expand All @@ -102,19 +109,22 @@ internal class PaymentMethodsAdapter : androidx.recyclerview.widget.RecyclerView
}

/**
* Sets the selected payment method based on ID.
* Sets the selected payment method based on ID, or most recently created
*
* @param paymentMethodId the ID of the [PaymentMethod] to select
* @return `true` if the value was found, `false` if not
*/
fun setSelectedPaymentMethod(paymentMethodId: String): Boolean {
for (i in paymentMethods.indices) {
if (paymentMethodId == paymentMethods[i].id) {
setSelectedIndex(i)
return true
private fun setSelectedPaymentMethod(paymentMethodId: String?) {
val indexToSelect = paymentMethodId?.let {
paymentMethods.indexOfFirst { paymentMethodId == it.id }
} ?: NO_SELECTION

setSelectedIndex(
if (indexToSelect >= 0) {
indexToSelect
} else {
newestPaymentMethodIndex
}
}
return false
)
}

fun setSelectedIndex(selectedIndex: Int) {
Expand All @@ -133,6 +143,10 @@ internal class PaymentMethodsAdapter : androidx.recyclerview.widget.RecyclerView
}
}

interface Listener {
fun onClick(paymentMethod: PaymentMethod)
}

companion object {
private const val TYPE_CARD = 0
private const val NO_SELECTION = -1
Expand Down
Loading

0 comments on commit 5af0dca

Please sign in to comment.