Skip to content
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

Tolu/link/attestation gate playground #9938

Closed
wants to merge 14 commits into from
4 changes: 1 addition & 3 deletions .idea/codestyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## XX.XX.XX - 20XX-XX-XX

### Financial Connections
- [FIXED] Fixes a rare crash that could occur when presenting a bottom sheet.

## 21.3.1 - 2025-01-06

### PaymentSheet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.stripe.android.financialconnections.features.partnerauth.PartnerAuthS
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.navigation.bottomsheet.LifecycleAwareContent
import com.stripe.android.financialconnections.navigation.bottomsheet.bottomSheet
import com.stripe.android.financialconnections.presentation.parentViewModel

Expand Down Expand Up @@ -283,6 +284,10 @@ internal fun NavGraphBuilder.bottomSheet(
route = destination.fullRoute,
arguments = destination.arguments,
deepLinks = deepLinks,
content = { destination.Composable(navBackStackEntry = it) }
content = { navBackStackEntry ->
LifecycleAwareContent(navBackStackEntry) {
destination.Composable(navBackStackEntry = navBackStackEntry)
}
}
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.stripe.android.financialconnections.navigation.bottomsheet

import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner

/**
* Prevents the content from being rendered until the lifecycle is ready.
*
* Addresses: You cannot access the NavBackStackEntry's ViewModels after the NavBackStackEntry is destroyed.
* https://github.com/google/accompanist/issues/1487#top
*/
@Composable
internal fun LifecycleAwareContent(
lifecycleOwner: LifecycleOwner,
content: @Composable () -> Unit
) {
val isReady = remember { mutableStateOf(false) }

DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_CREATE -> isReady.value = true
Lifecycle.Event.ON_DESTROY -> isReady.value = false
else -> Unit
}
}
lifecycleOwner.lifecycle.addObserver(observer)

onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}

// This will typically be used on bottom sheets, where we see the NavBackStackEntry issue.
// animating the content height will ensure we respect the bottom sheet open animation.
Box(
modifier = Modifier
.fillMaxWidth()
.animateContentSize(animationSpec = tween())
) {
if (isReady.value) { content() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ data class ElementsSession(
val cardBrandChoice: CardBrandChoice?,
val isGooglePayEnabled: Boolean,
val sessionsError: Throwable? = null,
val sessionId: String?,
) : StripeModel {

val linkPassthroughModeEnabled: Boolean
Expand All @@ -37,6 +38,9 @@ data class ElementsSession(
return (allowsLink && hasValidFundingSource) || linkPassthroughModeEnabled
}

val useAttestationEndpointsForLink: Boolean
get() = linkSettings?.useAttestationEndpoints ?: false

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Parcelize
data class LinkSettings(
Expand All @@ -46,6 +50,7 @@ data class ElementsSession(
val linkFlags: Map<String, Boolean>,
val disableLinkSignup: Boolean,
val linkConsumerIncentive: LinkConsumerIncentive?,
val useAttestationEndpoints: Boolean
) : StripeModel

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
Expand Down Expand Up @@ -127,6 +132,7 @@ data class ElementsSession(
cardBrandChoice = null,
isGooglePayEnabled = true,
sessionsError = sessionsError,
sessionId = null
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ internal class ElementsSessionJsonParser(
cardBrandChoice = cardBrandChoice,
isGooglePayEnabled = googlePayPreference != "disabled",
externalPaymentMethodData = externalPaymentMethodData,
sessionId = elementsSessionId
)
} else {
null
Expand Down Expand Up @@ -140,6 +141,7 @@ internal class ElementsSessionJsonParser(
): ElementsSession.LinkSettings {
val disableLinkSignup = json?.optBoolean(FIELD_DISABLE_LINK_SIGNUP) ?: false
val linkPassthroughModeEnabled = json?.optBoolean(FIELD_LINK_PASSTHROUGH_MODE_ENABLED) ?: false
val useLinkAttestationEndpoints = json?.optBoolean(FIELD_USE_LINK_ATTESTATION_ENDPOINTS) ?: false

val linkMode = json?.optString(FIELD_LINK_MODE)?.let { mode ->
LinkMode.entries.firstOrNull { it.value == mode }
Expand All @@ -162,6 +164,7 @@ internal class ElementsSessionJsonParser(
linkFlags = linkFlags,
disableLinkSignup = disableLinkSignup,
linkConsumerIncentive = linkConsumerIncentive,
useAttestationEndpoints = useLinkAttestationEndpoints
)
}

Expand Down Expand Up @@ -327,6 +330,7 @@ internal class ElementsSessionJsonParser(
private const val FIELD_LINK_PASSTHROUGH_MODE_ENABLED = "link_passthrough_mode_enabled"
private const val FIELD_LINK_MODE = "link_mode"
private const val FIELD_DISABLE_LINK_SIGNUP = "link_mobile_disable_signup"
private const val FIELD_USE_LINK_ATTESTATION_ENDPOINTS = "link_mobile_use_attestation_endpoints"
private const val FIELD_MERCHANT_COUNTRY = "merchant_country"
private const val FIELD_PAYMENT_METHOD_PREFERENCE = "payment_method_preference"
private const val FIELD_UNACTIVATED_PAYMENT_METHOD_TYPES = "unactivated_payment_method_types"
Expand Down
1 change: 1 addition & 0 deletions paymentsheet/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies {
implementation project(':payments-ui-core')
implementation project(':stripe-ui-core')
compileOnly project(':financial-connections')
implementation project(":stripe-attestation")

// Kotlin
implementation libs.kotlin.coroutines
Expand Down
6 changes: 6 additions & 0 deletions paymentsheet/res/values/themes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,10 @@
<item name="android:windowBackground">@color/stripe_link_window_background</item>
<item name="windowNoTitle">true</item>
</style>

<style name="StripeLinkTranslucentTheme" parent="@android:style/Theme.Translucent">
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="windowNoTitle">true</item>
</style>
</resources>
62 changes: 34 additions & 28 deletions paymentsheet/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,76 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >

<application>
<activity
android:name=".PaymentSheetActivity"
android:theme="@style/StripePaymentSheetDefaultTheme"
android:exported="false" />
android:exported="false"
android:theme="@style/StripePaymentSheetDefaultTheme" />
<activity
android:name=".PaymentOptionsActivity"
android:theme="@style/StripePaymentSheetDefaultTheme"
android:exported="false" />
android:exported="false"
android:theme="@style/StripePaymentSheetDefaultTheme" />
<activity
android:name="com.stripe.android.customersheet.CustomerSheetActivity"
android:theme="@style/StripePaymentSheetDefaultTheme"
android:exported="false" />
android:exported="false"
android:theme="@style/StripePaymentSheetDefaultTheme" />
<activity
android:name=".addresselement.AddressElementActivity"
android:theme="@style/StripePaymentSheetDefaultTheme"
android:exported="false" />
android:exported="false"
android:theme="@style/StripePaymentSheetDefaultTheme" />
<activity
android:name=".paymentdatacollection.bacs.BacsMandateConfirmationActivity"
android:theme="@style/StripePaymentSheetDefaultTheme"
android:exported="false" />
android:exported="false"
android:theme="@style/StripePaymentSheetDefaultTheme" />
<activity
android:name=".paymentdatacollection.polling.PollingActivity"
android:theme="@style/StripePaymentSheetDefaultTheme"
android:exported="false" />
android:exported="false"
android:theme="@style/StripePaymentSheetDefaultTheme" />
<activity
android:name=".ui.SepaMandateActivity"
android:theme="@style/StripePaymentSheetDefaultTheme"
android:exported="false" />
android:exported="false"
android:theme="@style/StripePaymentSheetDefaultTheme" />
<activity
android:name=".ExternalPaymentMethodProxyActivity"
android:theme="@style/StripePayLauncherDefaultTheme"
android:exported="false" />
android:exported="false"
android:theme="@style/StripePayLauncherDefaultTheme" />
<activity
android:name=".paymentdatacollection.cvcrecollection.CvcRecollectionActivity"
android:theme="@style/StripePaymentSheetDefaultTheme" />

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

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

<activity
android:name="com.stripe.android.link.LinkRedirectHandlerActivity"
android:theme="@style/StripeTransparentTheme"
android:autoRemoveFromRecents="true"
android:exported="true"
android:launchMode="singleInstance"
android:exported="true">
android:theme="@style/StripeTransparentTheme" >
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:scheme="link-popup"
android:host="complete"
android:path="/${applicationId}" />
android:path="/${applicationId}"
android:scheme="link-popup" />
</intent-filter>
</activity>
</application>
</manifest>

</manifest>
49 changes: 49 additions & 0 deletions paymentsheet/src/main/java/com/stripe/android/link/EagerPath.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.stripe.android.link

import androidx.compose.material.AlertDialog
import androidx.compose.material.Card
import androidx.compose.runtime.Composable
import androidx.compose.ui.window.Dialog
import com.stripe.android.link.model.LinkAccount
import com.stripe.android.link.theme.DefaultLinkTheme
import com.stripe.android.link.ui.verification.VerificationScreen
import com.stripe.android.link.ui.verification.VerificationViewModel

@Composable
internal fun EagerPath(
linkAccount: LinkAccount?,
viewModel: LinkActivityViewModel,
) {
VerificationDialog(
linkAccount = linkAccount,
navigateAndClearStack = { screen ->
viewModel.navigate(screen, clearStack = true)
},
goBack = {
viewModel.goBack()
}
)
}

@Composable
private fun VerificationDialog(
linkAccount: LinkAccount?,
navigateAndClearStack: (LinkScreen) -> Unit,
goBack: () -> Unit
) {
val viewModel: VerificationViewModel = linkViewModel { parentComponent ->
VerificationViewModel.factory(
parentComponent = parentComponent,
goBack = goBack,
navigateAndClearStack = navigateAndClearStack,
linkAccount = linkAccount
)
}
Dialog (onDismissRequest = {}) {
DefaultLinkTheme {
Card {
VerificationScreen(viewModel)
}
}
}
}
Loading
Loading