From 04110fb5249e27062eeffd703d592e619224aa6c Mon Sep 17 00:00:00 2001 From: Michael Shafrir Date: Thu, 1 Aug 2019 22:21:43 -0400 Subject: [PATCH 1/2] Convert samplestore app to Kotlin --- .travis.yml | 2 +- build.gradle | 2 + samplestore/build.gradle | 39 +- .../com/stripe/samplestore/ItemDivider.java | 42 -- .../com/stripe/samplestore/ItemDivider.kt | 37 + .../stripe/samplestore/PaymentActivity.java | 643 ------------------ .../com/stripe/samplestore/PaymentActivity.kt | 564 +++++++++++++++ .../stripe/samplestore/RetrofitFactory.java | 50 -- .../com/stripe/samplestore/RetrofitFactory.kt | 40 ++ .../samplestore/SampleStoreApplication.java | 26 - .../samplestore/SampleStoreApplication.kt | 25 + .../java/com/stripe/samplestore/Settings.java | 29 - .../java/com/stripe/samplestore/Settings.kt | 26 + .../com/stripe/samplestore/StoreActivity.java | 174 ----- .../com/stripe/samplestore/StoreActivity.kt | 156 +++++ .../com/stripe/samplestore/StoreAdapter.java | 194 ------ .../com/stripe/samplestore/StoreAdapter.kt | 162 +++++ .../com/stripe/samplestore/StoreCart.java | 99 --- .../java/com/stripe/samplestore/StoreCart.kt | 78 +++ .../com/stripe/samplestore/StoreLineItem.java | 69 -- .../com/stripe/samplestore/StoreLineItem.kt | 49 ++ .../com/stripe/samplestore/StoreUtils.java | 59 -- .../java/com/stripe/samplestore/StoreUtils.kt | 53 ++ .../SampleStoreEphemeralKeyProvider.java | 73 -- .../SampleStoreEphemeralKeyProvider.kt | 59 ++ .../samplestore/service/StripeService.java | 45 -- .../samplestore/service/StripeService.kt | 41 ++ 27 files changed, 1328 insertions(+), 1508 deletions(-) delete mode 100644 samplestore/src/main/java/com/stripe/samplestore/ItemDivider.java create mode 100644 samplestore/src/main/java/com/stripe/samplestore/ItemDivider.kt delete mode 100644 samplestore/src/main/java/com/stripe/samplestore/PaymentActivity.java create mode 100644 samplestore/src/main/java/com/stripe/samplestore/PaymentActivity.kt delete mode 100644 samplestore/src/main/java/com/stripe/samplestore/RetrofitFactory.java create mode 100644 samplestore/src/main/java/com/stripe/samplestore/RetrofitFactory.kt delete mode 100644 samplestore/src/main/java/com/stripe/samplestore/SampleStoreApplication.java create mode 100644 samplestore/src/main/java/com/stripe/samplestore/SampleStoreApplication.kt delete mode 100644 samplestore/src/main/java/com/stripe/samplestore/Settings.java create mode 100644 samplestore/src/main/java/com/stripe/samplestore/Settings.kt delete mode 100644 samplestore/src/main/java/com/stripe/samplestore/StoreActivity.java create mode 100644 samplestore/src/main/java/com/stripe/samplestore/StoreActivity.kt delete mode 100644 samplestore/src/main/java/com/stripe/samplestore/StoreAdapter.java create mode 100644 samplestore/src/main/java/com/stripe/samplestore/StoreAdapter.kt delete mode 100644 samplestore/src/main/java/com/stripe/samplestore/StoreCart.java create mode 100644 samplestore/src/main/java/com/stripe/samplestore/StoreCart.kt delete mode 100644 samplestore/src/main/java/com/stripe/samplestore/StoreLineItem.java create mode 100644 samplestore/src/main/java/com/stripe/samplestore/StoreLineItem.kt delete mode 100644 samplestore/src/main/java/com/stripe/samplestore/StoreUtils.java create mode 100644 samplestore/src/main/java/com/stripe/samplestore/StoreUtils.kt delete mode 100644 samplestore/src/main/java/com/stripe/samplestore/service/SampleStoreEphemeralKeyProvider.java create mode 100644 samplestore/src/main/java/com/stripe/samplestore/service/SampleStoreEphemeralKeyProvider.kt delete mode 100644 samplestore/src/main/java/com/stripe/samplestore/service/StripeService.java create mode 100644 samplestore/src/main/java/com/stripe/samplestore/service/StripeService.kt diff --git a/.travis.yml b/.travis.yml index 52dfeb1a77a..832fee57c0b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,5 +23,5 @@ before_install: - yes | sdkmanager "platforms;android-28" script: - - ./gradlew :stripe:checkstyle :example:checkstyle :samplestore:checkstyle lint + - ./gradlew :stripe:checkstyle :example:checkstyle :samplestore:checkstyle :samplestore:ktlint lint - ./gradlew clean test diff --git a/build.gradle b/build.gradle index 52b71bb8f9b..2645f60a7df 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext.kotlin_version = '1.3.41' repositories { jcenter() google() @@ -9,6 +10,7 @@ buildscript { classpath 'com.android.tools.build:gradle:3.5.0-rc02' classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.8.6' classpath 'io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.21.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/samplestore/build.gradle b/samplestore/build.gradle index 3720f819e2d..216cd3f895f 100644 --- a/samplestore/build.gradle +++ b/samplestore/build.gradle @@ -1,9 +1,15 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' apply plugin: 'checkstyle' assemble.dependsOn('lint') check.dependsOn('checkstyle') +configurations { + ktlint +} + android { compileSdkVersion rootProject.ext.compileSdkVersion buildToolsVersion rootProject.ext.buildToolsVersion @@ -59,15 +65,15 @@ dependencies { implementation 'com.jakewharton.rxbinding2:rxbinding:2.2.0' /* Used for server calls */ - implementation 'com.squareup.okio:okio:2.2.2' - implementation 'com.squareup.retrofit2:retrofit:2.6.0' + implementation 'com.squareup.okio:okio:2.3.0' + implementation 'com.squareup.retrofit2:retrofit:2.6.1' implementation 'com.facebook.stetho:stetho:1.5.1' implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1' /* Used to make Retrofit easier and GSON & Rx-compatible*/ implementation 'com.google.code.gson:gson:2.8.5' - implementation 'com.squareup.retrofit2:adapter-rxjava2:2.6.0' - implementation 'com.squareup.retrofit2:converter-gson:2.6.0' + implementation 'com.squareup.retrofit2:adapter-rxjava2:2.6.1' + implementation 'com.squareup.retrofit2:converter-gson:2.6.1' // Used to debug your Retrofit connections // Do not upgrade as it will require increasing minSdkVersion to 21 @@ -78,4 +84,29 @@ dependencies { releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3' // Optional, if you use support library fragments: debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.3' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + ktlint "com.pinterest:ktlint:0.33.0" +} + +repositories { + mavenCentral() +} + +task ktlint(type: JavaExec, group: "verification") { + description = "Check Kotlin code style." + main = "com.pinterest.ktlint.Main" + classpath = configurations.ktlint + args "src/**/*.kt" + // to generate report in checkstyle format prepend following args: + // "--reporter=plain", "--reporter=checkstyle,output=${buildDir}/ktlint.xml" + // see https://github.com/pinterest/ktlint#usage for more +} +check.dependsOn ktlint + +task ktlintFormat(type: JavaExec, group: "formatting") { + description = "Fix Kotlin code style deviations." + main = "com.pinterest.ktlint.Main" + classpath = configurations.ktlint + args "-F", "src/**/*.kt" } diff --git a/samplestore/src/main/java/com/stripe/samplestore/ItemDivider.java b/samplestore/src/main/java/com/stripe/samplestore/ItemDivider.java deleted file mode 100644 index 922eda8674a..00000000000 --- a/samplestore/src/main/java/com/stripe/samplestore/ItemDivider.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.stripe.samplestore; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.support.annotation.DrawableRes; -import android.support.annotation.NonNull; -import android.support.v4.content.ContextCompat; -import android.support.v7.widget.RecyclerView; -import android.view.View; - -class ItemDivider extends RecyclerView.ItemDecoration { - - @NonNull private final Drawable divider; - - /** - * Custom divider will be used in the list. - */ - ItemDivider(@NonNull Context context, @DrawableRes int resId) { - divider = ContextCompat.getDrawable(context, resId); - } - - @Override - public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, - @NonNull RecyclerView.State state) { - final int start = parent.getPaddingStart(); - final int end = parent.getWidth() - parent.getPaddingEnd(); - - int childCount = parent.getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = parent.getChildAt(i); - - RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); - - int top = child.getBottom() + params.bottomMargin; - int bottom = top + divider.getIntrinsicHeight(); - - divider.setBounds(start, top, end, bottom); - divider.draw(c); - } - } -} diff --git a/samplestore/src/main/java/com/stripe/samplestore/ItemDivider.kt b/samplestore/src/main/java/com/stripe/samplestore/ItemDivider.kt new file mode 100644 index 00000000000..7f7ce0c222f --- /dev/null +++ b/samplestore/src/main/java/com/stripe/samplestore/ItemDivider.kt @@ -0,0 +1,37 @@ +package com.stripe.samplestore + +import android.content.Context +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.support.annotation.DrawableRes +import android.support.v4.content.ContextCompat +import android.support.v7.widget.RecyclerView + +/** + * Custom divider will be used in the list. + */ +internal class ItemDivider( + context: Context, + @DrawableRes resId: Int +) : RecyclerView.ItemDecoration() { + + private val divider: Drawable = ContextCompat.getDrawable(context, resId)!! + + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + val start = parent.paddingStart + val end = parent.width - parent.paddingEnd + + val childCount = parent.childCount + for (i in 0 until childCount) { + val child = parent.getChildAt(i) + + val params = child.layoutParams as RecyclerView.LayoutParams + + val top = child.bottom + params.bottomMargin + val bottom = top + divider.intrinsicHeight + + divider.setBounds(start, top, end, bottom) + divider.draw(c) + } + } +} diff --git a/samplestore/src/main/java/com/stripe/samplestore/PaymentActivity.java b/samplestore/src/main/java/com/stripe/samplestore/PaymentActivity.java deleted file mode 100644 index 72fd24a7077..00000000000 --- a/samplestore/src/main/java/com/stripe/samplestore/PaymentActivity.java +++ /dev/null @@ -1,643 +0,0 @@ -package com.stripe.samplestore; - -import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.Size; -import android.support.v4.content.LocalBroadcastManager; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; -import android.view.View; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.ProgressBar; -import android.widget.TextView; - -import com.jakewharton.rxbinding2.view.RxView; -import com.stripe.android.ApiResultCallback; -import com.stripe.android.CustomerSession; -import com.stripe.android.PayWithGoogleUtils; -import com.stripe.android.PaymentAuthConfig; -import com.stripe.android.PaymentConfiguration; -import com.stripe.android.PaymentIntentResult; -import com.stripe.android.PaymentSession; -import com.stripe.android.PaymentSessionConfig; -import com.stripe.android.PaymentSessionData; -import com.stripe.android.SetupIntentResult; -import com.stripe.android.Stripe; -import com.stripe.android.StripeError; -import com.stripe.android.model.Address; -import com.stripe.android.model.Customer; -import com.stripe.android.model.PaymentIntent; -import com.stripe.android.model.PaymentMethod; -import com.stripe.android.model.SetupIntent; -import com.stripe.android.model.ShippingInformation; -import com.stripe.android.model.ShippingMethod; -import com.stripe.android.model.StripeIntent; -import com.stripe.samplestore.service.StripeService; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; - -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import okhttp3.ResponseBody; - -import static com.stripe.android.view.PaymentFlowExtras.EVENT_SHIPPING_INFO_PROCESSED; -import static com.stripe.android.view.PaymentFlowExtras.EVENT_SHIPPING_INFO_SUBMITTED; -import static com.stripe.android.view.PaymentFlowExtras.EXTRA_DEFAULT_SHIPPING_METHOD; -import static com.stripe.android.view.PaymentFlowExtras.EXTRA_IS_SHIPPING_INFO_VALID; -import static com.stripe.android.view.PaymentFlowExtras.EXTRA_SHIPPING_INFO_DATA; -import static com.stripe.android.view.PaymentFlowExtras.EXTRA_VALID_SHIPPING_METHODS; - -public class PaymentActivity extends AppCompatActivity { - - private static final String EXTRA_CART = "extra_cart"; - - @NonNull private final CompositeDisposable mCompositeDisposable = new CompositeDisposable(); - - private BroadcastReceiver mBroadcastReceiver; - private LinearLayout mCartItemLayout; - private TextView mEnterShippingInfo; - private TextView mEnterPaymentInfo; - private ProgressBar mProgressBar; - - private Button mConfirmPaymentButton; - private Button mSetupPaymentCredentialsButton; - - private Stripe mStripe; - private PaymentSession mPaymentSession; - private StripeService mService; - - private StoreCart mStoreCart; - private long mShippingCosts = 0L; - - @NonNull - public static Intent createIntent(@NonNull Activity activity, @NonNull StoreCart cart) { - return new Intent(activity, PaymentActivity.class) - .putExtra(EXTRA_CART, cart); - } - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_payment); - - mStripe = new Stripe(this, PaymentConfiguration.getInstance().getPublishableKey()); - if (Settings.STRIPE_ACCOUNT_ID != null) { - mStripe.setStripeAccount(Settings.STRIPE_ACCOUNT_ID); - } - - final PaymentAuthConfig.Stripe3ds2ButtonCustomization selectCustomization = - new PaymentAuthConfig.Stripe3ds2ButtonCustomization.Builder() - .setBackgroundColor("#EC4847") - .setTextColor("#000000") - .build(); - final PaymentAuthConfig.Stripe3ds2UiCustomization uiCustomization = - PaymentAuthConfig.Stripe3ds2UiCustomization.Builder.createWithAppTheme(this) - .setButtonCustomization(selectCustomization, - PaymentAuthConfig.Stripe3ds2UiCustomization.ButtonType.SELECT) - .build(); - PaymentAuthConfig.init(new PaymentAuthConfig.Builder() - .set3ds2Config(new PaymentAuthConfig.Stripe3ds2Config.Builder() - .setUiCustomization(uiCustomization) - .build()) - .build()); - - mService = RetrofitFactory.getInstance().create(StripeService.class); - - final Bundle extras = getIntent().getExtras(); - mStoreCart = extras != null ? extras.getParcelable(EXTRA_CART) : null; - - mProgressBar = findViewById(R.id.progress_bar); - mCartItemLayout = findViewById(R.id.cart_list_items); - mConfirmPaymentButton = findViewById(R.id.btn_confirm_payment); - mSetupPaymentCredentialsButton = findViewById(R.id.btn_setup_intent); - - setupPaymentSession(); - - addCartItems(); - - updateConfirmPaymentButton(); - mEnterShippingInfo = findViewById(R.id.shipping_info); - mEnterPaymentInfo = findViewById(R.id.payment_source); - mCompositeDisposable.add(RxView.clicks(mEnterShippingInfo) - .subscribe(aVoid -> mPaymentSession.presentShippingFlow())); - mCompositeDisposable.add(RxView.clicks(mEnterPaymentInfo) - .subscribe(aVoid -> mPaymentSession.presentPaymentMethodSelection(true))); - - final CustomerSession customerSession = CustomerSession.getInstance(); - mCompositeDisposable.add(RxView.clicks(mConfirmPaymentButton) - .subscribe(aVoid -> customerSession.retrieveCurrentCustomer( - new PaymentIntentCustomerRetrievalListener(PaymentActivity.this)))); - mCompositeDisposable.addAll(RxView.clicks(mSetupPaymentCredentialsButton) - .subscribe(aVoid -> customerSession.retrieveCurrentCustomer( - new SetupIntentCustomerRetrievalListener(PaymentActivity.this)))); - final LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this); - - mBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final ShippingInformation shippingInformation = intent - .getParcelableExtra(EXTRA_SHIPPING_INFO_DATA); - final Intent shippingInfoProcessedIntent = - new Intent(EVENT_SHIPPING_INFO_PROCESSED); - if (!isShippingInfoValid(shippingInformation)) { - shippingInfoProcessedIntent.putExtra(EXTRA_IS_SHIPPING_INFO_VALID, false); - } else { - final ArrayList shippingMethods = - getValidShippingMethods(shippingInformation); - shippingInfoProcessedIntent.putExtra(EXTRA_IS_SHIPPING_INFO_VALID, true); - shippingInfoProcessedIntent.putParcelableArrayListExtra( - EXTRA_VALID_SHIPPING_METHODS, shippingMethods); - shippingInfoProcessedIntent - .putExtra(EXTRA_DEFAULT_SHIPPING_METHOD, shippingMethods.get(0)); - } - localBroadcastManager.sendBroadcast(shippingInfoProcessedIntent); - } - }; - localBroadcastManager.registerReceiver(mBroadcastReceiver, - new IntentFilter(EVENT_SHIPPING_INFO_SUBMITTED)); - } - - private boolean isShippingInfoValid(@NonNull ShippingInformation shippingInfo) { - return shippingInfo.getAddress() != null && - Locale.US.getCountry().equals(shippingInfo.getAddress().getCountry()); - } - - @NonNull - private ShippingInformation getExampleShippingInfo() { - final Address address = new Address.Builder() - .setCity("San Francisco") - .setCountry("US") - .setLine1("123 Market St") - .setLine2("#345") - .setPostalCode("94107") - .setState("CA") - .build(); - return new ShippingInformation(address, "Fake Name", "(555) 555-5555"); - } - - /* - * Cleaning up all Rx subscriptions in onDestroy. - */ - @Override - protected void onDestroy() { - mCompositeDisposable.dispose(); - LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver); - mPaymentSession.onDestroy(); - super.onDestroy(); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - final boolean isPaymentIntentResult = mStripe.onPaymentResult( - requestCode, data, - new ApiResultCallback() { - @Override - public void onSuccess(@NonNull PaymentIntentResult result) { - stopLoading(); - processStripeIntent(result.getIntent()); - } - - @Override - public void onError(@NonNull Exception e) { - stopLoading(); - displayError(e.getMessage()); - } - }); - - if (isPaymentIntentResult) { - startLoading(); - } else { - final boolean isSetupIntentResult = mStripe.onSetupResult(requestCode, data, - new ApiResultCallback() { - @Override - public void onSuccess(@NonNull SetupIntentResult result) { - stopLoading(); - processStripeIntent(result.getIntent()); - } - - @Override - public void onError(@NonNull Exception e) { - stopLoading(); - displayError(e.getMessage()); - } - }); - if (!isSetupIntentResult) { - mPaymentSession.handlePaymentData(requestCode, resultCode, data); - } - } - } - - private void updateConfirmPaymentButton() { - final long price = mPaymentSession.getPaymentSessionData().getCartTotal(); - - mConfirmPaymentButton.setText( - getString(R.string.pay_label, StoreUtils.getPriceString(price, null))); - } - - private void addCartItems() { - mCartItemLayout.removeAllViewsInLayout(); - final String currencySymbol = mStoreCart.getCurrency().getSymbol(Locale.US); - - addLineItems(currencySymbol, mStoreCart.getLineItems() - .toArray(new StoreLineItem[mStoreCart.getSize()])); - - addLineItems(currencySymbol, - new StoreLineItem(getString(R.string.checkout_shipping_cost_label), 1, - mShippingCosts)); - - final View totalView = getLayoutInflater() - .inflate(R.layout.cart_item, mCartItemLayout, false); - setupTotalPriceView(totalView, currencySymbol); - mCartItemLayout.addView(totalView); - } - - private void addLineItems(@NonNull String currencySymbol, @NonNull StoreLineItem... items) { - for (StoreLineItem item : items) { - final View view = getLayoutInflater().inflate( - R.layout.cart_item, mCartItemLayout, false); - fillOutCartItemView(item, view, currencySymbol); - mCartItemLayout.addView(view); - } - } - - private void setupTotalPriceView(@NonNull View view, @NonNull String currencySymbol) { - final TextView[] itemViews = getItemViews(view); - final long totalPrice = mPaymentSession.getPaymentSessionData().getCartTotal(); - itemViews[0].setText(getString(R.string.checkout_total_cost_label)); - final String price = PayWithGoogleUtils.getPriceString(totalPrice, - mStoreCart.getCurrency()); - final String displayPrice = currencySymbol + price; - itemViews[3].setText(displayPrice); - } - - private void fillOutCartItemView(@NonNull StoreLineItem item, @NonNull View view, - @NonNull String currencySymbol) { - final TextView[] itemViews = getItemViews(view); - - itemViews[0].setText(item.getDescription()); - - if (!getString(R.string.checkout_shipping_cost_label).equals(item.getDescription())) { - String quantityPriceString = "X " + item.getQuantity() + " @"; - itemViews[1].setText(quantityPriceString); - - String unitPriceString = currencySymbol + - PayWithGoogleUtils.getPriceString(item.getUnitPrice(), - mStoreCart.getCurrency()); - itemViews[2].setText(unitPriceString); - } - - String totalPriceString = currencySymbol + - PayWithGoogleUtils.getPriceString(item.getTotalPrice(), mStoreCart.getCurrency()); - itemViews[3].setText(totalPriceString); - } - - @Size(value = 4) - @NonNull - private TextView[] getItemViews(@NonNull View view) { - final TextView labelView = view.findViewById(R.id.tv_cart_emoji); - final TextView quantityView = view.findViewById(R.id.tv_cart_quantity); - final TextView unitPriceView = view.findViewById(R.id.tv_cart_unit_price); - final TextView totalPriceView = view.findViewById(R.id.tv_cart_total_price); - return new TextView[]{labelView, quantityView, unitPriceView, totalPriceView}; - } - - @NonNull - private Map createCapturePaymentParams(@NonNull PaymentSessionData data, - @NonNull String customerId, - @Nullable String stripeAccountId) { - final AbstractMap params = new HashMap<>(); - params.put("amount", Long.toString(data.getCartTotal())); - params.put("payment_method", Objects.requireNonNull(data.getPaymentMethod()).id); - params.put("customer_id", customerId); - params.put("shipping", data.getShippingInformation() != null ? - data.getShippingInformation().toMap() : null); - params.put("return_url", "stripe://payment-auth-return"); - if (stripeAccountId != null) { - params.put("stripe_account", stripeAccountId); - } - return params; - } - - @NonNull - private Map createSetupIntentParams(@NonNull PaymentSessionData data, - @NonNull String customerId, - @Nullable String stripeAccountId) { - final AbstractMap params = new HashMap<>(); - params.put("payment_method", Objects.requireNonNull(data.getPaymentMethod()).id); - params.put("customer_id", customerId); - params.put("return_url", "stripe://payment-auth-return"); - if (stripeAccountId != null) { - params.put("stripe_account", stripeAccountId); - } - return params; - } - - private void capturePayment(@NonNull String customerId) { - if (mPaymentSession.getPaymentSessionData().getPaymentMethod() == null) { - displayError("No payment method selected"); - return; - } - - final Observable stripeResponse = mService.capturePayment( - createCapturePaymentParams(mPaymentSession.getPaymentSessionData(), customerId, - Settings.STRIPE_ACCOUNT_ID)); - mCompositeDisposable.add(stripeResponse - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSubscribe(disposable -> startLoading()) - .doFinally(this::stopLoading) - .subscribe(this::onStripeIntentClientSecretResponse, - throwable -> displayError(throwable.getLocalizedMessage()))); - } - - private void createSetupIntent(@NonNull String customerId) { - if (mPaymentSession.getPaymentSessionData().getPaymentMethod() == null) { - displayError("No payment method selected"); - return; - } - - final Observable stripeResponse = mService.createSetupIntent( - createSetupIntentParams(mPaymentSession.getPaymentSessionData(), customerId, - Settings.STRIPE_ACCOUNT_ID)); - mCompositeDisposable.add(stripeResponse - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSubscribe(disposable -> startLoading()) - .doFinally(this::stopLoading) - .subscribe(this::onStripeIntentClientSecretResponse, - throwable -> displayError(throwable.getLocalizedMessage()))); - } - - private void displayError(@NonNull String errorMessage) { - final AlertDialog alertDialog = new AlertDialog.Builder(this).create(); - alertDialog.setTitle("Error"); - alertDialog.setMessage(errorMessage); - alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", - (dialog, which) -> dialog.dismiss()); - alertDialog.show(); - } - - private void processStripeIntent(@NonNull StripeIntent stripeIntent) { - if (stripeIntent.requiresAction()) { - mStripe.authenticatePayment(this, - Objects.requireNonNull(stripeIntent.getClientSecret())); - } else if (stripeIntent.requiresConfirmation()) { - confirmStripeIntent(Objects.requireNonNull(stripeIntent.getId()), - Settings.STRIPE_ACCOUNT_ID); - } else if (stripeIntent.getStatus() == StripeIntent.Status.Succeeded) { - if (stripeIntent instanceof PaymentIntent) { - finishPayment(); - } else if (stripeIntent instanceof SetupIntent) { - finishSetup(); - } - } else if (stripeIntent.getStatus() == StripeIntent.Status.RequiresPaymentMethod) { - // reset payment method and shipping if authentication fails - setupPaymentSession(); - mEnterPaymentInfo.setText(getString(R.string.add_credit_card)); - mEnterShippingInfo.setText(getString(R.string.add_shipping_details)); - } else { - displayError("Unhandled Payment Intent Status: " + - Objects.requireNonNull(stripeIntent.getStatus()).toString()); - } - } - - private void confirmStripeIntent(@NonNull String stripeIntentId, - @Nullable String stripeAccountId) { - final Map params = new HashMap<>(); - params.put("payment_intent_id", stripeIntentId); - if (stripeAccountId != null) { - params.put("stripe_account", stripeAccountId); - } - - mCompositeDisposable.add(mService.confirmPayment(params) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSubscribe(disposable -> startLoading()) - .doFinally(this::stopLoading) - .subscribe( - this::onStripeIntentClientSecretResponse, - throwable -> displayError(throwable.getLocalizedMessage()))); - } - - private void onStripeIntentClientSecretResponse(@NonNull ResponseBody responseBody) - throws IOException, JSONException { - final String clientSecret = new JSONObject(responseBody.string()).getString("secret"); - mCompositeDisposable.add( - Observable - .fromCallable(() -> { - if (clientSecret.startsWith("pi_")) { - return mStripe.retrievePaymentIntentSynchronous(clientSecret); - } else if (clientSecret.startsWith("seti_")) { - return mStripe.retrieveSetupIntentSynchronous(clientSecret); - } else { - throw new IllegalArgumentException( - "Invalid client_secret: " + clientSecret); - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSubscribe(disposable -> startLoading()) - .doFinally(this::stopLoading) - .subscribe(this::processStripeIntent) - ); - } - - private void finishPayment() { - mPaymentSession.onCompleted(); - final Intent data = StoreActivity.createPurchaseCompleteIntent( - mStoreCart.getTotalPrice() + mShippingCosts); - setResult(RESULT_OK, data); - finish(); - } - - private void finishSetup() { - mPaymentSession.onCompleted(); - setResult(RESULT_OK, new Intent().putExtras(new Bundle())); - finish(); - } - - private void setupPaymentSession() { - mPaymentSession = new PaymentSession(this); - mPaymentSession.init(new PaymentSessionListenerImpl(this), - new PaymentSessionConfig.Builder() - .setPrepopulatedShippingInfo(getExampleShippingInfo()).build()); - mPaymentSession.setCartTotal(mStoreCart.getTotalPrice()); - - final boolean isPaymentReadyToCharge = mPaymentSession - .getPaymentSessionData().isPaymentReadyToCharge(); - mConfirmPaymentButton.setEnabled(isPaymentReadyToCharge); - mSetupPaymentCredentialsButton.setEnabled(isPaymentReadyToCharge); - } - - private void startLoading() { - mProgressBar.setVisibility(View.VISIBLE); - mEnterPaymentInfo.setEnabled(false); - mEnterShippingInfo.setEnabled(false); - - mConfirmPaymentButton.setTag(mConfirmPaymentButton.isEnabled()); - mConfirmPaymentButton.setEnabled(false); - - mSetupPaymentCredentialsButton.setTag(mSetupPaymentCredentialsButton.isEnabled()); - mSetupPaymentCredentialsButton.setEnabled(false); - } - - private void stopLoading() { - mProgressBar.setVisibility(View.INVISIBLE); - mEnterPaymentInfo.setEnabled(true); - mEnterShippingInfo.setEnabled(true); - - mConfirmPaymentButton.setEnabled(Boolean.TRUE.equals(mConfirmPaymentButton.getTag())); - mSetupPaymentCredentialsButton - .setEnabled(Boolean.TRUE.equals(mSetupPaymentCredentialsButton.getTag())); - } - - @Nullable - private String formatSourceDescription(@NonNull PaymentMethod paymentMethod) { - if (paymentMethod.card != null) { - return paymentMethod.card.brand + "-" + paymentMethod.card.last4; - } - return null; - } - - @NonNull - private ArrayList getValidShippingMethods( - @NonNull ShippingInformation shippingInformation) { - final ArrayList shippingMethods = new ArrayList<>(); - shippingMethods.add(new ShippingMethod("UPS Ground", "ups-ground", - "Arrives in 3-5 days", 0, "USD")); - shippingMethods.add(new ShippingMethod("FedEx", "fedex", - "Arrives tomorrow", 599, "USD")); - if (shippingInformation.getAddress() != null && - "94110".equals(shippingInformation.getAddress().getPostalCode())) { - shippingMethods.add(new ShippingMethod("1 Hour Courier", "courier", - "Arrives in the next hour", 1099, "USD")); - } - return shippingMethods; - } - - private void onPaymentSessionDataChanged(@NonNull PaymentSessionData data) { - if (data.getShippingMethod() != null) { - mEnterShippingInfo.setText(data.getShippingMethod().getLabel()); - mShippingCosts = data.getShippingMethod().getAmount(); - mPaymentSession.setCartTotal(mStoreCart.getTotalPrice() + mShippingCosts); - addCartItems(); - updateConfirmPaymentButton(); - } - - if (data.getPaymentMethod() != null) { - mEnterPaymentInfo.setText(formatSourceDescription(data.getPaymentMethod())); - } - - if (data.isPaymentReadyToCharge()) { - mConfirmPaymentButton.setEnabled(true); - mSetupPaymentCredentialsButton.setEnabled(true); - } - } - - private static final class PaymentSessionListenerImpl - extends PaymentSession.ActivityPaymentSessionListener { - private PaymentSessionListenerImpl(@NonNull PaymentActivity activity) { - super(activity); - } - - @Override - public void onCommunicatingStateChanged(boolean isCommunicating) { - } - - @Override - public void onError(int errorCode, @NonNull String errorMessage) { - final PaymentActivity activity = getListenerActivity(); - if (activity == null) { - return; - } - - activity.displayError(errorMessage); - } - - @Override - public void onPaymentSessionDataChanged(@NonNull PaymentSessionData data) { - final PaymentActivity activity = getListenerActivity(); - if (activity == null) { - return; - } - - activity.onPaymentSessionDataChanged(data); - } - } - - private static final class PaymentIntentCustomerRetrievalListener - extends CustomerSession.ActivityCustomerRetrievalListener { - private PaymentIntentCustomerRetrievalListener(@NonNull PaymentActivity activity) { - super(activity); - } - - @Override - public void onCustomerRetrieved(@NonNull Customer customer) { - final PaymentActivity activity = getActivity(); - if (activity == null) { - return; - } - - activity.capturePayment(Objects.requireNonNull(customer.getId())); - } - - @Override - public void onError(int httpCode, @NonNull String errorMessage, - @Nullable StripeError stripeError) { - final PaymentActivity activity = getActivity(); - if (activity == null) { - return; - } - - activity.displayError("Error getting payment method:. " + errorMessage); - } - } - - private static final class SetupIntentCustomerRetrievalListener - extends CustomerSession.ActivityCustomerRetrievalListener { - private SetupIntentCustomerRetrievalListener(@NonNull PaymentActivity activity) { - super(activity); - } - - @Override - public void onCustomerRetrieved(@NonNull Customer customer) { - final PaymentActivity activity = getActivity(); - if (activity == null) { - return; - } - - activity.createSetupIntent(Objects.requireNonNull(customer.getId())); - } - - @Override - public void onError(int httpCode, @NonNull String errorMessage, - @Nullable StripeError stripeError) { - final PaymentActivity activity = getActivity(); - if (activity == null) { - return; - } - - activity.displayError("Error getting payment method:. " + errorMessage); - } - } -} diff --git a/samplestore/src/main/java/com/stripe/samplestore/PaymentActivity.kt b/samplestore/src/main/java/com/stripe/samplestore/PaymentActivity.kt new file mode 100644 index 00000000000..f34c5bcb3bb --- /dev/null +++ b/samplestore/src/main/java/com/stripe/samplestore/PaymentActivity.kt @@ -0,0 +1,564 @@ +package com.stripe.samplestore + +import android.app.Activity +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import android.support.annotation.Size +import android.support.v4.content.LocalBroadcastManager +import android.support.v7.app.AlertDialog +import android.support.v7.app.AppCompatActivity +import android.view.View +import android.widget.Button +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.TextView +import com.jakewharton.rxbinding2.view.RxView +import com.stripe.android.* +import com.stripe.android.model.* +import com.stripe.android.view.PaymentFlowExtras.EVENT_SHIPPING_INFO_PROCESSED +import com.stripe.android.view.PaymentFlowExtras.EVENT_SHIPPING_INFO_SUBMITTED +import com.stripe.android.view.PaymentFlowExtras.EXTRA_DEFAULT_SHIPPING_METHOD +import com.stripe.android.view.PaymentFlowExtras.EXTRA_IS_SHIPPING_INFO_VALID +import com.stripe.android.view.PaymentFlowExtras.EXTRA_SHIPPING_INFO_DATA +import com.stripe.android.view.PaymentFlowExtras.EXTRA_VALID_SHIPPING_METHODS +import com.stripe.samplestore.service.StripeService +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import okhttp3.ResponseBody +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException +import java.util.ArrayList +import java.util.HashMap +import java.util.Locale + +class PaymentActivity : AppCompatActivity() { + + private val compositeDisposable = CompositeDisposable() + + private lateinit var broadcastReceiver: BroadcastReceiver + private lateinit var cartItemLayout: LinearLayout + private lateinit var enterShippingInfo: TextView + private lateinit var enterPaymentInfo: TextView + private lateinit var progressBar: ProgressBar + + private lateinit var confirmPaymentButton: Button + private lateinit var setupPaymentCredentialsButton: Button + + private lateinit var stripe: Stripe + private lateinit var paymentSession: PaymentSession + private lateinit var service: StripeService + + private lateinit var storeCart: StoreCart + private var shippingCosts = 0L + + private val exampleShippingInfo: ShippingInformation + get() { + val address = Address.Builder() + .setCity("San Francisco") + .setCountry("US") + .setLine1("123 Market St") + .setLine2("#345") + .setPostalCode("94107") + .setState("CA") + .build() + return ShippingInformation(address, "Fake Name", "(555) 555-5555") + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_payment) + + stripe = Stripe(this, PaymentConfiguration.getInstance().publishableKey) + if (Settings.STRIPE_ACCOUNT_ID != null) { + stripe.setStripeAccount(Settings.STRIPE_ACCOUNT_ID) + } + + val selectCustomization = PaymentAuthConfig.Stripe3ds2ButtonCustomization.Builder() + .setBackgroundColor("#EC4847") + .setTextColor("#000000") + .build() + val uiCustomization = + PaymentAuthConfig.Stripe3ds2UiCustomization.Builder.createWithAppTheme(this) + .setButtonCustomization(selectCustomization, + PaymentAuthConfig.Stripe3ds2UiCustomization.ButtonType.SELECT) + .build() + PaymentAuthConfig.init(PaymentAuthConfig.Builder() + .set3ds2Config(PaymentAuthConfig.Stripe3ds2Config.Builder() + .setUiCustomization(uiCustomization) + .build()) + .build()) + + service = RetrofitFactory.instance.create(StripeService::class.java) + + val extras = intent.extras + storeCart = extras?.getParcelable(EXTRA_CART)!! + + progressBar = findViewById(R.id.progress_bar) + cartItemLayout = findViewById(R.id.cart_list_items) + confirmPaymentButton = findViewById(R.id.btn_confirm_payment) + setupPaymentCredentialsButton = findViewById(R.id.btn_setup_intent) + + paymentSession = createPaymentSession() + + addCartItems() + + updateConfirmPaymentButton() + enterShippingInfo = findViewById(R.id.shipping_info) + enterPaymentInfo = findViewById(R.id.payment_source) + compositeDisposable.add(RxView.clicks(enterShippingInfo) + .subscribe { paymentSession.presentShippingFlow() }) + compositeDisposable.add(RxView.clicks(enterPaymentInfo) + .subscribe { paymentSession.presentPaymentMethodSelection(true) }) + + val customerSession = CustomerSession.getInstance() + compositeDisposable.add(RxView.clicks(confirmPaymentButton) + .subscribe { + customerSession.retrieveCurrentCustomer( + PaymentIntentCustomerRetrievalListener(this@PaymentActivity)) + }) + compositeDisposable.addAll(RxView.clicks(setupPaymentCredentialsButton) + .subscribe { + customerSession.retrieveCurrentCustomer( + SetupIntentCustomerRetrievalListener(this@PaymentActivity)) + }) + val localBroadcastManager = LocalBroadcastManager.getInstance(this) + + broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val shippingInformation = intent + .getParcelableExtra(EXTRA_SHIPPING_INFO_DATA) + val shippingInfoProcessedIntent = Intent(EVENT_SHIPPING_INFO_PROCESSED) + if (!isShippingInfoValid(shippingInformation)) { + shippingInfoProcessedIntent.putExtra(EXTRA_IS_SHIPPING_INFO_VALID, false) + } else { + val shippingMethods = getValidShippingMethods(shippingInformation) + shippingInfoProcessedIntent.putExtra(EXTRA_IS_SHIPPING_INFO_VALID, true) + shippingInfoProcessedIntent.putParcelableArrayListExtra( + EXTRA_VALID_SHIPPING_METHODS, shippingMethods) + shippingInfoProcessedIntent + .putExtra(EXTRA_DEFAULT_SHIPPING_METHOD, shippingMethods[0]) + } + localBroadcastManager.sendBroadcast(shippingInfoProcessedIntent) + } + } + localBroadcastManager.registerReceiver(broadcastReceiver, + IntentFilter(EVENT_SHIPPING_INFO_SUBMITTED)) + } + + private fun isShippingInfoValid(shippingInfo: ShippingInformation): Boolean { + return shippingInfo.address != null && Locale.US.country == shippingInfo.address!!.country + } + + /* + * Cleaning up all Rx subscriptions in onDestroy. + */ + override fun onDestroy() { + compositeDisposable.dispose() + LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver) + paymentSession.onDestroy() + super.onDestroy() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + val isPaymentIntentResult = stripe.onPaymentResult( + requestCode, data, + object : ApiResultCallback { + override fun onSuccess(result: PaymentIntentResult) { + stopLoading() + processStripeIntent(result.intent) + } + + override fun onError(e: Exception) { + stopLoading() + displayError(e.message) + } + }) + + if (isPaymentIntentResult) { + startLoading() + } else { + val isSetupIntentResult = stripe.onSetupResult(requestCode, data, + object : ApiResultCallback { + override fun onSuccess(result: SetupIntentResult) { + stopLoading() + processStripeIntent(result.intent) + } + + override fun onError(e: Exception) { + stopLoading() + displayError(e.message) + } + }) + if (!isSetupIntentResult) { + paymentSession.handlePaymentData(requestCode, resultCode, data!!) + } + } + } + + private fun updateConfirmPaymentButton() { + val price = paymentSession.paymentSessionData.cartTotal + + confirmPaymentButton.text = getString(R.string.pay_label, StoreUtils.getPriceString(price, null)) + } + + private fun addCartItems() { + cartItemLayout.removeAllViewsInLayout() + val currencySymbol = storeCart.currency.getSymbol(Locale.US) + + addLineItems(currencySymbol, storeCart.lineItems) + + addLineItems(currencySymbol, + listOf(StoreLineItem(getString(R.string.checkout_shipping_cost_label), 1, shippingCosts))) + + val totalView = layoutInflater + .inflate(R.layout.cart_item, cartItemLayout, false) + setupTotalPriceView(totalView, currencySymbol) + cartItemLayout.addView(totalView) + } + + private fun addLineItems(currencySymbol: String, items: List) { + for (item in items) { + val view = layoutInflater.inflate( + R.layout.cart_item, cartItemLayout, false) + fillOutCartItemView(item, view, currencySymbol) + cartItemLayout.addView(view) + } + } + + private fun setupTotalPriceView(view: View, currencySymbol: String) { + val itemViews = getItemViews(view) + val totalPrice = paymentSession.paymentSessionData.cartTotal + itemViews[0].text = getString(R.string.checkout_total_cost_label) + val price = PayWithGoogleUtils.getPriceString(totalPrice, + storeCart.currency) + val displayPrice = currencySymbol + price + itemViews[3].text = displayPrice + } + + private fun fillOutCartItemView(item: StoreLineItem, view: View, currencySymbol: String) { + val itemViews = getItemViews(view) + + itemViews[0].text = item.description + + if (getString(R.string.checkout_shipping_cost_label) != item.description) { + val quantityPriceString = "X " + item.quantity + " @" + itemViews[1].text = quantityPriceString + + val unitPriceString = currencySymbol + PayWithGoogleUtils.getPriceString(item.unitPrice, + storeCart.currency) + itemViews[2].text = unitPriceString + } + + val totalPriceString = currencySymbol + + PayWithGoogleUtils.getPriceString(item.totalPrice, storeCart.currency) + itemViews[3].text = totalPriceString + } + + @Size(value = 4) + private fun getItemViews(view: View): Array { + val labelView = view.findViewById(R.id.tv_cart_emoji) + val quantityView = view.findViewById(R.id.tv_cart_quantity) + val unitPriceView = view.findViewById(R.id.tv_cart_unit_price) + val totalPriceView = view.findViewById(R.id.tv_cart_total_price) + return arrayOf(labelView, quantityView, unitPriceView, totalPriceView) + } + + private fun createCapturePaymentParams( + data: PaymentSessionData, + customerId: String, + stripeAccountId: String? + ): HashMap { + val params = HashMap() + params["amount"] = data.cartTotal.toString() + params["payment_method"] = data.paymentMethod!!.id!! + params["customer_id"] = customerId + if (data.shippingInformation != null) { + params["shipping"] = data.shippingInformation!!.toMap() + } + params["return_url"] = "stripe://payment-auth-return" + if (stripeAccountId != null) { + params["stripe_account"] = stripeAccountId + } + return params + } + + private fun createSetupIntentParams( + data: PaymentSessionData, + customerId: String, + stripeAccountId: String? + ): HashMap { + val params = HashMap() + params["payment_method"] = data.paymentMethod!!.id!! + params["customer_id"] = customerId + params["return_url"] = "stripe://payment-auth-return" + if (stripeAccountId != null) { + params["stripe_account"] = stripeAccountId + } + return params + } + + private fun capturePayment(customerId: String) { + if (paymentSession.paymentSessionData.paymentMethod == null) { + displayError("No payment method selected") + return + } + + val stripeResponse = service.capturePayment( + createCapturePaymentParams(paymentSession.paymentSessionData, customerId, + Settings.STRIPE_ACCOUNT_ID)) + compositeDisposable.add(stripeResponse + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSubscribe { startLoading() } + .doFinally { this.stopLoading() } + .subscribe({ this.onStripeIntentClientSecretResponse(it) }, + { throwable -> displayError(throwable.localizedMessage) })) + } + + private fun createSetupIntent(customerId: String) { + if (paymentSession.paymentSessionData.paymentMethod == null) { + displayError("No payment method selected") + return + } + + val stripeResponse = service.createSetupIntent( + createSetupIntentParams(paymentSession.paymentSessionData, customerId, + Settings.STRIPE_ACCOUNT_ID)) + compositeDisposable.add(stripeResponse + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSubscribe { startLoading() } + .doFinally { this.stopLoading() } + .subscribe({ this.onStripeIntentClientSecretResponse(it) }, + { throwable -> displayError(throwable.localizedMessage) })) + } + + private fun displayError(errorMessage: String?) { + val alertDialog = AlertDialog.Builder(this).create() + alertDialog.setTitle("Error") + alertDialog.setMessage(errorMessage) + alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK") { + dialog, _ -> dialog.dismiss() + } + alertDialog.show() + } + + private fun processStripeIntent(stripeIntent: StripeIntent) { + if (stripeIntent.requiresAction()) { + stripe.authenticatePayment(this, stripeIntent.clientSecret!!) + } else if (stripeIntent.requiresConfirmation()) { + confirmStripeIntent(stripeIntent.id!!, Settings.STRIPE_ACCOUNT_ID) + } else if (stripeIntent.status == StripeIntent.Status.Succeeded) { + if (stripeIntent is PaymentIntent) { + finishPayment() + } else if (stripeIntent is SetupIntent) { + finishSetup() + } + } else if (stripeIntent.status == StripeIntent.Status.RequiresPaymentMethod) { + // reset payment method and shipping if authentication fails + paymentSession = createPaymentSession() + enterPaymentInfo.text = getString(R.string.add_credit_card) + enterShippingInfo.text = getString(R.string.add_shipping_details) + } else { + displayError( + "Unhandled Payment Intent Status: " + stripeIntent.status.toString()) + } + } + + private fun confirmStripeIntent(stripeIntentId: String, stripeAccountId: String?) { + val params = HashMap() + params["payment_intent_id"] = stripeIntentId + if (stripeAccountId != null) { + params["stripe_account"] = stripeAccountId + } + + compositeDisposable.add(service.confirmPayment(params) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSubscribe { startLoading() } + .doFinally { this.stopLoading() } + .subscribe( + { this.onStripeIntentClientSecretResponse(it) }, + { throwable -> displayError(throwable.localizedMessage) })) + } + + @Throws(IOException::class, JSONException::class) + private fun onStripeIntentClientSecretResponse(responseBody: ResponseBody) { + val clientSecret = JSONObject(responseBody.string()).getString("secret") + compositeDisposable.add( + Observable + .fromCallable { retrieveStripeIntent(clientSecret) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSubscribe { startLoading() } + .doFinally { this.stopLoading() } + .subscribe { this.processStripeIntent(it) } + ) + } + + private fun retrieveStripeIntent(clientSecret: String): StripeIntent { + return when { + clientSecret.startsWith("pi_") -> + stripe.retrievePaymentIntentSynchronous(clientSecret)!! + clientSecret.startsWith("seti_") -> + stripe.retrieveSetupIntentSynchronous(clientSecret)!! + else -> throw IllegalArgumentException("Invalid client_secret: $clientSecret") + } + } + + private fun finishPayment() { + paymentSession.onCompleted() + val data = StoreActivity.createPurchaseCompleteIntent( + storeCart.totalPrice + shippingCosts) + setResult(Activity.RESULT_OK, data) + finish() + } + + private fun finishSetup() { + paymentSession.onCompleted() + setResult(Activity.RESULT_OK, Intent().putExtras(Bundle())) + finish() + } + + private fun createPaymentSession(): PaymentSession { + val paymentSession = PaymentSession(this) + paymentSession.init(PaymentSessionListenerImpl(this), + PaymentSessionConfig.Builder() + .setPrepopulatedShippingInfo(exampleShippingInfo).build()) + paymentSession.setCartTotal(storeCart.totalPrice) + + val isPaymentReadyToCharge = paymentSession.paymentSessionData.isPaymentReadyToCharge + confirmPaymentButton.isEnabled = isPaymentReadyToCharge + setupPaymentCredentialsButton.isEnabled = isPaymentReadyToCharge + + return paymentSession + } + + private fun startLoading() { + progressBar.visibility = View.VISIBLE + enterPaymentInfo.isEnabled = false + enterShippingInfo.isEnabled = false + + confirmPaymentButton.tag = confirmPaymentButton.isEnabled + confirmPaymentButton.isEnabled = false + + setupPaymentCredentialsButton.tag = setupPaymentCredentialsButton.isEnabled + setupPaymentCredentialsButton.isEnabled = false + } + + private fun stopLoading() { + progressBar.visibility = View.INVISIBLE + enterPaymentInfo.isEnabled = true + enterShippingInfo.isEnabled = true + + confirmPaymentButton.isEnabled = java.lang.Boolean.TRUE == confirmPaymentButton.tag + setupPaymentCredentialsButton.isEnabled = java.lang.Boolean.TRUE == setupPaymentCredentialsButton.tag + } + + private fun formatSourceDescription(paymentMethod: PaymentMethod): String? { + return if (paymentMethod.card != null) { + paymentMethod.card!!.brand + "-" + paymentMethod.card!!.last4 + } else null + } + + private fun getValidShippingMethods( + shippingInformation: ShippingInformation + ): ArrayList { + val shippingMethods = ArrayList() + shippingMethods.add(ShippingMethod("UPS Ground", "ups-ground", + "Arrives in 3-5 days", 0, "USD")) + shippingMethods.add(ShippingMethod("FedEx", "fedex", + "Arrives tomorrow", 599, "USD")) + if (shippingInformation.address != null && "94110" == shippingInformation.address!!.postalCode) { + shippingMethods.add(ShippingMethod("1 Hour Courier", "courier", + "Arrives in the next hour", 1099, "USD")) + } + return shippingMethods + } + + private fun onPaymentSessionDataChanged(data: PaymentSessionData) { + if (data.shippingMethod != null) { + enterShippingInfo.text = data.shippingMethod!!.label + shippingCosts = data.shippingMethod!!.amount + paymentSession.setCartTotal(storeCart.totalPrice + shippingCosts) + addCartItems() + updateConfirmPaymentButton() + } + + if (data.paymentMethod != null) { + enterPaymentInfo.text = formatSourceDescription(data.paymentMethod!!) + } + + if (data.isPaymentReadyToCharge) { + confirmPaymentButton.isEnabled = true + setupPaymentCredentialsButton.isEnabled = true + } + } + + private class PaymentSessionListenerImpl constructor( + activity: PaymentActivity + ) : PaymentSession.ActivityPaymentSessionListener(activity) { + + override fun onCommunicatingStateChanged(isCommunicating: Boolean) {} + + override fun onError(errorCode: Int, errorMessage: String) { + val activity = listenerActivity ?: return + + activity.displayError(errorMessage) + } + + override fun onPaymentSessionDataChanged(data: PaymentSessionData) { + val activity = listenerActivity ?: return + activity.onPaymentSessionDataChanged(data) + } + } + + private class PaymentIntentCustomerRetrievalListener constructor( + activity: PaymentActivity + ) : CustomerSession.ActivityCustomerRetrievalListener(activity) { + + override fun onCustomerRetrieved(customer: Customer) { + val activity = activity ?: return + + activity.capturePayment(customer.id) + } + + override fun onError(httpCode: Int, errorMessage: String, stripeError: StripeError?) { + val activity = activity ?: return + + activity.displayError("Error getting payment method:. $errorMessage") + } + } + + private class SetupIntentCustomerRetrievalListener constructor( + activity: PaymentActivity + ) : CustomerSession.ActivityCustomerRetrievalListener(activity) { + + override fun onCustomerRetrieved(customer: Customer) { + val activity = activity ?: return + activity.createSetupIntent(customer.id) + } + + override fun onError(httpCode: Int, errorMessage: String, stripeError: StripeError?) { + val activity = activity ?: return + activity.displayError("Error getting payment method:. $errorMessage") + } + } + + companion object { + private const val EXTRA_CART = "extra_cart" + + fun createIntent(activity: Activity, cart: StoreCart): Intent { + return Intent(activity, PaymentActivity::class.java) + .putExtra(EXTRA_CART, cart) + } + } +} diff --git a/samplestore/src/main/java/com/stripe/samplestore/RetrofitFactory.java b/samplestore/src/main/java/com/stripe/samplestore/RetrofitFactory.java deleted file mode 100644 index 44bda9cbf0b..00000000000 --- a/samplestore/src/main/java/com/stripe/samplestore/RetrofitFactory.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.stripe.samplestore; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import com.facebook.stetho.okhttp3.StethoInterceptor; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import okhttp3.OkHttpClient; -import okhttp3.logging.HttpLoggingInterceptor; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.converter.gson.GsonConverterFactory; - -/** - * Factory instance to keep our Retrofit instance. - */ -public class RetrofitFactory { - @Nullable private static Retrofit mInstance = null; - - @NonNull - public static Retrofit getInstance() { - if (mInstance == null) { - // Set your desired log level. Use Level.BODY for debugging errors. - final HttpLoggingInterceptor logging = new HttpLoggingInterceptor() - .setLevel(HttpLoggingInterceptor.Level.BASIC); - - final OkHttpClient httpClient = new OkHttpClient.Builder() - .addInterceptor(logging) - .addNetworkInterceptor((new StethoInterceptor())) - .build(); - - final Gson gson = new GsonBuilder() - .setLenient() - .create(); - - // Adding Rx so the calls can be Observable, and adding a Gson converter with - // leniency to make parsing the results simple. - mInstance = new Retrofit.Builder() - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .addConverterFactory(GsonConverterFactory.create(gson)) - .baseUrl(Settings.BASE_URL) - .client(httpClient) - .build(); - } - - return mInstance; - } -} diff --git a/samplestore/src/main/java/com/stripe/samplestore/RetrofitFactory.kt b/samplestore/src/main/java/com/stripe/samplestore/RetrofitFactory.kt new file mode 100644 index 00000000000..a9e7fa692ab --- /dev/null +++ b/samplestore/src/main/java/com/stripe/samplestore/RetrofitFactory.kt @@ -0,0 +1,40 @@ +package com.stripe.samplestore + +import com.facebook.stetho.okhttp3.StethoInterceptor +import com.google.gson.GsonBuilder +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory + +/** + * Factory instance to keep our Retrofit instance. + */ +object RetrofitFactory { + val instance: Retrofit + + init { + // Set your desired log level. Use Level.BODY for debugging errors. + // Adding Rx so the calls can be Observable, and adding a Gson converter with + // leniency to make parsing the results simple. + + val logging = HttpLoggingInterceptor() + .setLevel(HttpLoggingInterceptor.Level.BASIC) + + val httpClient = OkHttpClient.Builder() + .addInterceptor(logging) + .addNetworkInterceptor(StethoInterceptor()) + .build() + + val gson = GsonBuilder() + .setLenient() + .create() + instance = Retrofit.Builder() + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addConverterFactory(GsonConverterFactory.create(gson)) + .baseUrl(Settings.BASE_URL) + .client(httpClient) + .build() + } +} diff --git a/samplestore/src/main/java/com/stripe/samplestore/SampleStoreApplication.java b/samplestore/src/main/java/com/stripe/samplestore/SampleStoreApplication.java deleted file mode 100644 index 3a99f31c901..00000000000 --- a/samplestore/src/main/java/com/stripe/samplestore/SampleStoreApplication.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.stripe.samplestore; - -import android.support.multidex.MultiDexApplication; - -import com.facebook.stetho.Stetho; -import com.squareup.leakcanary.LeakCanary; - -public class SampleStoreApplication extends MultiDexApplication { - - @Override - public void onCreate() { - super.onCreate(); - - Stetho.initializeWithDefaults(this); - - if (BuildConfig.DEBUG && LeakCanary.isInAnalyzerProcess(this)) { - // This process is dedicated to LeakCanary for heap analysis. - // You should not init your app in this process. - return; - } - - if (BuildConfig.DEBUG) { - LeakCanary.install(this); - } - } -} diff --git a/samplestore/src/main/java/com/stripe/samplestore/SampleStoreApplication.kt b/samplestore/src/main/java/com/stripe/samplestore/SampleStoreApplication.kt new file mode 100644 index 00000000000..1e91e6f607c --- /dev/null +++ b/samplestore/src/main/java/com/stripe/samplestore/SampleStoreApplication.kt @@ -0,0 +1,25 @@ +package com.stripe.samplestore + +import android.support.multidex.MultiDexApplication + +import com.facebook.stetho.Stetho +import com.squareup.leakcanary.LeakCanary + +class SampleStoreApplication : MultiDexApplication() { + + override fun onCreate() { + super.onCreate() + + Stetho.initializeWithDefaults(this) + + if (BuildConfig.DEBUG && LeakCanary.isInAnalyzerProcess(this)) { + // This process is dedicated to LeakCanary for heap analysis. + // You should not init your app in this process. + return + } + + if (BuildConfig.DEBUG) { + LeakCanary.install(this) + } + } +} diff --git a/samplestore/src/main/java/com/stripe/samplestore/Settings.java b/samplestore/src/main/java/com/stripe/samplestore/Settings.java deleted file mode 100644 index 9d547407382..00000000000 --- a/samplestore/src/main/java/com/stripe/samplestore/Settings.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.stripe.samplestore; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -/** - * See https://github.com/stripe/stripe-android#configuring-the-samplestore-app for instructions - * on how to configure the app before running it. - */ -final class Settings { - /** - * Set to the base URL of your test backend. If you are using - * example-ios-backend, - * the URL will be something like `https://hidden-beach-12345.herokuapp.com/`. - */ - @NonNull static final String BASE_URL = "put your base url here"; - - /** - * Set to publishable key from https://dashboard.stripe.com/test/apikeys - */ - @NonNull static final String PUBLISHABLE_KEY = "pk_test_your_key_goes_here"; - - /** - * Optionally, set to a Connect Account id to use for API requests to test Connect - * - * See https://dashboard.stripe.com/test/connect/accounts/overview - */ - @Nullable static final String STRIPE_ACCOUNT_ID = null; -} diff --git a/samplestore/src/main/java/com/stripe/samplestore/Settings.kt b/samplestore/src/main/java/com/stripe/samplestore/Settings.kt new file mode 100644 index 00000000000..91ac7ffb0a2 --- /dev/null +++ b/samplestore/src/main/java/com/stripe/samplestore/Settings.kt @@ -0,0 +1,26 @@ +package com.stripe.samplestore + +/** + * See [Configuring the samplestore app](https://github.com/stripe/stripe-android#configuring-the-samplestore-app) + * for instructions on how to configure the app before running it. + */ +internal object Settings { + /** + * Set to the base URL of your test backend. If you are using + * [example-ios-backend](https://github.com/stripe/example-ios-backend), + * the URL will be something like `https://hidden-beach-12345.herokuapp.com/`. + */ + const val BASE_URL = "put your base url here" + + /** + * Set to publishable key from https://dashboard.stripe.com/test/apikeys + */ + const val PUBLISHABLE_KEY = "pk_test_your_key_goes_here" + + /** + * Optionally, set to a Connect Account id to use for API requests to test Connect + * + * See https://dashboard.stripe.com/test/connect/accounts/overview + */ + val STRIPE_ACCOUNT_ID: String? = null +} diff --git a/samplestore/src/main/java/com/stripe/samplestore/StoreActivity.java b/samplestore/src/main/java/com/stripe/samplestore/StoreActivity.java deleted file mode 100644 index c526f9bb3a3..00000000000 --- a/samplestore/src/main/java/com/stripe/samplestore/StoreActivity.java +++ /dev/null @@ -1,174 +0,0 @@ -package com.stripe.samplestore; - -import android.app.Activity; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.FloatingActionButton; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.TextView; - -import com.stripe.android.CustomerSession; -import com.stripe.android.PaymentConfiguration; -import com.stripe.samplestore.service.SampleStoreEphemeralKeyProvider; - -import java.lang.ref.WeakReference; - -import io.reactivex.disposables.CompositeDisposable; - -public class StoreActivity - extends AppCompatActivity - implements StoreAdapter.TotalItemsChangedListener{ - - static final int PURCHASE_REQUEST = 37; - - private static final String EXTRA_PRICE_PAID = "EXTRA_PRICE_PAID"; - - @NonNull private final CompositeDisposable mCompositeDisposable = new CompositeDisposable(); - - private FloatingActionButton mGoToCartButton; - private StoreAdapter mStoreAdapter; - private SampleStoreEphemeralKeyProvider mEphemeralKeyProvider; - - @NonNull - public static Intent createPurchaseCompleteIntent(long amount) { - return new Intent() - .putExtra(EXTRA_PRICE_PAID, amount); - } - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_store); - - PaymentConfiguration.init(Settings.PUBLISHABLE_KEY); - mGoToCartButton = findViewById(R.id.fab_checkout); - mStoreAdapter = new StoreAdapter(this, getPriceMultiplier()); - - mGoToCartButton.hide(); - setSupportActionBar(findViewById(R.id.my_toolbar)); - - final RecyclerView recyclerView = findViewById(R.id.rv_store_items); - recyclerView.setLayoutManager(new LinearLayoutManager(this)); - recyclerView.addItemDecoration(new ItemDivider(this, R.drawable.item_divider)); - recyclerView.setAdapter(mStoreAdapter); - - mGoToCartButton.setOnClickListener(v -> mStoreAdapter.launchPurchaseActivityWithCart()); - setupCustomerSession(Settings.STRIPE_ACCOUNT_ID); - } - - private float getPriceMultiplier() { - try { - return getPackageManager() - .getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA) - .metaData - .getFloat("com.stripe.samplestore.price_multiplier"); - } catch (PackageManager.NameNotFoundException e) { - return 1.0f; - } - } - - @Override - protected void onDestroy() { - mCompositeDisposable.dispose(); - mEphemeralKeyProvider.destroy(); - super.onDestroy(); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (requestCode == PURCHASE_REQUEST - && resultCode == RESULT_OK - && data.getExtras() != null) { - final long price = data.getExtras().getLong(EXTRA_PRICE_PAID, -1L); - if (price != -1L) { - displayPurchase(price); - } else { - displaySetupComplete(); - } - mStoreAdapter.clearItemSelections(); - } - } - - @Override - public void onTotalItemsChanged(int totalItems) { - if (totalItems > 0) { - mGoToCartButton.show(); - } else { - mGoToCartButton.hide(); - } - } - - private void displayPurchase(long price) { - final View dialogView = LayoutInflater.from(this) - .inflate(R.layout.purchase_complete_notification, null); - - final TextView emojiView = dialogView.findViewById(R.id.dlg_emoji_display); - // Show a smiley face! - emojiView.setText(StoreUtils.getEmojiByUnicode(0x1F642)); - - final TextView priceView = dialogView.findViewById(R.id.dlg_price_display); - priceView.setText(StoreUtils.getPriceString(price, null)); - - new AlertDialog.Builder(this) - .setView(dialogView) - .create() - .show(); - } - - private void displaySetupComplete() { - final View dialogView = LayoutInflater.from(this) - .inflate(R.layout.setup_complete_notification, null); - - final TextView emojiView = dialogView.findViewById(R.id.dlg_emoji_display); - // Show a smiley face! - emojiView.setText(StoreUtils.getEmojiByUnicode(0x1F642)); - - new AlertDialog.Builder(this) - .setView(dialogView) - .create() - .show(); - } - - private void setupCustomerSession(@Nullable String stripeAccountId) { - // CustomerSession only needs to be initialized once per app. - mEphemeralKeyProvider = new SampleStoreEphemeralKeyProvider( - new ProgressListenerImpl(this), - stripeAccountId - ); - CustomerSession.initCustomerSession(this, mEphemeralKeyProvider, - stripeAccountId); - } - - private static final class ProgressListenerImpl - implements SampleStoreEphemeralKeyProvider.ProgressListener { - @NonNull private final WeakReference mActivityRef; - - private ProgressListenerImpl(@NonNull Activity activity) { - mActivityRef = new WeakReference<>(activity); - } - - @Override - public void onStringResponse(@NonNull String string) { - final Activity activity = mActivityRef.get(); - if (activity == null) { - return; - } - - if (string.startsWith("Error: ")) { - new AlertDialog.Builder(activity) - .setMessage(string) - .show(); - } - } - } -} diff --git a/samplestore/src/main/java/com/stripe/samplestore/StoreActivity.kt b/samplestore/src/main/java/com/stripe/samplestore/StoreActivity.kt new file mode 100644 index 00000000000..b88ad4d6b47 --- /dev/null +++ b/samplestore/src/main/java/com/stripe/samplestore/StoreActivity.kt @@ -0,0 +1,156 @@ +package com.stripe.samplestore + +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.support.design.widget.FloatingActionButton +import android.support.v7.app.AlertDialog +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.widget.TextView +import com.stripe.android.CustomerSession +import com.stripe.android.PaymentConfiguration +import com.stripe.samplestore.service.SampleStoreEphemeralKeyProvider +import io.reactivex.disposables.CompositeDisposable +import java.lang.ref.WeakReference + +class StoreActivity : AppCompatActivity(), StoreAdapter.TotalItemsChangedListener { + + private val compositeDisposable = CompositeDisposable() + + private lateinit var goToCartButton: FloatingActionButton + private lateinit var storeAdapter: StoreAdapter + private lateinit var ephemeralKeyProvider: SampleStoreEphemeralKeyProvider + + private val priceMultiplier: Float + get() { + return try { + packageManager + .getApplicationInfo(packageName, PackageManager.GET_META_DATA) + .metaData + .getFloat("com.stripe.samplestore.price_multiplier") + } catch (e: PackageManager.NameNotFoundException) { + 1.0f + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_store) + + PaymentConfiguration.init(Settings.PUBLISHABLE_KEY) + goToCartButton = findViewById(R.id.fab_checkout) + storeAdapter = StoreAdapter(this, priceMultiplier) + + goToCartButton.hide() + setSupportActionBar(findViewById(R.id.my_toolbar)) + + val recyclerView = findViewById(R.id.rv_store_items) + recyclerView.layoutManager = LinearLayoutManager(this) + recyclerView.addItemDecoration(ItemDivider(this, R.drawable.item_divider)) + recyclerView.adapter = storeAdapter + + goToCartButton.setOnClickListener { storeAdapter.launchPurchaseActivityWithCart() } + setupCustomerSession(Settings.STRIPE_ACCOUNT_ID) + } + + override fun onDestroy() { + compositeDisposable.dispose() + ephemeralKeyProvider.destroy() + super.onDestroy() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (requestCode == PURCHASE_REQUEST && + resultCode == Activity.RESULT_OK && + data!!.extras != null) { + val price = data.extras!!.getLong(EXTRA_PRICE_PAID, -1L) + if (price != -1L) { + displayPurchase(price) + } else { + displaySetupComplete() + } + storeAdapter.clearItemSelections() + } + } + + override fun onTotalItemsChanged(totalItems: Int) { + if (totalItems > 0) { + goToCartButton.show() + } else { + goToCartButton.hide() + } + } + + private fun displayPurchase(price: Long) { + val dialogView = LayoutInflater.from(this) + .inflate(R.layout.purchase_complete_notification, null) + + val emojiView = dialogView.findViewById(R.id.dlg_emoji_display) + // Show a smiley face! + emojiView.text = StoreUtils.getEmojiByUnicode(0x1F642) + + val priceView = dialogView.findViewById(R.id.dlg_price_display) + priceView.text = StoreUtils.getPriceString(price, null) + + AlertDialog.Builder(this) + .setView(dialogView) + .create() + .show() + } + + private fun displaySetupComplete() { + val dialogView = LayoutInflater.from(this) + .inflate(R.layout.setup_complete_notification, null) + + val emojiView = dialogView.findViewById(R.id.dlg_emoji_display) + // Show a smiley face! + emojiView.text = StoreUtils.getEmojiByUnicode(0x1F642) + + AlertDialog.Builder(this) + .setView(dialogView) + .create() + .show() + } + + private fun setupCustomerSession(stripeAccountId: String?) { + // CustomerSession only needs to be initialized once per app. + ephemeralKeyProvider = SampleStoreEphemeralKeyProvider( + ProgressListenerImpl(this), + stripeAccountId + ) + CustomerSession.initCustomerSession(this, ephemeralKeyProvider, stripeAccountId) + } + + private class ProgressListenerImpl constructor( + activity: Activity + ) : SampleStoreEphemeralKeyProvider.ProgressListener { + private val activityRef: WeakReference = WeakReference(activity) + + override fun onStringResponse(string: String) { + val activity = activityRef.get() ?: return + + if (string.startsWith("Error: ")) { + AlertDialog.Builder(activity) + .setMessage(string) + .show() + } + } + } + + companion object { + internal const val PURCHASE_REQUEST = 37 + + private const val EXTRA_PRICE_PAID = "EXTRA_PRICE_PAID" + + fun createPurchaseCompleteIntent(amount: Long): Intent { + return Intent() + .putExtra(EXTRA_PRICE_PAID, amount) + } + } +} diff --git a/samplestore/src/main/java/com/stripe/samplestore/StoreAdapter.java b/samplestore/src/main/java/com/stripe/samplestore/StoreAdapter.java deleted file mode 100644 index 1399af1b516..00000000000 --- a/samplestore/src/main/java/com/stripe/samplestore/StoreAdapter.java +++ /dev/null @@ -1,194 +0,0 @@ -package com.stripe.samplestore; - -import android.app.Activity; -import android.support.annotation.NonNull; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.TextView; - -import java.util.Currency; - -public class StoreAdapter extends RecyclerView.Adapter { - - private static final int[] EMOJI_CLOTHES = { - 0x1F455, - 0x1F456, - 0x1F457, - 0x1F458, - 0x1F459, - 0x1F45A, - 0x1F45B, - 0x1F45C, - 0x1F45D, - 0x1F45E, - 0x1F45F, - 0x1F460, - 0x1F461, - 0x1F462 - }; - - private static final int[] EMOJI_PRICES = { - 2000, - 4000, - 3000, - 700, - 600, - 1000, - 2000, - 2500, - 800, - 3000, - 2000, - 5000, - 5500, - 6000 - }; - - static class ViewHolder extends RecyclerView.ViewHolder { - @NonNull private final Currency mCurrency; - @NonNull private final TextView mEmojiTextView; - @NonNull private final TextView mPriceTextView; - @NonNull private final TextView mQuantityTextView; - @NonNull private final ImageButton mAddButton; - @NonNull private final ImageButton mRemoveButton; - - private int mPosition; - - ViewHolder(@NonNull final View pollingLayout, @NonNull Currency currency, - @NonNull StoreAdapter adapter) { - super(pollingLayout); - mEmojiTextView = pollingLayout.findViewById(R.id.tv_emoji); - mPriceTextView = pollingLayout.findViewById(R.id.tv_price); - mQuantityTextView = pollingLayout.findViewById(R.id.tv_quantity); - mAddButton = pollingLayout.findViewById(R.id.tv_plus); - mRemoveButton = pollingLayout.findViewById(R.id.tv_minus); - - mCurrency = currency; - mAddButton.setOnClickListener(v -> adapter.bumpItemQuantity(mPosition, true)); - - mRemoveButton.setOnClickListener(v -> adapter.bumpItemQuantity(mPosition, false)); - } - - void setHidden(boolean hidden) { - int visibility = hidden ? View.INVISIBLE : View.VISIBLE; - mEmojiTextView.setVisibility(visibility); - mPriceTextView.setVisibility(visibility); - mQuantityTextView.setVisibility(visibility); - mAddButton.setVisibility(visibility); - mRemoveButton.setVisibility(visibility); - } - - void setEmoji(int emojiUnicode) { - mEmojiTextView.setText(StoreUtils.getEmojiByUnicode(emojiUnicode)); - } - - void setPrice(int price) { - mPriceTextView.setText(StoreUtils.getPriceString(price, mCurrency)); - } - - void setQuantity(int quantity) { - mQuantityTextView.setText(String.valueOf(quantity)); - } - - void setPosition(int position) { - mPosition = position; - } - } - - // Storing an activity here only so we can launch for result - @NonNull private final Activity mActivity; - private final float mPriceMultiplier; - @NonNull private final Currency mCurrency; - - @NonNull private final int[] mQuantityOrdered; - private int mTotalOrdered; - @NonNull private final TotalItemsChangedListener mTotalItemsChangedListener; - - StoreAdapter(@NonNull StoreActivity activity, float priceMultiplier) { - mActivity = activity; - mPriceMultiplier = priceMultiplier; - mTotalItemsChangedListener = activity; - // Note: our sample backend assumes USD as currency. This code would be - // otherwise functional if you switched that assumption on the backend and passed - // currency code as a parameter. - mCurrency = Currency.getInstance("USD"); - mQuantityOrdered = new int[EMOJI_CLOTHES.length]; - } - - private void bumpItemQuantity(int index, boolean increase) { - if (index >= 0 && index < mQuantityOrdered.length) { - if (increase) { - mQuantityOrdered[index]++; - mTotalOrdered++; - mTotalItemsChangedListener.onTotalItemsChanged(mTotalOrdered); - } else if (mQuantityOrdered[index] > 0) { - mQuantityOrdered[index]--; - mTotalOrdered--; - mTotalItemsChangedListener.onTotalItemsChanged(mTotalOrdered); - } - } - notifyDataSetChanged(); - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - if (position == EMOJI_CLOTHES.length) { - holder.setHidden(true); - } else { - holder.setHidden(false); - holder.setEmoji(EMOJI_CLOTHES[position]); - holder.setPrice(getPrice(position)); - holder.setQuantity(mQuantityOrdered[position]); - holder.setPosition(position); - } - } - - @Override - public int getItemCount() { - return EMOJI_CLOTHES.length + 1; - } - - @NonNull - @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - final View pollingView = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.store_item, parent, false); - - return new ViewHolder(pollingView, mCurrency, this); - } - - void launchPurchaseActivityWithCart() { - final StoreCart cart = new StoreCart(mCurrency); - for (int i = 0; i < mQuantityOrdered.length; i++) { - if (mQuantityOrdered[i] > 0) { - cart.addStoreLineItem( - StoreUtils.getEmojiByUnicode(EMOJI_CLOTHES[i]), - mQuantityOrdered[i], - getPrice(i)); - } - } - - mActivity.startActivityForResult( - PaymentActivity.createIntent(mActivity, cart), - StoreActivity.PURCHASE_REQUEST); - } - - void clearItemSelections() { - for (int i = 0; i < mQuantityOrdered.length; i++) { - mQuantityOrdered[i] = 0; - } - notifyDataSetChanged(); - mTotalItemsChangedListener.onTotalItemsChanged(0); - } - - private int getPrice(int position) { - return (int) (EMOJI_PRICES[position] * mPriceMultiplier); - } - - public interface TotalItemsChangedListener { - void onTotalItemsChanged(int totalItems); - } -} diff --git a/samplestore/src/main/java/com/stripe/samplestore/StoreAdapter.kt b/samplestore/src/main/java/com/stripe/samplestore/StoreAdapter.kt new file mode 100644 index 00000000000..28c6de9ab67 --- /dev/null +++ b/samplestore/src/main/java/com/stripe/samplestore/StoreAdapter.kt @@ -0,0 +1,162 @@ +package com.stripe.samplestore + +import android.app.Activity +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.TextView + +import java.util.Currency + +class StoreAdapter internal constructor( + activity: StoreActivity, + private val priceMultiplier: Float +) : RecyclerView.Adapter() { + + // Storing an activity here only so we can launch for result + private val activity: Activity + private val currency: Currency + + private val quantityOrdered: IntArray + private var totalOrdered: Int = 0 + private val totalItemsChangedListener: TotalItemsChangedListener + + class ViewHolder( + pollingLayout: View, + private val currency: Currency, + adapter: StoreAdapter + ) : RecyclerView.ViewHolder(pollingLayout) { + private val emojiTextView: TextView = pollingLayout.findViewById(R.id.tv_emoji) + private val priceTextView: TextView = pollingLayout.findViewById(R.id.tv_price) + private val quantityTextView: TextView = pollingLayout.findViewById(R.id.tv_quantity) + private val addButton: ImageButton = pollingLayout.findViewById(R.id.tv_plus) + private val removeButton: ImageButton = pollingLayout.findViewById(R.id.tv_minus) + + private var mPosition: Int = 0 + + init { + addButton.setOnClickListener { adapter.bumpItemQuantity(mPosition, true) } + removeButton.setOnClickListener { adapter.bumpItemQuantity(mPosition, false) } + } + + fun setHidden(hidden: Boolean) { + val visibility = if (hidden) View.INVISIBLE else View.VISIBLE + emojiTextView.visibility = visibility + priceTextView.visibility = visibility + quantityTextView.visibility = visibility + addButton.visibility = visibility + removeButton.visibility = visibility + } + + fun setEmoji(emojiUnicode: Int) { + emojiTextView.text = StoreUtils.getEmojiByUnicode(emojiUnicode) + } + + fun setPrice(price: Int) { + priceTextView.text = StoreUtils.getPriceString(price.toLong(), currency) + } + + fun setQuantity(quantity: Int) { + quantityTextView.text = quantity.toString() + } + + fun setPosition(position: Int) { + mPosition = position + } + } + + init { + this.activity = activity + totalItemsChangedListener = activity + // Note: our sample backend assumes USD as currency. This code would be + // otherwise functional if you switched that assumption on the backend and passed + // currency code as a parameter. + currency = Currency.getInstance("USD") + quantityOrdered = IntArray(EMOJI_CLOTHES.size) + } + + private fun bumpItemQuantity(index: Int, increase: Boolean) { + if (index >= 0 && index < quantityOrdered.size) { + if (increase) { + quantityOrdered[index]++ + totalOrdered++ + totalItemsChangedListener.onTotalItemsChanged(totalOrdered) + } else if (quantityOrdered[index] > 0) { + quantityOrdered[index]-- + totalOrdered-- + totalItemsChangedListener.onTotalItemsChanged(totalOrdered) + } + } + notifyDataSetChanged() + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + if (position == EMOJI_CLOTHES.size) { + holder.setHidden(true) + } else { + holder.setHidden(false) + holder.setEmoji(EMOJI_CLOTHES[position]) + holder.setPrice(getPrice(position)) + holder.setQuantity(quantityOrdered[position]) + holder.position = position + } + } + + override fun getItemCount(): Int { + return EMOJI_CLOTHES.size + 1 + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val pollingView = LayoutInflater.from(parent.context) + .inflate(R.layout.store_item, parent, false) + + return ViewHolder(pollingView, currency, this) + } + + internal fun launchPurchaseActivityWithCart() { + val cart = StoreCart(currency) + for (i in quantityOrdered.indices) { + if (quantityOrdered[i] > 0) { + cart.addStoreLineItem( + StoreUtils.getEmojiByUnicode(EMOJI_CLOTHES[i]), + quantityOrdered[i], + getPrice(i).toLong()) + } + } + + activity.startActivityForResult( + PaymentActivity.createIntent(activity, cart), + StoreActivity.PURCHASE_REQUEST) + } + + internal fun clearItemSelections() { + for (i in quantityOrdered.indices) { + quantityOrdered[i] = 0 + } + notifyDataSetChanged() + totalItemsChangedListener.onTotalItemsChanged(0) + } + + private fun getPrice(position: Int): Int { + return (EMOJI_PRICES[position] * priceMultiplier).toInt() + } + + interface TotalItemsChangedListener { + fun onTotalItemsChanged(totalItems: Int) + } + + companion object { + + private val EMOJI_CLOTHES = intArrayOf( + 0x1F455, 0x1F456, 0x1F457, 0x1F458, 0x1F459, 0x1F45A, 0x1F45B, + 0x1F45C, 0x1F45D, 0x1F45E, 0x1F45F, 0x1F460, 0x1F461, 0x1F462 + ) + + private val EMOJI_PRICES = intArrayOf( + 2000, 4000, 3000, 700, 600, 1000, 2000, + 2500, 800, 3000, 2000, 5000, 5500, 6000 + ) + } +} diff --git a/samplestore/src/main/java/com/stripe/samplestore/StoreCart.java b/samplestore/src/main/java/com/stripe/samplestore/StoreCart.java deleted file mode 100644 index 1fd0fa8999c..00000000000 --- a/samplestore/src/main/java/com/stripe/samplestore/StoreCart.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.stripe.samplestore; - -import android.os.Parcel; -import android.os.Parcelable; -import android.support.annotation.NonNull; - -import java.util.ArrayList; -import java.util.Currency; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.UUID; - -public class StoreCart implements Parcelable { - - @NonNull private final Currency mCurrency; - @NonNull private final LinkedHashMap mStoreLineItems; - - StoreCart(@NonNull Currency currency) { - mCurrency = currency; - // LinkedHashMap because we want iteration order to be the same. - mStoreLineItems = new LinkedHashMap<>(); - } - - public StoreCart(@NonNull String currencyCode) { - this(Currency.getInstance(currencyCode)); - } - - void addStoreLineItem(@NonNull String description, int quantity, long unitPrice) { - addStoreLineItem(new StoreLineItem(description, quantity, unitPrice)); - } - - private void addStoreLineItem(@NonNull StoreLineItem storeLineItem) { - mStoreLineItems.put(UUID.randomUUID().toString(), storeLineItem); - } - - public boolean removeLineItem(@NonNull String itemId) { - return mStoreLineItems.remove(itemId) != null; - } - - @NonNull - List getLineItems() { - return new ArrayList<>(mStoreLineItems.values()); - } - - int getSize() { - return mStoreLineItems.size(); - } - - @NonNull - Currency getCurrency() { - return mCurrency; - } - - long getTotalPrice() { - long total = 0L; - for (StoreLineItem item : mStoreLineItems.values()) { - total += item.getTotalPrice(); - } - return total; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(mCurrency.getCurrencyCode()); - dest.writeInt(mStoreLineItems.size()); - for (String key : mStoreLineItems.keySet()) { - dest.writeString(key); - dest.writeParcelable(mStoreLineItems.get(key), 0); - } - } - - private StoreCart(@NonNull Parcel in) { - mCurrency = Currency.getInstance(in.readString()); - int count = in.readInt(); - mStoreLineItems = new LinkedHashMap<>(); - for (int i = 0; i < count; i++) { - mStoreLineItems.put(in.readString(), - in.readParcelable(StoreLineItem.class.getClassLoader())); - } - } - - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { - @Override - public StoreCart createFromParcel(Parcel source) { - return new StoreCart(source); - } - - @Override - public StoreCart[] newArray(int size) { - return new StoreCart[size]; - } - }; -} diff --git a/samplestore/src/main/java/com/stripe/samplestore/StoreCart.kt b/samplestore/src/main/java/com/stripe/samplestore/StoreCart.kt new file mode 100644 index 00000000000..7b5ebe3abb9 --- /dev/null +++ b/samplestore/src/main/java/com/stripe/samplestore/StoreCart.kt @@ -0,0 +1,78 @@ +package com.stripe.samplestore + +import android.os.Parcel +import android.os.Parcelable + +import java.util.ArrayList +import java.util.Currency +import java.util.LinkedHashMap +import java.util.UUID + +class StoreCart : Parcelable { + + val currency: Currency + private val storeLineItems: LinkedHashMap + + internal val lineItems: List + get() = ArrayList(storeLineItems.values) + + internal val totalPrice: Long + get() { + var total = 0L + for (item in storeLineItems.values) { + total += item.totalPrice + } + return total + } + + internal constructor(currency: Currency) { + this.currency = currency + // LinkedHashMap because we want iteration order to be the same. + storeLineItems = LinkedHashMap() + } + + fun addStoreLineItem(description: String, quantity: Int, unitPrice: Long) { + addStoreLineItem(StoreLineItem(description, quantity, unitPrice)) + } + + private fun addStoreLineItem(storeLineItem: StoreLineItem) { + storeLineItems[UUID.randomUUID().toString()] = storeLineItem + } + + fun removeLineItem(itemId: String): Boolean { + return storeLineItems.remove(itemId) != null + } + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(currency.currencyCode) + dest.writeInt(storeLineItems.size) + for (key in storeLineItems.keys) { + dest.writeString(key) + dest.writeParcelable(storeLineItems[key], 0) + } + } + + private constructor(input: Parcel) { + currency = Currency.getInstance(input.readString()) + val count = input.readInt() + storeLineItems = LinkedHashMap() + for (i in 0 until count) { + storeLineItems[input.readString()!!] = + input.readParcelable(StoreLineItem::class.java.classLoader) + } + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): StoreCart { + return StoreCart(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} diff --git a/samplestore/src/main/java/com/stripe/samplestore/StoreLineItem.java b/samplestore/src/main/java/com/stripe/samplestore/StoreLineItem.java deleted file mode 100644 index fa81de3fa21..00000000000 --- a/samplestore/src/main/java/com/stripe/samplestore/StoreLineItem.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.stripe.samplestore; - -import android.os.Parcel; -import android.os.Parcelable; -import android.support.annotation.NonNull; - -/** - * Represents a single line item for purchase in this store. - */ -public class StoreLineItem implements Parcelable { - - @NonNull private final String mDescription; - private final int mQuantity; - private final long mUnitPrice; - - StoreLineItem(@NonNull String description, int quantity, long unitPrice) { - mDescription = description; - mQuantity = quantity; - mUnitPrice = unitPrice; - } - - @NonNull - String getDescription() { - return mDescription; - } - - int getQuantity() { - return mQuantity; - } - - long getUnitPrice() { - return mUnitPrice; - } - - long getTotalPrice() { - return mUnitPrice * mQuantity; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeString(this.mDescription); - dest.writeInt(this.mQuantity); - dest.writeLong(this.mUnitPrice); - } - - protected StoreLineItem(@NonNull Parcel in) { - this.mDescription = in.readString(); - this.mQuantity = in.readInt(); - this.mUnitPrice = in.readLong(); - } - - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { - @Override - public StoreLineItem createFromParcel(Parcel source) { - return new StoreLineItem(source); - } - - @Override - public StoreLineItem[] newArray(int size) { - return new StoreLineItem[size]; - } - }; -} diff --git a/samplestore/src/main/java/com/stripe/samplestore/StoreLineItem.kt b/samplestore/src/main/java/com/stripe/samplestore/StoreLineItem.kt new file mode 100644 index 00000000000..6477ffa0d11 --- /dev/null +++ b/samplestore/src/main/java/com/stripe/samplestore/StoreLineItem.kt @@ -0,0 +1,49 @@ +package com.stripe.samplestore + +import android.os.Parcel +import android.os.Parcelable + +/** + * Represents a single line item for purchase in this store. + */ +class StoreLineItem : Parcelable { + + val description: String + val quantity: Int + val unitPrice: Long + + internal val totalPrice: Long + get() = unitPrice * quantity + + internal constructor(description: String, quantity: Int, unitPrice: Long) { + this.description = description + this.quantity = quantity + this.unitPrice = unitPrice + } + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(this.description) + dest.writeInt(this.quantity) + dest.writeLong(this.unitPrice) + } + + private constructor(input: Parcel) { + this.description = input.readString()!! + this.quantity = input.readInt() + this.unitPrice = input.readLong() + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): StoreLineItem { + return StoreLineItem(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} diff --git a/samplestore/src/main/java/com/stripe/samplestore/StoreUtils.java b/samplestore/src/main/java/com/stripe/samplestore/StoreUtils.java deleted file mode 100644 index cb78de10ffc..00000000000 --- a/samplestore/src/main/java/com/stripe/samplestore/StoreUtils.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.stripe.samplestore; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.text.DecimalFormat; -import java.util.Currency; - -/** - * Class for utility functions. - */ -class StoreUtils { - - @NonNull - static String getEmojiByUnicode(int unicode){ - return new String(Character.toChars(unicode)); - } - - @NonNull - static String getPriceString(long price, @Nullable Currency currency) { - Currency displayCurrency = currency == null - ? Currency.getInstance("USD") - : currency; - - int fractionDigits = displayCurrency.getDefaultFractionDigits(); - int totalLength = String.valueOf(price).length(); - StringBuilder builder = new StringBuilder(); - builder.append('\u00A4'); - - if (fractionDigits == 0) { - for (int i = 0; i < totalLength; i++) { - builder.append('#'); - } - DecimalFormat noDecimalCurrencyFormat = new DecimalFormat(builder.toString()); - noDecimalCurrencyFormat.setCurrency(displayCurrency); - return noDecimalCurrencyFormat.format(price); - } - - int beforeDecimal = totalLength - fractionDigits; - for (int i = 0; i < beforeDecimal; i++) { - builder.append('#'); - } - // So we display "$0.55" instead of "$.55" - if (totalLength <= fractionDigits) { - builder.append('0'); - } - builder.append('.'); - for (int i = 0; i < fractionDigits; i++) { - builder.append('0'); - } - double modBreak = Math.pow(10, fractionDigits); - double decimalPrice = price / modBreak; - - DecimalFormat decimalFormat = new DecimalFormat(builder.toString()); - decimalFormat.setCurrency(displayCurrency); - - return decimalFormat.format(decimalPrice); - } -} diff --git a/samplestore/src/main/java/com/stripe/samplestore/StoreUtils.kt b/samplestore/src/main/java/com/stripe/samplestore/StoreUtils.kt new file mode 100644 index 00000000000..b7cb3ee612c --- /dev/null +++ b/samplestore/src/main/java/com/stripe/samplestore/StoreUtils.kt @@ -0,0 +1,53 @@ +package com.stripe.samplestore + +import java.text.DecimalFormat +import java.util.Currency +import kotlin.math.pow + +/** + * Class for utility functions. + */ +internal object StoreUtils { + + fun getEmojiByUnicode(unicode: Int): String { + return String(Character.toChars(unicode)) + } + + fun getPriceString(price: Long, currency: Currency?): String { + val displayCurrency = currency ?: Currency.getInstance("USD") + + val fractionDigits = displayCurrency.defaultFractionDigits + val totalLength = price.toString().length + val builder = StringBuilder() + builder.append('\u00A4') + + if (fractionDigits == 0) { + for (i in 0 until totalLength) { + builder.append('#') + } + val noDecimalCurrencyFormat = DecimalFormat(builder.toString()) + noDecimalCurrencyFormat.currency = displayCurrency + return noDecimalCurrencyFormat.format(price) + } + + val beforeDecimal = totalLength - fractionDigits + for (i in 0 until beforeDecimal) { + builder.append('#') + } + // So we display "$0.55" instead of "$.55" + if (totalLength <= fractionDigits) { + builder.append('0') + } + builder.append('.') + for (i in 0 until fractionDigits) { + builder.append('0') + } + val modBreak = 10.0.pow(fractionDigits.toDouble()) + val decimalPrice = price / modBreak + + val decimalFormat = DecimalFormat(builder.toString()) + decimalFormat.currency = displayCurrency + + return decimalFormat.format(decimalPrice) + } +} diff --git a/samplestore/src/main/java/com/stripe/samplestore/service/SampleStoreEphemeralKeyProvider.java b/samplestore/src/main/java/com/stripe/samplestore/service/SampleStoreEphemeralKeyProvider.java deleted file mode 100644 index 647f45221a9..00000000000 --- a/samplestore/src/main/java/com/stripe/samplestore/service/SampleStoreEphemeralKeyProvider.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.stripe.samplestore.service; - -import android.support.annotation.NonNull; -import android.support.annotation.Size; - -import com.stripe.android.EphemeralKeyProvider; -import com.stripe.android.EphemeralKeyUpdateListener; -import com.stripe.samplestore.RetrofitFactory; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import javax.annotation.Nullable; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import retrofit2.Retrofit; - -public class SampleStoreEphemeralKeyProvider implements EphemeralKeyProvider { - @NonNull private final CompositeDisposable mCompositeDisposable; - @NonNull private final StripeService mStripeService; - @NonNull private final ProgressListener mProgressListener; - @Nullable private final String mStripeAccountId; - - public SampleStoreEphemeralKeyProvider(@NonNull ProgressListener progressListener, - @Nullable String stripeAccountId) { - final Retrofit retrofit = RetrofitFactory.getInstance(); - mStripeService = retrofit.create(StripeService.class); - mCompositeDisposable = new CompositeDisposable(); - mProgressListener = progressListener; - mStripeAccountId = stripeAccountId; - } - - public SampleStoreEphemeralKeyProvider(@NonNull ProgressListener progressListener) { - this(progressListener, null); - } - - @Override - public void createEphemeralKey(@NonNull @Size(min = 4) String apiVersion, - @NonNull final EphemeralKeyUpdateListener keyUpdateListener) { - final Map apiParamMap = new HashMap<>(); - apiParamMap.put("api_version", apiVersion); - if (mStripeAccountId != null) { - apiParamMap.put("stripe_account", mStripeAccountId); - } - - mCompositeDisposable.add(mStripeService.createEphemeralKey(apiParamMap) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - response -> { - try { - String rawKey = response.string(); - keyUpdateListener.onKeyUpdate(rawKey); - mProgressListener.onStringResponse(rawKey); - } catch (IOException e) { - keyUpdateListener.onKeyUpdateFailure(0, e.getMessage()); - } - }, - throwable -> mProgressListener - .onStringResponse(throwable.getMessage()))); - } - - public void destroy() { - mCompositeDisposable.dispose(); - } - - public interface ProgressListener { - void onStringResponse(@NonNull String string); - } -} diff --git a/samplestore/src/main/java/com/stripe/samplestore/service/SampleStoreEphemeralKeyProvider.kt b/samplestore/src/main/java/com/stripe/samplestore/service/SampleStoreEphemeralKeyProvider.kt new file mode 100644 index 00000000000..3176881d8dd --- /dev/null +++ b/samplestore/src/main/java/com/stripe/samplestore/service/SampleStoreEphemeralKeyProvider.kt @@ -0,0 +1,59 @@ +package com.stripe.samplestore.service + +import android.support.annotation.Size +import com.stripe.android.EphemeralKeyProvider +import com.stripe.android.EphemeralKeyUpdateListener +import com.stripe.samplestore.RetrofitFactory +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import java.io.IOException +import java.util.HashMap + +class SampleStoreEphemeralKeyProvider @JvmOverloads constructor( + private val progressListener: ProgressListener, + private val stripeAccountId: String? = null +) : EphemeralKeyProvider { + + private val compositeDisposable: CompositeDisposable = CompositeDisposable() + private val stripeService: StripeService = + RetrofitFactory.instance.create(StripeService::class.java) + + override fun createEphemeralKey( + @Size(min = 4) apiVersion: String, + keyUpdateListener: EphemeralKeyUpdateListener + ) { + val apiParamMap = HashMap() + apiParamMap["api_version"] = apiVersion + if (stripeAccountId != null) { + apiParamMap["stripe_account"] = stripeAccountId + } + + compositeDisposable.add(stripeService.createEphemeralKey(apiParamMap) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { response -> + try { + val rawKey = response.string() + keyUpdateListener.onKeyUpdate(rawKey) + progressListener.onStringResponse(rawKey) + } catch (e: IOException) { + keyUpdateListener + .onKeyUpdateFailure(0, e.message ?: "") + } + }, + { throwable -> + progressListener + .onStringResponse(throwable.message ?: "") + })) + } + + fun destroy() { + compositeDisposable.dispose() + } + + interface ProgressListener { + fun onStringResponse(string: String) + } +} diff --git a/samplestore/src/main/java/com/stripe/samplestore/service/StripeService.java b/samplestore/src/main/java/com/stripe/samplestore/service/StripeService.java deleted file mode 100644 index cff9a2e4fe9..00000000000 --- a/samplestore/src/main/java/com/stripe/samplestore/service/StripeService.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.stripe.samplestore.service; - -import java.util.Map; - -import io.reactivex.Observable; -import okhttp3.ResponseBody; -import retrofit2.http.Body; -import retrofit2.http.FieldMap; -import retrofit2.http.FormUrlEncoded; -import retrofit2.http.POST; - -/** - * The {@link retrofit2.Retrofit} interface that creates our API service. - */ -public interface StripeService { - - /** - * Returns the PaymentIntent client secret in the format shown below. - * - * {"secret": "pi_1Eu5SqCRMb_secret_O2Avhk5V0Pjeo"} - */ - @POST("capture_payment") - Observable capturePayment(@Body Map params); - - /** - * Used for Payment Intent Manual confirmation - * - * @see - * Manual Confirmation Flow - * - * Returns the PaymentIntent client secret in the format shown below. - * - * {"secret": "pi_1Eu5SqCRMb_secret_O2Avhk5V0Pjeo"} - */ - @POST("confirm_payment") - Observable confirmPayment(@Body Map params); - - @POST("create_setup_intent") - Observable createSetupIntent(@Body Map params); - - @FormUrlEncoded - @POST("ephemeral_keys") - Observable createEphemeralKey(@FieldMap Map apiVersionMap); -} diff --git a/samplestore/src/main/java/com/stripe/samplestore/service/StripeService.kt b/samplestore/src/main/java/com/stripe/samplestore/service/StripeService.kt new file mode 100644 index 00000000000..0c5751078f0 --- /dev/null +++ b/samplestore/src/main/java/com/stripe/samplestore/service/StripeService.kt @@ -0,0 +1,41 @@ +package com.stripe.samplestore.service + +import io.reactivex.Observable +import okhttp3.ResponseBody +import retrofit2.http.Body +import retrofit2.http.FieldMap +import retrofit2.http.FormUrlEncoded +import retrofit2.http.POST + +/** + * The [retrofit2.Retrofit] interface that creates our API service. + */ +interface StripeService { + + /** + * Returns the PaymentIntent client secret in the format shown below. + * + * {"secret": "pi_1Eu5SqCRMb_secret_O2Avhk5V0Pjeo"} + */ + @POST("capture_payment") + fun capturePayment(@Body params: HashMap): Observable + + /** + * Used for Payment Intent Manual confirmation + * + * @see [Manual Confirmation Flow](https://stripe.com/docs/payments/payment-intents/quickstart.manual-confirmation-flow) + * + * Returns the PaymentIntent client secret in the format shown below. + * + * {"secret": "pi_1Eu5SqCRMb_secret_O2Avhk5V0Pjeo"} + */ + @POST("confirm_payment") + fun confirmPayment(@Body params: HashMap): Observable + + @POST("create_setup_intent") + fun createSetupIntent(@Body params: HashMap): Observable + + @FormUrlEncoded + @POST("ephemeral_keys") + fun createEphemeralKey(@FieldMap apiVersionMap: HashMap): Observable +} From a5b09039c72719f12e1c5613021fee8a06628eea Mon Sep 17 00:00:00 2001 From: Michael Shafrir Date: Fri, 2 Aug 2019 12:55:56 -0400 Subject: [PATCH 2/2] respond to comments --- samplestore/src/main/java/com/stripe/samplestore/StoreUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samplestore/src/main/java/com/stripe/samplestore/StoreUtils.kt b/samplestore/src/main/java/com/stripe/samplestore/StoreUtils.kt index b7cb3ee612c..a96f3f80dd2 100644 --- a/samplestore/src/main/java/com/stripe/samplestore/StoreUtils.kt +++ b/samplestore/src/main/java/com/stripe/samplestore/StoreUtils.kt @@ -19,7 +19,7 @@ internal object StoreUtils { val fractionDigits = displayCurrency.defaultFractionDigits val totalLength = price.toString().length val builder = StringBuilder() - builder.append('\u00A4') + .append('\u00A4') // currency sign if (fractionDigits == 0) { for (i in 0 until totalLength) {