diff --git a/connect-example/src/main/java/com/stripe/android/connect/example/App.kt b/connect-example/src/main/java/com/stripe/android/connect/example/App.kt index c2fef666f4a..4711e9835a9 100644 --- a/connect-example/src/main/java/com/stripe/android/connect/example/App.kt +++ b/connect-example/src/main/java/com/stripe/android/connect/example/App.kt @@ -3,7 +3,6 @@ package com.stripe.android.connect.example import android.app.Application import android.os.StrictMode import com.github.kittinunf.fuel.core.FuelError -import com.stripe.android.connect.example.data.EmbeddedComponentManagerProvider import com.stripe.android.connect.example.data.EmbeddedComponentService import com.stripe.android.core.Logger import dagger.hilt.android.HiltAndroidApp @@ -19,8 +18,6 @@ class App : Application() { @Inject lateinit var embeddedComponentService: EmbeddedComponentService - @Inject lateinit var embeddedComponentManagerProvider: EmbeddedComponentManagerProvider - private val logger: Logger = Logger.getInstance(enableLogging = BuildConfig.DEBUG) override fun onCreate() { @@ -44,7 +41,6 @@ class App : Application() { ) attemptLoadPublishableKey() - embeddedComponentManagerProvider.initialize(GlobalScope) } private fun attemptLoadPublishableKey() { diff --git a/connect-example/src/main/java/com/stripe/android/connect/example/BaseActivity.kt b/connect-example/src/main/java/com/stripe/android/connect/example/BaseActivity.kt new file mode 100644 index 00000000000..a3dac306dcc --- /dev/null +++ b/connect-example/src/main/java/com/stripe/android/connect/example/BaseActivity.kt @@ -0,0 +1,25 @@ +package com.stripe.android.connect.example + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.viewModels +import androidx.annotation.CallSuper +import com.stripe.android.connect.EmbeddedComponentManager +import com.stripe.android.connect.PrivateBetaConnectSDK +import com.stripe.android.connect.example.ui.embeddedcomponentmanagerloader.EmbeddedComponentLoaderViewModel +import dagger.hilt.android.AndroidEntryPoint + +@OptIn(PrivateBetaConnectSDK::class) +@AndroidEntryPoint +abstract class BaseActivity : ComponentActivity() { + + protected val loaderViewModel: EmbeddedComponentLoaderViewModel by viewModels() + + @CallSuper + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + EmbeddedComponentManager.onActivityCreate(this) + lifecycle.addObserver(loaderViewModel) + } +} diff --git a/connect-example/src/main/java/com/stripe/android/connect/example/MainActivity.kt b/connect-example/src/main/java/com/stripe/android/connect/example/MainActivity.kt index 370a97da649..1aa4f10ea66 100644 --- a/connect-example/src/main/java/com/stripe/android/connect/example/MainActivity.kt +++ b/connect-example/src/main/java/com/stripe/android/connect/example/MainActivity.kt @@ -1,38 +1,29 @@ package com.stripe.android.connect.example import android.os.Bundle -import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import com.stripe.android.connect.EmbeddedComponentManager -import com.stripe.android.connect.PrivateBetaConnectSDK import com.stripe.android.connect.example.ui.common.ConnectSdkExampleTheme import com.stripe.android.connect.example.ui.componentpicker.ComponentPickerContent -import com.stripe.android.connect.example.ui.embeddedcomponentmanagerloader.EmbeddedComponentLoaderViewModel import com.stripe.android.connect.example.ui.settings.SettingsDestination import com.stripe.android.connect.example.ui.settings.settingsComposables import dagger.hilt.android.AndroidEntryPoint -@OptIn(PrivateBetaConnectSDK::class) @AndroidEntryPoint -class MainActivity : ComponentActivity() { +class MainActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - EmbeddedComponentManager.onActivityCreate(this@MainActivity) - setContent { - val viewModel = hiltViewModel(this@MainActivity) val navController = rememberNavController() ConnectSdkExampleTheme { NavHost(navController = navController, startDestination = MainDestination.ComponentPicker) { composable(MainDestination.ComponentPicker) { ComponentPickerContent( - viewModel = viewModel, + viewModel = loaderViewModel, openSettings = { navController.navigate(SettingsDestination.Settings) }, ) } diff --git a/connect-example/src/main/java/com/stripe/android/connect/example/data/EmbeddedComponentManagerProvider.kt b/connect-example/src/main/java/com/stripe/android/connect/example/data/EmbeddedComponentManagerFactory.kt similarity index 57% rename from connect-example/src/main/java/com/stripe/android/connect/example/data/EmbeddedComponentManagerProvider.kt rename to connect-example/src/main/java/com/stripe/android/connect/example/data/EmbeddedComponentManagerFactory.kt index 212d3e2db7f..f53ec618a05 100644 --- a/connect-example/src/main/java/com/stripe/android/connect/example/data/EmbeddedComponentManagerProvider.kt +++ b/connect-example/src/main/java/com/stripe/android/connect/example/data/EmbeddedComponentManagerFactory.kt @@ -1,20 +1,14 @@ package com.stripe.android.connect.example.data -import android.content.Context import com.github.kittinunf.fuel.core.FuelError import com.stripe.android.connect.EmbeddedComponentManager import com.stripe.android.connect.FetchClientSecretCallback.ClientSecretResultCallback import com.stripe.android.connect.PrivateBetaConnectSDK -import com.stripe.android.connect.appearance.Appearance import com.stripe.android.connect.appearance.fonts.CustomFontSource -import com.stripe.android.connect.example.ui.appearance.AppearanceInfo import com.stripe.android.core.BuildConfig import com.stripe.android.core.Logger -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import java.io.IOException import javax.inject.Inject @@ -22,43 +16,27 @@ import javax.inject.Singleton @OptIn(PrivateBetaConnectSDK::class) @Singleton -class EmbeddedComponentManagerProvider @Inject constructor( - @ApplicationContext private val context: Context, +class EmbeddedComponentManagerFactory @Inject constructor( private val embeddedComponentService: EmbeddedComponentService, private val settingsService: SettingsService, ) { - - // this factory manages the EmbeddedComponentManager instance, since it needs to wait for - // a publishable key to be received from the backend before building it. - // In the future it may manage multiple instances if needed. - private var embeddedComponentManager: EmbeddedComponentManager? = null - private val loggingTag = this::class.java.simpleName private val logger: Logger = Logger.getInstance(enableLogging = BuildConfig.DEBUG) private val ioScope: CoroutineScope by lazy { CoroutineScope(Dispatchers.IO) } - fun initialize(scope: CoroutineScope): Job = scope.launch { - // Update appearance in the SDK whenever the appearance setting changes. - settingsService.getAppearanceIdFlow() - .collectLatest { appearanceId -> - embeddedComponentManager?.update(getAppearance(context, appearanceId)) - } - } - /** - * Provides the EmbeddedComponentManager instance, creating it if it doesn't exist. - * Throws [IllegalStateException] if an EmbeddedComponentManager cannot be created at this time. + * Creates an instance of [EmbeddedComponentManager]. + * Returns null if it cannot be created at this time. */ - fun provideEmbeddedComponentManager(): EmbeddedComponentManager? { - embeddedComponentManager?.let { return it } // return the embedded component manager if it already exists + fun createEmbeddedComponentManager(): EmbeddedComponentManager? { + val publishableKey = embeddedComponentService.publishableKey.value + ?: return null - val publishableKey = embeddedComponentService.publishableKey.value ?: return null return EmbeddedComponentManager( configuration = EmbeddedComponentManager.Configuration( publishableKey = publishableKey, ), fetchClientSecretCallback = ::fetchClientSecret, - appearance = getAppearance(context, settingsService.getAppearanceId()), customFonts = listOf( CustomFontSource( "fonts/doto.ttf", @@ -66,9 +44,7 @@ class EmbeddedComponentManagerProvider @Inject constructor( weight = 1000, ) ) - ).also { - embeddedComponentManager = it - } + ) } /** @@ -92,10 +68,4 @@ class EmbeddedComponentManagerProvider @Inject constructor( } } } - - private fun getAppearance(context: Context, appearanceId: AppearanceInfo.AppearanceId?): Appearance { - return appearanceId - ?.let { AppearanceInfo.getAppearance(it, context).appearance } - ?: Appearance() - } } diff --git a/connect-example/src/main/java/com/stripe/android/connect/example/ui/common/BasicExampleComponentActivity.kt b/connect-example/src/main/java/com/stripe/android/connect/example/ui/common/BasicExampleComponentActivity.kt index 4e46df8f336..4974b2a3a4f 100644 --- a/connect-example/src/main/java/com/stripe/android/connect/example/ui/common/BasicExampleComponentActivity.kt +++ b/connect-example/src/main/java/com/stripe/android/connect/example/ui/common/BasicExampleComponentActivity.kt @@ -14,26 +14,22 @@ import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.viewinterop.AndroidView -import androidx.fragment.app.FragmentActivity import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.stripe.android.connect.EmbeddedComponentManager import com.stripe.android.connect.PrivateBetaConnectSDK +import com.stripe.android.connect.example.BaseActivity import com.stripe.android.connect.example.core.Async import com.stripe.android.connect.example.core.Success import com.stripe.android.connect.example.core.then -import com.stripe.android.connect.example.data.SettingsService -import com.stripe.android.connect.example.ui.appearance.AppearanceInfo import com.stripe.android.connect.example.ui.appearance.AppearanceView import com.stripe.android.connect.example.ui.appearance.AppearanceViewModel import com.stripe.android.connect.example.ui.embeddedcomponentmanagerloader.EmbeddedComponentLoaderViewModel @@ -42,17 +38,10 @@ import com.stripe.android.connect.example.ui.settings.SettingsViewModel import com.stripe.android.connect.example.ui.settings.settingsComposables import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch -import javax.inject.Inject - -@Suppress("ConstPropertyName") -private object BasicComponentExampleDestination { - const val Component = "Component" - const val Settings = "Settings" -} @OptIn(PrivateBetaConnectSDK::class) @AndroidEntryPoint -abstract class BasicExampleComponentActivity : FragmentActivity() { +abstract class BasicExampleComponentActivity : BaseActivity() { @get:StringRes abstract val titleRes: Int @@ -61,14 +50,9 @@ abstract class BasicExampleComponentActivity : FragmentActivity() { abstract fun createComponentView(context: Context, embeddedComponentManager: EmbeddedComponentManager): View - @Inject - lateinit var settingsService: SettingsService - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - EmbeddedComponentManager.onActivityCreate(this@BasicExampleComponentActivity) - val settings = settingsViewModel.state.value val enableEdgeToEdge = settings.presentationSettings.enableEdgeToEdge if (enableEdgeToEdge) { @@ -77,13 +61,12 @@ abstract class BasicExampleComponentActivity : FragmentActivity() { setContent { BackHandler(onBack = ::finish) - val viewModel = hiltViewModel(this@BasicExampleComponentActivity) val navController = rememberNavController() ConnectSdkExampleTheme { NavHost(navController = navController, startDestination = BasicComponentExampleDestination.Component) { composable(BasicComponentExampleDestination.Component) { ExampleComponentContent( - viewModel = viewModel, + viewModel = loaderViewModel, enableEdgeToEdge = enableEdgeToEdge, openSettings = { navController.navigate(BasicComponentExampleDestination.Settings) }, ) @@ -175,16 +158,15 @@ abstract class BasicExampleComponentActivity : FragmentActivity() { openSettings = openSettings, reload = reload, ) { embeddedComponentManager -> - val context = LocalContext.current - LaunchedEffect(context) { - val appearanceInfo = settingsService.getAppearanceId() - ?.let { AppearanceInfo.getAppearance(it, context).appearance } - ?: return@LaunchedEffect - embeddedComponentManager.update(appearanceInfo) - } AndroidView(modifier = Modifier.fillMaxSize(), factory = { createComponentView(it, embeddedComponentManager) }) } } } + +@Suppress("ConstPropertyName") +private object BasicComponentExampleDestination { + const val Component = "Component" + const val Settings = "Settings" +} diff --git a/connect-example/src/main/java/com/stripe/android/connect/example/ui/embeddedcomponentmanagerloader/EmbeddedComponentLoaderViewModel.kt b/connect-example/src/main/java/com/stripe/android/connect/example/ui/embeddedcomponentmanagerloader/EmbeddedComponentLoaderViewModel.kt index d94b1d095f1..b8e12ac8f92 100644 --- a/connect-example/src/main/java/com/stripe/android/connect/example/ui/embeddedcomponentmanagerloader/EmbeddedComponentLoaderViewModel.kt +++ b/connect-example/src/main/java/com/stripe/android/connect/example/ui/embeddedcomponentmanagerloader/EmbeddedComponentLoaderViewModel.kt @@ -1,6 +1,10 @@ package com.stripe.android.connect.example.ui.embeddedcomponentmanagerloader +import androidx.activity.ComponentActivity +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel +import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewModelScope import com.github.kittinunf.fuel.core.FuelError import com.stripe.android.connect.PrivateBetaConnectSDK @@ -8,13 +12,19 @@ import com.stripe.android.connect.example.BuildConfig import com.stripe.android.connect.example.core.Fail import com.stripe.android.connect.example.core.Loading import com.stripe.android.connect.example.core.Success -import com.stripe.android.connect.example.data.EmbeddedComponentManagerProvider +import com.stripe.android.connect.example.data.EmbeddedComponentManagerFactory import com.stripe.android.connect.example.data.EmbeddedComponentService +import com.stripe.android.connect.example.data.SettingsService +import com.stripe.android.connect.example.ui.appearance.AppearanceInfo import com.stripe.android.core.Logger import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -23,8 +33,9 @@ import javax.inject.Inject @HiltViewModel class EmbeddedComponentLoaderViewModel @Inject constructor( private val embeddedComponentService: EmbeddedComponentService, - private val embeddedComponentManagerProvider: EmbeddedComponentManagerProvider, -) : ViewModel() { + private val embeddedComponentManagerFactory: EmbeddedComponentManagerFactory, + private val settingsService: SettingsService, +) : ViewModel(), DefaultLifecycleObserver { private val logger: Logger = Logger.getInstance(enableLogging = BuildConfig.DEBUG) private val loggingTag = this::class.java.simpleName @@ -33,27 +44,42 @@ class EmbeddedComponentLoaderViewModel @Inject constructor( val state: StateFlow = _state.asStateFlow() init { - initializeManager() + loadManagerIfNecessary() } - // Public methods - fun reload() { loadManager() } - // Private methods + override fun onCreate(owner: LifecycleOwner) { + val activity = owner as? ComponentActivity + ?: return - private fun initializeManager() { - val manager = embeddedComponentManagerProvider.provideEmbeddedComponentManager() - if (manager == null) { - loadManager() - return + // Bind appearance settings to the manager. + activity.lifecycleScope.launch { + val managerFlow = _state + .map { it.embeddedComponentManagerAsync() } + .filterNotNull() + val appearanceFlow = settingsService.getAppearanceIdFlow() + .filterNotNull() + .map { id -> AppearanceInfo.getAppearance(id, activity).appearance } + combine(managerFlow, appearanceFlow, ::Pair).collectLatest { (manager, appearance) -> + logger.debug("($loggingTag) Updating appearance in $activity") + manager.update(appearance) + } } + } - _state.update { - it.copy(embeddedComponentManagerAsync = Success(manager)) + private fun loadManagerIfNecessary() { + if (_state.value.embeddedComponentManagerAsync() != null) { + return + } + val manager = embeddedComponentManagerFactory.createEmbeddedComponentManager() + if (manager != null) { + _state.update { it.copy(embeddedComponentManagerAsync = Success(manager)) } + return } + loadManager() } private fun loadManager() { @@ -68,7 +94,7 @@ class EmbeddedComponentLoaderViewModel @Inject constructor( embeddedComponentService.getAccounts() // initialize the SDK, or throw an error if we're unable to - val manager = embeddedComponentManagerProvider.provideEmbeddedComponentManager() + val manager = embeddedComponentManagerFactory.createEmbeddedComponentManager() val async = if (manager != null) { Success(manager) } else { diff --git a/connect-example/src/main/java/com/stripe/android/connect/example/ui/embeddedcomponentmanagerloader/EmbeddedComponentManagerLoader.kt b/connect-example/src/main/java/com/stripe/android/connect/example/ui/embeddedcomponentmanagerloader/EmbeddedComponentManagerLoader.kt index faf201256f1..aff3fd1436b 100644 --- a/connect-example/src/main/java/com/stripe/android/connect/example/ui/embeddedcomponentmanagerloader/EmbeddedComponentManagerLoader.kt +++ b/connect-example/src/main/java/com/stripe/android/connect/example/ui/embeddedcomponentmanagerloader/EmbeddedComponentManagerLoader.kt @@ -37,7 +37,10 @@ fun EmbeddedComponentManagerLoader( ) { val embeddedComponentManager = embeddedComponentAsync() when (embeddedComponentAsync) { - is Uninitialized, is Loading -> LoadingScreen() + is Uninitialized -> { + // Don't show anything to avoid flicker. + } + is Loading -> LoadingScreen() is Fail -> ErrorScreen( errorMessage = embeddedComponentAsync.error.message ?: stringResource(R.string.error_initializing), onReloadRequested = reload, diff --git a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt index bbbc6c957c3..9c53d5f24e7 100644 --- a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt +++ b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt @@ -248,6 +248,8 @@ internal class StripeConnectWebViewContainerImpl( private fun bindViewState(state: StripeConnectWebViewContainerState) { val viewBinding = this.viewBinding ?: return + logger.debug("Binding view state: $state") + viewBinding.root.setBackgroundColor(state.backgroundColor) viewBinding.stripeWebView.setBackgroundColor(state.backgroundColor) viewBinding.stripeWebViewProgressBar.isVisible = state.isNativeLoadingIndicatorVisible viewBinding.stripeWebView.isVisible = !state.isNativeLoadingIndicatorVisible