package com.suno.android.ui.screens.root import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.suno.android.analytics.MobileMeasurementPartnerManager import com.suno.android.billing.RevenueCatManager import com.suno.android.common_analytics.managers.AnalyticsManager import com.suno.android.common_core_utils.SunoLogger import com.suno.android.common_core_utils.environment.ThemeMode import com.suno.android.common_core_utils.global_errors.TopLevelErrorManager import com.suno.android.common_core_utils.helpers.AppLifecycleManager import com.suno.android.common_core_utils.helpers.SunoAppLifecycleEvent import com.suno.android.common_data.billing.SunoBillingRepo import com.suno.android.common_data.use_case.SetUserToServicesUseCase import com.suno.android.common_data.user.UserSessionRepository import com.suno.android.deeplink.DeferredDeepLinkManager import com.suno.android.deeplink.DeferredDeeplink import com.suno.android.framework.auth.api.AuthManager import com.suno.android.gating.statsig.StatsigFeatureDataSource import com.suno.android.push.BrazeManager import com.suno.android.push.PushNotificationManager import com.suno.android.utils.resolvers.ScreenVisitResolver import com.suno.feature_tweaks.TweaksEntryPoint import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @HiltViewModel class RootNavGraphVM @Inject constructor( loggerFactory: SunoLogger.Factory, private val authManager: AuthManager, private val userSessionRepository: UserSessionRepository, topLevelErrorManager: TopLevelErrorManager, appLifecycleManager: AppLifecycleManager, private val sunoBillingRepo: SunoBillingRepo, private val revenueCatManager: RevenueCatManager, private val deferredDeepLinkManager: DeferredDeepLinkManager, private val pushNotificationManager: PushNotificationManager, private val brazeManager: BrazeManager, private val analyticsManager: AnalyticsManager, private val screenVisitResolver: ScreenVisitResolver, private val statsigManager: StatsigFeatureDataSource, private val mmpManager: MobileMeasurementPartnerManager, private val setUserToServicesUseCase: SetUserToServicesUseCase, val tweaksEntryPoint: TweaksEntryPoint, ) : ViewModel() { private val logger = loggerFactory.create(this@RootNavGraphVM) private var isOnboarding = false private val _viewStateFlow = MutableStateFlow( UiState(), ) private val _effectFlow = MutableSharedFlow() val errorDialogFlow = topLevelErrorManager.errorDialogDisplayFlow init { startUpdateUserIdFlow() appLifecycleManager.appProcessEventFlow() .onEach { sunoAppLifecycleEvent -> when (sunoAppLifecycleEvent) { is SunoAppLifecycleEvent.OnAppForegrounded -> { authManager.refreshSunoJwtToken() } else -> Unit } }.catch { exception -> logger.e(exception) }.launchIn(viewModelScope) authManager.isLoggedInFlow().distinctUntilChanged() .onEach { isLoggedIn -> if (isLoggedIn) { _effectFlow.emit( RootEffects.OnAuthenticationSuccess, ) } else { _effectFlow.emit( RootEffects.OnAuthenticationFailure, ) } }.catch { exception -> // TODO: more granular exception handling for when a login check fails logger.e(exception) authManager.logOut() _effectFlow.emit( RootEffects.OnAuthenticationFailure, ) }.launchIn(viewModelScope) deferredDeepLinkManager .deferredDeeplinkFlow() .onEach { deeplink -> when (deeplink) { is DeferredDeeplink.OauthCallbackDeeplink -> { handleOauthCallbackSuspend( rotatingTokenNonce = deeplink.rotatingTokenNonce, ) deferredDeepLinkManager.reset() } // Should we handle other deeplinks here for setting revenueCat / analytics users? else -> {} } }.catch { exception -> logger.e(exception) }.launchIn(viewModelScope) } private fun startUpdateUserIdFlow() { userSessionRepository.sessionConfigurationStateFlow() .onEach { sessionConfiguration -> setUserToServicesUseCase(sessionConfiguration.user) }.catch { exception -> logger.e(exception) }.launchIn(viewModelScope) } fun viewStateFlow() = _viewStateFlow.asStateFlow() fun effectFlow() = _effectFlow.asSharedFlow() fun trackScreen( screenName: String?, ) { screenVisitResolver.resolve(screenName)?.value?.let { analyticsManager.trackScreenVisit(it) } } fun onAppConfigLoaded( isSuccessfulLoad: Boolean, ) { viewModelScope.launch { withContext(Dispatchers.IO) { if (isSuccessfulLoad) { authManager.startSessionJwtTokenPolling() _effectFlow.emit( RootEffects.OnAppConfigurationLoadSuccess, ) } else { _effectFlow.emit( RootEffects.OnAppConfigurationLoadFailure, ) } } } } fun setFlagForOnboarding( isOnboarding: Boolean, ) { this.isOnboarding = isOnboarding } fun checkIfOnboarding() { viewModelScope.launch { if (isOnboarding) { _effectFlow.emit( RootEffects.OnRouteToOnboarding, ) isOnboarding = false } else { _effectFlow.emit( RootEffects.OnRouteToHome, ) } } } fun loadAuthenticatedConfiguration() { viewModelScope.launch { postAuthConfig() _effectFlow.emit( RootEffects.OnAuthenticatedConfigurationLoaded, ) } } fun setAuthLoading( loading: Boolean, ) { _viewStateFlow.update { oldState -> oldState.copy( authLoading = loading, ) } } private fun handleOauthCallbackSuspend( rotatingTokenNonce: String, ) { viewModelScope.launch { withContext(Dispatchers.IO) { try { val isSignUp = authManager.handleRotatingNonceForSignUpOrSignIn( rotatingTokenNonce = rotatingTokenNonce, ) isOnboarding = isSignUp // THIS MUST HAPPEN FIRST BEFORE ANY OTHER AUTHENTICATED CALLS!!! authManager.startSessionJwtTokenPolling() postAuthConfig() _effectFlow.emit( RootEffects.OnOauthDeeplinkSuccess( isOnboarding = isSignUp, ), ) } catch (exception: Exception) { logger.e(exception) _effectFlow.emit( RootEffects.OnOauthDeeplinkFailure, ) } } } } private suspend fun postAuthConfig() { val sessionConfiguration = userSessionRepository.refreshUserSessionConfiguration() val user = sessionConfiguration?.user val userId = user?.id.orEmpty() val userEmail = user?.email val userPhone = if (userEmail.isNullOrEmpty()) user?.username else null if (user != null) { brazeManager.updateUser(user) _effectFlow.emit(RootEffects.OnReadyToLoadBraze) } if (userId.isNotEmpty()) { mmpManager.setCustomerIdAndLogSession( userId = userId, ) } sunoBillingRepo.refreshBillingState() revenueCatManager.triggerFullRefresh() revenueCatManager.setRevenueCatCustomerId( sunoUserId = userId, ) userEmail?.let { revenueCatManager.setRevenueCatEmail( email = userEmail, ) } statsigManager.updateUser( userId = userId, email = userEmail, custom = sessionConfiguration?.statsigCustomProperties?.custom, customIds = sessionConfiguration?.statsigCustomProperties?.customIds, ) pushNotificationManager.setProfile( userId = userId, email = userEmail, phoneNumber = userPhone, ) } data class UiState( val loginDialogActive: Boolean = false, val selectedSongUrl: String? = null, val artistName: String? = null, val title: String? = null, val albumUrl: String? = null, val onConfigLoaded: Boolean = false, val authLoading: Boolean = false, val themeMode: ThemeMode = ThemeMode.SYSTEM, ) sealed interface RootEffects { data object OnAppConfigurationLoadSuccess : RootEffects // things like lokalise or other non-authed configs data object OnAppConfigurationLoadFailure : RootEffects // things like lokalise or other non-authed configs data object OnAuthenticationSuccess : RootEffects data object OnAuthenticationFailure : RootEffects data object OnAuthenticatedConfigurationLoaded : RootEffects data class OnOauthDeeplinkSuccess( val isOnboarding: Boolean, ) : RootEffects data object OnOauthDeeplinkFailure : RootEffects data object OnRouteToOnboarding : RootEffects data object OnRouteToHome : RootEffects data object OnReadyToLoadBraze : RootEffects } }