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

Link Gate to determine link launch/attest behaviour #9956

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
@@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the advantage of keeping track of useNativeLink separately from useAttestationEndpoints if useNativeLink's value is equal to useAttestationEndpoints value?

ie is "native" link as opposed to "native + attest" going to exist in production? And if not, do we need to test it now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Native link without attestation will be the default in test-mode (there's no need to attest in this scenario). Since useAttestationEndpoints could be false in test mode, we'll use useNativeLink for the LinkActivityContract. This PR doesn't reflect the test mode logic. I'll push an update.

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
Loading