Skip to content

Commit

Permalink
support deep linking
Browse files Browse the repository at this point in the history
  • Loading branch information
DatL4g committed Apr 30, 2024
1 parent dd1adad commit 43f7ac2
Show file tree
Hide file tree
Showing 14 changed files with 129 additions and 130 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,6 @@ data class Medium(
}

companion object {
private const val SITE_URL = "https://anilist.co/"
private const val SITE_URL = "https://anilist.co/anime/"
}
}
5 changes: 0 additions & 5 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ kotlin {
implementation(libs.aboutlibraries)
implementation(libs.kasechange)

implementation(libs.oidc)
implementation(libs.kache)

implementation("dev.datlag.sheets-compose-dialogs:rating:2.0.0-SNAPSHOT")
Expand Down Expand Up @@ -168,10 +167,6 @@ android {

multiDexEnabled = true
vectorDrawables.useSupportLibrary = true

addManifestPlaceholders(
mapOf("oidcRedirectScheme" to "aniflow")
)
}
packaging {
resources {
Expand Down
20 changes: 20 additions & 0 deletions composeApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,26 @@

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="http" />
<data android:scheme="https" />

<data android:host="anilist.co"/>
<data android:pathPrefix="/anime"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="aniflow" />
<data android:host="anilist" />
</intent-filter>
</activity>

<!-- Trigger Google Play services to install the backported photo picker module. -->
Expand Down
4 changes: 0 additions & 4 deletions composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,13 @@ import dev.datlag.sekret.NativeLoader
import io.github.aakira.napier.DebugAntilog
import io.github.aakira.napier.Napier
import org.kodein.di.*
import org.publicvalue.multiplatform.oidc.appsupport.AndroidCodeAuthFlowFactory

class App : MultiDexApplication(), DIAware {

override val di: DI = DI {
bindSingleton<Context> {
applicationContext
}
bindEagerSingleton<AndroidCodeAuthFlowFactory> {
AndroidCodeAuthFlowFactory(useWebView = false)
}

import(NetworkModule.di)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
package dev.datlag.aniflow

import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.CompositionLocalProvider
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import com.arkivanov.decompose.DefaultComponentContext
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.defaultComponentContext
import com.arkivanov.decompose.handleDeepLink
import com.arkivanov.essenty.backhandler.backHandler
import com.arkivanov.essenty.lifecycle.Lifecycle
import com.arkivanov.essenty.lifecycle.LifecycleOwner
import com.arkivanov.essenty.lifecycle.essentyLifecycle
import com.arkivanov.essenty.statekeeper.stateKeeper
import dev.datlag.aniflow.other.UserHelper
import dev.datlag.aniflow.ui.navigation.RootComponent
import dev.datlag.tooling.compose.launchIO
import dev.datlag.tooling.decompose.lifecycle.LocalLifecycleOwner
import dev.datlag.tooling.safeCast
import io.github.aakira.napier.Napier
import org.kodein.di.DIAware
import org.kodein.di.instance
import org.publicvalue.multiplatform.oidc.appsupport.AndroidCodeAuthFlowFactory

class MainActivity : AppCompatActivity() {

private lateinit var root: RootComponent

@OptIn(ExperimentalDecomposeApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand All @@ -30,10 +43,8 @@ class MainActivity : AppCompatActivity() {
val lifecycleOwner = object : LifecycleOwner {
override val lifecycle: Lifecycle = essentyLifecycle()
}
val factory by di.instance<AndroidCodeAuthFlowFactory>()
factory.registerActivity(this)

val root = RootComponent(
root = RootComponent(
componentContext = DefaultComponentContext(
lifecycle = lifecycleOwner.lifecycle,
backHandler = backHandler()
Expand All @@ -54,4 +65,41 @@ class MainActivity : AppCompatActivity() {
}
}
}

@SuppressLint("MissingSuperCall")
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)

val uri = intent.data ?: return
val itemId = uri.pathSegments?.firstNotNullOfOrNull { it.trim().toIntOrNull() }
if (itemId != null && ::root.isInitialized) {
root.onDeepLink(itemId)
return
}

val accessToken = uri.getFragmentOrQueryParameter("access_token")
if (accessToken.isNullOrBlank()) {
return
}

root.onLogin(
accessToken = accessToken,
expiresIn = uri.getFragmentOrQueryParameter("expires_in")?.toIntOrNull()
)
}

private fun Uri.getFragmentOrQueryParameter(param: String): String? {
return this.fragment.getFragmentParameter(param) ?: getQueryParameter(param)?.ifBlank { null }
}

private fun String?.getFragmentParameter(param: String): String? {
val keys = this?.split("&").orEmpty()
keys.forEach { key ->
val values = key.split("=")
if (values[0] == param) {
return values.getOrNull(1)?.ifBlank { null }
}
}
return null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import dev.datlag.tooling.async.suspendCatching
import io.github.aakira.napier.Napier
import kotlinx.coroutines.flow.map
import org.kodein.di.bindProvider
import org.publicvalue.multiplatform.oidc.OpenIdConnectClient

data object NetworkModule {

Expand Down Expand Up @@ -118,26 +117,12 @@ data object NetworkModule {
crashlytics = nullableFirebaseInstance()?.crashlytics
)
}
bindSingleton<OpenIdConnectClient>(Constants.AniList.Auth.CLIENT) {
OpenIdConnectClient {
endpoints {
baseUrl(Constants.AniList.Auth.BASE_URL) {
authorizationEndpoint = "authorize"
tokenEndpoint = "token"
}
}
clientId = Sekret.anilistClientId(BuildKonfig.packageName)
clientSecret = Sekret.anilistClientSecret(BuildKonfig.packageName)
redirectUri = Constants.AniList.Auth.REDIRECT_URL
}
}
bindSingleton<UserHelper> {
UserHelper(
userSettings = instance(),
appSettings = instance(),
client = instance(Constants.AniList.APOLLO_CLIENT),
authFlowFactory = instance(),
oidc = instance(Constants.AniList.Auth.CLIENT)
clientId = Sekret.anilistClientId(BuildKonfig.packageName)!!
)
}
bindSingleton<Ktorfit.Builder> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package dev.datlag.aniflow.other

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler
import com.apollographql.apollo3.ApolloClient
import com.apollographql.apollo3.api.Optional
import dev.datlag.aniflow.anilist.ViewerMutation
Expand All @@ -11,22 +15,23 @@ import dev.datlag.aniflow.model.safeFirstOrNull
import dev.datlag.aniflow.settings.Settings
import dev.datlag.aniflow.settings.model.AppSettings
import dev.datlag.tooling.async.suspendCatching
import dev.datlag.tooling.compose.withIOContext
import dev.datlag.tooling.compose.withMainContext
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.Clock
import org.publicvalue.multiplatform.oidc.OpenIdConnectClient
import org.publicvalue.multiplatform.oidc.appsupport.CodeAuthFlowFactory
import org.publicvalue.multiplatform.oidc.types.remote.AccessTokenResponse
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

class UserHelper(
private val userSettings: Settings.PlatformUserSettings,
private val appSettings: Settings.PlatformAppSettings,
private val client: ApolloClient,
private val authFlowFactory: CodeAuthFlowFactory,
private val oidc: OpenIdConnectClient
private val clientId: String
) {

val isLoggedIn: Flow<Boolean> = userSettings.isAniListLoggedIn.distinctUntilChanged()
val loginUrl: String = "https://anilist.co/api/v2/oauth/authorize?client_id=$clientId&response_type=token"

private val changedUser: MutableStateFlow<User?> = MutableStateFlow(null)
private val userQuery = client.query(
Expand Down Expand Up @@ -65,26 +70,6 @@ class UserHelper(
)
}

suspend fun login(): Boolean {
if (isLoggedIn.safeFirstOrNull() == true) {
return true
}

val flow = withMainContext {
authFlowFactory.createAuthFlow(oidc)
}

val tokenResult = suspendCatching {
flow.getAccessToken()
}

tokenResult.getOrNull()?.let {
updateStoredToken(it)
}

return tokenResult.isSuccess
}

suspend fun updateAdultSetting(value: Boolean) {
appSettings.setAdultContent(value)
changedUser.emit(
Expand Down Expand Up @@ -127,12 +112,15 @@ class UserHelper(
}
}

private suspend fun updateStoredToken(tokenResponse: AccessTokenResponse) {
suspend fun saveLogin(
accessToken: String,
expiresIn: Int?,
) {
userSettings.setAniListTokens(
access = tokenResponse.access_token,
expires = tokenResponse.expires_in?.let {
Clock.System.now().epochSeconds + it
}?.toInt()
access = accessToken,
expires = expiresIn?.let {
Clock.System.now().plus(it.seconds).epochSeconds.toInt()
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@ import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.predictiveBackAnimation
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.push
import com.arkivanov.decompose.router.stack.*
import dev.datlag.aniflow.common.onRender
import dev.datlag.aniflow.model.ifValueOrNull
import dev.datlag.aniflow.other.UserHelper
import dev.datlag.aniflow.ui.navigation.screen.initial.InitialScreenComponent
import dev.datlag.aniflow.ui.navigation.screen.medium.MediumScreenComponent
import org.kodein.di.DI
import org.kodein.di.instance

class RootComponent(
componentContext: ComponentContext,
override val di: DI
) : Component, ComponentContext by componentContext {

private val userHelper by instance<UserHelper>()
private val navigation = StackNavigation<RootConfig>()
private val stack = childStack(
source = navigation,
Expand Down Expand Up @@ -70,4 +71,14 @@ class RootComponent(
}
}
}

fun onDeepLink(mediumId: Int) {
navigation.replaceAll(RootConfig.Home, RootConfig.Details(mediumId))
}

fun onLogin(accessToken: String, expiresIn: Int?) {
launchIO {
userHelper.saveLogin(accessToken, expiresIn)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ sealed class RootConfig {
data object Home : RootConfig()

@Serializable
data class Details(val medium: Medium) : RootConfig()
data class Details(val medium: Medium) : RootConfig() {
constructor(id: Int) : this(Medium(id))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import dev.datlag.aniflow.anilist.model.Medium
import dev.datlag.aniflow.common.preferred
import dev.datlag.aniflow.settings.Settings
import dev.datlag.aniflow.settings.model.AppSettings
import dev.datlag.aniflow.ui.theme.LocalDominantColorState
import dev.datlag.aniflow.ui.theme.SchemeTheme
import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle
import kotlinx.coroutines.flow.Flow
Expand All @@ -36,8 +35,6 @@ fun AiringCard(
modifier: Modifier = Modifier,
onClick: (Medium) -> Unit
) {
val schemeState = LocalDominantColorState.current

airing.media?.let(::Medium)?.let { media ->
Card(
modifier = modifier,
Expand Down Expand Up @@ -65,27 +62,15 @@ fun AiringCard(
model = media.coverImage.medium,
contentScale = ContentScale.Crop,
onSuccess = { state ->
if (schemeState != null) {
scope.launch {
schemeState.updateFrom(state.painter)
}
}

}
),
onSuccess = { state ->
if (schemeState != null) {
scope.launch {
schemeState.updateFrom(state.painter)
}
}

}
),
onSuccess = { state ->
if (schemeState != null) {
scope.launch {
schemeState.updateFrom(state.painter)
}
}

}
)
Column(
Expand Down
Loading

0 comments on commit 43f7ac2

Please sign in to comment.