From 73b6e3aeae8f0af97353b3e18b7bf13740de42a4 Mon Sep 17 00:00:00 2001 From: Bella Koch <160939932+amk-stripe@users.noreply.github.com> Date: Thu, 24 Oct 2024 11:02:47 -0700 Subject: [PATCH] Remove basic integration code (#9501) * Remove basic integration code * Increase visibility to fix build issue * Fix API changes for ActivityStarter * Fix API changes for ActivityStarter * Fix API changes for ActivityStarter * Fix API changes for ActivityStarter * Update CHANGELOG.md and MIGRATING.md --- CHANGELOG.md | 3 + MIGRATING.md | 3 + payments-core/AndroidManifest.xml | 12 - payments-core/api/payments-core.api | 483 ---------- .../layout/stripe_payment_flow_activity.xml | 10 - .../stripe_payment_methods_activity.xml | 47 - .../stripe/android/PaymentSessionFixtures.kt | 94 -- .../BasicIntegrationDeprecationWarning.kt | 5 - .../com/stripe/android/CustomerSession.kt | 576 ------------ .../CustomerSessionOperationExecutor.kt | 238 ----- .../com/stripe/android/EphemeralKeyManager.kt | 2 + .../java/com/stripe/android/PaymentSession.kt | 337 ------- .../stripe/android/PaymentSessionConfig.kt | 320 ------- .../com/stripe/android/PaymentSessionData.kt | 64 -- .../com/stripe/android/PaymentSessionPrefs.kt | 54 -- .../stripe/android/PaymentSessionViewModel.kt | 273 ------ .../DefaultPaymentSessionEventReporter.kt | 90 -- .../android/analytics/PaymentSessionEvent.kt | 93 -- .../analytics/PaymentSessionEventReporter.kt | 51 -- .../PaymentSessionEventReporterFactory.kt | 30 - .../stripe/android/view/ActivityStarter.kt | 65 +- .../android/view/AddPaymentMethodActivity.kt | 285 ------ .../view/AddPaymentMethodActivityStarter.kt | 178 ---- .../android/view/AddPaymentMethodCardView.kt | 126 --- .../android/view/AddPaymentMethodContract.kt | 24 - .../android/view/AddPaymentMethodFpxView.kt | 105 --- .../view/AddPaymentMethodListAdapter.kt | 109 --- .../view/AddPaymentMethodNetbankingView.kt | 66 -- .../android/view/AddPaymentMethodView.kt | 21 - .../android/view/AddPaymentMethodViewModel.kt | 161 ---- .../android/view/BillingAddressFields.kt | 21 - .../view/DeletePaymentMethodDialogFactory.kt | 60 -- .../com/stripe/android/view/FpxViewModel.kt | 52 -- .../android/view/PaymentFlowActivity.kt | 259 ------ .../view/PaymentFlowActivityStarter.kt | 86 -- .../stripe/android/view/PaymentFlowPage.kt | 12 - .../android/view/PaymentFlowPagerAdapter.kt | 188 ---- .../android/view/PaymentFlowViewModel.kt | 116 --- .../android/view/PaymentFlowViewPager.kt | 28 - .../view/PaymentMethodSwipeCallback.kt | 185 ---- .../android/view/PaymentMethodsActivity.kt | 315 ------- .../view/PaymentMethodsActivityStarter.kt | 209 ----- .../android/view/PaymentMethodsAdapter.kt | 382 -------- .../view/PaymentMethodsRecyclerView.kt | 39 - .../android/view/PaymentMethodsViewModel.kt | 148 --- .../view/SwipeToDeleteCallbackListener.kt | 14 - .../CustomerSessionOperationExecutorTest.kt | 234 ----- .../com/stripe/android/CustomerSessionTest.kt | 843 ------------------ .../android/PaymentSessionConfigTest.kt | 67 -- .../stripe/android/PaymentSessionDataTest.kt | 95 -- .../stripe/android/PaymentSessionFixtures.kt | 94 -- .../com/stripe/android/PaymentSessionTest.kt | 407 --------- .../android/PaymentSessionViewModelTest.kt | 328 ------- .../DefaultPaymentSessionEventReporterTest.kt | 170 ---- .../AddPaymentMethodActivityStarterTest.kt | 32 - .../view/AddPaymentMethodActivityTest.kt | 540 ----------- .../view/AddPaymentMethodCardViewTest.kt | 35 - .../view/AddPaymentMethodViewModelTest.kt | 228 ----- .../DeletePaymentMethodDialogFactoryTest.kt | 49 - .../stripe/android/view/FpxViewModelTest.kt | 36 - .../android/view/PaymentFlowActivityTest.kt | 231 ----- .../view/PaymentFlowPagerAdapterTest.kt | 28 - .../android/view/PaymentFlowViewModelTest.kt | 156 ---- .../view/PaymentMethodSwipeCallbackTest.kt | 25 - .../view/PaymentMethodsActivityStarterTest.kt | 65 -- .../view/PaymentMethodsActivityTest.kt | 258 ------ .../android/view/PaymentMethodsAdapterTest.kt | 309 ------- .../view/PaymentMethodsViewModelTest.kt | 184 ---- 68 files changed, 15 insertions(+), 10438 deletions(-) delete mode 100644 payments-core/res/layout/stripe_payment_flow_activity.xml delete mode 100644 payments-core/res/layout/stripe_payment_methods_activity.xml delete mode 100644 payments-core/src/androidTest/java/com/stripe/android/PaymentSessionFixtures.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/BasicIntegrationDeprecationWarning.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/CustomerSession.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/CustomerSessionOperationExecutor.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/PaymentSession.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/PaymentSessionConfig.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/PaymentSessionData.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/PaymentSessionPrefs.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/PaymentSessionViewModel.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/analytics/DefaultPaymentSessionEventReporter.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/analytics/PaymentSessionEvent.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/analytics/PaymentSessionEventReporter.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/analytics/PaymentSessionEventReporterFactory.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodActivity.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodActivityStarter.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodCardView.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodContract.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodFpxView.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodListAdapter.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodNetbankingView.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodView.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodViewModel.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/BillingAddressFields.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/DeletePaymentMethodDialogFactory.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/FpxViewModel.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/PaymentFlowActivity.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/PaymentFlowActivityStarter.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/PaymentFlowPage.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/PaymentFlowPagerAdapter.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/PaymentFlowViewModel.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/PaymentFlowViewPager.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/PaymentMethodSwipeCallback.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/PaymentMethodsActivity.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/PaymentMethodsActivityStarter.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/PaymentMethodsAdapter.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/PaymentMethodsRecyclerView.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/PaymentMethodsViewModel.kt delete mode 100644 payments-core/src/main/java/com/stripe/android/view/SwipeToDeleteCallbackListener.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/CustomerSessionOperationExecutorTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/CustomerSessionTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/PaymentSessionConfigTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/PaymentSessionDataTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/PaymentSessionFixtures.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/PaymentSessionTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/PaymentSessionViewModelTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/analytics/DefaultPaymentSessionEventReporterTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/view/AddPaymentMethodActivityStarterTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/view/AddPaymentMethodActivityTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/view/AddPaymentMethodCardViewTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/view/AddPaymentMethodViewModelTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/view/DeletePaymentMethodDialogFactoryTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/view/FpxViewModelTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/view/PaymentFlowActivityTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/view/PaymentFlowPagerAdapterTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/view/PaymentFlowViewModelTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/view/PaymentMethodSwipeCallbackTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/view/PaymentMethodsActivityStarterTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/view/PaymentMethodsActivityTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/view/PaymentMethodsAdapterTest.kt delete mode 100644 payments-core/src/test/java/com/stripe/android/view/PaymentMethodsViewModelTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cfb95ce85a..18e749b1e1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## XX.XX.XX - 20XX-XX-XX +### Basic Integration +* [REMOVED][9501](https://github.com/stripe/stripe-android/pull/9501) Basic Integration has been removed. [Please use Mobile Payment Element instead](https://docs.stripe.com/payments/mobile/migrating-to-mobile-payment-element-from-basic-integration). + ## 20.53.0 - 2024-10-21 ### PaymentSheet diff --git a/MIGRATING.md b/MIGRATING.md index 6b29c88e37a..6345c3940ec 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -1,5 +1,8 @@ # Migration Guide +## Migrating from versions < 21.0.0 +- Basic Integration has been removed. [Please use Mobile Payment Element instead](https://docs.stripe.com/payments/mobile/migrating-to-mobile-payment-element-from-basic-integration). + ## Migrating from versions < 20.26.0 - Changes to `PaymentSheetContract`: * The class has been deprecated and will be removed in a future release. Use the `PaymentSheet` constructor or the new `rememberPaymentSheet()` method. diff --git a/payments-core/AndroidManifest.xml b/payments-core/AndroidManifest.xml index 3093903d75e..10c16a4de9d 100644 --- a/payments-core/AndroidManifest.xml +++ b/payments-core/AndroidManifest.xml @@ -2,18 +2,6 @@ - - - ()V } -public final class com/stripe/android/CustomerSession { - public static final field $stable I - public static final field Companion Lcom/stripe/android/CustomerSession$Companion; - public final fun addCustomerSource (Ljava/lang/String;Ljava/lang/String;Lcom/stripe/android/CustomerSession$SourceRetrievalListener;)V - public final fun attachPaymentMethod (Ljava/lang/String;Lcom/stripe/android/CustomerSession$PaymentMethodRetrievalListener;)V - public static final fun cancelCallbacks ()V - public final fun deleteCustomerSource (Ljava/lang/String;Lcom/stripe/android/CustomerSession$SourceRetrievalListener;)V - public final fun detachPaymentMethod (Ljava/lang/String;Lcom/stripe/android/CustomerSession$PaymentMethodRetrievalListener;)V - public static final fun endCustomerSession ()V - public final fun getCachedCustomer ()Lcom/stripe/android/model/Customer; - public static final fun getInstance ()Lcom/stripe/android/CustomerSession; - public final fun getPaymentMethods (Lcom/stripe/android/model/PaymentMethod$Type;Lcom/stripe/android/CustomerSession$PaymentMethodsRetrievalListener;)V - public final fun getPaymentMethods (Lcom/stripe/android/model/PaymentMethod$Type;Ljava/lang/Integer;Lcom/stripe/android/CustomerSession$PaymentMethodsRetrievalListener;)V - public final fun getPaymentMethods (Lcom/stripe/android/model/PaymentMethod$Type;Ljava/lang/Integer;Ljava/lang/String;Lcom/stripe/android/CustomerSession$PaymentMethodsRetrievalListener;)V - public final fun getPaymentMethods (Lcom/stripe/android/model/PaymentMethod$Type;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Lcom/stripe/android/CustomerSession$PaymentMethodsRetrievalListener;)V - public static synthetic fun getPaymentMethods$default (Lcom/stripe/android/CustomerSession;Lcom/stripe/android/model/PaymentMethod$Type;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Lcom/stripe/android/CustomerSession$PaymentMethodsRetrievalListener;ILjava/lang/Object;)V - public static final fun initCustomerSession (Landroid/content/Context;Lcom/stripe/android/EphemeralKeyProvider;)V - public static final fun initCustomerSession (Landroid/content/Context;Lcom/stripe/android/EphemeralKeyProvider;Z)V - public final fun retrieveCurrentCustomer (Lcom/stripe/android/CustomerSession$CustomerRetrievalListener;)V - public final fun setCustomerDefaultSource (Ljava/lang/String;Ljava/lang/String;Lcom/stripe/android/CustomerSession$CustomerRetrievalListener;)V - public final fun setCustomerShippingInformation (Lcom/stripe/android/model/ShippingInformation;Lcom/stripe/android/CustomerSession$CustomerRetrievalListener;)V - public final fun updateCurrentCustomer (Lcom/stripe/android/CustomerSession$CustomerRetrievalListener;)V -} - -public final class com/stripe/android/CustomerSession$Companion { - public final fun cancelCallbacks ()V - public final fun endCustomerSession ()V - public final fun getInstance ()Lcom/stripe/android/CustomerSession; - public final fun initCustomerSession (Landroid/content/Context;Lcom/stripe/android/EphemeralKeyProvider;)V - public final fun initCustomerSession (Landroid/content/Context;Lcom/stripe/android/EphemeralKeyProvider;Z)V - public static synthetic fun initCustomerSession$default (Lcom/stripe/android/CustomerSession$Companion;Landroid/content/Context;Lcom/stripe/android/EphemeralKeyProvider;ZILjava/lang/Object;)V -} - -public abstract interface class com/stripe/android/CustomerSession$CustomerRetrievalListener : com/stripe/android/CustomerSession$RetrievalListener { - public abstract fun onCustomerRetrieved (Lcom/stripe/android/model/Customer;)V -} - -public abstract interface class com/stripe/android/CustomerSession$PaymentMethodRetrievalListener : com/stripe/android/CustomerSession$RetrievalListener { - public abstract fun onPaymentMethodRetrieved (Lcom/stripe/android/model/PaymentMethod;)V -} - -public abstract interface class com/stripe/android/CustomerSession$PaymentMethodsRetrievalListener : com/stripe/android/CustomerSession$RetrievalListener { - public abstract fun onPaymentMethodsRetrieved (Ljava/util/List;)V -} - -public abstract interface class com/stripe/android/CustomerSession$RetrievalListener { - public abstract fun onError (ILjava/lang/String;Lcom/stripe/android/core/StripeError;)V -} - -public abstract interface class com/stripe/android/CustomerSession$SourceRetrievalListener : com/stripe/android/CustomerSession$RetrievalListener { - public abstract fun onSourceRetrieved (Lcom/stripe/android/model/Source;)V -} - public final class com/stripe/android/DefaultCardBrandFilter$Creator : android/os/Parcelable$Creator { public fun ()V public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/DefaultCardBrandFilter; @@ -666,135 +613,6 @@ public final class com/stripe/android/PaymentRelayStarter$Args$SourceArgs$Creato public synthetic fun newArray (I)[Ljava/lang/Object; } -public final class com/stripe/android/PaymentSession { - public static final field $stable I - public fun (Landroidx/activity/ComponentActivity;Lcom/stripe/android/PaymentSessionConfig;)V - public fun (Landroidx/fragment/app/Fragment;Lcom/stripe/android/PaymentSessionConfig;)V - public final fun clearPaymentMethod ()V - public final fun handlePaymentData (IILandroid/content/Intent;)Z - public final fun init (Lcom/stripe/android/PaymentSession$PaymentSessionListener;)V - public final fun onCompleted ()V - public final fun presentPaymentMethodSelection (Ljava/lang/String;)V - public static synthetic fun presentPaymentMethodSelection$default (Lcom/stripe/android/PaymentSession;Ljava/lang/String;ILjava/lang/Object;)V - public final fun presentShippingFlow ()V - public final fun setCartTotal (J)V -} - -public abstract interface class com/stripe/android/PaymentSession$PaymentSessionListener { - public abstract fun onCommunicatingStateChanged (Z)V - public abstract fun onError (ILjava/lang/String;)V - public abstract fun onPaymentSessionDataChanged (Lcom/stripe/android/PaymentSessionData;)V -} - -public final class com/stripe/android/PaymentSessionConfig : android/os/Parcelable { - public static final field $stable I - public static final field CREATOR Landroid/os/Parcelable$Creator; - public fun ()V - public final fun component1 ()Ljava/util/List; - public final fun component10 ()Ljava/util/Set; - public final fun component11 ()Lcom/stripe/android/view/BillingAddressFields; - public final fun component12 ()Z - public final fun component2 ()Ljava/util/List; - public final fun component3 ()Lcom/stripe/android/model/ShippingInformation; - public final fun component4 ()Z - public final fun component5 ()Z - public final fun component6 ()I - public final fun component7 ()I - public final fun component8 ()Ljava/util/List; - public final fun component9 ()Z - public final fun copy (Ljava/util/List;Ljava/util/List;Lcom/stripe/android/model/ShippingInformation;ZZIILjava/util/List;ZLjava/util/Set;Lcom/stripe/android/view/BillingAddressFields;ZZLcom/stripe/android/PaymentSessionConfig$ShippingInformationValidator;Lcom/stripe/android/PaymentSessionConfig$ShippingMethodsFactory;Ljava/lang/Integer;)Lcom/stripe/android/PaymentSessionConfig; - public static synthetic fun copy$default (Lcom/stripe/android/PaymentSessionConfig;Ljava/util/List;Ljava/util/List;Lcom/stripe/android/model/ShippingInformation;ZZIILjava/util/List;ZLjava/util/Set;Lcom/stripe/android/view/BillingAddressFields;ZZLcom/stripe/android/PaymentSessionConfig$ShippingInformationValidator;Lcom/stripe/android/PaymentSessionConfig$ShippingMethodsFactory;Ljava/lang/Integer;ILjava/lang/Object;)Lcom/stripe/android/PaymentSessionConfig; - public fun describeContents ()I - public fun equals (Ljava/lang/Object;)Z - public final fun getAddPaymentMethodFooterLayoutId ()I - public final fun getAllowedShippingCountryCodes ()Ljava/util/Set; - public final fun getBillingAddressFields ()Lcom/stripe/android/view/BillingAddressFields; - public final fun getCanDeletePaymentMethods ()Z - public final fun getHiddenShippingInfoFields ()Ljava/util/List; - public final fun getOptionalShippingInfoFields ()Ljava/util/List; - public final fun getPaymentMethodTypes ()Ljava/util/List; - public final fun getPaymentMethodsFooterLayoutId ()I - public final fun getPrepopulatedShippingInfo ()Lcom/stripe/android/model/ShippingInformation; - public final fun getShouldShowGooglePay ()Z - public fun hashCode ()I - public final fun isShippingInfoRequired ()Z - public final fun isShippingMethodRequired ()Z - public fun toString ()Ljava/lang/String; - public fun writeToParcel (Landroid/os/Parcel;I)V -} - -public final class com/stripe/android/PaymentSessionConfig$Builder { - public static final field $stable I - public fun ()V - public final fun build ()Lcom/stripe/android/PaymentSessionConfig; - public final fun setAddPaymentMethodFooter (I)Lcom/stripe/android/PaymentSessionConfig$Builder; - public final fun setAllowedShippingCountryCodes (Ljava/util/Set;)Lcom/stripe/android/PaymentSessionConfig$Builder; - public final fun setBillingAddressFields (Lcom/stripe/android/view/BillingAddressFields;)Lcom/stripe/android/PaymentSessionConfig$Builder; - public final fun setCanDeletePaymentMethods (Z)Lcom/stripe/android/PaymentSessionConfig$Builder; - public final fun setHiddenShippingInfoFields ([Lcom/stripe/android/view/ShippingInfoWidget$CustomizableShippingField;)Lcom/stripe/android/PaymentSessionConfig$Builder; - public final fun setOptionalShippingInfoFields ([Lcom/stripe/android/view/ShippingInfoWidget$CustomizableShippingField;)Lcom/stripe/android/PaymentSessionConfig$Builder; - public final fun setPaymentMethodTypes (Ljava/util/List;)Lcom/stripe/android/PaymentSessionConfig$Builder; - public final fun setPaymentMethodsFooter (I)Lcom/stripe/android/PaymentSessionConfig$Builder; - public final fun setPrepopulatedShippingInfo (Lcom/stripe/android/model/ShippingInformation;)Lcom/stripe/android/PaymentSessionConfig$Builder; - public final fun setShippingInfoRequired (Z)Lcom/stripe/android/PaymentSessionConfig$Builder; - public final fun setShippingInformationValidator (Lcom/stripe/android/PaymentSessionConfig$ShippingInformationValidator;)Lcom/stripe/android/PaymentSessionConfig$Builder; - public final fun setShippingMethodsFactory (Lcom/stripe/android/PaymentSessionConfig$ShippingMethodsFactory;)Lcom/stripe/android/PaymentSessionConfig$Builder; - public final fun setShippingMethodsRequired (Z)Lcom/stripe/android/PaymentSessionConfig$Builder; - public final fun setShouldPrefetchCustomer (Z)Lcom/stripe/android/PaymentSessionConfig$Builder; - public final fun setShouldShowGooglePay (Z)Lcom/stripe/android/PaymentSessionConfig$Builder; - public final fun setWindowFlags (Ljava/lang/Integer;)Lcom/stripe/android/PaymentSessionConfig$Builder; -} - -public final class com/stripe/android/PaymentSessionConfig$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/PaymentSessionConfig; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/stripe/android/PaymentSessionConfig; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - -public abstract interface class com/stripe/android/PaymentSessionConfig$ShippingInformationValidator : java/io/Serializable { - public abstract fun getErrorMessage (Lcom/stripe/android/model/ShippingInformation;)Ljava/lang/String; - public abstract fun isValid (Lcom/stripe/android/model/ShippingInformation;)Z -} - -public abstract interface class com/stripe/android/PaymentSessionConfig$ShippingMethodsFactory : java/io/Serializable { - public abstract fun create (Lcom/stripe/android/model/ShippingInformation;)Ljava/util/List; -} - -public final class com/stripe/android/PaymentSessionData : android/os/Parcelable { - public static final field $stable I - public static final field CREATOR Landroid/os/Parcelable$Creator; - public final fun component3 ()J - public final fun component4 ()J - public final fun component5 ()Lcom/stripe/android/model/ShippingInformation; - public final fun component6 ()Lcom/stripe/android/model/ShippingMethod; - public final fun component7 ()Lcom/stripe/android/model/PaymentMethod; - public final fun component8 ()Z - public final fun copy (ZZJJLcom/stripe/android/model/ShippingInformation;Lcom/stripe/android/model/ShippingMethod;Lcom/stripe/android/model/PaymentMethod;Z)Lcom/stripe/android/PaymentSessionData; - public static synthetic fun copy$default (Lcom/stripe/android/PaymentSessionData;ZZJJLcom/stripe/android/model/ShippingInformation;Lcom/stripe/android/model/ShippingMethod;Lcom/stripe/android/model/PaymentMethod;ZILjava/lang/Object;)Lcom/stripe/android/PaymentSessionData; - public fun describeContents ()I - public fun equals (Ljava/lang/Object;)Z - public final fun getCartTotal ()J - public final fun getPaymentMethod ()Lcom/stripe/android/model/PaymentMethod; - public final fun getShippingInformation ()Lcom/stripe/android/model/ShippingInformation; - public final fun getShippingMethod ()Lcom/stripe/android/model/ShippingMethod; - public final fun getShippingTotal ()J - public final fun getUseGooglePay ()Z - public fun hashCode ()I - public final fun isPaymentReadyToCharge ()Z - public fun toString ()Ljava/lang/String; - public fun writeToParcel (Landroid/os/Parcel;I)V -} - -public final class com/stripe/android/PaymentSessionData$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/PaymentSessionData; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/stripe/android/PaymentSessionData; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - public final class com/stripe/android/SetupIntentResult : com/stripe/android/StripeIntentResult { public static final field $stable I public static final field CREATOR Landroid/os/Parcelable$Creator; @@ -7325,146 +7143,6 @@ public final class com/stripe/android/payments/paymentlauncher/PaymentResult$Fai public synthetic fun newArray (I)[Ljava/lang/Object; } -public abstract class com/stripe/android/view/ActivityStarter { - public static final field $stable I - public final fun startForResult (Lcom/stripe/android/view/ActivityStarter$Args;)V -} - -public abstract interface class com/stripe/android/view/ActivityStarter$Args : android/os/Parcelable { - public static final field Companion Lcom/stripe/android/view/ActivityStarter$Args$Companion; -} - -public final class com/stripe/android/view/ActivityStarter$Args$Companion { -} - -public abstract interface class com/stripe/android/view/ActivityStarter$Result : android/os/Parcelable { - public static final field EXTRA Ljava/lang/String; - public abstract fun toBundle ()Landroid/os/Bundle; -} - -public final class com/stripe/android/view/AddPaymentMethodActivity : com/stripe/android/view/StripeActivity { - public static final field $stable I - public fun ()V - public fun onActionSave ()V - public fun onUserInteraction ()V -} - -public final class com/stripe/android/view/AddPaymentMethodActivityStarter : com/stripe/android/view/ActivityStarter { - public static final field $stable I - public static final field Companion Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Companion; - public static final field REQUEST_CODE I - public fun (Landroid/app/Activity;)V - public fun (Landroidx/fragment/app/Fragment;)V -} - -public final class com/stripe/android/view/AddPaymentMethodActivityStarter$Args : com/stripe/android/view/ActivityStarter$Args { - public static final field $stable I - public static final field CREATOR Landroid/os/Parcelable$Creator; - public final fun copy (Lcom/stripe/android/view/BillingAddressFields;ZZLcom/stripe/android/model/PaymentMethod$Type;Lcom/stripe/android/PaymentConfiguration;ILjava/lang/Integer;)Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Args; - public static synthetic fun copy$default (Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Args;Lcom/stripe/android/view/BillingAddressFields;ZZLcom/stripe/android/model/PaymentMethod$Type;Lcom/stripe/android/PaymentConfiguration;ILjava/lang/Integer;ILjava/lang/Object;)Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Args; - public fun describeContents ()I - public fun equals (Ljava/lang/Object;)Z - public fun hashCode ()I - public fun toString ()Ljava/lang/String; - public fun writeToParcel (Landroid/os/Parcel;I)V -} - -public final class com/stripe/android/view/AddPaymentMethodActivityStarter$Args$Builder { - public static final field $stable I - public fun ()V - public final fun build ()Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Args; - public final fun setAddPaymentMethodFooter (I)Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Args$Builder; - public final fun setBillingAddressFields (Lcom/stripe/android/view/BillingAddressFields;)Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Args$Builder; - public final fun setPaymentMethodType (Lcom/stripe/android/model/PaymentMethod$Type;)Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Args$Builder; - public final fun setShouldAttachToCustomer (Z)Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Args$Builder; - public final fun setWindowFlags (Ljava/lang/Integer;)Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Args$Builder; -} - -public final class com/stripe/android/view/AddPaymentMethodActivityStarter$Args$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Args; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Args; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - -public final class com/stripe/android/view/AddPaymentMethodActivityStarter$Companion { -} - -public abstract class com/stripe/android/view/AddPaymentMethodActivityStarter$Result : com/stripe/android/view/ActivityStarter$Result { - public static final field $stable I - public static final field Companion Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Result$Companion; - public static final fun fromIntent (Landroid/content/Intent;)Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Result; - public fun toBundle ()Landroid/os/Bundle; -} - -public final class com/stripe/android/view/AddPaymentMethodActivityStarter$Result$Canceled : com/stripe/android/view/AddPaymentMethodActivityStarter$Result { - public static final field $stable I - public static final field CREATOR Landroid/os/Parcelable$Creator; - public static final field INSTANCE Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Result$Canceled; - public fun describeContents ()I - public fun equals (Ljava/lang/Object;)Z - public fun hashCode ()I - public fun toString ()Ljava/lang/String; - public fun writeToParcel (Landroid/os/Parcel;I)V -} - -public final class com/stripe/android/view/AddPaymentMethodActivityStarter$Result$Canceled$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Result$Canceled; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Result$Canceled; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - -public final class com/stripe/android/view/AddPaymentMethodActivityStarter$Result$Companion { - public final fun fromIntent (Landroid/content/Intent;)Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Result; -} - -public final class com/stripe/android/view/AddPaymentMethodActivityStarter$Result$Failure : com/stripe/android/view/AddPaymentMethodActivityStarter$Result { - public static final field $stable I - public static final field CREATOR Landroid/os/Parcelable$Creator; - public final fun component1 ()Ljava/lang/Throwable; - public final fun copy (Ljava/lang/Throwable;)Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Result$Failure; - public static synthetic fun copy$default (Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Result$Failure;Ljava/lang/Throwable;ILjava/lang/Object;)Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Result$Failure; - public fun describeContents ()I - public fun equals (Ljava/lang/Object;)Z - public final fun getException ()Ljava/lang/Throwable; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; - public fun writeToParcel (Landroid/os/Parcel;I)V -} - -public final class com/stripe/android/view/AddPaymentMethodActivityStarter$Result$Failure$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Result$Failure; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Result$Failure; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - -public final class com/stripe/android/view/AddPaymentMethodActivityStarter$Result$Success : com/stripe/android/view/AddPaymentMethodActivityStarter$Result { - public static final field $stable I - public static final field CREATOR Landroid/os/Parcelable$Creator; - public final fun component1 ()Lcom/stripe/android/model/PaymentMethod; - public final fun copy (Lcom/stripe/android/model/PaymentMethod;)Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Result$Success; - public static synthetic fun copy$default (Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Result$Success;Lcom/stripe/android/model/PaymentMethod;ILjava/lang/Object;)Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Result$Success; - public fun describeContents ()I - public fun equals (Ljava/lang/Object;)Z - public final fun getPaymentMethod ()Lcom/stripe/android/model/PaymentMethod; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; - public fun writeToParcel (Landroid/os/Parcel;I)V -} - -public final class com/stripe/android/view/AddPaymentMethodActivityStarter$Result$Success$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Result$Success; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/stripe/android/view/AddPaymentMethodActivityStarter$Result$Success; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - public final class com/stripe/android/view/BecsDebitBanks$Bank : android/os/Parcelable { public static final field $stable I public static final field CREATOR Landroid/os/Parcelable$Creator; @@ -7522,15 +7200,6 @@ public abstract interface class com/stripe/android/view/BecsDebitWidget$ValidPar public abstract fun onInputChanged (Z)V } -public final class com/stripe/android/view/BillingAddressFields : java/lang/Enum { - public static final field Full Lcom/stripe/android/view/BillingAddressFields; - public static final field None Lcom/stripe/android/view/BillingAddressFields; - public static final field PostalCode Lcom/stripe/android/view/BillingAddressFields; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public static fun valueOf (Ljava/lang/String;)Lcom/stripe/android/view/BillingAddressFields; - public static fun values ()[Lcom/stripe/android/view/BillingAddressFields; -} - public final class com/stripe/android/view/CardBrandView$SavedState$Creator : android/os/Parcelable$Creator { public fun ()V public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/view/CardBrandView$SavedState; @@ -7728,158 +7397,6 @@ public final class com/stripe/android/view/PaymentAuthWebViewActivity : androidx public fun onOptionsItemSelected (Landroid/view/MenuItem;)Z } -public final class com/stripe/android/view/PaymentFlowActivity : com/stripe/android/view/StripeActivity { - public static final field $stable I - public fun ()V - public fun onActionSave ()V -} - -public final class com/stripe/android/view/PaymentFlowActivityStarter : com/stripe/android/view/ActivityStarter { - public static final field $stable I - public static final field Companion Lcom/stripe/android/view/PaymentFlowActivityStarter$Companion; - public static final field REQUEST_CODE I - public fun (Landroid/app/Activity;Lcom/stripe/android/PaymentSessionConfig;)V - public fun (Landroidx/fragment/app/Fragment;Lcom/stripe/android/PaymentSessionConfig;)V -} - -public final class com/stripe/android/view/PaymentFlowActivityStarter$Args : com/stripe/android/view/ActivityStarter$Args { - public static final field $stable I - public static final field CREATOR Landroid/os/Parcelable$Creator; - public static final field Companion Lcom/stripe/android/view/PaymentFlowActivityStarter$Args$Companion; - public final fun copy (Lcom/stripe/android/PaymentSessionConfig;Lcom/stripe/android/PaymentSessionData;ZLjava/lang/Integer;)Lcom/stripe/android/view/PaymentFlowActivityStarter$Args; - public static synthetic fun copy$default (Lcom/stripe/android/view/PaymentFlowActivityStarter$Args;Lcom/stripe/android/PaymentSessionConfig;Lcom/stripe/android/PaymentSessionData;ZLjava/lang/Integer;ILjava/lang/Object;)Lcom/stripe/android/view/PaymentFlowActivityStarter$Args; - public static final fun create (Landroid/content/Intent;)Lcom/stripe/android/view/PaymentFlowActivityStarter$Args; - public fun describeContents ()I - public fun equals (Ljava/lang/Object;)Z - public fun hashCode ()I - public fun toString ()Ljava/lang/String; - public fun writeToParcel (Landroid/os/Parcel;I)V -} - -public final class com/stripe/android/view/PaymentFlowActivityStarter$Args$Builder { - public static final field $stable I - public fun ()V - public final fun build ()Lcom/stripe/android/view/PaymentFlowActivityStarter$Args; - public final fun setIsPaymentSessionActive (Z)Lcom/stripe/android/view/PaymentFlowActivityStarter$Args$Builder; - public final fun setPaymentSessionConfig (Lcom/stripe/android/PaymentSessionConfig;)Lcom/stripe/android/view/PaymentFlowActivityStarter$Args$Builder; - public final fun setPaymentSessionData (Lcom/stripe/android/PaymentSessionData;)Lcom/stripe/android/view/PaymentFlowActivityStarter$Args$Builder; - public final fun setWindowFlags (Ljava/lang/Integer;)Lcom/stripe/android/view/PaymentFlowActivityStarter$Args$Builder; -} - -public final class com/stripe/android/view/PaymentFlowActivityStarter$Args$Companion { - public final fun create (Landroid/content/Intent;)Lcom/stripe/android/view/PaymentFlowActivityStarter$Args; -} - -public final class com/stripe/android/view/PaymentFlowActivityStarter$Args$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/view/PaymentFlowActivityStarter$Args; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/stripe/android/view/PaymentFlowActivityStarter$Args; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - -public final class com/stripe/android/view/PaymentFlowActivityStarter$Companion { -} - -public final class com/stripe/android/view/PaymentFlowViewPager : androidx/viewpager/widget/ViewPager { - public static final field $stable I - public fun (Landroid/content/Context;)V - public fun (Landroid/content/Context;Landroid/util/AttributeSet;)V - public fun (Landroid/content/Context;Landroid/util/AttributeSet;Z)V - public synthetic fun (Landroid/content/Context;Landroid/util/AttributeSet;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun onInterceptTouchEvent (Landroid/view/MotionEvent;)Z - public fun onTouchEvent (Landroid/view/MotionEvent;)Z -} - -public final class com/stripe/android/view/PaymentMethodsActivity : androidx/appcompat/app/AppCompatActivity { - public static final field $stable I - public fun ()V - public fun onSupportNavigateUp ()Z -} - -public final class com/stripe/android/view/PaymentMethodsActivityStarter : com/stripe/android/view/ActivityStarter { - public static final field $stable I - public static final field Companion Lcom/stripe/android/view/PaymentMethodsActivityStarter$Companion; - public static final field REQUEST_CODE I - public fun (Landroid/app/Activity;)V - public fun (Landroidx/fragment/app/Fragment;)V -} - -public final class com/stripe/android/view/PaymentMethodsActivityStarter$Args : com/stripe/android/view/ActivityStarter$Args { - public static final field $stable I - public static final field CREATOR Landroid/os/Parcelable$Creator; - public final fun component2 ()I - public final fun component3 ()I - public final fun copy (Ljava/lang/String;IIZLjava/util/List;Lcom/stripe/android/PaymentConfiguration;Ljava/lang/Integer;Lcom/stripe/android/view/BillingAddressFields;ZZZ)Lcom/stripe/android/view/PaymentMethodsActivityStarter$Args; - public static synthetic fun copy$default (Lcom/stripe/android/view/PaymentMethodsActivityStarter$Args;Ljava/lang/String;IIZLjava/util/List;Lcom/stripe/android/PaymentConfiguration;Ljava/lang/Integer;Lcom/stripe/android/view/BillingAddressFields;ZZZILjava/lang/Object;)Lcom/stripe/android/view/PaymentMethodsActivityStarter$Args; - public fun describeContents ()I - public fun equals (Ljava/lang/Object;)Z - public final fun getAddPaymentMethodFooterLayoutId ()I - public final fun getPaymentMethodsFooterLayoutId ()I - public fun hashCode ()I - public fun toString ()Ljava/lang/String; - public fun writeToParcel (Landroid/os/Parcel;I)V -} - -public final class com/stripe/android/view/PaymentMethodsActivityStarter$Args$Builder { - public static final field $stable I - public fun ()V - public final fun build ()Lcom/stripe/android/view/PaymentMethodsActivityStarter$Args; - public final fun setAddPaymentMethodFooter (I)Lcom/stripe/android/view/PaymentMethodsActivityStarter$Args$Builder; - public final fun setBillingAddressFields (Lcom/stripe/android/view/BillingAddressFields;)Lcom/stripe/android/view/PaymentMethodsActivityStarter$Args$Builder; - public final fun setCanDeletePaymentMethods (Z)Lcom/stripe/android/view/PaymentMethodsActivityStarter$Args$Builder; - public final fun setInitialPaymentMethodId (Ljava/lang/String;)Lcom/stripe/android/view/PaymentMethodsActivityStarter$Args$Builder; - public final fun setIsPaymentSessionActive (Z)Lcom/stripe/android/view/PaymentMethodsActivityStarter$Args$Builder; - public final fun setPaymentConfiguration (Lcom/stripe/android/PaymentConfiguration;)Lcom/stripe/android/view/PaymentMethodsActivityStarter$Args$Builder; - public final fun setPaymentMethodTypes (Ljava/util/List;)Lcom/stripe/android/view/PaymentMethodsActivityStarter$Args$Builder; - public final fun setPaymentMethodsFooter (I)Lcom/stripe/android/view/PaymentMethodsActivityStarter$Args$Builder; - public final fun setShouldShowGooglePay (Z)Lcom/stripe/android/view/PaymentMethodsActivityStarter$Args$Builder; - public final fun setWindowFlags (Ljava/lang/Integer;)Lcom/stripe/android/view/PaymentMethodsActivityStarter$Args$Builder; -} - -public final class com/stripe/android/view/PaymentMethodsActivityStarter$Args$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/view/PaymentMethodsActivityStarter$Args; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/stripe/android/view/PaymentMethodsActivityStarter$Args; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - -public final class com/stripe/android/view/PaymentMethodsActivityStarter$Companion { -} - -public final class com/stripe/android/view/PaymentMethodsActivityStarter$Result : com/stripe/android/view/ActivityStarter$Result { - public static final field $stable I - public static final field CREATOR Landroid/os/Parcelable$Creator; - public static final field Companion Lcom/stripe/android/view/PaymentMethodsActivityStarter$Result$Companion; - public final field paymentMethod Lcom/stripe/android/model/PaymentMethod; - public fun ()V - public final fun component1 ()Lcom/stripe/android/model/PaymentMethod; - public final fun component2 ()Z - public final fun copy (Lcom/stripe/android/model/PaymentMethod;Z)Lcom/stripe/android/view/PaymentMethodsActivityStarter$Result; - public static synthetic fun copy$default (Lcom/stripe/android/view/PaymentMethodsActivityStarter$Result;Lcom/stripe/android/model/PaymentMethod;ZILjava/lang/Object;)Lcom/stripe/android/view/PaymentMethodsActivityStarter$Result; - public fun describeContents ()I - public fun equals (Ljava/lang/Object;)Z - public static final fun fromIntent (Landroid/content/Intent;)Lcom/stripe/android/view/PaymentMethodsActivityStarter$Result; - public final fun getUseGooglePay ()Z - public fun hashCode ()I - public fun toBundle ()Landroid/os/Bundle; - public fun toString ()Ljava/lang/String; - public fun writeToParcel (Landroid/os/Parcel;I)V -} - -public final class com/stripe/android/view/PaymentMethodsActivityStarter$Result$Companion { - public final fun fromIntent (Landroid/content/Intent;)Lcom/stripe/android/view/PaymentMethodsActivityStarter$Result; -} - -public final class com/stripe/android/view/PaymentMethodsActivityStarter$Result$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/view/PaymentMethodsActivityStarter$Result; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/stripe/android/view/PaymentMethodsActivityStarter$Result; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - public final class com/stripe/android/view/PaymentUtils { public static final field $stable I public static final field INSTANCE Lcom/stripe/android/view/PaymentUtils; diff --git a/payments-core/res/layout/stripe_payment_flow_activity.xml b/payments-core/res/layout/stripe_payment_flow_activity.xml deleted file mode 100644 index 8c84b5274e0..00000000000 --- a/payments-core/res/layout/stripe_payment_flow_activity.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/payments-core/res/layout/stripe_payment_methods_activity.xml b/payments-core/res/layout/stripe_payment_methods_activity.xml deleted file mode 100644 index cb2dcb1a9e5..00000000000 --- a/payments-core/res/layout/stripe_payment_methods_activity.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/payments-core/src/androidTest/java/com/stripe/android/PaymentSessionFixtures.kt b/payments-core/src/androidTest/java/com/stripe/android/PaymentSessionFixtures.kt deleted file mode 100644 index 64ccc19878f..00000000000 --- a/payments-core/src/androidTest/java/com/stripe/android/PaymentSessionFixtures.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.stripe.android - -import com.stripe.android.model.Address -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.ShippingInformation -import com.stripe.android.model.ShippingMethod -import com.stripe.android.view.BillingAddressFields -import com.stripe.android.view.PaymentFlowActivityStarter -import com.stripe.android.view.ShippingInfoWidget - -internal object PaymentSessionFixtures { - internal val CONFIG = PaymentSessionConfig.Builder() - // hide the phone field on the shipping information form - .setHiddenShippingInfoFields( - ShippingInfoWidget.CustomizableShippingField.Line2 - ) - // make the address line 2 field optional - .setOptionalShippingInfoFields( - ShippingInfoWidget.CustomizableShippingField.Phone - ) - // specify an address to pre-populate the shipping information form - .setPrepopulatedShippingInfo( - ShippingInformation( - Address.Builder() - .setLine1("123 Market St") - .setCity("San Francisco") - .setState("CA") - .setPostalCode("94107") - .setCountry("US") - .build(), - "Jenny Rosen", - "4158675309" - ) - ) - // collect shipping information - .setShippingInfoRequired(true) - // collect shipping method - .setShippingMethodsRequired(true) - // specify the payment method types that the customer can use; - // defaults to PaymentMethod.Type.Card - .setPaymentMethodTypes( - listOf(PaymentMethod.Type.Card) - ) - // only allowed US and Canada shipping addresses - .setAllowedShippingCountryCodes( - setOf("US", "CA") - ) - .setBillingAddressFields(BillingAddressFields.Full) - .setShouldPrefetchCustomer(true) - // Enable PaymentMethod Deletion from PaymentMethodActivity - // This is default behavior - .setCanDeletePaymentMethods(true) - .setShippingInformationValidator(FakeShippingInformationValidator()) - .setShippingMethodsFactory(FakeShippingMethodsFactory()) - .build() - - internal val PAYMENT_SESSION_DATA = PaymentSessionData(CONFIG) - - internal val PAYMENT_FLOW_ARGS = PaymentFlowActivityStarter.Args( - paymentSessionConfig = CONFIG, - paymentSessionData = PAYMENT_SESSION_DATA - ) - - private class FakeShippingInformationValidator : PaymentSessionConfig.ShippingInformationValidator { - override fun isValid(shippingInformation: ShippingInformation): Boolean { - return true - } - - override fun getErrorMessage(shippingInformation: ShippingInformation): String { - return "" - } - } - - private class FakeShippingMethodsFactory : PaymentSessionConfig.ShippingMethodsFactory { - override fun create(shippingInformation: ShippingInformation): List { - return listOf( - ShippingMethod( - "UPS Ground", - "ups-ground", - 0, - "USD", - "Arrives in 3-5 days" - ), - ShippingMethod( - "FedEx", - "fedex", - 599, - "USD", - "Arrives tomorrow" - ) - ) - } - } -} diff --git a/payments-core/src/main/java/com/stripe/android/BasicIntegrationDeprecationWarning.kt b/payments-core/src/main/java/com/stripe/android/BasicIntegrationDeprecationWarning.kt deleted file mode 100644 index 5883e506041..00000000000 --- a/payments-core/src/main/java/com/stripe/android/BasicIntegrationDeprecationWarning.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.stripe.android - -internal const val BASIC_INTEGRATION_DEPRECATION_WARNING = "Please use Mobile Payment Element instead. If you " + - "are already using Basic Integration, learn how to migrate here:" + - " https://docs.stripe.com/payments/mobile/migrating-to-mobile-payment-element-from-basic-integration" diff --git a/payments-core/src/main/java/com/stripe/android/CustomerSession.kt b/payments-core/src/main/java/com/stripe/android/CustomerSession.kt deleted file mode 100644 index 31205fc9c02..00000000000 --- a/payments-core/src/main/java/com/stripe/android/CustomerSession.kt +++ /dev/null @@ -1,576 +0,0 @@ -package com.stripe.android - -import android.content.Context -import androidx.annotation.IntRange -import androidx.annotation.VisibleForTesting -import com.stripe.android.Stripe.Companion.appInfo -import com.stripe.android.core.StripeError -import com.stripe.android.model.Customer -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.ShippingInformation -import com.stripe.android.model.Source -import com.stripe.android.model.Source.SourceType -import com.stripe.android.networking.StripeApiRepository -import com.stripe.android.networking.StripeRepository -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.launch -import java.util.Calendar -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.ThreadPoolExecutor -import java.util.concurrent.TimeUnit -import kotlin.coroutines.CoroutineContext - -/** - * Represents a logged-in session of a single Customer. - * - * See [Creating ephemeral keys](https://stripe.com/docs/mobile/android/standard#creating-ephemeral-keys) - */ -@Deprecated(BASIC_INTEGRATION_DEPRECATION_WARNING) -class CustomerSession @VisibleForTesting internal constructor( - stripeRepository: StripeRepository, - publishableKey: String, - stripeAccountId: String?, - private val workContext: CoroutineContext = createCoroutineDispatcher(), - private val operationIdFactory: OperationIdFactory = StripeOperationIdFactory(), - private val timeSupplier: TimeSupplier = { Calendar.getInstance().timeInMillis }, - ephemeralKeyManagerFactory: EphemeralKeyManager.Factory -) { - @JvmSynthetic - internal var customerCacheTime: Long = 0 - - @JvmSynthetic - internal var customer: Customer? = null - - private val listeners: MutableMap = mutableMapOf() - private val operationExecutor = CustomerSessionOperationExecutor( - stripeRepository, - publishableKey, - stripeAccountId, - listeners - ) { customer -> - this.customer = customer - customerCacheTime = timeSupplier() - } - - private val ephemeralKeyManager: EphemeralKeyManager = ephemeralKeyManagerFactory.create( - object : EphemeralKeyManager.KeyManagerListener { - override fun onKeyUpdate(ephemeralKey: EphemeralKey, operation: EphemeralOperation) { - CoroutineScope(workContext).launch { - operationExecutor.execute(ephemeralKey, operation) - } - } - - override fun onKeyError( - operationId: String, - errorCode: Int, - errorMessage: String, - throwable: Throwable - ) { - when (val listener = listeners.remove(operationId)) { - is RetrievalWithExceptionListener -> listener.onError( - errorCode, - errorMessage, - null, - throwable - ) - is RetrievalListener -> listener.onError( - errorCode, - errorMessage, - null - ) - else -> Unit - } - } - } - ) - - /** - * Retrieve the current [Customer]. If [customer] is not stale, this returns immediately with - * the cache. If not, it fetches a new value and returns that to the listener. - * - * @param listener a [CustomerRetrievalListener] to invoke with the result of getting the - * customer, either from the cache or from the server - */ - fun retrieveCurrentCustomer(listener: CustomerRetrievalListener) { - retrieveCurrentCustomer(emptySet(), listener) - } - - @JvmSynthetic - internal fun retrieveCurrentCustomer( - productUsage: Set, - listener: CustomerRetrievalListener - ) { - cachedCustomer?.let { - listener.onCustomerRetrieved(it) - } ?: updateCurrentCustomer(productUsage, listener) - } - - /** - * Force an update of the current customer, regardless of how much time has passed. - * - * @param listener a [CustomerRetrievalListener] to invoke with the result of getting - * the customer from the server - */ - fun updateCurrentCustomer(listener: CustomerRetrievalListener) { - updateCurrentCustomer(emptySet(), listener) - } - - @JvmSynthetic - internal fun updateCurrentCustomer( - productUsage: Set, - listener: CustomerRetrievalListener - ) { - customer = null - startOperation( - EphemeralOperation.RetrieveKey( - id = operationIdFactory.create(), - productUsage = productUsage - ), - listener - ) - } - - /** - * A cached [Customer], or `null` if the current customer has expired. - */ - val cachedCustomer: Customer? - get() { - return customer.takeIf { canUseCachedCustomer } - } - - /** - * Add the Source to the current customer. - * - * @param sourceId the ID of the source to be added - * @param listener a [SourceRetrievalListener] called when the API call completes - * with the added [Source]. - */ - fun addCustomerSource( - sourceId: String, - @SourceType sourceType: String, - listener: SourceRetrievalListener - ) { - addCustomerSource(sourceId, sourceType, emptySet(), listener) - } - - @JvmSynthetic - internal fun addCustomerSource( - sourceId: String, - @SourceType sourceType: String, - productUsage: Set, - listener: SourceRetrievalListener - ) { - startOperation( - EphemeralOperation.Customer.AddSource( - sourceId = sourceId, - sourceType = sourceType, - id = operationIdFactory.create(), - productUsage = productUsage - ), - listener - ) - } - - /** - * Delete the Source from the current customer. - * - * @param sourceId the ID of the source to be deleted - * @param listener a [SourceRetrievalListener] called when the API call completes - * with the added [Source]. - */ - fun deleteCustomerSource( - sourceId: String, - listener: SourceRetrievalListener - ) { - deleteCustomerSource(sourceId, emptySet(), listener) - } - - @JvmSynthetic - internal fun deleteCustomerSource( - sourceId: String, - productUsage: Set, - listener: SourceRetrievalListener - ) { - startOperation( - EphemeralOperation.Customer.DeleteSource( - sourceId = sourceId, - id = operationIdFactory.create(), - productUsage = productUsage - ), - listener - ) - } - - /** - * Attaches a PaymentMethod to a customer. - * - * @param paymentMethodId the ID of the payment method to be attached - * @param listener a [PaymentMethodRetrievalListener] called when the API call - * completes with the attached [PaymentMethod]. - */ - fun attachPaymentMethod( - paymentMethodId: String, - listener: PaymentMethodRetrievalListener - ) { - attachPaymentMethod(paymentMethodId, emptySet(), listener) - } - - @JvmSynthetic - internal fun attachPaymentMethod( - paymentMethodId: String, - productUsage: Set, - listener: PaymentMethodRetrievalListener - ) { - startOperation( - EphemeralOperation.Customer.AttachPaymentMethod( - paymentMethodId = paymentMethodId, - id = operationIdFactory.create(), - productUsage = productUsage - ), - listener - ) - } - - /** - * Detaches a PaymentMethod from a customer. - * - * @param paymentMethodId the ID of the payment method to be detached - * @param listener a [PaymentMethodRetrievalListener] called when the API call - * completes with the detached [PaymentMethod]. - */ - fun detachPaymentMethod( - paymentMethodId: String, - listener: PaymentMethodRetrievalListener - ) { - detachPaymentMethod(paymentMethodId, emptySet(), listener) - } - - @JvmSynthetic - internal fun detachPaymentMethod( - paymentMethodId: String, - productUsage: Set, - listener: PaymentMethodRetrievalListener - ) { - startOperation( - EphemeralOperation.Customer.DetachPaymentMethod( - paymentMethodId = paymentMethodId, - id = operationIdFactory.create(), - productUsage = productUsage - ), - listener - ) - } - - /** - * Retrieves all of the customer's PaymentMethod objects, filtered by a [PaymentMethod.Type]. - * - * See [List a Customer's PaymentMethods](https://stripe.com/docs/api/payment_methods/list) - * - * @param paymentMethodType the [PaymentMethod.Type] to filter by - * @param listener a [PaymentMethodRetrievalListener] called when the API call - * completes with a list of [PaymentMethod] objects - * - * @param limit Optional. A limit on the number of objects to be returned. Limit can range - * between 1 and 100, and the default is 10. - * @param endingBefore Optional. A cursor for use in pagination. `ending_before` is an object - * ID that defines your place in the list. For instance, if you make a list request and receive - * 100 objects, starting with `obj_bar`, your subsequent call can include - * `ending_before=obj_bar` in order to fetch the previous page of the list. - * @param startingAfter Optional. A cursor for use in pagination. `starting_after` is an object - * ID that defines your place in the list. For instance, if you make a list request and receive - * 100 objects, ending with `obj_foo`, your subsequent call can include `starting_after=obj_foo` - * in order to fetch the next page of the list. - */ - @JvmOverloads - fun getPaymentMethods( - paymentMethodType: PaymentMethod.Type, - @IntRange(from = 1, to = 100) limit: Int?, - endingBefore: String? = null, - startingAfter: String? = null, - listener: PaymentMethodsRetrievalListener - ) { - getPaymentMethods( - paymentMethodType = paymentMethodType, - limit = limit, - endingBefore = endingBefore, - startingAfter = startingAfter, - productUsage = emptySet(), - listener = listener - ) - } - - @JvmSynthetic - internal fun getPaymentMethods( - paymentMethodType: PaymentMethod.Type, - @IntRange(from = 1, to = 100) limit: Int? = null, - endingBefore: String? = null, - startingAfter: String? = null, - productUsage: Set, - listener: PaymentMethodsRetrievalListener - ) { - startOperation( - EphemeralOperation.Customer.GetPaymentMethods( - type = paymentMethodType, - limit = limit, - endingBefore = endingBefore, - startingAfter = startingAfter, - id = operationIdFactory.create(), - productUsage = productUsage - ), - listener - ) - } - - fun getPaymentMethods( - paymentMethodType: PaymentMethod.Type, - listener: PaymentMethodsRetrievalListener - ) { - getPaymentMethods( - paymentMethodType = paymentMethodType, - productUsage = emptySet(), - listener = listener - ) - } - - /** - * Set the shipping information on the current customer. - * - * @param shippingInformation the data to be set - */ - fun setCustomerShippingInformation( - shippingInformation: ShippingInformation, - listener: CustomerRetrievalListener - ) { - setCustomerShippingInformation(shippingInformation, emptySet(), listener) - } - - @JvmSynthetic - internal fun setCustomerShippingInformation( - shippingInformation: ShippingInformation, - productUsage: Set, - listener: CustomerRetrievalListener - ) { - startOperation( - EphemeralOperation.Customer.UpdateShipping( - shippingInformation = shippingInformation, - id = operationIdFactory.create(), - productUsage = productUsage - ), - listener - ) - } - - /** - * Set the default Source of the current customer. - * - * @param sourceId the ID of the source to be set - * @param listener a [CustomerRetrievalListener] called when the API call - * completes with the updated customer - */ - fun setCustomerDefaultSource( - sourceId: String, - @SourceType sourceType: String, - listener: CustomerRetrievalListener - ) { - setCustomerDefaultSource(sourceId, sourceType, emptySet(), listener) - } - - @JvmSynthetic - internal fun setCustomerDefaultSource( - sourceId: String, - @SourceType sourceType: String, - productUsage: Set, - listener: CustomerRetrievalListener - ) { - startOperation( - EphemeralOperation.Customer.UpdateDefaultSource( - sourceId = sourceId, - sourceType = sourceType, - id = operationIdFactory.create(), - productUsage = productUsage - ), - listener - ) - } - - private fun startOperation( - operation: EphemeralOperation, - listener: RetrievalListener? - ) { - listeners[operation.id] = listener - ephemeralKeyManager.retrieveEphemeralKey(operation) - } - - private val canUseCachedCustomer: Boolean - get() { - return customer != null && - timeSupplier() - customerCacheTime < CUSTOMER_CACHE_DURATION_MILLISECONDS - } - - @JvmSynthetic - internal fun cancel() { - listeners.clear() - workContext.cancelChildren() - } - - private fun getListener(operationId: String): L? { - return listeners.remove(operationId) as L? - } - - interface CustomerRetrievalListener : RetrievalListener { - fun onCustomerRetrieved(customer: Customer) - } - - interface SourceRetrievalListener : RetrievalListener { - fun onSourceRetrieved(source: Source) - } - - interface PaymentMethodRetrievalListener : RetrievalListener { - fun onPaymentMethodRetrieved(paymentMethod: PaymentMethod) - } - - interface PaymentMethodsRetrievalListener : RetrievalListener { - fun onPaymentMethodsRetrieved(paymentMethods: List) - } - - internal interface PaymentMethodsRetrievalWithExceptionListener : - PaymentMethodsRetrievalListener, - RetrievalWithExceptionListener - - interface RetrievalListener { - fun onError( - errorCode: Int, - errorMessage: String, - stripeError: StripeError? - ) - } - - internal interface RetrievalWithExceptionListener : RetrievalListener { - fun onError( - errorCode: Int, - errorMessage: String, - stripeError: StripeError?, - throwable: Throwable - ) - - override fun onError(errorCode: Int, errorMessage: String, stripeError: StripeError?) { - onError(errorCode, errorMessage, stripeError, Exception(errorMessage)) - } - } - - companion object { - // The maximum number of active threads we support - private const val THREAD_POOL_SIZE = 3 - - // Sets the amount of time an idle thread waits before terminating - private const val KEEP_ALIVE_TIME = 2 - - // Sets the Time Unit to seconds - private val KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS - - private val CUSTOMER_CACHE_DURATION_MILLISECONDS = TimeUnit.MINUTES.toMillis(1) - - /** - * Create a CustomerSession with the provided [EphemeralKeyProvider]. - * - * You must call [PaymentConfiguration.init] with your publishable key - * before calling this method. - * - * @param context The application context - * @param ephemeralKeyProvider An [EphemeralKeyProvider] used to retrieve - * [EphemeralKey] ephemeral keys - * @param shouldPrefetchEphemeralKey If true, will immediately fetch an ephemeral key using - * {@param ephemeralKeyProvider}. Otherwise, will only fetch - * an ephemeral key when needed. - */ - @JvmStatic - @JvmOverloads - fun initCustomerSession( - context: Context, - ephemeralKeyProvider: EphemeralKeyProvider, - shouldPrefetchEphemeralKey: Boolean = true - ) { - val operationIdFactory = StripeOperationIdFactory() - val timeSupplier = { Calendar.getInstance().timeInMillis } - val ephemeralKeyManagerFactory = EphemeralKeyManager.Factory.Default( - keyProvider = ephemeralKeyProvider, - shouldPrefetchEphemeralKey = shouldPrefetchEphemeralKey, - operationIdFactory = operationIdFactory, - timeSupplier = timeSupplier - ) - - val config = PaymentConfiguration.getInstance(context) - - instance = CustomerSession( - StripeApiRepository(context, { config.publishableKey }, appInfo), - config.publishableKey, - config.stripeAccountId, - createCoroutineDispatcher(), - operationIdFactory, - timeSupplier, - ephemeralKeyManagerFactory - ) - } - - @JvmSynthetic - internal var instance: CustomerSession? = null - - /** - * Gets the singleton instance of [CustomerSession]. If the session has not been - * initialized, this will throw a [RuntimeException]. - * - * @return the singleton [CustomerSession] instance. - */ - @JvmStatic - fun getInstance(): CustomerSession { - return checkNotNull(instance) { - "Attempted to get instance of CustomerSession without initialization." - } - } - - /** - * End the singleton instance of a [CustomerSession]. - * Calls to [getInstance] will throw an [IllegalStateException] - * after this call, until the user calls - * [initCustomerSession] again. - */ - @JvmStatic - fun endCustomerSession() { - clearInstance() - } - - @VisibleForTesting - @JvmSynthetic - internal fun clearInstance() { - cancelCallbacks() - instance = null - } - - /** - * Cancel any in-flight [CustomerSession] operations. - * Their callback listeners will not be called. - * - * It will not clear the singleton [CustomerSession] instance. - * - * It is not necessary to call [initCustomerSession] after calling [cancelCallbacks]. - */ - @JvmStatic - fun cancelCallbacks() { - instance?.cancel() - } - - private fun createCoroutineDispatcher(): CoroutineContext { - return ThreadPoolExecutor( - THREAD_POOL_SIZE, - THREAD_POOL_SIZE, - KEEP_ALIVE_TIME.toLong(), - KEEP_ALIVE_TIME_UNIT, - LinkedBlockingQueue() - ).asCoroutineDispatcher() - } - } -} - -internal typealias TimeSupplier = () -> Long diff --git a/payments-core/src/main/java/com/stripe/android/CustomerSessionOperationExecutor.kt b/payments-core/src/main/java/com/stripe/android/CustomerSessionOperationExecutor.kt deleted file mode 100644 index 3114bd7c555..00000000000 --- a/payments-core/src/main/java/com/stripe/android/CustomerSessionOperationExecutor.kt +++ /dev/null @@ -1,238 +0,0 @@ -package com.stripe.android - -import com.stripe.android.core.exception.StripeException -import com.stripe.android.core.networking.ApiRequest -import com.stripe.android.model.Customer -import com.stripe.android.model.ListPaymentMethodsParams -import com.stripe.android.networking.StripeRepository -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -internal class CustomerSessionOperationExecutor( - private val stripeRepository: StripeRepository, - private val publishableKey: String, - private val stripeAccountId: String?, - private val listeners: MutableMap, - private val onCustomerUpdated: (Customer) -> Unit -) { - @JvmSynthetic - internal suspend fun execute( - ephemeralKey: EphemeralKey, - operation: EphemeralOperation - ) { - when (operation) { - is EphemeralOperation.RetrieveKey -> { - val result = retrieveCustomerWithKey(ephemeralKey, operation.productUsage) - withContext(Dispatchers.Main) { - onCustomerRetrieved(operation, result) - } - } - is EphemeralOperation.Customer.AddSource -> { - val result = stripeRepository.addCustomerSource( - customerId = ephemeralKey.objectId, - publishableKey = publishableKey, - productUsageTokens = operation.productUsage, - sourceId = operation.sourceId, - sourceType = operation.sourceType, - requestOptions = ApiRequest.Options(ephemeralKey.secret, stripeAccountId), - ) - - withContext(Dispatchers.Main) { - val listener: CustomerSession.SourceRetrievalListener? = getListener(operation.id) - result.fold( - onSuccess = { source -> - listener?.onSourceRetrieved(source) - }, - onFailure = { - onError(listener, it) - } - ) - } - } - is EphemeralOperation.Customer.DeleteSource -> { - val result = stripeRepository.deleteCustomerSource( - customerId = ephemeralKey.objectId, - publishableKey = publishableKey, - productUsageTokens = operation.productUsage, - sourceId = operation.sourceId, - requestOptions = ApiRequest.Options(ephemeralKey.secret, stripeAccountId), - ) - - withContext(Dispatchers.Main) { - val listener: CustomerSession.SourceRetrievalListener? = getListener(operation.id) - result.fold( - onSuccess = { source -> - listener?.onSourceRetrieved(source) - }, - onFailure = { - onError(listener, it) - } - ) - } - } - is EphemeralOperation.Customer.AttachPaymentMethod -> { - val result = stripeRepository.attachPaymentMethod( - customerId = ephemeralKey.objectId, - productUsageTokens = operation.productUsage, - paymentMethodId = operation.paymentMethodId, - requestOptions = ApiRequest.Options(ephemeralKey.secret, stripeAccountId), - ) - - withContext(Dispatchers.Main) { - val listener: CustomerSession.PaymentMethodRetrievalListener? = getListener(operation.id) - result.fold( - onSuccess = { paymentMethod -> - listener?.onPaymentMethodRetrieved(paymentMethod) - }, - onFailure = { - onError(listener, it) - } - ) - } - } - is EphemeralOperation.Customer.DetachPaymentMethod -> { - val result = stripeRepository.detachPaymentMethod( - productUsageTokens = operation.productUsage, - paymentMethodId = operation.paymentMethodId, - requestOptions = ApiRequest.Options(ephemeralKey.secret, stripeAccountId), - ) - - withContext(Dispatchers.Main) { - val listener: CustomerSession.PaymentMethodRetrievalListener? = getListener(operation.id) - result.fold( - onSuccess = { paymentMethod -> - listener?.onPaymentMethodRetrieved(paymentMethod) - }, - onFailure = { - onError(listener, it) - } - ) - } - } - is EphemeralOperation.Customer.GetPaymentMethods -> { - val result = stripeRepository.getPaymentMethods( - listPaymentMethodsParams = ListPaymentMethodsParams( - customerId = ephemeralKey.objectId, - paymentMethodType = operation.type, - limit = operation.limit, - endingBefore = operation.endingBefore, - startingAfter = operation.startingAfter, - ), - productUsageTokens = operation.productUsage, - requestOptions = ApiRequest.Options(ephemeralKey.secret, stripeAccountId), - ) - - withContext(Dispatchers.Main) { - val listener = getListener(operation.id) - - result.fold( - onSuccess = { paymentMethods -> - listener?.onPaymentMethodsRetrieved(paymentMethods) - }, - onFailure = { - onError(listener, it) - } - ) - } - } - is EphemeralOperation.Customer.UpdateDefaultSource -> { - val result = stripeRepository.setDefaultCustomerSource( - customerId = ephemeralKey.objectId, - publishableKey = publishableKey, - productUsageTokens = operation.productUsage, - sourceId = operation.sourceId, - sourceType = operation.sourceType, - requestOptions = ApiRequest.Options(ephemeralKey.secret, stripeAccountId), - ) - - withContext(Dispatchers.Main) { - onCustomerRetrieved(operation, result) - } - } - is EphemeralOperation.Customer.UpdateShipping -> { - val result = stripeRepository.setCustomerShippingInfo( - customerId = ephemeralKey.objectId, - publishableKey = publishableKey, - productUsageTokens = operation.productUsage, - shippingInformation = operation.shippingInformation, - requestOptions = ApiRequest.Options(ephemeralKey.secret, stripeAccountId), - ) - - withContext(Dispatchers.Main) { - onCustomerRetrieved(operation, result) - } - } - else -> {} - } - } - - private fun onCustomerRetrieved( - operation: EphemeralOperation, - result: Result - ) { - val listener: CustomerSession.CustomerRetrievalListener? = getListener(operation.id) - result.fold( - onSuccess = { customer -> - onCustomerUpdated(customer) - listener?.onCustomerRetrieved(customer) - }, - onFailure = { - onError(listener, it) - } - ) - } - - private fun onError( - listener: CustomerSession.RetrievalListener?, - error: Throwable - ) { - val (statusCode, message, stripeError) = when (error) { - is StripeException -> Triple( - error.statusCode, - error.message.orEmpty(), - error.stripeError - ) - else -> Triple( - 0, - error.message.orEmpty(), - null - ) - } - - when (listener) { - is CustomerSession.RetrievalWithExceptionListener -> listener.onError( - statusCode, - message, - stripeError, - error, - ) - is CustomerSession.RetrievalListener -> listener.onError( - statusCode, - message, - stripeError, - ) - } - } - - private fun getListener(operationId: String): L? { - return listeners.remove(operationId) as L? - } - - /** - * Fetch a [Customer]. If the provided key is expired, this method **does not** update the key. - * Use [createUpdateCustomer] to validate the key before refreshing the customer. - * - * @param key the [EphemeralKey] used for this access - * @return a [Customer] if one can be found with this key, or `null` if one cannot. - */ - private suspend fun retrieveCustomerWithKey( - key: EphemeralKey, - productUsage: Set - ): Result { - return stripeRepository.retrieveCustomer( - key.objectId, - productUsage, - ApiRequest.Options(key.secret, stripeAccountId) - ) - } -} diff --git a/payments-core/src/main/java/com/stripe/android/EphemeralKeyManager.kt b/payments-core/src/main/java/com/stripe/android/EphemeralKeyManager.kt index 72504ec46dc..220607e978d 100644 --- a/payments-core/src/main/java/com/stripe/android/EphemeralKeyManager.kt +++ b/payments-core/src/main/java/com/stripe/android/EphemeralKeyManager.kt @@ -166,3 +166,5 @@ internal class EphemeralKeyManager( private const val REFRESH_BUFFER_IN_SECONDS = 30L } } + +internal typealias TimeSupplier = () -> Long diff --git a/payments-core/src/main/java/com/stripe/android/PaymentSession.kt b/payments-core/src/main/java/com/stripe/android/PaymentSession.kt deleted file mode 100644 index cbf98cfada4..00000000000 --- a/payments-core/src/main/java/com/stripe/android/PaymentSession.kt +++ /dev/null @@ -1,337 +0,0 @@ -package com.stripe.android - -import android.app.Activity -import android.content.Context -import android.content.Intent -import androidx.activity.ComponentActivity -import androidx.annotation.IntRange -import androidx.annotation.VisibleForTesting -import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.OnLifecycleEvent -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelStoreOwner -import androidx.lifecycle.lifecycleScope -import com.stripe.android.PaymentSession.PaymentSessionListener -import com.stripe.android.view.ActivityStarter -import com.stripe.android.view.PaymentFlowActivity -import com.stripe.android.view.PaymentFlowActivityStarter -import com.stripe.android.view.PaymentMethodsActivity -import com.stripe.android.view.PaymentMethodsActivityStarter -import kotlinx.coroutines.launch - -/** - * Represents a single start-to-finish payment operation. - * - * See [Using Android basic integration](https://stripe.com/docs/mobile/android/basic) for more - * information. - * - * If [PaymentSessionConfig.shouldPrefetchCustomer] is `true`, and the customer has previously - * selected a payment method, [PaymentSessionData.paymentMethod] will be updated with the - * payment method and [PaymentSessionListener.onPaymentSessionDataChanged] will be called. - */ -@Deprecated(BASIC_INTEGRATION_DEPRECATION_WARNING) -class PaymentSession @VisibleForTesting internal constructor( - private val context: Context, - viewModelStoreOwner: ViewModelStoreOwner, - private val lifecycleOwner: LifecycleOwner, - private val config: PaymentSessionConfig, - customerSession: CustomerSession, - private val paymentMethodsActivityStarter: - ActivityStarter, - private val paymentFlowActivityStarter: - ActivityStarter, - paymentSessionData: PaymentSessionData = PaymentSessionData(config) -) { - internal val viewModel: PaymentSessionViewModel = - ViewModelProvider( - viewModelStoreOwner, - PaymentSessionViewModel.Factory( - paymentSessionData, - customerSession - ) - )[PaymentSessionViewModel::class.java] - - @JvmSynthetic - internal var listener: PaymentSessionListener? = null - - private val lifecycleObserver = object : LifecycleObserver { - @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) - fun onDestroy() { - listener = null - } - } - - init { - lifecycleOwner.lifecycle.addObserver(lifecycleObserver) - - lifecycleOwner.lifecycleScope.launch { - viewModel.networkState.collect { networkState -> - listener?.onCommunicatingStateChanged( - when (networkState) { - PaymentSessionViewModel.NetworkState.Active -> true - PaymentSessionViewModel.NetworkState.Inactive -> false - } - ) - } - } - - lifecycleOwner.lifecycleScope.launch { - viewModel.paymentSessionDataStateFlow.collect { sessionData -> - listener?.onPaymentSessionDataChanged(sessionData) - } - } - } - - /** - * Create a PaymentSession attached to the given host Activity. - * - * @param activity a `ComponentActivity` from which to launch other Stripe Activities. This - * Activity will receive results in - * `Activity#onActivityResult(int, int, Intent)` that should be - * passed back to this session. - * @param config a [PaymentSessionConfig] that configures this [PaymentSession] instance - */ - constructor(activity: ComponentActivity, config: PaymentSessionConfig) : this( - activity.applicationContext, - activity, - activity, - config, - CustomerSession.getInstance(), - PaymentMethodsActivityStarter(activity), - PaymentFlowActivityStarter(activity, config) - ) - - /** - * Create a PaymentSession attached to the given host Fragment. - * - * @param fragment a `Fragment` from which to launch other Stripe Activities. This - * Fragment will receive results in `Fragment#onActivityResult(int, int, Intent)` that should be - * passed back to this session. - * @param config a [PaymentSessionConfig] that configures this [PaymentSession] instance - */ - constructor(fragment: Fragment, config: PaymentSessionConfig) : this( - fragment.requireActivity().applicationContext, - fragment, - fragment, - config, - CustomerSession.getInstance(), - PaymentMethodsActivityStarter(fragment), - PaymentFlowActivityStarter(fragment, config) - ) - - /** - * Notify this payment session that it is complete - */ - fun onCompleted() { - viewModel.onCompleted() - } - - /** - * Method to handle Activity results from Stripe activities. Pass data here from your - * host's `#onActivityResult(int, int, Intent)` function. - * - * @param requestCode the request code used to open the resulting activity - * @param resultCode a result code representing the success of the intended action - * @param data an [Intent] with the resulting data from the Activity - * - * @return `true` if the activity result was handled by this function, - * otherwise `false` - */ - fun handlePaymentData( - requestCode: Int, - resultCode: Int, - data: Intent? - ): Boolean { - if (data == null || !isValidRequestCode(requestCode)) { - return false - } - - return when (resultCode) { - Activity.RESULT_CANCELED -> { - if (requestCode == PaymentMethodsActivityStarter.REQUEST_CODE) { - // If resultCode of `PaymentMethodsActivity` is `Activity.RESULT_CANCELED`, - // the user tapped back via the toolbar or device back button. - onPaymentMethodResult(data) - } else { - fetchCustomer() - } - false - } - - Activity.RESULT_OK -> when (requestCode) { - PaymentMethodsActivityStarter.REQUEST_CODE -> { - onPaymentMethodResult(data) - true - } - - PaymentFlowActivityStarter.REQUEST_CODE -> { - data.getParcelableExtra(EXTRA_PAYMENT_SESSION_DATA)?.let { - viewModel.onPaymentFlowResult(it) - } - true - } - - else -> { - false - } - } - - else -> false - } - } - - private fun onPaymentMethodResult(data: Intent) { - viewModel.onPaymentMethodResult(PaymentMethodsActivityStarter.Result.fromIntent(data)) - } - - /** - * Initialize the [PaymentSession] with a [PaymentSessionListener] to be notified of - * data changes. The reference to the [listener] will be released when the host (i.e. - * `Activity` or `Fragment`) is destroyed. - * - * The [listener] will be immediately called with the current [PaymentSessionData]. - * - * If the [PaymentSessionConfig.shouldPrefetchCustomer] is true, a new `Customer` instance - * will be fetched. - * - * @param listener a [PaymentSessionListener] - */ - fun init( - listener: PaymentSessionListener - ) { - this.listener = listener - - viewModel.onListenerAttached() - - if (config.shouldPrefetchCustomer) { - fetchCustomer(isInitialFetch = true) - } - } - - /** - * Launch the [PaymentMethodsActivity] to allow the user to select a payment method, - * or to add a new one. - * - * The initial selected Payment Method ID uses the following logic. - * - * 1. If {@param userSelectedPaymentMethodId} is specified, use that - * 2. If the instance's [PaymentSessionData.paymentMethod] is non-null, use that - * 3. If the instance's [PaymentSessionPrefs.getPaymentMethod] is non-null, use that - * 4. Otherwise, choose the most recently added Payment Method - * - * @param selectedPaymentMethodId if non-null, the ID of the Payment Method that should be - * initially selected on the Payment Method selection screen - */ - fun presentPaymentMethodSelection(selectedPaymentMethodId: String? = null) { - val selection = viewModel.getSelectedPaymentMethod(selectedPaymentMethodId) - val useGooglePay = if (selection is PaymentSessionPrefs.SelectedPaymentMethod.GooglePay) { - true - } else { - viewModel.paymentSessionData.useGooglePay - } - paymentMethodsActivityStarter.startForResult( - PaymentMethodsActivityStarter.Args.Builder() - .setInitialPaymentMethodId(selection?.stringValue) - .setPaymentMethodsFooter(config.paymentMethodsFooterLayoutId) - .setAddPaymentMethodFooter(config.addPaymentMethodFooterLayoutId) - .setIsPaymentSessionActive(true) - .setPaymentConfiguration(PaymentConfiguration.getInstance(context)) - .setPaymentMethodTypes(config.paymentMethodTypes) - .setShouldShowGooglePay(config.shouldShowGooglePay) - .setWindowFlags(config.windowFlags) - .setBillingAddressFields(config.billingAddressFields) - .setUseGooglePay(useGooglePay) - .setCanDeletePaymentMethods(config.canDeletePaymentMethods) - .build() - ) - } - - /** - * Set the cart total for this PaymentSession. This should not include shipping costs. - * - * @param cartTotal the current total price for all non-shipping and non-tax items in - * a customer's cart - */ - fun setCartTotal(@IntRange(from = 0) cartTotal: Long) { - viewModel.updateCartTotal(cartTotal) - } - - /** - * Launch the [PaymentFlowActivity] to allow the user to fill in payment details. - */ - fun presentShippingFlow() { - paymentFlowActivityStarter.startForResult( - PaymentFlowActivityStarter.Args( - paymentSessionConfig = config, - paymentSessionData = viewModel.paymentSessionData, - isPaymentSessionActive = true, - windowFlags = config.windowFlags - ) - ) - } - - /** - * Clear the payment method associated with this [PaymentSession] in [PaymentSessionData]. - * - * Will trigger a call to [PaymentSessionListener.onPaymentSessionDataChanged]. - */ - fun clearPaymentMethod() { - viewModel.clearPaymentMethod() - } - - private fun fetchCustomer(isInitialFetch: Boolean = false) { - lifecycleOwner.lifecycleScope.launch { - val result = viewModel.fetchCustomer(isInitialFetch) - - if (result is PaymentSessionViewModel.FetchCustomerResult.Error) { - listener?.onError(result.errorCode, result.errorMessage) - } - } - } - - /** - * Represents a listener for PaymentSession actions, used to update the host activity - * when necessary. - */ - interface PaymentSessionListener { - /** - * Notification method called when network communication is beginning or ending. - * - * @param isCommunicating `true` if communication is starting, `false` if it is stopping. - */ - fun onCommunicatingStateChanged(isCommunicating: Boolean) - - /** - * Notification method called when an error has occurred. - * - * @param errorCode a network code associated with the error - * @param errorMessage a message associated with the error - */ - fun onError(errorCode: Int, errorMessage: String) - - /** - * Notification method called when the [PaymentSessionData] for this session has changed. - * - * @param data the updated [PaymentSessionData] - */ - fun onPaymentSessionDataChanged(data: PaymentSessionData) - } - - internal companion object { - internal const val PRODUCT_TOKEN: String = "PaymentSession" - - internal const val EXTRA_PAYMENT_SESSION_DATA: String = "extra_payment_session_data" - - private fun isValidRequestCode( - requestCode: Int - ) = VALID_REQUEST_CODES.contains(requestCode) - - private val VALID_REQUEST_CODES = setOf( - PaymentMethodsActivityStarter.REQUEST_CODE, - PaymentFlowActivityStarter.REQUEST_CODE - ) - } -} diff --git a/payments-core/src/main/java/com/stripe/android/PaymentSessionConfig.kt b/payments-core/src/main/java/com/stripe/android/PaymentSessionConfig.kt deleted file mode 100644 index 8d70e759fa2..00000000000 --- a/payments-core/src/main/java/com/stripe/android/PaymentSessionConfig.kt +++ /dev/null @@ -1,320 +0,0 @@ -package com.stripe.android - -import android.os.Parcelable -import androidx.annotation.LayoutRes -import androidx.annotation.WorkerThread -import com.stripe.android.model.Customer -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.ShippingInformation -import com.stripe.android.model.ShippingMethod -import com.stripe.android.view.AddPaymentMethodActivity -import com.stripe.android.view.BillingAddressFields -import com.stripe.android.view.PaymentFlowActivity -import com.stripe.android.view.PaymentMethodsActivity -import com.stripe.android.view.SelectShippingMethodWidget -import com.stripe.android.view.ShippingInfoWidget -import com.stripe.android.view.ShippingInfoWidget.CustomizableShippingField -import kotlinx.parcelize.Parcelize -import java.io.Serializable -import java.util.Locale - -/** - * Configuration for [PaymentSession]. - */ -@Parcelize -data class PaymentSessionConfig internal constructor( - val hiddenShippingInfoFields: List = emptyList(), - val optionalShippingInfoFields: List = emptyList(), - val prepopulatedShippingInfo: ShippingInformation? = null, - val isShippingInfoRequired: Boolean = false, - val isShippingMethodRequired: Boolean = false, - - @LayoutRes - @get:LayoutRes - val paymentMethodsFooterLayoutId: Int = 0, - - @LayoutRes - @get:LayoutRes - val addPaymentMethodFooterLayoutId: Int = 0, - - val paymentMethodTypes: List = listOf(PaymentMethod.Type.Card), - val shouldShowGooglePay: Boolean = false, - val allowedShippingCountryCodes: Set = emptySet(), - val billingAddressFields: BillingAddressFields = DEFAULT_BILLING_ADDRESS_FIELDS, - val canDeletePaymentMethods: Boolean = true, - - internal val shouldPrefetchCustomer: Boolean = true, - internal val shippingInformationValidator: ShippingInformationValidator = DefaultShippingInfoValidator(), - internal val shippingMethodsFactory: ShippingMethodsFactory? = null, - internal val windowFlags: Int? = null -) : Parcelable { - init { - val countryCodes = Locale.getISOCountries() - allowedShippingCountryCodes.forEach { allowedShippingCountryCode -> - require( - countryCodes.any { allowedShippingCountryCode.equals(it, ignoreCase = true) } - ) { - "'$allowedShippingCountryCode' is not a valid country code" - } - } - - if (isShippingMethodRequired) { - requireNotNull(shippingMethodsFactory) { - """ - If isShippingMethodRequired is true a ShippingMethodsFactory must also be provided. - """.trimIndent() - } - } - } - - interface ShippingInformationValidator : Serializable { - /** - * @return whether the customer's [ShippingInformation] is valid. Will run on - * a background thread. - */ - @WorkerThread - fun isValid(shippingInformation: ShippingInformation): Boolean - - /** - * @return the error message to show if [isValid] returns `false`. Will run on - * a background thread. - */ - @WorkerThread - fun getErrorMessage(shippingInformation: ShippingInformation): String - } - - interface ShippingMethodsFactory : Serializable { - /** - * @return a list of [ShippingMethod] options to present to the customer. Will run on - * a background thread. - */ - @WorkerThread - fun create( - shippingInformation: ShippingInformation - ): List - } - - class Builder { - private var billingAddressFields: BillingAddressFields = DEFAULT_BILLING_ADDRESS_FIELDS - private var shippingInfoRequired = true - private var shippingMethodsRequired = true - private var hiddenShippingInfoFields: List? = null - private var optionalShippingInfoFields: List? = null - private var shippingInformation: ShippingInformation? = null - private var paymentMethodTypes: List = listOf(PaymentMethod.Type.Card) - private var shouldShowGooglePay: Boolean = false - private var allowedShippingCountryCodes: Set = emptySet() - private var shippingInformationValidator: ShippingInformationValidator? = null - private var shippingMethodsFactory: ShippingMethodsFactory? = null - private var windowFlags: Int? = null - private var shouldPrefetchCustomer: Boolean = true - private var canDeletePaymentMethods: Boolean = true - - @LayoutRes - private var paymentMethodsFooterLayoutId: Int = 0 - - @LayoutRes - private var addPaymentMethodFooterLayoutId: Int = 0 - - /** - * @param billingAddressFields the billing address fields to require on [AddPaymentMethodActivity] - */ - fun setBillingAddressFields( - billingAddressFields: BillingAddressFields - ): Builder = apply { - this.billingAddressFields = billingAddressFields - } - - /** - * @param hiddenShippingInfoFields [CustomizableShippingField] fields that should be - * hidden in the shipping information screen. All fields will be shown if this list is - * empty. Note that not all fields can be hidden, such as country or name. - */ - fun setHiddenShippingInfoFields( - vararg hiddenShippingInfoFields: CustomizableShippingField - ): Builder = apply { - this.hiddenShippingInfoFields = listOf(*hiddenShippingInfoFields) - } - - /** - * @param optionalShippingInfoFields [CustomizableShippingField] fields that should be - * optional in the [ShippingInfoWidget] - */ - fun setOptionalShippingInfoFields( - vararg optionalShippingInfoFields: CustomizableShippingField - ): Builder = apply { - this.optionalShippingInfoFields = listOf(*optionalShippingInfoFields) - } - - /** - * @param shippingInfo [ShippingInformation] that will pre-populate the [ShippingInfoWidget] - */ - fun setPrepopulatedShippingInfo(shippingInfo: ShippingInformation?): Builder = apply { - shippingInformation = shippingInfo - } - - /** - * @param shippingInfoRequired whether a [ShippingInformation] should be required. - * If it is required, a screen with a [ShippingInfoWidget] is shown to collect it. - * - * Default is `true`. - */ - fun setShippingInfoRequired(shippingInfoRequired: Boolean): Builder = apply { - this.shippingInfoRequired = shippingInfoRequired - } - - /** - * @param shippingMethodsRequired whether a [ShippingMethod] should be required. - * If it is required, a screen with a [SelectShippingMethodWidget] is shown to collect it. - * - * Default is `true`. - */ - fun setShippingMethodsRequired(shippingMethodsRequired: Boolean): Builder = apply { - this.shippingMethodsRequired = shippingMethodsRequired - } - - /** - * @param paymentMethodsFooterLayoutId optional layout id that will be inflated and - * displayed beneath the payment method selection list on [PaymentMethodsActivity] - */ - fun setPaymentMethodsFooter( - @LayoutRes paymentMethodsFooterLayoutId: Int - ): Builder = apply { - this.paymentMethodsFooterLayoutId = paymentMethodsFooterLayoutId - } - - /** - * @param addPaymentMethodFooterLayoutId optional layout id that will be inflated and - * displayed beneath the payment details collection form on [AddPaymentMethodActivity] - */ - fun setAddPaymentMethodFooter( - @LayoutRes addPaymentMethodFooterLayoutId: Int - ): Builder = apply { - this.addPaymentMethodFooterLayoutId = addPaymentMethodFooterLayoutId - } - - /** - * @param paymentMethodTypes a list of [PaymentMethod.Type] that indicates the types of - * Payment Methods that the customer can select or add via Stripe UI components. - * - * The order of the [PaymentMethod.Type] values in the list will be used to - * arrange the add buttons in the Stripe UI components. They will be arranged vertically - * from first to last. - * - * Currently only [PaymentMethod.Type.Card] and [PaymentMethod.Type.Fpx] are supported. - * If not specified or empty, [PaymentMethod.Type.Card] will be used. - */ - fun setPaymentMethodTypes(paymentMethodTypes: List): Builder = apply { - this.paymentMethodTypes = paymentMethodTypes - } - - /** - * @param shouldShowGooglePay if `true`, will show "Google Pay" as an option on the - * Payment Methods selection screen. If a user selects the Google Pay option, - * [PaymentSessionData.useGooglePay] will be `true`. - */ - fun setShouldShowGooglePay(shouldShowGooglePay: Boolean): Builder = apply { - this.shouldShowGooglePay = shouldShowGooglePay - } - - /** - * @param canDeletePaymentMethods controls whether the user can - * delete a payment method by swiping on it in [PaymentMethodsActivity]. Defaults to true. - */ - fun setCanDeletePaymentMethods(canDeletePaymentMethods: Boolean): Builder = apply { - this.canDeletePaymentMethods = canDeletePaymentMethods - } - - /** - * @param allowedShippingCountryCodes A set of allowed country codes for the - * customer's shipping address. Will be ignored if empty. - */ - fun setAllowedShippingCountryCodes( - allowedShippingCountryCodes: Set - ): Builder = apply { - this.allowedShippingCountryCodes = allowedShippingCountryCodes - } - - /** - * @param windowFlags optional flags to set on the `Window` object of Stripe Activities - * - * See [WindowManager.LayoutParams](https://developer.android.com/reference/android/view/WindowManager.LayoutParams) - */ - fun setWindowFlags(windowFlags: Int?): Builder = apply { - this.windowFlags = windowFlags - } - - /** - * @param shippingInformationValidator used to validate [ShippingInformation] in [PaymentFlowActivity] - * - * Note: this instance must be [Serializable]. - */ - fun setShippingInformationValidator( - shippingInformationValidator: ShippingInformationValidator? - ): Builder = apply { - this.shippingInformationValidator = shippingInformationValidator - } - - /** - * @param shippingMethodsFactory required if [shippingInformationValidator] is specified - * and [shippingMethodsRequired] is `true`. Used to create the [ShippingMethod] options - * to be displayed in [PaymentFlowActivity]. - * - * Note: this instance must be [Serializable]. - */ - fun setShippingMethodsFactory( - shippingMethodsFactory: ShippingMethodsFactory? - ): Builder = apply { - this.shippingMethodsFactory = shippingMethodsFactory - } - - /** - * @param shouldPrefetchCustomer If true, will immediately fetch the [Customer] associated - * with this session. Otherwise, will only fetch when needed. - * - * Defaults to true. - */ - fun setShouldPrefetchCustomer(shouldPrefetchCustomer: Boolean): Builder = apply { - this.shouldPrefetchCustomer = shouldPrefetchCustomer - } - - fun build(): PaymentSessionConfig { - return PaymentSessionConfig( - hiddenShippingInfoFields = hiddenShippingInfoFields.orEmpty(), - optionalShippingInfoFields = optionalShippingInfoFields.orEmpty(), - prepopulatedShippingInfo = shippingInformation, - isShippingInfoRequired = shippingInfoRequired, - isShippingMethodRequired = shippingMethodsRequired, - paymentMethodsFooterLayoutId = paymentMethodsFooterLayoutId, - addPaymentMethodFooterLayoutId = addPaymentMethodFooterLayoutId, - paymentMethodTypes = paymentMethodTypes, - shouldShowGooglePay = shouldShowGooglePay, - allowedShippingCountryCodes = allowedShippingCountryCodes, - shippingInformationValidator = shippingInformationValidator - ?: DefaultShippingInfoValidator(), - shippingMethodsFactory = shippingMethodsFactory, - windowFlags = windowFlags, - billingAddressFields = billingAddressFields, - shouldPrefetchCustomer = shouldPrefetchCustomer, - canDeletePaymentMethods = canDeletePaymentMethods - ) - } - } - - /** - * A [ShippingInformationValidator] that accepts any [ShippingInformation] as valid. - */ - private class DefaultShippingInfoValidator : ShippingInformationValidator { - override fun isValid(shippingInformation: ShippingInformation): Boolean { - return true - } - - override fun getErrorMessage(shippingInformation: ShippingInformation): String { - return "" - } - } - - private companion object { - private val DEFAULT_BILLING_ADDRESS_FIELDS = BillingAddressFields.PostalCode - } -} diff --git a/payments-core/src/main/java/com/stripe/android/PaymentSessionData.kt b/payments-core/src/main/java/com/stripe/android/PaymentSessionData.kt deleted file mode 100644 index e18f8d5c170..00000000000 --- a/payments-core/src/main/java/com/stripe/android/PaymentSessionData.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.stripe.android - -import android.os.Parcelable -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.ShippingInformation -import com.stripe.android.model.ShippingMethod -import kotlinx.parcelize.Parcelize - -/** - * A data class representing the state of the associated [PaymentSession]. - */ -@Parcelize -data class PaymentSessionData internal constructor( - private val isShippingInfoRequired: Boolean, - - private val isShippingMethodRequired: Boolean, - - /** - * The cart total value, excluding shipping and tax items. - */ - val cartTotal: Long = 0L, - - /** - * The current value of the shipping items in the associated [PaymentSession] - */ - val shippingTotal: Long = 0L, - - /** - * Where the items being purchased should be shipped. - */ - val shippingInformation: ShippingInformation? = null, - - /** - * How the items being purchased should be shipped. - */ - val shippingMethod: ShippingMethod? = null, - - /** - * The selected payment method for the associated [PaymentSession]. - */ - val paymentMethod: PaymentMethod? = null, - - /** - * When `true`, the customer has indicated their intent to pay with Google Pay. Use the - * [Google Pay API](https://developers.google.com/pay/api/android/overview) to complete - * payment with Google Pay. - */ - val useGooglePay: Boolean = false -) : Parcelable { - internal constructor(config: PaymentSessionConfig) : this( - isShippingInfoRequired = config.isShippingInfoRequired, - isShippingMethodRequired = config.isShippingMethodRequired - ) - - /** - * Whether the payment data is ready for making a charge. This can be used to - * set a buy button to enabled for prompt a user to fill in more information. - */ - val isPaymentReadyToCharge: Boolean - get() = - (paymentMethod != null || useGooglePay) && - (!isShippingInfoRequired || shippingInformation != null) && - (!isShippingMethodRequired || shippingMethod != null) -} diff --git a/payments-core/src/main/java/com/stripe/android/PaymentSessionPrefs.kt b/payments-core/src/main/java/com/stripe/android/PaymentSessionPrefs.kt deleted file mode 100644 index 29e928e2b92..00000000000 --- a/payments-core/src/main/java/com/stripe/android/PaymentSessionPrefs.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.stripe.android - -import android.content.Context -import android.content.SharedPreferences - -internal interface PaymentSessionPrefs { - fun getPaymentMethod(customerId: String?): SelectedPaymentMethod? - fun savePaymentMethod(customerId: String, paymentMethod: SelectedPaymentMethod?) - - class Default(context: Context) : PaymentSessionPrefs { - val prefs: SharedPreferences by lazy { - context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE) - } - - override fun getPaymentMethod(customerId: String?): SelectedPaymentMethod? { - return SelectedPaymentMethod.fromString( - customerId?.let { - prefs.getString(getPaymentMethodKey(it), null) - } - ) - } - - override fun savePaymentMethod( - customerId: String, - paymentMethod: SelectedPaymentMethod? - ) { - prefs.edit() - .putString(getPaymentMethodKey(customerId), paymentMethod?.stringValue) - .apply() - } - } - - companion object { - private const val PREF_FILE = "PaymentSessionPrefs" - const val GOOGLE_PAY = "GooglePay" - - private fun getPaymentMethodKey(customerId: String?): String { - return "customer[$customerId].payment_method" - } - } - - sealed class SelectedPaymentMethod(val stringValue: String) { - class Saved(paymentMethodId: String) : SelectedPaymentMethod(paymentMethodId) - data object GooglePay : SelectedPaymentMethod(GOOGLE_PAY) - - companion object { - fun fromString(value: String?) = when (value) { - GOOGLE_PAY -> GooglePay - is String -> Saved(value) - else -> null - } - } - } -} diff --git a/payments-core/src/main/java/com/stripe/android/PaymentSessionViewModel.kt b/payments-core/src/main/java/com/stripe/android/PaymentSessionViewModel.kt deleted file mode 100644 index 06e15fe1bdc..00000000000 --- a/payments-core/src/main/java/com/stripe/android/PaymentSessionViewModel.kt +++ /dev/null @@ -1,273 +0,0 @@ -package com.stripe.android - -import android.app.Application -import androidx.annotation.IntRange -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.createSavedStateHandle -import androidx.lifecycle.viewmodel.CreationExtras -import com.stripe.android.analytics.SessionSavedStateHandler -import com.stripe.android.core.StripeError -import com.stripe.android.core.utils.requireApplication -import com.stripe.android.model.Customer -import com.stripe.android.model.PaymentMethod -import com.stripe.android.view.PaymentMethodsActivityStarter -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -internal class PaymentSessionViewModel( - application: Application, - private val savedStateHandle: SavedStateHandle, - paymentSessionData: PaymentSessionData, - private val customerSession: CustomerSession, - private val paymentSessionPrefs: PaymentSessionPrefs = PaymentSessionPrefs.Default(application) -) : AndroidViewModel(application) { - - var paymentSessionData: PaymentSessionData = paymentSessionData - set(value) { - if (value != field) { - field = value - savedStateHandle.set(KEY_PAYMENT_SESSION_DATA, value) - _paymentSessionDataStateFlow.tryEmit(value) - } - } - - private val _paymentSessionDataStateFlow = MutableSharedFlow( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - val paymentSessionDataStateFlow: SharedFlow = _paymentSessionDataStateFlow.asSharedFlow() - - init { - SessionSavedStateHandler.attachTo(this, savedStateHandle) - - // read from saved state handle - savedStateHandle.get(KEY_PAYMENT_SESSION_DATA)?.let { - this.paymentSessionData = it - } - } - - private val _networkState: MutableStateFlow = MutableStateFlow(NetworkState.Inactive) - internal val networkState: StateFlow = _networkState.asStateFlow() - - @JvmSynthetic - fun updateCartTotal(@IntRange(from = 0) cartTotal: Long) { - paymentSessionData = paymentSessionData.copy(cartTotal = cartTotal) - } - - fun clearPaymentMethod() { - paymentSessionData = paymentSessionData.copy(paymentMethod = null) - } - - @JvmSynthetic - fun onCompleted() { - } - - @JvmSynthetic - suspend fun fetchCustomer(isInitialFetch: Boolean = false): FetchCustomerResult = - suspendCoroutine { continuation -> - _networkState.value = NetworkState.Active - - customerSession.retrieveCurrentCustomer( - productUsage = setOf(PaymentSession.PRODUCT_TOKEN), - listener = object : CustomerSession.CustomerRetrievalListener { - override fun onCustomerRetrieved(customer: Customer) { - onCustomerRetrieved( - customer.id, - isInitialFetch - ) { - _networkState.value = NetworkState.Inactive - continuation.resume(FetchCustomerResult.Success) - } - } - - override fun onError( - errorCode: Int, - errorMessage: String, - stripeError: StripeError? - ) { - _networkState.value = NetworkState.Inactive - continuation.resume( - FetchCustomerResult.Error( - errorCode, - errorMessage, - stripeError - ) - ) - } - } - ) - } - - @JvmSynthetic - internal fun onCustomerRetrieved( - customerId: String?, - isInitialFetch: Boolean = false, - onComplete: () -> Unit - ) { - if (isInitialFetch) { - paymentSessionPrefs.getPaymentMethod(customerId)?.let { - if (it is PaymentSessionPrefs.SelectedPaymentMethod.GooglePay) { - paymentSessionData = paymentSessionData.copy( - useGooglePay = true - ) - onComplete() - } else { - fetchCustomerPaymentMethod(it.stringValue) { paymentMethod -> - paymentMethod?.let { - paymentSessionData = paymentSessionData.copy( - paymentMethod = it - ) - } - onComplete() - } - } - } ?: onComplete() - } else { - onComplete() - } - } - - private fun fetchCustomerPaymentMethod( - paymentMethodId: String?, - onComplete: (paymentMethod: PaymentMethod?) -> Unit - ) { - if (paymentMethodId != null) { - // fetch the maximum number of payment methods and attempt to find the - // payment method id in the list - customerSession.getPaymentMethods( - paymentMethodType = PaymentMethod.Type.Card, - limit = MAX_PAYMENT_METHODS_LIMIT, - listener = object : CustomerSession.PaymentMethodsRetrievalListener { - override fun onPaymentMethodsRetrieved( - paymentMethods: List - ) { - onComplete( - paymentMethods.firstOrNull { it.id == paymentMethodId } - ) - } - - override fun onError( - errorCode: Int, - errorMessage: String, - stripeError: StripeError? - ) { - // if an error is encountered, treat it as a no-op - onComplete(null) - } - } - ) - } else { - onComplete(null) - } - } - - @JvmSynthetic - fun getSelectedPaymentMethod( - userSelectedPaymentMethodId: String? = null - ): PaymentSessionPrefs.SelectedPaymentMethod? { - return if (paymentSessionData.useGooglePay) { - null - } else { - PaymentSessionPrefs.SelectedPaymentMethod.fromString(userSelectedPaymentMethodId) - ?: if (paymentSessionData.paymentMethod != null) { - PaymentSessionPrefs.SelectedPaymentMethod.fromString( - paymentSessionData.paymentMethod?.id - ) - } else { - customerSession.cachedCustomer?.id?.let { customerId -> - paymentSessionPrefs.getPaymentMethod(customerId) - } - } - } - } - - @JvmSynthetic - fun onPaymentMethodResult(result: PaymentMethodsActivityStarter.Result?) { - persistPaymentMethodResult( - paymentMethod = result?.paymentMethod, - useGooglePay = result?.useGooglePay ?: false - ) - } - - private fun persistPaymentMethodResult( - paymentMethod: PaymentMethod?, - useGooglePay: Boolean - ) { - customerSession.cachedCustomer?.id?.let { customerId -> - val selectedPaymentMethod = if (useGooglePay) { - PaymentSessionPrefs.SelectedPaymentMethod.GooglePay - } else { - paymentMethod?.id?.let { - PaymentSessionPrefs.SelectedPaymentMethod.Saved( - it - ) - } - } - paymentSessionPrefs.savePaymentMethod( - customerId, - selectedPaymentMethod - ) - } - paymentSessionData = paymentSessionData.copy( - paymentMethod = paymentMethod, - useGooglePay = useGooglePay - ) - } - - @JvmSynthetic - fun onPaymentFlowResult(paymentSessionData: PaymentSessionData) { - this.paymentSessionData = paymentSessionData - } - - @JvmSynthetic - fun onListenerAttached() { - _paymentSessionDataStateFlow.tryEmit(paymentSessionData) - } - - sealed class FetchCustomerResult { - data object Success : FetchCustomerResult() - - data class Error( - val errorCode: Int, - val errorMessage: String, - val stripeError: StripeError? - ) : FetchCustomerResult() - } - - enum class NetworkState { - Active, - Inactive - } - - internal class Factory( - private val paymentSessionData: PaymentSessionData, - private val customerSession: CustomerSession - ) : ViewModelProvider.Factory { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class, extras: CreationExtras): T { - return PaymentSessionViewModel( - extras.requireApplication(), - extras.createSavedStateHandle(), - paymentSessionData, - customerSession - ) as T - } - } - - internal companion object { - internal const val KEY_PAYMENT_SESSION_DATA = "key_payment_session_data" - - private const val MAX_PAYMENT_METHODS_LIMIT = 100 - } -} diff --git a/payments-core/src/main/java/com/stripe/android/analytics/DefaultPaymentSessionEventReporter.kt b/payments-core/src/main/java/com/stripe/android/analytics/DefaultPaymentSessionEventReporter.kt deleted file mode 100644 index bcdae29caf3..00000000000 --- a/payments-core/src/main/java/com/stripe/android/analytics/DefaultPaymentSessionEventReporter.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.stripe.android.analytics - -import com.stripe.android.core.injection.IOContext -import com.stripe.android.core.networking.AnalyticsRequestExecutor -import com.stripe.android.core.utils.DurationProvider -import com.stripe.android.model.PaymentMethodCode -import com.stripe.android.networking.PaymentAnalyticsRequestFactory -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.coroutines.CoroutineContext - -@Singleton -internal class DefaultPaymentSessionEventReporter @Inject internal constructor( - private val analyticsRequestExecutor: AnalyticsRequestExecutor, - private val paymentAnalyticsRequestFactory: PaymentAnalyticsRequestFactory, - private val durationProvider: DurationProvider, - @IOContext private val workContext: CoroutineContext -) : PaymentSessionEventReporter { - override fun onLoadStarted() { - durationProvider.start(DurationProvider.Key.Loading) - - fireEvent(PaymentSessionEvent.LoadStarted()) - } - - override fun onLoadSucceeded(code: PaymentMethodCode?) { - fireEvent( - PaymentSessionEvent.LoadSucceeded( - duration = durationProvider.end(DurationProvider.Key.Loading), - code = code, - ) - ) - } - - override fun onLoadFailed(error: Throwable) { - fireEvent( - PaymentSessionEvent.LoadFailed( - duration = durationProvider.end(DurationProvider.Key.Loading), - error = error - ) - ) - } - - override fun onOptionsShown() { - fireEvent( - PaymentSessionEvent.ShowPaymentOptions() - ) - } - - override fun onFormShown(code: PaymentMethodCode) { - durationProvider.start(DurationProvider.Key.ConfirmButtonClicked) - - fireEvent( - PaymentSessionEvent.ShowPaymentOptionForm(code) - ) - } - - override fun onFormInteracted(code: PaymentMethodCode) { - fireEvent( - PaymentSessionEvent.PaymentOptionFormInteraction(code) - ) - } - - override fun onCardNumberCompleted() { - fireEvent( - PaymentSessionEvent.CardNumberCompleted() - ) - } - - override fun onDoneButtonTapped(code: PaymentMethodCode) { - fireEvent( - PaymentSessionEvent.TapDoneButton( - code = code, - duration = durationProvider.end(DurationProvider.Key.ConfirmButtonClicked) - ) - ) - } - - private fun fireEvent(event: PaymentSessionEvent) { - CoroutineScope(workContext).launch { - analyticsRequestExecutor.executeAsync( - paymentAnalyticsRequestFactory.createRequest( - event = event, - additionalParams = event.additionalParams - ) - ) - } - } -} diff --git a/payments-core/src/main/java/com/stripe/android/analytics/PaymentSessionEvent.kt b/payments-core/src/main/java/com/stripe/android/analytics/PaymentSessionEvent.kt deleted file mode 100644 index 08446483bbc..00000000000 --- a/payments-core/src/main/java/com/stripe/android/analytics/PaymentSessionEvent.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.stripe.android.analytics - -import com.stripe.android.core.exception.safeAnalyticsMessage -import com.stripe.android.core.networking.AnalyticsEvent -import kotlin.time.Duration -import kotlin.time.DurationUnit - -internal sealed class PaymentSessionEvent : AnalyticsEvent { - abstract val additionalParams: Map - - class LoadStarted : PaymentSessionEvent() { - override val eventName: String = "bi_load_started" - - override val additionalParams: Map = emptyMap() - } - - class LoadSucceeded( - code: String?, - duration: Duration?, - ) : PaymentSessionEvent() { - override val eventName: String = "bi_load_succeeded" - - override val additionalParams: Map = mapOf( - FIELD_DURATION to duration?.asSeconds, - FIELD_SELECTED_LPM to code, - ) - } - - class LoadFailed( - duration: Duration?, - error: Throwable, - ) : PaymentSessionEvent() { - override val eventName: String = "bi_load_failed" - - override val additionalParams: Map = mapOf( - FIELD_DURATION to duration?.asSeconds, - FIELD_ERROR_MESSAGE to error.safeAnalyticsMessage, - ) - } - - class ShowPaymentOptions : PaymentSessionEvent() { - override val eventName: String = "bi_options_shown" - - override val additionalParams: Map = mapOf() - } - - class ShowPaymentOptionForm( - code: String, - ) : PaymentSessionEvent() { - override val eventName: String = "bi_form_shown" - - override val additionalParams: Map = mapOf( - FIELD_SELECTED_LPM to code, - ) - } - - class PaymentOptionFormInteraction( - code: String, - ) : PaymentSessionEvent() { - override val eventName: String = "bi_form_interacted" - - override val additionalParams: Map = mapOf( - FIELD_SELECTED_LPM to code, - ) - } - - class CardNumberCompleted : PaymentSessionEvent() { - override val eventName: String = "bi_card_number_completed" - - override val additionalParams: Map = mapOf() - } - - class TapDoneButton( - code: String, - duration: Duration?, - ) : PaymentSessionEvent() { - override val eventName: String = "bi_done_button_tapped" - - override val additionalParams: Map = mapOf( - FIELD_SELECTED_LPM to code, - FIELD_DURATION to duration?.asSeconds, - ) - } - - internal companion object { - private val Duration.asSeconds: Float - get() = toDouble(DurationUnit.SECONDS).toFloat() - - const val FIELD_DURATION = "duration" - const val FIELD_ERROR_MESSAGE = "error_message" - const val FIELD_SELECTED_LPM = "selected_lpm" - } -} diff --git a/payments-core/src/main/java/com/stripe/android/analytics/PaymentSessionEventReporter.kt b/payments-core/src/main/java/com/stripe/android/analytics/PaymentSessionEventReporter.kt deleted file mode 100644 index a52b57cd1d6..00000000000 --- a/payments-core/src/main/java/com/stripe/android/analytics/PaymentSessionEventReporter.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.stripe.android.analytics - -import com.stripe.android.model.PaymentMethodCode - -internal interface PaymentSessionEventReporter { - /** - * Payment session has started loading - */ - fun onLoadStarted() - - /** - * Payment session was successfully loaded. - */ - fun onLoadSucceeded(code: PaymentMethodCode?) - - /** - * Payment session failed to load. - */ - fun onLoadFailed(error: Throwable) - - /** - * User was shown available payment options - */ - fun onOptionsShown() - - /** - * User was shown a payment form - * - * @param code payment method code for the form type shown to the user - */ - fun onFormShown(code: PaymentMethodCode) - - /** - * User interacted with the payment form - * - * @param code payment method code for the form type interacted by the user - */ - fun onFormInteracted(code: PaymentMethodCode) - - /** - * User completes entering card number in payment form - */ - fun onCardNumberCompleted() - - /** - * User clicks the done button to indicate completing the payment form - * - * @param code payment method code for the form type the user has completed - */ - fun onDoneButtonTapped(code: PaymentMethodCode) -} diff --git a/payments-core/src/main/java/com/stripe/android/analytics/PaymentSessionEventReporterFactory.kt b/payments-core/src/main/java/com/stripe/android/analytics/PaymentSessionEventReporterFactory.kt deleted file mode 100644 index 9eed093f670..00000000000 --- a/payments-core/src/main/java/com/stripe/android/analytics/PaymentSessionEventReporterFactory.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.stripe.android.analytics - -import android.content.Context -import com.stripe.android.BuildConfig -import com.stripe.android.PaymentConfiguration -import com.stripe.android.core.Logger -import com.stripe.android.core.networking.DefaultAnalyticsRequestExecutor -import com.stripe.android.core.utils.DefaultDurationProvider -import com.stripe.android.networking.PaymentAnalyticsRequestFactory -import kotlinx.coroutines.Dispatchers - -internal object PaymentSessionEventReporterFactory { - fun create(context: Context): PaymentSessionEventReporter { - val workContext = Dispatchers.IO - val configuration = PaymentConfiguration.getInstance(context) - - return DefaultPaymentSessionEventReporter( - analyticsRequestExecutor = DefaultAnalyticsRequestExecutor( - logger = Logger.getInstance(BuildConfig.DEBUG), - workContext = workContext - ), - paymentAnalyticsRequestFactory = PaymentAnalyticsRequestFactory( - context = context, - publishableKey = configuration.publishableKey - ), - durationProvider = DefaultDurationProvider.instance, - workContext = Dispatchers.IO - ) - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/ActivityStarter.kt b/payments-core/src/main/java/com/stripe/android/view/ActivityStarter.kt index 18636e5c00f..0bd956f6463 100644 --- a/payments-core/src/main/java/com/stripe/android/view/ActivityStarter.kt +++ b/payments-core/src/main/java/com/stripe/android/view/ActivityStarter.kt @@ -1,82 +1,31 @@ package com.stripe.android.view -import android.app.Activity -import android.content.Intent import android.os.Bundle import android.os.Parcelable import androidx.annotation.RestrictTo import androidx.annotation.VisibleForTesting -import androidx.fragment.app.Fragment /** * Superclass for starting Stripe activities. - * - * See [PaymentMethodsActivityStarter], [AddPaymentMethodActivityStarter], - * [PaymentFlowActivityStarter]. */ -@Suppress("UnnecessaryAbstractClass") -abstract class ActivityStarter internal constructor( - private val activity: Activity, - private val fragment: Fragment? = null, - private val targetClass: Class, - private val requestCode: Int, - private val intentFlags: Int? = null -) { - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // For paymentsheet - constructor( - activity: Activity, - targetClass: Class, - requestCode: Int, - intentFlags: Int? = null - ) : this( - activity = activity, - fragment = null, - targetClass = targetClass, - requestCode = requestCode, - intentFlags = intentFlags - ) - - internal constructor( - fragment: Fragment, - targetClass: Class, - requestCode: Int, - intentFlags: Int? = null - ) : this( - activity = fragment.requireActivity(), - fragment = fragment, - targetClass = targetClass, - requestCode = requestCode, - intentFlags = intentFlags - ) - - fun startForResult(args: ArgsType) { - val intent = Intent(activity, targetClass) - .putExtra(Args.EXTRA, args) - .also { - if (intentFlags != null) { - it.addFlags(intentFlags) - } - } - - if (fragment != null) { - fragment.startActivityForResult(intent, requestCode) - } else { - activity.startActivityForResult(intent, requestCode) - } - } +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class ActivityStarter private constructor() { + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) interface Args : Parcelable { + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) companion object { - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @VisibleForTesting const val EXTRA: String = "extra_activity_args" } } + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) interface Result : Parcelable { fun toBundle(): Bundle - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // For paymentsheet + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) companion object { const val EXTRA: String = "extra_activity_result" } diff --git a/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodActivity.kt b/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodActivity.kt deleted file mode 100644 index 2853737a0fe..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodActivity.kt +++ /dev/null @@ -1,285 +0,0 @@ -package com.stripe.android.view - -import android.app.Activity -import android.content.Intent -import android.os.Build -import android.os.Bundle -import android.text.method.LinkMovementMethod -import android.text.util.Linkify -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.activity.viewModels -import androidx.annotation.StringRes -import androidx.core.text.util.LinkifyCompat -import androidx.core.view.ViewCompat -import androidx.lifecycle.lifecycleScope -import com.stripe.android.BASIC_INTEGRATION_DEPRECATION_WARNING -import com.stripe.android.CustomerSession -import com.stripe.android.PaymentConfiguration -import com.stripe.android.R -import com.stripe.android.Stripe -import com.stripe.android.databinding.StripeAddPaymentMethodActivityBinding -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.PaymentMethodCreateParams -import com.stripe.android.utils.argsAreInvalid -import kotlinx.coroutines.launch - -/** - * Activity used to display a [AddPaymentMethodView] and receive the resulting - * [PaymentMethod] in the `Activity#onActivityResult(int, int, Intent)` of the - * launching Activity. - * - * - * Should be started with [AddPaymentMethodActivityStarter]. - */ -@Deprecated(BASIC_INTEGRATION_DEPRECATION_WARNING) -class AddPaymentMethodActivity : StripeActivity() { - - private val args: AddPaymentMethodActivityStarter.Args by lazy { - AddPaymentMethodActivityStarter.Args.create(intent) - } - - private val stripe: Stripe by lazy { - val paymentConfiguration = args.paymentConfiguration - ?: PaymentConfiguration.getInstance(this) - Stripe( - applicationContext, - publishableKey = paymentConfiguration.publishableKey, - stripeAccountId = paymentConfiguration.stripeAccountId - ) - } - - private val paymentMethodType: PaymentMethod.Type by lazy { - args.paymentMethodType - } - - private val shouldAttachToCustomer: Boolean by lazy { - paymentMethodType.isReusable && args.shouldAttachToCustomer - } - - private val addPaymentMethodView: AddPaymentMethodView by lazy { - createPaymentMethodView(args).also { - it.id = R.id.stripe_add_payment_method_form - } - } - - private val viewModel: AddPaymentMethodViewModel by viewModels { - AddPaymentMethodViewModel.Factory( - stripe, - args - ) - } - - private val cardInputListener = object : CardInputListener { - override fun onFocusChange(focusField: CardInputListener.FocusField) { - // No-op - } - override fun onCardComplete() { - viewModel.onCardNumberCompleted() - } - override fun onExpirationComplete() { - // No-op - } - override fun onCvcComplete() { - // No-op - } - override fun onPostalCodeComplete() { - // No-op - } - } - - private val titleStringRes: Int - @StringRes - get() { - return when (paymentMethodType) { - PaymentMethod.Type.Card -> { - R.string.stripe_title_add_a_card - } - PaymentMethod.Type.Fpx -> { - R.string.stripe_title_bank_account - } - PaymentMethod.Type.Netbanking -> { - R.string.stripe_title_bank_account - } - else -> { - throw IllegalArgumentException( - "Unsupported Payment Method type: ${paymentMethodType.code}" - ) - } - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (argsAreInvalid { args }) { - return - } - viewModel.onFormShown() - configureView(args) - setResult( - Activity.RESULT_OK, - Intent() - .putExtras( - AddPaymentMethodActivityStarter.Result.Canceled - .toBundle() - ) - ) - } - - override fun onResume() { - super.onResume() - addPaymentMethodView.requestFocus() - } - - override fun onUserInteraction() { - super.onUserInteraction() - - viewModel.onFormInteracted() - } - - private fun configureView(args: AddPaymentMethodActivityStarter.Args) { - args.windowFlags?.let { - window.addFlags(it) - } - - viewStub.layoutResource = R.layout.stripe_add_payment_method_activity - val scrollView = viewStub.inflate() as ViewGroup - val viewBinding = StripeAddPaymentMethodActivityBinding.bind(scrollView) - - viewBinding.root.addView(addPaymentMethodView) - createFooterView(viewBinding.root)?.let { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { - addPaymentMethodView.accessibilityTraversalBefore = it.id - it.accessibilityTraversalAfter = addPaymentMethodView.id - } - viewBinding.root.addView(it) - } - - setTitle(titleStringRes) - } - - private fun createPaymentMethodView( - args: AddPaymentMethodActivityStarter.Args - ): AddPaymentMethodView { - return when (paymentMethodType) { - PaymentMethod.Type.Card -> { - AddPaymentMethodCardView( - context = this, - billingAddressFields = args.billingAddressFields - ).also { view -> - view.setCardInputListener(cardInputListener) - } - } - PaymentMethod.Type.Fpx -> { - AddPaymentMethodFpxView.create(this) - } - PaymentMethod.Type.Netbanking -> { - AddPaymentMethodNetbankingView.create(this) - } - else -> { - throw IllegalArgumentException( - "Unsupported Payment Method type: ${paymentMethodType.code}" - ) - } - } - } - - private fun createFooterView( - contentRoot: ViewGroup - ): View? { - return if (args.addPaymentMethodFooterLayoutId > 0) { - val footerView = layoutInflater.inflate( - args.addPaymentMethodFooterLayoutId, - contentRoot, - false - ) - footerView.id = R.id.stripe_add_payment_method_footer - if (footerView is TextView) { - LinkifyCompat.addLinks(footerView, Linkify.ALL) - ViewCompat.enableAccessibleClickableSpanSupport(footerView) - footerView.movementMethod = LinkMovementMethod.getInstance() - } - footerView - } else { - null - } - } - - public override fun onActionSave() { - viewModel.onSaveClicked() - createPaymentMethod(viewModel, addPaymentMethodView.createParams) - } - - internal fun createPaymentMethod( - viewModel: AddPaymentMethodViewModel, - params: PaymentMethodCreateParams? - ) { - if (params == null) { - return - } - - isProgressBarVisible = true - - lifecycleScope.launch { - viewModel.createPaymentMethod(params).fold( - onSuccess = { - if (shouldAttachToCustomer) { - attachPaymentMethodToCustomer(it) - } else { - finishWithPaymentMethod(it) - } - }, - onFailure = { - isProgressBarVisible = false - showError(it.message.orEmpty()) - } - ) - } - } - - private fun attachPaymentMethodToCustomer(paymentMethod: PaymentMethod) { - runCatching { - CustomerSession.getInstance() - }.fold( - onSuccess = { customerSession -> - lifecycleScope.launch { - viewModel.attachPaymentMethod( - customerSession, - paymentMethod - ).fold( - onSuccess = ::finishWithPaymentMethod, - onFailure = { - isProgressBarVisible = false - showError(it.message.orEmpty()) - } - ) - } - }, - onFailure = { - finishWithResult(AddPaymentMethodActivityStarter.Result.Failure(it)) - } - ) - } - - private fun finishWithPaymentMethod(paymentMethod: PaymentMethod) { - finishWithResult(AddPaymentMethodActivityStarter.Result.Success(paymentMethod)) - } - - private fun finishWithResult(result: AddPaymentMethodActivityStarter.Result) { - isProgressBarVisible = false - setResult( - Activity.RESULT_OK, - Intent().putExtras(result.toBundle()) - ) - finish() - } - - override fun onProgressBarVisibilityChanged(visible: Boolean) { - addPaymentMethodView.setCommunicatingProgress(visible) - } - - internal companion object { - internal const val PRODUCT_TOKEN: String = "AddPaymentMethodActivity" - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodActivityStarter.kt b/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodActivityStarter.kt deleted file mode 100644 index fe841a89eb0..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodActivityStarter.kt +++ /dev/null @@ -1,178 +0,0 @@ -package com.stripe.android.view - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import androidx.annotation.LayoutRes -import androidx.annotation.RestrictTo -import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment -import com.stripe.android.PaymentConfiguration -import com.stripe.android.model.PaymentMethod -import com.stripe.android.view.AddPaymentMethodActivityStarter.Args -import com.stripe.android.view.AddPaymentMethodActivityStarter.Companion.REQUEST_CODE -import com.stripe.android.view.AddPaymentMethodActivityStarter.Result.Companion.fromIntent -import kotlinx.parcelize.Parcelize - -/** - * A class to start [AddPaymentMethodActivity]. Arguments for the activity can be - * specified with [Args] and constructed with [Args.Builder]. - * - * The result will be returned with request code [REQUEST_CODE]. - */ -class AddPaymentMethodActivityStarter : ActivityStarter { - - constructor(activity: Activity) : super( - activity, - AddPaymentMethodActivity::class.java, - REQUEST_CODE - ) - - constructor(fragment: Fragment) : super( - fragment, - AddPaymentMethodActivity::class.java, - REQUEST_CODE - ) - - @Parcelize - data class Args internal constructor( - internal val billingAddressFields: BillingAddressFields, - internal val shouldAttachToCustomer: Boolean, - internal val isPaymentSessionActive: Boolean, - internal val paymentMethodType: PaymentMethod.Type, - internal val paymentConfiguration: PaymentConfiguration?, - @LayoutRes internal val addPaymentMethodFooterLayoutId: Int, - internal val windowFlags: Int? = null - ) : ActivityStarter.Args { - - class Builder { - private var billingAddressFields: BillingAddressFields = BillingAddressFields.PostalCode - private var shouldAttachToCustomer: Boolean = false - private var isPaymentSessionActive = false - private var paymentMethodType: PaymentMethod.Type? = PaymentMethod.Type.Card - private var paymentConfiguration: PaymentConfiguration? = null - private var windowFlags: Int? = null - - @LayoutRes - private var addPaymentMethodFooterLayoutId: Int = 0 - - /** - * If true, the created Payment Method will be attached to the current Customer - * using an already-initialized [com.stripe.android.CustomerSession]. - */ - fun setShouldAttachToCustomer(shouldAttachToCustomer: Boolean): Builder = apply { - this.shouldAttachToCustomer = shouldAttachToCustomer - } - - /** - * @param billingAddressFields the billing address fields to require on [AddPaymentMethodActivity] - */ - fun setBillingAddressFields( - billingAddressFields: BillingAddressFields - ): Builder = apply { - this.billingAddressFields = billingAddressFields - } - - /** - * Optional: specify the [PaymentMethod.Type] of the payment method to create based on - * the customer's input (i.e. the form that will be presented to the customer). - * If unspecified, defaults to [PaymentMethod.Type.Card]. - * Currently only [PaymentMethod.Type.Card] and [PaymentMethod.Type.Fpx] are supported. - */ - fun setPaymentMethodType(paymentMethodType: PaymentMethod.Type): Builder = apply { - this.paymentMethodType = paymentMethodType - } - - /** - * @param addPaymentMethodFooterLayoutId optional layout id that will be inflated and - * displayed beneath the payment details collection form on [AddPaymentMethodActivity] - */ - fun setAddPaymentMethodFooter( - @LayoutRes addPaymentMethodFooterLayoutId: Int - ): Builder = apply { - this.addPaymentMethodFooterLayoutId = addPaymentMethodFooterLayoutId - } - - /** - * @param windowFlags optional flags to set on the `Window` object of Stripe Activities - * - * See [WindowManager.LayoutParams](https://developer.android.com/reference/android/view/WindowManager.LayoutParams) - */ - fun setWindowFlags(windowFlags: Int?): Builder = apply { - this.windowFlags = windowFlags - } - - @JvmSynthetic - internal fun setIsPaymentSessionActive( - isPaymentSessionActive: Boolean - ): Builder = apply { - this.isPaymentSessionActive = isPaymentSessionActive - } - - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - @JvmSynthetic - fun setPaymentConfiguration( - paymentConfiguration: PaymentConfiguration? - ): Builder = apply { - this.paymentConfiguration = paymentConfiguration - } - - fun build(): Args { - return Args( - billingAddressFields = billingAddressFields, - shouldAttachToCustomer = shouldAttachToCustomer, - isPaymentSessionActive = isPaymentSessionActive, - paymentMethodType = paymentMethodType ?: PaymentMethod.Type.Card, - paymentConfiguration = paymentConfiguration, - addPaymentMethodFooterLayoutId = addPaymentMethodFooterLayoutId, - windowFlags = windowFlags - ) - } - } - - internal companion object { - @JvmSynthetic - internal fun create(intent: Intent): Args { - return requireNotNull(intent.getParcelableExtra(ActivityStarter.Args.EXTRA)) - } - } - } - - /** - * The result of a [AddPaymentMethodActivity]. - * - * Retrieve in `#onActivityResult()` using [fromIntent]. - */ - sealed class Result : ActivityStarter.Result { - override fun toBundle(): Bundle { - return bundleOf(ActivityStarter.Result.EXTRA to this) - } - - @Parcelize - data class Success internal constructor( - val paymentMethod: PaymentMethod - ) : Result() - - @Parcelize - data class Failure internal constructor( - val exception: Throwable - ) : Result() - - @Parcelize - data object Canceled : Result() - - companion object { - /** - * @return the [Result] object from the given `Intent`. [Canceled] by default. - */ - @JvmStatic - fun fromIntent(intent: Intent?): Result { - return intent?.getParcelableExtra(ActivityStarter.Result.EXTRA) ?: Canceled - } - } - } - - companion object { - const val REQUEST_CODE: Int = 6001 - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodCardView.kt b/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodCardView.kt deleted file mode 100644 index 77c5f466091..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodCardView.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.stripe.android.view - -import android.content.Context -import android.util.AttributeSet -import android.view.KeyEvent -import android.view.LayoutInflater -import android.view.View -import android.view.inputmethod.EditorInfo -import android.widget.TextView -import com.stripe.android.databinding.StripeAddPaymentMethodCardViewBinding -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.PaymentMethodCreateParams - -/** - * View for adding a payment method of type [PaymentMethod.Type.Card]. - * - * See [AddPaymentMethodActivity] for usage. - */ -internal class AddPaymentMethodCardView @JvmOverloads internal constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0, - private val billingAddressFields: BillingAddressFields = BillingAddressFields.PostalCode -) : AddPaymentMethodView(context, attrs, defStyleAttr) { - private val cardMultilineWidget: CardMultilineWidget - private val billingAddressWidget: ShippingInfoWidget - - override val createParams: PaymentMethodCreateParams? - get() { - return when (billingAddressFields) { - BillingAddressFields.Full -> { - val paymentMethodCard = cardMultilineWidget.paymentMethodCard - val billingDetails = this.billingDetails - if (paymentMethodCard != null && billingDetails != null) { - PaymentMethodCreateParams.create( - paymentMethodCard, - billingDetails - ) - } else { - null - } - } - BillingAddressFields.None -> { - cardMultilineWidget.paymentMethodCreateParams - } - BillingAddressFields.PostalCode -> { - cardMultilineWidget.paymentMethodCreateParams - } - } - } - - private val billingDetails: PaymentMethod.BillingDetails? - get() { - return if (billingAddressFields == BillingAddressFields.Full) { - billingAddressWidget.shippingInformation?.let { - PaymentMethod.BillingDetails.create(it) - } - } else { - null - } - } - - init { - val viewBinding = StripeAddPaymentMethodCardViewBinding.inflate( - LayoutInflater.from(context), - this, - true - ) - cardMultilineWidget = viewBinding.cardMultilineWidget - cardMultilineWidget.setShouldShowPostalCode( - billingAddressFields == BillingAddressFields.PostalCode - ) - - billingAddressWidget = viewBinding.billingAddressWidget - if (billingAddressFields == BillingAddressFields.Full) { - billingAddressWidget.visibility = View.VISIBLE - } - - (context as? AddPaymentMethodActivity?)?.let { - initEnterListeners(it) - } - } - - private fun initEnterListeners(activity: AddPaymentMethodActivity) { - val listener = OnEditorActionListenerImpl( - activity, - this, - KeyboardController(activity) - ) - - cardMultilineWidget.cardNumberEditText - .setOnEditorActionListener(listener) - cardMultilineWidget.expiryDateEditText - .setOnEditorActionListener(listener) - cardMultilineWidget.cvcEditText - .setOnEditorActionListener(listener) - cardMultilineWidget.postalCodeEditText - .setOnEditorActionListener(listener) - } - - override fun setCommunicatingProgress(communicating: Boolean) { - cardMultilineWidget.isEnabled = !communicating - } - - fun setCardInputListener(listener: CardInputListener?) { - cardMultilineWidget.setCardInputListener(listener) - } - - internal class OnEditorActionListenerImpl( - private val activity: AddPaymentMethodActivity, - private val addPaymentMethodCardView: AddPaymentMethodCardView, - private val keyboardController: KeyboardController - ) : TextView.OnEditorActionListener { - - override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { - if (actionId == EditorInfo.IME_ACTION_DONE) { - if (addPaymentMethodCardView.createParams != null) { - keyboardController.hide() - } - activity.onActionSave() - return true - } - return false - } - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodContract.kt b/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodContract.kt deleted file mode 100644 index 24b2060ecf2..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodContract.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.stripe.android.view - -import android.content.Context -import android.content.Intent -import androidx.activity.result.contract.ActivityResultContract - -internal class AddPaymentMethodContract : - ActivityResultContract() { - - override fun createIntent( - context: Context, - input: AddPaymentMethodActivityStarter.Args - ): Intent { - return Intent(context, AddPaymentMethodActivity::class.java) - .putExtra(ActivityStarter.Args.EXTRA, input) - } - - override fun parseResult( - resultCode: Int, - intent: Intent? - ): AddPaymentMethodActivityStarter.Result { - return AddPaymentMethodActivityStarter.Result.fromIntent(intent) - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodFpxView.kt b/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodFpxView.kt deleted file mode 100644 index f9d9d189a7d..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodFpxView.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.stripe.android.view - -import android.util.AttributeSet -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.DefaultItemAnimator -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.stripe.android.R -import com.stripe.android.databinding.StripeBankListPaymentMethodBinding -import com.stripe.android.model.BankStatuses -import com.stripe.android.model.PaymentMethodCreateParams -import kotlinx.coroutines.launch - -internal class AddPaymentMethodFpxView @JvmOverloads internal constructor( - activity: FragmentActivity, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : AddPaymentMethodView(activity, attrs, defStyleAttr) { - - private var fpxBankStatuses: BankStatuses = BankStatuses() - - private val fpxAdapter = AddPaymentMethodListAdapter( - ThemeConfig(activity), - items = FpxBank.entries, - itemSelectedCallback = { - viewModel.selectedPosition = it - } - ) - - private val viewModel: FpxViewModel by lazy { - ViewModelProvider( - activity, - FpxViewModel.Factory(activity.application) - ).get(FpxViewModel::class.java) - } - - override val createParams: PaymentMethodCreateParams? - get() { - return fpxAdapter.selectedPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { - val fpxBank = FpxBank.entries[it] - PaymentMethodCreateParams.create( - PaymentMethodCreateParams.Fpx(bank = fpxBank.code) - ) - } - } - - init { - val viewBinding = StripeBankListPaymentMethodBinding.inflate( - activity.layoutInflater, - this, - true - ) - - // an id is required for state to be saved - id = R.id.stripe_payment_methods_add_fpx - - activity.lifecycleScope.launch { - viewModel.fpxBankStatues.collect(::onFpxBankStatusesUpdated) - } - - with(viewBinding.bankList) { - adapter = fpxAdapter - setHasFixedSize(true) - layoutManager = LinearLayoutManager(activity) - itemAnimator = DefaultItemAnimator() - } - - viewModel.selectedPosition?.let { - fpxAdapter.updateSelected(it) - } - } - - private fun onFpxBankStatusesUpdated(fpxBankStatuses: BankStatuses?) { - fpxBankStatuses?.let { - updateStatuses(it) - } - } - - private fun updateStatuses(fpxBankStatuses: BankStatuses) { - this.fpxBankStatuses = fpxBankStatuses - this.fpxAdapter.bankStatuses = fpxBankStatuses - - // flag offline bank - FpxBank.entries.indices - .filterNot { position -> - fpxBankStatuses.isOnline(getItem(position)) - } - .forEach { position -> - fpxAdapter.notifyAdapterItemChanged(position) - } - } - - private fun getItem(position: Int): FpxBank { - return FpxBank.entries[position] - } - - internal companion object { - @JvmSynthetic - internal fun create(activity: FragmentActivity): AddPaymentMethodFpxView { - return AddPaymentMethodFpxView(activity) - } - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodListAdapter.kt b/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodListAdapter.kt deleted file mode 100644 index 10578cc8c86..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodListAdapter.kt +++ /dev/null @@ -1,109 +0,0 @@ -package com.stripe.android.view - -import android.content.res.ColorStateList -import android.content.res.Resources -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.core.widget.ImageViewCompat -import androidx.recyclerview.widget.RecyclerView -import com.stripe.android.R -import com.stripe.android.databinding.StripeBankItemBinding -import com.stripe.android.model.BankStatuses - -internal class AddPaymentMethodListAdapter( - val themeConfig: ThemeConfig, - val items: List, - val itemSelectedCallback: (Int) -> Unit -) : RecyclerView.Adapter() { - - internal var bankStatuses: BankStatuses? = null - - var selectedPosition = RecyclerView.NO_POSITION - set(value) { - if (value != field) { - if (selectedPosition != RecyclerView.NO_POSITION) { - notifyItemChanged(field) - } - notifyItemChanged(value) - itemSelectedCallback(value) - } - field = value - } - - init { - setHasStableIds(true) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return BankViewHolder( - StripeBankItemBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ), - themeConfig - ) - } - - override fun getItemCount(): Int { - return items.size - } - - override fun getItemId(position: Int): Long { - return position.toLong() - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - val item = items[position] - - holder.itemView.setOnClickListener { - selectedPosition = holder.bindingAdapterPosition - } - - val bankViewHolder = holder as BankViewHolder - bankViewHolder.setSelected(position == selectedPosition) - - bankViewHolder.update(bank = item, isOnline = bankStatuses?.isOnline(item) ?: true) - } - - internal fun updateSelected(position: Int) { - selectedPosition = position - notifyItemChanged(position) - } - - fun notifyAdapterItemChanged(position: Int) { - notifyItemChanged(position) - } - - internal class BankViewHolder constructor( - private val viewBinding: StripeBankItemBinding, - private val themeConfig: ThemeConfig - ) : RecyclerView.ViewHolder(viewBinding.root) { - private val resources: Resources = itemView.resources - - fun update(bank: Bank, isOnline: Boolean) { - viewBinding.name.text = if (isOnline) { - bank.displayName - } else { - resources.getString( - R.string.stripe_fpx_bank_offline, - bank.displayName - ) - } - - bank.brandIconResId?.let { - viewBinding.icon.setImageResource(it) - } - } - - internal fun setSelected(isSelected: Boolean) { - viewBinding.name.setTextColor(themeConfig.getTextColor(isSelected)) - ImageViewCompat.setImageTintList( - viewBinding.checkIcon, - ColorStateList.valueOf(themeConfig.getTintColor(isSelected)) - ) - viewBinding.checkIcon.isVisible = isSelected - } - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodNetbankingView.kt b/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodNetbankingView.kt deleted file mode 100644 index 70f59307675..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodNetbankingView.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.stripe.android.view - -import android.util.AttributeSet -import androidx.fragment.app.FragmentActivity -import androidx.recyclerview.widget.DefaultItemAnimator -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.stripe.android.R -import com.stripe.android.databinding.StripeBankListPaymentMethodBinding -import com.stripe.android.model.PaymentMethodCreateParams - -internal class AddPaymentMethodNetbankingView @JvmOverloads internal constructor( - activity: FragmentActivity, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : AddPaymentMethodView(activity, attrs, defStyleAttr) { - private var selectedPosition: Int? = null - - private val netbankingAdapter = AddPaymentMethodListAdapter( - ThemeConfig(activity), - items = NetbankingBank.entries, - itemSelectedCallback = { - this.selectedPosition = it - } - ) - - override val createParams: PaymentMethodCreateParams? - get() { - return netbankingAdapter.selectedPosition.takeIf { it != RecyclerView.NO_POSITION } - ?.let { - val netbankingBank = NetbankingBank.entries[netbankingAdapter.selectedPosition] - - return PaymentMethodCreateParams.create( - PaymentMethodCreateParams.Netbanking(bank = netbankingBank.code) - ) - } - } - - init { - val viewBinding = StripeBankListPaymentMethodBinding.inflate( - activity.layoutInflater, - this, - true - ) - - id = R.id.stripe_payment_methods_add_netbanking - - with(viewBinding.bankList) { - adapter = netbankingAdapter - setHasFixedSize(true) - layoutManager = LinearLayoutManager(activity) - itemAnimator = DefaultItemAnimator() - } - - selectedPosition?.let { - netbankingAdapter.updateSelected(it) - } - } - - internal companion object { - @JvmSynthetic - internal fun create(activity: FragmentActivity): AddPaymentMethodNetbankingView { - return AddPaymentMethodNetbankingView(activity) - } - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodView.kt b/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodView.kt deleted file mode 100644 index bda5d79030b..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodView.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.stripe.android.view - -import android.content.Context -import android.util.AttributeSet -import android.widget.FrameLayout -import com.stripe.android.model.PaymentMethodCreateParams - -internal abstract class AddPaymentMethodView constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : FrameLayout(context, attrs, defStyleAttr) { - - /** - * A [PaymentMethodCreateParams] if the customer's input for the given payment - * method type is valid; otherwise, `null`. - */ - abstract val createParams: PaymentMethodCreateParams? - - open fun setCommunicatingProgress(communicating: Boolean) {} -} diff --git a/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodViewModel.kt b/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodViewModel.kt deleted file mode 100644 index 995d1ab0ebc..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/AddPaymentMethodViewModel.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.stripe.android.view - -import android.app.Application -import androidx.annotation.VisibleForTesting -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.createSavedStateHandle -import androidx.lifecycle.viewmodel.CreationExtras -import com.stripe.android.ApiResultCallback -import com.stripe.android.CustomerSession -import com.stripe.android.PaymentSession -import com.stripe.android.Stripe -import com.stripe.android.analytics.PaymentSessionEventReporter -import com.stripe.android.analytics.PaymentSessionEventReporterFactory -import com.stripe.android.analytics.SessionSavedStateHandler -import com.stripe.android.core.StripeError -import com.stripe.android.core.utils.requireApplication -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.PaymentMethodCreateParams -import com.stripe.android.view.i18n.ErrorMessageTranslator -import com.stripe.android.view.i18n.TranslatorManager -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -internal class AddPaymentMethodViewModel( - application: Application, - private val savedStateHandle: SavedStateHandle, - private val stripe: Stripe, - private val args: AddPaymentMethodActivityStarter.Args, - private val errorMessageTranslator: ErrorMessageTranslator = - TranslatorManager.getErrorMessageTranslator(), - private val eventReporter: PaymentSessionEventReporter = - PaymentSessionEventReporterFactory.create(application) -) : AndroidViewModel(application) { - - private val productUsage: Set = listOfNotNull( - AddPaymentMethodActivity.PRODUCT_TOKEN, - PaymentSession.PRODUCT_TOKEN.takeIf { args.isPaymentSessionActive } - ).toSet() - - private var formShownEventReported: Boolean - get() = savedStateHandle.get(FORM_SHOWN_EVENT_REPORTED_KEY) ?: false - set(value) { - savedStateHandle[FORM_SHOWN_EVENT_REPORTED_KEY] = value - } - - private var formInteractedEventReported: Boolean - get() = savedStateHandle.get(FORM_INTERACTED_EVENT_REPORTED_KEY) ?: false - set(value) { - savedStateHandle[FORM_INTERACTED_EVENT_REPORTED_KEY] = value - } - - init { - SessionSavedStateHandler.attachTo(this, savedStateHandle) - - if (!formShownEventReported) { - eventReporter.onFormShown(args.paymentMethodType.code) - formShownEventReported = true - } - } - - internal suspend fun createPaymentMethod( - params: PaymentMethodCreateParams - ): Result = suspendCoroutine { continuation -> - stripe.createPaymentMethod( - paymentMethodCreateParams = updatedPaymentMethodCreateParams(params), - callback = object : ApiResultCallback { - override fun onSuccess(result: PaymentMethod) { - continuation.resume(Result.success(result)) - } - - override fun onError(e: Exception) { - continuation.resume(Result.failure(e)) - } - } - ) - } - - @VisibleForTesting - internal fun updatedPaymentMethodCreateParams( - params: PaymentMethodCreateParams - ) = params.copy(productUsage = productUsage) - - @JvmSynthetic - internal suspend fun attachPaymentMethod( - customerSession: CustomerSession, - paymentMethod: PaymentMethod - ): Result = suspendCoroutine { continuation -> - customerSession.attachPaymentMethod( - paymentMethodId = paymentMethod.id.orEmpty(), - productUsage = productUsage, - listener = object : CustomerSession.PaymentMethodRetrievalListener { - override fun onPaymentMethodRetrieved(paymentMethod: PaymentMethod) { - continuation.resume(Result.success(paymentMethod)) - } - - override fun onError( - errorCode: Int, - errorMessage: String, - stripeError: StripeError? - ) { - continuation.resume( - Result.failure( - RuntimeException( - errorMessageTranslator.translate( - errorCode, - errorMessage, - stripeError - ) - ) - ) - ) - } - } - ) - } - - internal fun onFormShown() { - if (!formShownEventReported) { - eventReporter.onFormShown(args.paymentMethodType.code) - formShownEventReported = true - } - } - - internal fun onFormInteracted() { - if (!formInteractedEventReported) { - eventReporter.onFormInteracted(args.paymentMethodType.code) - formInteractedEventReported = true - } - } - - internal fun onCardNumberCompleted() { - eventReporter.onCardNumberCompleted() - } - - internal fun onSaveClicked() { - eventReporter.onDoneButtonTapped(args.paymentMethodType.code) - } - - internal class Factory( - private val stripe: Stripe, - private val args: AddPaymentMethodActivityStarter.Args - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class, extras: CreationExtras): T { - return AddPaymentMethodViewModel( - application = extras.requireApplication(), - savedStateHandle = extras.createSavedStateHandle(), - stripe = stripe, - args = args - ) as T - } - } - - private companion object { - private const val FORM_SHOWN_EVENT_REPORTED_KEY = "FROM_SHOWN_EVENT_REPORTED" - private const val FORM_INTERACTED_EVENT_REPORTED_KEY = "FROM_INTERACTED_EVENT_REPORTED" - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/BillingAddressFields.kt b/payments-core/src/main/java/com/stripe/android/view/BillingAddressFields.kt deleted file mode 100644 index 092624afc0f..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/BillingAddressFields.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.stripe.android.view - -/** - * Configure [AddPaymentMethodActivity]'s UI and validation logic for billing address fields - */ -enum class BillingAddressFields { - /** - * Do not require any customer billing details when adding a new Payment Method - */ - None, - - /** - * Require the customer's postal code when adding a new Payment Method - */ - PostalCode, - - /** - * Require the customer's full billing address when adding a new Payment Method - */ - Full -} diff --git a/payments-core/src/main/java/com/stripe/android/view/DeletePaymentMethodDialogFactory.kt b/payments-core/src/main/java/com/stripe/android/view/DeletePaymentMethodDialogFactory.kt deleted file mode 100644 index 97aba142433..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/DeletePaymentMethodDialogFactory.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.stripe.android.view - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.stripe.android.CustomerSession -import com.stripe.android.R -import com.stripe.android.core.StripeError -import com.stripe.android.model.PaymentMethod - -internal class DeletePaymentMethodDialogFactory internal constructor( - private val context: Context, - private val adapter: PaymentMethodsAdapter, - private val cardDisplayTextFactory: CardDisplayTextFactory, - private val customerSession: Result, - private val productUsage: Set, - private val onDeletedPaymentMethodCallback: (PaymentMethod) -> Unit -) { - @JvmSynthetic - fun create(paymentMethod: PaymentMethod): AlertDialog { - val message = paymentMethod.card?.let { - cardDisplayTextFactory.createUnstyled(it) - } - return AlertDialog.Builder(context, R.style.StripeAlertDialogStyle) - .setTitle(R.string.stripe_delete_payment_method_prompt_title) - .setMessage(message) - .setPositiveButton(android.R.string.ok) { _, _ -> - onDeletedPaymentMethod(paymentMethod) - } - .setNegativeButton(android.R.string.cancel) { _, _ -> - adapter.resetPaymentMethod(paymentMethod) - } - .setOnCancelListener { - adapter.resetPaymentMethod(paymentMethod) - } - .create() - } - - @JvmSynthetic - internal fun onDeletedPaymentMethod(paymentMethod: PaymentMethod) { - adapter.deletePaymentMethod(paymentMethod) - - paymentMethod.id?.let { paymentMethodId -> - customerSession.getOrNull()?.detachPaymentMethod( - paymentMethodId = paymentMethodId, - productUsage = productUsage, - listener = PaymentMethodDeleteListener() - ) - } - - onDeletedPaymentMethodCallback(paymentMethod) - } - - private class PaymentMethodDeleteListener : CustomerSession.PaymentMethodRetrievalListener { - override fun onPaymentMethodRetrieved(paymentMethod: PaymentMethod) { - } - - override fun onError(errorCode: Int, errorMessage: String, stripeError: StripeError?) { - } - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/FpxViewModel.kt b/payments-core/src/main/java/com/stripe/android/view/FpxViewModel.kt deleted file mode 100644 index ce54b254126..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/FpxViewModel.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.stripe.android.view - -import android.app.Application -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import com.stripe.android.PaymentConfiguration -import com.stripe.android.core.networking.ApiRequest -import com.stripe.android.model.BankStatuses -import com.stripe.android.networking.StripeApiRepository -import com.stripe.android.networking.StripeRepository -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch - -internal class FpxViewModel internal constructor( - application: Application, - private val publishableKey: String, - private val stripeRepository: StripeRepository -) : AndroidViewModel(application) { - internal var selectedPosition: Int? = null - - private val _fpxBankStatues: MutableStateFlow = MutableStateFlow(null) - val fpxBankStatues: StateFlow = _fpxBankStatues.asStateFlow() - - init { - viewModelScope.launch { - _fpxBankStatues.value = stripeRepository.getFpxBankStatus( - options = ApiRequest.Options(publishableKey), - ).getOrElse { - BankStatuses() - } - } - } - - internal class Factory( - private val application: Application - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - val publishableKey = PaymentConfiguration.getInstance(application).publishableKey - val stripeRepository = StripeApiRepository( - application, - { publishableKey } - ) - - return FpxViewModel(application, publishableKey, stripeRepository) as T - } - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/PaymentFlowActivity.kt b/payments-core/src/main/java/com/stripe/android/view/PaymentFlowActivity.kt deleted file mode 100644 index 3efd1560c23..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/PaymentFlowActivity.kt +++ /dev/null @@ -1,259 +0,0 @@ -package com.stripe.android.view - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import android.view.ViewGroup -import androidx.activity.addCallback -import androidx.activity.viewModels -import androidx.lifecycle.lifecycleScope -import androidx.viewpager.widget.ViewPager -import com.stripe.android.BASIC_INTEGRATION_DEPRECATION_WARNING -import com.stripe.android.CustomerSession -import com.stripe.android.PaymentSession.Companion.EXTRA_PAYMENT_SESSION_DATA -import com.stripe.android.PaymentSessionConfig -import com.stripe.android.PaymentSessionData -import com.stripe.android.R -import com.stripe.android.databinding.StripePaymentFlowActivityBinding -import com.stripe.android.model.ShippingInformation -import com.stripe.android.model.ShippingMethod -import com.stripe.android.utils.argsAreInvalid -import kotlinx.coroutines.launch - -/** - * Activity containing a two-part payment flow that allows users to provide a shipping address - * as well as select a shipping method. - */ -@Deprecated(BASIC_INTEGRATION_DEPRECATION_WARNING) -class PaymentFlowActivity : StripeActivity() { - - private val viewBinding: StripePaymentFlowActivityBinding by lazy { - viewStub.layoutResource = R.layout.stripe_payment_flow_activity - val root = viewStub.inflate() as ViewGroup - StripePaymentFlowActivityBinding.bind(root) - } - - private val viewPager: PaymentFlowViewPager by lazy { - viewBinding.shippingFlowViewpager - } - - private val customerSession: CustomerSession by lazy { - CustomerSession.getInstance() - } - private val args: PaymentFlowActivityStarter.Args by lazy { - PaymentFlowActivityStarter.Args.create(intent) - } - private val paymentSessionConfig: PaymentSessionConfig by lazy { - args.paymentSessionConfig - } - private val viewModel: PaymentFlowViewModel by viewModels { - PaymentFlowViewModel.Factory(customerSession, args.paymentSessionData) - } - - private val paymentFlowPagerAdapter: PaymentFlowPagerAdapter by lazy { - PaymentFlowPagerAdapter( - this, - paymentSessionConfig, - paymentSessionConfig.allowedShippingCountryCodes - ) { - viewModel.selectedShippingMethod = it - } - } - - private val keyboardController: KeyboardController by lazy { - KeyboardController(this) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - if (argsAreInvalid { args }) { - return - } - - val args = PaymentFlowActivityStarter.Args.create(intent) - args.windowFlags?.let { window.addFlags(it) } - - val shippingInformation = - viewModel.submittedShippingInfo - ?: paymentSessionConfig.prepopulatedShippingInfo - - paymentFlowPagerAdapter.shippingMethods = viewModel.shippingMethods - paymentFlowPagerAdapter.isShippingInfoSubmitted = viewModel.isShippingInfoSubmitted - paymentFlowPagerAdapter.shippingInformation = shippingInformation - paymentFlowPagerAdapter.selectedShippingMethod = viewModel.selectedShippingMethod - - val onBackPressedCallback = onBackPressedDispatcher.addCallback { - viewModel.currentPage -= 1 - viewPager.currentItem = viewModel.currentPage - } - - viewPager.adapter = paymentFlowPagerAdapter - viewPager.addOnPageChangeListener( - object : ViewPager.OnPageChangeListener { - override fun onPageScrolled(i: Int, v: Float, i1: Int) {} - - override fun onPageSelected(i: Int) { - title = paymentFlowPagerAdapter.getPageTitle(i) - if (paymentFlowPagerAdapter.getPageAt(i) === PaymentFlowPage.ShippingInfo) { - viewModel.isShippingInfoSubmitted = false - paymentFlowPagerAdapter.isShippingInfoSubmitted = false - } - - onBackPressedCallback.isEnabled = hasPreviousPage() - } - - override fun onPageScrollStateChanged(i: Int) { - } - } - ) - - viewPager.currentItem = viewModel.currentPage - onBackPressedCallback.isEnabled = hasPreviousPage() - - title = paymentFlowPagerAdapter.getPageTitle(viewPager.currentItem) - } - - public override fun onActionSave() { - if (PaymentFlowPage.ShippingInfo == - paymentFlowPagerAdapter.getPageAt(viewPager.currentItem) - ) { - onShippingInfoSubmitted() - } else { - onShippingMethodSave() - } - } - - @JvmSynthetic - internal fun onShippingInfoSaved( - shippingInformation: ShippingInformation?, - shippingMethods: List = emptyList() - ) { - onShippingMethodsReady(shippingMethods) - viewModel.paymentSessionData = viewModel.paymentSessionData.copy( - shippingInformation = shippingInformation - ) - } - - private fun onShippingInfoValidated(shippingMethods: List) { - viewModel.paymentSessionData.shippingInformation?.let { shippingInfo -> - lifecycleScope.launch { - viewModel.saveCustomerShippingInformation(shippingInfo).fold( - onSuccess = { - onShippingInfoSaved( - it.shippingInformation, - shippingMethods - ) - }, - onFailure = { - showError(it.message.orEmpty()) - } - ) - } - } - } - - private fun onShippingMethodsReady( - shippingMethods: List - ) { - isProgressBarVisible = false - paymentFlowPagerAdapter.shippingMethods = shippingMethods - paymentFlowPagerAdapter.isShippingInfoSubmitted = true - - if (hasNextPage()) { - viewModel.currentPage += 1 - viewPager.currentItem = viewModel.currentPage - } else { - finishWithData(viewModel.paymentSessionData) - } - } - - private fun onShippingInfoSubmitted() { - keyboardController.hide() - - shippingInfo?.let { shippingInfo -> - viewModel.paymentSessionData = viewModel.paymentSessionData.copy( - shippingInformation = shippingInfo - ) - isProgressBarVisible = true - - validateShippingInformation( - paymentSessionConfig.shippingInformationValidator, - paymentSessionConfig.shippingMethodsFactory, - shippingInfo - ) - } - } - - private val shippingInfo: ShippingInformation? - get() { - return viewPager - .findViewById(R.id.shipping_info_widget) - .shippingInformation - } - - private fun hasNextPage(): Boolean { - return viewPager.currentItem + 1 < paymentFlowPagerAdapter.count - } - - private fun hasPreviousPage(): Boolean { - return viewPager.currentItem != 0 - } - - private fun onShippingMethodSave() { - val selectedShippingMethod = - viewPager - .findViewById(R.id.select_shipping_method_widget) - .selectedShippingMethod - finishWithData( - viewModel.paymentSessionData.copy( - shippingMethod = selectedShippingMethod - ) - ) - } - - private fun validateShippingInformation( - shippingInfoValidator: PaymentSessionConfig.ShippingInformationValidator, - shippingMethodsFactory: PaymentSessionConfig.ShippingMethodsFactory?, - shippingInformation: ShippingInformation - ) { - lifecycleScope.launch { - viewModel.validateShippingInformation( - shippingInfoValidator, - shippingMethodsFactory, - shippingInformation - ).fold( - // show shipping methods screen - onSuccess = ::onShippingInfoValidated, - - // show error on current screen - onFailure = ::onShippingInfoError - ) - } - } - - private fun onShippingInfoError(error: Throwable) { - val errorMessage = error.message - isProgressBarVisible = false - if (!errorMessage.isNullOrEmpty()) { - showError(errorMessage) - } else { - showError(getString(R.string.stripe_invalid_shipping_information)) - } - viewModel.paymentSessionData = viewModel.paymentSessionData.copy( - shippingInformation = null - ) - } - - private fun finishWithData(paymentSessionData: PaymentSessionData) { - setResult( - Activity.RESULT_OK, - Intent().putExtra(EXTRA_PAYMENT_SESSION_DATA, paymentSessionData) - ) - finish() - } - - internal companion object { - internal const val PRODUCT_TOKEN: String = "PaymentFlowActivity" - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/PaymentFlowActivityStarter.kt b/payments-core/src/main/java/com/stripe/android/view/PaymentFlowActivityStarter.kt deleted file mode 100644 index 7bf9cc2d144..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/PaymentFlowActivityStarter.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.stripe.android.view - -import android.app.Activity -import android.content.Intent -import androidx.fragment.app.Fragment -import com.stripe.android.PaymentSessionConfig -import com.stripe.android.PaymentSessionData -import kotlinx.parcelize.Parcelize - -class PaymentFlowActivityStarter : - ActivityStarter { - - constructor(activity: Activity, config: PaymentSessionConfig) : super( - activity, - PaymentFlowActivity::class.java, - REQUEST_CODE - ) - - constructor(fragment: Fragment, config: PaymentSessionConfig) : super( - fragment, - PaymentFlowActivity::class.java, - REQUEST_CODE - ) - - @Parcelize - data class Args internal constructor( - internal val paymentSessionConfig: PaymentSessionConfig, - internal val paymentSessionData: PaymentSessionData, - internal val isPaymentSessionActive: Boolean = false, - internal val windowFlags: Int? = null - ) : ActivityStarter.Args { - class Builder { - private var paymentSessionConfig: PaymentSessionConfig? = null - private var paymentSessionData: PaymentSessionData? = null - private var isPaymentSessionActive = false - private var windowFlags: Int? = null - - fun setPaymentSessionConfig( - paymentSessionConfig: PaymentSessionConfig? - ): Builder = apply { - this.paymentSessionConfig = paymentSessionConfig - } - - fun setPaymentSessionData(paymentSessionData: PaymentSessionData?): Builder = apply { - this.paymentSessionData = paymentSessionData - } - - fun setIsPaymentSessionActive(isPaymentSessionActive: Boolean): Builder = apply { - this.isPaymentSessionActive = isPaymentSessionActive - } - - /** - * @param windowFlags optional flags to set on the `Window` object of Stripe Activities - * - * See [WindowManager.LayoutParams](https://developer.android.com/reference/android/view/WindowManager.LayoutParams) - */ - fun setWindowFlags(windowFlags: Int?): Builder = apply { - this.windowFlags = windowFlags - } - - fun build(): Args { - return Args( - paymentSessionConfig = requireNotNull(paymentSessionConfig) { - "PaymentFlowActivity launched without PaymentSessionConfig" - }, - paymentSessionData = requireNotNull(paymentSessionData) { - "PaymentFlowActivity launched without PaymentSessionData" - }, - isPaymentSessionActive = isPaymentSessionActive, - windowFlags = windowFlags - ) - } - } - - companion object { - @JvmStatic - fun create(intent: Intent): Args { - return requireNotNull(intent.getParcelableExtra(ActivityStarter.Args.EXTRA)) - } - } - } - - companion object { - const val REQUEST_CODE: Int = 6002 - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/PaymentFlowPage.kt b/payments-core/src/main/java/com/stripe/android/view/PaymentFlowPage.kt deleted file mode 100644 index 8babf4c360f..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/PaymentFlowPage.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.stripe.android.view - -import androidx.annotation.StringRes -import com.stripe.android.R - -internal enum class PaymentFlowPage( - @param:StringRes @field:StringRes @get:StringRes - val titleResId: Int -) { - ShippingInfo(R.string.stripe_title_add_an_address), - ShippingMethod(R.string.stripe_title_select_shipping_method) -} diff --git a/payments-core/src/main/java/com/stripe/android/view/PaymentFlowPagerAdapter.kt b/payments-core/src/main/java/com/stripe/android/view/PaymentFlowPagerAdapter.kt deleted file mode 100644 index 7abe54870a1..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/PaymentFlowPagerAdapter.kt +++ /dev/null @@ -1,188 +0,0 @@ -package com.stripe.android.view - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import androidx.viewpager.widget.PagerAdapter -import com.stripe.android.PaymentSessionConfig -import com.stripe.android.databinding.StripeShippingInfoPageBinding -import com.stripe.android.databinding.StripeShippingMethodPageBinding -import com.stripe.android.model.ShippingInformation -import com.stripe.android.model.ShippingMethod -import kotlin.properties.Delegates - -internal class PaymentFlowPagerAdapter( - private val context: Context, - private val paymentSessionConfig: PaymentSessionConfig, - private val allowedShippingCountryCodes: Set = emptySet(), - private val onShippingMethodSelectedCallback: (ShippingMethod) -> Unit = {} -) : PagerAdapter() { - private val pages: List - get() { - return listOfNotNull( - PaymentFlowPage.ShippingInfo.takeIf { - paymentSessionConfig.isShippingInfoRequired - }, - PaymentFlowPage.ShippingMethod.takeIf { - paymentSessionConfig.isShippingMethodRequired && - (!paymentSessionConfig.isShippingInfoRequired || isShippingInfoSubmitted) - } - ) - } - - internal var shippingInformation: ShippingInformation? = null - set(value) { - field = value - notifyDataSetChanged() - } - - internal var isShippingInfoSubmitted: Boolean = false - set(value) { - field = value - notifyDataSetChanged() - } - - private var shouldRecreateShippingMethodsScreen = false - - internal var shippingMethods: List by Delegates.observable( - emptyList() - ) { _, oldValue, newValue -> - shouldRecreateShippingMethodsScreen = newValue != oldValue - } - - internal var selectedShippingMethod: ShippingMethod? by Delegates.observable( - null - ) { _, oldValue, newValue -> - shouldRecreateShippingMethodsScreen = newValue != oldValue - } - - override fun instantiateItem(collection: ViewGroup, position: Int): Any { - val page = pages[position] - val viewHolder = when (page) { - PaymentFlowPage.ShippingInfo -> { - PaymentFlowViewHolder.ShippingInformationViewHolder(collection) - } - PaymentFlowPage.ShippingMethod -> { - PaymentFlowViewHolder.ShippingMethodViewHolder(collection) - } - } - when (viewHolder) { - is PaymentFlowViewHolder.ShippingInformationViewHolder -> { - viewHolder.bind( - paymentSessionConfig, - shippingInformation, - allowedShippingCountryCodes - ) - } - is PaymentFlowViewHolder.ShippingMethodViewHolder -> { - viewHolder.bind( - shippingMethods, - selectedShippingMethod, - onShippingMethodSelectedCallback - ) - } - } - collection.addView(viewHolder.itemView) - viewHolder.itemView.tag = page - return viewHolder.itemView - } - - override fun destroyItem(collection: ViewGroup, position: Int, view: Any) { - collection.removeView(view as View) - } - - override fun getCount(): Int { - return pages.size - } - - internal fun getPageAt(position: Int): PaymentFlowPage? { - return pages.getOrNull(position) - } - - override fun isViewFromObject(view: View, o: Any): Boolean { - return view === o - } - - override fun getPageTitle(position: Int): CharSequence? { - return context.getString(pages[position].titleResId) - } - - override fun getItemPosition(obj: Any): Int { - return if (obj is View && obj.tag == PaymentFlowPage.ShippingMethod && - shouldRecreateShippingMethodsScreen - ) { - // if the shipping methods screen needs to be updated, return `POSITION_NONE` to - // indicate that the item is no longer valid and should be recreated - shouldRecreateShippingMethodsScreen = false - POSITION_NONE - } else { - super.getItemPosition(obj) - } - } - - internal sealed class PaymentFlowViewHolder( - itemView: View - ) : RecyclerView.ViewHolder(itemView) { - class ShippingInformationViewHolder( - viewBinding: StripeShippingInfoPageBinding - ) : PaymentFlowViewHolder(viewBinding.root) { - constructor( - root: ViewGroup - ) : this( - StripeShippingInfoPageBinding.inflate( - LayoutInflater.from(root.context), - root, - false - ) - ) - - private val shippingInfoWidget = viewBinding.shippingInfoWidget - - fun bind( - paymentSessionConfig: PaymentSessionConfig, - shippingInformation: ShippingInformation?, - allowedShippingCountryCodes: Set - ) { - shippingInfoWidget.hiddenFields = paymentSessionConfig.hiddenShippingInfoFields - shippingInfoWidget.optionalFields = paymentSessionConfig.optionalShippingInfoFields - shippingInfoWidget - .setAllowedCountryCodes(allowedShippingCountryCodes) - shippingInfoWidget - .populateShippingInfo(shippingInformation) - } - } - - class ShippingMethodViewHolder( - viewBinding: StripeShippingMethodPageBinding - ) : PaymentFlowViewHolder(viewBinding.root) { - constructor( - root: ViewGroup - ) : this( - StripeShippingMethodPageBinding.inflate( - LayoutInflater.from(root.context), - root, - false - ) - ) - - private val shippingMethodWidget = - viewBinding.selectShippingMethodWidget - - fun bind( - shippingMethods: List, - selectedShippingMethod: ShippingMethod?, - onShippingMethodSelectedCallback: (ShippingMethod) -> Unit - ) { - shippingMethodWidget.setShippingMethods(shippingMethods) - shippingMethodWidget.setShippingMethodSelectedCallback( - onShippingMethodSelectedCallback - ) - selectedShippingMethod?.let { - shippingMethodWidget.setSelectedShippingMethod(it) - } - } - } - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/PaymentFlowViewModel.kt b/payments-core/src/main/java/com/stripe/android/view/PaymentFlowViewModel.kt deleted file mode 100644 index 0b9821c3b67..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/PaymentFlowViewModel.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.stripe.android.view - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.stripe.android.CustomerSession -import com.stripe.android.PaymentSession -import com.stripe.android.PaymentSessionConfig -import com.stripe.android.PaymentSessionData -import com.stripe.android.core.StripeError -import com.stripe.android.model.Customer -import com.stripe.android.model.ShippingInformation -import com.stripe.android.model.ShippingMethod -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -internal class PaymentFlowViewModel( - private val customerSession: CustomerSession, - internal var paymentSessionData: PaymentSessionData, - private val workContext: CoroutineContext -) : ViewModel() { - internal var shippingMethods: List = emptyList() - internal var isShippingInfoSubmitted: Boolean = false - - internal var selectedShippingMethod: ShippingMethod? = null - internal var submittedShippingInfo: ShippingInformation? = null - - internal var currentPage: Int = 0 - - @JvmSynthetic - internal suspend fun saveCustomerShippingInformation( - shippingInformation: ShippingInformation - ): Result = suspendCoroutine { continuation -> - submittedShippingInfo = shippingInformation - customerSession.setCustomerShippingInformation( - shippingInformation = shippingInformation, - productUsage = PRODUCT_USAGE, - listener = object : CustomerSession.CustomerRetrievalListener { - override fun onCustomerRetrieved(customer: Customer) { - isShippingInfoSubmitted = true - continuation.resume(Result.success(customer)) - } - - override fun onError( - errorCode: Int, - errorMessage: String, - stripeError: StripeError? - ) { - isShippingInfoSubmitted = false - continuation.resume( - Result.failure(RuntimeException(errorMessage)) - ) - } - } - ) - } - - /** - * Validate [shippingInformation] using [shippingMethodsFactory]. If valid, use - * [shippingMethodsFactory] to create the [ShippingMethod] options. - */ - @JvmSynthetic - internal suspend fun validateShippingInformation( - shippingInfoValidator: PaymentSessionConfig.ShippingInformationValidator, - shippingMethodsFactory: PaymentSessionConfig.ShippingMethodsFactory?, - shippingInformation: ShippingInformation - ): Result> { - val result = withContext(workContext) { - val isValid = shippingInfoValidator.isValid(shippingInformation) - if (isValid) { - runCatching { - shippingMethodsFactory?.create(shippingInformation).orEmpty() - } - } else { - runCatching { - shippingInfoValidator.getErrorMessage(shippingInformation) - }.fold( - onSuccess = { RuntimeException(it) }, - onFailure = { it } - ).let { - Result.failure(it) - } - } - } - - shippingMethods = result.getOrDefault(emptyList()) - - return result - } - - internal class Factory( - private val customerSession: CustomerSession, - private val paymentSessionData: PaymentSessionData - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return PaymentFlowViewModel( - customerSession, - paymentSessionData, - Dispatchers.IO - ) as T - } - } - - internal companion object { - private const val SHIPPING_INFO_PRODUCT_TOKEN = "ShippingInfoScreen" - - val PRODUCT_USAGE = setOf( - PaymentSession.PRODUCT_TOKEN, - PaymentFlowActivity.PRODUCT_TOKEN, - SHIPPING_INFO_PRODUCT_TOKEN - ) - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/PaymentFlowViewPager.kt b/payments-core/src/main/java/com/stripe/android/view/PaymentFlowViewPager.kt deleted file mode 100644 index 0607f469aed..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/PaymentFlowViewPager.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.stripe.android.view - -import android.content.Context -import android.util.AttributeSet -import android.view.MotionEvent -import androidx.viewpager.widget.ViewPager - -class PaymentFlowViewPager @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - private val isSwipingAllowed: Boolean = false -) : ViewPager(context, attrs) { - override fun onInterceptTouchEvent(event: MotionEvent?): Boolean { - return if (isSwipingAllowed) { - super.onInterceptTouchEvent(event) - } else { - false - } - } - - override fun onTouchEvent(event: MotionEvent?): Boolean { - return if (isSwipingAllowed) { - super.onTouchEvent(event) - } else { - false - } - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/PaymentMethodSwipeCallback.kt b/payments-core/src/main/java/com/stripe/android/view/PaymentMethodSwipeCallback.kt deleted file mode 100644 index 5254d5e34d8..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/PaymentMethodSwipeCallback.kt +++ /dev/null @@ -1,185 +0,0 @@ -package com.stripe.android.view - -import android.content.Context -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.view.View -import androidx.annotation.ColorInt -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.RecyclerView -import com.stripe.android.R -import com.stripe.android.model.PaymentMethod - -/** - * A [ItemTouchHelper.SimpleCallback] subclass for enabling swiping on Payment Methods in - * [PaymentMethodsActivity]'s list - */ -internal class PaymentMethodSwipeCallback( - context: Context, - private val adapter: PaymentMethodsAdapter, - private val listener: Listener -) : ItemTouchHelper.SimpleCallback( - 0, - ItemTouchHelper.RIGHT -) { - private val trashIcon = - ContextCompat.getDrawable(context, R.drawable.stripe_ic_trash)!! - private val swipeStartColor = - ContextCompat.getColor(context, R.color.stripe_swipe_start_payment_method) - private val swipeThresholdColor = - ContextCompat.getColor(context, R.color.stripe_swipe_threshold_payment_method) - private val background = ColorDrawable(swipeStartColor) - private val itemViewStartPadding = trashIcon.intrinsicWidth / 2 - private val iconStartOffset = context.resources.getDimensionPixelSize(R.dimen.stripe_list_row_start_padding) - - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - return true - } - - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - val paymentMethod = adapter.getPaymentMethodAtPosition(viewHolder.bindingAdapterPosition) - listener.onSwiped(paymentMethod) - } - - override fun getSwipeDirs( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder - ): Int { - return if (viewHolder is PaymentMethodsAdapter.ViewHolder.PaymentMethodViewHolder) { - // only allow swiping on Payment Method items - super.getSwipeDirs(recyclerView, viewHolder) - } else { - 0 - } - } - - override fun onChildDraw( - canvas: Canvas, - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - dX: Float, - dY: Float, - actionState: Int, - isCurrentlyActive: Boolean - ) { - super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) - if (viewHolder is PaymentMethodsAdapter.ViewHolder.PaymentMethodViewHolder) { - val itemView = viewHolder.itemView - - val startTransition = itemView.width * START_TRANSITION_THRESHOLD - val endTransition = itemView.width * END_TRANSITION_THRESHOLD - - // calculate the transition fraction to animate the background color of the swipe - val transitionFraction: Float = - when { - dX < startTransition -> - 0F - dX >= endTransition -> - 1F - else -> - ((dX - startTransition) / (endTransition - startTransition)) - } - - updateSwipedPaymentMethod( - itemView, - dX.toInt(), - transitionFraction, - canvas - ) - } - } - - override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float { - return END_TRANSITION_THRESHOLD - } - - private fun updateSwipedPaymentMethod( - itemView: View, - dX: Int, - transitionFraction: Float, - canvas: Canvas - ) { - val iconTop = itemView.top + (itemView.height - trashIcon.intrinsicHeight) / 2 - val iconBottom = iconTop + trashIcon.intrinsicHeight - - when { - // swipe right - dX > 0 -> { - val iconLeft = itemView.left + iconStartOffset - val iconRight = iconLeft + trashIcon.intrinsicWidth - - // hide the icon until the swipe distance is enough that it won't clash - // with the view - if (dX > iconRight) { - trashIcon.setBounds(iconLeft, iconTop, iconRight, iconBottom) - } else { - trashIcon.setBounds(0, 0, 0, 0) - } - - background.setBounds( - itemView.left, - itemView.top, - itemView.left + dX + itemViewStartPadding, - itemView.bottom - ) - background.color = when { - transitionFraction <= 0.0F -> - swipeStartColor - transitionFraction >= 1.0F -> - swipeThresholdColor - else -> - calculateTransitionColor( - transitionFraction, - swipeStartColor, - swipeThresholdColor - ) - } - } - else -> { - // reset when done swiping - trashIcon.setBounds(0, 0, 0, 0) - background.setBounds(0, 0, 0, 0) - } - } - - background.draw(canvas) - trashIcon.draw(canvas) - } - - internal companion object { - // calculate the background color while transitioning from start to end threshold - internal fun calculateTransitionColor( - fraction: Float, - @ColorInt startValue: Int, - @ColorInt endValue: Int - ): Int { - val startAlpha = Color.alpha(startValue) - val startRed = Color.red(startValue) - val startGreen = Color.green(startValue) - val startBlue = Color.blue(startValue) - val deltaAlpha = (Color.alpha(endValue) - startAlpha) * fraction - val deltaRed = (Color.red(endValue) - startRed) * fraction - val deltaGreen = (Color.green(endValue) - startGreen) * fraction - val deltaBlue = (Color.blue(endValue) - startBlue) * fraction - return Color.argb( - (startAlpha + deltaAlpha).toInt(), - (startRed + deltaRed).toInt(), - (startGreen + deltaGreen).toInt(), - (startBlue + deltaBlue).toInt() - ) - } - - private const val START_TRANSITION_THRESHOLD = 0.25F - private const val END_TRANSITION_THRESHOLD = 0.5F - } - - interface Listener { - fun onSwiped(paymentMethod: PaymentMethod) - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/PaymentMethodsActivity.kt b/payments-core/src/main/java/com/stripe/android/view/PaymentMethodsActivity.kt deleted file mode 100644 index 9ca8a07bb49..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/PaymentMethodsActivity.kt +++ /dev/null @@ -1,315 +0,0 @@ -package com.stripe.android.view - -import android.app.Activity -import android.content.Intent -import android.os.Build -import android.os.Bundle -import android.text.method.LinkMovementMethod -import android.text.util.Linkify -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.activity.addCallback -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.viewModels -import androidx.annotation.VisibleForTesting -import androidx.appcompat.app.AppCompatActivity -import androidx.core.text.util.LinkifyCompat -import androidx.core.view.ViewCompat -import androidx.core.view.isVisible -import androidx.lifecycle.lifecycleScope -import com.google.android.material.snackbar.Snackbar -import com.stripe.android.BASIC_INTEGRATION_DEPRECATION_WARNING -import com.stripe.android.CustomerSession -import com.stripe.android.R -import com.stripe.android.core.exception.StripeException -import com.stripe.android.databinding.StripePaymentMethodsActivityBinding -import com.stripe.android.model.PaymentMethod -import com.stripe.android.utils.argsAreInvalid -import com.stripe.android.view.i18n.TranslatorManager -import kotlinx.coroutines.launch - -/** - * An activity that allows a customer to select from their attached payment methods, - * or add a new one via [AddPaymentMethodActivity]. - * - * This Activity is typically started through [com.stripe.android.PaymentSession]. - * To directly start this activity, use [PaymentMethodsActivityStarter.startForResult]. - * - * Use [PaymentMethodsActivityStarter.Result.fromIntent] - * to retrieve the result of this activity from an intent in onActivityResult(). - */ -@Deprecated(BASIC_INTEGRATION_DEPRECATION_WARNING) -class PaymentMethodsActivity : AppCompatActivity() { - internal val viewBinding: StripePaymentMethodsActivityBinding by lazy { - StripePaymentMethodsActivityBinding.inflate(layoutInflater) - } - - private val startedFromPaymentSession: Boolean by lazy { - args.isPaymentSessionActive - } - - private val customerSession: Result by lazy { - runCatching { CustomerSession.getInstance() } - } - private val cardDisplayTextFactory: CardDisplayTextFactory by lazy { - CardDisplayTextFactory(this) - } - - private val alertDisplayer: AlertDisplayer by lazy { - AlertDisplayer.DefaultAlertDisplayer(this) - } - - private val args: PaymentMethodsActivityStarter.Args by lazy { - PaymentMethodsActivityStarter.Args.create(intent) - } - - private val viewModel: PaymentMethodsViewModel by viewModels { - PaymentMethodsViewModel.Factory( - application, - customerSession, - args.initialPaymentMethodId, - startedFromPaymentSession - ) - } - - private val adapter: PaymentMethodsAdapter by lazy { - PaymentMethodsAdapter( - args, - addableTypes = args.paymentMethodTypes, - initiallySelectedPaymentMethodId = viewModel.selectedPaymentMethodId, - shouldShowGooglePay = args.shouldShowGooglePay, - useGooglePay = args.useGooglePay, - canDeletePaymentMethods = args.canDeletePaymentMethods - ) - } - - private var earlyExitDueToIllegalState: Boolean = false - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (customerSession.isFailure) { - finishWithResult( - null, - Activity.RESULT_CANCELED - ) - return - } - if (argsAreInvalid { args }) { - earlyExitDueToIllegalState = true - return - } - - setContentView(viewBinding.root) - - args.windowFlags?.let { - window.addFlags(it) - } - - onBackPressedDispatcher.addCallback { - finishWithResult(adapter.selectedPaymentMethod, Activity.RESULT_CANCELED) - } - - lifecycleScope.launch { - viewModel.snackbarData.collect { snackbarText -> - snackbarText?.let { - Snackbar.make(viewBinding.coordinator, it, Snackbar.LENGTH_SHORT).show() - } - } - } - - lifecycleScope.launch { - viewModel.progressData.collect { - viewBinding.progressBar.isVisible = it - } - } - - val addPaymentMethodLauncher = registerForActivityResult( - AddPaymentMethodContract(), - ::onAddPaymentMethodResult - ) - - observePaymentMethodData() - - setupRecyclerView(addPaymentMethodLauncher) - - setSupportActionBar(viewBinding.toolbar) - - supportActionBar?.apply { - setDisplayHomeAsUpEnabled(true) - setDisplayShowHomeEnabled(true) - } - - createFooterView(viewBinding.footerContainer)?.let { footer -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { - viewBinding.recycler.accessibilityTraversalBefore = footer.id - footer.accessibilityTraversalAfter = viewBinding.recycler.id - } - viewBinding.footerContainer.addView(footer) - viewBinding.footerContainer.isVisible = true - } - - // This prevents the first click from being eaten by the focus. - viewBinding.recycler.requestFocusFromTouch() - } - - private fun setupRecyclerView( - addPaymentMethodLauncher: ActivityResultLauncher, - ) { - val deletePaymentMethodDialogFactory = DeletePaymentMethodDialogFactory( - this, - adapter, - cardDisplayTextFactory, - customerSession, - viewModel.productUsage - ) { viewModel.onPaymentMethodRemoved(it) } - - adapter.listener = object : PaymentMethodsAdapter.Listener { - override fun onPaymentMethodClick(paymentMethod: PaymentMethod) { - viewBinding.recycler.tappedPaymentMethod = paymentMethod - } - - override fun onAddPaymentMethodClick(args: AddPaymentMethodActivityStarter.Args) { - addPaymentMethodLauncher.launch(args) - } - - override fun onGooglePayClick() { - finishWithGooglePay() - } - - override fun onDeletePaymentMethodAction(paymentMethod: PaymentMethod) { - deletePaymentMethodDialogFactory.create(paymentMethod).show() - } - } - - viewBinding.recycler.adapter = adapter - viewBinding.recycler.paymentMethodSelectedCallback = { finishWithResult(it) } - - if (args.canDeletePaymentMethods) { - viewBinding.recycler.attachItemTouchHelper( - PaymentMethodSwipeCallback( - this, - adapter, - SwipeToDeleteCallbackListener(deletePaymentMethodDialogFactory) - ) - ) - } - } - - override fun onSupportNavigateUp(): Boolean { - finishWithResult(adapter.selectedPaymentMethod, Activity.RESULT_CANCELED) - return true - } - - @VisibleForTesting - internal fun onAddPaymentMethodResult(result: AddPaymentMethodActivityStarter.Result) { - when (result) { - is AddPaymentMethodActivityStarter.Result.Success -> { - onAddedPaymentMethod(result.paymentMethod) - } - is AddPaymentMethodActivityStarter.Result.Failure -> { - // TODO(mshafrir-stripe): notify user that payment method can not be added at this time - } - else -> { - // no-op - } - } - } - - private fun onAddedPaymentMethod(paymentMethod: PaymentMethod) { - if (paymentMethod.type?.isReusable == true) { - // Refresh the list of Payment Methods with the new reusable Payment Method. - viewModel.onPaymentMethodAdded(paymentMethod) - } else { - // If the added Payment Method is not reusable, it also can't be attached to a - // customer, so immediately return to the launching host with the new - // Payment Method. - finishWithResult(paymentMethod) - } - } - - private fun finishWithGooglePay() { - setResult( - Activity.RESULT_OK, - Intent().putExtras( - PaymentMethodsActivityStarter.Result(useGooglePay = true).toBundle() - ) - ) - - finish() - } - - private fun finishWithResult( - paymentMethod: PaymentMethod?, - resultCode: Int = Activity.RESULT_OK - ) { - setResult( - resultCode, - Intent().also { - it.putExtras( - PaymentMethodsActivityStarter.Result( - paymentMethod = paymentMethod, - useGooglePay = args.useGooglePay && paymentMethod == null - ).toBundle() - ) - } - ) - - finish() - } - - private fun createFooterView( - contentRoot: ViewGroup - ): View? { - return if (args.paymentMethodsFooterLayoutId > 0) { - val footerView = layoutInflater.inflate( - args.paymentMethodsFooterLayoutId, - contentRoot, - false - ) - footerView.id = R.id.stripe_payment_methods_footer - if (footerView is TextView) { - LinkifyCompat.addLinks(footerView, Linkify.ALL) - ViewCompat.enableAccessibleClickableSpanSupport(footerView) - footerView.movementMethod = LinkMovementMethod.getInstance() - } - footerView - } else { - null - } - } - - private fun observePaymentMethodData() { - lifecycleScope.launch { - viewModel.paymentMethodsData.collect { result -> - result?.fold( - onSuccess = { adapter.setPaymentMethods(it) }, - onFailure = { - alertDisplayer.show( - when (it) { - is StripeException -> { - TranslatorManager.getErrorMessageTranslator() - .translate(it.statusCode, it.message, it.stripeError) - } - else -> { - it.message.orEmpty() - } - } - ) - } - ) - } - } - } - - override fun onDestroy() { - if (!earlyExitDueToIllegalState) { - viewModel.selectedPaymentMethodId = adapter.selectedPaymentMethod?.id - } - super.onDestroy() - } - - internal companion object { - internal const val PRODUCT_TOKEN: String = "PaymentMethodsActivity" - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/PaymentMethodsActivityStarter.kt b/payments-core/src/main/java/com/stripe/android/view/PaymentMethodsActivityStarter.kt deleted file mode 100644 index c778157ee27..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/PaymentMethodsActivityStarter.kt +++ /dev/null @@ -1,209 +0,0 @@ -package com.stripe.android.view - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import androidx.annotation.LayoutRes -import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment -import com.stripe.android.PaymentConfiguration -import com.stripe.android.model.PaymentMethod -import com.stripe.android.view.PaymentMethodsActivityStarter.Args -import com.stripe.android.view.PaymentMethodsActivityStarter.Companion.REQUEST_CODE -import com.stripe.android.view.PaymentMethodsActivityStarter.Result -import com.stripe.android.view.PaymentMethodsActivityStarter.Result.Companion.fromIntent -import kotlinx.parcelize.Parcelize - -/** - * A class to start [PaymentMethodsActivity]. Arguments for the activity can be specified - * with [Args] and constructed with [Args.Builder]. - * - * The result data is a [Result] instance, obtained using [Result.fromIntent]}. - * The result will be returned with request code [REQUEST_CODE]. - */ -class PaymentMethodsActivityStarter : ActivityStarter { - - constructor(activity: Activity) : super( - activity, - PaymentMethodsActivity::class.java, - REQUEST_CODE - ) - - constructor(fragment: Fragment) : super( - fragment, - PaymentMethodsActivity::class.java, - REQUEST_CODE - ) - - @Parcelize - data class Args internal constructor( - internal val initialPaymentMethodId: String?, - @LayoutRes val paymentMethodsFooterLayoutId: Int, - @LayoutRes val addPaymentMethodFooterLayoutId: Int, - internal val isPaymentSessionActive: Boolean, - internal val paymentMethodTypes: List, - internal val paymentConfiguration: PaymentConfiguration?, - internal val windowFlags: Int? = null, - internal val billingAddressFields: BillingAddressFields, - internal val shouldShowGooglePay: Boolean = false, - internal val useGooglePay: Boolean = false, - internal val canDeletePaymentMethods: Boolean = true - - ) : ActivityStarter.Args { - class Builder { - private var billingAddressFields: BillingAddressFields = BillingAddressFields.PostalCode - private var initialPaymentMethodId: String? = null - private var isPaymentSessionActive = false - private var paymentMethodTypes: List? = null - private var shouldShowGooglePay: Boolean = false - private var useGooglePay: Boolean = false - private var canDeletePaymentMethods: Boolean = true - private var paymentConfiguration: PaymentConfiguration? = null - private var windowFlags: Int? = null - - @LayoutRes - private var paymentMethodsFooterLayoutId: Int = 0 - - @LayoutRes - private var addPaymentMethodFooterLayoutId: Int = 0 - - /** - * @param billingAddressFields the billing address fields to require on [AddPaymentMethodActivity] - */ - fun setBillingAddressFields( - billingAddressFields: BillingAddressFields - ): Builder = apply { - this.billingAddressFields = billingAddressFields - } - - fun setInitialPaymentMethodId(initialPaymentMethodId: String?): Builder = apply { - this.initialPaymentMethodId = initialPaymentMethodId - } - - fun setIsPaymentSessionActive(isPaymentSessionActive: Boolean): Builder = apply { - this.isPaymentSessionActive = isPaymentSessionActive - } - - fun setPaymentConfiguration( - paymentConfiguration: PaymentConfiguration? - ): Builder = apply { - this.paymentConfiguration = paymentConfiguration - } - - /** - * @param paymentMethodTypes a list of [PaymentMethod.Type] that indicates the types of - * Payment Methods that the customer can select or add via Stripe UI components. - * - * The order of the [PaymentMethod.Type] values in the list will be used to - * arrange the add buttons in the Stripe UI components. They will be arranged vertically - * from first to last. - * - * Currently only [PaymentMethod.Type.Card] and [PaymentMethod.Type.Fpx] are supported. - * If not specified or empty, [PaymentMethod.Type.Card] will be used. - */ - fun setPaymentMethodTypes( - paymentMethodTypes: List - ): Builder = apply { - this.paymentMethodTypes = paymentMethodTypes - } - - /** - * @param shouldShowGooglePay if `true`, will show "Google Pay" as an option on the - * Payment Methods selection screen. If a user selects the Google Pay option, - * [PaymentMethodsActivityStarter.Result.useGooglePay] will be `true`. - */ - fun setShouldShowGooglePay(shouldShowGooglePay: Boolean): Builder = apply { - this.shouldShowGooglePay = shouldShowGooglePay - } - - /** - * @param paymentMethodsFooterLayoutId optional layout id that will be inflated and - * displayed beneath the payment method selection list on [PaymentMethodsActivity] - */ - fun setPaymentMethodsFooter( - @LayoutRes paymentMethodsFooterLayoutId: Int - ): Builder = apply { - this.paymentMethodsFooterLayoutId = paymentMethodsFooterLayoutId - } - - /** - * @param addPaymentMethodFooterLayoutId optional layout id that will be inflated and - * displayed beneath the payment details collection form on [AddPaymentMethodActivity] - */ - fun setAddPaymentMethodFooter( - @LayoutRes addPaymentMethodFooterLayoutId: Int - ): Builder = apply { - this.addPaymentMethodFooterLayoutId = addPaymentMethodFooterLayoutId - } - - /** - * @param windowFlags optional flags to set on the `Window` object of Stripe Activities - * - * See [WindowManager.LayoutParams](https://developer.android.com/reference/android/view/WindowManager.LayoutParams) - */ - fun setWindowFlags(windowFlags: Int?): Builder = apply { - this.windowFlags = windowFlags - } - - internal fun setUseGooglePay(useGooglePay: Boolean): Builder = apply { - this.useGooglePay = useGooglePay - } - - fun setCanDeletePaymentMethods(canDeletePaymentMethods: Boolean): Builder = apply { - this.canDeletePaymentMethods = canDeletePaymentMethods - } - - fun build(): Args { - return Args( - initialPaymentMethodId = initialPaymentMethodId, - isPaymentSessionActive = isPaymentSessionActive, - paymentMethodTypes = paymentMethodTypes ?: listOf(PaymentMethod.Type.Card), - shouldShowGooglePay = shouldShowGooglePay, - useGooglePay = useGooglePay, - paymentConfiguration = paymentConfiguration, - paymentMethodsFooterLayoutId = paymentMethodsFooterLayoutId, - addPaymentMethodFooterLayoutId = addPaymentMethodFooterLayoutId, - windowFlags = windowFlags, - billingAddressFields = billingAddressFields, - canDeletePaymentMethods = canDeletePaymentMethods - ) - } - } - - internal companion object { - @JvmSynthetic - internal fun create(intent: Intent): Args { - return requireNotNull(intent.getParcelableExtra(ActivityStarter.Args.EXTRA)) - } - } - } - - /** - * The result of a [PaymentMethodsActivity]. - * - * Retrieve in `#onActivityResult()` using [fromIntent]. - */ - @Parcelize - data class Result internal constructor( - @JvmField val paymentMethod: PaymentMethod? = null, - val useGooglePay: Boolean = false - ) : ActivityStarter.Result { - override fun toBundle(): Bundle { - return bundleOf(ActivityStarter.Result.EXTRA to this) - } - - companion object { - /** - * @return the [Result] object from the given `Intent` - */ - @JvmStatic - fun fromIntent(intent: Intent?): Result? { - return intent?.getParcelableExtra(ActivityStarter.Result.EXTRA) - } - } - } - - companion object { - const val REQUEST_CODE: Int = 6000 - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/PaymentMethodsAdapter.kt b/payments-core/src/main/java/com/stripe/android/view/PaymentMethodsAdapter.kt deleted file mode 100644 index ecd30536a1d..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/PaymentMethodsAdapter.kt +++ /dev/null @@ -1,382 +0,0 @@ -package com.stripe.android.view - -import android.content.Context -import android.content.res.ColorStateList -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.ViewCompat -import androidx.core.widget.ImageViewCompat -import androidx.recyclerview.widget.RecyclerView -import com.stripe.android.R -import com.stripe.android.databinding.StripeAddPaymentMethodRowBinding -import com.stripe.android.databinding.StripeGooglePayRowBinding -import com.stripe.android.databinding.StripeMaskedCardRowBinding -import com.stripe.android.model.PaymentMethod - -/** - * A [RecyclerView.Adapter] that holds a set of [MaskedCardView] items for a given set - * of [PaymentMethod] objects. - */ -internal class PaymentMethodsAdapter constructor( - intentArgs: PaymentMethodsActivityStarter.Args, - private val addableTypes: List = listOf(PaymentMethod.Type.Card), - initiallySelectedPaymentMethodId: String? = null, - private val shouldShowGooglePay: Boolean = false, - private val useGooglePay: Boolean = false, - private val canDeletePaymentMethods: Boolean = true -) : RecyclerView.Adapter() { - - internal val paymentMethods = mutableListOf() - internal var selectedPaymentMethodId: String? = initiallySelectedPaymentMethodId - internal val selectedPaymentMethod: PaymentMethod? - get() { - return selectedPaymentMethodId?.let { selectedPaymentMethodId -> - paymentMethods.firstOrNull { it.id == selectedPaymentMethodId } - } - } - - internal var listener: Listener? = null - private val googlePayCount = 1.takeIf { shouldShowGooglePay } ?: 0 - - internal val addCardArgs = AddPaymentMethodActivityStarter.Args.Builder() - .setBillingAddressFields(intentArgs.billingAddressFields) - .setShouldAttachToCustomer(true) - .setIsPaymentSessionActive(intentArgs.isPaymentSessionActive) - .setPaymentMethodType(PaymentMethod.Type.Card) - .setAddPaymentMethodFooter(intentArgs.addPaymentMethodFooterLayoutId) - .setPaymentConfiguration(intentArgs.paymentConfiguration) - .setWindowFlags(intentArgs.windowFlags) - .build() - - internal val addFpxArgs = AddPaymentMethodActivityStarter.Args.Builder() - .setIsPaymentSessionActive(intentArgs.isPaymentSessionActive) - .setPaymentMethodType(PaymentMethod.Type.Fpx) - .setPaymentConfiguration(intentArgs.paymentConfiguration) - .build() - - init { - setHasStableIds(true) - } - - @JvmSynthetic - internal fun setPaymentMethods(paymentMethods: List) { - this.paymentMethods.clear() - this.paymentMethods.addAll(paymentMethods) - notifyDataSetChanged() - } - - override fun getItemCount(): Int { - return paymentMethods.size + addableTypes.size + googlePayCount - } - - override fun getItemViewType(position: Int): Int { - return when { - isGooglePayPosition(position) -> ViewType.GooglePay.ordinal - isPaymentMethodsPosition(position) -> { - val type = getPaymentMethodAtPosition(position).type - if (PaymentMethod.Type.Card == type) { - ViewType.Card.ordinal - } else { - super.getItemViewType(position) - } - } - else -> { - val paymentMethodType = - addableTypes[getAddableTypesPosition(position)] - return when (paymentMethodType) { - PaymentMethod.Type.Card -> ViewType.AddCard.ordinal - PaymentMethod.Type.Fpx -> ViewType.AddFpx.ordinal - else -> - throw IllegalArgumentException( - "Unsupported PaymentMethod type: ${paymentMethodType.code}" - ) - } - } - } - } - - private fun isGooglePayPosition(position: Int): Boolean { - return shouldShowGooglePay && position == 0 - } - - private fun isPaymentMethodsPosition(position: Int): Boolean { - val range = if (shouldShowGooglePay) { - 1..paymentMethods.size - } else { - 0 until paymentMethods.size - } - return position in range - } - - override fun getItemId(position: Int): Long { - return when { - isGooglePayPosition(position) -> - GOOGLE_PAY_ITEM_ID - isPaymentMethodsPosition(position) -> - getPaymentMethodAtPosition(position).hashCode().toLong() - else -> - addableTypes[getAddableTypesPosition(position)].code.hashCode().toLong() - } - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is ViewHolder.PaymentMethodViewHolder -> { - val paymentMethod = getPaymentMethodAtPosition(position) - holder.setPaymentMethod(paymentMethod) - holder.setSelected(paymentMethod.id == selectedPaymentMethodId) - holder.itemView.setOnClickListener { - onPositionClicked(holder.bindingAdapterPosition) - } - } - is ViewHolder.GooglePayViewHolder -> { - holder.itemView.setOnClickListener { - selectedPaymentMethodId = null - listener?.onGooglePayClick() - } - holder.bind(useGooglePay) - } - is ViewHolder.AddCardPaymentMethodViewHolder -> { - holder.itemView.setOnClickListener { - listener?.onAddPaymentMethodClick(addCardArgs) - } - } - is ViewHolder.AddFpxPaymentMethodViewHolder -> { - holder.itemView.setOnClickListener { - listener?.onAddPaymentMethodClick(addFpxArgs) - } - } - } - } - - @JvmSynthetic - internal fun onPositionClicked(position: Int) { - updateSelectedPaymentMethod(position) - listener?.onPaymentMethodClick(getPaymentMethodAtPosition(position)) - } - - private fun updateSelectedPaymentMethod(position: Int) { - val currentlySelectedPosition = paymentMethods.indexOfFirst { - it.id == selectedPaymentMethodId - } - if (currentlySelectedPosition != position) { - // selected a new Payment Method - notifyItemChanged(currentlySelectedPosition) - selectedPaymentMethodId = paymentMethods.getOrNull(position)?.id - } - - // Notify the current position even if it's the currently selected position so that the - // ItemAnimator defined in PaymentMethodActivity is triggered. - notifyItemChanged(position) - } - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): RecyclerView.ViewHolder { - return when (ViewType.entries[viewType]) { - ViewType.Card -> createPaymentMethodViewHolder(parent) - ViewType.AddCard -> createAddCardPaymentMethodViewHolder(parent) - ViewType.AddFpx -> createAddFpxPaymentMethodViewHolder(parent) - ViewType.GooglePay -> createGooglePayViewHolder(parent) - } - } - - private fun createAddCardPaymentMethodViewHolder( - parent: ViewGroup - ): ViewHolder.AddCardPaymentMethodViewHolder { - return ViewHolder.AddCardPaymentMethodViewHolder(parent.context, parent) - } - - private fun createAddFpxPaymentMethodViewHolder( - parent: ViewGroup - ): ViewHolder.AddFpxPaymentMethodViewHolder { - return ViewHolder.AddFpxPaymentMethodViewHolder(parent.context, parent) - } - - private fun createPaymentMethodViewHolder( - parent: ViewGroup - ): ViewHolder.PaymentMethodViewHolder { - val viewHolder = ViewHolder.PaymentMethodViewHolder(parent) - - if (canDeletePaymentMethods) { - ViewCompat.addAccessibilityAction( - viewHolder.itemView, - parent.context.getString(R.string.stripe_delete_payment_method) - ) { _, _ -> - listener?.onDeletePaymentMethodAction( - paymentMethod = getPaymentMethodAtPosition(viewHolder.bindingAdapterPosition) - ) - true - } - } - return viewHolder - } - - private fun createGooglePayViewHolder( - parent: ViewGroup - ): ViewHolder.GooglePayViewHolder { - return ViewHolder.GooglePayViewHolder(parent.context, parent) - } - - @JvmSynthetic - internal fun deletePaymentMethod(paymentMethod: PaymentMethod) { - getPosition(paymentMethod)?.let { - paymentMethods.remove(paymentMethod) - notifyItemRemoved(it) - } - } - - @JvmSynthetic - internal fun resetPaymentMethod(paymentMethod: PaymentMethod) { - getPosition(paymentMethod)?.let { - notifyItemChanged(it) - } - } - - /** - * Given an adapter position, translate to a `paymentMethods` element - */ - @JvmSynthetic - internal fun getPaymentMethodAtPosition(position: Int): PaymentMethod { - return paymentMethods[getPaymentMethodIndex(position)] - } - - /** - * Given an adapter position, translate to a `paymentMethods` index - */ - private fun getPaymentMethodIndex(position: Int): Int { - return position - googlePayCount - } - - /** - * Given a Payment Method, get its adapter position. For example, if the Google Pay button is - * being shown, the 2nd element in [paymentMethods] is actually the 3rd item in the adapter. - */ - internal fun getPosition(paymentMethod: PaymentMethod): Int? { - return paymentMethods.indexOf(paymentMethod).takeIf { it >= 0 }?.let { - it + googlePayCount - } - } - - private fun getAddableTypesPosition(position: Int): Int { - return position - paymentMethods.size - googlePayCount - } - - internal sealed class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - internal class AddCardPaymentMethodViewHolder( - viewBinding: StripeAddPaymentMethodRowBinding - ) : RecyclerView.ViewHolder(viewBinding.root) { - constructor(context: Context, parent: ViewGroup) : this( - StripeAddPaymentMethodRowBinding.inflate( - LayoutInflater.from(context), - parent, - false - ) - ) - - init { - itemView.id = R.id.stripe_payment_methods_add_card - itemView.contentDescription = - itemView.resources.getString(R.string.stripe_payment_method_add_new_card) - viewBinding.label.text = - itemView.resources.getString(R.string.stripe_payment_method_add_new_card) - } - } - - internal class AddFpxPaymentMethodViewHolder( - viewBinding: StripeAddPaymentMethodRowBinding - ) : RecyclerView.ViewHolder(viewBinding.root) { - constructor(context: Context, parent: ViewGroup) : this( - StripeAddPaymentMethodRowBinding.inflate( - LayoutInflater.from(context), - parent, - false - ) - ) - - init { - itemView.id = R.id.stripe_payment_methods_add_fpx - itemView.contentDescription = - itemView.resources.getString(R.string.stripe_payment_method_add_new_fpx) - viewBinding.label.text = - itemView.resources.getString(R.string.stripe_payment_method_add_new_fpx) - } - } - - internal class GooglePayViewHolder( - private val viewBinding: StripeGooglePayRowBinding - ) : RecyclerView.ViewHolder(viewBinding.root) { - constructor(context: Context, parent: ViewGroup) : this( - StripeGooglePayRowBinding.inflate( - LayoutInflater.from(context), - parent, - false - ) - ) - - private val themeConfig = ThemeConfig(itemView.context) - - init { - ImageViewCompat.setImageTintList( - viewBinding.checkIcon, - ColorStateList.valueOf(themeConfig.getTintColor(true)) - ) - } - - fun bind(isSelected: Boolean) { - viewBinding.label.setTextColor( - ColorStateList.valueOf(themeConfig.getTextColor(isSelected)) - ) - - viewBinding.checkIcon.visibility = if (isSelected) { - View.VISIBLE - } else { - View.INVISIBLE - } - - itemView.isSelected = isSelected - } - } - - internal class PaymentMethodViewHolder constructor( - private val viewBinding: StripeMaskedCardRowBinding - ) : ViewHolder(viewBinding.root) { - constructor(parent: ViewGroup) : this( - StripeMaskedCardRowBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) - - fun setPaymentMethod(paymentMethod: PaymentMethod) { - viewBinding.maskedCardItem.setPaymentMethod(paymentMethod) - } - - fun setSelected(selected: Boolean) { - viewBinding.maskedCardItem.isSelected = selected - itemView.isSelected = selected - } - } - } - - internal interface Listener { - fun onPaymentMethodClick(paymentMethod: PaymentMethod) - fun onGooglePayClick() - fun onAddPaymentMethodClick(args: AddPaymentMethodActivityStarter.Args) - fun onDeletePaymentMethodAction(paymentMethod: PaymentMethod) - } - - internal enum class ViewType { - Card, - AddCard, - AddFpx, - GooglePay - } - - internal companion object { - internal val GOOGLE_PAY_ITEM_ID = "pm_google_pay".hashCode().toLong() - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/PaymentMethodsRecyclerView.kt b/payments-core/src/main/java/com/stripe/android/view/PaymentMethodsRecyclerView.kt deleted file mode 100644 index a53f82740e2..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/PaymentMethodsRecyclerView.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.stripe.android.view - -import android.content.Context -import android.util.AttributeSet -import androidx.recyclerview.widget.DefaultItemAnimator -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.stripe.android.model.PaymentMethod - -internal class PaymentMethodsRecyclerView @JvmOverloads internal constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : RecyclerView(context, attrs, defStyleAttr) { - - internal var paymentMethodSelectedCallback: (PaymentMethod) -> Unit = {} - internal var tappedPaymentMethod: PaymentMethod? = null - - init { - setHasFixedSize(false) - layoutManager = LinearLayoutManager(context) - - itemAnimator = object : DefaultItemAnimator() { - override fun onAnimationFinished(viewHolder: ViewHolder) { - super.onAnimationFinished(viewHolder) - - // wait until post-tap animations are completed before finishing activity - tappedPaymentMethod?.let { paymentMethodSelectedCallback(it) } - tappedPaymentMethod = null - } - } - } - - @JvmSynthetic - internal fun attachItemTouchHelper(callback: ItemTouchHelper.SimpleCallback) { - ItemTouchHelper(callback).attachToRecyclerView(this) - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/PaymentMethodsViewModel.kt b/payments-core/src/main/java/com/stripe/android/view/PaymentMethodsViewModel.kt deleted file mode 100644 index b8a7f35a421..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/PaymentMethodsViewModel.kt +++ /dev/null @@ -1,148 +0,0 @@ -package com.stripe.android.view - -import android.app.Application -import androidx.annotation.StringRes -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.createSavedStateHandle -import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.CreationExtras -import com.stripe.android.CustomerSession -import com.stripe.android.PaymentSession -import com.stripe.android.R -import com.stripe.android.analytics.PaymentSessionEventReporter -import com.stripe.android.analytics.PaymentSessionEventReporterFactory -import com.stripe.android.analytics.SessionSavedStateHandler -import com.stripe.android.core.StripeError -import com.stripe.android.model.PaymentMethod -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch - -internal class PaymentMethodsViewModel( - application: Application, - savedStateHandle: SavedStateHandle, - private val customerSession: Result, - internal var selectedPaymentMethodId: String? = null, - private val startedFromPaymentSession: Boolean, - private val eventReporter: PaymentSessionEventReporter = - PaymentSessionEventReporterFactory.create(application.applicationContext) -) : AndroidViewModel(application) { - private val resources = application.resources - private val cardDisplayTextFactory = CardDisplayTextFactory(application) - - @Volatile - private var paymentMethodsJob: Job? = null - - internal val productUsage: Set = listOfNotNull( - PaymentSession.PRODUCT_TOKEN.takeIf { startedFromPaymentSession }, - PaymentMethodsActivity.PRODUCT_TOKEN - ).toSet() - - internal val paymentMethodsData: MutableStateFlow>?> = MutableStateFlow(null) - internal val snackbarData: MutableStateFlow = MutableStateFlow(null) - internal val progressData: MutableStateFlow = MutableStateFlow(false) - - init { - SessionSavedStateHandler.attachTo(this, savedStateHandle) - - getPaymentMethods(isInitialFetch = true) - } - - internal fun onPaymentMethodAdded(paymentMethod: PaymentMethod) { - createSnackbarText(paymentMethod, R.string.stripe_added)?.let { - snackbarData.value = it - snackbarData.value = null - } - getPaymentMethods(isInitialFetch = false) - } - - internal fun onPaymentMethodRemoved(paymentMethod: PaymentMethod) { - createSnackbarText(paymentMethod, R.string.stripe_removed)?.let { - snackbarData.value = it - snackbarData.value = null - } - } - - private fun createSnackbarText( - paymentMethod: PaymentMethod, - @StringRes stringRes: Int - ): String? { - return paymentMethod.card?.let { paymentMethodId -> - resources.getString( - stringRes, - cardDisplayTextFactory.createUnstyled(paymentMethodId) - ) - } - } - - private fun getPaymentMethods(isInitialFetch: Boolean) { - paymentMethodsJob?.cancel() - - if (isInitialFetch) { - eventReporter.onLoadStarted() - } - - paymentMethodsJob = viewModelScope.launch { - progressData.value = true - - customerSession.fold( - onSuccess = { - it.getPaymentMethods( - paymentMethodType = PaymentMethod.Type.Card, - productUsage = productUsage, - listener = object : CustomerSession.PaymentMethodsRetrievalWithExceptionListener { - override fun onPaymentMethodsRetrieved(paymentMethods: List) { - if (isInitialFetch) { - eventReporter.onLoadSucceeded(selectedPaymentMethodId) - eventReporter.onOptionsShown() - } - - paymentMethodsData.value = Result.success(paymentMethods) - progressData.value = false - } - - override fun onError( - errorCode: Int, - errorMessage: String, - stripeError: StripeError?, - throwable: Throwable - ) { - if (isInitialFetch) { - eventReporter.onLoadFailed(throwable) - } - - paymentMethodsData.value = Result.failure(throwable) - progressData.value = false - } - } - ) - }, - onFailure = { - paymentMethodsData.value = Result.failure(it) - progressData.value = false - } - ) - } - } - - internal class Factory( - private val application: Application, - private val customerSession: Result, - private val initialPaymentMethodId: String?, - private val startedFromPaymentSession: Boolean - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class, extras: CreationExtras): T { - return PaymentMethodsViewModel( - application, - extras.createSavedStateHandle(), - customerSession, - initialPaymentMethodId, - startedFromPaymentSession - ) as T - } - } -} diff --git a/payments-core/src/main/java/com/stripe/android/view/SwipeToDeleteCallbackListener.kt b/payments-core/src/main/java/com/stripe/android/view/SwipeToDeleteCallbackListener.kt deleted file mode 100644 index d6381748b7b..00000000000 --- a/payments-core/src/main/java/com/stripe/android/view/SwipeToDeleteCallbackListener.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.stripe.android.view - -import com.stripe.android.model.PaymentMethod - -internal class SwipeToDeleteCallbackListener internal constructor( - private val deletePaymentMethodDialogFactory: DeletePaymentMethodDialogFactory -) : PaymentMethodSwipeCallback.Listener { - - override fun onSwiped(paymentMethod: PaymentMethod) { - deletePaymentMethodDialogFactory - .create(paymentMethod) - .show() - } -} diff --git a/payments-core/src/test/java/com/stripe/android/CustomerSessionOperationExecutorTest.kt b/payments-core/src/test/java/com/stripe/android/CustomerSessionOperationExecutorTest.kt deleted file mode 100644 index 0aba4cae43c..00000000000 --- a/payments-core/src/test/java/com/stripe/android/CustomerSessionOperationExecutorTest.kt +++ /dev/null @@ -1,234 +0,0 @@ -package com.stripe.android - -import com.google.common.truth.Truth.assertThat -import com.stripe.android.core.networking.ApiRequest -import com.stripe.android.model.Customer -import com.stripe.android.model.CustomerFixtures -import com.stripe.android.model.ListPaymentMethodsParams -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.PaymentMethodFixtures -import com.stripe.android.model.Source -import com.stripe.android.networking.StripeRepository -import com.stripe.android.testing.AbsFakeStripeRepository -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import org.mockito.ArgumentMatchers.eq -import org.mockito.kotlin.argWhere -import org.mockito.kotlin.isNull -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import kotlin.test.AfterTest -import kotlin.test.BeforeTest -import kotlin.test.Test - -internal class CustomerSessionOperationExecutorTest { - private val testDispatcher = StandardTestDispatcher() - - private val listeners = mutableMapOf() - private val customerCallbacks = mutableListOf() - - @BeforeTest - fun setup() { - Dispatchers.setMain(testDispatcher) - } - - @AfterTest - fun cleanup() { - Dispatchers.resetMain() - } - - @Test - fun `execute with AttachPaymentMethod operation when valid PaymentMethod returned should call listener with PaymentMethod`() = runTest { - val listener = mock() - listeners[OPERATION_ID] = listener - - val executor = createExecutor( - object : AbsFakeStripeRepository() { - override suspend fun attachPaymentMethod( - customerId: String, - productUsageTokens: Set, - paymentMethodId: String, - requestOptions: ApiRequest.Options - ): Result { - return Result.success(PaymentMethodFixtures.CARD_PAYMENT_METHOD) - } - } - ) - executor.execute( - EphemeralKeyFixtures.FIRST, - EphemeralOperation.Customer.AttachPaymentMethod( - "pm_123", - OPERATION_ID, - emptySet() - ) - ) - - verify(listener).onPaymentMethodRetrieved(PaymentMethodFixtures.CARD_PAYMENT_METHOD) - assertThat(customerCallbacks) - .isEmpty() - } - - @Test - fun `execute with AttachPaymentMethod operation should call listener with error on failure`() = runTest { - val listener = mock() - listeners[OPERATION_ID] = listener - - val errorMessage = "an error or something" - - val executor = createExecutor( - object : AbsFakeStripeRepository() { - override suspend fun attachPaymentMethod( - customerId: String, - productUsageTokens: Set, - paymentMethodId: String, - requestOptions: ApiRequest.Options - ): Result { - return Result.failure(RuntimeException(errorMessage)) - } - } - ) - executor.execute( - EphemeralKeyFixtures.FIRST, - EphemeralOperation.Customer.AttachPaymentMethod( - "pm_123", - OPERATION_ID, - emptySet() - ) - ) - - verify(listener).onError( - 0, - errorMessage, - null - ) - assertThat(customerCallbacks) - .isEmpty() - } - - @Test - fun `execute with GetPaymentMethods operation should call listener on failure`() = runTest { - val listener = mock() - listeners[OPERATION_ID] = listener - - val errorMessage = "an error or something" - - val executor = createExecutor( - object : AbsFakeStripeRepository() { - override suspend fun getPaymentMethods( - listPaymentMethodsParams: ListPaymentMethodsParams, - productUsageTokens: Set, - requestOptions: ApiRequest.Options - ): Result> { - return Result.failure(RuntimeException(errorMessage)) - } - } - ) - executor.execute( - EphemeralKeyFixtures.FIRST, - EphemeralOperation.Customer.GetPaymentMethods( - type = PaymentMethod.Type.Card, - id = OPERATION_ID, - productUsage = emptySet() - ) - ) - - verify(listener).onError( - 0, - errorMessage, - null - ) - assertThat(customerCallbacks) - .isEmpty() - } - - @Test - fun `execute with GetPaymentMethods operation should call exception listener on failure`() = runTest { - val listener = mock() - listeners[OPERATION_ID] = listener - - val errorMessage = "an error or something" - - val executor = createExecutor( - object : AbsFakeStripeRepository() { - override suspend fun getPaymentMethods( - listPaymentMethodsParams: ListPaymentMethodsParams, - productUsageTokens: Set, - requestOptions: ApiRequest.Options - ): Result> { - return Result.failure(RuntimeException(errorMessage)) - } - } - ) - executor.execute( - EphemeralKeyFixtures.FIRST, - EphemeralOperation.Customer.GetPaymentMethods( - type = PaymentMethod.Type.Card, - id = OPERATION_ID, - productUsage = emptySet() - ) - ) - - verify(listener).onError( - eq(0), - eq(errorMessage) ?: errorMessage, - isNull(), - argWhere { error -> - error is RuntimeException && error.message == errorMessage - } - ) - assertThat(customerCallbacks) - .isEmpty() - } - - @Test - fun `execute with UpdateDefaultSource operation when valid Customer returned should call listener with PaymentMethod and invoke customerCallback`() = runTest { - val listener = mock() - listeners[OPERATION_ID] = listener - - val executor = createExecutor( - object : AbsFakeStripeRepository() { - override suspend fun setDefaultCustomerSource( - customerId: String, - publishableKey: String, - productUsageTokens: Set, - sourceId: String, - sourceType: String, - requestOptions: ApiRequest.Options - ): Result = Result.success(CustomerFixtures.CUSTOMER) - } - ) - executor.execute( - EphemeralKeyFixtures.FIRST, - EphemeralOperation.Customer.UpdateDefaultSource( - "src_123", - Source.SourceType.CARD, - OPERATION_ID, - emptySet() - ) - ) - - verify(listener).onCustomerRetrieved(CustomerFixtures.CUSTOMER) - assertThat(customerCallbacks) - .containsExactly(CustomerFixtures.CUSTOMER) - } - - private fun createExecutor( - repository: StripeRepository - ): CustomerSessionOperationExecutor { - return CustomerSessionOperationExecutor( - repository, - ApiKeyFixtures.FAKE_PUBLISHABLE_KEY, - null, - listeners - ) { customer -> - customerCallbacks.add(customer) - } - } - - private companion object { - private const val OPERATION_ID = "123" - } -} diff --git a/payments-core/src/test/java/com/stripe/android/CustomerSessionTest.kt b/payments-core/src/test/java/com/stripe/android/CustomerSessionTest.kt deleted file mode 100644 index d57cbb12026..00000000000 --- a/payments-core/src/test/java/com/stripe/android/CustomerSessionTest.kt +++ /dev/null @@ -1,843 +0,0 @@ -package com.stripe.android - -import com.stripe.android.core.StripeError -import com.stripe.android.core.exception.APIException -import com.stripe.android.core.networking.ApiRequest -import com.stripe.android.model.Customer -import com.stripe.android.model.CustomerFixtures -import com.stripe.android.model.ListPaymentMethodsParams -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.PaymentMethodFixtures -import com.stripe.android.model.Source -import com.stripe.android.model.SourceFixtures -import com.stripe.android.networking.StripeRepository -import com.stripe.android.testharness.TestEphemeralKeyProvider -import com.stripe.android.utils.TestUtils.idleLooper -import com.stripe.android.view.AddPaymentMethodActivity -import com.stripe.android.view.PaymentMethodsActivity -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import org.junit.runner.RunWith -import org.mockito.kotlin.KArgumentCaptor -import org.mockito.kotlin.any -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoMoreInteractions -import org.mockito.kotlin.whenever -import org.robolectric.RobolectricTestRunner -import java.util.Calendar -import java.util.concurrent.TimeUnit -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertNotNull - -@RunWith(RobolectricTestRunner::class) -internal class CustomerSessionTest { - private val testDispatcher = UnconfinedTestDispatcher() - - private val stripeRepository: StripeRepository = mock() - - private val paymentMethodRetrievalListener: CustomerSession.PaymentMethodRetrievalListener = mock() - private val paymentMethodsRetrievalListener: CustomerSession.PaymentMethodsRetrievalListener = mock() - private val customerRetrievalListener: CustomerSession.CustomerRetrievalListener = mock() - private val sourceRetrievalListener: CustomerSession.SourceRetrievalListener = mock() - - private val productUsageArgumentCaptor: KArgumentCaptor> = argumentCaptor() - private val sourceArgumentCaptor: KArgumentCaptor = argumentCaptor() - private val paymentMethodArgumentCaptor: KArgumentCaptor = argumentCaptor() - private val paymentMethodsArgumentCaptor: KArgumentCaptor> = argumentCaptor() - private val customerArgumentCaptor: KArgumentCaptor = argumentCaptor() - private val requestOptionsArgumentCaptor: KArgumentCaptor = argumentCaptor() - - private val ephemeralKeyProvider: TestEphemeralKeyProvider = TestEphemeralKeyProvider() - - @BeforeTest - fun setup() { - Dispatchers.setMain(testDispatcher) - runBlocking { - whenever(stripeRepository.retrieveCustomer(any(), any(), any())) - .thenReturn( - Result.success(FIRST_CUSTOMER), - Result.success(SECOND_CUSTOMER), - ) - - whenever( - stripeRepository.addCustomerSource( - any(), - eq(ApiKeyFixtures.FAKE_PUBLISHABLE_KEY), - any(), - any(), - any(), - any() - ) - ).thenReturn(Result.success(SourceFixtures.SOURCE_CARD)) - - whenever( - stripeRepository.deleteCustomerSource( - any(), - eq(ApiKeyFixtures.FAKE_PUBLISHABLE_KEY), - any(), - any(), - any() - ) - ).thenReturn(Result.success(SourceFixtures.SOURCE_CARD)) - - whenever( - stripeRepository.setDefaultCustomerSource( - any(), - eq(ApiKeyFixtures.FAKE_PUBLISHABLE_KEY), - any(), - any(), - any(), - any() - ) - ).thenReturn(Result.success(SECOND_CUSTOMER)) - - whenever( - stripeRepository.attachPaymentMethod( - customerId = any(), - productUsageTokens = any(), - paymentMethodId = any(), - requestOptions = any() - ) - ).thenReturn(Result.success(PAYMENT_METHOD)) - - whenever( - stripeRepository.detachPaymentMethod( - productUsageTokens = any(), - paymentMethodId = any(), - requestOptions = any() - ) - ).thenReturn(Result.success(PAYMENT_METHOD)) - - whenever( - stripeRepository.getPaymentMethods( - listPaymentMethodsParams = any(), - productUsageTokens = any(), - requestOptions = any() - ) - ).thenReturn(Result.success(listOf(PAYMENT_METHOD))) - - whenever( - stripeRepository.setCustomerShippingInfo( - any(), - eq(ApiKeyFixtures.FAKE_PUBLISHABLE_KEY), - any(), - any(), - any() - ) - ).thenReturn(Result.success(FIRST_CUSTOMER)) - } - } - - @Test - fun getInstance_withoutInitializing_throwsException() { - CustomerSession.clearInstance() - - assertFailsWith { CustomerSession.getInstance() } - } - - @Test - fun addProductUsageTokenIfValid_whenValid_addsExpectedTokens() = runTest { - val customerSession = createCustomerSession( - ephemeralKeyManagerFactory = FakeEphemeralKeyManagerFactory( - ephemeralKeyProvider, - EphemeralKeyFixtures.create(2528128663000L) - ) - ) - - customerSession.attachPaymentMethod( - "pm_12345", - DEFAULT_PRODUCT_USAGE, - paymentMethodRetrievalListener - ) - idleLooper() - - verify(stripeRepository).attachPaymentMethod( - customerId = any(), - productUsageTokens = eq(DEFAULT_PRODUCT_USAGE), - paymentMethodId = any(), - requestOptions = any() - ) - } - - @Test - fun create_withoutInvokingFunctions_fetchesKeyAndCustomer() = runTest { - ephemeralKeyProvider.setNextRawEphemeralKey(EphemeralKeyFixtures.FIRST_JSON) - val customerSession = createCustomerSession() - idleLooper() - - verify(stripeRepository).retrieveCustomer( - eq(EphemeralKeyFixtures.FIRST.objectId), - productUsageArgumentCaptor.capture(), - requestOptionsArgumentCaptor.capture() - ) - assertEquals( - EphemeralKeyFixtures.FIRST.secret, - requestOptionsArgumentCaptor.firstValue.apiKey - ) - val customerId = customerSession.customer?.id - assertEquals(FIRST_CUSTOMER.id, customerId) - } - - @Test - fun setCustomerShippingInfo_withValidInfo_callsWithExpectedArgs() = runTest { - ephemeralKeyProvider.setNextRawEphemeralKey(EphemeralKeyFixtures.FIRST_JSON) - val customerSession = createCustomerSession() - val shippingInformation = - requireNotNull(CustomerFixtures.CUSTOMER_WITH_SHIPPING.shippingInformation) - customerSession.setCustomerShippingInformation( - shippingInformation = shippingInformation, - productUsage = DEFAULT_PRODUCT_USAGE, - listener = object : CustomerSession.CustomerRetrievalListener { - override fun onCustomerRetrieved(customer: Customer) { - } - - override fun onError( - errorCode: Int, - errorMessage: String, - stripeError: StripeError? - ) { - } - } - ) - idleLooper() - - assertNotNull(FIRST_CUSTOMER.id) - verify(stripeRepository).setCustomerShippingInfo( - eq(FIRST_CUSTOMER.id.orEmpty()), - eq(ApiKeyFixtures.FAKE_PUBLISHABLE_KEY), - eq(DEFAULT_PRODUCT_USAGE), - eq(shippingInformation), - requestOptionsArgumentCaptor.capture() - ) - assertEquals( - EphemeralKeyFixtures.FIRST.secret, - requestOptionsArgumentCaptor.firstValue.apiKey - ) - } - - @Test - fun retrieveCustomer_withExpiredCache_updatesCustomer() = runTest { - val firstKey = EphemeralKeyFixtures.FIRST - val secondKey = EphemeralKeyFixtures.SECOND - - val firstExpiryTimeInMillis = TimeUnit.SECONDS.toMillis(firstKey.expires) - var currentTime = firstExpiryTimeInMillis - 100L - - ephemeralKeyProvider.setNextRawEphemeralKey(EphemeralKeyFixtures.FIRST_JSON) - val customerSession = createCustomerSession( - timeSupplier = { currentTime } - ) - idleLooper() - assertEquals(firstKey.objectId, FIRST_CUSTOMER.id) - - val firstCustomerCacheTime = customerSession.customerCacheTime - assertEquals(firstExpiryTimeInMillis - 100L, firstCustomerCacheTime) - val timeForCustomerToExpire = TimeUnit.MINUTES.toMillis(2) - - currentTime = firstCustomerCacheTime + timeForCustomerToExpire - - // We want to make sure that the next ephemeral key will be different. - ephemeralKeyProvider.setNextRawEphemeralKey(EphemeralKeyFixtures.SECOND_JSON) - - // The key manager should think it is necessary to update the key, - // because the first one was expired. - customerSession - .retrieveCurrentCustomer(DEFAULT_PRODUCT_USAGE, customerRetrievalListener) - idleLooper() - - verify(customerRetrievalListener) - .onCustomerRetrieved(customerArgumentCaptor.capture()) - val capturedCustomer = customerArgumentCaptor.firstValue - assertEquals(SECOND_CUSTOMER.id, capturedCustomer.id) - - val customerId = customerSession.customer?.id - // Make sure the value is cached. - assertEquals(SECOND_CUSTOMER.id, customerId) - - verify(stripeRepository).retrieveCustomer( - eq(firstKey.objectId), - eq(emptySet()), - requestOptionsArgumentCaptor.capture() - ) - assertEquals( - firstKey.secret, - requestOptionsArgumentCaptor.firstValue.apiKey - ) - verify(stripeRepository).retrieveCustomer( - eq(secondKey.objectId), - eq(DEFAULT_PRODUCT_USAGE), - requestOptionsArgumentCaptor.capture() - ) - assertEquals( - secondKey.secret, - requestOptionsArgumentCaptor.allValues[1].apiKey - ) - } - - @Test - fun retrieveCustomer_withUnExpiredCache_returnsCustomerWithoutHittingApi() = runTest { - val firstKey = EphemeralKeyFixtures.FIRST - - var currentTime = DEFAULT_CURRENT_TIME - - ephemeralKeyProvider.setNextRawEphemeralKey(EphemeralKeyFixtures.FIRST_JSON) - val customerSession = createCustomerSession(timeSupplier = { currentTime }) - idleLooper() - - // Make sure we're in a good state and that we have the expected customer - assertEquals(firstKey.objectId, FIRST_CUSTOMER.id) - assertEquals(firstKey.objectId, customerSession.customer?.id) - - verify(stripeRepository).retrieveCustomer( - eq(firstKey.objectId), - productUsageArgumentCaptor.capture(), - requestOptionsArgumentCaptor.capture() - ) - assertEquals( - firstKey.secret, - requestOptionsArgumentCaptor.firstValue.apiKey - ) - - val firstCustomerCacheTime = customerSession.customerCacheTime - val shortIntervalInMilliseconds = 10L - currentTime = firstCustomerCacheTime + shortIntervalInMilliseconds - - // The key manager should think it is necessary to update the key, - // because the first one was expired. - customerSession.retrieveCurrentCustomer(customerRetrievalListener) - - verify(customerRetrievalListener) - .onCustomerRetrieved(customerArgumentCaptor.capture()) - val capturedCustomer = customerArgumentCaptor.firstValue - - assertEquals(FIRST_CUSTOMER.id, capturedCustomer.id) - // Make sure the value is cached. - assertEquals(FIRST_CUSTOMER.id, customerSession.customer?.id) - verifyNoMoreInteractions(stripeRepository) - } - - @Test - fun addSourceToCustomer_withUnExpiredCustomer_returnsAddedSource() = runTest { - val expectedProductUsage = setOf( - AddPaymentMethodActivity.PRODUCT_TOKEN, - PaymentMethodsActivity.PRODUCT_TOKEN - ) - - var currentTime = DEFAULT_CURRENT_TIME - ephemeralKeyProvider.setNextRawEphemeralKey(EphemeralKeyFixtures.FIRST_JSON) - - val customerSession = createCustomerSession(timeSupplier = { currentTime }) - - val firstCustomerCacheTime = customerSession.customerCacheTime - val shortIntervalInMilliseconds = 10L - - currentTime = firstCustomerCacheTime + shortIntervalInMilliseconds - - customerSession.addCustomerSource( - sourceId = "abc123", - sourceType = Source.SourceType.CARD, - productUsage = expectedProductUsage, - listener = sourceRetrievalListener - ) - idleLooper() - - assertNotNull(FIRST_CUSTOMER.id) - verify(stripeRepository).addCustomerSource( - eq(FIRST_CUSTOMER.id.orEmpty()), - eq(ApiKeyFixtures.FAKE_PUBLISHABLE_KEY), - eq(expectedProductUsage), - eq("abc123"), - eq(Source.SourceType.CARD), - requestOptionsArgumentCaptor.capture() - ) - assertEquals( - EphemeralKeyFixtures.FIRST.secret, - requestOptionsArgumentCaptor.firstValue.apiKey - ) - - verify(sourceRetrievalListener) - .onSourceRetrieved(sourceArgumentCaptor.capture()) - val capturedSource = sourceArgumentCaptor.firstValue - assertEquals(SourceFixtures.SOURCE_CARD.id, capturedSource.id) - } - - @Test - fun addSourceToCustomer_whenApiThrowsError_callsListener() = runTest { - val expectedProductUsage = setOf( - AddPaymentMethodActivity.PRODUCT_TOKEN, - PaymentMethodsActivity.PRODUCT_TOKEN - ) - - var currentTime = DEFAULT_CURRENT_TIME - - ephemeralKeyProvider.setNextRawEphemeralKey(EphemeralKeyFixtures.FIRST_JSON) - val customerSession = createCustomerSession(timeSupplier = { currentTime }) - - val firstCustomerCacheTime = customerSession.customerCacheTime - val shortIntervalInMilliseconds = 10L - - currentTime = firstCustomerCacheTime + shortIntervalInMilliseconds - - setupErrorProxy() - customerSession.addCustomerSource( - sourceId = "abc123", - sourceType = Source.SourceType.CARD, - productUsage = expectedProductUsage, - listener = sourceRetrievalListener - ) - idleLooper() - - verify(sourceRetrievalListener) - .onError(404, "The card is invalid", null) - } - - @Test - fun removeSourceFromCustomer_withUnExpiredCustomer_returnsRemovedSource() = runTest { - val expectedProductUsage = setOf( - AddPaymentMethodActivity.PRODUCT_TOKEN, - PaymentMethodsActivity.PRODUCT_TOKEN - ) - - var currentTime = DEFAULT_CURRENT_TIME - - ephemeralKeyProvider.setNextRawEphemeralKey(EphemeralKeyFixtures.FIRST_JSON) - val customerSession = createCustomerSession(timeSupplier = { currentTime }) - - val firstCustomerCacheTime = customerSession.customerCacheTime - val shortIntervalInMilliseconds = 10L - - currentTime = firstCustomerCacheTime + shortIntervalInMilliseconds - - customerSession.deleteCustomerSource( - sourceId = "abc123", - productUsage = expectedProductUsage, - listener = sourceRetrievalListener - ) - idleLooper() - - assertNotNull(FIRST_CUSTOMER.id) - verify(stripeRepository).deleteCustomerSource( - eq(FIRST_CUSTOMER.id.orEmpty()), - eq(ApiKeyFixtures.FAKE_PUBLISHABLE_KEY), - eq(expectedProductUsage), - eq("abc123"), - requestOptionsArgumentCaptor.capture() - ) - assertEquals( - EphemeralKeyFixtures.FIRST.secret, - requestOptionsArgumentCaptor.firstValue.apiKey - ) - - verify(sourceRetrievalListener) - .onSourceRetrieved(sourceArgumentCaptor.capture()) - val capturedSource = sourceArgumentCaptor.firstValue - assertEquals(SourceFixtures.SOURCE_CARD.id, capturedSource.id) - } - - @Test - fun removeSourceFromCustomer_whenApiThrowsError_callsListener() = runTest { - val expectedProductUsage = setOf( - AddPaymentMethodActivity.PRODUCT_TOKEN, - PaymentMethodsActivity.PRODUCT_TOKEN - ) - - var currentTime = DEFAULT_CURRENT_TIME - - ephemeralKeyProvider.setNextRawEphemeralKey(EphemeralKeyFixtures.FIRST_JSON) - val customerSession = createCustomerSession(timeSupplier = { currentTime }) - - val firstCustomerCacheTime = customerSession.customerCacheTime - val shortIntervalInMilliseconds = 10L - - currentTime = firstCustomerCacheTime + shortIntervalInMilliseconds - - setupErrorProxy() - customerSession.deleteCustomerSource( - sourceId = "abc123", - productUsage = expectedProductUsage, - listener = sourceRetrievalListener - ) - idleLooper() - - verify(sourceRetrievalListener) - .onError(404, "The card does not exist", null) - } - - @Test - fun setDefaultSourceForCustomer_withUnExpiredCustomer_returnsCustomerAndClearsLog() = runTest { - var currentTime = DEFAULT_CURRENT_TIME - - ephemeralKeyProvider.setNextRawEphemeralKey(EphemeralKeyFixtures.FIRST_JSON) - val customerSession = createCustomerSession(timeSupplier = { currentTime }) - - val firstCustomerCacheTime = customerSession.customerCacheTime - val shortIntervalInMilliseconds = 10L - - currentTime = firstCustomerCacheTime + shortIntervalInMilliseconds - - customerSession.setCustomerDefaultSource( - "abc123", - Source.SourceType.CARD, - setOf(PaymentMethodsActivity.PRODUCT_TOKEN), - customerRetrievalListener - ) - idleLooper() - - assertNotNull(FIRST_CUSTOMER.id) - verify(stripeRepository).setDefaultCustomerSource( - eq(FIRST_CUSTOMER.id.orEmpty()), - eq(ApiKeyFixtures.FAKE_PUBLISHABLE_KEY), - eq(setOf(PaymentMethodsActivity.PRODUCT_TOKEN)), - eq("abc123"), - eq(Source.SourceType.CARD), - requestOptionsArgumentCaptor.capture() - ) - assertEquals( - EphemeralKeyFixtures.FIRST.secret, - requestOptionsArgumentCaptor.firstValue.apiKey - ) - - verify(customerRetrievalListener) - .onCustomerRetrieved(customerArgumentCaptor.capture()) - val capturedCustomer = customerArgumentCaptor.firstValue - assertNotNull(SECOND_CUSTOMER) - assertEquals(SECOND_CUSTOMER.id, capturedCustomer.id) - } - - @Test - fun setDefaultSourceForCustomer_whenApiThrows_callsListenerAndClearsLogs() = runTest { - var currentTime = DEFAULT_CURRENT_TIME - - ephemeralKeyProvider.setNextRawEphemeralKey(EphemeralKeyFixtures.FIRST_JSON) - val customerSession = createCustomerSession(timeSupplier = { currentTime }) - - val firstCustomerCacheTime = customerSession.customerCacheTime - val shortIntervalInMilliseconds = 10L - - currentTime = firstCustomerCacheTime + shortIntervalInMilliseconds - - setupErrorProxy() - customerSession.setCustomerDefaultSource( - "abc123", - Source.SourceType.CARD, - customerRetrievalListener - ) - idleLooper() - - verify(customerRetrievalListener) - .onError(405, "auth error", null) -// assertTrue(customerSession.productUsageTokens.isEmpty()) - } - - @Test - fun attachPaymentMethodToCustomer_withUnExpiredCustomer_returnsAddedPaymentMethod() = runTest { - val expectedProductUsage = setOf( - AddPaymentMethodActivity.PRODUCT_TOKEN, - PaymentMethodsActivity.PRODUCT_TOKEN - ) - - var currentTime = DEFAULT_CURRENT_TIME - - ephemeralKeyProvider.setNextRawEphemeralKey(EphemeralKeyFixtures.FIRST_JSON) - val customerSession = createCustomerSession(timeSupplier = { currentTime }) - - val firstCustomerCacheTime = customerSession.customerCacheTime - val shortIntervalInMilliseconds = 10L - - currentTime = firstCustomerCacheTime + shortIntervalInMilliseconds - - customerSession.attachPaymentMethod( - paymentMethodId = "pm_abc123", - productUsage = expectedProductUsage, - listener = paymentMethodRetrievalListener - ) - idleLooper() - - assertNotNull(FIRST_CUSTOMER.id) - verify(stripeRepository).attachPaymentMethod( - customerId = eq(FIRST_CUSTOMER.id.orEmpty()), - productUsageTokens = eq(expectedProductUsage), - paymentMethodId = eq("pm_abc123"), - requestOptions = requestOptionsArgumentCaptor.capture() - ) - assertEquals( - EphemeralKeyFixtures.FIRST.secret, - requestOptionsArgumentCaptor.firstValue.apiKey - ) - - verify(paymentMethodRetrievalListener) - .onPaymentMethodRetrieved(paymentMethodArgumentCaptor.capture()) - val capturedPaymentMethod = paymentMethodArgumentCaptor.firstValue - assertEquals(PAYMENT_METHOD.id, capturedPaymentMethod.id) - } - - @Test - fun attachPaymentMethodToCustomer_whenApiThrowsError_callsListenerOnError() = runTest { - val expectedProductUsage = setOf( - AddPaymentMethodActivity.PRODUCT_TOKEN, - PaymentMethodsActivity.PRODUCT_TOKEN - ) - - var currentTime = DEFAULT_CURRENT_TIME - - ephemeralKeyProvider.setNextRawEphemeralKey(EphemeralKeyFixtures.FIRST_JSON) - val customerSession = createCustomerSession(timeSupplier = { currentTime }) - - val firstCustomerCacheTime = customerSession.customerCacheTime - val shortIntervalInMilliseconds = 10L - currentTime = firstCustomerCacheTime + shortIntervalInMilliseconds - - setupErrorProxy() - customerSession.attachPaymentMethod( - paymentMethodId = "pm_abc123", - productUsage = expectedProductUsage, - listener = paymentMethodRetrievalListener - ) - idleLooper() - - verify(paymentMethodRetrievalListener) - .onError(404, "The payment method is invalid", null) - } - - @Test - fun detachPaymentMethodFromCustomer_withUnExpiredCustomer_returnsRemovedPaymentMethod() = runTest { - val expectedProductUsage = setOf( - AddPaymentMethodActivity.PRODUCT_TOKEN, - PaymentMethodsActivity.PRODUCT_TOKEN - ) - - var currentTime = DEFAULT_CURRENT_TIME - - ephemeralKeyProvider.setNextRawEphemeralKey(EphemeralKeyFixtures.FIRST_JSON) - val customerSession = createCustomerSession(timeSupplier = { currentTime }) - - val firstCustomerCacheTime = customerSession.customerCacheTime - val shortIntervalInMilliseconds = 10L - - currentTime = firstCustomerCacheTime + shortIntervalInMilliseconds - - customerSession.detachPaymentMethod( - paymentMethodId = "pm_abc123", - productUsage = expectedProductUsage, - listener = paymentMethodRetrievalListener - ) - idleLooper() - - assertNotNull(FIRST_CUSTOMER.id) - verify(stripeRepository).detachPaymentMethod( - productUsageTokens = eq(expectedProductUsage), - paymentMethodId = eq("pm_abc123"), - requestOptions = requestOptionsArgumentCaptor.capture() - ) - assertEquals( - EphemeralKeyFixtures.FIRST.secret, - requestOptionsArgumentCaptor.firstValue.apiKey - ) - - verify(paymentMethodRetrievalListener) - .onPaymentMethodRetrieved(paymentMethodArgumentCaptor.capture()) - val capturedPaymentMethod = paymentMethodArgumentCaptor.firstValue - assertEquals(PAYMENT_METHOD.id, capturedPaymentMethod.id) - } - - @Test - fun detachPaymentMethodFromCustomer_whenApiThrowsError_callsListenerOnError() = runTest { - var currentTime = DEFAULT_CURRENT_TIME - - ephemeralKeyProvider.setNextRawEphemeralKey(EphemeralKeyFixtures.FIRST_JSON) - val customerSession = createCustomerSession(timeSupplier = { currentTime }) - - val firstCustomerCacheTime = customerSession.customerCacheTime - val shortIntervalInMilliseconds = 10L - - currentTime = firstCustomerCacheTime + shortIntervalInMilliseconds - - setupErrorProxy() - customerSession.detachPaymentMethod( - paymentMethodId = "pm_abc123", - listener = paymentMethodRetrievalListener - ) - idleLooper() - - verify(paymentMethodRetrievalListener) - .onError(404, "The payment method does not exist", null) - } - - @Test - fun getPaymentMethods_withUnExpiredCustomer_returnsAddedPaymentMethod() = runTest { - val expectedProductUsage = setOf( - AddPaymentMethodActivity.PRODUCT_TOKEN, - PaymentMethodsActivity.PRODUCT_TOKEN - ) - - var currentTime = DEFAULT_CURRENT_TIME - - ephemeralKeyProvider.setNextRawEphemeralKey(EphemeralKeyFixtures.FIRST_JSON) - val customerSession = createCustomerSession(timeSupplier = { currentTime }) - - val firstCustomerCacheTime = customerSession.customerCacheTime - val shortIntervalInMilliseconds = 10L - - currentTime = firstCustomerCacheTime + shortIntervalInMilliseconds - - customerSession.getPaymentMethods( - paymentMethodType = PaymentMethod.Type.Card, - productUsage = expectedProductUsage, - listener = paymentMethodsRetrievalListener - ) - idleLooper() - - assertNotNull(FIRST_CUSTOMER.id) - verify(stripeRepository).getPaymentMethods( - listPaymentMethodsParams = eq( - ListPaymentMethodsParams( - customerId = FIRST_CUSTOMER.id.orEmpty(), - paymentMethodType = PaymentMethod.Type.Card - ) - ), - productUsageTokens = eq(expectedProductUsage), - requestOptions = requestOptionsArgumentCaptor.capture() - ) - assertEquals( - EphemeralKeyFixtures.FIRST.secret, - requestOptionsArgumentCaptor.firstValue.apiKey - ) - - verify(paymentMethodsRetrievalListener) - .onPaymentMethodsRetrieved(paymentMethodsArgumentCaptor.capture()) - val paymentMethods = paymentMethodsArgumentCaptor.firstValue - assertNotNull(paymentMethods) - } - - private suspend fun setupErrorProxy() { - whenever( - stripeRepository.addCustomerSource( - any(), - eq(ApiKeyFixtures.FAKE_PUBLISHABLE_KEY), - any(), - any(), - any(), - any() - ) - ).thenReturn( - Result.failure(APIException(statusCode = 404, message = "The card is invalid")) - ) - - whenever( - stripeRepository.deleteCustomerSource( - any(), - eq(ApiKeyFixtures.FAKE_PUBLISHABLE_KEY), - any(), - any(), - any() - ) - ).thenReturn( - Result.failure(APIException(statusCode = 404, message = "The card does not exist")) - ) - - whenever( - stripeRepository.setDefaultCustomerSource( - any(), - eq(ApiKeyFixtures.FAKE_PUBLISHABLE_KEY), - any(), - any(), - any(), - any() - ) - ).thenReturn(Result.failure(APIException(statusCode = 405, message = "auth error"))) - - whenever( - stripeRepository.attachPaymentMethod( - customerId = any(), - productUsageTokens = any(), - paymentMethodId = any(), - requestOptions = any() - ) - ).thenReturn( - Result.failure(APIException(statusCode = 404, message = "The payment method is invalid")) - ) - - whenever( - stripeRepository.detachPaymentMethod( - productUsageTokens = any(), - paymentMethodId = any(), - requestOptions = any() - ) - ).thenReturn( - Result.failure(APIException(statusCode = 404, message = "The payment method does not exist")) - ) - - whenever( - stripeRepository.getPaymentMethods( - listPaymentMethodsParams = any(), - productUsageTokens = any(), - requestOptions = any() - ) - ).thenReturn( - Result.failure(APIException(statusCode = 404, message = "The payment method does not exist")) - ) - } - - private fun createCustomerSession( - timeSupplier: TimeSupplier = { Calendar.getInstance().timeInMillis }, - ephemeralKeyManagerFactory: EphemeralKeyManager.Factory = - createEphemeralKeyManagerFactory(timeSupplier) - ): CustomerSession { - return CustomerSession( - stripeRepository, - ApiKeyFixtures.FAKE_PUBLISHABLE_KEY, - "acct_abc123", - timeSupplier = timeSupplier, - workContext = testDispatcher, - ephemeralKeyManagerFactory = ephemeralKeyManagerFactory - ) - } - - private fun createEphemeralKeyManagerFactory( - timeSupplier: TimeSupplier - ): EphemeralKeyManager.Factory { - return EphemeralKeyManager.Factory.Default( - keyProvider = ephemeralKeyProvider, - shouldPrefetchEphemeralKey = true, - timeSupplier = timeSupplier - ) - } - - private class FakeEphemeralKeyManagerFactory( - private val ephemeralKeyProvider: EphemeralKeyProvider, - private val ephemeralKey: EphemeralKey - ) : EphemeralKeyManager.Factory { - override fun create( - arg: EphemeralKeyManager.KeyManagerListener - ): EphemeralKeyManager { - return EphemeralKeyManager( - ephemeralKeyProvider, - arg - ).also { - it.ephemeralKey = ephemeralKey - } - } - } - - private companion object { - private val FIRST_CUSTOMER = CustomerFixtures.CUSTOMER - private val SECOND_CUSTOMER = CustomerFixtures.OTHER_CUSTOMER - - private val PAYMENT_METHOD = PaymentMethodFixtures.CARD_PAYMENT_METHOD - - private val DEFAULT_CURRENT_TIME = - TimeUnit.SECONDS.toMillis(EphemeralKeyFixtures.FIRST.expires) + - TimeUnit.MINUTES.toMillis(2) - - private val DEFAULT_PRODUCT_USAGE = setOf(PaymentMethodsActivity.PRODUCT_TOKEN) - } -} diff --git a/payments-core/src/test/java/com/stripe/android/PaymentSessionConfigTest.kt b/payments-core/src/test/java/com/stripe/android/PaymentSessionConfigTest.kt deleted file mode 100644 index ce3d0f92e8e..00000000000 --- a/payments-core/src/test/java/com/stripe/android/PaymentSessionConfigTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.stripe.android - -import com.google.common.truth.Truth.assertThat -import com.stripe.android.PaymentSessionFixtures.CONFIG -import com.stripe.android.model.ShippingInformation -import com.stripe.android.utils.ParcelUtils -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import kotlin.test.Test -import kotlin.test.assertFailsWith - -@RunWith(RobolectricTestRunner::class) -class PaymentSessionConfigTest { - - @Test - fun testParcel() { - assertThat(ParcelUtils.create(CONFIG)).isEqualTo(CONFIG) - } - - @Test - fun create_withValidCountryCode_succeeds() { - val allowedShippingCountryCodes = setOf("us", "CA") - val config = CONFIG.copy( - allowedShippingCountryCodes = allowedShippingCountryCodes - ) - assertThat(config.allowedShippingCountryCodes).isEqualTo(allowedShippingCountryCodes) - } - - @Test - fun create_withEmptyCountryCodesList_succeeds() { - val config = CONFIG.copy( - allowedShippingCountryCodes = emptySet() - ) - assertThat(config.allowedShippingCountryCodes.isEmpty()).isTrue() - } - - @Test - fun create_withInvalidCountryCode_throwsException() { - val exception: IllegalArgumentException = assertFailsWith { - CONFIG.copy( - allowedShippingCountryCodes = setOf("invalid_country_code") - ) - } - assertThat(exception.message).isEqualTo("'invalid_country_code' is not a valid country code") - } - - @Test - fun create_withShippingMethodsRequiredAndShippingInformationValidatorProvided_withoutShippingMethodsFactory_throwsException() { - assertFailsWith { - PaymentSessionConfig.Builder() - .setShippingInfoRequired(true) - .setShippingMethodsRequired(true) - .setShippingInformationValidator(FakeShippingInfoValidator()) - .build() - } - } - - private class FakeShippingInfoValidator : PaymentSessionConfig.ShippingInformationValidator { - override fun isValid(shippingInformation: ShippingInformation): Boolean { - return true - } - - override fun getErrorMessage(shippingInformation: ShippingInformation): String { - return "" - } - } -} diff --git a/payments-core/src/test/java/com/stripe/android/PaymentSessionDataTest.kt b/payments-core/src/test/java/com/stripe/android/PaymentSessionDataTest.kt deleted file mode 100644 index 513961c6f38..00000000000 --- a/payments-core/src/test/java/com/stripe/android/PaymentSessionDataTest.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.stripe.android - -import com.stripe.android.model.PaymentMethodFixtures -import com.stripe.android.model.ShippingInformation -import com.stripe.android.model.ShippingMethod -import com.stripe.android.utils.ParcelUtils -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -@RunWith(RobolectricTestRunner::class) -class PaymentSessionDataTest { - - @Test - fun updateIsPaymentReadyToCharge_noShippingRequired() { - val config = PaymentSessionConfig.Builder() - .setShippingInfoRequired(false) - .setShippingMethodsRequired(false) - .build() - - val data = PaymentSessionData(config) - assertFalse(data.isPaymentReadyToCharge) - assertTrue( - data.copy(paymentMethod = PAYMENT_METHOD).isPaymentReadyToCharge - ) - } - - @Test - fun updateIsPaymentReadyToCharge_shippingRequired() { - val config = PaymentSessionFixtures.CONFIG - - assertFalse(PaymentSessionData(config).isPaymentReadyToCharge) - - assertFalse( - PaymentSessionData( - isShippingInfoRequired = config.isShippingInfoRequired, - isShippingMethodRequired = config.isShippingMethodRequired, - paymentMethod = PAYMENT_METHOD - ).isPaymentReadyToCharge - ) - - assertFalse( - PaymentSessionData( - isShippingInfoRequired = config.isShippingInfoRequired, - isShippingMethodRequired = config.isShippingMethodRequired, - paymentMethod = PAYMENT_METHOD, - shippingInformation = ShippingInformation() - ).isPaymentReadyToCharge - ) - - assertTrue( - PaymentSessionData( - isShippingInfoRequired = config.isShippingInfoRequired, - isShippingMethodRequired = config.isShippingMethodRequired, - paymentMethod = PAYMENT_METHOD, - shippingInformation = ShippingInformation(), - shippingMethod = ShippingMethod("label", "id", 0, "USD") - ).isPaymentReadyToCharge - ) - } - - @Test - fun writeToParcel_withNulls_readsFromParcelCorrectly() { - val data = PaymentSessionData( - isShippingInfoRequired = true, - isShippingMethodRequired = true, - cartTotal = 100L, - shippingTotal = 150L - ) - - assertEquals(data, ParcelUtils.create(data)) - } - - @Test - fun writeToParcel_withoutNulls_readsFromParcelCorrectly() { - val data = PaymentSessionData( - isShippingInfoRequired = true, - isShippingMethodRequired = true, - cartTotal = 100L, - shippingTotal = 150L, - paymentMethod = PAYMENT_METHOD, - shippingInformation = ShippingInformation(), - shippingMethod = ShippingMethod("UPS", "SuperFast", 10000L, "USD") - ) - - assertEquals(data, ParcelUtils.create(data)) - } - - private companion object { - private val PAYMENT_METHOD = PaymentMethodFixtures.CARD_PAYMENT_METHOD - } -} diff --git a/payments-core/src/test/java/com/stripe/android/PaymentSessionFixtures.kt b/payments-core/src/test/java/com/stripe/android/PaymentSessionFixtures.kt deleted file mode 100644 index 64ccc19878f..00000000000 --- a/payments-core/src/test/java/com/stripe/android/PaymentSessionFixtures.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.stripe.android - -import com.stripe.android.model.Address -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.ShippingInformation -import com.stripe.android.model.ShippingMethod -import com.stripe.android.view.BillingAddressFields -import com.stripe.android.view.PaymentFlowActivityStarter -import com.stripe.android.view.ShippingInfoWidget - -internal object PaymentSessionFixtures { - internal val CONFIG = PaymentSessionConfig.Builder() - // hide the phone field on the shipping information form - .setHiddenShippingInfoFields( - ShippingInfoWidget.CustomizableShippingField.Line2 - ) - // make the address line 2 field optional - .setOptionalShippingInfoFields( - ShippingInfoWidget.CustomizableShippingField.Phone - ) - // specify an address to pre-populate the shipping information form - .setPrepopulatedShippingInfo( - ShippingInformation( - Address.Builder() - .setLine1("123 Market St") - .setCity("San Francisco") - .setState("CA") - .setPostalCode("94107") - .setCountry("US") - .build(), - "Jenny Rosen", - "4158675309" - ) - ) - // collect shipping information - .setShippingInfoRequired(true) - // collect shipping method - .setShippingMethodsRequired(true) - // specify the payment method types that the customer can use; - // defaults to PaymentMethod.Type.Card - .setPaymentMethodTypes( - listOf(PaymentMethod.Type.Card) - ) - // only allowed US and Canada shipping addresses - .setAllowedShippingCountryCodes( - setOf("US", "CA") - ) - .setBillingAddressFields(BillingAddressFields.Full) - .setShouldPrefetchCustomer(true) - // Enable PaymentMethod Deletion from PaymentMethodActivity - // This is default behavior - .setCanDeletePaymentMethods(true) - .setShippingInformationValidator(FakeShippingInformationValidator()) - .setShippingMethodsFactory(FakeShippingMethodsFactory()) - .build() - - internal val PAYMENT_SESSION_DATA = PaymentSessionData(CONFIG) - - internal val PAYMENT_FLOW_ARGS = PaymentFlowActivityStarter.Args( - paymentSessionConfig = CONFIG, - paymentSessionData = PAYMENT_SESSION_DATA - ) - - private class FakeShippingInformationValidator : PaymentSessionConfig.ShippingInformationValidator { - override fun isValid(shippingInformation: ShippingInformation): Boolean { - return true - } - - override fun getErrorMessage(shippingInformation: ShippingInformation): String { - return "" - } - } - - private class FakeShippingMethodsFactory : PaymentSessionConfig.ShippingMethodsFactory { - override fun create(shippingInformation: ShippingInformation): List { - return listOf( - ShippingMethod( - "UPS Ground", - "ups-ground", - 0, - "USD", - "Arrives in 3-5 days" - ), - ShippingMethod( - "FedEx", - "fedex", - 599, - "USD", - "Arrives tomorrow" - ) - ) - } - } -} diff --git a/payments-core/src/test/java/com/stripe/android/PaymentSessionTest.kt b/payments-core/src/test/java/com/stripe/android/PaymentSessionTest.kt deleted file mode 100644 index 80c09548642..00000000000 --- a/payments-core/src/test/java/com/stripe/android/PaymentSessionTest.kt +++ /dev/null @@ -1,407 +0,0 @@ -package com.stripe.android - -import android.app.Activity.RESULT_CANCELED -import android.app.Activity.RESULT_OK -import android.content.Context -import android.content.Intent -import androidx.activity.ComponentActivity -import androidx.test.core.app.ActivityScenario -import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth.assertThat -import com.stripe.android.core.networking.ApiRequest -import com.stripe.android.model.Customer -import com.stripe.android.model.CustomerFixtures -import com.stripe.android.model.ListPaymentMethodsParams -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.PaymentMethodCreateParams -import com.stripe.android.model.PaymentMethodFixtures -import com.stripe.android.networking.StripeRepository -import com.stripe.android.testharness.TestEphemeralKeyProvider -import com.stripe.android.testing.AbsFakeStripeRepository -import com.stripe.android.utils.TestUtils.idleLooper -import com.stripe.android.view.ActivityScenarioFactory -import com.stripe.android.view.ActivityStarter -import com.stripe.android.view.BillingAddressFields -import com.stripe.android.view.PaymentFlowActivity -import com.stripe.android.view.PaymentFlowActivityStarter -import com.stripe.android.view.PaymentMethodsActivity -import com.stripe.android.view.PaymentMethodsActivityStarter -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import org.junit.runner.RunWith -import org.mockito.Mockito.never -import org.mockito.Mockito.reset -import org.mockito.kotlin.KArgumentCaptor -import org.mockito.kotlin.any -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.times -import org.mockito.kotlin.verify -import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows -import kotlin.test.BeforeTest -import kotlin.test.Test - -@RunWith(RobolectricTestRunner::class) -class PaymentSessionTest { - private val testDispatcher = UnconfinedTestDispatcher() - - private val ephemeralKeyProvider = TestEphemeralKeyProvider() - - private val paymentSessionListener = mock() - private val customerSession = mock() - private val paymentMethodsActivityStarter: - ActivityStarter = mock() - private val paymentFlowActivityStarter: - ActivityStarter = mock() - - private val paymentSessionDataArgumentCaptor: KArgumentCaptor = argumentCaptor() - private val paymentMethodsActivityStarterArgsCaptor: KArgumentCaptor = - argumentCaptor() - private val productUsageArgumentCaptor: KArgumentCaptor> = argumentCaptor() - - private val context: Context = ApplicationProvider.getApplicationContext() - private val activityScenarioFactory = ActivityScenarioFactory(context) - - @BeforeTest - fun setup() { - PaymentConfiguration.init(context, ApiKeyFixtures.FAKE_PUBLISHABLE_KEY) - CustomerSession.instance = createCustomerSession() - } - - @Test - fun init_addsPaymentSessionToken_andFetchesCustomer() { - CustomerSession.instance = customerSession - createActivity { activity -> - val paymentSession = PaymentSession( - activity, - DEFAULT_CONFIG.copy( - shouldPrefetchCustomer = true - ) - ) - paymentSession.init(paymentSessionListener) - idleLooper() - - verify(customerSession).retrieveCurrentCustomer( - productUsageArgumentCaptor.capture(), - any() - ) - - assertThat(productUsageArgumentCaptor.firstValue).isEqualTo(setOf(PaymentSession.PRODUCT_TOKEN)) - - verify(paymentSessionListener) - .onCommunicatingStateChanged(eq(true)) - } - } - - @Test - fun init_whenEphemeralKeyProviderContinues_fetchesCustomerAndNotifiesListener() { - ephemeralKeyProvider - .setNextRawEphemeralKey(EphemeralKeyFixtures.FIRST_JSON) - - createActivity { - val paymentSession = PaymentSession(it, DEFAULT_CONFIG) - paymentSession.init(paymentSessionListener) - idleLooper() - - verify(paymentSessionListener) - .onCommunicatingStateChanged(true) - verify(paymentSessionListener) - .onCommunicatingStateChanged(false) - } - } - - @Test - fun handlePaymentData_whenPaymentMethodSelected_notifiesListenerAndFetchesCustomer() { - createActivity { - val paymentSession = PaymentSession(it, DEFAULT_CONFIG) - paymentSession.init(paymentSessionListener) - - // We have already tested the functionality up to here. - reset(paymentSessionListener) - - val result = PaymentMethodsActivityStarter.Result( - paymentMethod = PaymentMethodFixtures.CARD_PAYMENT_METHOD - ) - val handled = paymentSession.handlePaymentData( - PaymentMethodsActivityStarter.REQUEST_CODE, - RESULT_OK, - Intent().putExtras(result.toBundle()) - ) - assertThat(handled).isTrue() - - verify(paymentSessionListener) - .onPaymentSessionDataChanged(paymentSessionDataArgumentCaptor.capture()) - val data = paymentSessionDataArgumentCaptor.firstValue - assertThat(data.paymentMethod).isEqualTo(PaymentMethodFixtures.CARD_PAYMENT_METHOD) - assertThat(data.useGooglePay).isFalse() - } - } - - @Test - fun handlePaymentData_whenGooglePaySelected_notifiesListenerAndFetchesCustomer() { - createActivity { - val paymentSession = PaymentSession(it, DEFAULT_CONFIG) - paymentSession.init(paymentSessionListener) - - // We have already tested the functionality up to here. - reset(paymentSessionListener) - - val result = PaymentMethodsActivityStarter.Result( - useGooglePay = true - ) - val handled = paymentSession.handlePaymentData( - PaymentMethodsActivityStarter.REQUEST_CODE, - RESULT_OK, - Intent().putExtras(result.toBundle()) - ) - assertThat(handled).isTrue() - - verify(paymentSessionListener) - .onPaymentSessionDataChanged(paymentSessionDataArgumentCaptor.capture()) - val data = paymentSessionDataArgumentCaptor.firstValue - assertThat(data.paymentMethod).isNull() - assertThat(data.useGooglePay).isTrue() - } - } - - @Test - fun selectPaymentMethod_launchesPaymentMethodsActivityWithLog() { - createActivity { activity -> - val paymentSession = PaymentSession(activity, DEFAULT_CONFIG) - paymentSession.init(paymentSessionListener) - paymentSession.presentPaymentMethodSelection() - - val nextStartedActivityForResult = - Shadows.shadowOf(activity).nextStartedActivityForResult - val intent = nextStartedActivityForResult.intent - - assertThat(nextStartedActivityForResult.requestCode) - .isEqualTo(PaymentMethodsActivityStarter.REQUEST_CODE) - assertThat(intent.component?.className) - .isEqualTo(PaymentMethodsActivity::class.java.name) - - val args = - PaymentMethodsActivityStarter.Args.create(intent) - assertThat(args.billingAddressFields).isEqualTo(BillingAddressFields.Full) - } - } - - @Test - fun presentPaymentMethodSelection_withShouldRequirePostalCode_shouldPassInIntent() { - createActivity { activity -> - val paymentSession = PaymentSession( - activity, - PaymentSessionConfig.Builder() - .setShippingMethodsRequired(false) - .setBillingAddressFields(BillingAddressFields.PostalCode) - .build() - ) - paymentSession.init(paymentSessionListener) - paymentSession.presentPaymentMethodSelection() - - val nextStartedActivityForResult = - Shadows.shadowOf(activity).nextStartedActivityForResult - val intent = nextStartedActivityForResult.intent - - assertThat(nextStartedActivityForResult.requestCode) - .isEqualTo(PaymentMethodsActivityStarter.REQUEST_CODE) - assertThat(intent.component?.className) - .isEqualTo(PaymentMethodsActivity::class.java.name) - - val args = - PaymentMethodsActivityStarter.Args.create(nextStartedActivityForResult.intent) - assertThat(args.billingAddressFields) - .isEqualTo(BillingAddressFields.PostalCode) - } - } - - @Test - fun getSelectedPaymentMethodId_whenHasPaymentSessionData_returnsExpectedId() { - createActivity { - val paymentSession = createPaymentSession( - it, - DEFAULT_CONFIG, - PaymentSessionFixtures.PAYMENT_SESSION_DATA.copy( - paymentMethod = PaymentMethodFixtures.CARD_PAYMENT_METHOD - ) - ) - paymentSession.presentPaymentMethodSelection() - verify(paymentMethodsActivityStarter).startForResult( - paymentMethodsActivityStarterArgsCaptor.capture() - ) - assertThat(paymentMethodsActivityStarterArgsCaptor.firstValue.initialPaymentMethodId) - .isEqualTo("pm_123456789") - } - } - - @Test - fun getSelectedPaymentMethodId_whenHasUserSpecifiedPaymentMethod_returnsExpectedId() { - createActivity { - val paymentSession = createPaymentSession( - it, - DEFAULT_CONFIG, - PaymentSessionFixtures.PAYMENT_SESSION_DATA.copy( - paymentMethod = PaymentMethodFixtures.CARD_PAYMENT_METHOD - ) - ) - paymentSession.presentPaymentMethodSelection("pm_987") - verify(paymentMethodsActivityStarter).startForResult( - paymentMethodsActivityStarterArgsCaptor.capture() - ) - assertThat(paymentMethodsActivityStarterArgsCaptor.firstValue.initialPaymentMethodId) - .isEqualTo("pm_987") - } - } - - @Test - fun init_withSavedState_setsPaymentSessionData() { - ephemeralKeyProvider.setNextRawEphemeralKey(EphemeralKeyFixtures.FIRST_JSON) - - createActivity { activity -> - val paymentSession = PaymentSession(activity, DEFAULT_CONFIG).also { - it.setCartTotal(300L) - it.init(paymentSessionListener) - } - - paymentSession.init(paymentSessionListener) - - verify(paymentSessionListener, times(2)) - .onPaymentSessionDataChanged(paymentSessionDataArgumentCaptor.capture()) - - assertThat(paymentSessionDataArgumentCaptor.allValues[1]) - .isEqualTo(paymentSessionDataArgumentCaptor.allValues[0]) - } - } - - @Test - fun handlePaymentData_withInvalidRequestCode_aborts() { - createActivity { - val paymentSession = createPaymentSession(it) - assertThat(paymentSession.handlePaymentData(-1, RESULT_CANCELED, Intent())).isFalse() - verify(customerSession, never()).retrieveCurrentCustomer(any()) - } - } - - @Test - fun handlePaymentData_withPaymentMethodsActivityRequestCodeAndCanceledResult_doesNotRetrieveCustomer() { - createActivity { - val paymentSession = createPaymentSession(it) - assertThat( - paymentSession.handlePaymentData( - PaymentMethodsActivityStarter.REQUEST_CODE, - RESULT_CANCELED, - Intent() - ) - ).isFalse() - verify(customerSession, never()).retrieveCurrentCustomer(any()) - } - } - - @Test - fun handlePaymentData_withPaymentFlowActivityRequestCodeAndCanceledResult_retrievesCustomer() { - createActivity { - val paymentSession = createPaymentSession(it) - assertThat( - paymentSession.handlePaymentData( - PaymentFlowActivityStarter.REQUEST_CODE, - RESULT_CANCELED, - Intent() - ) - ).isFalse() - verify(customerSession).retrieveCurrentCustomer( - eq(setOf(PaymentSession.PRODUCT_TOKEN)), - any() - ) - } - } - - private fun createPaymentSession( - activity: ComponentActivity, - config: PaymentSessionConfig = DEFAULT_CONFIG, - paymentSessionData: PaymentSessionData = PaymentSessionData(config) - ): PaymentSession { - return PaymentSession( - activity, - activity, - activity, - config, - customerSession, - paymentMethodsActivityStarter, - paymentFlowActivityStarter, - paymentSessionData - ) - } - - private fun createCustomerSession( - stripeRepository: StripeRepository = FakeStripeRepository() - ): CustomerSession { - return CustomerSession( - stripeRepository, - ApiKeyFixtures.FAKE_PUBLISHABLE_KEY, - "acct_abc123", - workContext = testDispatcher, - ephemeralKeyManagerFactory = EphemeralKeyManager.Factory.Default( - keyProvider = ephemeralKeyProvider, - shouldPrefetchEphemeralKey = true - ) - ) - } - - private fun createActivity(callback: (ComponentActivity) -> Unit) { - // start an arbitrary ComponentActivity - createActivityScenario { it.onActivity(callback) } - } - - private fun createActivityScenario( - callback: (ActivityScenario) -> Unit - ) { - activityScenarioFactory.create( - PaymentMethodsActivityStarter.Args.Builder().build() - ).use(callback) - } - - private class FakeStripeRepository : AbsFakeStripeRepository() { - - override suspend fun getPaymentMethods( - listPaymentMethodsParams: ListPaymentMethodsParams, - productUsageTokens: Set, - requestOptions: ApiRequest.Options - ): Result> { - return Result.success(emptyList()) - } - - override suspend fun createPaymentMethod( - paymentMethodCreateParams: PaymentMethodCreateParams, - options: ApiRequest.Options - ): Result { - return Result.success(PaymentMethodFixtures.CARD_PAYMENT_METHOD) - } - - override suspend fun setDefaultCustomerSource( - customerId: String, - publishableKey: String, - productUsageTokens: Set, - sourceId: String, - sourceType: String, - requestOptions: ApiRequest.Options - ): Result { - return Result.success(SECOND_CUSTOMER) - } - - override suspend fun retrieveCustomer( - customerId: String, - productUsageTokens: Set, - requestOptions: ApiRequest.Options - ): Result { - return Result.success(FIRST_CUSTOMER) - } - } - - private companion object { - private val FIRST_CUSTOMER = CustomerFixtures.CUSTOMER - private val SECOND_CUSTOMER = CustomerFixtures.OTHER_CUSTOMER - - private val DEFAULT_CONFIG = PaymentSessionFixtures.CONFIG - } -} diff --git a/payments-core/src/test/java/com/stripe/android/PaymentSessionViewModelTest.kt b/payments-core/src/test/java/com/stripe/android/PaymentSessionViewModelTest.kt deleted file mode 100644 index d51e7ea8990..00000000000 --- a/payments-core/src/test/java/com/stripe/android/PaymentSessionViewModelTest.kt +++ /dev/null @@ -1,328 +0,0 @@ -package com.stripe.android - -import androidx.lifecycle.SavedStateHandle -import androidx.test.core.app.ApplicationProvider -import app.cash.turbine.test -import com.google.common.truth.Truth.assertThat -import com.stripe.android.model.CustomerFixtures -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.PaymentMethodFixtures -import com.stripe.android.view.PaymentMethodsActivityStarter -import kotlinx.coroutines.test.runTest -import org.junit.runner.RunWith -import org.mockito.kotlin.any -import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.doNothing -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.never -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import org.robolectric.RobolectricTestRunner -import kotlin.test.Test - -@RunWith(RobolectricTestRunner::class) -class PaymentSessionViewModelTest { - private val customerSession: CustomerSession = mock() - private val paymentSessionPrefs: PaymentSessionPrefs = mock() - private val savedStateHandle: SavedStateHandle = mock() - - private val paymentMethodsListenerCaptor = - argumentCaptor() - - private val viewModel: PaymentSessionViewModel = createViewModel() - - @Test - fun init_shouldGetButNotSetPaymentSessionDataFromSavedStateHandle() { - viewModel.paymentSessionData - - verify(savedStateHandle).get( - PaymentSessionViewModel.KEY_PAYMENT_SESSION_DATA - ) - verify(savedStateHandle, never()).set( - eq(PaymentSessionViewModel.KEY_PAYMENT_SESSION_DATA), - any() - ) - } - - @Test - fun init_whenSavedStateHasData_shouldUpdatePaymentSessionData() = runTest { - whenever( - savedStateHandle.get( - PaymentSessionViewModel.KEY_PAYMENT_SESSION_DATA - ) - ).thenReturn(UPDATED_DATA) - - createViewModel().paymentSessionDataStateFlow.test { - assertThat(awaitItem()) - .isEqualTo(UPDATED_DATA) - } - } - - @Test - fun updateCartTotal_shouldUpdatePaymentSessionData() { - viewModel.updateCartTotal(5000) - assertThat(viewModel.paymentSessionData.cartTotal) - .isEqualTo(5000) - } - - @Test - fun getSelectedPaymentMethod_whenPrefsNotSet_returnsNull() { - whenever(customerSession.cachedCustomer) - .thenReturn(FIRST_CUSTOMER) - assertThat(viewModel.getSelectedPaymentMethod(null)) - .isNull() - } - - @Test - fun getSelectedPaymentMethod_whenHasPrefsSet_returnsExpectedId() { - val customerId = requireNotNull(FIRST_CUSTOMER.id) - whenever(paymentSessionPrefs.getPaymentMethod(customerId)) - .thenReturn(PaymentSessionPrefs.SelectedPaymentMethod.Saved("pm_12345")) - - whenever(customerSession.cachedCustomer).thenReturn(FIRST_CUSTOMER) - CustomerSession.instance = customerSession - - assertThat(viewModel.getSelectedPaymentMethod()?.stringValue) - .isEqualTo("pm_12345") - } - - @Test - fun getSelectedPaymentMethod_whenGooglePay_returnsExpectedValue() { - val customerId = requireNotNull(FIRST_CUSTOMER.id) - whenever(paymentSessionPrefs.getPaymentMethod(customerId)) - .thenReturn(PaymentSessionPrefs.SelectedPaymentMethod.GooglePay) - - whenever(customerSession.cachedCustomer).thenReturn(FIRST_CUSTOMER) - CustomerSession.instance = customerSession - - assertThat(viewModel.getSelectedPaymentMethod()) - .isEqualTo(PaymentSessionPrefs.SelectedPaymentMethod.GooglePay) - } - - @Test - fun settingPaymentSessionData_withSameValue_shouldUpdateSavedStateHandleOnce() { - repeat(3) { - viewModel.paymentSessionData = UPDATED_DATA - } - verify(savedStateHandle) - .set(PaymentSessionViewModel.KEY_PAYMENT_SESSION_DATA, UPDATED_DATA) - } - - @Test - fun settingPaymentSessionData_shouldUpdateStateFlow() = runTest { - viewModel.paymentSessionDataStateFlow.test { - viewModel.paymentSessionData = UPDATED_DATA - assertThat(awaitItem()) - .isEqualTo(UPDATED_DATA) - } - } - - @Test - fun onPaymentMethodResult_withGooglePay_shouldUpdateStateFlow() = runTest { - viewModel.paymentSessionDataStateFlow.test { - viewModel.onPaymentMethodResult( - PaymentMethodsActivityStarter.Result( - useGooglePay = true - ) - ) - assertThat(awaitItem().useGooglePay) - .isTrue() - } - } - - @Test - fun onCustomerRetrieved_whenIsInitialFetchAndPreviouslyUsedPaymentMethodExists_shouldUpdatePaymentSessionData() = runTest { - val customerPaymentMethods = PaymentMethodFixtures.createCards(20) - doNothing().whenever(customerSession).getPaymentMethods( - paymentMethodType = eq(PaymentMethod.Type.Card), - limit = eq(100), - endingBefore = anyOrNull(), - startingAfter = anyOrNull(), - listener = any() - ) - whenever(paymentSessionPrefs.getPaymentMethod("cus_123")) - .thenReturn(PaymentSessionPrefs.SelectedPaymentMethod.fromString(customerPaymentMethods.last().id)) - - var onCompleteCallbackCount = 0 - viewModel.onCustomerRetrieved( - customerId = "cus_123", - isInitialFetch = true - ) { - onCompleteCallbackCount++ - } - - verify(customerSession).getPaymentMethods( - paymentMethodType = eq(PaymentMethod.Type.Card), - limit = eq(100), - endingBefore = anyOrNull(), - startingAfter = anyOrNull(), - listener = paymentMethodsListenerCaptor.capture() - ) - paymentMethodsListenerCaptor.firstValue.onPaymentMethodsRetrieved( - customerPaymentMethods - ) - - viewModel.paymentSessionDataStateFlow.test { - assertThat(awaitItem()?.paymentMethod) - .isEqualTo(customerPaymentMethods.last()) - } - - assertThat(onCompleteCallbackCount) - .isEqualTo(1) - } - - @Test - fun onCustomerRetrieved_whenIsInitialFetchAndPreviouslyUsedPaymentMethodIsNotFoundInList_shouldNotUpdatePaymentSessionData() = runTest { - viewModel.paymentSessionDataStateFlow.test { - val customerPaymentMethods = PaymentMethodFixtures.createCards(20) - doNothing().whenever(customerSession).getPaymentMethods( - paymentMethodType = eq(PaymentMethod.Type.Card), - limit = eq(100), - endingBefore = anyOrNull(), - startingAfter = anyOrNull(), - listener = any() - ) - whenever(paymentSessionPrefs.getPaymentMethod("cus_123")) - .thenReturn(PaymentSessionPrefs.SelectedPaymentMethod.Saved("pm_not_in_list")) - - var onCompleteCallbackCount = 0 - viewModel.onCustomerRetrieved( - customerId = "cus_123", - isInitialFetch = true - ) { - onCompleteCallbackCount++ - } - - verify(customerSession).getPaymentMethods( - paymentMethodType = eq(PaymentMethod.Type.Card), - limit = eq(100), - endingBefore = anyOrNull(), - startingAfter = anyOrNull(), - listener = paymentMethodsListenerCaptor.capture() - ) - paymentMethodsListenerCaptor.firstValue.onPaymentMethodsRetrieved( - customerPaymentMethods - ) - - ensureAllEventsConsumed() - - assertThat(onCompleteCallbackCount) - .isEqualTo(1) - } - } - - @Test - fun onCustomerRetrieved_whenIsInitialFetchAndPreviouslyUsedPaymentMethodDoesNotExist_shouldNotFetchPaymentMethods() = runTest { - viewModel.paymentSessionDataStateFlow.test { - whenever(paymentSessionPrefs.getPaymentMethod("cus_123")) - .thenReturn(null) - - var onCompleteCallbackCount = 0 - viewModel.onCustomerRetrieved( - customerId = "cus_123", - isInitialFetch = true - ) { - onCompleteCallbackCount++ - } - - verify(customerSession, never()).getPaymentMethods( - paymentMethodType = eq(PaymentMethod.Type.Card), - limit = eq(100), - endingBefore = anyOrNull(), - startingAfter = anyOrNull(), - listener = any() - ) - - ensureAllEventsConsumed() - - assertThat(onCompleteCallbackCount) - .isEqualTo(1) - } - } - - @Test - fun fetchCustomer_onSuccess_returnsSuccessResult() = runTest { - viewModel.networkState.test { - assertThat(awaitItem()).isEqualTo(PaymentSessionViewModel.NetworkState.Inactive) // Initial Value - - whenever( - customerSession.retrieveCurrentCustomer( - productUsage = any(), - listener = any() - ) - ).thenAnswer { invocation -> - val listener = invocation.arguments[1] as CustomerSession.CustomerRetrievalListener - - listener.onCustomerRetrieved(CustomerFixtures.CUSTOMER) - } - - val result = viewModel.fetchCustomer() - - assertThat(awaitItem()).isEqualTo(PaymentSessionViewModel.NetworkState.Active) - - verify(customerSession).retrieveCurrentCustomer( - eq(setOf(PaymentSession.PRODUCT_TOKEN)), - any() - ) - - assertThat(result).isEqualTo(PaymentSessionViewModel.FetchCustomerResult.Success) - - assertThat(awaitItem()).isEqualTo(PaymentSessionViewModel.NetworkState.Inactive) - } - } - - @Test - fun fetchCustomer_onError_returnsErrorResult() = runTest { - viewModel.networkState.test { - assertThat(awaitItem()).isEqualTo(PaymentSessionViewModel.NetworkState.Inactive) // Initial Value - - whenever( - customerSession.retrieveCurrentCustomer( - productUsage = any(), - listener = any() - ) - ).thenAnswer { invocation -> - val listener = invocation.arguments[1] as CustomerSession.CustomerRetrievalListener - - listener.onError(500, "error", StripeErrorFixtures.INVALID_REQUEST_ERROR) - } - - val result = viewModel.fetchCustomer() - - assertThat(awaitItem()).isEqualTo(PaymentSessionViewModel.NetworkState.Active) - - verify(customerSession).retrieveCurrentCustomer( - eq(setOf(PaymentSession.PRODUCT_TOKEN)), - any() - ) - - assertThat(result) - .isEqualTo( - PaymentSessionViewModel.FetchCustomerResult.Error( - 500, - "error", - StripeErrorFixtures.INVALID_REQUEST_ERROR - ) - ) - - assertThat(awaitItem()).isEqualTo(PaymentSessionViewModel.NetworkState.Inactive) - } - } - - private fun createViewModel() = PaymentSessionViewModel( - ApplicationProvider.getApplicationContext(), - savedStateHandle, - PaymentSessionFixtures.PAYMENT_SESSION_DATA, - customerSession, - paymentSessionPrefs - ) - - private companion object { - private val FIRST_CUSTOMER = CustomerFixtures.CUSTOMER - - private val UPDATED_DATA = PaymentSessionFixtures.PAYMENT_SESSION_DATA - .copy(cartTotal = 999999) - } -} diff --git a/payments-core/src/test/java/com/stripe/android/analytics/DefaultPaymentSessionEventReporterTest.kt b/payments-core/src/test/java/com/stripe/android/analytics/DefaultPaymentSessionEventReporterTest.kt deleted file mode 100644 index c4d46a9761b..00000000000 --- a/payments-core/src/test/java/com/stripe/android/analytics/DefaultPaymentSessionEventReporterTest.kt +++ /dev/null @@ -1,170 +0,0 @@ -package com.stripe.android.analytics - -import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth.assertThat -import com.stripe.android.ApiKeyFixtures -import com.stripe.android.core.networking.AnalyticsRequestExecutor -import com.stripe.android.core.utils.DurationProvider -import com.stripe.android.networking.PaymentAnalyticsRequestFactory -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.kotlin.argWhere -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.robolectric.RobolectricTestRunner -import kotlin.time.Duration.Companion.seconds - -@RunWith(RobolectricTestRunner::class) -class DefaultPaymentSessionEventReporterTest { - private val testDispatcher = UnconfinedTestDispatcher() - private val durationProvider = FakeDurationProvider((2).seconds) - private val analyticsRequestExecutor = mock() - private val analyticsRequestFactory = PaymentAnalyticsRequestFactory( - ApplicationProvider.getApplicationContext(), - ApiKeyFixtures.DEFAULT_PUBLISHABLE_KEY - ) - - @Test - fun `onLoadStarted() should fire analytics request with expected event value`() { - val eventReporter = createEventReporter() - - eventReporter.onLoadStarted() - - verify(analyticsRequestExecutor).executeAsync( - argWhere { req -> - req.params["event"] == "bi_load_started" - } - ) - } - - @Test - fun `onLoadSucceeded() should fire analytics request with expected event value`() { - val eventReporter = createEventReporter() - - eventReporter.onLoadStarted() - eventReporter.onLoadSucceeded("card") - - verify(analyticsRequestExecutor).executeAsync( - argWhere { req -> - req.params["event"] == "bi_load_succeeded" && - req.params["selected_lpm"] == "card" && - req.params["duration"] == 2f - } - ) - } - - @Test - fun `onLoadFailed() should fire analytics request with expected event value`() { - val eventReporter = createEventReporter() - - eventReporter.onLoadStarted() - eventReporter.onLoadFailed(Exception()) - - verify(analyticsRequestExecutor).executeAsync( - argWhere { req -> - req.params["event"] == "bi_load_failed" && - req.params["error_message"] == "unknown" && - req.params["duration"] == 2f - } - ) - } - - @Test - fun `onOptionsShown() should fire analytics request with expected event value`() { - val eventReporter = createEventReporter() - - eventReporter.onOptionsShown() - - verify(analyticsRequestExecutor).executeAsync( - argWhere { req -> - req.params["event"] == "bi_options_shown" - } - ) - } - - @Test - fun `onFormShown() should fire analytics request with expected event value`() { - val eventReporter = createEventReporter() - - eventReporter.onFormShown("card") - - verify(analyticsRequestExecutor).executeAsync( - argWhere { req -> - req.params["event"] == "bi_form_shown" && - req.params["selected_lpm"] == "card" - } - ) - } - - @Test - fun `onFormShown() should restart duration on call`() { - val durationProvider = FakeDurationProvider() - - val eventReporter = createEventReporter(durationProvider) - - eventReporter.onFormShown("card") - - assertThat( - durationProvider.has( - FakeDurationProvider.Call.Start( - key = DurationProvider.Key.ConfirmButtonClicked, - reset = true - ) - ) - ).isTrue() - } - - @Test - fun `onFormInteracted() should fire analytics request with expected event value`() { - val eventReporter = createEventReporter() - - eventReporter.onFormInteracted("card") - - verify(analyticsRequestExecutor).executeAsync( - argWhere { req -> - req.params["event"] == "bi_form_interacted" && - req.params["selected_lpm"] == "card" - } - ) - } - - @Test - fun `onCardNumberCompleted() should fire analytics request with expected event value`() { - val eventReporter = createEventReporter() - - eventReporter.onCardNumberCompleted() - - verify(analyticsRequestExecutor).executeAsync( - argWhere { req -> - req.params["event"] == "bi_card_number_completed" - } - ) - } - - @Test - fun `onDoneButtonTapped() should fire analytics request with expected event value`() { - val eventReporter = createEventReporter() - - eventReporter.onDoneButtonTapped("card") - - verify(analyticsRequestExecutor).executeAsync( - argWhere { req -> - req.params["event"] == "bi_done_button_tapped" && - req.params["selected_lpm"] == "card" && - req.params["duration"] == 2f - } - ) - } - - private fun createEventReporter( - durationProvider: DurationProvider = this.durationProvider, - ): PaymentSessionEventReporter { - return DefaultPaymentSessionEventReporter( - analyticsRequestExecutor = analyticsRequestExecutor, - paymentAnalyticsRequestFactory = analyticsRequestFactory, - durationProvider = durationProvider, - workContext = testDispatcher - ) - } -} diff --git a/payments-core/src/test/java/com/stripe/android/view/AddPaymentMethodActivityStarterTest.kt b/payments-core/src/test/java/com/stripe/android/view/AddPaymentMethodActivityStarterTest.kt deleted file mode 100644 index 182a43f2115..00000000000 --- a/payments-core/src/test/java/com/stripe/android/view/AddPaymentMethodActivityStarterTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.stripe.android.view - -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.PaymentMethodFixtures -import com.stripe.android.utils.ParcelUtils -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import kotlin.test.Test -import kotlin.test.assertEquals - -@RunWith(RobolectricTestRunner::class) -class AddPaymentMethodActivityStarterTest { - - @Test - fun testArgsParceling() { - val args = AddPaymentMethodActivityStarter.Args.Builder() - .setPaymentMethodType(PaymentMethod.Type.Fpx) - .setIsPaymentSessionActive(true) - .setBillingAddressFields(BillingAddressFields.PostalCode) - .build() - - assertEquals(args, ParcelUtils.create(args)) - } - - @Test - fun testResultParceling() { - val result = AddPaymentMethodActivityStarter.Result.Success( - PaymentMethodFixtures.CARD_PAYMENT_METHOD - ) - assertEquals(result, ParcelUtils.create(result)) - } -} diff --git a/payments-core/src/test/java/com/stripe/android/view/AddPaymentMethodActivityTest.kt b/payments-core/src/test/java/com/stripe/android/view/AddPaymentMethodActivityTest.kt deleted file mode 100644 index 9345d48af05..00000000000 --- a/payments-core/src/test/java/com/stripe/android/view/AddPaymentMethodActivityTest.kt +++ /dev/null @@ -1,540 +0,0 @@ -package com.stripe.android.view - -import android.app.Activity.RESULT_OK -import android.app.Instrumentation -import android.content.Context -import android.content.Intent -import android.view.View -import android.widget.ProgressBar -import android.widget.TextView -import androidx.core.view.isGone -import androidx.core.view.isVisible -import androidx.lifecycle.Lifecycle -import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth.assertThat -import com.stripe.android.ApiKeyFixtures -import com.stripe.android.CustomerSession -import com.stripe.android.PaymentConfiguration -import com.stripe.android.PaymentSession -import com.stripe.android.R -import com.stripe.android.core.exception.StripeException -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.PaymentMethodCreateParams -import com.stripe.android.model.PaymentMethodCreateParamsFixtures -import com.stripe.android.model.PaymentMethodFixtures -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.StandardTestDispatcher -import org.junit.runner.RunWith -import org.mockito.Mockito.never -import org.mockito.kotlin.any -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.stub -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import org.robolectric.RobolectricTestRunner -import org.robolectric.fakes.RoboMenuItem -import org.robolectric.shadows.ShadowAlertDialog -import java.util.Calendar -import kotlin.test.BeforeTest -import kotlin.test.Test - -/** - * Test class for [AddPaymentMethodActivity]. - */ -@RunWith(RobolectricTestRunner::class) -class AddPaymentMethodActivityTest { - private val testDispatcher = StandardTestDispatcher() - - private val customerSession = mock() - private val viewModel = mock() - - private val paymentMethodIdCaptor = argumentCaptor() - private val listenerArgumentCaptor = argumentCaptor() - private val productUsageArgumentCaptor = argumentCaptor>() - - private val contract = AddPaymentMethodContract() - private val context: Context = ApplicationProvider.getApplicationContext() - private val activityScenarioFactory = ActivityScenarioFactory(context) - - @BeforeTest - fun setup() { - // The input in this test class will be invalid after 2050. Please update the test. - assertThat(Calendar.getInstance().get(Calendar.YEAR) < 2050) - .isTrue() - PaymentConfiguration.init( - context, - ApiKeyFixtures.DEFAULT_PUBLISHABLE_KEY, - "acct_12345" - ) - CustomerSession.instance = customerSession - } - - @Test - fun testActivityCallsInitOnCreate() { - activityScenarioFactory.create( - BASE_CARD_ARGS - ).use { activityScenario -> - activityScenario.onActivity { - runBlocking { - verify(viewModel, never()).onFormShown() - } - } - } - } - - @Test - fun testActivityIsFinishedWhenNoArgsPassed() { - activityScenarioFactory.create().use { activityScenario -> - assertThat(activityScenario.state).isEqualTo(Lifecycle.State.DESTROYED) - } - } - - @Test - fun testConstructionForLocal() { - activityScenarioFactory.create( - BASE_CARD_ARGS - ).use { activityScenario -> - activityScenario.onActivity { - val cardMultilineWidget: CardMultilineWidget = - it.findViewById(R.id.card_multiline_widget) - val widgetControlGroup = - CardMultilineWidgetTest.WidgetControlGroup(cardMultilineWidget, testDispatcher) - assertThat(widgetControlGroup.postalCodeInputLayout.isVisible) - .isTrue() - } - } - } - - @Test - fun testConstructionForCustomerSession() { - activityScenarioFactory.create( - createArgs(PaymentMethod.Type.Card) - ).use { activityScenario -> - activityScenario.onActivity { activity -> - val cardMultilineWidget: CardMultilineWidget = - activity.findViewById(R.id.card_multiline_widget) - val widgetControlGroup = - CardMultilineWidgetTest.WidgetControlGroup(cardMultilineWidget, testDispatcher) - assertThat(widgetControlGroup.postalCodeInputLayout.isVisible) - .isTrue() - } - } - } - - @Test - fun softEnterKey_whenDataIsNotValid_doesNotHideKeyboardAndDoesNotFinish() { - activityScenarioFactory.create( - BASE_CARD_ARGS - ).use { activityScenario -> - activityScenario.onActivity { activity -> - val progressBar: ProgressBar = activity.findViewById(R.id.progress_bar) - assertThat(progressBar.isGone) - .isTrue() - activity.createPaymentMethod(viewModel, null) - - runBlocking { - verify(viewModel, never()).createPaymentMethod(any()) - } - } - } - } - - @Test - fun addCardData_whenDataIsValidAndServerReturnsSuccess_finishesWithIntent() { - activityScenarioFactory.createForResult( - BASE_CARD_ARGS - ).use { activityScenario -> - activityScenario.onActivity { activity -> - val progressBar: ProgressBar = activity.findViewById(R.id.progress_bar) - assertThat(progressBar.isGone) - .isTrue() - - stubCreatePaymentMethod( - PaymentMethodCreateParamsFixtures.DEFAULT_CARD, - Result.success(PaymentMethodFixtures.CARD_PAYMENT_METHOD) - ) - activity.createPaymentMethod( - viewModel, - PaymentMethodCreateParamsFixtures.DEFAULT_CARD - ) - verifyFinishesWithResult(activityScenario.result) - } - } - } - - @Test - fun addFpx_whenServerReturnsSuccessAndUpdatesCustomer_finishesWithIntent() { - activityScenarioFactory.createForResult( - createArgs(PaymentMethod.Type.Fpx) - ).use { activityScenario -> - activityScenario.onActivity { activity -> - val progressBar: ProgressBar = activity.findViewById(R.id.progress_bar) - assertThat(progressBar.isGone) - .isTrue() - - stubCreatePaymentMethod( - PaymentMethodCreateParamsFixtures.DEFAULT_FPX, - Result.success(PaymentMethodFixtures.FPX_PAYMENT_METHOD) - ) - activity.createPaymentMethod(viewModel, PaymentMethodCreateParamsFixtures.DEFAULT_FPX) - - val expectedPaymentMethod = PaymentMethodFixtures.FPX_PAYMENT_METHOD - - verify(customerSession, never()).attachPaymentMethod( - any(), - any(), - any() - ) - - assertThat(activityScenario.result.resultCode) - .isEqualTo(RESULT_OK) - assertThat(getPaymentMethodFromIntent(activityScenario.result.resultData)) - .isEqualTo(expectedPaymentMethod) - } - } - } - - @Test - fun addCardData_withFullBillingFieldsRequirement_shouldShowBillingAddressWidget() { - activityScenarioFactory.create( - createArgs( - PaymentMethod.Type.Card, - billingAddressFields = BillingAddressFields.Full - ) - ).use { activityScenario -> - activityScenario.onActivity { activity -> - assertThat(activity.findViewById(R.id.billing_address_widget).isVisible) - .isTrue() - - val cardMultilineWidget: CardMultilineWidget = - activity.findViewById(R.id.card_multiline_widget) - assertThat(cardMultilineWidget.findViewById(R.id.tl_postal_code).isGone) - .isTrue() - } - } - } - - @Test - fun addCardData_withPostalCodeBillingFieldsRequirement_shouldHideBillingAddressWidget() { - activityScenarioFactory.create( - createArgs( - PaymentMethod.Type.Card, - billingAddressFields = BillingAddressFields.PostalCode - ) - ).use { activityScenario -> - activityScenario.onActivity { activity -> - assertThat(activity.findViewById(R.id.billing_address_widget).isGone) - .isTrue() - - val cardMultilineWidget: CardMultilineWidget = - activity.findViewById(R.id.card_multiline_widget) - assertThat(cardMultilineWidget.findViewById(R.id.tl_postal_code).isVisible) - .isTrue() - } - } - } - - @Test - fun addCardData_withNoBillingFieldsRequirement_shouldHideBillingAddressWidgetAndPostalCode() { - activityScenarioFactory.create( - createArgs( - PaymentMethod.Type.Card, - billingAddressFields = BillingAddressFields.None - ) - ).use { activityScenario -> - activityScenario.onActivity { activity -> - assertThat(activity.findViewById(R.id.billing_address_widget).isGone) - .isTrue() - - val cardMultilineWidget: CardMultilineWidget = - activity.findViewById(R.id.card_multiline_widget) - assertThat(cardMultilineWidget.findViewById(R.id.tl_postal_code).isGone) - .isTrue() - } - } - } - - @Test - fun addCardData_whenServerReturnsSuccessAndUpdatesCustomer_finishesWithIntent() { - activityScenarioFactory.createForResult( - createArgs(PaymentMethod.Type.Card) - ).use { activityScenario -> - activityScenario.onActivity { activity -> - val progressBar: ProgressBar = activity.findViewById(R.id.progress_bar) - val cardMultilineWidget: CardMultilineWidget = activity.findViewById(R.id.card_multiline_widget) - - assertThat(progressBar.isGone) - .isTrue() - assertThat(cardMultilineWidget.isEnabled) - .isTrue() - - stubCreatePaymentMethod( - PaymentMethodCreateParamsFixtures.DEFAULT_CARD, - Result.success(PaymentMethodFixtures.CARD_PAYMENT_METHOD) - ) - activity.createPaymentMethod(viewModel, PaymentMethodCreateParamsFixtures.DEFAULT_CARD) - assertThat(progressBar.isVisible) - .isTrue() - assertThat(cardMultilineWidget.isEnabled) - .isFalse() - - verify(customerSession).attachPaymentMethod( - paymentMethodIdCaptor.capture(), - productUsageArgumentCaptor.capture(), - listenerArgumentCaptor.capture() - ) - - assertThat( - productUsageArgumentCaptor.firstValue - ).containsExactly( - AddPaymentMethodActivity.PRODUCT_TOKEN, - PaymentSession.PRODUCT_TOKEN - ) - - assertThat(paymentMethodIdCaptor.firstValue) - .isEqualTo(EXPECTED_PAYMENT_METHOD.id) - listenerArgumentCaptor.firstValue.onPaymentMethodRetrieved(EXPECTED_PAYMENT_METHOD) - - verifyFinishesWithResult(activityScenario.result) - } - } - } - - @Test - fun addCardData_whenDataIsValidButServerReturnsError_doesNotFinish() { - activityScenarioFactory.createForResult( - BASE_CARD_ARGS - ).use { activityScenario -> - activityScenario.onActivity { activity -> - val progressBar: ProgressBar = activity.findViewById(R.id.progress_bar) - assertThat(progressBar.isGone) - .isTrue() - - stubCreatePaymentMethod( - PaymentMethodCreateParamsFixtures.DEFAULT_CARD, - Result.failure(RuntimeException(ERROR_MESSAGE)) - ) - activity.createPaymentMethod( - viewModel, - PaymentMethodCreateParamsFixtures.DEFAULT_CARD - ) - - assertThat(activity.isFinishing) - .isFalse() - assertThat(progressBar.isGone) - .isTrue() - - verifyDialogWithMessage() - - activity.finish() - } - - assertThat( - contract.parseResult( - activityScenario.result.resultCode, - activityScenario.result.resultData - ) - ).isEqualTo(AddPaymentMethodActivityStarter.Result.Canceled) - } - } - - @Test - fun addCardData_whenPaymentMethodCreateWorksButAddToCustomerFails_showErrorNotFinish() { - activityScenarioFactory.createForResult( - createArgs(PaymentMethod.Type.Card) - ).use { activityScenario -> - activityScenario.onActivity { activity -> - val progressBar: ProgressBar = activity.findViewById(R.id.progress_bar) - val cardMultilineWidget: CardMultilineWidget = activity.findViewById(R.id.card_multiline_widget) - - assertThat(progressBar.isGone) - .isTrue() - assertThat(cardMultilineWidget.isEnabled) - .isTrue() - - stubCreatePaymentMethod( - PaymentMethodCreateParamsFixtures.DEFAULT_CARD, - Result.success(PaymentMethodFixtures.CARD_PAYMENT_METHOD) - ) - activity.createPaymentMethod(viewModel, PaymentMethodCreateParamsFixtures.DEFAULT_CARD) - - assertThat(progressBar.isVisible) - .isTrue() - assertThat(cardMultilineWidget.isEnabled) - .isFalse() - - verify(customerSession).attachPaymentMethod( - paymentMethodIdCaptor.capture(), - productUsageArgumentCaptor.capture(), - listenerArgumentCaptor.capture() - ) - - assertThat( - productUsageArgumentCaptor.firstValue - ).containsExactly( - AddPaymentMethodActivity.PRODUCT_TOKEN, - PaymentSession.PRODUCT_TOKEN - ) - - assertThat(paymentMethodIdCaptor.firstValue) - .isEqualTo(EXPECTED_PAYMENT_METHOD.id) - - val error: StripeException = mock() - whenever(error.localizedMessage).thenReturn(ERROR_MESSAGE) - listenerArgumentCaptor.firstValue.onError(400, ERROR_MESSAGE, null) - - assertThat(activity.isFinishing) - .isFalse() - assertThat(progressBar.isGone) - .isTrue() - - verifyDialogWithMessage() - - activity.finish() - } - - val result = contract.parseResult(0, activityScenario.result.resultData) - assertThat(result) - .isEqualTo(AddPaymentMethodActivityStarter.Result.Canceled) - } - } - - @Test - fun `createPaymentMethod when CustomerSession is null and should attach should finish Activity`() { - CustomerSession.instance = null - - stubCreatePaymentMethod( - PaymentMethodCreateParamsFixtures.DEFAULT_CARD, - Result.success(PaymentMethodFixtures.CARD_PAYMENT_METHOD) - ) - - activityScenarioFactory.createForResult( - createArgs(PaymentMethod.Type.Card) - ).use { activityScenario -> - activityScenario.onActivity { activity -> - activity.createPaymentMethod( - viewModel, - PaymentMethodCreateParamsFixtures.DEFAULT_CARD - ) - - val result = contract.parseResult( - activityScenario.result.resultCode, - activityScenario.result.resultData - ) - as? AddPaymentMethodActivityStarter.Result.Failure - assertThat(result?.exception?.message) - .isEqualTo("Attempted to get instance of CustomerSession without initialization.") - - assertThat(activity.isFinishing) - .isTrue() - } - } - } - - @Test - fun `when user clicks on save in action menu, should call onSaveClicked() in view model`() { - activityScenarioFactory.create( - BASE_CARD_ARGS - ).use { activityScenario -> - activityScenario.onActivity { activity -> - activity.onOptionsItemSelected(RoboMenuItem(R.id.action_save)) - - runBlocking { - verify(viewModel, never()).onSaveClicked() - } - } - } - } - - @Test - fun `on user interaction with form, calls onFormInteracted() in view model`() { - activityScenarioFactory.create( - BASE_CARD_ARGS - ).use { activityScenario -> - activityScenario.onActivity { activity -> - val view: CardMultilineWidget = activity.findViewById(R.id.card_multiline_widget) - - view.setCardNumber("4242") - - runBlocking { - verify(viewModel, never()).onFormInteracted() - } - } - } - } - - @Test - fun `when card number is completely input, should call onCardNumberCompleted() in view model`() { - activityScenarioFactory.create( - BASE_CARD_ARGS - ).use { activityScenario -> - activityScenario.onActivity { activity -> - val view: CardMultilineWidget = activity.findViewById(R.id.card_multiline_widget) - - view.setCardNumber("4242424242424242") - - runBlocking { - verify(viewModel, never()).onCardNumberCompleted() - } - } - } - } - - private fun verifyDialogWithMessage() { - val dialog = ShadowAlertDialog.getShownDialogs().first() - val actualMessage = dialog.findViewById(android.R.id.message).text - assertThat(actualMessage) - .isEqualTo(ERROR_MESSAGE) - } - - private fun verifyFinishesWithResult(activityResult: Instrumentation.ActivityResult) { - assertThat(activityResult.resultCode) - .isEqualTo(RESULT_OK) - assertThat(getPaymentMethodFromIntent(activityResult.resultData)) - .isEqualTo(EXPECTED_PAYMENT_METHOD) - } - - private fun getPaymentMethodFromIntent( - intent: Intent - ) = when (val result = contract.parseResult(0, intent)) { - is AddPaymentMethodActivityStarter.Result.Success -> result.paymentMethod - else -> null - } - - private fun createArgs( - paymentMethodType: PaymentMethod.Type, - billingAddressFields: BillingAddressFields = BillingAddressFields.PostalCode - ): AddPaymentMethodActivityStarter.Args { - return AddPaymentMethodActivityStarter.Args.Builder() - .setShouldAttachToCustomer(true) - .setIsPaymentSessionActive(true) - .setPaymentMethodType(paymentMethodType) - .setPaymentConfiguration(PaymentConfiguration.getInstance(context)) - .setBillingAddressFields(billingAddressFields) - .build() - } - - private fun stubCreatePaymentMethod( - params: PaymentMethodCreateParams, - result: Result - ) { - viewModel.stub { - onBlocking { - createPaymentMethod(params) - }.doReturn(result) - } - } - - private companion object { - private const val ERROR_MESSAGE = "Oh no! An Error!" - - private val BASE_CARD_ARGS = - AddPaymentMethodActivityStarter.Args.Builder() - .setPaymentMethodType(PaymentMethod.Type.Card) - .build() - - private val EXPECTED_PAYMENT_METHOD = PaymentMethodFixtures.CARD_PAYMENT_METHOD - } -} diff --git a/payments-core/src/test/java/com/stripe/android/view/AddPaymentMethodCardViewTest.kt b/payments-core/src/test/java/com/stripe/android/view/AddPaymentMethodCardViewTest.kt deleted file mode 100644 index c5c251cdd51..00000000000 --- a/payments-core/src/test/java/com/stripe/android/view/AddPaymentMethodCardViewTest.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.stripe.android.view - -import android.view.inputmethod.EditorInfo -import com.stripe.android.model.PaymentMethodCreateParams -import com.stripe.android.model.PaymentMethodCreateParamsFixtures -import org.junit.runner.RunWith -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` -import org.mockito.kotlin.mock -import org.robolectric.RobolectricTestRunner -import kotlin.test.Test - -@RunWith(RobolectricTestRunner::class) -class AddPaymentMethodCardViewTest { - - private val activity: AddPaymentMethodActivity = mock() - private val addPaymentMethodCardView: AddPaymentMethodCardView = mock() - private val keyboardController: KeyboardController = mock() - - @Test - fun softEnterKey_whenDataIsValid_hidesKeyboardAndAttemptsToSave() { - `when`(addPaymentMethodCardView.createParams) - .thenReturn(PaymentMethodCreateParamsFixtures.DEFAULT_CARD) - - AddPaymentMethodCardView.OnEditorActionListenerImpl( - activity, - addPaymentMethodCardView, - keyboardController - ) - .onEditorAction(null, EditorInfo.IME_ACTION_DONE, null) - - verify(keyboardController).hide() - verify(activity).onActionSave() - } -} diff --git a/payments-core/src/test/java/com/stripe/android/view/AddPaymentMethodViewModelTest.kt b/payments-core/src/test/java/com/stripe/android/view/AddPaymentMethodViewModelTest.kt deleted file mode 100644 index 3f16b9d24c5..00000000000 --- a/payments-core/src/test/java/com/stripe/android/view/AddPaymentMethodViewModelTest.kt +++ /dev/null @@ -1,228 +0,0 @@ -package com.stripe.android.view - -import android.content.Context -import androidx.lifecycle.SavedStateHandle -import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth.assertThat -import com.stripe.android.ApiKeyFixtures -import com.stripe.android.CustomerSession -import com.stripe.android.Stripe -import com.stripe.android.analytics.PaymentSessionEventReporter -import com.stripe.android.core.StripeError -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.PaymentMethodCreateParams -import com.stripe.android.model.PaymentMethodCreateParamsFixtures -import com.stripe.android.model.PaymentMethodFixtures -import com.stripe.android.view.i18n.ErrorMessageTranslator -import com.stripe.android.view.i18n.TranslatorManager -import kotlinx.coroutines.test.runTest -import org.junit.runner.RunWith -import org.mockito.kotlin.any -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import org.robolectric.RobolectricTestRunner -import kotlin.test.Test - -@RunWith(RobolectricTestRunner::class) -class AddPaymentMethodViewModelTest { - private val context = ApplicationProvider.getApplicationContext() - private val stripe = Stripe(context, ApiKeyFixtures.FAKE_PUBLISHABLE_KEY) - - private val customerSession: CustomerSession = mock() - - @Test - fun `on form shown, should fire onFormShown event`() { - val eventReporter: PaymentSessionEventReporter = mock() - - val viewModel = createViewModel( - eventReporter = eventReporter, - args = AddPaymentMethodActivityStarter.Args.Builder() - .setPaymentMethodType(PaymentMethod.Type.Fpx) - .build() - ) - - viewModel.onFormShown() - - verify(eventReporter).onFormShown("fpx") - } - - @Test - fun `on form interacted, should fire onFormInteracted event`() { - val eventReporter: PaymentSessionEventReporter = mock() - - val viewModel = createViewModel( - eventReporter = eventReporter, - args = AddPaymentMethodActivityStarter.Args.Builder() - .setPaymentMethodType(PaymentMethod.Type.Fpx) - .build() - ) - - viewModel.onFormInteracted() - - verify(eventReporter).onFormInteracted("fpx") - } - - @Test - fun `on card number completed, should fire onCardNumberCompleted event`() { - val eventReporter: PaymentSessionEventReporter = mock() - - val viewModel = createViewModel( - eventReporter = eventReporter, - args = AddPaymentMethodActivityStarter.Args.Builder() - .setPaymentMethodType(PaymentMethod.Type.Fpx) - .build() - ) - - viewModel.onCardNumberCompleted() - - verify(eventReporter).onCardNumberCompleted() - } - - @Test - fun `on save, should fire onDoneButtonTapped event`() { - val eventReporter: PaymentSessionEventReporter = mock() - - val viewModel = createViewModel( - eventReporter = eventReporter, - args = AddPaymentMethodActivityStarter.Args.Builder() - .setPaymentMethodType(PaymentMethod.Type.Fpx) - .build() - ) - - viewModel.onSaveClicked() - - verify(eventReporter).onDoneButtonTapped("fpx") - } - - @Test - fun `updatedPaymentMethodCreateParams should include expected attribution`() { - val params = PaymentMethodCreateParams.create( - PaymentMethodCreateParamsFixtures.CARD.copy( - attribution = setOf("CardMultilineWidget") - ) - ) - assertThat( - createViewModel().updatedPaymentMethodCreateParams(params).attribution - ).containsExactly( - "CardMultilineWidget", - AddPaymentMethodActivity.PRODUCT_TOKEN - ) - } - - @Test - fun attachPaymentMethod_whenError_returnsError() = runTest { - whenever( - customerSession.attachPaymentMethod( - paymentMethodId = any(), - productUsage = any(), - listener = any() - ) - ).thenAnswer { invocation -> - val listener = invocation.arguments[2] as CustomerSession.PaymentMethodRetrievalListener - listener.onError( - 402, - ERROR_MESSAGE, - StripeError( - code = "incorrect_cvc", - docUrl = "https://stripe.com/docs/error-codes/incorrect-cvc", - message = ERROR_MESSAGE, - param = "cvc", - type = "card_error" - ) - ) - } - - val result = createViewModel().attachPaymentMethod( - customerSession, - PaymentMethodFixtures.CARD_PAYMENT_METHOD - ) - - verify(customerSession).attachPaymentMethod( - eq("pm_123456789"), - eq(EXPECTED_PRODUCT_USAGE), - any() - ) - - assertThat(result.exceptionOrNull()?.message) - .isEqualTo(ERROR_MESSAGE) - } - - @Test - fun attachPaymentMethod_withCustomErrorMessageTranslator_whenError_returnsLocalizedError() = runTest { - whenever( - customerSession.attachPaymentMethod( - paymentMethodId = any(), - productUsage = any(), - listener = any() - ) - ).thenAnswer { invocation -> - val listener = invocation.arguments[2] as CustomerSession.PaymentMethodRetrievalListener - listener.onError( - 402, - ERROR_MESSAGE, - StripeError( - code = "incorrect_cvc", - docUrl = "https://stripe.com/docs/error-codes/incorrect-cvc", - message = ERROR_MESSAGE, - param = "cvc", - type = "card_error" - ) - ) - } - - val result = createViewModel(translator = TRANSLATOR).attachPaymentMethod( - customerSession, - PaymentMethodFixtures.CARD_PAYMENT_METHOD - ) - - verify(customerSession).attachPaymentMethod( - eq("pm_123456789"), - eq(EXPECTED_PRODUCT_USAGE), - any() - ) - - assertThat(result.exceptionOrNull()?.message) - .isEqualTo(ERROR_MESSAGE_LOCALIZED) - } - - private fun createViewModel( - eventReporter: PaymentSessionEventReporter = mock(), - args: AddPaymentMethodActivityStarter.Args = AddPaymentMethodActivityStarter.Args.Builder().build(), - translator: ErrorMessageTranslator = TranslatorManager.getErrorMessageTranslator() - ): AddPaymentMethodViewModel { - return AddPaymentMethodViewModel( - ApplicationProvider.getApplicationContext(), - SavedStateHandle(), - stripe, - args, - translator, - eventReporter - ) - } - - private companion object { - private const val ERROR_MESSAGE = "Your card's security code is incorrect." - private const val ERROR_MESSAGE_LOCALIZED = - "El código de seguridad de la tarjeta es incorrecto." - - private val TRANSLATOR = object : ErrorMessageTranslator { - override fun translate( - httpCode: Int, - errorMessage: String?, - stripeError: StripeError? - ): String { - return if (stripeError?.code == "incorrect_cvc") { - ERROR_MESSAGE_LOCALIZED - } else { - errorMessage.orEmpty() - } - } - } - - private val EXPECTED_PRODUCT_USAGE = setOf( - AddPaymentMethodActivity.PRODUCT_TOKEN - ) - } -} diff --git a/payments-core/src/test/java/com/stripe/android/view/DeletePaymentMethodDialogFactoryTest.kt b/payments-core/src/test/java/com/stripe/android/view/DeletePaymentMethodDialogFactoryTest.kt deleted file mode 100644 index d5c1e3610df..00000000000 --- a/payments-core/src/test/java/com/stripe/android/view/DeletePaymentMethodDialogFactoryTest.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.stripe.android.view - -import android.content.Context -import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth.assertThat -import com.stripe.android.CustomerSession -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.PaymentMethodFixtures -import org.junit.runner.RunWith -import org.mockito.kotlin.any -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.robolectric.RobolectricTestRunner -import kotlin.test.Test - -@RunWith(RobolectricTestRunner::class) -class DeletePaymentMethodDialogFactoryTest { - - private val customerSession: CustomerSession = mock() - private val context: Context = ApplicationProvider.getApplicationContext() - - @Test - fun onDeletedPaymentMethod_shouldCallDetachPaymentMethodAndCallback() { - var callbackPaymentMethod: PaymentMethod? = null - val factory = DeletePaymentMethodDialogFactory( - context, - mock(), - CardDisplayTextFactory(context), - Result.success(customerSession), - setOf(PaymentMethodsActivity.PRODUCT_TOKEN) - ) { - callbackPaymentMethod = it - } - - factory.onDeletedPaymentMethod( - PaymentMethodFixtures.CARD_PAYMENT_METHOD - ) - - verify(customerSession).detachPaymentMethod( - eq(requireNotNull(PaymentMethodFixtures.CARD_PAYMENT_METHOD.id)), - eq(setOf(PaymentMethodsActivity.PRODUCT_TOKEN)), - any() - ) - - assertThat(callbackPaymentMethod) - .isEqualTo(PaymentMethodFixtures.CARD_PAYMENT_METHOD) - } -} diff --git a/payments-core/src/test/java/com/stripe/android/view/FpxViewModelTest.kt b/payments-core/src/test/java/com/stripe/android/view/FpxViewModelTest.kt deleted file mode 100644 index 533b4aea156..00000000000 --- a/payments-core/src/test/java/com/stripe/android/view/FpxViewModelTest.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.stripe.android.view - -import androidx.test.core.app.ApplicationProvider -import app.cash.turbine.test -import com.google.common.truth.Truth.assertThat -import com.stripe.android.ApiKeyFixtures -import com.stripe.android.core.networking.ApiRequest -import com.stripe.android.model.BankStatuses -import com.stripe.android.testing.AbsFakeStripeRepository -import kotlinx.coroutines.test.runTest -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import kotlin.test.Test - -@RunWith(RobolectricTestRunner::class) -class FpxViewModelTest { - private val viewModel = FpxViewModel( - ApplicationProvider.getApplicationContext(), - ApiKeyFixtures.DEFAULT_PUBLISHABLE_KEY, - FakeStripeRepository() - ) - - @Test - internal fun `fpxBankStatues should emit on view model init`() = runTest { - viewModel.fpxBankStatues.test { - assertThat(FpxBank.get("affin_bank")?.let { awaitItem()?.isOnline(it) }) - .isTrue() - } - } - - private class FakeStripeRepository : AbsFakeStripeRepository() { - override suspend fun getFpxBankStatus(options: ApiRequest.Options): Result { - return Result.success(BankStatuses()) - } - } -} diff --git a/payments-core/src/test/java/com/stripe/android/view/PaymentFlowActivityTest.kt b/payments-core/src/test/java/com/stripe/android/view/PaymentFlowActivityTest.kt deleted file mode 100644 index d7b8dec896c..00000000000 --- a/payments-core/src/test/java/com/stripe/android/view/PaymentFlowActivityTest.kt +++ /dev/null @@ -1,231 +0,0 @@ -package com.stripe.android.view - -import android.app.Activity -import android.content.Context -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.core.view.isGone -import androidx.core.view.isVisible -import androidx.lifecycle.Lifecycle -import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth.assertThat -import com.stripe.android.ApiKeyFixtures -import com.stripe.android.CustomerSession -import com.stripe.android.EphemeralKeyProvider -import com.stripe.android.PaymentConfiguration -import com.stripe.android.PaymentSession.Companion.EXTRA_PAYMENT_SESSION_DATA -import com.stripe.android.PaymentSessionConfig -import com.stripe.android.PaymentSessionData -import com.stripe.android.PaymentSessionFixtures -import com.stripe.android.R -import com.stripe.android.model.ShippingMethod -import com.stripe.android.utils.TestUtils.idleLooper -import org.junit.Rule -import org.junit.runner.RunWith -import org.mockito.Mockito.`when` -import org.mockito.kotlin.mock -import org.mockito.kotlin.never -import org.mockito.kotlin.verify -import org.robolectric.RobolectricTestRunner -import kotlin.test.BeforeTest -import kotlin.test.Test - -@RunWith(RobolectricTestRunner::class) -class PaymentFlowActivityTest { - @get:Rule - val rule = InstantTaskExecutorRule() - - private val ephemeralKeyProvider: EphemeralKeyProvider = mock() - private val shippingInformationValidator: PaymentSessionConfig.ShippingInformationValidator = mock() - private val shippingMethodsFactory: PaymentSessionConfig.ShippingMethodsFactory = mock() - - private val context = ApplicationProvider.getApplicationContext() - private val activityScenarioFactory = ActivityScenarioFactory(context) - - @BeforeTest - fun setup() { - PaymentConfiguration.init( - ApplicationProvider.getApplicationContext(), - ApiKeyFixtures.FAKE_PUBLISHABLE_KEY - ) - CustomerSession.initCustomerSession(context, ephemeralKeyProvider) - } - - @Test - fun launchPaymentFlowActivity_withInvalidArgs_finishesActivity() { - activityScenarioFactory.create().use { activityScenario -> - assertThat(activityScenario.state).isEqualTo(Lifecycle.State.DESTROYED) - } - } - - @Test - fun launchPaymentFlowActivity_withHideShippingInfoConfig_hidesShippingInfoView() { - activityScenarioFactory.create( - createStarterArgs( - PaymentSessionFixtures.CONFIG.copy( - isShippingInfoRequired = false - ) - ) - ).use { activityScenario -> - activityScenario.onActivity { paymentFlowActivity -> - assertThat(getShippingInfoWidget(paymentFlowActivity)) - .isNull() - assertThat(getShippingMethodWidget(paymentFlowActivity)) - .isNotNull() - } - } - } - - @Test - fun onShippingInfoSave_whenShippingNotPopulated_doesNotFinish() { - activityScenarioFactory.create( - PaymentSessionFixtures.PAYMENT_FLOW_ARGS - ).use { activityScenario -> - activityScenario.onActivity { paymentFlowActivity -> - assertThat(getShippingInfoWidget(paymentFlowActivity)) - .isNotNull() - paymentFlowActivity.onActionSave() - assertThat(paymentFlowActivity.isFinishing) - .isFalse() - } - } - } - - @Test - fun onShippingInfoSave_whenShippingInfoNotPopulated_doesNotContinue() { - activityScenarioFactory.create( - PaymentSessionFixtures.PAYMENT_FLOW_ARGS - ).use { activityScenario -> - activityScenario.onActivity { paymentFlowActivity -> - assertThat(getShippingInfoWidget(paymentFlowActivity)) - .isNotNull() - paymentFlowActivity.onActionSave() - assertThat(paymentFlowActivity.isFinishing) - .isFalse() - assertThat(getShippingInfoWidget(paymentFlowActivity)) - .isNotNull() - } - } - } - - @Test - fun onShippingInfoProcessed_whenValidShippingInfoSubmitted_rendersCorrectly() { - activityScenarioFactory.create( - createStarterArgs( - PaymentSessionFixtures.CONFIG.copy( - prepopulatedShippingInfo = SHIPPING_INFO - ) - ) - ).use { activityScenario -> - activityScenario.onActivity { paymentFlowActivity -> - // valid result - paymentFlowActivity.onActionSave() - - assertThat(paymentFlowActivity.progressBar.isVisible) - .isTrue() - paymentFlowActivity.onShippingInfoSaved(SHIPPING_INFO, SHIPPING_METHODS) - assertThat(paymentFlowActivity.progressBar.isGone) - .isTrue() - } - } - } - - @Test - fun onShippingInfoSaved_whenOnlyShippingInfo_finishWithSuccess() { - activityScenarioFactory.createForResult( - createStarterArgs( - PaymentSessionConfig.Builder() - .setPrepopulatedShippingInfo(SHIPPING_INFO) - .setShippingMethodsRequired(false) - .build() - ) - ).use { activityScenario -> - activityScenario.onActivity { paymentFlowActivity -> - // valid result - paymentFlowActivity.onActionSave() - - paymentFlowActivity.onShippingInfoSaved(SHIPPING_INFO) - assertThat(paymentFlowActivity.isFinishing) - .isTrue() - - assertThat(activityScenario.result.resultCode) - .isEqualTo(Activity.RESULT_OK) - val resultData = activityScenario.result.resultData - - val resultSessionData: PaymentSessionData? = - resultData.extras?.getParcelable(EXTRA_PAYMENT_SESSION_DATA) - assertThat(resultSessionData?.shippingInformation) - .isEqualTo(SHIPPING_INFO) - } - } - } - - @Test - fun onShippingInfoSaved_withValidatorAndFactory() { - `when`(shippingInformationValidator.isValid(SHIPPING_INFO)).thenReturn(true) - `when`(shippingMethodsFactory.create(SHIPPING_INFO)).thenReturn(SHIPPING_METHODS) - - activityScenarioFactory.create( - createStarterArgs( - PaymentSessionConfig.Builder() - .setPrepopulatedShippingInfo(SHIPPING_INFO) - .setShippingInformationValidator(shippingInformationValidator) - .setShippingMethodsFactory(shippingMethodsFactory) - .build() - ) - ).use { activityScenario -> - activityScenario.onActivity { paymentFlowActivity -> - // valid result - paymentFlowActivity.onActionSave() - idleLooper() - - assertThat(paymentFlowActivity.progressBar.isVisible) - .isTrue() - paymentFlowActivity.onShippingInfoSaved(SHIPPING_INFO, SHIPPING_METHODS) - idleLooper() - assertThat(paymentFlowActivity.progressBar.isGone) - .isTrue() - - verify(shippingInformationValidator).isValid(SHIPPING_INFO) - verify(shippingInformationValidator, never()).getErrorMessage(SHIPPING_INFO) - verify(shippingMethodsFactory).create(SHIPPING_INFO) - } - } - } - - private fun getShippingInfoWidget(activity: Activity): ShippingInfoWidget? { - return activity.findViewById(R.id.shipping_info_widget) - } - - private fun getShippingMethodWidget(activity: Activity): SelectShippingMethodWidget? { - return activity.findViewById(R.id.select_shipping_method_widget) - } - - private companion object { - private val SHIPPING_INFO = ShippingInfoFixtures.DEFAULT - private val SHIPPING_METHODS = arrayListOf( - ShippingMethod( - "UPS Ground", - "ups-ground", - 0, - "USD", - "Arrives in 3-5 days" - ), - ShippingMethod( - "FedEx", - "fedex", - 599, - "USD", - "Arrives tomorrow" - ) - ) - - private fun createStarterArgs( - config: PaymentSessionConfig - ): PaymentFlowActivityStarter.Args { - return PaymentFlowActivityStarter.Args( - paymentSessionConfig = config, - paymentSessionData = PaymentSessionData(config) - ) - } - } -} diff --git a/payments-core/src/test/java/com/stripe/android/view/PaymentFlowPagerAdapterTest.kt b/payments-core/src/test/java/com/stripe/android/view/PaymentFlowPagerAdapterTest.kt deleted file mode 100644 index 2eec046e728..00000000000 --- a/payments-core/src/test/java/com/stripe/android/view/PaymentFlowPagerAdapterTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.stripe.android.view - -import androidx.test.core.app.ApplicationProvider -import com.stripe.android.PaymentSessionFixtures -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import kotlin.test.Test -import kotlin.test.assertEquals - -@RunWith(RobolectricTestRunner::class) -class PaymentFlowPagerAdapterTest { - - private val adapter = PaymentFlowPagerAdapter( - ApplicationProvider.getApplicationContext(), - PaymentSessionFixtures.CONFIG - ) - - @Test - fun pageCount_updatesAfterSavingShippingInfo() { - assertEquals(1, adapter.count) - - adapter.isShippingInfoSubmitted = true - assertEquals(2, adapter.count) - - adapter.isShippingInfoSubmitted = false - assertEquals(1, adapter.count) - } -} diff --git a/payments-core/src/test/java/com/stripe/android/view/PaymentFlowViewModelTest.kt b/payments-core/src/test/java/com/stripe/android/view/PaymentFlowViewModelTest.kt deleted file mode 100644 index 2b304479174..00000000000 --- a/payments-core/src/test/java/com/stripe/android/view/PaymentFlowViewModelTest.kt +++ /dev/null @@ -1,156 +0,0 @@ -package com.stripe.android.view - -import com.google.common.truth.Truth.assertThat -import com.stripe.android.CustomerSession -import com.stripe.android.PaymentSessionConfig -import com.stripe.android.PaymentSessionData -import com.stripe.android.PaymentSessionFixtures -import com.stripe.android.model.CustomerFixtures -import com.stripe.android.model.ShippingInformation -import com.stripe.android.model.ShippingMethod -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.runner.RunWith -import org.mockito.kotlin.any -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import org.robolectric.RobolectricTestRunner -import kotlin.test.Test - -@RunWith(RobolectricTestRunner::class) -class PaymentFlowViewModelTest { - private val customerSession: CustomerSession = mock() - - private val testDispatcher = UnconfinedTestDispatcher() - private val viewModel = PaymentFlowViewModel( - customerSession = customerSession, - paymentSessionData = PaymentSessionData(PaymentSessionFixtures.CONFIG), - workContext = testDispatcher - ) - - @Test - fun saveCustomerShippingInformation_onSuccess_returnsExpectedData() = runTest { - whenever( - customerSession.setCustomerShippingInformation( - any(), - any(), - any() - ) - ).thenAnswer { invocation -> - val listener = invocation.arguments[2] as CustomerSession.CustomerRetrievalListener - listener.onCustomerRetrieved(CustomerFixtures.CUSTOMER) - } - - assertThat(viewModel.isShippingInfoSubmitted) - .isFalse() - - val result = viewModel.saveCustomerShippingInformation(ShippingInfoFixtures.DEFAULT) - - verify(customerSession).setCustomerShippingInformation( - eq(ShippingInfoFixtures.DEFAULT), - eq(PaymentFlowViewModel.PRODUCT_USAGE), - any() - ) - - assertThat(result.getOrNull()) - .isNotNull() - assertThat(viewModel.isShippingInfoSubmitted) - .isTrue() - } - - @Test - fun validateShippingInformation_withValidShippingInfo_createsExpectedShippingMethods() = runTest { - val result = viewModel.validateShippingInformation( - FakeShippingInformationValidator(), - FakeShippingMethodsFactory(), - ShippingInfoFixtures.DEFAULT - ) - - assertThat(result.getOrNull()) - .isEqualTo(SHIPPING_METHODS) - - assertThat(viewModel.shippingMethods) - .isEqualTo(SHIPPING_METHODS) - } - - @Test - fun validateShippingInformation_withValidShippingInfoAndNoShippingMethodsFactory_createsEmptyShippingMethods() = - runTest { - val result = viewModel.validateShippingInformation( - FakeShippingInformationValidator(), - null, - ShippingInfoFixtures.DEFAULT - ) - - assertThat(result.getOrNull()) - .isEmpty() - assertThat(viewModel.shippingMethods) - .isEmpty() - } - - @Test - fun validateShippingInformation_withInvalidValidShippingInfo_createsEmptyShippingMethods() = runTest { - val result = viewModel.validateShippingInformation( - FailingShippingInformationValidator(), - ThrowingShippingMethodsFactory(), - ShippingInfoFixtures.DEFAULT - ) - - assertThat(result.exceptionOrNull()?.message) - .isEqualTo(SHIPPING_ERROR_MESSAGE) - } - - private class FakeShippingInformationValidator : PaymentSessionConfig.ShippingInformationValidator { - override fun isValid(shippingInformation: ShippingInformation): Boolean { - return true - } - - override fun getErrorMessage(shippingInformation: ShippingInformation): String { - throw IllegalStateException("Should not be called because shipping info is valid") - } - } - - private class FakeShippingMethodsFactory : PaymentSessionConfig.ShippingMethodsFactory { - override fun create(shippingInformation: ShippingInformation): List { - return SHIPPING_METHODS - } - } - - private class FailingShippingInformationValidator : PaymentSessionConfig.ShippingInformationValidator { - override fun isValid(shippingInformation: ShippingInformation): Boolean { - return false - } - - override fun getErrorMessage(shippingInformation: ShippingInformation): String { - return SHIPPING_ERROR_MESSAGE - } - } - - private class ThrowingShippingMethodsFactory : PaymentSessionConfig.ShippingMethodsFactory { - override fun create(shippingInformation: ShippingInformation): List { - throw RuntimeException("Always throws an exception") - } - } - - private companion object { - private const val SHIPPING_ERROR_MESSAGE = "Shipping info was invalid" - private val SHIPPING_METHODS = listOf( - ShippingMethod( - "UPS Ground", - "ups-ground", - 0, - "USD", - "Arrives in 3-5 days" - ), - ShippingMethod( - "FedEx", - "fedex", - 599, - "USD", - "Arrives tomorrow" - ) - ) - } -} diff --git a/payments-core/src/test/java/com/stripe/android/view/PaymentMethodSwipeCallbackTest.kt b/payments-core/src/test/java/com/stripe/android/view/PaymentMethodSwipeCallbackTest.kt deleted file mode 100644 index 233f3a140c4..00000000000 --- a/payments-core/src/test/java/com/stripe/android/view/PaymentMethodSwipeCallbackTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.stripe.android.view - -import android.content.Context -import androidx.core.content.ContextCompat -import androidx.test.core.app.ApplicationProvider -import com.stripe.android.R -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import kotlin.test.Test -import kotlin.test.assertEquals - -@RunWith(RobolectricTestRunner::class) -class PaymentMethodSwipeCallbackTest { - - @Test - fun testCalculateTransitionColor() { - val context: Context = ApplicationProvider.getApplicationContext() - val calculatedColor = PaymentMethodSwipeCallback.calculateTransitionColor( - 0.25F, - ContextCompat.getColor(context, R.color.stripe_swipe_start_payment_method), - ContextCompat.getColor(context, R.color.stripe_swipe_threshold_payment_method) - ) - assertEquals(-2312009, calculatedColor) - } -} diff --git a/payments-core/src/test/java/com/stripe/android/view/PaymentMethodsActivityStarterTest.kt b/payments-core/src/test/java/com/stripe/android/view/PaymentMethodsActivityStarterTest.kt deleted file mode 100644 index c190cf19268..00000000000 --- a/payments-core/src/test/java/com/stripe/android/view/PaymentMethodsActivityStarterTest.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.stripe.android.view - -import android.content.Context -import androidx.test.core.app.ApplicationProvider -import com.stripe.android.ApiKeyFixtures -import com.stripe.android.PaymentConfiguration -import com.stripe.android.R -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.PaymentMethodFixtures -import com.stripe.android.utils.ParcelUtils -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse - -@RunWith(RobolectricTestRunner::class) -class PaymentMethodsActivityStarterTest { - - @Test - fun testArgsParceling() { - val context = ApplicationProvider.getApplicationContext() - PaymentConfiguration.init(context, ApiKeyFixtures.FAKE_PUBLISHABLE_KEY) - - val args = PaymentMethodsActivityStarter.Args.Builder() - .setInitialPaymentMethodId("pm_12345") - .setIsPaymentSessionActive(true) - .setPaymentMethodTypes(listOf(PaymentMethod.Type.Card, PaymentMethod.Type.Fpx)) - .setPaymentConfiguration(PaymentConfiguration.getInstance(context)) - .setAddPaymentMethodFooter(R.layout.stripe_payment_methods_activity) - .setBillingAddressFields(BillingAddressFields.Full) - .build() - - assertEquals(args, ParcelUtils.create(args)) - } - - @Test - fun testResultParceling() { - val result = PaymentMethodsActivityStarter.Result( - PaymentMethodFixtures.CARD_PAYMENT_METHOD, - true - ) - assertEquals(result, ParcelUtils.create(result)) - } - - @Test - fun testDefaultPaymentMethodTypes_isCard() { - assertEquals( - listOf(PaymentMethod.Type.Card), - PaymentMethodsActivityStarter.Args.Builder() - .build() - .paymentMethodTypes - ) - } - - @Test - fun testDisableDeletePaymentMethods() { - assertFalse( - PaymentMethodsActivityStarter.Args.Builder() - .setCanDeletePaymentMethods(false) - .build() - .canDeletePaymentMethods - ) - } -} diff --git a/payments-core/src/test/java/com/stripe/android/view/PaymentMethodsActivityTest.kt b/payments-core/src/test/java/com/stripe/android/view/PaymentMethodsActivityTest.kt deleted file mode 100644 index 98014da3080..00000000000 --- a/payments-core/src/test/java/com/stripe/android/view/PaymentMethodsActivityTest.kt +++ /dev/null @@ -1,258 +0,0 @@ -package com.stripe.android.view - -import android.app.Activity.RESULT_CANCELED -import android.content.Context -import android.view.View -import androidx.lifecycle.Lifecycle -import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth.assertThat -import com.stripe.android.ApiKeyFixtures -import com.stripe.android.CustomerSession -import com.stripe.android.PaymentConfiguration -import com.stripe.android.R -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.PaymentMethodFixtures -import org.junit.runner.RunWith -import org.mockito.Mockito.times -import org.mockito.kotlin.KArgumentCaptor -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.eq -import org.mockito.kotlin.isNull -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows.shadowOf -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -/** - * Test class for [PaymentMethodsActivity]. - */ -@RunWith(RobolectricTestRunner::class) -class PaymentMethodsActivityTest { - private val customerSession: CustomerSession = mock() - - private val listenerArgumentCaptor: KArgumentCaptor = - argumentCaptor() - - private val context = ApplicationProvider.getApplicationContext() - private val activityScenarioFactory = ActivityScenarioFactory(context) - - @BeforeTest - fun setup() { - CustomerSession.instance = customerSession - PaymentConfiguration.init(context, ApiKeyFixtures.FAKE_PUBLISHABLE_KEY) - } - - @Test - fun onCreate_finishesActivityWhenArgsAreMissing() { - activityScenarioFactory.create().use { activityScenario -> - assertThat(activityScenario.state).isEqualTo(Lifecycle.State.DESTROYED) - } - } - - @Test - fun onCreate_callsApiAndDisplaysProgressBarWhileWaiting() { - activityScenarioFactory.create( - PaymentMethodsActivityStarter.Args.Builder() - .setPaymentConfiguration(PaymentConfiguration.getInstance(context)) - .build() - ).use { activityScenario -> - activityScenario.onActivity { - val progressBar = it.viewBinding.progressBar - val recyclerView = it.viewBinding.recycler - val addCardView: View = it.findViewById(R.id.stripe_payment_methods_add_card) - - verify(customerSession).getPaymentMethods( - eq(PaymentMethod.Type.Card), - isNull(), - isNull(), - isNull(), - eq(setOf(PaymentMethodsActivity.PRODUCT_TOKEN)), - listenerArgumentCaptor.capture() - ) - - assertEquals(View.VISIBLE, progressBar.visibility) - assertEquals(View.VISIBLE, addCardView.visibility) - assertEquals(View.VISIBLE, recyclerView.visibility) - - listenerArgumentCaptor.firstValue - .onPaymentMethodsRetrieved(PaymentMethodFixtures.CARD_PAYMENT_METHODS) - - assertEquals(View.GONE, progressBar.visibility) - } - } - } - - @Test - fun onCreate_initialGivenPaymentMethodIsSelected() { - val paymentMethod = PaymentMethodFixtures.CARD_PAYMENT_METHODS[0] - - activityScenarioFactory.create( - PaymentMethodsActivityStarter.Args.Builder() - .setInitialPaymentMethodId(paymentMethod.id) - .setPaymentConfiguration(PaymentConfiguration.getInstance(context)) - .build() - ).use { activityScenario -> - activityScenario.onActivity { - val recyclerView = it.viewBinding.recycler - - verify(customerSession) - .getPaymentMethods( - eq(PaymentMethod.Type.Card), - isNull(), - isNull(), - isNull(), - eq(setOf(PaymentMethodsActivity.PRODUCT_TOKEN)), - listenerArgumentCaptor.capture() - ) - - listenerArgumentCaptor.firstValue - .onPaymentMethodsRetrieved(PaymentMethodFixtures.CARD_PAYMENT_METHODS) - - val paymentMethodsAdapter = - recyclerView.adapter as PaymentMethodsAdapter - assertEquals(paymentMethod.id, paymentMethodsAdapter.selectedPaymentMethod?.id) - } - } - } - - @Test - fun onClickAddClickView_withoutPaymentSession_launchesWithoutLog() { - activityScenarioFactory.create( - PaymentMethodsActivityStarter.Args.Builder() - .setPaymentConfiguration(PaymentConfiguration.getInstance(context)) - .build() - ).use { activityScenario -> - activityScenario.onActivity { activity -> - val addCardView: View = activity - .findViewById(R.id.stripe_payment_methods_add_card) - - addCardView.performClick() - - val intentForResult = - shadowOf(activity).nextStartedActivityForResult - val component = intentForResult.intent.component - assertEquals(AddPaymentMethodActivity::class.java.name, component?.className) - } - } - } - - @Test - fun onClickAddCardView_whenStartedFromPaymentSession_launchesActivityWithLog() { - activityScenarioFactory.create( - PaymentMethodsActivityStarter.Args.Builder() - .setPaymentConfiguration(PaymentConfiguration.getInstance(context)) - .setIsPaymentSessionActive(true) - .build() - ).use { activityScenario -> - activityScenario.onActivity { activity -> - val addCardView: View = activity - .findViewById(R.id.stripe_payment_methods_add_card) - addCardView.performClick() - - val intentForResult = shadowOf(activity).nextStartedActivityForResult - val component = intentForResult.intent.component - assertEquals(AddPaymentMethodActivity::class.java.name, component?.className) - assertTrue( - AddPaymentMethodActivityStarter.Args.create(intentForResult.intent) - .isPaymentSessionActive - ) - } - } - } - - @Test - fun onActivityResult_withValidPaymentMethod_refreshesPaymentMethods() { - activityScenarioFactory.create( - PaymentMethodsActivityStarter.Args.Builder() - .setPaymentConfiguration(PaymentConfiguration.getInstance(context)) - .build() - ).use { activityScenario -> - activityScenario.onActivity { activity -> - val progressBar = activity.viewBinding.progressBar - val recyclerView = activity.viewBinding.recycler - - val paymentMethod = PaymentMethodFixtures.CARD_PAYMENT_METHODS[2] - assertNotNull(paymentMethod) - - activity.onAddPaymentMethodResult( - AddPaymentMethodActivityStarter.Result.Success(paymentMethod) - ) - - assertEquals(View.VISIBLE, progressBar.visibility) - verify(customerSession, times(2)) - .getPaymentMethods( - eq(PaymentMethod.Type.Card), - isNull(), - isNull(), - isNull(), - eq(setOf(PaymentMethodsActivity.PRODUCT_TOKEN)), - listenerArgumentCaptor.capture() - ) - - listenerArgumentCaptor.firstValue - .onPaymentMethodsRetrieved(PaymentMethodFixtures.CARD_PAYMENT_METHODS) - assertEquals(View.GONE, progressBar.visibility) - assertEquals(4, recyclerView.adapter?.itemCount) - - val paymentMethodsAdapter = - recyclerView.adapter as PaymentMethodsAdapter - - paymentMethodsAdapter.selectedPaymentMethodId = paymentMethod.id - assertEquals(paymentMethod.id, paymentMethodsAdapter.selectedPaymentMethod?.id) - } - } - } - - @Test - fun setSelectionAndFinish_finishedWithExpectedResult() { - activityScenarioFactory.createForResult( - PaymentMethodsActivityStarter.Args.Builder() - .setPaymentConfiguration(PaymentConfiguration.getInstance(context)) - .build() - ).use { activityScenario -> - activityScenario.onActivity { activity -> - val progressBar = activity.viewBinding.progressBar - val recyclerView = activity.viewBinding.recycler - val addCardView: View = activity.findViewById(R.id.stripe_payment_methods_add_card) - - verify(customerSession).getPaymentMethods( - eq(PaymentMethod.Type.Card), - isNull(), - isNull(), - isNull(), - eq(setOf(PaymentMethodsActivity.PRODUCT_TOKEN)), - listenerArgumentCaptor.capture() - ) - - assertEquals(View.VISIBLE, progressBar.visibility) - assertEquals(View.VISIBLE, addCardView.visibility) - assertEquals(View.VISIBLE, recyclerView.visibility) - - listenerArgumentCaptor.firstValue - .onPaymentMethodsRetrieved(PaymentMethodFixtures.CARD_PAYMENT_METHODS) - val paymentMethodsAdapter = - recyclerView.adapter as PaymentMethodsAdapter - paymentMethodsAdapter.selectedPaymentMethodId = - PaymentMethodFixtures.CARD_PAYMENT_METHODS[0].id - - activity.onBackPressedDispatcher.onBackPressed() - - // Now it should be gone. - assertEquals(View.GONE, progressBar.visibility) - assertTrue(activity.isFinishing) - - // `resultCode` is `RESULT_CANCELED` because back was pressed - assertEquals(RESULT_CANCELED, activityScenario.result.resultCode) - - val result = - PaymentMethodsActivityStarter.Result.fromIntent(activityScenario.result.resultData) - assertEquals(PaymentMethodFixtures.CARD_PAYMENT_METHODS[0], result?.paymentMethod) - } - } - } -} diff --git a/payments-core/src/test/java/com/stripe/android/view/PaymentMethodsAdapterTest.kt b/payments-core/src/test/java/com/stripe/android/view/PaymentMethodsAdapterTest.kt deleted file mode 100644 index c0930eefed5..00000000000 --- a/payments-core/src/test/java/com/stripe/android/view/PaymentMethodsAdapterTest.kt +++ /dev/null @@ -1,309 +0,0 @@ -package com.stripe.android.view - -import android.widget.FrameLayout -import androidx.appcompat.view.ContextThemeWrapper -import androidx.recyclerview.widget.RecyclerView -import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth.assertThat -import com.stripe.android.R -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.PaymentMethodFixtures -import org.junit.runner.RunWith -import org.mockito.Mockito.times -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.robolectric.RobolectricTestRunner -import kotlin.test.Test - -/** - * Test class for [PaymentMethodsAdapter] - */ -@RunWith(RobolectricTestRunner::class) -class PaymentMethodsAdapterTest { - private val adapterDataObserver: RecyclerView.AdapterDataObserver = mock() - private val listener: PaymentMethodsAdapter.Listener = mock() - - private val context = ContextThemeWrapper( - ApplicationProvider.getApplicationContext(), - R.style.StripeDefaultTheme - ) - - private val parentView = FrameLayout(context) - - private val paymentMethodsAdapter = PaymentMethodsAdapter( - ARGS - ).also { - it.registerAdapterDataObserver(adapterDataObserver) - } - - @Test - fun setSelection_changesSelection() { - paymentMethodsAdapter.setPaymentMethods(PaymentMethodFixtures.CARD_PAYMENT_METHODS) - assertThat(paymentMethodsAdapter.itemCount) - .isEqualTo(4) - verify(adapterDataObserver).onChanged() - - paymentMethodsAdapter.selectedPaymentMethodId = paymentMethodsAdapter.paymentMethods[2].id - assertThat(requireNotNull(paymentMethodsAdapter.selectedPaymentMethod).id) - .isEqualTo(PaymentMethodFixtures.CARD_PAYMENT_METHODS[2].id) - - paymentMethodsAdapter.selectedPaymentMethodId = - PaymentMethodFixtures.CARD_PAYMENT_METHODS[1].id - assertThat(PaymentMethodFixtures.CARD_PAYMENT_METHODS[1].id) - .isEqualTo(paymentMethodsAdapter.selectedPaymentMethod?.id) - } - - @Test - fun updatePaymentMethods_removesExistingPaymentMethodsAndAddsAllPaymentMethods() { - val singlePaymentMethod = - listOf(PaymentMethodFixtures.CARD_PAYMENT_METHODS[0]) - - paymentMethodsAdapter.setPaymentMethods(singlePaymentMethod) - paymentMethodsAdapter.selectedPaymentMethodId = paymentMethodsAdapter.paymentMethods[0].id - assertThat(paymentMethodsAdapter.itemCount) - .isEqualTo(2) - assertThat(paymentMethodsAdapter.selectedPaymentMethod) - .isNotNull() - - assertThat(paymentMethodsAdapter.selectedPaymentMethod?.id) - .isEqualTo(PaymentMethodFixtures.CARD_PAYMENT_METHODS[0].id) - - paymentMethodsAdapter.setPaymentMethods(PaymentMethodFixtures.CARD_PAYMENT_METHODS) - paymentMethodsAdapter.selectedPaymentMethodId = paymentMethodsAdapter.paymentMethods[2].id - assertThat(paymentMethodsAdapter.itemCount) - .isEqualTo(4) - assertThat(paymentMethodsAdapter.selectedPaymentMethod?.id) - .isEqualTo(PaymentMethodFixtures.CARD_PAYMENT_METHODS[2].id) - verify(adapterDataObserver, times(2)) - .onChanged() - } - - @Test - fun updatePaymentMethods_withSelection_updatesPaymentMethodsAndSelectionMaintained() { - paymentMethodsAdapter.setPaymentMethods(PaymentMethodFixtures.CARD_PAYMENT_METHODS) - assertThat(paymentMethodsAdapter.itemCount) - .isEqualTo(4) - paymentMethodsAdapter.selectedPaymentMethodId = - PaymentMethodFixtures.CARD_PAYMENT_METHODS[2].id - assertThat(paymentMethodsAdapter.selectedPaymentMethod) - .isNotNull() - - paymentMethodsAdapter.setPaymentMethods(PaymentMethodFixtures.CARD_PAYMENT_METHODS) - assertThat(paymentMethodsAdapter.itemCount) - .isEqualTo(4) - assertThat(paymentMethodsAdapter.selectedPaymentMethod?.id) - .isEqualTo(PaymentMethodFixtures.CARD_PAYMENT_METHODS[2].id) - } - - @Test - fun setPaymentMethods_whenNoInitialSpecified_returnsNull() { - paymentMethodsAdapter - paymentMethodsAdapter.setPaymentMethods(PaymentMethodFixtures.CARD_PAYMENT_METHODS) - assertThat(paymentMethodsAdapter.selectedPaymentMethod) - .isNull() - } - - @Test - fun setPaymentMethods_whenInitialSpecified_selectsIt() { - val adapter = PaymentMethodsAdapter( - intentArgs = PaymentMethodsActivityStarter.Args.Builder() - .build(), - initiallySelectedPaymentMethodId = "pm_1000" - ) - adapter.setPaymentMethods(PaymentMethodFixtures.CARD_PAYMENT_METHODS) - assertThat(adapter.selectedPaymentMethod?.id) - .isEqualTo("pm_1000") - } - - @Test - fun testGetItemViewType_withGooglePayEnabled() { - val adapter = PaymentMethodsAdapter( - ARGS, - addableTypes = listOf(PaymentMethod.Type.Card, PaymentMethod.Type.Fpx), - shouldShowGooglePay = true - ) - adapter.setPaymentMethods(PaymentMethodFixtures.CARD_PAYMENT_METHODS) - - assertThat(adapter.itemCount) - .isEqualTo(6) - - assertThat( - adapter.viewTypes - ).isEqualTo( - listOf( - PaymentMethodsAdapter.ViewType.GooglePay, - PaymentMethodsAdapter.ViewType.Card, - PaymentMethodsAdapter.ViewType.Card, - PaymentMethodsAdapter.ViewType.Card, - PaymentMethodsAdapter.ViewType.AddCard, - PaymentMethodsAdapter.ViewType.AddFpx - ) - ) - } - - @Test - fun testGetItemId_withGooglePayEnabled() { - val adapter = PaymentMethodsAdapter( - ARGS, - addableTypes = listOf(PaymentMethod.Type.Card, PaymentMethod.Type.Fpx), - shouldShowGooglePay = true - ) - adapter.setPaymentMethods(PaymentMethodFixtures.CARD_PAYMENT_METHODS) - - assertThat(adapter.itemCount) - .isEqualTo(6) - - assertThat(adapter.getItemId(0)) - .isEqualTo(PaymentMethodsAdapter.GOOGLE_PAY_ITEM_ID) - - val uniqueItemIds = (1..5) - .map { adapter.getItemId(it) } - .toSet() - assertThat(uniqueItemIds) - .hasSize(5) - } - - @Test - fun testGetItemViewType_withGooglePayDisabled() { - val adapter = PaymentMethodsAdapter( - ARGS, - addableTypes = listOf(PaymentMethod.Type.Card, PaymentMethod.Type.Fpx), - shouldShowGooglePay = false - ) - adapter.setPaymentMethods(PaymentMethodFixtures.CARD_PAYMENT_METHODS) - - assertThat(adapter.viewTypes) - .isEqualTo( - listOf( - PaymentMethodsAdapter.ViewType.Card, - PaymentMethodsAdapter.ViewType.Card, - PaymentMethodsAdapter.ViewType.Card, - PaymentMethodsAdapter.ViewType.AddCard, - PaymentMethodsAdapter.ViewType.AddFpx - ) - ) - } - - @Test - fun testGooglePayRowClick_shouldCallListener() { - val adapter = PaymentMethodsAdapter( - ARGS, - shouldShowGooglePay = true - ) - adapter.setPaymentMethods(PaymentMethodFixtures.CARD_PAYMENT_METHODS) - adapter.listener = listener - - val viewHolder = PaymentMethodsAdapter.ViewHolder.GooglePayViewHolder(context, parentView) - adapter.onBindViewHolder(viewHolder, 0) - - viewHolder.itemView.performClick() - verify(listener).onGooglePayClick() - } - - @Test - fun `onPositionClicked() should call listener's onPaymentMethodClick()`() { - val adapter = PaymentMethodsAdapter( - ARGS - ) - - adapter.setPaymentMethods(PaymentMethodFixtures.CARD_PAYMENT_METHODS) - adapter.listener = listener - adapter.onPositionClicked(0) - - verify(listener).onPaymentMethodClick(PaymentMethodFixtures.CARD_PAYMENT_METHODS.first()) - } - - @Test - fun getPosition_withValidPaymentMethod_returnsPosition() { - val adapter = PaymentMethodsAdapter( - ARGS, - shouldShowGooglePay = true - ) - adapter.setPaymentMethods(PaymentMethodFixtures.CARD_PAYMENT_METHODS) - - assertThat(adapter.getPosition(PaymentMethodFixtures.CARD_PAYMENT_METHODS.last())) - .isEqualTo(3) - } - - @Test - fun getPosition_withInvalidPaymentMethod_returnsNull() { - val adapter = PaymentMethodsAdapter( - ARGS, - shouldShowGooglePay = true - ) - adapter.setPaymentMethods(PaymentMethodFixtures.CARD_PAYMENT_METHODS) - - assertThat(adapter.getPosition(PaymentMethodFixtures.FPX_PAYMENT_METHOD)) - .isNull() - } - - @Test - fun deletePaymentMethod_withValidPaymentMethod_removesPaymentMethod() { - val adapter = PaymentMethodsAdapter( - ARGS, - shouldShowGooglePay = true - ) - adapter.registerAdapterDataObserver(adapterDataObserver) - - adapter.setPaymentMethods(PaymentMethodFixtures.CARD_PAYMENT_METHODS) - assertThat(adapter.paymentMethods) - .contains(PaymentMethodFixtures.CARD_PAYMENT_METHODS.last()) - adapter.deletePaymentMethod(PaymentMethodFixtures.CARD_PAYMENT_METHODS.last()) - assertThat(adapter.paymentMethods) - .doesNotContain(PaymentMethodFixtures.CARD_PAYMENT_METHODS.last()) - verify(adapterDataObserver).onItemRangeRemoved(3, 1) - } - - @Test - fun `click on add card view should emit args`() { - val adapter = PaymentMethodsAdapter( - ARGS, - shouldShowGooglePay = true - ) - - val listener = mock() - adapter.listener = listener - - val viewHolder = adapter.createViewHolder( - FrameLayout(context), - PaymentMethodsAdapter.ViewType.AddCard.ordinal - ) - adapter.onBindViewHolder(viewHolder, 0) - viewHolder.itemView.performClick() - - verify(listener).onAddPaymentMethodClick(adapter.addCardArgs) - } - - @Test - fun `click on add FPX view should emit args`() { - val adapter = PaymentMethodsAdapter( - ARGS, - shouldShowGooglePay = true - ) - - val listener = mock() - adapter.listener = listener - - val viewHolder = adapter.createViewHolder( - FrameLayout(context), - PaymentMethodsAdapter.ViewType.AddFpx.ordinal - ) - adapter.onBindViewHolder(viewHolder, 0) - viewHolder.itemView.performClick() - - verify(listener).onAddPaymentMethodClick(adapter.addFpxArgs) - } - - private companion object { - private val ARGS = - PaymentMethodsActivityStarter.Args.Builder() - .build() - - private val PaymentMethodsAdapter.viewTypes: List - get() { - return (0 until itemCount).map { - PaymentMethodsAdapter.ViewType.entries[getItemViewType(it)] - } - } - } -} diff --git a/payments-core/src/test/java/com/stripe/android/view/PaymentMethodsViewModelTest.kt b/payments-core/src/test/java/com/stripe/android/view/PaymentMethodsViewModelTest.kt deleted file mode 100644 index 7ccee457442..00000000000 --- a/payments-core/src/test/java/com/stripe/android/view/PaymentMethodsViewModelTest.kt +++ /dev/null @@ -1,184 +0,0 @@ -package com.stripe.android.view - -import androidx.lifecycle.SavedStateHandle -import androidx.test.core.app.ApplicationProvider -import app.cash.turbine.test -import com.google.common.truth.Truth.assertThat -import com.stripe.android.CustomerSession -import com.stripe.android.PaymentSession -import com.stripe.android.analytics.PaymentSessionEventReporter -import com.stripe.android.core.exception.InvalidRequestException -import com.stripe.android.model.PaymentMethod -import com.stripe.android.model.PaymentMethodFixtures -import kotlinx.coroutines.test.runTest -import org.junit.runner.RunWith -import org.mockito.kotlin.KArgumentCaptor -import org.mockito.kotlin.any -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.eq -import org.mockito.kotlin.isNull -import org.mockito.kotlin.mock -import org.mockito.kotlin.times -import org.mockito.kotlin.verify -import org.robolectric.RobolectricTestRunner -import kotlin.test.Test - -@RunWith(RobolectricTestRunner::class) -class PaymentMethodsViewModelTest { - private val listenerArgumentCaptor: - KArgumentCaptor = - argumentCaptor() - - @Test - fun init_loadStartedEvent_shouldTrigger() { - val eventReporter: PaymentSessionEventReporter = mock() - - createViewModel(eventReporter = eventReporter) - - verify(eventReporter).onLoadStarted() - } - - @Test - fun paymentMethodsData_whenSuccess_returnsExpectedPaymentMethods() = runTest { - val customerSession: CustomerSession = mock() - val eventReporter: PaymentSessionEventReporter = mock() - - val viewModel = createViewModel( - customerSession = customerSession, - eventReporter = eventReporter, - selectedPaymentMethodId = "fpx" - ) - - verify(customerSession).getPaymentMethods( - eq(PaymentMethod.Type.Card), - isNull(), - isNull(), - isNull(), - eq(EXPECTED_PRODUCT_USAGE), - listenerArgumentCaptor.capture() - ) - - listenerArgumentCaptor.firstValue.onPaymentMethodsRetrieved( - listOf(PaymentMethodFixtures.CARD_PAYMENT_METHOD) - ) - - viewModel.paymentMethodsData.test { - assertThat(awaitItem()) - .isEqualTo(Result.success(listOf(PaymentMethodFixtures.CARD_PAYMENT_METHOD))) - - verify(eventReporter).onLoadSucceeded("fpx") - verify(eventReporter).onOptionsShown() - } - } - - @Test - fun paymentMethodsData_whenError_returnsExpectedException() = runTest { - val customerSession: CustomerSession = mock() - val eventReporter: PaymentSessionEventReporter = mock() - - val viewModel = createViewModel( - eventReporter = eventReporter, - customerSession = customerSession - ) - - verify(customerSession).getPaymentMethods( - eq(PaymentMethod.Type.Card), - isNull(), - isNull(), - isNull(), - eq(EXPECTED_PRODUCT_USAGE), - listenerArgumentCaptor.capture() - ) - - listenerArgumentCaptor.firstValue.onError( - 404, - "error!", - null, - InvalidRequestException() - ) - - viewModel.paymentMethodsData.test { - assertThat(awaitItem()?.exceptionOrNull()) - .isInstanceOf(InvalidRequestException::class.java) - - verify(eventReporter).onLoadFailed(any()) - } - } - - @Test - fun onPaymentMethodAdded_shouldUpdateSnackbarData() = runTest { - val customerSession: CustomerSession = mock() - - val viewModel = createViewModel(customerSession = customerSession) - - viewModel.snackbarData.test { - assertThat(awaitItem()) - .isNull() - viewModel.onPaymentMethodAdded(PaymentMethodFixtures.CARD_PAYMENT_METHOD) - assertThat(awaitItem()) - .isEqualTo("Added Visa ending in 4242") - assertThat(awaitItem()) - .isNull() - // First time is init, second time is from the onPaymentMethodAdded call. - verify(customerSession, times(2)).getPaymentMethods( - eq(PaymentMethod.Type.Card), - isNull(), - isNull(), - isNull(), - eq(EXPECTED_PRODUCT_USAGE), - listenerArgumentCaptor.capture() - ) - } - } - - @Test - fun onPaymentMethodRemoved_shouldUpdateSnackbarData() = runTest { - val viewModel = createViewModel() - - viewModel.snackbarData.test { - assertThat(awaitItem()) - .isNull() - viewModel.onPaymentMethodRemoved(PaymentMethodFixtures.CARD_PAYMENT_METHOD) - assertThat(awaitItem()) - .isEqualTo("Removed Visa ending in 4242") - assertThat(awaitItem()) - .isNull() - } - } - - @Test - fun `paymentMethodsData with CustomerSession failure should return failure result`() = runTest { - PaymentMethodsViewModel( - application = ApplicationProvider.getApplicationContext(), - customerSession = Result.failure(RuntimeException("failure")), - startedFromPaymentSession = true, - savedStateHandle = SavedStateHandle(), - eventReporter = mock(), - ).paymentMethodsData.test { - assertThat(awaitItem()?.isFailure) - .isTrue() - } - } - - private fun createViewModel( - customerSession: CustomerSession = mock(), - eventReporter: PaymentSessionEventReporter = mock(), - selectedPaymentMethodId: String? = null, - ): PaymentMethodsViewModel { - return PaymentMethodsViewModel( - application = ApplicationProvider.getApplicationContext(), - customerSession = Result.success(customerSession), - startedFromPaymentSession = true, - savedStateHandle = SavedStateHandle(), - eventReporter = eventReporter, - selectedPaymentMethodId = selectedPaymentMethodId, - ) - } - - private companion object { - private val EXPECTED_PRODUCT_USAGE = setOf( - PaymentSession.PRODUCT_TOKEN, - PaymentMethodsActivity.PRODUCT_TOKEN - ) - } -}