Skip to content

Commit

Permalink
[馃挸] Saving credit cards (#368)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
eoji committed Nov 6, 2018
1 parent 2436858 commit 6b5f4f1
Show file tree
Hide file tree
Showing 35 changed files with 905 additions and 116 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".ui.activities.ProfileActivity" />
</activity>
<activity android:name=".ui.activities.NewCardActivity" />
<activity android:name=".ui.activities.NewsletterActivity" />
<activity android:name=".ui.activities.NotificationsActivity" />
<activity android:name=".ui.activities.PaymentMethodsActivity"/>
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/graphql/payments.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,10 @@ query UserPayments {
}
}
}

mutation SavePaymentMethod($paymentType: PaymentTypes!, $stripeToken: String!, $stripeCardId: String!) {
createPaymentSource(input: {paymentType: $paymentType, stripeToken: $stripeToken, stripeCardId: $stripeCardId}) {
errorMessage
isSuccessful
}
}
12 changes: 12 additions & 0 deletions app/src/main/java/com/kickstarter/ApplicationModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {

Expand All @@ -146,6 +148,7 @@ static Environment provideEnvironment(final @NonNull @ActivitySamplePreference I
.playServicesCapability(playServicesCapability)
.scheduler(scheduler)
.sharedPreferences(sharedPreferences)
.stripe(stripe)
.webClient(webClient)
.webEndpoint(webEndpoint)
.build();
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
3 changes: 3 additions & 0 deletions app/src/main/java/com/kickstarter/libs/Environment.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();

Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
15 changes: 15 additions & 0 deletions app/src/main/java/com/kickstarter/mock/factories/CardFactory.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
package com.kickstarter.mock.services

import SavePaymentMethodMutation
import UpdateUserCurrencyMutation
import UpdateUserEmailMutation
import UpdateUserPasswordMutation
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<UserPaymentsQuery.Data> {
return Observable.just(UserPaymentsQuery.Data(UserPaymentsQuery.Me("",
UserPaymentsQuery.StoredCards("", List<UserPaymentsQuery.Node>(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<SavePaymentMethodMutation.Data> {
return Observable.just(SavePaymentMethodMutation.Data(SavePaymentMethodMutation.CreatePaymentSource("", null , true)))
}

override fun updateUserCurrencyPreference(currency: CurrencyCode): Observable<UpdateUserCurrencyMutation.Data> {
return Observable.just(UpdateUserCurrencyMutation.Data(UpdateUserCurrencyMutation.UpdateUserProfile("",
UpdateUserCurrencyMutation.User("", "USD"))))
Expand Down
19 changes: 19 additions & 0 deletions app/src/main/java/com/kickstarter/mock/services/MockStripe.kt
Original file line number Diff line number Diff line change
@@ -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()))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package com.kickstarter.services

import SavePaymentMethodMutation
import UpdateUserCurrencyMutation
import UpdateUserEmailMutation
import UpdateUserPasswordMutation
import UserPaymentsQuery
import UserPrivacyQuery
import rx.Observable
import type.CurrencyCode
import type.PaymentTypes

interface ApolloClientType {
fun getStoredCards(): Observable<UserPaymentsQuery.Data>

fun savePaymentMethod(paymentTypes: PaymentTypes, stripeToken: String, cardId: String): Observable<SavePaymentMethodMutation.Data>

fun updateUserCurrencyPreference(currency: CurrencyCode): Observable<UpdateUserCurrencyMutation.Data>

fun updateUserEmail(email: String, currentPassword: String): Observable<UpdateUserEmailMutation.Data>
Expand Down
33 changes: 33 additions & 0 deletions app/src/main/java/com/kickstarter/services/KSApolloClient.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.kickstarter.services

import SavePaymentMethodMutation
import UpdateUserCurrencyMutation
import UpdateUserEmailMutation
import UpdateUserPasswordMutation
Expand All @@ -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<UserPaymentsQuery.Data> {
Expand All @@ -35,6 +37,37 @@ class KSApolloClient(val service: ApolloClient) : ApolloClientType {
}
}

override fun savePaymentMethod(paymentTypes: PaymentTypes, stripeToken: String, cardId: String): Observable<SavePaymentMethodMutation.Data> {
return Observable.defer {
val ps = PublishSubject.create<SavePaymentMethodMutation.Data>()
service.mutate(SavePaymentMethodMutation.builder()
.paymentType(paymentTypes)
.stripeToken(stripeToken)
.stripeCardId(cardId)
.build())
.enqueue(object : ApolloCall.Callback<SavePaymentMethodMutation.Data>() {
override fun onFailure(exception: ApolloException) {
ps.onError(exception)
}

override fun onResponse(response: Response<SavePaymentMethodMutation.Data>) {
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<UpdateUserCurrencyMutation.Data> {
return Observable.defer {
val ps = PublishSubject.create<UpdateUserCurrencyMutation.Data>()
Expand Down
23 changes: 23 additions & 0 deletions app/src/main/java/com/kickstarter/ui/activities/NewCardActivity.kt
Original file line number Diff line number Diff line change
@@ -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<NewCardViewModel.ViewModel>(), NewCardFragment.OnCardSavedListener {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_new_card)
}

override fun cardSaved() {
setResult(Activity.RESULT_OK)
finish()
}
}
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -18,20 +23,30 @@ class PaymentMethodsActivity : BaseActivity<PaymentMethodsViewModel.ViewModel>()

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<UserPaymentsQuery.Node>) = 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ import com.kickstarter.ui.viewholders.PaymentMethodsViewHolder

class PaymentMethodsAdapter(private val delegate: PaymentMethodsViewHolder.Delegate): KSAdapter() {

init {
addSection(emptyList<Any>())
}

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<UserPaymentsQuery.Node>) {
addSection(cards)
setSection(0, cards)
notifyDataSetChanged()
}
}
Loading

0 comments on commit 6b5f4f1

Please sign in to comment.