Skip to content

[LiPS] Add bank account feature flag + base menu #10762

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ internal class PlaygroundSettings private constructor(
FeatureFlagSettingsDefinition(FeatureFlags.instantDebitsIncentives),
FeatureFlagSettingsDefinition(FeatureFlags.financialConnectionsFullSdkUnavailable),
FeatureFlagSettingsDefinition(FeatureFlags.enableCardEditInLinkNative),
FeatureFlagSettingsDefinition(FeatureFlags.addBankAccountInLinkNative),
EmbeddedViewDisplaysMandateSettingDefinition,
EmbeddedFormSheetActionSettingDefinition,
EmbeddedTwoStepSettingsDefinition,
Expand Down
3 changes: 3 additions & 0 deletions paymentsheet/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@
<ID>LongMethod:EmbeddedContentHelper.kt$DefaultEmbeddedContentHelper$private fun createInteractor( coroutineScope: CoroutineScope, paymentMethodMetadata: PaymentMethodMetadata, walletsState: StateFlow&lt;WalletsState?>, ): PaymentMethodVerticalLayoutInteractor</ID>
<ID>LongMethod:FormViewModelTest.kt$FormViewModelTest$@Test fun `Verify params are set when element address fields are complete`()</ID>
<ID>LongMethod:FormViewModelTest.kt$FormViewModelTest$@Test fun `Verify params are set when required address fields are complete`()</ID>
<ID>LongMethod:FullScreenContent.kt$@OptIn(ExperimentalMaterialApi::class) @Composable internal fun FullScreenContent( modifier: Modifier, bottomSheetState: StripeBottomSheetState, initialDestination: LinkScreen, appBarState: LinkAppBarState, eventReporter: EventReporter, onBackPressed: () -> Unit, moveToWeb: () -> Unit, goBack: () -> Unit, onNavBackStackEntryChanged: (NavBackStackEntryUpdate) -> Unit, navigationChannel: SharedFlow&lt;NavigationIntent>, handleViewAction: (LinkAction) -> Unit, navigate: (route: LinkScreen, clearStack: Boolean) -> Unit, dismiss: () -> Unit, dismissWithResult: (LinkActivityResult) -> Unit, getLinkAccount: () -> LinkAccount?, changeEmail: () -> Unit )</ID>
<ID>LongMethod:LinkInlineSignupFields.kt$@Composable internal fun LinkInlineSignupFields( sectionError: Int?, emailController: TextFieldController, phoneNumberController: PhoneNumberController, nameController: TextFieldController, signUpState: SignUpState, enabled: Boolean, isShowingPhoneFirst: Boolean, requiresNameCollection: Boolean, errorMessage: String?, didShowAllFields: Boolean, onShowingAllFields: () -> Unit, modifier: Modifier = Modifier, emailFocusRequester: FocusRequester = remember { FocusRequester() }, phoneFocusRequester: FocusRequester = remember { FocusRequester() }, nameFocusRequester: FocusRequester = remember { FocusRequester() }, )</ID>
<ID>LongMethod:PaymentMethodVerticalLayoutInteractor.kt$DefaultPaymentMethodVerticalLayoutInteractor.Companion$fun create( viewModel: BaseSheetViewModel, paymentMethodMetadata: PaymentMethodMetadata, customerStateHolder: CustomerStateHolder, bankFormInteractor: BankFormInteractor, ): PaymentMethodVerticalLayoutInteractor</ID>
<ID>LongMethod:PaymentSheetConfigurationKtx.kt$internal fun PaymentSheet.Appearance.parseAppearance()</ID>
<ID>LongMethod:PaymentSheetScreen.kt$@Composable private fun PaymentSheetContent( viewModel: BaseSheetViewModel, headerText: ResolvableString?, walletsState: WalletsState?, walletsProcessingState: WalletsProcessingState?, error: ResolvableString?, currentScreen: PaymentSheetScreen, mandateText: MandateText?, modifier: Modifier )</ID>
<ID>LongMethod:PaymentSheetViewModelTest.kt$PaymentSheetViewModelTest$@Test fun `Can complete payment after switching to another LPM from card selection with inline Link signup state`()</ID>
<ID>LongMethod:PlaceholderHelperTest.kt$PlaceholderHelperTest$@Test fun `Test correct placeholder is removed for placeholder spec`()</ID>
<ID>LongMethod:PrimaryButton.kt$@Composable internal fun PrimaryButton( modifier: Modifier = Modifier, label: String, state: PrimaryButtonState, onButtonClick: () -> Unit, @DrawableRes iconStart: Int? = null, @DrawableRes iconEnd: Int? = null )</ID>
<ID>LongMethod:SignUpScreen.kt$@Composable internal fun SignUpBody( emailController: TextFieldController, phoneNumberController: PhoneNumberController, nameController: TextFieldController, signUpScreenState: SignUpScreenState, onSignUpClick: () -> Unit )</ID>
<ID>LongMethod:USBankAccountForm.kt$@Composable private fun BillingDetailsForm( instantDebits: Boolean, formArgs: FormArguments, enabled: Boolean, isPaymentFlow: Boolean, nameController: TextFieldController, emailController: TextFieldController, phoneController: PhoneNumberController, addressController: AddressController, lastTextFieldIdentifier: IdentifierSpec?, sameAsShippingElement: SameAsShippingElement?, )</ID>
<ID>LongMethod:USBankAccountFormViewModel.kt$USBankAccountFormViewModel$private fun createNewPaymentSelection( resultIdentifier: ResultIdentifier, last4: String?, bankName: String?, billingDetails: PaymentMethod.BillingDetails, ): PaymentSelection.New.USBankAccount</ID>
Expand All @@ -50,6 +52,7 @@
<ID>MagicNumber:USBankAccountForm.kt$0.5f</ID>
<ID>MatchingDeclarationName:ErrorText.kt$ErrorTextStyle</ID>
<ID>MatchingDeclarationName:LinkTerms.kt$LinkTermsType</ID>
<ID>MatchingDeclarationName:Type.kt$LinkTypography</ID>
<ID>MaxLineLength:CardDefinition.kt$internal</ID>
<ID>MaxLineLength:CustomerRepositoryTest.kt$CustomerRepositoryTest$fun</ID>
<ID>MaxLineLength:CustomerSheetViewModelTest.kt$CustomerSheetViewModelTest$fun</ID>
Expand Down
14 changes: 14 additions & 0 deletions paymentsheet/res/drawable/stripe_ic_next_card.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M0,4.375C0,3.339 0.839,2.5 1.875,2.5H18.125C19.16,2.5 20,3.339 20,4.375V7.183L20,7.188L20,7.192V15.625C20,16.66 19.16,17.5 18.125,17.5H1.875C0.839,17.5 0,16.66 0,15.625V4.375ZM1.875,4.375H18.125V6.25H1.875V4.375ZM18.125,8.125V15.625H1.875V8.125H18.125Z"
android:fillColor="#474E5A"
android:fillType="evenOdd"/>
<path
android:pathData="M8.75,12.813C8.75,12.295 9.17,11.875 9.688,11.875H15.313C15.83,11.875 16.25,12.295 16.25,12.813C16.25,13.33 15.83,13.75 15.313,13.75H9.688C9.17,13.75 8.75,13.33 8.75,12.813Z"
android:fillColor="#474E5A"
android:fillType="evenOdd"/>
</vector>
4 changes: 4 additions & 0 deletions paymentsheet/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -266,4 +266,8 @@
<string name="stripe_wallet_update_card">Update card</string>
<!-- A text notice shown when the user selects an expired card. -->
<string name="stripe_wallet_update_expired_card_error">This card has expired. Update your card info or choose a different payment method.</string>
<!-- Menu option in the Link wallet screen to add a new card -->
<string name="stripe_link_add_debit_or_credit_card">Debit or credit card</string>
<!-- Menu option in the Link wallet screen to add a new bank account -->
<string name="stripe_link_add_bank">Bank</string>
</resources>
6 changes: 0 additions & 6 deletions paymentsheet/res/values/themes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,4 @@
<item name="colorPrimary">@color/stripe_paymentsheet_form</item>
<item name="colorAccent">@color/stripe_paymentsheet_form</item>
</style>

<style name="StripeLinkBaseTheme" parent="@android:style/Theme.Translucent">
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="windowNoTitle">true</item>
</style>
</resources>
6 changes: 2 additions & 4 deletions paymentsheet/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,10 @@

<activity
android:name="com.stripe.android.link.LinkActivity"
android:theme="@style/StripeLinkBaseTheme"
android:theme="@style/StripePaymentSheetDefaultTheme"
android:exported="false"
android:label="@string/stripe_link"
android:windowSoftInputMode="adjustResize"
android:autoRemoveFromRecents="true"
android:configChanges="orientation|keyboard|keyboardHidden|screenLayout|screenSize|smallestScreenSize" />
android:autoRemoveFromRecents="true" />

<activity
android:name="com.stripe.android.link.LinkForegroundActivity"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.stripe.android.link

import javax.inject.Inject

internal interface DismissalCoordinator {
val canDismiss: Boolean
fun setDismissible(dismissible: Boolean)
}

internal inline fun <R> DismissalCoordinator.withDismissalDisabled(
action: () -> R,
): R {
val originalDismissible = canDismiss
setDismissible(false)
try {
return action()
} finally {
setDismissible(originalDismissible)
}
}

internal class RealDismissalCoordinator @Inject constructor() : DismissalCoordinator {

private var _canDismiss: Boolean = true

override val canDismiss: Boolean
get() = _canDismiss

override fun setDismissible(dismissible: Boolean) {
_canDismiss = dismissible
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.stripe.android.link

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
Expand All @@ -9,11 +8,15 @@ import androidx.activity.addCallback
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.LaunchedEffect
import androidx.core.os.bundleOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModelProvider
import com.stripe.android.core.Logger
import com.stripe.android.paymentsheet.BuildConfig
import com.stripe.android.paymentsheet.utils.renderEdgeToEdge
import com.stripe.android.uicore.elements.bottomsheet.rememberStripeBottomSheetState
import com.stripe.android.uicore.utils.fadeOut

internal class LinkActivity : ComponentActivity() {
@VisibleForTesting
Expand All @@ -25,12 +28,13 @@ internal class LinkActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
renderEdgeToEdge()

try {
viewModel = ViewModelProvider(this, viewModelFactory)[LinkActivityViewModel::class.java]
} catch (e: NoArgsException) {
Logger.getInstance(BuildConfig.DEBUG).error("Failed to create LinkActivityViewModel", e)
setResult(Activity.RESULT_CANCELED)
setResult(RESULT_CANCELED)
finish()
}

Expand All @@ -41,18 +45,28 @@ internal class LinkActivity : ComponentActivity() {
)

webLauncher = registerForActivityResult(vm.activityRetainedComponent.webLinkActivityContract) { result ->
dismissWithResult(result)
vm.handleResult(result)
}

vm.launchWebFlow = ::launchWebFlow
vm.dismissWithResult = ::dismissWithResult
lifecycle.addObserver(vm)
observeBackPress()

setContent {
val bottomSheetState = rememberStripeBottomSheetState(
confirmValueChange = { vm.canDismissSheet },
)

LaunchedEffect(Unit) {
vm.result.collect { result ->
bottomSheetState.hide()
dismissWithResult(result)
}
}

LinkScreenContent(
viewModel = vm,
onBackPressed = onBackPressedDispatcher::onBackPressed
bottomSheetState = bottomSheetState,
)
}
}
Expand All @@ -77,6 +91,11 @@ internal class LinkActivity : ComponentActivity() {
viewModel?.unregisterActivity()
}

override fun finish() {
super.finish()
fadeOut()
}

fun launchWebFlow(configuration: LinkConfiguration) {
webLauncher?.launch(
LinkActivityContract.Args(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ import com.stripe.android.uicore.navigation.PopUpToBehavior
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
Expand All @@ -59,6 +62,9 @@ internal class LinkActivityViewModel @Inject constructor(
private val _linkAppBarState = MutableStateFlow(LinkAppBarState.initial())
val linkAppBarState: StateFlow<LinkAppBarState> = _linkAppBarState.asStateFlow()

private val _result = MutableSharedFlow<LinkActivityResult>(extraBufferCapacity = 1)
val result: SharedFlow<LinkActivityResult> = _result.asSharedFlow()

val navigationFlow = navigationManager.navigationFlow

private val _linkScreenState = MutableStateFlow<ScreenState>(ScreenState.Loading)
Expand All @@ -67,9 +73,11 @@ internal class LinkActivityViewModel @Inject constructor(
val linkAccount: LinkAccount?
get() = linkAccountManager.linkAccount.value

var dismissWithResult: ((LinkActivityResult) -> Unit)? = null
var launchWebFlow: ((LinkConfiguration) -> Unit)? = null

val canDismissSheet: Boolean
get() = activityRetainedComponent.dismissalCoordinator.canDismiss

fun handleViewAction(action: LinkAction) {
when (action) {
LinkAction.BackPressed -> handleBackPressed()
Expand All @@ -84,19 +92,34 @@ internal class LinkActivityViewModel @Inject constructor(
}

fun onDismissVerificationClicked() {
dismissWithResult?.invoke(
dismissWithResult(
LinkActivityResult.Canceled(
linkAccountUpdate = linkAccountManager.linkAccountUpdate
)
)
}

fun handleResult(result: LinkActivityResult) {
dismissWithResult(result)
}

fun dismissSheet() {
if (activityRetainedComponent.dismissalCoordinator.canDismiss) {
dismissWithResult(
LinkActivityResult.Canceled(
linkAccountUpdate = linkAccountManager.linkAccountUpdate
)
)
}
}

@OptIn(DelicateCoroutinesApi::class)
private fun handleLogoutClicked() {
GlobalScope.launch {
linkAccountManager.logOut()
}
dismissWithResult?.invoke(

dismissWithResult(
LinkActivityResult.Canceled(
reason = LinkActivityResult.Canceled.Reason.LoggedOut,
linkAccountUpdate = LinkAccountUpdate.Value(null)
Expand Down Expand Up @@ -128,7 +151,7 @@ internal class LinkActivityViewModel @Inject constructor(
* to the container activity. [onBackPressed] will be triggered on these empty backstack cases.
*/
fun handleBackPressed() {
dismissWithResult?.invoke(
dismissWithResult(
LinkActivityResult.Canceled(
linkAccountUpdate = linkAccountManager.linkAccountUpdate
)
Expand All @@ -152,7 +175,9 @@ internal class LinkActivityViewModel @Inject constructor(
}

fun goBack() {
navigationManager.tryNavigateBack()
if (activityRetainedComponent.dismissalCoordinator.canDismiss) {
navigationManager.tryNavigateBack()
}
}

fun changeEmail() {
Expand All @@ -161,7 +186,6 @@ internal class LinkActivityViewModel @Inject constructor(
}

fun unregisterActivity() {
dismissWithResult = null
launchWebFlow = null
}

Expand Down Expand Up @@ -238,6 +262,12 @@ internal class LinkActivityViewModel @Inject constructor(
updateScreenState()
}

private fun dismissWithResult(result: LinkActivityResult) {
viewModelScope.launch {
_result.emit(result)
}
}

companion object {
fun factory(savedStateHandle: SavedStateHandle? = null): ViewModelProvider.Factory = viewModelFactory {
initializer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.stripe.android.link.ui.FullScreenContent
import com.stripe.android.link.ui.LinkAppBarState
import com.stripe.android.link.ui.verification.VerificationDialog
import com.stripe.android.paymentsheet.analytics.EventReporter
import com.stripe.android.uicore.elements.bottomsheet.StripeBottomSheetState
import com.stripe.android.uicore.navigation.NavBackStackEntryUpdate
import com.stripe.android.uicore.navigation.NavigationIntent
import com.stripe.android.uicore.utils.collectAsState
Expand All @@ -17,22 +18,22 @@ import kotlinx.coroutines.flow.SharedFlow
@Composable
internal fun LinkScreenContent(
viewModel: LinkActivityViewModel,
onBackPressed: () -> Unit
bottomSheetState: StripeBottomSheetState,
) {
val screenState by viewModel.linkScreenState.collectAsState()
val appBarState by viewModel.linkAppBarState.collectAsState()

LinkScreenContentBody(
bottomSheetState = bottomSheetState,
screenState = screenState,
appBarState = appBarState,
eventReporter = viewModel.eventReporter,
onVerificationSucceeded = viewModel::onVerificationSucceeded,
onDismissClicked = viewModel::onDismissVerificationClicked,
onBackPressed = onBackPressed,
onBackPressed = viewModel::goBack,
navigate = viewModel::navigate,
dismissWithResult = { result ->
viewModel.dismissWithResult?.invoke(result)
},
dismiss = viewModel::dismissSheet,
dismissWithResult = viewModel::handleResult,
getLinkAccount = {
viewModel.linkAccount
},
Expand All @@ -47,6 +48,7 @@ internal fun LinkScreenContent(

@Composable
internal fun LinkScreenContentBody(
bottomSheetState: StripeBottomSheetState,
screenState: ScreenState,
appBarState: LinkAppBarState,
eventReporter: EventReporter,
Expand All @@ -56,7 +58,8 @@ internal fun LinkScreenContentBody(
onDismissClicked: () -> Unit,
onBackPressed: () -> Unit,
navigate: (route: LinkScreen, clearStack: Boolean) -> Unit,
dismissWithResult: ((LinkActivityResult) -> Unit)?,
dismiss: () -> Unit,
dismissWithResult: (LinkActivityResult) -> Unit,
getLinkAccount: () -> LinkAccount?,
handleViewAction: (LinkAction) -> Unit,
moveToWeb: () -> Unit,
Expand All @@ -68,10 +71,12 @@ internal fun LinkScreenContentBody(
FullScreenContent(
modifier = Modifier
.testTag(FULL_SCREEN_CONTENT_TAG),
bottomSheetState = bottomSheetState,
initialDestination = screenState.initialDestination,
onBackPressed = onBackPressed,
appBarState = appBarState,
navigate = navigate,
dismiss = dismiss,
dismissWithResult = dismissWithResult,
getLinkAccount = getLinkAccount,
moveToWeb = moveToWeb,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.stripe.android.common.di.ApplicationIdModule
import com.stripe.android.core.Logger
import com.stripe.android.core.injection.PUBLISHABLE_KEY
import com.stripe.android.core.injection.STRIPE_ACCOUNT_ID
import com.stripe.android.link.DismissalCoordinator
import com.stripe.android.link.LinkActivityViewModel
import com.stripe.android.link.LinkConfiguration
import com.stripe.android.link.WebLinkActivityContract
Expand Down Expand Up @@ -58,6 +59,7 @@ internal interface NativeLinkComponent {
val viewModel: LinkActivityViewModel
val eventReporter: EventReporter
val navigationManager: NavigationManager
val dismissalCoordinator: DismissalCoordinator

@Component.Builder
interface Builder {
Expand Down
Loading
Loading