Skip to content

Commit

Permalink
Improve prefill in bank flows (#9518)
Browse files Browse the repository at this point in the history
* Prefill email and phone number in bank flows

* Tweak code and address test issue
  • Loading branch information
tillh-stripe authored Oct 29, 2024
1 parent 0f3d653 commit 64142f2
Show file tree
Hide file tree
Showing 16 changed files with 191 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ internal class FinancialConnectionsPlaygroundViewModel(
billingDetails = ElementsSessionContext.BillingDetails(
email = settings.get<EmailSetting>().selectedOption,
),
prefillDetails = ElementsSessionContext.PrefillDetails(
email = settings.get<EmailSetting>().selectedOption,
phone = null,
phoneCountryCode = null,
),
),
experience = settings.get<ExperienceSetting>().selectedOption,
integrationType = settings.get<IntegrationTypeSetting>().selectedOption,
Expand Down
8 changes: 8 additions & 0 deletions financial-connections/api/financial-connections.api
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,14 @@ public final class com/stripe/android/financialconnections/FinancialConnectionsS
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/financialconnections/FinancialConnectionsSheet$ElementsSessionContext$PrefillDetails$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$ElementsSessionContext$PrefillDetails;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lcom/stripe/android/financialconnections/FinancialConnectionsSheet$ElementsSessionContext$PrefillDetails;
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/financialconnections/FinancialConnectionsSheetComposeKt {
public static final fun rememberFinancialConnectionsSheet (Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)Lcom/stripe/android/financialconnections/FinancialConnectionsSheet;
public static final fun rememberFinancialConnectionsSheetForToken (Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)Lcom/stripe/android/financialconnections/FinancialConnectionsSheet;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class FinancialConnectionsSheet internal constructor(
val currency: String?,
val linkMode: LinkMode?,
val billingDetails: BillingDetails?,
val prefillDetails: PrefillDetails,
) : Parcelable {

val paymentIntentId: String?
Expand Down Expand Up @@ -91,6 +92,14 @@ class FinancialConnectionsSheet internal constructor(
val country: String? = null,
) : Parcelable
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Parcelize
data class PrefillDetails(
val email: String?,
val phone: String?,
val phoneCountryCode: String?,
) : Parcelable
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ import com.stripe.android.financialconnections.navigation.topappbar.TopAppBarSta
import com.stripe.android.financialconnections.presentation.FinancialConnectionsViewModel
import com.stripe.android.financialconnections.ui.FinancialConnectionsSheetNativeActivity
import com.stripe.android.financialconnections.utils.parcelable
import com.stripe.android.model.LinkMode
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
Expand Down Expand Up @@ -128,16 +127,13 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor(
logNoBrowserAvailableAndFinish()
return
}

val manifest = sync.manifest
val isInstantDebits = stateFlow.value.isInstantDebits
val nativeAuthFlowEnabled = nativeRouter.nativeAuthFlowEnabled(manifest)
nativeRouter.logExposure(manifest)

val linkMode = initialState.initialArgs.elementsSessionContext?.linkMode
val hostedAuthUrl = buildHostedAuthUrl(
hostedAuthUrl = manifest.hostedAuthUrl,
isInstantDebits = isInstantDebits,
linkMode = linkMode,
)
if (hostedAuthUrl == null) {
finishWithResult(
Expand Down Expand Up @@ -171,15 +167,15 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor(
}
}

private fun buildHostedAuthUrl(
hostedAuthUrl: String?,
isInstantDebits: Boolean,
linkMode: LinkMode?,
): String? {
private fun buildHostedAuthUrl(hostedAuthUrl: String?): String? {
if (hostedAuthUrl == null) {
return null
}

val isInstantDebits = stateFlow.value.isInstantDebits
val elementsSessionContext = initialState.initialArgs.elementsSessionContext
val linkMode = elementsSessionContext?.linkMode

val queryParams = mutableListOf(hostedAuthUrl)
if (isInstantDebits) {
// For Instant Debits, add a query parameter to the hosted auth URL so that payment account creation
Expand All @@ -188,6 +184,12 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor(
linkMode?.let { queryParams.add("link_mode=${it.value}") }
}

elementsSessionContext?.prefillDetails?.run {
email?.let { queryParams.add("email=$it") }
phone?.let { queryParams.add("linkMobilePhone=$it") }
phoneCountryCode?.let { queryParams.add("linkMobilePhoneCountry=$it") }
}

return queryParams.joinToString("&")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ private fun NetworkingLinkSignupLoaded(
LaunchedEffect(showFullForm) {
if (showFullForm) {
scrollState.animateScrollToBottom()
phoneNumberFocusRequester.requestFocus()
if (payload.focusPhoneFieldOnShow) {
phoneNumberFocusRequester.requestFocus()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.stripe.android.core.Logger
import com.stripe.android.financialconnections.FinancialConnectionsSheet.ElementsSessionContext
import com.stripe.android.financialconnections.R
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.Click
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.NetworkingNewConsumer
Expand Down Expand Up @@ -68,6 +69,7 @@ internal class NetworkingLinkSignupViewModel @AssistedInject constructor(
private val logger: Logger,
private val presentSheet: PresentSheet,
private val linkSignupHandler: LinkSignupHandler,
private val elementsSessionContext: ElementsSessionContext?,
) : FinancialConnectionsViewModel<NetworkingLinkSignupState>(initialState, nativeAuthFlowCoordinator) {

private val pane: Pane
Expand All @@ -93,16 +95,19 @@ internal class NetworkingLinkSignupViewModel @AssistedInject constructor(

eventTracker.track(PaneLoaded(pane))

val prefillDetails = elementsSessionContext?.prefillDetails

NetworkingLinkSignupState.Payload(
content = requireNotNull(content),
merchantName = sync.manifest.getBusinessName(),
emailController = SimpleTextFieldController(
textFieldConfig = EmailConfig(label = R.string.stripe_networking_signup_email_label),
initialValue = sync.manifest.accountholderCustomerEmailAddress,
initialValue = sync.manifest.accountholderCustomerEmailAddress ?: prefillDetails?.email,
showOptionalLabel = false
),
phoneController = PhoneNumberController.createPhoneNumberController(
initialValue = sync.manifest.accountholderPhoneNumber ?: "",
initialValue = sync.manifest.accountholderPhoneNumber ?: prefillDetails?.phone ?: "",
initiallySelectedCountryCode = prefillDetails?.phoneCountryCode,
),
isInstantDebits = initialState.isInstantDebits,
)
Expand Down Expand Up @@ -340,6 +345,9 @@ internal data class NetworkingLinkSignupState(

val focusEmailField: Boolean
get() = isInstantDebits && emailController.initialValue.isNullOrBlank()

val focusPhoneFieldOnShow: Boolean
get() = phoneController.initialPhoneNumber.isBlank()
}

data class Content(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@ sealed class FinancialConnectionsSheetActivityArgs(
@Parcelize
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
data class ForData(
override val configuration: FinancialConnectionsSheet.Configuration
) : FinancialConnectionsSheetActivityArgs(configuration, elementsSessionContext = null)
override val configuration: FinancialConnectionsSheet.Configuration,
override val elementsSessionContext: FinancialConnectionsSheet.ElementsSessionContext? = null,
) : FinancialConnectionsSheetActivityArgs(configuration, elementsSessionContext)

@Parcelize
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
data class ForToken(
override val configuration: FinancialConnectionsSheet.Configuration
) : FinancialConnectionsSheetActivityArgs(configuration, elementsSessionContext = null)
override val configuration: FinancialConnectionsSheet.Configuration,
override val elementsSessionContext: FinancialConnectionsSheet.ElementsSessionContext? = null,
) : FinancialConnectionsSheetActivityArgs(configuration, elementsSessionContext)

@Parcelize
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ class FinancialConnectionsSheetForDataLauncher(
) {
activityResultLauncher.launch(
FinancialConnectionsSheetActivityArgs.ForData(
configuration
configuration = configuration,
elementsSessionContext = elementsSessionContext,
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ internal class FinancialConnectionsSheetForTokenLauncher(
) {
activityResultLauncher.launch(
FinancialConnectionsSheetActivityArgs.ForToken(
configuration
configuration = configuration,
elementsSessionContext = elementsSessionContext,
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.stripe.android.financialconnections.model.SynchronizeSessionResponse
import kotlinx.parcelize.Parcelize

@Parcelize
internal data class FinancialConnectionsSheetNativeActivityArgs constructor(
internal data class FinancialConnectionsSheetNativeActivityArgs(
val configuration: FinancialConnectionsSheet.Configuration,
val initialSyncResponse: SynchronizeSessionResponse,
val elementsSessionContext: FinancialConnectionsSheet.ElementsSessionContext? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ class FinancialConnectionsSheetViewModelTest {
currency = "usd",
linkMode = LinkMode.LinkPaymentMethod,
billingDetails = null,
prefillDetails = ElementsSessionContext.PrefillDetails(
email = null,
phone = null,
phoneCountryCode = null,
)
),
)
)
Expand Down Expand Up @@ -182,6 +187,11 @@ class FinancialConnectionsSheetViewModelTest {
currency = "usd",
linkMode = null,
billingDetails = null,
prefillDetails = ElementsSessionContext.PrefillDetails(
email = null,
phone = null,
phoneCountryCode = null,
)
),
)
)
Expand All @@ -196,6 +206,47 @@ class FinancialConnectionsSheetViewModelTest {
}
}

@Test
fun `init - hosted auth url contains prefill details`() = runTest {
// Given
whenever(browserManager.canOpenHttpsUrl()).thenReturn(true)
whenever(getOrFetchSync(any())).thenReturn(syncResponse)
whenever(nativeRouter.nativeAuthFlowEnabled(any())).thenReturn(false)

// When
val viewModel = createViewModel(
defaultInitialState.copy(
initialArgs = ForInstantDebits(
configuration = configuration,
elementsSessionContext = ElementsSessionContext(
initializationMode = ElementsSessionContext.InitializationMode.PaymentIntent("pi_123"),
amount = 123,
currency = "usd",
linkMode = null,
billingDetails = null,
prefillDetails = ElementsSessionContext.PrefillDetails(
email = "email@email.com",
phone = "5555551234",
phoneCountryCode = "US",
)
),
)
)
)

// Then
withState(viewModel) {
val viewEffect = it.viewEffect as OpenAuthFlowWithUrl
assertThat(viewEffect.url).isEqualTo(
syncResponse.manifest.hostedAuthUrl +
"&return_payment_method=true" +
"&email=email@email.com" +
"&linkMobilePhone=5555551234" +
"&linkMobilePhoneCountry=US"
)
}
}

@Test
fun `init - when data flow and non-native, hosted auth url without query params is launched`() = runTest {
// Given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@ class RealCreateInstantDebitsResultTest {
currency = "usd",
linkMode = linkMode,
billingDetails = billingDetails,
prefillDetails = ElementsSessionContext.PrefillDetails(
email = null,
phone = null,
phoneCountryCode = null,
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import com.stripe.android.financialconnections.ApiKeyFixtures.consumerSessionSig
import com.stripe.android.financialconnections.ApiKeyFixtures.sessionManifest
import com.stripe.android.financialconnections.ApiKeyFixtures.syncResponse
import com.stripe.android.financialconnections.CoroutineTestRule
import com.stripe.android.financialconnections.FinancialConnectionsSheet.ElementsSessionContext
import com.stripe.android.financialconnections.FinancialConnectionsSheet.ElementsSessionContext.InitializationMode
import com.stripe.android.financialconnections.TestFinancialConnectionsAnalyticsTracker
import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.ConsentAgree.analyticsValue
import com.stripe.android.financialconnections.domain.GetCachedAccounts
Expand All @@ -32,6 +34,7 @@ import com.stripe.android.financialconnections.repository.FinancialConnectionsCo
import com.stripe.android.financialconnections.utils.TestHandleError
import com.stripe.android.financialconnections.utils.UriUtils
import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.LinkMode
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Rule
Expand All @@ -58,6 +61,7 @@ class NetworkingLinkSignupViewModelTest {
private fun buildViewModel(
state: NetworkingLinkSignupState,
signupHandler: LinkSignupHandler = mockLinkSignupHandlerForNetworking(),
elementsSessionContext: ElementsSessionContext? = null,
) = NetworkingLinkSignupViewModel(
getOrFetchSync = getOrFetchSync,
logger = Logger.noop(),
Expand All @@ -69,6 +73,7 @@ class NetworkingLinkSignupViewModelTest {
nativeAuthFlowCoordinator = nativeAuthFlowCoordinator,
presentSheet = mock(),
linkSignupHandler = signupHandler,
elementsSessionContext = elementsSessionContext,
)

@Test
Expand All @@ -95,6 +100,44 @@ class NetworkingLinkSignupViewModelTest {
assertThat(payload.emailController.fieldValue.value).isEqualTo("test@test.com")
}

@Test
fun `init - creates controllers with Elements billing details`() = runTest {
val manifest = sessionManifest()

whenever(getOrFetchSync(any())).thenReturn(
syncResponse().copy(
manifest = manifest,
text = TextUpdate(
consent = null,
networkingLinkSignupPane = networkingLinkSignupPane(),
)
)
)
whenever(lookupAccount(any())).thenReturn(ConsumerSessionLookup(exists = false))

val viewModel = buildViewModel(
state = NetworkingLinkSignupState(),
elementsSessionContext = ElementsSessionContext(
initializationMode = InitializationMode.PaymentIntent("pi_1234"),
amount = null,
currency = null,
linkMode = LinkMode.LinkPaymentMethod,
billingDetails = null,
prefillDetails = ElementsSessionContext.PrefillDetails(
email = "email@email.com",
phone = "5555555555",
phoneCountryCode = "US",
),
)
)

val state = viewModel.stateFlow.value
val payload = requireNotNull(state.payload())
assertThat(payload.emailController.fieldValue.value).isEqualTo("email@email.com")
assertThat(payload.phoneController.fieldValue.value).isEqualTo("5555555555")
assertThat(payload.phoneController.countryDropdownController.rawFieldValue.value).isEqualTo("US")
}

@Test
fun `init - focuses email field if no email provided in Instant Debits flow`() = runTest {
val manifest = sessionManifest().copy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ class FinancialConnectionsConsumerSessionRepositoryImplTest {
currency = "cad",
linkMode = LinkMode.LinkPaymentMethod,
billingDetails = null,
prefillDetails = ElementsSessionContext.PrefillDetails(
email = null,
phone = null,
phoneCountryCode = null,
),
)
)

Expand Down
Loading

0 comments on commit 64142f2

Please sign in to comment.