Skip to content

Commit

Permalink
scope EmbeddedComponentManager instances to Activities
Browse files Browse the repository at this point in the history
  • Loading branch information
lng-stripe committed Dec 20, 2024
1 parent c72b3ed commit 47f60e0
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() {
Expand All @@ -44,7 +41,6 @@ class App : Application() {
)

attemptLoadPublishableKey()
embeddedComponentManagerProvider.initialize(GlobalScope)
}

private fun attemptLoadPublishableKey() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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<EmbeddedComponentLoaderViewModel>(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) },
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,74 +1,50 @@
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
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",
"doto",
weight = 1000,
)
)
).also {
embeddedComponentManager = it
}
)
}

/**
Expand All @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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) {
Expand All @@ -77,13 +61,12 @@ abstract class BasicExampleComponentActivity : FragmentActivity() {

setContent {
BackHandler(onBack = ::finish)
val viewModel = hiltViewModel<EmbeddedComponentLoaderViewModel>(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) },
)
Expand Down Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
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
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
Expand All @@ -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
Expand All @@ -33,27 +44,42 @@ class EmbeddedComponentLoaderViewModel @Inject constructor(
val state: StateFlow<EmbeddedComponentManagerLoaderState> = _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() {
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit 47f60e0

Please sign in to comment.