Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 @@ -101,17 +101,17 @@ internal class OnrampActivity : ComponentActivity() {

FeatureFlags.nativeLinkEnabled.setEnabled(true)

val callbacks = OnrampCallbacks(
authenticateUserCallback = viewModel::onAuthenticateUserResult,
verifyIdentityCallback = viewModel::onVerifyIdentityResult,
verifyKycCallback = viewModel::onVerifyKycResult,
checkoutCallback = viewModel::onCheckoutResult,
collectPaymentCallback = viewModel::onCollectPaymentResult,
authorizeCallback = viewModel::onAuthorizeResult
)
val callbacksState = OnrampCallbacks()
.authenticateUserCallback(callback = viewModel::onAuthenticateUserResult)
Copy link
Collaborator

Choose a reason for hiding this comment

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

(not for this PR) Given the view model is the one that owns the coordinator, and is the implementor of all of these callbacks, I think it does shine a light on the fact that these should likely just move to the coordinator constructor.

.verifyIdentityCallback(callback = viewModel::onVerifyIdentityResult)
.verifyKycCallback(callback = viewModel::onVerifyKycResult)
.checkoutCallback(callback = viewModel::onCheckoutResult)
.collectPaymentCallback(callback = viewModel::onCollectPaymentResult)
.authorizeCallback(callback = viewModel::onAuthorizeResult)
.build()

onrampPresenter = viewModel.onrampCoordinator
.createPresenter(this, callbacks)
.createPresenter(this, callbacksState)

// ViewModel notifies UI to launch checkout flow.
// Note checkout requires an Activity context since it might launch UI to handle next actions (e.g. 3DS2).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,26 +104,28 @@ internal class OnrampViewModel(
init {
viewModelScope.launch {
@Suppress("MagicNumber", "MaxLineLength")
val configuration = OnrampConfiguration(
merchantDisplayName = "Onramp Example",
publishableKey = "pk_test_51K9W3OHMaDsveWq0oLP0ZjldetyfHIqyJcz27k2BpMGHxu9v9Cei2tofzoHncPyk3A49jMkFEgTOBQyAMTUffRLa00xzzARtZO",
appearance = LinkAppearance(
lightColors = LinkAppearance.Colors(
primary = Color.Blue,
contentOnPrimary = Color.White,
borderSelected = Color.Red
),
darkColors = LinkAppearance.Colors(
primary = Color(0xFF9886E6),
contentOnPrimary = Color(0xFF222222),
borderSelected = Color.White
),
style = LinkAppearance.Style.ALWAYS_DARK,
primaryButton = LinkAppearance.PrimaryButton()
val configurationState = OnrampConfiguration()
.merchantDisplayName(merchantDisplayName = "Onramp Example")
.publishableKey(publishableKey = "pk_test_51K9W3OHMaDsveWq0oLP0ZjldetyfHIqyJcz27k2BpMGHxu9v9Cei2tofzoHncPyk3A49jMkFEgTOBQyAMTUffRLa00xzzARtZO")
.appearance(
appearance = LinkAppearance(
lightColors = LinkAppearance.Colors(
primary = Color.Blue,
contentOnPrimary = Color.White,
borderSelected = Color.Red
),
darkColors = LinkAppearance.Colors(
primary = Color(0xFF9886E6),
contentOnPrimary = Color(0xFF222222),
borderSelected = Color.White
),
style = LinkAppearance.Style.ALWAYS_DARK,
primaryButton = LinkAppearance.PrimaryButton()
)
)
)
.build()

onrampCoordinator.configure(configuration = configuration)
onrampCoordinator.configure(configurationState = configurationState)

loadUserData()?.let { data ->
_uiState.update { it.copy(email = data.email, authToken = data.token, screen = Screen.SeamlessSignIn) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ class OnrampCoordinator @Inject internal constructor(
/**
* Initialize the coordinator with the provided configuration.
*
* @param configuration The OnrampConfiguration to apply.
* @param configurationState The OnrampConfiguration to apply.
*/
suspend fun configure(
configuration: OnrampConfiguration,
configurationState: OnrampConfiguration.State,
): OnrampConfigurationResult {
return interactor.configure(configuration)
return interactor.configure(configurationState)
}

/**
Expand Down Expand Up @@ -141,7 +141,7 @@ class OnrampCoordinator @Inject internal constructor(
*/
fun createPresenter(
activity: ComponentActivity,
onrampCallbacks: OnrampCallbacks
onrampCallbacks: OnrampCallbacks.State
): Presenter {
return presenterComponentFactory
.build(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,22 @@ internal class OnrampInteractor @Inject constructor(

private var analyticsService: OnrampAnalyticsService? = null

suspend fun configure(configuration: OnrampConfiguration): OnrampConfigurationResult {
suspend fun configure(configurationState: OnrampConfiguration.State): OnrampConfigurationResult {
_state.value = OnrampState(
configuration = configuration,
cryptoCustomerId = configuration.cryptoCustomerId,
configurationState = configurationState,
cryptoCustomerId = configurationState.cryptoCustomerId,
)

// We are *not* calling `PaymentConfiguration.init()` here because we're relying on
// `LinkController.configure()` to do it.
val linkResult: ConfigureResult = linkController.configure(
LinkController.Configuration.Builder(
merchantDisplayName = configuration.merchantDisplayName,
publishableKey = configuration.publishableKey,
merchantDisplayName = configurationState.merchantDisplayName,
publishableKey = configurationState.publishableKey,
)
.allowLogOut(false)
.allowUserEmailEdits(false)
.appearance(configuration.appearance)
.appearance(configurationState.appearance)
.build()
)

Expand Down Expand Up @@ -371,7 +371,7 @@ internal class OnrampInteractor @Inject constructor(

OnrampStartKycVerificationResult.Completed(
response = kycInfo,
appearance = state.value.configuration?.appearance
appearance = state.value.configurationState?.appearance
)
},
onFailure = { error ->
Expand Down Expand Up @@ -900,7 +900,7 @@ internal class OnrampInteractor @Inject constructor(
}

internal data class OnrampState(
val configuration: OnrampConfiguration? = null,
val configurationState: OnrampConfiguration.State? = null,
val linkControllerState: LinkController.State? = null,
val cryptoCustomerId: String? = null,
val collectingPaymentMethodType: PaymentMethodType? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ internal class OnrampPresenterCoordinator @Inject constructor(
linkController: LinkController,
lifecycleOwner: LifecycleOwner,
private val activity: ComponentActivity,
private val onrampCallbacks: OnrampCallbacks,
private val onrampCallbacks: OnrampCallbacks.State,
private val coroutineScope: CoroutineScope,
) {
private val linkControllerState = linkController.state(activity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ internal interface OnrampPresenterComponent {
@BindsInstance activity: ComponentActivity,
@BindsInstance lifecycleOwner: LifecycleOwner,
@BindsInstance activityResultRegistryOwner: ActivityResultRegistryOwner,
@BindsInstance onrampCallbacks: OnrampCallbacks,
@BindsInstance onrampCallbacks: OnrampCallbacks.State,
): OnrampPresenterComponent
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.stripe.android.crypto.onramp.model

import androidx.annotation.RestrictTo
import dev.drewhamilton.poko.Poko

/**
* Container for all callbacks required by the Onramp coordinator.
Expand All @@ -12,37 +11,88 @@ import dev.drewhamilton.poko.Poko
* Each callback represents a distinct stage in the onramp process and is
* invoked by the coordinator at the appropriate time.
*/
@Poko
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class OnrampCallbacks(
class OnrampCallbacks {

private var authenticateUserCallback: OnrampAuthenticateUserCallback? = null
private var verifyIdentityCallback: OnrampVerifyIdentityCallback? = null
private var verifyKycCallback: OnrampVerifyKycCallback? = null
private var collectPaymentCallback: OnrampCollectPaymentMethodCallback? = null
private var authorizeCallback: OnrampAuthorizeCallback? = null
private var checkoutCallback: OnrampCheckoutCallback? = null

/**
* Callback invoked to authenticate the user before starting the onramp flow.
*/
internal val authenticateUserCallback: OnrampAuthenticateUserCallback,
fun authenticateUserCallback(callback: OnrampAuthenticateUserCallback) = apply {
this.authenticateUserCallback = callback
}

/**
* Callback invoked when signaling the result of verifying the user's identity.
*/
internal val verifyIdentityCallback: OnrampVerifyIdentityCallback,
fun verifyIdentityCallback(callback: OnrampVerifyIdentityCallback) = apply {
this.verifyIdentityCallback = callback
}

/**
* Callback invoked when KYC verification was attempted to be completed.
*/
internal val verifyKycCallback: OnrampVerifyKycCallback,
fun verifyKycCallback(callback: OnrampVerifyKycCallback) = apply {
this.verifyKycCallback = callback
}

/**
* Callback invoked when a payment method was attempted to be collected.
*/
internal val collectPaymentCallback: OnrampCollectPaymentMethodCallback,
fun collectPaymentCallback(callback: OnrampCollectPaymentMethodCallback) = apply {
this.collectPaymentCallback = callback
}

/**
* Callback invoked when gaining user authorization was attempted.
*/
internal val authorizeCallback: OnrampAuthorizeCallback,
fun authorizeCallback(callback: OnrampAuthorizeCallback) = apply {
this.authorizeCallback = callback
}

/**
* Callback invoked to when the checkout process has completed.
* Callback invoked when the checkout process has completed.
*/
internal val checkoutCallback: OnrampCheckoutCallback
)
fun checkoutCallback(callback: OnrampCheckoutCallback) = apply {
this.checkoutCallback = callback
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class State internal constructor(
internal val authenticateUserCallback: OnrampAuthenticateUserCallback,
internal val verifyIdentityCallback: OnrampVerifyIdentityCallback,
internal val verifyKycCallback: OnrampVerifyKycCallback,
internal val collectPaymentCallback: OnrampCollectPaymentMethodCallback,
internal val authorizeCallback: OnrampAuthorizeCallback,
internal val checkoutCallback: OnrampCheckoutCallback,
)

fun build(): State {
return State(
authenticateUserCallback = requireNotNull(authenticateUserCallback) {
"authenticateUserCallback must not be null"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are all of these truly required? Is there zero use case for a merchant not to supply them?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

A merchant should supply all of them, yes.

},
verifyIdentityCallback = requireNotNull(verifyIdentityCallback) {
"verifyIdentityCallback must not be null"
},
verifyKycCallback = requireNotNull(verifyKycCallback) {
"verifyKycCallback must not be null"
},
collectPaymentCallback = requireNotNull(collectPaymentCallback) {
"collectPaymentCallback must not be null"
},
authorizeCallback = requireNotNull(authorizeCallback) {
"authorizeCallback must not be null"
},
checkoutCallback = requireNotNull(checkoutCallback) {
"checkoutCallback must not be null"
},
)
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package com.stripe.android.crypto.onramp.model

import android.os.Parcelable
import androidx.annotation.RestrictTo
import com.stripe.android.link.LinkAppearance
import dev.drewhamilton.poko.Poko
import kotlinx.parcelize.Parcelize

/**
* Configuration options required to initialize the Onramp flow.
Expand All @@ -14,12 +11,49 @@ import kotlinx.parcelize.Parcelize
* @property appearance Appearance settings for the PaymentSheet UI.
* @property cryptoCustomerId The unique customer ID for crypto onramp.
*/
@Parcelize
@Poko
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class OnrampConfiguration(
internal val merchantDisplayName: String,
internal val publishableKey: String,
internal val appearance: LinkAppearance,
internal val cryptoCustomerId: String? = null,
) : Parcelable
class OnrampConfiguration {
private var merchantDisplayName: String? = null
private var publishableKey: String? = null
private var appearance: LinkAppearance? = null
private var cryptoCustomerId: String? = null

fun merchantDisplayName(merchantDisplayName: String) = apply {
this.merchantDisplayName = merchantDisplayName
}

fun publishableKey(publishableKey: String) = apply {
this.publishableKey = publishableKey
}

fun appearance(appearance: LinkAppearance) = apply {
this.appearance = appearance
}

fun cryptoCustomerId(cryptoCustomerId: String?) = apply {
this.cryptoCustomerId = cryptoCustomerId
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class State internal constructor(
internal val merchantDisplayName: String,
internal val publishableKey: String,
internal val appearance: LinkAppearance,
internal val cryptoCustomerId: String? = null
)

fun build(): State {
return State(
merchantDisplayName = requireNotNull(merchantDisplayName) {
"merchantDisplayName must not be null"
},
publishableKey = requireNotNull(publishableKey) {
"publishableKey must not be null"
},
appearance = requireNotNull(appearance) {
"appearance must not be null"
},
cryptoCustomerId = cryptoCustomerId,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class OnrampInteractorTest {
fun testConfigureIsSuccessful() = runTest {
whenever(linkController.configure(any())).thenReturn(ConfigureResult.Success)

val result = interactor.configure(createConfiguration())
val result = interactor.configure(createConfigurationState())

assert(result is OnrampConfigurationResult.Completed)
}
Expand Down Expand Up @@ -206,7 +206,7 @@ class OnrampInteractorTest {
interactor.onLinkControllerState(mockLinkStateWithAccount())

whenever(linkController.configure(any())).thenReturn(ConfigureResult.Success)
interactor.configure(createConfiguration(cryptoCustomerId = "cpt_123"))
interactor.configure(createConfigurationState(cryptoCustomerId = "cpt_123"))

val mockPlatformSettings = mock<GetPlatformSettingsResponse>()
doReturn("pk_platform_123").whenever(mockPlatformSettings).publishableKey
Expand Down Expand Up @@ -547,13 +547,13 @@ class OnrampInteractorTest {
consumerSessionClientSecret = null
)

private fun createConfiguration(
private fun createConfigurationState(
cryptoCustomerId: String? = null
): OnrampConfiguration =
OnrampConfiguration(
merchantDisplayName = "merchant-display-name",
publishableKey = "pk_test_12345",
appearance = mock(),
cryptoCustomerId = cryptoCustomerId
)
): OnrampConfiguration.State =
OnrampConfiguration()
.merchantDisplayName("merchant-display-name")
.publishableKey("pk_test_12345")
.appearance(mock())
.cryptoCustomerId(cryptoCustomerId)
.build()
}
Loading
Loading