Skip to content
Merged
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 @@ -101,14 +101,13 @@ 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 callbacks = 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)

onrampPresenter = viewModel.onrampCoordinator
.createPresenter(this, callbacks)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,24 +104,26 @@ 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 configuration = OnrampConfiguration()
.merchantDisplayName(merchantDisplayName = "Onramp Example")
.publishableKey(publishableKey = "pk_test_51K9W3OHMaDsveWq0oLP0ZjldetyfHIqyJcz27k2BpMGHxu9v9Cei2tofzoHncPyk3A49jMkFEgTOBQyAMTUffRLa00xzzARtZO")
.appearance(
appearance = LinkAppearance()
.lightColors(
LinkAppearance.Colors()
.primary(Color(0xFF635BFF))
.contentOnPrimary(Color.White)
.borderSelected(Color.Black)
)
.darkColors(
LinkAppearance.Colors()
.primary(Color(0xFF9886E6))
.contentOnPrimary(Color(0xFF222222))
.borderSelected(Color.White)
)
.style(LinkAppearance.Style.ALWAYS_DARK)
.primaryButton(LinkAppearance.PrimaryButton())
)
)

onrampCoordinator.configure(configuration = configuration)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class OnrampCoordinator @Inject internal constructor(
suspend fun configure(
configuration: OnrampConfiguration,
): OnrampConfigurationResult {
return interactor.configure(configuration)
return interactor.configure(configuration.build())
}

/**
Expand Down Expand Up @@ -148,7 +148,7 @@ class OnrampCoordinator @Inject internal constructor(
activity = activity,
lifecycleOwner = activity,
activityResultRegistryOwner = activity,
onrampCallbacks = onrampCallbacks,
onrampCallbacks = onrampCallbacks.build(),
)
.presenter
}
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,87 @@ 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
}

internal class State(
val authenticateUserCallback: OnrampAuthenticateUserCallback,
val verifyIdentityCallback: OnrampVerifyIdentityCallback,
val verifyKycCallback: OnrampVerifyKycCallback,
val collectPaymentCallback: OnrampCollectPaymentMethodCallback,
val authorizeCallback: OnrampAuthorizeCallback,
val checkoutCallback: OnrampCheckoutCallback,
)

internal 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,59 @@ 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

/**
* Sets the display name of the merchant.
*/
fun merchantDisplayName(merchantDisplayName: String) = apply {
this.merchantDisplayName = merchantDisplayName
}

/**
* Sets the publishable key of the merchant.
*/
fun publishableKey(publishableKey: String) = apply {
this.publishableKey = publishableKey
}

/**
* Sets appearance settings for the payment sheet user interface presented by Stripe.
* This does have a default appearance.
*/
fun appearance(appearance: LinkAppearance) = apply {
this.appearance = appearance
}

/**
* Sets the unique crypto customer ID to use.
*/
fun cryptoCustomerId(cryptoCustomerId: String?) = apply {
this.cryptoCustomerId = cryptoCustomerId
}

internal class State(
val merchantDisplayName: String,
val publishableKey: String,
val appearance: LinkAppearance,
val cryptoCustomerId: String? = null
)

internal fun build(): State {
return State(
merchantDisplayName = requireNotNull(merchantDisplayName) {
"merchantDisplayName must not be null"
},
publishableKey = requireNotNull(publishableKey) {
"publishableKey must not be null"
},
appearance = appearance ?: LinkAppearance(),
cryptoCustomerId = cryptoCustomerId,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ internal class VerifyKycInfoActivityContract : ActivityResultContract<
context = context,
args = VerifyKycArgs(
kycRetrieveResponse = input.kycRetrieveResponse,
appearance = input.linkAppearance
appearance = input.linkAppearance?.build()
)
)
}
Expand All @@ -128,7 +128,7 @@ internal class VerifyKycInfoActivityContract : ActivityResultContract<
@Parcelize
internal data class VerifyKycArgs(
val kycRetrieveResponse: KycRetrieveResponse,
val appearance: LinkAppearance?,
val appearance: LinkAppearance.State?,
) : Parcelable

private fun KycRetrieveResponse.toVerifyKYCInfo(): VerifyKYCInfo {
Expand Down
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