diff --git a/dependencies.gradle b/dependencies.gradle index 55ded03d54c..d910f91ebe3 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -70,6 +70,7 @@ ext.versions = [ truth : '1.1.3', turbine : '1.0.0', uiAutomator : '2.2.0', + workManager : '2.9.0', zxing : '3.5.2', ] @@ -121,6 +122,7 @@ ext.libs = [ navigationUi : "androidx.navigation:navigation-ui-ktx:${versions.androidxNavigation}", preference : "androidx.preference:preference-ktx:${versions.androidxPreference}", recyclerView : "androidx.recyclerview:recyclerview:${versions.androidxRecyclerview}", + workManager : "androidx.work:work-runtime-ktx:${versions.workManager}", ], camera : [ core : "androidx.camera:camera-core:${versions.cameraX}", @@ -197,6 +199,7 @@ ext.testLibs = [ coreKtx : "androidx.test:core-ktx:${versions.androidTest}", truth : "androidx.test.ext:truth:${versions.androidTest}", uiAutomator: "androidx.test.uiautomator:uiautomator:${versions.uiAutomator}", + workManager: "androidx.work:work-testing:${versions.workManager}", ], espresso : [ accessibility : "androidx.test.espresso:espresso-accessibility:${versions.espresso}", diff --git a/docs/payments-core/com.stripe.android.view.i18n/-translator-manager/index.html b/docs/payments-core/com.stripe.android.view.i18n/-translator-manager/index.html index c1cca4743dd..7ce37faefec 100644 --- a/docs/payments-core/com.stripe.android.view.i18n/-translator-manager/index.html +++ b/docs/payments-core/com.stripe.android.view.i18n/-translator-manager/index.html @@ -62,7 +62,7 @@

TranslatorManager

-

A class that provides a ErrorMessageTranslator for translating server-provided error messages, as defined in Stripe API Errors Reference.

See com.stripe.android.view.PaymentMethodsActivity for example usage.

To use a custom ErrorMessageTranslator in your app, override Application.onCreate in your app's Application subclass and call setErrorMessageTranslator.

public class MyApp extends Application {
+    

A class that provides a ErrorMessageTranslator for translating server-provided error messages, as defined in Stripe API Errors Reference.

See com.stripe.android.view.PaymentMethodsActivity for example usage.

To use a custom ErrorMessageTranslator in your app, override Application.onCreate in your app's Application subclass and call setErrorMessageTranslator.

public class MyApp extends Application {
 public void onCreate() {
     super.onCreate();
     TranslatorManager.setErrorMessageTranslator(new MyErrorMessageTranslator());
diff --git a/financial-connections-example/build.gradle b/financial-connections-example/build.gradle
index d0c2db464b6..5e96cefca5b 100644
--- a/financial-connections-example/build.gradle
+++ b/financial-connections-example/build.gradle
@@ -55,6 +55,7 @@ dependencies {
     implementation libs.androidx.coreKtx
     implementation libs.androidx.lifecycle
     implementation libs.androidx.liveDataKtx
+    implementation libs.androidx.workManager
     implementation libs.compose.activity
     implementation libs.compose.material
     implementation libs.compose.liveData
diff --git a/financial-connections-example/dependencies/dependencies.txt b/financial-connections-example/dependencies/dependencies.txt
index cb2b9daba98..4cd13ef2c2d 100644
--- a/financial-connections-example/dependencies/dependencies.txt
+++ b/financial-connections-example/dependencies/dependencies.txt
@@ -57,6 +57,7 @@
 |    |    |    |    |    |    +--- androidx.lifecycle:lifecycle-livedata-ktx:2.7.0 (c)
 |    |    |    |    |    |    +--- androidx.lifecycle:lifecycle-runtime:2.7.0 (c)
 |    |    |    |    |    |    +--- androidx.lifecycle:lifecycle-runtime-ktx:2.7.0 (c)
+|    |    |    |    |    |    +--- androidx.lifecycle:lifecycle-service:2.7.0 (c)
 |    |    |    |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel:2.7.0 (c)
 |    |    |    |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 (c)
 |    |    |    |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 (c)
@@ -82,6 +83,7 @@
 |    |    |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 (c)
 |    |    |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 (c)
 |    |    |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0 (c)
+|    |    |    |    |    +--- androidx.lifecycle:lifecycle-service:2.7.0 (c)
 |    |    |    |    |    \--- androidx.lifecycle:lifecycle-process:2.7.0 (c)
 |    |    |    |    +--- androidx.versionedparcelable:versionedparcelable:1.1.1
 |    |    |    |    |    +--- androidx.annotation:annotation:1.1.0 -> 1.7.1 (*)
@@ -161,6 +163,7 @@
 |    |    |         |         |         |         |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 (c)
 |    |    |         |         |         |         |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 (c)
 |    |    |         |         |         |         |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0 (c)
+|    |    |         |         |         |         |    |    |    +--- androidx.lifecycle:lifecycle-service:2.7.0 (c)
 |    |    |         |         |         |         |    |    |    \--- androidx.lifecycle:lifecycle-process:2.7.0 (c)
 |    |    |         |         |         |         |    |    +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1 -> 2.7.0
 |    |    |         |         |         |         |    |    |    +--- androidx.annotation:annotation:1.0.0 -> 1.7.1 (*)
@@ -177,6 +180,7 @@
 |    |    |         |         |         |         |    |    |    |    +--- androidx.lifecycle:lifecycle-livedata-ktx:2.7.0 (c)
 |    |    |         |         |         |         |    |    |    |    +--- androidx.lifecycle:lifecycle-runtime:2.7.0 (c)
 |    |    |         |         |         |         |    |    |    |    +--- androidx.lifecycle:lifecycle-runtime-ktx:2.7.0 (c)
+|    |    |         |         |         |         |    |    |    |    +--- androidx.lifecycle:lifecycle-service:2.7.0 (c)
 |    |    |         |         |         |         |    |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel:2.7.0 (c)
 |    |    |         |         |         |         |    |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 (c)
 |    |    |         |         |         |         |    |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 (c)
@@ -202,6 +206,7 @@
 |    |    |         |         |         |         |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 (c)
 |    |    |         |         |         |         |    |    |    +--- androidx.lifecycle:lifecycle-common-java8:2.7.0 (c)
 |    |    |         |         |         |         |    |    |    +--- androidx.lifecycle:lifecycle-common:2.7.0 (c)
+|    |    |         |         |         |         |    |    |    +--- androidx.lifecycle:lifecycle-service:2.7.0 (c)
 |    |    |         |         |         |         |    |    |    \--- androidx.lifecycle:lifecycle-process:2.7.0 (c)
 |    |    |         |         |         |         |    |    +--- androidx.profileinstaller:profileinstaller:1.3.0 (*)
 |    |    |         |         |         |         |    |    +--- androidx.savedstate:savedstate:1.2.1 (*)
@@ -226,6 +231,7 @@
 |    |    |         |         |         |         |    |    +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 (c)
 |    |    |         |         |         |         |    |    +--- androidx.lifecycle:lifecycle-common-java8:2.7.0 (c)
 |    |    |         |         |         |         |    |    +--- androidx.lifecycle:lifecycle-common:2.7.0 (c)
+|    |    |         |         |         |         |    |    +--- androidx.lifecycle:lifecycle-service:2.7.0 (c)
 |    |    |         |         |         |         |    |    \--- androidx.lifecycle:lifecycle-process:2.7.0 (c)
 |    |    |         |         |         |         |    +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1 -> 2.7.0
 |    |    |         |         |         |         |    |    +--- androidx.lifecycle:lifecycle-viewmodel:2.7.0 (*)
@@ -242,6 +248,7 @@
 |    |    |         |         |         |         |    |    +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 (c)
 |    |    |         |         |         |         |    |    +--- androidx.lifecycle:lifecycle-common-java8:2.7.0 (c)
 |    |    |         |         |         |         |    |    +--- androidx.lifecycle:lifecycle-common:2.7.0 (c)
+|    |    |         |         |         |         |    |    +--- androidx.lifecycle:lifecycle-service:2.7.0 (c)
 |    |    |         |         |         |         |    |    \--- androidx.lifecycle:lifecycle-process:2.7.0 (c)
 |    |    |         |         |         |         |    +--- androidx.savedstate:savedstate-ktx:1.2.1
 |    |    |         |         |         |         |    |    +--- androidx.savedstate:savedstate:1.2.1 (*)
@@ -336,6 +343,7 @@
 |    |    |         |         |         |         |         |    |    +--- androidx.lifecycle:lifecycle-livedata-ktx:2.7.0 (c)
 |    |    |         |         |         |         |         |    |    +--- androidx.lifecycle:lifecycle-runtime:2.7.0 (c)
 |    |    |         |         |         |         |         |    |    +--- androidx.lifecycle:lifecycle-runtime-ktx:2.7.0 (c)
+|    |    |         |         |         |         |         |    |    +--- androidx.lifecycle:lifecycle-service:2.7.0 (c)
 |    |    |         |         |         |         |         |    |    +--- androidx.lifecycle:lifecycle-viewmodel:2.7.0 (c)
 |    |    |         |         |         |         |         |    |    +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 (c)
 |    |    |         |         |         |         |         |    |    +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 (c)
@@ -523,6 +531,7 @@
 |    |    |    |    |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 (c)
 |    |    |    |    |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 (c)
 |    |    |    |    |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0 (c)
+|    |    |    |    |    |    |    +--- androidx.lifecycle:lifecycle-service:2.7.0 (c)
 |    |    |    |    |    |    |    \--- androidx.lifecycle:lifecycle-process:2.7.0 (c)
 |    |    |    |    |    |    +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 1.9.22 (*)
 |    |    |    |    |    |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.7.3 (*)
@@ -537,6 +546,7 @@
 |    |    |    |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 (c)
 |    |    |    |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 (c)
 |    |    |    |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0 (c)
+|    |    |    |    |    |    +--- androidx.lifecycle:lifecycle-service:2.7.0 (c)
 |    |    |    |    |    |    \--- androidx.lifecycle:lifecycle-process:2.7.0 (c)
 |    |    |    |    |    \--- androidx.lifecycle:lifecycle-viewmodel:2.0.0 -> 2.7.0 (*)
 |    |    |    |    +--- androidx.profileinstaller:profileinstaller:1.3.0 (*)
@@ -597,6 +607,7 @@
 |    |    |    |    +--- androidx.lifecycle:lifecycle-livedata-ktx:2.7.0 (c)
 |    |    |    |    +--- androidx.lifecycle:lifecycle-runtime:2.7.0 (c)
 |    |    |    |    +--- androidx.lifecycle:lifecycle-runtime-ktx:2.7.0 (c)
+|    |    |    |    +--- androidx.lifecycle:lifecycle-service:2.7.0 (c)
 |    |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel:2.7.0 (c)
 |    |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 (c)
 |    |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 (c)
@@ -613,6 +624,7 @@
 |    |    |    +--- androidx.lifecycle:lifecycle-livedata-ktx:2.7.0 (c)
 |    |    |    +--- androidx.lifecycle:lifecycle-runtime:2.7.0 (c)
 |    |    |    +--- androidx.lifecycle:lifecycle-runtime-ktx:2.7.0 (c)
+|    |    |    +--- androidx.lifecycle:lifecycle-service:2.7.0 (c)
 |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel:2.7.0 (c)
 |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 (c)
 |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0 (c)
@@ -863,6 +875,7 @@
 |    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 (c)
 |    |    |    +--- androidx.lifecycle:lifecycle-common-java8:2.7.0 (c)
 |    |    |    +--- androidx.lifecycle:lifecycle-common:2.7.0 (c)
+|    |    |    +--- androidx.lifecycle:lifecycle-service:2.7.0 (c)
 |    |    |    \--- androidx.lifecycle:lifecycle-process:2.7.0 (c)
 |    |    +--- androidx.core:core-ktx:1.12.0 (*)
 |    |    +--- androidx.activity:activity-ktx:1.8.2 (*)
@@ -881,6 +894,51 @@
 +--- androidx.core:core-ktx:1.12.0 (*)
 +--- androidx.lifecycle:lifecycle-runtime-ktx:2.7.0 (*)
 +--- androidx.lifecycle:lifecycle-livedata-ktx:2.7.0 (*)
++--- androidx.work:work-runtime-ktx:2.9.0
+|    +--- androidx.work:work-runtime:2.9.0
+|    |    +--- androidx.annotation:annotation-experimental:1.0.0 -> 1.3.0 (*)
+|    |    +--- androidx.core:core:1.9.0 -> 1.12.0 (*)
+|    |    +--- androidx.lifecycle:lifecycle-livedata:2.5.1 -> 2.7.0 (*)
+|    |    +--- androidx.lifecycle:lifecycle-service:2.5.1 -> 2.7.0
+|    |    |    +--- androidx.lifecycle:lifecycle-runtime:2.7.0 (*)
+|    |    |    +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 1.9.22 (*)
+|    |    |    +--- androidx.lifecycle:lifecycle-common:2.7.0 (c)
+|    |    |    +--- androidx.lifecycle:lifecycle-common-java8:2.7.0 (c)
+|    |    |    +--- androidx.lifecycle:lifecycle-livedata:2.7.0 (c)
+|    |    |    +--- androidx.lifecycle:lifecycle-livedata-core:2.7.0 (c)
+|    |    |    +--- androidx.lifecycle:lifecycle-livedata-core-ktx:2.7.0 (c)
+|    |    |    +--- androidx.lifecycle:lifecycle-livedata-ktx:2.7.0 (c)
+|    |    |    +--- androidx.lifecycle:lifecycle-runtime:2.7.0 (c)
+|    |    |    +--- androidx.lifecycle:lifecycle-runtime-ktx:2.7.0 (c)
+|    |    |    +--- androidx.lifecycle:lifecycle-viewmodel:2.7.0 (c)
+|    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 (c)
+|    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 (c)
+|    |    |    +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0 (c)
+|    |    |    \--- androidx.lifecycle:lifecycle-process:2.7.0 (c)
+|    |    +--- androidx.room:room-ktx:2.5.0
+|    |    |    +--- androidx.room:room-common:2.5.0
+|    |    |    |    +--- androidx.annotation:annotation:1.3.0 -> 1.7.1 (*)
+|    |    |    |    \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.20 -> 1.9.10 (*)
+|    |    |    +--- androidx.room:room-runtime:2.5.0
+|    |    |    |    +--- androidx.annotation:annotation-experimental:1.1.0 -> 1.3.0 (*)
+|    |    |    |    +--- androidx.arch.core:core-runtime:2.0.1 -> 2.2.0 (*)
+|    |    |    |    +--- androidx.room:room-common:2.5.0 (*)
+|    |    |    |    +--- androidx.sqlite:sqlite:2.3.0
+|    |    |    |    |    +--- androidx.annotation:annotation:1.0.0 -> 1.7.1 (*)
+|    |    |    |    |    \--- org.jetbrains.kotlin:kotlin-stdlib:1.7.20 -> 1.9.22 (*)
+|    |    |    |    \--- androidx.sqlite:sqlite-framework:2.3.0
+|    |    |    |         +--- androidx.annotation:annotation:1.2.0 -> 1.7.1 (*)
+|    |    |    |         +--- androidx.sqlite:sqlite:2.3.0 (*)
+|    |    |    |         \--- org.jetbrains.kotlin:kotlin-stdlib:1.7.20 -> 1.9.22 (*)
+|    |    |    +--- org.jetbrains.kotlin:kotlin-stdlib:1.7.20 -> 1.9.22 (*)
+|    |    |    \--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 -> 1.7.3 (*)
+|    |    +--- androidx.sqlite:sqlite-framework:2.3.0 (*)
+|    |    +--- androidx.startup:startup-runtime:1.1.1 (*)
+|    |    +--- com.google.guava:listenablefuture:1.0
+|    |    +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 1.9.22 (*)
+|    |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1 -> 1.7.3 (*)
+|    |    \--- androidx.work:work-runtime-ktx:2.9.0 (c)
+|    \--- androidx.work:work-runtime:2.9.0 (c)
 +--- androidx.activity:activity-compose:1.8.2 (*)
 +--- androidx.compose.material:material:1.5.4 (*)
 +--- androidx.compose.runtime:runtime-livedata:1.5.4
diff --git a/financial-connections-example/src/main/AndroidManifest.xml b/financial-connections-example/src/main/AndroidManifest.xml
index d3a7c027fb8..cd1365a985a 100644
--- a/financial-connections-example/src/main/AndroidManifest.xml
+++ b/financial-connections-example/src/main/AndroidManifest.xml
@@ -34,6 +34,8 @@
             android:theme="@style/AppTheme.NoActionBar" />
         
             
diff --git a/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/FinancialConnectionsPlaygroundActivity.kt b/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/FinancialConnectionsPlaygroundActivity.kt
index f707ba76cfe..e16a78157dc 100644
--- a/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/FinancialConnectionsPlaygroundActivity.kt
+++ b/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/FinancialConnectionsPlaygroundActivity.kt
@@ -5,7 +5,6 @@ import androidx.activity.compose.setContent
 import androidx.activity.viewModels
 import androidx.appcompat.app.AppCompatActivity
 import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -144,10 +143,10 @@ class FinancialConnectionsPlaygroundActivity : AppCompatActivity() {
             },
             content = {
                 PlaygroundContent(
-                    padding = it,
                     state = state,
                     onSettingsChanged = onSettingsChanged,
-                    onButtonClick = onButtonClick
+                    onButtonClick = onButtonClick,
+                    modifier = Modifier.padding(it),
                 )
             }
         )
@@ -157,56 +156,71 @@ class FinancialConnectionsPlaygroundActivity : AppCompatActivity() {
     @Composable
     @Suppress("LongMethod")
     private fun PlaygroundContent(
-        padding: PaddingValues,
         state: FinancialConnectionsPlaygroundState,
         onSettingsChanged: (PlaygroundSettings) -> Unit,
-        onButtonClick: () -> Unit
+        onButtonClick: () -> Unit,
+        modifier: Modifier = Modifier,
     ) {
-        Column(
-            modifier = Modifier
-                .padding(padding)
-                .padding(16.dp)
+        LazyColumn(
+            contentPadding = PaddingValues(16.dp),
+            modifier = modifier,
         ) {
-            SettingsUi(
-                playgroundSettings = state.settings,
-                onSettingsChanged = onSettingsChanged
-            )
+            item {
+                SettingsUi(
+                    playgroundSettings = state.settings,
+                    onSettingsChanged = onSettingsChanged
+                )
+            }
+
             if (state.loading) {
-                LinearProgressIndicator(
-                    modifier = Modifier.fillMaxWidth(),
+                item {
+                    LinearProgressIndicator(
+                        modifier = Modifier.fillMaxWidth(),
+                    )
+                }
+            }
+
+            item {
+                Divider(Modifier.padding(vertical = 8.dp))
+            }
+
+            item {
+                Text(
+                    text = "backend: ${state.backendUrl}",
+                    color = MaterialTheme.colors.secondaryVariant
                 )
             }
-            Divider(Modifier.padding(vertical = 8.dp))
-            Text(
-                text = "backend: ${state.backendUrl}",
-                color = MaterialTheme.colors.secondaryVariant
-            )
-            Text(
-                text = "env: ${BuildConfig.TEST_ENVIRONMENT}",
-                color = MaterialTheme.colors.secondaryVariant
-            )
-            Button(
-                modifier = Modifier
-                    .semantics { testTagsAsResourceId = true }
-                    .testTag("connect_accounts"),
-                onClick = onButtonClick,
-            ) {
-                Text("Connect Accounts")
+
+            item {
+                Text(
+                    text = "env: ${BuildConfig.TEST_ENVIRONMENT}",
+                    color = MaterialTheme.colors.secondaryVariant
+                )
+            }
+
+            item {
+                Button(
+                    modifier = Modifier
+                        .semantics { testTagsAsResourceId = true }
+                        .testTag("connect_accounts"),
+                    onClick = onButtonClick,
+                ) {
+                    Text("Connect Accounts")
+                }
             }
-            LazyColumn {
-                items(state.status) { item ->
-                    Row(Modifier.padding(4.dp), verticalAlignment = Alignment.Top) {
-                        val primary = MaterialTheme.colors.primary
-                        Canvas(
-                            modifier = Modifier
-                                .padding(end = 8.dp, top = 6.dp)
-                                .size(6.dp)
-                        ) {
-                            drawCircle(primary)
-                        }
-                        SelectionContainer {
-                            Text(text = item, fontSize = 12.sp)
-                        }
+
+            items(state.status) { item ->
+                Row(Modifier.padding(4.dp), verticalAlignment = Alignment.Top) {
+                    val primary = MaterialTheme.colors.primary
+                    Canvas(
+                        modifier = Modifier
+                            .padding(end = 8.dp, top = 6.dp)
+                            .size(6.dp)
+                    ) {
+                        drawCircle(primary)
+                    }
+                    SelectionContainer {
+                        Text(text = item, fontSize = 12.sp)
                     }
                 }
             }
diff --git a/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/settings/EmailSetting.kt b/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/settings/EmailSetting.kt
index 871c2e107b1..682dc0bb343 100644
--- a/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/settings/EmailSetting.kt
+++ b/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/settings/EmailSetting.kt
@@ -4,8 +4,9 @@ import com.stripe.android.financialconnections.example.data.model.LinkAccountSes
 import com.stripe.android.financialconnections.example.data.model.PaymentIntentBody
 
 internal data class EmailSetting(
-    override val selectedOption: String = ""
-) : SingleChoiceSetting(
+    override val selectedOption: String = "",
+    override val key: String = "email",
+) : Saveable, SingleChoiceSetting(
     displayName = "Customer email",
     options = emptyList(),
     selectedOption = selectedOption
@@ -24,4 +25,7 @@ internal data class EmailSetting(
     ): List> {
         return replace(currentSettings, this.copy(selectedOption = value))
     }
+
+    override fun convertToString(value: String): String = value
+    override fun convertToValue(value: String): String = value
 }
diff --git a/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/settings/SettingsUi.kt b/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/settings/SettingsUi.kt
index 21318c76e40..43eeacd6d95 100644
--- a/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/settings/SettingsUi.kt
+++ b/financial-connections-example/src/main/java/com/stripe/android/financialconnections/example/settings/SettingsUi.kt
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.material.Checkbox
 import androidx.compose.material.DropdownMenuItem
 import androidx.compose.material.ExperimentalMaterialApi
@@ -30,6 +31,7 @@ import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.testTagsAsResourceId
 import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.unit.dp
 
 @Composable
@@ -161,6 +163,9 @@ private fun TextSetting(
         placeholder = { Text(text = name) },
         label = { Text(text = name) },
         value = value,
+        keyboardOptions = KeyboardOptions(
+            imeAction = ImeAction.Done,
+        ),
         onValueChange = { newValue: String ->
             onOptionChanged(newValue)
         },
diff --git a/financial-connections/api/financial-connections.api b/financial-connections/api/financial-connections.api
index 0440e3997d8..e31e5f71620 100644
--- a/financial-connections/api/financial-connections.api
+++ b/financial-connections/api/financial-connections.api
@@ -306,9 +306,11 @@ public final class com/stripe/android/financialconnections/features/accountpicke
 	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/accountpicker/ComposableSingletons$AccountPickerScreenKt;
 	public static field lambda-1 Lkotlin/jvm/functions/Function3;
 	public static field lambda-2 Lkotlin/jvm/functions/Function3;
+	public static field lambda-3 Lkotlin/jvm/functions/Function4;
 	public fun  ()V
 	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function3;
 	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-3$financial_connections_release ()Lkotlin/jvm/functions/Function4;
 }
 
 public final class com/stripe/android/financialconnections/features/attachpayment/ComposableSingletons$AttachPaymentScreenKt {
@@ -318,151 +320,158 @@ public final class com/stripe/android/financialconnections/features/attachpaymen
 	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 }
 
-public final class com/stripe/android/financialconnections/features/common/ComposableSingletons$AccessibleDataCalloutKt {
-	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/common/ComposableSingletons$AccessibleDataCalloutKt;
+public final class com/stripe/android/financialconnections/features/common/ComposableSingletons$AccountItemKt {
+	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/common/ComposableSingletons$AccountItemKt;
+	public static field lambda-1 Lkotlin/jvm/functions/Function2;
+	public static field lambda-2 Lkotlin/jvm/functions/Function3;
+	public static field lambda-3 Lkotlin/jvm/functions/Function2;
+	public fun  ()V
+	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function2;
+	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-3$financial_connections_release ()Lkotlin/jvm/functions/Function2;
+}
+
+public final class com/stripe/android/financialconnections/features/common/ComposableSingletons$ErrorContentKt {
+	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/common/ComposableSingletons$ErrorContentKt;
 	public static field lambda-1 Lkotlin/jvm/functions/Function2;
 	public static field lambda-2 Lkotlin/jvm/functions/Function2;
 	public static field lambda-3 Lkotlin/jvm/functions/Function2;
-	public static field lambda-4 Lkotlin/jvm/functions/Function2;
+	public static field lambda-4 Lkotlin/jvm/functions/Function3;
 	public static field lambda-5 Lkotlin/jvm/functions/Function2;
-	public static field lambda-6 Lkotlin/jvm/functions/Function2;
 	public fun  ()V
 	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 	public final fun getLambda-3$financial_connections_release ()Lkotlin/jvm/functions/Function2;
-	public final fun getLambda-4$financial_connections_release ()Lkotlin/jvm/functions/Function2;
+	public final fun getLambda-4$financial_connections_release ()Lkotlin/jvm/functions/Function3;
 	public final fun getLambda-5$financial_connections_release ()Lkotlin/jvm/functions/Function2;
-	public final fun getLambda-6$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 }
 
-public final class com/stripe/android/financialconnections/features/common/ComposableSingletons$CloseDialogKt {
-	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/common/ComposableSingletons$CloseDialogKt;
-	public static field lambda-1 Lkotlin/jvm/functions/Function3;
+public final class com/stripe/android/financialconnections/features/common/ComposableSingletons$LoadingContentKt {
+	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/common/ComposableSingletons$LoadingContentKt;
+	public static field lambda-1 Lkotlin/jvm/functions/Function2;
 	public static field lambda-2 Lkotlin/jvm/functions/Function3;
 	public static field lambda-3 Lkotlin/jvm/functions/Function2;
 	public static field lambda-4 Lkotlin/jvm/functions/Function2;
+	public static field lambda-5 Lkotlin/jvm/functions/Function3;
+	public static field lambda-6 Lkotlin/jvm/functions/Function3;
+	public static field lambda-7 Lkotlin/jvm/functions/Function3;
+	public static field lambda-8 Lkotlin/jvm/functions/Function2;
 	public fun  ()V
-	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function3;
 	public final fun getLambda-3$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 	public final fun getLambda-4$financial_connections_release ()Lkotlin/jvm/functions/Function2;
+	public final fun getLambda-5$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-6$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-7$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-8$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 }
 
-public final class com/stripe/android/financialconnections/features/common/ComposableSingletons$ErrorContentKt {
-	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/common/ComposableSingletons$ErrorContentKt;
+public final class com/stripe/android/financialconnections/features/common/ComposableSingletons$MerchantDataAccessTextKt {
+	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/common/ComposableSingletons$MerchantDataAccessTextKt;
 	public static field lambda-1 Lkotlin/jvm/functions/Function2;
 	public static field lambda-2 Lkotlin/jvm/functions/Function3;
 	public static field lambda-3 Lkotlin/jvm/functions/Function2;
-	public static field lambda-4 Lkotlin/jvm/functions/Function2;
-	public static field lambda-5 Lkotlin/jvm/functions/Function3;
-	public static field lambda-6 Lkotlin/jvm/functions/Function2;
-	public static field lambda-7 Lkotlin/jvm/functions/Function2;
-	public static field lambda-8 Lkotlin/jvm/functions/Function3;
-	public static field lambda-9 Lkotlin/jvm/functions/Function2;
 	public fun  ()V
 	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function3;
 	public final fun getLambda-3$financial_connections_release ()Lkotlin/jvm/functions/Function2;
-	public final fun getLambda-4$financial_connections_release ()Lkotlin/jvm/functions/Function2;
-	public final fun getLambda-5$financial_connections_release ()Lkotlin/jvm/functions/Function3;
-	public final fun getLambda-6$financial_connections_release ()Lkotlin/jvm/functions/Function2;
-	public final fun getLambda-7$financial_connections_release ()Lkotlin/jvm/functions/Function2;
-	public final fun getLambda-8$financial_connections_release ()Lkotlin/jvm/functions/Function3;
-	public final fun getLambda-9$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 }
 
-public final class com/stripe/android/financialconnections/features/common/ComposableSingletons$ModalBottomSheetContentKt {
-	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/common/ComposableSingletons$ModalBottomSheetContentKt;
+public final class com/stripe/android/financialconnections/features/common/ComposableSingletons$SharedPartnerAuthKt {
+	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/common/ComposableSingletons$SharedPartnerAuthKt;
 	public static field lambda-1 Lkotlin/jvm/functions/Function3;
+	public static field lambda-2 Lkotlin/jvm/functions/Function3;
 	public fun  ()V
 	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function3;
 }
 
-public final class com/stripe/android/financialconnections/features/common/ComposableSingletons$SharedPartnerAuthKt {
-	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/common/ComposableSingletons$SharedPartnerAuthKt;
+public final class com/stripe/android/financialconnections/features/consent/ui/ComposableSingletons$ConsentLogoHeaderKt {
+	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/consent/ui/ComposableSingletons$ConsentLogoHeaderKt;
 	public static field lambda-1 Lkotlin/jvm/functions/Function3;
 	public fun  ()V
 	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function3;
 }
 
-public final class com/stripe/android/financialconnections/features/consent/ComposableSingletons$ConsentScreenKt {
-	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/consent/ComposableSingletons$ConsentScreenKt;
+public final class com/stripe/android/financialconnections/features/error/ComposableSingletons$ErrorScreenKt {
+	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/error/ComposableSingletons$ErrorScreenKt;
+	public static field lambda-1 Lkotlin/jvm/functions/Function2;
+	public fun  ()V
+	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function2;
+}
+
+public final class com/stripe/android/financialconnections/features/exit/ComposableSingletons$ExitModalKt {
+	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/exit/ComposableSingletons$ExitModalKt;
 	public static field lambda-1 Lkotlin/jvm/functions/Function3;
+	public static field lambda-2 Lkotlin/jvm/functions/Function3;
+	public static field lambda-3 Lkotlin/jvm/functions/Function2;
+	public static field lambda-4 Lkotlin/jvm/functions/Function2;
 	public fun  ()V
 	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-3$financial_connections_release ()Lkotlin/jvm/functions/Function2;
+	public final fun getLambda-4$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 }
 
 public final class com/stripe/android/financialconnections/features/institutionpicker/ComposableSingletons$InstitutionPickerScreenKt {
 	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/institutionpicker/ComposableSingletons$InstitutionPickerScreenKt;
-	public static field lambda-1 Lkotlin/jvm/functions/Function2;
-	public static field lambda-2 Lkotlin/jvm/functions/Function2;
+	public static field lambda-1 Lkotlin/jvm/functions/Function3;
+	public static field lambda-2 Lkotlin/jvm/functions/Function3;
 	public static field lambda-3 Lkotlin/jvm/functions/Function3;
-	public static field lambda-4 Lkotlin/jvm/functions/Function3;
-	public static field lambda-5 Lkotlin/jvm/functions/Function3;
-	public static field lambda-6 Lkotlin/jvm/functions/Function3;
+	public static field lambda-4 Lkotlin/jvm/functions/Function2;
+	public static field lambda-5 Lkotlin/jvm/functions/Function2;
 	public fun  ()V
-	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function2;
-	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function2;
+	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function3;
 	public final fun getLambda-3$financial_connections_release ()Lkotlin/jvm/functions/Function3;
-	public final fun getLambda-4$financial_connections_release ()Lkotlin/jvm/functions/Function3;
-	public final fun getLambda-5$financial_connections_release ()Lkotlin/jvm/functions/Function3;
-	public final fun getLambda-6$financial_connections_release ()Lkotlin/jvm/functions/Function3;
-}
-
-public final class com/stripe/android/financialconnections/features/linkstepupverification/ComposableSingletons$LinkStepUpVerificationScreenKt {
-	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/linkstepupverification/ComposableSingletons$LinkStepUpVerificationScreenKt;
-	public static field lambda-1 Lkotlin/jvm/functions/Function2;
-	public static field lambda-2 Lkotlin/jvm/functions/Function2;
-	public fun  ()V
-	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function2;
-	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function2;
+	public final fun getLambda-4$financial_connections_release ()Lkotlin/jvm/functions/Function2;
+	public final fun getLambda-5$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 }
 
-public final class com/stripe/android/financialconnections/features/manualentry/ComposableSingletons$ManualEntryScreenKt {
-	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/manualentry/ComposableSingletons$ManualEntryScreenKt;
+public final class com/stripe/android/financialconnections/features/linkaccountpicker/ComposableSingletons$LinkAccountPickerScreenKt {
+	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/linkaccountpicker/ComposableSingletons$LinkAccountPickerScreenKt;
 	public static field lambda-1 Lkotlin/jvm/functions/Function3;
+	public static field lambda-2 Lkotlin/jvm/functions/Function3;
+	public static field lambda-3 Lkotlin/jvm/functions/Function4;
 	public fun  ()V
 	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-3$financial_connections_release ()Lkotlin/jvm/functions/Function4;
 }
 
-public final class com/stripe/android/financialconnections/features/manualentrysuccess/ComposableSingletons$ManualEntrySuccessScreenKt {
-	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/manualentrysuccess/ComposableSingletons$ManualEntrySuccessScreenKt;
+public final class com/stripe/android/financialconnections/features/manualentry/ComposableSingletons$ManualEntryScreenKt {
+	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/manualentry/ComposableSingletons$ManualEntryScreenKt;
 	public static field lambda-1 Lkotlin/jvm/functions/Function3;
-	public static field lambda-2 Lkotlin/jvm/functions/Function2;
-	public static field lambda-3 Lkotlin/jvm/functions/Function2;
-	public static field lambda-4 Lkotlin/jvm/functions/Function2;
-	public static field lambda-5 Lkotlin/jvm/functions/Function2;
 	public fun  ()V
 	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function3;
-	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function2;
-	public final fun getLambda-3$financial_connections_release ()Lkotlin/jvm/functions/Function2;
-	public final fun getLambda-4$financial_connections_release ()Lkotlin/jvm/functions/Function2;
-	public final fun getLambda-5$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 }
 
 public final class com/stripe/android/financialconnections/features/networkinglinkloginwarmup/ComposableSingletons$NetworkingLinkLoginWarmupScreenKt {
 	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/networkinglinkloginwarmup/ComposableSingletons$NetworkingLinkLoginWarmupScreenKt;
-	public static field lambda-1 Lkotlin/jvm/functions/Function2;
+	public static field lambda-1 Lkotlin/jvm/functions/Function3;
+	public static field lambda-2 Lkotlin/jvm/functions/Function3;
+	public static field lambda-3 Lkotlin/jvm/functions/Function3;
 	public fun  ()V
-	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function2;
+	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-3$financial_connections_release ()Lkotlin/jvm/functions/Function3;
 }
 
 public final class com/stripe/android/financialconnections/features/networkinglinkverification/ComposableSingletons$NetworkingLinkVerificationScreenKt {
 	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/networkinglinkverification/ComposableSingletons$NetworkingLinkVerificationScreenKt;
-	public static field lambda-1 Lkotlin/jvm/functions/Function2;
-	public static field lambda-2 Lkotlin/jvm/functions/Function2;
+	public static field lambda-1 Lkotlin/jvm/functions/Function3;
 	public fun  ()V
-	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function2;
-	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function2;
+	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function3;
 }
 
 public final class com/stripe/android/financialconnections/features/networkingsavetolinkverification/ComposableSingletons$NetworkingSaveToLinkVerificationScreenKt {
 	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/networkingsavetolinkverification/ComposableSingletons$NetworkingSaveToLinkVerificationScreenKt;
 	public static field lambda-1 Lkotlin/jvm/functions/Function3;
-	public static field lambda-2 Lkotlin/jvm/functions/Function2;
+	public static field lambda-2 Lkotlin/jvm/functions/Function3;
 	public fun  ()V
 	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function3;
-	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function2;
+	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function3;
 }
 
 public final class com/stripe/android/financialconnections/features/reset/ComposableSingletons$ResetScreenKt {
@@ -472,15 +481,11 @@ public final class com/stripe/android/financialconnections/features/reset/Compos
 	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 }
 
-public final class com/stripe/android/financialconnections/features/success/ComposableSingletons$SuccessScreenKt {
-	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/success/ComposableSingletons$SuccessScreenKt;
+public final class com/stripe/android/financialconnections/features/success/ComposableSingletons$SuccessContentKt {
+	public static final field INSTANCE Lcom/stripe/android/financialconnections/features/success/ComposableSingletons$SuccessContentKt;
 	public static field lambda-1 Lkotlin/jvm/functions/Function3;
-	public static field lambda-2 Lkotlin/jvm/functions/Function2;
-	public static field lambda-3 Lkotlin/jvm/functions/Function2;
 	public fun  ()V
 	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function3;
-	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function2;
-	public final fun getLambda-3$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 }
 
 public final class com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs$Companion {
@@ -839,6 +844,14 @@ public final class com/stripe/android/financialconnections/model/CashBalance$Cre
 	public synthetic fun newArray (I)[Ljava/lang/Object;
 }
 
+public final class com/stripe/android/financialconnections/model/ConnectedAccessNotice$Creator : android/os/Parcelable$Creator {
+	public fun  ()V
+	public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/model/ConnectedAccessNotice;
+	public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
+	public final fun newArray (I)[Lcom/stripe/android/financialconnections/model/ConnectedAccessNotice;
+	public synthetic fun newArray (I)[Ljava/lang/Object;
+}
+
 public final class com/stripe/android/financialconnections/model/ConsentPane$Creator : android/os/Parcelable$Creator {
 	public fun  ()V
 	public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/model/ConsentPane;
@@ -1576,6 +1589,14 @@ public final class com/stripe/android/financialconnections/model/ReturningNetwor
 	public synthetic fun newArray (I)[Ljava/lang/Object;
 }
 
+public final class com/stripe/android/financialconnections/model/ServerLink$Creator : android/os/Parcelable$Creator {
+	public fun  ()V
+	public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/model/ServerLink;
+	public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
+	public final fun newArray (I)[Lcom/stripe/android/financialconnections/model/ServerLink;
+	public synthetic fun newArray (I)[Ljava/lang/Object;
+}
+
 public final class com/stripe/android/financialconnections/model/SynchronizeSessionResponse$Creator : android/os/Parcelable$Creator {
 	public fun  ()V
 	public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/model/SynchronizeSessionResponse;
@@ -1610,6 +1631,9 @@ public final class com/stripe/android/financialconnections/navigation/Composable
 	public static field lambda-14 Lkotlin/jvm/functions/Function3;
 	public static field lambda-15 Lkotlin/jvm/functions/Function3;
 	public static field lambda-16 Lkotlin/jvm/functions/Function3;
+	public static field lambda-17 Lkotlin/jvm/functions/Function3;
+	public static field lambda-18 Lkotlin/jvm/functions/Function3;
+	public static field lambda-19 Lkotlin/jvm/functions/Function3;
 	public static field lambda-2 Lkotlin/jvm/functions/Function3;
 	public static field lambda-3 Lkotlin/jvm/functions/Function3;
 	public static field lambda-4 Lkotlin/jvm/functions/Function3;
@@ -1627,6 +1651,9 @@ public final class com/stripe/android/financialconnections/navigation/Composable
 	public final fun getLambda-14$financial_connections_release ()Lkotlin/jvm/functions/Function3;
 	public final fun getLambda-15$financial_connections_release ()Lkotlin/jvm/functions/Function3;
 	public final fun getLambda-16$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-17$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-18$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-19$financial_connections_release ()Lkotlin/jvm/functions/Function3;
 	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function3;
 	public final fun getLambda-3$financial_connections_release ()Lkotlin/jvm/functions/Function3;
 	public final fun getLambda-4$financial_connections_release ()Lkotlin/jvm/functions/Function3;
@@ -1637,6 +1664,13 @@ public final class com/stripe/android/financialconnections/navigation/Composable
 	public final fun getLambda-9$financial_connections_release ()Lkotlin/jvm/functions/Function3;
 }
 
+public final class com/stripe/android/financialconnections/navigation/bottomsheet/ComposableSingletons$BottomSheetNavigationKt {
+	public static final field INSTANCE Lcom/stripe/android/financialconnections/navigation/bottomsheet/ComposableSingletons$BottomSheetNavigationKt;
+	public static field lambda-1 Lkotlin/jvm/functions/Function4;
+	public fun  ()V
+	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function4;
+}
+
 public final class com/stripe/android/financialconnections/presentation/WebAuthFlowState$Canceled$Creator : android/os/Parcelable$Creator {
 	public fun  ()V
 	public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/presentation/WebAuthFlowState$Canceled;
@@ -1694,11 +1728,24 @@ public final class com/stripe/android/financialconnections/ui/components/Composa
 	public final fun getLambda-6$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 }
 
+public final class com/stripe/android/financialconnections/ui/components/ComposableSingletons$TestModeBannerKt {
+	public static final field INSTANCE Lcom/stripe/android/financialconnections/ui/components/ComposableSingletons$TestModeBannerKt;
+	public static field lambda-1 Lkotlin/jvm/functions/Function2;
+	public static field lambda-2 Lkotlin/jvm/functions/Function2;
+	public fun  ()V
+	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function2;
+	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function2;
+}
+
 public final class com/stripe/android/financialconnections/ui/components/ComposableSingletons$TextFieldKt {
 	public static final field INSTANCE Lcom/stripe/android/financialconnections/ui/components/ComposableSingletons$TextFieldKt;
 	public static field lambda-1 Lkotlin/jvm/functions/Function2;
+	public static field lambda-2 Lkotlin/jvm/functions/Function3;
+	public static field lambda-3 Lkotlin/jvm/functions/Function2;
 	public fun  ()V
 	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function2;
+	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-3$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 }
 
 public final class com/stripe/android/financialconnections/ui/components/ComposableSingletons$TopAppBarKt {
@@ -1707,15 +1754,33 @@ public final class com/stripe/android/financialconnections/ui/components/Composa
 	public static field lambda-2 Lkotlin/jvm/functions/Function2;
 	public static field lambda-3 Lkotlin/jvm/functions/Function2;
 	public static field lambda-4 Lkotlin/jvm/functions/Function2;
-	public static field lambda-5 Lkotlin/jvm/functions/Function2;
-	public static field lambda-6 Lkotlin/jvm/functions/Function2;
 	public fun  ()V
 	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 	public final fun getLambda-3$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 	public final fun getLambda-4$financial_connections_release ()Lkotlin/jvm/functions/Function2;
+}
+
+public final class com/stripe/android/financialconnections/ui/theme/ComposableSingletons$ColorKt {
+	public static final field INSTANCE Lcom/stripe/android/financialconnections/ui/theme/ComposableSingletons$ColorKt;
+	public static field lambda-1 Lkotlin/jvm/functions/Function2;
+	public fun  ()V
+	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function2;
+}
+
+public final class com/stripe/android/financialconnections/ui/theme/ComposableSingletons$LayoutKt {
+	public static final field INSTANCE Lcom/stripe/android/financialconnections/ui/theme/ComposableSingletons$LayoutKt;
+	public static field lambda-1 Lkotlin/jvm/functions/Function3;
+	public static field lambda-2 Lkotlin/jvm/functions/Function3;
+	public static field lambda-3 Lkotlin/jvm/functions/Function3;
+	public static field lambda-4 Lkotlin/jvm/functions/Function2;
+	public static field lambda-5 Lkotlin/jvm/functions/Function2;
+	public fun  ()V
+	public final fun getLambda-1$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-2$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-3$financial_connections_release ()Lkotlin/jvm/functions/Function3;
+	public final fun getLambda-4$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 	public final fun getLambda-5$financial_connections_release ()Lkotlin/jvm/functions/Function2;
-	public final fun getLambda-6$financial_connections_release ()Lkotlin/jvm/functions/Function2;
 }
 
 public final class com/stripe/android/financialconnections/ui/theme/ComposableSingletons$TypeKt {
diff --git a/financial-connections/build.gradle b/financial-connections/build.gradle
index 0ba7b8db0ba..5b695b7e4af 100644
--- a/financial-connections/build.gradle
+++ b/financial-connections/build.gradle
@@ -56,6 +56,7 @@ dependencies {
     testImplementation testLibs.androidx.core
     testImplementation testLibs.androidx.fragment
     testImplementation testLibs.androidx.junit
+    testImplementation testLibs.androidx.lifecycle
     testImplementation testLibs.json
     testImplementation testLibs.junit
     testImplementation testLibs.kotlin.annotations
diff --git a/financial-connections/detekt-baseline.xml b/financial-connections/detekt-baseline.xml
index c373eea4339..43504de42f7 100644
--- a/financial-connections/detekt-baseline.xml
+++ b/financial-connections/detekt-baseline.xml
@@ -1,5 +1,47 @@
 
 
   
-  
+  
+    ConstructorParameterNaming:FinancialConnectionsAuthorizationSession.kt$FinancialConnectionsAuthorizationSession$@SerialName(value = "is_oauth") private val _isOAuth: Boolean? = false
+    ConstructorParameterNaming:PartnerAccountsList.kt$PartnerAccount$@SerialName(value = "allow_selection") private val _allowSelection: Boolean? = null
+    LongMethod:AccountItem.kt$@Composable @Preview internal fun AccountItemPreview()
+    LongMethod:InstitutionPickerScreen.kt$@Composable private fun SearchRow( modifier: Modifier = Modifier, focusRequester: FocusRequester, query: TextFieldValue, onQueryChanged: (TextFieldValue) -> Unit, )
+    LongMethod:InstitutionPickerScreen.kt$private fun LazyListScope.searchResults( isInputEmpty: Boolean, payload: Payload, selectedInstitutionId: String?, onInstitutionSelected: (FinancialConnectionsInstitution, Boolean) -> Unit, institutions: Async<InstitutionResponse>, onManualEntryClick: () -> Unit, onSearchMoreClick: () -> Unit )
+    LongMethod:LinkAccountPickerPreviewParameterProvider.kt$LinkAccountPickerPreviewParameterProvider$private fun partnerAccountList()
+    LongMethod:ManualEntryScreen.kt$@Composable private fun ManualEntryLoaded( scrollState: ScrollState, payload: Payload, linkPaymentAccountStatus: Async<LinkAccountSessionPaymentAccount>, routing: Pair<String?, Int?>, onRoutingEntered: (String) -> Unit, account: Pair<String?, Int?>, onAccountEntered: (String) -> Unit, accountConfirm: Pair<String?, Int?>, onAccountConfirmEntered: (String) -> Unit, isValidForm: Boolean, onSubmit: () -> Unit )
+    MagicNumber:AccountPickerScreen.kt$3
+    MagicNumber:ConsentLogoHeader.kt$3
+    MagicNumber:ConsentLogoHeader.kt$4
+    MagicNumber:ConsentLogoHeader.kt$5
+    MagicNumber:ConsumerSessionExtensions.kt$10
+    MagicNumber:ConsumerSessionExtensions.kt$5
+    MagicNumber:ConsumerSessionExtensions.kt$7
+    MagicNumber:InstitutionPickerScreen.kt$0.5f
+    MagicNumber:InstitutionPickerScreen.kt$0.75f
+    MagicNumber:InstitutionPickerScreen.kt$10
+    MagicNumber:Layout.kt$4
+    MagicNumber:Layout.kt$50
+    MagicNumber:LinkAccountPickerScreen.kt$3
+    MagicNumber:ManualEntryInputValidator.kt$ManualEntryInputValidator$10
+    MagicNumber:ManualEntryInputValidator.kt$ManualEntryInputValidator$3
+    MagicNumber:ManualEntryInputValidator.kt$ManualEntryInputValidator$7
+    MagicNumber:ManualEntryViewModel.kt$ManualEntryViewModel$4
+    MagicNumber:SharedPartnerAuth.kt$.50f
+    MatchingDeclarationName:ServerDrivenUi.kt$BulletUI
+    MatchingDeclarationName:Type.kt$FinancialConnectionsTypography
+    MaxLineLength:FinancialConnectionsUrls.kt$FinancialConnectionsUrls.DataPolicy$const val merchant = "https://support.stripe.com/user/questions/what-data-does-stripe-access-from-my-linked-financial-account"
+    MaxLineLength:FinancialConnectionsUrls.kt$FinancialConnectionsUrls.Disconnect$const val link = "https://support.link.co/questions/connecting-your-bank-account#how-do-i-disconnect-my-connected-bank-account"
+    MaxLineLength:FinancialConnectionsUrls.kt$FinancialConnectionsUrls.PartnerNotice$const val merchant = "https://support.stripe.com/user/questions/what-is-the-relationship-between-stripe-and-stripes-service-providers"
+    MaximumLineLength:com.stripe.android.financialconnections.presentation.FinancialConnectionsUrls.kt:41
+    MaximumLineLength:com.stripe.android.financialconnections.presentation.FinancialConnectionsUrls.kt:46
+    MaximumLineLength:com.stripe.android.financialconnections.presentation.FinancialConnectionsUrls.kt:9
+    NestedBlockDepth:InstitutionPickerScreen.kt$private fun LazyListScope.searchResults( isInputEmpty: Boolean, payload: Payload, selectedInstitutionId: String?, onInstitutionSelected: (FinancialConnectionsInstitution, Boolean) -> Unit, institutions: Async<InstitutionResponse>, onManualEntryClick: () -> Unit, onSearchMoreClick: () -> Unit )
+    SwallowedException:PollAttachPaymentAccount.kt$PollAttachPaymentAccount$e: StripeException
+    SwallowedException:PollAuthorizationSessionAccounts.kt$PollAuthorizationSessionAccounts$e: StripeException
+    SwallowedException:PostAuthorizationSession.kt$PostAuthorizationSession$e: StripeException
+    TooManyFunctions:FinancialConnectionsManifestRepository.kt$FinancialConnectionsManifestRepository
+    TooManyFunctions:FinancialConnectionsManifestRepository.kt$FinancialConnectionsManifestRepositoryImpl : FinancialConnectionsManifestRepository
+    TooManyFunctions:FinancialConnectionsSheetNativeViewModel.kt$FinancialConnectionsSheetNativeViewModel : MavericksViewModel
+    TooManyFunctions:FinancialConnectionsSheetSharedModule.kt$FinancialConnectionsSheetSharedModule$Companion
+  
 
diff --git a/financial-connections/res/drawable/stripe_check_account.png b/financial-connections/res/drawable/stripe_check_account.png
deleted file mode 100644
index 9de99abb457..00000000000
Binary files a/financial-connections/res/drawable/stripe_check_account.png and /dev/null differ
diff --git a/financial-connections/res/drawable/stripe_check_base.png b/financial-connections/res/drawable/stripe_check_base.png
deleted file mode 100644
index 776e62487ca..00000000000
Binary files a/financial-connections/res/drawable/stripe_check_base.png and /dev/null differ
diff --git a/financial-connections/res/drawable/stripe_check_routing.png b/financial-connections/res/drawable/stripe_check_routing.png
deleted file mode 100644
index 1aa9a343945..00000000000
Binary files a/financial-connections/res/drawable/stripe_check_routing.png and /dev/null differ
diff --git a/financial-connections/res/drawable/stripe_consent_logo_ellipsis.xml b/financial-connections/res/drawable/stripe_consent_logo_ellipsis.xml
deleted file mode 100644
index 16cca89cfbd..00000000000
--- a/financial-connections/res/drawable/stripe_consent_logo_ellipsis.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-  
-  
-  
-
diff --git a/financial-connections/res/drawable/stripe_ic_add.xml b/financial-connections/res/drawable/stripe_ic_add.xml
new file mode 100644
index 00000000000..14022ff5375
--- /dev/null
+++ b/financial-connections/res/drawable/stripe_ic_add.xml
@@ -0,0 +1,10 @@
+
+  
+
diff --git a/financial-connections/res/drawable/stripe_ic_arrow_right_circle.xml b/financial-connections/res/drawable/stripe_ic_arrow_right_circle.xml
deleted file mode 100644
index 4da21a9f0a5..00000000000
--- a/financial-connections/res/drawable/stripe_ic_arrow_right_circle.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-  
-
diff --git a/financial-connections/res/drawable/stripe_ic_brandicon_institution_circle.xml b/financial-connections/res/drawable/stripe_ic_brandicon_institution_circle.xml
deleted file mode 100644
index 40e82295f12..00000000000
--- a/financial-connections/res/drawable/stripe_ic_brandicon_institution_circle.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-  
-  
-
diff --git a/financial-connections/res/drawable/stripe_ic_check.xml b/financial-connections/res/drawable/stripe_ic_check.xml
deleted file mode 100644
index cb13d09c321..00000000000
--- a/financial-connections/res/drawable/stripe_ic_check.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-  
-
diff --git a/financial-connections/res/drawable/stripe_ic_check_circle_emtpy.xml b/financial-connections/res/drawable/stripe_ic_check_circle_emtpy.xml
deleted file mode 100644
index 65e406a9f5a..00000000000
--- a/financial-connections/res/drawable/stripe_ic_check_circle_emtpy.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-  
-  
-
diff --git a/financial-connections/res/drawable/stripe_ic_checkbox_no.xml b/financial-connections/res/drawable/stripe_ic_checkbox_no.xml
deleted file mode 100644
index 2e9564b67ea..00000000000
--- a/financial-connections/res/drawable/stripe_ic_checkbox_no.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-  
-
diff --git a/financial-connections/res/drawable/stripe_ic_checkbox_yes.xml b/financial-connections/res/drawable/stripe_ic_checkbox_yes.xml
deleted file mode 100644
index 1cc0e12726d..00000000000
--- a/financial-connections/res/drawable/stripe_ic_checkbox_yes.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-  
-  
-
diff --git a/financial-connections/res/drawable/stripe_ic_info.xml b/financial-connections/res/drawable/stripe_ic_info.xml
new file mode 100644
index 00000000000..6468e0e09f2
--- /dev/null
+++ b/financial-connections/res/drawable/stripe_ic_info.xml
@@ -0,0 +1,10 @@
+
+  
+
diff --git a/financial-connections/res/drawable/stripe_ic_panel_arrow_right.xml b/financial-connections/res/drawable/stripe_ic_panel_arrow_right.xml
new file mode 100644
index 00000000000..21a5bc0a5f7
--- /dev/null
+++ b/financial-connections/res/drawable/stripe_ic_panel_arrow_right.xml
@@ -0,0 +1,10 @@
+
+  
+
diff --git a/financial-connections/res/drawable/stripe_ic_person.xml b/financial-connections/res/drawable/stripe_ic_person.xml
new file mode 100644
index 00000000000..8c1c4a3e41e
--- /dev/null
+++ b/financial-connections/res/drawable/stripe_ic_person.xml
@@ -0,0 +1,12 @@
+
+  
+  
+
diff --git a/financial-connections/res/drawable/stripe_ic_radio_no.xml b/financial-connections/res/drawable/stripe_ic_radio_no.xml
deleted file mode 100644
index 351f582d752..00000000000
--- a/financial-connections/res/drawable/stripe_ic_radio_no.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-  
-
diff --git a/financial-connections/res/drawable/stripe_ic_radio_yes.xml b/financial-connections/res/drawable/stripe_ic_radio_yes.xml
deleted file mode 100644
index 6c65f44fd0b..00000000000
--- a/financial-connections/res/drawable/stripe_ic_radio_yes.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-  
-  
-
diff --git a/financial-connections/res/drawable/stripe_ic_search.xml b/financial-connections/res/drawable/stripe_ic_search.xml
new file mode 100644
index 00000000000..9f9724f6ad6
--- /dev/null
+++ b/financial-connections/res/drawable/stripe_ic_search.xml
@@ -0,0 +1,10 @@
+
+  
+
diff --git a/financial-connections/res/drawable/stripe_prepane_phone_bg.xml b/financial-connections/res/drawable/stripe_prepane_phone_bg.xml
deleted file mode 100644
index cc00d54c918..00000000000
--- a/financial-connections/res/drawable/stripe_prepane_phone_bg.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-
-  
-  
-  
-  
-  
-  
-  
-  
-  
-
diff --git a/financial-connections/res/values/strings.xml b/financial-connections/res/values/strings.xml
index 506a5f61d09..131b927d008 100644
--- a/financial-connections/res/values/strings.xml
+++ b/financial-connections/res/values/strings.xml
@@ -6,14 +6,13 @@
     OK
     Link with %1$s
     Continue
-    Select your bank
-    Double check your spelling and search terms
-    No results
+    Select bank
     Don’t see your bank?
+    Search for more banks
     Enter your account and routing numbers
-    Search is currently unavailable
-    Please try again later
-    Please try again later or enter your bank details manually.
+    No results
+    Try searching another bank
+    Try searching another bank or manually\u00a0enter\u00a0details.
     Establishing connection
     Please wait while we connect to your bank.
     No results for "%1$s"
@@ -36,23 +35,35 @@
     Select accounts
     Confirm
     
-        Link account
-        Link accounts
+        Connect account
+        Connect accounts
     
-    Success!
     Search
     Choose one
-    Data accessible to Stripe: %2$s. Learn more
-    Data accessible to %1$s: %2$s through Stripe. Learn more
-    Data accessible to this business: %2$s through Stripe. Learn more
-    Data accessible to %1$s: %2$s through Link. Learn more
-    Data accessible to this business: %2$s through Link. Learn more
+    Stripe can access %2$s. Learn more
+    %1$s can access %2$s. Learn more
+    This business can access %2$s. Learn more
     account details
     balances
     transactions
     account ownership details
+    Success
+    You can expect micro–deposits to account ••••%1$s in 1–2 days and an email with further instructions.
+    You can expect micro–deposits to your account in 1–2 days and an email with further instructions.
+    
+        Your account was connected
+        Your accounts were connected
+    
+    
+        Your account was connected and saved with Link
+        Your accounts were connected and saved with Link
+    
+    
+        Your account was connected but couldn\'t be saved with Link
+        Your accounts were connected but couldn\'t be saved with Link
+    
     Done
-    You can disconnect your account any time.
+    Back to %1$s
     Stripe works with partners like %1$s to reliably offer access to thousands of financial institutions. Learn more
     No payment accounts available
     
@@ -61,27 +72,19 @@
     
     There was a problem accessing your %1$s account
     Please select another bank or try again
-    Enter bank account details
+    Enter bank details
     Routing number
     Account number
     Confirm account number
-    Continue
+    Submit
     Please enter 9 digits for your routing number.
     Invalid routing number.
     Routing number required.
     Account number required.
     Invalid bank account number: must be at most 17 digits long.
     Your account numbers don\'t match.
-    Your bank information will be verified with micro-deposits to your account.
-    Please enter a checking account.
-    Micro-deposits initiated
-    Micro-deposit initiated
-    Expect two small deposits to the account ending in ••••%1$s in 1–2 business days and an email with additional instructions to verify your bank account.
-    Expect two small deposits to your account in 1–2 business days and an email with additional instructions to verify your bank account.
-    Expect a $0.01 deposit to the account ending in ••••%1$s in 1–2 business days and an email with additional instructions to verify your bank account.
-    Expect a $0.01 deposit to your account in 1–2 business days and an email with additional instructions to verify your bank account.
-    ••••%1$s BANK STATEMENT
-    Link another account
+    Your bank information will be verified via micro-deposits to your account, typically within 1–2 business days. Only checking accounts are supported.
+    Use test account
     
         Linking account
         Linking accounts
@@ -97,9 +100,6 @@
     Your account number couldn’t be accessed at this time
     Please enter your bank details manually
     Please enter your bank details manually or select another bank.
-    Are you sure you want to cancel?
-    You haven\'t finished linking your bank account.
-    If you cancel now, your account will be linked to %1$s but it will not be saved to Link.
     Yes, cancel
     Back
     Close
@@ -109,13 +109,11 @@
     Please select another bank.
     Select the account you\'d like to link.
     Select all accounts
-    Finishing up
-    Hold on, we\'re almost done…
     We’ll use this to verify and manage your account.
-    Continue as
-    Not you? Continue without signing in
-    It looks like you have a Link account. Signing in will let you quickly access your saved bank accounts.
-    Sign in to Link
+    Use information you previously saved with your Link account.
+    Continue with Link
+    Continue with Link
+    Not now
     Save your account to Link
     Enter the code sent to %1$s
     Signing in as %1$s
@@ -123,18 +121,29 @@
     Select an account to connect to this business
     New bank account
     Connect account
-    Check your email to confirm your identity
-    To keep your Link account safe, we periodically need to confirm you\'re you. Enter the code sent to your email %1$s.
+    Confirm your email
+    Enter the code sent to %1$s. We periodically request this extra step to keep your account safe.
     Resend code
-    Sign in to Link
+    Verify it\'s you
     Not now
     Hmm, that code didn’t work. Double check it and try again.
     It looks like the verification code you provided is not valid anymore. Get a new code and try again.
     It looks like the verification code you provided is not valid anymore. Click “Resend code” and try again, or Contact us.
     It looks like the verification code you provided is not valid anymore. Try again, or Contact us.
+    You\'re in test mode.
     We\'re experiencing high volumes. Try again in an hour and if you need immediate assistance, please contact us.
     Something went wrong.
+    Use test code
     Disconnected
+    Exit without connecting?
+    Your bank account won’t be connected to %1$s and all progress will be lost.
+    Your bank account won’t be connected to this business and all progress will be lost.
+    If you cancel now, your account will be linked to %1$s but it will not be saved to Link.
+    If you cancel now, your account will be linked to this business but it will not be saved to Link.
+    Yes, exit
+    Cancel
+    Cancel
+    Email address
     
         Your account was connected to %1$s but could not be saved to Link with Stripe at this time.
         Your accounts were connected to %1$s but could not be saved to Link with Stripe at this time.
diff --git a/financial-connections/src/androidTest/java/com/stripe/android/financialconnections/navigation/DestinationMappersTest.kt b/financial-connections/src/androidTest/java/com/stripe/android/financialconnections/navigation/DestinationMappersTest.kt
index 7c41642ae5f..0cb800a75e1 100644
--- a/financial-connections/src/androidTest/java/com/stripe/android/financialconnections/navigation/DestinationMappersTest.kt
+++ b/financial-connections/src/androidTest/java/com/stripe/android/financialconnections/navigation/DestinationMappersTest.kt
@@ -9,7 +9,6 @@ class DestinationMappersTest {
 
     // Panes that don't have a matching screen on the Android SDK side
     private val nonImplementedPanes = listOf(
-        Pane.UNEXPECTED_ERROR,
         Pane.AUTH_OPTIONS,
         Pane.LINK_CONSENT,
         Pane.LINK_LOGIN,
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt
index 11ffe2c77c2..fe3376f566f 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt
@@ -8,7 +8,13 @@ import androidx.activity.addCallback
 import androidx.activity.compose.setContent
 import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
 import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
 import com.airbnb.mvrx.Mavericks
 import com.airbnb.mvrx.MavericksView
 import com.airbnb.mvrx.withState
@@ -16,7 +22,7 @@ import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffe
 import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.OpenAuthFlowWithUrl
 import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.OpenNativeAuthFlow
 import com.stripe.android.financialconnections.browser.BrowserManager
-import com.stripe.android.financialconnections.features.common.FullScreenGenericLoading
+import com.stripe.android.financialconnections.features.common.LoadingSpinner
 import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs
 import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult
 import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetNativeActivityArgs
@@ -60,7 +66,12 @@ internal class FinancialConnectionsSheetActivity : AppCompatActivity(), Maverick
     @Composable
     private fun Loading() {
         FinancialConnectionsTheme {
-            FullScreenGenericLoading()
+            Box(
+                modifier = Modifier.fillMaxSize(),
+                contentAlignment = Alignment.Center
+            ) {
+                LoadingSpinner(Modifier.size(52.dp))
+            }
         }
     }
 
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt
index 675fc37a90f..c661f97c982 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt
@@ -48,7 +48,6 @@ import kotlinx.coroutines.sync.withLock
 import javax.inject.Inject
 import javax.inject.Named
 
-@Suppress("LongParameterList", "TooManyFunctions")
 internal class FinancialConnectionsSheetViewModel @Inject constructor(
     @Named(APPLICATION_ID) private val applicationId: String,
     private val synchronizeFinancialConnectionsSession: SynchronizeFinancialConnectionsSession,
@@ -139,7 +138,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor(
     }
 
     private fun logNoBrowserAvailableAndFinish() {
-        viewModelScope.launch {
+        withState { state ->
             val error = AppInitializationError("No Web browser available to launch AuthFlow")
             analyticsTracker.logError(
                 "error Launching the Auth Flow",
@@ -148,7 +147,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor(
                 error = error
             )
             finishWithResult(
-                state = awaitState(),
+                state = state,
                 result = Failed(error)
             )
         }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/analytics/FinancialConnectionsAnalyticsEvent.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/analytics/FinancialConnectionsAnalyticsEvent.kt
index 555504d740e..5fb53422f34 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/analytics/FinancialConnectionsAnalyticsEvent.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/analytics/FinancialConnectionsAnalyticsEvent.kt
@@ -196,6 +196,28 @@ internal sealed class FinancialConnectionsAnalyticsEvent(
         ).filterNotNullValues()
     )
 
+    class AccountsSubmitted(
+        accountIds: Set,
+        isSkipAccountSelection: Boolean
+    ) : FinancialConnectionsAnalyticsEvent(
+        name = "account_picker.accounts_submitted",
+        mapOf(
+            "account_ids" to accountIds.joinToString(" "),
+            "is_skip_account_selection" to isSkipAccountSelection.toString(),
+        ).filterNotNullValues()
+    )
+
+    class AccountsAutoSelected(
+        accountIds: Set,
+        isSingleAccount: Boolean
+    ) : FinancialConnectionsAnalyticsEvent(
+        name = "account_picker.accounts_auto_selected",
+        mapOf(
+            "account_ids" to accountIds.joinToString(" "),
+            "is_single_account" to isSingleAccount.toString(),
+        ).filterNotNullValues()
+    )
+
     class PollAttachPaymentsSucceeded(
         authSessionId: String,
         duration: Long
@@ -386,6 +408,13 @@ internal sealed class FinancialConnectionsAnalyticsEvent(
         ).filterNotNullValues()
     )
 
+    class UncaughtException(
+        params: Map,
+    ) : FinancialConnectionsAnalyticsEvent(
+        name = "mobile.uncaught_exception",
+        params = params.filterNotNullValues(),
+    )
+
     override fun toString(): String {
         return "FinancialConnectionsEvent(name='$name', params=$params)"
     }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/analytics/FinancialConnectionsAnalyticsTracker.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/analytics/FinancialConnectionsAnalyticsTracker.kt
index 4c616e00db0..5a7b25e094b 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/analytics/FinancialConnectionsAnalyticsTracker.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/analytics/FinancialConnectionsAnalyticsTracker.kt
@@ -3,8 +3,8 @@ package com.stripe.android.financialconnections.analytics
 import android.content.Context
 import com.stripe.android.core.Logger
 import com.stripe.android.core.exception.StripeException
+import com.stripe.android.core.networking.AnalyticsRequestV2Executor
 import com.stripe.android.core.networking.AnalyticsRequestV2Factory
-import com.stripe.android.core.networking.StripeNetworkClient
 import com.stripe.android.financialconnections.FinancialConnections
 import com.stripe.android.financialconnections.FinancialConnectionsSheet
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsEvent.ErrorCode
@@ -12,17 +12,20 @@ import com.stripe.android.financialconnections.analytics.FinancialConnectionsRes
 import com.stripe.android.financialconnections.domain.GetManifest
 import com.stripe.android.financialconnections.exception.AppInitializationError
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
 import java.util.Locale
 
 /**
  * Event tracker for Financial Connections.
  */
-internal interface FinancialConnectionsAnalyticsTracker {
-
-    suspend fun track(event: FinancialConnectionsAnalyticsEvent): Result
+internal fun interface FinancialConnectionsAnalyticsTracker {
+    fun track(event: FinancialConnectionsAnalyticsEvent)
 }
 
-internal suspend fun FinancialConnectionsAnalyticsTracker.logError(
+internal fun FinancialConnectionsAnalyticsTracker.logError(
     extraMessage: String,
     error: Throwable,
     logger: Logger,
@@ -78,12 +81,11 @@ private fun emitPublicClientErrorEventIfNeeded(error: Throwable) {
 }
 
 internal class FinancialConnectionsAnalyticsTrackerImpl(
-    private val stripeNetworkClient: StripeNetworkClient,
     private val getManifest: GetManifest,
     private val configuration: FinancialConnectionsSheet.Configuration,
-    private val logger: Logger,
     private val locale: Locale,
     context: Context,
+    private val requestExecutor: AnalyticsRequestV2Executor,
 ) : FinancialConnectionsAnalyticsTracker {
 
     private val requestFactory = AnalyticsRequestV2Factory(
@@ -92,8 +94,9 @@ internal class FinancialConnectionsAnalyticsTrackerImpl(
         origin = ORIGIN
     )
 
-    override suspend fun track(event: FinancialConnectionsAnalyticsEvent): Result {
-        return runCatching {
+    @OptIn(DelicateCoroutinesApi::class)
+    override fun track(event: FinancialConnectionsAnalyticsEvent) {
+        GlobalScope.launch(Dispatchers.IO) {
             val eventParams: Map = event.params ?: emptyMap()
             val commonParams = commonParams()
             val request = requestFactory.createRequest(
@@ -101,12 +104,7 @@ internal class FinancialConnectionsAnalyticsTrackerImpl(
                 additionalParams = eventParams + commonParams,
                 includeSDKParams = true
             )
-            stripeNetworkClient.executeRequest(
-                request
-            )
-            logger.debug("EVENT: ${request.eventName}: ${request.params}")
-        }.onFailure {
-            logger.error("Exception while making analytics request", it)
+            requestExecutor.enqueue(request)
         }
     }
 
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetNativeComponent.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetNativeComponent.kt
index 3db2098c58b..a4292f0278b 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetNativeComponent.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetNativeComponent.kt
@@ -8,6 +8,8 @@ import com.stripe.android.financialconnections.features.accountpicker.AccountPic
 import com.stripe.android.financialconnections.features.attachpayment.AttachPaymentSubcomponent
 import com.stripe.android.financialconnections.features.bankauthrepair.BankAuthRepairSubcomponent
 import com.stripe.android.financialconnections.features.consent.ConsentSubcomponent
+import com.stripe.android.financialconnections.features.error.ErrorSubcomponent
+import com.stripe.android.financialconnections.features.exit.ExitSubcomponent
 import com.stripe.android.financialconnections.features.institutionpicker.InstitutionPickerSubcomponent
 import com.stripe.android.financialconnections.features.linkaccountpicker.LinkAccountPickerSubcomponent
 import com.stripe.android.financialconnections.features.linkstepupverification.LinkStepUpVerificationSubcomponent
@@ -54,6 +56,8 @@ internal interface FinancialConnectionsSheetNativeComponent {
     val successSubcomponent: SuccessSubcomponent.Builder
     val attachPaymentSubcomponent: AttachPaymentSubcomponent.Builder
     val resetSubcomponent: ResetSubcomponent.Builder
+    val errorSubcomponent: ErrorSubcomponent.Factory
+    val exitSubcomponent: ExitSubcomponent.Factory
     val networkingLinkSignupSubcomponent: NetworkingLinkSignupSubcomponent.Builder
     val networkingLinkLoginWarmupSubcomponent: NetworkingLinkLoginWarmupSubcomponent.Builder
     val networkingLinkVerificationSubcomponent: NetworkingLinkVerificationSubcomponent.Builder
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetNativeModule.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetNativeModule.kt
index 85d32d5ce20..65d5dbaae6f 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetNativeModule.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetNativeModule.kt
@@ -8,6 +8,8 @@ import com.stripe.android.core.networking.ApiRequest
 import com.stripe.android.core.networking.StripeNetworkClient
 import com.stripe.android.core.version.StripeSdkVersion
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
+import com.stripe.android.financialconnections.domain.HandleError
+import com.stripe.android.financialconnections.domain.RealHandleError
 import com.stripe.android.financialconnections.features.accountpicker.AccountPickerSubcomponent
 import com.stripe.android.financialconnections.features.attachpayment.AttachPaymentSubcomponent
 import com.stripe.android.financialconnections.features.consent.ConsentSubcomponent
@@ -23,6 +25,7 @@ import com.stripe.android.financialconnections.network.FinancialConnectionsReque
 import com.stripe.android.financialconnections.repository.CoreAuthorizationPendingNetworkingRepairRepository
 import com.stripe.android.financialconnections.repository.FinancialConnectionsAccountsRepository
 import com.stripe.android.financialconnections.repository.FinancialConnectionsConsumerSessionRepository
+import com.stripe.android.financialconnections.repository.FinancialConnectionsErrorRepository
 import com.stripe.android.financialconnections.repository.FinancialConnectionsInstitutionsRepository
 import com.stripe.android.financialconnections.repository.FinancialConnectionsManifestRepository
 import com.stripe.android.financialconnections.repository.SaveToLinkWithStripeSucceededRepository
@@ -60,6 +63,11 @@ internal interface FinancialConnectionsSheetNativeModule {
         impl: NavigationManagerImpl
     ): NavigationManager
 
+    @Binds
+    fun bindsHandleError(
+        impl: RealHandleError
+    ): HandleError
+
     companion object {
         @Provides
         @Singleton
@@ -150,6 +158,14 @@ internal interface FinancialConnectionsSheetNativeModule {
             CoroutineScope(SupervisorJob() + workContext)
         )
 
+        @Singleton
+        @Provides
+        fun providesFinancialConnectionsErrorRepository(
+            @IOContext workContext: CoroutineContext
+        ) = FinancialConnectionsErrorRepository(
+            CoroutineScope(SupervisorJob() + workContext)
+        )
+
         @Singleton
         @Provides
         fun providesPartnerToCoreAuthsRepository(
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetSharedModule.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetSharedModule.kt
index 132b4afa822..34453738bd2 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetSharedModule.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetSharedModule.kt
@@ -7,12 +7,16 @@ import com.stripe.android.core.injection.IOContext
 import com.stripe.android.core.injection.STRIPE_ACCOUNT_ID
 import com.stripe.android.core.networking.AnalyticsRequestExecutor
 import com.stripe.android.core.networking.AnalyticsRequestFactory
+import com.stripe.android.core.networking.AnalyticsRequestV2Executor
 import com.stripe.android.core.networking.ApiRequest
 import com.stripe.android.core.networking.DefaultAnalyticsRequestExecutor
+import com.stripe.android.core.networking.DefaultAnalyticsRequestV2Executor
 import com.stripe.android.core.networking.DefaultStripeNetworkClient
 import com.stripe.android.core.networking.NetworkTypeDetector
 import com.stripe.android.core.networking.StripeNetworkClient
 import com.stripe.android.core.utils.ContextUtils.packageInfo
+import com.stripe.android.core.utils.IsWorkManagerAvailable
+import com.stripe.android.core.utils.RealIsWorkManagerAvailable
 import com.stripe.android.financialconnections.FinancialConnectionsSheet
 import com.stripe.android.financialconnections.analytics.DefaultFinancialConnectionsEventReporter
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
@@ -21,8 +25,11 @@ import com.stripe.android.financialconnections.analytics.FinancialConnectionsEve
 import com.stripe.android.financialconnections.domain.GetManifest
 import com.stripe.android.financialconnections.repository.FinancialConnectionsRepository
 import com.stripe.android.financialconnections.repository.FinancialConnectionsRepositoryImpl
+import dagger.Binds
 import dagger.Module
 import dagger.Provides
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
 import kotlinx.serialization.json.Json
 import java.util.Locale
 import javax.inject.Named
@@ -46,91 +53,108 @@ import kotlin.coroutines.CoroutineContext
 @Module(
     includes = [FinancialConnectionsSheetConfigurationModule::class]
 )
-internal object FinancialConnectionsSheetSharedModule {
+internal interface FinancialConnectionsSheetSharedModule {
 
-    @Provides
+    @Binds
     @Singleton
-    internal fun providesApiOptions(
-        @Named(PUBLISHABLE_KEY) publishableKey: String,
-        @Named(STRIPE_ACCOUNT_ID) stripeAccountId: String?
-    ): ApiRequest.Options = ApiRequest.Options(
-        apiKey = publishableKey,
-        stripeAccount = stripeAccountId
-    )
+    fun bindsAnalyticsRequestV2Executor(impl: DefaultAnalyticsRequestV2Executor): AnalyticsRequestV2Executor
 
-    @Provides
-    @Singleton
-    internal fun providesJson(): Json = Json {
-        coerceInputValues = true
-        ignoreUnknownKeys = true
-        isLenient = true
-        encodeDefaults = true
-    }
+    companion object {
 
-    @Provides
-    @Singleton
-    fun provideStripeNetworkClient(
-        @IOContext context: CoroutineContext,
-        logger: Logger
-    ): StripeNetworkClient = DefaultStripeNetworkClient(
-        workContext = context,
-        logger = logger
-    )
+        @Provides
+        @Singleton
+        internal fun providesApiOptions(
+            @Named(PUBLISHABLE_KEY) publishableKey: String,
+            @Named(STRIPE_ACCOUNT_ID) stripeAccountId: String?
+        ): ApiRequest.Options = ApiRequest.Options(
+            apiKey = publishableKey,
+            stripeAccount = stripeAccountId
+        )
 
-    @Singleton
-    @Provides
-    fun providesAnalyticsTracker(
-        context: Application,
-        logger: Logger,
-        getManifest: GetManifest,
-        locale: Locale?,
-        configuration: FinancialConnectionsSheet.Configuration,
-        stripeNetworkClient: StripeNetworkClient
-    ): FinancialConnectionsAnalyticsTracker = FinancialConnectionsAnalyticsTrackerImpl(
-        context = context,
-        configuration = configuration,
-        getManifest = getManifest,
-        logger = logger,
-        locale = locale ?: Locale.getDefault(),
-        stripeNetworkClient = stripeNetworkClient
-    )
+        @Provides
+        @Singleton
+        internal fun providesJson(): Json = Json {
+            coerceInputValues = true
+            ignoreUnknownKeys = true
+            isLenient = true
+            encodeDefaults = true
+        }
 
-    @Provides
-    @Singleton
-    fun providesApiRequestFactory(
-        apiVersion: ApiVersion
-    ): ApiRequest.Factory = ApiRequest.Factory(
-        apiVersion = apiVersion.code
-    )
+        @Provides
+        @Singleton
+        fun provideStripeNetworkClient(
+            @IOContext context: CoroutineContext,
+            logger: Logger
+        ): StripeNetworkClient = DefaultStripeNetworkClient(
+            workContext = context,
+            logger = logger
+        )
 
-    @Provides
-    @Singleton
-    fun provideConnectionsRepository(
-        repository: FinancialConnectionsRepositoryImpl
-    ): FinancialConnectionsRepository = repository
+        @Singleton
+        @Provides
+        fun providesAnalyticsTracker(
+            context: Application,
+            getManifest: GetManifest,
+            locale: Locale?,
+            configuration: FinancialConnectionsSheet.Configuration,
+            requestExecutor: AnalyticsRequestV2Executor,
+        ): FinancialConnectionsAnalyticsTracker = FinancialConnectionsAnalyticsTrackerImpl(
+            context = context,
+            configuration = configuration,
+            getManifest = getManifest,
+            locale = locale ?: Locale.getDefault(),
+            requestExecutor = requestExecutor,
+        )
 
-    @Provides
-    @Singleton
-    fun provideEventReporter(
-        defaultFinancialConnectionsEventReporter: DefaultFinancialConnectionsEventReporter
-    ): FinancialConnectionsEventReporter = defaultFinancialConnectionsEventReporter
+        @Provides
+        @Singleton
+        fun providesApiRequestFactory(
+            apiVersion: ApiVersion
+        ): ApiRequest.Factory = ApiRequest.Factory(
+            apiVersion = apiVersion.code
+        )
 
-    @Provides
-    @Singleton
-    internal fun providesAnalyticsRequestExecutor(
-        executor: DefaultAnalyticsRequestExecutor
-    ): AnalyticsRequestExecutor = executor
+        @Provides
+        @Singleton
+        fun provideConnectionsRepository(
+            repository: FinancialConnectionsRepositoryImpl
+        ): FinancialConnectionsRepository = repository
 
-    @Provides
-    @Singleton
-    internal fun provideAnalyticsRequestFactory(
-        application: Application,
-        @Named(PUBLISHABLE_KEY) publishableKey: String
-    ): AnalyticsRequestFactory = AnalyticsRequestFactory(
-        packageManager = application.packageManager,
-        packageName = application.packageName.orEmpty(),
-        packageInfo = application.packageInfo,
-        publishableKeyProvider = { publishableKey },
-        networkTypeProvider = NetworkTypeDetector(application)::invoke,
-    )
+        @Provides
+        @Singleton
+        fun provideEventReporter(
+            defaultFinancialConnectionsEventReporter: DefaultFinancialConnectionsEventReporter
+        ): FinancialConnectionsEventReporter = defaultFinancialConnectionsEventReporter
+
+        @Provides
+        @Singleton
+        internal fun providesAnalyticsRequestExecutor(
+            executor: DefaultAnalyticsRequestExecutor
+        ): AnalyticsRequestExecutor = executor
+
+        @Provides
+        @Singleton
+        internal fun provideAnalyticsRequestFactory(
+            application: Application,
+            @Named(PUBLISHABLE_KEY) publishableKey: String
+        ): AnalyticsRequestFactory = AnalyticsRequestFactory(
+            packageManager = application.packageManager,
+            packageName = application.packageName.orEmpty(),
+            packageInfo = application.packageInfo,
+            publishableKeyProvider = { publishableKey },
+            networkTypeProvider = NetworkTypeDetector(application)::invoke,
+        )
+
+        @Provides
+        @Singleton
+        internal fun providesIsWorkManagerAvailable(): IsWorkManagerAvailable {
+            return RealIsWorkManagerAvailable
+        }
+
+        @Provides
+        @Singleton
+        internal fun providesIoDispatcher(): CoroutineDispatcher {
+            return Dispatchers.IO
+        }
+    }
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/HandleError.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/HandleError.kt
new file mode 100644
index 00000000000..ffc56b6c0fd
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/HandleError.kt
@@ -0,0 +1,60 @@
+package com.stripe.android.financialconnections.domain
+
+import com.stripe.android.core.Logger
+import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
+import com.stripe.android.financialconnections.analytics.logError
+import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
+import com.stripe.android.financialconnections.navigation.Destination
+import com.stripe.android.financialconnections.navigation.NavigationManager
+import com.stripe.android.financialconnections.repository.FinancialConnectionsErrorRepository
+import javax.inject.Inject
+
+internal interface HandleError {
+    operator fun invoke(
+        extraMessage: String,
+        error: Throwable,
+        pane: FinancialConnectionsSessionManifest.Pane,
+        displayErrorScreen: Boolean
+    )
+}
+
+internal class RealHandleError @Inject constructor(
+    private val errorRepository: FinancialConnectionsErrorRepository,
+    private val analyticsTracker: FinancialConnectionsAnalyticsTracker,
+    private val logger: Logger,
+    private val navigationManager: NavigationManager
+) : HandleError {
+
+    /**
+     * Handle an error by logging it and navigating to the error screen if necessary.
+     *
+     *  - logs error to analytics.
+     *  - logs error locally.
+     *  - logs error to live events listener if needed.
+     *
+     * @param extraMessage a message to include in the analytics event
+     * @param error the error to handle
+     * @param pane the pane where the error occurred
+     * @param displayErrorScreen whether to navigate to the error screen
+     *
+     */
+    override operator fun invoke(
+        extraMessage: String,
+        error: Throwable,
+        pane: FinancialConnectionsSessionManifest.Pane,
+        displayErrorScreen: Boolean
+    ) {
+        analyticsTracker.logError(
+            extraMessage = extraMessage,
+            error = error,
+            logger = logger,
+            pane = pane
+        )
+
+        // Navigate to error screen
+        if (displayErrorScreen) {
+            errorRepository.set(error)
+            navigationManager.tryNavigateTo(route = Destination.Error(referrer = pane))
+        }
+    }
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/NativeAuthFlowCoordinator.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/NativeAuthFlowCoordinator.kt
index ba296e5de57..37899efaa4d 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/NativeAuthFlowCoordinator.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/NativeAuthFlowCoordinator.kt
@@ -35,5 +35,12 @@ internal class NativeAuthFlowCoordinator @Inject constructor() {
                 USER_INITIATED_WITH_CUSTOM_MANUAL_ENTRY("user_initiated_with_custom_manual_entry")
             }
         }
+
+        /**
+         * Triggers a termination of the AuthFlow with an exception.
+         */
+        data class CloseWithError(
+            val cause: Throwable
+        ) : Message
     }
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/NativeAuthFlowRouter.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/NativeAuthFlowRouter.kt
index 6f07e8e2f45..7dac253bf40 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/NativeAuthFlowRouter.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/NativeAuthFlowRouter.kt
@@ -25,7 +25,6 @@ internal class NativeAuthFlowRouter @Inject constructor(
         return killSwitchEnabled.not() && nativeExperimentEnabled
     }
 
-    @Suppress("ComplexCondition")
     suspend fun logExposure(manifest: FinancialConnectionsSessionManifest) {
         debugConfiguration.overriddenNative?.let { return }
         if (nativeKillSwitchActive(manifest).not()) {
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/PollAttachPaymentAccount.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/PollAttachPaymentAccount.kt
index acdb1110a1e..e6863053003 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/PollAttachPaymentAccount.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/PollAttachPaymentAccount.kt
@@ -40,9 +40,7 @@ internal class PollAttachPaymentAccount @Inject constructor(
                     paymentAccount = params,
                     consumerSessionClientSecret = consumerSessionClientSecret
                 )
-            } catch (
-                @Suppress("SwallowedException") e: StripeException
-            ) {
+            } catch (e: StripeException) {
                 throw e.toDomainException(
                     activeInstitution,
                     sync.showManualEntryInErrors()
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/PollAuthorizationSessionAccounts.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/PollAuthorizationSessionAccounts.kt
index 87fac2926f1..b07fbed930a 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/PollAuthorizationSessionAccounts.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/PollAuthorizationSessionAccounts.kt
@@ -56,7 +56,7 @@ internal class PollAuthorizationSessionAccounts @Inject constructor(
                 accounts
             }
         }
-    } catch (@Suppress("SwallowedException") e: StripeException) {
+    } catch (e: StripeException) {
         throw e.toDomainException(
             institution = sync.manifest.activeInstitution,
             businessName = sync.manifest.getBusinessName(),
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/PostAuthorizationSession.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/PostAuthorizationSession.kt
index 755b49fa8a4..f5ec658a931 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/PostAuthorizationSession.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/domain/PostAuthorizationSession.kt
@@ -39,9 +39,7 @@ internal class PostAuthorizationSession @Inject constructor(
                 institution = institution,
                 applicationId = applicationId
             )
-        } catch (
-            @Suppress("SwallowedException") e: StripeException
-        ) {
+        } catch (e: StripeException) {
             throw e.toDomainException(
                 showManualEntry = sync.showManualEntryInErrors(),
                 institution = institution
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/FinancialConnectionsError.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/FinancialConnectionsError.kt
index 82692cc5e79..98e00222cc7 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/FinancialConnectionsError.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/FinancialConnectionsError.kt
@@ -1,5 +1,6 @@
 package com.stripe.android.financialconnections.exception
 
+import com.stripe.android.core.StripeError
 import com.stripe.android.core.exception.StripeException
 
 /**
@@ -15,5 +16,24 @@ internal abstract class FinancialConnectionsError(
     stripeException.cause,
     stripeException.message
 ) {
+
     override fun analyticsValue(): String = "fcError"
+
+    constructor(
+        name: String,
+        stripeError: StripeError? = null,
+        requestId: String? = null,
+        statusCode: Int = 0,
+        cause: Throwable? = null,
+        message: String? = stripeError?.message
+    ) : this(
+        name = name,
+        stripeException = object : StripeException(
+            stripeError = stripeError,
+            requestId = requestId,
+            statusCode = statusCode,
+            cause = cause,
+            message = message
+        ) {}
+    )
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/FinancialConnectionsErrorHandler.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/FinancialConnectionsErrorHandler.kt
new file mode 100644
index 00000000000..7f05062a157
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/FinancialConnectionsErrorHandler.kt
@@ -0,0 +1,35 @@
+package com.stripe.android.financialconnections.exception
+
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent
+import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
+import com.stripe.android.financialconnections.analytics.toEventParams
+import javax.inject.Inject
+
+internal class FinancialConnectionsErrorHandler @Inject constructor(
+    private val analyticsTracker: FinancialConnectionsAnalyticsTracker,
+) {
+
+    fun setup(lifecycleOwner: LifecycleOwner) {
+        val originalHandler = Thread.getDefaultUncaughtExceptionHandler()
+
+        Thread.setDefaultUncaughtExceptionHandler { thread, error ->
+            try {
+                val params = error.toEventParams(extraMessage = null)
+                analyticsTracker.track(FinancialConnectionsAnalyticsEvent.UncaughtException(params))
+            } finally {
+                originalHandler?.uncaughtException(thread, error)
+            }
+        }
+
+        lifecycleOwner.lifecycle.addObserver(
+            object : DefaultLifecycleObserver {
+                override fun onDestroy(owner: LifecycleOwner) {
+                    Thread.setDefaultUncaughtExceptionHandler(originalHandler)
+                    super.onDestroy(owner)
+                }
+            }
+        )
+    }
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/PartnerAuthError.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/PartnerAuthError.kt
new file mode 100644
index 00000000000..20e639e5ff7
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/exception/PartnerAuthError.kt
@@ -0,0 +1,6 @@
+package com.stripe.android.financialconnections.exception
+
+internal class PartnerAuthError(message: String?) : FinancialConnectionsError(
+    name = "PartnerAuthError",
+    message = message,
+)
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/accountpicker/AccountPickerPreviewParameterProvider.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/accountpicker/AccountPickerPreviewParameterProvider.kt
index 7d22b8e5192..22422523b05 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/accountpicker/AccountPickerPreviewParameterProvider.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/accountpicker/AccountPickerPreviewParameterProvider.kt
@@ -1,39 +1,75 @@
-@file:Suppress("LongMethod")
-
 package com.stripe.android.financialconnections.features.accountpicker
 
 import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Loading
 import com.airbnb.mvrx.Success
-import com.stripe.android.financialconnections.features.common.AccessibleDataCalloutModel
+import com.stripe.android.core.exception.APIException
+import com.stripe.android.financialconnections.exception.AccountNoneEligibleForPaymentMethodError
+import com.stripe.android.financialconnections.features.accountpicker.AccountPickerState.SelectionMode
+import com.stripe.android.financialconnections.features.common.MerchantDataAccessModel
+import com.stripe.android.financialconnections.model.Bullet
+import com.stripe.android.financialconnections.model.ConnectedAccessNotice
+import com.stripe.android.financialconnections.model.DataAccessNotice
+import com.stripe.android.financialconnections.model.DataAccessNoticeBody
 import com.stripe.android.financialconnections.model.FinancialConnectionsAccount
+import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
+import com.stripe.android.financialconnections.model.Image
 import com.stripe.android.financialconnections.model.PartnerAccount
 
 internal class AccountPickerPreviewParameterProvider :
     PreviewParameterProvider {
     override val values = sequenceOf(
+        loading(),
+        error(),
         multiSelect(),
         singleSelect(),
-        singleSelectWithConfirm()
     )
 
     override val count: Int
         get() = super.count
 
+    private fun loading() = AccountPickerState(
+        payload = Loading(),
+        selectedIds = emptySet(),
+    )
+
+    private fun error() = AccountPickerState(
+        payload = Fail(
+            AccountNoneEligibleForPaymentMethodError(
+                accountsCount = 1,
+                institution = FinancialConnectionsInstitution(
+                    id = "2",
+                    name = "Institution 2",
+                    url = "Institution 2 url",
+                    featured = false,
+                    featuredOrder = null,
+                    icon = null,
+                    logo = null,
+                    mobileHandoffCapable = false
+                ),
+                merchantName = "Merchant name",
+                stripeException = APIException()
+            )
+        ),
+        selectedIds = emptySet(),
+    )
+
     private fun multiSelect() = AccountPickerState(
         payload = Success(
             AccountPickerState.Payload(
                 skipAccountSelection = false,
                 accounts = partnerAccountList(),
-                selectionMode = AccountPickerState.SelectionMode.CHECKBOXES,
-                accessibleData = accessibleCallout(),
+                dataAccessNotice = dataAccessNotice(),
+                selectionMode = SelectionMode.Multiple,
+                merchantDataAccess = accessibleCallout(),
                 singleAccount = false,
                 stripeDirect = false,
                 businessName = "Random business",
                 userSelectedSingleAccountInInstitution = false,
-                requiresSingleAccountConfirmation = false
             )
         ),
-        selectedIds = setOf("id1"),
+        selectedIds = setOf("id1", "id3"),
     )
 
     private fun singleSelect() = AccountPickerState(
@@ -41,33 +77,46 @@ internal class AccountPickerPreviewParameterProvider :
             AccountPickerState.Payload(
                 skipAccountSelection = false,
                 accounts = partnerAccountList(),
-                selectionMode = AccountPickerState.SelectionMode.RADIO,
-                accessibleData = accessibleCallout(),
+                dataAccessNotice = dataAccessNotice(),
+                selectionMode = SelectionMode.Single,
+                merchantDataAccess = accessibleCallout(),
                 singleAccount = true,
                 stripeDirect = false,
                 businessName = "Random business",
                 userSelectedSingleAccountInInstitution = false,
-                requiresSingleAccountConfirmation = false
             )
         ),
         selectedIds = setOf("id1"),
     )
 
-    private fun singleSelectWithConfirm() = AccountPickerState(
-        payload = Success(
-            AccountPickerState.Payload(
-                skipAccountSelection = false,
-                accounts = partnerAccountList(),
-                selectionMode = AccountPickerState.SelectionMode.RADIO,
-                accessibleData = accessibleCallout(),
-                singleAccount = true,
-                stripeDirect = false,
-                businessName = "Random business",
-                userSelectedSingleAccountInInstitution = false,
-                requiresSingleAccountConfirmation = true
+    private fun dataAccessNotice() = DataAccessNotice(
+        icon = Image("https://www.cdn.stripe.com/12321312321.png"),
+        title = "Goldilocks uses Stripe to link your accounts",
+        subtitle = "Goldilocks will use your account and routing number, balances and transactions:",
+        body = DataAccessNoticeBody(
+            bullets = bullets()
+        ),
+        disclaimer = "Learn more about data access",
+        connectedAccountNotice = ConnectedAccessNotice(
+            subtitle = "Connected account placeholder",
+            body = DataAccessNoticeBody(
+                bullets = bullets()
             )
         ),
-        selectedIds = setOf("id1"),
+        cta = "OK"
+    )
+
+    private fun bullets() = listOf(
+        Bullet(
+            icon = Image("https://www.cdn.stripe.com/12321312321.png"),
+            title = "Account details",
+            content = "Account number, routing number, account type, account nickname."
+        ),
+        Bullet(
+            icon = Image("https://www.cdn.stripe.com/12321312321.png"),
+            title = "Account details",
+            content = "Account number, routing number, account type, account nickname."
+        ),
     )
 
     private fun partnerAccountList() = listOf(
@@ -130,7 +179,7 @@ internal class AccountPickerPreviewParameterProvider :
         ),
     )
 
-    private fun accessibleCallout() = AccessibleDataCalloutModel(
+    private fun accessibleCallout() = MerchantDataAccessModel(
         businessName = "My business",
         permissions = listOf(
             FinancialConnectionsAccount.Permissions.PAYMENT_METHOD,
@@ -138,8 +187,6 @@ internal class AccountPickerPreviewParameterProvider :
             FinancialConnectionsAccount.Permissions.OWNERSHIP,
             FinancialConnectionsAccount.Permissions.TRANSACTIONS
         ),
-        isStripeDirect = false,
-        isNetworking = false,
-        dataPolicyUrl = ""
+        isStripeDirect = false
     )
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/accountpicker/AccountPickerScreen.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/accountpicker/AccountPickerScreen.kt
index 5abcfd913f4..19bb34ee754 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/accountpicker/AccountPickerScreen.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/accountpicker/AccountPickerScreen.kt
@@ -1,30 +1,37 @@
-@file:Suppress("TooManyFunctions", "LongMethod")
-
 package com.stripe.android.financialconnections.features.accountpicker
 
 import androidx.activity.compose.BackHandler
-import androidx.compose.animation.Crossfade
-import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.ModalBottomSheetLayout
+import androidx.compose.material.ModalBottomSheetState
+import androidx.compose.material.ModalBottomSheetValue
 import androidx.compose.material.Text
+import androidx.compose.material.rememberModalBottomSheetState
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.State
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalUriHandler
 import androidx.compose.ui.res.pluralStringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.tooling.preview.PreviewParameter
 import androidx.compose.ui.unit.dp
+import com.airbnb.mvrx.Async
 import com.airbnb.mvrx.Fail
 import com.airbnb.mvrx.Loading
 import com.airbnb.mvrx.Success
@@ -34,195 +41,277 @@ import com.airbnb.mvrx.compose.mavericksViewModel
 import com.stripe.android.financialconnections.R
 import com.stripe.android.financialconnections.exception.AccountLoadError
 import com.stripe.android.financialconnections.exception.AccountNoneEligibleForPaymentMethodError
+import com.stripe.android.financialconnections.features.accountpicker.AccountPickerClickableText.DATA
 import com.stripe.android.financialconnections.features.accountpicker.AccountPickerState.SelectionMode
-import com.stripe.android.financialconnections.features.common.AccessibleDataCallout
-import com.stripe.android.financialconnections.features.common.AccessibleDataCalloutModel
+import com.stripe.android.financialconnections.features.accountpicker.AccountPickerState.ViewEffect.OpenBottomSheet
+import com.stripe.android.financialconnections.features.accountpicker.AccountPickerState.ViewEffect.OpenUrl
 import com.stripe.android.financialconnections.features.common.AccountItem
-import com.stripe.android.financialconnections.features.common.LoadingContent
+import com.stripe.android.financialconnections.features.common.DataAccessBottomSheetContent
+import com.stripe.android.financialconnections.features.common.LoadingShimmerEffect
+import com.stripe.android.financialconnections.features.common.MerchantDataAccessModel
+import com.stripe.android.financialconnections.features.common.MerchantDataAccessText
 import com.stripe.android.financialconnections.features.common.NoAccountsAvailableErrorContent
 import com.stripe.android.financialconnections.features.common.NoSupportedPaymentMethodTypeAccountsErrorContent
 import com.stripe.android.financialconnections.features.common.UnclassifiedErrorContent
-import com.stripe.android.financialconnections.model.FinancialConnectionsAccount
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
 import com.stripe.android.financialconnections.model.PartnerAccount
 import com.stripe.android.financialconnections.presentation.parentViewModel
 import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
-import com.stripe.android.financialconnections.ui.TextResource
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsButton
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsScaffold
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsTopAppBar
+import com.stripe.android.financialconnections.ui.components.elevation
 import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
+import com.stripe.android.financialconnections.ui.theme.LazyLayout
+import com.stripe.android.financialconnections.ui.theme.Neutral900
+import kotlinx.coroutines.launch
 
 @Composable
 internal fun AccountPickerScreen() {
     val viewModel: AccountPickerViewModel = mavericksViewModel()
     val parentViewModel = parentViewModel()
-    BackHandler(true) {}
     val state: State = viewModel.collectAsState()
+    BackHandler(true) {}
+
+    val bottomSheetState = rememberModalBottomSheetState(
+        initialValue = ModalBottomSheetValue.Hidden,
+        skipHalfExpanded = true
+    )
+    val uriHandler = LocalUriHandler.current
+
+    state.value.viewEffect?.let { viewEffect ->
+        LaunchedEffect(viewEffect) {
+            when (viewEffect) {
+                is OpenUrl -> uriHandler.openUri(viewEffect.url)
+                is OpenBottomSheet -> bottomSheetState.show()
+            }
+            viewModel.onViewEffectLaunched()
+        }
+    }
+
     AccountPickerContent(
         state = state.value,
+        bottomSheetState = bottomSheetState,
         onAccountClicked = viewModel::onAccountClicked,
         onSubmit = viewModel::onSubmit,
-        onSelectAllAccountsClicked = viewModel::onSelectAllAccountsClicked,
         onSelectAnotherBank = viewModel::selectAnotherBank,
         onEnterDetailsManually = viewModel::onEnterDetailsManually,
         onLoadAccountsAgain = viewModel::onLoadAccountsAgain,
         onCloseClick = { parentViewModel.onCloseWithConfirmationClick(Pane.ACCOUNT_PICKER) },
-        onCloseFromErrorClick = parentViewModel::onCloseFromErrorClick,
-        onLearnMoreAboutDataAccessClick = viewModel::onLearnMoreAboutDataAccessClick
+        onClickableTextClick = viewModel::onClickableTextClick,
+        onCloseFromErrorClick = parentViewModel::onCloseFromErrorClick
     )
 }
 
 @Composable
 private fun AccountPickerContent(
     state: AccountPickerState,
+    bottomSheetState: ModalBottomSheetState,
     onAccountClicked: (PartnerAccount) -> Unit,
+    onClickableTextClick: (String) -> Unit,
     onSubmit: () -> Unit,
-    onSelectAllAccountsClicked: () -> Unit,
     onSelectAnotherBank: () -> Unit,
     onEnterDetailsManually: () -> Unit,
     onLoadAccountsAgain: () -> Unit,
     onCloseClick: () -> Unit,
-    onLearnMoreAboutDataAccessClick: () -> Unit,
     onCloseFromErrorClick: (Throwable) -> Unit
+) {
+    val scope = rememberCoroutineScope()
+    val lazyListState = rememberLazyListState()
+    ModalBottomSheetLayout(
+        sheetState = bottomSheetState,
+        sheetBackgroundColor = FinancialConnectionsTheme.colors.backgroundSurface,
+        sheetShape = RoundedCornerShape(8.dp),
+        scrimColor = Neutral900.copy(alpha = 0.32f),
+        sheetContent = {
+            when (val dataAccessNotice = state.payload()?.dataAccessNotice) {
+                null -> Unit
+                else -> DataAccessBottomSheetContent(
+                    dataDialog = dataAccessNotice,
+                    onConfirmModalClick = { scope.launch { bottomSheetState.hide() } },
+                    onClickableTextClick = onClickableTextClick
+                )
+            }
+        },
+        content = {
+            AccountPickerMainContent(
+                onCloseClick,
+                lazyListState,
+                state,
+                onSelectAnotherBank,
+                onEnterDetailsManually,
+                onLoadAccountsAgain,
+                onCloseFromErrorClick,
+                onAccountClicked,
+                onClickableTextClick,
+                onSubmit
+            )
+        },
+    )
+}
+
+@Composable
+private fun AccountPickerMainContent(
+    onCloseClick: () -> Unit,
+    lazyListState: LazyListState,
+    state: AccountPickerState,
+    onSelectAnotherBank: () -> Unit,
+    onEnterDetailsManually: () -> Unit,
+    onLoadAccountsAgain: () -> Unit,
+    onCloseFromErrorClick: (Throwable) -> Unit,
+    onAccountClicked: (PartnerAccount) -> Unit,
+    onClickableTextClick: (String) -> Unit,
+    onSubmit: () -> Unit
 ) {
     FinancialConnectionsScaffold(
         topBar = {
             FinancialConnectionsTopAppBar(
-                showBack = false,
-                onCloseClick = onCloseClick
+                allowBackNavigation = false,
+                onCloseClick = onCloseClick,
+                elevation = lazyListState.elevation
             )
         }
     ) {
         when (val payload = state.payload) {
-            Uninitialized, is Loading -> AccountPickerLoading()
-            is Success -> when (payload().shouldSkipPane) {
-                // ensures account picker is not shown momentarily
-                // if account selection should be skipped.
-                true -> AccountPickerLoading()
-                false -> AccountPickerLoaded(
-                    submitEnabled = state.submitEnabled,
-                    submitLoading = state.submitLoading,
-                    accounts = payload().accounts,
-                    allAccountsSelected = state.allAccountsSelected,
-                    subtitle = payload().subtitle,
-                    selectedIds = state.selectedIds,
-                    onAccountClicked = onAccountClicked,
-                    onSubmit = onSubmit,
-                    selectionMode = payload().selectionMode,
-                    accessibleDataCalloutModel = payload().accessibleData,
-                    requiresSingleAccountConfirmation = payload().requiresSingleAccountConfirmation,
-                    onSelectAllAccountsClicked = onSelectAllAccountsClicked,
-                    onLearnMoreAboutDataAccessClick = onLearnMoreAboutDataAccessClick
-                )
-            }
+            is Fail -> {
+                when (val error = payload.error) {
+                    is AccountNoneEligibleForPaymentMethodError ->
+                        NoSupportedPaymentMethodTypeAccountsErrorContent(
+                            exception = error,
+                            onSelectAnotherBank = onSelectAnotherBank
+                        )
 
-            is Fail -> when (val error = payload.error) {
-                is AccountNoneEligibleForPaymentMethodError ->
-                    NoSupportedPaymentMethodTypeAccountsErrorContent(
+                    is AccountLoadError -> NoAccountsAvailableErrorContent(
                         exception = error,
+                        onEnterDetailsManually = onEnterDetailsManually,
+                        onTryAgain = onLoadAccountsAgain,
                         onSelectAnotherBank = onSelectAnotherBank
                     )
 
-                is AccountLoadError -> NoAccountsAvailableErrorContent(
-                    exception = error,
-                    onEnterDetailsManually = onEnterDetailsManually,
-                    onTryAgain = onLoadAccountsAgain,
-                    onSelectAnotherBank = onSelectAnotherBank
-                )
-
-                else -> UnclassifiedErrorContent(
-                    error = error,
-                    onCloseFromErrorClick = onCloseFromErrorClick
-                )
+                    else -> UnclassifiedErrorContent(
+                        error,
+                        onCloseFromErrorClick = onCloseFromErrorClick
+                    )
+                }
             }
+
+            is Loading,
+            is Uninitialized,
+            is Success -> AccountPickerLoaded(
+                payload = payload,
+                state = state,
+                onAccountClicked = onAccountClicked,
+                onClickableTextClick = onClickableTextClick,
+                lazyListState = lazyListState,
+                onSubmit = onSubmit
+            )
         }
     }
 }
 
 @Composable
-private fun AccountPickerLoading() {
-    LoadingContent(
-        title = stringResource(R.string.stripe_account_picker_loading_title),
-        content = stringResource(R.string.stripe_account_picker_loading_desc)
+private fun AccountPickerLoaded(
+    payload: Async,
+    state: AccountPickerState,
+    lazyListState: LazyListState,
+    onAccountClicked: (PartnerAccount) -> Unit,
+    onClickableTextClick: (String) -> Unit,
+    onSubmit: () -> Unit
+) {
+    LazyLayout(
+        lazyListState = lazyListState,
+        verticalArrangement = Arrangement.spacedBy(16.dp),
+        body = {
+            payload()
+                ?.takeIf { it.shouldSkipPane.not() }
+                ?.let {
+                    loadedContent(
+                        payload = it,
+                        state = state,
+                        onAccountClicked = onAccountClicked
+                    )
+                } ?: run { loadingContent() }
+        },
+        footer = {
+            payload()
+                ?.takeIf { it.shouldSkipPane.not() }
+                ?.let {
+                    Footer(
+                        merchantDataAccessModel = it.merchantDataAccess,
+                        onClickableTextClick = onClickableTextClick,
+                        submitEnabled = state.submitEnabled,
+                        submitLoading = state.submitLoading,
+                        onSubmit = onSubmit,
+                        selectedIds = state.selectedIds
+                    )
+                }
+        }
+
     )
 }
 
+private fun LazyListScope.loadedContent(
+    payload: AccountPickerState.Payload,
+    state: AccountPickerState,
+    onAccountClicked: (PartnerAccount) -> Unit
+) {
+    item {
+        Text(
+            modifier = Modifier.fillMaxWidth(),
+            text = stringResource(
+                when (payload.selectionMode) {
+                    SelectionMode.Single -> R.string.stripe_account_picker_singleselect_account
+                    SelectionMode.Multiple -> R.string.stripe_account_picker_multiselect_account
+                }
+            ),
+            style = FinancialConnectionsTheme.typography.headingXLarge
+        )
+    }
+    items(payload.accounts, key = { it.id }) { account ->
+        AccountItem(
+            selected = state.selectedIds.contains(account.id),
+            showInstitutionIcon = false,
+            onAccountClicked = onAccountClicked,
+            account = account,
+        )
+    }
+}
+
+private fun LazyListScope.loadingContent() {
+    item {
+        Text(
+            modifier = Modifier.fillMaxWidth(),
+            text = "Retrieving accounts",
+            style = FinancialConnectionsTheme.typography.headingXLarge
+        )
+    }
+    items(3) {
+        LoadingShimmerEffect {
+            Box(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .height(72.dp)
+                    .clip(RoundedCornerShape(16.dp))
+                    .background(it)
+            )
+        }
+    }
+}
+
 @Composable
-private fun AccountPickerLoaded(
+private fun Footer(
+    merchantDataAccessModel: MerchantDataAccessModel?,
+    onClickableTextClick: (String) -> Unit,
     submitEnabled: Boolean,
     submitLoading: Boolean,
-    accounts: List,
-    allAccountsSelected: Boolean,
-    accessibleDataCalloutModel: AccessibleDataCalloutModel?,
-    requiresSingleAccountConfirmation: Boolean,
-    selectionMode: SelectionMode,
-    selectedIds: Set,
-    onAccountClicked: (PartnerAccount) -> Unit,
-    onSelectAllAccountsClicked: () -> Unit,
     onSubmit: () -> Unit,
-    onLearnMoreAboutDataAccessClick: () -> Unit,
-    subtitle: TextResource?
+    selectedIds: Set
 ) {
-    Column(
-        Modifier
-            .fillMaxSize()
-            .padding(
-                top = 16.dp,
-                start = 24.dp,
-                end = 24.dp,
-                bottom = 24.dp
-            )
-    ) {
-        Column(
-            modifier = Modifier
-                .weight(1f)
-        ) {
-            Text(
-                modifier = Modifier
-                    .fillMaxWidth(),
-                text = stringResource(
-                    when (requiresSingleAccountConfirmation) {
-                        true -> R.string.stripe_account_picker_confirm_account
-                        false -> when (selectionMode) {
-                            SelectionMode.RADIO -> R.string.stripe_account_picker_singleselect_account
-                            SelectionMode.CHECKBOXES -> R.string.stripe_account_picker_multiselect_account
-                        }
-                    }
-                ),
-                style = FinancialConnectionsTheme.typography.subtitle
-            )
-            subtitle?.let {
-                Spacer(modifier = Modifier.size(8.dp))
-                Text(
-                    modifier = Modifier
-                        .fillMaxWidth(),
-                    text = it.toText().toString(),
-                    style = FinancialConnectionsTheme.typography.body
-                )
-            }
-            Spacer(modifier = Modifier.size(24.dp))
-            when (selectionMode) {
-                SelectionMode.RADIO -> SingleSelectContent(
-                    accounts = accounts,
-                    selectedIds = selectedIds,
-                    onAccountClicked = onAccountClicked
-                )
-
-                SelectionMode.CHECKBOXES -> MultiSelectContent(
-                    accounts = accounts,
-                    allAccountsSelected = allAccountsSelected,
-                    selectedIds = selectedIds,
-                    onAccountClicked = onAccountClicked,
-                    onSelectAllAccountsClicked = onSelectAllAccountsClicked
-                )
-            }
-            Spacer(modifier = Modifier.weight(1f))
-        }
-        accessibleDataCalloutModel?.let {
-            AccessibleDataCallout(
-                it,
-                onLearnMoreAboutDataAccessClick
+    Column {
+        merchantDataAccessModel?.let {
+            MerchantDataAccessText(
+                model = it,
+                onLearnMoreClick = { onClickableTextClick(DATA.value) }
             )
         }
         Spacer(modifier = Modifier.size(12.dp))
@@ -234,125 +323,16 @@ private fun AccountPickerLoaded(
                 .fillMaxWidth()
         ) {
             Text(
-                text = when (requiresSingleAccountConfirmation) {
-                    true -> stringResource(R.string.stripe_account_picker_cta_confirm)
-                    false -> pluralStringResource(
-                        count = selectedIds.size,
-                        id = R.plurals.stripe_account_picker_cta_link
-                    )
-                }
-            )
-        }
-    }
-}
+                text = pluralStringResource(
+                    count = selectedIds.size,
+                    id = R.plurals.stripe_account_picker_cta_link
+                )
 
-@Composable
-private fun SingleSelectContent(
-    accounts: List,
-    selectedIds: Set,
-    onAccountClicked: (PartnerAccount) -> Unit
-) {
-    LazyColumn(
-        contentPadding = PaddingValues(bottom = 12.dp),
-        verticalArrangement = Arrangement.spacedBy(12.dp)
-    ) {
-        items(accounts, key = { it.id }) { account ->
-            AccountItem(
-                selected = selectedIds.contains(account.id),
-                onAccountClicked = onAccountClicked,
-                account = account,
-                selectorContent = {
-                    FinancialConnectionRadioButton(
-                        checked = selectedIds.contains(account.id),
-                    )
-                },
             )
         }
     }
 }
 
-@Composable
-private fun MultiSelectContent(
-    accounts: List,
-    selectedIds: Set,
-    onAccountClicked: (PartnerAccount) -> Unit,
-    onSelectAllAccountsClicked: () -> Unit,
-    allAccountsSelected: Boolean
-) {
-    LazyColumn(
-        contentPadding = PaddingValues(bottom = 12.dp),
-        verticalArrangement = Arrangement.spacedBy(12.dp)
-    ) {
-        item("select_all_accounts") {
-            AccountItem(
-                selected = allAccountsSelected,
-                onAccountClicked = { onSelectAllAccountsClicked() },
-                account = PartnerAccount(
-                    id = "select_all_accounts",
-                    _allowSelection = true,
-                    allowSelectionMessage = "",
-                    authorization = "",
-                    category = FinancialConnectionsAccount.Category.UNKNOWN,
-                    subcategory = FinancialConnectionsAccount.Subcategory.UNKNOWN,
-                    name = stringResource(R.string.stripe_account_picker_select_all_accounts),
-                    supportedPaymentMethodTypes = emptyList()
-                ),
-            ) {
-                FinancialConnectionCheckbox(
-                    allAccountsSelected,
-                )
-            }
-        }
-        items(accounts, key = { it.id }) { account ->
-            AccountItem(
-                selected = selectedIds.contains(account.id),
-                onAccountClicked = onAccountClicked,
-                account = account
-            ) {
-                FinancialConnectionCheckbox(
-                    checked = selectedIds.contains(account.id),
-                )
-            }
-        }
-    }
-}
-
-@Composable
-private fun FinancialConnectionCheckbox(
-    checked: Boolean,
-) {
-    Crossfade(targetState = checked) {
-        Image(
-            painter = painterResource(
-                if (it) {
-                    R.drawable.stripe_ic_checkbox_yes
-                } else {
-                    R.drawable.stripe_ic_checkbox_no
-                },
-            ),
-            contentDescription = null,
-        )
-    }
-}
-
-@Composable
-private fun FinancialConnectionRadioButton(
-    checked: Boolean,
-) {
-    Crossfade(targetState = checked) {
-        Image(
-            painter = painterResource(
-                if (it) {
-                    R.drawable.stripe_ic_radio_yes
-                } else {
-                    R.drawable.stripe_ic_radio_no
-                },
-            ),
-            contentDescription = null,
-        )
-    }
-}
-
 @Preview(
     showBackground = true,
     group = "Account Picker Pane",
@@ -366,12 +346,16 @@ internal fun AccountPickerPreview(
             state = state,
             onAccountClicked = {},
             onSubmit = {},
-            onSelectAllAccountsClicked = {},
             onSelectAnotherBank = {},
             onEnterDetailsManually = {},
             onLoadAccountsAgain = {},
             onCloseClick = {},
-            onLearnMoreAboutDataAccessClick = {}
-        ) {}
+            onCloseFromErrorClick = {},
+            onClickableTextClick = {},
+            bottomSheetState = rememberModalBottomSheetState(
+                initialValue = ModalBottomSheetValue.Hidden,
+                skipHalfExpanded = true
+            ),
+        )
     }
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/accountpicker/AccountPickerViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/accountpicker/AccountPickerViewModel.kt
index 83d4d535c9b..5ccb50fa241 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/accountpicker/AccountPickerViewModel.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/accountpicker/AccountPickerViewModel.kt
@@ -9,8 +9,9 @@ import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.ViewModelContext
 import com.stripe.android.core.Logger
 import com.stripe.android.financialconnections.FinancialConnections
-import com.stripe.android.financialconnections.R
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.AccountSelected
+import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.AccountsAutoSelected
+import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.AccountsSubmitted
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.ClickLearnMoreDataAccess
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.ClickLinkAccounts
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.PaneLoaded
@@ -21,9 +22,12 @@ import com.stripe.android.financialconnections.analytics.logError
 import com.stripe.android.financialconnections.domain.GetOrFetchSync
 import com.stripe.android.financialconnections.domain.PollAuthorizationSessionAccounts
 import com.stripe.android.financialconnections.domain.SelectAccounts
+import com.stripe.android.financialconnections.features.accountpicker.AccountPickerClickableText.DATA
 import com.stripe.android.financialconnections.features.accountpicker.AccountPickerState.SelectionMode
-import com.stripe.android.financialconnections.features.common.AccessibleDataCalloutModel
-import com.stripe.android.financialconnections.features.consent.FinancialConnectionsUrlResolver
+import com.stripe.android.financialconnections.features.accountpicker.AccountPickerState.ViewEffect
+import com.stripe.android.financialconnections.features.accountpicker.AccountPickerState.ViewEffect.OpenBottomSheet
+import com.stripe.android.financialconnections.features.common.MerchantDataAccessModel
+import com.stripe.android.financialconnections.model.DataAccessNotice
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
 import com.stripe.android.financialconnections.model.PartnerAccount
 import com.stripe.android.financialconnections.model.PartnerAccountsList
@@ -32,18 +36,19 @@ import com.stripe.android.financialconnections.navigation.Destination.Reset
 import com.stripe.android.financialconnections.navigation.NavigationManager
 import com.stripe.android.financialconnections.navigation.destination
 import com.stripe.android.financialconnections.ui.FinancialConnectionsSheetNativeActivity
-import com.stripe.android.financialconnections.ui.TextResource
+import com.stripe.android.financialconnections.ui.HandleClickableUrl
 import com.stripe.android.financialconnections.utils.measureTimeMillis
 import kotlinx.coroutines.launch
+import java.util.Date
 import javax.inject.Inject
 
-@Suppress("LongParameterList", "TooManyFunctions")
 internal class AccountPickerViewModel @Inject constructor(
     initialState: AccountPickerState,
     private val eventTracker: FinancialConnectionsAnalyticsTracker,
     private val selectAccounts: SelectAccounts,
     private val getOrFetchSync: GetOrFetchSync,
     private val navigationManager: NavigationManager,
+    private val handleClickableUrl: HandleClickableUrl,
     private val logger: Logger,
     private val pollAuthorizationSessionAccounts: PollAuthorizationSessionAccounts
 ) : MavericksViewModel(initialState) {
@@ -58,6 +63,7 @@ internal class AccountPickerViewModel @Inject constructor(
         suspend {
             val state = awaitState()
             val sync = getOrFetchSync()
+            val dataAccessNotice = sync.text?.consent?.dataAccessNotice
             val manifest = sync.manifest
             val activeAuthSession = requireNotNull(manifest.activeAuthSession)
             val (partnerAccountList, millis) = measureTimeMillis {
@@ -80,22 +86,13 @@ internal class AccountPickerViewModel @Inject constructor(
                 skipAccountSelection = partnerAccountList.skipAccountSelection == true ||
                     activeAuthSession.skipAccountSelection == true,
                 accounts = accounts,
-                selectionMode = if (manifest.singleAccount) SelectionMode.RADIO else SelectionMode.CHECKBOXES,
-                accessibleData = AccessibleDataCalloutModel(
+                selectionMode = if (manifest.singleAccount) SelectionMode.Single else SelectionMode.Multiple,
+                dataAccessNotice = dataAccessNotice,
+                merchantDataAccess = MerchantDataAccessModel(
                     businessName = manifest.businessName,
                     permissions = manifest.permissions,
-                    isNetworking = false,
-                    isStripeDirect = manifest.isStripeDirect ?: false,
-                    dataPolicyUrl = FinancialConnectionsUrlResolver.getDataPolicyUrl(manifest)
+                    isStripeDirect = manifest.isStripeDirect ?: false
                 ),
-                /**
-                 * in the special case that this is single account and the institution would have
-                 * skipped account selection but _didn't_ (because we still saw this), we should
-                 * render specific text that tells the user to "confirm" their account.
-                 */
-                requiresSingleAccountConfirmation = activeAuthSession.institutionSkipAccountSelection == true &&
-                    manifest.singleAccount &&
-                    activeAuthSession.isOAuth,
                 singleAccount = manifest.singleAccount,
                 userSelectedSingleAccountInInstitution = manifest.singleAccount &&
                     activeAuthSession.institutionSkipAccountSelection == true &&
@@ -114,23 +111,40 @@ internal class AccountPickerViewModel @Inject constructor(
                 // If account selection has to be skipped, submit all selectable accounts.
                 payload.skipAccountSelection -> submitAccounts(
                     selectedIds = payload.selectableAccounts.map { it.id }.toSet(),
-                    updateLocalCache = false
+                    updateLocalCache = false,
+                    isSkipAccountSelection = true
                 )
                 // the user saw an OAuth account selection screen and selected
                 // just one to send back in a single-account context. treat these as if
                 // we had done account selection, and submit.
                 payload.userSelectedSingleAccountInInstitution -> submitAccounts(
                     selectedIds = setOf(payload.accounts.first().id),
-                    updateLocalCache = true
+                    updateLocalCache = true,
+                    isSkipAccountSelection = true
                 )
 
-                // Auto-select the first selectable account.
-                payload.selectionMode == SelectionMode.RADIO -> setState {
-                    copy(
-                        selectedIds = setOfNotNull(
-                            payload.selectableAccounts.firstOrNull()?.id
+                // Auto-select the first selectable account and log.
+                payload.selectionMode == SelectionMode.Single -> {
+                    val selectedId = setOfNotNull(payload.selectableAccounts.firstOrNull()?.id)
+                    eventTracker.track(
+                        AccountsAutoSelected(
+                            isSingleAccount = true,
+                            accountIds = selectedId
                         )
                     )
+                    setState { copy(selectedIds = selectedId) }
+                }
+
+                // Auto-select all selectable accounts and log.
+                payload.selectionMode == SelectionMode.Multiple -> {
+                    val selectedIds = payload.selectableAccounts.map { it.id }.toSet()
+                    eventTracker.track(
+                        AccountsAutoSelected(
+                            isSingleAccount = false,
+                            accountIds = selectedIds
+                        )
+                    )
+                    setState { copy(selectedIds = selectedIds) }
                 }
             }
         })
@@ -165,8 +179,8 @@ internal class AccountPickerViewModel @Inject constructor(
         state.payload()?.let { payload ->
             val selectedIds = state.selectedIds
             val newSelectedIds = when (payload.selectionMode) {
-                SelectionMode.RADIO -> setOf(account.id)
-                SelectionMode.CHECKBOXES -> if (selectedIds.contains(account.id)) {
+                SelectionMode.Single -> setOf(account.id)
+                SelectionMode.Multiple -> if (selectedIds.contains(account.id)) {
                     selectedIds - account.id
                 } else {
                     selectedIds + account.id
@@ -219,7 +233,11 @@ internal class AccountPickerViewModel @Inject constructor(
         FinancialConnections.emitEvent(name = Name.ACCOUNTS_SELECTED)
         withState { state ->
             state.payload()?.let {
-                submitAccounts(state.selectedIds, updateLocalCache = true)
+                submitAccounts(
+                    selectedIds = state.selectedIds,
+                    updateLocalCache = true,
+                    isSkipAccountSelection = false
+                )
             } ?: run {
                 logger.error("account clicked without available payload.")
             }
@@ -228,9 +246,16 @@ internal class AccountPickerViewModel @Inject constructor(
 
     private fun submitAccounts(
         selectedIds: Set,
-        updateLocalCache: Boolean
+        updateLocalCache: Boolean,
+        isSkipAccountSelection: Boolean
     ) {
         suspend {
+            eventTracker.track(
+                AccountsSubmitted(
+                    accountIds = selectedIds,
+                    isSkipAccountSelection = isSkipAccountSelection
+                )
+            )
             val manifest = getOrFetchSync().manifest
             val accountsList: PartnerAccountsList = selectAccounts(
                 selectedAccountIds = selectedIds,
@@ -255,29 +280,25 @@ internal class AccountPickerViewModel @Inject constructor(
         loadAccounts()
     }
 
-    fun onSelectAllAccountsClicked() = withState { state ->
-        state.payload()?.let { payload ->
-            val selectedIds = state.selectedIds
-            val newIds = if (state.allAccountsSelected) {
-                // unselect all accounts
-                emptySet()
-            } else {
-                // select all accounts
-                payload.selectableAccounts.map { it.id }.toSet()
-            }
-            setState { copy(selectedIds = newIds) }
-            logAccountSelectionChanges(
-                idsBefore = selectedIds,
-                idsAfter = newIds,
-                isSingleAccount = payload.singleAccount
+    fun onClickableTextClick(uri: String) = viewModelScope.launch {
+        val date = Date()
+        handleClickableUrl(
+            currentPane = PANE,
+            uri = uri,
+            onNetworkUrlClicked = {
+                setState { copy(viewEffect = ViewEffect.OpenUrl(uri, date.time)) }
+            },
+            knownDeeplinkActions = mapOf(
+                DATA.value to {
+                    eventTracker.track(ClickLearnMoreDataAccess(PANE))
+                    setState { copy(viewEffect = OpenBottomSheet(date.time)) }
+                }
             )
-        }
+        )
     }
 
-    fun onLearnMoreAboutDataAccessClick() {
-        viewModelScope.launch {
-            eventTracker.track(ClickLearnMoreDataAccess(Pane.ACCOUNT_PICKER))
-        }
+    fun onViewEffectLaunched() {
+        setState { copy(viewEffect = null) }
     }
 
     companion object :
@@ -305,6 +326,7 @@ internal data class AccountPickerState(
     val canRetry: Boolean = true,
     val selectAccounts: Async = Uninitialized,
     val selectedIds: Set = emptySet(),
+    val viewEffect: ViewEffect? = null
 ) : MavericksState {
 
     val submitLoading: Boolean
@@ -313,19 +335,16 @@ internal data class AccountPickerState(
     val submitEnabled: Boolean
         get() = selectedIds.isNotEmpty()
 
-    val allAccountsSelected: Boolean
-        get() = payload()?.selectableAccounts?.count() == selectedIds.count()
-
     data class Payload(
         val skipAccountSelection: Boolean,
         val accounts: List,
+        val dataAccessNotice: DataAccessNotice?,
         val selectionMode: SelectionMode,
-        val accessibleData: AccessibleDataCalloutModel,
+        val merchantDataAccess: MerchantDataAccessModel,
         val singleAccount: Boolean,
         val stripeDirect: Boolean,
         val businessName: String?,
         val userSelectedSingleAccountInInstitution: Boolean,
-        val requiresSingleAccountConfirmation: Boolean
     ) {
 
         val selectableAccounts
@@ -333,18 +352,24 @@ internal data class AccountPickerState(
 
         val shouldSkipPane: Boolean
             get() = skipAccountSelection || userSelectedSingleAccountInInstitution
-
-        val subtitle: TextResource?
-            get() = when {
-                requiresSingleAccountConfirmation -> TextResource.StringId(
-                    R.string.stripe_accountpicker_singleaccount_description
-                )
-
-                else -> null
-            }
     }
 
     enum class SelectionMode {
-        RADIO, CHECKBOXES
+        Single, Multiple
     }
+
+    sealed class ViewEffect {
+        data class OpenUrl(
+            val url: String,
+            val id: Long
+        ) : ViewEffect()
+
+        data class OpenBottomSheet(
+            val id: Long
+        ) : ViewEffect()
+    }
+}
+
+internal enum class AccountPickerClickableText(val value: String) {
+    DATA("stripe://data-access-notice"),
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/attachpayment/AttachPaymentScreen.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/attachpayment/AttachPaymentScreen.kt
index e028b027d1b..49acbbdf826 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/attachpayment/AttachPaymentScreen.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/attachpayment/AttachPaymentScreen.kt
@@ -2,7 +2,6 @@ package com.stripe.android.financialconnections.features.attachpayment
 
 import androidx.activity.compose.BackHandler
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.res.pluralStringResource
 import androidx.compose.ui.tooling.preview.Preview
 import com.airbnb.mvrx.Async
 import com.airbnb.mvrx.Fail
@@ -11,11 +10,9 @@ import com.airbnb.mvrx.Success
 import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.compose.collectAsState
 import com.airbnb.mvrx.compose.mavericksViewModel
-import com.stripe.android.financialconnections.R
 import com.stripe.android.financialconnections.exception.AccountNumberRetrievalError
 import com.stripe.android.financialconnections.features.common.AccountNumberRetrievalErrorContent
 import com.stripe.android.financialconnections.features.common.FullScreenGenericLoading
-import com.stripe.android.financialconnections.features.common.LoadingContent
 import com.stripe.android.financialconnections.features.common.UnclassifiedErrorContent
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane.ATTACH_LINKED_PAYMENT_ACCOUNT
 import com.stripe.android.financialconnections.model.LinkAccountSessionPaymentAccount
@@ -31,7 +28,6 @@ internal fun AttachPaymentScreen() {
     val state = viewModel.collectAsState()
     BackHandler(enabled = true) {}
     AttachPaymentContent(
-        payload = state.value.payload,
         attachPayment = state.value.linkPaymentAccount,
         onSelectAnotherBank = viewModel::onSelectAnotherBank,
         onEnterDetailsManually = viewModel::onEnterDetailsManually,
@@ -42,7 +38,6 @@ internal fun AttachPaymentScreen() {
 
 @Composable
 private fun AttachPaymentContent(
-    payload: Async,
     attachPayment: Async,
     onSelectAnotherBank: () -> Unit,
     onEnterDetailsManually: () -> Unit,
@@ -52,44 +47,17 @@ private fun AttachPaymentContent(
     FinancialConnectionsScaffold(
         topBar = {
             FinancialConnectionsTopAppBar(
-                showBack = false,
+                allowBackNavigation = false,
                 onCloseClick = onCloseClick
             )
         }
     ) {
-        when (payload) {
-            Uninitialized, is Loading -> FullScreenGenericLoading()
-            is Success -> when (attachPayment) {
-                is Loading,
-                is Uninitialized,
-                is Success -> LoadingContent(
-                    title = pluralStringResource(
-                        id = R.plurals.stripe_attachlinkedpaymentaccount_title,
-                        count = payload().accountsCount
-                    ),
-                    content = when (val businessName = payload().businessName) {
-                        null -> pluralStringResource(
-                            id = R.plurals.stripe_attachlinkedpaymentaccount_desc,
-                            count = payload().accountsCount
-                        )
-
-                        else -> pluralStringResource(
-                            id = R.plurals.stripe_attachlinkedpaymentaccount_desc,
-                            count = payload().accountsCount,
-                            businessName
-                        )
-                    }
-                )
-                is Fail -> ErrorContent(
-                    error = attachPayment.error,
-                    onSelectAnotherBank = onSelectAnotherBank,
-                    onEnterDetailsManually = onEnterDetailsManually,
-                    onCloseFromErrorClick = onCloseFromErrorClick
-                )
-            }
-
+        when (attachPayment) {
+            is Loading,
+            is Uninitialized,
+            is Success -> FullScreenGenericLoading()
             is Fail -> ErrorContent(
-                error = payload.error,
+                error = attachPayment.error,
                 onSelectAnotherBank = onSelectAnotherBank,
                 onEnterDetailsManually = onEnterDetailsManually,
                 onCloseFromErrorClick = onCloseFromErrorClick
@@ -129,12 +97,6 @@ internal fun AttachPaymentScreenPreview() {
     FinancialConnectionsPreview {
         AttachPaymentContent(
             attachPayment = Loading(),
-            payload = Success(
-                AttachPaymentState.Payload(
-                    accountsCount = 10,
-                    businessName = "Random Business"
-                )
-            ),
             onSelectAnotherBank = {},
             onEnterDetailsManually = {},
             onCloseClick = {}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/attachpayment/AttachPaymentViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/attachpayment/AttachPaymentViewModel.kt
index 774740818a6..284403e3825 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/attachpayment/AttachPaymentViewModel.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/attachpayment/AttachPaymentViewModel.kt
@@ -7,7 +7,6 @@ import com.airbnb.mvrx.MavericksViewModelFactory
 import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.ViewModelContext
 import com.stripe.android.core.Logger
-import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.PollAttachPaymentsSucceeded
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
 import com.stripe.android.financialconnections.analytics.logError
@@ -27,7 +26,6 @@ import com.stripe.android.financialconnections.ui.FinancialConnectionsSheetNativ
 import com.stripe.android.financialconnections.utils.measureTimeMillis
 import javax.inject.Inject
 
-@Suppress("LongParameterList")
 internal class AttachPaymentViewModel @Inject constructor(
     initialState: AttachPaymentState,
     private val saveToLinkWithStripeSucceeded: SaveToLinkWithStripeSucceededRepository,
@@ -42,14 +40,6 @@ internal class AttachPaymentViewModel @Inject constructor(
 
     init {
         logErrors()
-        suspend {
-            val sync = getOrFetchSync()
-            val manifest = requireNotNull(sync.manifest)
-            AttachPaymentState.Payload(
-                businessName = manifest.businessName,
-                accountsCount = getCachedAccounts().size
-            )
-        }.execute { copy(payload = it) }
         suspend {
             val sync = getOrFetchSync()
             val manifest = requireNotNull(sync.manifest)
@@ -65,10 +55,10 @@ internal class AttachPaymentViewModel @Inject constructor(
                     activeInstitution = activeInstitution,
                     consumerSessionClientSecret = consumerSession?.clientSecret,
                     params = PaymentAccountParams.LinkedAccount(requireNotNull(id))
-                ).also {
-                    val nextPane = it.nextPane ?: Pane.SUCCESS
-                    navigationManager.tryNavigateTo(nextPane.destination(referrer = PANE))
-                }
+                )
+            }
+            if (manifest.isNetworkingUserFlow == true && manifest.accountholderIsLinkConsumer == true) {
+                result.networkingSuccessful?.let { saveToLinkWithStripeSucceeded.set(it) }
             }
             eventTracker.track(
                 PollAttachPaymentsSucceeded(
@@ -76,32 +66,16 @@ internal class AttachPaymentViewModel @Inject constructor(
                     duration = millis
                 )
             )
+            val nextPane = result.nextPane ?: Pane.SUCCESS
+            navigationManager.tryNavigateTo(nextPane.destination(referrer = PANE))
             result
         }.execute { copy(linkPaymentAccount = it) }
     }
 
     private fun logErrors() {
-        onAsync(
-            AttachPaymentState::payload,
-            onFail = {
-                eventTracker.logError(
-                    logger = logger,
-                    pane = PANE,
-                    extraMessage = "Error retrieving accounts to attach payment",
-                    error = it
-                )
-            },
-            onSuccess = {
-                eventTracker.track(FinancialConnectionsAnalyticsEvent.PaneLoaded(PANE))
-            }
-        )
         onAsync(
             AttachPaymentState::linkPaymentAccount,
-            onSuccess = {
-                runCatching { saveToLinkWithStripeSucceeded.set(true) }
-            },
             onFail = {
-                runCatching { saveToLinkWithStripeSucceeded.set(false) }
                 eventTracker.logError(
                     logger = logger,
                     pane = PANE,
@@ -138,11 +112,5 @@ internal class AttachPaymentViewModel @Inject constructor(
 }
 
 internal data class AttachPaymentState(
-    val payload: Async = Uninitialized,
     val linkPaymentAccount: Async = Uninitialized
-) : MavericksState {
-    data class Payload(
-        val accountsCount: Int,
-        val businessName: String?
-    )
-}
+) : MavericksState
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/bankauthrepair/BankAuthRepairScreen.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/bankauthrepair/BankAuthRepairScreen.kt
index 6acaabda611..f469803e8dd 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/bankauthrepair/BankAuthRepairScreen.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/bankauthrepair/BankAuthRepairScreen.kt
@@ -1,5 +1,3 @@
-@file:Suppress("LongMethod")
-
 package com.stripe.android.financialconnections.features.bankauthrepair
 
 import androidx.compose.runtime.Composable
@@ -18,10 +16,10 @@ internal fun BankAuthRepairScreen() {
     SharedPartnerAuth(
         state = state.value,
         onContinueClick = { /*TODO*/ },
-        onSelectAnotherBank = { /*TODO*/ },
+        onCancelClick = { /*TODO*/ },
         onClickableTextClick = { /*TODO*/ },
-        onEnterDetailsManually = { /*TODO*/ },
         onWebAuthFlowFinished = { /*TODO*/ },
-        onViewEffectLaunched = { /*TODO*/ }
+        onViewEffectLaunched = { /*TODO*/ },
+        inModal = false
     )
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/AccessibleDataCallout.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/AccessibleDataCallout.kt
deleted file mode 100644
index 04ffb4bbd19..00000000000
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/AccessibleDataCallout.kt
+++ /dev/null
@@ -1,534 +0,0 @@
-@file:Suppress("TooManyFunctions", "LongMethod")
-
-package com.stripe.android.financialconnections.features.common
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.Divider
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.platform.LocalUriHandler
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import com.stripe.android.financialconnections.R
-import com.stripe.android.financialconnections.model.FinancialConnectionsAccount
-import com.stripe.android.financialconnections.model.FinancialConnectionsAccount.Permissions
-import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
-import com.stripe.android.financialconnections.model.PartnerAccount
-import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
-import com.stripe.android.financialconnections.ui.LocalImageLoader
-import com.stripe.android.financialconnections.ui.TextResource
-import com.stripe.android.financialconnections.ui.components.AnnotatedText
-import com.stripe.android.financialconnections.ui.components.StringAnnotation
-import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
-import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
-import com.stripe.android.uicore.image.StripeImage
-
-private const val COLLAPSE_ACCOUNTS_THRESHOLD = 5
-
-@Composable
-internal fun AccessibleDataCallout(
-    model: AccessibleDataCalloutModel,
-    onLearnMoreClick: () -> Unit
-) {
-    AccessibleDataCalloutBox {
-        AccessibleDataText(
-            model = model,
-            onLearnMoreClick = onLearnMoreClick
-        )
-    }
-}
-
-@Composable
-internal fun AccessibleDataCalloutWithAccounts(
-    model: AccessibleDataCalloutModel,
-    institution: FinancialConnectionsInstitution,
-    accounts: List,
-    onLearnMoreClick: () -> Unit
-) {
-    AccessibleDataCalloutBox {
-        Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
-            if (accounts.count() >= COLLAPSE_ACCOUNTS_THRESHOLD) {
-                AccountRow(
-                    iconUrl = institution.icon?.default,
-                    text = institution.name,
-                    subText = stringResource(
-                        id = R.string.stripe_success_infobox_accounts,
-                        accounts.count()
-                    )
-                )
-            } else {
-                accounts.forEach {
-                    AccountRow(
-                        iconUrl = institution.icon?.default,
-                        text = listOfNotNull(
-                            it.name,
-                            it.redactedAccountNumbers
-                        ).joinToString(" ")
-                    )
-                }
-            }
-
-            Divider(color = colors.backgroundBackdrop)
-            AccessibleDataText(
-                model = model,
-                onLearnMoreClick = onLearnMoreClick
-            )
-        }
-    }
-}
-
-@Composable
-private fun AccountRow(
-    text: String,
-    subText: String? = null,
-    iconUrl: String?
-) {
-    Row(
-        modifier = Modifier.fillMaxWidth(),
-        verticalAlignment = Alignment.CenterVertically,
-        horizontalArrangement = Arrangement.SpaceBetween
-    ) {
-        Row(
-            verticalAlignment = Alignment.CenterVertically,
-            horizontalArrangement = Arrangement.spacedBy(8.dp)
-        ) {
-            val modifier = Modifier
-                .size(24.dp)
-                .clip(RoundedCornerShape(4.dp))
-            when {
-                iconUrl.isNullOrEmpty() -> InstitutionPlaceholder(modifier)
-                else -> StripeImage(
-                    url = iconUrl,
-                    imageLoader = LocalImageLoader.current,
-                    contentDescription = null,
-                    modifier = modifier,
-                    errorContent = { InstitutionPlaceholder(modifier) }
-                )
-            }
-            Text(
-                text,
-                style = typography.captionTightEmphasized,
-                color = colors.textSecondary
-            )
-        }
-        if (subText != null) {
-            Text(
-                subText,
-                style = typography.captionTightEmphasized,
-                color = colors.textSecondary
-            )
-        }
-    }
-}
-
-@Composable
-private fun AccessibleDataText(
-    model: AccessibleDataCalloutModel,
-    onLearnMoreClick: () -> Unit
-) {
-    val uriHandler = LocalUriHandler.current
-    val permissionsReadable = remember(model.permissions) { model.permissions.toStringRes() }
-    AnnotatedText(
-        text = TextResource.StringId(
-            value = when {
-                model.isNetworking -> when (model.businessName) {
-                    null -> R.string.stripe_data_accessible_callout_through_link_no_business
-                    else -> R.string.stripe_data_accessible_callout_through_link
-                }
-
-                model.isStripeDirect -> R.string.stripe_data_accessible_callout_stripe_direct
-
-                else -> when (model.businessName) {
-                    null -> R.string.stripe_data_accessible_callout_through_stripe_no_business
-                    else -> R.string.stripe_data_accessible_callout_through_stripe
-                }
-            },
-            args = listOfNotNull(
-                model.businessName,
-                readableListOfPermissions(permissionsReadable)
-            )
-        ),
-        onClickableTextClick = {
-            uriHandler.openUri(model.dataPolicyUrl)
-            onLearnMoreClick()
-        },
-        defaultStyle = typography.caption.copy(
-            color = colors.textSecondary
-        ),
-        annotationStyles = mapOf(
-            StringAnnotation.CLICKABLE to typography.captionEmphasized
-                .toSpanStyle()
-                .copy(color = colors.textBrand),
-            StringAnnotation.BOLD to typography.captionEmphasized
-                .toSpanStyle()
-                .copy(color = colors.textSecondary)
-        )
-    )
-}
-
-@Composable
-private fun AccessibleDataCalloutBox(
-    content: @Composable BoxScope.() -> Unit
-) {
-    Box(
-        modifier = Modifier
-            .fillMaxWidth()
-            .clip(shape = RoundedCornerShape(8.dp))
-            .background(color = colors.backgroundContainer)
-            .padding(12.dp),
-        content = content
-    )
-}
-
-@Composable
-private fun readableListOfPermissions(permissionsReadable: List): String =
-    permissionsReadable
-        // TODO@carlosmuvi localize enumeration of permissions once more languages are supported.
-        .map { stringResource(id = it) }
-        .foldIndexed("") { index, current, arg ->
-            when {
-                index == 0 -> arg.replaceFirstChar { char ->
-                    if (char.isLowerCase()) char.titlecase() else char.toString()
-                }
-
-                permissionsReadable.lastIndex == index -> "$current and $arg"
-                else -> "$current, $arg"
-            }
-        }
-
-private fun List.toStringRes(): List = mapNotNull {
-    when (it) {
-        Permissions.BALANCES -> R.string.stripe_data_accessible_type_balances
-        Permissions.OWNERSHIP -> R.string.stripe_data_accessible_type_ownership
-        Permissions.PAYMENT_METHOD,
-        Permissions.ACCOUNT_NUMBERS -> R.string.stripe_data_accessible_type_accountdetails
-
-        Permissions.TRANSACTIONS -> R.string.stripe_data_accessible_type_transactions
-        Permissions.UNKNOWN -> null
-    }
-}.distinct()
-
-internal data class AccessibleDataCalloutModel(
-    val businessName: String?,
-    val permissions: List,
-    val isStripeDirect: Boolean,
-    val isNetworking: Boolean,
-    val dataPolicyUrl: String
-)
-
-@Preview(
-    group = "Data Callout",
-    name = "Default"
-)
-@Composable
-internal fun AccessibleDataCalloutPreview() {
-    FinancialConnectionsPreview {
-        AccessibleDataCallout(
-            AccessibleDataCalloutModel(
-                businessName = "My business",
-                permissions = listOf(
-                    Permissions.PAYMENT_METHOD,
-                    Permissions.BALANCES,
-                    Permissions.OWNERSHIP,
-                    Permissions.TRANSACTIONS,
-                    Permissions.ACCOUNT_NUMBERS
-                ),
-                isNetworking = false,
-                isStripeDirect = false,
-                dataPolicyUrl = ""
-            ),
-            onLearnMoreClick = {}
-        )
-    }
-}
-
-@Preview(
-    group = "Data Callout",
-    name = "Many Accounts"
-)
-@Composable
-@Suppress("LongMethod")
-internal fun AccessibleDataCalloutWithManyAccountsPreview() {
-    FinancialConnectionsPreview {
-        AccessibleDataCalloutWithAccounts(
-            AccessibleDataCalloutModel(
-                businessName = "My business",
-                permissions = listOf(
-                    Permissions.PAYMENT_METHOD,
-                    Permissions.BALANCES,
-                    Permissions.OWNERSHIP,
-                    Permissions.TRANSACTIONS
-                ),
-                isStripeDirect = false,
-                isNetworking = false,
-                dataPolicyUrl = ""
-            ),
-            accounts = partnerAccountsForPreview(),
-            institution = FinancialConnectionsInstitution(
-                id = "id",
-                name = "name",
-                url = "url",
-                featured = true,
-                icon = null,
-                logo = null,
-                featuredOrder = null,
-                mobileHandoffCapable = false
-            ),
-            onLearnMoreClick = {}
-        )
-    }
-}
-
-@Preview(
-    group = "Data Callout",
-    name = "Many Accounts and Stripe Direct"
-)
-@Composable
-@Suppress("LongMethod")
-internal fun AccessibleDataCalloutStripeDirectPreview() {
-    FinancialConnectionsPreview {
-        AccessibleDataCalloutWithAccounts(
-            AccessibleDataCalloutModel(
-                businessName = "My business",
-                permissions = listOf(
-                    Permissions.PAYMENT_METHOD,
-                    Permissions.BALANCES,
-                    Permissions.OWNERSHIP,
-                    Permissions.TRANSACTIONS
-                ),
-                isStripeDirect = true,
-                isNetworking = false,
-                dataPolicyUrl = ""
-            ),
-            accounts = partnerAccountsForPreview(),
-            institution = FinancialConnectionsInstitution(
-                id = "id",
-                name = "name",
-                url = "url",
-                featured = true,
-                icon = null,
-                logo = null,
-                featuredOrder = null,
-                mobileHandoffCapable = false
-            ),
-            onLearnMoreClick = {}
-        )
-    }
-}
-
-@Preview(
-    group = "Data Callout",
-    name = "Networking"
-)
-@Composable
-@Suppress("LongMethod")
-internal fun AccessibleDataCalloutNetworkingPreview() {
-    FinancialConnectionsPreview {
-        AccessibleDataCalloutWithAccounts(
-            AccessibleDataCalloutModel(
-                businessName = "My business",
-                permissions = listOf(
-                    Permissions.PAYMENT_METHOD,
-                    Permissions.BALANCES,
-                    Permissions.OWNERSHIP,
-                    Permissions.TRANSACTIONS
-                ),
-                isStripeDirect = false,
-                isNetworking = true,
-                dataPolicyUrl = ""
-            ),
-            accounts = partnerAccountsForPreview(),
-            institution = FinancialConnectionsInstitution(
-                id = "id",
-                name = "name",
-                url = "url",
-                featured = true,
-                icon = null,
-                logo = null,
-                featuredOrder = null,
-                mobileHandoffCapable = false
-            ),
-            onLearnMoreClick = {}
-        )
-    }
-}
-
-@Preview(
-    group = "Data Callout",
-    name = "Multiple accounts"
-)
-@Composable
-internal fun AccessibleDataCalloutWithMultipleAccountsPreview() {
-    FinancialConnectionsPreview {
-        AccessibleDataCalloutWithAccounts(
-            AccessibleDataCalloutModel(
-                businessName = "My business",
-                permissions = listOf(
-                    Permissions.PAYMENT_METHOD,
-                    Permissions.BALANCES,
-                    Permissions.OWNERSHIP,
-                    Permissions.TRANSACTIONS
-                ),
-                isStripeDirect = true,
-                isNetworking = false,
-                dataPolicyUrl = ""
-            ),
-            accounts = listOf(
-                PartnerAccount(
-                    authorization = "Authorization",
-                    institutionName = "Random bank",
-                    category = FinancialConnectionsAccount.Category.CASH,
-                    id = "id1",
-                    name = "Account 1",
-                    balanceAmount = 1000,
-                    displayableAccountNumbers = "1234",
-                    currency = "$",
-                    subcategory = FinancialConnectionsAccount.Subcategory.CHECKING,
-                    _allowSelection = true,
-                    allowSelectionMessage = "",
-                    supportedPaymentMethodTypes = emptyList()
-                ),
-                PartnerAccount(
-                    authorization = "Authorization",
-                    category = FinancialConnectionsAccount.Category.CASH,
-                    id = "id2",
-                    name = "Account 2 - no acct numbers",
-                    subcategory = FinancialConnectionsAccount.Subcategory.SAVINGS,
-                    _allowSelection = true,
-                    allowSelectionMessage = "",
-                    supportedPaymentMethodTypes = emptyList()
-                )
-            ),
-            institution = FinancialConnectionsInstitution(
-                id = "id",
-                name = "name",
-                url = "url",
-                featured = true,
-                icon = null,
-                logo = null,
-                featuredOrder = null,
-                mobileHandoffCapable = false
-            ),
-            onLearnMoreClick = {}
-        )
-    }
-}
-
-@Preview(
-    group = "Data Callout",
-    name = "One account"
-)
-@Composable
-internal fun AccessibleDataCalloutWithOneAccountPreview() {
-    FinancialConnectionsPreview {
-        AccessibleDataCalloutWithAccounts(
-            AccessibleDataCalloutModel(
-                businessName = "My business",
-                permissions = listOf(
-                    Permissions.PAYMENT_METHOD,
-                    Permissions.BALANCES,
-                    Permissions.OWNERSHIP,
-                    Permissions.TRANSACTIONS
-                ),
-                isStripeDirect = true,
-                isNetworking = false,
-                dataPolicyUrl = ""
-            ),
-            accounts = listOf(
-                PartnerAccount(
-                    authorization = "Authorization",
-                    category = FinancialConnectionsAccount.Category.CASH,
-                    id = "id1",
-                    name = "Account 1",
-                    balanceAmount = 1000,
-                    displayableAccountNumbers = "1234",
-                    currency = "$",
-                    subcategory = FinancialConnectionsAccount.Subcategory.CHECKING,
-                    _allowSelection = true,
-                    allowSelectionMessage = "",
-                    supportedPaymentMethodTypes = emptyList()
-                )
-            ),
-            institution = FinancialConnectionsInstitution(
-                id = "id",
-                name = "name",
-                url = "url",
-                featured = true,
-                featuredOrder = null,
-                icon = null,
-                logo = null,
-                mobileHandoffCapable = false
-            ),
-            onLearnMoreClick = {}
-        )
-    }
-}
-
-@Composable
-private fun partnerAccountsForPreview() = listOf(
-    PartnerAccount(
-        authorization = "Authorization",
-        institutionName = "Random bank",
-        category = FinancialConnectionsAccount.Category.CASH,
-        id = "id1",
-        name = "Account 1 - no acct numbers",
-        _allowSelection = true,
-        allowSelectionMessage = "",
-        subcategory = FinancialConnectionsAccount.Subcategory.CHECKING,
-        supportedPaymentMethodTypes = emptyList()
-    ),
-    PartnerAccount(
-        authorization = "Authorization",
-        category = FinancialConnectionsAccount.Category.CASH,
-        id = "id2",
-        name = "Account 2 - no acct numbers",
-        _allowSelection = true,
-        allowSelectionMessage = "",
-        subcategory = FinancialConnectionsAccount.Subcategory.SAVINGS,
-        supportedPaymentMethodTypes = emptyList()
-    ),
-    PartnerAccount(
-        authorization = "Authorization",
-        category = FinancialConnectionsAccount.Category.CASH,
-        id = "id3",
-        name = "Account 3 - no acct numbers",
-        _allowSelection = true,
-        allowSelectionMessage = "",
-        subcategory = FinancialConnectionsAccount.Subcategory.SAVINGS,
-        supportedPaymentMethodTypes = emptyList()
-    ),
-    PartnerAccount(
-        authorization = "Authorization",
-        category = FinancialConnectionsAccount.Category.CASH,
-        id = "id4",
-        name = "Account 4 - no acct numbers",
-        _allowSelection = true,
-        allowSelectionMessage = "",
-        subcategory = FinancialConnectionsAccount.Subcategory.SAVINGS,
-        supportedPaymentMethodTypes = emptyList()
-    ),
-    PartnerAccount(
-        authorization = "Authorization",
-        category = FinancialConnectionsAccount.Category.CASH,
-        id = "id5",
-        name = "Account 5 - no acct numbers",
-        _allowSelection = true,
-        allowSelectionMessage = "",
-        subcategory = FinancialConnectionsAccount.Subcategory.SAVINGS,
-        supportedPaymentMethodTypes = emptyList()
-    )
-)
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/AccountItem.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/AccountItem.kt
index 21f718d6f66..42dab02bd61 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/AccountItem.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/AccountItem.kt
@@ -1,34 +1,48 @@
 package com.stripe.android.financialconnections.features.common
 
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES.M
+import android.view.HapticFeedbackConstants.CONTEXT_CLICK
+import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 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.RowScope
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.Icon
 import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 import androidx.core.os.ConfigurationCompat.getLocales
+import com.stripe.android.financialconnections.model.FinancialConnectionsAccount
+import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
+import com.stripe.android.financialconnections.model.Image
 import com.stripe.android.financialconnections.model.NetworkedAccount
 import com.stripe.android.financialconnections.model.PartnerAccount
-import com.stripe.android.financialconnections.ui.LocalImageLoader
+import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsScaffold
 import com.stripe.android.financialconnections.ui.components.clickableSingle
-import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
 import com.stripe.android.uicore.format.CurrencyFormatter
-import com.stripe.android.uicore.image.StripeImage
 import com.stripe.android.uicore.text.MiddleEllipsisText
 import java.util.Locale
 
@@ -39,26 +53,19 @@ import java.util.Locale
  * @param onAccountClicked callback when this account is clicked
  * @param account the account info to display
  * @param networkedAccount For networked accounts, extra info to display
- * @param selectorContent content to display on the left side of the account item
  */
 @Composable
-@Suppress("LongMethod")
 internal fun AccountItem(
     selected: Boolean,
+    showInstitutionIcon: Boolean = true,
     onAccountClicked: (PartnerAccount) -> Unit,
     account: PartnerAccount,
     networkedAccount: NetworkedAccount? = null,
-    selectorContent: @Composable RowScope.() -> Unit
 ) {
+    val view = LocalView.current
     // networked account's allowSelection takes precedence over the account's.
     val selectable = networkedAccount?.allowSelection ?: account.allowSelection
-    val (title, subtitle) = getAccountTexts(
-        account = account,
-        networkedAccount = networkedAccount,
-        allowSelection = selectable
-    )
-    val verticalPadding = remember(account) { if (subtitle != null) 10.dp else 12.dp }
-    val shape = remember { RoundedCornerShape(8.dp) }
+    val shape = remember { RoundedCornerShape(12.dp) }
     Box(
         modifier = Modifier
             .fillMaxWidth()
@@ -66,85 +73,96 @@ internal fun AccountItem(
             .border(
                 width = if (selected) 2.dp else 1.dp,
                 color = when {
-                    selected -> FinancialConnectionsTheme.colors.textBrand
-                    else -> FinancialConnectionsTheme.colors.borderDefault
+                    selected -> colors.borderBrand
+                    else -> colors.border
                 },
                 shape = shape
             )
-            .clickableSingle(enabled = selectable) { onAccountClicked(account) }
-            .padding(vertical = verticalPadding, horizontal = 16.dp)
+            .clickableSingle(enabled = selectable) {
+                if (SDK_INT >= M) view.performHapticFeedback(CONTEXT_CLICK)
+                onAccountClicked(account)
+            }
+            .alpha(if (selectable) 1f else ContentAlpha.disabled)
+            .padding(16.dp)
     ) {
         Row(
-            horizontalArrangement = Arrangement.Start,
+            horizontalArrangement = Arrangement.spacedBy(12.dp),
             verticalAlignment = Alignment.CenterVertically
         ) {
-            selectorContent()
-            Spacer(modifier = Modifier.size(16.dp))
+            account.institution
+                ?.icon?.default
+                ?.takeIf { showInstitutionIcon }?.let {
+                    InstitutionIcon(institutionIcon = it)
+                }
             Column(
-                Modifier.weight(ACCOUNT_COLUMN_WEIGHT)
+                Modifier.weight(1f)
             ) {
                 Text(
-                    text = title,
+                    text = account.name,
                     overflow = TextOverflow.Ellipsis,
                     maxLines = 1,
-                    color = if (selectable) {
-                        FinancialConnectionsTheme.colors.textPrimary
-                    } else {
-                        FinancialConnectionsTheme.colors.textDisabled
-                    },
-                    style = FinancialConnectionsTheme.typography.bodyEmphasized
-                )
-                subtitle?.let {
-                    Spacer(modifier = Modifier.size(4.dp))
-                    MiddleEllipsisText(
-                        text = it,
-                        color = FinancialConnectionsTheme.colors.textDisabled,
-                        style = FinancialConnectionsTheme.typography.captionTight
-                    )
-                }
-            }
-            networkedAccount?.icon?.default?.let {
-                StripeImage(
-                    url = it,
-                    imageLoader = LocalImageLoader.current,
-                    contentDescription = null,
-                    modifier = Modifier
-                        .size(16.dp)
+                    color = colors.textDefault,
+                    style = typography.labelLargeEmphasized
                 )
+                AccountSubtitle(selectable, account, networkedAccount)
             }
+            Icon(
+                modifier = Modifier
+                    .size(16.dp)
+                    .alpha(if (selected) 1f else 0f),
+                imageVector = Icons.Default.Check,
+                tint = colors.iconBrand,
+                contentDescription = "Selected"
+            )
         }
     }
 }
 
 @Composable
-private fun getAccountTexts(
-    allowSelection: Boolean,
+private fun AccountSubtitle(
+    selectable: Boolean,
     account: PartnerAccount,
     networkedAccount: NetworkedAccount?,
-): Pair {
-    val formattedBalance = account.getFormattedBalance()
-
-    val subtitle = when {
-        networkedAccount?.caption != null -> networkedAccount.caption
-        allowSelection.not() && account.allowSelectionMessage?.isNotBlank() == true ->
-            account.allowSelectionMessage
-
-        formattedBalance != null -> formattedBalance
-        account.redactedAccountNumbers != null -> account.redactedAccountNumbers
-        else -> null
-    }
-
-    // just show redacted account numbers in the title if they're not in the subtitle.
-    val title = when {
-        subtitle != account.redactedAccountNumbers -> listOfNotNull(
-            account.name,
-            account.redactedAccountNumbers
-        ).joinToString(" ")
+) {
+    val subtitle = getSubtitle(allowSelection = selectable, account = account, networkedAccount = networkedAccount)
+    Row(
+        verticalAlignment = Alignment.CenterVertically
+    ) {
+        MiddleEllipsisText(
+            text = subtitle ?: account.redactedAccountNumbers,
+            color = colors.textSubdued,
+            style = typography.labelMedium
+        )
+        account.getFormattedBalance()
+            // Only show balance if there is no custom subtitle (e.g. "Account unavailable, Repair account, etc")
+            ?.takeIf { subtitle == null }
+            ?.let {
+                Spacer(modifier = Modifier.size(8.dp))
+                Text(
+                    text = it,
+                    color = colors.textSubdued,
+                    style = typography.labelSmall,
+                    modifier = Modifier
+                        .background(
+                            color = colors.backgroundOffset,
+                            shape = RoundedCornerShape(4.dp)
+                        )
+                        .padding(horizontal = 6.dp, vertical = 4.dp)
 
-        else -> account.name
+                )
+            }
     }
+}
 
-    return title to subtitle
+@Composable
+private fun getSubtitle(
+    allowSelection: Boolean,
+    account: PartnerAccount,
+    networkedAccount: NetworkedAccount?,
+): String? = when {
+    networkedAccount?.caption != null -> networkedAccount.caption
+    allowSelection.not() && account.allowSelectionMessage?.isNotBlank() == true -> account.allowSelectionMessage
+    else -> null
 }
 
 @Composable
@@ -162,4 +180,121 @@ private fun PartnerAccount.getFormattedBalance(): String? {
     }
 }
 
-private const val ACCOUNT_COLUMN_WEIGHT = 0.7f
+@Composable
+@Preview
+internal fun AccountItemPreview() {
+    FinancialConnectionsPreview {
+        FinancialConnectionsScaffold(topBar = { }) {
+            Column(
+                modifier = Modifier.padding(16.dp),
+                verticalArrangement = Arrangement.spacedBy(16.dp)
+            ) {
+                AccountItem(
+                    selected = false,
+                    onAccountClicked = { },
+                    account = PartnerAccount(
+                        id = "id",
+                        name = "Regular Checking (Unselected)",
+                        allowSelectionMessage = "allowSelectionMessage",
+                        institution = FinancialConnectionsInstitution(
+                            id = "id",
+                            name = "Bank of America",
+                            featured = false,
+                            mobileHandoffCapable = false,
+                            icon = Image(default = "www.image.com")
+                        ),
+                        authorization = "",
+                        currency = "USD",
+                        category = FinancialConnectionsAccount.Category.CASH,
+                        subcategory = FinancialConnectionsAccount.Subcategory.CHECKING,
+                        supportedPaymentMethodTypes = emptyList(),
+                        balanceAmount = 100,
+                    ),
+                )
+                AccountItem(
+                    selected = true,
+                    onAccountClicked = { },
+                    account = PartnerAccount(
+                        id = "id",
+                        name = "Regular Checking (Selected)",
+                        allowSelectionMessage = "allowSelectionMessage",
+                        institution = FinancialConnectionsInstitution(
+                            id = "id",
+                            name = "Bank of America",
+                            featured = false,
+                            mobileHandoffCapable = false,
+                            icon = Image(default = "www.image.com")
+                        ),
+                        authorization = "",
+                        currency = "USD",
+                        category = FinancialConnectionsAccount.Category.CASH,
+                        subcategory = FinancialConnectionsAccount.Subcategory.CHECKING,
+                        supportedPaymentMethodTypes = emptyList(),
+                        balanceAmount = 100,
+                    ),
+                    networkedAccount = NetworkedAccount(
+                        id = "id",
+                        allowSelection = true,
+                        icon = Image(default = "www.image.com")
+                    )
+                )
+                AccountItem(
+                    selected = false,
+                    onAccountClicked = { },
+                    account = PartnerAccount(
+                        id = "id",
+                        name = "Regular Checking (Disabled)",
+                        _allowSelection = false,
+                        allowSelectionMessage = null,
+                        institution = FinancialConnectionsInstitution(
+                            id = "id",
+                            name = "Bank of America",
+                            featured = false,
+                            mobileHandoffCapable = false,
+                            icon = Image(default = "www.image.com")
+                        ),
+                        authorization = "",
+                        currency = "USD",
+                        category = FinancialConnectionsAccount.Category.CASH,
+                        subcategory = FinancialConnectionsAccount.Subcategory.CHECKING,
+                        supportedPaymentMethodTypes = emptyList(),
+                        balanceAmount = 100,
+                    ),
+                    networkedAccount = NetworkedAccount(
+                        id = "id",
+                        allowSelection = false,
+                        icon = Image(default = "www.image.com")
+                    )
+                )
+                AccountItem(
+                    selected = false,
+                    onAccountClicked = { },
+                    account = PartnerAccount(
+                        id = "id",
+                        name = "Regular Checking (Disabled)",
+                        _allowSelection = false,
+                        allowSelectionMessage = "Unselectable with custom message",
+                        institution = FinancialConnectionsInstitution(
+                            id = "id",
+                            name = "Bank of America",
+                            featured = false,
+                            mobileHandoffCapable = false,
+                            icon = Image(default = "www.image.com")
+                        ),
+                        authorization = "",
+                        currency = "USD",
+                        category = FinancialConnectionsAccount.Category.CASH,
+                        subcategory = FinancialConnectionsAccount.Subcategory.CHECKING,
+                        supportedPaymentMethodTypes = emptyList(),
+                        balanceAmount = 100,
+                    ),
+                    networkedAccount = NetworkedAccount(
+                        id = "id",
+                        allowSelection = false,
+                        icon = Image(default = "www.image.com")
+                    )
+                )
+            }
+        }
+    }
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/CloseDialog.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/CloseDialog.kt
deleted file mode 100644
index 05ec0390e78..00000000000
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/CloseDialog.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-package com.stripe.android.financialconnections.features.common
-
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.AlertDialog
-import androidx.compose.material.ButtonDefaults
-import androidx.compose.material.Text
-import androidx.compose.material.TextButton
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import com.stripe.android.financialconnections.R
-import com.stripe.android.financialconnections.ui.TextResource
-import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
-
-@Composable
-internal fun CloseDialog(
-    description: TextResource,
-    onConfirmClick: () -> Unit,
-    onDismissClick: () -> Unit
-) {
-    AlertDialog(
-        shape = RoundedCornerShape(8.dp),
-        backgroundColor = FinancialConnectionsTheme.colors.backgroundContainer,
-        onDismissRequest = onDismissClick,
-        title = {
-            Text(
-                text = stringResource(R.string.stripe_close_dialog_title),
-            )
-        },
-        text = {
-            Text(
-                text = description.toText().toString(),
-            )
-        },
-        confirmButton = {
-            TextButton(
-                onClick = onConfirmClick,
-                colors = ButtonDefaults.textButtonColors(
-                    contentColor = FinancialConnectionsTheme.colors.textCritical
-                ),
-            ) {
-                Text(stringResource(R.string.stripe_close_dialog_confirm))
-            }
-        },
-        dismissButton = {
-            TextButton(
-                onClick = onDismissClick,
-                colors = ButtonDefaults.textButtonColors(
-                    contentColor = FinancialConnectionsTheme.colors.textPrimary
-                ),
-            ) {
-                Text(stringResource(R.string.stripe_close_dialog_back))
-            }
-        },
-    )
-}
-
-@Preview(group = "Close Dialog", name = "Default")
-@Composable
-internal fun CloseDialogPreview() {
-    FinancialConnectionsTheme {
-        CloseDialog(
-            description = TextResource.StringId(R.string.stripe_close_dialog_desc),
-            onConfirmClick = {},
-            onDismissClick = {}
-        )
-    }
-}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ConsumerSessionExtensions.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ConsumerSessionExtensions.kt
index a4fd5569097..bf7405f5b71 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ConsumerSessionExtensions.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ConsumerSessionExtensions.kt
@@ -3,7 +3,6 @@ import com.stripe.android.model.ConsumerSession
 /**
  * Mask the phone number to show only the last 4 digits.
  */
-@Suppress("MagicNumber")
 internal fun ConsumerSession.getRedactedPhoneNumber(): String {
     val number = redactedPhoneNumber.replace("*", "•")
     val formattedPhoneNumber = buildString {
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ErrorContent.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ErrorContent.kt
index 2f7368fddb4..83a9a586f53 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ErrorContent.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ErrorContent.kt
@@ -1,30 +1,18 @@
-@file:Suppress("TooManyFunctions")
-
 package com.stripe.android.financialconnections.features.common
 
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
+import android.os.Build
+import android.view.HapticFeedbackConstants.REJECT
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.Icon
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.graphics.painter.Painter
-import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.pluralStringResource
 import androidx.compose.ui.res.stringResource
@@ -40,12 +28,11 @@ import com.stripe.android.financialconnections.exception.InstitutionPlannedDownt
 import com.stripe.android.financialconnections.exception.InstitutionUnplannedDowntimeError
 import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
 import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
-import com.stripe.android.financialconnections.ui.LocalImageLoader
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsButton
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsScaffold
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsTopAppBar
-import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
-import com.stripe.android.uicore.image.StripeImage
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
+import com.stripe.android.financialconnections.ui.theme.LazyLayout
 import java.text.SimpleDateFormat
 
 @Composable
@@ -54,7 +41,12 @@ internal fun UnclassifiedErrorContent(
     onCloseFromErrorClick: (Throwable) -> Unit
 ) {
     ErrorContent(
-        null, // TODO show warning icon.
+        iconContent = {
+            ShapedIcon(
+                painter = painterResource(id = R.drawable.stripe_ic_warning),
+                contentDescription = null
+            )
+        },
         title = stringResource(R.string.stripe_error_generic_title),
         content = stringResource(R.string.stripe_error_generic_desc),
         primaryCta = stringResource(R.string.stripe_error_cta_close) to {
@@ -68,7 +60,12 @@ internal fun InstitutionUnknownErrorContent(
     onSelectAnotherBank: () -> Unit
 ) {
     ErrorContent(
-        iconUrl = null, // TODO show institution icon.
+        iconContent = {
+            ShapedIcon(
+                painter = painterResource(id = R.drawable.stripe_ic_warning),
+                contentDescription = null
+            )
+        },
         title = stringResource(R.string.stripe_error_generic_title),
         content = stringResource(R.string.stripe_error_unplanned_downtime_desc),
         primaryCta = Pair(
@@ -85,7 +82,9 @@ internal fun InstitutionUnplannedDowntimeErrorContent(
     onEnterDetailsManually: () -> Unit
 ) {
     ErrorContent(
-        iconUrl = exception.institution.icon?.default ?: "",
+        iconContent = {
+            InstitutionIcon(institutionIcon = exception.institution.icon?.default ?: "")
+        },
         title = stringResource(
             R.string.stripe_error_unplanned_downtime_title,
             exception.institution.name
@@ -117,7 +116,9 @@ internal fun InstitutionPlannedDowntimeErrorContent(
         SimpleDateFormat("dd/MM/yyyy HH:mm", javaLocale).format(exception.backUpAt)
     }
     ErrorContent(
-        iconUrl = exception.institution.icon?.default ?: "",
+        iconContent = {
+            InstitutionIcon(institutionIcon = exception.institution.icon?.default ?: "")
+        },
         title = stringResource(
             R.string.stripe_error_planned_downtime_title,
             exception.institution.name
@@ -147,7 +148,9 @@ internal fun NoSupportedPaymentMethodTypeAccountsErrorContent(
     onSelectAnotherBank: () -> Unit
 ) {
     ErrorContent(
-        iconUrl = exception.institution.icon?.default ?: "",
+        iconContent = {
+            InstitutionIcon(institutionIcon = exception.institution.icon?.default ?: "")
+        },
         title = stringResource(
             R.string.stripe_account_picker_error_no_payment_method_title
         ),
@@ -205,7 +208,9 @@ internal fun NoAccountsAvailableErrorContent(
     }
 
     ErrorContent(
-        iconUrl = exception.institution.icon?.default ?: "",
+        iconContent = {
+            InstitutionIcon(institutionIcon = exception.institution.icon?.default ?: "")
+        },
         title = stringResource(
             R.string.stripe_account_picker_error_no_account_available_title,
             exception.institution.name
@@ -223,7 +228,9 @@ internal fun AccountNumberRetrievalErrorContent(
     onEnterDetailsManually: () -> Unit
 ) {
     ErrorContent(
-        iconUrl = exception.institution.icon?.default ?: "",
+        iconContent = {
+            InstitutionIcon(institutionIcon = exception.institution.icon?.default ?: "")
+        },
         title = stringResource(
             R.string.stripe_attachlinkedpaymentaccount_error_title
         ),
@@ -250,147 +257,54 @@ internal fun AccountNumberRetrievalErrorContent(
 
 @Composable
 internal fun ErrorContent(
-    iconUrl: String?,
-    badge: Pair = Pair(
-        painterResource(id = R.drawable.stripe_ic_warning_circle),
-        CircleShape
-    ),
+    iconContent: @Composable (() -> Unit)?,
     title: String,
     content: String,
     primaryCta: Pair Unit>? = null,
     secondaryCta: Pair Unit>? = null
 ) {
-    val scrollState = rememberScrollState()
-    Column(
-        Modifier
-            .padding(
-                top = 8.dp,
-                start = 24.dp,
-                end = 24.dp,
-                bottom = 24.dp
-            )
-            .fillMaxSize()
-    ) {
-        Column(
-            modifier = Modifier
-                .weight(1f)
-                .verticalScroll(scrollState)
-        ) {
-            BadgedInstitutionImage(iconUrl, badge)
-            Spacer(modifier = Modifier.size(16.dp))
-            Text(
-                text = title,
-                style = FinancialConnectionsTheme.typography.subtitle
-            )
-            Spacer(modifier = Modifier.size(16.dp))
-            Text(
-                text = content,
-                style = FinancialConnectionsTheme.typography.body
-            )
+    val view = LocalView.current
+    LaunchedEffect(Unit) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            view.performHapticFeedback(REJECT)
         }
-        secondaryCta?.let { (text, onClick) ->
-            FinancialConnectionsButton(
-                type = FinancialConnectionsButton.Type.Secondary,
-                onClick = onClick,
-                modifier = Modifier
-                    .fillMaxWidth()
-            ) {
-                Text(text = text)
+    }
+    LazyLayout(
+        verticalArrangement = Arrangement.spacedBy(16.dp),
+        body = {
+            iconContent?.let {
+                item { Box(modifier = Modifier.padding(top = 16.dp)) { it() } }
             }
-        }
-        if (primaryCta != null && secondaryCta != null) Spacer(Modifier.size(8.dp))
-        primaryCta?.let { (text, onClick) ->
-            FinancialConnectionsButton(
-                type = FinancialConnectionsButton.Type.Primary,
-                onClick = onClick,
-                modifier = Modifier
-                    .fillMaxWidth()
+            item { Text(title, style = typography.headingXLarge) }
+            item { Text(content, style = typography.bodyMedium) }
+        },
+        footer = {
+            Column(
+                verticalArrangement = Arrangement.spacedBy(8.dp)
             ) {
-                Text(text = text)
+                secondaryCta?.let { (text, onClick) ->
+                    FinancialConnectionsButton(
+                        type = FinancialConnectionsButton.Type.Secondary,
+                        onClick = onClick,
+                        modifier = Modifier
+                            .fillMaxWidth()
+                    ) {
+                        Text(text = text)
+                    }
+                }
+                primaryCta?.let { (text, onClick) ->
+                    FinancialConnectionsButton(
+                        type = FinancialConnectionsButton.Type.Primary,
+                        onClick = onClick,
+                        modifier = Modifier
+                            .fillMaxWidth()
+                    ) {
+                        Text(text = text)
+                    }
+                }
             }
         }
-    }
-}
-
-@Composable
-private fun BadgedInstitutionImage(
-    institutionIconUrl: String?,
-    badge: Pair
-) {
-    Box(
-        modifier = Modifier
-            .size(40.dp)
-    ) {
-        val modifier = Modifier
-            .size(36.dp)
-            .align(Alignment.BottomStart)
-            .clip(RoundedCornerShape(6.dp))
-        when {
-            institutionIconUrl.isNullOrEmpty() -> InstitutionPlaceholder(modifier)
-            else -> StripeImage(
-                url = institutionIconUrl,
-                imageLoader = LocalImageLoader.current,
-                errorContent = { InstitutionPlaceholder(modifier) },
-                contentDescription = null,
-                modifier = modifier
-            )
-        }
-        Icon(
-            painter = badge.first,
-            contentDescription = "",
-            tint = FinancialConnectionsTheme.colors.textCritical,
-            modifier = Modifier
-                .align(Alignment.TopEnd)
-                .size(12.dp)
-                .clip(badge.second)
-                // draws a background with padding around the badge to simulate a border.
-                .background(FinancialConnectionsTheme.colors.textWhite)
-                .padding(1.dp)
-        )
-    }
-}
-
-@Preview(group = "Errors", name = "unclassified error")
-@Composable
-internal fun UnclassifiedErrorContentPreview() {
-    FinancialConnectionsPreview {
-        FinancialConnectionsScaffold(
-            topBar = { FinancialConnectionsTopAppBar { } }
-        ) {
-            UnclassifiedErrorContent(APIException()) {}
-        }
-    }
-}
-
-@Preview(group = "Errors", name = "institution down planned error")
-@Composable
-internal fun InstitutionPlannedDowntimeErrorContentPreview() {
-    FinancialConnectionsPreview {
-        FinancialConnectionsScaffold(
-            topBar = { FinancialConnectionsTopAppBar { } }
-        ) {
-            InstitutionPlannedDowntimeErrorContent(
-                exception = InstitutionPlannedDowntimeError(
-                    institution = FinancialConnectionsInstitution(
-                        id = "3",
-                        name = "Random Institution",
-                        url = "Random Institution url",
-                        featured = false,
-                        featuredOrder = null,
-                        icon = null,
-                        logo = null,
-                        mobileHandoffCapable = false
-                    ),
-                    showManualEntry = true,
-                    isToday = true,
-                    backUpAt = 10000L,
-                    stripeException = APIException()
-                ),
-                onEnterDetailsManually = {},
-                onSelectAnotherBank = {}
-            )
-        }
-    }
+    )
 }
 
 @Preview(group = "Errors", name = "no accounts available error")
@@ -423,13 +337,3 @@ internal fun NoAccountsAvailableErrorContentPreview() {
         }
     }
 }
-
-@Composable
-internal fun InstitutionPlaceholder(modifier: Modifier) {
-    Image(
-        modifier = modifier,
-        painter = painterResource(id = R.drawable.stripe_ic_brandicon_institution),
-        contentDescription = "Bank icon placeholder",
-        contentScale = ContentScale.Crop
-    )
-}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/InstitutionIcon.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/InstitutionIcon.kt
new file mode 100644
index 00000000000..c71c1e39c62
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/InstitutionIcon.kt
@@ -0,0 +1,48 @@
+package com.stripe.android.financialconnections.features.common
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.stripe.android.financialconnections.R
+import com.stripe.android.financialconnections.ui.LocalImageLoader
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
+import com.stripe.android.uicore.image.StripeImage
+
+@Composable
+internal fun InstitutionIcon(institutionIcon: String?) {
+    val previewMode = LocalInspectionMode.current
+    val iconModifier = Modifier
+        .size(56.dp)
+        .shadow(1.dp, RoundedCornerShape(12.dp), clip = true)
+    when {
+        previewMode || institutionIcon == null -> InstitutionPlaceholder(iconModifier)
+        else -> StripeImage(
+            url = institutionIcon,
+            imageLoader = LocalImageLoader.current,
+            contentDescription = null,
+            modifier = iconModifier,
+            contentScale = ContentScale.Crop,
+            loadingContent = { Box(modifier = iconModifier.background(colors.backgroundOffset)) },
+            errorContent = { InstitutionPlaceholder(iconModifier) }
+        )
+    }
+}
+
+@Composable
+private fun InstitutionPlaceholder(modifier: Modifier) {
+    Image(
+        modifier = modifier,
+        painter = painterResource(id = R.drawable.stripe_ic_brandicon_institution),
+        contentDescription = "Bank icon placeholder",
+        contentScale = ContentScale.Crop
+    )
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ListItem.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ListItem.kt
new file mode 100644
index 00000000000..26eac665d96
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ListItem.kt
@@ -0,0 +1,102 @@
+package com.stripe.android.financialconnections.features.common
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.stripe.android.financialconnections.R
+import com.stripe.android.financialconnections.ui.ImageResource
+import com.stripe.android.financialconnections.ui.LocalImageLoader
+import com.stripe.android.financialconnections.ui.TextResource
+import com.stripe.android.financialconnections.ui.components.AnnotatedText
+import com.stripe.android.financialconnections.ui.sdui.BulletUI
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
+import com.stripe.android.uicore.image.StripeImage
+
+@Composable
+internal fun ListItem(
+    bullet: BulletUI,
+    onClickableTextClick: (String) -> Unit,
+) {
+    val firstText = bullet.title ?: bullet.content ?: TextResource.Text("")
+    val secondText = remember(firstText) { bullet.content?.takeIf { bullet.title != null } }
+    val titleStyle = if (secondText != null) typography.bodyMediumEmphasized else typography.bodyMedium
+    Row {
+        ListItemIcon(icon = bullet.imageResource)
+        Spacer(modifier = Modifier.size(16.dp))
+        Column {
+            AnnotatedText(
+                text = firstText,
+                defaultStyle = titleStyle.copy(color = FinancialConnectionsTheme.colors.textDefault),
+                onClickableTextClick = onClickableTextClick
+            )
+            secondText?.let {
+                AnnotatedText(
+                    text = requireNotNull(bullet.content),
+                    defaultStyle = typography.bodySmall.copy(color = FinancialConnectionsTheme.colors.textSubdued),
+                    onClickableTextClick = onClickableTextClick
+                )
+            }
+        }
+    }
+}
+
+@Composable
+private fun ListItemIcon(icon: ImageResource?) {
+    val bulletColor = FinancialConnectionsTheme.colors.iconDefault
+    val iconSize = 20.dp
+    val modifier = Modifier
+        .size(iconSize)
+        .offset(y = 1.dp)
+    when (icon) {
+        // Render a bullet if no icon is provided
+        null -> Canvas(
+            modifier = modifier.padding((iconSize - 8.dp) / 2),
+            onDraw = { drawCircle(color = bulletColor) }
+        )
+
+        // Render the icon if it's a local resource
+        is ImageResource.Local -> Image(
+            modifier = modifier,
+            painter = painterResource(id = icon.resId),
+            contentDescription = null,
+        )
+
+        // Render the icon if it's a network resource, or fallback to a bullet if it fails to load
+        is ImageResource.Network -> StripeImage(
+            url = icon.url,
+            debugPainter = painterResource(id = R.drawable.stripe_ic_check_circle),
+            errorContent = {
+                Canvas(
+                    modifier = modifier.padding((iconSize - 8.dp) / 2),
+                    onDraw = { drawCircle(color = bulletColor) }
+                )
+            },
+            loadingContent = {
+                LoadingShimmerEffect { shimmer ->
+                    Spacer(
+                        modifier = modifier
+                            .clip(RoundedCornerShape(4.dp))
+                            .background(shimmer)
+                    )
+                }
+            },
+            imageLoader = LocalImageLoader.current,
+            contentDescription = null,
+            modifier = modifier
+        )
+    }
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/LoadingContent.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/LoadingContent.kt
index ae4a9fbf0e6..ba60e8f9aab 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/LoadingContent.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/LoadingContent.kt
@@ -1,116 +1,137 @@
 package com.stripe.android.financialconnections.features.common
 
-import androidx.compose.animation.core.FastOutLinearInEasing
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
 import androidx.compose.animation.core.animateFloat
 import androidx.compose.animation.core.infiniteRepeatable
 import androidx.compose.animation.core.rememberInfiniteTransition
 import androidx.compose.animation.core.tween
-import androidx.compose.foundation.Image
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
-import androidx.compose.material.CircularProgressIndicator
-import androidx.compose.material.Text
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.ProgressIndicatorDefaults
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.drawscope.withTransform
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import com.stripe.android.financialconnections.R
-import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
+import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsScaffold
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsTopAppBar
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
+
+private const val SHIMMER_SIZE_MULTIPLIER = 0.2f
+private const val SHIMMER_GRADIENT_ALPHA = 0.4f
 
 @Composable
-@Suppress("MagicNumber")
 internal fun LoadingShimmerEffect(
     content: @Composable (Brush) -> Unit
 ) {
+    val screenWidthDp = with(LocalConfiguration.current) { screenWidthDp.dp }
+    val screenWidth = with(LocalDensity.current) { screenWidthDp.toPx() }
+    val shimmerWidth = screenWidth * SHIMMER_SIZE_MULTIPLIER
+
     val gradient = listOf(
-        FinancialConnectionsTheme.colors.backgroundContainer,
-        FinancialConnectionsTheme.colors.textWhite,
-        FinancialConnectionsTheme.colors.backgroundContainer
+        colors.backgroundOffset,
+        Color.White.copy(alpha = SHIMMER_GRADIENT_ALPHA),
+        colors.backgroundOffset
     )
-    val transition = rememberInfiniteTransition()
+    val transition = rememberInfiniteTransition(label = "shimmer_transition")
     val translateAnimation = transition.animateFloat(
+        label = "shimmer_translate_animation",
         initialValue = 0f,
-        targetValue = 1000f,
+        targetValue = screenWidth,
         animationSpec = infiniteRepeatable(
             animation = tween(
-                durationMillis = 1000,
-                easing = FastOutLinearInEasing
+                durationMillis = LOADING_SPINNER_ROTATION_MS,
+                easing = LinearEasing
             )
         )
     )
     val brush = Brush.linearGradient(
         colors = gradient,
-        start = Offset(200f, 200f),
+        start = Offset(
+            translateAnimation.value - shimmerWidth,
+            translateAnimation.value - shimmerWidth
+        ),
         end = Offset(
-            x = translateAnimation.value,
-            y = translateAnimation.value
+            translateAnimation.value,
+            translateAnimation.value
         )
     )
     content(brush)
 }
 
 @Composable
-internal fun LoadingContent(
-    modifier: Modifier = Modifier,
-    title: String? = null,
-    content: String? = null
-) {
-    Column(
-        modifier = modifier
-            .padding(horizontal = 24.dp)
-    ) {
-        Spacer(modifier = Modifier.size(8.dp))
-        LoadingSpinner()
-        if (title != null) {
-            Spacer(modifier = Modifier.size(16.dp))
-            Text(
-                text = title,
-                style = FinancialConnectionsTheme.typography.subtitle
-            )
-        }
-        if (content != null) {
-            Spacer(modifier = Modifier.size(16.dp))
-            Text(
-                text = content,
-                style = FinancialConnectionsTheme.typography.body
-            )
-        }
+internal fun FullScreenGenericLoading() {
+    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+        LoadingSpinner(Modifier.size(52.dp))
     }
 }
 
 @Composable
-internal fun LoadingSpinner() {
-    val infiniteTransition = rememberInfiniteTransition()
+internal fun LoadingSpinner(
+    modifier: Modifier = Modifier,
+    strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth,
+    gradient: Brush = Brush.sweepGradient(listOf(colors.iconWhite, colors.borderBrand))
+) {
+    val infiniteTransition = rememberInfiniteTransition(label = "loading_transition")
     val angle by infiniteTransition.animateFloat(
-        initialValue = 0F,
-        targetValue = 360F,
+        label = "loading_animation",
+        initialValue = 0f,
+        targetValue = 360f,
         animationSpec = infiniteRepeatable(
-            animation = tween(LOADING_SPINNER_ROTATION_MS)
-        )
+            animation = tween(LOADING_SPINNER_ROTATION_MS, easing = LinearEasing),
+            repeatMode = RepeatMode.Restart
+        ),
     )
-    Image(
-        painter = painterResource(id = R.drawable.stripe_ic_loading_spinner),
-        modifier = Modifier.graphicsLayer { rotationZ = angle },
-        contentDescription = "Loading spinner."
-    )
-}
 
-@Composable
-internal fun FullScreenGenericLoading() {
-    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
-        CircularProgressIndicator(
-            strokeWidth = 2.dp,
-            color = FinancialConnectionsTheme.colors.textSecondary,
+    Canvas(modifier = modifier) {
+        val diameter = size.minDimension
+        val radius = diameter / 2f
+        val strokePx = strokeWidth.toPx()
+        val arcDiameter = diameter - strokePx
+        val arcRadius = arcDiameter / 2f
+        val topLeftArc = Offset(radius - arcRadius, radius - arcRadius)
+
+        withTransform(
+            transformBlock = {
+                rotate(
+                    degrees = angle,
+                    pivot = Offset(size.width / 2, size.height / 2)
+                )
+            },
+            drawBlock = {
+                drawArc(
+                    brush = gradient,
+                    startAngle = 90f,
+                    sweepAngle = 260f,
+                    useCenter = false,
+                    topLeft = topLeftArc,
+                    size = Size(arcDiameter, arcDiameter),
+                    style = Stroke(width = strokePx, cap = StrokeCap.Round)
+                )
+            }
         )
     }
 }
@@ -123,5 +144,50 @@ private const val LOADING_SPINNER_ROTATION_MS = 1000
 )
 @Composable
 internal fun LoadingSpinnerPreview() {
-    LoadingSpinner()
+    FinancialConnectionsPreview {
+        FinancialConnectionsScaffold(
+            topBar = { FinancialConnectionsTopAppBar(onCloseClick = {}) },
+            content = {
+                FullScreenGenericLoading()
+            }
+        )
+    }
+}
+
+@Preview(
+    group = "Loading",
+    name = "Shimmer",
+
+)
+@Composable
+internal fun LoadingShimmerPreview() {
+    FinancialConnectionsPreview {
+        FinancialConnectionsScaffold(
+            topBar = { FinancialConnectionsTopAppBar(onCloseClick = {}) },
+            content = {
+                Column(
+                    modifier = Modifier.padding(16.dp),
+                    verticalArrangement = Arrangement.spacedBy(16.dp),
+                ) {
+                    LoadingShimmerEffect {
+                        Box(
+                            modifier = Modifier
+                                .fillMaxWidth()
+                                .height(72.dp)
+                                .clip(RoundedCornerShape(16.dp))
+                                .background(it)
+                        )
+                    }
+                    LoadingShimmerEffect {
+                        Box(
+                            modifier = Modifier
+                                .size(100.dp)
+                                .clip(RoundedCornerShape(16.dp))
+                                .background(it)
+                        )
+                    }
+                }
+            }
+        )
+    }
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ManifestExtensions.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ManifestExtensions.kt
index 0989968e8f4..a1ba5c2d419 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ManifestExtensions.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ManifestExtensions.kt
@@ -29,6 +29,10 @@ internal fun FinancialConnectionsSessionManifest.enableRetrieveAuthSession(): Bo
     features
         ?.get("bank_connections_disable_defensive_auth_session_retrieval_on_complete") != true
 
+internal fun FinancialConnectionsSessionManifest.useContinueWithMerchantText(): Boolean =
+    features
+        ?.get("bank_connections_continue_with_merchant_text") == true
+
 internal fun SynchronizeSessionResponse.showManualEntryInErrors(): Boolean {
     return manifest.allowManualEntry && visual.reducedManualEntryProminenceInErrors.not()
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/MerchantDataAccessText.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/MerchantDataAccessText.kt
new file mode 100644
index 00000000000..9460c5ef981
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/MerchantDataAccessText.kt
@@ -0,0 +1,139 @@
+package com.stripe.android.financialconnections.features.common
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.stripe.android.financialconnections.R
+import com.stripe.android.financialconnections.model.FinancialConnectionsAccount.Permissions
+import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
+import com.stripe.android.financialconnections.ui.TextResource
+import com.stripe.android.financialconnections.ui.components.AnnotatedText
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsScaffold
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
+
+@Composable
+internal fun MerchantDataAccessText(
+    model: MerchantDataAccessModel,
+    onLearnMoreClick: () -> Unit
+) {
+    val permissionsReadable = remember(model.permissions) { model.permissions.toStringRes() }
+    AnnotatedText(
+        modifier = Modifier.fillMaxWidth(),
+        text = TextResource.StringId(
+            value = when {
+                model.isStripeDirect -> R.string.stripe_data_accessible_callout_stripe
+                else -> when (model.businessName) {
+                    null -> R.string.stripe_data_accessible_callout_business
+                    else -> R.string.stripe_data_accessible_callout_no_business
+                }
+            },
+            args = listOfNotNull(
+                model.businessName,
+                readableListOfPermissions(permissionsReadable)
+            )
+        ),
+        onClickableTextClick = {
+            onLearnMoreClick()
+        },
+        defaultStyle = FinancialConnectionsTheme.typography.labelSmall.copy(
+            color = FinancialConnectionsTheme.colors.textDefault,
+            textAlign = TextAlign.Center
+        ),
+    )
+}
+
+@Composable
+private fun readableListOfPermissions(permissionsReadable: List): String =
+    permissionsReadable
+        // TODO@carlosmuvi localize enumeration of permissions once more languages are supported.
+        .map { stringResource(id = it) }
+        .foldIndexed("") { index, current, arg ->
+            when {
+                index == 0 -> arg
+                permissionsReadable.lastIndex == index -> "$current and $arg"
+                else -> "$current, $arg"
+            }
+        }
+
+private fun List.toStringRes(): List = mapNotNull {
+    when (it) {
+        Permissions.BALANCES -> R.string.stripe_data_accessible_type_balances
+        Permissions.OWNERSHIP -> R.string.stripe_data_accessible_type_ownership
+        Permissions.PAYMENT_METHOD,
+        Permissions.ACCOUNT_NUMBERS -> R.string.stripe_data_accessible_type_accountdetails
+
+        Permissions.TRANSACTIONS -> R.string.stripe_data_accessible_type_transactions
+        Permissions.UNKNOWN -> null
+    }
+}.distinct()
+
+internal data class MerchantDataAccessModel(
+    val businessName: String?,
+    val permissions: List,
+    val isStripeDirect: Boolean,
+)
+
+@Preview(
+    group = "Merchant data access text",
+    name = "Default"
+)
+@Composable
+internal fun MerchantDataAccessTextPreview() {
+    FinancialConnectionsPreview {
+        FinancialConnectionsScaffold(
+            topBar = { /*TODO*/ }
+        ) {
+            Column(
+                Modifier.fillMaxWidth().padding(16.dp),
+                verticalArrangement = Arrangement.spacedBy(16.dp),
+            ) {
+                // all permissions
+                MerchantDataAccessText(
+                    MerchantDataAccessModel(
+                        businessName = "My business",
+                        permissions = listOf(
+                            Permissions.PAYMENT_METHOD,
+                            Permissions.BALANCES,
+                            Permissions.OWNERSHIP,
+                            Permissions.TRANSACTIONS,
+                            Permissions.ACCOUNT_NUMBERS
+                        ),
+                        isStripeDirect = false,
+                    ),
+                    onLearnMoreClick = {}
+                )
+                // one permission
+                MerchantDataAccessText(
+                    MerchantDataAccessModel(
+                        businessName = "My business",
+                        permissions = listOf(
+                            Permissions.TRANSACTIONS,
+                        ),
+                        isStripeDirect = false,
+                    ),
+                    onLearnMoreClick = {}
+                )
+                // two permissions
+                MerchantDataAccessText(
+                    MerchantDataAccessModel(
+                        businessName = "My business",
+                        permissions = listOf(
+                            Permissions.TRANSACTIONS,
+                            Permissions.OWNERSHIP,
+                        ),
+                        isStripeDirect = false,
+                    ),
+                    onLearnMoreClick = {}
+                )
+            }
+        }
+    }
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ModalBottomSheetContent.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ModalBottomSheetContent.kt
index 8b2d3b127fb..a30210083d5 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ModalBottomSheetContent.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ModalBottomSheetContent.kt
@@ -1,38 +1,31 @@
-@file:Suppress("LongMethod")
-
 package com.stripe.android.financialconnections.features.common
 
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Divider
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
 import com.stripe.android.financialconnections.model.DataAccessNotice
 import com.stripe.android.financialconnections.model.LegalDetailsNotice
-import com.stripe.android.financialconnections.ui.ImageResource
-import com.stripe.android.financialconnections.ui.LocalImageLoader
 import com.stripe.android.financialconnections.ui.TextResource
 import com.stripe.android.financialconnections.ui.components.AnnotatedText
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsButton
 import com.stripe.android.financialconnections.ui.components.StringAnnotation
 import com.stripe.android.financialconnections.ui.sdui.BulletUI
 import com.stripe.android.financialconnections.ui.sdui.fromHtml
+import com.stripe.android.financialconnections.ui.sdui.rememberHtml
 import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
 import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
-import com.stripe.android.uicore.image.StripeImage
+import com.stripe.android.financialconnections.ui.theme.Layout
 
 @Composable
 internal fun DataAccessBottomSheetContent(
@@ -40,30 +33,53 @@ internal fun DataAccessBottomSheetContent(
     onClickableTextClick: (String) -> Unit,
     onConfirmModalClick: () -> Unit
 ) {
-    val title = remember(dataDialog.title) {
-        TextResource.Text(fromHtml(dataDialog.title))
-    }
-    val subtitle = remember(dataDialog.subtitle) {
-        dataDialog.subtitle?.let { TextResource.Text(fromHtml(it)) }
-    }
-    val learnMore = remember(dataDialog.learnMore) {
-        TextResource.Text(fromHtml(dataDialog.learnMore))
-    }
-    val connectedAccountNotice = remember(dataDialog.connectedAccountNotice) {
-        dataDialog.connectedAccountNotice?.let { TextResource.Text(fromHtml(it)) }
-    }
+    val title = rememberHtml(dataDialog.title)
+    val subtitle = dataDialog.subtitle?.let { rememberHtml(it) }
+    val disclaimer = dataDialog.disclaimer?.let { rememberHtml(it) }
     val bullets = remember(dataDialog.body.bullets) {
         dataDialog.body.bullets.map { BulletUI.from(it) }
     }
     ModalBottomSheetContent(
-        title = title,
-        subtitle = subtitle,
         onClickableTextClick = onClickableTextClick,
-        bullets = bullets,
-        connectedAccountNotice = connectedAccountNotice,
         cta = dataDialog.cta,
-        learnMore = learnMore,
+        disclaimer = disclaimer,
         onConfirmModalClick = onConfirmModalClick,
+        content = {
+            dataDialog.icon?.default?.let {
+                ShapedIcon(url = it, contentDescription = "Icon")
+                Spacer(modifier = Modifier.size(16.dp))
+            }
+            Title(title = title, onClickableTextClick = onClickableTextClick)
+            // FOR CONNECTED ACCOUNTS: Permissions granted to Stripe by the connected account
+            dataDialog.connectedAccountNotice?.let {
+                Spacer(modifier = Modifier.size(16.dp))
+                Subtitle(
+                    text = rememberHtml(it.subtitle),
+                    onClickableTextClick = onClickableTextClick
+                )
+                Spacer(modifier = Modifier.size(24.dp))
+                it.body.bullets.forEach { bullet ->
+                    ListItem(
+                        bullet = BulletUI.from(bullet),
+                        onClickableTextClick = onClickableTextClick
+                    )
+                    Spacer(modifier = Modifier.size(16.dp))
+                }
+            }
+            // FOR ALL MERCHANTS: Permissions granted to Stripe by the merchant
+            subtitle?.let {
+                Spacer(modifier = Modifier.size(16.dp))
+                Subtitle(it, onClickableTextClick)
+            }
+            Spacer(modifier = Modifier.size(24.dp))
+            bullets.forEach { bullet ->
+                ListItem(
+                    bullet = bullet,
+                    onClickableTextClick = onClickableTextClick
+                )
+                Spacer(modifier = Modifier.size(16.dp))
+            }
+        }
     )
 }
 
@@ -73,253 +89,136 @@ internal fun LegalDetailsBottomSheetContent(
     onClickableTextClick: (String) -> Unit,
     onConfirmModalClick: () -> Unit
 ) {
-    val title = remember(legalDetails.title) {
-        TextResource.Text(fromHtml(legalDetails.title))
-    }
-    val learnMore = remember(legalDetails.learnMore) {
-        TextResource.Text(fromHtml(legalDetails.learnMore))
-    }
-    val bullets = remember(legalDetails.body.bullets) {
-        legalDetails.body.bullets.map { BulletUI.from(it) }
+    val title = rememberHtml(legalDetails.title)
+    val subtitle = legalDetails.subtitle?.let { rememberHtml(it) }
+    val learnMore = legalDetails.disclaimer?.let { rememberHtml(it) }
+    val links = remember(legalDetails.body.links) {
+        legalDetails.body.links.map { TextResource.Text(fromHtml(it.title)) }
     }
     ModalBottomSheetContent(
-        title = title,
-        subtitle = null,
         onClickableTextClick = onClickableTextClick,
-        bullets = bullets,
-        connectedAccountNotice = null,
         cta = legalDetails.cta,
-        learnMore = learnMore,
-        onConfirmModalClick = onConfirmModalClick,
-    )
+        disclaimer = learnMore,
+        onConfirmModalClick = onConfirmModalClick
+    ) {
+        legalDetails.icon?.default?.let {
+            ShapedIcon(it, contentDescription = "legal details icon")
+            Spacer(modifier = Modifier.size(16.dp))
+        }
+
+        Title(title = title, onClickableTextClick = onClickableTextClick)
+
+        subtitle?.let {
+            Spacer(modifier = Modifier.size(16.dp))
+            Subtitle(text = it, onClickableTextClick = onClickableTextClick)
+        }
+
+        Spacer(modifier = Modifier.size(24.dp))
+
+        Links(links, onClickableTextClick)
+    }
 }
 
 @Composable
-private fun ModalBottomSheetContent(
-    title: TextResource.Text,
-    subtitle: TextResource.Text?,
+private fun Links(
+    links: List,
     onClickableTextClick: (String) -> Unit,
-    bullets: List,
-    connectedAccountNotice: TextResource?,
-    cta: String,
-    learnMore: TextResource,
-    onConfirmModalClick: () -> Unit,
 ) {
-    val scrollState = rememberScrollState()
     Column {
-        Column(
-            Modifier
-                .weight(1f, fill = false)
-                .verticalScroll(scrollState)
-                .padding(24.dp)
-        ) {
+        val linkStyle = typography.labelLargeEmphasized.copy(color = colors.textBrand)
+        links.forEachIndexed { index, link ->
+            Divider(color = colors.border)
             AnnotatedText(
-                text = title,
-                defaultStyle = typography.heading.copy(
-                    color = colors.textPrimary
+                modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp),
+                text = link,
+                defaultStyle = linkStyle,
+                annotationStyles = mapOf(
+                    StringAnnotation.CLICKABLE to linkStyle.toSpanStyle()
                 ),
-                annotationStyles = emptyMap(),
-                onClickableTextClick = onClickableTextClick
-            )
-            subtitle?.let {
-                Spacer(modifier = Modifier.size(4.dp))
-                AnnotatedText(
-                    text = it,
-                    defaultStyle = typography.body.copy(
-                        color = colors.textPrimary
-                    ),
-                    annotationStyles = mapOf(
-                        StringAnnotation.CLICKABLE to typography.detail
-                            .toSpanStyle()
-                            .copy(color = colors.textBrand),
-                        StringAnnotation.BOLD to typography.detailEmphasized
-                            .toSpanStyle()
-                            .copy(color = colors.textPrimary),
-                    ),
-                    onClickableTextClick = onClickableTextClick
-                )
-            }
-            bullets.forEach {
-                Spacer(modifier = Modifier.size(16.dp))
-                BulletItem(
-                    bullet = it,
-                    onClickableTextClick = onClickableTextClick
-                )
-            }
-        }
-        Column(
-            Modifier.padding(
-                bottom = 24.dp,
-                start = 24.dp,
-                end = 24.dp
-            )
-        ) {
-            if (connectedAccountNotice != null) {
-                AnnotatedText(
-                    text = connectedAccountNotice,
-                    onClickableTextClick = onClickableTextClick,
-                    defaultStyle = typography.caption.copy(
-                        color = colors.textSecondary
-                    ),
-                    annotationStyles = mapOf(
-                        StringAnnotation.CLICKABLE to typography.captionEmphasized
-                            .toSpanStyle()
-                            .copy(color = colors.textBrand),
-                        StringAnnotation.BOLD to typography.captionEmphasized
-                            .toSpanStyle()
-                            .copy(color = colors.textSecondary),
-                    )
-                )
-                Spacer(modifier = Modifier.size(12.dp))
-            }
-            AnnotatedText(
-                text = learnMore,
                 onClickableTextClick = onClickableTextClick,
-                defaultStyle = typography.caption.copy(
-                    color = colors.textSecondary
-                ),
-                annotationStyles = mapOf(
-                    StringAnnotation.CLICKABLE to typography.captionEmphasized
-                        .toSpanStyle()
-                        .copy(color = colors.textBrand),
-                    StringAnnotation.BOLD to typography.captionEmphasized
-                        .toSpanStyle()
-                        .copy(color = colors.textSecondary),
-                )
             )
-            Spacer(modifier = Modifier.size(16.dp))
-            FinancialConnectionsButton(
-                onClick = { onConfirmModalClick() },
-                modifier = Modifier.fillMaxWidth()
-            ) {
-                Text(text = cta)
+            if (links.lastIndex == index) {
+                Divider(color = colors.border)
             }
         }
     }
 }
 
 @Composable
-internal fun BulletItem(
-    bullet: BulletUI,
+private fun Title(
+    title: TextResource.Text,
     onClickableTextClick: (String) -> Unit
 ) {
-    Row {
-        BulletIcon(icon = bullet.imageResource)
-        Spacer(modifier = Modifier.size(8.dp))
-        Column {
-            when {
-                // title + content
-                bullet.title != null && bullet.content != null -> {
-                    AnnotatedText(
-                        text = bullet.title,
-                        defaultStyle = typography.body.copy(
-                            color = colors.textPrimary
-                        ),
-                        annotationStyles = mapOf(
-                            StringAnnotation.CLICKABLE to typography.bodyEmphasized
-                                .toSpanStyle()
-                                .copy(color = colors.textBrand),
-                            StringAnnotation.BOLD to typography.bodyEmphasized
-                                .toSpanStyle()
-                                .copy(color = colors.textPrimary),
-                        ),
-                        onClickableTextClick = onClickableTextClick
-                    )
-                    Spacer(modifier = Modifier.size(2.dp))
-                    AnnotatedText(
-                        text = bullet.content,
-                        defaultStyle = typography.detail.copy(
-                            color = colors.textSecondary
-                        ),
-                        annotationStyles = mapOf(
-                            StringAnnotation.CLICKABLE to typography.detailEmphasized
-                                .toSpanStyle()
-                                .copy(color = colors.textBrand),
-                            StringAnnotation.BOLD to typography.detailEmphasized
-                                .toSpanStyle()
-                                .copy(color = colors.textSecondary),
-                        ),
-                        onClickableTextClick = onClickableTextClick
-                    )
-                }
-                // only title
-                bullet.title != null -> {
-                    AnnotatedText(
-                        text = bullet.title,
-                        defaultStyle = typography.body.copy(
-                            color = colors.textPrimary
-                        ),
-                        annotationStyles = mapOf(
-                            StringAnnotation.CLICKABLE to typography.bodyEmphasized
-                                .toSpanStyle()
-                                .copy(color = colors.textBrand),
-                            StringAnnotation.BOLD to typography.bodyEmphasized
-                                .toSpanStyle()
-                                .copy(color = colors.textPrimary),
-                        ),
-                        onClickableTextClick = onClickableTextClick
-                    )
-                }
-                // only content
-                bullet.content != null -> {
-                    AnnotatedText(
-                        text = bullet.content,
-                        defaultStyle = typography.body.copy(
-                            color = colors.textSecondary
-                        ),
-                        annotationStyles = mapOf(
-                            StringAnnotation.CLICKABLE to typography.bodyEmphasized
-                                .toSpanStyle()
-                                .copy(color = colors.textBrand),
-                            StringAnnotation.BOLD to typography.bodyEmphasized
-                                .toSpanStyle()
-                                .copy(color = colors.textSecondary),
-                        ),
-                        onClickableTextClick = onClickableTextClick
-                    )
-                }
-            }
-        }
-    }
+    AnnotatedText(
+        text = title,
+        defaultStyle = typography.headingMedium.copy(
+            color = colors.textDefault
+        ),
+        onClickableTextClick = onClickableTextClick
+    )
 }
 
 @Composable
-private fun BulletIcon(icon: ImageResource?) {
-    val modifier = Modifier
-        .size(16.dp)
-        .offset(y = 2.dp)
-    if (icon == null) {
-        val color = colors.textPrimary
-        Canvas(
-            modifier = Modifier
-                .size(16.dp)
-                .padding(6.dp)
-                .offset(y = 2.dp),
-            onDraw = { drawCircle(color = color) }
-        )
-    } else {
-        when (icon) {
-            is ImageResource.Local -> Image(
-                modifier = modifier,
-                painter = painterResource(id = icon.resId),
-                contentDescription = null,
-            )
-
-            is ImageResource.Network -> StripeImage(
-                url = icon.url,
-                errorContent = {
-                    val color = colors.textSecondary
-                    Canvas(
-                        modifier = Modifier
-                            .size(6.dp)
-                            .align(Alignment.Center),
-                        onDraw = { drawCircle(color = color) }
-                    )
-                },
-                imageLoader = LocalImageLoader.current,
-                contentDescription = null,
-                modifier = modifier
+private fun ModalBottomSheetContent(
+    onClickableTextClick: (String) -> Unit,
+    cta: String,
+    disclaimer: TextResource?,
+    onConfirmModalClick: () -> Unit,
+    content: @Composable ColumnScope.() -> Unit,
+) {
+    Layout(
+        modifier = Modifier.padding(top = 32.dp),
+        inModal = true,
+        body = { content() },
+        footer = {
+            ModalBottomSheetFooter(
+                onClickableTextClick = onClickableTextClick,
+                disclaimer = disclaimer,
+                onConfirmModalClick = onConfirmModalClick,
+                cta = cta
             )
         }
+    )
+}
+
+@Composable
+private fun Subtitle(
+    text: TextResource,
+    onClickableTextClick: (String) -> Unit
+) {
+    AnnotatedText(
+        text = text,
+        defaultStyle = typography.bodyMedium.copy(
+            color = colors.textDefault
+        ),
+        onClickableTextClick = onClickableTextClick
+    )
+}
+
+@Composable
+private fun ModalBottomSheetFooter(
+    onClickableTextClick: (String) -> Unit,
+    disclaimer: TextResource?,
+    onConfirmModalClick: () -> Unit,
+    cta: String
+) = Column {
+    disclaimer?.let {
+        Spacer(modifier = Modifier.size(16.dp))
+        AnnotatedText(
+            modifier = Modifier.align(Alignment.CenterHorizontally),
+            text = it,
+            onClickableTextClick = onClickableTextClick,
+            defaultStyle = typography.labelSmall.copy(
+                color = colors.textDefault,
+                textAlign = TextAlign.Center
+            ),
+        )
+    }
+    Spacer(modifier = Modifier.size(16.dp))
+    FinancialConnectionsButton(
+        onClick = { onConfirmModalClick() },
+        modifier = Modifier.fillMaxWidth()
+    ) {
+        Text(text = cta)
     }
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/PaneFooter.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/PaneFooter.kt
deleted file mode 100644
index 38406be3312..00000000000
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/PaneFooter.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.stripe.android.financialconnections.features.common
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material.Surface
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
-
-/**
- * A Composable function that displays a Pane footer with a given elevation and content.
- *
- * @param elevation The elevation of the Surface that wraps the Column content.
- * @param content of the footer.
- */
-@Composable
-internal fun PaneFooter(
-    elevation: Dp,
-    content: @Composable ColumnScope.() -> Unit
-) {
-    Surface(
-        color = FinancialConnectionsTheme.colors.backgroundSurface,
-        elevation = elevation
-    ) {
-        Column(
-            modifier = Modifier.padding(
-                start = 24.dp,
-                end = 24.dp,
-                top = 16.dp,
-                bottom = 24.dp
-            ),
-            content = content
-        )
-    }
-}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/PartnerCallout.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/PartnerCallout.kt
deleted file mode 100644
index e12098391cc..00000000000
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/PartnerCallout.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.stripe.android.financialconnections.features.common
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.unit.dp
-import com.stripe.android.financialconnections.model.PartnerNotice
-import com.stripe.android.financialconnections.ui.LocalImageLoader
-import com.stripe.android.financialconnections.ui.TextResource
-import com.stripe.android.financialconnections.ui.components.AnnotatedText
-import com.stripe.android.financialconnections.ui.components.StringAnnotation
-import com.stripe.android.financialconnections.ui.sdui.fromHtml
-import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
-import com.stripe.android.uicore.image.StripeImage
-
-@Composable
-internal fun PartnerCallout(
-    modifier: Modifier = Modifier,
-    partnerNotice: PartnerNotice,
-    onClickableTextClick: (String) -> Unit
-) {
-    Row(
-        modifier = modifier
-            .fillMaxWidth()
-            .clip(shape = RoundedCornerShape(8.dp))
-            .background(color = FinancialConnectionsTheme.colors.backgroundContainer)
-            .padding(12.dp)
-    ) {
-        StripeImage(
-            url = partnerNotice.partnerIcon.default ?: "",
-            imageLoader = LocalImageLoader.current,
-            contentDescription = null,
-            modifier = Modifier
-                .size(24.dp)
-                .clip(RoundedCornerShape(6.dp))
-        )
-        Spacer(modifier = Modifier.size(16.dp))
-        AnnotatedText(
-            TextResource.Text(
-                fromHtml(partnerNotice.text)
-            ),
-            defaultStyle = FinancialConnectionsTheme.typography.caption.copy(
-                color = FinancialConnectionsTheme.colors.textSecondary
-            ),
-            annotationStyles = mapOf(
-                StringAnnotation.CLICKABLE to FinancialConnectionsTheme.typography.captionEmphasized
-                    .toSpanStyle()
-                    .copy(color = FinancialConnectionsTheme.colors.textBrand),
-                StringAnnotation.BOLD to FinancialConnectionsTheme.typography.captionEmphasized
-                    .toSpanStyle()
-                    .copy(color = FinancialConnectionsTheme.colors.textSecondary)
-            ),
-            onClickableTextClick = onClickableTextClick
-        )
-    }
-}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ShapedIcon.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ShapedIcon.kt
new file mode 100644
index 00000000000..44d95f39e39
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/ShapedIcon.kt
@@ -0,0 +1,100 @@
+package com.stripe.android.financialconnections.features.common
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.stripe.android.financialconnections.R
+import com.stripe.android.financialconnections.ui.LocalImageLoader
+import com.stripe.android.financialconnections.ui.theme.Brand50
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
+import com.stripe.android.uicore.image.StripeImage
+
+private val iconSize = 20.dp
+
+/**
+ * A circular icon with a branded background color.
+ *
+ * @param painter the [Painter] to use for the icon
+ */
+@Composable
+internal fun ShapedIcon(
+    painter: Painter,
+    backgroundShape: Shape = CircleShape,
+    contentDescription: String?
+) {
+    CircleBox(
+        backgroundShape = backgroundShape
+    ) {
+        LocalIcon(
+            painter = painter,
+            contentDescription = contentDescription
+        )
+    }
+}
+
+/**
+ * A circular icon with a branded background color.
+ *
+ * @param url the URL to use for the icon
+ * @param errorPainter the [Painter] to use for the icon if the URL fails to load. If null,
+ *        no icon will be rendered inside the circle.
+ */
+@Composable
+internal fun ShapedIcon(
+    url: String,
+    backgroundShape: Shape = CircleShape,
+    contentDescription: String?,
+    errorPainter: Painter? = null
+) {
+    CircleBox(
+        backgroundShape = backgroundShape,
+    ) {
+        StripeImage(
+            modifier = Modifier.size(iconSize),
+            url = url,
+            imageLoader = LocalImageLoader.current,
+            debugPainter = painterResource(id = R.drawable.stripe_ic_person),
+            contentDescription = contentDescription,
+            errorContent = { errorPainter?.let { LocalIcon(errorPainter, contentDescription) } },
+            contentScale = ContentScale.Crop
+        )
+    }
+}
+
+@Composable
+private fun LocalIcon(
+    painter: Painter,
+    contentDescription: String?
+) {
+    Icon(
+        painter = painter,
+        tint = FinancialConnectionsTheme.colors.iconBrand,
+        contentDescription = contentDescription,
+        modifier = Modifier.size(iconSize),
+    )
+}
+
+@Composable
+private fun CircleBox(
+    backgroundShape: Shape,
+    content: @Composable () -> Unit,
+) {
+    Box(
+        contentAlignment = Alignment.Center,
+        modifier = Modifier
+            .size(56.dp)
+            .background(color = Brand50, shape = backgroundShape)
+    ) {
+        content()
+    }
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/SharedPartnerAuth.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/SharedPartnerAuth.kt
index e664943e83f..b0e88ba9ffb 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/SharedPartnerAuth.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/SharedPartnerAuth.kt
@@ -3,38 +3,36 @@ package com.stripe.android.financialconnections.features.common
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import android.webkit.WebView
-import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.ModalBottomSheetLayout
-import androidx.compose.material.ModalBottomSheetState
 import androidx.compose.material.ModalBottomSheetValue
 import androidx.compose.material.Text
 import androidx.compose.material.rememberModalBottomSheetState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalInspectionMode
 import androidx.compose.ui.platform.LocalUriHandler
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.testTagsAsResourceId
@@ -50,10 +48,10 @@ import com.airbnb.mvrx.Success
 import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.compose.collectAsState
 import com.stripe.android.financialconnections.R
-import com.stripe.android.financialconnections.exception.InstitutionPlannedDowntimeError
-import com.stripe.android.financialconnections.exception.InstitutionUnplannedDowntimeError
 import com.stripe.android.financialconnections.features.partnerauth.PartnerAuthPreviewParameterProvider
 import com.stripe.android.financialconnections.features.partnerauth.SharedPartnerAuthState
+import com.stripe.android.financialconnections.features.partnerauth.SharedPartnerAuthState.AuthenticationStatus
+import com.stripe.android.financialconnections.features.partnerauth.SharedPartnerAuthState.AuthenticationStatus.Action
 import com.stripe.android.financialconnections.features.partnerauth.SharedPartnerAuthState.ViewEffect
 import com.stripe.android.financialconnections.model.Entry
 import com.stripe.android.financialconnections.model.OauthPrepane
@@ -64,23 +62,24 @@ import com.stripe.android.financialconnections.ui.LocalImageLoader
 import com.stripe.android.financialconnections.ui.TextResource
 import com.stripe.android.financialconnections.ui.components.AnnotatedText
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsButton
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsButton.Type
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsScaffold
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsTopAppBar
-import com.stripe.android.financialconnections.ui.components.StringAnnotation
 import com.stripe.android.financialconnections.ui.sdui.fromHtml
-import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
+import com.stripe.android.financialconnections.ui.theme.LazyLayout
 import com.stripe.android.uicore.image.StripeImage
-import kotlinx.coroutines.launch
 
 @Composable
 internal fun SharedPartnerAuth(
     state: SharedPartnerAuthState,
     onContinueClick: () -> Unit,
-    onSelectAnotherBank: () -> Unit,
+    onCancelClick: () -> Unit,
     onClickableTextClick: (String) -> Unit,
-    onEnterDetailsManually: () -> Unit,
     onWebAuthFlowFinished: (WebAuthFlowState) -> Unit,
-    onViewEffectLaunched: () -> Unit
+    onViewEffectLaunched: () -> Unit,
+    inModal: Boolean
 ) {
     val viewModel = parentViewModel()
 
@@ -108,268 +107,306 @@ internal fun SharedPartnerAuth(
     }
 
     SharedPartnerAuthContent(
-        bottomSheetState = bottomSheetState,
+        inModal = inModal,
         state = state,
         onClickableTextClick = onClickableTextClick,
-        onSelectAnotherBank = onSelectAnotherBank,
-        onEnterDetailsManually = onEnterDetailsManually,
-        onCloseClick = { viewModel.onCloseWithConfirmationClick(state.pane) },
         onContinueClick = onContinueClick,
-        onCloseFromErrorClick = viewModel::onCloseFromErrorClick
+        onCloseClick = { viewModel.onCloseWithConfirmationClick(state.pane) },
+        onCancelClick = onCancelClick,
     )
 }
 
 @Composable
 private fun SharedPartnerAuthContent(
-    bottomSheetState: ModalBottomSheetState,
     state: SharedPartnerAuthState,
+    inModal: Boolean,
     onClickableTextClick: (String) -> Unit,
-    onSelectAnotherBank: () -> Unit,
-    onEnterDetailsManually: () -> Unit,
     onContinueClick: () -> Unit,
     onCloseClick: () -> Unit,
-    onCloseFromErrorClick: (Throwable) -> Unit
+    onCancelClick: () -> Unit,
 ) {
-    val scope = rememberCoroutineScope()
-    ModalBottomSheetLayout(
-        sheetState = bottomSheetState,
-        sheetBackgroundColor = FinancialConnectionsTheme.colors.backgroundSurface,
-        sheetShape = RoundedCornerShape(8.dp),
-        scrimColor = FinancialConnectionsTheme.colors.textSecondary.copy(alpha = 0.5f),
-        sheetContent = {
-            state.dataAccess?.let {
-                DataAccessBottomSheetContent(
-                    dataDialog = it,
-                    onConfirmModalClick = { scope.launch { bottomSheetState.hide() } },
-                    onClickableTextClick = onClickableTextClick
-                )
-            } ?: Spacer(modifier = Modifier.size(16.dp))
-        },
-        content = {
-            SharedPartnerAuthBody(
-                state = state,
-                onCloseClick = onCloseClick,
-                onSelectAnotherBank = onSelectAnotherBank,
-                onEnterDetailsManually = onEnterDetailsManually,
-                onCloseFromErrorClick = onCloseFromErrorClick,
-                onClickableTextClick = onClickableTextClick,
-                onContinueClick = onContinueClick,
+    SharedPartnerAuthBody(
+        inModal = inModal,
+        state = state,
+        onCloseClick = onCloseClick,
+        onClickableTextClick = onClickableTextClick,
+        onCancelClick = onCancelClick,
+        onContinueClick = onContinueClick,
+    )
+}
+
+@Composable
+private fun SharedPartnerLoading(inModal: Boolean) {
+    LoadingShimmerEffect { shimmerBrush ->
+        Column(
+            Modifier.padding(horizontal = 24.dp)
+        ) {
+            Spacer(modifier = Modifier.size(24.dp))
+            Box(
+                modifier = Modifier
+                    .size(56.dp)
+                    .background(shimmerBrush, RoundedCornerShape(8.dp))
+            )
+            Spacer(modifier = Modifier.size(16.dp))
+            Box(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .height(32.dp)
+                    .background(shimmerBrush, RoundedCornerShape(8.dp))
+
+            )
+            Spacer(modifier = Modifier.size(16.dp))
+
+            Box(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .height(16.dp)
+                    .background(shimmerBrush, RoundedCornerShape(8.dp))
+
+            )
+            Spacer(modifier = Modifier.size(8.dp))
+            Box(
+                modifier = Modifier
+                    .fillMaxWidth(.50f)
+                    .height(16.dp)
+                    .background(shimmerBrush, RoundedCornerShape(8.dp))
+
             )
+            if (inModal) {
+                Spacer(modifier = Modifier.height(16.dp))
+            } else {
+                Spacer(modifier = Modifier.weight(1f))
+            }
+            Box(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .height(56.dp)
+                    .background(shimmerBrush, RoundedCornerShape(8.dp))
+
+            )
+            Spacer(modifier = Modifier.height(16.dp))
+            Box(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .height(56.dp)
+                    .background(shimmerBrush, RoundedCornerShape(8.dp))
+
+            )
+            Spacer(modifier = Modifier.size(24.dp))
         }
-    )
+    }
 }
 
 @Composable
 private fun SharedPartnerAuthBody(
     state: SharedPartnerAuthState,
+    inModal: Boolean,
     onCloseClick: () -> Unit,
-    onSelectAnotherBank: () -> Unit,
-    onEnterDetailsManually: () -> Unit,
-    onCloseFromErrorClick: (Throwable) -> Unit,
+    onCancelClick: () -> Unit,
     onContinueClick: () -> Unit,
     onClickableTextClick: (String) -> Unit
 ) {
-    FinancialConnectionsScaffold(
-        topBar = {
-            FinancialConnectionsTopAppBar(
-                showBack = state.canNavigateBack,
-                onCloseClick = onCloseClick
-            )
-        }
+    SharedPartnerAuthContentWrapper(
+        inModal = inModal,
+        canNavigateBack = state.canNavigateBack,
+        onCloseClick = onCloseClick
     ) {
-        when (val payload = state.payload) {
-            Uninitialized, is Loading -> LoadingContent(
-                title = stringResource(id = R.string.stripe_partnerauth_loading_title),
-                content = stringResource(id = R.string.stripe_partnerauth_loading_desc)
-            )
-
-            is Fail -> ErrorContent(
-                error = payload.error,
-                onSelectAnotherBank = onSelectAnotherBank,
-                onEnterDetailsManually = onEnterDetailsManually,
-                onCloseFromErrorClick = onCloseFromErrorClick
-            )
-
-            is Success -> LoadedContent(
+        state.payload()?.let {
+            LoadedContent(
+                showInModal = inModal,
                 authenticationStatus = state.authenticationStatus,
-                payload = payload(),
-                onClickableTextClick = onClickableTextClick,
+                payload = it,
                 onContinueClick = onContinueClick,
-                onSelectAnotherBank = onSelectAnotherBank,
+                onCancelClick = onCancelClick,
+                onClickableTextClick = onClickableTextClick,
             )
-        }
+        } ?: SharedPartnerLoading(inModal)
     }
 }
 
+/**
+ * Wrapper for the content of the partner auth screen, that based on the [inModal] parameter
+ * will render the content in a modal or in a full screen.
+ */
 @Composable
-private fun ErrorContent(
-    error: Throwable,
-    onSelectAnotherBank: () -> Unit,
-    onEnterDetailsManually: () -> Unit,
-    onCloseFromErrorClick: (Throwable) -> Unit
+private fun SharedPartnerAuthContentWrapper(
+    inModal: Boolean,
+    canNavigateBack: Boolean,
+    onCloseClick: () -> Unit,
+    content: @Composable () -> Unit
 ) {
-    when (error) {
-        is InstitutionPlannedDowntimeError -> InstitutionPlannedDowntimeErrorContent(
-            exception = error,
-            onSelectAnotherBank = onSelectAnotherBank,
-            onEnterDetailsManually = onEnterDetailsManually
-        )
-
-        is InstitutionUnplannedDowntimeError -> InstitutionUnplannedDowntimeErrorContent(
-            exception = error,
-            onSelectAnotherBank = onSelectAnotherBank,
-            onEnterDetailsManually = onEnterDetailsManually
-        )
-
-        else -> UnclassifiedErrorContent(error, onCloseFromErrorClick)
+    if (inModal) {
+        Box(
+            modifier = Modifier.fillMaxWidth()
+        ) {
+            content()
+        }
+    } else {
+        FinancialConnectionsScaffold(
+            topBar = {
+                FinancialConnectionsTopAppBar(
+                    allowBackNavigation = canNavigateBack,
+                    onCloseClick = onCloseClick
+                )
+            }
+        ) {
+            content()
+        }
     }
 }
 
 @Composable
 private fun LoadedContent(
-    authenticationStatus: Async,
+    showInModal: Boolean,
+    authenticationStatus: Async,
     payload: SharedPartnerAuthState.Payload,
     onContinueClick: () -> Unit,
-    onSelectAnotherBank: () -> Unit,
+    onCancelClick: () -> Unit,
     onClickableTextClick: (String) -> Unit
 ) {
     when (authenticationStatus) {
-        is Uninitialized -> when (payload.authSession.isOAuth) {
-            true -> InstitutionalPrePaneContent(
+        is Uninitialized,
+        is Loading,
+        is Fail,
+        is Success -> when (payload.authSession.isOAuth) {
+            true -> PrePaneContent(
+                // show loading prepane when authenticationStatus
+                // is Loading or Success (completing auth after redirect)
+                authenticationStatus = authenticationStatus,
+                showInModal = showInModal,
                 onContinueClick = onContinueClick,
+                onCancelClick = onCancelClick,
                 content = requireNotNull(payload.authSession.display?.text?.oauthPrepane),
                 onClickableTextClick = onClickableTextClick,
             )
 
-            false -> LoadingContent(
-                title = stringResource(id = R.string.stripe_partnerauth_loading_title),
-                content = stringResource(id = R.string.stripe_partnerauth_loading_desc)
-            )
-        }
-
-        is Loading -> FullScreenGenericLoading()
-
-        is Success -> LoadingContent(
-            title = stringResource(R.string.stripe_account_picker_loading_title),
-            content = stringResource(R.string.stripe_account_picker_loading_desc)
-        )
-
-        is Fail -> {
-            // TODO@carlosmuvi translate error type to specific error screen.
-            InstitutionUnknownErrorContent(onSelectAnotherBank)
+            false -> SharedPartnerLoading(showInModal)
         }
     }
 }
 
-@OptIn(ExperimentalComposeUiApi::class)
 @Composable
-@Suppress("LongMethod")
-private fun InstitutionalPrePaneContent(
-    onContinueClick: () -> Unit,
+private fun PrePaneContent(
+    showInModal: Boolean,
     content: OauthPrepane,
-    onClickableTextClick: (String) -> Unit
+    authenticationStatus: Async,
+    onContinueClick: () -> Unit,
+    onCancelClick: () -> Unit,
+    onClickableTextClick: (String) -> Unit,
 ) {
-    val title = remember(content.title) {
-        TextResource.Text(fromHtml(content.title))
-    }
-    val scrollState = rememberScrollState()
-    Column(
-        Modifier
-            .fillMaxSize()
-            .padding(
-                top = 16.dp,
-                start = 24.dp,
-                end = 24.dp,
-                bottom = 24.dp
-            )
-    ) {
-        content.institutionIcon?.default?.let {
-            val institutionIconModifier = Modifier
-                .size(36.dp)
-                .clip(RoundedCornerShape(6.dp))
-            StripeImage(
-                url = it,
-                contentDescription = null,
-                imageLoader = LocalImageLoader.current,
-                errorContent = { InstitutionPlaceholder(institutionIconModifier) },
-                modifier = institutionIconModifier
-            )
-            Spacer(modifier = Modifier.size(16.dp))
-        }
-        AnnotatedText(
-            text = title,
-            onClickableTextClick = { },
-            defaultStyle = FinancialConnectionsTheme.typography.subtitle,
-            annotationStyles = mapOf(
-                StringAnnotation.BOLD to FinancialConnectionsTheme.typography.subtitleEmphasized.toSpanStyle()
-            )
-        )
-        Column(
-            modifier = Modifier
-                .padding(top = 16.dp, bottom = 16.dp)
-                .weight(1f)
-                .verticalScroll(scrollState)
-        ) {
-            // CONTENT
-            content.body.entries.forEachIndexed { index, bodyItem ->
+    LazyLayout(
+        inModal = showInModal,
+        // Overrides padding values to allow full-span prepane image background
+        verticalArrangement = Arrangement.spacedBy(24.dp),
+        bodyPadding = PaddingValues(top = 24.dp),
+        body = {
+            item {
+                PrepaneHeader(
+                    modifier = Modifier.padding(horizontal = 24.dp),
+                    content = content
+                )
+            }
+            items(content.body.entries) { bodyItem ->
                 when (bodyItem) {
-                    is Entry.Image -> {
-                        Box(
-                            modifier = Modifier
-                                .fillMaxWidth()
-                                .background(
-                                    shape = RoundedCornerShape(8.dp),
-                                    color = FinancialConnectionsTheme.colors.backgroundContainer
-                                )
-
-                        ) {
-                            Image(
-                                painter = painterResource(id = R.drawable.stripe_prepane_phone_bg),
-                                contentDescription = "Test",
-                                contentScale = ContentScale.Crop,
-                                modifier = Modifier
-                                    .align(Alignment.Center)
-                                    .width(PHONE_BACKGROUND_HEIGHT_DP.dp)
-                                    .height(PHONE_BACKGROUND_WIDTH_DP.dp)
-                            )
-                            GifWebView(
-                                modifier = Modifier
-                                    .align(Alignment.Center)
-                                    .width(PHONE_BACKGROUND_HEIGHT_DP.dp)
-                                    .height(PHONE_BACKGROUND_WIDTH_DP.dp)
-                                    .padding(horizontal = 16.dp),
-                                bodyItem.content.default!!
-                            )
-                        }
-                    }
+                    is Entry.Image -> PrepaneImage(bodyItem)
 
                     is Entry.Text -> AnnotatedText(
+                        modifier = Modifier.padding(horizontal = 24.dp),
                         text = TextResource.Text(fromHtml(bodyItem.content)),
                         onClickableTextClick = onClickableTextClick,
-                        defaultStyle = FinancialConnectionsTheme.typography.body,
-                        annotationStyles = mapOf(
-                            StringAnnotation.BOLD to FinancialConnectionsTheme.typography.bodyEmphasized.toSpanStyle(),
-                            StringAnnotation.CLICKABLE to FinancialConnectionsTheme.typography.bodyEmphasized
-                                .toSpanStyle()
-                                .copy(color = FinancialConnectionsTheme.colors.textBrand)
-                        )
+                        defaultStyle = typography.bodyMedium
                     )
                 }
-                if (index != content.body.entries.lastIndex) {
-                    Spacer(modifier = Modifier.size(16.dp))
-                }
-            }
-            Box(modifier = Modifier.weight(1f))
-            content.partnerNotice?.let {
-                Spacer(modifier = Modifier.size(16.dp))
-                PartnerCallout(
-                    partnerNotice = content.partnerNotice,
-                    onClickableTextClick = onClickableTextClick
-                )
             }
+        },
+        footer = {
+            PrepaneFooter(
+                onContinueClick = onContinueClick,
+                onCancelClick = onCancelClick,
+                status = authenticationStatus,
+                oAuthPrepane = content
+            )
         }
+    )
+}
+
+@Composable
+private fun PrepaneImage(bodyItem: Entry.Image) {
+    Row(
+        horizontalArrangement = Arrangement.Center,
+        modifier = Modifier
+            .fillMaxWidth()
+            .height(PHONE_BACKGROUND_HEIGHT_DP.dp)
+    ) {
+        // left gradient
+        Box(
+            modifier = Modifier
+                .background(
+                    brush = Brush.horizontalGradient(
+                        colors = listOf(
+                            colors.backgroundOffset,
+                            colors.border,
+                        ),
+                    )
+                )
+                .weight(1f)
+                .fillMaxHeight()
+        )
+
+        // left separator
+        Box(
+            modifier = Modifier
+                .background(color = colors.backgroundOffset)
+                .width(8.dp)
+                .fillMaxHeight()
+        )
+        // image / gif
+        GifWebView(
+            modifier = Modifier
+                .width(PHONE_BACKGROUND_WIDTH_DP.dp)
+                .fillMaxHeight(),
+            bodyItem.content.default!!
+        )
+        // right separator
+        Box(
+            modifier = Modifier
+                .background(color = colors.backgroundOffset)
+                .width(8.dp)
+                .fillMaxHeight()
+        )
+        // right gradient
+        Box(
+            modifier = Modifier
+                .background(
+                    brush = Brush.horizontalGradient(
+                        colors = listOf(
+                            colors.border,
+                            colors.backgroundOffset,
+                        ),
+                    )
+                )
+                .weight(1f)
+                .fillMaxHeight()
+        )
+    }
+}
+
+@Composable
+@OptIn(ExperimentalComposeUiApi::class)
+private fun PrepaneFooter(
+    onContinueClick: () -> Unit,
+    onCancelClick: () -> Unit,
+    status: Async,
+    oAuthPrepane: OauthPrepane
+) {
+    Column(
+        verticalArrangement = Arrangement.spacedBy(16.dp),
+    ) {
         FinancialConnectionsButton(
             onClick = onContinueClick,
+            type = Type.Primary,
+            loading = status is Loading && status()?.action == Action.AUTHENTICATING,
+            enabled = status !is Loading,
             modifier = Modifier
                 .semantics { testTagsAsResourceId = true }
                 .testTag("prepane_cta")
@@ -379,10 +416,10 @@ private fun InstitutionalPrePaneContent(
                 verticalAlignment = Alignment.CenterVertically
             ) {
                 Text(
-                    text = content.cta.text,
+                    text = oAuthPrepane.cta.text,
                     textAlign = TextAlign.Center
                 )
-                content.cta.icon?.default?.let {
+                oAuthPrepane.cta.icon?.default?.let {
                     Spacer(modifier = Modifier.size(12.dp))
                     StripeImage(
                         url = it,
@@ -394,6 +431,51 @@ private fun InstitutionalPrePaneContent(
                 }
             }
         }
+        FinancialConnectionsButton(
+            onClick = onCancelClick,
+            type = Type.Secondary,
+            enabled = status !is Loading,
+            modifier = Modifier
+                .semantics { testTagsAsResourceId = true }
+                .testTag("cancel_cta")
+                .fillMaxWidth()
+        ) {
+            Text(
+                text = stringResource(R.string.stripe_prepane_cancel_cta),
+                textAlign = TextAlign.Center
+            )
+        }
+    }
+}
+
+@Composable
+private fun PrepaneHeader(
+    content: OauthPrepane,
+    modifier: Modifier = Modifier
+) {
+    val title = remember(content.title) { TextResource.Text(fromHtml(content.title)) }
+    val subtitle = remember(content.subtitle) { TextResource.Text(fromHtml(content.subtitle)) }
+    Column(
+        modifier = modifier,
+        verticalArrangement = Arrangement.spacedBy(16.dp)
+    ) {
+        content.institutionIcon?.default?.let {
+            InstitutionIcon(institutionIcon = it)
+        }
+        AnnotatedText(
+            text = title,
+            onClickableTextClick = { },
+            defaultStyle = typography.headingLarge.copy(
+                color = colors.textDefault
+            ),
+        )
+        AnnotatedText(
+            text = subtitle,
+            onClickableTextClick = { },
+            defaultStyle = typography.bodyMedium.copy(
+                color = colors.textDefault
+            ),
+        )
     }
 }
 
@@ -402,30 +484,45 @@ private fun GifWebView(
     modifier: Modifier,
     gifUrl: String
 ) {
-    val body = ""
+    val isPreview = LocalInspectionMode.current
+    val htmlContent = remember(gifUrl) {
+        buildString {
+            append("")
+            append("")
+            append("")
+            append("")
+        }
+    }
+    val backgroundColor = colors.backgroundOffset.toArgb()
     AndroidView(
-        modifier = modifier,
+        modifier = modifier.background(Color.Transparent),
         factory = {
             WebView(it).apply {
                 /**
                  * WebView crashes when leaving the composition. Adding alpha acts as a workaround.
                  * https://stackoverflow.com/questions/74829526/
                  */
+                setBackgroundColor(backgroundColor)
                 alpha = WEBVIEW_ALPHA
                 layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
-                isVerticalScrollBarEnabled = false
-                isVerticalFadingEdgeEnabled = false
-                loadData(body, null, "UTF-8")
+                if (!isPreview) {
+                    isVerticalScrollBarEnabled = false
+                    isHorizontalScrollBarEnabled = false
+                    settings.loadWithOverviewMode = true
+                    settings.useWideViewPort = true
+                    isVerticalFadingEdgeEnabled = false
+                }
+                loadDataWithBaseURL(null, htmlContent, "text/html", "UTF-8", null)
             }
         },
         update = {
-            it.loadData(body, null, "UTF-8")
+            it.loadDataWithBaseURL(null, htmlContent, "text/html", "UTF-8", null)
         }
     )
 }
 
 @Preview(
-    group = "Shared Partner Auth"
+    group = "SharedPartnerAuth"
 )
 @Composable
 internal fun PartnerAuthPreview(
@@ -435,19 +532,37 @@ internal fun PartnerAuthPreview(
     FinancialConnectionsPreview {
         SharedPartnerAuthContent(
             state = state,
-            bottomSheetState = rememberModalBottomSheetState(
-                ModalBottomSheetValue.Hidden,
-            ),
-            onContinueClick = {},
-            onSelectAnotherBank = {},
-            onEnterDetailsManually = {},
+            inModal = false,
             onClickableTextClick = {},
+            onContinueClick = {},
             onCloseClick = {},
-            onCloseFromErrorClick = {}
+            onCancelClick = {}
         )
     }
 }
 
-private const val PHONE_BACKGROUND_WIDTH_DP = 272
-private const val PHONE_BACKGROUND_HEIGHT_DP = 264
+@Preview(
+    group = "SharedPartnerAuth - Drawer"
+)
+@Composable
+internal fun PartnerAuthDrawerPreview(
+    @PreviewParameter(PartnerAuthPreviewParameterProvider::class)
+    state: SharedPartnerAuthState
+) {
+    FinancialConnectionsPreview {
+        Box(modifier = Modifier.background(Color.White)) {
+            SharedPartnerAuthContent(
+                state = state,
+                inModal = true,
+                onClickableTextClick = {},
+                onContinueClick = {},
+                onCloseClick = {},
+                onCancelClick = {}
+            )
+        }
+    }
+}
+
+private const val PHONE_BACKGROUND_WIDTH_DP = 240
+private const val PHONE_BACKGROUND_HEIGHT_DP = 200
 private const val WEBVIEW_ALPHA = 0.99f
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/VerificationSection.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/VerificationSection.kt
index f3a998bfb2d..3d52fef6c0a 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/VerificationSection.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/common/VerificationSection.kt
@@ -1,27 +1,34 @@
 package com.stripe.android.financialconnections.features.common
 
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES
+import android.view.HapticFeedbackConstants
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.size
-import androidx.compose.material.Icon
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.platform.LocalUriHandler
-import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextDecoration
 import androidx.compose.ui.unit.dp
 import com.stripe.android.financialconnections.R
 import com.stripe.android.financialconnections.domain.ConfirmVerification.OTPError
 import com.stripe.android.financialconnections.domain.ConfirmVerification.OTPError.Type
 import com.stripe.android.financialconnections.features.consent.FinancialConnectionsUrlResolver
+import com.stripe.android.financialconnections.ui.LocalTestMode
 import com.stripe.android.financialconnections.ui.TextResource
 import com.stripe.android.financialconnections.ui.components.AnnotatedText
 import com.stripe.android.financialconnections.ui.components.StringAnnotation
-import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
+import com.stripe.android.financialconnections.ui.components.TestModeBanner
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
 import com.stripe.android.financialconnections.ui.theme.StripeThemeForConnections
 import com.stripe.android.uicore.elements.OTPElement
 import com.stripe.android.uicore.elements.OTPElementUI
@@ -33,16 +40,41 @@ internal fun VerificationSection(
     enabled: Boolean,
     confirmVerificationError: Throwable?,
 ) {
+    val view = LocalView.current
     Column {
         StripeThemeForConnections {
+            if (LocalTestMode.current) {
+                TestModeBanner(
+                    enabled = enabled,
+                    buttonLabel = stringResource(R.string.stripe_verification_useTestCode),
+                    onButtonClick = otpElement::populateTestCode,
+                )
+
+                Spacer(modifier = Modifier.height(24.dp))
+            }
+
             OTPElementUI(
+                otpInputPlaceholder = "",
+                boxSpacing = 8.dp,
+                middleSpacing = 8.dp,
+                boxTextStyle = typography.headingXLargeSubdued.copy(
+                    color = colors.textDefault,
+                    textAlign = TextAlign.Center
+                ),
                 focusRequester = focusRequester,
                 enabled = enabled,
                 element = otpElement
             )
         }
+        LaunchedEffect(confirmVerificationError) {
+            if (confirmVerificationError is OTPError) {
+                if (SDK_INT >= VERSION_CODES.R) {
+                    view.performHapticFeedback(HapticFeedbackConstants.REJECT)
+                }
+            }
+        }
         if (confirmVerificationError is OTPError) {
-            Spacer(modifier = Modifier.size(4.dp))
+            Spacer(modifier = Modifier.size(16.dp))
             VerificationErrorText(
                 error = confirmVerificationError,
             )
@@ -58,32 +90,24 @@ private fun VerificationErrorText(
     error: OTPError,
 ) {
     val uriHandler = LocalUriHandler.current
-    Row {
-        Icon(
-            modifier = Modifier
-                .size(12.dp)
-                .offset(y = 2.dp),
-            painter = painterResource(R.drawable.stripe_ic_warning),
-            contentDescription = "Warning icon",
-            tint = FinancialConnectionsTheme.colors.textCritical
-        )
-        AnnotatedText(
-            modifier = Modifier.padding(horizontal = 4.dp),
-            text = error.toMessage(),
-            defaultStyle = FinancialConnectionsTheme.typography.caption.copy(
-                color = FinancialConnectionsTheme.colors.textCritical
-            ),
-            onClickableTextClick = {
-                uriHandler.openUri(FinancialConnectionsUrlResolver.linkVerificationSupportUrl)
-            },
-            annotationStyles = mapOf(
-                StringAnnotation.CLICKABLE to FinancialConnectionsTheme.typography.caption.copy(
-                    color = FinancialConnectionsTheme.colors.textCritical,
-                    textDecoration = TextDecoration.Underline
-                ).toSpanStyle()
-            ),
-        )
-    }
+    AnnotatedText(
+        modifier = Modifier.fillMaxWidth(),
+        text = error.toMessage(),
+        defaultStyle = typography.labelMedium.copy(
+            color = colors.textCritical,
+            textAlign = TextAlign.Center
+        ),
+        onClickableTextClick = {
+            uriHandler.openUri(FinancialConnectionsUrlResolver.linkVerificationSupportUrl)
+        },
+        annotationStyles = mapOf(
+            StringAnnotation.CLICKABLE to typography.labelMedium.copy(
+                color = colors.textCritical,
+                textDecoration = TextDecoration.Underline,
+                textAlign = TextAlign.Center
+            ).toSpanStyle()
+        ),
+    )
 }
 
 private fun OTPError.toMessage(): TextResource = TextResource.StringId(
@@ -93,3 +117,9 @@ private fun OTPError.toMessage(): TextResource = TextResource.StringId(
         Type.CODE_INVALID -> R.string.stripe_verification_codeInvalid
     }
 )
+
+private fun OTPElement.populateTestCode() {
+    for (character in "000000") {
+        controller.onAutofillDigit(character.toString())
+    }
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/consent/ConsentPreviewParameterProvider.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/consent/ConsentPreviewParameterProvider.kt
index 9461a7e714a..1e4de0c3b73 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/consent/ConsentPreviewParameterProvider.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/consent/ConsentPreviewParameterProvider.kt
@@ -5,6 +5,7 @@ import androidx.compose.material.ModalBottomSheetValue
 import androidx.compose.ui.tooling.preview.PreviewParameterProvider
 import com.airbnb.mvrx.Success
 import com.stripe.android.financialconnections.model.Bullet
+import com.stripe.android.financialconnections.model.ConnectedAccessNotice
 import com.stripe.android.financialconnections.model.ConsentPane
 import com.stripe.android.financialconnections.model.ConsentPaneBody
 import com.stripe.android.financialconnections.model.DataAccessNotice
@@ -12,45 +13,23 @@ import com.stripe.android.financialconnections.model.DataAccessNoticeBody
 import com.stripe.android.financialconnections.model.Image
 import com.stripe.android.financialconnections.model.LegalDetailsBody
 import com.stripe.android.financialconnections.model.LegalDetailsNotice
+import com.stripe.android.financialconnections.model.ServerLink
 
 @OptIn(ExperimentalMaterialApi::class)
 internal class ConsentPreviewParameterProvider :
     PreviewParameterProvider> {
     override val values = sequenceOf(
-        ModalBottomSheetValue.Hidden to canonical(),
-        ModalBottomSheetValue.Hidden to withNoLogos(),
         ModalBottomSheetValue.Hidden to withPlatformLogos(),
         ModalBottomSheetValue.Hidden to withConnectedAccountLogos(),
         ModalBottomSheetValue.Hidden to manualEntryPlusMicrodeposits(),
         ModalBottomSheetValue.Expanded to withDataBottomSheet(),
-        ModalBottomSheetValue.Expanded to withLegalDetailsBottomSheet()
+        ModalBottomSheetValue.Expanded to withLegalDetailsBottomSheet(),
+        ModalBottomSheetValue.Expanded to withDataBottomSheetAndConnectedAccount()
     )
 
     override val count: Int
         get() = super.count
 
-    private fun canonical() =
-        ConsentState(
-            consent = Success(
-                ConsentState.Payload(
-                    consent = sampleConsent().copy(belowCta = null),
-                    merchantLogos = emptyList(),
-                    shouldShowMerchantLogos = false
-                )
-            )
-        )
-
-    private fun withNoLogos() =
-        ConsentState(
-            consent = Success(
-                ConsentState.Payload(
-                    consent = sampleConsent().copy(belowCta = null),
-                    merchantLogos = emptyList(),
-                    shouldShowMerchantLogos = true
-                )
-            )
-        )
-
     private fun withPlatformLogos() =
         ConsentState(
             consent = Success(
@@ -84,18 +63,42 @@ internal class ConsentPreviewParameterProvider :
         consent = Success(
             ConsentState.Payload(
                 consent = sampleConsent(),
-                merchantLogos = emptyList(),
+                merchantLogos = listOf(
+                    "www.logo1.com",
+                    "www.logo2.com"
+                ),
                 shouldShowMerchantLogos = false
             )
         )
     )
 
     private fun withDataBottomSheet() = ConsentState(
+        currentBottomSheet = ConsentState.BottomSheetContent.DATA,
+        consent = Success(
+            ConsentState.Payload(
+                consent = sampleConsent().copy(
+                    dataAccessNotice = sampleConsent().dataAccessNotice.copy(
+                        connectedAccountNotice = null
+                    )
+                ),
+                merchantLogos = listOf(
+                    "www.logo1.com",
+                    "www.logo2.com"
+                ),
+                shouldShowMerchantLogos = false
+            )
+        )
+    )
+
+    private fun withDataBottomSheetAndConnectedAccount() = ConsentState(
         currentBottomSheet = ConsentState.BottomSheetContent.DATA,
         consent = Success(
             ConsentState.Payload(
                 consent = sampleConsent(),
-                merchantLogos = emptyList(),
+                merchantLogos = listOf(
+                    "www.logo1.com",
+                    "www.logo2.com"
+                ),
                 shouldShowMerchantLogos = false
             )
         )
@@ -106,15 +109,17 @@ internal class ConsentPreviewParameterProvider :
         consent = Success(
             ConsentState.Payload(
                 consent = sampleConsent().copy(belowCta = null),
-                merchantLogos = emptyList(),
+                merchantLogos = listOf(
+                    "www.logo1.com",
+                    "www.logo2.com"
+                ),
                 shouldShowMerchantLogos = false
             )
         )
     )
 
-    @Suppress("LongMethod")
     private fun sampleConsent(): ConsentPane = ConsentPane(
-        title = "Goldilocks works with Stripe to link your accounts",
+        title = "Goldilocks uses Stripe to link your accounts",
         body = ConsentPaneBody(
             bullets = listOf(
                 Bullet(
@@ -124,12 +129,10 @@ internal class ConsentPreviewParameterProvider :
                 ),
                 Bullet(
                     icon = Image("https://www.cdn.stripe.com/12321312321.png"),
-                    content = "Stripe will allow Goldilocks to access only the data requested",
                     title = "Stripe will allow Goldilocks to access only the data requested"
                 ),
                 Bullet(
                     icon = Image("https://www.cdn.stripe.com/12321312321.png"),
-                    content = "Stripe will allow Goldilocks to access only the data requested",
                     title = "Stripe will allow Goldilocks to access only the data requested"
                 ),
             )
@@ -139,40 +142,56 @@ internal class ConsentPreviewParameterProvider :
             " We never share your login details with them.",
         cta = "Agree",
         dataAccessNotice = DataAccessNotice(
-            title = "Goldilocks works with Stripe to link your accounts",
+            icon = Image("https://www.cdn.stripe.com/12321312321.png"),
+            title = "Goldilocks uses Stripe to link your accounts",
             subtitle = "Goldilocks will use your account and routing number, balances and transactions:",
             body = DataAccessNoticeBody(
-                bullets = listOf(
-                    Bullet(
-                        icon = Image("https://www.cdn.stripe.com/12321312321.png"),
-                        title = "Account details",
-                        content = "Account number, routing number, account type, account nickname."
-                    ),
-                    Bullet(
-                        icon = Image("https://www.cdn.stripe.com/12321312321.png"),
-                        title = "Account details",
-                        content = "Account number, routing number, account type, account nickname."
-                    ),
+                bullets = bullets()
+            ),
+            disclaimer = "Learn more about data access",
+            connectedAccountNotice = ConnectedAccessNotice(
+                subtitle = "Connected account placeholder",
+                body = DataAccessNoticeBody(
+                    bullets = bullets()
                 )
             ),
-            learnMore = "Learn more about data access",
-            connectedAccountNotice = "Connected account placeholder",
             cta = "OK"
         ),
         legalDetailsNotice = LegalDetailsNotice(
-            title = "Stripe uses your account data as described in the Terms, including:",
+            icon = Image("https://www.cdn.stripe.com/12321312321.png"),
+            title = "Terms and privacy policy",
+            subtitle = "Stripe only uses your data and credentials as described in the Terms, " +
+                "such as to improve its services, manage loss, and mitigate fraud.",
             body = LegalDetailsBody(
-                bullets = listOf(
-                    Bullet(
-                        content = "To improve our services"
+                links = listOf(
+                    ServerLink(
+                        title = "Terms",
                     ),
-                    Bullet(
-                        content = "To manage fraud and loss risk of transactions"
+                    ServerLink(
+                        title = "Privacy Policy",
                     ),
                 )
             ),
-            learnMore = "Learn more",
+            disclaimer = "Learn more",
             cta = "OK"
         ),
     )
+
+    private fun bullets() = listOf(
+        Bullet(
+            icon = Image("https://www.cdn.stripe.com/12321312321.png"),
+            title = "Account details",
+            content = null
+        ),
+        Bullet(
+            icon = Image("https://www.cdn.stripe.com/12321312321.png"),
+            title = "Balances",
+            content = null
+        ),
+        Bullet(
+            icon = Image("https://www.cdn.stripe.com/12321312321.png"),
+            title = "Transactions",
+            content = null
+        ),
+    )
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/consent/ConsentScreen.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/consent/ConsentScreen.kt
index 3edcaa19805..0c0fd4acf63 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/consent/ConsentScreen.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/consent/ConsentScreen.kt
@@ -1,31 +1,16 @@
-@file:OptIn(
-    ExperimentalMaterialApi::class,
-    ExperimentalComposeUiApi::class
-)
-@file:Suppress("LongMethod", "TooManyFunctions")
-
 package com.stripe.android.financialconnections.features.consent
 
 import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-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.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.ModalBottomSheetLayout
 import androidx.compose.material.ModalBottomSheetState
 import androidx.compose.material.ModalBottomSheetValue
 import androidx.compose.material.Text
@@ -37,11 +22,8 @@ import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.platform.LocalUriHandler
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.testTagsAsResourceId
 import androidx.compose.ui.text.style.TextAlign
@@ -55,32 +37,30 @@ import com.airbnb.mvrx.Success
 import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.compose.collectAsState
 import com.airbnb.mvrx.compose.mavericksViewModel
-import com.stripe.android.financialconnections.R
-import com.stripe.android.financialconnections.features.common.BulletItem
 import com.stripe.android.financialconnections.features.common.DataAccessBottomSheetContent
 import com.stripe.android.financialconnections.features.common.LegalDetailsBottomSheetContent
-import com.stripe.android.financialconnections.features.common.LoadingShimmerEffect
+import com.stripe.android.financialconnections.features.common.ListItem
 import com.stripe.android.financialconnections.features.common.UnclassifiedErrorContent
 import com.stripe.android.financialconnections.features.consent.ConsentState.ViewEffect.OpenBottomSheet
 import com.stripe.android.financialconnections.features.consent.ConsentState.ViewEffect.OpenUrl
+import com.stripe.android.financialconnections.features.consent.ui.ConsentLogoHeader
 import com.stripe.android.financialconnections.model.ConsentPane
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
 import com.stripe.android.financialconnections.presentation.parentViewModel
 import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
-import com.stripe.android.financialconnections.ui.LocalImageLoader
 import com.stripe.android.financialconnections.ui.LocalReducedBranding
 import com.stripe.android.financialconnections.ui.TextResource
 import com.stripe.android.financialconnections.ui.components.AnnotatedText
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsButton
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsModalBottomSheetLayout
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsScaffold
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsTopAppBar
-import com.stripe.android.financialconnections.ui.components.StringAnnotation
 import com.stripe.android.financialconnections.ui.components.elevation
 import com.stripe.android.financialconnections.ui.sdui.BulletUI
 import com.stripe.android.financialconnections.ui.sdui.fromHtml
 import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
 import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
-import com.stripe.android.uicore.image.StripeImage
+import com.stripe.android.financialconnections.ui.theme.LazyLayout
 import kotlinx.coroutines.launch
 
 @ExperimentalMaterialApi
@@ -169,7 +149,7 @@ private fun ConsentMainContent(
     onContinueClick: () -> Unit,
     onCloseClick: () -> Unit
 ) {
-    val scrollState = rememberScrollState()
+    val scrollState = rememberLazyListState()
     val title = remember(payload.consent.title) {
         TextResource.Text(fromHtml(payload.consent.title))
     }
@@ -188,133 +168,58 @@ private fun ConsentMainContent(
             )
         }
     ) {
-        Column(
-            Modifier.fillMaxSize()
-        ) {
-            Column(
-                modifier = Modifier
-                    .weight(1f)
-                    .verticalScroll(scrollState)
-                    .padding(
-                        top = 0.dp,
-                        start = 24.dp,
-                        end = 24.dp,
-                        bottom = 24.dp
-                    )
-            ) {
-                if (payload.shouldShowMerchantLogos) {
-                    // Merchant logos: Control
-                    ConsentLogoHeader(
-                        logos = payload.merchantLogos,
-                        modifier = Modifier.align(Alignment.CenterHorizontally)
-                    )
-                    Spacer(modifier = Modifier.size(20.dp))
-                    AnnotatedText(
-                        text = title,
-                        onClickableTextClick = { onClickableTextClick(it) },
-                        defaultStyle = typography.subtitle.copy(
-                            textAlign = TextAlign.Center
-                        ),
-                        annotationStyles = mapOf(
-                            StringAnnotation.CLICKABLE to typography.subtitle
-                                .toSpanStyle()
-                                .copy(color = colors.textBrand),
-                        )
-                    )
-                } else {
-                    // Merchant logos: Treatment
-                    Spacer(modifier = Modifier.size(16.dp))
-                    AnnotatedText(
-                        text = title,
-                        onClickableTextClick = { onClickableTextClick(it) },
-                        defaultStyle = typography.subtitle,
-                        annotationStyles = mapOf(
-                            StringAnnotation.CLICKABLE to typography.subtitle
-                                .toSpanStyle()
-                                .copy(color = colors.textBrand),
-                        )
-                    )
-                    Spacer(modifier = Modifier.size(24.dp))
-                }
-                bullets.forEach { bullet ->
-                    Spacer(modifier = Modifier.size(16.dp))
-                    BulletItem(
-                        bullet,
-                        onClickableTextClick = onClickableTextClick
-                    )
-                }
-                Spacer(modifier = Modifier.weight(1f))
+        LazyLayout(
+            lazyListState = scrollState,
+            body = {
+                consentBody(
+                    payload = payload,
+                    title = title,
+                    onClickableTextClick = onClickableTextClick,
+                    bullets = bullets
+                )
+            },
+            footer = {
+                ConsentFooter(
+                    consent = payload.consent,
+                    acceptConsent = acceptConsent,
+                    onClickableTextClick = onClickableTextClick,
+                    onContinueClick = onContinueClick
+                )
             }
-            ConsentFooter(
-                consent = payload.consent,
-                acceptConsent = acceptConsent,
-                onClickableTextClick = onClickableTextClick,
-                onContinueClick = onContinueClick
-            )
-        }
+        )
     }
 }
 
-@Composable
-@Suppress("MagicNumber")
-private fun ConsentLogoHeader(
-    modifier: Modifier = Modifier,
-    logos: List
+private fun LazyListScope.consentBody(
+    payload: ConsentState.Payload,
+    title: TextResource.Text,
+    onClickableTextClick: (String) -> Unit,
+    bullets: List
 ) {
-    Row(
-        modifier = modifier,
-        verticalAlignment = Alignment.CenterVertically,
-        horizontalArrangement = Arrangement.spacedBy(16.dp)
-    ) {
-        /**
-         * - 2 logos: (platform or institution)
-         * - 3 logos: (connected account)
-         * - Other # of logos: Fallback to Stripe logo as client can't render.
-         */
-        if (logos.size != 2 && logos.size != 3) {
-            Image(
-                painterResource(id = R.drawable.stripe_logo),
-                contentDescription = null,
-                modifier = Modifier
-                    .width(60.dp)
-                    .height(25.dp)
-                    .clip(CircleShape),
+    item {
+        Spacer(modifier = Modifier.size(8.dp))
+        ConsentLogoHeader(
+            modifier = Modifier.fillMaxWidth(),
+            logos = payload.merchantLogos,
+        )
+        Spacer(modifier = Modifier.size(32.dp))
+    }
+    item {
+        AnnotatedText(
+            text = title,
+            onClickableTextClick = { onClickableTextClick(it) },
+            defaultStyle = typography.headingXLarge.copy(
+                textAlign = TextAlign.Center
             )
-        } else {
-            logos.forEachIndexed { index, logoUrl ->
-                StripeImage(
-                    url = logoUrl,
-                    debugPainter = painterResource(
-                        id = R.drawable.stripe_ic_brandicon_institution_circle
-                    ),
-                    loadingContent = {
-                        LoadingShimmerEffect { shimmer ->
-                            Spacer(
-                                modifier = Modifier
-                                    .align(Alignment.Center)
-                                    .size(40.dp)
-                                    .clip(RoundedCornerShape(10.dp))
-                                    .fillMaxWidth(fraction = 0.5f)
-                                    .background(shimmer)
-                            )
-                        }
-                    },
-                    imageLoader = LocalImageLoader.current,
-                    contentScale = ContentScale.Crop,
-                    contentDescription = null,
-                    modifier = Modifier
-                        .size(40.dp)
-                        .clip(CircleShape)
-                )
-                // Adds ellipsis to link logos.
-                if (index != logos.lastIndex) {
-                    Image(
-                        painterResource(id = R.drawable.stripe_consent_logo_ellipsis),
-                        contentDescription = null
-                    )
-                }
-            }
-        }
+        )
+        Spacer(modifier = Modifier.size(32.dp))
+    }
+    items(bullets) { bullet ->
+        ListItem(
+            bullet = bullet,
+            onClickableTextClick = onClickableTextClick
+        )
+        Spacer(modifier = Modifier.size(24.dp))
     }
 }
 
@@ -329,11 +234,8 @@ private fun LoadedContent(
     onConfirmModalClick: () -> Unit,
     bottomSheetMode: ConsentState.BottomSheetContent?,
 ) {
-    ModalBottomSheetLayout(
+    FinancialConnectionsModalBottomSheetLayout(
         sheetState = bottomSheetState,
-        sheetBackgroundColor = colors.backgroundSurface,
-        sheetShape = RoundedCornerShape(8.dp),
-        scrimColor = colors.textSecondary.copy(alpha = 0.5f),
         sheetContent = {
             when (bottomSheetMode) {
                 ConsentState.BottomSheetContent.LEGAL -> LegalDetailsBottomSheetContent(
@@ -363,6 +265,7 @@ private fun LoadedContent(
     )
 }
 
+@OptIn(ExperimentalComposeUiApi::class)
 @Composable
 private fun ConsentFooter(
     acceptConsent: Async,
@@ -376,28 +279,14 @@ private fun ConsentFooter(
     val belowCta = remember(consent.belowCta) {
         consent.belowCta?.let { TextResource.Text(fromHtml(consent.belowCta)) }
     }
-    Column(
-        modifier = Modifier.padding(
-            start = 24.dp,
-            end = 24.dp,
-            top = 16.dp,
-            bottom = 24.dp
-        )
-    ) {
+    Column {
         AnnotatedText(
+            modifier = Modifier.fillMaxWidth(),
             text = aboveCta,
             onClickableTextClick = onClickableTextClick,
-            defaultStyle = typography.detail.copy(
+            defaultStyle = typography.labelSmall.copy(
                 textAlign = TextAlign.Center,
-                color = colors.textSecondary
-            ),
-            annotationStyles = mapOf(
-                StringAnnotation.CLICKABLE to typography.detailEmphasized
-                    .toSpanStyle()
-                    .copy(color = colors.textBrand),
-                StringAnnotation.BOLD to typography.detailEmphasized
-                    .toSpanStyle()
-                    .copy(color = colors.textSecondary)
+                color = colors.textDefault
             )
         )
         Spacer(modifier = Modifier.size(16.dp))
@@ -412,25 +301,16 @@ private fun ConsentFooter(
             Text(text = consent.cta)
         }
         if (belowCta != null) {
-            Spacer(modifier = Modifier.size(24.dp))
+            Spacer(modifier = Modifier.size(16.dp))
             AnnotatedText(
                 modifier = Modifier.fillMaxWidth(),
                 text = belowCta,
                 onClickableTextClick = onClickableTextClick,
-                defaultStyle = typography.detail.copy(
+                defaultStyle = typography.labelSmall.copy(
                     textAlign = TextAlign.Center,
-                    color = colors.textSecondary
-                ),
-                annotationStyles = mapOf(
-                    StringAnnotation.CLICKABLE to typography.detailEmphasized
-                        .toSpanStyle()
-                        .copy(color = colors.textBrand),
-                    StringAnnotation.BOLD to typography.detailEmphasized
-                        .toSpanStyle()
-                        .copy(color = colors.textSecondary)
+                    color = colors.textDefault
                 )
             )
-            Spacer(modifier = Modifier.size(16.dp))
         }
     }
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/consent/ConsentViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/consent/ConsentViewModel.kt
index 07ff5083544..1eac34eb74f 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/consent/ConsentViewModel.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/consent/ConsentViewModel.kt
@@ -1,12 +1,10 @@
 package com.stripe.android.financialconnections.features.consent
 
-import android.webkit.URLUtil
 import com.airbnb.mvrx.MavericksViewModel
 import com.airbnb.mvrx.MavericksViewModelFactory
 import com.airbnb.mvrx.ViewModelContext
 import com.stripe.android.core.Logger
 import com.stripe.android.financialconnections.FinancialConnections
-import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.Click
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.ConsentAgree
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.PaneLoaded
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
@@ -19,12 +17,12 @@ import com.stripe.android.financialconnections.features.consent.ConsentState.Vie
 import com.stripe.android.financialconnections.features.consent.ConsentState.ViewEffect.OpenUrl
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
-import com.stripe.android.financialconnections.navigation.Destination.ManualEntry
+import com.stripe.android.financialconnections.navigation.Destination
 import com.stripe.android.financialconnections.navigation.NavigationManager
 import com.stripe.android.financialconnections.navigation.destination
 import com.stripe.android.financialconnections.ui.FinancialConnectionsSheetNativeActivity
+import com.stripe.android.financialconnections.ui.HandleClickableUrl
 import com.stripe.android.financialconnections.utils.Experiment.CONNECTIONS_CONSENT_COMBINED_LOGO
-import com.stripe.android.financialconnections.utils.UriUtils
 import com.stripe.android.financialconnections.utils.experimentAssignment
 import com.stripe.android.financialconnections.utils.trackExposure
 import kotlinx.coroutines.launch
@@ -37,7 +35,7 @@ internal class ConsentViewModel @Inject constructor(
     private val getOrFetchSync: GetOrFetchSync,
     private val navigationManager: NavigationManager,
     private val eventTracker: FinancialConnectionsAnalyticsTracker,
-    private val uriUtils: UriUtils,
+    private val handleClickableUrl: HandleClickableUrl,
     private val logger: Logger
 ) : MavericksViewModel(initialState) {
 
@@ -82,45 +80,37 @@ internal class ConsentViewModel @Inject constructor(
         }.execute { copy(acceptConsent = it) }
     }
 
-    fun onClickableTextClick(uri: String) {
-        // if clicked uri contains an eventName query param, track click event.
-        viewModelScope.launch {
-            uriUtils.getQueryParameter(uri, "eventName")?.let { eventName ->
-                eventTracker.track(Click(eventName, pane = Pane.CONSENT))
-            }
-            val date = Date()
-            if (URLUtil.isNetworkUrl(uri)) {
-                setState { copy(viewEffect = OpenUrl(uri, date.time)) }
-            } else {
-                val managedUri = ConsentClickableText.entries
-                    .firstOrNull { uriUtils.compareSchemeAuthorityAndPath(it.value, uri) }
-                when (managedUri) {
-                    ConsentClickableText.DATA -> {
-                        setState {
-                            copy(
-                                currentBottomSheet = BottomSheetContent.DATA,
-                                viewEffect = ViewEffect.OpenBottomSheet(date.time)
-                            )
-                        }
+    fun onClickableTextClick(uri: String) = viewModelScope.launch {
+        val date = Date()
+        handleClickableUrl(
+            currentPane = Pane.CONSENT,
+            uri = uri,
+            onNetworkUrlClicked = { setState { copy(viewEffect = OpenUrl(uri, date.time)) } },
+            knownDeeplinkActions = mapOf(
+                // Clicked on the "Data Access" link -> Open the Data Access bottom sheet
+                ConsentClickableText.DATA.value to {
+                    setState {
+                        copy(
+                            currentBottomSheet = BottomSheetContent.DATA,
+                            viewEffect = ViewEffect.OpenBottomSheet(date.time)
+                        )
                     }
-
-                    ConsentClickableText.MANUAL_ENTRY -> {
-                        navigationManager.tryNavigateTo(ManualEntry(referrer = Pane.CONSENT))
-                    }
-
-                    ConsentClickableText.LEGAL_DETAILS -> {
-                        setState {
-                            copy(
-                                currentBottomSheet = BottomSheetContent.LEGAL,
-                                viewEffect = ViewEffect.OpenBottomSheet(date.time)
-                            )
-                        }
+                },
+                // Clicked on the "Legal details" link -> Open the Legal Details bottom sheet
+                ConsentClickableText.LEGAL_DETAILS.value to {
+                    setState {
+                        copy(
+                            viewEffect = ViewEffect.OpenBottomSheet(date.time),
+                            currentBottomSheet = BottomSheetContent.LEGAL
+                        )
                     }
-
-                    null -> logger.error("Unrecognized clickable text: $uri")
-                }
-            }
-        }
+                },
+                // Clicked on the "Manual entry" link -> Navigate to the Manual Entry screen
+                ConsentClickableText.MANUAL_ENTRY.value to {
+                    navigationManager.tryNavigateTo(Destination.ManualEntry(referrer = Pane.CONSENT))
+                },
+            )
+        )
     }
 
     fun onViewEffectLaunched() {
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/consent/ui/ConsentLogoHeader.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/consent/ui/ConsentLogoHeader.kt
new file mode 100644
index 00000000000..1d7cb8a3aa3
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/consent/ui/ConsentLogoHeader.kt
@@ -0,0 +1,265 @@
+package com.stripe.android.financialconnections.features.consent.ui
+
+import android.graphics.Bitmap
+import androidx.compose.animation.Crossfade
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.drawscope.clipPath
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.unit.dp
+import com.stripe.android.financialconnections.ui.LocalImageLoader
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+
+private val LogoSize = 72.dp
+private val DotsContainerHeight = 6.dp
+private val DotsContainerWidth = 32.dp
+
+@Composable
+internal fun ConsentLogoHeader(
+    modifier: Modifier = Modifier,
+    logos: List
+) {
+    val isPreview = LocalInspectionMode.current
+    val localDensity = LocalDensity.current
+    val bitmapLoadSize = remember { with(localDensity) { 36.dp.toPx().toInt() } }
+    val stripeImageLoader = LocalImageLoader.current
+    val placeholderBitmap: ImageBitmap = rememberPlaceholderBitmap(bitmapLoadSize, colors.backgroundOffset)
+    var bitmaps: List by remember {
+        mutableStateOf(
+            if (isPreview) {
+                debugPreviewBitmaps(logos, bitmapLoadSize)
+            } else {
+                List(logos.size) { placeholderBitmap }
+            }
+        )
+    }
+
+    LaunchedEffect(logos) {
+        bitmaps = logos.map {
+            async {
+                stripeImageLoader.load(it, bitmapLoadSize, bitmapLoadSize)
+                    .getOrNull()
+                    ?.asImageBitmap()
+                    ?: placeholderBitmap
+            }
+        }.awaitAll()
+    }
+
+    Box(
+        modifier = modifier
+            .height(LogoSize)
+            .fillMaxWidth(),
+        contentAlignment = Alignment.Center
+    ) {
+        BackgroundRow(images = bitmaps)
+        ForegroundRow(images = bitmaps)
+    }
+}
+
+@Composable
+private fun BackgroundRow(images: List) {
+    Row(verticalAlignment = Alignment.CenterVertically) {
+        for ((index, image) in images.withIndex()) {
+            Spacer(modifier = Modifier.width(LogoSize))
+
+            if (index != images.lastIndex) {
+                val nextImage = images[index + 1]
+                AnimatedDotsWithFixedGradient(
+                    startColor = getPrevalentColorCloseToDots(
+                        bitmap = image.asAndroidBitmap(),
+                        startSide = false,
+                    ),
+                    endColor = getPrevalentColorCloseToDots(
+                        bitmap = nextImage.asAndroidBitmap(),
+                        startSide = true,
+                    )
+                )
+            }
+        }
+    }
+}
+
+@Composable
+private fun ForegroundRow(images: List) {
+    Row(verticalAlignment = Alignment.CenterVertically) {
+        for ((index, image) in images.withIndex()) {
+            Logo(image)
+            if (index != images.lastIndex) {
+                Spacer(modifier = Modifier.width(DotsContainerWidth))
+            }
+        }
+    }
+}
+
+private fun debugPreviewBitmaps(
+    size: List,
+    bitmapLoadSize: Int,
+): List {
+    return listOf(Color.Red, Color.Blue, Color.Green).take(size.size).map {
+        val androidBitmap = Bitmap.createBitmap(bitmapLoadSize, bitmapLoadSize, Bitmap.Config.ARGB_8888)
+        val canvas = android.graphics.Canvas(androidBitmap)
+        canvas.drawColor(it.toArgb())
+        androidBitmap.asImageBitmap()
+    }
+}
+
+@Composable
+private fun rememberPlaceholderBitmap(bitmapLoadSize: Int, placeholderColor: Color): ImageBitmap = remember {
+    val androidBitmap = Bitmap.createBitmap(
+        bitmapLoadSize,
+        bitmapLoadSize,
+        Bitmap.Config.ARGB_8888
+    )
+    androidBitmap.eraseColor(placeholderColor.toArgb())
+    androidBitmap.asImageBitmap()
+}
+
+@Composable
+private fun AnimatedDotsWithFixedGradient(
+    modifier: Modifier = Modifier,
+    startColor: Color,
+    endColor: Color
+) {
+    val infiniteTransition = rememberInfiniteTransition(
+        label = "animated-dots-transition"
+    )
+    val animatedOffset by infiniteTransition.animateFloat(
+        label = "animated-dots",
+        initialValue = 0f,
+        targetValue = with(LocalDensity.current) { 10.dp.toPx() },
+        animationSpec = infiniteRepeatable(
+            animation = tween(durationMillis = 1000, easing = LinearEasing),
+            repeatMode = RepeatMode.Restart
+        ),
+    )
+
+    val gradientBrush = Brush.horizontalGradient(
+        colors = listOf(startColor, endColor)
+    )
+
+    Box(
+        modifier = modifier
+            .width(DotsContainerWidth)
+            .height(DotsContainerHeight)
+            .background(Color.White)
+    ) {
+        Canvas(modifier = Modifier.matchParentSize()) {
+            val dotRadius = 3.dp.toPx()
+            val dotSpacing = 10.dp.toPx()
+            val dotY = center.y
+            val numberOfDots = (size.width / dotSpacing).toInt() + 2 // Enough dots to fill the screen + 2 extra
+
+            // Create a path for the animated dots
+            val path = Path().apply {
+                for (i in -1 until numberOfDots) { // Start with -1 to have an off-screen dot
+                    val x = i * dotSpacing + animatedOffset - dotSpacing // Offset by one dot spacing
+                    addOval(Rect(Offset(x, dotY - dotRadius), Size(dotRadius * 2, dotRadius * 2)))
+                }
+            }
+
+            // Clip the canvas to the path of the dots
+            clipPath(path) {
+                // Draw the gradient within the clipped area (where the dots are)
+                drawRect(
+                    brush = gradientBrush,
+                    size = Size(size.width, dotRadius * 2),
+                    topLeft = Offset(0f, dotY - dotRadius)
+                )
+            }
+        }
+    }
+}
+
+@Composable
+private fun Logo(imageBitmap: ImageBitmap) {
+    val shape = RoundedCornerShape(18.dp)
+    Box(
+        modifier = Modifier
+            .size(LogoSize)
+            .shadow(8.dp, shape)
+            .clip(shape)
+            .background(color = colors.backgroundOffset, shape = shape)
+    ) {
+        Crossfade(
+            targetState = imageBitmap,
+            animationSpec = tween(durationMillis = 300)
+        ) { image ->
+            Image(
+                bitmap = image,
+                contentScale = ContentScale.Crop,
+                contentDescription = null,
+                modifier = Modifier.fillMaxSize()
+            )
+        }
+    }
+}
+
+/**
+ * Get the prevalent color of the bitmap in the given quarter to generate the gradient for the dots.
+ *
+ * It focuses on the horizontal center and either on the 1st or 4th vertical quarter of the
+ * bitmap, depending on the [startSide] parameter, narrowing the main color search to the area of the logo
+ * the dots will be merging with.
+ *
+ * @param startSide whether the quarter is the start or end of the bitmap
+ */
+private fun getPrevalentColorCloseToDots(bitmap: Bitmap, startSide: Boolean): Color {
+    val colorMap = HashMap()
+    val width = bitmap.width
+    val height = bitmap.height
+    val startX = if (startSide) 0 else (width * 3) / 4
+    val endX = if (startSide) width / 4 else width
+    val startY = height * 2 / 5 // 40% of the height
+    val endY = height * 3 / 5 // 60% of the height
+
+    for (x in startX until endX) {
+        for (y in startY until endY) {
+            val pixelColor = bitmap.getPixel(x, y)
+            colorMap[pixelColor] = (colorMap[pixelColor] ?: 0) + 1
+        }
+    }
+    return colorMap
+        .maxByOrNull { it.value }
+        ?.let { Color(it.key) }
+        ?: Color.Black // Default color if no prevalent color is found
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/error/ErrorPreviewParameterProvider.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/error/ErrorPreviewParameterProvider.kt
new file mode 100644
index 00000000000..281d1ce491f
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/error/ErrorPreviewParameterProvider.kt
@@ -0,0 +1,74 @@
+package com.stripe.android.financialconnections.features.error
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.Success
+import com.stripe.android.core.exception.APIException
+import com.stripe.android.financialconnections.exception.InstitutionPlannedDowntimeError
+import com.stripe.android.financialconnections.exception.InstitutionUnplannedDowntimeError
+import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
+
+internal class ErrorPreviewParameterProvider :
+    PreviewParameterProvider {
+    override val values = sequenceOf(
+        loading(),
+        unclassified(),
+        expectedDowntime(),
+        unexpectedDowntime()
+    )
+
+    private fun loading() = ErrorState(
+        payload = Loading(),
+    )
+
+    private fun unclassified() = ErrorState(
+        payload = Success(
+            ErrorState.Payload(
+                error = IllegalArgumentException("An unknown error occurred."),
+                allowManualEntry = true,
+                disableLinkMoreAccounts = true,
+            )
+        ),
+    )
+
+    private fun expectedDowntime() = ErrorState(
+        payload = Success(
+            ErrorState.Payload(
+                error = InstitutionPlannedDowntimeError(
+                    institution = institution(),
+                    showManualEntry = true,
+                    isToday = true,
+                    backUpAt = 10000L,
+                    stripeException = APIException()
+                ),
+                allowManualEntry = true,
+                disableLinkMoreAccounts = true,
+            )
+        ),
+    )
+
+    private fun unexpectedDowntime() = ErrorState(
+        payload = Success(
+            ErrorState.Payload(
+                error = InstitutionUnplannedDowntimeError(
+                    institution = institution(),
+                    showManualEntry = true,
+                    stripeException = APIException()
+                ),
+                allowManualEntry = true,
+                disableLinkMoreAccounts = true,
+            )
+        ),
+    )
+
+    private fun institution() = FinancialConnectionsInstitution(
+        id = "3",
+        name = "Random Institution",
+        url = "Random Institution url",
+        featured = false,
+        featuredOrder = null,
+        icon = null,
+        logo = null,
+        mobileHandoffCapable = false
+    )
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/error/ErrorScreen.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/error/ErrorScreen.kt
new file mode 100644
index 00000000000..a7311756308
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/error/ErrorScreen.kt
@@ -0,0 +1,160 @@
+package com.stripe.android.financialconnections.features.error
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import com.airbnb.mvrx.Async
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.Uninitialized
+import com.airbnb.mvrx.compose.collectAsState
+import com.airbnb.mvrx.compose.mavericksViewModel
+import com.stripe.android.financialconnections.exception.InstitutionPlannedDowntimeError
+import com.stripe.android.financialconnections.exception.InstitutionUnplannedDowntimeError
+import com.stripe.android.financialconnections.exception.PartnerAuthError
+import com.stripe.android.financialconnections.features.common.FullScreenGenericLoading
+import com.stripe.android.financialconnections.features.common.InstitutionPlannedDowntimeErrorContent
+import com.stripe.android.financialconnections.features.common.InstitutionUnknownErrorContent
+import com.stripe.android.financialconnections.features.common.InstitutionUnplannedDowntimeErrorContent
+import com.stripe.android.financialconnections.features.common.UnclassifiedErrorContent
+import com.stripe.android.financialconnections.presentation.parentViewModel
+import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsScaffold
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsTopAppBar
+
+@Composable
+internal fun ErrorScreen() {
+    val viewModel: ErrorViewModel = mavericksViewModel()
+    val parentViewModel = parentViewModel()
+    BackHandler(true) { }
+    val payload = viewModel.collectAsState { it.payload }
+    ErrorContent(
+        payload = payload.value,
+        onManualEntryClick = viewModel::onManualEntryClick,
+        onSelectBankClick = viewModel::onSelectAnotherBank,
+        onCloseFromErrorClick = parentViewModel::onCloseFromErrorClick
+    )
+}
+
+@Composable
+private fun ErrorContent(
+    payload: Async,
+    onSelectBankClick: () -> Unit,
+    onManualEntryClick: () -> Unit,
+    onCloseFromErrorClick: (Throwable) -> Unit
+) {
+    when (payload) {
+        Uninitialized,
+        is Loading -> FullScreenError(
+            showBack = false,
+            onCloseClick = { },
+            content = { FullScreenGenericLoading() }
+        )
+
+        // Render error successfully retrieved from a previous pane
+        is Success -> ErrorContent(
+            payload().error,
+            onSelectAnotherBank = onSelectBankClick,
+            onEnterDetailsManually = onManualEntryClick,
+            onCloseFromErrorClick = onCloseFromErrorClick
+        )
+
+        // Something wrong happened while trying to retrieve the error, render the unclassified error
+        is Fail -> ErrorContent(
+            payload.error,
+            onSelectAnotherBank = onSelectBankClick,
+            onEnterDetailsManually = onManualEntryClick,
+            onCloseFromErrorClick = onCloseFromErrorClick
+        )
+    }
+}
+
+@Composable
+private fun ErrorContent(
+    error: Throwable,
+    onSelectAnotherBank: () -> Unit,
+    onEnterDetailsManually: () -> Unit,
+    onCloseFromErrorClick: (Throwable) -> Unit
+) {
+    when (error) {
+        is InstitutionPlannedDowntimeError -> FullScreenError(
+            showBack = false,
+            onCloseClick = { onCloseFromErrorClick(error) },
+            content = {
+                InstitutionPlannedDowntimeErrorContent(
+                    exception = error,
+                    onSelectAnotherBank = onSelectAnotherBank,
+                    onEnterDetailsManually = onEnterDetailsManually
+                )
+            }
+        )
+
+        is InstitutionUnplannedDowntimeError -> FullScreenError(
+            showBack = false,
+            onCloseClick = { onCloseFromErrorClick(error) },
+            content = {
+                InstitutionUnplannedDowntimeErrorContent(
+                    exception = error,
+                    onSelectAnotherBank = onSelectAnotherBank,
+                    onEnterDetailsManually = onEnterDetailsManually
+                )
+            }
+        )
+
+        is PartnerAuthError -> FullScreenError(
+            showBack = false,
+            onCloseClick = { onCloseFromErrorClick(error) },
+            content = {
+                InstitutionUnknownErrorContent(
+                    onSelectAnotherBank = onSelectAnotherBank,
+                )
+            }
+        )
+
+        else -> FullScreenError(
+            showBack = false,
+            onCloseClick = { onCloseFromErrorClick(error) },
+            content = {
+                UnclassifiedErrorContent(
+                    error = error,
+                    onCloseFromErrorClick = onCloseFromErrorClick
+                )
+            }
+        )
+    }
+}
+
+@Composable
+private fun FullScreenError(
+    showBack: Boolean,
+    onCloseClick: () -> Unit,
+    content: @Composable () -> Unit
+) {
+    FinancialConnectionsScaffold(
+        topBar = {
+            FinancialConnectionsTopAppBar(
+                allowBackNavigation = showBack,
+                onCloseClick = onCloseClick
+            )
+        }
+    ) {
+        content()
+    }
+}
+
+@Preview
+@Composable
+internal fun ErrorScreenPreview(
+    @PreviewParameter(ErrorPreviewParameterProvider::class) state: ErrorState
+) {
+    FinancialConnectionsPreview {
+        ErrorContent(
+            payload = state.payload,
+            onSelectBankClick = {},
+            onManualEntryClick = {},
+            onCloseFromErrorClick = {}
+        )
+    }
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/error/ErrorSubcomponent.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/error/ErrorSubcomponent.kt
new file mode 100644
index 00000000000..43f6a2e7c07
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/error/ErrorSubcomponent.kt
@@ -0,0 +1,15 @@
+package com.stripe.android.financialconnections.features.error
+
+import dagger.BindsInstance
+import dagger.Subcomponent
+
+@Subcomponent
+internal interface ErrorSubcomponent {
+
+    val viewModel: ErrorViewModel
+
+    @Subcomponent.Factory
+    interface Factory {
+        fun create(@BindsInstance initialState: ErrorState): ErrorSubcomponent
+    }
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/error/ErrorViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/error/ErrorViewModel.kt
new file mode 100644
index 00000000000..a84e837f654
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/error/ErrorViewModel.kt
@@ -0,0 +1,128 @@
+package com.stripe.android.financialconnections.features.error
+
+import com.airbnb.mvrx.Async
+import com.airbnb.mvrx.MavericksState
+import com.airbnb.mvrx.MavericksViewModel
+import com.airbnb.mvrx.MavericksViewModelFactory
+import com.airbnb.mvrx.Uninitialized
+import com.airbnb.mvrx.ViewModelContext
+import com.stripe.android.core.Logger
+import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
+import com.stripe.android.financialconnections.analytics.logError
+import com.stripe.android.financialconnections.domain.GetManifest
+import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator
+import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator.Message
+import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
+import com.stripe.android.financialconnections.navigation.Destination
+import com.stripe.android.financialconnections.navigation.NavigationManager
+import com.stripe.android.financialconnections.navigation.PopUpToBehavior
+import com.stripe.android.financialconnections.repository.FinancialConnectionsErrorRepository
+import com.stripe.android.financialconnections.ui.FinancialConnectionsSheetNativeActivity
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+internal class ErrorViewModel @Inject constructor(
+    initialState: ErrorState,
+    private val coordinator: NativeAuthFlowCoordinator,
+    private val getManifest: GetManifest,
+    private val errorRepository: FinancialConnectionsErrorRepository,
+    private val eventTracker: FinancialConnectionsAnalyticsTracker,
+    private val navigationManager: NavigationManager,
+    private val logger: Logger
+) : MavericksViewModel(initialState) {
+
+    init {
+        logErrors()
+        suspend {
+            // Clear the partner web auth state if it exists, so that if the user lands back in the partner_auth
+            // pane after an error, they will be able to start over.
+            coordinator().emit(Message.ClearPartnerWebAuth)
+            ErrorState.Payload(
+                error = requireNotNull(errorRepository.get()),
+                disableLinkMoreAccounts = getManifest().disableLinkMoreAccounts,
+                allowManualEntry = getManifest().allowManualEntry
+            )
+        }.execute { copy(payload = it) }
+    }
+
+    private fun logErrors() {
+        onAsync(
+            ErrorState::payload,
+            onFail = { error ->
+                eventTracker.logError(
+                    extraMessage = "Error linking more accounts",
+                    error = error,
+                    logger = logger,
+                    pane = PANE
+                )
+            },
+        )
+    }
+
+    fun onManualEntryClick() {
+        // NOTE: we do not clear error when going to manual entry
+        // this allows us to enable the user to go back to this pane.
+        // we may still attach `terminal_error` in the /complete call,
+        // but we do that today in v2 and we will still successfully complete
+        // the session.
+        navigationManager.tryNavigateTo(
+            route = Destination.ManualEntry(referrer = PANE),
+        )
+    }
+
+    private fun reset() {
+        navigationManager.tryNavigateTo(
+            route = Destination.Reset(referrer = PANE),
+            popUpTo = PopUpToBehavior.Current(inclusive = true),
+        )
+    }
+
+    suspend fun close(error: Throwable) {
+        coordinator().emit(Message.CloseWithError(error))
+    }
+
+    fun onSelectAnotherBank() = viewModelScope.launch {
+        kotlin.runCatching {
+            val payload = requireNotNull(awaitState().payload())
+            if (payload.disableLinkMoreAccounts) {
+                close(payload.error)
+            } else {
+                reset()
+            }
+        }.onFailure {
+            close(it)
+        }
+    }
+
+    override fun onCleared() {
+        errorRepository.clear()
+        super.onCleared()
+    }
+
+    companion object : MavericksViewModelFactory {
+
+        override fun create(
+            viewModelContext: ViewModelContext,
+            state: ErrorState
+        ): ErrorViewModel {
+            return viewModelContext.activity()
+                .viewModel
+                .activityRetainedComponent
+                .errorSubcomponent
+                .create(state)
+                .viewModel
+        }
+
+        internal val PANE = Pane.UNEXPECTED_ERROR
+    }
+}
+
+internal data class ErrorState(
+    val payload: Async = Uninitialized
+) : MavericksState {
+    data class Payload(
+        val error: Throwable,
+        val disableLinkMoreAccounts: Boolean,
+        val allowManualEntry: Boolean
+    )
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/exit/ExitModal.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/exit/ExitModal.kt
new file mode 100644
index 00000000000..1151bee82c8
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/exit/ExitModal.kt
@@ -0,0 +1,101 @@
+package com.stripe.android.financialconnections.features.exit
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavBackStackEntry
+import com.airbnb.mvrx.compose.collectAsState
+import com.airbnb.mvrx.compose.mavericksViewModel
+import com.stripe.android.financialconnections.R
+import com.stripe.android.financialconnections.features.common.ShapedIcon
+import com.stripe.android.financialconnections.ui.TextResource
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsButton
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
+
+@Composable
+internal fun ExitModal(
+    backStackEntry: NavBackStackEntry
+) {
+    val viewModel: ExitViewModel = mavericksViewModel(argsFactory = { backStackEntry.arguments })
+
+    val state by viewModel.collectAsState()
+    state.payload()?.let {
+        ExitModalContent(
+            description = it.description,
+            loading = state.closing,
+            onExit = viewModel::onCloseConfirm,
+            onCancel = viewModel::onCloseDismiss
+        )
+    }
+}
+
+@Composable
+private fun ExitModalContent(
+    description: TextResource,
+    loading: Boolean,
+    onExit: () -> Unit,
+    onCancel: () -> Unit,
+) {
+    Column(
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(24.dp),
+    ) {
+        ShapedIcon(
+            painter = painterResource(id = R.drawable.stripe_ic_panel_arrow_right),
+            contentDescription = stringResource(R.string.stripe_exit_modal_title)
+        )
+        Spacer(modifier = Modifier.size(16.dp))
+        Text(
+            text = stringResource(R.string.stripe_exit_modal_title),
+            style = typography.headingMedium,
+        )
+        Spacer(modifier = Modifier.size(8.dp))
+        Text(text = description.toText().toString())
+        Spacer(modifier = Modifier.size(24.dp))
+        FinancialConnectionsButton(
+            modifier = Modifier.fillMaxWidth(),
+            loading = loading,
+            onClick = onExit
+        ) {
+            Text(text = stringResource(id = R.string.stripe_exit_modal_cta_accept))
+        }
+        Spacer(modifier = Modifier.size(8.dp))
+        FinancialConnectionsButton(
+            modifier = Modifier.fillMaxWidth(),
+            enabled = !loading,
+            type = FinancialConnectionsButton.Type.Secondary,
+            onClick = onCancel
+        ) {
+            Text(text = stringResource(id = R.string.stripe_exit_modal_cta_cancel))
+        }
+    }
+}
+
+@Composable
+@Preview
+internal fun ExitModalPreview() {
+    FinancialConnectionsTheme {
+        Surface(color = colors.backgroundSurface) {
+            ExitModalContent(
+                description = TextResource.StringId(R.string.stripe_exit_modal_desc, listOf("MerchantName")),
+                loading = false,
+                onExit = {},
+                onCancel = {}
+            )
+        }
+    }
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/exit/ExitSubcomponent.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/exit/ExitSubcomponent.kt
new file mode 100644
index 00000000000..b48d7c96424
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/exit/ExitSubcomponent.kt
@@ -0,0 +1,15 @@
+package com.stripe.android.financialconnections.features.exit
+
+import dagger.BindsInstance
+import dagger.Subcomponent
+
+@Subcomponent
+internal interface ExitSubcomponent {
+
+    val viewModel: ExitViewModel
+
+    @Subcomponent.Factory
+    interface Factory {
+        fun create(@BindsInstance initialState: ExitState): ExitSubcomponent
+    }
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/exit/ExitViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/exit/ExitViewModel.kt
new file mode 100644
index 00000000000..37e98ec4c34
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/exit/ExitViewModel.kt
@@ -0,0 +1,121 @@
+package com.stripe.android.financialconnections.features.exit
+
+import android.os.Bundle
+import com.airbnb.mvrx.Async
+import com.airbnb.mvrx.MavericksState
+import com.airbnb.mvrx.MavericksViewModel
+import com.airbnb.mvrx.MavericksViewModelFactory
+import com.airbnb.mvrx.Uninitialized
+import com.airbnb.mvrx.ViewModelContext
+import com.stripe.android.core.Logger
+import com.stripe.android.financialconnections.R
+import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
+import com.stripe.android.financialconnections.analytics.logError
+import com.stripe.android.financialconnections.domain.GetManifest
+import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator
+import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator.Message
+import com.stripe.android.financialconnections.features.common.getBusinessName
+import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
+import com.stripe.android.financialconnections.navigation.Destination
+import com.stripe.android.financialconnections.navigation.NavigationManager
+import com.stripe.android.financialconnections.ui.FinancialConnectionsSheetNativeActivity
+import com.stripe.android.financialconnections.ui.TextResource
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+internal class ExitViewModel @Inject constructor(
+    initialState: ExitState,
+    private val getManifest: GetManifest,
+    private val coordinator: NativeAuthFlowCoordinator,
+    private val eventTracker: FinancialConnectionsAnalyticsTracker,
+    private val navigationManager: NavigationManager,
+    private val logger: Logger
+) : MavericksViewModel(initialState) {
+
+    init {
+        logErrors()
+        suspend {
+            val manifest = kotlin.runCatching { getManifest() }.getOrNull()
+            val businessName = manifest?.getBusinessName()
+            val isNetworkingSignupPane =
+                manifest?.isNetworkingUserFlow == true && awaitState().referrer == Pane.NETWORKING_LINK_SIGNUP_PANE
+            val description = when {
+                isNetworkingSignupPane -> when (businessName) {
+                    null -> TextResource.StringId(R.string.stripe_close_dialog_networking_desc_no_business)
+                    else -> TextResource.StringId(
+                        value = R.string.stripe_close_dialog_networking_desc,
+                        args = listOf(businessName)
+                    )
+                }
+
+                else -> when (businessName) {
+                    null -> TextResource.StringId(R.string.stripe_exit_modal_desc_no_business)
+                    else -> TextResource.StringId(
+                        value = R.string.stripe_exit_modal_desc,
+                        args = listOf(businessName)
+                    )
+                }
+            }
+            ExitState.Payload(
+                description = description,
+            )
+        }.execute { copy(payload = it) }
+    }
+
+    fun onCloseConfirm() = viewModelScope.launch {
+        setState { copy(closing = true) }
+        coordinator().emit(Message.Complete(cause = null))
+    }
+
+    fun onCloseDismiss() {
+        navigationManager.tryNavigateBack()
+    }
+
+    private fun logErrors() {
+        onAsync(
+            ExitState::payload,
+            onFail = { error ->
+                eventTracker.logError(
+                    extraMessage = "Error loading payload",
+                    error = error,
+                    logger = logger,
+                    pane = PANE
+                )
+            },
+        )
+    }
+
+    companion object : MavericksViewModelFactory {
+
+        override fun create(
+            viewModelContext: ViewModelContext,
+            state: ExitState
+        ): ExitViewModel {
+            return viewModelContext.activity()
+                .viewModel
+                .activityRetainedComponent
+                .exitSubcomponent
+                .create(state)
+                .viewModel
+        }
+
+        internal val PANE = Pane.EXIT
+    }
+}
+
+internal data class ExitState(
+    val referrer: Pane?,
+    val payload: Async,
+    val closing: Boolean
+) : MavericksState {
+    data class Payload(
+        val description: TextResource,
+    )
+
+    @Suppress("unused") // used by mavericks to create initial state.
+    constructor(args: Bundle?) : this(
+        referrer = Destination.referrer(args),
+        payload = Uninitialized,
+        closing = false
+    )
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/institutionpicker/InstitutionPickerPreviewParameterProvider.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/institutionpicker/InstitutionPickerPreviewParameterProvider.kt
index 7f805a87ace..04cbaa75798 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/institutionpicker/InstitutionPickerPreviewParameterProvider.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/institutionpicker/InstitutionPickerPreviewParameterProvider.kt
@@ -9,132 +9,183 @@ import com.stripe.android.financialconnections.model.FinancialConnectionsInstitu
 import com.stripe.android.financialconnections.model.InstitutionResponse
 
 internal class InstitutionPickerPreviewParameterProvider :
-    PreviewParameterProvider {
+    PreviewParameterProvider {
+
     override val values = sequenceOf(
         initialLoading(),
-        searchModeSearchingInstitutions(),
-        searchModeWithResults(),
-        searchModeWithResultsNoManualEntry(),
-        searchModeNoResults(),
-        searchModeNoResultsNoManualEntry(),
-        searchModeFailed(),
-        searchModeFailedNoManualEntry(),
-        noSearchMode()
+        featured(),
+        searchInProgress(),
+        searchSuccess(),
+        searchSuccessNoManualEntry(),
+        searchNoResults(),
+        searchNoResultsNoManualEntry(),
+        searchFailed(),
+        searchFailedNoManualEntry(),
+        selectedInstitution(),
+        partiallyScrolled()
     )
 
-    private fun initialLoading() = InstitutionPickerState(
-        previewText = null,
-        payload = Loading(),
-        searchInstitutions = Uninitialized,
-        searchMode = false,
+    private fun initialLoading() = InstitutionPreviewState(
+        state = InstitutionPickerState(
+            previewText = null,
+            payload = Loading(),
+            searchInstitutions = Uninitialized,
+        ),
+        initialScroll = 0
+    )
+
+    private fun featured() = InstitutionPreviewState(
+        state = InstitutionPickerState(
+            previewText = null,
+            payload = Success(payload()),
+            searchInstitutions = Uninitialized,
+        ),
+        initialScroll = 0
     )
 
-    private fun searchModeSearchingInstitutions() = InstitutionPickerState(
-        previewText = "Some query",
-        payload = Success(payload()),
-        searchInstitutions = Loading(),
-        searchMode = true,
+    private fun searchInProgress() = InstitutionPreviewState(
+        state = InstitutionPickerState(
+            previewText = "Some query",
+            payload = Success(payload()),
+            searchInstitutions = Loading(),
+        ),
+        initialScroll = 0
     )
 
-    private fun searchModeWithResults() = InstitutionPickerState(
-        previewText = "Some query",
-        payload = Success(payload()),
-        searchInstitutions = Success(institutionResponse().copy(showManualEntry = true)),
-        searchMode = true,
+    private fun searchSuccess() = InstitutionPreviewState(
+        state = InstitutionPickerState(
+            previewText = "Some query",
+            payload = Success(payload()),
+            searchInstitutions = Success(institutionResponse(FEW_INSTITUTIONS).copy(showManualEntry = true)),
+        ),
+        initialScroll = 0
     )
 
-    private fun searchModeWithResultsNoManualEntry() = InstitutionPickerState(
-        previewText = "Some query",
-        payload = Success(payload()),
-        searchInstitutions = Success(institutionResponse().copy(showManualEntry = false)),
-        searchMode = true,
+    private fun searchSuccessNoManualEntry() = InstitutionPreviewState(
+        state = InstitutionPickerState(
+            previewText = "Some query",
+            payload = Success(payload()),
+            searchInstitutions = Success(institutionResponse(FEW_INSTITUTIONS).copy(showManualEntry = false)),
+        ),
+        initialScroll = 0
     )
 
-    private fun searchModeNoResults() = InstitutionPickerState(
-        previewText = "Some query",
-        payload = Success(payload()),
-        searchInstitutions = Success(
-            InstitutionResponse(
-                data = emptyList(),
-                showManualEntry = true
-            )
+    private fun searchNoResults() = InstitutionPreviewState(
+        state = InstitutionPickerState(
+            previewText = "Some query",
+            payload = Success(payload()),
+            searchInstitutions = Success(
+                InstitutionResponse(
+                    data = emptyList(),
+                    showManualEntry = true
+                )
+            ),
         ),
-        searchMode = true,
+        initialScroll = 0
     )
 
-    private fun searchModeNoResultsNoManualEntry() = InstitutionPickerState(
-        previewText = "Some query",
-        payload = Success(payload()),
-        searchInstitutions = Success(
-            InstitutionResponse(
-                data = emptyList(),
-                showManualEntry = false
-            )
+    private fun searchNoResultsNoManualEntry() = InstitutionPreviewState(
+        state = InstitutionPickerState(
+            previewText = "Some query",
+            payload = Success(payload()),
+            searchInstitutions = Success(
+                InstitutionResponse(
+                    data = emptyList(),
+                    showManualEntry = false
+                )
+            ),
         ),
-        searchMode = true,
+        initialScroll = 0
     )
 
-    private fun searchModeFailed() = InstitutionPickerState(
-        previewText = "Some query",
-        payload = Success(payload().copy(allowManualEntry = true)),
-        searchInstitutions = Fail(java.lang.Exception("Something went wrong")),
-        searchMode = true,
+    private fun searchFailed() = InstitutionPreviewState(
+        state = InstitutionPickerState(
+            previewText = "Some query",
+            payload = Success(payload(manualEntry = true)),
+            searchInstitutions = Fail(java.lang.Exception("Something went wrong")),
+        ),
+        initialScroll = 0
     )
 
-    private fun searchModeFailedNoManualEntry() = InstitutionPickerState(
-        previewText = "Some query",
-        payload = Success(payload().copy(allowManualEntry = false)),
-        searchInstitutions = Fail(java.lang.Exception("Something went wrong")),
-        searchMode = true,
+    private fun searchFailedNoManualEntry() = InstitutionPreviewState(
+        state = InstitutionPickerState(
+            previewText = "Some query",
+            payload = Success(payload(manualEntry = false)),
+            searchInstitutions = Fail(java.lang.Exception("Something went wrong")),
+        ),
+        initialScroll = 0
     )
 
-    private fun noSearchMode() = InstitutionPickerState(
-        previewText = "Some query",
-        payload = Success(payload()),
-        searchInstitutions = Success(institutionResponse()),
-        searchMode = false,
+    private fun selectedInstitution() = InstitutionPreviewState(
+        state = InstitutionPickerState(
+            previewText = "Some query",
+            payload = Success(payload()),
+            searchInstitutions = Success(institutionResponse(FEW_INSTITUTIONS)),
+            selectedInstitutionId = "2",
+            createSessionForInstitution = Loading(),
+        ),
+        initialScroll = 0
     )
 
-    private fun payload() = InstitutionPickerState.Payload(
-        featuredInstitutions = institutionResponse().data,
-        allowManualEntry = false,
+    private fun partiallyScrolled(): InstitutionPreviewState {
+        return InstitutionPreviewState(
+            state = InstitutionPickerState(
+                previewText = "Some query",
+                payload = Success(payload()),
+                searchInstitutions = Success(institutionResponse(MANY_INSTITUTIONS)),
+            ),
+            initialScroll = 1000
+        )
+    }
+
+    private fun payload(manualEntry: Boolean = true) = InstitutionPickerState.Payload(
+        featuredInstitutions = institutionResponse(institutions = FEW_INSTITUTIONS).copy(showManualEntry = manualEntry),
         searchDisabled = false,
         featuredInstitutionsDuration = 0
     )
 
-    private fun institutionResponse() = InstitutionResponse(
+    @Suppress("MagicNumber")
+    private fun institutionResponse(institutions: Int) = InstitutionResponse(
         showManualEntry = true,
         listOf(
-            FinancialConnectionsInstitution(
-                id = "1",
-                name = "Very very long institution 1",
-                url = "institution 1 url",
-                featured = false,
-                featuredOrder = null,
-                icon = null,
-                logo = null,
-                mobileHandoffCapable = false
+            institution(1).copy(
+                name = "Very very long institution content does not fit - 1",
+                url = "https://www.institutionUrl.com/1",
             ),
-            FinancialConnectionsInstitution(
-                id = "2",
-                name = "Institution 2",
-                url = "Institution 2 url",
-                featured = false,
-                featuredOrder = null,
-                icon = null,
-                logo = null,
-                mobileHandoffCapable = false
+            institution(2),
+            institution(3).copy(
+                url = "Unparseable URL"
             ),
-            FinancialConnectionsInstitution(
-                id = "3",
-                name = "Institution 3",
-                url = "Institution 3 url",
-                featured = false,
-                featuredOrder = null,
-                icon = null,
-                logo = null,
-                mobileHandoffCapable = false
-            )
+            institution(4),
+            institution(5),
+            institution(6),
+            institution(7),
+            institution(8),
+            institution(9),
+            institution(10)
+        ).take(institutions)
+    )
+
+    private fun institution(i: Int): FinancialConnectionsInstitution {
+        return FinancialConnectionsInstitution(
+            id = i.toString(),
+            name = "Institution $i",
+            url = "otherUrl.com",
+            featured = false,
+            featuredOrder = null,
+            icon = null,
+            logo = null,
+            mobileHandoffCapable = false
         )
+    }
+
+    data class InstitutionPreviewState(
+        val state: InstitutionPickerState,
+        val initialScroll: Int
     )
+
+    companion object {
+        const val FEW_INSTITUTIONS = 3
+        const val MANY_INSTITUTIONS = 10
+    }
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/institutionpicker/InstitutionPickerScreen.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/institutionpicker/InstitutionPickerScreen.kt
index c9b177a0536..6f9191f6501 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/institutionpicker/InstitutionPickerScreen.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/institutionpicker/InstitutionPickerScreen.kt
@@ -1,17 +1,13 @@
-@file:Suppress("TooManyFunctions", "LongMethod")
-
 package com.stripe.android.financialconnections.features.institutionpicker
 
-import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.LocalOverscrollConfiguration
 import androidx.compose.foundation.background
-import androidx.compose.foundation.border
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
@@ -20,42 +16,42 @@ import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.grid.GridCells
-import androidx.compose.foundation.lazy.grid.GridItemSpan
-import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.itemsIndexed
 import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardActions
 import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material.Divider
 import androidx.compose.material.Icon
 import androidx.compose.material.Text
 import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.ArrowBack
-import androidx.compose.material.icons.filled.Search
-import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.material.icons.filled.Clear
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.draw.clip
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.testTagsAsResourceId
 import androidx.compose.ui.text.input.ImeAction
-import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.input.KeyboardCapitalization
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.tooling.preview.Preview
@@ -69,314 +65,407 @@ import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.compose.collectAsState
 import com.airbnb.mvrx.compose.mavericksViewModel
 import com.stripe.android.financialconnections.R
-import com.stripe.android.financialconnections.features.common.InstitutionPlaceholder
+import com.stripe.android.financialconnections.features.common.FullScreenGenericLoading
+import com.stripe.android.financialconnections.features.common.InstitutionIcon
 import com.stripe.android.financialconnections.features.common.LoadingShimmerEffect
 import com.stripe.android.financialconnections.features.common.LoadingSpinner
+import com.stripe.android.financialconnections.features.common.ShapedIcon
 import com.stripe.android.financialconnections.features.institutionpicker.InstitutionPickerState.Payload
 import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
 import com.stripe.android.financialconnections.model.InstitutionResponse
 import com.stripe.android.financialconnections.presentation.parentViewModel
 import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
-import com.stripe.android.financialconnections.ui.LocalImageLoader
+import com.stripe.android.financialconnections.ui.TextResource
+import com.stripe.android.financialconnections.ui.components.AnnotatedText
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsOutlinedTextField
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsScaffold
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsTopAppBar
-import com.stripe.android.financialconnections.ui.components.clickableSingle
-import com.stripe.android.financialconnections.ui.theme.Brand100
-import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
-import com.stripe.android.uicore.image.StripeImage
+import com.stripe.android.financialconnections.ui.components.StringAnnotation
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsColors
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
+import kotlinx.coroutines.launch
 
 @Composable
 internal fun InstitutionPickerScreen() {
     val viewModel: InstitutionPickerViewModel = mavericksViewModel()
     val parentViewModel = parentViewModel()
     val state: InstitutionPickerState by viewModel.collectAsState()
-
-    // when in search mode, back closes search.
-    val focusManager = LocalFocusManager.current
-    BackHandler(state.searchMode) {
-        focusManager.clearFocus()
-        viewModel.onCancelSearchClick()
-    }
+    val listState = rememberLazyListState()
 
     InstitutionPickerContent(
+        listState = listState,
         payload = state.payload,
         institutions = state.searchInstitutions,
-        searchMode = state.searchMode,
         // This is just used to provide a text in Compose previews
         previewText = state.previewText,
+        selectedInstitutionId = state.selectedInstitutionId,
         onQueryChanged = viewModel::onQueryChanged,
         onInstitutionSelected = viewModel::onInstitutionSelected,
-        onCancelSearchClick = viewModel::onCancelSearchClick,
         onCloseClick = { parentViewModel.onCloseWithConfirmationClick(Pane.INSTITUTION_PICKER) },
-        onSearchFocused = viewModel::onSearchFocused,
         onManualEntryClick = viewModel::onManualEntryClick,
-        onScrollChanged = viewModel::onScrollChanged
+        onScrollChanged = viewModel::onScrollChanged,
     )
 }
 
 @Composable
 private fun InstitutionPickerContent(
+    listState: LazyListState,
     payload: Async,
     institutions: Async,
-    searchMode: Boolean,
     previewText: String?,
+    selectedInstitutionId: String?,
     onQueryChanged: (String) -> Unit,
     onInstitutionSelected: (FinancialConnectionsInstitution, Boolean) -> Unit,
-    onCancelSearchClick: () -> Unit,
     onCloseClick: () -> Unit,
-    onSearchFocused: () -> Unit,
     onManualEntryClick: () -> Unit,
     onScrollChanged: () -> Unit
 ) {
     FinancialConnectionsScaffold(
         topBar = {
-            if (!searchMode) {
-                FinancialConnectionsTopAppBar(
-                    onCloseClick = onCloseClick
-                )
-            }
+            FinancialConnectionsTopAppBar(
+                onCloseClick = onCloseClick
+            )
         }
     ) {
-        LoadedContent(
-            searchMode = searchMode,
-            previewText = previewText,
-            onQueryChanged = onQueryChanged,
-            onSearchFocused = onSearchFocused,
-            onCancelSearchClick = onCancelSearchClick,
-            institutions = institutions,
-            onInstitutionSelected = onInstitutionSelected,
-            payload = payload,
-            onManualEntryClick = onManualEntryClick,
-            onScrollChanged = onScrollChanged
-        )
+        when (payload) {
+            is Uninitialized,
+            is Loading,
+            is Fail -> FullScreenGenericLoading()
+
+            is Success -> LoadedContent(
+                listState = listState,
+                previewText = previewText,
+                selectedInstitutionId = selectedInstitutionId,
+                onQueryChanged = onQueryChanged,
+                institutions = institutions,
+                onInstitutionSelected = onInstitutionSelected,
+                payload = payload(),
+                onManualEntryClick = onManualEntryClick,
+                onScrollChanged = onScrollChanged
+            )
+        }
     }
 }
 
+@OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun LoadedContent(
-    searchMode: Boolean,
+    listState: LazyListState,
     previewText: String?,
+    selectedInstitutionId: String?,
     onQueryChanged: (String) -> Unit,
-    onSearchFocused: () -> Unit,
-    onCancelSearchClick: () -> Unit,
     institutions: Async,
     onInstitutionSelected: (FinancialConnectionsInstitution, Boolean) -> Unit,
-    payload: Async,
+    payload: Payload,
     onManualEntryClick: () -> Unit,
     onScrollChanged: () -> Unit,
 ) {
-    var input by remember { mutableStateOf(TextFieldValue(previewText ?: "")) }
-    LaunchedEffect(searchMode) { if (!searchMode) input = TextFieldValue() }
-    Column {
-        if (searchMode.not()) {
-            Spacer(Modifier.size(16.dp))
-            Text(
-                modifier = Modifier
-                    .padding(horizontal = 24.dp)
-                    .fillMaxWidth(),
-                text = stringResource(R.string.stripe_institutionpicker_pane_select_bank),
-                style = FinancialConnectionsTheme.typography.subtitle
-            )
+    var input by remember { mutableStateOf(previewText ?: "") }
+    var shouldEmitScrollEvent by remember { mutableStateOf(true) }
+    val searchInputFocusRequester = remember { FocusRequester() }
+    val coroutineScope = rememberCoroutineScope()
+
+    // Scroll event should be emitted just once per search
+    LaunchedEffect(institutions) { shouldEmitScrollEvent = true }
+    // Trigger onScrollChanged with the list of institutions when scrolling stops (true -> false)
+    LaunchedEffect(listState.isScrollInProgress) {
+        if (institutions()?.data?.isNotEmpty() == true &&
+            !listState.isScrollInProgress &&
+            shouldEmitScrollEvent
+        ) {
+            onScrollChanged()
+            shouldEmitScrollEvent = false
         }
-        Spacer(modifier = Modifier.size(16.dp))
-        if (payload()?.searchDisabled == false) {
-            FinancialConnectionsSearchRow(
-                query = input,
-                searchMode = searchMode,
-                onQueryChanged = {
-                    input = it
-                    onQueryChanged(input.text)
-                },
-                onSearchFocused = onSearchFocused,
-                onCancelSearchClick = onCancelSearchClick
+    }
+
+    CompositionLocalProvider(
+        // Disable overscroll as it does not play well with sticky headers.
+        LocalOverscrollConfiguration provides null
+    ) {
+        LazyColumn(
+            Modifier.padding(horizontal = 16.dp),
+            state = listState,
+            content = {
+                item { SearchTitle(modifier = Modifier.padding(horizontal = 8.dp)) }
+                item { Spacer(modifier = Modifier.height(24.dp)) }
+                stickyHeader(key = "searchRow") {
+                    SearchRow(
+                        focusRequester = searchInputFocusRequester,
+                        query = input,
+                        onQueryChanged = {
+                            input = it
+                            onQueryChanged(input)
+                        },
+                    )
+                }
+                item { Spacer(modifier = Modifier.height(8.dp)) }
+
+                searchResults(
+                    isInputEmpty = input.isBlank(),
+                    payload = payload,
+                    selectedInstitutionId = selectedInstitutionId,
+                    onInstitutionSelected = onInstitutionSelected,
+                    institutions = institutions,
+                    onManualEntryClick = onManualEntryClick,
+                    onSearchMoreClick = {
+                        // Scroll to the top of the list and focus on the search input
+                        coroutineScope.launch { listState.animateScrollToItem(index = 1) }
+                        searchInputFocusRequester.requestFocus()
+                    }
+                )
+            }
+        )
+    }
+}
+
+private fun LazyListScope.searchResults(
+    isInputEmpty: Boolean,
+    payload: Payload,
+    selectedInstitutionId: String?,
+    onInstitutionSelected: (FinancialConnectionsInstitution, Boolean) -> Unit,
+    institutions: Async,
+    onManualEntryClick: () -> Unit,
+    onSearchMoreClick: () -> Unit
+) {
+    when {
+        // No input: Display featured institutions.
+        isInputEmpty -> {
+            itemsIndexed(
+                items = payload.featuredInstitutions.data,
+                key = { _, institution -> institution.id },
+                itemContent = { index, institution ->
+                    InstitutionResultTile(
+                        modifier = Modifier.padding(8.dp),
+                        loading = selectedInstitutionId == institution.id,
+                        enabled = selectedInstitutionId?.let { it == institution.id } ?: true,
+                        institution = institution,
+                        index = index,
+                        onInstitutionSelected = { onInstitutionSelected(it, true) }
+                    )
+                }
             )
+            item(key = "search_more") {
+                SearchMoreRow(
+                    modifier = Modifier.padding(8.dp),
+                    onClick = onSearchMoreClick,
+                    enabled = selectedInstitutionId == null,
+                )
+            }
         }
-        if (input.text.isNotBlank()) {
-            SearchInstitutionsList(
-                institutions = institutions,
-                onInstitutionSelected = onInstitutionSelected,
-                onManualEntryClick = onManualEntryClick,
-                onScrollChanged = onScrollChanged,
-                allowManualEntry = payload()?.allowManualEntry ?: false
-            )
-        } else {
-            FeaturedInstitutionsGrid(
-                modifier = Modifier.weight(1f),
-                payload = payload,
-                onInstitutionSelected = onInstitutionSelected
-            )
+
+        else -> when (institutions) {
+            // Load failure: Display error message.
+            is Fail -> item {
+                NoResultsTile(
+                    modifier = Modifier.padding(8.dp),
+                    showManualEntry = payload.featuredInstitutions.showManualEntry,
+                    onManualEntryClick = onManualEntryClick
+                )
+            }
+
+            // Loading: Display shimmer.
+            is Uninitialized,
+            is Loading -> items((0..10).toList()) {
+                InstitutionResultShimmer(
+                    modifier = Modifier.padding(8.dp)
+                )
+            }
+
+            // Success: Display search results.
+            is Success -> if (institutions().data.isEmpty()) {
+                // NO RESULTS CASE
+                item {
+                    NoResultsTile(
+                        modifier = Modifier.padding(8.dp),
+                        showManualEntry = institutions().showManualEntry,
+                        onManualEntryClick = onManualEntryClick
+                    )
+                }
+            } else {
+                // RESULTS CASE: Institution List + Manual Entry final row if needed.
+                itemsIndexed(
+                    items = institutions().data,
+                    key = { _, institution -> institution.id },
+                    itemContent = { index, institution ->
+                        InstitutionResultTile(
+                            modifier = Modifier.padding(8.dp),
+                            loading = selectedInstitutionId == institution.id,
+                            enabled = selectedInstitutionId?.let { it == institution.id } ?: true,
+                            institution = institution,
+                            index = index,
+                            onInstitutionSelected = { onInstitutionSelected(it, false) }
+                        )
+                    }
+                )
+                if (institutions().showManualEntry == true) {
+                    item {
+                        ManualEntryRow(
+                            modifier = Modifier.padding(8.dp),
+                            enabled = selectedInstitutionId == null,
+                            onManualEntryClick = onManualEntryClick
+                        )
+                    }
+                }
+            }
         }
     }
 }
 
 @Composable
-private fun FinancialConnectionsSearchRow(
-    query: TextFieldValue,
-    onQueryChanged: (TextFieldValue) -> Unit,
-    onCancelSearchClick: () -> Unit,
-    onSearchFocused: () -> Unit,
-    searchMode: Boolean
+private fun NoResultsTile(
+    modifier: Modifier = Modifier,
+    showManualEntry: Boolean?,
+    onManualEntryClick: () -> Unit
+) {
+    Column(
+        modifier = modifier
+            .fillMaxWidth(),
+        horizontalAlignment = Alignment.CenterHorizontally,
+        verticalArrangement = Arrangement.Center
+    ) {
+        Text(
+            text = stringResource(id = R.string.stripe_institutionpicker_pane_error_title),
+            style = typography.headingLarge
+        )
+        Spacer(modifier = Modifier.height(16.dp))
+        AnnotatedText(
+            text = when (showManualEntry ?: false) {
+                true -> TextResource.StringId(R.string.stripe_institutionpicker_pane_error_desc_manual_entry)
+                false -> TextResource.StringId(R.string.stripe_institutionpicker_pane_error_desc)
+            },
+            onClickableTextClick = { onManualEntryClick() },
+            defaultStyle = typography.bodyMedium.copy(
+                textAlign = TextAlign.Center,
+            ),
+            annotationStyles = mapOf(
+                StringAnnotation.CLICKABLE to typography.bodyMediumEmphasized.toSpanStyle().copy(
+                    color = colors.textBrand,
+                )
+            ),
+        )
+    }
+}
+
+@Composable
+private fun SearchTitle(modifier: Modifier = Modifier) {
+    Text(
+        modifier = modifier.fillMaxWidth(),
+        text = stringResource(R.string.stripe_institutionpicker_pane_select_bank),
+        style = typography.headingXLarge
+    )
+}
+
+@Composable
+private fun SearchRow(
+    modifier: Modifier = Modifier,
+    focusRequester: FocusRequester,
+    query: String,
+    onQueryChanged: (String) -> Unit,
 ) {
     val focusManager = LocalFocusManager.current
-    Row(
-        verticalAlignment = Alignment.CenterVertically,
-        modifier = Modifier.padding(horizontal = 24.dp)
+    Box(
+        modifier = modifier
+            .fillMaxWidth()
+            .background(colors.backgroundSurface)
+            .padding(top = 0.dp, bottom = 8.dp, start = 8.dp, end = 8.dp)
     ) {
         FinancialConnectionsOutlinedTextField(
+            modifier = modifier
+                .fillMaxWidth()
+                .focusRequester(focusRequester),
+            leadingIcon = {
+                Icon(
+                    painter = painterResource(id = R.drawable.stripe_ic_search),
+                    tint = colors.iconDefault,
+                    contentDescription = "Search icon",
+                )
+            },
             keyboardOptions = KeyboardOptions(
-                keyboardType = KeyboardType.Text,
-                imeAction = ImeAction.Done
+                capitalization = KeyboardCapitalization.Words,
+                imeAction = ImeAction.Search,
             ),
-            leadingIcon = if (searchMode) {
-                {
-                    Icon(
-                        Icons.Filled.ArrowBack,
-                        tint = FinancialConnectionsTheme.colors.textPrimary,
-                        contentDescription = "Back button",
-                        modifier = Modifier.clickable {
-                            onCancelSearchClick()
-                            focusManager.clearFocus()
-                        }
-                    )
-                }
-            } else {
-                {
-                    Icon(
-                        Icons.Filled.Search,
-                        tint = FinancialConnectionsTheme.colors.textPrimary,
-                        contentDescription = "Search icon",
-                    )
-                }
-            },
-            modifier = Modifier
-                .onFocusChanged { if (it.isFocused) onSearchFocused() }
-                .weight(1f),
+            keyboardActions = KeyboardActions(
+                onSearch = { focusManager.clearFocus() },
+            ),
+            trailingIcon = query
+                .takeIf { it.isNotEmpty() }
+                ?.let {
+                    { ClearSearchButton(onQueryChanged = onQueryChanged, colors = colors) }
+                },
             placeholder = {
                 Text(
                     text = stringResource(id = R.string.stripe_search),
-                    style = FinancialConnectionsTheme.typography.body,
-                    color = FinancialConnectionsTheme.colors.textDisabled
+                    style = typography.labelLarge,
+                    color = colors.textSubdued
                 )
             },
             value = query,
+            enabled = true,
             onValueChange = { onQueryChanged(it) }
         )
     }
 }
 
 @Composable
-private fun SearchInstitutionsList(
-    institutions: Async,
-    onInstitutionSelected: (FinancialConnectionsInstitution, Boolean) -> Unit,
-    onManualEntryClick: () -> Unit,
-    onScrollChanged: () -> Unit,
-    allowManualEntry: Boolean
+private fun ClearSearchButton(
+    onQueryChanged: (String) -> Unit,
+    colors: FinancialConnectionsColors
 ) {
-    val listState = rememberLazyListState()
-    val shouldEmitScrollEvent = remember { mutableStateOf(true) }
-
-    // Scroll event should be emitted just once per search
-    LaunchedEffect(institutions) { shouldEmitScrollEvent.value = true }
-    // Trigger onScrollChanged with the list of institutions when scrolling stops (true -> false)
-    LaunchedEffect(listState.isScrollInProgress) {
-        if (institutions()?.data?.isNotEmpty() == true &&
-            !listState.isScrollInProgress &&
-            shouldEmitScrollEvent.value
-        ) {
-            onScrollChanged()
-            shouldEmitScrollEvent.value = false
-        }
+    Box(
+        Modifier
+            .size(16.dp)
+            .clickable { onQueryChanged("") }
+            .background(
+                color = colors.border,
+                shape = CircleShape
+            )
+            .padding(2.dp)
+    ) {
+        Icon(
+            Icons.Filled.Clear,
+            tint = colors.backgroundSurface,
+            contentDescription = "Clear search",
+        )
     }
-
-    LazyColumn(
-        state = listState,
-        horizontalAlignment = Alignment.CenterHorizontally,
-        contentPadding = PaddingValues(top = 16.dp),
-        content = {
-            when (institutions) {
-                Uninitialized,
-                is Fail -> item {
-                    if (allowManualEntry) {
-                        ManualEntryRow(onManualEntryClick)
-                    } else {
-                        NoResultsRow()
-                    }
-                }
-
-                is Loading -> item {
-                    Box(
-                        contentAlignment = Alignment.Center,
-                        modifier = Modifier.fillMaxWidth()
-                    ) { LoadingSpinner() }
-                }
-
-                is Success -> {
-                    if (institutions().data.isEmpty()) {
-                        // NO RESULTS CASE
-                        item {
-                            if (institutions().showManualEntry == true) {
-                                ManualEntryRow(onManualEntryClick)
-                            } else {
-                                NoResultsRow()
-                            }
-                        }
-                    } else {
-                        itemsIndexed(
-                            items = institutions().data,
-                            key = { _, institution -> institution.id },
-                            itemContent = { index, institution ->
-                                InstitutionResultTile(
-                                    institution = institution,
-                                    index = index
-                                ) { onInstitutionSelected(it, false) }
-                            }
-                        )
-                        if (institutions().showManualEntry == true) {
-                            item {
-                                Divider(
-                                    modifier = Modifier
-                                        .fillMaxWidth()
-                                        .padding(
-                                            vertical = 8.dp,
-                                            horizontal = 24.dp
-                                        ),
-                                    color = FinancialConnectionsTheme.colors.borderDefault
-                                )
-                            }
-                            item {
-                                ManualEntryRow(onManualEntryClick)
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    )
 }
 
 @Composable
-private fun NoResultsRow() {
+private fun ManualEntryRow(
+    modifier: Modifier = Modifier,
+    enabled: Boolean,
+    onManualEntryClick: () -> Unit
+) {
     Row(
         verticalAlignment = Alignment.CenterVertically,
-        modifier = Modifier
+        modifier = modifier
             .fillMaxSize()
-            .padding(
-                vertical = 8.dp,
-                horizontal = 24.dp
+            .clickable(
+                enabled = enabled,
+                interactionSource = remember { MutableInteractionSource() },
+                indication = null,
+                onClick = onManualEntryClick
             )
+            .alpha(if (enabled) 1f else DISABLED_DEPTH_ALPHA)
     ) {
+        ShapedIcon(
+            backgroundShape = RoundedCornerShape(12.dp),
+            painter = painterResource(id = R.drawable.stripe_ic_add),
+            contentDescription = "Manually enter details"
+        )
+
+        Spacer(modifier = Modifier.size(8.dp))
         Column {
             Text(
-                text = stringResource(id = R.string.stripe_institutionpicker_no_results_title),
-                color = FinancialConnectionsTheme.colors.textPrimary,
-                style = FinancialConnectionsTheme.typography.bodyEmphasized
+                text = stringResource(R.string.stripe_institutionpicker_manual_entry_title),
+                color = colors.textDefault,
+                style = typography.labelLargeEmphasized,
             )
             Text(
-                text = stringResource(id = R.string.stripe_institutionpicker_no_results_desc),
-                color = FinancialConnectionsTheme.colors.textSecondary,
-                style = FinancialConnectionsTheme.typography.captionTight,
+                text = stringResource(R.string.stripe_institutionpicker_manual_entry_desc),
+                color = colors.textSubdued,
+                style = typography.labelMedium,
                 maxLines = 1,
                 overflow = TextOverflow.Ellipsis
             )
@@ -385,208 +474,147 @@ private fun NoResultsRow() {
 }
 
 @Composable
-private fun ManualEntryRow(onManualEntryClick: () -> Unit) {
+private fun SearchMoreRow(
+    modifier: Modifier = Modifier,
+    onClick: () -> Unit,
+    enabled: Boolean
+) {
     Row(
         verticalAlignment = Alignment.CenterVertically,
-        modifier = Modifier
+        modifier = modifier
             .fillMaxSize()
-            .clickable(onClick = onManualEntryClick)
-            .padding(
-                vertical = 8.dp,
-                horizontal = 24.dp
+            .clickable(
+                enabled = enabled,
+                interactionSource = remember { MutableInteractionSource() },
+                indication = null,
+                onClick = onClick
             )
+            .alpha(if (enabled) 1f else DISABLED_DEPTH_ALPHA)
     ) {
-        Icon(
-            imageVector = Icons.Filled.Add,
-            tint = FinancialConnectionsTheme.colors.textBrand,
-            modifier = Modifier
-                .size(36.dp)
-                .clip(RoundedCornerShape(6.dp))
-                .background(Brand100)
-                .padding(8.dp),
+        ShapedIcon(
+            backgroundShape = RoundedCornerShape(12.dp),
+            painter = painterResource(id = R.drawable.stripe_ic_search),
             contentDescription = "Add icon"
         )
         Spacer(modifier = Modifier.size(8.dp))
-        Column {
-            Text(
-                text = stringResource(R.string.stripe_institutionpicker_manual_entry_title),
-                color = FinancialConnectionsTheme.colors.textPrimary,
-                style = FinancialConnectionsTheme.typography.bodyEmphasized
-            )
-            Text(
-                text = stringResource(R.string.stripe_institutionpicker_manual_entry_desc),
-                color = FinancialConnectionsTheme.colors.textSecondary,
-                style = FinancialConnectionsTheme.typography.captionTight,
-                maxLines = 1,
-                overflow = TextOverflow.Ellipsis
-            )
-        }
+        Text(
+            text = stringResource(R.string.stripe_institutionpicker_search_more_title),
+            color = colors.textDefault,
+            style = typography.labelLargeEmphasized,
+        )
     }
 }
 
 @OptIn(ExperimentalComposeUiApi::class)
 @Composable
 private fun InstitutionResultTile(
+    modifier: Modifier = Modifier,
     institution: FinancialConnectionsInstitution,
+    loading: Boolean = false,
+    enabled: Boolean = true,
     index: Int,
     onInstitutionSelected: (FinancialConnectionsInstitution) -> Unit
 ) {
     Row(
         verticalAlignment = Alignment.CenterVertically,
-        modifier = Modifier
+        modifier = modifier
             .fillMaxSize()
             .semantics { testTagsAsResourceId = true }
             .testTag("search_result_$index")
-            .clickableSingle { onInstitutionSelected(institution) }
-            .padding(
-                vertical = 8.dp,
-                horizontal = 24.dp
-            )
+            .clickable(
+                enabled = enabled && loading.not(),
+                interactionSource = remember { MutableInteractionSource() },
+                indication = null,
+            ) { onInstitutionSelected(institution) }
+            .alpha(if (enabled) 1f else DISABLED_DEPTH_ALPHA)
     ) {
-        val modifier = Modifier
-            .size(36.dp)
-            .clip(RoundedCornerShape(6.dp))
-        when {
-            institution.icon?.default.isNullOrEmpty() -> InstitutionPlaceholder(modifier)
-            else -> StripeImage(
-                url = requireNotNull(institution.icon?.default),
-                imageLoader = LocalImageLoader.current,
-                contentDescription = null,
-                modifier = modifier,
-                contentScale = ContentScale.Crop,
-                errorContent = { InstitutionPlaceholder(modifier) }
-            )
-        }
+        InstitutionIcon(institution.icon?.default)
         Spacer(modifier = Modifier.size(8.dp))
-        Column {
+        Column(
+            modifier = Modifier.weight(1f)
+        ) {
             Text(
                 text = institution.name,
-                color = FinancialConnectionsTheme.colors.textPrimary,
-                style = FinancialConnectionsTheme.typography.bodyEmphasized
+                maxLines = 1,
+                color = colors.textDefault,
+                style = typography.labelLargeEmphasized,
+                overflow = TextOverflow.Ellipsis
             )
             Text(
-                text = institution.url ?: "",
-                color = FinancialConnectionsTheme.colors.textDisabled,
-                style = FinancialConnectionsTheme.typography.captionTight,
+                text = institution.formattedUrl,
+                color = colors.textSubdued,
+                style = typography.labelMedium,
                 maxLines = 1,
                 overflow = TextOverflow.Ellipsis
             )
         }
+        // add a trailing icon if this is the manual entry row
+        if (loading) {
+            Spacer(modifier = Modifier.size(8.dp))
+            LoadingSpinner(modifier = Modifier.size(24.dp))
+        }
     }
 }
 
 @Composable
-private fun FeaturedInstitutionsGrid(
-    modifier: Modifier,
-    payload: Async,
-    onInstitutionSelected: (FinancialConnectionsInstitution, Boolean) -> Unit
-) {
-    LazyVerticalGrid(
-        modifier = modifier,
-        columns = GridCells.Fixed(2),
-        contentPadding = PaddingValues(
-            top = 16.dp,
-            start = 24.dp,
-            end = 24.dp,
-            bottom = 16.dp
-        ),
-        verticalArrangement = Arrangement.spacedBy(8.dp),
-        horizontalArrangement = Arrangement.spacedBy(8.dp),
-        content = {
-            when (payload) {
-                Uninitialized, is Loading -> {
-                    item(span = { GridItemSpan(2) }) {
-                        LoadingSpinner()
-                    }
-                }
-                // Show empty featured institutions grid. Users will be able to search using search bar.
-                is Fail -> Unit
-                is Success -> items(payload().featuredInstitutions) { institution ->
-                    Box(
-                        contentAlignment = Alignment.Center,
-                        modifier = Modifier
-                            .height(80.dp)
-                            .fillMaxWidth()
-                            .clip(RoundedCornerShape(6.dp))
-                            .border(
-                                width = 1.dp,
-                                color = FinancialConnectionsTheme.colors.borderDefault,
-                                shape = RoundedCornerShape(6.dp)
-                            )
-                            .clickableSingle(
-                                interactionSource = remember { MutableInteractionSource() },
-                                indication = rememberRipple(
-                                    color = FinancialConnectionsTheme.colors.textSecondary
-                                ),
-                            ) { onInstitutionSelected(institution, true) }
-                    ) {
-                        when {
-                            institution.logo?.default.isNullOrBlank() ->
-                                FeaturedInstitutionPlaceholder(institution)
+private fun InstitutionResultShimmer(modifier: Modifier) {
+    LoadingShimmerEffect { shimmer ->
+        Row(
+            verticalAlignment = Alignment.CenterVertically,
+            modifier = modifier.fillMaxSize()
+        ) {
+            Box(
+                modifier = Modifier
+                    .size(56.dp)
+                    .clip(RoundedCornerShape(6.dp))
+                    .background(shimmer)
+            )
+            Spacer(modifier = Modifier.size(8.dp))
+            Column {
+                Box(
+                    modifier = Modifier
+                        .fillMaxWidth(0.75f)
+                        .height(16.dp)
+                        .clip(RoundedCornerShape(6.dp))
+                        .background(shimmer)
 
-                            else -> StripeImage(
-                                modifier = Modifier
-                                    .fillMaxSize()
-                                    .padding(8.dp),
-                                url = requireNotNull(institution.logo?.default),
-                                imageLoader = LocalImageLoader.current,
-                                contentScale = ContentScale.Fit,
-                                loadingContent = { FeaturedInstitutionLoading() },
-                                errorContent = { FeaturedInstitutionPlaceholder(institution) },
-                                contentDescription = "Institution logo"
-                            )
-                        }
-                    }
-                }
+                )
+                Spacer(modifier = Modifier.size(8.dp))
+                Box(
+                    modifier = Modifier
+                        .fillMaxWidth(0.5f)
+                        .height(16.dp)
+                        .clip(RoundedCornerShape(6.dp))
+                        .background(shimmer)
+                )
             }
         }
-    )
-}
-
-@Composable
-private fun BoxScope.FeaturedInstitutionLoading() {
-    LoadingShimmerEffect { shimmer ->
-        Spacer(
-            modifier = Modifier.Companion
-                .align(Alignment.Center)
-                .height(20.dp)
-                .clip(RoundedCornerShape(10.dp))
-                .fillMaxWidth(fraction = 0.5f)
-                .background(shimmer)
-        )
     }
 }
 
-@Composable
-private fun BoxScope.FeaturedInstitutionPlaceholder(institution: FinancialConnectionsInstitution) {
-    Text(
-        modifier = Modifier.Companion.align(Alignment.Center),
-        text = institution.name,
-        color = FinancialConnectionsTheme.colors.textPrimary,
-        style = FinancialConnectionsTheme.typography.bodyEmphasized,
-        textAlign = TextAlign.Center
-    )
-}
+private const val DISABLED_DEPTH_ALPHA = 0.3f
 
 @Preview(group = "Institution Picker Pane")
 @Composable
 internal fun InstitutionPickerPreview(
     @PreviewParameter(InstitutionPickerPreviewParameterProvider::class)
-    state: InstitutionPickerState
+    previewState: InstitutionPickerPreviewParameterProvider.InstitutionPreviewState
 ) {
+    val state = previewState.state
+    val listState = rememberLazyListState(
+        initialFirstVisibleItemScrollOffset = previewState.initialScroll
+    )
     FinancialConnectionsPreview {
         InstitutionPickerContent(
+            listState = listState,
             payload = state.payload,
             institutions = state.searchInstitutions,
-            searchMode = state.searchMode,
             previewText = state.previewText,
+            selectedInstitutionId = state.selectedInstitutionId,
             onQueryChanged = {},
             onInstitutionSelected = { _, _ -> },
-            onCancelSearchClick = {},
             onCloseClick = {},
-            onSearchFocused = {},
             onManualEntryClick = {},
-            onScrollChanged = {},
-        )
+        ) {}
     }
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/institutionpicker/InstitutionPickerViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/institutionpicker/InstitutionPickerViewModel.kt
index e71bf448f73..36ab15ff383 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/institutionpicker/InstitutionPickerViewModel.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/institutionpicker/InstitutionPickerViewModel.kt
@@ -5,7 +5,6 @@ import com.airbnb.mvrx.Loading
 import com.airbnb.mvrx.MavericksState
 import com.airbnb.mvrx.MavericksViewModel
 import com.airbnb.mvrx.MavericksViewModelFactory
-import com.airbnb.mvrx.Success
 import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.ViewModelContext
 import com.stripe.android.core.Logger
@@ -21,15 +20,19 @@ import com.stripe.android.financialconnections.analytics.FinancialConnectionsEve
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsEvent.Name
 import com.stripe.android.financialconnections.analytics.logError
 import com.stripe.android.financialconnections.domain.FeaturedInstitutions
-import com.stripe.android.financialconnections.domain.GetManifest
+import com.stripe.android.financialconnections.domain.GetOrFetchSync
+import com.stripe.android.financialconnections.domain.HandleError
+import com.stripe.android.financialconnections.domain.PostAuthorizationSession
 import com.stripe.android.financialconnections.domain.SearchInstitutions
 import com.stripe.android.financialconnections.domain.UpdateLocalManifest
 import com.stripe.android.financialconnections.features.institutionpicker.InstitutionPickerState.Payload
+import com.stripe.android.financialconnections.model.FinancialConnectionsAuthorizationSession
 import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
 import com.stripe.android.financialconnections.model.InstitutionResponse
 import com.stripe.android.financialconnections.navigation.Destination.ManualEntry
 import com.stripe.android.financialconnections.navigation.Destination.PartnerAuth
+import com.stripe.android.financialconnections.navigation.Destination.PartnerAuthDrawer
 import com.stripe.android.financialconnections.navigation.NavigationManager
 import com.stripe.android.financialconnections.ui.FinancialConnectionsSheetNativeActivity
 import com.stripe.android.financialconnections.utils.ConflatedJob
@@ -39,13 +42,14 @@ import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import javax.inject.Inject
 
-@Suppress("LongParameterList")
 internal class InstitutionPickerViewModel @Inject constructor(
     private val configuration: FinancialConnectionsSheet.Configuration,
+    private val postAuthorizationSession: PostAuthorizationSession,
+    private val getOrFetchSync: GetOrFetchSync,
     private val searchInstitutions: SearchInstitutions,
     private val featuredInstitutions: FeaturedInstitutions,
-    private val getManifest: GetManifest,
     private val eventTracker: FinancialConnectionsAnalyticsTracker,
+    private val handleError: HandleError,
     private val navigationManager: NavigationManager,
     private val updateLocalManifest: UpdateLocalManifest,
     private val logger: Logger,
@@ -57,29 +61,31 @@ internal class InstitutionPickerViewModel @Inject constructor(
     init {
         logErrors()
         suspend {
-            val manifest = getManifest()
-            val result: Result, Long>> =
-                kotlin.runCatching {
-                    measureTimeMillis {
-                        featuredInstitutions(
-                            clientSecret = configuration.financialConnectionsSessionClientSecret
-                        ).data
-                    }
-                }.onFailure {
-                    eventTracker.logError(
-                        extraMessage = "Error fetching featured institutions",
-                        error = it,
-                        pane = Pane.INSTITUTION_PICKER,
-                        logger = logger
+            val manifest = getOrFetchSync().manifest
+            val (featuredInstitutions: InstitutionResponse, duration: Long) = runCatching {
+                measureTimeMillis {
+                    featuredInstitutions(
+                        clientSecret = configuration.financialConnectionsSessionClientSecret
                     )
                 }
-
-            val (institutions, duration) = result.getOrNull() ?: Pair(emptyList(), 0L)
+            }.onFailure {
+                eventTracker.logError(
+                    extraMessage = "Error fetching featured institutions",
+                    error = it,
+                    pane = PANE,
+                    logger = logger
+                )
+            }.getOrElse {
+                // Allow users to search for institutions even if featured institutions fails.
+                InstitutionResponse(
+                    data = emptyList(),
+                    showManualEntry = manifest.allowManualEntry
+                ) to 0L
+            }
             Payload(
                 featuredInstitutionsDuration = duration,
-                featuredInstitutions = institutions,
+                featuredInstitutions = featuredInstitutions,
                 searchDisabled = manifest.institutionSearchDisabled,
-                allowManualEntry = manifest.allowManualEntry
             )
         }.execute { copy(payload = it) }
     }
@@ -88,43 +94,43 @@ internal class InstitutionPickerViewModel @Inject constructor(
         onAsync(
             InstitutionPickerState::payload,
             onSuccess = { payload ->
-                eventTracker.track(PaneLoaded(Pane.INSTITUTION_PICKER))
+                eventTracker.track(PaneLoaded(PANE))
                 eventTracker.track(
                     FeaturedInstitutionsLoaded(
-                        pane = Pane.INSTITUTION_PICKER,
+                        pane = PANE,
                         duration = payload.featuredInstitutionsDuration,
-                        institutionIds = payload.featuredInstitutions.map { it.id }.toSet()
+                        institutionIds = payload.featuredInstitutions.data.map { it.id }.toSet()
                     )
                 )
             },
             onFail = {
-                eventTracker.logError(
+                handleError(
                     extraMessage = "Error fetching initial payload",
                     error = it,
-                    pane = Pane.INSTITUTION_PICKER,
-                    logger = logger
+                    pane = PANE,
+                    displayErrorScreen = true
                 )
             }
         )
         onAsync(
             InstitutionPickerState::searchInstitutions,
             onFail = {
-                eventTracker.logError(
+                handleError(
                     extraMessage = "Error searching institutions",
                     error = it,
-                    pane = Pane.INSTITUTION_PICKER,
-                    logger = logger
+                    pane = PANE,
+                    displayErrorScreen = false // don't show error screen for search errors.
                 )
             }
         )
         onAsync(
-            InstitutionPickerState::selectInstitution,
+            InstitutionPickerState::createSessionForInstitution,
             onFail = {
-                eventTracker.logError(
-                    extraMessage = "Error selecting institution institutions",
+                handleError(
+                    extraMessage = "Error selecting or creating session for institution",
                     error = it,
-                    pane = Pane.INSTITUTION_PICKER,
-                    logger = logger
+                    pane = PANE,
+                    displayErrorScreen = true
                 )
             }
         )
@@ -142,7 +148,7 @@ internal class InstitutionPickerViewModel @Inject constructor(
                 }
                 eventTracker.track(
                     SearchSucceeded(
-                        pane = Pane.INSTITUTION_PICKER,
+                        pane = PANE,
                         query = query,
                         duration = millis,
                         resultCount = result.data.count()
@@ -162,11 +168,10 @@ internal class InstitutionPickerViewModel @Inject constructor(
     }
 
     fun onInstitutionSelected(institution: FinancialConnectionsInstitution, fromFeatured: Boolean) {
-        clearSearch()
         suspend {
             eventTracker.track(
                 InstitutionSelected(
-                    pane = Pane.INSTITUTION_PICKER,
+                    pane = PANE,
                     fromFeatured = fromFeatured,
                     institutionId = institution.id
                 )
@@ -183,43 +188,44 @@ internal class InstitutionPickerViewModel @Inject constructor(
                 )
             }
             // navigate to next step
-            navigationManager.tryNavigateTo(PartnerAuth(referrer = Pane.INSTITUTION_PICKER))
-        }.execute { this }
-    }
-
-    fun onCancelSearchClick() {
-        clearSearch()
-    }
-
-    private fun clearSearch() {
-        setState {
+            val authSession = postAuthorizationSession(institution, getOrFetchSync())
+            navigateToPartnerAuth(authSession)
+        }.execute { async ->
             copy(
-                searchInstitutions = Success(
-                    InstitutionResponse(
-                        data = emptyList(),
-                        showManualEntry = false
-                    )
-                ),
-                searchMode = false
+                selectedInstitutionId = institution.id.takeIf { async is Loading },
+                createSessionForInstitution = async
             )
         }
     }
 
-    fun onSearchFocused() {
-        setState {
-            copy(searchMode = true)
-        }
+    /**
+     * Navigates to the partner auth screen:
+     *
+     * - If the [authSession] is OAuth, it will navigate to [PartnerAuthDrawer]. The pre-pane will show
+     *   as a bottom sheet, where users can accept which will open the browser with the bank OAuth login.
+     * - If the [authSession] is not-OAuth, it will navigate to [PartnerAuth] (full screen).
+     *   non-OAuth sessions don't have a pre-pane, so partner auth will show a full-screen loading
+     *   and open the browser right away.
+     */
+    private fun navigateToPartnerAuth(authSession: FinancialConnectionsAuthorizationSession) {
+        navigationManager.tryNavigateTo(
+            if (authSession.isOAuth) {
+                PartnerAuthDrawer(referrer = PANE)
+            } else {
+                PartnerAuth(referrer = PANE)
+            }
+        )
     }
 
     fun onManualEntryClick() {
-        navigationManager.tryNavigateTo(ManualEntry(referrer = Pane.INSTITUTION_PICKER))
+        navigationManager.tryNavigateTo(ManualEntry(referrer = PANE))
     }
 
     fun onScrollChanged() {
         viewModelScope.launch {
             eventTracker.track(
                 SearchScroll(
-                    pane = Pane.INSTITUTION_PICKER,
+                    pane = PANE,
                     institutionIds = awaitState().searchInstitutions()
                         ?.data
                         ?.map { it.id }
@@ -233,6 +239,7 @@ internal class InstitutionPickerViewModel @Inject constructor(
         MavericksViewModelFactory {
 
         private const val SEARCH_DEBOUNCE_MS = 300L
+        private val PANE = Pane.INSTITUTION_PICKER
         override fun create(
             viewModelContext: ViewModelContext,
             state: InstitutionPickerState
@@ -251,15 +258,14 @@ internal class InstitutionPickerViewModel @Inject constructor(
 internal data class InstitutionPickerState(
     // This is just used to provide a text in Compose previews
     val previewText: String? = null,
-    val searchMode: Boolean = false,
+    val selectedInstitutionId: String? = null,
     val payload: Async = Uninitialized,
     val searchInstitutions: Async = Uninitialized,
-    val selectInstitution: Async = Uninitialized
+    val createSessionForInstitution: Async = Uninitialized
 ) : MavericksState {
 
     data class Payload(
-        val featuredInstitutions: List,
-        val allowManualEntry: Boolean,
+        val featuredInstitutions: InstitutionResponse,
         val searchDisabled: Boolean,
         val featuredInstitutionsDuration: Long
     )
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerPreviewParameterProvider.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerPreviewParameterProvider.kt
index b9ae39071e9..72d201d2f8f 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerPreviewParameterProvider.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerPreviewParameterProvider.kt
@@ -1,13 +1,17 @@
-@file:Suppress("LongMethod")
-
 package com.stripe.android.financialconnections.features.linkaccountpicker
 
 import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import com.airbnb.mvrx.Loading
 import com.airbnb.mvrx.Success
-import com.stripe.android.financialconnections.features.common.AccessibleDataCalloutModel
+import com.stripe.android.financialconnections.features.common.MerchantDataAccessModel
 import com.stripe.android.financialconnections.model.AddNewAccount
+import com.stripe.android.financialconnections.model.Bullet
+import com.stripe.android.financialconnections.model.ConnectedAccessNotice
+import com.stripe.android.financialconnections.model.DataAccessNotice
+import com.stripe.android.financialconnections.model.DataAccessNoticeBody
 import com.stripe.android.financialconnections.model.FinancialConnectionsAccount
 import com.stripe.android.financialconnections.model.FinancialConnectionsAccount.Status
+import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
 import com.stripe.android.financialconnections.model.Image
 import com.stripe.android.financialconnections.model.NetworkedAccount
@@ -18,19 +22,39 @@ internal class LinkAccountPickerPreviewParameterProvider :
     PreviewParameterProvider {
     override val values = sequenceOf(
         canonical(),
-        accountSelected()
+        loading(),
+        accountSelected(),
+        oneAccount()
     )
 
-    override val count: Int
-        get() = super.count
-
     private fun canonical() = LinkAccountPickerState(
         payload = Success(
             LinkAccountPickerState.Payload(
                 title = display().title,
                 accounts = partnerAccountList(),
+                dataAccessNotice = dataAccessNotice(),
+                addNewAccount = requireNotNull(display().addNewAccount),
+                merchantDataAccess = accessibleCallout(),
+                consumerSessionClientSecret = "secret",
+                defaultCta = display().defaultCta,
+                nextPaneOnNewAccount = Pane.INSTITUTION_PICKER,
+                partnerToCoreAuths = emptyMap(),
+            )
+        ),
+    )
+
+    private fun loading() = LinkAccountPickerState(
+        payload = Loading(),
+    )
+
+    private fun oneAccount() = LinkAccountPickerState(
+        payload = Success(
+            LinkAccountPickerState.Payload(
+                title = display().title,
+                accounts = partnerAccountList().subList(0, 1),
+                dataAccessNotice = dataAccessNotice(),
                 addNewAccount = requireNotNull(display().addNewAccount),
-                accessibleData = accessibleCallout(),
+                merchantDataAccess = accessibleCallout(),
                 consumerSessionClientSecret = "secret",
                 defaultCta = display().defaultCta,
                 nextPaneOnNewAccount = Pane.INSTITUTION_PICKER,
@@ -45,8 +69,9 @@ internal class LinkAccountPickerPreviewParameterProvider :
             LinkAccountPickerState.Payload(
                 title = display().title,
                 accounts = partnerAccountList(),
+                dataAccessNotice = dataAccessNotice(),
                 addNewAccount = requireNotNull(display().addNewAccount),
-                accessibleData = accessibleCallout(),
+                merchantDataAccess = accessibleCallout(),
                 consumerSessionClientSecret = "secret",
                 defaultCta = display().defaultCta,
                 nextPaneOnNewAccount = Pane.INSTITUTION_PICKER,
@@ -64,6 +89,7 @@ internal class LinkAccountPickerPreviewParameterProvider :
             balanceAmount = 1000,
             status = Status.ACTIVE,
             displayableAccountNumbers = "1234",
+            institution = institution(),
             currency = "USD",
             _allowSelection = true,
             allowSelectionMessage = "",
@@ -87,6 +113,7 @@ internal class LinkAccountPickerPreviewParameterProvider :
             balanceAmount = 1000,
             status = Status.ACTIVE,
             displayableAccountNumbers = "1234",
+            institution = institution(),
             currency = "USD",
             _allowSelection = true,
             allowSelectionMessage = "",
@@ -103,6 +130,7 @@ internal class LinkAccountPickerPreviewParameterProvider :
             id = "id2",
             name = "With balance disabled",
             balanceAmount = 1000,
+            institution = institution(),
             _allowSelection = false,
             allowSelectionMessage = "Disconnected",
             subcategory = FinancialConnectionsAccount.Subcategory.SAVINGS,
@@ -117,6 +145,7 @@ internal class LinkAccountPickerPreviewParameterProvider :
             id = "id3",
             name = "No balance",
             displayableAccountNumbers = "1234",
+            institution = institution(),
             subcategory = FinancialConnectionsAccount.Subcategory.CREDIT_CARD,
             _allowSelection = true,
             allowSelectionMessage = "",
@@ -131,6 +160,7 @@ internal class LinkAccountPickerPreviewParameterProvider :
             id = "id4",
             name = "No balance disabled",
             displayableAccountNumbers = "1234",
+            institution = institution(),
             subcategory = FinancialConnectionsAccount.Subcategory.CHECKING,
             _allowSelection = false,
             allowSelectionMessage = "Disconnected",
@@ -145,6 +175,7 @@ internal class LinkAccountPickerPreviewParameterProvider :
             id = "id5",
             name = "Very long institution that is already linked",
             displayableAccountNumbers = "1234",
+            institution = institution(),
             linkedAccountId = "linkedAccountId",
             _allowSelection = true,
             subcategory = FinancialConnectionsAccount.Subcategory.CHECKING,
@@ -155,7 +186,37 @@ internal class LinkAccountPickerPreviewParameterProvider :
         ),
     )
 
-    private fun accessibleCallout() = AccessibleDataCalloutModel(
+    private fun dataAccessNotice() = DataAccessNotice(
+        icon = Image("https://www.cdn.stripe.com/12321312321.png"),
+        title = "Goldilocks uses Stripe to link your accounts",
+        subtitle = "Goldilocks will use your account and routing number, balances and transactions:",
+        body = DataAccessNoticeBody(
+            bullets = bullets()
+        ),
+        disclaimer = "Learn more about data access",
+        connectedAccountNotice = ConnectedAccessNotice(
+            subtitle = "Connected account placeholder",
+            body = DataAccessNoticeBody(
+                bullets = bullets()
+            )
+        ),
+        cta = "OK"
+    )
+
+    private fun bullets() = listOf(
+        Bullet(
+            icon = Image("https://www.cdn.stripe.com/12321312321.png"),
+            title = "Account details",
+            content = "Account number, routing number, account type, account nickname."
+        ),
+        Bullet(
+            icon = Image("https://www.cdn.stripe.com/12321312321.png"),
+            title = "Account details",
+            content = "Account number, routing number, account type, account nickname."
+        ),
+    )
+
+    private fun accessibleCallout() = MerchantDataAccessModel(
         businessName = "My business",
         permissions = listOf(
             FinancialConnectionsAccount.Permissions.PAYMENT_METHOD,
@@ -164,8 +225,6 @@ internal class LinkAccountPickerPreviewParameterProvider :
             FinancialConnectionsAccount.Permissions.TRANSACTIONS
         ),
         isStripeDirect = true,
-        isNetworking = true,
-        dataPolicyUrl = ""
     )
 
     fun display() = ReturningNetworkingUserAccountPicker(
@@ -179,4 +238,14 @@ internal class LinkAccountPickerPreviewParameterProvider :
             ),
         )
     )
+
+    fun institution() = FinancialConnectionsInstitution(
+        name = "Bank of America",
+        featured = true,
+        mobileHandoffCapable = true,
+        id = "in_123",
+        icon = Image(
+            default = "https://b.stripecdn.com/connections-statics-srv/assets/InstitutionIcons/bankofamerica.png"
+        ),
+    )
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerScreen.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerScreen.kt
index 639b04d66be..a0fbada5b06 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerScreen.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerScreen.kt
@@ -4,32 +4,39 @@ package com.stripe.android.financialconnections.features.linkaccountpicker
 
 import androidx.activity.compose.BackHandler
 import androidx.annotation.RestrictTo
-import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.Image
-import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.background
 import androidx.compose.foundation.border
+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.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.ModalBottomSheetLayout
+import androidx.compose.material.ModalBottomSheetState
+import androidx.compose.material.ModalBottomSheetValue.Hidden
 import androidx.compose.material.Text
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.rememberModalBottomSheetState
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -42,13 +49,15 @@ import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.compose.collectAsState
 import com.airbnb.mvrx.compose.mavericksViewModel
 import com.stripe.android.financialconnections.R
-import com.stripe.android.financialconnections.features.common.AccessibleDataCallout
 import com.stripe.android.financialconnections.features.common.AccountItem
-import com.stripe.android.financialconnections.features.common.InstitutionPlaceholder
-import com.stripe.android.financialconnections.features.common.LoadingContent
-import com.stripe.android.financialconnections.features.common.PaneFooter
+import com.stripe.android.financialconnections.features.common.DataAccessBottomSheetContent
+import com.stripe.android.financialconnections.features.common.LoadingShimmerEffect
+import com.stripe.android.financialconnections.features.common.MerchantDataAccessText
 import com.stripe.android.financialconnections.features.common.UnclassifiedErrorContent
+import com.stripe.android.financialconnections.features.linkaccountpicker.LinkAccountPickerClickableText.DATA
 import com.stripe.android.financialconnections.features.linkaccountpicker.LinkAccountPickerState.Payload
+import com.stripe.android.financialconnections.features.linkaccountpicker.LinkAccountPickerState.ViewEffect.OpenBottomSheet
+import com.stripe.android.financialconnections.features.linkaccountpicker.LinkAccountPickerState.ViewEffect.OpenUrl
 import com.stripe.android.financialconnections.model.AddNewAccount
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
 import com.stripe.android.financialconnections.model.NetworkedAccount
@@ -63,20 +72,55 @@ import com.stripe.android.financialconnections.ui.components.FinancialConnection
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsTopAppBar
 import com.stripe.android.financialconnections.ui.components.clickableSingle
 import com.stripe.android.financialconnections.ui.components.elevation
-import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
+import com.stripe.android.financialconnections.ui.theme.LazyLayout
+import com.stripe.android.financialconnections.ui.theme.Neutral900
 import com.stripe.android.uicore.image.StripeImage
+import kotlinx.coroutines.launch
 
+/*
+  The returning user account picker contains a lot of logic handling what happens after a user selects an account.
+  Accounts might require step-up verification, repair, or relinking to grant additional permissions (supportability).
+  - For flows where users are only allowed to select one account, the next pane to display is just whatever is set as
+    the `next_pane_on_selection` from the API
+  - For flows where users may select multiple accounts, we use the following logic:
+      - If a selected account requires repair, we immediately pop up a drawer to initiate the repair flow
+      - If a selected account requires additional permissions to be shared (supportability),
+        we immediately pop up a drawer to initiate partner auth
+      - If a selected account requires step-up verification, we assume that all accounts require step-up verification
+        and push the step-up verification pane after they click the CTA
+      - If a selected account does not require any further action, we continue to the success pane
+*/
 @Composable
 internal fun LinkAccountPickerScreen() {
     val viewModel: LinkAccountPickerViewModel = mavericksViewModel()
     val parentViewModel = parentViewModel()
     val state = viewModel.collectAsState()
     BackHandler(enabled = true) {}
+
+    val bottomSheetState = rememberModalBottomSheetState(
+        initialValue = Hidden,
+        skipHalfExpanded = true
+    )
+    val uriHandler = LocalUriHandler.current
+
+    state.value.viewEffect?.let { viewEffect ->
+        LaunchedEffect(viewEffect) {
+            when (viewEffect) {
+                is OpenUrl -> uriHandler.openUri(viewEffect.url)
+                is OpenBottomSheet -> bottomSheetState.show()
+            }
+            viewModel.onViewEffectLaunched()
+        }
+    }
+
     LinkAccountPickerContent(
         state = state.value,
+        bottomSheetState = bottomSheetState,
         onCloseClick = { parentViewModel.onCloseWithConfirmationClick(Pane.LINK_ACCOUNT_PICKER) },
         onCloseFromErrorClick = parentViewModel::onCloseFromErrorClick,
-        onLearnMoreAboutDataAccessClick = viewModel::onLearnMoreAboutDataAccessClick,
+        onClickableTextClick = viewModel::onClickableTextClick,
         onNewBankAccountClick = viewModel::onNewBankAccountClick,
         onSelectAccountClick = viewModel::onSelectAccountClick,
         onAccountClick = viewModel::onAccountClick
@@ -86,32 +130,76 @@ internal fun LinkAccountPickerScreen() {
 @Composable
 private fun LinkAccountPickerContent(
     state: LinkAccountPickerState,
+    bottomSheetState: ModalBottomSheetState,
     onCloseClick: () -> Unit,
     onCloseFromErrorClick: (Throwable) -> Unit,
-    onLearnMoreAboutDataAccessClick: () -> Unit,
+    onClickableTextClick: (String) -> Unit,
     onNewBankAccountClick: () -> Unit,
     onSelectAccountClick: () -> Unit,
     onAccountClick: (PartnerAccount) -> Unit
 ) {
-    val scrollState = rememberScrollState()
+    val scrollState = rememberLazyListState()
+    val scope = rememberCoroutineScope()
+    ModalBottomSheetLayout(
+        sheetState = bottomSheetState,
+        sheetBackgroundColor = colors.backgroundSurface,
+        sheetShape = RoundedCornerShape(8.dp),
+        scrimColor = Neutral900.copy(alpha = 0.32f),
+        sheetContent = {
+            when (val dataAccessNotice = state.payload()?.dataAccessNotice) {
+                null -> Unit
+                else -> DataAccessBottomSheetContent(
+                    dataDialog = dataAccessNotice,
+                    onConfirmModalClick = { scope.launch { bottomSheetState.hide() } },
+                    onClickableTextClick = onClickableTextClick
+                )
+            }
+        },
+        content = {
+            LinkAccountPickerMainContent(
+                scrollState = scrollState,
+                onCloseClick = onCloseClick,
+                state = state,
+                onClickableTextClick = onClickableTextClick,
+                onSelectAccountClick = onSelectAccountClick,
+                onNewBankAccountClick = onNewBankAccountClick,
+                onAccountClick = onAccountClick,
+                onCloseFromErrorClick = onCloseFromErrorClick
+            )
+        },
+    )
+}
+
+@Composable
+private fun LinkAccountPickerMainContent(
+    scrollState: LazyListState,
+    onCloseClick: () -> Unit,
+    state: LinkAccountPickerState,
+    onClickableTextClick: (String) -> Unit,
+    onSelectAccountClick: () -> Unit,
+    onNewBankAccountClick: () -> Unit,
+    onAccountClick: (PartnerAccount) -> Unit,
+    onCloseFromErrorClick: (Throwable) -> Unit
+) {
     FinancialConnectionsScaffold(
         topBar = {
             FinancialConnectionsTopAppBar(
-                showBack = false,
+                allowBackNavigation = false,
                 elevation = scrollState.elevation,
                 onCloseClick = onCloseClick
             )
         }
     ) {
         when (val payload = state.payload) {
-            Uninitialized, is Loading -> LinkAccountPickerLoading()
+            Uninitialized,
+            is Loading,
             is Success -> LinkAccountPickerLoaded(
                 scrollState = scrollState,
-                payload = payload(),
+                payload = payload,
                 cta = state.cta,
                 selectedAccountId = state.selectedAccountId,
                 selectNetworkedAccountAsync = state.selectNetworkedAccountAsync,
-                onLearnMoreAboutDataAccessClick = onLearnMoreAboutDataAccessClick,
+                onClickableTextClick = onClickableTextClick,
                 onSelectAccountClick = onSelectAccountClick,
                 onNewBankAccountClick = onNewBankAccountClick,
                 onAccountClick = onAccountClick
@@ -125,72 +213,107 @@ private fun LinkAccountPickerContent(
     }
 }
 
-@Composable
-private fun LinkAccountPickerLoading() {
-    LoadingContent(
-        title = stringResource(R.string.stripe_account_picker_loading_title),
-        content = stringResource(R.string.stripe_account_picker_loading_desc)
-    )
-}
-
 @Composable
 private fun LinkAccountPickerLoaded(
+    scrollState: LazyListState,
+    payload: Async,
     selectedAccountId: String?,
     selectNetworkedAccountAsync: Async,
-    payload: Payload,
-    onLearnMoreAboutDataAccessClick: () -> Unit,
-    onSelectAccountClick: () -> Unit,
-    onNewBankAccountClick: () -> Unit,
     onAccountClick: (PartnerAccount) -> Unit,
-    scrollState: ScrollState,
+    onNewBankAccountClick: () -> Unit,
+    onClickableTextClick: (String) -> Unit,
+    onSelectAccountClick: () -> Unit,
     cta: String?
 ) {
-    Column(
-        Modifier
-            .fillMaxSize()
-    ) {
-        Column(
-            modifier = Modifier
-                .verticalScroll(scrollState)
-                .padding(horizontal = 24.dp)
-                .weight(1f)
-        ) {
-            Spacer(modifier = Modifier.size(16.dp))
-            Title(payload.title)
-            Spacer(modifier = Modifier.size(24.dp))
-            payload.accounts.forEach { account ->
-                NetworkedAccountItem(
-                    selected = account.first.id == selectedAccountId,
-                    account = account,
-                    onAccountClicked = { selected ->
-                        if (selectNetworkedAccountAsync !is Loading) onAccountClick(selected)
-                    }
+    LazyLayout(
+        verticalArrangement = Arrangement.spacedBy(16.dp),
+        lazyListState = scrollState,
+        body = {
+            payload()?.let {
+                loadedContent(
+                    payload = it,
+                    selectedAccountId = selectedAccountId,
+                    selectNetworkedAccountAsync = selectNetworkedAccountAsync,
+                    onAccountClick = onAccountClick,
+                    onNewBankAccountClick = onNewBankAccountClick
                 )
-                Spacer(modifier = Modifier.height(12.dp))
-            }
-            SelectNewAccount(
-                text = payload.addNewAccount,
-                onClick = {
-                    if (selectNetworkedAccountAsync !is Loading) onNewBankAccountClick()
+            } ?: loadingContent()
+        },
+        footer = {
+            payload()?.let {
+                Column {
+                    MerchantDataAccessText(
+                        model = it.merchantDataAccess,
+                        onLearnMoreClick = { onClickableTextClick(DATA.value) }
+                    )
+                    Spacer(modifier = Modifier.size(12.dp))
+                    FinancialConnectionsButton(
+                        enabled = selectedAccountId != null,
+                        loading = selectNetworkedAccountAsync is Loading,
+                        onClick = onSelectAccountClick,
+                        modifier = Modifier
+                            .fillMaxWidth()
+                    ) {
+                        Text(text = cta ?: stringResource(R.string.stripe_link_account_picker_cta))
+                    }
                 }
-            )
-            Spacer(modifier = Modifier.size(16.dp))
+            }
         }
-        PaneFooter(elevation = scrollState.elevation) {
-            AccessibleDataCallout(
-                payload.accessibleData,
-                onLearnMoreAboutDataAccessClick
-            )
-            Spacer(modifier = Modifier.size(12.dp))
-            FinancialConnectionsButton(
-                enabled = selectedAccountId != null,
-                loading = selectNetworkedAccountAsync is Loading,
-                onClick = onSelectAccountClick,
+    )
+}
+
+private fun LazyListScope.loadedContent(
+    payload: Payload,
+    selectedAccountId: String?,
+    selectNetworkedAccountAsync: Async,
+    onAccountClick: (PartnerAccount) -> Unit,
+    onNewBankAccountClick: () -> Unit
+) {
+    item {
+        AnnotatedText(
+            text = TextResource.Text(payload.title),
+            defaultStyle = typography.headingXLarge,
+            onClickableTextClick = {}
+        )
+        Spacer(modifier = Modifier.size(8.dp))
+    }
+    items(payload.accounts) {
+        NetworkedAccountItem(
+            selected = it.first.id == selectedAccountId,
+            account = it,
+            onAccountClicked = { selected ->
+                if (selectNetworkedAccountAsync !is Loading) onAccountClick(selected)
+            }
+        )
+    }
+    item {
+        SelectNewAccount(
+            text = payload.addNewAccount,
+            onClick = {
+                if (selectNetworkedAccountAsync !is Loading) onNewBankAccountClick()
+            }
+        )
+    }
+}
+
+private fun LazyListScope.loadingContent() {
+    item {
+        Text(
+            modifier = Modifier.fillMaxWidth(),
+            text = "Retrieving accounts",
+            style = typography.headingXLarge
+        )
+        Spacer(modifier = Modifier.size(8.dp))
+    }
+    items(3) {
+        LoadingShimmerEffect {
+            Box(
                 modifier = Modifier
                     .fillMaxWidth()
-            ) {
-                Text(text = cta ?: stringResource(R.string.stripe_link_account_picker_cta))
-            }
+                    .height(88.dp)
+                    .clip(RoundedCornerShape(12.dp))
+                    .background(it)
+            )
         }
     }
 }
@@ -207,23 +330,7 @@ private fun NetworkedAccountItem(
         onAccountClicked = onAccountClicked,
         account = partnerAccount,
         networkedAccount = networkedAccount
-    ) {
-        val modifier = Modifier
-            .size(24.dp)
-            .clip(RoundedCornerShape(3.dp))
-        val institutionIcon = partnerAccount.institution?.icon?.default
-        when {
-            institutionIcon.isNullOrEmpty() -> InstitutionPlaceholder(modifier)
-            else -> StripeImage(
-                url = institutionIcon,
-                imageLoader = LocalImageLoader.current,
-                contentDescription = null,
-                modifier = modifier,
-                contentScale = ContentScale.Crop,
-                errorContent = { InstitutionPlaceholder(modifier) }
-            )
-        }
-    }
+    )
 }
 
 @Composable
@@ -231,18 +338,18 @@ private fun SelectNewAccount(
     onClick: () -> Unit,
     text: AddNewAccount
 ) {
-    val shape = remember { RoundedCornerShape(8.dp) }
+    val shape = remember { RoundedCornerShape(16.dp) }
     Box(
         modifier = Modifier
             .fillMaxWidth()
             .clip(shape)
             .border(
                 width = 1.dp,
-                color = FinancialConnectionsTheme.colors.borderDefault,
+                color = colors.border,
                 shape = shape
             )
-            .clickableSingle { onClick() }
             .padding(16.dp)
+            .clickableSingle { onClick() }
     ) {
         Row(
             verticalAlignment = Alignment.CenterVertically
@@ -254,8 +361,8 @@ private fun SelectNewAccount(
             Spacer(modifier = Modifier.size(16.dp))
             Text(
                 text = text.body,
-                style = FinancialConnectionsTheme.typography.body,
-                color = FinancialConnectionsTheme.colors.textBrand
+                style = typography.labelLargeEmphasized,
+                color = colors.textDefault
             )
         }
     }
@@ -266,60 +373,51 @@ fun SelectNewAccountIcon(
     icon: String?,
     contentDescription: String,
 ) {
-    Box {
-        val brandColor = FinancialConnectionsTheme.colors.textBrand
-        val modifier = Modifier
-            .size(12.dp)
-            .align(Alignment.Center)
+    Box(
+        contentAlignment = Alignment.Center,
+        modifier = Modifier
+            .size(56.dp)
+            .clip(RoundedCornerShape(12.dp))
+            .background(colors.backgroundOffset)
+    ) {
+        val iconModifier = Modifier.size(20.dp)
         val placeholderImage = @Composable {
             Image(
-                modifier = modifier,
-                imageVector = Icons.Filled.Add,
-                contentScale = ContentScale.FillBounds,
-                colorFilter = ColorFilter.tint(brandColor),
+                painter = painterResource(R.drawable.stripe_ic_add),
+                modifier = iconModifier,
+                colorFilter = ColorFilter.tint(colors.textBrand),
                 contentDescription = contentDescription
             )
         }
-        Canvas(modifier = Modifier.size(24.dp)) {
-            drawCircle(color = brandColor.copy(alpha = 0.1f))
-        }
         when {
-            icon.isNullOrEmpty() -> placeholderImage()
+            LocalInspectionMode.current ||
+                icon.isNullOrEmpty() -> placeholderImage()
+
             else -> StripeImage(
                 url = icon,
                 imageLoader = LocalImageLoader.current,
                 contentDescription = null,
-                modifier = modifier.padding(),
+                modifier = iconModifier,
                 errorContent = { placeholderImage() }
             )
         }
     }
 }
 
-@Composable
-private fun Title(
-    title: String
-) {
-    AnnotatedText(
-        text = TextResource.Text(title),
-        defaultStyle = FinancialConnectionsTheme.typography.subtitle,
-        annotationStyles = emptyMap(),
-        onClickableTextClick = {}
-    )
-}
-
 @Composable
 @Preview(group = "LinkAccountPicker Pane")
 internal fun LinkAccountPickerScreenPreview(
     @PreviewParameter(LinkAccountPickerPreviewParameterProvider::class)
     state: LinkAccountPickerState
 ) {
+    val bottomSheetState = rememberModalBottomSheetState(initialValue = Hidden)
     FinancialConnectionsPreview {
         LinkAccountPickerContent(
             state = state,
+            bottomSheetState = bottomSheetState,
             onCloseClick = {},
             onCloseFromErrorClick = {},
-            onLearnMoreAboutDataAccessClick = {},
+            onClickableTextClick = {},
             onNewBankAccountClick = {},
             onSelectAccountClick = {},
             onAccountClick = {}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerViewModel.kt
index a9f2f3336d4..caa588bb98d 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerViewModel.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerViewModel.kt
@@ -16,13 +16,16 @@ import com.stripe.android.financialconnections.analytics.FinancialConnectionsEve
 import com.stripe.android.financialconnections.analytics.logError
 import com.stripe.android.financialconnections.domain.FetchNetworkedAccounts
 import com.stripe.android.financialconnections.domain.GetCachedConsumerSession
-import com.stripe.android.financialconnections.domain.GetManifest
+import com.stripe.android.financialconnections.domain.GetOrFetchSync
 import com.stripe.android.financialconnections.domain.SelectNetworkedAccount
 import com.stripe.android.financialconnections.domain.UpdateCachedAccounts
 import com.stripe.android.financialconnections.domain.UpdateLocalManifest
-import com.stripe.android.financialconnections.features.common.AccessibleDataCalloutModel
-import com.stripe.android.financialconnections.features.consent.FinancialConnectionsUrlResolver
+import com.stripe.android.financialconnections.features.common.MerchantDataAccessModel
+import com.stripe.android.financialconnections.features.linkaccountpicker.LinkAccountPickerClickableText.DATA
+import com.stripe.android.financialconnections.features.linkaccountpicker.LinkAccountPickerState.ViewEffect.OpenBottomSheet
+import com.stripe.android.financialconnections.features.linkaccountpicker.LinkAccountPickerState.ViewEffect.OpenUrl
 import com.stripe.android.financialconnections.model.AddNewAccount
+import com.stripe.android.financialconnections.model.DataAccessNotice
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
 import com.stripe.android.financialconnections.model.NetworkedAccount
 import com.stripe.android.financialconnections.model.PartnerAccount
@@ -31,19 +34,22 @@ import com.stripe.android.financialconnections.navigation.NavigationManager
 import com.stripe.android.financialconnections.navigation.destination
 import com.stripe.android.financialconnections.repository.CoreAuthorizationPendingNetworkingRepairRepository
 import com.stripe.android.financialconnections.ui.FinancialConnectionsSheetNativeActivity
+import com.stripe.android.financialconnections.ui.HandleClickableUrl
 import kotlinx.coroutines.launch
+import java.util.Date
 import javax.inject.Inject
 
 internal class LinkAccountPickerViewModel @Inject constructor(
     initialState: LinkAccountPickerState,
     private val eventTracker: FinancialConnectionsAnalyticsTracker,
     private val getCachedConsumerSession: GetCachedConsumerSession,
+    private val handleClickableUrl: HandleClickableUrl,
     private val fetchNetworkedAccounts: FetchNetworkedAccounts,
     private val selectNetworkedAccount: SelectNetworkedAccount,
     private val updateLocalManifest: UpdateLocalManifest,
     private val updateCachedAccounts: UpdateCachedAccounts,
     private val coreAuthorizationPendingNetworkingRepair: CoreAuthorizationPendingNetworkingRepairRepository,
-    private val getManifest: GetManifest,
+    private val getSync: GetOrFetchSync,
     private val navigationManager: NavigationManager,
     private val logger: Logger
 ) : MavericksViewModel(initialState) {
@@ -51,13 +57,13 @@ internal class LinkAccountPickerViewModel @Inject constructor(
     init {
         observeAsyncs()
         suspend {
-            val manifest = getManifest()
-            val accessibleData = AccessibleDataCalloutModel(
+            val sync = getSync()
+            val manifest = sync.manifest
+            val dataAccessNotice = sync.text?.consent?.dataAccessNotice
+            val merchantDataAccess = MerchantDataAccessModel(
                 businessName = manifest.businessName,
                 permissions = manifest.permissions,
-                isNetworking = true,
-                isStripeDirect = manifest.isStripeDirect ?: false,
-                dataPolicyUrl = FinancialConnectionsUrlResolver.getDataPolicyUrl(manifest)
+                isStripeDirect = manifest.isStripeDirect ?: false
             )
             val consumerSession = requireNotNull(getCachedConsumerSession())
             val accountsResponse = fetchNetworkedAccounts(consumerSession.clientSecret)
@@ -75,6 +81,7 @@ internal class LinkAccountPickerViewModel @Inject constructor(
 
             eventTracker.track(PaneLoaded(PANE))
             LinkAccountPickerState.Payload(
+                dataAccessNotice = dataAccessNotice,
                 partnerToCoreAuths = accountsResponse.partnerToCoreAuths,
                 accounts = accounts,
                 nextPaneOnNewAccount = accountsResponse.nextPaneOnAddAccount,
@@ -83,7 +90,7 @@ internal class LinkAccountPickerViewModel @Inject constructor(
                 defaultCta = display.defaultCta,
                 consumerSessionClientSecret = consumerSession.clientSecret,
                 // We always want to refer to Link rather than Stripe on Link panes.
-                accessibleData = accessibleData.copy(isStripeDirect = false)
+                merchantDataAccess = merchantDataAccess.copy(isStripeDirect = false)
             )
         }.execute { copy(payload = it) }
     }
@@ -114,9 +121,21 @@ internal class LinkAccountPickerViewModel @Inject constructor(
         )
     }
 
-    fun onLearnMoreAboutDataAccessClick() {
-        // navigation to learn more about data access happens within the view component.
-        viewModelScope.launch { eventTracker.track(ClickLearnMoreDataAccess(PANE)) }
+    fun onClickableTextClick(uri: String) = viewModelScope.launch {
+        val date = Date()
+        handleClickableUrl(
+            currentPane = PANE,
+            uri = uri,
+            onNetworkUrlClicked = {
+                setState { copy(viewEffect = OpenUrl(uri, date.time)) }
+            },
+            knownDeeplinkActions = mapOf(
+                DATA.value to {
+                    eventTracker.track(ClickLearnMoreDataAccess(PANE))
+                    setState { copy(viewEffect = OpenBottomSheet(date.time)) }
+                }
+            )
+        )
     }
 
     fun onNewBankAccountClick() = viewModelScope.launch {
@@ -169,6 +188,10 @@ internal class LinkAccountPickerViewModel @Inject constructor(
         setState { copy(selectedAccountId = partnerAccount.id) }
     }
 
+    fun onViewEffectLaunched() {
+        setState { copy(viewEffect = null) }
+    }
+
     companion object :
         MavericksViewModelFactory {
 
@@ -193,13 +216,15 @@ internal data class LinkAccountPickerState(
     val payload: Async = Uninitialized,
     val selectNetworkedAccountAsync: Async = Uninitialized,
     val selectedAccountId: String? = null,
+    val viewEffect: ViewEffect? = null
 ) : MavericksState {
 
     data class Payload(
         val title: String,
         val accounts: List>,
+        val dataAccessNotice: DataAccessNotice?,
         val addNewAccount: AddNewAccount,
-        val accessibleData: AccessibleDataCalloutModel,
+        val merchantDataAccess: MerchantDataAccessModel,
         val consumerSessionClientSecret: String,
         val defaultCta: String,
         val nextPaneOnNewAccount: Pane?,
@@ -212,4 +237,19 @@ internal data class LinkAccountPickerState(
                 ?.second?.selectionCta
                 ?: payload.defaultCta
         }
+
+    sealed class ViewEffect {
+        data class OpenUrl(
+            val url: String,
+            val id: Long
+        ) : ViewEffect()
+
+        data class OpenBottomSheet(
+            val id: Long
+        ) : ViewEffect()
+    }
+}
+
+internal enum class LinkAccountPickerClickableText(val value: String) {
+    DATA("stripe://data-access-notice"),
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkstepupverification/LinkStepUpVerificationPreviewParameterProvider.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkstepupverification/LinkStepUpVerificationPreviewParameterProvider.kt
new file mode 100644
index 00000000000..fb2bb75c8d3
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkstepupverification/LinkStepUpVerificationPreviewParameterProvider.kt
@@ -0,0 +1,65 @@
+package com.stripe.android.financialconnections.features.linkstepupverification
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.Uninitialized
+import com.stripe.android.financialconnections.domain.ConfirmVerification
+import com.stripe.android.uicore.elements.IdentifierSpec
+import com.stripe.android.uicore.elements.OTPController
+import com.stripe.android.uicore.elements.OTPElement
+
+internal class LinkStepUpVerificationPreviewParameterProvider :
+    PreviewParameterProvider {
+    override val values = sequenceOf(
+        loading(),
+        canonical(),
+        submitting(),
+        otpError(),
+        randomError()
+    )
+
+    private fun canonical() = LinkStepUpVerificationState(
+        payload = payload(),
+        confirmVerification = Uninitialized
+    )
+
+    private fun submitting() = LinkStepUpVerificationState(
+        payload = payload(),
+        confirmVerification = Loading()
+    )
+
+    private fun otpError() = LinkStepUpVerificationState(
+        payload = payload(),
+        confirmVerification = Fail(
+            ConfirmVerification.OTPError(
+                "12345678",
+                ConfirmVerification.OTPError.Type.EMAIL_CODE_EXPIRED
+            )
+        )
+    )
+
+    private fun randomError() = LinkStepUpVerificationState(
+        payload = payload(),
+        confirmVerification = Fail(
+            Exception("Random error")
+        )
+    )
+
+    private fun payload() = Success(
+        LinkStepUpVerificationState.Payload(
+            email = "theLargestEmailYoulleverseeThatCouldBreakALayout@email.com",
+            phoneNumber = "12345678",
+            otpElement = OTPElement(
+                IdentifierSpec.Generic("otp"),
+                OTPController()
+            ),
+            consumerSessionClientSecret = "12345678"
+        )
+    )
+
+    private fun loading() = LinkStepUpVerificationState(
+        payload = Loading(),
+    )
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkstepupverification/LinkStepUpVerificationScreen.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkstepupverification/LinkStepUpVerificationScreen.kt
index 2ebd870d63b..71ea8ffa84c 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkstepupverification/LinkStepUpVerificationScreen.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkstepupverification/LinkStepUpVerificationScreen.kt
@@ -4,17 +4,14 @@ package com.stripe.android.financialconnections.features.linkstepupverification
 
 import androidx.activity.compose.BackHandler
 import androidx.annotation.RestrictTo
-import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.remember
@@ -24,10 +21,9 @@ import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.LocalTextInputService
 import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
 import androidx.compose.ui.unit.dp
-import com.airbnb.mvrx.Async
 import com.airbnb.mvrx.Fail
 import com.airbnb.mvrx.Loading
 import com.airbnb.mvrx.Success
@@ -35,7 +31,9 @@ import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.compose.collectAsState
 import com.airbnb.mvrx.compose.mavericksViewModel
 import com.stripe.android.financialconnections.R
+import com.stripe.android.financialconnections.domain.ConfirmVerification.OTPError
 import com.stripe.android.financialconnections.features.common.FullScreenGenericLoading
+import com.stripe.android.financialconnections.features.common.LoadingSpinner
 import com.stripe.android.financialconnections.features.common.UnclassifiedErrorContent
 import com.stripe.android.financialconnections.features.common.VerificationSection
 import com.stripe.android.financialconnections.features.linkstepupverification.LinkStepUpVerificationState.Payload
@@ -47,11 +45,10 @@ import com.stripe.android.financialconnections.ui.components.AnnotatedText
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsScaffold
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsTopAppBar
 import com.stripe.android.financialconnections.ui.components.StringAnnotation
-import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
+import com.stripe.android.financialconnections.ui.components.elevation
 import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
-import com.stripe.android.uicore.elements.IdentifierSpec
-import com.stripe.android.uicore.elements.OTPController
-import com.stripe.android.uicore.elements.OTPElement
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
+import com.stripe.android.financialconnections.ui.theme.LazyLayout
 
 @Composable
 internal fun LinkStepUpVerificationScreen() {
@@ -61,7 +58,7 @@ internal fun LinkStepUpVerificationScreen() {
     BackHandler(enabled = true) {}
     LinkStepUpVerificationContent(
         state = state.value,
-        onCloseClick = { parentViewModel.onCloseWithConfirmationClick(Pane.NETWORKING_LINK_SIGNUP_PANE) },
+        onCloseClick = { parentViewModel.onCloseWithConfirmationClick(Pane.LINK_STEP_UP_VERIFICATION) },
         onCloseFromErrorClick = parentViewModel::onCloseFromErrorClick,
         onClickableTextClick = viewModel::onClickableTextClick
     )
@@ -74,11 +71,12 @@ private fun LinkStepUpVerificationContent(
     onCloseFromErrorClick: (Throwable) -> Unit,
     onClickableTextClick: (String) -> Unit
 ) {
-    val scrollState = rememberScrollState()
+    val lazyListState = rememberLazyListState()
     FinancialConnectionsScaffold(
         topBar = {
             FinancialConnectionsTopAppBar(
-                showBack = false,
+                allowBackNavigation = false,
+                elevation = lazyListState.elevation,
                 onCloseClick = onCloseClick
             )
         }
@@ -90,10 +88,11 @@ private fun LinkStepUpVerificationContent(
                 onCloseFromErrorClick = onCloseFromErrorClick
             )
             is Success -> LinkStepUpVerificationLoaded(
-                scrollState = scrollState,
+                lazyListState = lazyListState,
+                state.submitError,
+                state.submitLoading,
                 payload = payload(),
-                confirmVerificationAsync = state.confirmVerification,
-                resendOtpAsync = state.resendOtp,
+                onCloseFromErrorClick = onCloseFromErrorClick,
                 onClickableTextClick = onClickableTextClick
             )
         }
@@ -102,9 +101,10 @@ private fun LinkStepUpVerificationContent(
 
 @Composable
 private fun LinkStepUpVerificationLoaded(
-    confirmVerificationAsync: Async,
-    resendOtpAsync: Async,
-    scrollState: ScrollState,
+    lazyListState: LazyListState,
+    submitError: Throwable?,
+    submitLoading: Boolean,
+    onCloseFromErrorClick: (Throwable) -> Unit,
     payload: Payload,
     onClickableTextClick: (String) -> Unit
 ) {
@@ -112,162 +112,99 @@ private fun LinkStepUpVerificationLoaded(
     val focusRequester: FocusRequester = remember { FocusRequester() }
     LaunchedEffect(Unit) { focusRequester.requestFocus() }
     val textInputService = LocalTextInputService.current
-    LaunchedEffect(confirmVerificationAsync) {
-        if (confirmVerificationAsync is Loading) {
+    LaunchedEffect(submitLoading) {
+        if (submitLoading) {
             focusManager.clearFocus(true)
             @Suppress("DEPRECATION")
             textInputService?.hideSoftwareKeyboard()
         }
     }
+    if (submitError != null && submitError !is OTPError) {
+        UnclassifiedErrorContent(
+            error = submitError,
+            onCloseFromErrorClick = onCloseFromErrorClick
+        )
+    } else {
+        LazyLayout(
+            verticalArrangement = Arrangement.spacedBy(24.dp),
+            lazyListState = lazyListState,
+            body = {
+                item {
+                    HeaderSection(payload.email)
+                }
+                item {
+                    VerificationSection(
+                        focusRequester = focusRequester,
+                        otpElement = payload.otpElement,
+                        enabled = !submitLoading,
+                        confirmVerificationError = submitError
+                    )
+                }
+                item {
+                    ResendCodeSection(
+                        isLoading = submitLoading,
+                        onClickableTextClick = onClickableTextClick
+                    )
+                }
+            }
+        )
+    }
+}
+
+@Composable
+private fun HeaderSection(
+    email: String,
+) {
     Column(
-        modifier = Modifier
-            .fillMaxSize()
-            .verticalScroll(scrollState)
-            .padding(
-                top = 0.dp,
-                start = 24.dp,
-                end = 24.dp,
-                bottom = 24.dp
-            )
+        verticalArrangement = Arrangement.spacedBy(16.dp)
     ) {
-        Spacer(modifier = Modifier.size(16.dp))
-        Title()
-        Spacer(modifier = Modifier.size(8.dp))
-        Description(email = payload.email)
-        Spacer(modifier = Modifier.size(24.dp))
-        VerificationSection(
-            focusRequester = focusRequester,
-            otpElement = payload.otpElement,
-            enabled = confirmVerificationAsync !is Loading,
-            confirmVerificationError = (confirmVerificationAsync as? Fail)?.error
+        Text(
+            text = stringResource(R.string.stripe_link_stepup_verification_title),
+            style = typography.headingXLarge,
         )
-        Spacer(modifier = Modifier.size(24.dp))
-        EmailSubtext(
-            email = payload.email,
-            isLoading = resendOtpAsync is Loading,
-            onClickableTextClick = onClickableTextClick
+        Text(
+            text = stringResource(id = R.string.stripe_link_stepup_verification_desc, email),
+            style = typography.bodyMedium,
         )
     }
 }
 
 @Composable
-private fun EmailSubtext(
-    email: String,
+private fun ResendCodeSection(
     isLoading: Boolean,
     onClickableTextClick: (String) -> Unit
 ) {
     Row(
-        horizontalArrangement = Arrangement.spacedBy(8.dp),
+        modifier = Modifier.fillMaxWidth(),
+        horizontalArrangement = Arrangement.Center,
         verticalAlignment = Alignment.CenterVertically
     ) {
-        listOf(
-            TextResource.Text(email) to 1f,
-            TextResource.Text("•") to null,
-            TextResource.StringId(R.string.stripe_link_stepup_verification_resend_code) to null
-        ).forEach { (text, weight) ->
+        if (isLoading) {
+            LoadingSpinner(modifier = Modifier.size(24.dp),)
+        } else {
             AnnotatedText(
-                modifier = if (weight != null) Modifier.weight(weight, fill = false) else Modifier,
-                text = text,
+                text = TextResource.StringId(R.string.stripe_link_stepup_verification_resend_code),
                 maxLines = 1,
-                overflow = TextOverflow.Ellipsis,
-                defaultStyle = FinancialConnectionsTheme.typography.caption.copy(
-                    color = colors.textSecondary,
-                ),
+                defaultStyle = typography.labelMedium,
                 annotationStyles = mapOf(
-                    StringAnnotation.CLICKABLE to FinancialConnectionsTheme.typography.captionEmphasized
+                    StringAnnotation.CLICKABLE to typography.labelMediumEmphasized
                         .toSpanStyle()
-                        .copy(color = if (isLoading) colors.textSecondary else colors.textBrand),
+                        .copy(color = colors.textBrand),
                 ),
                 onClickableTextClick = onClickableTextClick,
             )
         }
-        if (isLoading) {
-            CircularProgressIndicator(
-                Modifier.size(12.dp),
-                strokeWidth = 1.dp,
-                color = colors.textSecondary
-            )
-        }
-    }
-}
-
-@Composable
-private fun Description(email: String) {
-    AnnotatedText(
-        text = TextResource.Text(
-            stringResource(
-                R.string.stripe_link_stepup_verification_desc,
-                email
-            )
-        ),
-        defaultStyle = FinancialConnectionsTheme.typography.body.copy(
-            color = colors.textSecondary
-        ),
-        annotationStyles = mapOf(
-            StringAnnotation.BOLD to FinancialConnectionsTheme.typography.bodyEmphasized
-                .toSpanStyle()
-                .copy(color = colors.textSecondary),
-        ),
-        onClickableTextClick = {},
-    )
-}
-
-@Composable
-private fun Title() {
-    AnnotatedText(
-        text = TextResource.Text(
-            stringResource(R.string.stripe_link_stepup_verification_title)
-        ),
-        defaultStyle = FinancialConnectionsTheme.typography.subtitle,
-        annotationStyles = emptyMap(),
-        onClickableTextClick = {},
-    )
-}
-
-@Composable
-@Preview(group = "LinkStepUpVerification Pane", name = "Canonical")
-internal fun LinkStepUpVerificationScreenPreview() {
-    FinancialConnectionsPreview {
-        LinkStepUpVerificationContent(
-            state = LinkStepUpVerificationState(
-                payload = Success(
-                    Payload(
-                        email = "theLargestEmailYoulleverseeThatCouldBreakALayout@email.com",
-                        phoneNumber = "12345678",
-                        otpElement = OTPElement(
-                            IdentifierSpec.Generic("otp"),
-                            OTPController()
-                        ),
-                        consumerSessionClientSecret = "12345678"
-                    )
-                )
-            ),
-            onCloseClick = {},
-            onCloseFromErrorClick = {},
-            onClickableTextClick = {}
-        )
     }
 }
 
 @Composable
-@Preview(group = "LinkStepUpVerification Pane", name = "Resending code")
-internal fun LinkStepUpVerificationScreenResendingCodePreview() {
+@Preview
+internal fun LinkStepUpVerificationPreview(
+    @PreviewParameter(LinkStepUpVerificationPreviewParameterProvider::class) state: LinkStepUpVerificationState
+) {
     FinancialConnectionsPreview {
         LinkStepUpVerificationContent(
-            state = LinkStepUpVerificationState(
-                resendOtp = Loading(),
-                payload = Success(
-                    Payload(
-                        email = "shortEmail@email.com",
-                        phoneNumber = "12345678",
-                        otpElement = OTPElement(
-                            IdentifierSpec.Generic("otp"),
-                            OTPController()
-                        ),
-                        consumerSessionClientSecret = "12345678"
-                    )
-                )
-            ),
+            state = state,
             onCloseClick = {},
             onCloseFromErrorClick = {},
             onClickableTextClick = {}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkstepupverification/LinkStepUpVerificationViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkstepupverification/LinkStepUpVerificationViewModel.kt
index d0f1dceb817..64e308810be 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkstepupverification/LinkStepUpVerificationViewModel.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/linkstepupverification/LinkStepUpVerificationViewModel.kt
@@ -37,6 +37,7 @@ import com.stripe.android.model.VerificationType
 import com.stripe.android.uicore.elements.IdentifierSpec
 import com.stripe.android.uicore.elements.OTPController
 import com.stripe.android.uicore.elements.OTPElement
+import getRedactedPhoneNumber
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.launch
 import javax.inject.Inject
@@ -93,7 +94,7 @@ internal class LinkStepUpVerificationViewModel @Inject constructor(
 
     private fun buildPayload(consumerSession: ConsumerSession) = Payload(
         email = consumerSession.emailAddress,
-        phoneNumber = consumerSession.redactedPhoneNumber,
+        phoneNumber = consumerSession.getRedactedPhoneNumber(),
         consumerSessionClientSecret = consumerSession.clientSecret,
         otpElement = OTPElement(
             IdentifierSpec.Generic("otp"),
@@ -118,6 +119,17 @@ internal class LinkStepUpVerificationViewModel @Inject constructor(
                 )
             },
         )
+        onAsync(
+            LinkStepUpVerificationState::confirmVerification,
+            onFail = { error ->
+                eventTracker.logError(
+                    extraMessage = "Error confirming verification",
+                    error = error,
+                    logger = logger,
+                    pane = PANE
+                )
+            },
+        )
     }
 
     private fun onOTPEntered(otp: String) = suspend {
@@ -218,6 +230,11 @@ internal data class LinkStepUpVerificationState(
     val resendOtp: Async = Uninitialized,
 ) : MavericksState {
 
+    val submitLoading: Boolean
+        get() = confirmVerification is Loading || resendOtp is Loading
+    val submitError: Throwable?
+        get() = (confirmVerification as? Fail)?.error ?: (resendOtp as? Fail)?.error
+
     data class Payload(
         val email: String,
         val phoneNumber: String,
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentry/ManualEntryInputValidator.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentry/ManualEntryInputValidator.kt
index 6c0b517a824..3d46e24fdd0 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentry/ManualEntryInputValidator.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentry/ManualEntryInputValidator.kt
@@ -1,23 +1,24 @@
 package com.stripe.android.financialconnections.features.manualentry
 
+import androidx.annotation.StringRes
 import com.stripe.android.financialconnections.R
 
 internal object ManualEntryInputValidator {
 
-    fun getRoutingErrorIdOrNull(input: String): Int? = when {
+    @StringRes fun getRoutingErrorIdOrNull(input: String): Int? = when {
         input.isEmpty() -> R.string.stripe_validation_routing_required
         input.length != ROUTING_NUMBER_LENGTH -> R.string.stripe_validation_routing_too_short
         input.isUSRoutingNumber().not() -> R.string.stripe_validation_no_us_routing
         else -> null
     }
 
-    fun getAccountErrorIdOrNull(input: String): Int? = when {
+    @StringRes fun getAccountErrorIdOrNull(input: String): Int? = when {
         input.isEmpty() -> R.string.stripe_validation_account_required
         input.length > ACCOUNT_NUMBER_MAX_LENGTH -> R.string.stripe_validation_account_too_long
         else -> null
     }
 
-    fun getAccountConfirmIdOrNull(
+    @StringRes fun getAccountConfirmIdOrNull(
         accountInput: String,
         accountConfirmInput: String
     ): Int? = when {
@@ -26,7 +27,6 @@ internal object ManualEntryInputValidator {
         else -> null
     }
 
-    @Suppress("MagicNumber")
     private fun String.isUSRoutingNumber(): Boolean {
         val usRoutingFactor: (Int) -> Int = {
             when (it % 3) {
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentry/ManualEntryPreviewParameterProvider.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentry/ManualEntryPreviewParameterProvider.kt
index f26522699e4..60192644091 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentry/ManualEntryPreviewParameterProvider.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentry/ManualEntryPreviewParameterProvider.kt
@@ -2,28 +2,45 @@ package com.stripe.android.financialconnections.features.manualentry
 
 import androidx.compose.ui.tooling.preview.PreviewParameterProvider
 import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Loading
 import com.airbnb.mvrx.Success
 import com.airbnb.mvrx.Uninitialized
 import com.stripe.android.core.exception.APIException
+import com.stripe.android.financialconnections.R
 
 internal class ManualEntryPreviewParameterProvider : PreviewParameterProvider {
     override val values = sequenceOf(
         canonical(),
+        loading(),
         failure(),
+        fieldFailure(),
+        testMode(),
     )
 
     override val count: Int
         get() = super.count
 
+    private fun loading() = ManualEntryState(
+        payload = Success(
+            ManualEntryState.Payload(
+                verifyWithMicrodeposits = true,
+                customManualEntry = false,
+                testMode = false
+            )
+        ),
+        linkPaymentAccount = Loading(),
+    )
+
     private fun failure() = ManualEntryState(
         payload = Success(
             ManualEntryState.Payload(
                 verifyWithMicrodeposits = true,
-                customManualEntry = false
+                customManualEntry = false,
+                testMode = false
             )
         ),
         linkPaymentAccount = Fail(
-            APIException(message = "Error linking accounts")
+            APIException(message = "Test bank accounts cannot be used in live mode")
         ),
     )
 
@@ -31,9 +48,38 @@ internal class ManualEntryPreviewParameterProvider : PreviewParameterProvider = viewModel.collectAsState()
+    val state: ManualEntryState by viewModel.collectAsState()
     ManualEntryContent(
-        routing = state.value.routing to state.value.routingError,
-        account = state.value.account to state.value.accountError,
-        accountConfirm = state.value.accountConfirm to state.value.accountConfirmError,
-        isValidForm = state.value.isValidForm,
-        payload = state.value.payload,
-        linkPaymentAccountStatus = state.value.linkPaymentAccount,
+        routing = state.routing,
+        routingError = state.routingError,
+        account = state.account,
+        accountError = state.accountError,
+        accountConfirm = state.accountConfirm,
+        accountConfirmError = state.accountConfirmError,
+        isValidForm = state.isValidForm,
+        payload = state.payload,
+        linkPaymentAccountStatus = state.linkPaymentAccount,
         onRoutingEntered = viewModel::onRoutingEntered,
         onAccountEntered = viewModel::onAccountEntered,
         onAccountConfirmEntered = viewModel::onAccountConfirmEntered,
         onSubmit = viewModel::onSubmit,
-    ) { parentViewModel.onCloseWithConfirmationClick(Pane.MANUAL_ENTRY) }
+        onTestFill = viewModel::onTestFill,
+        onCloseClick = { parentViewModel.onCloseWithConfirmationClick(Pane.MANUAL_ENTRY) }
+    )
 }
 
 @Composable
 private fun ManualEntryContent(
-    routing: Pair,
-    account: Pair,
-    accountConfirm: Pair,
+    routing: String,
+    routingError: Int?,
+    account: String,
+    accountError: Int?,
+    accountConfirm: String,
+    accountConfirmError: Int?,
     isValidForm: Boolean,
     payload: Async,
     linkPaymentAccountStatus: Async,
@@ -89,7 +87,8 @@ private fun ManualEntryContent(
     onAccountEntered: (String) -> Unit,
     onAccountConfirmEntered: (String) -> Unit,
     onSubmit: () -> Unit,
-    onCloseClick: () -> Unit
+    onCloseClick: () -> Unit,
+    onTestFill: () -> Unit
 ) {
     val scrollState = rememberScrollState()
     FinancialConnectionsScaffold(
@@ -114,13 +113,17 @@ private fun ManualEntryContent(
                     linkPaymentAccountStatus = linkPaymentAccountStatus,
                     payload = payload(),
                     routing = routing,
-                    onRoutingEntered = onRoutingEntered,
+                    routingError = routingError,
                     account = account,
-                    onAccountEntered = onAccountEntered,
+                    accountError = accountError,
                     accountConfirm = accountConfirm,
+                    accountConfirmError = accountConfirmError,
+                    onRoutingEntered = onRoutingEntered,
+                    onAccountEntered = onAccountEntered,
                     onAccountConfirmEntered = onAccountConfirmEntered,
                     isValidForm = isValidForm,
-                    onSubmit = onSubmit
+                    onSubmit = onSubmit,
+                    onTestFill = onTestFill
                 )
             }
         }
@@ -128,122 +131,151 @@ private fun ManualEntryContent(
 }
 
 @Composable
-@Suppress("LongMethod")
 private fun ManualEntryLoaded(
     scrollState: ScrollState,
     payload: Payload,
     linkPaymentAccountStatus: Async,
-    routing: Pair,
+    routing: String,
+    routingError: Int?,
+    account: String,
+    accountError: Int?,
+    accountConfirm: String,
+    accountConfirmError: Int?,
     onRoutingEntered: (String) -> Unit,
-    account: Pair,
     onAccountEntered: (String) -> Unit,
-    accountConfirm: Pair,
     onAccountConfirmEntered: (String) -> Unit,
     isValidForm: Boolean,
-    onSubmit: () -> Unit
+    onSubmit: () -> Unit,
+    onTestFill: () -> Unit
 ) {
-    Column(
-        Modifier.fillMaxSize()
-    ) {
-        // Scrollable content
-        Column(
-            modifier = Modifier
-                .weight(1f)
-                .verticalScroll(scrollState)
-                .padding(
-                    top = 16.dp,
-                    start = 24.dp,
-                    end = 24.dp,
-                    bottom = 24.dp
-                )
-        ) {
-            var currentCheck: Int? by remember { mutableStateOf(R.drawable.stripe_check_base) }
-            Text(
-                modifier = Modifier.fillMaxWidth(),
-                text = stringResource(R.string.stripe_manualentry_title),
-                color = FinancialConnectionsTheme.colors.textPrimary,
-                style = FinancialConnectionsTheme.typography.subtitle
-            )
-            Spacer(modifier = Modifier.size(24.dp))
-            Box {
-                Image(
-                    painter = painterResource(id = R.drawable.stripe_check_base),
-                    contentDescription = "Image of bank check referencing routing number"
-                )
-                currentCheck?.let {
-                    Image(
-                        painter = painterResource(id = it),
-                        contentDescription = "Image of bank check referencing routing number"
-                    )
-                }
-            }
-            if (linkPaymentAccountStatus is Fail) {
-                Text(
-                    text = (linkPaymentAccountStatus.error as? StripeException)?.message
-                        ?: stringResource(R.string.stripe_error_generic_title),
-                    color = FinancialConnectionsTheme.colors.textCritical,
-                    style = FinancialConnectionsTheme.typography.body
-                )
-                Spacer(modifier = Modifier.size(8.dp))
-            }
+    val loading = linkPaymentAccountStatus is Loading
+    Layout(
+        scrollState = scrollState,
+        body = {
+            Spacer(modifier = Modifier.size(8.dp))
+            Title()
+            Spacer(modifier = Modifier.size(16.dp))
             if (payload.verifyWithMicrodeposits) {
                 Spacer(modifier = Modifier.size(8.dp))
                 Text(
                     text = stringResource(R.string.stripe_manualentry_microdeposits_desc),
-                    color = FinancialConnectionsTheme.colors.textPrimary,
-                    style = FinancialConnectionsTheme.typography.body
+                    color = FinancialConnectionsTheme.colors.textDefault,
+                    style = FinancialConnectionsTheme.typography.bodyMedium
+                )
+            }
+            if (payload.testMode) {
+                Spacer(modifier = Modifier.size(8.dp))
+                TestModeBanner(
+                    enabled = loading.not(),
+                    buttonLabel = stringResource(id = R.string.stripe_manualentry_test_banner),
+                    onButtonClick = onTestFill
                 )
             }
-            Spacer(modifier = Modifier.size(8.dp))
-
-            InputWithError(
-                label = R.string.stripe_manualentry_routing,
-                hint = "123456789",
-                inputWithError = routing,
-                testTag = "RoutingInput",
-                onInputChanged = onRoutingEntered,
-                onFocusGained = { currentCheck = R.drawable.stripe_check_routing }
-            )
             Spacer(modifier = Modifier.size(24.dp))
-            InputWithError(
-                label = R.string.stripe_manualentry_account,
-                hint = "000123456789",
-                inputWithError = account,
-                testTag = "AccountInput",
-                onInputChanged = onAccountEntered,
-                onFocusGained = { currentCheck = R.drawable.stripe_check_account }
+            AccountForm(
+                enabled = loading.not(),
+                routing = routing,
+                routingError = routingError,
+                onRoutingEntered = onRoutingEntered,
+                account = account,
+                accountError = accountError,
+                onAccountEntered = onAccountEntered,
+                accountConfirm = accountConfirm,
+                accountConfirmError = accountConfirmError,
+                onAccountConfirmEntered = onAccountConfirmEntered
             )
-            Spacer(modifier = Modifier.size(8.dp))
-            Text(
-                text = stringResource(R.string.stripe_manualentry_account_type_disclaimer),
-                color = FinancialConnectionsTheme.colors.textSecondary,
-                style = FinancialConnectionsTheme.typography.caption
-            )
-            Spacer(modifier = Modifier.size(24.dp))
-            InputWithError(
-                label = R.string.stripe_manualentry_accountconfirm,
-                hint = "000123456789",
-                inputWithError = accountConfirm,
-                testTag = "ConfirmAccountInput",
-                onInputChanged = onAccountConfirmEntered,
-                onFocusGained = { currentCheck = R.drawable.stripe_check_account }
+            if (linkPaymentAccountStatus is Fail) {
+                Spacer(modifier = Modifier.size(16.dp))
+                ErrorMessage(linkPaymentAccountStatus.error)
+            }
+        },
+        footer = {
+            ManualEntryFooter(
+                isValidForm = isValidForm,
+                loading = loading,
+                onSubmit = onSubmit
             )
-            Spacer(modifier = Modifier.weight(1f))
         }
-        // Footer
-        ManualEntryFooter(isValidForm, onSubmit)
+    )
+}
+
+@Composable
+private fun ErrorMessage(
+    error: Throwable
+) {
+    Text(
+        modifier = Modifier.fillMaxWidth(),
+        textAlign = TextAlign.Center,
+        text = (error as? StripeException)?.message
+            ?: stringResource(R.string.stripe_error_generic_title),
+        style = FinancialConnectionsTheme.typography.bodyMedium,
+        color = FinancialConnectionsTheme.colors.textCritical,
+    )
+}
+
+@Composable
+private fun Title() {
+    Text(
+        modifier = Modifier.fillMaxWidth(),
+        text = stringResource(R.string.stripe_manualentry_title),
+        color = FinancialConnectionsTheme.colors.textDefault,
+        style = FinancialConnectionsTheme.typography.headingXLarge
+    )
+}
+
+@Composable
+private fun AccountForm(
+    enabled: Boolean,
+    routing: String,
+    routingError: Int?,
+    onRoutingEntered: (String) -> Unit,
+    account: String,
+    accountError: Int?,
+    onAccountEntered: (String) -> Unit,
+    accountConfirm: String,
+    accountConfirmError: Int?,
+    onAccountConfirmEntered: (String) -> Unit
+) {
+    Column(
+        modifier = Modifier.fillMaxWidth(),
+        verticalArrangement = Arrangement.spacedBy(16.dp)
+    ) {
+        InputWithError(
+            enabled = enabled,
+            label = R.string.stripe_manualentry_routing,
+            input = routing,
+            error = routingError,
+            testTag = "RoutingInput",
+            onInputChanged = onRoutingEntered,
+        )
+        InputWithError(
+            enabled = enabled,
+            label = R.string.stripe_manualentry_account,
+            input = account,
+            error = accountError,
+            testTag = "AccountInput",
+            onInputChanged = onAccountEntered,
+        )
+        InputWithError(
+            enabled = enabled,
+            label = R.string.stripe_manualentry_accountconfirm,
+            input = accountConfirm,
+            error = accountConfirmError,
+            testTag = "ConfirmAccountInput",
+            onInputChanged = onAccountConfirmEntered,
+        )
     }
 }
 
 @Composable
 private fun ManualEntryFooter(
     isValidForm: Boolean,
+    loading: Boolean,
     onSubmit: () -> Unit
 ) {
-    Column(
-        modifier = Modifier.padding(24.dp)
-    ) {
+    Column {
         FinancialConnectionsButton(
+            loading = loading,
             enabled = isValidForm,
             onClick = onSubmit,
             modifier = Modifier
@@ -257,49 +289,43 @@ private fun ManualEntryFooter(
 @OptIn(ExperimentalComposeUiApi::class)
 @Composable
 private fun InputWithError(
-    inputWithError: Pair,
+    enabled: Boolean,
+    input: String,
+    @StringRes error: Int?,
     label: Int,
     testTag: String,
-    hint: String,
-    onFocusGained: () -> Unit,
     onInputChanged: (String) -> Unit
 ) {
-    var textValue by remember { mutableStateOf(TextFieldValue()) }
-    Text(
-        text = stringResource(id = label),
-        color = FinancialConnectionsTheme.colors.textSecondary,
-        style = FinancialConnectionsTheme.typography.body
-    )
-    Spacer(modifier = Modifier.size(4.dp))
-    FinancialConnectionsOutlinedTextField(
-        value = textValue,
-        keyboardOptions = KeyboardOptions(
-            keyboardType = KeyboardType.Number,
-        ),
-        placeholder = {
+    Column(
+        modifier = Modifier.fillMaxWidth(),
+        verticalArrangement = Arrangement.spacedBy(4.dp)
+    ) {
+        FinancialConnectionsOutlinedTextField(
+            enabled = enabled,
+            value = input,
+            keyboardOptions = KeyboardOptions(
+                keyboardType = KeyboardType.Number,
+            ),
+            placeholder = {
+                Text(
+                    text = stringResource(id = label),
+                    style = FinancialConnectionsTheme.typography.labelLarge,
+                    color = FinancialConnectionsTheme.colors.textSubdued
+                )
+            },
+            isError = error != null,
+            onValueChange = onInputChanged,
+            modifier = Modifier
+                .semantics { testTagsAsResourceId = true }
+                .testTag(testTag)
+        )
+        if (error != null) {
             Text(
-                text = hint,
-                style = FinancialConnectionsTheme.typography.body,
-                color = FinancialConnectionsTheme.colors.textDisabled
+                text = stringResource(id = error),
+                color = FinancialConnectionsTheme.colors.textCritical,
+                style = FinancialConnectionsTheme.typography.labelSmall,
             )
-        },
-        isError = inputWithError.second != null,
-        onValueChange = { text ->
-            textValue = text.filtered { it.isDigit() }
-            onInputChanged(textValue.text)
-        },
-        modifier = Modifier
-            .semantics { testTagsAsResourceId = true }
-            .testTag(testTag)
-            .onFocusChanged { if (it.isFocused) onFocusGained() }
-    )
-    if (inputWithError.second != null) {
-        Text(
-            text = stringResource(id = inputWithError.second!!),
-            color = FinancialConnectionsTheme.colors.textCritical,
-            style = FinancialConnectionsTheme.typography.captionEmphasized,
-            modifier = Modifier.padding(start = 16.dp)
-        )
+        }
     }
 }
 
@@ -312,16 +338,21 @@ internal fun ManualEntryPreview(
 ) {
     FinancialConnectionsPreview {
         ManualEntryContent(
-            routing = "" to null,
-            account = "" to null,
-            accountConfirm = "" to null,
+            routing = state.routing,
+            routingError = state.routingError,
+            account = state.account,
+            accountError = state.accountError,
+            accountConfirm = state.accountConfirm,
+            accountConfirmError = state.accountConfirmError,
             isValidForm = true,
             payload = state.payload,
             linkPaymentAccountStatus = state.linkPaymentAccount,
             onRoutingEntered = {},
             onAccountEntered = {},
             onAccountConfirmEntered = {},
+            onTestFill = {},
             onSubmit = {},
-        ) {}
+            onCloseClick = {}
+        )
     }
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentry/ManualEntryViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentry/ManualEntryViewModel.kt
index aa2339458c1..e5e8819d439 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentry/ManualEntryViewModel.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentry/ManualEntryViewModel.kt
@@ -24,7 +24,6 @@ import com.stripe.android.financialconnections.navigation.NavigationManager
 import com.stripe.android.financialconnections.ui.FinancialConnectionsSheetNativeActivity
 import javax.inject.Inject
 
-@Suppress("LongParameterList")
 internal class ManualEntryViewModel @Inject constructor(
     initialState: ManualEntryState,
     private val nativeAuthFlowCoordinator: NativeAuthFlowCoordinator,
@@ -37,50 +36,20 @@ internal class ManualEntryViewModel @Inject constructor(
 
     init {
         observeAsyncs()
-        observeInputs()
         suspend {
             val sync = getOrFetchSync()
             val manifest = requireNotNull(sync.manifest)
             eventTracker.track(PaneLoaded(Pane.MANUAL_ENTRY))
             ManualEntryState.Payload(
                 verifyWithMicrodeposits = manifest.manualEntryUsesMicrodeposits,
-                customManualEntry = manifest.manualEntryMode == ManualEntryMode.CUSTOM
+                customManualEntry = manifest.manualEntryMode == ManualEntryMode.CUSTOM,
+                testMode = manifest.livemode.not()
             )
         }.execute {
             copy(payload = it)
         }
     }
 
-    private fun observeInputs() {
-        onEach(ManualEntryState::accountConfirm) { input ->
-            if (input != null) {
-                withState {
-                    val error: Int? = ManualEntryInputValidator.getAccountConfirmIdOrNull(
-                        accountInput = it.account ?: "",
-                        accountConfirmInput = input
-                    )
-                    setState { copy(accountConfirmError = error) }
-                }
-            }
-        }
-        onEach(ManualEntryState::account) { input ->
-            if (input != null) {
-                setState {
-                    val error = ManualEntryInputValidator.getAccountErrorIdOrNull(input)
-                    copy(accountError = error)
-                }
-            }
-        }
-        onEach(ManualEntryState::routing) { input ->
-            if (input != null) {
-                setState {
-                    val error = ManualEntryInputValidator.getRoutingErrorIdOrNull(input)
-                    copy(routingError = error)
-                }
-            }
-        }
-    }
-
     private fun observeAsyncs() {
         onAsync(
             ManualEntryState::payload,
@@ -108,23 +77,32 @@ internal class ManualEntryViewModel @Inject constructor(
     }
 
     fun onRoutingEntered(input: String) {
-        val filteredInput = input.filter { it.isDigit() }
-        setState { copy(routing = filteredInput) }
+        setState { copy(routing = input.filter { it.isDigit() }) }
+        withState {
+            val error = ManualEntryInputValidator.getRoutingErrorIdOrNull(input)
+            setState { copy(routingError = error) }
+        }
     }
 
     fun onAccountEntered(input: String) {
-        val filteredInput = input.filter { it.isDigit() }
-        setState { copy(account = filteredInput) }
+        setState { copy(account = input.filter { it.isDigit() }) }
+        withState {
+            val error = ManualEntryInputValidator.getAccountErrorIdOrNull(input)
+            setState { copy(accountError = error) }
+        }
     }
 
     fun onAccountConfirmEntered(input: String) {
-        val filteredInput = input.filter { it.isDigit() }
-        setState {
-            copy(accountConfirm = filteredInput)
+        setState { copy(accountConfirm = input.filter { it.isDigit() }) }
+        withState {
+            val error: Int? = ManualEntryInputValidator.getAccountConfirmIdOrNull(
+                accountInput = it.account,
+                accountConfirmInput = input
+            )
+            setState { copy(accountConfirmError = error) }
         }
     }
 
-    @Suppress("MagicNumber")
     fun onSubmit() {
         suspend {
             val state = awaitState()
@@ -155,6 +133,20 @@ internal class ManualEntryViewModel @Inject constructor(
         }.execute { copy(linkPaymentAccount = it) }
     }
 
+    fun onTestFill() {
+        setState {
+            copy(
+                routing = "110000000",
+                account = "000123456789",
+                accountConfirm = "000123456789",
+                routingError = null,
+                accountError = null,
+                accountConfirmError = null
+            )
+        }
+        onSubmit()
+    }
+
     companion object :
         MavericksViewModelFactory {
 
@@ -177,9 +169,9 @@ internal class ManualEntryViewModel @Inject constructor(
 
 internal data class ManualEntryState(
     val payload: Async = Uninitialized,
-    val routing: String? = null,
-    val account: String? = null,
-    val accountConfirm: String? = null,
+    val routing: String = "",
+    val account: String = "",
+    val accountConfirm: String = "",
     val routingError: Int? = null,
     val accountError: Int? = null,
     val accountConfirmError: Int? = null,
@@ -188,7 +180,8 @@ internal data class ManualEntryState(
 
     data class Payload(
         val verifyWithMicrodeposits: Boolean,
-        val customManualEntry: Boolean
+        val customManualEntry: Boolean,
+        val testMode: Boolean
     )
 
     val isValidForm
@@ -197,5 +190,5 @@ internal data class ManualEntryState(
                 (account to accountError).valid() &&
                 (accountConfirm to accountConfirmError).valid()
 
-    private fun Pair.valid() = first != null && second == null
+    private fun Pair.valid() = first.isNotEmpty() && second == null
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentrysuccess/ManualEntrySuccessScreen.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentrysuccess/ManualEntrySuccessScreen.kt
index a21d6f0651a..14bc430e24d 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentrysuccess/ManualEntrySuccessScreen.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentrysuccess/ManualEntrySuccessScreen.kt
@@ -1,355 +1,31 @@
-@file:Suppress("TooManyFunctions", "LongMethod")
-
 package com.stripe.android.financialconnections.features.manualentrysuccess
 
 import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-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.RowScope
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.Divider
-import androidx.compose.material.Icon
-import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
+import androidx.compose.runtime.getValue
 import androidx.navigation.NavBackStackEntry
-import com.airbnb.mvrx.Loading
 import com.airbnb.mvrx.compose.collectAsState
 import com.airbnb.mvrx.compose.mavericksViewModel
-import com.stripe.android.financialconnections.R
-import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
-import com.stripe.android.financialconnections.model.LinkAccountSessionPaymentAccount.MicrodepositVerificationMethod
-import com.stripe.android.financialconnections.navigation.Destination
+import com.stripe.android.financialconnections.features.success.SuccessContent
+import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
 import com.stripe.android.financialconnections.presentation.parentViewModel
-import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
-import com.stripe.android.financialconnections.ui.components.FinancialConnectionsButton
-import com.stripe.android.financialconnections.ui.components.FinancialConnectionsScaffold
-import com.stripe.android.financialconnections.ui.components.FinancialConnectionsTopAppBar
-import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
 
 @Composable
 internal fun ManualEntrySuccessScreen(
     backStackEntry: NavBackStackEntry,
 ) {
     val parentViewModel = parentViewModel()
-    val viewModel: ManualEntrySuccessViewModel = mavericksViewModel()
+    val viewModel: ManualEntrySuccessViewModel = mavericksViewModel(argsFactory = { backStackEntry.arguments })
+    val state by viewModel.collectAsState()
     BackHandler(true) {}
-    val completeAuthSessionAsync = viewModel
-        .collectAsState(ManualEntrySuccessState::completeSession)
-    ManualEntrySuccessContent(
-        microdepositVerificationMethod = Destination.ManualEntrySuccess.microdeposits(backStackEntry),
-        last4 = Destination.ManualEntrySuccess.last4(backStackEntry),
-        loading = completeAuthSessionAsync.value is Loading,
-        onCloseClick = { parentViewModel.onCloseNoConfirmationClick(Pane.MANUAL_ENTRY_SUCCESS) },
-        onDoneClick = viewModel::onSubmit
-    )
-}
-
-@Composable
-internal fun ManualEntrySuccessContent(
-    microdepositVerificationMethod: MicrodepositVerificationMethod,
-    last4: String?,
-    loading: Boolean,
-    onCloseClick: () -> Unit,
-    onDoneClick: () -> Unit
-) {
-    FinancialConnectionsScaffold(
-        topBar = {
-            FinancialConnectionsTopAppBar(
-                showBack = false,
-                onCloseClick = onCloseClick,
+    SuccessContent(
+        completeSessionAsync = state.completeSession,
+        payloadAsync = state.payload,
+        onDoneClick = viewModel::onSubmit,
+        onCloseClick = {
+            parentViewModel.onCloseNoConfirmationClick(
+                FinancialConnectionsSessionManifest.Pane.MANUAL_ENTRY_SUCCESS
             )
         }
-    ) {
-        Column(
-            verticalArrangement = Arrangement.spacedBy(16.dp),
-            modifier = Modifier
-                .fillMaxSize()
-                .padding(
-                    top = 8.dp,
-                    start = 24.dp,
-                    end = 24.dp,
-                    bottom = 24.dp
-                )
-        ) {
-            Icon(
-                modifier = Modifier.size(40.dp),
-                painter = painterResource(R.drawable.stripe_ic_check_circle),
-                contentDescription = null,
-                tint = FinancialConnectionsTheme.colors.textSuccess
-            )
-            Text(
-                modifier = Modifier
-                    .fillMaxWidth(),
-                text = when (microdepositVerificationMethod) {
-                    MicrodepositVerificationMethod.UNKNOWN,
-                    MicrodepositVerificationMethod.AMOUNTS ->
-                        stringResource(R.string.stripe_manualentrysuccess_title)
-                    MicrodepositVerificationMethod.DESCRIPTOR_CODE ->
-                        stringResource(R.string.stripe_manualentrysuccess_title_descriptorcode)
-                },
-                style = FinancialConnectionsTheme.typography.subtitle.copy(
-                    color = FinancialConnectionsTheme.colors.textPrimary
-                )
-            )
-            Text(
-                text = resolveText(microdepositVerificationMethod, last4),
-                style = FinancialConnectionsTheme.typography.body.copy(
-                    color = FinancialConnectionsTheme.colors.textSecondary
-                )
-            )
-            Spacer(modifier = Modifier.height(8.dp))
-            TransactionHistoryTable(
-                microdepositVerificationMethod = microdepositVerificationMethod,
-                last4 = last4
-            )
-            Spacer(modifier = Modifier.weight(1f))
-            FinancialConnectionsButton(
-                loading = loading,
-                onClick = onDoneClick,
-                modifier = Modifier
-                    .fillMaxWidth()
-            ) {
-                Text(text = stringResource(R.string.stripe_success_pane_done))
-            }
-        }
-    }
-}
-
-@Composable
-internal fun resolveText(
-    microdepositVerificationMethod: MicrodepositVerificationMethod,
-    last4: String?
-) = when (microdepositVerificationMethod) {
-    MicrodepositVerificationMethod.AMOUNTS -> when {
-        last4 != null -> stringResource(R.string.stripe_manualentrysuccess_desc, last4)
-        else -> stringResource(R.string.stripe_manualentrysuccess_desc_noaccount)
-    }
-
-    MicrodepositVerificationMethod.DESCRIPTOR_CODE -> when {
-        last4 != null -> stringResource(
-            R.string.stripe_manualentrysuccess_desc_descriptorcode,
-            last4
-        )
-
-        else -> stringResource(R.string.stripe_manualentrysuccess_desc_noaccount_descriptorcode)
-    }
-
-    MicrodepositVerificationMethod.UNKNOWN -> TODO()
-}
-
-@Composable
-internal fun TransactionHistoryTable(
-    last4: String?,
-    microdepositVerificationMethod: MicrodepositVerificationMethod
-) {
-    val shape = RoundedCornerShape(8.dp)
-    Box(
-        Modifier
-            .clip(shape)
-            .background(FinancialConnectionsTheme.colors.backgroundContainer)
-            .border(
-                border = BorderStroke(1.dp, FinancialConnectionsTheme.colors.borderDefault),
-                shape = shape
-            )
-    ) {
-        Column(
-            Modifier
-                .padding(start = 16.dp, end = 16.dp, top = 16.dp)
-        ) {
-            val titleColor = FinancialConnectionsTheme.colors.textSecondary
-            val tableData =
-                buildTableRows(microdepositVerificationMethod)
-            last4?.let {
-                Row(
-                    verticalAlignment = Alignment.CenterVertically,
-                    horizontalArrangement = Arrangement.spacedBy(8.dp)
-                ) {
-                    Icon(
-                        painter = painterResource(id = R.drawable.stripe_ic_bank),
-                        tint = FinancialConnectionsTheme.colors.textSecondary,
-                        contentDescription = "Bank icon"
-                    )
-                    Text(
-                        text = stringResource(R.string.stripe_manualentrysuccess_table_title, it),
-                        style = FinancialConnectionsTheme.typography.bodyCode.copy(color = titleColor)
-                    )
-                }
-                Spacer(Modifier.size(8.dp))
-            }
-            Row {
-                TitleCell(text = "Transaction")
-                TitleCell(text = "Amount")
-                TitleCell(text = "Type")
-            }
-            Divider(
-                modifier = Modifier.padding(top = 4.dp, bottom = 8.dp),
-                color = FinancialConnectionsTheme.colors.borderDefault
-            )
-            tableData.withIndex().forEach { (index, item) ->
-                val (transaction, amount, type) = item
-                val highlight = tableData.lastIndex != index
-                Row(Modifier.fillMaxWidth()) {
-                    TableCell(text = transaction.first, transaction.second, highlight)
-                    TableCell(text = amount.first, amount.second, highlight)
-                    TableCell(text = type.first, type.second, highlight)
-                }
-            }
-        }
-        // Bottom fade.
-        Box(
-            modifier = Modifier
-                .fillMaxWidth()
-                .height(26.dp)
-                .align(Alignment.BottomCenter)
-                .background(
-                    brush = Brush.verticalGradient(
-                        colors = listOf(
-                            FinancialConnectionsTheme.colors.textWhite.copy(alpha = 0f),
-                            FinancialConnectionsTheme.colors.textWhite.copy(alpha = 1f)
-                        )
-                    )
-                )
-        )
-    }
-}
-
-@Composable
-private fun buildTableRows(
-    microdepositVerificationMethod: MicrodepositVerificationMethod
-): List, Pair, Pair>> {
-    val rowColor = FinancialConnectionsTheme.colors.textPrimary
-    val highlightColor = FinancialConnectionsTheme.colors.textBrand
-    return when (microdepositVerificationMethod) {
-        MicrodepositVerificationMethod.DESCRIPTOR_CODE -> listOf(
-            Triple("SMXXXX" to highlightColor, "$0.01" to rowColor, "ACH CREDIT" to rowColor),
-            Triple("GROCERIES" to rowColor, "$56.12" to rowColor, "VISA" to rowColor)
-        )
-
-        MicrodepositVerificationMethod.AMOUNTS -> listOf(
-            Triple("AMTS" to rowColor, "$0.XX" to highlightColor, "ACH CREDIT" to rowColor),
-            Triple("AMTS" to rowColor, "$0.XX" to highlightColor, "ACH CREDIT" to rowColor),
-            Triple("GROCERIES" to rowColor, "$56.12" to rowColor, "VISA" to rowColor)
-        )
-
-        MicrodepositVerificationMethod.UNKNOWN -> error("Unknown microdeposits type")
-    }
-}
-
-@Composable
-private fun RowScope.TitleCell(
-    text: String,
-) {
-    Text(
-        text = text,
-        style = FinancialConnectionsTheme.typography.caption.copy(
-            color = FinancialConnectionsTheme.colors.textSecondary
-        ),
-        modifier = Modifier
-            .padding(vertical = 4.dp)
-            .weight(1f)
-    )
-}
-
-@Composable
-private fun RowScope.TableCell(
-    text: String,
-    color: Color,
-    highlight: Boolean
-) {
-    val typography = if (highlight) {
-        FinancialConnectionsTheme.typography.captionCodeEmphasized
-    } else {
-        FinancialConnectionsTheme.typography.captionCode
-    }
-    Text(
-        text = text,
-        style = typography.copy(color = color),
-        modifier = Modifier
-            .padding(vertical = 4.dp)
-            .weight(1f)
     )
 }
-
-@Preview(
-    group = "Manual Entry Success",
-    name = "Amount"
-)
-@Composable
-internal fun ManualEntrySuccessScreenPreviewAmount() {
-    FinancialConnectionsPreview {
-        ManualEntrySuccessContent(
-            MicrodepositVerificationMethod.AMOUNTS,
-            last4 = "1234",
-            loading = false,
-            onCloseClick = {}
-        ) {}
-    }
-}
-
-@Preview(
-    group = "Manual Entry Success",
-    name = "Descriptor"
-)
-@Composable
-internal fun ManualEntrySuccessDescriptor() {
-    FinancialConnectionsPreview {
-        ManualEntrySuccessContent(
-            MicrodepositVerificationMethod.DESCRIPTOR_CODE,
-            last4 = "1234",
-            loading = false,
-            onCloseClick = {}
-        ) {}
-    }
-}
-
-@Preview(
-    group = "Manual Entry Success",
-    name = "Amount no account"
-)
-@Composable
-internal fun ManualEntrySuccessAmountNoAccount() {
-    FinancialConnectionsPreview {
-        ManualEntrySuccessContent(
-            MicrodepositVerificationMethod.AMOUNTS,
-            last4 = null,
-            loading = false,
-            onCloseClick = {}
-        ) {}
-    }
-}
-
-@Preview(
-    group = "Manual Entry Success",
-    name = "Descriptor no account"
-)
-@Composable
-internal fun ManualEntrySuccessDescriptorNoAccount() {
-    FinancialConnectionsPreview {
-        ManualEntrySuccessContent(
-            MicrodepositVerificationMethod.DESCRIPTOR_CODE,
-            last4 = null,
-            loading = false,
-            onCloseClick = {}
-        ) {}
-    }
-}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentrysuccess/ManualEntrySuccessViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentrysuccess/ManualEntrySuccessViewModel.kt
index 17c49880077..3721b78831f 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentrysuccess/ManualEntrySuccessViewModel.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/manualentrysuccess/ManualEntrySuccessViewModel.kt
@@ -1,5 +1,6 @@
 package com.stripe.android.financialconnections.features.manualentrysuccess
 
+import android.os.Bundle
 import com.airbnb.mvrx.Async
 import com.airbnb.mvrx.Loading
 import com.airbnb.mvrx.MavericksState
@@ -7,27 +8,43 @@ import com.airbnb.mvrx.MavericksViewModel
 import com.airbnb.mvrx.MavericksViewModelFactory
 import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.ViewModelContext
+import com.stripe.android.financialconnections.R
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.ClickDone
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.PaneLoaded
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
+import com.stripe.android.financialconnections.domain.GetManifest
 import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator
+import com.stripe.android.financialconnections.features.success.SuccessState
 import com.stripe.android.financialconnections.model.FinancialConnectionsSession
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
+import com.stripe.android.financialconnections.navigation.Destination
 import com.stripe.android.financialconnections.ui.FinancialConnectionsSheetNativeActivity
+import com.stripe.android.financialconnections.ui.TextResource.StringId
 import kotlinx.coroutines.launch
 import javax.inject.Inject
 
-@Suppress("LongParameterList")
 internal class ManualEntrySuccessViewModel @Inject constructor(
     initialState: ManualEntrySuccessState,
+    private val getManifest: GetManifest,
     private val eventTracker: FinancialConnectionsAnalyticsTracker,
     private val nativeAuthFlowCoordinator: NativeAuthFlowCoordinator,
 ) : MavericksViewModel(initialState) {
 
     init {
-        viewModelScope.launch {
-            eventTracker.track(PaneLoaded(Pane.MANUAL_ENTRY_SUCCESS))
-        }
+        suspend {
+            val manifest = getManifest()
+            SuccessState.Payload(
+                businessName = manifest.businessName,
+                customSuccessMessage = when (val last4 = initialState.last4) {
+                    null -> StringId(R.string.stripe_success_pane_desc_microdeposits_no_account)
+                    else -> StringId(R.string.stripe_success_pane_desc_microdeposits, listOf(last4))
+                },
+                accountsCount = 1, // on manual entry just one account is connected,
+                skipSuccessPane = false
+            ).also {
+                eventTracker.track(PaneLoaded(Pane.MANUAL_ENTRY_SUCCESS))
+            }
+        }.execute { copy(payload = it) }
     }
 
     fun onSubmit() {
@@ -57,5 +74,15 @@ internal class ManualEntrySuccessViewModel @Inject constructor(
 }
 
 internal data class ManualEntrySuccessState(
-    val completeSession: Async = Uninitialized
-) : MavericksState
+    val last4: String?,
+    val payload: Async,
+    val completeSession: Async
+) : MavericksState {
+
+    @Suppress("unused") // used by mavericks to create initial state.
+    constructor(args: Bundle?) : this(
+        last4 = Destination.ManualEntrySuccess.last4(args),
+        payload = Uninitialized,
+        completeSession = Uninitialized
+    )
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupPreviewParameterProvider.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupPreviewParameterProvider.kt
new file mode 100644
index 00000000000..f5b2cc3c40d
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupPreviewParameterProvider.kt
@@ -0,0 +1,58 @@
+package com.stripe.android.financialconnections.features.networkinglinkloginwarmup
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.Uninitialized
+
+internal class NetworkingLinkLoginWarmupPreviewParameterProvider :
+    PreviewParameterProvider {
+    override val values = sequenceOf(
+        canonical(),
+        loading(),
+        disablingNetworking(),
+        payloadError(),
+        disablingError()
+    )
+
+    private fun canonical() = NetworkingLinkLoginWarmupState(
+        payload = Success(
+            NetworkingLinkLoginWarmupState.Payload(
+                merchantName = "Test",
+                email = "email@test.com"
+            )
+        ),
+        disableNetworkingAsync = Uninitialized
+    )
+
+    private fun loading() = NetworkingLinkLoginWarmupState(
+        payload = Loading(),
+        disableNetworkingAsync = Uninitialized
+    )
+
+    private fun payloadError() = NetworkingLinkLoginWarmupState(
+        payload = Fail(Exception("Error")),
+        disableNetworkingAsync = Uninitialized
+    )
+
+    private fun disablingError() = NetworkingLinkLoginWarmupState(
+        payload = Success(
+            NetworkingLinkLoginWarmupState.Payload(
+                merchantName = "Test",
+                email = "email@test.com"
+            )
+        ),
+        disableNetworkingAsync = Fail(Exception("Error"))
+    )
+
+    private fun disablingNetworking() = NetworkingLinkLoginWarmupState(
+        payload = Success(
+            NetworkingLinkLoginWarmupState.Payload(
+                merchantName = "Test",
+                email = "email@test.com"
+            )
+        ),
+        disableNetworkingAsync = Loading()
+    )
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupScreen.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupScreen.kt
index a35db60f1f2..4fecc27de16 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupScreen.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupScreen.kt
@@ -3,23 +3,22 @@
 package com.stripe.android.financialconnections.features.networkinglinkloginwarmup
 
 import androidx.annotation.RestrictTo
-import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.background
 import androidx.compose.foundation.border
+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.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.CircularProgressIndicator
-import androidx.compose.material.Icon
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
@@ -31,37 +30,33 @@ import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.testTagsAsResourceId
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
 import androidx.compose.ui.unit.dp
-import com.airbnb.mvrx.Fail
+import androidx.navigation.NavBackStackEntry
 import com.airbnb.mvrx.Loading
-import com.airbnb.mvrx.Success
-import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.compose.collectAsState
 import com.airbnb.mvrx.compose.mavericksViewModel
 import com.stripe.android.financialconnections.R
-import com.stripe.android.financialconnections.features.common.UnclassifiedErrorContent
-import com.stripe.android.financialconnections.features.networkinglinkloginwarmup.NetworkingLinkLoginWarmupState.Payload
-import com.stripe.android.financialconnections.features.networkinglinkloginwarmup.NetworkingLinkLoginWarmupViewModel.Companion.PANE
-import com.stripe.android.financialconnections.presentation.parentViewModel
+import com.stripe.android.financialconnections.features.common.ShapedIcon
 import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
-import com.stripe.android.financialconnections.ui.TextResource.StringId
-import com.stripe.android.financialconnections.ui.components.AnnotatedText
-import com.stripe.android.financialconnections.ui.components.FinancialConnectionsScaffold
-import com.stripe.android.financialconnections.ui.components.FinancialConnectionsTopAppBar
-import com.stripe.android.financialconnections.ui.components.StringAnnotation
-import com.stripe.android.financialconnections.ui.components.clickableSingle
-import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsButton
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsButton.Type
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
+import com.stripe.android.financialconnections.ui.theme.LazyLayout
+import com.stripe.android.financialconnections.ui.theme.LinkColors
 
 @Composable
-internal fun NetworkingLinkLoginWarmupScreen() {
-    val viewModel: NetworkingLinkLoginWarmupViewModel = mavericksViewModel()
-    val parentViewModel = parentViewModel()
-    val state = viewModel.collectAsState()
+internal fun NetworkingLinkLoginWarmupScreen(
+    backStackEntry: NavBackStackEntry,
+) {
+    val viewModel: NetworkingLinkLoginWarmupViewModel = mavericksViewModel(
+        argsFactory = { backStackEntry.arguments },
+    )
+    val state by viewModel.collectAsState()
     NetworkingLinkLoginWarmupContent(
-        state = state.value,
-        onCloseClick = { parentViewModel.onCloseWithConfirmationClick(PANE) },
-        onCloseFromErrorClick = parentViewModel::onCloseFromErrorClick,
-        onClickableTextClick = viewModel::onClickableTextClick,
+        state = state,
+        onSkipClicked = viewModel::onSkipClicked,
         onContinueClick = viewModel::onContinueClick,
     )
 }
@@ -69,197 +64,140 @@ internal fun NetworkingLinkLoginWarmupScreen() {
 @Composable
 private fun NetworkingLinkLoginWarmupContent(
     state: NetworkingLinkLoginWarmupState,
-    onCloseClick: () -> Unit,
     onContinueClick: () -> Unit,
-    onCloseFromErrorClick: (Throwable) -> Unit,
-    onClickableTextClick: (String) -> Unit,
+    onSkipClicked: () -> Unit,
 ) {
-    val scrollState = rememberScrollState()
-    FinancialConnectionsScaffold(
-        topBar = {
-            FinancialConnectionsTopAppBar(
-                showBack = true,
-                onCloseClick = onCloseClick
-            )
-        }
-    ) {
-        when (val payload = state.payload) {
-            Uninitialized, is Loading -> NetworkingLinkLoginWarmupLoading()
-            is Success -> when (val disableNetworking = state.disableNetworkingAsync) {
-                is Loading -> NetworkingLinkLoginWarmupLoading()
-                is Uninitialized,
-                is Success -> NetworkingLinkLoginWarmupLoaded(
-                    scrollState = scrollState,
-                    payload = payload(),
-                    onClickableTextClick = onClickableTextClick,
-                    onContinueClick = onContinueClick
-                )
-
-                is Fail -> UnclassifiedErrorContent(
-                    error = disableNetworking.error,
-                    onCloseFromErrorClick = onCloseFromErrorClick
-                )
-            }
-
-            is Fail -> UnclassifiedErrorContent(
-                error = payload.error,
-                onCloseFromErrorClick = onCloseFromErrorClick
+    val lazyListState = rememberLazyListState()
+    LazyLayout(
+        modifier = Modifier
+            .fillMaxWidth()
+            .background(color = colors.backgroundSurface)
+            .padding(top = 24.dp),
+        inModal = true,
+        verticalArrangement = Arrangement.spacedBy(24.dp),
+        lazyListState = lazyListState,
+        body = {
+            item { HeaderSection() }
+            item { ExistingEmailSection(email = state.payload()?.email ?: "") }
+        },
+        footer = {
+            Footer(
+                loading = state.disableNetworkingAsync is Loading || state.payload() == null,
+                onContinueClick = onContinueClick,
+                onSkipClicked = onSkipClicked
             )
         }
-    }
+    )
 }
 
 @Composable
-private fun NetworkingLinkLoginWarmupLoading() {
-    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
-        CircularProgressIndicator(
-            color = FinancialConnectionsTheme.colors.iconBrand
+private fun HeaderSection() {
+    Column(
+        verticalArrangement = Arrangement.spacedBy(16.dp),
+    ) {
+        ShapedIcon(
+            painter = painterResource(id = R.drawable.stripe_ic_person),
+            contentDescription = stringResource(R.string.stripe_networking_link_login_warmup_title)
+        )
+        Text(
+            text = stringResource(R.string.stripe_networking_link_login_warmup_title),
+            style = typography.headingLarge,
+        )
+        Text(
+            text = stringResource(R.string.stripe_networking_link_login_warmup_description),
+            style = typography.bodyMedium,
         )
     }
 }
 
 @Composable
-private fun NetworkingLinkLoginWarmupLoaded(
-    scrollState: ScrollState,
-    payload: Payload,
-    onClickableTextClick: (String) -> Unit,
+@OptIn(ExperimentalComposeUiApi::class)
+private fun Footer(
+    loading: Boolean,
     onContinueClick: () -> Unit,
+    onSkipClicked: () -> Unit
 ) {
-    Column(
-        modifier = Modifier
-            .fillMaxSize()
-            .verticalScroll(scrollState)
-            .padding(
-                top = 0.dp,
-                start = 24.dp,
-                end = 24.dp,
-                bottom = 24.dp
-            )
-    ) {
+    Column {
+        FinancialConnectionsButton(
+            loading = false,
+            enabled = loading.not(),
+            type = Type.Primary,
+            onClick = onContinueClick,
+            modifier = Modifier
+                .semantics { testTagsAsResourceId = true }
+                .testTag("existing_email-button")
+                .fillMaxWidth()
+        ) {
+            Text(text = stringResource(id = R.string.stripe_networking_link_login_warmup_cta_continue))
+        }
         Spacer(modifier = Modifier.size(16.dp))
-        Title(
-            onClickableTextClick = onClickableTextClick
-        )
-        Spacer(modifier = Modifier.size(8.dp))
-        Description(onClickableTextClick)
-        Spacer(modifier = Modifier.size(24.dp))
-        ExistingEmailSection(
-            email = payload.email,
-            onContinueClick = onContinueClick
-        )
-        Spacer(modifier = Modifier.size(20.dp))
-        SkipSignIn(onClickableTextClick)
+        FinancialConnectionsButton(
+            loading = false,
+            enabled = loading.not(),
+            type = Type.Secondary,
+            onClick = { onSkipClicked() },
+            modifier = Modifier
+                .semantics { testTagsAsResourceId = true }
+                .testTag("skip-button")
+                .fillMaxWidth()
+        ) {
+            Text(text = stringResource(id = R.string.stripe_networking_link_login_warmup_cta_skip))
+        }
     }
 }
 
-@Composable
-private fun SkipSignIn(onClickableTextClick: (String) -> Unit) {
-    AnnotatedText(
-        text = StringId(R.string.stripe_networking_link_login_warmup_skip),
-        defaultStyle = FinancialConnectionsTheme.typography.caption.copy(
-            color = FinancialConnectionsTheme.colors.textSecondary
-        ),
-        annotationStyles = mapOf(
-            StringAnnotation.CLICKABLE to FinancialConnectionsTheme.typography.captionEmphasized
-                .toSpanStyle()
-                .copy(color = FinancialConnectionsTheme.colors.textBrand),
-        ),
-        onClickableTextClick = onClickableTextClick,
-    )
-}
-
-@Composable
-private fun Description(onClickableTextClick: (String) -> Unit) {
-    AnnotatedText(
-        text = StringId(R.string.stripe_networking_link_login_warmup_description),
-        defaultStyle = FinancialConnectionsTheme.typography.body.copy(
-            color = FinancialConnectionsTheme.colors.textSecondary
-        ),
-        annotationStyles = mapOf(
-            StringAnnotation.CLICKABLE to FinancialConnectionsTheme.typography.body
-                .toSpanStyle()
-                .copy(color = FinancialConnectionsTheme.colors.textBrand),
-        ),
-        onClickableTextClick = onClickableTextClick,
-    )
-}
-
-@Composable
-private fun Title(onClickableTextClick: (String) -> Unit) {
-    AnnotatedText(
-        text = StringId(R.string.stripe_networking_link_login_warmup_title),
-        defaultStyle = FinancialConnectionsTheme.typography.subtitle,
-        annotationStyles = mapOf(
-            StringAnnotation.CLICKABLE to FinancialConnectionsTheme.typography.subtitle
-                .toSpanStyle()
-                .copy(color = FinancialConnectionsTheme.colors.textBrand),
-        ),
-        onClickableTextClick = onClickableTextClick,
-    )
-}
-
 @OptIn(ExperimentalComposeUiApi::class)
 @Composable
 internal fun ExistingEmailSection(
-    email: String,
-    onContinueClick: () -> Unit
+    email: String
 ) {
     Row(
         verticalAlignment = Alignment.CenterVertically,
         modifier = Modifier
             .fillMaxWidth()
             .semantics { testTagsAsResourceId = true }
-            .testTag("existing_email-button")
-            .clickableSingle { onContinueClick() }
-            .clip(RoundedCornerShape(8.dp))
+            .clip(RoundedCornerShape(12.dp))
             .border(
                 width = 1.dp,
-                color = FinancialConnectionsTheme.colors.borderDefault,
-                shape = RoundedCornerShape(8.dp)
+                color = colors.border,
+                shape = RoundedCornerShape(12.dp)
             )
-            .padding(12.dp)
+            .padding(horizontal = 16.dp, vertical = 12.dp)
     ) {
-        Column(
-            Modifier.weight(1f)
+        Box(
+            contentAlignment = Alignment.Center,
+            modifier = Modifier
+                .size(32.dp)
+                .background(color = LinkColors.Brand200, shape = CircleShape)
         ) {
             Text(
-                text = stringResource(id = R.string.stripe_networking_link_login_warmup_email_label),
-                style = FinancialConnectionsTheme.typography.caption,
-                color = FinancialConnectionsTheme.colors.textSecondary
-            )
-            Text(
-                text = email,
-                maxLines = 1,
-                overflow = TextOverflow.Ellipsis,
-                style = FinancialConnectionsTheme.typography.body,
-                color = FinancialConnectionsTheme.colors.textPrimary
+                text = email.getOrElse(0) { '@' }.uppercaseChar().toString(),
+                style = typography.bodySmall,
+                color = LinkColors.Brand600
             )
         }
-        Icon(
-            painter = painterResource(id = R.drawable.stripe_ic_arrow_right_circle),
-            tint = FinancialConnectionsTheme.colors.textBrand,
-            contentDescription = null
+        Spacer(modifier = Modifier.size(12.dp))
+        Text(
+            modifier = Modifier.weight(1f),
+            text = email,
+            maxLines = 1,
+            overflow = TextOverflow.Ellipsis,
+            style = typography.bodySmall,
+            color = colors.textDefault
         )
     }
 }
 
 @Composable
 @Preview(group = "NetworkingLinkLoginWarmup Pane", name = "Canonical")
-internal fun NetworkingLinkLoginWarmupScreenEnteringEmailPreview() {
+internal fun NetworkingLinkLoginWarmupScreenPreview(
+    @PreviewParameter(NetworkingLinkLoginWarmupPreviewParameterProvider::class) state: NetworkingLinkLoginWarmupState
+) {
     FinancialConnectionsPreview {
         NetworkingLinkLoginWarmupContent(
-            state = NetworkingLinkLoginWarmupState(
-                payload = Success(
-                    Payload(
-                        merchantName = "Test",
-                        email = "verylongemailthatshouldellipsize@gmail.com",
-                    )
-                ),
-            ),
-            onCloseClick = {},
-            onClickableTextClick = {},
+            state = state,
             onContinueClick = {},
-            onCloseFromErrorClick = {}
+            onSkipClicked = {},
         )
     }
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupViewModel.kt
index 1a53efbae21..30be9b816aa 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupViewModel.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupViewModel.kt
@@ -1,24 +1,25 @@
 package com.stripe.android.financialconnections.features.networkinglinkloginwarmup
 
+import android.os.Bundle
 import com.airbnb.mvrx.Async
 import com.airbnb.mvrx.MavericksState
 import com.airbnb.mvrx.MavericksViewModel
 import com.airbnb.mvrx.MavericksViewModelFactory
 import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.ViewModelContext
-import com.stripe.android.core.Logger
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.Click
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.PaneLoaded
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
-import com.stripe.android.financialconnections.analytics.logError
 import com.stripe.android.financialconnections.domain.DisableNetworking
 import com.stripe.android.financialconnections.domain.GetManifest
+import com.stripe.android.financialconnections.domain.HandleError
 import com.stripe.android.financialconnections.features.common.getBusinessName
 import com.stripe.android.financialconnections.features.common.getRedactedEmail
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
 import com.stripe.android.financialconnections.navigation.Destination
 import com.stripe.android.financialconnections.navigation.NavigationManager
+import com.stripe.android.financialconnections.navigation.PopUpToBehavior
 import com.stripe.android.financialconnections.navigation.destination
 import com.stripe.android.financialconnections.ui.FinancialConnectionsSheetNativeActivity
 import kotlinx.coroutines.launch
@@ -27,10 +28,10 @@ import javax.inject.Inject
 internal class NetworkingLinkLoginWarmupViewModel @Inject constructor(
     initialState: NetworkingLinkLoginWarmupState,
     private val eventTracker: FinancialConnectionsAnalyticsTracker,
+    private val handleError: HandleError,
     private val getManifest: GetManifest,
     private val disableNetworking: DisableNetworking,
-    private val navigationManager: NavigationManager,
-    private val logger: Logger
+    private val navigationManager: NavigationManager
 ) : MavericksViewModel(initialState) {
 
     init {
@@ -49,21 +50,21 @@ internal class NetworkingLinkLoginWarmupViewModel @Inject constructor(
         onAsync(
             NetworkingLinkLoginWarmupState::payload,
             onFail = { error ->
-                eventTracker.logError(
+                handleError(
                     extraMessage = "Error fetching payload",
                     error = error,
-                    logger = logger,
-                    pane = PANE
+                    pane = PANE,
+                    displayErrorScreen = true
                 )
             },
         )
         onAsync(
             NetworkingLinkLoginWarmupState::disableNetworkingAsync,
             onFail = { error ->
-                eventTracker.logError(
+                handleError(
                     extraMessage = "Error disabling networking",
                     error = error,
-                    logger = logger,
+                    displayErrorScreen = true,
                     pane = PANE
                 )
             },
@@ -75,34 +76,41 @@ internal class NetworkingLinkLoginWarmupViewModel @Inject constructor(
         navigationManager.tryNavigateTo(Destination.NetworkingLinkVerification(referrer = PANE))
     }
 
-    fun onClickableTextClick(text: String) = when (text) {
-        CLICKABLE_TEXT_SKIP_LOGIN -> onSkipClicked()
-        else -> logger.error("Unknown clicked text $text")
-    }
-
-    private fun onSkipClicked() {
+    fun onSkipClicked() {
         suspend {
             eventTracker.track(Click("click.skip_sign_in", PANE))
             disableNetworking().also {
+                val popUpToBehavior = determinePopUpToBehaviorForSkip()
                 navigationManager.tryNavigateTo(
-                    // skipping disables networking, which means
-                    // we don't want the user to navigate back to
-                    // the warm-up pane.
-                    it.nextPane.destination(referrer = PANE),
-                    popUpToCurrent = true,
-                    inclusive = true
+                    route = it.nextPane.destination(referrer = PANE),
+                    popUpTo = popUpToBehavior,
                 )
             }
         }.execute { copy(disableNetworkingAsync = it) }
     }
 
+    private suspend fun determinePopUpToBehaviorForSkip(): PopUpToBehavior {
+        // Skipping disables networking, which means we don't want the user to navigate back to
+        // the warm-up pane. Since the warmup pane is displayed as a bottom sheet, we need to
+        // pop up all the way to the pane that opened it.
+        val referrer = awaitState().referrer
+
+        return if (referrer != null) {
+            PopUpToBehavior.Route(
+                route = referrer.destination.fullRoute,
+                inclusive = true,
+            )
+        } else {
+            // Let's give it our best shot even though we don't know the referrer
+            PopUpToBehavior.Current(inclusive = true)
+        }
+    }
+
     companion object :
         MavericksViewModelFactory {
 
         internal val PANE = Pane.NETWORKING_LINK_LOGIN_WARMUP
 
-        private const val CLICKABLE_TEXT_SKIP_LOGIN = "skip_login"
-
         override fun create(
             viewModelContext: ViewModelContext,
             state: NetworkingLinkLoginWarmupState
@@ -119,10 +127,18 @@ internal class NetworkingLinkLoginWarmupViewModel @Inject constructor(
 }
 
 internal data class NetworkingLinkLoginWarmupState(
+    val referrer: Pane? = null,
     val payload: Async = Uninitialized,
     val disableNetworkingAsync: Async = Uninitialized,
 ) : MavericksState {
 
+    @Suppress("unused") // used by mavericks to create initial state.
+    constructor(args: Bundle?) : this(
+        referrer = Destination.referrer(args),
+        payload = Uninitialized,
+        disableNetworkingAsync = Uninitialized,
+    )
+
     data class Payload(
         val merchantName: String?,
         val email: String
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupPreviewParameterProvider.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupPreviewParameterProvider.kt
index 5ed031aa379..e40f4e89a7d 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupPreviewParameterProvider.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupPreviewParameterProvider.kt
@@ -14,54 +14,75 @@ internal class NetworkingLinkSignupPreviewParameterProvider :
     PreviewParameterProvider {
     override val values = sequenceOf(
         default(),
-        emailEntered()
+        emailEntered(),
+        invalidEmail()
     )
 
-    internal fun default(): NetworkingLinkSignupState {
-        return NetworkingLinkSignupState(
-            payload = Success(
-                NetworkingLinkSignupState.Payload(
-                    merchantName = "Test",
-                    emailController = EmailConfig.createController(""),
-                    phoneController = PhoneNumberController.createPhoneNumberController(
-                        initialValue = "",
-                        initiallySelectedCountryCode = null,
-                    ),
-                    content = networkingLinkSignupPane()
-                )
-            ),
-            validEmail = null,
-            validPhone = null,
-            lookupAccount = Uninitialized,
-            saveAccountToLink = Uninitialized
-        )
-    }
+    private fun default() = NetworkingLinkSignupState(
+        payload = Success(
+            NetworkingLinkSignupState.Payload(
+                merchantName = "Test",
+                emailController = EmailConfig.createController(""),
+                phoneController = PhoneNumberController.createPhoneNumberController(
+                    initialValue = "",
+                    initiallySelectedCountryCode = null,
+                ),
+                content = networkingLinkSignupPane()
+            )
+        ),
+        validEmail = null,
+        validPhone = null,
+        lookupAccount = Uninitialized,
+        saveAccountToLink = Uninitialized
+    )
 
-    private fun emailEntered(): NetworkingLinkSignupState {
-        return NetworkingLinkSignupState(
-            payload = Success(
-                NetworkingLinkSignupState.Payload(
-                    merchantName = "Test",
-                    emailController = EmailConfig.createController("email"),
-                    phoneController = PhoneNumberController.createPhoneNumberController(
-                        initialValue = "",
-                        initiallySelectedCountryCode = null,
-                    ),
-                    content = networkingLinkSignupPane()
-                )
-            ),
-            validEmail = "test@test.com",
-            validPhone = null,
-            lookupAccount = Success(
-                ConsumerSessionLookup(
-                    exists = false,
-                    consumerSession = null,
-                    errorMessage = null
-                )
-            ),
-            saveAccountToLink = Uninitialized
-        )
-    }
+    private fun emailEntered() = NetworkingLinkSignupState(
+        payload = Success(
+            NetworkingLinkSignupState.Payload(
+                merchantName = "Test",
+                emailController = EmailConfig.createController("valid@email.com"),
+                phoneController = PhoneNumberController.createPhoneNumberController(
+                    initialValue = "",
+                    initiallySelectedCountryCode = null,
+                ),
+                content = networkingLinkSignupPane()
+            )
+        ),
+        validEmail = "test@test.com",
+        validPhone = null,
+        lookupAccount = Success(
+            ConsumerSessionLookup(
+                exists = false,
+                consumerSession = null,
+                errorMessage = null
+            )
+        ),
+        saveAccountToLink = Uninitialized
+    )
+
+    private fun invalidEmail() = NetworkingLinkSignupState(
+        payload = Success(
+            NetworkingLinkSignupState.Payload(
+                merchantName = "Test",
+                emailController = EmailConfig.createController("invalid_email.com"),
+                phoneController = PhoneNumberController.createPhoneNumberController(
+                    initialValue = "",
+                    initiallySelectedCountryCode = null,
+                ),
+                content = networkingLinkSignupPane()
+            )
+        ),
+        validEmail = "test@test.com",
+        validPhone = null,
+        lookupAccount = Success(
+            ConsumerSessionLookup(
+                exists = false,
+                consumerSession = null,
+                errorMessage = null
+            )
+        ),
+        saveAccountToLink = Uninitialized
+    )
 
     private fun networkingLinkSignupPane() = NetworkingLinkSignupPane(
         aboveCta = "By saving your account to Link, you agree to Link’s Terms and Privacy Policy",
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupScreen.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupScreen.kt
index 5be3feec45f..d70ed75165a 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupScreen.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupScreen.kt
@@ -1,24 +1,38 @@
 package com.stripe.android.financialconnections.features.networkinglinksignup
 
 import androidx.activity.compose.BackHandler
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.tween
 import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.animateScrollBy
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
+import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.material.ModalBottomSheetState
+import androidx.compose.material.ModalBottomSheetValue
 import androidx.compose.material.Text
+import androidx.compose.material.rememberModalBottomSheetState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.platform.LocalUriHandler
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.semantics
@@ -35,11 +49,12 @@ import com.airbnb.mvrx.Success
 import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.compose.collectAsState
 import com.airbnb.mvrx.compose.mavericksViewModel
-import com.stripe.android.financialconnections.R
-import com.stripe.android.financialconnections.features.common.BulletItem
 import com.stripe.android.financialconnections.features.common.FullScreenGenericLoading
+import com.stripe.android.financialconnections.features.common.LegalDetailsBottomSheetContent
+import com.stripe.android.financialconnections.features.common.ListItem
 import com.stripe.android.financialconnections.features.common.UnclassifiedErrorContent
 import com.stripe.android.financialconnections.features.networkinglinksignup.NetworkingLinkSignupState.Payload
+import com.stripe.android.financialconnections.features.networkinglinksignup.NetworkingLinkSignupState.ViewEffect
 import com.stripe.android.financialconnections.features.networkinglinksignup.NetworkingLinkSignupState.ViewEffect.OpenUrl
 import com.stripe.android.financialconnections.features.networkinglinksignup.NetworkingLinkSignupViewModel.Companion.PANE
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
@@ -48,18 +63,22 @@ import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
 import com.stripe.android.financialconnections.ui.TextResource
 import com.stripe.android.financialconnections.ui.components.AnnotatedText
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsButton
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsModalBottomSheetLayout
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsScaffold
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsTopAppBar
-import com.stripe.android.financialconnections.ui.components.StringAnnotation
 import com.stripe.android.financialconnections.ui.components.elevation
 import com.stripe.android.financialconnections.ui.sdui.BulletUI
 import com.stripe.android.financialconnections.ui.sdui.fromHtml
-import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
+import com.stripe.android.financialconnections.ui.theme.Layout
 import com.stripe.android.financialconnections.ui.theme.StripeThemeForConnections
 import com.stripe.android.model.ConsumerSessionLookup
+import com.stripe.android.uicore.elements.DropDown
 import com.stripe.android.uicore.elements.PhoneNumberCollectionSection
 import com.stripe.android.uicore.elements.TextFieldController
 import com.stripe.android.uicore.elements.TextFieldSection
+import kotlinx.coroutines.launch
 
 @Composable
 internal fun NetworkingLinkSignupScreen() {
@@ -68,11 +87,15 @@ internal fun NetworkingLinkSignupScreen() {
     val state = viewModel.collectAsState()
     BackHandler(enabled = true) {}
     val uriHandler = LocalUriHandler.current
+    val bottomSheetState: ModalBottomSheetState = rememberModalBottomSheetState(
+        initialValue = ModalBottomSheetValue.Hidden
+    )
 
     state.value.viewEffect?.let { viewEffect ->
         LaunchedEffect(viewEffect) {
             when (viewEffect) {
                 is OpenUrl -> uriHandler.openUri(viewEffect.url)
+                is ViewEffect.OpenBottomSheet -> bottomSheetState.show()
             }
             viewModel.onViewEffectLaunched()
         }
@@ -80,6 +103,7 @@ internal fun NetworkingLinkSignupScreen() {
 
     NetworkingLinkSignupContent(
         state = state.value,
+        bottomSheetState = bottomSheetState,
         onCloseClick = { parentViewModel.onCloseWithConfirmationClick(PANE) },
         onCloseFromErrorClick = parentViewModel::onCloseFromErrorClick,
         onClickableTextClick = viewModel::onClickableTextClick,
@@ -90,20 +114,57 @@ internal fun NetworkingLinkSignupScreen() {
 
 @Composable
 private fun NetworkingLinkSignupContent(
+    bottomSheetState: ModalBottomSheetState,
     state: NetworkingLinkSignupState,
     onCloseClick: () -> Unit,
     onCloseFromErrorClick: (Throwable) -> Unit,
     onClickableTextClick: (String) -> Unit,
     onSaveToLink: () -> Unit,
     onSkipClick: () -> Unit
+) {
+    val coroutineScope = rememberCoroutineScope()
+
+    FinancialConnectionsModalBottomSheetLayout(
+        sheetState = bottomSheetState,
+        sheetContent = {
+            when (val legalDetails = state.payload()?.content?.legalDetailsNotice) {
+                null -> {}
+                else -> LegalDetailsBottomSheetContent(
+                    legalDetails = legalDetails,
+                    onConfirmModalClick = { coroutineScope.launch { bottomSheetState.hide() } },
+                    onClickableTextClick = onClickableTextClick
+                )
+            }
+        },
+        content = {
+            NetworkingLinkSignupMainContent(
+                onCloseClick = onCloseClick,
+                state = state,
+                onSaveToLink = onSaveToLink,
+                onClickableTextClick = onClickableTextClick,
+                onSkipClick = onSkipClick,
+                onCloseFromErrorClick = onCloseFromErrorClick
+            )
+        }
+    )
+}
+
+@Composable
+private fun NetworkingLinkSignupMainContent(
+    onCloseClick: () -> Unit,
+    state: NetworkingLinkSignupState,
+    onSaveToLink: () -> Unit,
+    onClickableTextClick: (String) -> Unit,
+    onSkipClick: () -> Unit,
+    onCloseFromErrorClick: (Throwable) -> Unit
 ) {
     val scrollState = rememberScrollState()
     FinancialConnectionsScaffold(
         topBar = {
             FinancialConnectionsTopAppBar(
-                showBack = false,
+                elevation = scrollState.elevation,
+                allowBackNavigation = false,
                 onCloseClick = onCloseClick,
-                elevation = scrollState.elevation
             )
         }
     ) {
@@ -111,7 +172,7 @@ private fun NetworkingLinkSignupContent(
             Uninitialized, is Loading -> FullScreenGenericLoading()
             is Success -> NetworkingLinkSignupLoaded(
                 scrollState = scrollState,
-                validForm = state.valid(),
+                validForm = state.valid,
                 payload = payload(),
                 lookupAccountSync = state.lookupAccount,
                 saveAccountToLinkSync = state.saveAccountToLink,
@@ -141,63 +202,87 @@ private fun NetworkingLinkSignupLoaded(
     onSaveToLink: () -> Unit,
     onSkipClick: () -> Unit
 ) {
-    Column(
-        Modifier.fillMaxSize()
-    ) {
-        Column(
-            modifier = Modifier
-                .weight(1f)
-                .verticalScroll(scrollState)
-                .padding(
-                    top = 0.dp,
-                    start = 24.dp,
-                    end = 24.dp,
-                    bottom = 24.dp
-                )
-        ) {
-            Spacer(modifier = Modifier.size(16.dp))
+    val phoneNumberFocusRequester = remember { FocusRequester() }
+
+    LaunchedEffect(showFullForm) {
+        if (showFullForm) {
+            scrollState.animateScrollToBottom()
+            phoneNumberFocusRequester.requestFocus()
+        }
+    }
+
+    Layout(
+        scrollState = scrollState,
+        body = {
             Title(payload.content.title)
-            Spacer(modifier = Modifier.size(8.dp))
-            payload.content.body.bullets.forEach {
-                BulletItem(
-                    bullet = BulletUI.from(it),
+            Spacer(modifier = Modifier.size(24.dp))
+
+            for (bullet in payload.content.body.bullets) {
+                ListItem(
+                    bullet = BulletUI.from(bullet),
                     onClickableTextClick = onClickableTextClick
                 )
-                Spacer(modifier = Modifier.size(8.dp))
+                Spacer(modifier = Modifier.size(16.dp))
             }
+
             EmailSection(
                 showFullForm = showFullForm,
                 loading = lookupAccountSync is Loading,
                 emailController = payload.emailController,
                 enabled = true,
             )
-            if (showFullForm) {
+
+            AnimatedVisibility(showFullForm) {
                 PhoneNumberSection(
                     payload = payload,
-                    onClickableTextClick = onClickableTextClick
+                    focusRequester = phoneNumberFocusRequester,
                 )
             }
-            Spacer(modifier = Modifier.weight(1f))
-            Spacer(modifier = Modifier.height(16.dp))
-            if (showFullForm) {
-                SaveToLinkCta(
-                    text = payload.content.cta,
-                    aboveCta = payload.content.aboveCta,
-                    onClickableTextClick = onClickableTextClick,
-                    saveAccountToLinkSync = saveAccountToLinkSync,
-                    validForm = validForm,
-                    onSaveToLink = onSaveToLink
-                )
-            }
-            Spacer(modifier = Modifier.size(12.dp))
-            SkipCta(payload.content.skipCta, onSkipClick)
+        },
+        footer = {
+            NetworkingLinkSignupFooter(
+                payload = payload,
+                onClickableTextClick = onClickableTextClick,
+                saveAccountToLinkSync = saveAccountToLinkSync,
+                validForm = validForm,
+                onSaveToLink = onSaveToLink,
+                onSkipClick = onSkipClick
+            )
         }
-    }
+    )
 }
 
 @OptIn(ExperimentalComposeUiApi::class)
 @Composable
-private fun SkipCta(text: String, onSkipClick: () -> Unit) {
+private fun NetworkingLinkSignupFooter(
+    payload: Payload,
+    onClickableTextClick: (String) -> Unit,
+    saveAccountToLinkSync: Async,
+    validForm: Boolean,
+    onSaveToLink: () -> Unit,
+    onSkipClick: () -> Unit
+) = Column {
+    AnnotatedText(
+        modifier = Modifier.fillMaxWidth(),
+        text = TextResource.Text(fromHtml(payload.content.aboveCta)),
+        onClickableTextClick = onClickableTextClick,
+        defaultStyle = typography.labelSmall.copy(
+            textAlign = TextAlign.Center,
+            color = colors.textDefault
+        )
+    )
+    Spacer(modifier = Modifier.size(16.dp))
+    FinancialConnectionsButton(
+        loading = saveAccountToLinkSync is Loading,
+        enabled = validForm,
+        type = FinancialConnectionsButton.Type.Primary,
+        onClick = onSaveToLink,
+        modifier = Modifier
+            .fillMaxWidth()
+    ) {
+        Text(text = payload.content.cta)
+    }
+    Spacer(modifier = Modifier.size(8.dp))
     FinancialConnectionsButton(
         type = FinancialConnectionsButton.Type.Secondary,
         onClick = onSkipClick,
@@ -206,80 +291,39 @@ private fun SkipCta(text: String, onSkipClick: () -> Unit) {
             .semantics { testTagsAsResourceId = true }
             .testTag("skip_cta")
     ) {
-        Text(text = text)
-    }
-}
-
-@Composable
-private fun SaveToLinkCta(
-    aboveCta: String,
-    text: String,
-    onClickableTextClick: (String) -> Unit,
-    saveAccountToLinkSync: Async,
-    validForm: Boolean,
-    onSaveToLink: () -> Unit
-) {
-    Column {
-        AnnotatedText(
-            modifier = Modifier.fillMaxWidth(),
-            text = TextResource.Text(fromHtml(aboveCta)),
-            onClickableTextClick = onClickableTextClick,
-            defaultStyle = FinancialConnectionsTheme.typography.caption.copy(
-                textAlign = TextAlign.Center,
-                color = FinancialConnectionsTheme.colors.textSecondary
-            ),
-            annotationStyles = mapOf(
-                StringAnnotation.CLICKABLE to FinancialConnectionsTheme.typography.captionEmphasized
-                    .toSpanStyle()
-                    .copy(color = FinancialConnectionsTheme.colors.textBrand),
-                StringAnnotation.BOLD to FinancialConnectionsTheme.typography.captionEmphasized
-                    .toSpanStyle()
-                    .copy(color = FinancialConnectionsTheme.colors.textSecondary),
-            )
-        )
-        Spacer(modifier = Modifier.size(8.dp))
-        FinancialConnectionsButton(
-            loading = saveAccountToLinkSync is Loading,
-            enabled = validForm,
-            type = FinancialConnectionsButton.Type.Primary,
-            onClick = onSaveToLink,
-            modifier = Modifier
-                .fillMaxWidth()
-        ) {
-            Text(text = text)
-        }
+        Text(text = payload.content.skipCta)
     }
 }
 
 @Composable
 private fun PhoneNumberSection(
     payload: Payload,
-    onClickableTextClick: (String) -> Unit
+    focusRequester: FocusRequester,
 ) {
+    var focused by remember { mutableStateOf(false) }
     Column {
         StripeThemeForConnections {
             PhoneNumberCollectionSection(
-                requestFocusWhenShown = payload.phoneController.initialPhoneNumber.isEmpty(),
+                modifier = Modifier.onFocusChanged { focused = it.isFocused },
+                countryDropdown = {
+                    DropDown(
+                        controller = payload.phoneController.countryDropdownController,
+                        enabled = true,
+                        showChevron = false,
+                        modifier = Modifier
+                            .padding(horizontal = 6.dp)
+                            .clip(RoundedCornerShape(8.dp))
+                            .background(colors.background)
+                            .padding(vertical = 12.dp, horizontal = 8.dp)
+                    )
+                },
+                isSelected = focused,
                 phoneNumberController = payload.phoneController,
                 imeAction = ImeAction.Default,
+                focusRequester = focusRequester,
                 enabled = true,
             )
         }
-        AnnotatedText(
-            text = TextResource.StringId(R.string.stripe_networking_signup_phone_number_disclaimer),
-            onClickableTextClick = onClickableTextClick,
-            defaultStyle = FinancialConnectionsTheme.typography.caption.copy(
-                color = FinancialConnectionsTheme.colors.textSecondary
-            ),
-            annotationStyles = mapOf(
-                StringAnnotation.CLICKABLE to FinancialConnectionsTheme.typography.captionEmphasized
-                    .toSpanStyle()
-                    .copy(color = FinancialConnectionsTheme.colors.textBrand),
-                StringAnnotation.BOLD to FinancialConnectionsTheme.typography.captionEmphasized
-                    .toSpanStyle()
-                    .copy(color = FinancialConnectionsTheme.colors.textSecondary),
-            )
-        )
     }
 }
 
@@ -287,12 +331,7 @@ private fun PhoneNumberSection(
 private fun Title(title: String) {
     AnnotatedText(
         text = TextResource.Text(fromHtml(title)),
-        defaultStyle = FinancialConnectionsTheme.typography.subtitle,
-        annotationStyles = mapOf(
-            StringAnnotation.CLICKABLE to FinancialConnectionsTheme.typography.subtitle
-                .toSpanStyle()
-                .copy(color = FinancialConnectionsTheme.colors.textBrand),
-        ),
+        defaultStyle = typography.headingXLarge,
         onClickableTextClick = {},
     )
 }
@@ -304,6 +343,7 @@ internal fun EmailSection(
     showFullForm: Boolean,
     loading: Boolean
 ) {
+    var focused by remember { mutableStateOf(false) }
     StripeThemeForConnections {
         Box(
             modifier = Modifier
@@ -312,12 +352,10 @@ internal fun EmailSection(
             contentAlignment = Alignment.CenterEnd
         ) {
             TextFieldSection(
+                modifier = Modifier.onFocusChanged { focused = it.isFocused },
+                isSelected = focused,
                 textFieldController = emailController,
-                imeAction = if (showFullForm) {
-                    ImeAction.Next
-                } else {
-                    ImeAction.Done
-                },
+                imeAction = if (showFullForm) ImeAction.Next else ImeAction.Done,
                 enabled = enabled
             )
             if (loading) {
@@ -330,7 +368,7 @@ internal fun EmailSection(
                             end = 16.dp,
                             bottom = 8.dp
                         ),
-                    color = FinancialConnectionsTheme.colors.iconBrand,
+                    color = colors.iconBrand,
                     strokeWidth = 2.dp
                 )
             }
@@ -338,6 +376,12 @@ internal fun EmailSection(
     }
 }
 
+private suspend fun ScrollState.animateScrollToBottom(
+    animationSpec: AnimationSpec = tween(),
+) {
+    animateScrollBy(Float.MAX_VALUE, animationSpec)
+}
+
 @Composable
 @Preview(group = "NetworkingLinkSignup Pane")
 internal fun NetworkingLinkSignupScreenPreview(
@@ -347,6 +391,9 @@ internal fun NetworkingLinkSignupScreenPreview(
     FinancialConnectionsPreview {
         NetworkingLinkSignupContent(
             state = state,
+            bottomSheetState = rememberModalBottomSheetState(
+                initialValue = ModalBottomSheetValue.Hidden
+            ),
             onCloseClick = {},
             onSaveToLink = {},
             onClickableTextClick = {},
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupViewModel.kt
index 8fefbac9436..6fd7bb09f4b 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupViewModel.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupViewModel.kt
@@ -8,6 +8,7 @@ import com.airbnb.mvrx.MavericksViewModelFactory
 import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.ViewModelContext
 import com.stripe.android.core.Logger
+import com.stripe.android.financialconnections.R
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.Click
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.NetworkingNewConsumer
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.NetworkingReturningConsumer
@@ -20,6 +21,7 @@ import com.stripe.android.financialconnections.domain.LookupAccount
 import com.stripe.android.financialconnections.domain.SaveAccountToLink
 import com.stripe.android.financialconnections.domain.SynchronizeFinancialConnectionsSession
 import com.stripe.android.financialconnections.features.common.getBusinessName
+import com.stripe.android.financialconnections.features.networkinglinksignup.NetworkingLinkSignupState.ViewEffect.OpenBottomSheet
 import com.stripe.android.financialconnections.features.networkinglinksignup.NetworkingLinkSignupState.ViewEffect.OpenUrl
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
@@ -43,7 +45,6 @@ import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
-import java.security.InvalidParameterException
 import java.util.Date
 import javax.inject.Inject
 
@@ -72,8 +73,11 @@ internal class NetworkingLinkSignupViewModel @Inject constructor(
             NetworkingLinkSignupState.Payload(
                 content = content,
                 merchantName = manifest.getBusinessName(),
-                emailController = EmailConfig
-                    .createController(manifest.accountholderCustomerEmailAddress),
+                emailController = SimpleTextFieldController(
+                    textFieldConfig = EmailConfig(label = R.string.stripe_networking_signup_email_label),
+                    initialValue = manifest.accountholderCustomerEmailAddress,
+                    showOptionalLabel = false
+                ),
                 phoneController = PhoneNumberController
                     .createPhoneNumberController(manifest.accountholderPhoneNumber ?: ""),
             )
@@ -92,7 +96,7 @@ internal class NetworkingLinkSignupViewModel @Inject constructor(
             onSuccess = { consumerSession ->
                 if (consumerSession.exists) {
                     eventTracker.track(NetworkingReturningConsumer(PANE))
-                    navigationManager.tryNavigateTo(NetworkingSaveToLinkVerification(referrer = PANE))
+                    navigateToLinkVerification()
                 } else {
                     eventTracker.track(NetworkingNewConsumer(PANE))
                 }
@@ -113,6 +117,7 @@ internal class NetworkingLinkSignupViewModel @Inject constructor(
             NetworkingLinkSignupState::saveAccountToLink,
             onSuccess = {
                 saveToLinkWithStripeSucceeded.set(true)
+                navigationManager.tryNavigateTo(Success(referrer = PANE))
             },
             onFail = { error ->
                 saveToLinkWithStripeSucceeded.set(false)
@@ -188,23 +193,38 @@ internal class NetworkingLinkSignupViewModel @Inject constructor(
     }
 
     fun onSaveAccount() {
+        withState { state ->
+            eventTracker.track(Click(eventName = "click.save_to_link", pane = PANE))
+
+            val hasExistingAccount = state.lookupAccount()?.exists == true
+            if (hasExistingAccount) {
+                navigateToLinkVerification()
+            } else {
+                saveNewAccount()
+            }
+        }
+    }
+
+    private fun saveNewAccount() {
         suspend {
             eventTracker.track(Click(eventName = "click.save_to_link", pane = PANE))
             val state = awaitState()
             val selectedAccounts = getCachedAccounts()
             val phoneController = state.payload()!!.phoneController
-            require(state.valid()) { "Form invalid! ${state.validEmail} ${state.validPhone}" }
+            require(state.valid) { "Form invalid! ${state.validEmail} ${state.validPhone}" }
             saveAccountToLink.new(
                 country = phoneController.getCountryCode(),
                 email = state.validEmail!!,
                 phoneNumber = phoneController.getE164PhoneNumber(state.validPhone!!),
                 selectedAccounts = selectedAccounts.map { it.id },
-            ).also {
-                navigationManager.tryNavigateTo(Success(referrer = PANE))
-            }
+            )
         }.execute { copy(saveAccountToLink = it) }
     }
 
+    private fun navigateToLinkVerification() {
+        navigationManager.tryNavigateTo(NetworkingSaveToLinkVerification(referrer = PANE))
+    }
+
     fun onClickableTextClick(uri: String) = viewModelScope.launch {
         // if clicked uri contains an eventName query param, track click event.
         uriUtils.getQueryParameter(uri, "eventName")?.let { eventName ->
@@ -214,12 +234,17 @@ internal class NetworkingLinkSignupViewModel @Inject constructor(
         if (URLUtil.isNetworkUrl(uri)) {
             setState { copy(viewEffect = OpenUrl(uri, date.time)) }
         } else {
-            eventTracker.logError(
-                extraMessage = "Error clicking text",
-                logger = logger,
-                pane = PANE,
-                error = InvalidParameterException("Unrecognized clickable text: $uri")
-            )
+            val managedUri = NetworkingLinkSignupClickableText.values()
+                .firstOrNull { uriUtils.compareSchemeAuthorityAndPath(it.value, uri) }
+            when (managedUri) {
+                NetworkingLinkSignupClickableText.LEGAL_DETAILS -> {
+                    setState {
+                        copy(viewEffect = OpenBottomSheet(date.time))
+                    }
+                }
+
+                null -> logger.error("Unrecognized clickable text: $uri")
+            }
         }
     }
 
@@ -269,9 +294,11 @@ internal data class NetworkingLinkSignupState(
     val showFullForm: Boolean
         get() = lookupAccount()?.let { !it.exists } ?: false
 
-    fun valid(): Boolean {
-        return validEmail != null && validPhone != null
-    }
+    val valid: Boolean
+        get() {
+            val hasExistingAccount = lookupAccount()?.exists == true
+            return validEmail != null && (hasExistingAccount || validPhone != null)
+        }
 
     data class Payload(
         val merchantName: String?,
@@ -285,5 +312,13 @@ internal data class NetworkingLinkSignupState(
             val url: String,
             val id: Long
         ) : ViewEffect()
+
+        data class OpenBottomSheet(
+            val id: Long
+        ) : ViewEffect()
     }
 }
+
+private enum class NetworkingLinkSignupClickableText(val value: String) {
+    LEGAL_DETAILS("stripe://legal-details-notice"),
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkverification/NetworkingLinkVerificationPreviewParameterProvider.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkverification/NetworkingLinkVerificationPreviewParameterProvider.kt
new file mode 100644
index 00000000000..17b189bd825
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkverification/NetworkingLinkVerificationPreviewParameterProvider.kt
@@ -0,0 +1,65 @@
+package com.stripe.android.financialconnections.features.networkinglinkverification
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.Uninitialized
+import com.stripe.android.financialconnections.domain.ConfirmVerification
+import com.stripe.android.uicore.elements.IdentifierSpec
+import com.stripe.android.uicore.elements.OTPController
+import com.stripe.android.uicore.elements.OTPElement
+
+internal class NetworkingLinkVerificationPreviewParameterProvider :
+    PreviewParameterProvider {
+    override val values = sequenceOf(
+        loading(),
+        canonical(),
+        submitting(),
+        otpError(),
+        unknownError()
+    )
+
+    private fun canonical() = NetworkingLinkVerificationState(
+        payload = payload(),
+        confirmVerification = Uninitialized
+    )
+
+    private fun submitting() = NetworkingLinkVerificationState(
+        payload = payload(),
+        confirmVerification = Loading()
+    )
+
+    private fun otpError() = NetworkingLinkVerificationState(
+        payload = payload(),
+        confirmVerification = Fail(
+            ConfirmVerification.OTPError(
+                "12345678",
+                ConfirmVerification.OTPError.Type.EMAIL_CODE_EXPIRED
+            )
+        )
+    )
+
+    private fun unknownError() = NetworkingLinkVerificationState(
+        payload = payload(),
+        confirmVerification = Fail(
+            Exception("Random error")
+        )
+    )
+
+    private fun payload() = Success(
+        NetworkingLinkVerificationState.Payload(
+            email = "theLargestEmailYoulleverseeThatCouldBreakALayout@email.com",
+            phoneNumber = "12345678",
+            otpElement = OTPElement(
+                IdentifierSpec.Generic("otp"),
+                OTPController()
+            ),
+            consumerSessionClientSecret = "12345678"
+        )
+    )
+
+    private fun loading() = NetworkingLinkVerificationState(
+        payload = Loading(),
+    )
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkverification/NetworkingLinkVerificationScreen.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkverification/NetworkingLinkVerificationScreen.kt
index ee2134d53ad..1171fca952f 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkverification/NetworkingLinkVerificationScreen.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkverification/NetworkingLinkVerificationScreen.kt
@@ -3,23 +3,23 @@
 package com.stripe.android.financialconnections.features.networkinglinkverification
 
 import androidx.annotation.RestrictTo
-import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.LocalTextInputService
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
 import androidx.compose.ui.unit.dp
 import com.airbnb.mvrx.Async
 import com.airbnb.mvrx.Fail
@@ -29,24 +29,19 @@ import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.compose.collectAsState
 import com.airbnb.mvrx.compose.mavericksViewModel
 import com.stripe.android.financialconnections.R
-import com.stripe.android.financialconnections.domain.ConfirmVerification
-import com.stripe.android.financialconnections.domain.ConfirmVerification.OTPError.Type
+import com.stripe.android.financialconnections.domain.ConfirmVerification.OTPError
 import com.stripe.android.financialconnections.features.common.FullScreenGenericLoading
+import com.stripe.android.financialconnections.features.common.LoadingSpinner
 import com.stripe.android.financialconnections.features.common.UnclassifiedErrorContent
 import com.stripe.android.financialconnections.features.common.VerificationSection
 import com.stripe.android.financialconnections.features.networkinglinkverification.NetworkingLinkVerificationState.Payload
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
 import com.stripe.android.financialconnections.presentation.parentViewModel
 import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
-import com.stripe.android.financialconnections.ui.TextResource
-import com.stripe.android.financialconnections.ui.components.AnnotatedText
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsScaffold
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsTopAppBar
-import com.stripe.android.financialconnections.ui.components.StringAnnotation
 import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
-import com.stripe.android.uicore.elements.IdentifierSpec
-import com.stripe.android.uicore.elements.OTPController
-import com.stripe.android.uicore.elements.OTPElement
+import com.stripe.android.financialconnections.ui.theme.LazyLayout
 
 @Composable
 internal fun NetworkingLinkVerificationScreen() {
@@ -55,7 +50,7 @@ internal fun NetworkingLinkVerificationScreen() {
     val state = viewModel.collectAsState()
     NetworkingLinkVerificationContent(
         state = state.value,
-        onCloseClick = { parentViewModel.onCloseWithConfirmationClick(Pane.NETWORKING_LINK_SIGNUP_PANE) },
+        onCloseClick = { parentViewModel.onCloseWithConfirmationClick(Pane.NETWORKING_LINK_VERIFICATION) },
         onCloseFromErrorClick = parentViewModel::onCloseFromErrorClick,
     )
 }
@@ -66,7 +61,6 @@ private fun NetworkingLinkVerificationContent(
     onCloseClick: () -> Unit,
     onCloseFromErrorClick: (Throwable) -> Unit,
 ) {
-    val scrollState = rememberScrollState()
     FinancialConnectionsScaffold(
         topBar = {
             FinancialConnectionsTopAppBar(
@@ -77,9 +71,9 @@ private fun NetworkingLinkVerificationContent(
         when (val payload = state.payload) {
             Uninitialized, is Loading -> FullScreenGenericLoading()
             is Success -> NetworkingLinkVerificationLoaded(
-                scrollState = scrollState,
                 payload = payload(),
-                confirmVerificationAsync = state.confirmVerification
+                confirmVerificationAsync = state.confirmVerification,
+                onCloseFromErrorClick = onCloseFromErrorClick
             )
 
             is Fail -> UnclassifiedErrorContent(
@@ -93,8 +87,8 @@ private fun NetworkingLinkVerificationContent(
 @Composable
 private fun NetworkingLinkVerificationLoaded(
     confirmVerificationAsync: Async,
-    scrollState: ScrollState,
     payload: Payload,
+    onCloseFromErrorClick: (Throwable) -> Unit,
 ) {
     val focusManager = LocalFocusManager.current
     val focusRequester: FocusRequester = remember { FocusRequester() }
@@ -103,135 +97,68 @@ private fun NetworkingLinkVerificationLoaded(
     LaunchedEffect(confirmVerificationAsync) {
         if (confirmVerificationAsync is Loading) {
             focusManager.clearFocus(true)
-            @Suppress()
+            @Suppress("DEPRECATION")
             textInputService?.hideSoftwareKeyboard()
         }
     }
-    Column(
-        modifier = Modifier
-            .fillMaxSize()
-            .verticalScroll(scrollState)
-            .padding(
-                top = 0.dp,
-                start = 24.dp,
-                end = 24.dp,
-                bottom = 24.dp
-            )
-    ) {
-        Spacer(modifier = Modifier.size(16.dp))
-        Title()
-        Spacer(modifier = Modifier.size(8.dp))
-        Description(
-            phoneNumber = payload.phoneNumber
+    if (confirmVerificationAsync is Fail && confirmVerificationAsync.error !is OTPError) {
+        UnclassifiedErrorContent(
+            error = confirmVerificationAsync.error,
+            onCloseFromErrorClick = onCloseFromErrorClick
         )
-        Spacer(modifier = Modifier.size(24.dp))
-        VerificationSection(
-            focusRequester = focusRequester,
-            otpElement = payload.otpElement,
-            enabled = confirmVerificationAsync !is Loading,
-            confirmVerificationError = (confirmVerificationAsync as? Fail)?.error
+    } else {
+        LazyLayout(
+            verticalArrangement = Arrangement.spacedBy(24.dp),
+            body = {
+                item { Header(payload) }
+                item {
+                    VerificationSection(
+                        focusRequester = focusRequester,
+                        otpElement = payload.otpElement,
+                        enabled = confirmVerificationAsync !is Loading,
+                        confirmVerificationError = (confirmVerificationAsync as? Fail)?.error
+                    )
+                }
+                if (confirmVerificationAsync is Loading) {
+                    item {
+                        Box(
+                            modifier = Modifier.fillMaxWidth(),
+                            contentAlignment = Alignment.Center
+                        ) {
+                            LoadingSpinner(Modifier.size(24.dp))
+                        }
+                    }
+                }
+            }
         )
-        Spacer(modifier = Modifier.size(24.dp))
-        EmailSubtext(payload.email)
     }
 }
 
 @Composable
-private fun EmailSubtext(email: String) {
-    AnnotatedText(
-        text = TextResource.Text(
-            stringResource(
-                R.string.stripe_networking_verification_email,
-                email
-            )
-        ),
-        defaultStyle = FinancialConnectionsTheme.typography.caption.copy(
-            color = FinancialConnectionsTheme.colors.textDisabled
-        ),
-        annotationStyles = emptyMap(),
-        onClickableTextClick = {},
-    )
-}
-
-@Composable
-private fun Description(phoneNumber: String) {
-    AnnotatedText(
-        text = TextResource.StringId(
-            R.string.stripe_networking_verification_desc,
-            listOf(phoneNumber)
-        ),
-        defaultStyle = FinancialConnectionsTheme.typography.body.copy(
-            color = FinancialConnectionsTheme.colors.textSecondary
-        ),
-        annotationStyles = mapOf(
-            StringAnnotation.BOLD to FinancialConnectionsTheme.typography.bodyEmphasized
-                .toSpanStyle()
-                .copy(color = FinancialConnectionsTheme.colors.textSecondary),
-        ),
-        onClickableTextClick = {},
-    )
-}
-
-@Composable
-private fun Title() {
-    AnnotatedText(
-        text = TextResource.Text(
-            stringResource(R.string.stripe_networking_verification_title)
-        ),
-        defaultStyle = FinancialConnectionsTheme.typography.subtitle,
-        annotationStyles = emptyMap(),
-        onClickableTextClick = {},
-    )
-}
-
-@Composable
-@Preview(group = "NetworkingLinkVerification Pane", name = "Entering OTP")
-internal fun NetworkingLinkVerificationScreenPreview() {
-    FinancialConnectionsPreview {
-        NetworkingLinkVerificationContent(
-            state = NetworkingLinkVerificationState(
-                payload = Success(
-                    Payload(
-                        email = "email@gmail.com",
-                        phoneNumber = "12345678",
-                        otpElement = OTPElement(
-                            IdentifierSpec.Generic("otp"),
-                            OTPController()
-                        ),
-                        consumerSessionClientSecret = "12345678"
-                    )
-                )
-            ),
-            onCloseClick = {},
-            onCloseFromErrorClick = {}
+private fun Header(payload: Payload) {
+    Column(
+        verticalArrangement = Arrangement.spacedBy(16.dp),
+    ) {
+        Text(
+            text = stringResource(R.string.stripe_networking_verification_title),
+            style = FinancialConnectionsTheme.typography.headingXLarge,
+        )
+        Text(
+            text = stringResource(R.string.stripe_networking_verification_desc, payload.phoneNumber),
+            style = FinancialConnectionsTheme.typography.bodyMedium,
         )
     }
 }
 
 @Composable
-@Preview(group = "NetworkingLinkVerification Pane", name = "Error")
-internal fun NetworkingLinkVerificationScreenWithErrorPreview() {
+@Preview
+internal fun NetworkingLinkVerificationPreview(
+    @PreviewParameter(NetworkingLinkVerificationPreviewParameterProvider::class)
+    state: NetworkingLinkVerificationState
+) {
     FinancialConnectionsPreview {
         NetworkingLinkVerificationContent(
-            state = NetworkingLinkVerificationState(
-                confirmVerification = Fail(
-                    ConfirmVerification.OTPError(
-                        message = "consumer_verification_max_attempts_exceeded",
-                        type = Type.SMS_CODE_EXPIRED
-                    )
-                ),
-                payload = Success(
-                    Payload(
-                        email = "email@gmail.com",
-                        phoneNumber = "12345678",
-                        otpElement = OTPElement(
-                            IdentifierSpec.Generic("otp"),
-                            OTPController()
-                        ),
-                        consumerSessionClientSecret = "12345678"
-                    )
-                )
-            ),
+            state = state,
             onCloseClick = {},
             onCloseFromErrorClick = {}
         )
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkverification/NetworkingLinkVerificationViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkverification/NetworkingLinkVerificationViewModel.kt
index ecf181ab068..29009d7eae5 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkverification/NetworkingLinkVerificationViewModel.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkinglinkverification/NetworkingLinkVerificationViewModel.kt
@@ -133,7 +133,7 @@ internal class NetworkingLinkVerificationViewModel @Inject constructor(
             )
     }.execute { copy(confirmVerification = it) }
 
-    private suspend fun onNetworkedAccountsFailed(
+    private fun onNetworkedAccountsFailed(
         error: Throwable,
         updatedManifest: FinancialConnectionsSessionManifest
     ) {
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkingsavetolinkverification/NetworkingSaveToLinkVerificationPreviewParameterProvider.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkingsavetolinkverification/NetworkingSaveToLinkVerificationPreviewParameterProvider.kt
new file mode 100644
index 00000000000..129a76a2aed
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkingsavetolinkverification/NetworkingSaveToLinkVerificationPreviewParameterProvider.kt
@@ -0,0 +1,65 @@
+package com.stripe.android.financialconnections.features.networkingsavetolinkverification
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.Uninitialized
+import com.stripe.android.financialconnections.domain.ConfirmVerification
+import com.stripe.android.uicore.elements.IdentifierSpec
+import com.stripe.android.uicore.elements.OTPController
+import com.stripe.android.uicore.elements.OTPElement
+
+internal class NetworkingSaveToLinkVerificationPreviewParameterProvider :
+    PreviewParameterProvider {
+    override val values = sequenceOf(
+        loading(),
+        canonical(),
+        submitting(),
+        otpError(),
+        randomError()
+    )
+
+    private fun canonical() = NetworkingSaveToLinkVerificationState(
+        payload = payload(),
+        confirmVerification = Uninitialized
+    )
+
+    private fun submitting() = NetworkingSaveToLinkVerificationState(
+        payload = payload(),
+        confirmVerification = Loading()
+    )
+
+    private fun otpError() = NetworkingSaveToLinkVerificationState(
+        payload = payload(),
+        confirmVerification = Fail(
+            ConfirmVerification.OTPError(
+                "12345678",
+                ConfirmVerification.OTPError.Type.EMAIL_CODE_EXPIRED
+            )
+        )
+    )
+
+    private fun randomError() = NetworkingSaveToLinkVerificationState(
+        payload = payload(),
+        confirmVerification = Fail(
+            Exception("Random error")
+        )
+    )
+
+    private fun payload() = Success(
+        NetworkingSaveToLinkVerificationState.Payload(
+            email = "theLargestEmailYoulleverseeThatCouldBreakALayout@email.com",
+            phoneNumber = "12345678",
+            otpElement = OTPElement(
+                IdentifierSpec.Generic("otp"),
+                OTPController()
+            ),
+            consumerSessionClientSecret = "12345678"
+        )
+    )
+
+    private fun loading() = NetworkingSaveToLinkVerificationState(
+        payload = Loading(),
+    )
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkingsavetolinkverification/NetworkingSaveToLinkVerificationScreen.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkingsavetolinkverification/NetworkingSaveToLinkVerificationScreen.kt
index 6c22af653cc..1fd42a11e63 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkingsavetolinkverification/NetworkingSaveToLinkVerificationScreen.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkingsavetolinkverification/NetworkingSaveToLinkVerificationScreen.kt
@@ -3,25 +3,25 @@
 package com.stripe.android.financialconnections.features.networkingsavetolinkverification
 
 import androidx.annotation.RestrictTo
-import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.LocalTextInputService
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
 import androidx.compose.ui.unit.dp
 import com.airbnb.mvrx.Async
 import com.airbnb.mvrx.Fail
@@ -31,23 +31,21 @@ import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.compose.collectAsState
 import com.airbnb.mvrx.compose.mavericksViewModel
 import com.stripe.android.financialconnections.R
+import com.stripe.android.financialconnections.domain.ConfirmVerification
 import com.stripe.android.financialconnections.features.common.FullScreenGenericLoading
+import com.stripe.android.financialconnections.features.common.LoadingSpinner
 import com.stripe.android.financialconnections.features.common.UnclassifiedErrorContent
 import com.stripe.android.financialconnections.features.common.VerificationSection
 import com.stripe.android.financialconnections.features.networkingsavetolinkverification.NetworkingSaveToLinkVerificationState.Payload
 import com.stripe.android.financialconnections.features.networkingsavetolinkverification.NetworkingSaveToLinkVerificationViewModel.Companion.PANE
 import com.stripe.android.financialconnections.presentation.parentViewModel
 import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
-import com.stripe.android.financialconnections.ui.TextResource
-import com.stripe.android.financialconnections.ui.components.AnnotatedText
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsButton
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsScaffold
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsTopAppBar
-import com.stripe.android.financialconnections.ui.components.StringAnnotation
+import com.stripe.android.financialconnections.ui.components.elevation
 import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
-import com.stripe.android.uicore.elements.IdentifierSpec
-import com.stripe.android.uicore.elements.OTPController
-import com.stripe.android.uicore.elements.OTPElement
+import com.stripe.android.financialconnections.ui.theme.LazyLayout
 
 @Composable
 internal fun NetworkingSaveToLinkVerificationScreen() {
@@ -69,21 +67,22 @@ private fun NetworkingSaveToLinkVerificationContent(
     onSkipClick: () -> Unit,
     onCloseFromErrorClick: (Throwable) -> Unit,
 ) {
-    val scrollState = rememberScrollState()
+    val lazyListState = rememberLazyListState()
     FinancialConnectionsScaffold(
         topBar = {
             FinancialConnectionsTopAppBar(
-                showBack = true,
-                onCloseClick = onCloseClick
+                onCloseClick = onCloseClick,
+                elevation = rememberLazyListState().elevation
             )
         }
     ) {
         when (val payload = state.payload) {
             Uninitialized, is Loading -> FullScreenGenericLoading()
             is Success -> NetworkingSaveToLinkVerificationLoaded(
-                scrollState = scrollState,
+                lazyListState = lazyListState,
                 payload = payload(),
                 confirmVerificationAsync = state.confirmVerification,
+                onCloseFromErrorClick = onCloseFromErrorClick,
                 onSkipClick = onSkipClick
             )
 
@@ -98,8 +97,9 @@ private fun NetworkingSaveToLinkVerificationContent(
 @Composable
 private fun NetworkingSaveToLinkVerificationLoaded(
     confirmVerificationAsync: Async,
-    scrollState: ScrollState,
+    lazyListState: LazyListState,
     payload: Payload,
+    onCloseFromErrorClick: (Throwable) -> Unit,
     onSkipClick: () -> Unit,
 ) {
     val focusManager = LocalFocusManager.current
@@ -113,113 +113,81 @@ private fun NetworkingSaveToLinkVerificationLoaded(
         }
     }
     LaunchedEffect(Unit) { focusRequester.requestFocus() }
-    Column(
-        modifier = Modifier
-            .fillMaxSize()
-            .verticalScroll(scrollState)
-            .padding(
-                top = 0.dp,
-                start = 24.dp,
-                end = 24.dp,
-                bottom = 24.dp
-            )
-    ) {
-        Spacer(modifier = Modifier.size(16.dp))
-        Title()
-        Spacer(modifier = Modifier.size(8.dp))
-        Description(payload.phoneNumber)
-        Spacer(modifier = Modifier.size(24.dp))
-        VerificationSection(
-            focusRequester = focusRequester,
-            otpElement = payload.otpElement,
-            enabled = confirmVerificationAsync !is Loading,
-            confirmVerificationError = (confirmVerificationAsync as? Fail)?.error
+    if (confirmVerificationAsync is Fail && confirmVerificationAsync.error !is ConfirmVerification.OTPError) {
+        UnclassifiedErrorContent(
+            error = confirmVerificationAsync.error,
+            onCloseFromErrorClick = onCloseFromErrorClick
+        )
+    } else {
+        LazyLayout(
+            lazyListState = lazyListState,
+            verticalArrangement = Arrangement.spacedBy(24.dp),
+            body = {
+                item { Header(payload) }
+                item {
+                    VerificationSection(
+                        focusRequester = focusRequester,
+                        otpElement = payload.otpElement,
+                        enabled = confirmVerificationAsync !is Loading,
+                        confirmVerificationError = (confirmVerificationAsync as? Fail)?.error
+                    )
+                }
+                if (confirmVerificationAsync is Loading) {
+                    item {
+                        Box(
+                            modifier = Modifier.fillMaxWidth(),
+                            contentAlignment = Alignment.Center
+                        ) {
+                            LoadingSpinner(Modifier.size(24.dp))
+                        }
+                    }
+                }
+            },
+            footer = {
+                FinancialConnectionsButton(
+                    type = FinancialConnectionsButton.Type.Secondary,
+                    onClick = onSkipClick,
+                    modifier = Modifier
+                        .fillMaxWidth()
+                ) {
+                    Text(text = stringResource(R.string.stripe_networking_save_to_link_verification_cta_negative))
+                }
+            }
         )
-        Spacer(modifier = Modifier.size(24.dp))
-        EmailSubtext(payload.email)
-        Spacer(modifier = Modifier.weight(1f))
-        FinancialConnectionsButton(
-            type = FinancialConnectionsButton.Type.Secondary,
-            onClick = onSkipClick,
-            modifier = Modifier
-                .fillMaxWidth()
-        ) {
-            Text(text = stringResource(R.string.stripe_networking_save_to_link_verification_cta_negative))
-        }
     }
 }
 
 @Composable
-private fun EmailSubtext(email: String) {
-    AnnotatedText(
-        text = TextResource.Text(
-            stringResource(
-                R.string.stripe_networking_verification_email,
-                email
-            )
-        ),
-        defaultStyle = FinancialConnectionsTheme.typography.caption.copy(
-            color = FinancialConnectionsTheme.colors.textDisabled
-        ),
-        annotationStyles = emptyMap(),
-        onClickableTextClick = {},
-    )
-}
-
-@Composable
-private fun Description(phoneNumber: String) {
-    AnnotatedText(
-        text = TextResource.Text(
-            stringResource(
+private fun Header(payload: Payload) {
+    Column(
+        verticalArrangement = Arrangement.spacedBy(16.dp),
+    ) {
+        Text(
+            text = stringResource(R.string.stripe_networking_save_to_link_verification_title),
+            style = FinancialConnectionsTheme.typography.headingXLarge,
+        )
+        Text(
+            text = stringResource(
                 R.string.stripe_networking_verification_desc,
-                phoneNumber
-            )
-        ),
-        defaultStyle = FinancialConnectionsTheme.typography.body.copy(
-            color = FinancialConnectionsTheme.colors.textSecondary
-        ),
-        annotationStyles = mapOf(
-            StringAnnotation.BOLD to FinancialConnectionsTheme.typography.bodyEmphasized
-                .toSpanStyle()
-                .copy(color = FinancialConnectionsTheme.colors.textSecondary),
-        ),
-        onClickableTextClick = {},
-    )
-}
-
-@Composable
-private fun Title() {
-    AnnotatedText(
-        text = TextResource.Text(
-            stringResource(R.string.stripe_networking_save_to_link_verification_title)
-        ),
-        defaultStyle = FinancialConnectionsTheme.typography.subtitle,
-        annotationStyles = emptyMap(),
-        onClickableTextClick = {},
-    )
+                payload.phoneNumber
+            ),
+            style = FinancialConnectionsTheme.typography.bodyMedium,
+        )
+    }
 }
 
 @Composable
-@Preview(group = "NetworkingVerification", name = "Default")
-internal fun NetworkingSaveToLinkVerificationScreenPreview() {
+@Preview
+internal fun SaveToLinkVerificationPreview(
+    @PreviewParameter(NetworkingSaveToLinkVerificationPreviewParameterProvider::class)
+    state: NetworkingSaveToLinkVerificationState
+) {
     FinancialConnectionsPreview {
         NetworkingSaveToLinkVerificationContent(
-            state = NetworkingSaveToLinkVerificationState(
-                payload = Success(
-                    Payload(
-                        email = "12345678",
-                        phoneNumber = "12345678",
-                        otpElement = OTPElement(
-                            IdentifierSpec.Generic("otp"),
-                            OTPController()
-                        ),
-                        consumerSessionClientSecret = "12345678"
-                    )
-                )
-            ),
+            state = state,
             onCloseClick = {},
-            onCloseFromErrorClick = {},
-            onSkipClick = {}
+            onSkipClick = {},
+            onCloseFromErrorClick = {}
         )
     }
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkingsavetolinkverification/NetworkingSaveToLinkVerificationViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkingsavetolinkverification/NetworkingSaveToLinkVerificationViewModel.kt
index f0a01565900..d5e052cf79e 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkingsavetolinkverification/NetworkingSaveToLinkVerificationViewModel.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/networkingsavetolinkverification/NetworkingSaveToLinkVerificationViewModel.kt
@@ -29,6 +29,7 @@ import com.stripe.android.financialconnections.ui.FinancialConnectionsSheetNativ
 import com.stripe.android.uicore.elements.IdentifierSpec
 import com.stripe.android.uicore.elements.OTPController
 import com.stripe.android.uicore.elements.OTPElement
+import getRedactedPhoneNumber
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.launch
 import javax.inject.Inject
@@ -59,7 +60,7 @@ internal class NetworkingSaveToLinkVerificationViewModel @Inject constructor(
             eventTracker.track(PaneLoaded(PANE))
             NetworkingSaveToLinkVerificationState.Payload(
                 email = consumerSession.emailAddress,
-                phoneNumber = consumerSession.redactedPhoneNumber,
+                phoneNumber = consumerSession.getRedactedPhoneNumber(),
                 consumerSessionClientSecret = consumerSession.clientSecret,
                 otpElement = OTPElement(
                     IdentifierSpec.Generic("otp"),
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthPreviewParameterProvider.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthPreviewParameterProvider.kt
index bf366648aa1..052016136bc 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthPreviewParameterProvider.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthPreviewParameterProvider.kt
@@ -1,5 +1,3 @@
-@file:Suppress("LongMethod")
-
 package com.stripe.android.financialconnections.features.partnerauth
 
 import androidx.compose.ui.tooling.preview.PreviewParameterProvider
@@ -22,6 +20,7 @@ internal class PartnerAuthPreviewParameterProvider :
     PreviewParameterProvider {
     override val values = sequenceOf(
         canonical(),
+        prepaneLoading(),
         browserLoading()
     )
 
@@ -31,6 +30,7 @@ internal class PartnerAuthPreviewParameterProvider :
     private fun canonical() = SharedPartnerAuthState(
         payload = Success(
             SharedPartnerAuthState.Payload(
+                isStripeDirect = false,
                 institution = FinancialConnectionsInstitution(
                     id = "id",
                     name = "name",
@@ -42,7 +42,6 @@ internal class PartnerAuthPreviewParameterProvider :
                     mobileHandoffCapable = false
                 ),
                 authSession = session(),
-                isStripeDirect = false
             )
         ),
         authenticationStatus = Uninitialized,
@@ -51,9 +50,18 @@ internal class PartnerAuthPreviewParameterProvider :
         pane = Pane.PARTNER_AUTH
     )
 
+    private fun prepaneLoading() = SharedPartnerAuthState(
+        payload = Loading(),
+        authenticationStatus = Uninitialized,
+        viewEffect = null,
+        activeAuthSession = null,
+        pane = Pane.PARTNER_AUTH
+    )
+
     private fun browserLoading() = SharedPartnerAuthState(
         payload = Success(
             SharedPartnerAuthState.Payload(
+                isStripeDirect = false,
                 institution = FinancialConnectionsInstitution(
                     id = "id",
                     name = "name",
@@ -65,7 +73,6 @@ internal class PartnerAuthPreviewParameterProvider :
                     mobileHandoffCapable = false
                 ),
                 authSession = session(),
-                isStripeDirect = false
             )
         ),
         // While browser is showing, this Async is loading.
@@ -94,23 +101,14 @@ internal class PartnerAuthPreviewParameterProvider :
             "https://b.stripecdn.com/connections-statics-srv/assets/PrepaneAsset--account_numbers-capitalone-2x.gif"
         return OauthPrepane(
             title = "Sign in with Sample bank",
+            subtitle = "Next, you'll be prompted to log in and connect your accounts.",
             body = Body(
                 listOf(
-                    Entry.Text(
-                        "Some very large text will most likely go here!" +
-                            "Some very large text will most likely go here!"
-                    ),
                     Entry.Image(
                         Image(sampleImage)
                     ),
                     Entry.Text(
-                        "Some very large text will most likely go here!"
-                    ),
-                    Entry.Text(
-                        "Some very large text will most likely go here!"
-                    ),
-                    Entry.Text(
-                        "Some very large text will most likely go here!"
+                        "Dynamic content placeholder that will show below image."
                     )
                 )
             ),
@@ -118,7 +116,7 @@ internal class PartnerAuthPreviewParameterProvider :
                 icon = null,
                 text = "Continue!"
             ),
-            institutionIcon = null,
+            institutionIcon = Image("www.image.url"),
             partnerNotice = PartnerNotice(
                 partnerIcon = Image(sampleImage),
                 text = "Stripe works with partners like MX to reliably" +
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthScreen.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthScreen.kt
index 668bfc2d3d8..a5b11b0a818 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthScreen.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthScreen.kt
@@ -1,5 +1,3 @@
-@file:Suppress("LongMethod")
-
 package com.stripe.android.financialconnections.features.partnerauth
 
 import androidx.compose.runtime.Composable
@@ -9,15 +7,15 @@ import com.airbnb.mvrx.compose.mavericksViewModel
 import com.stripe.android.financialconnections.features.common.SharedPartnerAuth
 
 @Composable
-internal fun PartnerAuthScreen() {
+internal fun PartnerAuthScreen(inModal: Boolean) {
     val viewModel: PartnerAuthViewModel = mavericksViewModel()
     val state: State = viewModel.collectAsState()
 
     SharedPartnerAuth(
+        inModal = inModal,
         state = state.value,
         onContinueClick = viewModel::onLaunchAuthClick,
-        onSelectAnotherBank = viewModel::onSelectAnotherBank,
-        onEnterDetailsManually = viewModel::onEnterDetailsManuallyClick,
+        onCancelClick = viewModel::onCancelClick,
         onClickableTextClick = viewModel::onClickableTextClick,
         onWebAuthFlowFinished = viewModel::onWebAuthFlowFinished,
         onViewEffectLaunched = viewModel::onViewEffectLaunched
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthViewModel.kt
index e3f7c3bae02..2f314ec4b59 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthViewModel.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthViewModel.kt
@@ -27,12 +27,16 @@ import com.stripe.android.financialconnections.di.APPLICATION_ID
 import com.stripe.android.financialconnections.domain.CancelAuthorizationSession
 import com.stripe.android.financialconnections.domain.CompleteAuthorizationSession
 import com.stripe.android.financialconnections.domain.GetOrFetchSync
+import com.stripe.android.financialconnections.domain.HandleError
 import com.stripe.android.financialconnections.domain.PollAuthorizationSessionOAuthResults
 import com.stripe.android.financialconnections.domain.PostAuthSessionEvent
 import com.stripe.android.financialconnections.domain.PostAuthorizationSession
 import com.stripe.android.financialconnections.domain.RetrieveAuthorizationSession
+import com.stripe.android.financialconnections.exception.FinancialConnectionsError
+import com.stripe.android.financialconnections.exception.PartnerAuthError
 import com.stripe.android.financialconnections.exception.WebAuthFlowFailedException
 import com.stripe.android.financialconnections.features.common.enableRetrieveAuthSession
+import com.stripe.android.financialconnections.features.partnerauth.SharedPartnerAuthState.AuthenticationStatus.Action
 import com.stripe.android.financialconnections.features.partnerauth.SharedPartnerAuthState.Payload
 import com.stripe.android.financialconnections.features.partnerauth.SharedPartnerAuthState.ViewEffect
 import com.stripe.android.financialconnections.features.partnerauth.SharedPartnerAuthState.ViewEffect.OpenPartnerAuth
@@ -41,9 +45,8 @@ import com.stripe.android.financialconnections.model.FinancialConnectionsSession
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
 import com.stripe.android.financialconnections.model.SynchronizeSessionResponse
 import com.stripe.android.financialconnections.navigation.Destination.AccountPicker
-import com.stripe.android.financialconnections.navigation.Destination.ManualEntry
-import com.stripe.android.financialconnections.navigation.Destination.Reset
 import com.stripe.android.financialconnections.navigation.NavigationManager
+import com.stripe.android.financialconnections.navigation.PopUpToBehavior
 import com.stripe.android.financialconnections.navigation.destination
 import com.stripe.android.financialconnections.presentation.WebAuthFlowState
 import com.stripe.android.financialconnections.ui.FinancialConnectionsSheetNativeActivity
@@ -52,8 +55,8 @@ import kotlinx.coroutines.launch
 import java.util.Date
 import javax.inject.Inject
 import javax.inject.Named
+import com.stripe.android.financialconnections.features.partnerauth.SharedPartnerAuthState.AuthenticationStatus as Status
 
-@Suppress("LongParameterList")
 internal class PartnerAuthViewModel @Inject constructor(
     private val completeAuthorizationSession: CompleteAuthorizationSession,
     private val createAuthorizationSession: PostAuthorizationSession,
@@ -65,6 +68,7 @@ internal class PartnerAuthViewModel @Inject constructor(
     private val postAuthSessionEvent: PostAuthSessionEvent,
     private val getOrFetchSync: GetOrFetchSync,
     private val browserManager: BrowserManager,
+    private val handleError: HandleError,
     private val navigationManager: NavigationManager,
     private val pollAuthorizationSessionOAuthResults: PollAuthorizationSessionOAuthResults,
     private val logger: Logger,
@@ -72,64 +76,58 @@ internal class PartnerAuthViewModel @Inject constructor(
 ) : MavericksViewModel(initialState) {
 
     init {
-        logErrors()
-        withState {
-            if (it.activeAuthSession == null) {
-                launchBrowserIfNonOauth()
-                createAuthSession()
-            } else {
-                logger.debug("Restoring auth session ${it.activeAuthSession}")
-                restoreAuthSession()
-            }
-        }
+        handleErrors()
+        launchBrowserIfNonOauth()
+        restoreOrCreateAuthSession()
     }
 
-    private fun restoreAuthSession() {
-        suspend {
-            // if coming from a process kill, there should be a session
-            // re-fetch the manifest and use its active auth session instead of creating a new one
-            val sync: SynchronizeSessionResponse = getOrFetchSync()
-            val manifest: FinancialConnectionsSessionManifest = sync.manifest
-            val authSession = manifest.activeAuthSession ?: createAuthorizationSession(
-                institution = requireNotNull(manifest.activeInstitution),
-                sync = sync
-            )
-            Payload(
-                authSession = authSession,
-                institution = requireNotNull(manifest.activeInstitution),
-                isStripeDirect = manifest.isStripeDirect ?: false
-            )
-        }.execute { copy(payload = it) }
-    }
+    private fun restoreOrCreateAuthSession() = suspend {
+        // A session should have been created in the previous pane and set as the active
+        // auth session in the manifest.
+        // if coming from a process kill, we'll fetch the current manifest from network,
+        // that should contain the active auth session.
+        val sync: SynchronizeSessionResponse = getOrFetchSync()
+        val manifest: FinancialConnectionsSessionManifest = sync.manifest
+        val authSession = manifest.activeAuthSession ?: createAuthorizationSession(
+            institution = requireNotNull(manifest.activeInstitution),
+            sync = sync
+        )
+        Payload(
+            isStripeDirect = manifest.isStripeDirect ?: false,
+            institution = requireNotNull(manifest.activeInstitution),
+            authSession = authSession,
+        )
+    }.execute { copy(payload = it) }
 
-    private fun createAuthSession() {
-        suspend {
-            val launchedEvent = Launched(Date())
-            val sync: SynchronizeSessionResponse = getOrFetchSync()
-            val manifest: FinancialConnectionsSessionManifest = sync.manifest
-            val authSession = createAuthorizationSession(
-                institution = requireNotNull(manifest.activeInstitution),
-                sync = sync
-            )
-            logger.debug("Created auth session ${authSession.id}")
-            Payload(
-                authSession = authSession,
-                institution = requireNotNull(manifest.activeInstitution),
-                isStripeDirect = manifest.isStripeDirect ?: false
-            ).also {
-                // just send loaded event on OAuth flows (prepane). Non-OAuth handled by shim.
-                val loadedEvent: Loaded? = Loaded(Date()).takeIf { authSession.isOAuth }
-                postAuthSessionEvent(
-                    authSession.id,
-                    listOfNotNull(launchedEvent, loadedEvent)
-                )
-            }
-        }.execute {
-            copy(
-                payload = it,
-                activeAuthSession = it()?.authSession?.id
+    private fun recreateAuthSession() = suspend {
+        val launchedEvent = Launched(Date())
+        val sync: SynchronizeSessionResponse = getOrFetchSync()
+        val manifest: FinancialConnectionsSessionManifest = sync.manifest
+        val authSession = createAuthorizationSession(
+            institution = requireNotNull(manifest.activeInstitution),
+            sync = sync
+        )
+        logger.debug("Created auth session ${authSession.id}")
+        Payload(
+            authSession = authSession,
+            institution = requireNotNull(manifest.activeInstitution),
+            isStripeDirect = manifest.isStripeDirect ?: false
+        ).also {
+            // just send loaded event on OAuth flows (prepane). Non-OAuth handled by shim.
+            val loadedEvent: Loaded? = Loaded(Date()).takeIf { authSession.isOAuth }
+            postAuthSessionEvent(
+                authSession.id,
+                listOfNotNull(launchedEvent, loadedEvent)
             )
         }
+    }.execute(
+        // keeps existing payload to prevent showing full-screen loading.
+        retainValue = SharedPartnerAuthState::payload
+    ) {
+        copy(
+            payload = it,
+            activeAuthSession = it()?.authSession?.id
+        )
     }
 
     private fun launchBrowserIfNonOauth() {
@@ -142,22 +140,34 @@ internal class PartnerAuthViewModel @Inject constructor(
         )
     }
 
-    private fun logErrors() {
+    private fun handleErrors() {
         onAsync(
             SharedPartnerAuthState::payload,
             onFail = {
-                eventTracker.logError(
+                handleError(
                     extraMessage = "Error fetching payload / posting AuthSession",
                     error = it,
-                    logger = logger,
-                    pane = PANE
+                    pane = PANE,
+                    displayErrorScreen = true
                 )
             },
             onSuccess = { eventTracker.track(PaneLoaded(PANE)) }
         )
+        onAsync(
+            SharedPartnerAuthState::authenticationStatus,
+            onFail = {
+                handleError(
+                    extraMessage = "Error with authentication status",
+                    error = if (it is FinancialConnectionsError) it else PartnerAuthError(it.message),
+                    pane = PANE,
+                    displayErrorScreen = true
+                )
+            }
+        )
     }
 
     fun onLaunchAuthClick() {
+        setState { copy(authenticationStatus = Loading(value = Status(Action.AUTHENTICATING))) }
         viewModelScope.launch {
             awaitState().payload()?.authSession?.let {
                 postAuthSessionEvent(it.id, AuthSessionEvent.OAuthLaunched(Date()))
@@ -201,14 +211,6 @@ internal class PartnerAuthViewModel @Inject constructor(
     private fun FinancialConnectionsAuthorizationSession.browserReadyUrl(): String? =
         url?.replaceFirst("stripe-auth://native-redirect/$applicationId/", "")
 
-    fun onSelectAnotherBank() {
-        navigationManager.tryNavigateTo(
-            Reset(referrer = PANE),
-            popUpToCurrent = true,
-            inclusive = true
-        )
-    }
-
     fun onWebAuthFlowFinished(
         webStatus: WebAuthFlowState
     ) {
@@ -224,7 +226,11 @@ internal class PartnerAuthViewModel @Inject constructor(
                 }
 
                 WebAuthFlowState.InProgress -> {
-                    setState { copy(authenticationStatus = Loading()) }
+                    setState {
+                        copy(
+                            authenticationStatus = Loading(Status(Action.AUTHENTICATING))
+                        )
+                    }
                 }
 
                 is WebAuthFlowState.Success -> {
@@ -279,7 +285,7 @@ internal class PartnerAuthViewModel @Inject constructor(
     private suspend fun onAuthCancelled(url: String?) {
         kotlin.runCatching {
             logger.debug("Auth cancelled, cancelling AuthSession")
-            setState { copy(authenticationStatus = Loading()) }
+            setState { copy(authenticationStatus = Loading(value = Status(Action.AUTHENTICATING))) }
             val manifest = getOrFetchSync().manifest
             val authSession = manifest.activeAuthSession
             eventTracker.track(
@@ -339,21 +345,20 @@ internal class PartnerAuthViewModel @Inject constructor(
             // for OAuth institutions, we remain on the pre-pane,
             // but create a brand new auth session
             setState { copy(authenticationStatus = Uninitialized) }
-            createAuthSession()
+            recreateAuthSession()
         } else {
             // For non-OAuth institutions, navigate to Session cancellation's next pane.
             postAuthSessionEvent(authSession.id, AuthSessionEvent.Cancel(Date()))
             navigationManager.tryNavigateTo(
-                result.nextPane.destination(referrer = PANE),
-                popUpToCurrent = true,
-                inclusive = true
+                route = result.nextPane.destination(referrer = PANE),
+                popUpTo = PopUpToBehavior.Current(inclusive = true),
             )
         }
     }
 
     private suspend fun completeAuthorizationSession(url: String) {
         kotlin.runCatching {
-            setState { copy(authenticationStatus = Loading()) }
+            setState { copy(authenticationStatus = Loading(value = Status(Action.AUTHENTICATING))) }
             val authSession = getOrFetchSync().manifest.activeAuthSession
             eventTracker.track(
                 AuthSessionUrlReceived(
@@ -390,12 +395,6 @@ internal class PartnerAuthViewModel @Inject constructor(
         }
     }
 
-    fun onEnterDetailsManuallyClick() = navigationManager.tryNavigateTo(
-        ManualEntry(referrer = PANE),
-        popUpToCurrent = true,
-        inclusive = true
-    )
-
     // if clicked uri contains an eventName query param, track click event.
     fun onClickableTextClick(uri: String) = viewModelScope.launch {
         uriUtils.getQueryParameter(uri, "eventName")?.let { eventName ->
@@ -433,6 +432,16 @@ internal class PartnerAuthViewModel @Inject constructor(
         }
     }
 
+    fun onCancelClick() = viewModelScope.launch {
+        // set loading state while cancelling the active auth session, and navigate back
+        setState { copy(authenticationStatus = Loading(value = Status(Action.CANCELLING))) }
+        runCatching {
+            val authSession = requireNotNull(getOrFetchSync().manifest.activeAuthSession)
+            cancelAuthorizationSession(authSession.id)
+        }
+        navigationManager.tryNavigateBack()
+    }
+
     companion object : MavericksViewModelFactory {
 
         override fun initialState(viewModelContext: ViewModelContext) =
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/SharedPartnerAuthState.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/SharedPartnerAuthState.kt
index 469987a1ecb..be7e55237d8 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/SharedPartnerAuthState.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/partnerauth/SharedPartnerAuthState.kt
@@ -7,7 +7,6 @@ import com.airbnb.mvrx.MavericksState
 import com.airbnb.mvrx.PersistState
 import com.airbnb.mvrx.Success
 import com.airbnb.mvrx.Uninitialized
-import com.stripe.android.financialconnections.model.DataAccessNotice
 import com.stripe.android.financialconnections.model.FinancialConnectionsAuthorizationSession
 import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
@@ -22,18 +21,24 @@ internal data class SharedPartnerAuthState(
     val pane: FinancialConnectionsSessionManifest.Pane,
     val payload: Async = Uninitialized,
     val viewEffect: ViewEffect? = null,
-    val authenticationStatus: Async = Uninitialized,
+    val authenticationStatus: Async = Uninitialized,
 ) : MavericksState {
 
-    val dataAccess: DataAccessNotice?
-        get() = payload()?.authSession?.display?.text?.oauthPrepane?.dataAccessNotice
-
     data class Payload(
         val isStripeDirect: Boolean,
         val institution: FinancialConnectionsInstitution,
-        val authSession: FinancialConnectionsAuthorizationSession
+        val authSession: FinancialConnectionsAuthorizationSession,
     )
 
+    data class AuthenticationStatus(
+        val action: Action,
+    ) {
+        enum class Action {
+            CANCELLING,
+            AUTHENTICATING
+        }
+    }
+
     val canNavigateBack: Boolean
         get() =
             // Authentication running -> don't allow back navigation
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/reset/ResetViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/reset/ResetViewModel.kt
index 5d639b7cf77..6fda1929291 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/reset/ResetViewModel.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/reset/ResetViewModel.kt
@@ -15,6 +15,7 @@ import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator
 import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator.Message.ClearPartnerWebAuth
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
 import com.stripe.android.financialconnections.navigation.NavigationManager
+import com.stripe.android.financialconnections.navigation.PopUpToBehavior
 import com.stripe.android.financialconnections.navigation.destination
 import com.stripe.android.financialconnections.ui.FinancialConnectionsSheetNativeActivity
 import javax.inject.Inject
@@ -35,9 +36,8 @@ internal class ResetViewModel @Inject constructor(
             nativeAuthFlowCoordinator().emit(ClearPartnerWebAuth)
             eventTracker.track(PaneLoaded(PANE))
             navigationManager.tryNavigateTo(
-                updatedManifest.nextPane.destination(referrer = PANE),
-                popUpToCurrent = true,
-                inclusive = true
+                route = updatedManifest.nextPane.destination(referrer = PANE),
+                popUpTo = PopUpToBehavior.Current(inclusive = true),
             )
         }.execute { copy(payload = it) }
     }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/success/SuccessContent.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/success/SuccessContent.kt
new file mode 100644
index 00000000000..51b4d09dfe1
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/success/SuccessContent.kt
@@ -0,0 +1,294 @@
+package com.stripe.android.financialconnections.features.success
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.Icon
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import com.airbnb.mvrx.Async
+import com.airbnb.mvrx.Loading
+import com.stripe.android.financialconnections.R
+import com.stripe.android.financialconnections.features.common.LoadingSpinner
+import com.stripe.android.financialconnections.features.success.SuccessState.Payload
+import com.stripe.android.financialconnections.model.FinancialConnectionsSession
+import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
+import com.stripe.android.financialconnections.ui.TextResource
+import com.stripe.android.financialconnections.ui.components.AnnotatedText
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsButton
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsScaffold
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsTopAppBar
+import com.stripe.android.financialconnections.ui.components.StringAnnotation
+import com.stripe.android.financialconnections.ui.components.elevation
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
+import kotlinx.coroutines.delay
+
+private const val ENTER_TRANSITION_DURATION_MS = 1000
+private const val CHECK_ALPHA_DURATION_MS = 250
+private const val SLIDE_IN_ANIMATION_FRACTION = 4
+
+@Composable
+internal fun SuccessContent(
+    completeSessionAsync: Async,
+    payloadAsync: Async,
+    onDoneClick: () -> Unit,
+    onCloseClick: () -> Unit,
+) {
+    SuccessContentInternal(
+        // Just enabled on Compose Previews: allows to preview the post-animation state.
+        overrideAnimationForPreview = false,
+        payloadAsync = payloadAsync,
+        onCloseClick = onCloseClick,
+        completeSessionAsync = completeSessionAsync,
+        onDoneClick = onDoneClick
+    )
+}
+
+@Composable
+private fun SuccessContentInternal(
+    overrideAnimationForPreview: Boolean,
+    payloadAsync: Async,
+    onCloseClick: () -> Unit,
+    completeSessionAsync: Async,
+    onDoneClick: () -> Unit
+) {
+    val scrollState = rememberScrollState()
+    var showSpinner by remember { mutableStateOf(overrideAnimationForPreview.not()) }
+    val payload by remember(payloadAsync) { mutableStateOf(payloadAsync()) }
+
+    payload?.let {
+        if (it.skipSuccessPane.not()) {
+            LaunchedEffect(true) {
+                delay(ENTER_TRANSITION_DURATION_MS.toLong())
+                showSpinner = false
+            }
+        }
+    }
+
+    FinancialConnectionsScaffold(
+        topBar = {
+            FinancialConnectionsTopAppBar(
+                allowBackNavigation = false,
+                onCloseClick = onCloseClick,
+                elevation = scrollState.elevation
+            )
+        }
+    ) {
+        Column(
+            modifier = Modifier
+                .fillMaxSize()
+                .padding(24.dp)
+        ) {
+            Box(
+                modifier = Modifier
+                    .weight(1f)
+                    .fillMaxWidth(),
+                contentAlignment = Alignment.Center
+            ) {
+                SpinnerToSuccessAnimation(
+                    customSuccessMessage = payload?.customSuccessMessage,
+                    accountsCount = payload?.accountsCount ?: 0,
+                    showSpinner = showSpinner || payload == null
+                )
+            }
+            SuccessFooter(
+                modifier = Modifier.alpha(if (showSpinner) 0f else 1f),
+                merchantName = payload?.businessName,
+                loading = completeSessionAsync is Loading,
+                onDoneClick = onDoneClick
+            )
+        }
+    }
+}
+
+@Composable
+private fun SpinnerToSuccessAnimation(
+    showSpinner: Boolean,
+    accountsCount: Int,
+    customSuccessMessage: TextResource?
+) {
+    Box(
+        modifier = Modifier.fillMaxSize(),
+        contentAlignment = Alignment.Center,
+    ) {
+        // Define the animation specs
+        val enterTransition = fadeIn(animationSpec = tween(ENTER_TRANSITION_DURATION_MS))
+        val exitTransition = fadeOut(animationSpec = tween(ENTER_TRANSITION_DURATION_MS))
+
+        // Delay the appearance of the check icon
+        val checkAlpha: Float by animateFloatAsState(
+            targetValue = if (showSpinner) 0f else 1f,
+            animationSpec = tween(
+                delayMillis = CHECK_ALPHA_DURATION_MS,
+                durationMillis = CHECK_ALPHA_DURATION_MS,
+                easing = LinearEasing,
+            ),
+            label = "check_icon_alpha"
+        )
+
+        // Fade out loading spinner
+        AnimatedVisibility(
+            visible = showSpinner,
+            enter = enterTransition,
+            exit = exitTransition,
+        ) {
+            LoadingSpinner(
+                modifier = Modifier.size(56.dp)
+            )
+        }
+
+        // Fade in + slide success content.
+        AnimatedVisibility(
+            visible = !showSpinner,
+            enter = enterTransition + slideInVertically(initialOffsetY = { it / SLIDE_IN_ANIMATION_FRACTION }),
+            exit = exitTransition
+        ) {
+            SuccessCompletedContent(
+                checkAlpha = checkAlpha,
+                customSuccessMessage = customSuccessMessage,
+                accountsCount = accountsCount
+            )
+        }
+    }
+}
+
+@Composable
+private fun SuccessCompletedContent(
+    checkAlpha: Float,
+    customSuccessMessage: TextResource?,
+    accountsCount: Int
+) {
+    Column(
+        verticalArrangement = Arrangement.spacedBy(16.dp),
+        horizontalAlignment = Alignment.CenterHorizontally,
+    ) {
+        Box(
+            contentAlignment = Alignment.Center,
+            modifier = Modifier
+                .size(56.dp)
+                .background(FinancialConnectionsTheme.colors.iconBrand, CircleShape)
+        ) {
+            Icon(
+                modifier = Modifier.graphicsLayer { alpha = checkAlpha },
+                imageVector = Icons.Default.Check,
+                contentDescription = stringResource(id = R.string.stripe_success_pane_title),
+                tint = Color.White
+            )
+        }
+        Text(
+            stringResource(id = R.string.stripe_success_pane_title),
+            style = typography.headingXLarge,
+            textAlign = TextAlign.Center
+        )
+        AnnotatedText(
+            text = customSuccessMessage ?: TextResource.PluralId(
+                value = R.plurals.stripe_success_pane_desc,
+                count = accountsCount,
+                args = emptyList()
+            ),
+            defaultStyle = typography.bodyMedium.copy(
+                textAlign = TextAlign.Center
+            ),
+            annotationStyles = mapOf(
+                StringAnnotation.BOLD to typography.bodyMediumEmphasized.copy(
+                    textAlign = TextAlign.Center,
+                ).toSpanStyle()
+            ),
+            onClickableTextClick = {}
+        )
+    }
+}
+
+@Composable
+private fun SuccessFooter(
+    modifier: Modifier = Modifier,
+    loading: Boolean,
+    merchantName: String?,
+    onDoneClick: () -> Unit
+) {
+    Box(modifier) {
+        FinancialConnectionsButton(
+            loading = loading,
+            onClick = onDoneClick,
+            modifier = Modifier
+                .fillMaxWidth()
+        ) {
+            Text(
+                text = when (merchantName) {
+                    null -> stringResource(id = R.string.stripe_success_pane_done)
+                    else -> stringResource(id = R.string.stripe_success_pane_done_with_merchant, merchantName)
+                }
+            )
+        }
+    }
+}
+
+@Preview(
+    group = "Success",
+    name = "Loading"
+)
+@Composable
+internal fun SuccessScreenPreview(
+    @PreviewParameter(SuccessPreviewParameterProvider::class) state: SuccessState
+) {
+    FinancialConnectionsPreview {
+        SuccessContentInternal(
+            overrideAnimationForPreview = false,
+            completeSessionAsync = state.completeSession,
+            payloadAsync = state.payload,
+            onDoneClick = {},
+            onCloseClick = {}
+        )
+    }
+}
+
+@Preview(
+    group = "Success",
+    name = "Animation completed"
+)
+@Composable
+internal fun SuccessScreenAnimationCompletedPreview(
+    @PreviewParameter(SuccessPreviewParameterProvider::class) state: SuccessState
+) {
+    FinancialConnectionsPreview {
+        SuccessContentInternal(
+            overrideAnimationForPreview = true,
+            completeSessionAsync = state.completeSession,
+            payloadAsync = state.payload,
+            onDoneClick = {},
+            onCloseClick = {}
+        )
+    }
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/success/SuccessPreviewParameterProvider.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/success/SuccessPreviewParameterProvider.kt
new file mode 100644
index 00000000000..3a8d7486303
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/success/SuccessPreviewParameterProvider.kt
@@ -0,0 +1,41 @@
+package com.stripe.android.financialconnections.features.success
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.Uninitialized
+import com.stripe.android.financialconnections.ui.TextResource
+
+internal class SuccessPreviewParameterProvider :
+    PreviewParameterProvider {
+    override val values = sequenceOf(
+        canonical(),
+        customMessage()
+    )
+
+    private fun canonical() = SuccessState(
+        payload = Success(
+            SuccessState.Payload(
+                skipSuccessPane = false,
+                accountsCount = 1,
+                customSuccessMessage = null,
+                businessName = "Stripe",
+            )
+        ),
+        completeSession = Uninitialized,
+    )
+
+    private fun customMessage() = SuccessState(
+        payload = Success(
+            SuccessState.Payload(
+                skipSuccessPane = false,
+                accountsCount = 1,
+                customSuccessMessage = TextResource.Text(
+                    "You can expect micro-deposits to account " +
+                        "••••1234 in 1-2 days and an email with further instructions."
+                ),
+                businessName = "Stripe",
+            )
+        ),
+        completeSession = Uninitialized,
+    )
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/success/SuccessScreen.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/success/SuccessScreen.kt
index 561168a1833..babfad2aa79 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/success/SuccessScreen.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/success/SuccessScreen.kt
@@ -1,399 +1,22 @@
 package com.stripe.android.financialconnections.features.success
 
 import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.ScrollState
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.Icon
-import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.platform.LocalUriHandler
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import com.airbnb.mvrx.Loading
+import androidx.compose.runtime.State
 import com.airbnb.mvrx.compose.collectAsState
 import com.airbnb.mvrx.compose.mavericksViewModel
-import com.stripe.android.financialconnections.R
-import com.stripe.android.financialconnections.features.common.AccessibleDataCalloutModel
-import com.stripe.android.financialconnections.features.common.AccessibleDataCalloutWithAccounts
-import com.stripe.android.financialconnections.features.common.LoadingContent
-import com.stripe.android.financialconnections.model.FinancialConnectionsAccount
-import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
-import com.stripe.android.financialconnections.model.PartnerAccount
 import com.stripe.android.financialconnections.presentation.parentViewModel
-import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
-import com.stripe.android.financialconnections.ui.TextResource
-import com.stripe.android.financialconnections.ui.components.AnnotatedText
-import com.stripe.android.financialconnections.ui.components.FinancialConnectionsButton
-import com.stripe.android.financialconnections.ui.components.FinancialConnectionsScaffold
-import com.stripe.android.financialconnections.ui.components.FinancialConnectionsTopAppBar
-import com.stripe.android.financialconnections.ui.components.StringAnnotation
-import com.stripe.android.financialconnections.ui.components.elevation
-import com.stripe.android.financialconnections.ui.theme.Attention100
-import com.stripe.android.financialconnections.ui.theme.Attention50
-import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
 
 @Composable
 internal fun SuccessScreen() {
     val viewModel: SuccessViewModel = mavericksViewModel()
     val parentViewModel = parentViewModel()
-    val state = viewModel.collectAsState()
+    val state: State = viewModel.collectAsState()
     BackHandler(enabled = true) {}
-    state.value.payload()?.let { payload ->
-        SuccessContent(
-            accessibleDataModel = payload.accessibleData,
-            disconnectUrl = payload.disconnectUrl,
-            accounts = payload.accounts,
-            institution = payload.institution,
-            successMessage = payload.successMessage,
-            loading = state.value.completeSession is Loading,
-            skipSuccessPane = payload.skipSuccessPane,
-            onDoneClick = viewModel::onDoneClick,
-            accountFailedToLinkMessage = payload.accountFailedToLinkMessage,
-            onLearnMoreAboutDataAccessClick = viewModel::onLearnMoreAboutDataAccessClick,
-            onDisconnectLinkClick = viewModel::onDisconnectLinkClick
-        ) { parentViewModel.onCloseNoConfirmationClick(Pane.SUCCESS) }
-    }
+    SuccessContent(
+        completeSessionAsync = state.value.completeSession,
+        payloadAsync = state.value.payload,
+        onDoneClick = viewModel::onDoneClick
+    ) { parentViewModel.onCloseNoConfirmationClick(Pane.SUCCESS) }
 }
-
-@Composable
-private fun SuccessContent(
-    accessibleDataModel: AccessibleDataCalloutModel,
-    disconnectUrl: String,
-    accounts: List,
-    institution: FinancialConnectionsInstitution,
-    successMessage: TextResource,
-    loading: Boolean,
-    skipSuccessPane: Boolean,
-    accountFailedToLinkMessage: TextResource?,
-    onDoneClick: () -> Unit,
-    onLearnMoreAboutDataAccessClick: () -> Unit,
-    onDisconnectLinkClick: () -> Unit,
-    onCloseClick: () -> Unit,
-) {
-    val scrollState = rememberScrollState()
-    FinancialConnectionsScaffold(
-        topBar = {
-            FinancialConnectionsTopAppBar(
-                showBack = false,
-                onCloseClick = onCloseClick,
-                elevation = scrollState.elevation
-            )
-        }
-    ) {
-        if (skipSuccessPane) {
-            SuccessLoading()
-        } else {
-            SuccessLoaded(
-                scrollState = scrollState,
-                accounts = accounts,
-                accessibleDataModel = accessibleDataModel,
-                disconnectUrl = disconnectUrl,
-                institution = institution,
-                loading = loading,
-                successMessage = successMessage,
-                accountFailedToLinkMessage = accountFailedToLinkMessage,
-                onLearnMoreAboutDataAccessClick = onLearnMoreAboutDataAccessClick,
-                onDisconnectLinkClick = onDisconnectLinkClick,
-                onDoneClick = onDoneClick
-            )
-        }
-    }
-}
-
-@Composable
-private fun SuccessLoading() {
-    LoadingContent(
-        title = stringResource(id = R.string.stripe_success_pane_skip_title),
-        content = stringResource(id = R.string.stripe_success_pane_skip_desc),
-    )
-}
-
-@Composable
-@Suppress("LongMethod")
-private fun SuccessLoaded(
-    scrollState: ScrollState,
-    accounts: List,
-    accessibleDataModel: AccessibleDataCalloutModel,
-    disconnectUrl: String,
-    successMessage: TextResource,
-    institution: FinancialConnectionsInstitution,
-    loading: Boolean,
-    accountFailedToLinkMessage: TextResource?,
-    onLearnMoreAboutDataAccessClick: () -> Unit,
-    onDisconnectLinkClick: () -> Unit,
-    onDoneClick: () -> Unit
-) {
-    val uriHandler = LocalUriHandler.current
-    Column(
-        Modifier
-            .fillMaxSize()
-    ) {
-        Column(
-            modifier = Modifier
-                .weight(1f)
-                .verticalScroll(scrollState)
-                .padding(
-                    top = 8.dp,
-                    start = 24.dp,
-                    end = 24.dp
-                )
-        ) {
-            Icon(
-                modifier = Modifier.size(40.dp),
-                painter = painterResource(R.drawable.stripe_ic_check_circle),
-                contentDescription = null,
-                tint = FinancialConnectionsTheme.colors.textSuccess
-            )
-            Spacer(modifier = Modifier.size(16.dp))
-            Text(
-                modifier = Modifier
-                    .fillMaxWidth(),
-                text = stringResource(R.string.stripe_success_title),
-                style = FinancialConnectionsTheme.typography.subtitle
-            )
-            Spacer(modifier = Modifier.size(8.dp))
-            Text(
-                modifier = Modifier
-                    .fillMaxWidth(),
-                text = successMessage.toText().toString(),
-                style = FinancialConnectionsTheme.typography.body
-            )
-            if (accounts.isNotEmpty()) {
-                Spacer(modifier = Modifier.size(24.dp))
-                AccessibleDataCalloutWithAccounts(
-                    model = accessibleDataModel,
-                    accounts = accounts,
-                    institution = institution,
-                    onLearnMoreClick = { onLearnMoreAboutDataAccessClick() }
-                )
-            }
-            Spacer(modifier = Modifier.size(12.dp))
-            AnnotatedText(
-                text = TextResource.StringId(R.string.stripe_success_pane_disconnect),
-                onClickableTextClick = {
-                    onDisconnectLinkClick()
-                    uriHandler.openUri(disconnectUrl)
-                },
-                defaultStyle = FinancialConnectionsTheme.typography.caption.copy(
-                    color = FinancialConnectionsTheme.colors.textSecondary
-                ),
-                annotationStyles = mapOf(
-                    StringAnnotation.CLICKABLE to FinancialConnectionsTheme.typography.captionEmphasized
-                        .toSpanStyle()
-                        .copy(color = FinancialConnectionsTheme.colors.textBrand)
-                )
-            )
-            Spacer(modifier = Modifier.weight(1f))
-        }
-        SuccessLoadedFooter(
-            accountFailedToLinkMessage = accountFailedToLinkMessage,
-            loading = loading,
-            onDoneClick = onDoneClick
-        )
-    }
-}
-
-@Composable
-private fun AccountNotSavedToLinkNotice(message: TextResource) {
-    Box(
-        modifier = Modifier
-            .fillMaxWidth()
-            .clip(shape = RoundedCornerShape(8.dp))
-            .border(color = Attention100, width = 1.dp)
-            .background(color = Attention50)
-            .padding(12.dp)
-    ) {
-        Row {
-            Icon(
-                modifier = Modifier
-                    .size(12.dp)
-                    .offset(y = 2.dp),
-                painter = painterResource(R.drawable.stripe_ic_warning),
-                contentDescription = null,
-                tint = FinancialConnectionsTheme.colors.textAttention
-            )
-            Spacer(modifier = Modifier.size(8.dp))
-            Text(
-                text = message.toText().toString(),
-                style = FinancialConnectionsTheme.typography.caption.copy(
-                    color = FinancialConnectionsTheme.colors.textSecondary
-                )
-            )
-        }
-    }
-}
-
-@Composable
-private fun SuccessLoadedFooter(
-    loading: Boolean,
-    accountFailedToLinkMessage: TextResource?,
-    onDoneClick: () -> Unit
-) {
-    Column(
-        Modifier.padding(
-            bottom = 24.dp,
-            start = 24.dp,
-            end = 24.dp
-        )
-    ) {
-        accountFailedToLinkMessage?.let {
-            AccountNotSavedToLinkNotice(it)
-            Spacer(modifier = Modifier.size(20.dp))
-        }
-        FinancialConnectionsButton(
-            loading = loading,
-            onClick = onDoneClick,
-            modifier = Modifier
-                .fillMaxWidth()
-        ) {
-            Text(text = stringResource(R.string.stripe_success_pane_done))
-        }
-    }
-}
-
-@Preview(
-    group = "Success",
-    name = "Default"
-)
-@Suppress("LongMethod")
-@Composable
-internal fun SuccessScreenPreview() {
-    FinancialConnectionsPreview {
-        SuccessContent(
-            accessibleDataModel = AccessibleDataCalloutModel(
-                businessName = "My business",
-                permissions = listOf(
-                    FinancialConnectionsAccount.Permissions.PAYMENT_METHOD,
-                    FinancialConnectionsAccount.Permissions.BALANCES,
-                    FinancialConnectionsAccount.Permissions.OWNERSHIP,
-                    FinancialConnectionsAccount.Permissions.TRANSACTIONS
-                ),
-                isStripeDirect = true,
-                isNetworking = false,
-                dataPolicyUrl = ""
-            ),
-            disconnectUrl = "",
-            accounts = previewAccounts(),
-            institution = FinancialConnectionsInstitution(
-                id = "id",
-                name = "name",
-                url = "url",
-                featured = true,
-                featuredOrder = null,
-                icon = null,
-                logo = null,
-                mobileHandoffCapable = false
-            ),
-            successMessage = TextResource.PluralId(
-                value = R.plurals.stripe_success_pane_link_with_connected_account_name,
-                count = 2,
-                args = listOf("ConnectedAccount", "BusinessName")
-            ),
-            loading = false,
-            skipSuccessPane = false,
-            accountFailedToLinkMessage = null,
-            onDoneClick = {},
-            onLearnMoreAboutDataAccessClick = {},
-            onDisconnectLinkClick = {}
-        ) {}
-    }
-}
-
-@Composable
-@Preview
-@Suppress("LongMethod")
-internal fun SuccessScreenPreviewFailedToLink() {
-    FinancialConnectionsPreview {
-        SuccessContent(
-            accessibleDataModel = AccessibleDataCalloutModel(
-                businessName = "My business",
-                permissions = listOf(
-                    FinancialConnectionsAccount.Permissions.PAYMENT_METHOD,
-                    FinancialConnectionsAccount.Permissions.BALANCES,
-                    FinancialConnectionsAccount.Permissions.OWNERSHIP,
-                    FinancialConnectionsAccount.Permissions.TRANSACTIONS
-                ),
-                isStripeDirect = true,
-                isNetworking = false,
-                dataPolicyUrl = ""
-            ),
-            disconnectUrl = "",
-            accounts = previewAccounts(),
-            institution = FinancialConnectionsInstitution(
-                id = "id",
-                name = "name",
-                url = "url",
-                featured = true,
-                featuredOrder = null,
-                icon = null,
-                logo = null,
-                mobileHandoffCapable = false
-            ),
-            successMessage = TextResource.Text("Hola"),
-            loading = false,
-            skipSuccessPane = false,
-            accountFailedToLinkMessage = TextResource.PluralId(
-                R.plurals.stripe_success_networking_save_to_link_failed,
-                1,
-                listOf("Random Business")
-            ),
-            onDoneClick = {},
-            onLearnMoreAboutDataAccessClick = {},
-            onDisconnectLinkClick = {}
-        ) {}
-    }
-}
-
-@Composable
-private fun previewAccounts() = listOf(
-    PartnerAccount(
-        authorization = "Authorization",
-        category = FinancialConnectionsAccount.Category.CASH,
-        id = "id2",
-        name = "Account 2 - no acct numbers",
-        _allowSelection = true,
-        allowSelectionMessage = "",
-        subcategory = FinancialConnectionsAccount.Subcategory.SAVINGS,
-        supportedPaymentMethodTypes = emptyList()
-    ),
-    PartnerAccount(
-        authorization = "Authorization",
-        category = FinancialConnectionsAccount.Category.CASH,
-        id = "id3",
-        name = "Account 3",
-        _allowSelection = true,
-        allowSelectionMessage = "",
-        displayableAccountNumbers = "1234",
-        subcategory = FinancialConnectionsAccount.Subcategory.CREDIT_CARD,
-        supportedPaymentMethodTypes = emptyList()
-    ),
-    PartnerAccount(
-        authorization = "Authorization",
-        category = FinancialConnectionsAccount.Category.CASH,
-        id = "id4",
-        name = "Account 4",
-        _allowSelection = true,
-        allowSelectionMessage = "",
-        displayableAccountNumbers = "1234",
-        subcategory = FinancialConnectionsAccount.Subcategory.CHECKING,
-        supportedPaymentMethodTypes = emptyList()
-    )
-)
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/success/SuccessViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/success/SuccessViewModel.kt
index bca1ab92973..83b37327bb9 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/features/success/SuccessViewModel.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/features/success/SuccessViewModel.kt
@@ -1,6 +1,5 @@
 package com.stripe.android.financialconnections.features.success
 
-import androidx.annotation.VisibleForTesting
 import com.airbnb.mvrx.Async
 import com.airbnb.mvrx.Loading
 import com.airbnb.mvrx.MavericksState
@@ -10,29 +9,22 @@ import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.ViewModelContext
 import com.stripe.android.core.Logger
 import com.stripe.android.financialconnections.R
-import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.ClickDisconnectLink
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.ClickDone
-import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.ClickLearnMoreDataAccess
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.PaneLoaded
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
 import com.stripe.android.financialconnections.domain.GetCachedAccounts
 import com.stripe.android.financialconnections.domain.GetManifest
 import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator
 import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator.Message.Complete
-import com.stripe.android.financialconnections.features.common.AccessibleDataCalloutModel
-import com.stripe.android.financialconnections.features.consent.FinancialConnectionsUrlResolver
-import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
+import com.stripe.android.financialconnections.features.common.useContinueWithMerchantText
 import com.stripe.android.financialconnections.model.FinancialConnectionsSession
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
-import com.stripe.android.financialconnections.model.PartnerAccount
 import com.stripe.android.financialconnections.repository.SaveToLinkWithStripeSucceededRepository
 import com.stripe.android.financialconnections.ui.FinancialConnectionsSheetNativeActivity
 import com.stripe.android.financialconnections.ui.TextResource
-import com.stripe.android.financialconnections.ui.TextResource.PluralId
 import kotlinx.coroutines.launch
 import javax.inject.Inject
 
-@Suppress("LongParameterList")
 internal class SuccessViewModel @Inject constructor(
     initialState: SuccessState,
     getCachedAccounts: GetCachedAccounts,
@@ -51,36 +43,24 @@ internal class SuccessViewModel @Inject constructor(
             val saveToLinkWithStripeSucceeded: Boolean? = saveToLinkWithStripeSucceeded.get()
             SuccessState.Payload(
                 skipSuccessPane = manifest.skipSuccessPane ?: false,
-                successMessage = getSuccessMessages(
-                    isLinkWithStripe = manifest.isLinkWithStripe,
-                    isNetworkingUserFlow = manifest.isNetworkingUserFlow,
-                    saveToLinkWithStripeSucceeded = saveToLinkWithStripeSucceeded,
-                    businessName = manifest.businessName,
-                    connectedAccountName = manifest.connectedAccountName,
-                    count = accounts.size
-                ),
-                accessibleData = AccessibleDataCalloutModel(
-                    businessName = manifest.businessName,
-                    permissions = manifest.permissions,
-                    isNetworking = manifest.isNetworkingUserFlow == true && saveToLinkWithStripeSucceeded == true,
-                    isStripeDirect = manifest.isStripeDirect ?: false,
-                    dataPolicyUrl = FinancialConnectionsUrlResolver.getDataPolicyUrl(manifest)
-                ),
-                accounts = accounts,
-                institution = manifest.activeInstitution!!,
-                businessName = manifest.businessName,
-                disconnectUrl = FinancialConnectionsUrlResolver.getDisconnectUrl(manifest),
-                accountFailedToLinkMessage = getFailedToLinkMessage(
-                    businessName = manifest.businessName,
-                    saveToLinkWithStripeSucceeded = saveToLinkWithStripeSucceeded,
-                    count = accounts.size
-                )
+                accountsCount = accounts.size,
+                customSuccessMessage = saveToLinkWithStripeSucceeded?.let { buildCustomMessage(it, accounts.size) },
+                // We just want to use the business name in the CTA if the feature is enabled in the manifest.
+                businessName = manifest.businessName?.takeIf { manifest.useContinueWithMerchantText() },
             )
         }.execute {
             copy(payload = it)
         }
     }
 
+    private fun buildCustomMessage(
+        saveToLinkWithStripeSucceeded: Boolean,
+        accountsCount: Int
+    ): TextResource = when (saveToLinkWithStripeSucceeded) {
+        true -> TextResource.PluralId(R.plurals.stripe_success_pane_desc_link_success, accountsCount)
+        false -> TextResource.PluralId(R.plurals.stripe_success_pane_desc_link_error, accountsCount)
+    }
+
     private fun observeAsyncs() {
         onAsync(
             SuccessState::payload,
@@ -97,69 +77,6 @@ internal class SuccessViewModel @Inject constructor(
         )
     }
 
-    @VisibleForTesting
-    fun getFailedToLinkMessage(
-        businessName: String?,
-        saveToLinkWithStripeSucceeded: Boolean?,
-        count: Int
-    ): TextResource? = when {
-        saveToLinkWithStripeSucceeded != false -> null
-        businessName != null -> PluralId(
-            value = R.plurals.stripe_success_networking_save_to_link_failed,
-            count = count,
-            args = listOf(businessName)
-        )
-
-        else -> PluralId(
-            R.plurals.stripe_success_pane_networking_save_to_link_failed_no_business,
-            count,
-        )
-    }
-
-    @VisibleForTesting
-    internal fun getSuccessMessages(
-        isLinkWithStripe: Boolean?,
-        isNetworkingUserFlow: Boolean?,
-        saveToLinkWithStripeSucceeded: Boolean?,
-        connectedAccountName: String?,
-        businessName: String?,
-        count: Int
-    ): TextResource = when {
-        isLinkWithStripe == true ||
-            (isNetworkingUserFlow == true && saveToLinkWithStripeSucceeded == true) -> when {
-            businessName != null && connectedAccountName != null -> PluralId(
-                value = R.plurals.stripe_success_pane_link_with_connected_account_name,
-                count = count,
-                args = listOf(connectedAccountName, businessName)
-            )
-
-            businessName != null -> PluralId(
-                value = R.plurals.stripe_success_pane_link_with_business_name,
-                count = count,
-                args = listOf(businessName)
-            )
-
-            else -> PluralId(
-                R.plurals.stripe_success_pane_link_with_no_business_name,
-                count,
-            )
-        }
-
-        businessName != null && connectedAccountName != null -> PluralId(
-            R.plurals.stripe_success_pane_has_connected_account_name,
-            count = count,
-            args = listOf(connectedAccountName, businessName)
-        )
-
-        businessName != null -> PluralId(
-            R.plurals.stripe_success_pane_has_business_name,
-            count,
-            args = listOf(businessName)
-        )
-
-        else -> PluralId(R.plurals.stripe_success_pane_no_business_name, count)
-    }
-
     fun onDoneClick() = viewModelScope.launch {
         eventTracker.track(ClickDone(PANE))
         setState { copy(completeSession = Loading()) }
@@ -170,18 +87,6 @@ internal class SuccessViewModel @Inject constructor(
         nativeAuthFlowCoordinator().emit(Complete())
     }
 
-    fun onLearnMoreAboutDataAccessClick() {
-        viewModelScope.launch {
-            eventTracker.track(ClickLearnMoreDataAccess(PANE))
-        }
-    }
-
-    fun onDisconnectLinkClick() {
-        viewModelScope.launch {
-            eventTracker.track(ClickDisconnectLink(PANE))
-        }
-    }
-
     companion object : MavericksViewModelFactory {
 
         override fun create(
@@ -207,13 +112,9 @@ internal data class SuccessState(
 ) : MavericksState {
 
     data class Payload(
-        val accessibleData: AccessibleDataCalloutModel,
-        val institution: FinancialConnectionsInstitution,
-        val accounts: List,
-        val disconnectUrl: String,
         val businessName: String?,
-        val skipSuccessPane: Boolean,
-        val successMessage: TextResource,
-        val accountFailedToLinkMessage: TextResource?
+        val customSuccessMessage: TextResource?,
+        val accountsCount: Int,
+        val skipSuccessPane: Boolean
     )
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/model/FinancialConnectionsAuthorizationSession.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/model/FinancialConnectionsAuthorizationSession.kt
index db3452c9e42..8fe20507aa6 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/model/FinancialConnectionsAuthorizationSession.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/model/FinancialConnectionsAuthorizationSession.kt
@@ -20,7 +20,6 @@ import kotlinx.serialization.Serializable
  */
 @Serializable
 @Parcelize
-@Suppress("ConstructorParameterNaming")
 internal data class FinancialConnectionsAuthorizationSession(
 
     @SerialName(value = "id")
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/model/FinancialConnectionsInstitution.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/model/FinancialConnectionsInstitution.kt
index 4ba0a96f6b9..1df950235db 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/model/FinancialConnectionsInstitution.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/model/FinancialConnectionsInstitution.kt
@@ -37,4 +37,25 @@ internal data class FinancialConnectionsInstitution(
 
     @SerialName(value = "url") val url: String? = null
 
-) : Parcelable
+) : Parcelable {
+
+    /**
+     * This returns a cleaned up url containing only the root domain.
+     * Works for up to two-part tlds, but falls apart for longer ones e.g. hello.al.sp.gov.br
+     * Once the URLs are cleaned up on the backend, won't need this anymore.
+     */
+    val formattedUrl: String
+        get() = runCatching {
+            // This match would still have subdomains
+            val matchResult = Regex("""^(?:https?://)?(?:www\.|[^@\n]+@)?([^:/\n]+)""")
+                .find(url ?: return "")
+            val rootUrl = matchResult?.groups?.get(1)?.value ?: return ""
+            val parts = rootUrl.split('.')
+            val len = parts.size
+            return if (len > 2 && parts[len - 2].length <= 3 && parts[len - 1].length <= 2) {
+                "${parts[len - 3]}.${parts[len - 2]}.${parts[len - 1]}"
+            } else {
+                "${parts[len - 2]}.${parts[len - 1]}"
+            }
+        }.getOrDefault(url ?: "")
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/model/FinancialConnectionsSessionManifest.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/model/FinancialConnectionsSessionManifest.kt
index ab849c78107..2d9f6c7935c 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/model/FinancialConnectionsSessionManifest.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/model/FinancialConnectionsSessionManifest.kt
@@ -47,7 +47,6 @@ import kotlinx.serialization.Serializable
  * @param paymentMethodType
  * @param successUrl
  */
-@Suppress("MaxLineLength")
 @Serializable
 @Parcelize
 internal data class FinancialConnectionsSessionManifest(
@@ -243,11 +242,17 @@ internal data class FinancialConnectionsSessionManifest(
         @SerialName(value = "link_account_picker")
         LINK_ACCOUNT_PICKER("link_account_picker"),
 
+        @SerialName(value = "partner_auth_drawer")
+        PARTNER_AUTH_DRAWER("partner_auth_drawer"),
+
         @SerialName(value = "networking_save_to_link_verification")
         NETWORKING_SAVE_TO_LINK_VERIFICATION("networking_save_to_link_verification"),
 
         @SerialName(value = "reset")
-        RESET("reset");
+        RESET("reset"),
+
+        @SerialName(value = "exit")
+        EXIT("exit");
 
         internal object Serializer :
             EnumIgnoreUnknownSerializer(entries.toTypedArray(), UNEXPECTED_ERROR)
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/model/PartnerAccountsList.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/model/PartnerAccountsList.kt
index 7bd727f9543..25c802ea476 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/model/PartnerAccountsList.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/model/PartnerAccountsList.kt
@@ -43,7 +43,6 @@ internal data class PartnerAccountsList(
  */
 @Serializable
 @Parcelize
-@Suppress("ConstructorParameterNaming")
 internal data class PartnerAccount(
 
     @SerialName(value = "authorization") @Required val authorization: String,
@@ -91,5 +90,6 @@ internal data class PartnerAccount(
     internal val allowSelection: Boolean
         get() = _allowSelection ?: true
 
-    internal val redactedAccountNumbers: String? get() = displayableAccountNumbers?.let { "••••$it" }
+    internal val redactedAccountNumbers: String
+        get() = "••••${displayableAccountNumbers.orEmpty()}"
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/model/TextUpdate.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/model/TextUpdate.kt
index 0dcd03d2840..96e640cb377 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/model/TextUpdate.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/model/TextUpdate.kt
@@ -58,7 +58,10 @@ internal data class OauthPrepane(
     val dataAccessNotice: DataAccessNotice? = null,
     @SerialName("title")
     @Serializable(with = MarkdownToHtmlSerializer::class)
-    val title: String
+    val title: String,
+    @SerialName("subtitle")
+    @Serializable(with = MarkdownToHtmlSerializer::class)
+    val subtitle: String
 ) : Parcelable
 
 @Serializable
@@ -125,7 +128,9 @@ internal data class NetworkingLinkSignupPane(
     val cta: String,
     @SerialName("skip_cta")
     @Serializable(with = MarkdownToHtmlSerializer::class)
-    val skipCta: String
+    val skipCta: String,
+    @SerialName("legal_details_notice")
+    val legalDetailsNotice: LegalDetailsNotice? = null
 ) : Parcelable
 
 @Serializable
@@ -165,47 +170,73 @@ internal data class Bullet(
 @Serializable
 @Parcelize
 internal data class DataAccessNotice(
-    @SerialName("body")
-    val body: DataAccessNoticeBody,
+    @SerialName("icon")
+    val icon: Image? = null,
     @SerialName("title")
     @Serializable(with = MarkdownToHtmlSerializer::class)
     val title: String,
     @SerialName("subtitle")
     @Serializable(with = MarkdownToHtmlSerializer::class)
     val subtitle: String? = null,
+    @SerialName("body")
+    val body: DataAccessNoticeBody,
+    @SerialName("connected_account_notice")
+    val connectedAccountNotice: ConnectedAccessNotice? = null,
+    @SerialName("disclaimer")
+    @Serializable(with = MarkdownToHtmlSerializer::class)
+    val disclaimer: String? = null,
     @SerialName("cta")
     @Serializable(with = MarkdownToHtmlSerializer::class)
     val cta: String,
-    @SerialName("learn_more")
-    @Serializable(with = MarkdownToHtmlSerializer::class)
-    val learnMore: String,
-    @SerialName("connected_account_notice")
+) : Parcelable
+
+@Serializable
+@Parcelize
+internal data class ConnectedAccessNotice(
+    @SerialName("subtitle")
     @Serializable(with = MarkdownToHtmlSerializer::class)
-    val connectedAccountNotice: String? = null,
+    val subtitle: String,
+    @SerialName("body")
+    val body: DataAccessNoticeBody
 ) : Parcelable
 
 @Serializable
 @Parcelize
 internal data class LegalDetailsNotice(
-    @SerialName("body")
-    val body: LegalDetailsBody,
+    @SerialName("icon")
+    val icon: Image? = null,
     @SerialName("title")
     @Serializable(with = MarkdownToHtmlSerializer::class)
     val title: String,
+    @SerialName("subtitle")
+    @Serializable(with = MarkdownToHtmlSerializer::class)
+    val subtitle: String? = null,
+    @SerialName("body")
+    val body: LegalDetailsBody,
     @SerialName("cta")
     @Serializable(with = MarkdownToHtmlSerializer::class)
     val cta: String,
-    @SerialName("learn_more")
+    @SerialName("disclaimer")
     @Serializable(with = MarkdownToHtmlSerializer::class)
-    val learnMore: String,
-
+    val disclaimer: String? = null
 ) : Parcelable
 
 @Serializable
 @Parcelize
 internal data class LegalDetailsBody(
-    @SerialName("bullets")
-    val bullets: List
+    @SerialName("links")
+    val links: List
+) : Parcelable
+
+@Serializable
+@Parcelize
+internal data class ServerLink(
+    @SerialName("title")
+    @Serializable(with = MarkdownToHtmlSerializer::class)
+    val title: String,
+    @SerialName("content")
+    @Serializable(with = MarkdownToHtmlSerializer::class)
+    val content: String? = null
 ) : Parcelable
 
 @Serializable
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/Destination.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/Destination.kt
index 6d453f482e6..a4fdb780cef 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/Destination.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/Destination.kt
@@ -1,5 +1,6 @@
 package com.stripe.android.financialconnections.navigation
 
+import android.os.Bundle
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
@@ -15,6 +16,8 @@ import com.stripe.android.financialconnections.features.accountpicker.AccountPic
 import com.stripe.android.financialconnections.features.attachpayment.AttachPaymentScreen
 import com.stripe.android.financialconnections.features.bankauthrepair.BankAuthRepairScreen
 import com.stripe.android.financialconnections.features.consent.ConsentScreen
+import com.stripe.android.financialconnections.features.error.ErrorScreen
+import com.stripe.android.financialconnections.features.exit.ExitModal
 import com.stripe.android.financialconnections.features.institutionpicker.InstitutionPickerScreen
 import com.stripe.android.financialconnections.features.linkaccountpicker.LinkAccountPickerScreen
 import com.stripe.android.financialconnections.features.linkstepupverification.LinkStepUpVerificationScreen
@@ -29,6 +32,7 @@ import com.stripe.android.financialconnections.features.reset.ResetScreen
 import com.stripe.android.financialconnections.features.success.SuccessScreen
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
 import com.stripe.android.financialconnections.model.LinkAccountSessionPaymentAccount.MicrodepositVerificationMethod
+import com.stripe.android.financialconnections.navigation.bottomsheet.bottomSheet
 import com.stripe.android.financialconnections.presentation.parentViewModel
 
 internal sealed class Destination(
@@ -52,7 +56,7 @@ internal sealed class Destination(
         if (!paneLaunchedTriggered) {
             LaunchedEffect(Unit) {
                 viewModel.onPaneLaunched(
-                    referrer = referrer(navBackStackEntry),
+                    referrer = referrer(navBackStackEntry.arguments),
                     pane = navBackStackEntry.destination.pane
                 )
                 paneLaunchedTriggered = true
@@ -97,9 +101,14 @@ internal sealed class Destination(
         composable = { ConsentScreen() }
     )
 
+    data object PartnerAuthDrawer : NoArgumentsDestination(
+        route = Pane.PARTNER_AUTH_DRAWER.value,
+        composable = { PartnerAuthScreen(inModal = true) }
+    )
+
     data object PartnerAuth : NoArgumentsDestination(
         route = Pane.PARTNER_AUTH.value,
-        composable = { PartnerAuthScreen() }
+        composable = { PartnerAuthScreen(inModal = false) }
     )
 
     data object AccountPicker : NoArgumentsDestination(
@@ -129,7 +138,7 @@ internal sealed class Destination(
 
     data object NetworkingLinkLoginWarmup : NoArgumentsDestination(
         route = Pane.NETWORKING_LINK_LOGIN_WARMUP.value,
-        composable = { NetworkingLinkLoginWarmupScreen() }
+        composable = { NetworkingLinkLoginWarmupScreen(it) }
     )
 
     data object NetworkingLinkVerification : NoArgumentsDestination(
@@ -159,6 +168,16 @@ internal sealed class Destination(
         composable = { ResetScreen() }
     )
 
+    data object Exit : NoArgumentsDestination(
+        route = Pane.EXIT.value,
+        composable = { ExitModal(it) }
+    )
+
+    data object Error : NoArgumentsDestination(
+        route = Pane.UNEXPECTED_ERROR.value,
+        composable = { ErrorScreen() }
+    )
+
     data object BankAuthRepair : NoArgumentsDestination(
         route = Pane.BANK_AUTH_REPAIR.value,
         composable = { BankAuthRepairScreen() }
@@ -178,15 +197,7 @@ internal sealed class Destination(
             KEY_LAST4 to last4
         )
 
-        fun microdeposits(backStackEntry: NavBackStackEntry): MicrodepositVerificationMethod =
-            backStackEntry.arguments
-                ?.getString(KEY_MICRODEPOSITS)
-                ?.let { value ->
-                    MicrodepositVerificationMethod.entries.firstOrNull { it.value == value }
-                } ?: MicrodepositVerificationMethod.UNKNOWN
-
-        fun last4(backStackEntry: NavBackStackEntry): String? =
-            backStackEntry.arguments?.getString(KEY_LAST4)
+        fun last4(args: Bundle?): String? = args?.getString(KEY_LAST4)
 
         override fun invoke(
             referrer: Pane?,
@@ -199,7 +210,7 @@ internal sealed class Destination(
     }
 
     companion object {
-        private fun referrer(entry: NavBackStackEntry): Pane? = entry.arguments
+        internal fun referrer(args: Bundle?): Pane? = args
             ?.getString(KEY_REFERRER)
             ?.let { value -> Pane.entries.firstOrNull { it.value == value } }
 
@@ -233,3 +244,16 @@ internal fun NavGraphBuilder.composable(
         content = { destination.Composable(navBackStackEntry = it) }
     )
 }
+
+internal fun NavGraphBuilder.bottomSheet(
+    destination: Destination,
+    arguments: List = emptyList(),
+    deepLinks: List = emptyList(),
+) {
+    bottomSheet(
+        route = destination.fullRoute,
+        arguments = arguments,
+        deepLinks = deepLinks,
+        content = { destination.Composable(navBackStackEntry = it) }
+    )
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/DestinationMappers.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/DestinationMappers.kt
index 2bc1d2b6160..ed6f990405c 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/DestinationMappers.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/DestinationMappers.kt
@@ -7,6 +7,7 @@ private val paneToDestination = mapOf(
     Pane.INSTITUTION_PICKER to Destination.InstitutionPicker,
     Pane.CONSENT to Destination.Consent,
     Pane.PARTNER_AUTH to Destination.PartnerAuth,
+    Pane.PARTNER_AUTH_DRAWER to Destination.PartnerAuthDrawer,
     Pane.ACCOUNT_PICKER to Destination.AccountPicker,
     Pane.SUCCESS to Destination.Success,
     Pane.MANUAL_ENTRY to Destination.ManualEntry,
@@ -18,6 +19,8 @@ private val paneToDestination = mapOf(
     Pane.LINK_ACCOUNT_PICKER to Destination.LinkAccountPicker,
     Pane.LINK_STEP_UP_VERIFICATION to Destination.LinkStepUpVerification,
     Pane.RESET to Destination.Reset,
+    Pane.UNEXPECTED_ERROR to Destination.Error,
+    Pane.EXIT to Destination.Exit,
     Pane.BANK_AUTH_REPAIR to Destination.BankAuthRepair,
     Pane.MANUAL_ENTRY_SUCCESS to Destination.ManualEntrySuccess,
 )
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/NavigationManager.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/NavigationManager.kt
index 30f51e7c595..9becc8eeddf 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/NavigationManager.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/NavigationManager.kt
@@ -10,19 +10,34 @@ internal interface NavigationManager {
 
     fun tryNavigateTo(
         route: String,
-        popUpToCurrent: Boolean = false,
-        inclusive: Boolean = false,
+        popUpTo: PopUpToBehavior? = null,
         isSingleTop: Boolean = true,
     )
+
+    fun tryNavigateBack()
+}
+
+internal sealed interface PopUpToBehavior {
+    val inclusive: Boolean
+
+    data class Current(
+        override val inclusive: Boolean,
+    ) : PopUpToBehavior
+
+    data class Route(
+        override val inclusive: Boolean,
+        val route: String,
+    ) : PopUpToBehavior
 }
 
 internal sealed class NavigationIntent {
     data class NavigateTo(
         val route: String,
-        val popUpToCurrent: Boolean,
-        val inclusive: Boolean,
+        val popUpTo: PopUpToBehavior?,
         val isSingleTop: Boolean,
     ) : NavigationIntent()
+
+    data object NavigateBack : NavigationIntent()
 }
 
 internal class NavigationManagerImpl @Inject constructor() : NavigationManager {
@@ -32,17 +47,19 @@ internal class NavigationManagerImpl @Inject constructor() : NavigationManager {
 
     override fun tryNavigateTo(
         route: String,
-        popUpToCurrent: Boolean,
-        inclusive: Boolean,
-        isSingleTop: Boolean
+        popUpTo: PopUpToBehavior?,
+        isSingleTop: Boolean,
     ) {
         _navigationFlow.tryEmit(
             NavigationIntent.NavigateTo(
                 route = route,
-                popUpToCurrent = popUpToCurrent,
-                inclusive = inclusive,
+                popUpTo = popUpTo,
                 isSingleTop = isSingleTop,
             )
         )
     }
+
+    override fun tryNavigateBack() {
+        _navigationFlow.tryEmit(NavigationIntent.NavigateBack)
+    }
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/bottomsheet/BottomSheet.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/bottomsheet/BottomSheet.kt
new file mode 100644
index 00000000000..142a083cb63
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/bottomsheet/BottomSheet.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.stripe.android.financialconnections.navigation.bottomsheet
+
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.ModalBottomSheetDefaults
+import androidx.compose.material.ModalBottomSheetLayout
+import androidx.compose.material.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+
+/**
+ * Helper function to create a [ModalBottomSheetLayout] from a [BottomSheetNavigator].
+ *
+ * @see [ModalBottomSheetLayout]
+ */
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+internal fun ModalBottomSheetLayout(
+    bottomSheetNavigator: BottomSheetNavigator,
+    modifier: Modifier = Modifier,
+    sheetShape: Shape = MaterialTheme.shapes.large,
+    sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
+    sheetBackgroundColor: Color = MaterialTheme.colors.surface,
+    sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
+    scrimColor: Color = ModalBottomSheetDefaults.scrimColor,
+    content: @Composable () -> Unit
+) {
+    ModalBottomSheetLayout(
+        sheetState = bottomSheetNavigator.sheetState,
+        sheetContent = bottomSheetNavigator.sheetContent,
+        modifier = modifier,
+        sheetShape = sheetShape,
+        sheetElevation = sheetElevation,
+        sheetBackgroundColor = sheetBackgroundColor,
+        sheetContentColor = sheetContentColor,
+        scrimColor = scrimColor,
+        content = content
+    )
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/bottomsheet/BottomSheetNavigation.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/bottomsheet/BottomSheetNavigation.kt
new file mode 100644
index 00000000000..11a0df82cf8
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/bottomsheet/BottomSheetNavigation.kt
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.stripe.android.financialconnections.navigation.bottomsheet
+
+import android.annotation.SuppressLint
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ModalBottomSheetState
+import androidx.compose.material.ModalBottomSheetValue
+import androidx.compose.material.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveableStateHolder
+import androidx.compose.runtime.setValue
+import androidx.navigation.FloatingWindow
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavDestination
+import androidx.navigation.NavOptions
+import androidx.navigation.Navigator
+import androidx.navigation.NavigatorState
+import com.stripe.android.financialconnections.navigation.bottomsheet.BottomSheetNavigator.Destination
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.transform
+
+/**
+ * The state of a [ModalBottomSheetLayout] that the [BottomSheetNavigator] drives
+ *
+ * @param sheetState The sheet state that is driven by the [BottomSheetNavigator]
+ */
+@OptIn(ExperimentalMaterialApi::class)
+@Stable
+internal class BottomSheetNavigatorSheetState(internal val sheetState: ModalBottomSheetState) {
+    /**
+     * @see ModalBottomSheetState.isVisible
+     */
+    internal val isVisible: Boolean
+        get() = sheetState.isVisible
+
+    /**
+     * @see ModalBottomSheetState.currentValue
+     */
+    internal val currentValue: ModalBottomSheetValue
+        get() = sheetState.currentValue
+
+    /**
+     * @see ModalBottomSheetState.targetValue
+     */
+    internal val targetValue: ModalBottomSheetValue
+        get() = sheetState.targetValue
+}
+
+/**
+ * Create and remember a [BottomSheetNavigator]
+ */
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+internal fun rememberBottomSheetNavigator(
+    animationSpec: AnimationSpec = SpringSpec()
+): BottomSheetNavigator {
+    val sheetState = rememberModalBottomSheetState(
+        ModalBottomSheetValue.Hidden,
+        animationSpec = animationSpec
+    )
+    return remember { BottomSheetNavigator(sheetState) }
+}
+
+/**
+ * Navigator that drives a [ModalBottomSheetState] for use of [ModalBottomSheetLayout]s
+ * with the navigation library. Every destination using this Navigator must set a valid
+ * [Composable] by setting it directly on an instantiated [Destination] or calling
+ * [androidx.navigation.compose.material.bottomSheet].
+ *
+ * The [sheetContent] [Composable] will always host the latest entry of the back stack. When
+ * navigating from a [BottomSheetNavigator.Destination] to another
+ * [BottomSheetNavigator.Destination], the content of the sheet will be replaced instead of a
+ * new bottom sheet being shown.
+ *
+ * When the sheet is dismissed by the user, the [state]'s [NavigatorState.backStack] will be popped.
+ *
+ * The primary constructor is not intended for public use. Please refer to
+ * [rememberBottomSheetNavigator] instead.
+ *
+ * @param sheetState The [ModalBottomSheetState] that the [BottomSheetNavigator] will use to
+ * drive the sheet state
+ */
+@OptIn(ExperimentalMaterialApi::class)
+@Navigator.Name("BottomSheetNavigator")
+internal class BottomSheetNavigator(
+    internal val sheetState: ModalBottomSheetState
+) : Navigator() {
+
+    private var attached by mutableStateOf(false)
+
+    /**
+     * Get the back stack from the [state]. In some cases, the [sheetContent] might be composed
+     * before the Navigator is attached, so we specifically return an empty flow if we aren't
+     * attached yet.
+     */
+    private val backStack: StateFlow>
+        get() = if (attached) {
+            state.backStack
+        } else {
+            MutableStateFlow(emptyList())
+        }
+
+    /**
+     * Get the transitionsInProgress from the [state]. In some cases, the [sheetContent] might be
+     * composed before the Navigator is attached, so we specifically return an empty flow if we
+     * aren't attached yet.
+     */
+    internal val transitionsInProgress: StateFlow>
+        get() = if (attached) {
+            state.transitionsInProgress
+        } else {
+            MutableStateFlow(emptySet())
+        }
+
+    /**
+     * Access properties of the [ModalBottomSheetLayout]'s [ModalBottomSheetState]
+     */
+    internal val navigatorSheetState: BottomSheetNavigatorSheetState = BottomSheetNavigatorSheetState(sheetState)
+
+    /**
+     * A [Composable] function that hosts the current sheet content. This should be set as
+     * sheetContent of your [ModalBottomSheetLayout].
+     */
+    internal val sheetContent: @Composable ColumnScope.() -> Unit = {
+        val saveableStateHolder = rememberSaveableStateHolder()
+        val transitionsInProgressEntries by transitionsInProgress.collectAsState()
+
+        // The latest back stack entry, retained until the sheet is completely hidden
+        // While the back stack is updated immediately, we might still be hiding the sheet, so
+        // we keep the entry around until the sheet is hidden
+        val retainedEntry by produceState(
+            initialValue = null,
+            key1 = backStack
+        ) {
+            backStack
+                .transform { backStackEntries ->
+                    // Always hide the sheet when the back stack is updated
+                    // Regardless of whether we're popping or pushing, we always want to hide
+                    // the sheet first before deciding whether to re-show it or keep it hidden
+                    try {
+                        sheetState.hide()
+                    } catch (_: CancellationException) {
+                        // We catch but ignore possible cancellation exceptions as we don't want
+                        // them to bubble up and cancel the whole produceState coroutine
+                    } finally {
+                        emit(backStackEntries.lastOrNull())
+                    }
+                }
+                .collect {
+                    value = it
+                }
+        }
+
+        if (retainedEntry != null) {
+            LaunchedEffect(retainedEntry) {
+                sheetState.show()
+            }
+
+            BackHandler {
+                state.popWithTransition(popUpTo = retainedEntry!!, saveState = false)
+            }
+        }
+
+        SheetContentHost(
+            backStackEntry = retainedEntry,
+            sheetState = sheetState,
+            saveableStateHolder = saveableStateHolder,
+            onSheetShown = {
+                transitionsInProgressEntries.forEach(state::markTransitionComplete)
+            },
+            onSheetDismissed = { backStackEntry ->
+                // Sheet dismissal can be started through popBackStack in which case we have a
+                // transition that we'll want to complete
+                if (transitionsInProgressEntries.contains(backStackEntry)) {
+                    state.markTransitionComplete(backStackEntry)
+                }
+                // If there is no transition in progress, the sheet has been dimissed by the
+                // user (for example by tapping on the scrim or through an accessibility action)
+                // In this case, we will immediately pop without a transition as the sheet has
+                // already been hidden
+                else {
+                    state.pop(popUpTo = backStackEntry, saveState = false)
+                }
+            }
+        )
+    }
+
+    override fun onAttach(state: NavigatorState) {
+        super.onAttach(state)
+        attached = true
+    }
+
+    override fun createDestination(): Destination = Destination(
+        navigator = this,
+        content = {}
+    )
+
+    @SuppressLint("NewApi") // b/187418647
+    override fun navigate(
+        entries: List,
+        navOptions: NavOptions?,
+        navigatorExtras: Extras?
+    ) {
+        entries.forEach { entry ->
+            state.pushWithTransition(entry)
+        }
+    }
+
+    override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
+        state.popWithTransition(popUpTo, savedState)
+    }
+
+    /**
+     * [NavDestination] specific to [BottomSheetNavigator]
+     */
+    @NavDestination.ClassType(Composable::class)
+    internal class Destination(
+        navigator: BottomSheetNavigator,
+        internal val content: @Composable ColumnScope.(NavBackStackEntry) -> Unit
+    ) : NavDestination(navigator), FloatingWindow
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/bottomsheet/NavGraphBuilder.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/bottomsheet/NavGraphBuilder.kt
new file mode 100644
index 00000000000..81de7da1899
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/bottomsheet/NavGraphBuilder.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.stripe.android.financialconnections.navigation.bottomsheet
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.runtime.Composable
+import androidx.navigation.NamedNavArgument
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavDeepLink
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.get
+
+/**
+ * Add the [content] [Composable] as bottom sheet content to the [NavGraphBuilder]
+ *
+ * @param route route for the destination
+ * @param arguments list of arguments to associate with destination
+ * @param deepLinks list of deep links to associate with the destinations
+ * @param content the sheet content at the given destination
+ */
+@SuppressLint("NewApi") // b/187418647
+internal fun NavGraphBuilder.bottomSheet(
+    route: String,
+    arguments: List = emptyList(),
+    deepLinks: List = emptyList(),
+    content: @Composable ColumnScope.(backstackEntry: NavBackStackEntry) -> Unit
+) {
+    addDestination(
+        BottomSheetNavigator.Destination(
+            provider[BottomSheetNavigator::class],
+            content
+        ).apply {
+            this.route = route
+            arguments.forEach { (argumentName, argument) ->
+                addArgument(argumentName, argument)
+            }
+            deepLinks.forEach { deepLink ->
+                addDeepLink(deepLink)
+            }
+        }
+    )
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/bottomsheet/SheetContentHost.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/bottomsheet/SheetContentHost.kt
new file mode 100644
index 00000000000..271657e63b7
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/navigation/bottomsheet/SheetContentHost.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.stripe.android.financialconnections.navigation.bottomsheet
+
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.saveable.SaveableStateHolder
+import androidx.compose.runtime.snapshotFlow
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.compose.LocalOwnersProvider
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.drop
+
+/**
+ * Hosts a [BottomSheetNavigator.Destination]'s [NavBackStackEntry] and its
+ * [BottomSheetNavigator.Destination.content] and provides a [onSheetDismissed] callback. It also
+ * shows and hides the [ModalBottomSheetLayout] through the [sheetState] when the sheet content
+ * enters or leaves the composition.
+ *
+ * @param backStackEntry The [NavBackStackEntry] holding the [BottomSheetNavigator.Destination],
+ * or null if there is no [NavBackStackEntry]
+ * @param sheetState The [ModalBottomSheetState] used to observe and control the sheet visibility
+ * @param onSheetDismissed Callback when the sheet has been dismissed. Typically, you'll want to
+ * pop the back stack here.
+ */
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+internal fun ColumnScope.SheetContentHost(
+    backStackEntry: NavBackStackEntry?,
+    sheetState: ModalBottomSheetState,
+    saveableStateHolder: SaveableStateHolder,
+    onSheetShown: (entry: NavBackStackEntry) -> Unit,
+    onSheetDismissed: (entry: NavBackStackEntry) -> Unit,
+) {
+    if (backStackEntry != null) {
+        val currentOnSheetShown by rememberUpdatedState(onSheetShown)
+        val currentOnSheetDismissed by rememberUpdatedState(onSheetDismissed)
+        LaunchedEffect(sheetState, backStackEntry) {
+            snapshotFlow { sheetState.isVisible }
+                // We are only interested in changes in the sheet's visibility
+                .distinctUntilChanged()
+                // distinctUntilChanged emits the initial value which we don't need
+                .drop(1)
+                .collect { visible ->
+                    if (visible) {
+                        currentOnSheetShown(backStackEntry)
+                    } else {
+                        currentOnSheetDismissed(backStackEntry)
+                    }
+                }
+        }
+        backStackEntry.LocalOwnersProvider(saveableStateHolder) {
+            val content =
+                (backStackEntry.destination as BottomSheetNavigator.Destination).content
+            content(backStackEntry)
+        }
+    }
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/presentation/FinancialConnectionsSheetNativeViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/presentation/FinancialConnectionsSheetNativeViewModel.kt
index 7dbaa3734df..5f1d2f2870d 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/presentation/FinancialConnectionsSheetNativeViewModel.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/presentation/FinancialConnectionsSheetNativeViewModel.kt
@@ -14,7 +14,6 @@ import com.airbnb.mvrx.compose.mavericksActivityViewModel
 import com.stripe.android.core.Logger
 import com.stripe.android.financialconnections.FinancialConnections
 import com.stripe.android.financialconnections.FinancialConnectionsSheet
-import com.stripe.android.financialconnections.R
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.AppBackgrounded
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.ClickNavBarBack
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.ClickNavBarClose
@@ -27,12 +26,10 @@ import com.stripe.android.financialconnections.di.APPLICATION_ID
 import com.stripe.android.financialconnections.di.DaggerFinancialConnectionsSheetNativeComponent
 import com.stripe.android.financialconnections.di.FinancialConnectionsSheetNativeComponent
 import com.stripe.android.financialconnections.domain.CompleteFinancialConnectionsSession
-import com.stripe.android.financialconnections.domain.GetManifest
 import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator
 import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator.Message
 import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator.Message.Complete.EarlyTerminationCause
 import com.stripe.android.financialconnections.exception.CustomManualEntryRequiredError
-import com.stripe.android.financialconnections.features.common.getBusinessName
 import com.stripe.android.financialconnections.features.manualentry.isCustomManualEntryError
 import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult
 import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Canceled
@@ -42,14 +39,13 @@ import com.stripe.android.financialconnections.launcher.FinancialConnectionsShee
 import com.stripe.android.financialconnections.model.BankAccount
 import com.stripe.android.financialconnections.model.FinancialConnectionsSession
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
-import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane.NETWORKING_LINK_SIGNUP_PANE
+import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane.EXIT
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane.UNEXPECTED_ERROR
+import com.stripe.android.financialconnections.navigation.Destination
 import com.stripe.android.financialconnections.navigation.NavigationManager
 import com.stripe.android.financialconnections.navigation.pane
-import com.stripe.android.financialconnections.presentation.FinancialConnectionsSheetNativeState.CloseDialog
 import com.stripe.android.financialconnections.presentation.FinancialConnectionsSheetNativeViewEffect.Finish
 import com.stripe.android.financialconnections.presentation.FinancialConnectionsSheetNativeViewEffect.OpenUrl
-import com.stripe.android.financialconnections.ui.TextResource
 import com.stripe.android.financialconnections.utils.UriUtils
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.sync.Mutex
@@ -58,7 +54,6 @@ import kotlinx.parcelize.Parcelize
 import javax.inject.Inject
 import javax.inject.Named
 
-@Suppress("LongParameterList", "TooManyFunctions")
 internal class FinancialConnectionsSheetNativeViewModel @Inject constructor(
     /**
      * Exposes parent dagger component (activity viewModel scoped so that it survives config changes)
@@ -66,13 +61,12 @@ internal class FinancialConnectionsSheetNativeViewModel @Inject constructor(
      */
     val activityRetainedComponent: FinancialConnectionsSheetNativeComponent,
     private val nativeAuthFlowCoordinator: NativeAuthFlowCoordinator,
-    private val getManifest: GetManifest,
     private val uriUtils: UriUtils,
     private val completeFinancialConnectionsSession: CompleteFinancialConnectionsSession,
     private val eventTracker: FinancialConnectionsAnalyticsTracker,
     private val logger: Logger,
+    private val navigationManager: NavigationManager,
     @Named(APPLICATION_ID) private val applicationId: String,
-    navigationManager: NavigationManager,
     initialState: FinancialConnectionsSheetNativeState
 ) : MavericksViewModel(initialState) {
 
@@ -91,6 +85,10 @@ internal class FinancialConnectionsSheetNativeViewModel @Inject constructor(
                     is Message.Complete -> closeAuthFlow(
                         earlyTerminationCause = message.cause
                     )
+
+                    is Message.CloseWithError -> closeAuthFlow(
+                        closeAuthFlowError = message.cause
+                    )
                 }
             }
         }
@@ -183,22 +181,8 @@ internal class FinancialConnectionsSheetNativeViewModel @Inject constructor(
     }
 
     fun onCloseWithConfirmationClick(pane: Pane) = viewModelScope.launch {
-        val manifest = kotlin.runCatching { getManifest() }.getOrNull()
-        val businessName = manifest?.getBusinessName()
-        val isNetworkingSignupPane =
-            manifest?.isNetworkingUserFlow == true && pane == NETWORKING_LINK_SIGNUP_PANE
-        val description = when {
-            isNetworkingSignupPane && businessName != null -> TextResource.StringId(
-                value = R.string.stripe_close_dialog_networking_desc,
-                args = listOf(businessName)
-            )
-
-            else -> TextResource.StringId(
-                value = R.string.stripe_close_dialog_desc
-            )
-        }
         eventTracker.track(ClickNavBarClose(pane))
-        setState { copy(closeDialog = CloseDialog(description = description)) }
+        navigationManager.tryNavigateTo(Destination.Exit(referrer = pane))
     }
 
     fun onBackClick(pane: Pane?) {
@@ -218,12 +202,6 @@ internal class FinancialConnectionsSheetNativeViewModel @Inject constructor(
         closeAuthFlowError = error
     )
 
-    fun onCloseConfirm() = closeAuthFlow(
-        closeAuthFlowError = null
-    )
-
-    fun onCloseDismiss() = setState { copy(closeDialog = null) }
-
     /**
      * [NavHost] handles back presses except for when backstack is empty, where it delegates
      * to the container activity. [onBackPressed] will be triggered on these empty backstack cases.
@@ -315,13 +293,16 @@ internal class FinancialConnectionsSheetNativeViewModel @Inject constructor(
             bankAccountToken != null
 
     fun onPaneLaunched(pane: Pane, referrer: Pane?) {
-        viewModelScope.launch {
-            eventTracker.track(
-                PaneLaunched(
-                    referrer = referrer,
-                    pane = pane
+        // Do not track pane loaded for exit pane as it is not a real pane.
+        if (pane != EXIT) {
+            viewModelScope.launch {
+                eventTracker.track(
+                    PaneLaunched(
+                        referrer = referrer,
+                        pane = pane
+                    )
                 )
-            )
+            }
         }
     }
 
@@ -374,21 +355,13 @@ internal data class FinancialConnectionsSheetNativeState(
     @PersistState
     val firstInit: Boolean,
     val configuration: FinancialConnectionsSheet.Configuration,
-    val closeDialog: CloseDialog?,
     val reducedBranding: Boolean,
+    val testMode: Boolean,
     val viewEffect: FinancialConnectionsSheetNativeViewEffect?,
     val completed: Boolean,
     val initialPane: Pane
 ) : MavericksState {
 
-    /**
-     * Payload for the close confirmation dialog,
-     * which is shown when the user clicks the close button.
-     */
-    data class CloseDialog(
-        val description: TextResource,
-    )
-
     /**
      * Used by Mavericks to build initial state based on args.
      */
@@ -396,11 +369,11 @@ internal data class FinancialConnectionsSheetNativeState(
     constructor(args: FinancialConnectionsSheetNativeActivityArgs) : this(
         webAuthFlow = WebAuthFlowState.Uninitialized,
         reducedBranding = args.initialSyncResponse.visual.reducedBranding,
+        testMode = args.initialSyncResponse.manifest.livemode.not(),
         firstInit = true,
         completed = false,
         initialPane = args.initialSyncResponse.manifest.nextPane,
         configuration = args.configuration,
-        closeDialog = null,
         viewEffect = null
     )
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/presentation/FinancialConnectionsUrls.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/presentation/FinancialConnectionsUrls.kt
index 1f101a387ba..9cbcc862f01 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/presentation/FinancialConnectionsUrls.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/presentation/FinancialConnectionsUrls.kt
@@ -1,5 +1,3 @@
-@file:Suppress("MaxLineLength", "MaximumLineLength")
-
 package com.stripe.android.financialconnections.presentation
 
 internal object FinancialConnectionsUrls {
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/CoreAuthorizationPendingNetworkingRepairRepository.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/CoreAuthorizationPendingNetworkingRepairRepository.kt
index 279820b1dc8..0cb2693fdf4 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/CoreAuthorizationPendingNetworkingRepairRepository.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/CoreAuthorizationPendingNetworkingRepairRepository.kt
@@ -35,7 +35,7 @@ internal class CoreAuthorizationPendingNetworkingRepairRepository(
         )
     }.getOrNull()
 
-    suspend fun set(coreAuthorization: String) = runCatching {
+    fun set(coreAuthorization: String) = runCatching {
         logger.debug("core authorization set to $coreAuthorization")
         setState { copy(coreAuthorization = coreAuthorization) }
     }.onFailure {
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsErrorRepository.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsErrorRepository.kt
new file mode 100644
index 00000000000..5857fcd2311
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsErrorRepository.kt
@@ -0,0 +1,35 @@
+package com.stripe.android.financialconnections.repository
+
+import com.airbnb.mvrx.ExperimentalMavericksApi
+import com.airbnb.mvrx.MavericksRepository
+import com.airbnb.mvrx.MavericksState
+import com.stripe.android.financialconnections.BuildConfig
+import kotlinx.coroutines.CoroutineScope
+
+@OptIn(ExperimentalMavericksApi::class)
+internal class FinancialConnectionsErrorRepository(
+    coroutineScope: CoroutineScope
+) : MavericksRepository(
+    initialState = State(),
+    coroutineScope = coroutineScope,
+    performCorrectnessValidations = BuildConfig.DEBUG,
+) {
+
+    suspend fun get() = awaitState().error
+
+    fun set(error: Throwable) {
+        setState {
+            copy(error = error)
+        }
+    }
+
+    fun clear() {
+        setState {
+            copy(error = null)
+        }
+    }
+
+    data class State(
+        val error: Throwable? = null
+    ) : MavericksState
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsManifestRepository.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsManifestRepository.kt
index 8f97215a117..e75caf07994 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsManifestRepository.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/repository/FinancialConnectionsManifestRepository.kt
@@ -23,7 +23,6 @@ import java.util.Locale
  * Repository to centralize reads and writes to the [FinancialConnectionsSessionManifest]
  * of the current flow.
  */
-@Suppress("TooManyFunctions")
 internal interface FinancialConnectionsManifestRepository {
 
     /**
@@ -199,7 +198,6 @@ internal interface FinancialConnectionsManifestRepository {
     }
 }
 
-@Suppress("TooManyFunctions")
 private class FinancialConnectionsManifestRepositoryImpl(
     val requestExecutor: FinancialConnectionsRequestExecutor,
     val apiRequestFactory: ApiRequest.Factory,
@@ -258,6 +256,8 @@ private class FinancialConnectionsManifestRepositoryImpl(
             "emit_events" to true,
             "locale" to locale.toLanguageTag(),
             "mobile" to mapOf(
+                // TODO REMOVE BEFORE MERGING INTEGRATION BRANCH
+                "forced_authflow_version" to "v3",
                 PARAMS_FULLSCREEN to true,
                 PARAMS_HIDE_CLOSE_BUTTON to true,
                 NetworkConstants.PARAMS_APPLICATION_ID to applicationId
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/CompositionLocal.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/CompositionLocal.kt
index 7fd1a541743..fbb3219fa70 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/CompositionLocal.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/CompositionLocal.kt
@@ -18,12 +18,14 @@ import com.stripe.android.uicore.image.StripeImageLoader
 @Composable
 internal fun FinancialConnectionsPreview(
     reducedBrandingOverride: Boolean = false,
+    testMode: Boolean = false,
     content: @Composable () -> Unit
 ) {
     val navController = rememberNavController()
     FinancialConnectionsTheme {
         CompositionLocalProvider(
             LocalNavHostController provides navController,
+            LocalTestMode provides testMode,
             LocalReducedBranding provides reducedBrandingOverride,
             LocalImageLoader provides StripeImageLoader(
                 context = LocalContext.current,
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/FinancialConnectionsSheetNativeActivity.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/FinancialConnectionsSheetNativeActivity.kt
index 3e9462d2f3a..205a8aa0b37 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/FinancialConnectionsSheetNativeActivity.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/FinancialConnectionsSheetNativeActivity.kt
@@ -8,21 +8,22 @@ import androidx.activity.addCallback
 import androidx.activity.compose.BackHandler
 import androidx.activity.compose.setContent
 import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
+import androidx.compose.material.ModalBottomSheetValue
+import androidx.compose.material.rememberModalBottomSheetState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.staticCompositionLocalOf
-import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.platform.LocalUriHandler
 import androidx.lifecycle.DefaultLifecycleObserver
 import androidx.lifecycle.LifecycleOwner
 import androidx.navigation.NavHostController
+import androidx.navigation.NavOptionsBuilder
 import androidx.navigation.compose.NavHost
 import androidx.navigation.compose.rememberNavController
 import com.airbnb.mvrx.MavericksView
@@ -30,19 +31,25 @@ import com.airbnb.mvrx.compose.collectAsState
 import com.airbnb.mvrx.withState
 import com.stripe.android.core.Logger
 import com.stripe.android.financialconnections.browser.BrowserManager
-import com.stripe.android.financialconnections.features.common.CloseDialog
+import com.stripe.android.financialconnections.exception.FinancialConnectionsErrorHandler
 import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetNativeActivityArgs
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
 import com.stripe.android.financialconnections.navigation.Destination
 import com.stripe.android.financialconnections.navigation.NavigationIntent
+import com.stripe.android.financialconnections.navigation.PopUpToBehavior
+import com.stripe.android.financialconnections.navigation.bottomSheet
+import com.stripe.android.financialconnections.navigation.bottomsheet.BottomSheetNavigator
 import com.stripe.android.financialconnections.navigation.composable
 import com.stripe.android.financialconnections.navigation.destination
 import com.stripe.android.financialconnections.navigation.pane
 import com.stripe.android.financialconnections.presentation.FinancialConnectionsSheetNativeViewEffect.Finish
 import com.stripe.android.financialconnections.presentation.FinancialConnectionsSheetNativeViewEffect.OpenUrl
 import com.stripe.android.financialconnections.presentation.FinancialConnectionsSheetNativeViewModel
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsModalBottomSheetLayout
 import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
+import com.stripe.android.financialconnections.utils.KeyboardController
 import com.stripe.android.financialconnections.utils.argsOrNull
+import com.stripe.android.financialconnections.utils.rememberKeyboardController
 import com.stripe.android.financialconnections.utils.viewModelLazy
 import com.stripe.android.uicore.image.StripeImageLoader
 import kotlinx.coroutines.flow.SharedFlow
@@ -65,6 +72,9 @@ internal class FinancialConnectionsSheetNativeActivity : AppCompatActivity(), Ma
     @Inject
     lateinit var browserManager: BrowserManager
 
+    @Inject
+    lateinit var errorHandler: FinancialConnectionsErrorHandler
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         if (args == null) {
@@ -73,28 +83,17 @@ internal class FinancialConnectionsSheetNativeActivity : AppCompatActivity(), Ma
             viewModel.activityRetainedComponent.inject(this)
             viewModel.onEach { postInvalidate() }
             onBackPressedDispatcher.addCallback { viewModel.onBackPressed() }
+            errorHandler.setup(this)
             setContent {
                 FinancialConnectionsTheme {
-                    Column {
-                        Box(modifier = Modifier.weight(1f)) {
-                            val closeDialog = viewModel.collectAsState { it.closeDialog }
-                            val firstPane =
-                                viewModel.collectAsState { it.initialPane }
-                            val reducedBranding =
-                                viewModel.collectAsState { it.reducedBranding }
-                            closeDialog.value?.let {
-                                CloseDialog(
-                                    description = it.description,
-                                    onConfirmClick = viewModel::onCloseConfirm,
-                                    onDismissClick = viewModel::onCloseDismiss
-                                )
-                            }
-                            NavHost(
-                                firstPane.value,
-                                reducedBranding.value
-                            )
-                        }
-                    }
+                    val firstPane by viewModel.collectAsState { it.initialPane }
+                    val reducedBranding by viewModel.collectAsState { it.reducedBranding }
+                    val testMode by viewModel.collectAsState { it.testMode }
+                    NavHost(
+                        initialPane = firstPane,
+                        testMode = testMode,
+                        reducedBranding = reducedBranding
+                    )
                 }
             }
         }
@@ -124,23 +123,31 @@ internal class FinancialConnectionsSheetNativeActivity : AppCompatActivity(), Ma
         }
     }
 
-    @Suppress("LongMethod")
     @Composable
     fun NavHost(
         initialPane: Pane,
+        testMode: Boolean,
         reducedBranding: Boolean
     ) {
         val context = LocalContext.current
-        val navController = rememberNavController()
-
         val uriHandler = remember { CustomTabUriHandler(context, browserManager) }
         val initialDestination = remember(initialPane) { initialPane.destination }
 
+        val sheetState = rememberModalBottomSheetState(
+            ModalBottomSheetValue.Hidden,
+            skipHalfExpanded = true
+        )
+
+        val bottomSheetNavigator = remember { BottomSheetNavigator(sheetState) }
+        val navController = rememberNavController(bottomSheetNavigator)
+        val keyboardController = rememberKeyboardController()
+
         PaneBackgroundEffects(navController)
-        NavigationEffects(viewModel.navigationFlow, navController)
+        NavigationEffects(viewModel.navigationFlow, navController, keyboardController)
 
         CompositionLocalProvider(
             LocalReducedBranding provides reducedBranding,
+            LocalTestMode provides testMode,
             LocalNavHostController provides navController,
             LocalImageLoader provides imageLoader,
             LocalUriHandler provides uriHandler
@@ -149,26 +156,33 @@ internal class FinancialConnectionsSheetNativeActivity : AppCompatActivity(), Ma
                 viewModel.onBackClick(navController.currentDestination?.pane)
                 if (navController.popBackStack().not()) viewModel.onBackPressed()
             }
-            NavHost(
-                navController,
-                startDestination = initialDestination.fullRoute,
+            FinancialConnectionsModalBottomSheetLayout(
+                bottomSheetNavigator = bottomSheetNavigator,
             ) {
-                composable(Destination.Consent)
-                composable(Destination.ManualEntry)
-                composable(Destination.PartnerAuth)
-                composable(Destination.InstitutionPicker)
-                composable(Destination.AccountPicker)
-                composable(Destination.Success)
-                composable(Destination.Reset)
-                composable(Destination.AttachLinkedPaymentAccount)
-                composable(Destination.NetworkingLinkSignup)
-                composable(Destination.NetworkingLinkLoginWarmup)
-                composable(Destination.NetworkingLinkVerification)
-                composable(Destination.NetworkingSaveToLinkVerification)
-                composable(Destination.LinkAccountPicker)
-                composable(Destination.BankAuthRepair)
-                composable(Destination.LinkStepUpVerification)
-                composable(Destination.ManualEntrySuccess)
+                NavHost(
+                    navController,
+                    startDestination = initialDestination.fullRoute,
+                ) {
+                    composable(Destination.Consent)
+                    composable(Destination.ManualEntry)
+                    composable(Destination.PartnerAuth)
+                    bottomSheet(Destination.PartnerAuthDrawer)
+                    bottomSheet(Destination.Exit)
+                    composable(Destination.InstitutionPicker)
+                    composable(Destination.AccountPicker)
+                    composable(Destination.Success)
+                    composable(Destination.Reset)
+                    composable(Destination.Error)
+                    composable(Destination.AttachLinkedPaymentAccount)
+                    composable(Destination.NetworkingLinkSignup)
+                    bottomSheet(Destination.NetworkingLinkLoginWarmup)
+                    composable(Destination.NetworkingLinkVerification)
+                    composable(Destination.NetworkingSaveToLinkVerification)
+                    composable(Destination.LinkAccountPicker)
+                    composable(Destination.BankAuthRepair)
+                    composable(Destination.LinkStepUpVerification)
+                    composable(Destination.ManualEntrySuccess)
+                }
             }
         }
     }
@@ -209,7 +223,8 @@ internal class FinancialConnectionsSheetNativeActivity : AppCompatActivity(), Ma
     @Composable
     fun NavigationEffects(
         navigationChannel: SharedFlow,
-        navHostController: NavHostController
+        navHostController: NavHostController,
+        keyboardController: KeyboardController,
     ) {
         val activity = (LocalContext.current as? Activity)
         LaunchedEffect(activity, navHostController, navigationChannel) {
@@ -217,20 +232,29 @@ internal class FinancialConnectionsSheetNativeActivity : AppCompatActivity(), Ma
                 if (activity?.isFinishing == true) {
                     return@onEach
                 }
+
+                keyboardController.dismiss()
+
                 when (intent) {
                     is NavigationIntent.NavigateTo -> {
                         val from: String? = navHostController.currentDestination?.route
                         val destination: String = intent.route
+
                         if (destination.isNotEmpty() && destination != from) {
                             logger.debug("Navigating from $from to $destination")
                             navHostController.navigate(destination) {
                                 launchSingleTop = intent.isSingleTop
-                                if (from != null && intent.popUpToCurrent) {
-                                    popUpTo(from) { inclusive = true }
+
+                                if (intent.popUpTo != null) {
+                                    apply(from, intent.popUpTo)
                                 }
                             }
                         }
                     }
+
+                    NavigationIntent.NavigateBack -> {
+                        navHostController.popBackStack()
+                    }
                 }
             }.launchIn(this)
         }
@@ -249,6 +273,10 @@ internal val LocalReducedBranding = staticCompositionLocalOf {
     error("No ReducedBranding provided")
 }
 
+internal val LocalTestMode = staticCompositionLocalOf {
+    error("No TestMode provided")
+}
+
 internal val LocalImageLoader = staticCompositionLocalOf {
     error("No ImageLoader provided")
 }
@@ -285,3 +313,19 @@ private class ActivityVisibilityObserver(
         }
     }
 }
+
+private fun NavOptionsBuilder.apply(
+    currentRoute: String?,
+    popUpTo: PopUpToBehavior,
+) {
+    val popUpToRoute = when (popUpTo) {
+        is PopUpToBehavior.Current -> currentRoute
+        is PopUpToBehavior.Route -> popUpTo.route
+    }
+
+    if (popUpToRoute != null) {
+        popUpTo(popUpToRoute) {
+            inclusive = popUpTo.inclusive
+        }
+    }
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/HandleClickableUrl.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/HandleClickableUrl.kt
new file mode 100644
index 00000000000..3662eb917d6
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/HandleClickableUrl.kt
@@ -0,0 +1,36 @@
+package com.stripe.android.financialconnections.ui
+
+import android.webkit.URLUtil
+import com.stripe.android.core.Logger
+import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.Click
+import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
+import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
+import com.stripe.android.financialconnections.utils.UriUtils
+import javax.inject.Inject
+
+internal class HandleClickableUrl @Inject constructor(
+    private val uriUtils: UriUtils,
+    private val eventTracker: FinancialConnectionsAnalyticsTracker,
+    private val logger: Logger,
+) {
+
+    suspend operator fun invoke(
+        currentPane: Pane,
+        uri: String,
+        onNetworkUrlClicked: (String) -> Unit,
+        knownDeeplinkActions: Map Unit>
+    ) {
+        uriUtils.getQueryParameter(uri, "eventName")?.let { eventName ->
+            eventTracker.track(Click(eventName, pane = currentPane))
+        }
+        when {
+            URLUtil.isNetworkUrl(uri) -> onNetworkUrlClicked(uri)
+            else -> {
+                val clickedEntry = knownDeeplinkActions.entries.firstOrNull { (key, _) ->
+                    uriUtils.compareSchemeAuthorityAndPath(key, uri)
+                }
+                clickedEntry?.value?.invoke() ?: logger.error("Unrecognized clickable text: $uri")
+            }
+        }
+    }
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/BottomSheet.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/BottomSheet.kt
new file mode 100644
index 00000000000..a814ecb6fbd
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/BottomSheet.kt
@@ -0,0 +1,41 @@
+package com.stripe.android.financialconnections.ui.components
+
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.ModalBottomSheetLayout
+import androidx.compose.material.ModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+import com.stripe.android.financialconnections.navigation.bottomsheet.BottomSheetNavigator
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
+import com.stripe.android.financialconnections.ui.theme.Neutral900
+
+@Composable
+internal fun FinancialConnectionsModalBottomSheetLayout(
+    sheetContent: @Composable ColumnScope.() -> Unit,
+    sheetState: ModalBottomSheetState,
+    content: @Composable () -> Unit
+) {
+    ModalBottomSheetLayout(
+        sheetState = sheetState,
+        sheetBackgroundColor = colors.backgroundSurface,
+        sheetShape = RoundedCornerShape(20.dp, 20.dp, 0.dp, 0.dp),
+        scrimColor = Neutral900.copy(alpha = 0.32f),
+        sheetContent = sheetContent,
+        content = content
+    )
+}
+
+@Composable
+internal fun FinancialConnectionsModalBottomSheetLayout(
+    bottomSheetNavigator: BottomSheetNavigator,
+    content: @Composable () -> Unit
+) {
+    com.stripe.android.financialconnections.navigation.bottomsheet.ModalBottomSheetLayout(
+        bottomSheetNavigator = bottomSheetNavigator,
+        sheetBackgroundColor = colors.backgroundSurface,
+        sheetShape = RoundedCornerShape(20.dp, 20.dp, 0.dp, 0.dp),
+        scrimColor = Neutral900.copy(alpha = 0.32f),
+        content = content,
+    )
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/Button.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/Button.kt
index 7a4b9c98bf0..cc485f08099 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/Button.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/Button.kt
@@ -1,14 +1,16 @@
-@file:Suppress("ktlint:filename")
-
 package com.stripe.android.financialconnections.ui.components
 
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES.R
+import android.view.HapticFeedbackConstants.CONFIRM
+import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.RowScope
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
@@ -18,7 +20,7 @@ import androidx.compose.material.Button
 import androidx.compose.material.ButtonColors
 import androidx.compose.material.ButtonDefaults
 import androidx.compose.material.ButtonDefaults.buttonColors
-import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.material.ButtonElevation
 import androidx.compose.material.ProvideTextStyle
 import androidx.compose.material.Text
 import androidx.compose.material.ripple.LocalRippleTheme
@@ -26,20 +28,32 @@ import androidx.compose.material.ripple.RippleAlpha
 import androidx.compose.material.ripple.RippleTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import com.stripe.android.financialconnections.features.common.LoadingSpinner
 import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
 import com.stripe.android.financialconnections.ui.components.FinancialConnectionsButton.Type
 import com.stripe.android.financialconnections.ui.theme.Brand400
-import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
 import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
+import com.stripe.android.financialconnections.ui.theme.Neutral0
 import com.stripe.android.financialconnections.ui.theme.Neutral50
 
+private val DefaultSpinnerHeight = 24.dp
+
 @Composable
 internal fun FinancialConnectionsButton(
     onClick: () -> Unit,
@@ -50,43 +64,59 @@ internal fun FinancialConnectionsButton(
     loading: Boolean = false,
     content: @Composable (RowScope.() -> Unit)
 ) {
+    val view = LocalView.current
+    val density = LocalDensity.current
+
     val multipleEventsCutter = remember { MultipleEventsCutter.get() }
+    var spinnerHeight by remember { mutableStateOf(DefaultSpinnerHeight) }
+
+    val loadingIndicatorAlpha by animateFloatAsState(
+        targetValue = if (loading) 1f else 0f,
+        label = "LoadingIndicatorAlpha",
+    )
+
     CompositionLocalProvider(LocalRippleTheme provides type.rippleTheme()) {
         Button(
             onClick = {
                 multipleEventsCutter.processEvent {
-                    if (loading.not()) onClick()
+                    if (loading.not()) {
+                        if (SDK_INT >= R) view.performHapticFeedback(CONFIRM)
+                        onClick()
+                    }
                 }
             },
             modifier = modifier,
-            elevation = ButtonDefaults.elevation(
-                defaultElevation = 0.dp,
-                pressedElevation = 0.dp,
-                disabledElevation = 0.dp,
-                hoveredElevation = 0.dp,
-                focusedElevation = 0.dp,
-            ),
+            elevation = type.elevation(),
             enabled = enabled,
             shape = RoundedCornerShape(size = size.radius),
-            contentPadding = size.paddingValues(),
+            contentPadding = PaddingValues(0.dp),
             colors = type.buttonColors(),
             content = {
                 ProvideTextStyle(
-                    value = FinancialConnectionsTheme.typography.bodyEmphasized.copy(
+                    value = typography.labelLargeEmphasized.copy(
                         // material button adds letter spacing internally, this removes it.
                         letterSpacing = 0.sp
                     )
                 ) {
-                    Row {
-                        if (loading) {
-                            CircularProgressIndicator(
-                                strokeWidth = 4.dp,
-                                modifier = Modifier.size(21.dp),
-                                color = colors.textWhite
-                            )
-                            Spacer(modifier = Modifier.size(8.dp))
-                        }
-                        content()
+                    Box(contentAlignment = Alignment.Center) {
+                        Row(
+                            modifier = Modifier
+                                .alpha(1f - loadingIndicatorAlpha)
+                                .padding(size.paddingValues())
+                                .onSizeChanged {
+                                    // Set the spinner to the same height as the label,
+                                    // so we avoid visual jitter.
+                                    spinnerHeight = with(density) { it.height.toDp() }
+                                },
+                            content = content,
+                        )
+
+                        LoadingSpinner(
+                            strokeWidth = 2.dp,
+                            modifier = Modifier
+                                .size(spinnerHeight)
+                                .alpha(loadingIndicatorAlpha),
+                        )
                     }
                 }
             }
@@ -97,9 +127,8 @@ internal fun FinancialConnectionsButton(
 private fun Type.rippleTheme() = object : RippleTheme {
     @Composable
     override fun defaultColor() = when (this@rippleTheme) {
-        Type.Primary -> Color.White
-        Type.Secondary -> colors.textSecondary
-        Type.Critical -> Color.White
+        Type.Primary -> Neutral0
+        Type.Secondary -> colors.textDefault
     }
 
     @Composable
@@ -117,46 +146,43 @@ internal object FinancialConnectionsButton {
         abstract fun buttonColors(): ButtonColors
         abstract fun rippleColor(): Color
 
+        @Composable
+        abstract fun elevation(): ButtonElevation
+
         data object Primary : Type() {
             @Composable
-            override fun buttonColors(): ButtonColors {
-                return buttonColors(
-                    backgroundColor = colors.textBrand,
-                    contentColor = colors.textWhite,
-                    disabledBackgroundColor = colors.textBrand,
-                    disabledContentColor = colors.textWhite.copy(alpha = 0.3f)
-                )
-            }
+            override fun buttonColors(): ButtonColors = buttonColors(
+                backgroundColor = colors.iconBrand,
+                contentColor = colors.textWhite,
+                disabledBackgroundColor = colors.iconBrand,
+                disabledContentColor = colors.textWhite.copy(alpha = 0.4f)
+            )
 
             override fun rippleColor(): Color = Brand400
+
+            @Composable
+            override fun elevation(): ButtonElevation = ButtonDefaults.elevation()
         }
 
         data object Secondary : Type() {
             @Composable
-            override fun buttonColors(): ButtonColors {
-                return buttonColors(
-                    backgroundColor = colors.backgroundContainer,
-                    contentColor = colors.textPrimary,
-                    disabledBackgroundColor = colors.backgroundContainer,
-                    disabledContentColor = colors.textPrimary.copy(alpha = 0.12f)
-                )
-            }
+            override fun buttonColors(): ButtonColors = buttonColors(
+                backgroundColor = Neutral50,
+                contentColor = colors.textDefault,
+                disabledBackgroundColor = Neutral50,
+                disabledContentColor = colors.textDefault.copy(alpha = 0.4f)
+            )
 
             override fun rippleColor(): Color = Neutral50
-        }
 
-        data object Critical : Type() {
             @Composable
-            override fun buttonColors(): ButtonColors {
-                return buttonColors(
-                    backgroundColor = colors.textCritical,
-                    contentColor = colors.textWhite,
-                    disabledBackgroundColor = colors.textCritical.copy(alpha = 0.12f),
-                    disabledContentColor = colors.textPrimary.copy(alpha = 0.12f)
-                )
-            }
-
-            override fun rippleColor(): Color = Neutral50
+            override fun elevation(): ButtonElevation = ButtonDefaults.elevation(
+                defaultElevation = 0.dp,
+                pressedElevation = 0.dp,
+                disabledElevation = 0.dp,
+                hoveredElevation = 0.dp,
+                focusedElevation = 0.dp,
+            )
         }
     }
 
@@ -166,18 +192,6 @@ internal object FinancialConnectionsButton {
         abstract fun paddingValues(): PaddingValues
         abstract val radius: Dp
 
-        data object Pill : Size() {
-            override val radius: Dp = 4.dp
-
-            @Composable
-            override fun paddingValues(): PaddingValues = PaddingValues(
-                start = 8.dp,
-                top = 4.dp,
-                end = 8.dp,
-                bottom = 4.dp
-            )
-        }
-
         data object Regular : Size() {
             override val radius: Dp = 12.dp
 
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/Scaffold.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/Scaffold.kt
index a1e3b688249..f23c840700e 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/Scaffold.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/Scaffold.kt
@@ -1,5 +1,3 @@
-@file:Suppress("ktlint:filename")
-
 package com.stripe.android.financialconnections.ui.components
 
 import androidx.compose.foundation.layout.PaddingValues
@@ -14,7 +12,7 @@ internal fun FinancialConnectionsScaffold(
 ) {
     Scaffold(
         backgroundColor = FinancialConnectionsTheme.colors.backgroundSurface,
-        contentColor = FinancialConnectionsTheme.colors.textPrimary,
+        contentColor = FinancialConnectionsTheme.colors.textDefault,
         topBar = topBar,
         content = content
     )
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/TestModeBanner.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/TestModeBanner.kt
new file mode 100644
index 00000000000..614752eb89c
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/TestModeBanner.kt
@@ -0,0 +1,103 @@
+package com.stripe.android.financialconnections.ui.components
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.stripe.android.financialconnections.R
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
+
+@Composable
+internal fun TestModeBanner(
+    enabled: Boolean,
+    buttonLabel: String,
+    onButtonClick: () -> Unit,
+    modifier: Modifier = Modifier,
+    description: String = stringResource(R.string.stripe_verification_inTestMode),
+) {
+    val contentAlpha = if (enabled) ContentAlpha.high else ContentAlpha.disabled
+
+    Row(
+        verticalAlignment = Alignment.CenterVertically,
+        horizontalArrangement = Arrangement.spacedBy(8.dp),
+        modifier = modifier
+            .fillMaxWidth()
+            .background(
+                color = FinancialConnectionsTheme.colors.backgroundCaution,
+                shape = RoundedCornerShape(12.dp),
+            )
+            .alpha(contentAlpha)
+            .padding(
+                vertical = 8.dp,
+                horizontal = 16.dp,
+            ),
+    ) {
+        Image(
+            painter = painterResource(R.drawable.stripe_ic_info),
+            colorFilter = ColorFilter.tint(
+                color = FinancialConnectionsTheme.colors.iconCaution,
+            ),
+            contentDescription = null,
+        )
+
+        Text(
+            text = description,
+            style = FinancialConnectionsTheme.typography.bodyMedium,
+            modifier = Modifier.weight(1f),
+        )
+
+        Text(
+            text = buttonLabel,
+            color = FinancialConnectionsTheme.colors.textBrand,
+            style = FinancialConnectionsTheme.typography.bodyMediumEmphasized,
+            modifier = Modifier.clickable(enabled = enabled, onClick = onButtonClick)
+        )
+    }
+}
+
+@Preview(
+    group = "Test Mode Banner",
+    name = "Enabled",
+)
+@Composable
+internal fun TestModeBannerPreviewEnabled() {
+    FinancialConnectionsTheme {
+        TestModeBanner(
+            enabled = true,
+            buttonLabel = "Use test code",
+            onButtonClick = {},
+            modifier = Modifier.padding(16.dp),
+        )
+    }
+}
+
+@Preview(
+    group = "Test Mode Banner",
+    name = "Disabled",
+)
+@Composable
+internal fun TestModeBannerPreviewDisabled() {
+    FinancialConnectionsTheme {
+        TestModeBanner(
+            enabled = false,
+            buttonLabel = "Use test code",
+            onButtonClick = {},
+            modifier = Modifier.padding(16.dp),
+        )
+    }
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/Text.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/Text.kt
index e9ac028cce9..e10bc72b6e9 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/Text.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/Text.kt
@@ -1,5 +1,3 @@
-@file:Suppress("MatchingDeclarationName")
-
 package com.stripe.android.financialconnections.ui.components
 
 import android.graphics.Typeface
@@ -21,6 +19,7 @@ import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextDecoration
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.core.text.getSpans
 import com.stripe.android.financialconnections.ui.TextResource
@@ -33,14 +32,14 @@ internal fun AnnotatedText(
     defaultStyle: TextStyle,
     modifier: Modifier = Modifier,
     annotationStyles: Map = mapOf(
-        StringAnnotation.CLICKABLE to FinancialConnectionsTheme.typography.bodyEmphasized
+        StringAnnotation.CLICKABLE to defaultStyle
             .toSpanStyle()
-            .copy(color = FinancialConnectionsTheme.colors.textBrand)
+            .copy(textDecoration = TextDecoration.Underline)
     ),
     maxLines: Int = Int.MAX_VALUE,
     overflow: TextOverflow = TextOverflow.Clip
 ) {
-    val pressedColor = FinancialConnectionsTheme.colors.textPrimary
+    val pressedColor = FinancialConnectionsTheme.colors.textDefault
     var pressedAnnotation: String? by remember { mutableStateOf(null) }
     val resource = annotatedStringResource(
         resource = text,
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/TextField.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/TextField.kt
index 2354ac379e2..3601170ef86 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/TextField.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/TextField.kt
@@ -1,19 +1,19 @@
-@file:OptIn(ExperimentalMaterialApi::class)
-@file:Suppress("ktlint:filename")
-
 package com.stripe.android.financialconnections.ui.components
 
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardActions
 import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ContentAlpha
 import androidx.compose.material.ExposedDropdownMenuDefaults.outlinedTextFieldColors
 import androidx.compose.material.OutlinedTextField
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.shadow
 import androidx.compose.ui.text.input.VisualTransformation
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
@@ -22,68 +22,77 @@ import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsThem
 
 @Composable
 internal fun FinancialConnectionsOutlinedTextField(
-    value: TextFieldValue,
+    value: String,
+    enabled: Boolean,
     modifier: Modifier = Modifier,
-    onValueChange: (TextFieldValue) -> Unit,
+    onValueChange: (String) -> Unit,
     readOnly: Boolean = false,
     isError: Boolean = false,
     keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+    keyboardActions: KeyboardActions = KeyboardActions.Default,
     placeholder: @Composable (() -> Unit)? = null,
     visualTransformation: VisualTransformation = VisualTransformation.None,
     trailingIcon: @Composable (() -> Unit)? = null,
     leadingIcon: @Composable (() -> Unit)? = null,
     label: @Composable (() -> Unit)? = null
 ) {
+    val contentAlpha = if (enabled) ContentAlpha.high else ContentAlpha.disabled
+    val shape = RoundedCornerShape(12.dp)
     OutlinedTextField(
-        shape = RoundedCornerShape(8.dp),
-        modifier = modifier.fillMaxWidth(),
+        enabled = enabled,
+        shape = shape,
+        modifier = modifier
+            .fillMaxWidth()
+            .alpha(contentAlpha)
+            .shadow(1.dp, shape),
         leadingIcon = leadingIcon,
         trailingIcon = trailingIcon,
         placeholder = placeholder,
+        maxLines = 1,
         visualTransformation = visualTransformation,
         keyboardOptions = keyboardOptions,
+        keyboardActions = keyboardActions,
         readOnly = readOnly,
         isError = isError,
         value = value,
         colors = outlinedTextFieldColors(
-            focusedBorderColor = FinancialConnectionsTheme.colors.textBrand,
-            unfocusedBorderColor = FinancialConnectionsTheme.colors.borderDefault,
+            backgroundColor = FinancialConnectionsTheme.colors.backgroundSurface,
+            focusedBorderColor = FinancialConnectionsTheme.colors.borderBrand,
+            unfocusedBorderColor = FinancialConnectionsTheme.colors.border,
             disabledBorderColor = FinancialConnectionsTheme.colors.textDisabled,
-            unfocusedLabelColor = FinancialConnectionsTheme.colors.textSecondary,
+            unfocusedLabelColor = FinancialConnectionsTheme.colors.textSubdued,
             errorBorderColor = FinancialConnectionsTheme.colors.textCritical,
-            focusedLabelColor = FinancialConnectionsTheme.colors.textBrand,
-            cursorColor = FinancialConnectionsTheme.colors.textBrand,
+            focusedLabelColor = FinancialConnectionsTheme.colors.textSubdued,
+            cursorColor = FinancialConnectionsTheme.colors.borderBrand,
             errorCursorColor = FinancialConnectionsTheme.colors.textCritical,
             errorLabelColor = FinancialConnectionsTheme.colors.textCritical,
             errorTrailingIconColor = FinancialConnectionsTheme.colors.textCritical,
-            trailingIconColor = FinancialConnectionsTheme.colors.borderDefault,
-            focusedTrailingIconColor = FinancialConnectionsTheme.colors.borderDefault
+            trailingIconColor = FinancialConnectionsTheme.colors.iconDefault,
+            focusedTrailingIconColor = FinancialConnectionsTheme.colors.iconDefault
         ),
         onValueChange = onValueChange,
         label = label
     )
 }
 
-internal fun TextFieldValue.filtered(predicate: (Char) -> Boolean): TextFieldValue = copy(
-    text = text.filter(predicate),
-    selection = selection.adjustForFilter(text, predicate),
-    composition = composition?.adjustForFilter(text, predicate),
-)
-
-private fun TextRange.adjustForFilter(
-    text: String,
-    predicate: (Char) -> Boolean
-): TextRange = TextRange(
-    start = text.subSequence(0, start).count(predicate),
-    end = text.subSequence(0, end).count(predicate),
-)
-
 @Preview(group = "Components", name = "TextField - idle")
 @Composable
 internal fun FinancialConnectionsOutlinedTextFieldPreview() {
     FinancialConnectionsPreview {
-        Column {
-            FinancialConnectionsOutlinedTextField(value = TextFieldValue("test"), onValueChange = {})
-        }
+        FinancialConnectionsScaffold(
+            topBar = { FinancialConnectionsTopAppBar { } },
+            content = {
+                Column(
+                    Modifier.padding(16.dp),
+                    verticalArrangement = Arrangement.spacedBy(8.dp)
+                ) {
+                    FinancialConnectionsOutlinedTextField(
+                        value = "test",
+                        enabled = true,
+                        onValueChange = {}
+                    )
+                }
+            }
+        )
     }
 }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/TopAppBar.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/TopAppBar.kt
index bcc48753c59..52a2cdef1c2 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/TopAppBar.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/components/TopAppBar.kt
@@ -1,77 +1,187 @@
-@file:Suppress("ktlint:filename")
-
 package com.stripe.android.financialconnections.ui.components
 
+import androidx.activity.OnBackPressedDispatcher
 import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
 import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.material.AppBarDefaults
 import androidx.compose.material.Icon
 import androidx.compose.material.IconButton
+import androidx.compose.material.Text
 import androidx.compose.material.TopAppBar
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.ArrowBack
 import androidx.compose.material.icons.filled.Close
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTagsAsResourceId
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
+import androidx.navigation.NavHostController
 import com.stripe.android.financialconnections.R
+import com.stripe.android.financialconnections.navigation.bottomsheet.BottomSheetNavigator
 import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
 import com.stripe.android.financialconnections.ui.LocalNavHostController
 import com.stripe.android.financialconnections.ui.LocalReducedBranding
+import com.stripe.android.financialconnections.ui.LocalTestMode
+import com.stripe.android.financialconnections.ui.theme.Attention200
 import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme
+import com.stripe.android.financialconnections.utils.KeyboardController
+import com.stripe.android.financialconnections.utils.rememberKeyboardController
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+private val LOGO_WIDTH = 50.dp
+private val LOGO_HEIGHT = 20.dp
+private val PILL_HORIZONTAL_PADDING = 4.dp
+private val PILL_VERTICAL_PADDING = 2.dp
+private const val PILL_RADIUS = 8f
 
 @Composable
 internal fun FinancialConnectionsTopAppBar(
     hideStripeLogo: Boolean = LocalReducedBranding.current,
+    testMode: Boolean = LocalTestMode.current,
     elevation: Dp = 0.dp,
-    showBack: Boolean = true,
+    allowBackNavigation: Boolean = true,
     onCloseClick: () -> Unit
 ) {
-    val localBackPressed = LocalOnBackPressedDispatcherOwner.current
-        ?.onBackPressedDispatcher
+    val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current
+    val localBackPressed = onBackPressedDispatcher?.onBackPressedDispatcher
+
     val navController = LocalNavHostController.current
+    val canShowBackIcon by navController.collectCanShowBackIconAsState()
+
+    val keyboardController = rememberKeyboardController()
+    val scope = rememberCoroutineScope()
+
     TopAppBar(
-        title = if (hideStripeLogo) {
-            { /* Empty content */ }
-        } else {
-            {
-                Icon(
-                    painter = painterResource(id = R.drawable.stripe_logo),
-                    contentDescription = null // decorative element
-                )
-            }
+        title = {
+            Title(
+                hideStripeLogo = hideStripeLogo,
+                testmode = testMode
+            )
         },
         elevation = elevation,
-        navigationIcon = if (navController.previousBackStackEntry != null && showBack) {
+        navigationIcon = if (canShowBackIcon && allowBackNavigation) {
             {
-                IconButton(onClick = { localBackPressed?.onBackPressed() }) {
-                    Icon(
-                        imageVector = Icons.Filled.ArrowBack,
-                        contentDescription = "Back icon",
-                        tint = FinancialConnectionsTheme.colors.textSecondary
-                    )
-                }
+                BackButton(
+                    scope = scope,
+                    keyboardController = keyboardController,
+                    localBackPressed = localBackPressed
+                )
             }
         } else {
             null
         },
         actions = {
-            IconButton(onClick = onCloseClick) {
-                Icon(
-                    imageVector = Icons.Filled.Close,
-                    contentDescription = "Close icon",
-                    tint = FinancialConnectionsTheme.colors.textSecondary
-                )
-            }
+            CloseButton(
+                scope = scope,
+                keyboardController = keyboardController,
+                onCloseClick = onCloseClick
+            )
         },
         backgroundColor = FinancialConnectionsTheme.colors.textWhite,
         contentColor = FinancialConnectionsTheme.colors.textBrand
     )
 }
 
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+private fun BackButton(
+    scope: CoroutineScope,
+    keyboardController: KeyboardController,
+    localBackPressed: OnBackPressedDispatcher?
+) {
+    IconButton(
+        onClick = {
+            scope.launch {
+                keyboardController.dismiss()
+                localBackPressed?.onBackPressed()
+            }
+        },
+    ) {
+        Icon(
+            imageVector = Icons.Filled.ArrowBack,
+            contentDescription = "Back icon",
+            tint = FinancialConnectionsTheme.colors.iconDefault,
+            modifier = Modifier
+                .testTag("top-app-bar-back-button")
+                .semantics { testTagsAsResourceId = true }
+        )
+    }
+}
+
+@Composable
+private fun CloseButton(
+    scope: CoroutineScope,
+    keyboardController: KeyboardController,
+    onCloseClick: () -> Unit
+) {
+    IconButton(
+        onClick = {
+            scope.launch {
+                keyboardController.dismiss()
+                onCloseClick()
+            }
+        }
+    ) {
+        Icon(
+            imageVector = Icons.Filled.Close,
+            contentDescription = "Close icon",
+            tint = FinancialConnectionsTheme.colors.iconDefault
+        )
+    }
+}
+
+@Composable
+private fun Title(hideStripeLogo: Boolean, testmode: Boolean) = Row(
+    verticalAlignment = Alignment.CenterVertically,
+    horizontalArrangement = Arrangement.spacedBy(8.dp)
+) {
+    if (hideStripeLogo.not()) {
+        Icon(
+            modifier = Modifier.size(width = LOGO_WIDTH, height = LOGO_HEIGHT),
+            painter = painterResource(id = R.drawable.stripe_logo),
+            contentDescription = null // decorative element
+        )
+    }
+    // show a test mode pill if in test mode
+    if (testmode) {
+        Text(
+            modifier = Modifier
+                .drawBehind {
+                    drawRoundRect(
+                        color = Attention200,
+                        cornerRadius = CornerRadius(PILL_RADIUS)
+                    )
+                }
+                .padding(vertical = PILL_VERTICAL_PADDING, horizontal = PILL_HORIZONTAL_PADDING),
+            text = "Test",
+            style = FinancialConnectionsTheme.typography.labelMediumEmphasized,
+            color = FinancialConnectionsTheme.colors.textWhite
+        )
+    }
+}
+
 /**
  * calculates toolbar elevation based on [ScrollState]
  */
@@ -93,6 +203,25 @@ internal val LazyListState.elevation: Dp
         AppBarDefaults.TopAppBarElevation
     }
 
+@Composable
+private fun NavHostController.collectCanShowBackIconAsState(): State {
+    val canShowBackIcon = remember { mutableStateOf(false) }
+    DisposableEffect(Unit) {
+        val listener = NavController.OnDestinationChangedListener { controller, destination, _ ->
+            if (destination.navigatorName == BottomSheetNavigator::class.java.simpleName) {
+                // We're looking at a bottom sheet, so don't change the back button
+            } else {
+                canShowBackIcon.value = controller.previousBackStackEntry != null
+            }
+        }
+        addOnDestinationChangedListener(listener)
+        onDispose {
+            removeOnDestinationChangedListener(listener)
+        }
+    }
+    return canShowBackIcon
+}
+
 @Preview(group = "Components", name = "TopAppBar")
 @Composable
 internal fun TopAppBarNoStripeLogoPreview() {
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/sdui/ServerDrivenUi.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/sdui/ServerDrivenUi.kt
index 7bedeaabd43..aee7877be6d 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/sdui/ServerDrivenUi.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/sdui/ServerDrivenUi.kt
@@ -1,10 +1,10 @@
-@file:Suppress("MatchingDeclarationName")
-
 package com.stripe.android.financialconnections.ui.sdui
 
 import android.os.Build
 import android.text.Html
 import android.text.Spanned
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import com.stripe.android.financialconnections.model.Bullet
 import com.stripe.android.financialconnections.ui.ImageResource
 import com.stripe.android.financialconnections.ui.TextResource
@@ -34,3 +34,6 @@ internal fun fromHtml(source: String): Spanned {
         Html.fromHtml(source)
     }
 }
+
+@Composable
+internal fun rememberHtml(html: String): TextResource.Text = remember(html) { TextResource.Text(fromHtml(html)) }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/Color.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/Color.kt
index 79227f96cc1..d4045438c34 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/Color.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/Color.kt
@@ -1,63 +1,107 @@
-@file:Suppress("MagicNumber")
-
 package com.stripe.android.financialconnections.ui.theme
 
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
 
-internal val Info100 = Color(0xffCFF5F6)
-internal val Success100 = Color(0xffD7F7C2)
-
-internal val Neutral50 = Color(0xffF6F8FA)
-internal val Neutral150 = Color(0xffE0E6EB)
-internal val Neutral200 = Color(0xffC0C8D2)
-internal val Neutral300 = Color(0xffA3ACBA)
-internal val Neutral500 = Color(0xff6A7383)
-internal val Neutral800 = Color(0xff30313D)
+internal val Neutral0 = Color(0xffFFFFFF)
+internal val Neutral50 = Color(0xffF5F6F8)
+internal val Neutral900 = Color(0xff21252C)
 
-internal val Attention500 = Color(0xffC84801)
-internal val Attention400 = Color(0xffED6704)
-internal val Attention100 = Color(0xffFCEDB9)
-internal val Attention50 = Color(0xffFEF9DA)
-
-internal val Brand100 = Color(0xffF2EBFF)
+internal val Brand50 = Color(0xffF7F5FD)
 internal val Brand400 = Color(0xff8D7FFA)
-internal val Brand500 = Color(0xff625AFA)
-
-internal val Blue500 = Color(0xff0570DE)
-internal val Blue400 = Color(0xFF0196ED)
 
-internal val Red500 = Color(0xffDF1B41)
+internal val Attention200 = Color(0xffFCAF4F)
 
-internal val Green400 = Color(0xff3FA40D)
-internal val Green500 = Color(0xff228403)
+internal object LinkColors {
+    val Brand200 = Color(0xffA6FBDD)
+    val Brand600 = Color(0xff1AC59B)
+}
 
-/**
- * Financial Connections custom Color Palette
- */
 @Immutable
 internal data class FinancialConnectionsColors(
-    // backgrounds
-    val backgroundSurface: Color,
-    val backgroundContainer: Color,
-    val backgroundBackdrop: Color,
-    // borders
-    val borderDefault: Color,
-    val borderFocus: Color,
-    val borderInvalid: Color,
-    // text
-    val textPrimary: Color,
-    val textSecondary: Color,
+    val textDefault: Color,
+    val textSubdued: Color,
     val textDisabled: Color,
     val textWhite: Color,
     val textBrand: Color,
-    val textInfo: Color,
-    val textSuccess: Color,
-    val textAttention: Color,
     val textCritical: Color,
-    // icons
+    val iconDefault: Color,
+    val iconSubdued: Color,
+    val iconWhite: Color,
     val iconBrand: Color,
-    val iconInfo: Color,
-    val iconSuccess: Color,
-    val iconAttention: Color
+    val iconCaution: Color,
+    val buttonPrimary: Color,
+    val buttonPrimaryHover: Color,
+    val buttonPrimaryPressed: Color,
+    val buttonSecondary: Color,
+    val buttonSecondaryHover: Color,
+    val buttonSecondaryPressed: Color,
+    val backgroundSurface: Color,
+    val background: Color,
+    val backgroundOffset: Color,
+    val backgroundBrand: Color,
+    val backgroundCaution: Color,
+    val border: Color,
+    val borderBrand: Color
 )
+
+@Preview(group = "Components", name = "Colors")
+@Composable
+internal fun ColorsPreview() {
+    FinancialConnectionsPreview {
+        Column(
+            modifier = Modifier.background(Color.White)
+        ) {
+            ColorPreview("textDefault", colors.textDefault)
+            ColorPreview("textSubdued", colors.textSubdued)
+            ColorPreview("textDisabled", colors.textDisabled)
+            ColorPreview("textWhite", colors.textWhite)
+            ColorPreview("textBrand", colors.textBrand)
+            ColorPreview("textCritical", colors.textCritical)
+            ColorPreview("iconDefault", colors.iconDefault)
+            ColorPreview("iconSubdued", colors.iconSubdued)
+            ColorPreview("iconWhite", colors.iconWhite)
+            ColorPreview("iconBrand", colors.iconBrand)
+            ColorPreview("buttonPrimary", colors.buttonPrimary)
+            ColorPreview("buttonPrimaryHover", colors.buttonPrimaryHover)
+            ColorPreview("buttonPrimaryPressed", colors.buttonPrimaryPressed)
+            ColorPreview("buttonSecondary", colors.buttonSecondary)
+            ColorPreview("buttonSecondaryHover", colors.buttonSecondaryHover)
+            ColorPreview("buttonSecondaryPressed", colors.buttonSecondaryPressed)
+            ColorPreview("background", colors.background)
+            ColorPreview("backgroundBrand", colors.backgroundBrand)
+            ColorPreview("border", colors.border)
+            ColorPreview("borderBrand", colors.borderBrand)
+        }
+    }
+}
+
+@Composable
+private fun ColorPreview(colorText: String, color: Color) {
+    Row {
+        Box(
+            Modifier
+                .size(40.dp)
+                .background(color)
+        )
+        Text(
+            text = colorText,
+            style = typography.bodyMedium,
+            modifier = Modifier.padding(10.dp)
+        )
+    }
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/Layout.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/Layout.kt
new file mode 100644
index 00000000000..db7660bc007
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/Layout.kt
@@ -0,0 +1,233 @@
+package com.stripe.android.financialconnections.ui.theme
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsButton
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsScaffold
+import com.stripe.android.financialconnections.ui.components.FinancialConnectionsTopAppBar
+import com.stripe.android.financialconnections.ui.components.elevation
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
+
+/**
+ * A layout that contains a body, and an optional, bottom fixed footer.
+ *
+ * @param modifier the modifier to apply to the layout.
+ * @param body the content of the layout.
+ * @param footer the content of the footer.
+ * @param inModal whether the layout is being used in a modal or not. If true, the [body] won't expand to fill the
+ * available content.
+ * @param showFooterShadowWhenScrollable whether to show a shadow at the top of the footer when the body is scrollable.
+ * @param scrollState the [ScrollState] to use for the scrollable body.
+ */
+@Composable
+internal fun Layout(
+    modifier: Modifier = Modifier,
+    body: @Composable ColumnScope.() -> Unit,
+    footer: (@Composable () -> Unit)? = null,
+    bodyPadding: PaddingValues = PaddingValues(horizontal = 24.dp),
+    inModal: Boolean = false,
+    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
+    showFooterShadowWhenScrollable: Boolean = true,
+    scrollState: ScrollState = rememberScrollState(),
+) {
+    LayoutScaffold(
+        canScrollForward = scrollState.canScrollForward,
+        inModal = inModal,
+        showFooterShadowWhenScrollable = showFooterShadowWhenScrollable,
+        modifier = modifier,
+        footer = footer,
+    ) {
+        Column(
+            modifier = Modifier
+                .verticalScroll(scrollState)
+                .animateContentSize(),
+            verticalArrangement = verticalArrangement,
+        ) {
+            // Nested columns to achieve proper content padding
+            Column(modifier = Modifier.padding(bodyPadding)) {
+                body()
+            }
+        }
+    }
+}
+
+/**
+ * A layout that contains a body, and an optional, bottom fixed footer.
+ *
+ * @param modifier the modifier to apply to the layout.
+ * @param body the content of the layout.
+ * @param footer the content of the footer.
+ * @param inModal whether the layout is being used in a modal or not. If true, the [body] won't expand to fill the
+ * available content.
+ * @param showFooterShadowWhenScrollable whether to show a shadow at the top of the footer when the body is scrollable.
+ * @param lazyListState the [LazyListState] to use for the scrollable body.
+ */
+@Composable
+internal fun LazyLayout(
+    modifier: Modifier = Modifier,
+    body: LazyListScope.() -> Unit,
+    footer: (@Composable () -> Unit)? = null,
+    bodyPadding: PaddingValues = PaddingValues(horizontal = 24.dp),
+    inModal: Boolean = false,
+    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
+    showFooterShadowWhenScrollable: Boolean = true,
+    lazyListState: LazyListState = rememberLazyListState()
+) {
+    LayoutScaffold(
+        canScrollForward = lazyListState.canScrollForward,
+        inModal = inModal,
+        showFooterShadowWhenScrollable = showFooterShadowWhenScrollable,
+        modifier = modifier,
+        footer = footer,
+    ) {
+        LazyColumn(
+            state = lazyListState,
+            verticalArrangement = verticalArrangement,
+            contentPadding = bodyPadding,
+        ) {
+            body()
+        }
+    }
+}
+
+@Composable
+private fun LayoutScaffold(
+    canScrollForward: Boolean,
+    inModal: Boolean,
+    showFooterShadowWhenScrollable: Boolean,
+    modifier: Modifier = Modifier,
+    footer: (@Composable () -> Unit)?,
+    body: @Composable () -> Unit,
+) {
+    Column(
+        modifier
+            .also { if (inModal.not()) it.fillMaxSize() }
+    ) {
+        // Box to contain the layout body and an optional footer shadow drawn on top.
+        Box(
+            Modifier
+                .fillMaxWidth()
+                .weight(1f, fill = inModal.not())
+        ) {
+            // Footer shadow (top aligned)
+            if (showFooterShadowWhenScrollable && canScrollForward) {
+                FooterTopShadow()
+            }
+            // Body content
+            body()
+        }
+        // Footer content (bottom aligned)
+        footer?.let {
+            Box(
+                modifier = Modifier.padding(
+                    top = 16.dp,
+                    bottom = 24.dp,
+                    start = 24.dp,
+                    end = 24.dp,
+                ),
+                content = { it() }
+            )
+        }
+    }
+}
+
+@Composable
+private fun BoxScope.FooterTopShadow() {
+    val shadowSize = 4
+    Box(
+        modifier = Modifier.Companion
+            .align(Alignment.BottomCenter)
+            .fillMaxWidth()
+            .height(shadowSize.dp)
+            .background(
+                brush = Brush.verticalGradient(
+                    colors = listOf(
+                        Color.Transparent,
+                        Color.LightGray.copy(alpha = 0.2f)
+                    ),
+                    0.0f,
+                    shadowSize.toFloat()
+                )
+            )
+    )
+}
+
+@Preview(showBackground = true)
+@Composable
+internal fun LayoutPreview() {
+    FinancialConnectionsTheme {
+        val state = rememberLazyListState()
+        FinancialConnectionsScaffold(
+            topBar = {
+                FinancialConnectionsTopAppBar(
+                    hideStripeLogo = false,
+                    elevation = state.elevation,
+                    onCloseClick = {}
+                )
+            },
+            content = {
+                LazyLayout(
+                    lazyListState = state,
+                    body = {
+                        item {
+                            Text(
+                                "Title",
+                                style = typography.headingXLarge
+                            )
+                        }
+                        for (index in 1..50) {
+                            item {
+                                Text("Body item $index")
+                            }
+                        }
+                    },
+                    footer = {
+                        Column(
+                            Modifier.fillMaxWidth()
+                        ) {
+                            FinancialConnectionsButton(
+                                modifier = Modifier.fillMaxWidth(),
+                                onClick = {}
+                            ) {
+                                Text("Button 1")
+                            }
+                            Spacer(modifier = Modifier.height(16.dp))
+                            FinancialConnectionsButton(
+                                modifier = Modifier.fillMaxWidth(),
+                                onClick = { }
+                            ) {
+                                Text("Button 1")
+                            }
+                        }
+                    }
+                )
+            }
+        )
+    }
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/StripeThemeForConnections.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/StripeThemeForConnections.kt
index fda3b1c0311..2c8338c7934 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/StripeThemeForConnections.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/StripeThemeForConnections.kt
@@ -1,6 +1,7 @@
 package com.stripe.android.financialconnections.ui.theme
 
 import androidx.compose.runtime.Composable
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.colors
 import com.stripe.android.uicore.StripeTheme
 import com.stripe.android.uicore.StripeThemeDefaults
 
@@ -12,12 +13,16 @@ internal fun StripeThemeForConnections(
     val stripeDefaultColors = StripeThemeDefaults.colors(isDark = false)
     StripeTheme(
         colors = stripeDefaultColors.copy(
+            onComponent = colors.textDefault,
+            componentBorder = colors.border,
+            placeholderText = colors.textSubdued,
             materialColors = stripeDefaultColors.materialColors.copy(
-                primary = FinancialConnectionsTheme.colors.iconBrand
+                primary = colors.iconBrand,
+                error = colors.textCritical,
             )
         ),
         shapes = StripeThemeDefaults.shapes.copy(
-            cornerRadius = 9f
+            cornerRadius = 12f
         ),
         typography = StripeThemeDefaults.typography
     ) {
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/Theme.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/Theme.kt
index 8666b046549..ec497e1c811 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/Theme.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/Theme.kt
@@ -22,136 +22,140 @@ import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.text.PlatformTextStyle
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.LineHeightStyle
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.window.DialogWindowProvider
 import androidx.core.view.WindowCompat
+import androidx.navigation.compose.rememberNavController
+import com.stripe.android.financialconnections.ui.LocalNavHostController
 
-private val LightColorPalette = FinancialConnectionsColors(
-    backgroundSurface = Color.White,
-    backgroundContainer = Neutral50,
-    backgroundBackdrop = Neutral200.copy(alpha = .70f),
-    borderDefault = Neutral150,
-    borderFocus = Blue400.copy(alpha = .36f),
-    borderInvalid = Red500,
-    textPrimary = Neutral800,
-    textSecondary = Neutral500,
-    textDisabled = Neutral300,
-    textWhite = Color.White,
-    textBrand = Brand500,
-    textInfo = Blue500,
-    textSuccess = Green500,
-    textAttention = Attention500,
-    textCritical = Red500,
-    iconBrand = Brand400,
-    iconInfo = Blue400,
-    iconSuccess = Green400,
-    iconAttention = Attention400
+private val Colors = FinancialConnectionsColors(
+    textDefault = Color(0xFF353A44),
+    textSubdued = Color(0xFF596171),
+    textDisabled = Color(0xFF818DA0),
+    textWhite = Color(0xFFFFFFFF),
+    textBrand = Color(0xFF533AFD),
+    textCritical = Color(0xFFC0123C),
+    iconDefault = Color(0xFF474E5A),
+    iconSubdued = Color(0xFF6C7688),
+    iconWhite = Color(0xFFFFFFFF),
+    iconBrand = Color(0xFF675DFF),
+    iconCaution = Color(0xFFFF8F0E),
+    buttonPrimary = Color(0xFF675DFF),
+    buttonPrimaryHover = Color(0xFF857AFE),
+    buttonPrimaryPressed = Color(0xFF533AFD),
+    buttonSecondary = Color(0xFFF5F6F8),
+    buttonSecondaryHover = Color(0xFFF5F6F8),
+    buttonSecondaryPressed = Color(0xFFEBEEF1),
+    background = Color(0xFFF5F6F8),
+    backgroundSurface = Color(0xFFFFFFFF),
+    backgroundOffset = Color(0xFFF6F8FA),
+    backgroundBrand = Color(0xFFF5F6F8),
+    backgroundCaution = Color(0xFFFEF9DA),
+    border = Color(0xFFD8DEE4),
+    borderBrand = Color(0xFF675DFF),
+)
+
+private val lineHeightStyle = LineHeightStyle(
+    alignment = LineHeightStyle.Alignment.Center,
+    trim = LineHeightStyle.Trim.None
 )
 
 private val Typography = FinancialConnectionsTypography(
-    subtitle = TextStyle(
-        fontSize = 24.sp,
+    headingXLarge = TextStyle(
+        fontSize = 28.sp,
         lineHeight = 32.sp,
-        fontWeight = FontWeight.W700
+        letterSpacing = 0.38.sp,
+        fontWeight = FontWeight.W700,
+        lineHeightStyle = lineHeightStyle
     ).toCompat(),
-    subtitleEmphasized = TextStyle(
-        fontSize = 24.sp,
+    headingXLargeSubdued = TextStyle(
+        fontSize = 28.sp,
         lineHeight = 32.sp,
-        fontWeight = FontWeight.W700
+        letterSpacing = 0.38.sp,
+        fontWeight = FontWeight.W400,
+        lineHeightStyle = lineHeightStyle
     ).toCompat(),
-    heading = TextStyle(
-        fontSize = 18.sp,
-        lineHeight = 24.sp,
-        fontWeight = FontWeight.W700
-    ).toCompat(),
-    subheading = TextStyle(
-        fontSize = 18.sp,
-        lineHeight = 24.sp,
-        fontWeight = FontWeight.W600
+    headingLarge = TextStyle(
+        fontSize = 24.sp,
+        lineHeight = 32.sp,
+        letterSpacing = 0.30.sp,
+        fontWeight = FontWeight.W700,
+        lineHeightStyle = lineHeightStyle
     ).toCompat(),
-    kicker = TextStyle(
-        fontSize = 12.sp,
-        lineHeight = 20.sp,
-        fontWeight = FontWeight.W600
+    headingMedium = TextStyle(
+        fontSize = 20.sp,
+        lineHeight = 28.sp,
+        letterSpacing = 0.30.sp,
+        fontWeight = FontWeight.W700,
+        lineHeightStyle = lineHeightStyle
     ).toCompat(),
-    body = TextStyle(
+    bodyMediumEmphasized = TextStyle(
         fontSize = 16.sp,
         lineHeight = 24.sp,
-        fontWeight = FontWeight.W400
+        fontWeight = FontWeight.W600,
+        lineHeightStyle = lineHeightStyle
     ).toCompat(),
-    bodyEmphasized = TextStyle(
+    bodyMedium = TextStyle(
         fontSize = 16.sp,
         lineHeight = 24.sp,
-        fontWeight = FontWeight.W600
-    ).toCompat(),
-    detail = TextStyle(
-        fontSize = 14.sp,
-        lineHeight = 20.sp,
-        fontWeight = FontWeight.W400
+        fontWeight = FontWeight.W400,
+        lineHeightStyle = lineHeightStyle
     ).toCompat(),
-    detailEmphasized = TextStyle(
+    bodySmall = TextStyle(
         fontSize = 14.sp,
         lineHeight = 20.sp,
-        fontWeight = FontWeight.W600
-    ).toCompat(),
-    caption = TextStyle(
-        fontSize = 12.sp,
-        lineHeight = 18.sp,
-        fontWeight = FontWeight.W400
+        fontWeight = FontWeight.W400,
+        lineHeightStyle = lineHeightStyle
     ).toCompat(),
-    captionEmphasized = TextStyle(
-        fontSize = 12.sp,
-        lineHeight = 18.sp,
-        fontWeight = FontWeight.W600
-    ).toCompat(),
-    captionTight = TextStyle(
-        fontSize = 12.sp,
-        lineHeight = 16.sp,
-        fontWeight = FontWeight.W400
+    labelLargeEmphasized = TextStyle(
+        fontSize = 16.sp,
+        lineHeight = 24.sp,
+        fontWeight = FontWeight.W600,
+        lineHeightStyle = lineHeightStyle
     ).toCompat(),
-    captionTightEmphasized = TextStyle(
-        fontSize = 12.sp,
-        lineHeight = 16.sp,
-        fontWeight = FontWeight.W600
+    labelLarge = TextStyle(
+        fontSize = 16.sp,
+        lineHeight = 24.sp,
+        fontWeight = FontWeight.W400,
+        lineHeightStyle = lineHeightStyle
     ).toCompat(),
-    bodyCode = TextStyle(
+    labelMediumEmphasized = TextStyle(
         fontSize = 14.sp,
         lineHeight = 20.sp,
-        fontWeight = FontWeight.W400
+        fontWeight = FontWeight.W600,
+        lineHeightStyle = lineHeightStyle
     ).toCompat(),
-    bodyCodeEmphasized = TextStyle(
+    labelMedium = TextStyle(
         fontSize = 14.sp,
         lineHeight = 20.sp,
-        fontWeight = FontWeight.W700
+        fontWeight = FontWeight.W400,
+        lineHeightStyle = lineHeightStyle
     ).toCompat(),
-    captionCode = TextStyle(
+    labelSmall = TextStyle(
         fontSize = 12.sp,
         lineHeight = 16.sp,
-        fontWeight = FontWeight.W400
-    ).toCompat(),
-    captionCodeEmphasized = TextStyle(
-        fontSize = 12.sp,
-        lineHeight = 16.sp,
-        fontWeight = FontWeight.W700
+        fontWeight = FontWeight.W400,
+        lineHeightStyle = lineHeightStyle
     ).toCompat(),
 )
 
 private val TextSelectionColors = TextSelectionColors(
-    handleColor = LightColorPalette.textBrand,
-    backgroundColor = LightColorPalette.textBrand.copy(alpha = 0.4f)
+    handleColor = Colors.textBrand,
+    backgroundColor = Colors.textBrand.copy(alpha = 0.4f)
 )
 
 @Immutable
 private object FinancialConnectionsRippleTheme : RippleTheme {
     @Composable
     override fun defaultColor() = RippleTheme.defaultRippleColor(
-        contentColor = LightColorPalette.textBrand,
+        contentColor = Colors.textBrand,
         lightTheme = MaterialTheme.colors.isLight
     )
 
     @Composable
     override fun rippleAlpha() = RippleTheme.defaultRippleAlpha(
-        contentColor = LightColorPalette.textBrand,
+        contentColor = Colors.textBrand,
         lightTheme = MaterialTheme.colors.isLight
     )
 }
@@ -159,12 +163,13 @@ private object FinancialConnectionsRippleTheme : RippleTheme {
 @Composable
 internal fun FinancialConnectionsTheme(content: @Composable () -> Unit) {
     CompositionLocalProvider(
-        LocalFinancialConnectionsTypography provides Typography,
-        LocalFinancialConnectionsColors provides LightColorPalette
+        LocalNavHostController provides rememberNavController(),
+        LocalTypography provides Typography,
+        LocalColors provides Colors
     ) {
         val view = LocalView.current
         val window = findWindow()
-        val barColor = FinancialConnectionsTheme.colors.borderDefault
+        val barColor = FinancialConnectionsTheme.colors.border
         if (!view.isInEditMode) {
             SideEffect {
                 window?.let { window ->
@@ -203,22 +208,23 @@ private tailrec fun Context.findWindow(): Window? =
         else -> null
     }
 
-private val LocalFinancialConnectionsTypography =
+private val LocalTypography =
     staticCompositionLocalOf {
-        error("no FinancialConnectionsTypography provided")
+        error("no Typography provided")
     }
 
-private val LocalFinancialConnectionsColors = staticCompositionLocalOf {
-    error("No FinancialConnectionsColors provided")
-}
+private val LocalColors =
+    staticCompositionLocalOf {
+        error("no Colors provided")
+    }
 
 internal object FinancialConnectionsTheme {
-    val colors: FinancialConnectionsColors
+    val typography
         @Composable
-        get() = LocalFinancialConnectionsColors.current
-    val typography: FinancialConnectionsTypography
+        get() = LocalTypography.current
+    val colors
         @Composable
-        get() = LocalFinancialConnectionsTypography.current
+        get() = LocalColors.current
 }
 
 private fun TextStyle.toCompat(useDefaultLineHeight: Boolean = false): TextStyle {
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/Type.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/Type.kt
index 77a960b3b4f..8538bd1155d 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/Type.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/ui/theme/Type.kt
@@ -1,93 +1,90 @@
-@file:Suppress("MatchingDeclarationName", "ktlint:filename")
-
 package com.stripe.android.financialconnections.ui.theme
 
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Column
+import androidx.compose.material.Divider
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.tooling.preview.Preview
 import com.stripe.android.financialconnections.ui.FinancialConnectionsPreview
+import com.stripe.android.financialconnections.ui.theme.FinancialConnectionsTheme.typography
 
 @Immutable
 internal data class FinancialConnectionsTypography(
-    val subtitle: TextStyle,
-    val subtitleEmphasized: TextStyle,
-    val heading: TextStyle,
-    val subheading: TextStyle,
-    val kicker: TextStyle,
-    val body: TextStyle,
-    val bodyEmphasized: TextStyle,
-    val detail: TextStyle,
-    val detailEmphasized: TextStyle,
-    val caption: TextStyle,
-    val captionEmphasized: TextStyle,
-    val captionTight: TextStyle,
-    val captionTightEmphasized: TextStyle,
-    val bodyCode: TextStyle,
-    val bodyCodeEmphasized: TextStyle,
-    val captionCode: TextStyle,
-    val captionCodeEmphasized: TextStyle,
-
+    val headingXLarge: TextStyle,
+    val headingXLargeSubdued: TextStyle,
+    val headingLarge: TextStyle,
+    val headingMedium: TextStyle,
+    val bodyMediumEmphasized: TextStyle,
+    val bodyMedium: TextStyle,
+    val bodySmall: TextStyle,
+    val labelLargeEmphasized: TextStyle,
+    val labelLarge: TextStyle,
+    val labelMediumEmphasized: TextStyle,
+    val labelMedium: TextStyle,
+    val labelSmall: TextStyle
 )
 
 @Preview(group = "Components", name = "Type")
 @Composable
 internal fun TypePreview() {
     FinancialConnectionsPreview {
-        Column {
-            Text(
-                text = "subtitle",
-                style = FinancialConnectionsTheme.typography.subtitle
-            )
+        Column(
+            modifier = Modifier.background(Color.White)
+        ) {
             Text(
-                text = "subtitleEmphasized",
-                style = FinancialConnectionsTheme.typography.subtitleEmphasized
+                text = "Heading XLarge",
+                style = typography.headingXLarge
             )
             Text(
-                text = "heading",
-                style = FinancialConnectionsTheme.typography.heading
+                text = "Heading XLarge Subdued",
+                style = typography.headingXLargeSubdued
             )
             Text(
-                text = "subheading",
-                style = FinancialConnectionsTheme.typography.subheading
+                text = "Heading Large",
+                style = typography.headingLarge
             )
             Text(
-                text = "KICKER",
-                style = FinancialConnectionsTheme.typography.kicker
+                text = "Heading Medium",
+                style = typography.headingMedium
             )
+            Divider()
             Text(
-                text = "body",
-                style = FinancialConnectionsTheme.typography.body
+                text = "Body Medium Emphasized",
+                style = typography.bodyMediumEmphasized
             )
             Text(
-                text = "bodyEmphasized",
-                style = FinancialConnectionsTheme.typography.bodyEmphasized
+                text = "Body Medium",
+                style = typography.bodyMedium
             )
             Text(
-                text = "detail",
-                style = FinancialConnectionsTheme.typography.detail
+                text = "Body Small",
+                style = typography.bodySmall
             )
+            Divider()
             Text(
-                text = "detailEmphasized",
-                style = FinancialConnectionsTheme.typography.detailEmphasized
+                text = "Label Large Emphasized",
+                style = typography.labelLargeEmphasized
             )
             Text(
-                text = "caption",
-                style = FinancialConnectionsTheme.typography.caption
+                text = "Label Large",
+                style = typography.labelLarge
             )
             Text(
-                text = "captionEmphasized",
-                style = FinancialConnectionsTheme.typography.captionEmphasized
+                text = "Label Medium Emphasized",
+                style = typography.labelMediumEmphasized
             )
             Text(
-                text = "captionTight",
-                style = FinancialConnectionsTheme.typography.captionTight
+                text = "Label Medium",
+                style = typography.labelMedium
             )
             Text(
-                text = "captionTightEmphasized",
-                style = FinancialConnectionsTheme.typography.captionTightEmphasized
+                text = "Label Small",
+                style = typography.labelSmall
             )
         }
     }
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/utils/KeyboardController.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/utils/KeyboardController.kt
new file mode 100644
index 00000000000..f7a50e921e8
--- /dev/null
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/utils/KeyboardController.kt
@@ -0,0 +1,68 @@
+package com.stripe.android.financialconnections.utils
+
+import android.view.ViewTreeObserver
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.platform.LocalTextInputService
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import kotlinx.coroutines.flow.first
+
+internal class KeyboardController(
+    private val dismissKeyboard: () -> Unit,
+    private val isKeyboardVisible: State,
+) {
+
+    suspend fun dismiss() {
+        if (isKeyboardVisible.value) {
+            dismissKeyboard()
+            awaitKeyboardDismissed()
+        }
+    }
+
+    private suspend fun awaitKeyboardDismissed() {
+        snapshotFlow { isKeyboardVisible.value }.first { !it }
+    }
+}
+
+@Composable
+internal fun rememberKeyboardController(): KeyboardController {
+    val textInputService = LocalTextInputService.current
+    val keyboardState = isKeyboardVisibleAsState()
+
+    return KeyboardController(
+        dismissKeyboard = {
+            // We're using this method because LocalSoftwareKeyboardController is
+            // still experimental in Compose 1.5. We should switch over once we update
+            // to Compose 1.6, in which the experimental state has been removed.
+            textInputService?.hideSoftwareKeyboard()
+        },
+        isKeyboardVisible = keyboardState,
+    )
+}
+
+@Composable
+private fun isKeyboardVisibleAsState(): State {
+    val view = LocalView.current
+    val state = remember { mutableStateOf(false) }
+
+    DisposableEffect(view) {
+        val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
+            val insets = ViewCompat.getRootWindowInsets(view)
+            val isKeyboardOpen = insets?.isVisible(WindowInsetsCompat.Type.ime()) ?: true
+            state.value = isKeyboardOpen
+        }
+
+        view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)
+        onDispose {
+            view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
+        }
+    }
+
+    return state
+}
diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/utils/UriUtils.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/utils/UriUtils.kt
index a67775657f3..26bc05610c9 100644
--- a/financial-connections/src/main/java/com/stripe/android/financialconnections/utils/UriUtils.kt
+++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/utils/UriUtils.kt
@@ -11,7 +11,7 @@ internal class UriUtils @Inject constructor(
     private val logger: Logger,
     private val tracker: FinancialConnectionsAnalyticsTracker,
 ) {
-    suspend fun compareSchemeAuthorityAndPath(
+    fun compareSchemeAuthorityAndPath(
         uriString1: String,
         uriString2: String
     ): Boolean {
@@ -28,7 +28,7 @@ internal class UriUtils @Inject constructor(
      *
      * stripe-auth://link-accounts/authentication_return?code=failure
      */
-    suspend fun getQueryParameter(uri: String, key: String): String? = kotlin.runCatching {
+    fun getQueryParameter(uri: String, key: String): String? = kotlin.runCatching {
         uri.toUriOrNull()?.getQueryParameter(key)
     }.onFailure { error ->
         tracker.logError(
@@ -66,7 +66,7 @@ internal class UriUtils @Inject constructor(
         )
     }.getOrNull()
 
-    private suspend fun String.toUriOrNull(): Uri? = kotlin.runCatching {
+    private fun String.toUriOrNull(): Uri? = kotlin.runCatching {
         return Uri.parse(this)
     }.onFailure { error ->
         tracker.logError(
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/TestFinancialConnectionsAnalyticsTracker.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/TestFinancialConnectionsAnalyticsTracker.kt
index 671b17f743e..d5bd11929c2 100644
--- a/financial-connections/src/test/java/com/stripe/android/financialconnections/TestFinancialConnectionsAnalyticsTracker.kt
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/TestFinancialConnectionsAnalyticsTracker.kt
@@ -8,9 +8,8 @@ internal class TestFinancialConnectionsAnalyticsTracker : FinancialConnectionsAn
 
     val sentEvents = mutableListOf()
 
-    override suspend fun track(event: FinancialConnectionsAnalyticsEvent): Result {
+    override fun track(event: FinancialConnectionsAnalyticsEvent) {
         sentEvents += event
-        return Result.success(Unit)
     }
 
     /**
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/exception/FinancialConnectionsErrorHandlerTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/exception/FinancialConnectionsErrorHandlerTest.kt
new file mode 100644
index 00000000000..e595d4e0f30
--- /dev/null
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/exception/FinancialConnectionsErrorHandlerTest.kt
@@ -0,0 +1,104 @@
+package com.stripe.android.financialconnections.exception
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.testing.TestLifecycleOwner
+import com.google.common.truth.Truth.assertThat
+import com.stripe.android.core.exception.APIConnectionException
+import com.stripe.android.core.exception.StripeException
+import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent
+import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import java.lang.Thread.UncaughtExceptionHandler
+
+@OptIn(ExperimentalCoroutinesApi::class)
+internal class FinancialConnectionsErrorHandlerTest {
+
+    @Before
+    fun before() {
+        Dispatchers.setMain(StandardTestDispatcher())
+    }
+
+    @After
+    fun after() {
+        Dispatchers.resetMain()
+    }
+
+    @Test
+    fun `Correctly consumes exceptions when merchant has uncaught exception handler`() = runTest {
+        val merchantHandler = RecordingUncaughtExceptionHandler()
+        val financialConnectionsHandler = RecordingFinancialConnectionsErrorHandler()
+
+        Thread.setDefaultUncaughtExceptionHandler(merchantHandler)
+
+        val lifecycleOwner = TestLifecycleOwner()
+        financialConnectionsHandler.handler.setup(lifecycleOwner)
+
+        mockUncaughtException()
+
+        assertThat(merchantHandler.encounteredErrors).hasSize(1)
+        assertThat(financialConnectionsHandler.encounteredErrors).hasSize(1)
+
+        lifecycleOwner.setCurrentState(Lifecycle.State.DESTROYED)
+
+        mockUncaughtException()
+
+        assertThat(merchantHandler.encounteredErrors).hasSize(2)
+        assertThat(financialConnectionsHandler.encounteredErrors).hasSize(1)
+    }
+
+    @Test
+    fun `Correctly consumes exceptions when merchant does not have uncaught exception handler`() = runTest {
+        val financialConnectionsHandler = RecordingFinancialConnectionsErrorHandler()
+        Thread.setDefaultUncaughtExceptionHandler(null)
+
+        val lifecycleOwner = TestLifecycleOwner()
+        financialConnectionsHandler.handler.setup(lifecycleOwner)
+
+        mockUncaughtException()
+        assertThat(financialConnectionsHandler.encounteredErrors).hasSize(1)
+
+        lifecycleOwner.setCurrentState(Lifecycle.State.DESTROYED)
+
+        // The FinancialConnectionsErrorHandler no longer receives uncaught exceptions
+        mockUncaughtException()
+        assertThat(financialConnectionsHandler.encounteredErrors).hasSize(1)
+    }
+
+    private fun mockUncaughtException() {
+        Thread.getDefaultUncaughtExceptionHandler()?.uncaughtException(
+            Thread.currentThread(),
+            StripeException.create(APIConnectionException()),
+        )
+    }
+}
+
+private class RecordingUncaughtExceptionHandler : UncaughtExceptionHandler {
+
+    private val _encounteredErrors = mutableListOf()
+
+    val encounteredErrors: List
+        get() = _encounteredErrors
+
+    override fun uncaughtException(thread: Thread, error: Throwable) {
+        _encounteredErrors += error
+    }
+}
+
+private class RecordingFinancialConnectionsErrorHandler {
+
+    private val trackedEvents = mutableListOf()
+    private val tracker = FinancialConnectionsAnalyticsTracker(trackedEvents::add)
+
+    val handler = FinancialConnectionsErrorHandler(tracker)
+
+    val encounteredErrors: List
+        get() = trackedEvents
+}
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/accountpicker/AccountPickerViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/accountpicker/AccountPickerViewModelTest.kt
index 257176962d1..1577b4cd713 100644
--- a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/accountpicker/AccountPickerViewModelTest.kt
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/accountpicker/AccountPickerViewModelTest.kt
@@ -46,6 +46,7 @@ internal class AccountPickerViewModelTest {
         getOrFetchSync = getSync,
         navigationManager = navigationManager,
         logger = Logger.noop(),
+        handleClickableUrl = mock(),
         pollAuthorizationSessionAccounts = pollAuthorizationSessionAccounts
     )
 
@@ -146,6 +147,48 @@ internal class AccountPickerViewModelTest {
         withState(viewModel) { state ->
             assertThat(state.selectedIds).isEqualTo(setOf("selectable"))
         }
+
+        eventTracker.assertContainsEvent(
+            "linked_accounts.account_picker.accounts_auto_selected",
+            mapOf(
+                "account_ids" to "selectable",
+                "is_single_account" to "true",
+            )
+        )
+    }
+
+    @Test
+    fun `init - if not singleAccount, pre-selects first available account`() = runTest {
+        givenManifestReturns(
+            sessionManifest().copy(
+                singleAccount = false,
+                activeAuthSession = authorizationSession()
+            )
+        )
+
+        givenPollAccountsReturns(
+            partnerAccountList().copy(
+                data = listOf(
+                    partnerAccount().copy(id = "unelectable", _allowSelection = false),
+                    partnerAccount().copy(id = "selectable_1"),
+                    partnerAccount().copy(id = "selectable_2")
+                )
+            )
+        )
+
+        val viewModel = buildViewModel(AccountPickerState())
+
+        withState(viewModel) { state ->
+            assertThat(state.selectedIds).isEqualTo(setOf("selectable_1", "selectable_2"))
+        }
+
+        eventTracker.assertContainsEvent(
+            "linked_accounts.account_picker.accounts_auto_selected",
+            mapOf(
+                "account_ids" to "selectable_1 selectable_2",
+                "is_single_account" to "false",
+            )
+        )
     }
 
     private suspend fun givenManifestReturns(manifest: FinancialConnectionsSessionManifest) {
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/institutionpicker/InstitutionPickerViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/institutionpicker/InstitutionPickerViewModelTest.kt
index 1313da95c26..475f85539ee 100644
--- a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/institutionpicker/InstitutionPickerViewModelTest.kt
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/institutionpicker/InstitutionPickerViewModelTest.kt
@@ -8,18 +8,25 @@ import com.stripe.android.financialconnections.ApiKeyFixtures
 import com.stripe.android.financialconnections.FinancialConnectionsSheet
 import com.stripe.android.financialconnections.TestFinancialConnectionsAnalyticsTracker
 import com.stripe.android.financialconnections.domain.FeaturedInstitutions
-import com.stripe.android.financialconnections.domain.GetManifest
+import com.stripe.android.financialconnections.domain.GetOrFetchSync
+import com.stripe.android.financialconnections.domain.PostAuthorizationSession
 import com.stripe.android.financialconnections.domain.SearchInstitutions
 import com.stripe.android.financialconnections.domain.UpdateLocalManifest
+import com.stripe.android.financialconnections.exception.InstitutionPlannedDowntimeError
+import com.stripe.android.financialconnections.model.FinancialConnectionsAuthorizationSession
 import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
+import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
 import com.stripe.android.financialconnections.model.InstitutionResponse
-import com.stripe.android.financialconnections.navigation.NavigationManager
+import com.stripe.android.financialconnections.navigation.Destination
+import com.stripe.android.financialconnections.utils.TestHandleError
+import com.stripe.android.financialconnections.utils.TestNavigationManager
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.advanceUntilIdle
 import kotlinx.coroutines.test.runTest
 import org.junit.Rule
 import org.junit.Test
+import org.mockito.kotlin.any
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verifyNoInteractions
 import org.mockito.kotlin.whenever
@@ -35,9 +42,11 @@ internal class InstitutionPickerViewModelTest {
 
     private val searchInstitutions = mock()
     private val featuredInstitutions = mock()
-    private val getManifest = mock()
+    private val sync = mock()
+    private val handleError = TestHandleError()
     private val updateLocalManifest = mock()
-    private val navigationManager = mock()
+    private val navigationManager = TestNavigationManager()
+    private val postAuthorizationSession = mock()
     private val eventTracker = TestFinancialConnectionsAnalyticsTracker()
     private val defaultConfiguration = FinancialConnectionsSheet.Configuration(
         ApiKeyFixtures.DEFAULT_FINANCIAL_CONNECTIONS_SESSION_SECRET,
@@ -51,11 +60,13 @@ internal class InstitutionPickerViewModelTest {
             configuration = defaultConfiguration,
             searchInstitutions = searchInstitutions,
             featuredInstitutions = featuredInstitutions,
-            getManifest = getManifest,
+            getOrFetchSync = sync,
             navigationManager = navigationManager,
             updateLocalManifest = updateLocalManifest,
             logger = Logger.noop(),
             eventTracker = eventTracker,
+            postAuthorizationSession = postAuthorizationSession,
+            handleError = handleError,
             initialState = state
         )
     }
@@ -82,11 +93,45 @@ internal class InstitutionPickerViewModelTest {
         val viewModel = buildViewModel(InstitutionPickerState())
 
         withState(viewModel) { state ->
-            assertEquals(state.payload()!!.featuredInstitutions, institutionResponse.data)
+            assertEquals(state.payload()!!.featuredInstitutions, institutionResponse)
             assertIs(state.searchInstitutions)
         }
     }
 
+    @Test
+    fun `init - fails to fetch featured institutions succeeds with empty list`() = runTest {
+        val error = RuntimeException("error")
+        whenever(featuredInstitutions(defaultConfiguration.financialConnectionsSessionClientSecret))
+            .thenThrow(error)
+
+        givenManifestReturns(ApiKeyFixtures.sessionManifest())
+
+        val viewModel = buildViewModel(InstitutionPickerState())
+
+        withState(viewModel) { state ->
+            // payload with empty list
+            assertTrue(state.payload()!!.featuredInstitutions.data.isEmpty())
+        }
+    }
+
+    @Test
+    fun `init - fail to fetch payload launches error screen`() = runTest {
+        val error = RuntimeException("error")
+        whenever(sync()).thenThrow(error)
+
+        val viewModel = buildViewModel(InstitutionPickerState())
+
+        withState(viewModel) { state ->
+            assertTrue(state.payload() == null)
+            handleError.assertError(
+                error = error,
+                extraMessage = "Error fetching initial payload",
+                pane = Pane.INSTITUTION_PICKER,
+                displayErrorScreen = true
+            )
+        }
+    }
+
     @Test
     fun `onQueryChanged - institutions are searched and event sent`() = runTest {
         val query = "query"
@@ -126,8 +171,8 @@ internal class InstitutionPickerViewModelTest {
         advanceUntilIdle()
 
         withState(viewModel) { state ->
-            assertEquals(state.payload()!!.featuredInstitutions, featuredResults.data)
-            assertEquals(state.searchInstitutions()!!.data, searchResults.data)
+            assertEquals(state.payload()!!.featuredInstitutions, featuredResults)
+            assertEquals(state.searchInstitutions()!!, searchResults)
             eventTracker.assertContainsEvent(
                 expectedEventName = "linked_accounts.search.succeeded",
                 expectedParams = mapOf(
@@ -155,8 +200,79 @@ internal class InstitutionPickerViewModelTest {
         }
     }
 
+    @Test
+    fun `onInstitutionSelected - OAuth institution navigates to partner Auth in modal mode`() = runTest {
+        val institution = ApiKeyFixtures.institution()
+
+        givenManifestReturns(ApiKeyFixtures.sessionManifest())
+        givenCreateSessionForInstitutionReturns(ApiKeyFixtures.authorizationSession().copy(_isOAuth = true))
+
+        val viewModel = buildViewModel(InstitutionPickerState())
+
+        viewModel.onInstitutionSelected(institution, fromFeatured = true)
+
+        navigationManager.assertNavigatedTo(
+            destination = Destination.PartnerAuthDrawer,
+            pane = Pane.INSTITUTION_PICKER,
+        )
+    }
+
+    @Test
+    fun `onInstitutionSelected - non-OAuth institution navigates to partner Auth in full-screen mode`() = runTest {
+        val institution = ApiKeyFixtures.institution()
+
+        givenManifestReturns(ApiKeyFixtures.sessionManifest())
+        givenCreateSessionForInstitutionReturns(ApiKeyFixtures.authorizationSession().copy(_isOAuth = false))
+
+        val viewModel = buildViewModel(InstitutionPickerState())
+
+        viewModel.onInstitutionSelected(institution, fromFeatured = true)
+
+        navigationManager.assertNavigatedTo(
+            destination = Destination.PartnerAuth,
+            pane = Pane.INSTITUTION_PICKER,
+        )
+    }
+
+    @Test
+    fun `onInstitutionSelected - Failed to create AuthSession navigates to error screen`() = runTest {
+        val institution = ApiKeyFixtures.institution()
+
+        givenManifestReturns(ApiKeyFixtures.sessionManifest())
+        val error = InstitutionPlannedDowntimeError(
+            institution,
+            showManualEntry = true,
+            isToday = true,
+            backUpAt = 10000L,
+            stripeException = mock()
+        )
+        givenCreateSessionForInstitutionThrows(error)
+
+        val viewModel = buildViewModel(InstitutionPickerState())
+
+        viewModel.onInstitutionSelected(institution, fromFeatured = true)
+
+        handleError.assertError(
+            error = error,
+            extraMessage = "Error selecting or creating session for institution",
+            pane = Pane.INSTITUTION_PICKER,
+            displayErrorScreen = true
+        )
+    }
+
+    private suspend fun givenCreateSessionForInstitutionThrows(throwable: Throwable) {
+        whenever(postAuthorizationSession(any(), any())).then { throw throwable }
+    }
+
+    private suspend fun InstitutionPickerViewModelTest.givenCreateSessionForInstitutionReturns(
+        financialConnectionsAuthorizationSession: FinancialConnectionsAuthorizationSession
+    ) {
+        whenever(postAuthorizationSession(any(), any()))
+            .thenReturn(financialConnectionsAuthorizationSession)
+    }
+
     private suspend fun givenManifestReturns(manifest: FinancialConnectionsSessionManifest) {
-        whenever(getManifest()).thenReturn(manifest)
+        whenever(sync()).thenReturn(ApiKeyFixtures.syncResponse(manifest))
     }
 
     private suspend fun givenSearchInstitutionsReturns(
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerViewModelTest.kt
index fc7def9ab08..cb28ed3c8a1 100644
--- a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerViewModelTest.kt
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/linkaccountpicker/LinkAccountPickerViewModelTest.kt
@@ -6,11 +6,11 @@ import com.stripe.android.core.Logger
 import com.stripe.android.financialconnections.ApiKeyFixtures.consumerSession
 import com.stripe.android.financialconnections.ApiKeyFixtures.institution
 import com.stripe.android.financialconnections.ApiKeyFixtures.partnerAccount
-import com.stripe.android.financialconnections.ApiKeyFixtures.sessionManifest
+import com.stripe.android.financialconnections.ApiKeyFixtures.syncResponse
 import com.stripe.android.financialconnections.TestFinancialConnectionsAnalyticsTracker
 import com.stripe.android.financialconnections.domain.FetchNetworkedAccounts
 import com.stripe.android.financialconnections.domain.GetCachedConsumerSession
-import com.stripe.android.financialconnections.domain.GetManifest
+import com.stripe.android.financialconnections.domain.GetOrFetchSync
 import com.stripe.android.financialconnections.domain.SelectNetworkedAccount
 import com.stripe.android.financialconnections.domain.UpdateCachedAccounts
 import com.stripe.android.financialconnections.domain.UpdateLocalManifest
@@ -44,7 +44,7 @@ class LinkAccountPickerViewModelTest {
     @get:Rule
     val mavericksTestRule = MavericksTestRule()
 
-    private val getManifest = mock()
+    private val getSync = mock()
     private val navigationManager = TestNavigationManager()
     private val getCachedConsumerSession = mock()
     private val fetchNetworkedAccounts = mock()
@@ -57,7 +57,7 @@ class LinkAccountPickerViewModelTest {
         state: LinkAccountPickerState
     ) = LinkAccountPickerViewModel(
         navigationManager = navigationManager,
-        getManifest = getManifest,
+        getSync = getSync,
         logger = Logger.noop(),
         eventTracker = eventTracker,
         getCachedConsumerSession = getCachedConsumerSession,
@@ -66,12 +66,13 @@ class LinkAccountPickerViewModelTest {
         updateLocalManifest = updateLocalManifest,
         updateCachedAccounts = updateCachedAccounts,
         initialState = state,
+        handleClickableUrl = mock(),
         coreAuthorizationPendingNetworkingRepair = mock()
     )
 
     @Test
     fun `init - Fetches existing accounts and zips them by id`() = runTest {
-        whenever(getManifest()).thenReturn(sessionManifest())
+        whenever(getSync()).thenReturn(syncResponse())
         whenever(getCachedConsumerSession()).thenReturn(consumerSession())
         whenever(fetchNetworkedAccounts(any())).thenReturn(
             NetworkedAccountsList(
@@ -110,7 +111,7 @@ class LinkAccountPickerViewModelTest {
         val response = twoAccounts().copy(
             nextPaneOnAddAccount = Pane.INSTITUTION_PICKER
         )
-        whenever(getManifest()).thenReturn(sessionManifest())
+        whenever(getSync()).thenReturn(syncResponse())
         whenever(getCachedConsumerSession()).thenReturn(consumerSession())
         whenever(fetchNetworkedAccounts(any())).thenReturn(response)
 
@@ -140,7 +141,7 @@ class LinkAccountPickerViewModelTest {
             )
         )
         val selectedAccount = accounts.data.first()
-        whenever(getManifest()).thenReturn(sessionManifest())
+        whenever(getSync()).thenReturn(syncResponse())
         whenever(getCachedConsumerSession()).thenReturn(consumerSession())
         whenever(fetchNetworkedAccounts(any())).thenReturn(accounts)
         whenever(
@@ -183,7 +184,7 @@ class LinkAccountPickerViewModelTest {
                 )
             )
             val selectedAccount = accounts.data.first()
-            whenever(getManifest()).thenReturn(sessionManifest())
+            whenever(getSync()).thenReturn(syncResponse())
             whenever(getCachedConsumerSession()).thenReturn(consumerSession())
             whenever(fetchNetworkedAccounts(any())).thenReturn(accounts)
             whenever(
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/manualentry/ManualEntryViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/manualentry/ManualEntryViewModelTest.kt
index 59f01ab9e6a..934fee07019 100644
--- a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/manualentry/ManualEntryViewModelTest.kt
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/manualentry/ManualEntryViewModelTest.kt
@@ -67,7 +67,8 @@ class ManualEntryViewModelTest {
                     Success(
                         Payload(
                             customManualEntry = true,
-                            verifyWithMicrodeposits = sync.manifest.manualEntryUsesMicrodeposits
+                            verifyWithMicrodeposits = sync.manifest.manualEntryUsesMicrodeposits,
+                            testMode = false
                         )
                     )
                 )
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupViewModelTest.kt
index de769be954d..cfa88956807 100644
--- a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupViewModelTest.kt
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinkloginwarmup/NetworkingLinkLoginWarmupViewModelTest.kt
@@ -1,14 +1,15 @@
 package com.stripe.android.financialconnections.features.networkinglinkloginwarmup
 
 import com.airbnb.mvrx.test.MavericksTestRule
-import com.stripe.android.core.Logger
 import com.stripe.android.financialconnections.ApiKeyFixtures
 import com.stripe.android.financialconnections.TestFinancialConnectionsAnalyticsTracker
 import com.stripe.android.financialconnections.domain.DisableNetworking
 import com.stripe.android.financialconnections.domain.GetManifest
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
 import com.stripe.android.financialconnections.navigation.Destination
+import com.stripe.android.financialconnections.navigation.PopUpToBehavior
 import com.stripe.android.financialconnections.navigation.destination
+import com.stripe.android.financialconnections.utils.TestHandleError
 import com.stripe.android.financialconnections.utils.TestNavigationManager
 import kotlinx.coroutines.test.runTest
 import org.junit.Rule
@@ -24,6 +25,7 @@ class NetworkingLinkLoginWarmupViewModelTest {
 
     private val getManifest = mock()
     private val navigationManager = TestNavigationManager()
+    private val handleError = TestHandleError()
     private val disableNetworking = mock()
     private val eventTracker = TestFinancialConnectionsAnalyticsTracker()
 
@@ -32,14 +34,29 @@ class NetworkingLinkLoginWarmupViewModelTest {
     ) = NetworkingLinkLoginWarmupViewModel(
         navigationManager = navigationManager,
         getManifest = getManifest,
-        logger = Logger.noop(),
+        handleError = handleError,
         disableNetworking = disableNetworking,
         eventTracker = eventTracker,
         initialState = state
     )
 
     @Test
-    fun `onContinueClick - navigates to verification pane`() {
+    fun `init - payload error navigates to error screen`() = runTest {
+        val error = RuntimeException("Failed to fetch manifest")
+        whenever(getManifest()).thenAnswer { throw error }
+
+        buildViewModel(NetworkingLinkLoginWarmupState())
+
+        handleError.assertError(
+            extraMessage = "Error fetching payload",
+            pane = Pane.NETWORKING_LINK_LOGIN_WARMUP,
+            error = error,
+            displayErrorScreen = true
+        )
+    }
+
+    @Test
+    fun `onContinueClick - navigates to verification pane`() = runTest {
         val viewModel = buildViewModel(NetworkingLinkLoginWarmupState())
 
         viewModel.onContinueClick()
@@ -49,6 +66,26 @@ class NetworkingLinkLoginWarmupViewModelTest {
         )
     }
 
+    @Test
+    fun `onSkipClicked - navigates to institution picker and clears back stack`() = runTest {
+        val referrer = Pane.CONSENT
+        val viewModel = buildViewModel(NetworkingLinkLoginWarmupState(referrer))
+
+        whenever(disableNetworking()).thenReturn(
+            ApiKeyFixtures.sessionManifest().copy(nextPane = Pane.INSTITUTION_PICKER)
+        )
+
+        viewModel.onSkipClicked()
+        navigationManager.assertNavigatedTo(
+            destination = Destination.InstitutionPicker,
+            popUpTo = PopUpToBehavior.Route(
+                route = referrer.destination.fullRoute,
+                inclusive = true,
+            ),
+            pane = Pane.NETWORKING_LINK_LOGIN_WARMUP,
+        )
+    }
+
     @Test
     fun `onClickableTextClick - skip_login disables networking and navigates`() = runTest {
         val viewModel = buildViewModel(NetworkingLinkLoginWarmupState())
@@ -58,11 +95,12 @@ class NetworkingLinkLoginWarmupViewModelTest {
             ApiKeyFixtures.sessionManifest().copy(nextPane = expectedNextPane)
         )
 
-        viewModel.onClickableTextClick("skip_login")
+        viewModel.onSkipClicked()
 
         verify(disableNetworking).invoke()
         navigationManager.assertNavigatedTo(
             destination = expectedNextPane.destination,
+            popUpTo = PopUpToBehavior.Current(inclusive = true),
             pane = Pane.NETWORKING_LINK_LOGIN_WARMUP
         )
     }
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupViewModelTest.kt
index 5c4fc4403e8..2210d4f903e 100644
--- a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupViewModelTest.kt
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/networkinglinksignup/NetworkingLinkSignupViewModelTest.kt
@@ -1,5 +1,6 @@
 package com.stripe.android.financialconnections.features.networkinglinksignup
 
+import app.cash.turbine.test
 import com.airbnb.mvrx.test.MavericksTestRule
 import com.google.common.truth.Truth.assertThat
 import com.stripe.android.core.Logger
@@ -11,11 +12,14 @@ import com.stripe.android.financialconnections.domain.GetManifest
 import com.stripe.android.financialconnections.domain.LookupAccount
 import com.stripe.android.financialconnections.domain.SaveAccountToLink
 import com.stripe.android.financialconnections.domain.SynchronizeFinancialConnectionsSession
+import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane.NETWORKING_LINK_SIGNUP_PANE
 import com.stripe.android.financialconnections.model.NetworkingLinkSignupBody
 import com.stripe.android.financialconnections.model.NetworkingLinkSignupPane
 import com.stripe.android.financialconnections.model.TextUpdate
+import com.stripe.android.financialconnections.navigation.Destination.NetworkingSaveToLinkVerification
+import com.stripe.android.financialconnections.navigation.NavigationIntent
+import com.stripe.android.financialconnections.navigation.NavigationManagerImpl
 import com.stripe.android.financialconnections.repository.SaveToLinkWithStripeSucceededRepository
-import com.stripe.android.financialconnections.utils.TestNavigationManager
 import com.stripe.android.financialconnections.utils.UriUtils
 import com.stripe.android.model.ConsumerSessionLookup
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -26,6 +30,8 @@ import org.junit.Rule
 import org.junit.Test
 import org.mockito.kotlin.any
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -39,7 +45,7 @@ class NetworkingLinkSignupViewModelTest {
     private val getManifest = mock()
     private val eventTracker = TestFinancialConnectionsAnalyticsTracker()
     private val getAuthorizationSessionAccounts = mock()
-    private val navigationManager = TestNavigationManager()
+    private val navigationManager = NavigationManagerImpl()
     private val lookupAccount = mock()
     private val saveAccountToLink = mock()
     private val sync = mock()
@@ -85,6 +91,83 @@ class NetworkingLinkSignupViewModelTest {
         assertThat(payload.emailController.fieldValue.first()).isEqualTo("test@test.com")
     }
 
+    @Test
+    fun `Redirects to verification screen if entering returning user email`() = runTest {
+        val manifest = ApiKeyFixtures.sessionManifest()
+
+        whenever(sync()).thenReturn(
+            syncResponse().copy(
+                text = TextUpdate(
+                    consent = null,
+                    networkingLinkSignupPane = networkingLinkSignupPane()
+                )
+            )
+        )
+        whenever(getManifest()).thenReturn(manifest)
+        whenever(lookupAccount(any())).thenReturn(ConsumerSessionLookup(exists = true))
+
+        val viewModel = buildViewModel(NetworkingLinkSignupState())
+
+        navigationManager.navigationFlow.test {
+            val state = viewModel.awaitState()
+            val payload = requireNotNull(state.payload())
+            payload.emailController.onValueChange("email@email.com")
+
+            assertThat(awaitItem()).isEqualTo(
+                NavigationIntent.NavigateTo(
+                    route = NetworkingSaveToLinkVerification(referrer = NETWORKING_LINK_SIGNUP_PANE),
+                    popUpTo = null,
+                    isSingleTop = true,
+                )
+            )
+        }
+    }
+
+    @Test
+    fun `Enables Save To Link button if we encounter a returning user`() = runTest {
+        val manifest = ApiKeyFixtures.sessionManifest()
+
+        whenever(sync()).thenReturn(
+            syncResponse().copy(
+                text = TextUpdate(
+                    consent = null,
+                    networkingLinkSignupPane = networkingLinkSignupPane()
+                )
+            )
+        )
+        whenever(getManifest()).thenReturn(manifest)
+        whenever(lookupAccount(any())).thenReturn(ConsumerSessionLookup(exists = true))
+
+        val viewModel = buildViewModel(NetworkingLinkSignupState())
+
+        navigationManager.navigationFlow.test {
+            val state = viewModel.awaitState()
+            val payload = requireNotNull(state.payload())
+            payload.emailController.onValueChange("email@email.com")
+
+            assertThat(awaitItem()).isEqualTo(
+                NavigationIntent.NavigateTo(
+                    route = NetworkingSaveToLinkVerification(referrer = NETWORKING_LINK_SIGNUP_PANE),
+                    popUpTo = null,
+                    isSingleTop = true,
+                )
+            )
+
+            // Simulate the user pressing Save To Link after returning from the OTP screen
+            viewModel.onSaveAccount()
+
+            verify(saveAccountToLink, never()).new(any(), any(), any(), any())
+
+            assertThat(awaitItem()).isEqualTo(
+                NavigationIntent.NavigateTo(
+                    route = NetworkingSaveToLinkVerification(referrer = NETWORKING_LINK_SIGNUP_PANE),
+                    popUpTo = null,
+                    isSingleTop = true,
+                )
+            )
+        }
+    }
+
     private fun networkingLinkSignupPane() = NetworkingLinkSignupPane(
         aboveCta = "Above CTA",
         body = NetworkingLinkSignupBody(emptyList()),
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthViewModelTest.kt
index 46e69d3263e..afe87cbf1d5 100644
--- a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthViewModelTest.kt
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/partnerauth/PartnerAuthViewModelTest.kt
@@ -12,7 +12,6 @@ import com.stripe.android.financialconnections.ApiKeyFixtures.sessionManifest
 import com.stripe.android.financialconnections.ApiKeyFixtures.syncResponse
 import com.stripe.android.financialconnections.analytics.AuthSessionEvent
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsTracker
-import com.stripe.android.financialconnections.analytics.logError
 import com.stripe.android.financialconnections.domain.CancelAuthorizationSession
 import com.stripe.android.financialconnections.domain.CompleteAuthorizationSession
 import com.stripe.android.financialconnections.domain.GetOrFetchSync
@@ -24,6 +23,7 @@ import com.stripe.android.financialconnections.exception.InstitutionUnplannedDow
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
 import com.stripe.android.financialconnections.model.MixedOAuthParams
 import com.stripe.android.financialconnections.presentation.WebAuthFlowState
+import com.stripe.android.financialconnections.utils.TestHandleError
 import com.stripe.android.financialconnections.utils.TestNavigationManager
 import com.stripe.android.financialconnections.utils.UriUtils
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -34,9 +34,7 @@ import org.junit.Test
 import org.mockito.kotlin.any
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
-import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
-import org.mockito.kotlin.verifyBlocking
 import org.mockito.kotlin.verifyNoInteractions
 import org.mockito.kotlin.whenever
 
@@ -58,9 +56,10 @@ internal class PartnerAuthViewModelTest {
     private val navigationManager = TestNavigationManager()
     private val createAuthorizationSession = mock()
     private val logger = mock()
+    private val handleError = TestHandleError()
 
     @Test
-    fun `init - when creating auth session returns unplanned downtime, error is logged`() =
+    fun `init - when creating auth session returns unplanned downtime, error is logged and displayed`() =
         runTest {
             val unplannedDowntimeError = InstitutionUnplannedDowntimeError(
                 institution = institution(),
@@ -77,14 +76,12 @@ internal class PartnerAuthViewModelTest {
             val viewModel = createViewModel()
 
             withState(viewModel) {
-                verifyBlocking(eventTracker) {
-                    logError(
-                        extraMessage = "Error fetching payload / posting AuthSession",
-                        error = unplannedDowntimeError,
-                        logger = logger,
-                        pane = Pane.PARTNER_AUTH
-                    )
-                }
+                handleError.assertError(
+                    extraMessage = "Error fetching payload / posting AuthSession",
+                    error = unplannedDowntimeError,
+                    pane = Pane.PARTNER_AUTH,
+                    displayErrorScreen = true
+                )
             }
         }
 
@@ -200,6 +197,7 @@ internal class PartnerAuthViewModelTest {
         runTest {
             val activeAuthSession = authorizationSession().copy(url = null)
             val activeInstitution = institution()
+            // An auth session was created on the previous pane, before launching Partner Auth.
             val manifest = sessionManifest().copy(
                 activeAuthSession = activeAuthSession.copy(_isOAuth = true),
                 activeInstitution = activeInstitution,
@@ -222,8 +220,8 @@ internal class PartnerAuthViewModelTest {
             // stays in partner auth pane
             assertThat(navigationManager.emittedIntents).isEmpty()
 
-            // creates two sessions (initial and retry)
-            verify(createAuthorizationSession, times(2)).invoke(
+            // creates an additional session (cancel triggers a retry)
+            verify(createAuthorizationSession).invoke(
                 eq(activeInstitution),
                 eq(syncResponse)
             )
@@ -240,6 +238,7 @@ internal class PartnerAuthViewModelTest {
         runTest {
             val activeAuthSession = authorizationSession().copy(url = null)
             val activeInstitution = institution()
+            // An auth session was created on the previous pane, before launching Partner Auth.
             val manifest = sessionManifest().copy(
                 activeAuthSession = activeAuthSession.copy(_isOAuth = true),
                 activeInstitution = activeInstitution
@@ -259,8 +258,8 @@ internal class PartnerAuthViewModelTest {
             // stays in partner auth pane
             assertThat(navigationManager.emittedIntents).isEmpty()
 
-            // creates two sessions (initial and retry)
-            verify(createAuthorizationSession, times(2)).invoke(
+            // creates an additional session (cancel triggers a retry)
+            verify(createAuthorizationSession).invoke(
                 eq(activeInstitution),
                 eq(syncResponse)
             )
@@ -295,6 +294,7 @@ internal class PartnerAuthViewModelTest {
             initialState = initialState,
             browserManager = mock(),
             uriUtils = UriUtils(Logger.noop(), mock()),
+            handleError = handleError,
             applicationId = applicationId
         )
     }
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/success/SuccessViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/success/SuccessViewModelTest.kt
index 9d90109c64c..f7b0531cd6a 100644
--- a/financial-connections/src/test/java/com/stripe/android/financialconnections/features/success/SuccessViewModelTest.kt
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/features/success/SuccessViewModelTest.kt
@@ -5,7 +5,6 @@ import com.airbnb.mvrx.test.MavericksTestRule
 import com.google.common.truth.Truth.assertThat
 import com.stripe.android.core.Logger
 import com.stripe.android.financialconnections.ApiKeyFixtures
-import com.stripe.android.financialconnections.R
 import com.stripe.android.financialconnections.TestFinancialConnectionsAnalyticsTracker
 import com.stripe.android.financialconnections.analytics.FinancialConnectionsAnalyticsEvent.PaneLoaded
 import com.stripe.android.financialconnections.domain.GetCachedAccounts
@@ -13,8 +12,6 @@ import com.stripe.android.financialconnections.domain.GetManifest
 import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator
 import com.stripe.android.financialconnections.domain.NativeAuthFlowCoordinator.Message.Complete
 import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
-import com.stripe.android.financialconnections.repository.SaveToLinkWithStripeSucceededRepository
-import com.stripe.android.financialconnections.ui.TextResource.PluralId
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -38,7 +35,6 @@ internal class SuccessViewModelTest {
     private val eventTracker = TestFinancialConnectionsAnalyticsTracker()
     private val nativeAuthFlowCoordinator = mock()
     private val getCachedAccounts = mock()
-    private val saveToLinkWithStripeSucceeded = mock()
 
     private fun buildViewModel(
         state: SuccessState
@@ -48,8 +44,8 @@ internal class SuccessViewModelTest {
         eventTracker = eventTracker,
         initialState = state,
         nativeAuthFlowCoordinator = nativeAuthFlowCoordinator,
+        saveToLinkWithStripeSucceeded = mock(),
         getCachedAccounts = getCachedAccounts,
-        saveToLinkWithStripeSucceeded = saveToLinkWithStripeSucceeded,
     )
 
     @Test
@@ -108,163 +104,4 @@ internal class SuccessViewModelTest {
             )
         }
     }
-
-    @Test
-    fun `getSuccessMessage - link with stripe and connected account name and business name`() {
-        val result = buildViewModel(SuccessState()).getSuccessMessages(
-            isLinkWithStripe = true,
-            isNetworkingUserFlow = null,
-            saveToLinkWithStripeSucceeded = true,
-            connectedAccountName = "Connected Account Name",
-            businessName = "Business Name",
-            count = 2
-        ) as PluralId
-        assertEquals(R.plurals.stripe_success_pane_link_with_connected_account_name, result.value)
-        assertEquals(listOf("Connected Account Name", "Business Name"), result.args)
-        assertEquals(2, result.count)
-    }
-
-    @Test
-    fun `getSuccessMessage - link with stripe and business name`() {
-        val result = buildViewModel(SuccessState()).getSuccessMessages(
-            isLinkWithStripe = true,
-            isNetworkingUserFlow = null,
-            saveToLinkWithStripeSucceeded = true,
-            connectedAccountName = null,
-            businessName = "Business Name",
-            count = 3
-        ) as PluralId
-        assertEquals(R.plurals.stripe_success_pane_link_with_business_name, result.value)
-        assertEquals(listOf("Business Name"), result.args)
-        assertEquals(3, result.count)
-    }
-
-    @Test
-    fun `getSuccessMessage - link with stripe and no business name`() {
-        val result = buildViewModel(SuccessState()).getSuccessMessages(
-            isLinkWithStripe = true,
-            isNetworkingUserFlow = null,
-            saveToLinkWithStripeSucceeded = true,
-            connectedAccountName = null,
-            businessName = null,
-            count = 1
-        ) as PluralId
-        assertEquals(R.plurals.stripe_success_pane_link_with_no_business_name, result.value)
-        assertEquals(listOf(), result.args)
-        assertEquals(1, result.count)
-    }
-
-    @Test
-    fun `getSuccessMessage - networking user flow and save to link with stripe succeeded and connected account name and business name`() {
-        val result = buildViewModel(SuccessState()).getSuccessMessages(
-            isLinkWithStripe = false,
-            isNetworkingUserFlow = true,
-            saveToLinkWithStripeSucceeded = true,
-            connectedAccountName = "Connected Account Name",
-            businessName = "Business Name",
-            count = 4
-        ) as PluralId
-        assertEquals(R.plurals.stripe_success_pane_link_with_connected_account_name, result.value)
-        assertEquals(listOf("Connected Account Name", "Business Name"), result.args)
-        assertEquals(4, result.count)
-    }
-
-    @Test
-    fun `getSuccessMessage - networking user flow and save to link with stripe succeeded and business name`() {
-        val result = buildViewModel(SuccessState()).getSuccessMessages(
-            isLinkWithStripe = false,
-            isNetworkingUserFlow = true,
-            saveToLinkWithStripeSucceeded = true,
-            connectedAccountName = null,
-            businessName = "Business Name",
-            count = 5
-        ) as PluralId
-        assertEquals(R.plurals.stripe_success_pane_link_with_business_name, result.value)
-        assertEquals(listOf("Business Name"), result.args)
-        assertEquals(5, result.count)
-    }
-
-    @Test
-    fun `getSuccessMessage - no link with stripe and connected account name and business name`() {
-        val result = buildViewModel(SuccessState()).getSuccessMessages(
-            isLinkWithStripe = false,
-            isNetworkingUserFlow = null,
-            saveToLinkWithStripeSucceeded = null,
-            connectedAccountName = "Connected Account Name",
-            businessName = "Business Name",
-            count = 6
-        ) as PluralId
-        assertEquals(R.plurals.stripe_success_pane_has_connected_account_name, result.value)
-        assertEquals(listOf("Connected Account Name", "Business Name"), result.args)
-        assertEquals(6, result.count)
-    }
-
-    @Test
-    fun `getSuccessMessage - no link with stripe and business name`() {
-        val result = buildViewModel(SuccessState()).getSuccessMessages(
-            isLinkWithStripe = false,
-            isNetworkingUserFlow = null,
-            saveToLinkWithStripeSucceeded = null,
-            connectedAccountName = null,
-            businessName = "Business Name",
-            count = 7
-        ) as PluralId
-        assertEquals(R.plurals.stripe_success_pane_has_business_name, result.value)
-        assertEquals(listOf("Business Name"), result.args)
-        assertEquals(7, result.count)
-    }
-
-    @Test
-    fun `getSuccessMessage - no link with stripe and no business name`() {
-        val result = buildViewModel(SuccessState()).getSuccessMessages(
-            isLinkWithStripe = false,
-            isNetworkingUserFlow = null,
-            saveToLinkWithStripeSucceeded = null,
-            connectedAccountName = null,
-            businessName = null,
-            count = 8
-        ) as PluralId
-        assertEquals(R.plurals.stripe_success_pane_no_business_name, result.value)
-        assertEquals(listOf(), result.args)
-        assertEquals(8, result.count)
-    }
-
-    @Test
-    fun `getFailedToLinkMessage - saveToLinkWithStripeSucceeded is true, should return null`() {
-        val message = buildViewModel(SuccessState()).getFailedToLinkMessage(
-            businessName = "Business",
-            saveToLinkWithStripeSucceeded = true,
-            count = 1
-        )
-        assertThat(message).isNull()
-    }
-
-    @Test
-    fun `getFailedToLinkMessage - saveToLinkWithStripeSucceeded is false and businessName is not null`() {
-        val message = buildViewModel(SuccessState()).getFailedToLinkMessage(
-            businessName = "Business",
-            saveToLinkWithStripeSucceeded = false,
-            count = 1
-        )
-        require(message is PluralId)
-        assertEquals(R.plurals.stripe_success_networking_save_to_link_failed, message.value)
-        assertEquals(1, message.count)
-        assertEquals(listOf("Business"), message.args)
-    }
-
-    @Test
-    fun `getFailedToLinkMessage - saveToLinkWithStripeSucceeded is false and businessName is null`() {
-        val message = buildViewModel(SuccessState()).getFailedToLinkMessage(
-            businessName = null,
-            saveToLinkWithStripeSucceeded = false,
-            count = 2
-        )
-        require(message is PluralId)
-        assertEquals(
-            R.plurals.stripe_success_pane_networking_save_to_link_failed_no_business,
-            message.value
-        )
-        assertEquals(2, message.count)
-        assertEquals(emptyList(), message.args)
-    }
 }
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/presentation/FinancialConnectionsSheetNativeViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/presentation/FinancialConnectionsSheetNativeViewModelTest.kt
index 009edf83639..cb64b9551f3 100644
--- a/financial-connections/src/test/java/com/stripe/android/financialconnections/presentation/FinancialConnectionsSheetNativeViewModelTest.kt
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/presentation/FinancialConnectionsSheetNativeViewModelTest.kt
@@ -100,9 +100,12 @@ internal class FinancialConnectionsSheetNativeViewModelTest {
         whenever(nativeAuthFlowCoordinator()).thenReturn(MutableSharedFlow())
         whenever(completeFinancialConnectionsSession(anyOrNull())).thenReturn(sessionWithAccounts)
 
+        val messagesFlow = MutableSharedFlow()
+        whenever(nativeAuthFlowCoordinator()).thenReturn(messagesFlow)
+
         val viewModel = createViewModel()
 
-        viewModel.onCloseConfirm()
+        messagesFlow.emit(Complete(null))
 
         withState(viewModel) {
             require(it.viewEffect is Finish)
@@ -121,12 +124,14 @@ internal class FinancialConnectionsSheetNativeViewModelTest {
     @Test
     fun `onCloseClick - when closing, no accounts, no errors, finish with cancel`() = runTest {
         val sessionWithNoAccounts = financialConnectionsSessionNoAccounts()
-        whenever(nativeAuthFlowCoordinator()).thenReturn(MutableSharedFlow())
         whenever(completeFinancialConnectionsSession(anyOrNull())).thenReturn(sessionWithNoAccounts)
 
+        val messagesFlow = MutableSharedFlow()
+        whenever(nativeAuthFlowCoordinator()).thenReturn(messagesFlow)
+
         val viewModel = createViewModel()
 
-        viewModel.onCloseConfirm()
+        messagesFlow.emit(Complete(null))
 
         withState(viewModel) {
             require(it.viewEffect is Finish)
@@ -151,12 +156,14 @@ internal class FinancialConnectionsSheetNativeViewModelTest {
                 )
             )
         )
-        whenever(nativeAuthFlowCoordinator()).thenReturn(MutableSharedFlow())
         whenever(completeFinancialConnectionsSession(anyOrNull())).thenReturn(sessionWithNoAccounts)
 
+        val messagesFlow = MutableSharedFlow()
+        whenever(nativeAuthFlowCoordinator()).thenReturn(messagesFlow)
+
         val viewModel = createViewModel()
 
-        viewModel.onCloseConfirm()
+        messagesFlow.emit(Complete(null))
 
         withState(viewModel) {
             require(it.viewEffect is Finish)
@@ -292,7 +299,6 @@ internal class FinancialConnectionsSheetNativeViewModelTest {
         completeFinancialConnectionsSession = completeFinancialConnectionsSession,
         nativeAuthFlowCoordinator = nativeAuthFlowCoordinator,
         logger = mock(),
-        getManifest = mock(),
         navigationManager = TestNavigationManager(),
         initialState = initialState
     )
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/screenshottests/FinancialConnectionsShotShowkaseScreenshotTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/screenshottests/FinancialConnectionsShotShowkaseScreenshotTest.kt
index 082c27b4933..3ba6c9ea9f3 100644
--- a/financial-connections/src/test/java/com/stripe/android/financialconnections/screenshottests/FinancialConnectionsShotShowkaseScreenshotTest.kt
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/screenshottests/FinancialConnectionsShotShowkaseScreenshotTest.kt
@@ -12,6 +12,7 @@ import com.airbnb.android.showkase.models.ShowkaseBrowserComponent
 import com.google.testing.junit.testparameterinjector.TestParameter
 import com.google.testing.junit.testparameterinjector.TestParameterInjector
 import com.stripe.android.financialconnections.getMetadata
+import com.stripe.android.financialconnections.utils.TimeZoneRule
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -34,6 +35,9 @@ class PaparazziSampleScreenshotTest {
         PIXEL_C(DeviceConfig.PIXEL_C),
     }
 
+    @get:Rule
+    val timeZoneRule = TimeZoneRule()
+
     @get:Rule
     val paparazzi = Paparazzi(
         environment = detectEnvironment().run {
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/utils/TestHandleError.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/utils/TestHandleError.kt
new file mode 100644
index 00000000000..6cf955879be
--- /dev/null
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/utils/TestHandleError.kt
@@ -0,0 +1,50 @@
+package com.stripe.android.financialconnections.utils
+
+import com.stripe.android.financialconnections.domain.HandleError
+import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
+
+internal class TestHandleError : HandleError {
+
+    private val invocations = mutableListOf()
+
+    override fun invoke(
+        extraMessage: String,
+        error: Throwable,
+        pane: FinancialConnectionsSessionManifest.Pane,
+        displayErrorScreen: Boolean
+    ) {
+        invocations.add(HandleErrorInvocation(extraMessage, error, pane, displayErrorScreen))
+    }
+
+    fun assertError(
+        extraMessage: String,
+        error: Throwable,
+        pane: FinancialConnectionsSessionManifest.Pane,
+        displayErrorScreen: Boolean
+    ) {
+        // Check if there is any invocation matching the given parameters
+        val match = invocations.any { invocation ->
+            invocation.extraMessage == extraMessage &&
+                invocation.error == error &&
+                invocation.pane == pane &&
+                invocation.displayErrorScreen == displayErrorScreen
+        }
+
+        // Perform the assertion
+        assert(match) {
+            "Expected to find an error invocation with " +
+                "extraMessage=$extraMessage, " +
+                "error=$error, " +
+                "pane=$pane, " +
+                "displayErrorScreen=$displayErrorScreen, " +
+                "but none was found in the invocations list."
+        }
+    }
+}
+
+internal data class HandleErrorInvocation(
+    val extraMessage: String,
+    val error: Throwable,
+    val pane: FinancialConnectionsSessionManifest.Pane,
+    val displayErrorScreen: Boolean
+)
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/utils/TestNavigationManager.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/utils/TestNavigationManager.kt
index 13abfc5ad18..45fb5a783c1 100644
--- a/financial-connections/src/test/java/com/stripe/android/financialconnections/utils/TestNavigationManager.kt
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/utils/TestNavigationManager.kt
@@ -5,6 +5,7 @@ import com.stripe.android.financialconnections.model.FinancialConnectionsSession
 import com.stripe.android.financialconnections.navigation.Destination
 import com.stripe.android.financialconnections.navigation.NavigationIntent
 import com.stripe.android.financialconnections.navigation.NavigationManager
+import com.stripe.android.financialconnections.navigation.PopUpToBehavior
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.SharedFlow
 import kotlin.test.assertIs
@@ -22,27 +23,31 @@ internal class TestNavigationManager : NavigationManager {
 
     override fun tryNavigateTo(
         route: String,
-        popUpToCurrent: Boolean,
-        inclusive: Boolean,
-        isSingleTop: Boolean
+        popUpTo: PopUpToBehavior?,
+        isSingleTop: Boolean,
     ) {
         emittedIntents.add(
             NavigationIntent.NavigateTo(
                 route = route,
-                popUpToCurrent = popUpToCurrent,
-                inclusive = inclusive,
+                popUpTo = popUpTo,
                 isSingleTop = isSingleTop,
             )
         )
     }
 
+    override fun tryNavigateBack() {
+        emittedIntents.add(NavigationIntent.NavigateBack)
+    }
+
     fun assertNavigatedTo(
         destination: Destination,
         pane: Pane,
+        popUpTo: PopUpToBehavior? = null,
         args: Map = emptyMap()
     ) {
         val last: NavigationIntent = emittedIntents.last()
         assertIs(last)
         assertThat(last.route).isEqualTo(destination(pane, args))
+        assertThat(last.popUpTo).isEqualTo(popUpTo)
     }
 }
diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/utils/TimeZoneRule.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/utils/TimeZoneRule.kt
new file mode 100644
index 00000000000..30613305709
--- /dev/null
+++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/utils/TimeZoneRule.kt
@@ -0,0 +1,22 @@
+package com.stripe.android.financialconnections.utils
+
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+import java.util.TimeZone
+
+class TimeZoneRule(
+    private val timeZone: TimeZone = TimeZone.getTimeZone("America/Los_Angeles"),
+) : TestWatcher() {
+
+    private val original: TimeZone = TimeZone.getDefault()
+
+    override fun starting(description: Description) {
+        super.starting(description)
+        TimeZone.setDefault(timeZone)
+    }
+
+    override fun finished(description: Description) {
+        TimeZone.setDefault(original)
+        super.finished(description)
+    }
+}
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_0,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_0,NEXUS_5].png
index 36433d91624..140fb259832 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_0,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_0,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_0,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_0,PIXEL_C].png
index ed6c7213a57..410ca3a4367 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_0,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_0,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_1,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_1,NEXUS_5].png
index 7c258510376..1f4389f38cc 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_1,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_1,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_1,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_1,PIXEL_C].png
index 9ed7c1e5f53..7dbb870fa8b 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_1,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_1,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_2,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_2,NEXUS_5].png
index 94d243c4327..9fd6163074b 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_2,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_2,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_2,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_2,PIXEL_C].png
index 1dfac864318..685f59742b1 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_2,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_2,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_3,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_3,NEXUS_5].png
new file mode 100644
index 00000000000..447e574d26a
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_3,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_3,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_3,PIXEL_C].png
new file mode 100644
index 00000000000..6e195747119
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.accountpicker_null_AccountPickerPane_AccountPickerPreview_0_null_3,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.attachpayment_null_AttachPaymentPane_Default_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.attachpayment_null_AttachPaymentPane_Default_0_null,NEXUS_5].png
index 6470acbab31..d67f69bc660 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.attachpayment_null_AttachPaymentPane_Default_0_null,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.attachpayment_null_AttachPaymentPane_Default_0_null,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.attachpayment_null_AttachPaymentPane_Default_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.attachpayment_null_AttachPaymentPane_Default_0_null,PIXEL_C].png
index 9aaedb8e4b4..614176912dd 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.attachpayment_null_AttachPaymentPane_Default_0_null,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.attachpayment_null_AttachPaymentPane_Default_0_null,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_CloseDialog_Default_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_CloseDialog_Default_0_null,NEXUS_5].png
deleted file mode 100644
index 68d41814514..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_CloseDialog_Default_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_CloseDialog_Default_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_CloseDialog_Default_0_null,PIXEL_C].png
deleted file mode 100644
index 9407cc923cd..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_CloseDialog_Default_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Default_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Default_0_null,NEXUS_5].png
deleted file mode 100644
index 85984acd0eb..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Default_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_ManyAccounts_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_ManyAccounts_0_null,NEXUS_5].png
deleted file mode 100644
index 9309dd0a3c7..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_ManyAccounts_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_ManyAccounts_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_ManyAccounts_0_null,PIXEL_C].png
deleted file mode 100644
index 5bc015b6f38..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_ManyAccounts_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_ManyAccountsandStripeDirect_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_ManyAccountsandStripeDirect_0_null,NEXUS_5].png
deleted file mode 100644
index 2ab9eb17688..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_ManyAccountsandStripeDirect_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_ManyAccountsandStripeDirect_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_ManyAccountsandStripeDirect_0_null,PIXEL_C].png
deleted file mode 100644
index 544123fcb43..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_ManyAccountsandStripeDirect_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Multipleaccounts_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Multipleaccounts_0_null,NEXUS_5].png
deleted file mode 100644
index e368be21a5f..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Multipleaccounts_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Multipleaccounts_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Multipleaccounts_0_null,PIXEL_C].png
deleted file mode 100644
index be3fefb35fa..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Multipleaccounts_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Networking_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Networking_0_null,NEXUS_5].png
deleted file mode 100644
index 9309dd0a3c7..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Networking_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Networking_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Networking_0_null,PIXEL_C].png
deleted file mode 100644
index 5bc015b6f38..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Networking_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Oneaccount_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Oneaccount_0_null,NEXUS_5].png
deleted file mode 100644
index fc5d4a831fc..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Oneaccount_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Oneaccount_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Oneaccount_0_null,PIXEL_C].png
deleted file mode 100644
index 92915235dd1..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Oneaccount_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DefaultGroup_AccountItemPreview_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DefaultGroup_AccountItemPreview_0_null,NEXUS_5].png
new file mode 100644
index 00000000000..732cfeaa985
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DefaultGroup_AccountItemPreview_0_null,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DefaultGroup_AccountItemPreview_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DefaultGroup_AccountItemPreview_0_null,PIXEL_C].png
new file mode 100644
index 00000000000..f34e6a22489
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DefaultGroup_AccountItemPreview_0_null,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_institutiondownplannederror_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_institutiondownplannederror_0_null,NEXUS_5].png
deleted file mode 100644
index a07fd9c1b59..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_institutiondownplannederror_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_institutiondownplannederror_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_institutiondownplannederror_0_null,PIXEL_C].png
deleted file mode 100644
index a471c8aed31..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_institutiondownplannederror_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_noaccountsavailableerror_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_noaccountsavailableerror_0_null,NEXUS_5].png
index eb5a3a91e95..496f7893016 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_noaccountsavailableerror_0_null,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_noaccountsavailableerror_0_null,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_noaccountsavailableerror_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_noaccountsavailableerror_0_null,PIXEL_C].png
index 379b73ff3d8..9a90c42156e 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_noaccountsavailableerror_0_null,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_noaccountsavailableerror_0_null,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_unclassifiederror_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_unclassifiederror_0_null,NEXUS_5].png
deleted file mode 100644
index 8537fa1f579..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_unclassifiederror_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_unclassifiederror_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_unclassifiederror_0_null,PIXEL_C].png
deleted file mode 100644
index b7da0a204cf..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Errors_unclassifiederror_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Loading_Default_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Loading_Default_0_null,NEXUS_5].png
index ad3d960dc2c..d67f69bc660 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Loading_Default_0_null,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Loading_Default_0_null,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Loading_Default_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Loading_Default_0_null,PIXEL_C].png
index 90f2f3579b6..614176912dd 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Loading_Default_0_null,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Loading_Default_0_null,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Loading_Shimmer_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Loading_Shimmer_0_null,NEXUS_5].png
new file mode 100644
index 00000000000..ee4d0f46891
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Loading_Shimmer_0_null,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Loading_Shimmer_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Loading_Shimmer_0_null,PIXEL_C].png
new file mode 100644
index 00000000000..1c8f280d130
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Loading_Shimmer_0_null,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Merchantdataaccesstext_Default_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Merchantdataaccesstext_Default_0_null,NEXUS_5].png
new file mode 100644
index 00000000000..5b23c7c5336
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Merchantdataaccesstext_Default_0_null,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Merchantdataaccesstext_Default_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Merchantdataaccesstext_Default_0_null,PIXEL_C].png
new file mode 100644
index 00000000000..a2fa5f8a963
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_Merchantdataaccesstext_Default_0_null,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth-Drawer_PartnerAuthDrawerPreview_0_null_0,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth-Drawer_PartnerAuthDrawerPreview_0_null_0,NEXUS_5].png
new file mode 100644
index 00000000000..dc45ccc7e94
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth-Drawer_PartnerAuthDrawerPreview_0_null_0,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth-Drawer_PartnerAuthDrawerPreview_0_null_0,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth-Drawer_PartnerAuthDrawerPreview_0_null_0,PIXEL_C].png
new file mode 100644
index 00000000000..7c27b861a45
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth-Drawer_PartnerAuthDrawerPreview_0_null_0,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth-Drawer_PartnerAuthDrawerPreview_0_null_1,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth-Drawer_PartnerAuthDrawerPreview_0_null_1,NEXUS_5].png
new file mode 100644
index 00000000000..f911f6f73e7
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth-Drawer_PartnerAuthDrawerPreview_0_null_1,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth-Drawer_PartnerAuthDrawerPreview_0_null_1,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth-Drawer_PartnerAuthDrawerPreview_0_null_1,PIXEL_C].png
new file mode 100644
index 00000000000..04edbc72d96
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth-Drawer_PartnerAuthDrawerPreview_0_null_1,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth-Drawer_PartnerAuthDrawerPreview_0_null_2,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth-Drawer_PartnerAuthDrawerPreview_0_null_2,NEXUS_5].png
new file mode 100644
index 00000000000..da1ae526cd0
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth-Drawer_PartnerAuthDrawerPreview_0_null_2,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth-Drawer_PartnerAuthDrawerPreview_0_null_2,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth-Drawer_PartnerAuthDrawerPreview_0_null_2,PIXEL_C].png
new file mode 100644
index 00000000000..1c24f7a5434
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth-Drawer_PartnerAuthDrawerPreview_0_null_2,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_0,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_0,NEXUS_5].png
index f5bc72cd34c..24e4a4f2e0b 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_0,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_0,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_0,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_0,PIXEL_C].png
index a0378842a86..af1222d0e8d 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_0,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_0,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_1,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_1,NEXUS_5].png
index 2a2edcdc5d6..9003ffed600 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_1,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_1,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_1,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_1,PIXEL_C].png
index d22b50ea2df..66ad248f265 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_1,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_1,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_2,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_2,NEXUS_5].png
new file mode 100644
index 00000000000..a02d0074577
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_2,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_2,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_2,PIXEL_C].png
new file mode 100644
index 00000000000..76c5fe1b2cb
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_SharedPartnerAuth_PartnerAuthPreview_0_null_2,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_0,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_0,NEXUS_5].png
index f3b2c1e7966..da000aaecb6 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_0,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_0,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_0,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_0,PIXEL_C].png
index febc800aadf..9431c78c2f5 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_0,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_0,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_1,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_1,NEXUS_5].png
index 2bb8eb9a0ac..5a832c9e74a 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_1,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_1,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_1,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_1,PIXEL_C].png
index ac05aaca6ee..f5627824999 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_1,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_1,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_2,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_2,NEXUS_5].png
index 36e75f34194..6c9b1d435c8 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_2,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_2,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_2,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_2,PIXEL_C].png
index 33b887983da..282933c3369 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_2,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_2,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_3,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_3,NEXUS_5].png
index 744013288d5..fdbea349328 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_3,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_3,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_3,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_3,PIXEL_C].png
index 2fc492e8012..d957924ebc0 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_3,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_3,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_4,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_4,NEXUS_5].png
index 8546c7aa420..547756dc7ab 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_4,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_4,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_4,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_4,PIXEL_C].png
index c5d5d817b3f..5951f9db012 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_4,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_4,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_5,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_5,NEXUS_5].png
index fd93f1a0184..7f2b82a080a 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_5,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_5,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_5,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_5,PIXEL_C].png
index 6030d83aa2e..6f58de7e15c 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_5,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_5,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_6,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_6,NEXUS_5].png
deleted file mode 100644
index 8bf6226e2f6..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_6,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_6,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_6,PIXEL_C].png
deleted file mode 100644
index d0260666b10..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.consent_null_ConsentPane_ContentPreview_0_null_6,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_0,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_0,NEXUS_5].png
new file mode 100644
index 00000000000..d67f69bc660
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_0,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_0,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_0,PIXEL_C].png
new file mode 100644
index 00000000000..614176912dd
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_0,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_1,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_1,NEXUS_5].png
new file mode 100644
index 00000000000..1ffa3cfda61
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_1,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_1,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_1,PIXEL_C].png
new file mode 100644
index 00000000000..b481c30fdec
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_1,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_2,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_2,NEXUS_5].png
new file mode 100644
index 00000000000..046a6c060fc
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_2,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_2,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_2,PIXEL_C].png
new file mode 100644
index 00000000000..e76537c1d6b
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_2,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_3,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_3,NEXUS_5].png
new file mode 100644
index 00000000000..e290ece9931
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_3,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_3,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_3,PIXEL_C].png
new file mode 100644
index 00000000000..95313cc8f6a
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.error_null_DefaultGroup_ErrorScreenPreview_0_null_3,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.exit_null_DefaultGroup_ExitModalPreview_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.exit_null_DefaultGroup_ExitModalPreview_0_null,NEXUS_5].png
new file mode 100644
index 00000000000..dcff35a6c38
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.exit_null_DefaultGroup_ExitModalPreview_0_null,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.exit_null_DefaultGroup_ExitModalPreview_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.exit_null_DefaultGroup_ExitModalPreview_0_null,PIXEL_C].png
new file mode 100644
index 00000000000..6a2d82c9b15
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.exit_null_DefaultGroup_ExitModalPreview_0_null,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_0,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_0,NEXUS_5].png
index a53464de3d9..d67f69bc660 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_0,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_0,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_0,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_0,PIXEL_C].png
index e48d03231dc..614176912dd 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_0,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_0,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_1,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_1,NEXUS_5].png
index e852be0e25d..a9a8bbd7ea2 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_1,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_1,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_1,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_1,PIXEL_C].png
index de8bd47049f..084becb75c7 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_1,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_1,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_10,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_10,NEXUS_5].png
new file mode 100644
index 00000000000..0fc946545e2
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_10,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_10,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_10,PIXEL_C].png
new file mode 100644
index 00000000000..65ccee43f0c
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_10,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_2,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_2,NEXUS_5].png
index afb59c9fd4e..9bd0fb30f0d 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_2,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_2,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_2,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_2,PIXEL_C].png
index 02793b93a25..732f2375293 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_2,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_2,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_3,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_3,NEXUS_5].png
index 42fc2816601..afdf777c482 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_3,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_3,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_3,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_3,PIXEL_C].png
index e3d459fc4a0..02835a2381d 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_3,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_3,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_4,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_4,NEXUS_5].png
index 3bb8a1ee045..f5dbd773bf5 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_4,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_4,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_4,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_4,PIXEL_C].png
index 62b1a6c1ff3..15b5c085a8e 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_4,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_4,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_5,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_5,NEXUS_5].png
index 8758fb0384a..0a6728bb5e4 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_5,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_5,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_5,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_5,PIXEL_C].png
index 2cdd64d62aa..004225f72ed 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_5,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_5,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_6,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_6,NEXUS_5].png
index 3bb8a1ee045..864cdcb1011 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_6,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_6,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_6,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_6,PIXEL_C].png
index 62b1a6c1ff3..8999d67f33e 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_6,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_6,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_7,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_7,NEXUS_5].png
index 8758fb0384a..0a6728bb5e4 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_7,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_7,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_7,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_7,PIXEL_C].png
index 2cdd64d62aa..004225f72ed 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_7,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_7,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_8,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_8,NEXUS_5].png
index 041bd0ff109..864cdcb1011 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_8,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_8,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_8,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_8,PIXEL_C].png
index 2ab1519c52e..8999d67f33e 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_8,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_8,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_9,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_9,NEXUS_5].png
new file mode 100644
index 00000000000..cf3a2e13eec
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_9,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_9,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_9,PIXEL_C].png
new file mode 100644
index 00000000000..fe3dfc44476
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.institutionpicker_null_InstitutionPickerPane_InstitutionPickerPreview_0_null_9,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_0,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_0,NEXUS_5].png
index 9213a64e841..36916c6a9f1 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_0,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_0,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_0,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_0,PIXEL_C].png
index af334b3c9fc..9b17b5c936e 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_0,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_0,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_1,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_1,NEXUS_5].png
index 7de449e92b2..0f7b581c583 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_1,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_1,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_1,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_1,PIXEL_C].png
index 4e5e7a7b419..cd06d35bda1 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_1,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_1,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_2,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_2,NEXUS_5].png
new file mode 100644
index 00000000000..c61d96b5aeb
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_2,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_2,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_2,PIXEL_C].png
new file mode 100644
index 00000000000..72fd403d8d1
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_2,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_3,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_3,NEXUS_5].png
new file mode 100644
index 00000000000..116e6257e0a
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_3,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_3,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_3,PIXEL_C].png
new file mode 100644
index 00000000000..0fdb2afedb0
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkaccountpicker_null_LinkAccountPickerPane_LinkAccountPickerScreenPreview_0_null_3,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_0,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_0,NEXUS_5].png
new file mode 100644
index 00000000000..d67f69bc660
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_0,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_0,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_0,PIXEL_C].png
new file mode 100644
index 00000000000..614176912dd
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_0,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_1,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_1,NEXUS_5].png
new file mode 100644
index 00000000000..5fb714aa485
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_1,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_1,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_1,PIXEL_C].png
new file mode 100644
index 00000000000..c9500c02b83
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_1,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_2,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_2,NEXUS_5].png
new file mode 100644
index 00000000000..3b3bfab1402
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_2,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_2,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_2,PIXEL_C].png
new file mode 100644
index 00000000000..4ffff133c7d
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_2,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_3,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_3,NEXUS_5].png
new file mode 100644
index 00000000000..828abbc9e09
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_3,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_3,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_3,PIXEL_C].png
new file mode 100644
index 00000000000..1439acadc83
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_3,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_4,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_4,NEXUS_5].png
new file mode 100644
index 00000000000..1ffa3cfda61
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_4,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_4,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_4,PIXEL_C].png
new file mode 100644
index 00000000000..b481c30fdec
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_DefaultGroup_LinkStepUpVerificationPreview_0_null_4,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_LinkStepUpVerificationPane_Canonical_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_LinkStepUpVerificationPane_Canonical_0_null,NEXUS_5].png
deleted file mode 100644
index 85b96a70334..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_LinkStepUpVerificationPane_Canonical_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_LinkStepUpVerificationPane_Canonical_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_LinkStepUpVerificationPane_Canonical_0_null,PIXEL_C].png
deleted file mode 100644
index d4a5979a2b3..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_LinkStepUpVerificationPane_Canonical_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_LinkStepUpVerificationPane_Resendingcode_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_LinkStepUpVerificationPane_Resendingcode_0_null,NEXUS_5].png
deleted file mode 100644
index e9ff3385e75..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_LinkStepUpVerificationPane_Resendingcode_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_LinkStepUpVerificationPane_Resendingcode_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_LinkStepUpVerificationPane_Resendingcode_0_null,PIXEL_C].png
deleted file mode 100644
index 9a3aa825ab3..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.linkstepupverification_null_LinkStepUpVerificationPane_Resendingcode_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_0,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_0,NEXUS_5].png
index 8d4bf00a2b4..a9876f994b5 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_0,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_0,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_0,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_0,PIXEL_C].png
index 5fbd03a1a62..bf6e3740401 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_0,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_0,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_1,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_1,NEXUS_5].png
index a58bb98d07e..eef92835261 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_1,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_1,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_1,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_1,PIXEL_C].png
index 1d3cf2cda14..7deea392fc2 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_1,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_1,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_2,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_2,NEXUS_5].png
new file mode 100644
index 00000000000..466d60f986a
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_2,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_2,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_2,PIXEL_C].png
new file mode 100644
index 00000000000..8b331329834
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_2,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_3,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_3,NEXUS_5].png
new file mode 100644
index 00000000000..bf9066f570c
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_3,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_3,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_3,PIXEL_C].png
new file mode 100644
index 00000000000..3e61e2a8ae8
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_3,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_4,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_4,NEXUS_5].png
new file mode 100644
index 00000000000..e939d5b474b
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_4,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_4,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_4,PIXEL_C].png
new file mode 100644
index 00000000000..ceb1f16bf9e
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentry_null_ManualEntryPane_ManualEntryPreview_0_null_4,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Amount_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Amount_0_null,NEXUS_5].png
deleted file mode 100644
index fa40be406f0..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Amount_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Amount_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Amount_0_null,PIXEL_C].png
deleted file mode 100644
index fa25da8893b..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Amount_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Amountnoaccount_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Amountnoaccount_0_null,NEXUS_5].png
deleted file mode 100644
index 08df414d7a1..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Amountnoaccount_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Amountnoaccount_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Amountnoaccount_0_null,PIXEL_C].png
deleted file mode 100644
index 947c3b4086d..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Amountnoaccount_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Descriptor_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Descriptor_0_null,NEXUS_5].png
deleted file mode 100644
index e729ac605ca..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Descriptor_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Descriptor_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Descriptor_0_null,PIXEL_C].png
deleted file mode 100644
index 49f4c24cfba..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Descriptor_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Descriptornoaccount_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Descriptornoaccount_0_null,NEXUS_5].png
deleted file mode 100644
index 6e65f8f042c..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Descriptornoaccount_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Descriptornoaccount_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Descriptornoaccount_0_null,PIXEL_C].png
deleted file mode 100644
index fdccf8a955a..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.manualentrysuccess_null_ManualEntrySuccess_Descriptornoaccount_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null,NEXUS_5].png
deleted file mode 100644
index 64eebeba76f..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null,PIXEL_C].png
deleted file mode 100644
index 8a965f53111..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_0,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_0,NEXUS_5].png
new file mode 100644
index 00000000000..a96f879b5d9
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_0,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_0,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_0,PIXEL_C].png
new file mode 100644
index 00000000000..c213d0b073a
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_0,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_1,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_1,NEXUS_5].png
new file mode 100644
index 00000000000..2c45543ad56
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_1,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_1,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_1,PIXEL_C].png
new file mode 100644
index 00000000000..444152e1fdc
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_1,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_2,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_2,NEXUS_5].png
new file mode 100644
index 00000000000..2c5d431986f
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_2,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_2,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_2,PIXEL_C].png
new file mode 100644
index 00000000000..e945162c214
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_2,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_3,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_3,NEXUS_5].png
new file mode 100644
index 00000000000..2c45543ad56
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_3,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_3,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_3,PIXEL_C].png
new file mode 100644
index 00000000000..444152e1fdc
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_3,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_4,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_4,NEXUS_5].png
new file mode 100644
index 00000000000..a96f879b5d9
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_4,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_4,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_4,PIXEL_C].png
new file mode 100644
index 00000000000..c213d0b073a
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkloginwarmup_null_NetworkingLinkLoginWarmupPane_Canonical_0_null_4,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_0,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_0,NEXUS_5].png
index 83d3e547540..ee100e34c52 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_0,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_0,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_0,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_0,PIXEL_C].png
index ac98bc19787..3dc2cd0e1ef 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_0,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_0,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_1,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_1,NEXUS_5].png
index d5ed1659897..8e0835a6703 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_1,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_1,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_1,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_1,PIXEL_C].png
index 60024a81997..d028eec907e 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_1,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_1,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_2,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_2,NEXUS_5].png
new file mode 100644
index 00000000000..8e0835a6703
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_2,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_2,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_2,PIXEL_C].png
new file mode 100644
index 00000000000..d028eec907e
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinksignup_null_NetworkingLinkSignupPane_NetworkingLinkSignupScreenPreview_0_null_2,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_0,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_0,NEXUS_5].png
new file mode 100644
index 00000000000..d67f69bc660
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_0,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_0,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_0,PIXEL_C].png
new file mode 100644
index 00000000000..614176912dd
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_0,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_1,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_1,NEXUS_5].png
new file mode 100644
index 00000000000..384704f06d5
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_1,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_1,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_1,PIXEL_C].png
new file mode 100644
index 00000000000..61d9037e001
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_1,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_2,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_2,NEXUS_5].png
new file mode 100644
index 00000000000..80c5a03553b
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_2,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_2,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_2,PIXEL_C].png
new file mode 100644
index 00000000000..03cbaf29cfb
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_2,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_3,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_3,NEXUS_5].png
new file mode 100644
index 00000000000..2293ede947a
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_3,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_3,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_3,PIXEL_C].png
new file mode 100644
index 00000000000..5fb51f7c248
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_3,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_4,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_4,NEXUS_5].png
new file mode 100644
index 00000000000..1ffa3cfda61
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_4,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_4,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_4,PIXEL_C].png
new file mode 100644
index 00000000000..b481c30fdec
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_DefaultGroup_NetworkingLinkVerificationPreview_0_null_4,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_NetworkingLinkVerificationPane_EnteringOTP_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_NetworkingLinkVerificationPane_EnteringOTP_0_null,NEXUS_5].png
deleted file mode 100644
index e3b93f8b12d..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_NetworkingLinkVerificationPane_EnteringOTP_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_NetworkingLinkVerificationPane_EnteringOTP_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_NetworkingLinkVerificationPane_EnteringOTP_0_null,PIXEL_C].png
deleted file mode 100644
index 2f158e9c546..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_NetworkingLinkVerificationPane_EnteringOTP_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_NetworkingLinkVerificationPane_Error_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_NetworkingLinkVerificationPane_Error_0_null,NEXUS_5].png
deleted file mode 100644
index 061e6d4a387..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_NetworkingLinkVerificationPane_Error_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_NetworkingLinkVerificationPane_Error_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_NetworkingLinkVerificationPane_Error_0_null,PIXEL_C].png
deleted file mode 100644
index 0e44d4294e8..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkinglinkverification_null_NetworkingLinkVerificationPane_Error_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_0,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_0,NEXUS_5].png
new file mode 100644
index 00000000000..d67f69bc660
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_0,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_0,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_0,PIXEL_C].png
new file mode 100644
index 00000000000..614176912dd
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_0,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_1,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_1,NEXUS_5].png
new file mode 100644
index 00000000000..90be3b6bb25
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_1,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_1,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_1,PIXEL_C].png
new file mode 100644
index 00000000000..9c26d13777a
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_1,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_2,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_2,NEXUS_5].png
new file mode 100644
index 00000000000..d1e38d6e7a7
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_2,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_2,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_2,PIXEL_C].png
new file mode 100644
index 00000000000..e58363621a4
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_2,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_3,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_3,NEXUS_5].png
new file mode 100644
index 00000000000..020f2ef3309
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_3,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_3,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_3,PIXEL_C].png
new file mode 100644
index 00000000000..932c2f09430
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_3,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_4,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_4,NEXUS_5].png
new file mode 100644
index 00000000000..1ffa3cfda61
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_4,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_4,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_4,PIXEL_C].png
new file mode 100644
index 00000000000..b481c30fdec
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_DefaultGroup_SaveToLinkVerificationPreview_0_null_4,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_NetworkingVerification_Default_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_NetworkingVerification_Default_0_null,NEXUS_5].png
deleted file mode 100644
index 8ced313cb37..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_NetworkingVerification_Default_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_NetworkingVerification_Default_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_NetworkingVerification_Default_0_null,PIXEL_C].png
deleted file mode 100644
index 203e1171143..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.networkingsavetolinkverification_null_NetworkingVerification_Default_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.reset_null_Reset_Default_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.reset_null_Reset_Default_0_null,NEXUS_5].png
index 45f8871ec0f..d67f69bc660 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.reset_null_Reset_Default_0_null,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.reset_null_Reset_Default_0_null,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.reset_null_Reset_Default_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.reset_null_Reset_Default_0_null,PIXEL_C].png
index 3331a8c9f23..614176912dd 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.reset_null_Reset_Default_0_null,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.reset_null_Reset_Default_0_null,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_DefaultGroup_SuccessScreenPreviewFailedToLink_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_DefaultGroup_SuccessScreenPreviewFailedToLink_0_null,NEXUS_5].png
deleted file mode 100644
index be3483c7e2d..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_DefaultGroup_SuccessScreenPreviewFailedToLink_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_DefaultGroup_SuccessScreenPreviewFailedToLink_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_DefaultGroup_SuccessScreenPreviewFailedToLink_0_null,PIXEL_C].png
deleted file mode 100644
index 9444cc9c8f3..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_DefaultGroup_SuccessScreenPreviewFailedToLink_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Animationcompleted_0_null_0,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Animationcompleted_0_null_0,NEXUS_5].png
new file mode 100644
index 00000000000..123c98e8b4b
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Animationcompleted_0_null_0,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Animationcompleted_0_null_0,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Animationcompleted_0_null_0,PIXEL_C].png
new file mode 100644
index 00000000000..5ecd6a615a3
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Animationcompleted_0_null_0,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Animationcompleted_0_null_1,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Animationcompleted_0_null_1,NEXUS_5].png
new file mode 100644
index 00000000000..12914df72b3
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Animationcompleted_0_null_1,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Animationcompleted_0_null_1,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Animationcompleted_0_null_1,PIXEL_C].png
new file mode 100644
index 00000000000..eabd50e9eea
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Animationcompleted_0_null_1,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Default_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Default_0_null,NEXUS_5].png
deleted file mode 100644
index f443546c769..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Default_0_null,NEXUS_5].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Default_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Default_0_null,PIXEL_C].png
deleted file mode 100644
index 9e77a88fe50..00000000000
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Default_0_null,PIXEL_C].png and /dev/null differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Loading_0_null_0,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Loading_0_null_0,NEXUS_5].png
new file mode 100644
index 00000000000..412f9c26cca
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Loading_0_null_0,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Loading_0_null_0,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Loading_0_null_0,PIXEL_C].png
new file mode 100644
index 00000000000..dc296e5d94b
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Loading_0_null_0,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Loading_0_null_1,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Loading_0_null_1,NEXUS_5].png
new file mode 100644
index 00000000000..412f9c26cca
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Loading_0_null_1,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Loading_0_null_1,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Loading_0_null_1,PIXEL_C].png
new file mode 100644
index 00000000000..dc296e5d94b
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.success_null_Success_Loading_0_null_1,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_Button-primary-idle_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_Button-primary-idle_0_null,NEXUS_5].png
index 6099f41136a..f498e3e3a2b 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_Button-primary-idle_0_null,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_Button-primary-idle_0_null,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_Button-primary-idle_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_Button-primary-idle_0_null,PIXEL_C].png
index 2eabe987957..fc2b4f07f5f 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_Button-primary-idle_0_null,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_Button-primary-idle_0_null,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TextField-idle_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TextField-idle_0_null,NEXUS_5].png
index b011a387acb..72c643a361b 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TextField-idle_0_null,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TextField-idle_0_null,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TextField-idle_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TextField-idle_0_null,PIXEL_C].png
index 37cd030b1b1..a8f48133763 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TextField-idle_0_null,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TextField-idle_0_null,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TopAppBar-noStripelogo_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TopAppBar-noStripelogo_0_null,NEXUS_5].png
index f387155405b..d8ab997d016 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TopAppBar-noStripelogo_0_null,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TopAppBar-noStripelogo_0_null,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TopAppBar-noStripelogo_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TopAppBar-noStripelogo_0_null,PIXEL_C].png
index e8d32eb6219..8fd9cbb4ec3 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TopAppBar-noStripelogo_0_null,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TopAppBar-noStripelogo_0_null,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TopAppBar_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TopAppBar_0_null,NEXUS_5].png
index aaf5ffff6de..236f6f981d2 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TopAppBar_0_null,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TopAppBar_0_null,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TopAppBar_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TopAppBar_0_null,PIXEL_C].png
index 66a6a8142dd..54fdd797e76 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TopAppBar_0_null,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_Components_TopAppBar_0_null,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_TestModeBanner_Disabled_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_TestModeBanner_Disabled_0_null,NEXUS_5].png
new file mode 100644
index 00000000000..833d49cd105
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_TestModeBanner_Disabled_0_null,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Default_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_TestModeBanner_Disabled_0_null,PIXEL_C].png
similarity index 50%
rename from financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Default_0_null,PIXEL_C].png
rename to financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_TestModeBanner_Disabled_0_null,PIXEL_C].png
index 5884beefd38..9ee14a43096 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.features.common_null_DataCallout_Default_0_null,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_TestModeBanner_Disabled_0_null,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_TestModeBanner_Enabled_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_TestModeBanner_Enabled_0_null,NEXUS_5].png
new file mode 100644
index 00000000000..20455bca7b8
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_TestModeBanner_Enabled_0_null,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_TestModeBanner_Enabled_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_TestModeBanner_Enabled_0_null,PIXEL_C].png
new file mode 100644
index 00000000000..0ac0e7c5ef6
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.components_null_TestModeBanner_Enabled_0_null,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_Components_Colors_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_Components_Colors_0_null,NEXUS_5].png
new file mode 100644
index 00000000000..adde7031371
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_Components_Colors_0_null,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_Components_Colors_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_Components_Colors_0_null,PIXEL_C].png
new file mode 100644
index 00000000000..d153c4b1da3
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_Components_Colors_0_null,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_Components_Type_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_Components_Type_0_null,NEXUS_5].png
index a42c472a048..1fba3e8d831 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_Components_Type_0_null,NEXUS_5].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_Components_Type_0_null,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_Components_Type_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_Components_Type_0_null,PIXEL_C].png
index 8519edfd6fd..b7cf4acbdac 100644
Binary files a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_Components_Type_0_null,PIXEL_C].png and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_Components_Type_0_null,PIXEL_C].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_DefaultGroup_LayoutPreview_0_null,NEXUS_5].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_DefaultGroup_LayoutPreview_0_null,NEXUS_5].png
new file mode 100644
index 00000000000..abde01ac43f
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_DefaultGroup_LayoutPreview_0_null,NEXUS_5].png differ
diff --git a/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_DefaultGroup_LayoutPreview_0_null,PIXEL_C].png b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_DefaultGroup_LayoutPreview_0_null,PIXEL_C].png
new file mode 100644
index 00000000000..4dcc43194a2
Binary files /dev/null and b/financial-connections/src/test/snapshots/images/com.stripe.android.financialconnections.screenshottests_PaparazziSampleScreenshotTest_preview_tests[com.stripe.android.financialconnections.ui.theme_null_DefaultGroup_LayoutPreview_0_null,PIXEL_C].png differ
diff --git a/identity/src/main/java/com/stripe/android/identity/injection/IdentityCommonModule.kt b/identity/src/main/java/com/stripe/android/identity/injection/IdentityCommonModule.kt
index aaf66adcdc6..29efd27de42 100644
--- a/identity/src/main/java/com/stripe/android/identity/injection/IdentityCommonModule.kt
+++ b/identity/src/main/java/com/stripe/android/identity/injection/IdentityCommonModule.kt
@@ -1,9 +1,16 @@
 package com.stripe.android.identity.injection
 
+import android.app.Application
 import android.content.Context
 import android.content.res.Resources
+import com.stripe.android.core.Logger
+import com.stripe.android.core.networking.AnalyticsRequestV2Executor
+import com.stripe.android.core.networking.DefaultAnalyticsRequestV2Executor
 import com.stripe.android.core.networking.DefaultStripeNetworkClient
 import com.stripe.android.core.networking.StripeNetworkClient
+import com.stripe.android.core.utils.IsWorkManagerAvailable
+import com.stripe.android.core.utils.RealIsWorkManagerAvailable
+import com.stripe.android.identity.BuildConfig
 import com.stripe.android.identity.networking.DefaultIdentityModelFetcher
 import com.stripe.android.identity.networking.DefaultIdentityRepository
 import com.stripe.android.identity.networking.IdentityModelFetcher
@@ -15,6 +22,8 @@ import com.stripe.android.mlcore.impl.InterpreterInitializerImpl
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
 import javax.inject.Singleton
 
 @Module(
@@ -33,6 +42,10 @@ internal abstract class IdentityCommonModule {
     @Singleton
     abstract fun bindIDDetectorFetcher(defaultIDDetectorFetcher: DefaultIdentityModelFetcher): IdentityModelFetcher
 
+    @Binds
+    @Singleton
+    abstract fun bindsAnalyticsRequestV2Executor(impl: DefaultAnalyticsRequestV2Executor): AnalyticsRequestV2Executor
+
     companion object {
         @Provides
         @Singleton
@@ -45,5 +58,27 @@ internal abstract class IdentityCommonModule {
         @Provides
         @Singleton
         fun provideInterpreterInitializer(): InterpreterInitializer = InterpreterInitializerImpl
+
+        @Provides
+        @Singleton
+        fun provideLogger(): Logger = Logger.getInstance(BuildConfig.DEBUG)
+
+        @Provides
+        @Singleton
+        fun provideApplication(context: Context): Application {
+            return context.applicationContext as Application
+        }
+
+        @Provides
+        @Singleton
+        internal fun providesIsWorkManagerAvailable(): IsWorkManagerAvailable {
+            return RealIsWorkManagerAvailable
+        }
+
+        @Provides
+        @Singleton
+        internal fun providesIoDispatcher(): CoroutineDispatcher {
+            return Dispatchers.IO
+        }
     }
 }
diff --git a/identity/src/main/java/com/stripe/android/identity/networking/DefaultIdentityRepository.kt b/identity/src/main/java/com/stripe/android/identity/networking/DefaultIdentityRepository.kt
index b9d4fccd12b..7380de976a7 100644
--- a/identity/src/main/java/com/stripe/android/identity/networking/DefaultIdentityRepository.kt
+++ b/identity/src/main/java/com/stripe/android/identity/networking/DefaultIdentityRepository.kt
@@ -1,6 +1,5 @@
 package com.stripe.android.identity.networking
 
-import android.util.Log
 import androidx.annotation.VisibleForTesting
 import com.stripe.android.camera.framework.time.Clock
 import com.stripe.android.core.exception.APIConnectionException
@@ -13,6 +12,7 @@ import com.stripe.android.core.model.parsers.ModelJsonParser
 import com.stripe.android.core.model.parsers.StripeErrorJsonParser
 import com.stripe.android.core.model.parsers.StripeFileJsonParser
 import com.stripe.android.core.networking.AnalyticsRequestV2
+import com.stripe.android.core.networking.AnalyticsRequestV2Executor
 import com.stripe.android.core.networking.ApiRequest
 import com.stripe.android.core.networking.StripeNetworkClient
 import com.stripe.android.core.networking.StripeRequest
@@ -32,7 +32,8 @@ import javax.inject.Inject
 
 internal class DefaultIdentityRepository @Inject constructor(
     private val stripeNetworkClient: StripeNetworkClient,
-    private val identityIO: IdentityIO
+    private val identityIO: IdentityIO,
+    private val analyticsRequestExecutor: AnalyticsRequestV2Executor,
 ) : IdentityRepository {
 
     @VisibleForTesting
@@ -234,11 +235,7 @@ internal class DefaultIdentityRepository @Inject constructor(
     )
 
     override suspend fun sendAnalyticsRequest(analyticsRequestV2: AnalyticsRequestV2) {
-        runCatching {
-            stripeNetworkClient.executeRequest(analyticsRequestV2)
-        }.onFailure {
-            Log.e(TAG, "Exception while making analytics request")
-        }
+        analyticsRequestExecutor.enqueue(analyticsRequestV2)
     }
 
     private suspend fun  executeRequestWithKSerializer(
diff --git a/identity/src/test/java/com/stripe/android/identity/networking/DefaultIdentityRepositoryTest.kt b/identity/src/test/java/com/stripe/android/identity/networking/DefaultIdentityRepositoryTest.kt
index 85f8d63e6fb..177a50cbffd 100644
--- a/identity/src/test/java/com/stripe/android/identity/networking/DefaultIdentityRepositoryTest.kt
+++ b/identity/src/test/java/com/stripe/android/identity/networking/DefaultIdentityRepositoryTest.kt
@@ -7,6 +7,7 @@ import com.stripe.android.core.model.Country
 import com.stripe.android.core.model.CountryCode
 import com.stripe.android.core.model.StripeFile
 import com.stripe.android.core.model.parsers.StripeErrorJsonParser
+import com.stripe.android.core.networking.AnalyticsRequestV2Executor
 import com.stripe.android.core.networking.ApiRequest
 import com.stripe.android.core.networking.HEADER_AUTHORIZATION
 import com.stripe.android.core.networking.StripeNetworkClient
@@ -44,9 +45,11 @@ class DefaultIdentityRepositoryTest {
         whenever(it.createTFLiteFile(any())).thenReturn(mock())
     }
     private val mockStripeNetworkClient: StripeNetworkClient = mock()
+    private val fakeAnalyticsRequestExecutor: AnalyticsRequestV2Executor = mock()
     private val identityRepository = DefaultIdentityRepository(
         mockStripeNetworkClient,
-        mockIO
+        mockIO,
+        fakeAnalyticsRequestExecutor,
     )
 
     private val requestCaptor: KArgumentCaptor = argumentCaptor()
diff --git a/maestro/financial-connections/Testmode-PaymentIntent-TestInstitution-Networking-ReturningUser.yaml b/maestro/financial-connections/Testmode-PaymentIntent-TestInstitution-Networking-ReturningUser.yaml
new file mode 100644
index 00000000000..856e07c0842
--- /dev/null
+++ b/maestro/financial-connections/Testmode-PaymentIntent-TestInstitution-Networking-ReturningUser.yaml
@@ -0,0 +1,51 @@
+appId: com.stripe.android.financialconnections.example
+tags:
+  - all
+  - edge
+  - testmode-payments
+---
+- startRecording: ${'/tmp/test_results/testmode-paymentintent-testinstitution-networking-returning-user-' + new Date().getTime()}
+- clearState
+- openLink: stripeconnectionsexample://playground?flow=PaymentIntent&financial_connections_override_native=native&merchant=networking_testmode&permissions=transactions,payment_method
+- tapOn:
+    id: "Customer email setting"
+- inputText: "email@email.com"
+- hideKeyboard
+- tapOn:
+    id: "connect_accounts"
+# Wait until the consent button is visible
+- extendedWaitUntil:
+    visible:
+      id: "consent_cta"
+    timeout: 30000
+- tapOn:
+    id: "consent_cta"
+# LINK WARMUP PANE
+- extendedWaitUntil:
+    visible:
+      id: "existing_email-button"
+    timeout: 5000
+- assertNotVisible:
+    id: "top-app-bar-back-button"
+- tapOn:
+    id: "existing_email-button"
+# OTP SCREEN
+- extendedWaitUntil:
+    visible: "Save your account to Link"
+    timeout: 5000
+- assertVisible:
+    id: "top-app-bar-back-button"
+- inputText: "000000"
+# SELECT ACCOUNT
+- extendedWaitUntil:
+    visible: "Select account"
+    timeout: 5000
+- tapOn: "Account Closed"
+- tapOn: "Connect account"
+# EMAIL OTP SCREEN
+- waitForAnimationToEnd
+- inputText: "000000"
+# CONFIRM AND COMPLETE
+- waitForAnimationToEnd
+- assertVisible: "Success"
+- stopRecording
diff --git a/payments-model/src/main/java/com/stripe/android/model/ConsumerSession.kt b/payments-model/src/main/java/com/stripe/android/model/ConsumerSession.kt
index 512b0599507..453b2b79e65 100644
--- a/payments-model/src/main/java/com/stripe/android/model/ConsumerSession.kt
+++ b/payments-model/src/main/java/com/stripe/android/model/ConsumerSession.kt
@@ -31,7 +31,7 @@ data class ConsumerSession(
     @Parcelize
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @Serializable
-    data class VerificationSession constructor(
+    data class VerificationSession(
         val type: SessionType,
         val state: SessionState
     ) : StripeModel {
diff --git a/stripe-core/api/stripe-core.api b/stripe-core/api/stripe-core.api
index 1ea18c83dc5..b6b7e65f5bf 100644
--- a/stripe-core/api/stripe-core.api
+++ b/stripe-core/api/stripe-core.api
@@ -253,6 +253,17 @@ public abstract interface class com/stripe/android/core/model/StripeModel : andr
 	public abstract fun hashCode ()I
 }
 
+public final class com/stripe/android/core/networking/AnalyticsRequestV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+	public static final field INSTANCE Lcom/stripe/android/core/networking/AnalyticsRequestV2$$serializer;
+	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/stripe/android/core/networking/AnalyticsRequestV2;
+	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/core/networking/AnalyticsRequestV2;)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/core/networking/ApiRequest$Options$Creator : android/os/Parcelable$Creator {
 	public fun  ()V
 	public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/core/networking/ApiRequest$Options;
diff --git a/stripe-core/build.gradle b/stripe-core/build.gradle
index dec0fba6fef..d35458d28c1 100644
--- a/stripe-core/build.gradle
+++ b/stripe-core/build.gradle
@@ -18,12 +18,15 @@ dependencies {
     implementation libs.kotlin.coroutinesAndroid
     implementation libs.kotlin.serialization
 
+    compileOnly libs.androidx.workManager
+
     ksp libs.daggerCompiler
 
     testImplementation testLibs.androidx.archCore
     testImplementation testLibs.androidx.core
     testImplementation testLibs.androidx.fragment
     testImplementation testLibs.androidx.testRunner
+    testImplementation testLibs.androidx.workManager
     testImplementation testLibs.json
     testImplementation testLibs.junit
     testImplementation testLibs.kotlin.annotations
diff --git a/stripe-core/src/main/java/com/stripe/android/core/networking/AnalyticsRequestV2.kt b/stripe-core/src/main/java/com/stripe/android/core/networking/AnalyticsRequestV2.kt
index 9223a4e8975..6663cb807d7 100644
--- a/stripe-core/src/main/java/com/stripe/android/core/networking/AnalyticsRequestV2.kt
+++ b/stripe-core/src/main/java/com/stripe/android/core/networking/AnalyticsRequestV2.kt
@@ -9,6 +9,11 @@ import com.stripe.android.core.networking.AnalyticsRequestV2.Companion.PARAM_EVE
 import com.stripe.android.core.networking.AnalyticsRequestV2.Companion.PARAM_EVENT_NAME
 import com.stripe.android.core.networking.StripeRequest.MimeType
 import com.stripe.android.core.version.StripeSdkVersion.VERSION_NAME
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
 import java.io.OutputStream
 import java.io.UnsupportedEncodingException
 import java.net.URLEncoder
@@ -33,14 +38,15 @@ import kotlin.time.DurationUnit.SECONDS
  * Additional params can be passed as constructor parameters.
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class AnalyticsRequestV2(
+@Serializable
+class AnalyticsRequestV2 private constructor(
     @get:VisibleForTesting
     val eventName: String,
     private val clientId: String,
-    origin: String,
-    @get:VisibleForTesting
-    val params: Map
+    private val origin: String,
+    private val params: JsonElement,
 ) : StripeRequest() {
+
     // Note: nested params are calculated as a json string, which is different from other requests
     // that uses form encoding.
     // E.g for a nested map with value {"key", {"nestedKey1" -> "value1", "nestedKey2" -> "value2"}}
@@ -52,7 +58,7 @@ class AnalyticsRequestV2(
     // As opposed to
     // key[nestedKey1]="value1"&key[nestedKey2]="value2"
     @VisibleForTesting
-    internal val postParameters: String = createParams(params + analyticParams())
+    internal val postParameters: String = createPostParams()
 
     private val postBodyBytes: ByteArray
         @Throws(UnsupportedEncodingException::class)
@@ -78,9 +84,10 @@ class AnalyticsRequestV2(
         }
     }
 
-    private fun createParams(map: Map): String {
+    private fun createPostParams(): String {
+        val postParams = params.toMap() + analyticParams()
         val paramList = mutableListOf()
-        QueryStringFactory.compactParams(map).forEach { (key, value) ->
+        QueryStringFactory.compactParams(postParams).forEach { (key, value) ->
             when (value) {
                 is Map<*, *> -> {
                     paramList.add(Parameter(key, encodeMapParam(value)))
@@ -142,11 +149,12 @@ class AnalyticsRequestV2(
         }
     }
 
-    override val headers = mapOf(
+    override val headers: Map = mapOf(
         HEADER_CONTENT_TYPE to "${MimeType.Form.code}; charset=${Charsets.UTF_8.name()}",
         HEADER_ORIGIN to origin, // required by r.stripe.com
         HEADER_USER_AGENT to "Stripe/v1 android/$VERSION_NAME" // required by r.stripe.com
     )
+
     override val method: Method = Method.POST
 
     override val mimeType: MimeType = MimeType.Form
@@ -165,5 +173,45 @@ class AnalyticsRequestV2(
         internal const val PARAM_EVENT_ID = "event_id"
 
         private const val INDENTATION = "  "
+
+        fun create(
+            eventName: String,
+            clientId: String,
+            origin: String,
+            params: Map,
+        ): AnalyticsRequestV2 {
+            return AnalyticsRequestV2(
+                eventName = eventName,
+                clientId = clientId,
+                origin = origin,
+                params = params.toJsonElement(),
+            )
+        }
+    }
+}
+
+private fun List<*>.toJsonElement(): JsonElement {
+    val list: MutableList = mutableListOf()
+    filterNotNull().forEach { value ->
+        when (value) {
+            is Map<*, *> -> list.add((value).toJsonElement())
+            is List<*> -> list.add(value.toJsonElement())
+            else -> list.add(JsonPrimitive(value.toString()))
+        }
+    }
+    return JsonArray(list)
+}
+
+private fun Map<*, *>.toJsonElement(): JsonElement {
+    val map: MutableMap = mutableMapOf()
+    this.forEach { entry ->
+        val key = entry.key as? String ?: return@forEach
+        val value = entry.value ?: return@forEach
+        when (value) {
+            is Map<*, *> -> map[key] = (value).toJsonElement()
+            is List<*> -> map[key] = value.toJsonElement()
+            else -> map[key] = JsonPrimitive(value.toString())
+        }
     }
+    return JsonObject(map)
 }
diff --git a/stripe-core/src/main/java/com/stripe/android/core/networking/AnalyticsRequestV2Executor.kt b/stripe-core/src/main/java/com/stripe/android/core/networking/AnalyticsRequestV2Executor.kt
new file mode 100644
index 00000000000..6f0795bf055
--- /dev/null
+++ b/stripe-core/src/main/java/com/stripe/android/core/networking/AnalyticsRequestV2Executor.kt
@@ -0,0 +1,60 @@
+package com.stripe.android.core.networking
+
+import android.app.Application
+import androidx.annotation.RestrictTo
+import androidx.work.Constraints
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.await
+import com.stripe.android.core.Logger
+import com.stripe.android.core.utils.IsWorkManagerAvailable
+import javax.inject.Inject
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun interface AnalyticsRequestV2Executor {
+    suspend fun enqueue(request: AnalyticsRequestV2)
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class DefaultAnalyticsRequestV2Executor @Inject constructor(
+    private val application: Application,
+    private val networkClient: StripeNetworkClient,
+    private val logger: Logger,
+    private val isWorkManagerAvailable: IsWorkManagerAvailable,
+) : AnalyticsRequestV2Executor {
+
+    override suspend fun enqueue(request: AnalyticsRequestV2) {
+        val isEnqueued = isWorkManagerAvailable() && enqueueRequest(request)
+        if (!isEnqueued) {
+            executeRequest(request)
+        }
+    }
+
+    private suspend fun enqueueRequest(request: AnalyticsRequestV2): Boolean {
+        val workManager = WorkManager.getInstance(application)
+        val inputData = SendAnalyticsRequestV2Worker.createInputData(request)
+
+        val constraints = Constraints.Builder()
+            .setRequiredNetworkType(NetworkType.CONNECTED)
+            .setRequiresBatteryNotLow(true)
+            .build()
+
+        val workRequest = OneTimeWorkRequestBuilder()
+            .addTag(SendAnalyticsRequestV2Worker.TAG)
+            .setInputData(inputData)
+            .setConstraints(constraints)
+            .build()
+
+        return runCatching { workManager.enqueue(workRequest).await() }.isSuccess
+    }
+
+    private suspend fun executeRequest(request: AnalyticsRequestV2) {
+        runCatching {
+            networkClient.executeRequest(request)
+            logger.debug("EVENT: ${request.eventName}")
+        }.onFailure {
+            logger.error("Exception while making analytics request", it)
+        }
+    }
+}
diff --git a/stripe-core/src/main/java/com/stripe/android/core/networking/AnalyticsRequestV2Factory.kt b/stripe-core/src/main/java/com/stripe/android/core/networking/AnalyticsRequestV2Factory.kt
index 9bcf376fd60..fafe55185fb 100644
--- a/stripe-core/src/main/java/com/stripe/android/core/networking/AnalyticsRequestV2Factory.kt
+++ b/stripe-core/src/main/java/com/stripe/android/core/networking/AnalyticsRequestV2Factory.kt
@@ -37,7 +37,7 @@ class AnalyticsRequestV2Factory(
         eventName: String,
         additionalParams: Map = mapOf(),
         includeSDKParams: Boolean = true
-    ) = AnalyticsRequestV2(
+    ) = AnalyticsRequestV2.create(
         eventName = eventName,
         clientId = clientId,
         origin = origin,
diff --git a/stripe-core/src/main/java/com/stripe/android/core/networking/SendAnalyticsRequestV2Worker.kt b/stripe-core/src/main/java/com/stripe/android/core/networking/SendAnalyticsRequestV2Worker.kt
new file mode 100644
index 00000000000..f8fe722e19d
--- /dev/null
+++ b/stripe-core/src/main/java/com/stripe/android/core/networking/SendAnalyticsRequestV2Worker.kt
@@ -0,0 +1,80 @@
+package com.stripe.android.core.networking
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.work.CoroutineWorker
+import androidx.work.Data
+import androidx.work.WorkerParameters
+import androidx.work.workDataOf
+import com.stripe.android.core.exception.APIException
+import com.stripe.android.core.exception.InvalidRequestException
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+
+private const val DataKey = "data"
+
+internal class SendAnalyticsRequestV2Worker(
+    appContext: Context,
+    params: WorkerParameters,
+) : CoroutineWorker(appContext, params) {
+
+    override suspend fun doWork(): Result = withRequest { request ->
+        val stripeNetworkClient = getNetworkClient()
+
+        return runCatching {
+            stripeNetworkClient.executeRequest(request)
+        }.fold(
+            onSuccess = {
+                Result.success()
+            },
+            onFailure = { error ->
+                if (error.shouldRetry) {
+                    Result.retry()
+                } else {
+                    Result.failure()
+                }
+            },
+        )
+    }
+
+    private inline fun withRequest(block: (AnalyticsRequestV2) -> Result): Result {
+        val request = getRequest(inputData) ?: return Result.failure()
+        return block(request)
+    }
+
+    companion object {
+
+        const val TAG = "SendAnalyticsRequestV2Worker"
+
+        private var networkClient: StripeNetworkClient? = null
+
+        fun getNetworkClient(): StripeNetworkClient {
+            if (networkClient == null) {
+                networkClient = DefaultStripeNetworkClient()
+            }
+            return networkClient!!
+        }
+
+        fun createInputData(request: AnalyticsRequestV2): Data {
+            val encodedRequest = Json.encodeToString(request)
+            return workDataOf(DataKey to encodedRequest)
+        }
+
+        private fun getRequest(data: Data): AnalyticsRequestV2? {
+            val encodedRequest = data.getString(DataKey)
+            return encodedRequest?.let {
+                runCatching {
+                    Json.decodeFromString(it)
+                }.getOrNull()
+            }
+        }
+
+        @VisibleForTesting
+        fun setNetworkClient(networkClient: StripeNetworkClient) {
+            this.networkClient = networkClient
+        }
+    }
+}
+
+private val Throwable.shouldRetry: Boolean
+    get() = this !is APIException && this !is InvalidRequestException
diff --git a/stripe-core/src/main/java/com/stripe/android/core/utils/IsWorkManagerAvailable.kt b/stripe-core/src/main/java/com/stripe/android/core/utils/IsWorkManagerAvailable.kt
new file mode 100644
index 00000000000..2bc8e83194f
--- /dev/null
+++ b/stripe-core/src/main/java/com/stripe/android/core/utils/IsWorkManagerAvailable.kt
@@ -0,0 +1,20 @@
+package com.stripe.android.core.utils
+
+import androidx.annotation.RestrictTo
+import androidx.work.WorkManager
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun interface IsWorkManagerAvailable {
+    operator fun invoke(): Boolean
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+object RealIsWorkManagerAvailable : IsWorkManagerAvailable {
+    override fun invoke(): Boolean {
+        val workManagerInClasspath = runCatching {
+            Class.forName("androidx.work.WorkManager")
+        }.isSuccess
+
+        return workManagerInClasspath && WorkManager.isInitialized()
+    }
+}
diff --git a/stripe-core/src/test/java/com/stripe/android/core/networking/DefaultAnalyticsRequestV2ExecutorTest.kt b/stripe-core/src/test/java/com/stripe/android/core/networking/DefaultAnalyticsRequestV2ExecutorTest.kt
new file mode 100644
index 00000000000..cf2d84dbdd3
--- /dev/null
+++ b/stripe-core/src/test/java/com/stripe/android/core/networking/DefaultAnalyticsRequestV2ExecutorTest.kt
@@ -0,0 +1,88 @@
+package com.stripe.android.core.networking
+
+import android.app.Application
+import androidx.test.core.app.ApplicationProvider
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import androidx.work.testing.WorkManagerTestInitHelper
+import com.google.common.truth.Truth.assertThat
+import com.stripe.android.core.Logger
+import com.stripe.android.core.utils.FakeStripeNetworkClient
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.withContext
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+internal class DefaultAnalyticsRequestV2ExecutorTest {
+
+    private val application = ApplicationProvider.getApplicationContext()
+
+    @Before
+    fun before() {
+        initializeWorkManager()
+    }
+
+    @Test
+    fun `Enqueues requests directly if WorkManager is available`() = runTest {
+        val networkClient = FakeStripeNetworkClient()
+
+        val executor = DefaultAnalyticsRequestV2Executor(
+            application = application,
+            networkClient = networkClient,
+            logger = Logger.noop(),
+            isWorkManagerAvailable = { true },
+        )
+
+        val request = mockAnalyticsRequest()
+        executor.enqueue(request)
+
+        val work = findWork()
+        assertThat(work?.state).isEqualTo(WorkInfo.State.ENQUEUED)
+
+        assertThat(networkClient.executeRequestCalled).isFalse()
+    }
+
+    @Test
+    fun `Executes requests directly if WorkManager isn't available`() = runTest {
+        val networkClient = FakeStripeNetworkClient()
+
+        val executor = DefaultAnalyticsRequestV2Executor(
+            application = application,
+            networkClient = networkClient,
+            logger = Logger.noop(),
+            isWorkManagerAvailable = { false },
+        )
+
+        val request = mockAnalyticsRequest()
+        executor.enqueue(request)
+
+        assertThat(networkClient.executeRequestCalled).isTrue()
+    }
+
+    private fun initializeWorkManager() {
+        WorkManagerTestInitHelper.initializeTestWorkManager(application)
+    }
+
+    private fun mockAnalyticsRequest(): AnalyticsRequestV2 {
+        return AnalyticsRequestV2.create(
+            eventName = "event_name",
+            clientId = "123",
+            origin = "origin",
+            params = emptyMap(),
+        )
+    }
+
+    private suspend fun findWork(): WorkInfo? {
+        val workManager = WorkManager.getInstance(application)
+        val tag = SendAnalyticsRequestV2Worker.TAG
+
+        return withContext(Dispatchers.IO) {
+            workManager.getWorkInfosByTag(tag).get().singleOrNull()
+        }
+    }
+}
diff --git a/stripe-core/src/test/java/com/stripe/android/core/networking/SendAnalyticsRequestV2WorkerTest.kt b/stripe-core/src/test/java/com/stripe/android/core/networking/SendAnalyticsRequestV2WorkerTest.kt
new file mode 100644
index 00000000000..8d05af50dd8
--- /dev/null
+++ b/stripe-core/src/test/java/com/stripe/android/core/networking/SendAnalyticsRequestV2WorkerTest.kt
@@ -0,0 +1,72 @@
+package com.stripe.android.core.networking
+
+import android.app.Application
+import androidx.test.core.app.ApplicationProvider
+import androidx.work.ListenableWorker
+import androidx.work.testing.TestListenableWorkerBuilder
+import com.google.common.truth.Truth.assertThat
+import com.stripe.android.core.exception.APIConnectionException
+import com.stripe.android.core.exception.APIException
+import com.stripe.android.core.utils.FakeStripeNetworkClient
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+internal class SendAnalyticsRequestV2WorkerTest {
+
+    private val application = ApplicationProvider.getApplicationContext()
+
+    @Test
+    fun `Returns success upon successful network response`() = runTest {
+        runWorkerTest(
+            executeRequest = { StripeResponse(200, body = null) },
+            expectedResult = ListenableWorker.Result.success(),
+        )
+    }
+
+    @Test
+    fun `Returns retry upon retryable network response`() = runTest {
+        runWorkerTest(
+            executeRequest = { throw APIConnectionException() },
+            expectedResult = ListenableWorker.Result.retry(),
+        )
+    }
+
+    @Test
+    fun `Returns failure upon network response that can't be retried`() = runTest {
+        runWorkerTest(
+            executeRequest = { throw APIException() },
+            expectedResult = ListenableWorker.Result.failure(),
+        )
+    }
+
+    private suspend fun runWorkerTest(
+        executeRequest: () -> StripeResponse,
+        expectedResult: ListenableWorker.Result,
+    ) {
+        val request = mockAnalyticsRequest()
+        val input = SendAnalyticsRequestV2Worker.createInputData(request)
+
+        val worker = TestListenableWorkerBuilder(application)
+            .setInputData(input)
+            .build()
+
+        val networkClient = FakeStripeNetworkClient(executeRequest = executeRequest)
+
+        SendAnalyticsRequestV2Worker.setNetworkClient(networkClient)
+
+        val result = worker.doWork()
+        assertThat(result).isEqualTo(expectedResult)
+    }
+
+    private fun mockAnalyticsRequest(): AnalyticsRequestV2 {
+        return AnalyticsRequestV2.create(
+            eventName = "event_name",
+            clientId = "123",
+            origin = "origin",
+            params = emptyMap(),
+        )
+    }
+}
diff --git a/stripe-core/src/test/java/com/stripe/android/core/utils/FakeStripeNetworkClient.kt b/stripe-core/src/test/java/com/stripe/android/core/utils/FakeStripeNetworkClient.kt
new file mode 100644
index 00000000000..adfee748f69
--- /dev/null
+++ b/stripe-core/src/test/java/com/stripe/android/core/utils/FakeStripeNetworkClient.kt
@@ -0,0 +1,24 @@
+package com.stripe.android.core.utils
+
+import com.stripe.android.core.networking.StripeNetworkClient
+import com.stripe.android.core.networking.StripeRequest
+import com.stripe.android.core.networking.StripeResponse
+import java.io.File
+
+internal class FakeStripeNetworkClient(
+    private val executeRequest: () -> StripeResponse = { StripeResponse(200, null) },
+    private val executeRequestForFile: () -> StripeResponse = { StripeResponse(200, null) },
+) : StripeNetworkClient {
+
+    var executeRequestCalled: Boolean = false
+        private set
+
+    override suspend fun executeRequest(request: StripeRequest): StripeResponse {
+        executeRequestCalled = true
+        return executeRequest()
+    }
+
+    override suspend fun executeRequestForFile(request: StripeRequest, outputFile: File): StripeResponse {
+        return executeRequestForFile()
+    }
+}
diff --git a/stripe-ui-core/detekt-baseline.xml b/stripe-ui-core/detekt-baseline.xml
index 2e81f5e3f97..157c5202a03 100644
--- a/stripe-ui-core/detekt-baseline.xml
+++ b/stripe-ui-core/detekt-baseline.xml
@@ -7,11 +7,12 @@
     ConstructorParameterNaming:RowElement.kt$RowElement$_identifier: IdentifierSpec
     CyclomaticComplexMethod:Html.kt$@Composable @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) fun annotatedStringResource( text: String, imageGetter: Map<String, EmbeddableImage> = emptyMap(), urlSpanStyle: SpanStyle = SpanStyle(textDecoration = TextDecoration.Underline) ): AnnotatedString
     CyclomaticComplexMethod:IdentifierSpec.kt$IdentifierSpec.Companion$@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) fun get(value: String)
+    CyclomaticComplexMethod:OTPElementUI.kt$@OptIn(ExperimentalComposeUiApi::class) @Composable @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) fun OTPElementUI( enabled: Boolean, element: OTPElement, modifier: Modifier = Modifier, boxShape: Shape = MaterialTheme.shapes.medium, boxTextStyle: TextStyle = OTPElementUI.defaultTextStyle(), boxSpacing: Dp = 8.dp, middleSpacing: Dp = 20.dp, otpInputPlaceholder: String = "●", colors: OTPElementColors = OTPElementColors( selectedBorder = MaterialTheme.colors.primary, placeholder = MaterialTheme.stripeColors.placeholderText ), focusRequester: FocusRequester = remember { FocusRequester() } )
     CyclomaticComplexMethod:StripeTheme.kt$@Composable @ReadOnlyComposable @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) fun StripeTypography.toComposeTypography(): Typography
-    CyclomaticComplexMethod:TextFieldUI.kt$@OptIn(ExperimentalComposeUiApi::class) @Composable fun TextField( textFieldController: TextFieldController, enabled: Boolean, imeAction: ImeAction, modifier: Modifier = Modifier, onTextStateChanged: (TextFieldState?) -> Unit = {}, nextFocusDirection: FocusDirection = FocusDirection.Next, previousFocusDirection: FocusDirection = FocusDirection.Previous, focusRequester: FocusRequester = remember { FocusRequester() }, )
     EmptyFunctionBlock:DrawablePainter.kt$EmptyPainter${}
     FunctionParameterNaming:IdentifierSpec.kt$IdentifierSpec.Companion$_value: String
     LongMethod:Html.kt$@Composable @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) fun annotatedStringResource( text: String, imageGetter: Map<String, EmbeddableImage> = emptyMap(), urlSpanStyle: SpanStyle = SpanStyle(textDecoration = TextDecoration.Underline) ): AnnotatedString
+    LongMethod:OTPElementUI.kt$@OptIn(ExperimentalComposeUiApi::class) @Composable @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) fun OTPElementUI( enabled: Boolean, element: OTPElement, modifier: Modifier = Modifier, boxShape: Shape = MaterialTheme.shapes.medium, boxTextStyle: TextStyle = OTPElementUI.defaultTextStyle(), boxSpacing: Dp = 8.dp, middleSpacing: Dp = 20.dp, otpInputPlaceholder: String = "●", colors: OTPElementColors = OTPElementColors( selectedBorder = MaterialTheme.colors.primary, placeholder = MaterialTheme.stripeColors.placeholderText ), focusRequester: FocusRequester = remember { FocusRequester() } )
     LongMethod:TextFieldUI.kt$@OptIn(ExperimentalComposeUiApi::class) @Composable fun TextField( textFieldController: TextFieldController, enabled: Boolean, imeAction: ImeAction, modifier: Modifier = Modifier, onTextStateChanged: (TextFieldState?) -> Unit = {}, nextFocusDirection: FocusDirection = FocusDirection.Next, previousFocusDirection: FocusDirection = FocusDirection.Previous, focusRequester: FocusRequester = remember { FocusRequester() }, )
     MagicNumber:DateConfig.kt$DateConfig$4
     MagicNumber:DateConfig.kt$DateConfig.Companion$100
diff --git a/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/EmailConfig.kt b/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/EmailConfig.kt
index 829744769fb..7e1e40a2ccb 100644
--- a/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/EmailConfig.kt
+++ b/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/EmailConfig.kt
@@ -13,12 +13,12 @@ import kotlinx.coroutines.flow.StateFlow
 import java.util.regex.Pattern
 
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class EmailConfig : TextFieldConfig {
+class EmailConfig(
+    @StringRes override val label: Int = R.string.stripe_email
+) : TextFieldConfig {
+
     override val capitalization: KeyboardCapitalization = KeyboardCapitalization.None
     override val debugLabel = "email"
-
-    @StringRes
-    override val label = R.string.stripe_email
     override val keyboard = KeyboardType.Email
     override val visualTransformation: VisualTransformation? = null
     override val trailingIcon: MutableStateFlow = MutableStateFlow(null)
diff --git a/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/OTPController.kt b/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/OTPController.kt
index 3177d4fc946..c36a78724ff 100644
--- a/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/OTPController.kt
+++ b/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/OTPController.kt
@@ -9,6 +9,11 @@ import kotlinx.coroutines.flow.distinctUntilChanged
 
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 class OTPController(val otpLength: Int = 6) : Controller {
+
+    // SMS autofill delivers us one character at a time. We store them here until
+    // we have received all.
+    private var autofillAccumulator = ""
+
     internal val keyboardType = KeyboardType.NumberPassword
 
     internal val fieldValues: List> = (0 until otpLength).map {
@@ -51,6 +56,15 @@ class OTPController(val otpLength: Int = 6) : Controller {
         return inputLength
     }
 
+    fun onAutofillDigit(digit: String) {
+        autofillAccumulator += digit
+
+        if (autofillAccumulator.length == otpLength) {
+            onValueChanged(0, autofillAccumulator)
+            autofillAccumulator = ""
+        }
+    }
+
     fun reset() {
         fieldValues.forEach { it.value = "" }
     }
diff --git a/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/OTPElementUI.kt b/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/OTPElementUI.kt
index 3862ab91ec2..938c34cb19f 100644
--- a/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/OTPElementUI.kt
+++ b/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/OTPElementUI.kt
@@ -26,7 +26,9 @@ import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.autofill.AutofillType
 import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.focus.FocusDirection
 import androidx.compose.ui.focus.FocusManager
@@ -56,6 +58,7 @@ import androidx.compose.ui.unit.sp
 import com.stripe.android.uicore.StripeTheme
 import com.stripe.android.uicore.getBorderStrokeWidth
 import com.stripe.android.uicore.stripeColors
+import com.stripe.android.uicore.text.autofill
 
 @Composable
 @Preview(backgroundColor = 0xFFFFFFFF, showBackground = true)
@@ -85,8 +88,8 @@ internal fun OTPElementUIDisabledPreview() {
     }
 }
 
+@OptIn(ExperimentalComposeUiApi::class)
 @Composable
-@Suppress("LongMethod")
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 fun OTPElementUI(
     enabled: Boolean,
@@ -134,8 +137,13 @@ fun OTPElementUI(
                 )
             ) {
                 val value by element.controller.fieldValues[index].collectAsState("")
+
                 var textFieldModifier = Modifier
                     .height(56.dp)
+                    .autofill(
+                        types = listOf(AutofillType.SmsOtpCode),
+                        onFill = element.controller::onAutofillDigit,
+                    )
                     .onFocusChanged { focusState ->
                         if (focusState.isFocused) {
                             focusedElementIndex = index
@@ -182,7 +190,6 @@ fun OTPElementUI(
 }
 
 @Composable
-@OptIn(ExperimentalMaterialApi::class)
 private fun OTPInputBox(
     value: String,
     isSelected: Boolean,
diff --git a/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/PhoneNumberElementUI.kt b/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/PhoneNumberElementUI.kt
index 0b6a4611cca..9ab1ebf7d07 100644
--- a/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/PhoneNumberElementUI.kt
+++ b/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/PhoneNumberElementUI.kt
@@ -22,7 +22,9 @@ import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.autofill.AutofillType
 import androidx.compose.ui.focus.FocusDirection
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
@@ -37,6 +39,7 @@ import androidx.compose.ui.text.input.VisualTransformation
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 import com.stripe.android.uicore.R
+import com.stripe.android.uicore.text.autofill
 import kotlinx.coroutines.job
 import kotlinx.coroutines.launch
 import com.stripe.android.core.R as CoreR
@@ -96,7 +99,7 @@ fun PhoneNumberCollectionSection(
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
+@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
 @Composable
 @Suppress("LongMethod")
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -139,6 +142,10 @@ fun PhoneNumberElementUI(
             .fillMaxWidth()
             .bringIntoViewRequester(bringIntoViewRequester)
             .focusRequester(focusRequester)
+            .autofill(
+                types = listOf(AutofillType.PhoneNumberNational),
+                onFill = controller::onValueChange,
+            )
             .onFocusEvent {
                 if (it.isFocused) {
                     coroutineScope.launch { bringIntoViewRequester.bringIntoView() }
diff --git a/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/TextFieldUI.kt b/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/TextFieldUI.kt
index 43f8ab03923..99fe1e5e042 100644
--- a/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/TextFieldUI.kt
+++ b/stripe-ui-core/src/main/java/com/stripe/android/uicore/elements/TextFieldUI.kt
@@ -36,7 +36,6 @@ import androidx.compose.runtime.staticCompositionLocalOf
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.autofill.AutofillNode
 import androidx.compose.ui.focus.FocusDirection
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusProperties
@@ -46,10 +45,6 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.key.KeyEventType
 import androidx.compose.ui.input.key.onPreviewKeyEvent
 import androidx.compose.ui.input.key.type
-import androidx.compose.ui.layout.boundsInWindow
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.LocalAutofill
-import androidx.compose.ui.platform.LocalAutofillTree
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.res.painterResource
@@ -67,6 +62,7 @@ import com.stripe.android.uicore.LocalInstrumentationTest
 import com.stripe.android.uicore.R
 import com.stripe.android.uicore.elements.compat.CompatTextField
 import com.stripe.android.uicore.stripeColors
+import com.stripe.android.uicore.text.autofill
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 
@@ -163,24 +159,6 @@ fun TextField(
 
     val autofillReporter = LocalAutofillEventReporter.current
 
-    val autofillNode = remember {
-        AutofillNode(
-            autofillTypes = listOfNotNull(textFieldController.autofillType),
-            onFill = {
-                textFieldController.autofillType?.let { type ->
-                    autofillReporter(type.name)
-                }
-                textFieldController.onValueChange(it)
-            }
-        )
-    }
-    val autofill = LocalAutofill.current
-    val autofillTree = LocalAutofillTree.current
-
-    LaunchedEffect(autofillNode) {
-        autofillTree += autofillNode
-    }
-
     TextFieldUi(
         value = value,
         loading = loading,
@@ -208,22 +186,20 @@ fun TextField(
                     false
                 }
             }
-            .onGloballyPositioned {
-                autofillNode.boundingBox = it.boundsInWindow()
-            }
+            .autofill(
+                types = listOfNotNull(textFieldController.autofillType),
+                onFill = {
+                    textFieldController.autofillType?.let { type ->
+                        autofillReporter(type.name)
+                    }
+                    textFieldController.onValueChange(it)
+                }
+            )
             .onFocusChanged {
                 if (hasFocus != it.isFocused) {
                     textFieldController.onFocusChange(it.isFocused)
                 }
                 hasFocus = it.isFocused
-
-                if (autofill != null && autofillNode.boundingBox != null) {
-                    if (it.isFocused) {
-                        autofill.requestAutofillForNode(autofillNode)
-                    } else {
-                        autofill.cancelAutofillForNode(autofillNode)
-                    }
-                }
             }
             .focusRequester(focusRequester)
             .semantics {
@@ -303,6 +279,7 @@ internal fun TextFieldUi(
                         is TextFieldIcon.Trailing -> {
                             TrailingIcon(it, loading)
                         }
+
                         is TextFieldIcon.MultiTrailing -> {
                             Row(modifier = Modifier.padding(10.dp)) {
                                 it.staticIcons.forEach {
@@ -311,6 +288,7 @@ internal fun TextFieldUi(
                                 AnimatedIcons(icons = it.animatedIcons, loading = loading)
                             }
                         }
+
                         is TextFieldIcon.Dropdown -> {
                             TrailingDropdown(
                                 icon = it,
diff --git a/stripe-ui-core/src/main/java/com/stripe/android/uicore/image/StripeImage.kt b/stripe-ui-core/src/main/java/com/stripe/android/uicore/image/StripeImage.kt
index 94abf443315..b081d5b51a1 100644
--- a/stripe-ui-core/src/main/java/com/stripe/android/uicore/image/StripeImage.kt
+++ b/stripe-ui-core/src/main/java/com/stripe/android/uicore/image/StripeImage.kt
@@ -1,6 +1,7 @@
 package com.stripe.android.uicore.image
 
 import androidx.annotation.RestrictTo
+import androidx.compose.animation.AnimatedContent
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.BoxWithConstraints
 import androidx.compose.foundation.layout.BoxWithConstraintsScope
@@ -51,7 +52,7 @@ fun StripeImage(
     errorContent: @Composable BoxWithConstraintsScope.() -> Unit = {},
     loadingContent: @Composable BoxWithConstraintsScope.() -> Unit = {}
 ) {
-    BoxWithConstraints(modifier) {
+    BoxWithConstraints {
         val debugMode = LocalInspectionMode.current
         val (width, height) = calculateBoxSize()
         val state: MutableState = remember {
@@ -73,16 +74,21 @@ fun StripeImage(
                     state.value = Error
                 }
         }
-        when (val result = state.value) {
-            Error -> errorContent()
-            Loading -> loadingContent()
-            is Success -> Image(
-                modifier = modifier,
-                colorFilter = colorFilter,
-                contentDescription = contentDescription,
-                contentScale = contentScale,
-                painter = result.painter
-            )
+        AnimatedContent(
+            targetState = state.value,
+            label = "loading_image_animation"
+        ) {
+            when (it) {
+                Error -> errorContent()
+                Loading -> loadingContent()
+                is Success -> Image(
+                    modifier = modifier,
+                    colorFilter = colorFilter,
+                    contentDescription = contentDescription,
+                    contentScale = contentScale,
+                    painter = it.painter
+                )
+            }
         }
     }
 }
diff --git a/stripe-ui-core/src/main/java/com/stripe/android/uicore/text/AutofillModifier.kt b/stripe-ui-core/src/main/java/com/stripe/android/uicore/text/AutofillModifier.kt
new file mode 100644
index 00000000000..0711ef9db13
--- /dev/null
+++ b/stripe-ui-core/src/main/java/com/stripe/android/uicore/text/AutofillModifier.kt
@@ -0,0 +1,50 @@
+package com.stripe.android.uicore.text
+
+import androidx.annotation.RestrictTo
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.autofill.AutofillNode
+import androidx.compose.ui.autofill.AutofillType
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalAutofill
+import androidx.compose.ui.platform.LocalAutofillTree
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun Modifier.autofill(
+    types: List,
+    onFill: (String) -> Unit,
+): Modifier {
+    val currentOnFill by rememberUpdatedState(onFill)
+
+    val autofillNode = remember(types) {
+        AutofillNode(
+            autofillTypes = types,
+            onFill = currentOnFill,
+        )
+    }
+
+    val autofill = LocalAutofill.current
+    LocalAutofillTree.current += autofillNode
+
+    return onGloballyPositioned {
+        autofillNode.boundingBox = it.boundsInWindow()
+    }.onFocusChanged { focusState ->
+        if (autofillNode.boundingBox != null) {
+            autofill?.run {
+                if (focusState.isFocused) {
+                    requestAutofillForNode(autofillNode)
+                } else {
+                    cancelAutofillForNode(autofillNode)
+                }
+            }
+        }
+    }
+}