Skip to content

Commit

Permalink
Link Gate to determine link launch/attest behaviour (#9956)
Browse files Browse the repository at this point in the history
  • Loading branch information
toluo-stripe authored Jan 24, 2025
1 parent 6e58b87 commit 2c1ab02
Show file tree
Hide file tree
Showing 13 changed files with 285 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.stripe.android.paymentsheet.example.playground.settings

import com.stripe.android.core.utils.FeatureFlags

internal object LinkTypeSettingsDefinition :
PlaygroundSettingDefinition<LinkType>,
PlaygroundSettingDefinition.Saveable<LinkType> by EnumSaveable(
key = "LinkType",
values = LinkType.entries.toTypedArray(),
defaultValue = LinkType.Native
),
PlaygroundSettingDefinition.Displayable<LinkType> {
override val displayName: String = "Link Type"

override fun createOptions(
configurationData: PlaygroundConfigurationData
): List<PlaygroundSettingDefinition.Displayable.Option<LinkType>> {
return listOf(
option("Native", LinkType.Native),
option("Native + Attest", LinkType.NativeAttest),
option("Web", LinkType.Web),
)
}

override fun setValue(value: LinkType) {
when (value) {
LinkType.Native -> {
FeatureFlags.nativeLinkEnabled.setEnabled(true)
FeatureFlags.nativeLinkAttestationEnabled.setEnabled(false)
}
LinkType.NativeAttest -> {
FeatureFlags.nativeLinkEnabled.setEnabled(true)
FeatureFlags.nativeLinkAttestationEnabled.setEnabled(true)
}
LinkType.Web -> {
FeatureFlags.nativeLinkEnabled.setEnabled(false)
FeatureFlags.nativeLinkAttestationEnabled.setEnabled(false)
}
}
}
}

enum class LinkType(override val value: String) : ValueEnum {
Native("Native"),
NativeAttest("Native + Attest"),
Web("Web"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ internal class PlaygroundSettings private constructor(
CustomerSettingsDefinition,
CheckoutModeSettingsDefinition,
LinkSettingsDefinition,
FeatureFlagSettingsDefinition(FeatureFlags.nativeLinkEnabled),
LinkTypeSettingsDefinition,
CountrySettingsDefinition,
CurrencySettingsDefinition,
GooglePaySettingsDefinition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@ package com.stripe.android.link
import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContract
import com.stripe.android.core.utils.FeatureFlags
import com.stripe.android.link.gate.LinkGate
import javax.inject.Inject

/**
* Contract used to explicitly launch Link. It will launch either a native or web flow.
*/
internal class LinkActivityContract @Inject internal constructor(
private val nativeLinkActivityContract: NativeLinkActivityContract,
private val webLinkActivityContract: WebLinkActivityContract
private val webLinkActivityContract: WebLinkActivityContract,
private val linkGateFactory: LinkGate.Factory
) : ActivityResultContract<LinkActivityContract.Args, LinkActivityResult>() {

override fun createIntent(context: Context, input: Args): Intent {
return if (FeatureFlags.nativeLinkEnabled.isEnabled) {
val linkGate = linkGateFactory.create(input.configuration)
return if (linkGate.useNativeLink) {
nativeLinkActivityContract.createIntent(context, input)
} else {
webLinkActivityContract.createIntent(context, input)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.stripe.android.link.gate

import com.stripe.android.core.utils.FeatureFlags
import com.stripe.android.link.LinkConfiguration
import javax.inject.Inject

internal class DefaultLinkGate @Inject constructor(
private val configuration: LinkConfiguration
) : LinkGate {
override val useNativeLink: Boolean
get() {
if (configuration.stripeIntent.isLiveMode) {
return useAttestationEndpoints
}
return FeatureFlags.nativeLinkEnabled.isEnabled
}

override val useAttestationEndpoints: Boolean
get() {
if (configuration.stripeIntent.isLiveMode) {
return configuration.useAttestationEndpointsForLink
}
return FeatureFlags.nativeLinkAttestationEnabled.isEnabled
}

class Factory @Inject constructor() : LinkGate.Factory {
override fun create(configuration: LinkConfiguration): LinkGate {
return DefaultLinkGate(configuration)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.stripe.android.link.gate

import com.stripe.android.link.LinkConfiguration

internal interface LinkGate {
val useNativeLink: Boolean
val useAttestationEndpoints: Boolean

fun interface Factory {
fun create(configuration: LinkConfiguration): LinkGate
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import com.stripe.android.core.utils.requireApplication
import com.stripe.android.googlepaylauncher.injection.GooglePayLauncherModule
import com.stripe.android.link.LinkConfigurationCoordinator
import com.stripe.android.link.RealLinkConfigurationCoordinator
import com.stripe.android.link.gate.DefaultLinkGate
import com.stripe.android.link.gate.LinkGate
import com.stripe.android.link.injection.LinkAnalyticsComponent
import com.stripe.android.link.injection.LinkComponent
import com.stripe.android.paymentelement.EmbeddedPaymentElement
Expand Down Expand Up @@ -239,6 +241,9 @@ internal interface SharedPaymentElementViewModelModule {
@Binds
fun bindsLinkConfigurationCoordinator(impl: RealLinkConfigurationCoordinator): LinkConfigurationCoordinator

@Binds
fun bindLinkGateFactory(linkGateFactory: DefaultLinkGate.Factory): LinkGate.Factory

companion object {
@Provides
fun providePrefsRepositoryFactory(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import com.stripe.android.core.utils.RealUserFacingLogger
import com.stripe.android.core.utils.UserFacingLogger
import com.stripe.android.link.LinkConfigurationCoordinator
import com.stripe.android.link.RealLinkConfigurationCoordinator
import com.stripe.android.link.gate.DefaultLinkGate
import com.stripe.android.link.gate.LinkGate
import com.stripe.android.link.injection.LinkAnalyticsComponent
import com.stripe.android.link.injection.LinkComponent
import com.stripe.android.payments.core.analytics.ErrorReporter
Expand Down Expand Up @@ -48,6 +50,7 @@ import javax.inject.Provider
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext

@SuppressWarnings("TooManyFunctions")
@Module(
subcomponents = [
LinkAnalyticsComponent::class,
Expand Down Expand Up @@ -90,6 +93,9 @@ internal abstract class PaymentSheetCommonModule {
@Binds
abstract fun bindsLinkConfigurationCoordinator(impl: RealLinkConfigurationCoordinator): LinkConfigurationCoordinator

@Binds
abstract fun bindLinkGateFactory(linkGateFactory: DefaultLinkGate.Factory): LinkGate.Factory

@Binds
abstract fun bindsCardAccountRangeRepositoryFactory(
defaultCardAccountRangeRepositoryFactory: DefaultCardAccountRangeRepositoryFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import android.content.Intent
import android.net.Uri
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import com.stripe.android.core.utils.FeatureFlags
import com.stripe.android.testing.FeatureFlagTestRule
import org.junit.Rule
import com.stripe.android.link.gate.FakeLinkGate
import com.stripe.android.link.gate.LinkGate
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
Expand All @@ -22,19 +21,17 @@ class LinkActivityContractTest {
private val context: Context = ApplicationProvider.getApplicationContext()
private val args = LinkActivityContract.Args(TestFactory.LINK_CONFIGURATION)

@get:Rule
val featureFlagTestRule = FeatureFlagTestRule(
featureFlag = FeatureFlags.nativeLinkEnabled,
isEnabled = false
)

@Test
fun `creates intent with WebLinkActivityContract when native link disabled`() {
featureFlagTestRule.setEnabled(false)
val linkGate = FakeLinkGate()
linkGate.setUseNativeLink(false)

val (webLinkActivityContract, expectedIntent) = mockWebLinkContract()

val contract = linkActivityContract(webLinkActivityContract = webLinkActivityContract)
val contract = linkActivityContract(
webLinkActivityContract = webLinkActivityContract,
linkGate = linkGate
)

val actualIntent = contract.createIntent(context, args)

Expand All @@ -58,7 +55,8 @@ class LinkActivityContractTest {

@Test
fun `LinkActivityContract creates intent with with NativeLinkActivityContract when native link is enabled`() {
featureFlagTestRule.setEnabled(true)
val linkGate = FakeLinkGate()
linkGate.setUseNativeLink(true)

val (nativeLinkActivityContract, expectedIntent) = mockNativeLinkContract()

Expand Down Expand Up @@ -100,11 +98,15 @@ class LinkActivityContractTest {

private fun linkActivityContract(
webLinkActivityContract: WebLinkActivityContract = mock(),
nativeLinkActivityContract: NativeLinkActivityContract = mock()
nativeLinkActivityContract: NativeLinkActivityContract = mock(),
linkGate: LinkGate = FakeLinkGate()
): LinkActivityContract {
return LinkActivityContract(
nativeLinkActivityContract = nativeLinkActivityContract,
webLinkActivityContract = webLinkActivityContract
webLinkActivityContract = webLinkActivityContract,
linkGateFactory = {
linkGate
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.stripe.android.link.gate

import com.google.common.truth.Truth.assertThat
import com.stripe.android.core.utils.FeatureFlags
import com.stripe.android.link.TestFactory
import com.stripe.android.model.PaymentIntent
import com.stripe.android.model.SetupIntent
import com.stripe.android.testing.FeatureFlagTestRule
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
internal class DefaultLinkGateTest {

@get:Rule
val nativeLinkFeatureFlagTestRule = FeatureFlagTestRule(
featureFlag = FeatureFlags.nativeLinkEnabled,
isEnabled = false
)

@get:Rule
val attestationFeatureFlagTestRule = FeatureFlagTestRule(
featureFlag = FeatureFlags.nativeLinkAttestationEnabled,
isEnabled = false
)

// useNativeLink tests for test mode
@Test
fun `useNativeLink - test mode - returns true when feature flag enabled`() {
nativeLinkFeatureFlagTestRule.setEnabled(true)
val gate = gate(isLiveMode = false)

assertThat(gate.useNativeLink).isTrue()
}

@Test
fun `useNativeLink - test mode - returns false when feature flag disabled`() {
nativeLinkFeatureFlagTestRule.setEnabled(false)
val gate = gate(isLiveMode = false)

assertThat(gate.useNativeLink).isFalse()
}

// useNativeLink tests for live mode
@Test
fun `useNativeLink - live mode - returns true when attestation enabled`() {
val gate = gate(isLiveMode = true, useAttestationEndpoints = true)

assertThat(gate.useNativeLink).isTrue()
}

@Test
fun `useNativeLink - live mode - returns false when attestation disabled`() {
val gate = gate(isLiveMode = true, useAttestationEndpoints = false)

assertThat(gate.useNativeLink).isFalse()
}

// useAttestationEndpoints tests for test mode
@Test
fun `useAttestationEndpoints - test mode - returns true when feature flag enabled`() {
attestationFeatureFlagTestRule.setEnabled(true)
val gate = gate(isLiveMode = false)

assertThat(gate.useAttestationEndpoints).isTrue()
}

@Test
fun `useAttestationEndpoints - test mode - returns false when feature flag disabled`() {
attestationFeatureFlagTestRule.setEnabled(false)
val gate = gate(isLiveMode = false)

assertThat(gate.useAttestationEndpoints).isFalse()
}

// useAttestationEndpoints tests for live mode
@Test
fun `useAttestationEndpoints - live mode - returns true when configuration enabled`() {
val gate = gate(isLiveMode = true, useAttestationEndpoints = true)

assertThat(gate.useAttestationEndpoints).isTrue()
}

@Test
fun `useAttestationEndpoints - live mode - returns false when configuration disabled`() {
val gate = gate(isLiveMode = true, useAttestationEndpoints = false)

assertThat(gate.useAttestationEndpoints).isFalse()
}

// Feature flag independence tests
@Test
fun `useNativeLink - test mode - not affected by attestation feature flag`() {
nativeLinkFeatureFlagTestRule.setEnabled(true)
attestationFeatureFlagTestRule.setEnabled(false)
val gate = gate(isLiveMode = false)

assertThat(gate.useNativeLink).isTrue()
}

@Test
fun `useAttestationEndpoints - test mode - not affected by native link feature flag`() {
attestationFeatureFlagTestRule.setEnabled(true)
nativeLinkFeatureFlagTestRule.setEnabled(false)
val gate = gate(isLiveMode = false)

assertThat(gate.useAttestationEndpoints).isTrue()
}

private fun gate(
isLiveMode: Boolean = true,
useAttestationEndpoints: Boolean = true
): DefaultLinkGate {
val newIntent = when (val intent = TestFactory.LINK_CONFIGURATION.stripeIntent) {
is PaymentIntent -> {
intent.copy(isLiveMode = isLiveMode)
}
is SetupIntent -> {
intent.copy(isLiveMode = isLiveMode)
}
else -> intent
}
return DefaultLinkGate(
configuration = TestFactory.LINK_CONFIGURATION.copy(
useAttestationEndpointsForLink = useAttestationEndpoints,
stripeIntent = newIntent
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.stripe.android.link.gate

internal class FakeLinkGate : LinkGate {
private var _useNativeLink = true
override val useNativeLink: Boolean
get() = _useNativeLink

private var _useAttestationEndpoints = true
override val useAttestationEndpoints: Boolean
get() = _useAttestationEndpoints

fun setUseNativeLink(value: Boolean) {
_useNativeLink = value
}

fun setUseAttestationEndpoints(value: Boolean) {
_useAttestationEndpoints = value
}
}
Loading

0 comments on commit 2c1ab02

Please sign in to comment.