From 616f488307a16eeba4d9dd806a3cb06309e0f304 Mon Sep 17 00:00:00 2001 From: jameswoo-stripe <99316447+jameswoo-stripe@users.noreply.github.com> Date: Thu, 30 Jun 2022 13:54:45 -0700 Subject: [PATCH 1/2] Add Autocomplete address screen --- payments-ui-core/api/payments-ui-core.api | 67 ++++++ payments-ui-core/res/values/colors.xml | 2 + payments-ui-core/res/values/totranslate.xml | 4 + .../autocomplete/PlacesClientProxy.kt | 7 +- .../core/elements/autocomplete/model/Place.kt | 16 +- .../model/TransformGoogleToStripeAddress.kt | 4 +- paymentsheet-example/build.gradle | 1 + paymentsheet/api/paymentsheet.api | 35 ++- paymentsheet/build.gradle | 1 + .../addresselement/AutocompleteScreenTest.kt | 174 ++++++++++++++ .../android/paymentsheet/PaymentSheet.kt | 9 +- .../addresselement/AddressElementActivity.kt | 10 +- .../addresselement/AddressElementNavigator.kt | 2 +- .../addresselement/AddressElementViewModel.kt | 2 +- .../addresselement/AutoCompleteScreen.kt | 44 ---- .../addresselement/AutoCompleteViewModel.kt | 31 --- .../addresselement/AutocompleteScreen.kt | 219 +++++++++++++++++ .../addresselement/AutocompleteViewModel.kt | 220 ++++++++++++++++++ .../addresselement/InputAddressScreen.kt | 4 +- .../addresselement/InputAddressViewModel.kt | 18 +- ...AddressElementViewModelFactoryComponent.kt | 4 +- .../AutoCompleteViewModelSubcomponent.kt | 8 +- .../AutocompleteViewModelTest.kt | 198 ++++++++++++++++ .../InputAddressViewModelTest.kt | 15 +- 24 files changed, 973 insertions(+), 122 deletions(-) create mode 100644 paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreenTest.kt delete mode 100644 paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutoCompleteScreen.kt delete mode 100644 paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutoCompleteViewModel.kt create mode 100644 paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreen.kt create mode 100644 paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteViewModel.kt create mode 100644 paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AutocompleteViewModelTest.kt diff --git a/payments-ui-core/api/payments-ui-core.api b/payments-ui-core/api/payments-ui-core.api index bbf447e2cd1..962e999cc17 100644 --- a/payments-ui-core/api/payments-ui-core.api +++ b/payments-ui-core/api/payments-ui-core.api @@ -584,6 +584,73 @@ public final class com/stripe/android/ui/core/elements/TranslationId$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class com/stripe/android/ui/core/elements/autocomplete/PlacesClientProxy$Companion { + public final fun create (Landroid/content/Context;Ljava/lang/String;Lcom/stripe/android/ui/core/elements/autocomplete/IsPlacesAvailable;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)Lcom/stripe/android/ui/core/elements/autocomplete/PlacesClientProxy; + public static synthetic fun create$default (Lcom/stripe/android/ui/core/elements/autocomplete/PlacesClientProxy$Companion;Landroid/content/Context;Ljava/lang/String;Lcom/stripe/android/ui/core/elements/autocomplete/IsPlacesAvailable;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/stripe/android/ui/core/elements/autocomplete/PlacesClientProxy; + public final fun getPlacesPoweredByGoogleDrawable (ZLcom/stripe/android/ui/core/elements/autocomplete/IsPlacesAvailable;)Ljava/lang/Integer; + public static synthetic fun getPlacesPoweredByGoogleDrawable$default (Lcom/stripe/android/ui/core/elements/autocomplete/PlacesClientProxy$Companion;ZLcom/stripe/android/ui/core/elements/autocomplete/IsPlacesAvailable;ILjava/lang/Object;)Ljava/lang/Integer; +} + +public final class com/stripe/android/ui/core/elements/autocomplete/model/AddressComponent$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field $stable I + public static final field INSTANCE Lcom/stripe/android/ui/core/elements/autocomplete/model/AddressComponent$$serializer; + public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/stripe/android/ui/core/elements/autocomplete/model/AddressComponent; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/stripe/android/ui/core/elements/autocomplete/model/AddressComponent;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/stripe/android/ui/core/elements/autocomplete/model/AddressComponent$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/stripe/android/ui/core/elements/autocomplete/model/Place$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field $stable I + public static final field INSTANCE Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$$serializer; + public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/stripe/android/ui/core/elements/autocomplete/model/Place; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/stripe/android/ui/core/elements/autocomplete/model/Place;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/stripe/android/ui/core/elements/autocomplete/model/Place$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/stripe/android/ui/core/elements/autocomplete/model/Place$Type : java/lang/Enum { + public static final field ADMINISTRATIVE_AREA_LEVEL_1 Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; + public static final field ADMINISTRATIVE_AREA_LEVEL_2 Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; + public static final field ADMINISTRATIVE_AREA_LEVEL_3 Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; + public static final field ADMINISTRATIVE_AREA_LEVEL_4 Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; + public static final field COUNTRY Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; + public static final field LOCALITY Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; + public static final field NEIGHBORHOOD Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; + public static final field POSTAL_CODE Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; + public static final field POSTAL_TOWN Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; + public static final field PREMISE Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; + public static final field ROUTE Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; + public static final field STREET_NUMBER Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; + public static final field SUBLOCALITY Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; + public static final field SUBLOCALITY_LEVEL_1 Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; + public static final field SUBLOCALITY_LEVEL_2 Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; + public static final field SUBLOCALITY_LEVEL_3 Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; + public static final field SUBLOCALITY_LEVEL_4 Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; + public final fun getValue ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; + public static fun values ()[Lcom/stripe/android/ui/core/elements/autocomplete/model/Place$Type; +} + +public final class com/stripe/android/ui/core/elements/autocomplete/model/TransformGoogleToStripeAddressKt { +} + public final class com/stripe/android/ui/core/elements/menu/CheckboxKt { } diff --git a/payments-ui-core/res/values/colors.xml b/payments-ui-core/res/values/colors.xml index f18761190cd..f963a0a1a88 100644 --- a/payments-ui-core/res/values/colors.xml +++ b/payments-ui-core/res/values/colors.xml @@ -49,4 +49,6 @@ + #1A1A1A0D + diff --git a/payments-ui-core/res/values/totranslate.xml b/payments-ui-core/res/values/totranslate.xml index da7c99a2030..5416b57db03 100644 --- a/payments-ui-core/res/values/totranslate.xml +++ b/payments-ui-core/res/values/totranslate.xml @@ -7,4 +7,8 @@ Something went wrong when linking your account.\nPlease try again later. Pay with your bank account in just a few steps. Remove bank account + + + Enter address manually + No results found diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/autocomplete/PlacesClientProxy.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/autocomplete/PlacesClientProxy.kt index 683c15455ae..b922a663ffc 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/autocomplete/PlacesClientProxy.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/autocomplete/PlacesClientProxy.kt @@ -3,6 +3,7 @@ package com.stripe.android.ui.core.elements.autocomplete import android.content.Context import android.graphics.Typeface import android.text.style.StyleSpan +import androidx.annotation.RestrictTo import com.google.android.libraries.places.api.Places import com.google.android.libraries.places.api.model.AutocompleteSessionToken import com.google.android.libraries.places.api.model.TypeFilter @@ -18,7 +19,8 @@ import com.stripe.android.ui.core.elements.autocomplete.model.FindAutocompletePr import com.stripe.android.ui.core.elements.autocomplete.model.Place import kotlinx.coroutines.tasks.await -internal interface PlacesClientProxy { +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +interface PlacesClientProxy { suspend fun findAutocompletePredictions( query: String?, country: String, @@ -166,7 +168,8 @@ internal class UnsupportedPlacesClientProxy : PlacesClientProxy { } } -internal interface IsPlacesAvailable { +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +interface IsPlacesAvailable { operator fun invoke(): Boolean } diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/autocomplete/model/Place.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/autocomplete/model/Place.kt index 8d9967b91c5..4797c041d1b 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/autocomplete/model/Place.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/autocomplete/model/Place.kt @@ -1,25 +1,30 @@ package com.stripe.android.ui.core.elements.autocomplete.model import android.text.SpannableString +import androidx.annotation.RestrictTo import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -internal data class FindAutocompletePredictionsResponse( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class FindAutocompletePredictionsResponse( val autocompletePredictions: List ) -internal data class AutocompletePrediction( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class AutocompletePrediction( val primaryText: SpannableString, val secondaryText: SpannableString, val placeId: String ) -internal data class FetchPlaceResponse( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class FetchPlaceResponse( val place: Place ) @Serializable -internal data class Place( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class Place( @SerialName("address_components") val addressComponents: List? ) { enum class Type(val value: String) { @@ -44,7 +49,8 @@ internal data class Place( } @Serializable -internal data class AddressComponent( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class AddressComponent( @SerialName("short_name") val shortName: String?, @SerialName("long_name") val longName: String, @SerialName("types") val types: List diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/autocomplete/model/TransformGoogleToStripeAddress.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/autocomplete/model/TransformGoogleToStripeAddress.kt index efc4a765908..e171ca1a0d4 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/autocomplete/model/TransformGoogleToStripeAddress.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/autocomplete/model/TransformGoogleToStripeAddress.kt @@ -2,6 +2,7 @@ package com.stripe.android.ui.core.elements.autocomplete.model import android.content.Context import android.os.Build +import androidx.annotation.RestrictTo import java.util.Locale // Largely duplicated from @@ -176,7 +177,8 @@ internal fun Address.modifyStripeAddressByCountry(place: Place): Address { return newAddress } -internal fun Place.transformGoogleToStripeAddress( +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +fun Place.transformGoogleToStripeAddress( context: Context ): com.stripe.android.model.Address { var address = Address() diff --git a/paymentsheet-example/build.gradle b/paymentsheet-example/build.gradle index 49facefccdb..f7c8684d888 100644 --- a/paymentsheet-example/build.gradle +++ b/paymentsheet-example/build.gradle @@ -16,6 +16,7 @@ dependencies { implementation project(':stripecardscan') implementation project(':financial-connections') + implementation "com.google.android.libraries.places:places:$placesVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$androidxLifecycleVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$androidxLifecycleVersion" implementation "androidx.preference:preference-ktx:$androidxPreferenceVersion" diff --git a/paymentsheet/api/paymentsheet.api b/paymentsheet/api/paymentsheet.api index b08b6d21dd0..51ffa7952e2 100644 --- a/paymentsheet/api/paymentsheet.api +++ b/paymentsheet/api/paymentsheet.api @@ -266,7 +266,8 @@ public final class com/stripe/android/paymentsheet/PaymentSheet$Configuration : public fun (Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;)V public fun (Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;Z)V public fun (Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;ZLcom/stripe/android/paymentsheet/PaymentSheet$Appearance;)V - public synthetic fun (Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;ZLcom/stripe/android/paymentsheet/PaymentSheet$Appearance;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;ZLcom/stripe/android/paymentsheet/PaymentSheet$Appearance;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;ZLcom/stripe/android/paymentsheet/PaymentSheet$Appearance;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration; public final fun component3 ()Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration; @@ -274,8 +275,9 @@ public final class com/stripe/android/paymentsheet/PaymentSheet$Configuration : public final fun component5 ()Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails; public final fun component6 ()Z public final fun component7 ()Lcom/stripe/android/paymentsheet/PaymentSheet$Appearance; - public final fun copy (Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;ZLcom/stripe/android/paymentsheet/PaymentSheet$Appearance;)Lcom/stripe/android/paymentsheet/PaymentSheet$Configuration; - public static synthetic fun copy$default (Lcom/stripe/android/paymentsheet/PaymentSheet$Configuration;Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;ZLcom/stripe/android/paymentsheet/PaymentSheet$Appearance;ILjava/lang/Object;)Lcom/stripe/android/paymentsheet/PaymentSheet$Configuration; + public final fun component8 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;ZLcom/stripe/android/paymentsheet/PaymentSheet$Appearance;Ljava/lang/String;)Lcom/stripe/android/paymentsheet/PaymentSheet$Configuration; + public static synthetic fun copy$default (Lcom/stripe/android/paymentsheet/PaymentSheet$Configuration;Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;ZLcom/stripe/android/paymentsheet/PaymentSheet$Appearance;Ljava/lang/String;ILjava/lang/Object;)Lcom/stripe/android/paymentsheet/PaymentSheet$Configuration; public fun describeContents ()I public fun equals (Ljava/lang/Object;)Z public final fun getAllowsDelayedPaymentMethods ()Z @@ -798,20 +800,24 @@ public final class com/stripe/android/paymentsheet/addresselement/AddressElement public static fun injectViewModel (Lcom/stripe/android/paymentsheet/addresselement/AddressElementViewModel$Factory;Lcom/stripe/android/paymentsheet/addresselement/AddressElementViewModel;)V } -public final class com/stripe/android/paymentsheet/addresselement/AutoCompleteViewModel_Factory : dagger/internal/Factory { - public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;)V - public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/paymentsheet/addresselement/AutoCompleteViewModel_Factory; - public fun get ()Lcom/stripe/android/paymentsheet/addresselement/AutoCompleteViewModel; +public final class com/stripe/android/paymentsheet/addresselement/AutocompleteScreenKt { + public static final field TEST_TAG_ATTRIBUTION_DRAWABLE Ljava/lang/String; +} + +public final class com/stripe/android/paymentsheet/addresselement/AutocompleteViewModel_Factory : dagger/internal/Factory { + public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V + public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/paymentsheet/addresselement/AutocompleteViewModel_Factory; + public fun get ()Lcom/stripe/android/paymentsheet/addresselement/AutocompleteViewModel; public synthetic fun get ()Ljava/lang/Object; - public static fun newInstance (Lcom/stripe/android/paymentsheet/addresselement/AddressElementActivityContract$Args;Lcom/stripe/android/paymentsheet/addresselement/AddressElementNavigator;)Lcom/stripe/android/paymentsheet/addresselement/AutoCompleteViewModel; + public static fun newInstance (Lcom/stripe/android/paymentsheet/addresselement/AddressElementActivityContract$Args;Lcom/stripe/android/paymentsheet/addresselement/AddressElementNavigator;Landroid/app/Application;)Lcom/stripe/android/paymentsheet/addresselement/AutocompleteViewModel; } -public final class com/stripe/android/paymentsheet/addresselement/AutoCompleteViewModel_Factory_MembersInjector : dagger/MembersInjector { +public final class com/stripe/android/paymentsheet/addresselement/AutocompleteViewModel_Factory_MembersInjector : dagger/MembersInjector { public fun (Ljavax/inject/Provider;)V public static fun create (Ljavax/inject/Provider;)Ldagger/MembersInjector; - public fun injectMembers (Lcom/stripe/android/paymentsheet/addresselement/AutoCompleteViewModel$Factory;)V + public fun injectMembers (Lcom/stripe/android/paymentsheet/addresselement/AutocompleteViewModel$Factory;)V public synthetic fun injectMembers (Ljava/lang/Object;)V - public static fun injectSubComponentBuilderProvider (Lcom/stripe/android/paymentsheet/addresselement/AutoCompleteViewModel$Factory;Ljavax/inject/Provider;)V + public static fun injectSubComponentBuilderProvider (Lcom/stripe/android/paymentsheet/addresselement/AutocompleteViewModel$Factory;Ljavax/inject/Provider;)V } public final class com/stripe/android/paymentsheet/addresselement/ComposableSingletons$AddressElementActivityKt { @@ -821,13 +827,6 @@ public final class com/stripe/android/paymentsheet/addresselement/ComposableSing public final fun getLambda-1$paymentsheet_release ()Lkotlin/jvm/functions/Function2; } -public final class com/stripe/android/paymentsheet/addresselement/ComposableSingletons$AutoCompleteScreenKt { - public static final field INSTANCE Lcom/stripe/android/paymentsheet/addresselement/ComposableSingletons$AutoCompleteScreenKt; - public static field lambda-1 Lkotlin/jvm/functions/Function3; - public fun ()V - public final fun getLambda-1$paymentsheet_release ()Lkotlin/jvm/functions/Function3; -} - public final class com/stripe/android/paymentsheet/addresselement/ComposableSingletons$InputAddressScreenKt { public static final field INSTANCE Lcom/stripe/android/paymentsheet/addresselement/ComposableSingletons$InputAddressScreenKt; public static field lambda-1 Lkotlin/jvm/functions/Function3; diff --git a/paymentsheet/build.gradle b/paymentsheet/build.gradle index 8f94030aea4..b3454c7287f 100644 --- a/paymentsheet/build.gradle +++ b/paymentsheet/build.gradle @@ -81,6 +81,7 @@ dependencies { testImplementation "androidx.arch.core:core-testing:$androidxArchCoreVersion" testImplementation "androidx.fragment:fragment-testing:$androidxFragmentVersion" + androidTestImplementation "com.google.android.libraries.places:places:$placesVersion" androidTestImplementation "androidx.test.ext:junit:$androidTestJunitVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" androidTestImplementation ("androidx.test.espresso:espresso-contrib:$espressoVersion") { diff --git a/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreenTest.kt b/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreenTest.kt new file mode 100644 index 00000000000..9e9196f7706 --- /dev/null +++ b/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreenTest.kt @@ -0,0 +1,174 @@ +package com.stripe.android.paymentsheet.addresselement + +import android.app.Application +import android.text.SpannableString +import androidx.activity.ComponentActivity +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performTextInput +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.stripe.android.model.parsers.PaymentIntentJsonParser +import com.stripe.android.paymentsheet.PaymentSheet +import com.stripe.android.ui.core.DefaultPaymentsTheme +import com.stripe.android.ui.core.elements.autocomplete.PlacesClientProxy +import com.stripe.android.ui.core.elements.autocomplete.model.AddressComponent +import com.stripe.android.ui.core.elements.autocomplete.model.AutocompletePrediction +import com.stripe.android.ui.core.elements.autocomplete.model.FetchPlaceResponse +import com.stripe.android.ui.core.elements.autocomplete.model.FindAutocompletePredictionsResponse +import com.stripe.android.ui.core.elements.autocomplete.model.Place +import org.json.JSONObject +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalAnimationApi +@RunWith(AndroidJUnit4::class) +class AutocompleteScreenTest { + @get:Rule + val composeTestRule = createAndroidComposeRule() + + private val paymentIntent = requireNotNull( + PaymentIntentJsonParser().parse( + JSONObject( + """ + { + "id": "pi_1IRg6VCRMbs6F", + "object": "payment_intent", + "amount": 1099, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "client_secret": "pi_1IRg6VCRMbs6F_secret_7oH5g4v8GaCrHfsGYS6kiSnwF", + "confirmation_method": "automatic", + "created": 1614960135, + "currency": "usd", + "description": "Example PaymentIntent", + "last_payment_error": null, + "livemode": false, + "next_action": null, + "payment_method": "pm_1IJs3ZCRMbs", + "payment_method_types": ["card"], + "receipt_email": null, + "setup_future_usage": null, + "shipping": null, + "source": null, + "status": "succeeded" + } + """.trimIndent() + ) + ) + ) + + private val args = AddressElementActivityContract.Args( + paymentIntent, + PaymentSheet.Configuration( + merchantDisplayName = "Merchant, Inc.", + customer = PaymentSheet.CustomerConfiguration( + "customer_id", + "ephemeral_key" + ) + ), + AddressElementActivityContract.Args.InjectionParams( + "injectorKey", + setOf("Product Usage"), + true + ) + ) + private val application = ApplicationProvider.getApplicationContext() + + @Test + fun ensure_elements_exist() { + setContent() + onAddressOptionsAppBar().assertExists() + onQueryField().assertExists() + onEnterAddressManually().assertExists() + } + + @Test + fun no_results_found_should_appear() { + setContent() + onQueryField().performTextInput("Some text") + composeTestRule.waitUntil { + composeTestRule + .onAllNodesWithText("No results found") + .fetchSemanticsNodes().size == 1 + } + composeTestRule.waitUntil { + composeTestRule + .onAllNodesWithTag(TEST_TAG_ATTRIBUTION_DRAWABLE) + .fetchSemanticsNodes().size == 1 + } + } + + @Test + fun results_found_should_appear() { + setContent( + mockClient = FakeGooglePlacesClient( + predictions = listOf( + AutocompletePrediction( + SpannableString("primaryText"), + SpannableString("secondaryText"), + "placeId" + ) + ) + ) + ) + onQueryField().performTextInput("Some text") + composeTestRule.waitUntil { + composeTestRule + .onAllNodesWithText("primaryText") + .fetchSemanticsNodes().size == 1 + } + } + + private fun setContent( + mockClient: FakeGooglePlacesClient = FakeGooglePlacesClient() + ) = + composeTestRule.setContent { + DefaultPaymentsTheme { + AutocompleteTextField( + viewModel = AutocompleteViewModel( + args, + AddressElementNavigator(), + application + ).apply { + initialize { + mockClient + } + } + ) + } + } + + private fun onAddressOptionsAppBar() = composeTestRule.onNodeWithContentDescription("Back") + private fun onQueryField() = composeTestRule.onNodeWithText("Address") + private fun onEnterAddressManually() = composeTestRule.onNodeWithText("Enter address manually") + + private class FakeGooglePlacesClient( + private val predictions: List = listOf(), + private val addressComponents: List = listOf() + ) : PlacesClientProxy { + override suspend fun findAutocompletePredictions( + query: String?, + country: String, + limit: Int + ): Result { + return Result.success( + FindAutocompletePredictionsResponse(predictions) + ) + } + + override suspend fun fetchPlace(placeId: String): Result { + return Result.success( + FetchPlaceResponse( + Place(addressComponents) + ) + ) + } + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheet.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheet.kt index ba8ec99d2f0..3cf0e5f75e0 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheet.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheet.kt @@ -6,6 +6,7 @@ import android.os.Parcelable import androidx.activity.ComponentActivity import androidx.annotation.ColorInt import androidx.annotation.FontRes +import androidx.annotation.RestrictTo import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.fragment.app.Fragment @@ -141,7 +142,13 @@ class PaymentSheet internal constructor( /** * Describes the appearance of Payment Sheet. */ - val appearance: Appearance = Appearance() + val appearance: Appearance = Appearance(), + + /** + * Google Places API key used for autocomplete addresses. + */ + @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + val googlePlacesApiKey: String? = null ) : Parcelable { /** * [Configuration] builder for cleaner object creation from Java. diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivity.kt index 0417df0ae1c..79a98a0e8a6 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivity.kt @@ -8,6 +8,7 @@ import androidx.activity.viewModels import androidx.annotation.VisibleForTesting import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetValue @@ -17,7 +18,9 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier +import androidx.core.view.WindowCompat import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import com.google.accompanist.navigation.animation.composable import com.google.accompanist.navigation.animation.AnimatedNavHost @@ -47,6 +50,8 @@ internal class AddressElementActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) + // set a default result in case the user closes the sheet manually setResult() @@ -92,13 +97,14 @@ internal class AddressElementActivity : ComponentActivity() { InputAddressScreen(viewModel.injector) } composable(AddressElementScreen.Autocomplete.route) { - AutoCompleteScreen(viewModel.injector) + AutocompleteScreen(viewModel.injector) } } } } }, - content = {} + content = {}, + modifier = Modifier.navigationBarsPadding() ) } } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementNavigator.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementNavigator.kt index dfdaebde9dc..071fc7a68bb 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementNavigator.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementNavigator.kt @@ -18,7 +18,7 @@ internal class AddressElementNavigator @Inject constructor() { target: AddressElementScreen ) = navigationController?.navigate(target.route) - fun setResult(key: String, value: Any) = + fun setResult(key: String, value: Any?) = navigationController?.previousBackStackEntry?.savedStateHandle?.set(key, value) fun getResultFlow(key: String) = diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementViewModel.kt index 70a4116ee3b..760f1dfdc4a 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementViewModel.kt @@ -85,7 +85,7 @@ internal class AddressElementViewModel @Inject internal constructor( when (injectable) { is Factory -> viewModelComponent.inject(injectable) is InputAddressViewModel.Factory -> viewModelComponent.inject(injectable) - is AutoCompleteViewModel.Factory -> viewModelComponent.inject(injectable) + is AutocompleteViewModel.Factory -> viewModelComponent.inject(injectable) else -> { throw IllegalArgumentException( "invalid Injectable $injectable requested in $this" diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutoCompleteScreen.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutoCompleteScreen.kt deleted file mode 100644 index 16786f83b18..00000000000 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutoCompleteScreen.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.stripe.android.paymentsheet.addresselement - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Button -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import com.stripe.android.paymentsheet.ui.AddressOptionsAppBar -import com.stripe.android.ui.core.injection.NonFallbackInjector - -@Composable -internal fun AutoCompleteScreen( - injector: NonFallbackInjector -) { - val viewModel: AutoCompleteViewModel = viewModel( - factory = AutoCompleteViewModel.Factory( - injector - ) - ) - - Column { - AddressOptionsAppBar( - isRootScreen = false, - onButtonClick = { - viewModel.navigator.onBack() - } - ) - Column(Modifier.padding(horizontal = 20.dp)) { - Text("AutoComplete Screen") - Button( - onClick = { - // TODO implement this screen - val dummyAddress = ShippingAddress(name = "Skyler") - viewModel.navigator.setResult(ShippingAddress.KEY, dummyAddress) - viewModel.navigator.onBack() - }, - content = { Text(text = "Input Autocompleted Address") } - ) - } - } -} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutoCompleteViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutoCompleteViewModel.kt deleted file mode 100644 index d7627e17351..00000000000 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutoCompleteViewModel.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.stripe.android.paymentsheet.addresselement - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.stripe.android.ui.core.injection.NonFallbackInjectable -import com.stripe.android.paymentsheet.injection.AutoCompleteViewModelSubcomponent -import com.stripe.android.ui.core.injection.NonFallbackInjector -import javax.inject.Inject -import javax.inject.Provider - -internal class AutoCompleteViewModel @Inject constructor( - val args: AddressElementActivityContract.Args, - val navigator: AddressElementNavigator -) : ViewModel() { - - internal class Factory( - private val injector: NonFallbackInjector - ) : ViewModelProvider.Factory, NonFallbackInjectable { - - @Inject - lateinit var subComponentBuilderProvider: - Provider - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - injector.inject(this) - return subComponentBuilderProvider.get() - .build().autoCompleteViewModel as T - } - } -} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreen.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreen.kt new file mode 100644 index 00000000000..4853f4cd623 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreen.kt @@ -0,0 +1,219 @@ +package com.stripe.android.paymentsheet.addresselement + +import android.app.Application +import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Divider +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.stripe.android.paymentsheet.R +import com.stripe.android.paymentsheet.ui.AddressOptionsAppBar +import com.stripe.android.ui.core.elements.TextFieldSection +import com.stripe.android.ui.core.elements.annotatedStringResource +import com.stripe.android.ui.core.elements.autocomplete.PlacesClientProxy +import com.stripe.android.ui.core.injection.NonFallbackInjector +import com.stripe.android.ui.core.paymentsColors + +@VisibleForTesting +const val TEST_TAG_ATTRIBUTION_DRAWABLE = "AutocompleteAttributionDrawable" + +@Composable +internal fun AutocompleteScreen(injector: NonFallbackInjector) { + val application = LocalContext.current.applicationContext as Application + val viewModel: AutocompleteViewModel = + viewModel( + factory = AutocompleteViewModel.Factory( + injector + ) { application } + ).also { + it.initialize() + } + + AutocompleteTextField(viewModel = viewModel) +} + +@Composable +internal fun AutocompleteTextField(viewModel: AutocompleteViewModel) { + val predictions by viewModel.predictions.collectAsState() + val loading by viewModel.loading.collectAsState(initial = false) + val query = viewModel.textFieldController.fieldValue.collectAsState(initial = "") + val attributionDrawable = + PlacesClientProxy.getPlacesPoweredByGoogleDrawable(isSystemInDarkTheme()) + + Scaffold( + bottomBar = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .background( + color = colorResource( + id = R.color.stripe_paymentsheet_shipping_address_background + ) + ) + .fillMaxWidth() + .imePadding() + .navigationBarsPadding() + .padding(vertical = 8.dp) + ) { + ClickableText( + text = buildAnnotatedString { + append( + stringResource( + id = R.string.stripe_paymentsheet_enter_address_manually + ) + ) + }, + style = TextStyle.Default.copy( + color = MaterialTheme.paymentsColors.materialColors.primary + ) + ) { + viewModel.onEnterAddressManually() + } + } + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .systemBarsPadding() + .padding(paddingValues) + ) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + AddressOptionsAppBar(false) { + viewModel.setResultAndGoBack() + } + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + TextFieldSection( + textFieldController = viewModel.textFieldController, + imeAction = ImeAction.Done, + enabled = true, + modifier = Modifier.fillMaxWidth() + ) + } + if (loading) { + Row( + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth() + ) { + CircularProgressIndicator() + } + } else if (query.value.isNotBlank()) { + predictions?.let { + if (it.isNotEmpty()) { + Divider( + modifier = Modifier.padding(vertical = 8.dp) + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + it.forEach { prediction -> + val primaryText = prediction.primaryText + val secondaryText = prediction.secondaryText + Column( + modifier = Modifier + .fillMaxWidth() + .clickable { + viewModel.selectPrediction(prediction) + } + ) { + val regex = query.value + .replace(" ", "|") + .toRegex(RegexOption.IGNORE_CASE) + val matches = regex.findAll(primaryText).toList() + val values = matches.map { + it.value + }.filter { it.isNotBlank() } + var text = primaryText.toString() + values.forEach { + text = text.replace(it, "$it") + } + Text( + text = annotatedStringResource(text = text), + color = MaterialTheme.paymentsColors.onComponent, + style = MaterialTheme.typography.body1 + ) + Text( + text = secondaryText.toString(), + color = MaterialTheme.paymentsColors.onComponent, + style = MaterialTheme.typography.body1 + ) + } + Divider( + modifier = Modifier.padding(vertical = 8.dp) + ) + } + } + } else { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + Text( + text = stringResource( + R.string.stripe_paymentsheet_autocomplete_no_results_found + ), + color = MaterialTheme.paymentsColors.onComponent, + style = MaterialTheme.typography.body1 + ) + } + } + attributionDrawable?.let { drawable -> + Image( + painter = painterResource( + id = drawable + ), + contentDescription = null, + modifier = Modifier + .padding(top = 8.dp) + .padding(horizontal = 16.dp) + .testTag(TEST_TAG_ATTRIBUTION_DRAWABLE) + ) + } + } + } + } + } + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteViewModel.kt new file mode 100644 index 00000000000..46f64d24526 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteViewModel.kt @@ -0,0 +1,220 @@ +package com.stripe.android.paymentsheet.addresselement + +import android.app.Application +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.stripe.android.model.Address +import com.stripe.android.paymentsheet.R +import com.stripe.android.ui.core.injection.NonFallbackInjectable +import com.stripe.android.paymentsheet.injection.AutoCompleteViewModelSubcomponent +import com.stripe.android.ui.core.elements.SimpleTextFieldConfig +import com.stripe.android.ui.core.elements.SimpleTextFieldController +import com.stripe.android.ui.core.elements.TextFieldIcon +import com.stripe.android.ui.core.elements.autocomplete.PlacesClientProxy +import com.stripe.android.ui.core.elements.autocomplete.model.AutocompletePrediction +import com.stripe.android.ui.core.elements.autocomplete.model.transformGoogleToStripeAddress +import com.stripe.android.ui.core.injection.NonFallbackInjector +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import javax.inject.Inject +import javax.inject.Provider + +internal class AutocompleteViewModel @Inject constructor( + val args: AddressElementActivityContract.Args, + val navigator: AddressElementNavigator, + application: Application +) : AndroidViewModel(application) { + private var client: PlacesClientProxy? = null + + private val _predictions = MutableStateFlow?>(null) + val predictions: StateFlow?> + get() = _predictions + + private val _loading = MutableStateFlow(false) + val loading: StateFlow + get() = _loading + + @VisibleForTesting + val addressResult = MutableStateFlow?>(null) + + val textFieldController = SimpleTextFieldController( + SimpleTextFieldConfig( + label = R.string.address_label_address, + trailingIcon = MutableStateFlow( + TextFieldIcon.Trailing( + idRes = R.drawable.stripe_ic_clear, + isTintable = true, + onClick = { clearQuery() } + ) + ) + ) + ) + + private val queryFlow = textFieldController.fieldValue + .map { it } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "") + + private val debouncer = Debouncer() + + fun initialize( + clientProvider: () -> PlacesClientProxy? = { + args.config?.googlePlacesApiKey?.let { + PlacesClientProxy.create(getApplication(), it) + } + } + ) { + client = clientProvider() + debouncer.startWatching( + coroutineScope = viewModelScope, + queryFlow = queryFlow, + onValidQuery = { + viewModelScope.launch { + client?.findAutocompletePredictions( + query = it, + country = "US", + limit = MAX_DISPLAYED_RESULTS + )?.fold( + onSuccess = { + _loading.value = false + _predictions.value = it.autocompletePredictions + }, + onFailure = { + _loading.value = false + addressResult.value = Result.failure(it) + } + ) + } + } + ) + } + + fun selectPrediction(prediction: AutocompletePrediction) { + viewModelScope.launch { + _loading.value = true + client?.fetchPlace( + placeId = prediction.placeId + )?.fold( + onSuccess = { + _loading.value = false + addressResult.value = Result.success( + it.place.transformGoogleToStripeAddress(getApplication()) + ) + setResultAndGoBack() + }, + onFailure = { + _loading.value = false + addressResult.value = Result.failure(it) + setResultAndGoBack() + } + ) + } + } + + fun onEnterAddressManually() { + setResultAndGoBack() + } + + fun setResultAndGoBack() { + addressResult.value?.fold( + onSuccess = { + navigator.setResult(ShippingAddress.KEY, it) + }, + onFailure = { + navigator.setResult(ShippingAddress.KEY, null) + } + ) + navigator.onBack() + } + + private fun clearQuery() { + textFieldController.onRawValueChange("") + } + + internal class Debouncer { + private var searchJob: Job? = null + + fun startWatching( + coroutineScope: CoroutineScope, + queryFlow: StateFlow, + onValidQuery: (String) -> Unit + ) { + coroutineScope.launch { + queryFlow.collect { query -> + query?.let { + searchJob?.cancel() + if (query.length > MIN_CHARS_AUTOCOMPLETE) { + searchJob = launch { + delay(SEARCH_DEBOUNCE_MS) + if (isActive) { + onValidQuery(it) + } + } + } + } + } + } + } + } + + internal class Factory( + private val injector: NonFallbackInjector, + private val applicationSupplier: () -> Application + ) : ViewModelProvider.Factory, NonFallbackInjectable { + + @Inject + lateinit var subComponentBuilderProvider: + Provider + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + injector.inject(this) + return subComponentBuilderProvider.get() + .application(applicationSupplier()) + .build().autoCompleteViewModel as T + } + } + + companion object { + const val SEARCH_DEBOUNCE_MS = 1000L + const val MAX_DISPLAYED_RESULTS = 4 + const val MIN_CHARS_AUTOCOMPLETE = 3 + val autocompleteSupportedCountries = setOf( + "AU", + "BE", + "BR", + "CA", + "CH", + "DE", + "ES", + "FR", + "GB", + "IE", + "IN", + "IT", + "JP", + "MX", + "MY", + "NO", + "NL", + "PH", + "PL", + "RU", + "SE", + "SG", + "TR", + "US", + "ZA" + ) + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/InputAddressScreen.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/InputAddressScreen.kt index bac78581a5b..e224013a9c4 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/InputAddressScreen.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/InputAddressScreen.kt @@ -34,9 +34,7 @@ internal fun InputAddressScreen( Column(Modifier.padding(horizontal = 20.dp)) { Text("BaseAddress Screen") collectedAddress?.let { address -> - address.name?.let { - Text(it) - } + Text(address.toString()) } if (collectedAddress == null) { Button( diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/InputAddressViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/InputAddressViewModel.kt index 03f1d2fcd1e..38c80668659 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/InputAddressViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/InputAddressViewModel.kt @@ -3,6 +3,7 @@ package com.stripe.android.paymentsheet.addresselement import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import com.stripe.android.model.Address import com.stripe.android.ui.core.injection.NonFallbackInjectable import com.stripe.android.paymentsheet.injection.InputAddressViewModelSubcomponent import com.stripe.android.ui.core.injection.FormControllerSubcomponent @@ -24,8 +25,21 @@ internal class InputAddressViewModel @Inject constructor( init { viewModelScope.launch { - navigator.getResultFlow(ShippingAddress.KEY)?.collect { - _collectedAddress.value = it + navigator.getResultFlow(ShippingAddress.KEY)?.collect { + val oldShippingAddress = _collectedAddress.value + _collectedAddress.emit( + ShippingAddress( + name = oldShippingAddress?.name, + company = oldShippingAddress?.company, + phoneNumber = oldShippingAddress?.phoneNumber, + city = it?.city, + country = it?.country, + line1 = it?.line1, + line2 = it?.line2, + state = it?.state, + postalCode = it?.postalCode + ) + ) } } } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/injection/AddressElementViewModelFactoryComponent.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/injection/AddressElementViewModelFactoryComponent.kt index b918a8673c5..11dd23c8ddd 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/injection/AddressElementViewModelFactoryComponent.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/injection/AddressElementViewModelFactoryComponent.kt @@ -8,7 +8,7 @@ import com.stripe.android.payments.core.injection.PRODUCT_USAGE import com.stripe.android.payments.core.injection.StripeRepositoryModule import com.stripe.android.paymentsheet.addresselement.AddressElementActivityContract import com.stripe.android.paymentsheet.addresselement.AddressElementViewModel -import com.stripe.android.paymentsheet.addresselement.AutoCompleteViewModel +import com.stripe.android.paymentsheet.addresselement.AutocompleteViewModel import com.stripe.android.paymentsheet.addresselement.InputAddressViewModel import com.stripe.android.ui.core.forms.resources.injection.ResourceRepositoryModule import com.stripe.android.ui.core.injection.FormControllerModule @@ -32,7 +32,7 @@ import javax.inject.Singleton internal interface AddressElementViewModelFactoryComponent { fun inject(factory: AddressElementViewModel.Factory) fun inject(factory: InputAddressViewModel.Factory) - fun inject(factory: AutoCompleteViewModel.Factory) + fun inject(factory: AutocompleteViewModel.Factory) @Component.Builder interface Builder { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/injection/AutoCompleteViewModelSubcomponent.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/injection/AutoCompleteViewModelSubcomponent.kt index ee8f7fd44c0..82d23e755bf 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/injection/AutoCompleteViewModelSubcomponent.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/injection/AutoCompleteViewModelSubcomponent.kt @@ -1,14 +1,18 @@ package com.stripe.android.paymentsheet.injection -import com.stripe.android.paymentsheet.addresselement.AutoCompleteViewModel +import android.app.Application +import com.stripe.android.paymentsheet.addresselement.AutocompleteViewModel +import dagger.BindsInstance import dagger.Subcomponent @Subcomponent internal interface AutoCompleteViewModelSubcomponent { - val autoCompleteViewModel: AutoCompleteViewModel + val autoCompleteViewModel: AutocompleteViewModel @Subcomponent.Builder interface Builder { + @BindsInstance + fun application(application: Application): Builder fun build(): AutoCompleteViewModelSubcomponent } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AutocompleteViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AutocompleteViewModelTest.kt new file mode 100644 index 00000000000..0fff29ecae9 --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AutocompleteViewModelTest.kt @@ -0,0 +1,198 @@ +package com.stripe.android.paymentsheet.addresselement + +import android.app.Application +import android.text.SpannableString +import androidx.lifecycle.viewModelScope +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import com.stripe.android.model.Address +import com.stripe.android.ui.core.elements.TextFieldIcon +import com.stripe.android.ui.core.elements.autocomplete.PlacesClientProxy +import com.stripe.android.ui.core.elements.autocomplete.model.AutocompletePrediction +import com.stripe.android.ui.core.elements.autocomplete.model.FetchPlaceResponse +import com.stripe.android.ui.core.elements.autocomplete.model.FindAutocompletePredictionsResponse +import com.stripe.android.ui.core.elements.autocomplete.model.Place +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.eq +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 kotlin.test.AfterTest +import kotlin.test.BeforeTest + +@ExperimentalCoroutinesApi +@RunWith(RobolectricTestRunner::class) +class AutocompleteViewModelTest { + private val args = mock() + private val navigator = mock() + private val application = ApplicationProvider.getApplicationContext() + private val mockClient = mock() + + private fun createViewModel() = + AutocompleteViewModel( + args, + navigator, + application + ).apply { + initialize { + mockClient + } + } + + @BeforeTest + fun setUp() { + Dispatchers.setMain(UnconfinedTestDispatcher()) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `selectPrediction emits successful result`() = runTest(UnconfinedTestDispatcher()) { + val viewModel = createViewModel() + val fetchPlaceResponse = Result.success( + FetchPlaceResponse( + Place( + listOf() + ) + ) + ) + val expectedResult = Result.success( + Address( + city = null, + country = null, + line1 = "", + line2 = null, + postalCode = null, + state = null + ) + ) + whenever(mockClient.fetchPlace(any())).thenReturn(fetchPlaceResponse) + + viewModel.selectPrediction( + AutocompletePrediction( + SpannableString("primaryText"), + SpannableString("secondaryText"), + "placeId" + ) + ) + + assertThat(viewModel.loading.value).isEqualTo(false) + assertThat(viewModel.addressResult.value) + .isEqualTo(expectedResult) + + verify(navigator).setResult(anyOrNull(), eq(expectedResult.getOrNull())) + verify(navigator).onBack() + } + + @Test + fun `selectPrediction emits failure result`() = runTest(UnconfinedTestDispatcher()) { + val viewModel = createViewModel() + val exception = Exception("fake exception") + val result = Result.failure(exception) + + mockClient.stub { + onBlocking { fetchPlace(any()) }.thenReturn(result) + } + + viewModel.selectPrediction( + AutocompletePrediction( + SpannableString("primaryText"), + SpannableString("secondaryText"), + "placeId" + ) + ) + + assertThat(viewModel.loading.value).isEqualTo(false) + assertThat(viewModel.addressResult.value) + .isEqualTo(Result.failure(exception)) + + verify(navigator).setResult(anyOrNull(), eq(null)) + verify(navigator).onBack() + } + + @Test + fun `onEnterAddressManually sets the current address and navigates back`() = runTest(UnconfinedTestDispatcher()) { + val viewModel = createViewModel() + val expectedResult = Result.success( + Address( + city = "city", + country = null, + line1 = "", + line2 = null, + postalCode = null, + state = null + ) + ) + + viewModel.addressResult.value = expectedResult + viewModel.onEnterAddressManually() + + verify(navigator).setResult(anyOrNull(), eq(expectedResult.getOrNull())) + verify(navigator).onBack() + } + + @Test + fun `when user presses clear text field is cleared`() = runTest { + val viewModel = createViewModel() + val trailingIcon = viewModel.textFieldController.trailingIcon.stateIn(viewModel.viewModelScope) + + (trailingIcon.value as? TextFieldIcon.Trailing)?.onClick?.invoke() + + assertThat(viewModel.textFieldController.rawFieldValue.stateIn(viewModel.viewModelScope).value) + .isEqualTo("") + } + + @Test + fun `when query is valid then search is triggered with delay`() = runTest(UnconfinedTestDispatcher()) { + val viewModel = createViewModel() + + viewModel.textFieldController.onRawValueChange("Some valid query") + + whenever(mockClient.findAutocompletePredictions(any(), any(), any())).thenReturn( + Result.success( + FindAutocompletePredictionsResponse( + listOf( + AutocompletePrediction( + SpannableString("primaryText"), + SpannableString("secondaryText"), + "placeId" + ) + ) + ) + ) + ) + + // Advance past search debounce delay + advanceTimeBy(AutocompleteViewModel.SEARCH_DEBOUNCE_MS + 1) + + assertThat(viewModel.predictions.value?.size).isEqualTo(1) + } + + @Test + fun `when query is invalid then search is not triggered`() = runTest(UnconfinedTestDispatcher()) { + val viewModel = createViewModel() + + viewModel.textFieldController.onRawValueChange("a") + + // Advance past search debounce delay + advanceTimeBy(AutocompleteViewModel.SEARCH_DEBOUNCE_MS + 1) + + assertThat(viewModel.predictions.value?.size).isEqualTo(null) + } +} diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/InputAddressViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/InputAddressViewModelTest.kt index 120069ed59f..c24f894a18d 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/InputAddressViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/InputAddressViewModelTest.kt @@ -1,6 +1,7 @@ package com.stripe.android.paymentsheet.addresselement import com.google.common.truth.Truth.assertThat +import com.stripe.android.model.Address import com.stripe.android.ui.core.injection.FormControllerSubcomponent import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -29,20 +30,20 @@ class InputAddressViewModelTest { @Test fun `no autocomplete address passed has an empty address to start`() = runTest { - val flow = MutableStateFlow(null) - whenever(navigator.getResultFlow(any())).thenReturn(flow) + val flow = MutableStateFlow(null) + whenever(navigator.getResultFlow(any())).thenReturn(flow) val viewModel = createViewModel() - assertThat(viewModel.collectedAddress.value).isEqualTo(null) + assertThat(viewModel.collectedAddress.value).isEqualTo(ShippingAddress()) } @Test fun `autocomplete address passed is collected to start`() = runTest { - val expectedAddress = ShippingAddress(name = "skyler", company = "stripe") - val flow = MutableStateFlow(expectedAddress) - whenever(navigator.getResultFlow(any())).thenReturn(flow) + val expectedAddress = Address(city = "Seattle") + val flow = MutableStateFlow(expectedAddress) + whenever(navigator.getResultFlow(any())).thenReturn(flow) val viewModel = createViewModel() - assertThat(viewModel.collectedAddress.value).isEqualTo(expectedAddress) + assertThat(viewModel.collectedAddress.value).isEqualTo(ShippingAddress(city = "Seattle")) } } From e6c211a9d2dfa6ea970d840394f6f73fb4e69a0a Mon Sep 17 00:00:00 2001 From: jameswoo-stripe <99316447+jameswoo-stripe@users.noreply.github.com> Date: Wed, 6 Jul 2022 11:37:56 -0700 Subject: [PATCH 2/2] Pairing with skyler --- paymentsheet/api/paymentsheet.api | 8 +++---- .../addresselement/AutocompleteScreenTest.kt | 2 +- .../android/paymentsheet/PaymentSheet.kt | 9 +------- .../addresselement/AutocompleteScreen.kt | 9 ++++---- .../addresselement/AutocompleteViewModel.kt | 21 +++++++++++++------ .../addresselement/InputAddressViewModel.kt | 9 ++++---- .../AutocompleteViewModelTest.kt | 5 ++--- .../InputAddressViewModelTest.kt | 13 ++++++------ 8 files changed, 36 insertions(+), 40 deletions(-) diff --git a/paymentsheet/api/paymentsheet.api b/paymentsheet/api/paymentsheet.api index 51ffa7952e2..072dddb49a3 100644 --- a/paymentsheet/api/paymentsheet.api +++ b/paymentsheet/api/paymentsheet.api @@ -266,8 +266,7 @@ public final class com/stripe/android/paymentsheet/PaymentSheet$Configuration : public fun (Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;)V public fun (Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;Z)V public fun (Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;ZLcom/stripe/android/paymentsheet/PaymentSheet$Appearance;)V - public fun (Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;ZLcom/stripe/android/paymentsheet/PaymentSheet$Appearance;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;ZLcom/stripe/android/paymentsheet/PaymentSheet$Appearance;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;ZLcom/stripe/android/paymentsheet/PaymentSheet$Appearance;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration; public final fun component3 ()Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration; @@ -275,9 +274,8 @@ public final class com/stripe/android/paymentsheet/PaymentSheet$Configuration : public final fun component5 ()Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails; public final fun component6 ()Z public final fun component7 ()Lcom/stripe/android/paymentsheet/PaymentSheet$Appearance; - public final fun component8 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;ZLcom/stripe/android/paymentsheet/PaymentSheet$Appearance;Ljava/lang/String;)Lcom/stripe/android/paymentsheet/PaymentSheet$Configuration; - public static synthetic fun copy$default (Lcom/stripe/android/paymentsheet/PaymentSheet$Configuration;Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;ZLcom/stripe/android/paymentsheet/PaymentSheet$Appearance;Ljava/lang/String;ILjava/lang/Object;)Lcom/stripe/android/paymentsheet/PaymentSheet$Configuration; + public final fun copy (Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;ZLcom/stripe/android/paymentsheet/PaymentSheet$Appearance;)Lcom/stripe/android/paymentsheet/PaymentSheet$Configuration; + public static synthetic fun copy$default (Lcom/stripe/android/paymentsheet/PaymentSheet$Configuration;Ljava/lang/String;Lcom/stripe/android/paymentsheet/PaymentSheet$CustomerConfiguration;Lcom/stripe/android/paymentsheet/PaymentSheet$GooglePayConfiguration;Landroid/content/res/ColorStateList;Lcom/stripe/android/paymentsheet/PaymentSheet$BillingDetails;ZLcom/stripe/android/paymentsheet/PaymentSheet$Appearance;ILjava/lang/Object;)Lcom/stripe/android/paymentsheet/PaymentSheet$Configuration; public fun describeContents ()I public fun equals (Ljava/lang/Object;)Z public final fun getAllowsDelayedPaymentMethods ()Z diff --git a/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreenTest.kt b/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreenTest.kt index 9e9196f7706..98ca258ef08 100644 --- a/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreenTest.kt +++ b/paymentsheet/src/androidTest/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreenTest.kt @@ -131,7 +131,7 @@ class AutocompleteScreenTest { ) = composeTestRule.setContent { DefaultPaymentsTheme { - AutocompleteTextField( + AutocompleteScreenUI( viewModel = AutocompleteViewModel( args, AddressElementNavigator(), diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheet.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheet.kt index 3cf0e5f75e0..ba8ec99d2f0 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheet.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheet.kt @@ -6,7 +6,6 @@ import android.os.Parcelable import androidx.activity.ComponentActivity import androidx.annotation.ColorInt import androidx.annotation.FontRes -import androidx.annotation.RestrictTo import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.fragment.app.Fragment @@ -142,13 +141,7 @@ class PaymentSheet internal constructor( /** * Describes the appearance of Payment Sheet. */ - val appearance: Appearance = Appearance(), - - /** - * Google Places API key used for autocomplete addresses. - */ - @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - val googlePlacesApiKey: String? = null + val appearance: Appearance = Appearance() ) : Parcelable { /** * [Configuration] builder for cleaner object creation from Java. diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreen.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreen.kt index 4853f4cd623..5a6480fc691 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreen.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreen.kt @@ -32,7 +32,6 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp @@ -60,11 +59,11 @@ internal fun AutocompleteScreen(injector: NonFallbackInjector) { it.initialize() } - AutocompleteTextField(viewModel = viewModel) + AutocompleteScreenUI(viewModel = viewModel) } @Composable -internal fun AutocompleteTextField(viewModel: AutocompleteViewModel) { +internal fun AutocompleteScreenUI(viewModel: AutocompleteViewModel) { val predictions by viewModel.predictions.collectAsState() val loading by viewModel.loading.collectAsState(initial = false) val query = viewModel.textFieldController.fieldValue.collectAsState(initial = "") @@ -95,8 +94,8 @@ internal fun AutocompleteTextField(viewModel: AutocompleteViewModel) { ) ) }, - style = TextStyle.Default.copy( - color = MaterialTheme.paymentsColors.materialColors.primary + style = MaterialTheme.typography.body1.copy( + color = MaterialTheme.colors.primary ) ) { viewModel.onEnterAddressManually() diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteViewModel.kt index 46f64d24526..7c5b71b807c 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteViewModel.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.stripe.android.model.Address import com.stripe.android.paymentsheet.R import com.stripe.android.ui.core.injection.NonFallbackInjectable import com.stripe.android.paymentsheet.injection.AutoCompleteViewModelSubcomponent @@ -46,7 +45,7 @@ internal class AutocompleteViewModel @Inject constructor( get() = _loading @VisibleForTesting - val addressResult = MutableStateFlow?>(null) + val addressResult = MutableStateFlow?>(null) val textFieldController = SimpleTextFieldController( SimpleTextFieldConfig( @@ -69,9 +68,11 @@ internal class AutocompleteViewModel @Inject constructor( fun initialize( clientProvider: () -> PlacesClientProxy? = { - args.config?.googlePlacesApiKey?.let { - PlacesClientProxy.create(getApplication(), it) - } + // TODO: Update the PaymentSheet Configuration to include api key +// args.config?.googlePlacesApiKey?.let { +// PlacesClientProxy.create(getApplication(), it) +// } + PlacesClientProxy.create(getApplication(), "") } ) { client = clientProvider() @@ -107,8 +108,16 @@ internal class AutocompleteViewModel @Inject constructor( )?.fold( onSuccess = { _loading.value = false + val address = it.place.transformGoogleToStripeAddress(getApplication()) addressResult.value = Result.success( - it.place.transformGoogleToStripeAddress(getApplication()) + ShippingAddress( + city = address.city, + country = address.country, + line1 = address.line1, + line2 = address.line2, + postalCode = address.postalCode, + state = address.state + ) ) setResultAndGoBack() }, diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/InputAddressViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/InputAddressViewModel.kt index 38c80668659..a38ca2d376c 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/InputAddressViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/InputAddressViewModel.kt @@ -3,7 +3,6 @@ package com.stripe.android.paymentsheet.addresselement import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.stripe.android.model.Address import com.stripe.android.ui.core.injection.NonFallbackInjectable import com.stripe.android.paymentsheet.injection.InputAddressViewModelSubcomponent import com.stripe.android.ui.core.injection.FormControllerSubcomponent @@ -25,13 +24,13 @@ internal class InputAddressViewModel @Inject constructor( init { viewModelScope.launch { - navigator.getResultFlow(ShippingAddress.KEY)?.collect { + navigator.getResultFlow(ShippingAddress.KEY)?.collect { val oldShippingAddress = _collectedAddress.value _collectedAddress.emit( ShippingAddress( - name = oldShippingAddress?.name, - company = oldShippingAddress?.company, - phoneNumber = oldShippingAddress?.phoneNumber, + name = oldShippingAddress?.name ?: it?.name, + company = oldShippingAddress?.company ?: it?.company, + phoneNumber = oldShippingAddress?.phoneNumber ?: it?.phoneNumber, city = it?.city, country = it?.country, line1 = it?.line1, diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AutocompleteViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AutocompleteViewModelTest.kt index 0fff29ecae9..cf6ad5620b9 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AutocompleteViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AutocompleteViewModelTest.kt @@ -5,7 +5,6 @@ import android.text.SpannableString import androidx.lifecycle.viewModelScope import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat -import com.stripe.android.model.Address import com.stripe.android.ui.core.elements.TextFieldIcon import com.stripe.android.ui.core.elements.autocomplete.PlacesClientProxy import com.stripe.android.ui.core.elements.autocomplete.model.AutocompletePrediction @@ -73,7 +72,7 @@ class AutocompleteViewModelTest { ) ) val expectedResult = Result.success( - Address( + ShippingAddress( city = null, country = null, line1 = "", @@ -130,7 +129,7 @@ class AutocompleteViewModelTest { fun `onEnterAddressManually sets the current address and navigates back`() = runTest(UnconfinedTestDispatcher()) { val viewModel = createViewModel() val expectedResult = Result.success( - Address( + ShippingAddress( city = "city", country = null, line1 = "", diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/InputAddressViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/InputAddressViewModelTest.kt index c24f894a18d..e6949ea2e96 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/InputAddressViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/InputAddressViewModelTest.kt @@ -1,7 +1,6 @@ package com.stripe.android.paymentsheet.addresselement import com.google.common.truth.Truth.assertThat -import com.stripe.android.model.Address import com.stripe.android.ui.core.injection.FormControllerSubcomponent import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -30,8 +29,8 @@ class InputAddressViewModelTest { @Test fun `no autocomplete address passed has an empty address to start`() = runTest { - val flow = MutableStateFlow(null) - whenever(navigator.getResultFlow(any())).thenReturn(flow) + val flow = MutableStateFlow(null) + whenever(navigator.getResultFlow(any())).thenReturn(flow) val viewModel = createViewModel() assertThat(viewModel.collectedAddress.value).isEqualTo(ShippingAddress()) @@ -39,11 +38,11 @@ class InputAddressViewModelTest { @Test fun `autocomplete address passed is collected to start`() = runTest { - val expectedAddress = Address(city = "Seattle") - val flow = MutableStateFlow(expectedAddress) - whenever(navigator.getResultFlow(any())).thenReturn(flow) + val expectedAddress = ShippingAddress(name = "skyler", company = "stripe") + val flow = MutableStateFlow(expectedAddress) + whenever(navigator.getResultFlow(any())).thenReturn(flow) val viewModel = createViewModel() - assertThat(viewModel.collectedAddress.value).isEqualTo(ShippingAddress(city = "Seattle")) + assertThat(viewModel.collectedAddress.value).isEqualTo(expectedAddress) } }