All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.supertokens.sdk.SuperTokensClient.kt Maven / Gradle / Ivy

package com.supertokens.sdk

import com.russhwolf.settings.Settings
import com.supertokens.sdk.recipes.BuildRecipe
import com.supertokens.sdk.recipes.Recipe
import com.supertokens.sdk.recipes.RecipeBuilder
import com.supertokens.sdk.recipes.RecipeConfig
import com.supertokens.sdk.repositories.AuthRepository
import com.supertokens.sdk.repositories.AuthState
import com.supertokens.sdk.repositories.user.UserRepository
import com.supertokens.sdk.repositories.user.UserRepositorySettings
import com.supertokens.sdk.repositories.AuthRepositoryImpl
import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.request.header
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json

internal expect fun getDefaultSettings(): Settings

@SuperTokensDslMarker
class SuperTokensClientConfig(
    val apiBaseUrl: String,
) {

    var tenantId: String? = null

    // Modify the http client config used by the SDK
    var clientConfig: HttpClientConfig<*>.() -> Unit = {}

    var userRepository: UserRepository? = null

    var authRepository: AuthRepository? = null

    var clientName: String = "MyMobileApp"

    val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())

    var recipes: List = emptyList()
        private set

    operator fun BuildRecipe.unaryPlus() {
        recipes = recipes + this
    }

    fun > recipe(builder: RecipeBuilder, configure: C.() -> Unit = {}) {
        +builder.install(configure)
    }

}

class SuperTokensClient(
    private val config: SuperTokensClientConfig,
) {

    internal val scope: CoroutineScope
        get() = config.scope

    val tenantId: String?
        get() = config.tenantId

    val recipes: List> = config.recipes.map { it.invoke(this) }

    @OptIn(ExperimentalSerializationApi::class)
    val apiClient by lazy {
        HttpClient {

            install(ContentNegotiation) {
                json(Json {
                    isLenient = true
                    explicitNulls = false
                    encodeDefaults = true
                    ignoreUnknownKeys = true
                })
            }

            recipes.forEach {
                with(it) {
                    configureClient()
                }
            }

            config.clientConfig(this)

            defaultRequest {
                url(config.apiBaseUrl)
                contentType(ContentType.Application.Json)
                header(HttpHeaders.Origin, config.clientName)
            }
        }
    }


    val userRepository by lazy { config.userRepository ?: UserRepositorySettings(getDefaultSettings()) }
    val authRepository by lazy { config.authRepository ?: AuthRepositoryImpl() }

    private val _isInitialized = MutableStateFlow(false)
    val isInitialized = _isInitialized.asStateFlow()

    inline fun > getRecipe(): T = recipes.filterIsInstance().firstOrNull()
        ?: throw RuntimeException("Recipe ${T::class.simpleName} not configured")

    inline fun > hasRecipe(): Boolean = recipes.filterIsInstance().isNotEmpty()

    /** true, if the the user is at least logged in (but may not be authenticated from the backend yet)
     *  It essentially means, there is a refresh token present, but no access token yet, e.g. during startup
     *  when a new access token hasn't been fetched yet.
     */
    fun isLoggedIn(): Boolean = authRepository.authState.value !is AuthState.Unauthenticated
    // true, if the user was authenticated from the backend (an access token is present)
    fun isAuthenticated():Boolean = authRepository.authState.value is AuthState.Authenticated

    init {
        scope.launch {
            recipes.forEach {  recipe ->
                recipe.postInit()
            }

            _isInitialized.value = true
        }
    }

}

fun superTokensClient(apiBaseUrl: String, init: SuperTokensClientConfig.() -> Unit = {}): SuperTokensClient {
    val config = SuperTokensClientConfig(
        apiBaseUrl = apiBaseUrl,
    ).apply(init)
    return SuperTokensClient(config)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy