From 9fa3ee2efa7b9a056c52b9dbd7e21175fd53a997 Mon Sep 17 00:00:00 2001 From: jameswoo-stripe <99316447+jameswoo-stripe@users.noreply.github.com> Date: Wed, 27 Jul 2022 12:49:06 -0700 Subject: [PATCH] Add analytics for address element --- paymentsheet/api/paymentsheet.api | 56 ++++++++- .../addresselement/AutocompleteScreenTest.kt | 25 ++++- .../AddressElementActivityContract.kt | 2 + .../addresselement/AddressLauncher.kt | 6 +- .../AddressLauncherResultCallback.kt | 2 +- .../addresselement/AddressUtils.kt | 48 ++++++++ .../addresselement/AutocompleteScreen.kt | 15 ++- .../addresselement/AutocompleteViewModel.kt | 21 ++-- .../addresselement/InputAddressViewModel.kt | 63 ++++++----- .../analytics/AddressLauncherEvent.kt | 49 ++++++++ .../analytics/AddressLauncherEventReporter.kt | 11 ++ .../DefaultAddressLauncherEventReporter.kt | 51 +++++++++ .../AddressElementViewModelModule.kt | 46 ++++++++ .../AddressElementActivityContractTest.kt | 1 + .../AddressElementViewModelTest.kt | 1 + .../addresselement/AddressUtilsTest.kt | 106 ++++++++++++++++++ .../AutocompleteViewModelTest.kt | 16 ++- .../InputAddressViewModelTest.kt | 28 +++++ 18 files changed, 479 insertions(+), 68 deletions(-) create mode 100644 paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressUtils.kt create mode 100644 paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/analytics/AddressLauncherEvent.kt create mode 100644 paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/analytics/AddressLauncherEventReporter.kt create mode 100644 paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/analytics/DefaultAddressLauncherEventReporter.kt create mode 100644 paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressUtilsTest.kt diff --git a/paymentsheet/api/paymentsheet.api b/paymentsheet/api/paymentsheet.api index 98adc9adead..c822eb5dcd0 100644 --- a/paymentsheet/api/paymentsheet.api +++ b/paymentsheet/api/paymentsheet.api @@ -810,16 +810,20 @@ public final class com/stripe/android/paymentsheet/addresselement/AddressLaunche public synthetic fun newArray (I)[Ljava/lang/Object; } +public final class com/stripe/android/paymentsheet/addresselement/AddressUtilsKt { + public static final fun levenshtein (Ljava/lang/CharSequence;Ljava/lang/CharSequence;)I +} + 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;Ljavax/inject/Provider;)V - public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/paymentsheet/addresselement/AutocompleteViewModel_Factory; + public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V + public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;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$Args;Landroid/app/Application;)Lcom/stripe/android/paymentsheet/addresselement/AutocompleteViewModel; + public static fun newInstance (Lcom/stripe/android/paymentsheet/addresselement/AddressElementActivityContract$Args;Lcom/stripe/android/paymentsheet/addresselement/AddressElementNavigator;Lcom/stripe/android/ui/core/elements/autocomplete/PlacesClientProxy;Lcom/stripe/android/paymentsheet/addresselement/AutocompleteViewModel$Args;Lcom/stripe/android/paymentsheet/addresselement/analytics/AddressLauncherEventReporter;Landroid/app/Application;)Lcom/stripe/android/paymentsheet/addresselement/AutocompleteViewModel; } public final class com/stripe/android/paymentsheet/addresselement/AutocompleteViewModel_Factory_MembersInjector : dagger/MembersInjector { @@ -845,11 +849,11 @@ public final class com/stripe/android/paymentsheet/addresselement/ComposableSing } public final class com/stripe/android/paymentsheet/addresselement/InputAddressViewModel_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/InputAddressViewModel_Factory; + public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V + public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/paymentsheet/addresselement/InputAddressViewModel_Factory; public fun get ()Lcom/stripe/android/paymentsheet/addresselement/InputAddressViewModel; public synthetic fun get ()Ljava/lang/Object; - public static fun newInstance (Lcom/stripe/android/paymentsheet/addresselement/AddressElementActivityContract$Args;Lcom/stripe/android/paymentsheet/addresselement/AddressElementNavigator;Ljavax/inject/Provider;)Lcom/stripe/android/paymentsheet/addresselement/InputAddressViewModel; + public static fun newInstance (Lcom/stripe/android/paymentsheet/addresselement/AddressElementActivityContract$Args;Lcom/stripe/android/paymentsheet/addresselement/AddressElementNavigator;Lcom/stripe/android/paymentsheet/addresselement/analytics/AddressLauncherEventReporter;Ljavax/inject/Provider;)Lcom/stripe/android/paymentsheet/addresselement/InputAddressViewModel; } public final class com/stripe/android/paymentsheet/addresselement/InputAddressViewModel_Factory_MembersInjector : dagger/MembersInjector { @@ -860,6 +864,14 @@ public final class com/stripe/android/paymentsheet/addresselement/InputAddressVi public static fun injectSubComponentBuilderProvider (Lcom/stripe/android/paymentsheet/addresselement/InputAddressViewModel$Factory;Ljavax/inject/Provider;)V } +public final class com/stripe/android/paymentsheet/addresselement/analytics/DefaultAddressLauncherEventReporter_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/analytics/DefaultAddressLauncherEventReporter_Factory; + public fun get ()Lcom/stripe/android/paymentsheet/addresselement/analytics/DefaultAddressLauncherEventReporter; + public synthetic fun get ()Ljava/lang/Object; + public static fun newInstance (Lcom/stripe/android/core/networking/AnalyticsRequestExecutor;Lcom/stripe/android/core/networking/AnalyticsRequestFactory;Lkotlin/coroutines/CoroutineContext;)Lcom/stripe/android/paymentsheet/addresselement/analytics/DefaultAddressLauncherEventReporter; +} + public final class com/stripe/android/paymentsheet/analytics/DefaultEventReporter_Factory : dagger/internal/Factory { public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/paymentsheet/analytics/DefaultEventReporter_Factory; @@ -1073,6 +1085,14 @@ public final class com/stripe/android/paymentsheet/forms/TransformSpecToElement_ public static fun newInstance (Lcom/stripe/android/ui/core/forms/resources/ResourceRepository;Lcom/stripe/android/paymentsheet/paymentdatacollection/FormFragmentArguments;Landroid/content/Context;)Lcom/stripe/android/paymentsheet/forms/TransformSpecToElement; } +public final class com/stripe/android/paymentsheet/injection/AddressElementViewModelModule_ProvideAnalyticsRequestFactory$paymentsheet_releaseFactory : dagger/internal/Factory { + public fun (Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule;Ljavax/inject/Provider;Ljavax/inject/Provider;)V + public static fun create (Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule_ProvideAnalyticsRequestFactory$paymentsheet_releaseFactory; + public fun get ()Lcom/stripe/android/core/networking/AnalyticsRequestFactory; + public synthetic fun get ()Ljava/lang/Object; + public static fun provideAnalyticsRequestFactory$paymentsheet_release (Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule;Landroid/content/Context;Ljava/lang/String;)Lcom/stripe/android/core/networking/AnalyticsRequestFactory; +} + public final class com/stripe/android/paymentsheet/injection/AddressElementViewModelModule_ProvideDummyInjectorKeyFactory : dagger/internal/Factory { public fun (Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule;)V public static fun create (Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule;)Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule_ProvideDummyInjectorKeyFactory; @@ -1081,6 +1101,14 @@ public final class com/stripe/android/paymentsheet/injection/AddressElementViewM public static fun provideDummyInjectorKey (Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule;)Ljava/lang/String; } +public final class com/stripe/android/paymentsheet/injection/AddressElementViewModelModule_ProvideEventReporterFactory : dagger/internal/Factory { + public fun (Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule;Ljavax/inject/Provider;)V + public static fun create (Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule;Ljavax/inject/Provider;)Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule_ProvideEventReporterFactory; + public fun get ()Lcom/stripe/android/paymentsheet/addresselement/analytics/AddressLauncherEventReporter; + public synthetic fun get ()Ljava/lang/Object; + public static fun provideEventReporter (Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule;Lcom/stripe/android/paymentsheet/addresselement/analytics/DefaultAddressLauncherEventReporter;)Lcom/stripe/android/paymentsheet/addresselement/analytics/AddressLauncherEventReporter; +} + public final class com/stripe/android/paymentsheet/injection/AddressElementViewModelModule_ProvideEventReporterModeFactory : dagger/internal/Factory { public fun (Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule;)V public static fun create (Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule;)Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule_ProvideEventReporterModeFactory; @@ -1089,6 +1117,22 @@ public final class com/stripe/android/paymentsheet/injection/AddressElementViewM public static fun provideEventReporterMode (Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule;)Lcom/stripe/android/paymentsheet/analytics/EventReporter$Mode; } +public final class com/stripe/android/paymentsheet/injection/AddressElementViewModelModule_ProvideGooglePlacesClient$paymentsheet_releaseFactory : dagger/internal/Factory { + public fun (Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule;Ljavax/inject/Provider;Ljavax/inject/Provider;)V + public static fun create (Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule_ProvideGooglePlacesClient$paymentsheet_releaseFactory; + public fun get ()Lcom/stripe/android/ui/core/elements/autocomplete/PlacesClientProxy; + public synthetic fun get ()Ljava/lang/Object; + public static fun provideGooglePlacesClient$paymentsheet_release (Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule;Landroid/content/Context;Lcom/stripe/android/paymentsheet/addresselement/AddressElementActivityContract$Args;)Lcom/stripe/android/ui/core/elements/autocomplete/PlacesClientProxy; +} + +public final class com/stripe/android/paymentsheet/injection/AddressElementViewModelModule_ProvidesPublishableKeyFactory : dagger/internal/Factory { + public fun (Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule;Ljavax/inject/Provider;)V + public static fun create (Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule;Ljavax/inject/Provider;)Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule_ProvidesPublishableKeyFactory; + public synthetic fun get ()Ljava/lang/Object; + public fun get ()Ljava/lang/String; + public static fun providesPublishableKey (Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelModule;Lcom/stripe/android/paymentsheet/addresselement/AddressElementActivityContract$Args;)Ljava/lang/String; +} + public final class com/stripe/android/paymentsheet/injection/DaggerAddressElementViewModelFactoryComponent { public static fun builder ()Lcom/stripe/android/paymentsheet/injection/AddressElementViewModelFactoryComponent$Builder; } 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 69b19a7b586..aff1ffa7936 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 @@ -12,6 +12,7 @@ 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.paymentsheet.addresselement.analytics.AddressLauncherEventReporter 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 @@ -30,10 +31,12 @@ class AutocompleteScreenTest { val composeTestRule = createAndroidComposeRule() private val args = AddressElementActivityContract.Args( + "publishableKey", AddressLauncher.Configuration(), "injectorKey" ) private val application = ApplicationProvider.getApplicationContext() + private val eventReporter = FakeEventReporter() @Test fun ensure_elements_exist() { @@ -89,15 +92,13 @@ class AutocompleteScreenTest { viewModel = AutocompleteViewModel( args, AddressElementNavigator(), + mockClient, AutocompleteViewModel.Args( "US" ), + eventReporter, application - ).apply { - initialize { - mockClient - } - } + ) ) } } @@ -128,4 +129,18 @@ class AutocompleteScreenTest { ) } } + + private class FakeEventReporter : AddressLauncherEventReporter { + override fun onShow(country: String) { + // no-op + } + + override fun onCompleted( + country: String, + autocompleteResultSelected: Boolean, + editDistance: Int? + ) { + // no-op + } + } } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivityContract.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivityContract.kt index 4b155ccf7fe..a1017caa719 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivityContract.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivityContract.kt @@ -25,12 +25,14 @@ internal class AddressElementActivityContract : /** * Arguments for launching [AddressElementActivity] to collect an address. * + * @param publishableKey the Stripe publishable key * @param config the paymentsheet configuration passed from the merchant * @param injectorKey Parameter needed to perform dependency injection. * If default, a new graph is created */ @Parcelize data class Args internal constructor( + internal val publishableKey: String, internal val config: AddressLauncher.Configuration?, @InjectorKey internal val injectorKey: String = DUMMY_INJECTOR_KEY ) : ActivityStarter.Args { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressLauncher.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressLauncher.kt index f7c132c9c89..0414391448d 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressLauncher.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressLauncher.kt @@ -54,9 +54,13 @@ internal class AddressLauncher internal constructor( ) @JvmOverloads - fun present(configuration: Configuration = Configuration()) { + fun present( + publishableKey: String, + configuration: Configuration = Configuration() + ) { activityResultLauncher.launch( AddressElementActivityContract.Args( + publishableKey, configuration, injectorKey ) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressLauncherResultCallback.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressLauncherResultCallback.kt index c03f699d2e6..8b56abc3939 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressLauncherResultCallback.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressLauncherResultCallback.kt @@ -3,6 +3,6 @@ package com.stripe.android.paymentsheet.addresselement /** * Callback that is invoked when a [AddressLauncherResult] is available. */ -internal interface AddressLauncherResultCallback { +internal fun interface AddressLauncherResultCallback { fun onAddressLauncherResult(addressLauncherResult: AddressLauncherResult) } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressUtils.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressUtils.kt new file mode 100644 index 00000000000..04e80fb14ce --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressUtils.kt @@ -0,0 +1,48 @@ +package com.stripe.android.paymentsheet.addresselement + +import kotlin.math.min + +// https://gist.github.com/ademar111190/34d3de41308389a0d0d8 + +fun CharSequence.levenshtein(other: CharSequence): Int { + if (this == other) { return 0 } + if (this.isEmpty()) { return other.length } + if (other.isEmpty()) { return this.length } + + val thisLength = this.length + 1 + val otherLength = other.length + 1 + + var cost = Array(thisLength) { it } + var newCost = Array(thisLength) { 0 } + + for (i in 1 until otherLength) { + newCost[0] = i + + for (j in 1 until thisLength) { + val match = if (this[j - 1] == other[i - 1]) 0 else 1 + + val costReplace = cost[j - 1] + match + val costInsert = cost[j] + 1 + val costDelete = newCost[j - 1] + 1 + + newCost[j] = min(min(costInsert, costDelete), costReplace) + } + + val swap = cost + cost = newCost + newCost = swap + } + + return cost[thisLength - 1] +} + +internal fun AddressDetails.editDistance(otherAddress: AddressDetails?): Int { + var editDistance = 0 + editDistance += (city ?: "").levenshtein(otherAddress?.city ?: "") + editDistance += (country ?: "").levenshtein(otherAddress?.country ?: "") + editDistance += (line1 ?: "").levenshtein(otherAddress?.line1 ?: "") + editDistance += (line2 ?: "").levenshtein(otherAddress?.line2 ?: "") + editDistance += (postalCode ?: "").levenshtein(otherAddress?.postalCode ?: "") + editDistance += (state ?: "").levenshtein(otherAddress?.state ?: "") + return editDistance +} 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 7e6c18bd1c8..34b4875a73a 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 @@ -56,16 +56,15 @@ internal fun AutocompleteScreen( ) { val application = LocalContext.current.applicationContext as Application val viewModel: AutocompleteViewModel = - viewModel( + viewModel( factory = AutocompleteViewModel.Factory( - injector, - AutocompleteViewModel.Args( + injector = injector, + args = AutocompleteViewModel.Args( country = country - ) - ) { application } - ).also { - it.initialize() - } + ), + applicationSupplier = { application } + ) + ) AutocompleteScreenUI(viewModel = viewModel) } 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 a608ca915f3..049953cd93f 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 @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.stripe.android.paymentsheet.R +import com.stripe.android.paymentsheet.addresselement.analytics.AddressLauncherEventReporter import com.stripe.android.ui.core.injection.NonFallbackInjectable import com.stripe.android.paymentsheet.injection.AutocompleteViewModelSubcomponent import com.stripe.android.ui.core.elements.SimpleTextFieldConfig @@ -34,11 +35,11 @@ import javax.inject.Provider internal class AutocompleteViewModel @Inject constructor( val args: AddressElementActivityContract.Args, val navigator: AddressElementNavigator, + private val placesClient: PlacesClientProxy?, private val autocompleteArgs: Args, + private val eventReporter: AddressLauncherEventReporter, application: Application ) : AndroidViewModel(application) { - private var client: PlacesClientProxy? = null - private val _predictions = MutableStateFlow?>(null) val predictions: StateFlow?> get() = _predictions @@ -63,20 +64,13 @@ internal class AutocompleteViewModel @Inject constructor( private val debouncer = Debouncer() - fun initialize( - clientProvider: () -> PlacesClientProxy? = { - args.config?.googlePlacesApiKey?.let { - PlacesClientProxy.create(getApplication(), it) - } - } - ) { - client = clientProvider() + init { debouncer.startWatching( coroutineScope = viewModelScope, queryFlow = queryFlow, onValidQuery = { viewModelScope.launch { - client?.findAutocompletePredictions( + placesClient?.findAutocompletePredictions( query = it, country = autocompleteArgs.country ?: throw IllegalStateException("Country cannot be empty"), @@ -111,12 +105,15 @@ internal class AutocompleteViewModel @Inject constructor( } } } + autocompleteArgs.country?.let { country -> + eventReporter.onShow(country) + } } fun selectPrediction(prediction: AutocompletePrediction) { viewModelScope.launch { _loading.value = true - client?.fetchPlace( + placesClient?.fetchPlace( placeId = prediction.placeId )?.fold( onSuccess = { 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 3cc72bb0ae4..ebcb76bfe55 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 @@ -1,8 +1,10 @@ package com.stripe.android.paymentsheet.addresselement +import androidx.annotation.VisibleForTesting import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import com.stripe.android.paymentsheet.addresselement.analytics.AddressLauncherEventReporter import com.stripe.android.ui.core.injection.NonFallbackInjectable import com.stripe.android.paymentsheet.injection.InputAddressViewModelSubcomponent import com.stripe.android.ui.core.FormController @@ -23,9 +25,10 @@ import javax.inject.Provider internal class InputAddressViewModel @Inject constructor( val args: AddressElementActivityContract.Args, val navigator: AddressElementNavigator, + private val eventReporter: AddressLauncherEventReporter, formControllerProvider: Provider ) : ViewModel() { - private val _collectedAddress = MutableStateFlow(args.config?.defaultValues) + private val _collectedAddress = MutableStateFlow(args.config?.defaultValues) val collectedAddress: StateFlow = _collectedAddress private val _formController = MutableStateFlow(null) @@ -38,19 +41,18 @@ internal class InputAddressViewModel @Inject constructor( viewModelScope.launch { navigator.getResultFlow(AddressDetails.KEY)?.collect { val oldShippingAddress = _collectedAddress.value - _collectedAddress.emit( - AddressDetails( - name = oldShippingAddress?.name ?: it?.name, - company = oldShippingAddress?.company ?: it?.company, - phoneNumber = oldShippingAddress?.phoneNumber ?: it?.phoneNumber, - city = it?.city, - country = it?.country, - line1 = it?.line1, - line2 = it?.line2, - state = it?.state, - postalCode = it?.postalCode - ) + val autocompleteAddress = AddressDetails( + name = oldShippingAddress?.name ?: it?.name, + company = oldShippingAddress?.company ?: it?.company, + phoneNumber = oldShippingAddress?.phoneNumber ?: it?.phoneNumber, + city = it?.city, + country = it?.country, + line1 = it?.line1, + line2 = it?.line2, + state = it?.state, + postalCode = it?.postalCode ) + _collectedAddress.emit(autocompleteAddress) } } @@ -82,7 +84,7 @@ internal class InputAddressViewModel @Inject constructor( } private suspend fun getCurrentAddress(): AddressDetails? { - return _formController.value + return formController.value ?.formValues ?.stateIn(viewModelScope) ?.value @@ -147,26 +149,27 @@ internal class InputAddressViewModel @Inject constructor( fun clickPrimaryButton() { _formEnabled.value = false viewModelScope.launch { - formController.value?.let { controller -> - controller.formValues.collect { - val result = AddressLauncherResult.Succeeded( - AddressDetails( - name = it[IdentifierSpec.Name]?.value, - city = it[IdentifierSpec.City]?.value, - country = it[IdentifierSpec.Country]?.value, - line1 = it[IdentifierSpec.Line1]?.value, - line2 = it[IdentifierSpec.Line2]?.value, - postalCode = it[IdentifierSpec.PostalCode]?.value, - state = it[IdentifierSpec.State]?.value, - phoneNumber = it[IdentifierSpec.Phone]?.value - ) - ) - navigator.dismiss(result) - } + val address = getCurrentAddress() + address?.let { + dismissWithAddress(it) } } } + @VisibleForTesting + fun dismissWithAddress(address: AddressDetails) { + address.country?.let { country -> + eventReporter.onCompleted( + country = country, + autocompleteResultSelected = collectedAddress.value?.line1 != null, + editDistance = address.editDistance(collectedAddress.value) + ) + } + navigator.dismiss( + AddressLauncherResult.Succeeded(address) + ) + } + internal class Factory( private val injector: NonFallbackInjector ) : ViewModelProvider.Factory, NonFallbackInjectable { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/analytics/AddressLauncherEvent.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/analytics/AddressLauncherEvent.kt new file mode 100644 index 00000000000..df101e95254 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/analytics/AddressLauncherEvent.kt @@ -0,0 +1,49 @@ +package com.stripe.android.paymentsheet.addresselement.analytics + +import com.stripe.android.core.networking.AnalyticsEvent + +internal sealed class AddressLauncherEvent : AnalyticsEvent { + abstract val additionalParams: Map + + class Show( + val country: String + ) : AddressLauncherEvent() { + override val eventName: String = "mc_address_show" + override val additionalParams: Map + get() { + return mapOf( + FIELD_ADDRESS_DATA_BLOB to mapOf( + FIELD_ADDRESS_COUNTRY_CODE to country + ) + ) + } + } + + class Completed( + val country: String, + private val autocompleteResultSelected: Boolean, + private val editDistance: Int? + ) : AddressLauncherEvent() { + override val eventName: String = "mc_address_completed" + override val additionalParams: Map + get() { + val data = mutableMapOf( + FIELD_ADDRESS_COUNTRY_CODE to country, + FIELD_AUTO_COMPLETE_RESULT_SELECTED to autocompleteResultSelected + ) + editDistance?.let { + data[FIELD_EDIT_DISTANCE] = it + } + return mapOf( + FIELD_ADDRESS_DATA_BLOB to data + ) + } + } + + internal companion object { + const val FIELD_ADDRESS_DATA_BLOB = "address_data_blob" + const val FIELD_ADDRESS_COUNTRY_CODE = "address_country_code" + const val FIELD_AUTO_COMPLETE_RESULT_SELECTED = "auto_complete_result_selected" + const val FIELD_EDIT_DISTANCE = "edit_distance" + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/analytics/AddressLauncherEventReporter.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/analytics/AddressLauncherEventReporter.kt new file mode 100644 index 00000000000..68c9b4f7116 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/analytics/AddressLauncherEventReporter.kt @@ -0,0 +1,11 @@ +package com.stripe.android.paymentsheet.addresselement.analytics + +internal interface AddressLauncherEventReporter { + fun onShow(country: String) + + fun onCompleted( + country: String, + autocompleteResultSelected: Boolean, + editDistance: Int? + ) +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/analytics/DefaultAddressLauncherEventReporter.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/analytics/DefaultAddressLauncherEventReporter.kt new file mode 100644 index 00000000000..718ab126be2 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/analytics/DefaultAddressLauncherEventReporter.kt @@ -0,0 +1,51 @@ +package com.stripe.android.paymentsheet.addresselement.analytics + +import com.stripe.android.core.injection.IOContext +import com.stripe.android.core.networking.AnalyticsRequestExecutor +import com.stripe.android.core.networking.AnalyticsRequestFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.coroutines.CoroutineContext + +@Singleton +internal class DefaultAddressLauncherEventReporter @Inject internal constructor( + private val analyticsRequestExecutor: AnalyticsRequestExecutor, + private val analyticsRequestFactory: AnalyticsRequestFactory, + @IOContext private val workContext: CoroutineContext +) : AddressLauncherEventReporter { + + override fun onShow(country: String) { + fireEvent( + AddressLauncherEvent.Show( + country = country + ) + ) + } + + override fun onCompleted( + country: String, + autocompleteResultSelected: Boolean, + editDistance: Int? + ) { + fireEvent( + AddressLauncherEvent.Completed( + country = country, + autocompleteResultSelected = autocompleteResultSelected, + editDistance = editDistance + ) + ) + } + + private fun fireEvent(event: AddressLauncherEvent) { + CoroutineScope(workContext).launch { + analyticsRequestExecutor.executeAsync( + analyticsRequestFactory.createRequest( + event, + event.additionalParams + ) + ) + } + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/injection/AddressElementViewModelModule.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/injection/AddressElementViewModelModule.kt index 1fed098d436..a115c8c358c 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/injection/AddressElementViewModelModule.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/injection/AddressElementViewModelModule.kt @@ -1,12 +1,21 @@ package com.stripe.android.paymentsheet.injection +import android.content.Context import com.stripe.android.core.injection.DUMMY_INJECTOR_KEY import com.stripe.android.core.injection.Injector import com.stripe.android.core.injection.InjectorKey +import com.stripe.android.core.injection.PUBLISHABLE_KEY +import com.stripe.android.core.networking.AnalyticsRequestFactory +import com.stripe.android.core.utils.ContextUtils.packageInfo +import com.stripe.android.paymentsheet.addresselement.AddressElementActivityContract +import com.stripe.android.paymentsheet.addresselement.analytics.AddressLauncherEventReporter +import com.stripe.android.paymentsheet.addresselement.analytics.DefaultAddressLauncherEventReporter import com.stripe.android.paymentsheet.analytics.EventReporter +import com.stripe.android.ui.core.elements.autocomplete.PlacesClientProxy import com.stripe.android.ui.core.injection.FormControllerSubcomponent import dagger.Module import dagger.Provides +import javax.inject.Named import javax.inject.Singleton @Module( @@ -29,4 +38,41 @@ internal class AddressElementViewModelModule { @Provides @InjectorKey fun provideDummyInjectorKey(): String = DUMMY_INJECTOR_KEY + + @Provides + @Named(PUBLISHABLE_KEY) + @Singleton + fun providesPublishableKey( + args: AddressElementActivityContract.Args + ): String = args.publishableKey + + @Provides + @Singleton + internal fun provideAnalyticsRequestFactory( + context: Context, + @Named(PUBLISHABLE_KEY) publishableKey: String + ): AnalyticsRequestFactory = AnalyticsRequestFactory( + packageManager = context.packageManager, + packageName = context.packageName.orEmpty(), + packageInfo = context.packageInfo, + publishableKeyProvider = { publishableKey } + ) + + @Provides + @Singleton + internal fun provideGooglePlacesClient( + context: Context, + args: AddressElementActivityContract.Args + ): PlacesClientProxy? = args.config?.googlePlacesApiKey?.let { + PlacesClientProxy.create( + context, + it + ) + } + + @Provides + @Singleton + fun provideEventReporter( + defaultAddressLauncherEventReporter: DefaultAddressLauncherEventReporter + ): AddressLauncherEventReporter = defaultAddressLauncherEventReporter } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivityContractTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivityContractTest.kt index cffd87b3fa5..187bc52445c 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivityContractTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivityContractTest.kt @@ -13,6 +13,7 @@ class AddressElementActivityContractTest { @Test fun `AddressElementActivityContract args parcelize correctly`() { val args = AddressElementActivityContract.Args( + "publishableKey", AddressLauncherFixtures.BASIC_CONFIG, "injectorKey" ) diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressElementViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressElementViewModelTest.kt index 1c2d608f540..57f040f2648 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressElementViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressElementViewModelTest.kt @@ -23,6 +23,7 @@ import kotlin.test.assertNotNull @RunWith(RobolectricTestRunner::class) class AddressElementViewModelTest { private val defaultArgs = AddressElementActivityContract.Args( + "publishableKey", AddressLauncherFixtures.BASIC_CONFIG, INJECTOR_KEY ) diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressUtilsTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressUtilsTest.kt new file mode 100644 index 00000000000..a4841178427 --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressUtilsTest.kt @@ -0,0 +1,106 @@ +package com.stripe.android.paymentsheet.addresselement + +import com.google.common.truth.Truth +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class AddressUtilsTest { + @Test + fun `test edit distance equal address`() { + val address = AddressDetails( + city = "San Francisco", + country = "AT", + line1 = "510 Townsend St.", + postalCode = "94102", + state = "California" + ) + + Truth.assertThat(address.editDistance(address)).isEqualTo(0) + } + + @Test + fun `test edit distance one char diff`() { + val address = AddressDetails( + city = "San Francisco", + country = "AT", + line1 = "510 Townsend St.", + postalCode = "94102", + state = "California" + ) + + val otherAddress = AddressDetails( + city = "Sa Francisco", // One char diff here + country = "AT", + line1 = "510 Townsend St.", + postalCode = "94102", + state = "California" + ) + + Truth.assertThat(address.editDistance(otherAddress)).isEqualTo(1) + } + + @Test + fun `test edit distance different city`() { + val address = AddressDetails( + city = "San Francisco", + country = "AT", + line1 = "510 Townsend St.", + postalCode = "94102", + state = "California" + ) + + val otherAddress = AddressDetails( + city = "Freemont", + country = "AT", + line1 = "510 Townsend St.", + postalCode = "94102", + state = "California" + ) + + Truth.assertThat(address.editDistance(otherAddress)).isEqualTo(11) + } + + @Test + fun `test edit distance missing city original`() { + val address = AddressDetails( + city = null, + country = "AT", + line1 = "510 Townsend St.", + postalCode = "94102", + state = "California" + ) + + val otherAddress = AddressDetails( + city = "San Francisco", + country = "AT", + line1 = "510 Townsend St.", + postalCode = "94102", + state = "California" + ) + + Truth.assertThat(address.editDistance(otherAddress)).isEqualTo(13) + } + + @Test + fun `test edit distance missing city other`() { + val address = AddressDetails( + city = "San Francisco", + country = "AT", + line1 = "510 Townsend St.", + postalCode = "94102", + state = "California" + ) + + val otherAddress = AddressDetails( + city = null, + country = "AT", + line1 = "510 Townsend St.", + postalCode = "94102", + state = "California" + ) + + Truth.assertThat(address.editDistance(otherAddress)).isEqualTo(13) + } +} 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 f9862d0d27e..7ebd0f9bed8 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,6 +5,7 @@ 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.paymentsheet.addresselement.analytics.AddressLauncherEventReporter 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 @@ -40,20 +41,19 @@ class AutocompleteViewModelTest { private val navigator = mock() private val application = ApplicationProvider.getApplicationContext() private val mockClient = mock() + private val mockEventReporter = mock() private fun createViewModel() = AutocompleteViewModel( args, navigator, + mockClient, AutocompleteViewModel.Args( "US" ), + mockEventReporter, application - ).apply { - initialize { - mockClient - } - } + ) @BeforeTest fun setUp() { @@ -241,4 +241,10 @@ class AutocompleteViewModelTest { verify(viewModel.navigator, never()).setResult(any(), any()) } + + @Test + fun `initializing ViewModel emits onShow event`() { + createViewModel() + verify(mockEventReporter).onShow(eq("US")) + } } 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 4646e4f19a6..4e768b47705 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.paymentsheet.addresselement.analytics.AddressLauncherEventReporter import com.stripe.android.ui.core.injection.FormControllerSubcomponent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -13,7 +14,9 @@ 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.verify import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import javax.inject.Provider @@ -40,6 +43,7 @@ class InputAddressViewModelTest { whenever(build()).thenReturn(formControllerSubcomponent) } } + private val eventReporter = mock() private fun createViewModel(defaultAddress: AddressDetails? = null): InputAddressViewModel { defaultAddress?.let { @@ -49,6 +53,7 @@ class InputAddressViewModelTest { return InputAddressViewModel( args, navigator, + eventReporter, formControllerProvider ) } @@ -89,4 +94,27 @@ class InputAddressViewModelTest { val viewModel = createViewModel(expectedAddress) assertThat(viewModel.collectedAddress.value).isEqualTo(expectedAddress) } + + @Test + fun `viewModel emits onComplete event`() = runTest(UnconfinedTestDispatcher()) { + val viewModel = createViewModel( + AddressDetails( + line1 = "99 Broadway St", + city = "Seattle", + country = "US" + ) + ) + viewModel.dismissWithAddress( + AddressDetails( + line1 = "99 Broadway St", + city = "Seattle", + country = "US" + ) + ) + verify(eventReporter).onCompleted( + country = eq("US"), + autocompleteResultSelected = eq(true), + editDistance = eq(0) + ) + } }