package com.suno.android.deeplink import android.net.Uri import arrow.core.getOrElse import com.suno.android.common_core_utils.BackendHostUrl import com.suno.android.common_core_utils.Id import com.suno.android.common_core_utils.SunoLogger import com.suno.android.common_core_utils.model.UserHandle import com.suno.android.common_data.comments.Comment import com.suno.android.common_data.mappers.clips.SongListData import com.suno.android.common_data.mappers.hooks.LocalHookData import com.suno.android.common_networking.extensions.toThrowable import com.suno.android.common_networking.remote.share.ShareLinkService import com.suno.android.framework.auth.api.AuthRedirectUrl import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import java.net.URL import javax.inject.Inject import javax.inject.Singleton interface DeferredDeepLinkManager { fun setDeferredDeepLink( deferredDeeplink: DeferredDeeplink, ) fun deferredDeeplinkFlow(): Flow suspend fun processRawDeeplinkUri( uri: Uri, fromInternalSource: Boolean = false, ) suspend fun processRawDeeplinkPath( uri: Uri, path: String, fromInternalSource: Boolean = false, styleTag: String? = null, ) fun reset() } @Singleton class DeferredDeepLinkManagerImpl @Inject constructor( loggerFactory: SunoLogger.Factory, private val shareLinkService: ShareLinkService, @BackendHostUrl private val backendHostUrl: URL, @AuthRedirectUrl private val authRedirectUrl: URL, ) : DeferredDeepLinkManager { private val logger = loggerFactory.create(this@DeferredDeepLinkManagerImpl) private val _deferredDeepLinkStateFlow: MutableStateFlow = MutableStateFlow(DeferredDeeplink.None) override fun setDeferredDeepLink( deferredDeeplink: DeferredDeeplink, ) { logger.d { deferredDeeplink } _deferredDeepLinkStateFlow.update { deferredDeeplink } } override fun deferredDeeplinkFlow(): Flow = _deferredDeepLinkStateFlow.asStateFlow() override fun reset() { logger.d { "!!!! RESET DEEPLINKER !!!!" } _deferredDeepLinkStateFlow.update { DeferredDeeplink.None } } override suspend fun processRawDeeplinkUri( uri: Uri, fromInternalSource: Boolean, ) { logger.d { "Deeplink uri: $uri, scheme: ${uri.scheme}, host: ${uri.host}, path: ${uri.path}, " + "encodedQuery: ${uri.encodedQuery}" } val isValidWebDeeplink = uri.host == backendHostUrl.host && (uri.scheme == "https" || uri.scheme == "suno") if (isValidWebDeeplink) { uri.path?.let { path -> processRawDeeplinkPath(uri = uri, path = path, fromInternalSource = fromInternalSource) } } } enum class DeeplinkContentType( val contentType: String, ) { SONG("song"), HOOK("hook"), } override suspend fun processRawDeeplinkPath( uri: Uri, path: String, fromInternalSource: Boolean, styleTag: String?, ) { when { path.startsWith("/s/") -> { val shortcode = path.substringAfter("/s/") val response = shareLinkService.unwrapShortcode(shortcode).getOrElse { error -> logger.e(error.toThrowable()) return } when (response.contentType) { DeeplinkContentType.SONG.contentType -> { val deferredDeeplink = DeferredDeeplink.OnClipIdDeeplink(clipId = Id(response.contentId)) setDeferredDeepLink(deferredDeeplink) } DeeplinkContentType.HOOK.contentType -> { val deferredDeeplink = DeferredDeeplink.OnHookIdDeeplink(hookId = Id(response.contentId)) setDeferredDeepLink(deferredDeeplink) } } } path.startsWith("/create") -> { setDeferredDeepLink( deferredDeeplink = DeferredDeeplink.Create, ) } path.startsWith("/notifications") -> { setDeferredDeepLink( deferredDeeplink = DeferredDeeplink.Notifications, ) } path.startsWith(authRedirectUrl.path) -> { val rotatingTokenNonce = uri.getQueryParameter("rotating_token_nonce") .orEmpty() setDeferredDeepLink( deferredDeeplink = DeferredDeeplink.OauthCallbackDeeplink( rotatingTokenNonce = rotatingTokenNonce, ), ) } path.startsWith("/song/") -> { val songId = path.substringAfter("/song/") setDeferredDeepLink( deferredDeeplink = DeferredDeeplink.OnClipIdDeeplink( clipId = Id(songId), ), ) } path.startsWith("/playlist/") -> { val playlistId = path.substringAfter("/playlist/") setDeferredDeepLink( deferredDeeplink = DeferredDeeplink.OnPlaylistIdDeeplink( playlistId = playlistId, fromInternalSource = fromInternalSource, styleTag = styleTag, ), ) } path.startsWith("/hooks") -> { setDeferredDeepLink( deferredDeeplink = DeferredDeeplink.Hooks, ) } path.startsWith("/hook/") -> { val hookId = path.substringAfter("/hook/") val commentId = uri.getQueryParameter("comment") setDeferredDeepLink( deferredDeeplink = DeferredDeeplink.OnHookIdDeeplink( hookId = Id(hookId), commentId = commentId?.let { Id(it) }, ), ) } path.startsWith("/account") || path.startsWith("/subscribe") -> { setDeferredDeepLink( deferredDeeplink = DeferredDeeplink.Subscribe, ) } } } } sealed interface DeferredDeeplink { data object None : DeferredDeeplink data object Notifications : DeferredDeeplink data object Create : DeferredDeeplink data object Subscribe : DeferredDeeplink data object Hooks : DeferredDeeplink data class OauthCallbackDeeplink( val rotatingTokenNonce: String, ) : DeferredDeeplink data class OnProfileIdDeeplink( val userHandle: UserHandle, ) : DeferredDeeplink data class OnClipIdDeeplink( val clipId: Id, val commentId: Id? = null, ) : DeferredDeeplink data class OnPlaylistIdDeeplink( val playlistId: String, val fromInternalSource: Boolean, val styleTag: String? = null, ) : DeferredDeeplink data class OnHookIdDeeplink( val hookId: Id, val commentId: Id? = null, ) : DeferredDeeplink }