From 6b5f4f1888466f3ce3b289933ba76900e992fbb3 Mon Sep 17 00:00:00 2001 From: Izzy Oji Date: Tue, 6 Nov 2018 16:40:56 -0500 Subject: [PATCH] =?UTF-8?q?[=F0=9F=92=B3]=20Saving=20credit=20cards=20(#36?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Only showing edit profile and payment methods on debug builds. * Updated Stripe library. Updated strings. First pass of new card screen. Does basic card validation. * Moved add new card form to fragment. Added focus listeners and text watchers to properly disable and enable save button * added todo * Added mutation and Stripe object to config. * Successfully adding cards woohoo. * merging in settings-v3 and adding fake stripe keys * moved mutation * checkstyle and refactored payment method row because it was breaking AS and it was easier to just do it as a LinearLayout * starting NewCardActivity for result and fixing import * actually starting NewCardActivity for result * some more refactoring and refreshing list after card is saved * Added MockStripe and CardFactory for testing. Finished tests. * cleanup, bizarre error handling and actually emitting something after saving the card * PR feedback from Rashad --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 1 + app/src/main/graphql/payments.graphql | 7 + .../com/kickstarter/ApplicationModule.java | 12 + .../com/kickstarter/extensions/ActivityExt.kt | 2 +- .../libs/ActivityRequestCodes.java | 1 + .../com/kickstarter/libs/Environment.java | 3 + .../libs/utils/Secrets.java.example | 5 + .../kickstarter/mock/factories/CardFactory.kt | 15 ++ .../mock/services/MockApolloClient.kt | 14 +- .../kickstarter/mock/services/MockStripe.kt | 19 ++ .../kickstarter/services/ApolloClientType.kt | 4 + .../kickstarter/services/KSApolloClient.kt | 33 +++ .../ui/activities/NewCardActivity.kt | 23 ++ .../ui/activities/PaymentMethodsActivity.kt | 25 ++- .../ui/adapters/PaymentMethodsAdapter.kt | 8 +- .../ui/fragments/NewCardFragment.kt | 150 +++++++++++++ .../viewholders/PaymentMethodsViewHolder.kt | 2 +- .../viewmodels/NewCardFragmentViewModel.kt | 206 ++++++++++++++++++ .../viewmodels/NewCardViewModel.kt | 10 + .../viewmodels/PaymentMethodsViewModel.kt | 33 ++- .../res/drawable/divider_green_horizontal.xml | 5 + app/src/main/res/layout/activity_account.xml | 2 +- .../main/res/layout/activity_change_email.xml | 6 +- .../res/layout/activity_change_password.xml | 8 +- app/src/main/res/layout/activity_new_card.xml | 7 + ... => activity_settings_payment_methods.xml} | 26 +-- app/src/main/res/layout/fragment_new_card.xml | 128 +++++++++++ .../main/res/layout/item_payment_method.xml | 58 +++++ .../res/layout/list_item_payment_methods.xml | 69 ------ .../res/layout/payment_methods_toolbar.xml | 4 +- app/src/main/res/values/styles.xml | 8 +- .../kickstarter/KSRobolectricTestCase.java | 4 +- .../NewCardFragmentViewModelTest.kt | 119 ++++++++++ .../viewmodels/PaymentMethodsViewModelTest.kt | 2 +- 35 files changed, 905 insertions(+), 116 deletions(-) create mode 100644 app/src/main/java/com/kickstarter/mock/factories/CardFactory.kt create mode 100644 app/src/main/java/com/kickstarter/mock/services/MockStripe.kt create mode 100644 app/src/main/java/com/kickstarter/ui/activities/NewCardActivity.kt create mode 100644 app/src/main/java/com/kickstarter/ui/fragments/NewCardFragment.kt create mode 100644 app/src/main/java/com/kickstarter/viewmodels/NewCardFragmentViewModel.kt create mode 100644 app/src/main/java/com/kickstarter/viewmodels/NewCardViewModel.kt create mode 100644 app/src/main/res/drawable/divider_green_horizontal.xml create mode 100644 app/src/main/res/layout/activity_new_card.xml rename app/src/main/res/layout/{activity_payment_method.xml => activity_settings_payment_methods.xml} (70%) create mode 100644 app/src/main/res/layout/fragment_new_card.xml create mode 100644 app/src/main/res/layout/item_payment_method.xml delete mode 100644 app/src/main/res/layout/list_item_payment_methods.xml create mode 100644 app/src/test/java/com/kickstarter/viewmodels/NewCardFragmentViewModelTest.kt diff --git a/app/build.gradle b/app/build.gradle index d365022440..2c668577a6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -232,7 +232,7 @@ final rx_android_version = "1.2.0" final rx_binding_version = "0.3.0" final rx_java_version = "1.1.5" final rx_lifecycle_version = "0.3.0" -final stripe_version = "6.1.2" +final stripe_version = "8.0.0" final support_version = "27.1.0" final support_annotations_version = "27.1.1" final timber_version = "3.0.1" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8cb6c84894..ffcab1eecb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -189,6 +189,7 @@ android:name="android.support.PARENT_ACTIVITY" android:value=".ui.activities.ProfileActivity" /> + diff --git a/app/src/main/graphql/payments.graphql b/app/src/main/graphql/payments.graphql index d01bca4756..c31d83a1cf 100644 --- a/app/src/main/graphql/payments.graphql +++ b/app/src/main/graphql/payments.graphql @@ -12,3 +12,10 @@ query UserPayments { } } } + +mutation SavePaymentMethod($paymentType: PaymentTypes!, $stripeToken: String!, $stripeCardId: String!) { + createPaymentSource(input: {paymentType: $paymentType, stripeToken: $stripeToken, stripeCardId: $stripeCardId}) { + errorMessage + isSuccessful + } +} diff --git a/app/src/main/java/com/kickstarter/ApplicationModule.java b/app/src/main/java/com/kickstarter/ApplicationModule.java index 44ef6ee493..8f5f79a2bb 100644 --- a/app/src/main/java/com/kickstarter/ApplicationModule.java +++ b/app/src/main/java/com/kickstarter/ApplicationModule.java @@ -72,6 +72,7 @@ import com.kickstarter.services.interceptors.KSRequestInterceptor; import com.kickstarter.services.interceptors.WebRequestInterceptor; import com.kickstarter.ui.SharedPreferenceKey; +import com.stripe.android.Stripe; import org.joda.time.DateTime; @@ -122,6 +123,7 @@ static Environment provideEnvironment(final @NonNull @ActivitySamplePreference I final @NonNull PlayServicesCapability playServicesCapability, final @NonNull Scheduler scheduler, final @NonNull SharedPreferences sharedPreferences, + final @NonNull Stripe stripe, final @NonNull WebClientType webClient, final @NonNull @WebEndpoint String webEndpoint) { @@ -146,6 +148,7 @@ static Environment provideEnvironment(final @NonNull @ActivitySamplePreference I .playServicesCapability(playServicesCapability) .scheduler(scheduler) .sharedPreferences(sharedPreferences) + .stripe(stripe) .webClient(webClient) .webEndpoint(webEndpoint) .build(); @@ -534,4 +537,13 @@ SharedPreferences provideSharedPreferences() { static StringPreferenceType provideUserPreference(final @NonNull SharedPreferences sharedPreferences) { return new StringPreference(sharedPreferences, SharedPreferenceKey.USER); } + + @Provides + @Singleton + Stripe provideStripe(final @ApplicationContext @NonNull Context context, final @NonNull ApiEndpoint apiEndpoint) { + final String stripePublishableKey = apiEndpoint == ApiEndpoint.PRODUCTION + ? Secrets.StripePublishableKey.PRODUCTION + : Secrets.StripePublishableKey.STAGING; + return new Stripe(context, stripePublishableKey); + } } diff --git a/app/src/main/java/com/kickstarter/extensions/ActivityExt.kt b/app/src/main/java/com/kickstarter/extensions/ActivityExt.kt index 3ae7dc7cbb..e7ab4abc5e 100644 --- a/app/src/main/java/com/kickstarter/extensions/ActivityExt.kt +++ b/app/src/main/java/com/kickstarter/extensions/ActivityExt.kt @@ -33,7 +33,7 @@ fun showErrorSnackbar(anchor: View, message: String) { } fun Activity.showHeadsUpSnackbar(anchor: View, stringResId: Int) { - showErrorSnackbar(anchor, getString(stringResId)) + showHeadsUpSnackbar(anchor, getString(stringResId)) } fun showHeadsUpSnackbar(anchor: View, message: String) { diff --git a/app/src/main/java/com/kickstarter/libs/ActivityRequestCodes.java b/app/src/main/java/com/kickstarter/libs/ActivityRequestCodes.java index 4de4a21e6c..2d425b5033 100644 --- a/app/src/main/java/com/kickstarter/libs/ActivityRequestCodes.java +++ b/app/src/main/java/com/kickstarter/libs/ActivityRequestCodes.java @@ -7,4 +7,5 @@ private ActivityRequestCodes() {} public final static int CHECKOUT_ACTIVITY_WALLET_REQUEST = 592_10; public final static int CHECKOUT_ACTIVITY_WALLET_CHANGE_REQUEST = 592_11; public final static int CHECKOUT_ACTIVITY_WALLET_OBTAINED_FULL = 592_12; + public final static int SAVE_NEW_PAYMENT_METHOD = 592_13; } diff --git a/app/src/main/java/com/kickstarter/libs/Environment.java b/app/src/main/java/com/kickstarter/libs/Environment.java index 0d7cbba0ab..33c40ce6ad 100644 --- a/app/src/main/java/com/kickstarter/libs/Environment.java +++ b/app/src/main/java/com/kickstarter/libs/Environment.java @@ -10,6 +10,7 @@ import com.kickstarter.services.ApiClientType; import com.kickstarter.services.ApolloClientType; import com.kickstarter.services.WebClientType; +import com.stripe.android.Stripe; import java.net.CookieManager; @@ -38,6 +39,7 @@ public abstract class Environment implements Parcelable { public abstract PlayServicesCapability playServicesCapability(); public abstract Scheduler scheduler(); public abstract SharedPreferences sharedPreferences(); + public abstract Stripe stripe(); public abstract WebClientType webClient(); public abstract String webEndpoint(); @@ -63,6 +65,7 @@ public abstract static class Builder { public abstract Builder playServicesCapability(PlayServicesCapability __); public abstract Builder scheduler(Scheduler __); public abstract Builder sharedPreferences(SharedPreferences __); + public abstract Builder stripe(Stripe __); public abstract Builder webClient(WebClientType __); public abstract Builder webEndpoint(String __); public abstract Environment build(); diff --git a/app/src/main/java/com/kickstarter/libs/utils/Secrets.java.example b/app/src/main/java/com/kickstarter/libs/utils/Secrets.java.example index 40e09fec19..b5f948c7f6 100644 --- a/app/src/main/java/com/kickstarter/libs/utils/Secrets.java.example +++ b/app/src/main/java/com/kickstarter/libs/utils/Secrets.java.example @@ -43,6 +43,11 @@ public final class Secrets { public static final Pattern STAGING = Pattern.compile("\\Astaging\\z"); } + public static final class StripePublishableKey { + public static final String PRODUCTION = "pk_live"; + public static final String STAGING = "pk_test"; + } + public static final class WebEndpoint { public static final String PRODUCTION = "https://www.kickstarter.com"; public static final String STAGING = "https://staging.com"; diff --git a/app/src/main/java/com/kickstarter/mock/factories/CardFactory.kt b/app/src/main/java/com/kickstarter/mock/factories/CardFactory.kt new file mode 100644 index 0000000000..b5b0ff8656 --- /dev/null +++ b/app/src/main/java/com/kickstarter/mock/factories/CardFactory.kt @@ -0,0 +1,15 @@ +package com.kickstarter.mock.factories + +import com.stripe.android.model.Card + +class CardFactory { + + companion object { + @JvmOverloads + fun card(number: String? = "4242424242424242", expMonth: Int? = 1, expYear: Int? = 2025, cvc: String? = "555"): Card { + return Card.Builder(number, expMonth, expYear, cvc) + .id("3") + .build() + } + } +} diff --git a/app/src/main/java/com/kickstarter/mock/services/MockApolloClient.kt b/app/src/main/java/com/kickstarter/mock/services/MockApolloClient.kt index 79c1c7dbf9..27c9e5db5e 100644 --- a/app/src/main/java/com/kickstarter/mock/services/MockApolloClient.kt +++ b/app/src/main/java/com/kickstarter/mock/services/MockApolloClient.kt @@ -1,5 +1,6 @@ package com.kickstarter.mock.services +import SavePaymentMethodMutation import UpdateUserCurrencyMutation import UpdateUserEmailMutation import UpdateUserPasswordMutation @@ -7,20 +8,21 @@ import UserPaymentsQuery import UserPrivacyQuery import com.kickstarter.services.ApolloClientType import rx.Observable -import type.CreditCardPaymentType -import type.CreditCardState -import type.CreditCardTypes -import type.CurrencyCode +import type.* import java.util.* open class MockApolloClient : ApolloClientType { override fun getStoredCards(): Observable { return Observable.just(UserPaymentsQuery.Data(UserPaymentsQuery.Me("", - UserPaymentsQuery.StoredCards("", List(1 - ) { _ -> UserPaymentsQuery.Node("","4333", Date(), "1234", + UserPaymentsQuery.StoredCards("", List(1) + { _ -> UserPaymentsQuery.Node("","4333", Date(), "1234", CreditCardState.ACTIVE, CreditCardPaymentType.CREDIT_CARD, CreditCardTypes.VISA )})))) } + override fun savePaymentMethod(paymentTypes: PaymentTypes, stripeToken: String, cardId: String): Observable { + return Observable.just(SavePaymentMethodMutation.Data(SavePaymentMethodMutation.CreatePaymentSource("", null , true))) + } + override fun updateUserCurrencyPreference(currency: CurrencyCode): Observable { return Observable.just(UpdateUserCurrencyMutation.Data(UpdateUserCurrencyMutation.UpdateUserProfile("", UpdateUserCurrencyMutation.User("", "USD")))) diff --git a/app/src/main/java/com/kickstarter/mock/services/MockStripe.kt b/app/src/main/java/com/kickstarter/mock/services/MockStripe.kt new file mode 100644 index 0000000000..20d443adb5 --- /dev/null +++ b/app/src/main/java/com/kickstarter/mock/services/MockStripe.kt @@ -0,0 +1,19 @@ +package com.kickstarter.mock.services + +import android.content.Context +import android.support.annotation.NonNull +import com.kickstarter.mock.factories.CardFactory +import com.stripe.android.Stripe +import com.stripe.android.TokenCallback +import com.stripe.android.model.Card +import com.stripe.android.model.Token +import java.util.* + +class MockStripe(@NonNull val context: Context, private val withErrors: Boolean) : Stripe(context) { + override fun createToken(card: Card, callback: TokenCallback) { + when { + this.withErrors -> callback.onError(Exception("Stripe error")) + else -> callback.onSuccess(Token("25", false, Date(), false, CardFactory.card())) + } + } +} diff --git a/app/src/main/java/com/kickstarter/services/ApolloClientType.kt b/app/src/main/java/com/kickstarter/services/ApolloClientType.kt index 392bb6df7e..a7acccd8fe 100644 --- a/app/src/main/java/com/kickstarter/services/ApolloClientType.kt +++ b/app/src/main/java/com/kickstarter/services/ApolloClientType.kt @@ -1,5 +1,6 @@ package com.kickstarter.services +import SavePaymentMethodMutation import UpdateUserCurrencyMutation import UpdateUserEmailMutation import UpdateUserPasswordMutation @@ -7,10 +8,13 @@ import UserPaymentsQuery import UserPrivacyQuery import rx.Observable import type.CurrencyCode +import type.PaymentTypes interface ApolloClientType { fun getStoredCards(): Observable + fun savePaymentMethod(paymentTypes: PaymentTypes, stripeToken: String, cardId: String): Observable + fun updateUserCurrencyPreference(currency: CurrencyCode): Observable fun updateUserEmail(email: String, currentPassword: String): Observable diff --git a/app/src/main/java/com/kickstarter/services/KSApolloClient.kt b/app/src/main/java/com/kickstarter/services/KSApolloClient.kt index a519809ea3..3b50f49a0f 100644 --- a/app/src/main/java/com/kickstarter/services/KSApolloClient.kt +++ b/app/src/main/java/com/kickstarter/services/KSApolloClient.kt @@ -1,5 +1,6 @@ package com.kickstarter.services +import SavePaymentMethodMutation import UpdateUserCurrencyMutation import UpdateUserEmailMutation import UpdateUserPasswordMutation @@ -12,6 +13,7 @@ import com.apollographql.apollo.exception.ApolloException import rx.Observable import rx.subjects.PublishSubject import type.CurrencyCode +import type.PaymentTypes class KSApolloClient(val service: ApolloClient) : ApolloClientType { override fun getStoredCards(): Observable { @@ -35,6 +37,37 @@ class KSApolloClient(val service: ApolloClient) : ApolloClientType { } } + override fun savePaymentMethod(paymentTypes: PaymentTypes, stripeToken: String, cardId: String): Observable { + return Observable.defer { + val ps = PublishSubject.create() + service.mutate(SavePaymentMethodMutation.builder() + .paymentType(paymentTypes) + .stripeToken(stripeToken) + .stripeCardId(cardId) + .build()) + .enqueue(object : ApolloCall.Callback() { + override fun onFailure(exception: ApolloException) { + ps.onError(exception) + } + + override fun onResponse(response: Response) { + if (response.hasErrors()) { + ps.onError(Exception(response.errors().first().message())) + } + //why wouldn't this just be an error? + val createPaymentSource = response.data()?.createPaymentSource() + if (!createPaymentSource?.isSuccessful!!) { + ps.onError(Exception(createPaymentSource.errorMessage())) + } else { + ps.onNext(response.data()) + ps.onCompleted() + } + } + }) + return@defer ps + } + } + override fun updateUserCurrencyPreference(currency: CurrencyCode): Observable { return Observable.defer { val ps = PublishSubject.create() diff --git a/app/src/main/java/com/kickstarter/ui/activities/NewCardActivity.kt b/app/src/main/java/com/kickstarter/ui/activities/NewCardActivity.kt new file mode 100644 index 0000000000..9f35b8496b --- /dev/null +++ b/app/src/main/java/com/kickstarter/ui/activities/NewCardActivity.kt @@ -0,0 +1,23 @@ +package com.kickstarter.ui.activities + +import android.app.Activity +import android.os.Bundle +import com.kickstarter.R +import com.kickstarter.libs.BaseActivity +import com.kickstarter.libs.qualifiers.RequiresActivityViewModel +import com.kickstarter.ui.fragments.NewCardFragment +import com.kickstarter.viewmodels.NewCardViewModel + +@RequiresActivityViewModel(NewCardViewModel.ViewModel::class) +class NewCardActivity : BaseActivity(), NewCardFragment.OnCardSavedListener { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_new_card) + } + + override fun cardSaved() { + setResult(Activity.RESULT_OK) + finish() + } +} diff --git a/app/src/main/java/com/kickstarter/ui/activities/PaymentMethodsActivity.kt b/app/src/main/java/com/kickstarter/ui/activities/PaymentMethodsActivity.kt index b180936c44..e3685d896d 100644 --- a/app/src/main/java/com/kickstarter/ui/activities/PaymentMethodsActivity.kt +++ b/app/src/main/java/com/kickstarter/ui/activities/PaymentMethodsActivity.kt @@ -1,14 +1,19 @@ package com.kickstarter.ui.activities import UserPaymentsQuery +import android.app.Activity +import android.content.Intent import android.os.Bundle import android.support.v7.widget.LinearLayoutManager import com.kickstarter.R +import com.kickstarter.extensions.showConfirmationSnackbar +import com.kickstarter.libs.ActivityRequestCodes import com.kickstarter.libs.BaseActivity import com.kickstarter.libs.qualifiers.RequiresActivityViewModel import com.kickstarter.ui.adapters.PaymentMethodsAdapter import com.kickstarter.viewmodels.PaymentMethodsViewModel -import kotlinx.android.synthetic.main.activity_payment_method.* +import kotlinx.android.synthetic.main.activity_settings_payment_methods.* +import kotlinx.android.synthetic.main.payment_methods_toolbar.* import rx.android.schedulers.AndroidSchedulers @RequiresActivityViewModel(PaymentMethodsViewModel.ViewModel::class) @@ -18,20 +23,30 @@ class PaymentMethodsActivity : BaseActivity() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_payment_method) + setContentView(R.layout.activity_settings_payment_methods) - setupRecyclerview() + setUpRecyclerView() - this.viewModel.outputs.getCards() + this.viewModel.outputs.cards() .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe { setCards(it) } + add_new_card.setOnClickListener { startActivityForResult(Intent(this, NewCardActivity::class.java), ActivityRequestCodes.SAVE_NEW_PAYMENT_METHOD) } + + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { + super.onActivityResult(requestCode, resultCode, intent) + if (requestCode == ActivityRequestCodes.SAVE_NEW_PAYMENT_METHOD && resultCode == Activity.RESULT_OK) { + showConfirmationSnackbar(payment_methods_toolbar, R.string.Got_it_your_changes_have_been_saved) + this@PaymentMethodsActivity.viewModel.inputs.refreshCards() + } } private fun setCards(cards: MutableList) = this.adapter.populateCards(cards) - private fun setupRecyclerview() { + private fun setUpRecyclerView() { this.adapter = PaymentMethodsAdapter(this.viewModel) recycler_view.adapter = this.adapter recycler_view.layoutManager = LinearLayoutManager(this) diff --git a/app/src/main/java/com/kickstarter/ui/adapters/PaymentMethodsAdapter.kt b/app/src/main/java/com/kickstarter/ui/adapters/PaymentMethodsAdapter.kt index 2ad217ef73..77511b2fa2 100644 --- a/app/src/main/java/com/kickstarter/ui/adapters/PaymentMethodsAdapter.kt +++ b/app/src/main/java/com/kickstarter/ui/adapters/PaymentMethodsAdapter.kt @@ -8,14 +8,18 @@ import com.kickstarter.ui.viewholders.PaymentMethodsViewHolder class PaymentMethodsAdapter(private val delegate: PaymentMethodsViewHolder.Delegate): KSAdapter() { + init { + addSection(emptyList()) + } + interface Delegate: PaymentMethodsViewHolder.Delegate - override fun layout(sectionRow: SectionRow): Int = R.layout.list_item_payment_methods + override fun layout(sectionRow: SectionRow): Int = R.layout.item_payment_method override fun viewHolder(layout: Int, view: View): KSViewHolder = PaymentMethodsViewHolder(view, delegate) fun populateCards(cards: MutableList) { - addSection(cards) + setSection(0, cards) notifyDataSetChanged() } } diff --git a/app/src/main/java/com/kickstarter/ui/fragments/NewCardFragment.kt b/app/src/main/java/com/kickstarter/ui/fragments/NewCardFragment.kt new file mode 100644 index 0000000000..24099c0522 --- /dev/null +++ b/app/src/main/java/com/kickstarter/ui/fragments/NewCardFragment.kt @@ -0,0 +1,150 @@ +package com.kickstarter.ui.fragments + +import android.content.Context +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.text.Editable +import android.text.TextWatcher +import android.view.* +import com.kickstarter.R +import com.kickstarter.extensions.onChange +import com.kickstarter.extensions.showErrorSnackbar +import com.kickstarter.libs.BaseFragment +import com.kickstarter.libs.qualifiers.RequiresFragmentViewModel +import com.kickstarter.libs.rx.transformers.Transformers +import com.kickstarter.libs.utils.ViewUtils +import com.kickstarter.viewmodels.NewCardFragmentViewModel +import com.stripe.android.view.CardInputListener +import kotlinx.android.synthetic.main.fragment_new_card.* + +@RequiresFragmentViewModel(NewCardFragmentViewModel.ViewModel::class) +class NewCardFragment : BaseFragment() { + interface OnCardSavedListener { + fun cardSaved() + } + + private var saveEnabled = false + private var onCardSavedListener: OnCardSavedListener? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + super.onCreateView(inflater, container, savedInstanceState) + return inflater.inflate(R.layout.fragment_new_card, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + (activity as AppCompatActivity).setSupportActionBar(new_card_toolbar) + setHasOptionsMenu(true) + + this.viewModel.outputs.cardWidgetFocusDrawable() + .compose(bindToLifecycle()) + .compose(Transformers.observeForUI()) + .subscribe { card_focus.setImageResource(it) } + + this.viewModel.outputs.progressBarIsVisible() + .compose(bindToLifecycle()) + .compose(Transformers.observeForUI()) + .subscribe { ViewUtils.setGone(progress_bar, !it) } + + this.viewModel.outputs.saveButtonIsEnabled() + .compose(bindToLifecycle()) + .compose(Transformers.observeForUI()) + .subscribe { updateMenu(it) } + + this.viewModel.outputs.success() + .compose(bindToLifecycle()) + .compose(Transformers.observeForUI()) + .subscribe { onCardSavedListener?.cardSaved() } + + this.viewModel.outputs.error() + .compose(bindToLifecycle()) + .compose(Transformers.observeForUI()) + .subscribe { showErrorSnackbar(new_card_toolbar, it) } + + cardholder_name.onChange { this.viewModel.inputs.name(it) } + postal_code.onChange { this.viewModel.inputs.postalCode(it) } + addListeners() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.save -> { + this.viewModel.inputs.saveCardClicked() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + onCardSavedListener = context as? OnCardSavedListener + if (onCardSavedListener == null) { + throw ClassCastException("$context must implement OnArticleSelectedListener") + } + } + + override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.save, menu) + } + + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + val save = menu.findItem(R.id.save) + save.isEnabled = saveEnabled + } + + private fun addListeners() { + card_input_widget.clearFocus() + cardholder_name.onFocusChangeListener = cardFocusChangeListener + postal_code.onFocusChangeListener = cardFocusChangeListener + card_input_widget.setCardNumberTextWatcher(cardValidityWatcher) + card_input_widget.setCvcNumberTextWatcher(cardValidityWatcher) + card_input_widget.setExpiryDateTextWatcher(cardValidityWatcher) + + card_input_widget.setCardInputListener(object : CardInputListener { + override fun onFocusChange(focusField: String?) { + this@NewCardFragment.viewModel.inputs.cardFocus(true) + } + + override fun onPostalCodeComplete() { + } + + override fun onCardComplete() { + cardChanged() + } + + override fun onExpirationComplete() { + cardChanged() + } + + override fun onCvcComplete() { + cardChanged() + } + }) + } + + private fun cardChanged() { + this.viewModel.inputs.card(card_input_widget.card) + } + + private fun updateMenu(saveEnabled: Boolean) { + this.saveEnabled = saveEnabled + activity?.invalidateOptionsMenu() + } + + private val cardValidityWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + cardChanged() + } + } + + private val cardFocusChangeListener = View.OnFocusChangeListener { _, _ -> this@NewCardFragment.viewModel.inputs.cardFocus(false) } +} diff --git a/app/src/main/java/com/kickstarter/ui/viewholders/PaymentMethodsViewHolder.kt b/app/src/main/java/com/kickstarter/ui/viewholders/PaymentMethodsViewHolder.kt index ea7e3c6807..dc230537df 100644 --- a/app/src/main/java/com/kickstarter/ui/viewholders/PaymentMethodsViewHolder.kt +++ b/app/src/main/java/com/kickstarter/ui/viewholders/PaymentMethodsViewHolder.kt @@ -6,7 +6,7 @@ import android.view.View import com.kickstarter.R import com.kickstarter.libs.rx.transformers.Transformers.observeForUI import com.kickstarter.viewmodels.PaymentMethodsViewHolderViewModel -import kotlinx.android.synthetic.main.list_item_payment_methods.view.* +import kotlinx.android.synthetic.main.item_payment_method.view.* class PaymentMethodsViewHolder(@NonNull view: View, @NonNull delegate: Delegate) : KSViewHolder(view) { diff --git a/app/src/main/java/com/kickstarter/viewmodels/NewCardFragmentViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/NewCardFragmentViewModel.kt new file mode 100644 index 0000000000..7707b8b9e1 --- /dev/null +++ b/app/src/main/java/com/kickstarter/viewmodels/NewCardFragmentViewModel.kt @@ -0,0 +1,206 @@ +package com.kickstarter.viewmodels + +import android.support.annotation.NonNull +import com.kickstarter.R +import com.kickstarter.libs.Environment +import com.kickstarter.libs.FragmentViewModel +import com.kickstarter.libs.rx.transformers.Transformers +import com.kickstarter.libs.rx.transformers.Transformers.takeWhen +import com.kickstarter.libs.rx.transformers.Transformers.values +import com.kickstarter.services.ApolloClientType +import com.kickstarter.ui.fragments.NewCardFragment +import com.stripe.android.Stripe +import com.stripe.android.TokenCallback +import com.stripe.android.model.Card +import com.stripe.android.model.Token +import rx.Observable +import rx.subjects.BehaviorSubject +import rx.subjects.PublishSubject +import type.PaymentTypes +import java.lang.Exception + +interface NewCardFragmentViewModel { + interface Inputs { + /** Call when the card validity changes. */ + fun card(card: Card?) + + /** Call when the name field changes. */ + fun name(name: String) + + /** Call when the postal code field changes. */ + fun postalCode(postalCode: String) + + /** Call when the user clicks the save icon. */ + fun saveCardClicked() + + /** Call when the card input has focus. */ + fun cardFocus(hasFocus: Boolean) + } + + interface Outputs { + /** Emits when the drawable to be shown when the card widget has focus. */ + fun cardWidgetFocusDrawable(): Observable + + /** Emits when the password update was unsuccessful. */ + fun error(): Observable + + /** Emits when the progress bar should be visible. */ + fun progressBarIsVisible(): Observable + + /** Emits when the save button should be enabled. */ + fun saveButtonIsEnabled(): Observable + + /** Emits when the card was saved successfully. */ + fun success(): Observable + + } + + class ViewModel(@NonNull val environment: Environment) : FragmentViewModel(environment), Inputs, Outputs { + + private val card = PublishSubject.create() + private val cardFocus = PublishSubject.create() + private val name = PublishSubject.create() + private val postalCode = PublishSubject.create() + private val saveCardClicked = PublishSubject.create() + + private val cardWidgetFocusDrawable = BehaviorSubject.create() + private val error = BehaviorSubject.create() + private val progressBarIsVisible = BehaviorSubject.create() + private val saveButtonIsEnabled = BehaviorSubject.create() + private val success = BehaviorSubject.create() + + val inputs: Inputs = this + val outputs: Outputs = this + + private val apolloClient = this.environment.apolloClient() + private val stripe = this.environment.stripe() + + init { + val cardForm = Observable.combineLatest(this.name.startWith(""), + this.card.startWith(null, null), + this.postalCode.startWith(""), + { name, card, postalCode -> CardForm(name, card, postalCode) }) + .skip(1) + + cardForm + .map { it.isValid() } + .distinctUntilChanged() + .compose(bindToLifecycle()) + .subscribe(this.saveButtonIsEnabled) + + this.cardFocus + .map { + when { + it -> R.drawable.divider_green_horizontal + else -> R.drawable.divider_dark_grey_500_horizontal + } + } + .subscribe { this.cardWidgetFocusDrawable.onNext(it) } + + val saveCardNotification = cardForm + .compose(takeWhen(this.saveCardClicked)) + .map { storeNameAndPostalCode(it) } + .switchMap { createTokenAndSaveCard(it).materialize() } + .compose(bindToLifecycle()) + .share() + + saveCardNotification + .compose(values()) + .subscribe { this.success.onNext(null) } + + saveCardNotification + .compose(Transformers.errors()) + .subscribe { this.error.onNext(it.localizedMessage) } + + } + + private fun storeNameAndPostalCode(cardForm: CardForm): Card { + val card = cardForm.card!! + card.name = cardForm.name + card.addressZip = cardForm.postalCode + return card + } + + override fun card(card: Card?) { + this.card.onNext(card) + } + + override fun cardFocus(hasFocus: Boolean) { + this.cardFocus.onNext(hasFocus) + } + + override fun name(name: String) { + this.name.onNext(name) + } + + override fun postalCode(postalCode: String) { + this.postalCode.onNext(postalCode) + } + + override fun saveCardClicked() { + this.saveCardClicked.onNext(null) + } + + override fun cardWidgetFocusDrawable(): Observable { + return this.cardWidgetFocusDrawable + } + + override fun error(): Observable { + return this.error + } + + override fun progressBarIsVisible(): Observable { + return this.progressBarIsVisible + } + + override fun saveButtonIsEnabled(): Observable { + return this.saveButtonIsEnabled + } + + override fun success(): Observable { + return this.success + } + + data class CardForm(val name: String, val card: Card?, val postalCode: String) { + fun isValid(): Boolean { + return isNotEmpty(this.name) + && isNotEmpty(this.postalCode) + && isValidCard(this.card) + } + + private fun isValidCard(card: Card?): Boolean { + return card != null && card.validateNumber() && card.validateExpiryDate() && card.validateCVC() + } + + private fun isNotEmpty(s: String): Boolean { + return !s.isEmpty() + } + } + + private fun createTokenAndSaveCard(card: Card): Observable { + return Observable.defer { + val ps = PublishSubject.create() + this.stripe.createToken(card, object : TokenCallback { + override fun onSuccess(token: Token) { + saveCard(token, ps) + } + + override fun onError(error: Exception?) { + ps.onError(error) + } + }) + return@defer ps + } + .doOnSubscribe { this.progressBarIsVisible.onNext(true) } + .doAfterTerminate { this.progressBarIsVisible.onNext(false) } + } + + private fun saveCard(token: Token, ps: PublishSubject) { + this.apolloClient.savePaymentMethod(PaymentTypes.CREDIT_CARD, token.id, token.card.id) + .subscribe({ + ps.onNext(null) + ps.onCompleted() + }, { ps.onError(it) }) + } + } +} diff --git a/app/src/main/java/com/kickstarter/viewmodels/NewCardViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/NewCardViewModel.kt new file mode 100644 index 0000000000..3ea9105fd6 --- /dev/null +++ b/app/src/main/java/com/kickstarter/viewmodels/NewCardViewModel.kt @@ -0,0 +1,10 @@ +package com.kickstarter.viewmodels + +import android.support.annotation.NonNull +import com.kickstarter.libs.ActivityViewModel +import com.kickstarter.libs.Environment +import com.kickstarter.ui.activities.NewCardActivity + +interface NewCardViewModel { + class ViewModel(@NonNull val environment: Environment) : ActivityViewModel(environment) +} diff --git a/app/src/main/java/com/kickstarter/viewmodels/PaymentMethodsViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/PaymentMethodsViewModel.kt index 24b9e5801d..381568df3c 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/PaymentMethodsViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/PaymentMethodsViewModel.kt @@ -8,30 +8,53 @@ import com.kickstarter.ui.activities.PaymentMethodsActivity import com.kickstarter.ui.adapters.PaymentMethodsAdapter import rx.Observable import rx.subjects.BehaviorSubject +import rx.subjects.PublishSubject interface PaymentMethodsViewModel { + interface Inputs { + /** Call when a new card has been added and the list needs to be updated. */ + fun refreshCards() + } + + interface Outputs { /** Emits a list of stored cards for a user. */ - fun getCards(): Observable> + fun cards(): Observable> } - class ViewModel(environment: Environment) : ActivityViewModel(environment), PaymentMethodsAdapter.Delegate, Outputs { + class ViewModel(environment: Environment) : ActivityViewModel(environment), PaymentMethodsAdapter.Delegate, Inputs, Outputs { + + private val refreshCards = PublishSubject.create() private val cards = BehaviorSubject.create>() private val client = environment.apolloClient() + val inputs: Inputs = this val outputs: Outputs = this init { - this.client.getStoredCards() + getListOfStoredCards() + .subscribe { this.cards.onNext(it) } + + this.refreshCards + .switchMap { getListOfStoredCards() } + .subscribe { this.cards.onNext(it) } + + } + + private fun getListOfStoredCards(): Observable> { + return this.client.getStoredCards() .compose(bindToLifecycle()) .compose(neverError()) .map { cards -> cards.me()?.storedCards()?.nodes() } - .subscribe { this.cards.onNext(it) } } - override fun getCards(): Observable> = this.cards + override fun refreshCards() { + this.refreshCards.onNext(null) + } + + override fun cards(): Observable> = this.cards } } diff --git a/app/src/main/res/drawable/divider_green_horizontal.xml b/app/src/main/res/drawable/divider_green_horizontal.xml new file mode 100644 index 0000000000..8bbf54b4a6 --- /dev/null +++ b/app/src/main/res/drawable/divider_green_horizontal.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml index 26a060a339..3105701431 100644 --- a/app/src/main/res/layout/activity_account.xml +++ b/app/src/main/res/layout/activity_account.xml @@ -103,7 +103,7 @@ diff --git a/app/src/main/res/layout/activity_change_email.xml b/app/src/main/res/layout/activity_change_email.xml index b15d55c0b6..939eed5829 100644 --- a/app/src/main/res/layout/activity_change_email.xml +++ b/app/src/main/res/layout/activity_change_email.xml @@ -63,7 +63,7 @@ @@ -77,7 +77,7 @@ diff --git a/app/src/main/res/layout/activity_change_password.xml b/app/src/main/res/layout/activity_change_password.xml index f980df6379..dc94fe6b1b 100644 --- a/app/src/main/res/layout/activity_change_password.xml +++ b/app/src/main/res/layout/activity_change_password.xml @@ -36,7 +36,7 @@ android:layout_marginTop="@dimen/grid_3"> @@ -66,7 +66,7 @@ @@ -93,7 +93,7 @@ diff --git a/app/src/main/res/layout/activity_new_card.xml b/app/src/main/res/layout/activity_new_card.xml new file mode 100644 index 0000000000..9b5a3aa137 --- /dev/null +++ b/app/src/main/res/layout/activity_new_card.xml @@ -0,0 +1,7 @@ + + diff --git a/app/src/main/res/layout/activity_payment_method.xml b/app/src/main/res/layout/activity_settings_payment_methods.xml similarity index 70% rename from app/src/main/res/layout/activity_payment_method.xml rename to app/src/main/res/layout/activity_settings_payment_methods.xml index 904cedb969..ffb39ab078 100644 --- a/app/src/main/res/layout/activity_payment_method.xml +++ b/app/src/main/res/layout/activity_settings_payment_methods.xml @@ -1,11 +1,11 @@ - + android:background="@color/ksr_grey_100"> + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + android:layout_height="wrap_content" + tools:listitem="@layout/item_payment_method" /> + android:id="@+id/add_new_card" + style="@style/SettingsLinearRow" + android:layout_marginTop="@dimen/grid_1"> + android:textColor="@color/ksr_green_500" /> - + diff --git a/app/src/main/res/layout/fragment_new_card.xml b/app/src/main/res/layout/fragment_new_card.xml new file mode 100644 index 0000000000..0511fe7d9a --- /dev/null +++ b/app/src/main/res/layout/fragment_new_card.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_payment_method.xml b/app/src/main/res/layout/item_payment_method.xml new file mode 100644 index 0000000000..a2afe23828 --- /dev/null +++ b/app/src/main/res/layout/item_payment_method.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/list_item_payment_methods.xml b/app/src/main/res/layout/list_item_payment_methods.xml deleted file mode 100644 index 749a6126ff..0000000000 --- a/app/src/main/res/layout/list_item_payment_methods.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/layout/payment_methods_toolbar.xml b/app/src/main/res/layout/payment_methods_toolbar.xml index 0a22d6f378..a8e2805dc0 100644 --- a/app/src/main/res/layout/payment_methods_toolbar.xml +++ b/app/src/main/res/layout/payment_methods_toolbar.xml @@ -3,7 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" - tools:showIn="@layout/activity_payment_method" + tools:showIn="@layout/activity_settings_payment_methods" style="@style/Toolbar" android:id="@+id/payment_methods_toolbar" app:contentInsetLeft="0dp" @@ -11,7 +11,7 @@ diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 37e8161360..61f2d8056a 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -569,7 +569,7 @@ @dimen/grid_1 - + + diff --git a/app/src/test/java/com/kickstarter/KSRobolectricTestCase.java b/app/src/test/java/com/kickstarter/KSRobolectricTestCase.java index fabc49a621..15bf1e134d 100644 --- a/app/src/test/java/com/kickstarter/KSRobolectricTestCase.java +++ b/app/src/test/java/com/kickstarter/KSRobolectricTestCase.java @@ -6,10 +6,11 @@ import com.kickstarter.libs.Environment; import com.kickstarter.libs.KSString; import com.kickstarter.libs.Koala; -import com.kickstarter.mock.MockCurrentConfig; import com.kickstarter.libs.MockTrackingClient; +import com.kickstarter.mock.MockCurrentConfig; import com.kickstarter.mock.services.MockApiClient; import com.kickstarter.mock.services.MockApolloClient; +import com.kickstarter.mock.services.MockStripe; import com.kickstarter.mock.services.MockWebClient; import junit.framework.TestCase; @@ -47,6 +48,7 @@ public void setUp() throws Exception { .apolloClient(new MockApolloClient()) .currentConfig(new MockCurrentConfig()) .webClient(new MockWebClient()) + .stripe(new MockStripe(context(), false)) .koala(new Koala(testTrackingClient)) .build(); } diff --git a/app/src/test/java/com/kickstarter/viewmodels/NewCardFragmentViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/NewCardFragmentViewModelTest.kt new file mode 100644 index 0000000000..4801f25b5b --- /dev/null +++ b/app/src/test/java/com/kickstarter/viewmodels/NewCardFragmentViewModelTest.kt @@ -0,0 +1,119 @@ +package com.kickstarter.viewmodels + +import SavePaymentMethodMutation +import com.kickstarter.KSRobolectricTestCase +import com.kickstarter.R +import com.kickstarter.libs.Environment +import com.kickstarter.mock.factories.CardFactory +import com.kickstarter.mock.services.MockApolloClient +import com.kickstarter.mock.services.MockStripe +import org.junit.Test +import rx.Observable +import rx.observers.TestSubscriber +import type.PaymentTypes + +class NewCardFragmentViewModelTest : KSRobolectricTestCase() { + + private lateinit var vm: NewCardFragmentViewModel.ViewModel + private val cardWidgetFocusDrawable = TestSubscriber() + private val error = TestSubscriber() + private val progressBarIsVisible = TestSubscriber() + private val saveButtonIsEnabled = TestSubscriber() + private val success = TestSubscriber() + + private fun setUpEnvironment(environment: Environment) { + this.vm = NewCardFragmentViewModel.ViewModel(environment) + this.vm.outputs.cardWidgetFocusDrawable().subscribe(this.cardWidgetFocusDrawable) + this.vm.outputs.error().subscribe(this.error) + this.vm.outputs.progressBarIsVisible().subscribe(this.progressBarIsVisible) + this.vm.outputs.saveButtonIsEnabled().subscribe(this.saveButtonIsEnabled) + this.vm.outputs.success().subscribe(this.success) + } + + @Test + fun testCardWidgetFocusDrawable() { + setUpEnvironment(environment()) + + this.vm.inputs.cardFocus(true) + this.cardWidgetFocusDrawable.assertValuesAndClear(R.drawable.divider_green_horizontal) + + this.vm.inputs.cardFocus(false) + this.cardWidgetFocusDrawable.assertValue(R.drawable.divider_dark_grey_500_horizontal) + } + + @Test + fun testAPIError() { + val apolloClient = object : MockApolloClient() { + override fun savePaymentMethod(paymentTypes: PaymentTypes, stripeToken: String, cardId: String): Observable { + return Observable.error(Exception("oops")) + } + } + setUpEnvironment(environment().toBuilder().apolloClient(apolloClient).build()) + + this.vm.inputs.name("Nathan Squid") + this.vm.inputs.postalCode("11222") + this.vm.inputs.card(CardFactory.card()) + this.vm.inputs.saveCardClicked() + this.error.assertValue("oops") + } + + @Test + fun testStripeError() { + val mockStripe = MockStripe(context(), true) + setUpEnvironment(environment().toBuilder().stripe(mockStripe).build()) + + this.vm.inputs.name("Nathan Squid") + this.vm.inputs.postalCode("11222") + this.vm.inputs.card(CardFactory.card()) + this.vm.inputs.saveCardClicked() + this.error.assertValue("Stripe error") + } + + @Test + fun testProgressBarIsVisible() { + setUpEnvironment(environment()) + + this.vm.inputs.name("Nathan Squid") + this.vm.inputs.postalCode("11222") + this.vm.inputs.card(CardFactory.card()) + this.vm.inputs.saveCardClicked() + this.progressBarIsVisible.assertValues(true, false) + } + + @Test + fun testSaveButtonIsEnabled() { + setUpEnvironment(environment()) + + this.vm.inputs.name("Nathan Squid") + this.saveButtonIsEnabled.assertValues(false) + this.vm.inputs.card(CardFactory.card("4242424242424242", null, null, null)) + this.saveButtonIsEnabled.assertValues(false) + this.vm.inputs.card(CardFactory.card("4242424242424242", 1, null, null)) + this.saveButtonIsEnabled.assertValues(false) + this.vm.inputs.card(CardFactory.card("4242424242424242", 1, 2020, null)) + this.saveButtonIsEnabled.assertValues(false) + this.vm.inputs.card(CardFactory.card("4242424242424242", 1, 2020, "555")) + this.saveButtonIsEnabled.assertValues(false) + this.vm.inputs.postalCode("11222") + this.saveButtonIsEnabled.assertValues(false, true) + this.vm.inputs.card(CardFactory.card("424242424242424", 1, 2020, "555")) + this.saveButtonIsEnabled.assertValues(false, true, false) + this.vm.inputs.card(CardFactory.card("4242424242424242", null, 2020, "555")) + this.saveButtonIsEnabled.assertValues(false, true, false) + this.vm.inputs.card(CardFactory.card("4242424242424242", 1, null, "555")) + this.saveButtonIsEnabled.assertValues(false, true, false) + this.vm.inputs.card(CardFactory.card("4242424242424242", 1, 2020, null)) + this.saveButtonIsEnabled.assertValues(false, true, false) + } + + @Test + fun testSuccess() { + setUpEnvironment(environment()) + + this.vm.inputs.name("Nathan Squid") + this.vm.inputs.postalCode("11222") + this.vm.inputs.card(CardFactory.card()) + this.vm.inputs.saveCardClicked() + this.success.assertValues() + } +} diff --git a/app/src/test/java/com/kickstarter/viewmodels/PaymentMethodsViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/PaymentMethodsViewModelTest.kt index c893e64de3..94797b375d 100644 --- a/app/src/test/java/com/kickstarter/viewmodels/PaymentMethodsViewModelTest.kt +++ b/app/src/test/java/com/kickstarter/viewmodels/PaymentMethodsViewModelTest.kt @@ -21,7 +21,7 @@ class PaymentMethodsViewModelTest : KSRobolectricTestCase() { private fun setUpEnvironment(environment: Environment) { this.vm = PaymentMethodsViewModel.ViewModel(environment) - this.vm.outputs.getCards().subscribe(this.cards) + this.vm.outputs.cards().subscribe(this.cards) } @Test