Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ cd mobile-sync
```kotlin
import com.quran.shared.auth.di.AuthFlowFactoryProvider
import com.quran.shared.persistence.DriverFactory
import com.quran.shared.pipeline.AppEnvironment
import com.quran.shared.pipeline.di.SharedDependencyGraph
import com.quran.shared.syncengine.SynchronizationEnvironment
import org.publicvalue.multiplatform.oidc.appsupport.AndroidCodeAuthFlowFactory

val authFactory = AndroidCodeAuthFlowFactory(useWebView = false)
Expand All @@ -94,9 +94,7 @@ AuthFlowFactoryProvider.initialize(authFactory)

val graph = SharedDependencyGraph.init(
driverFactory = DriverFactory(context = applicationContext),
environment = SynchronizationEnvironment(
endPointURL = "https://apis-prelive.quran.foundation/auth"
)
appEnvironment = AppEnvironment.PRELIVE
)

val authService = graph.authService
Expand All @@ -117,17 +115,29 @@ final class AppContainer {
private init() {
Shared.AuthFlowFactoryProvider.shared.doInitialize()
let driverFactory = DriverFactory()
let environment = SynchronizationEnvironment(
endPointURL: "https://apis-prelive.quran.foundation/auth"
)
graph = SharedDependencyGraph.shared.doInit(
driverFactory: driverFactory,
environment: environment
appEnvironment: AppEnvironment.prelive
)
}
}
```

Advanced override for custom endpoints remains available:

```kotlin
import com.quran.shared.auth.model.AuthEnvironment
import com.quran.shared.syncengine.SynchronizationEnvironment

val graph = SharedDependencyGraph.init(
driverFactory = DriverFactory(context = applicationContext),
environment = SynchronizationEnvironment(
endPointURL = "https://custom-sync.example.com/auth"
),
authEnvironment = AuthEnvironment.PRELIVE
)
```

### 3. Use `SyncService`

Core API examples:
Expand All @@ -139,11 +149,22 @@ Lifecycle note:
- `SyncService` is app-scoped. Initialize once via `SharedDependencyGraph.init(...)`.
- Do not clear app-scoped services from UI/view-model teardown.

### 4. Build type flavor
### 4. App environment selection

The published artifact can target either environment at runtime:

- `AppEnvironment.PRELIVE` / `AppEnvironment.prelive`
- `AppEnvironment.PRODUCTION` / `AppEnvironment.production`

`AppEnvironment` keeps auth and sync aligned by default:

- `PRELIVE` -> `https://prelive-oauth2.quran.foundation` and `https://apis-prelive.quran.foundation/auth`
- `PRODUCTION` -> `https://oauth2.quran.foundation` and `https://apis.quran.foundation/auth`

`buildkonfig.flavor` now only controls the default fallback used when the app does not pass an explicit app environment.

`auth` uses BuildKonfig flavor to expose build type values.
Default fallback (`gradle.properties`):

Default (`gradle.properties`):
```properties
buildkonfig.flavor=debug
```
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.quran.shared.auth.di

import com.quran.shared.auth.BuildKonfig
import com.quran.shared.auth.model.AuthEnvironment
import com.quran.shared.auth.model.AuthConfig
import com.quran.shared.auth.model.defaultAuthConfig
import com.quran.shared.auth.repository.AuthRepository
import com.quran.shared.auth.repository.OidcAuthRepository
import com.quran.shared.di.AppScope
Expand Down Expand Up @@ -49,12 +50,8 @@ abstract class AuthModule {

@Provides
@SingleIn(AppScope::class)
fun provideAuthConfig(): AuthConfig {
return AuthConfig(
usePreProduction = BuildKonfig.IS_DEBUG,
clientId = BuildKonfig.CLIENT_ID,
clientSecret = BuildKonfig.CLIENT_SECRET
)
fun provideAuthConfig(authEnvironment: AuthEnvironment): AuthConfig {
return defaultAuthConfig(authEnvironment)
}

@Provides
Expand All @@ -65,7 +62,7 @@ abstract class AuthModule {
logger = object : Logger {
override fun log(message: String) = println("HTTP Client: $message")
}
level = if (config.usePreProduction) LogLevel.ALL else LogLevel.NONE
level = if (config.environment.enableVerboseLogging) LogLevel.ALL else LogLevel.NONE
}
install(ContentNegotiation) {
json(json)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,45 @@
package com.quran.shared.auth.model

import com.quran.shared.auth.BuildKonfig

enum class AuthEnvironment(
val baseUrl: String,
val enableVerboseLogging: Boolean
) {
PRELIVE(
baseUrl = "https://prelive-oauth2.quran.foundation",
enableVerboseLogging = true
),
PRODUCTION(
baseUrl = "https://oauth2.quran.foundation",
enableVerboseLogging = false
)
}

data class AuthConfig(
val usePreProduction: Boolean = false,
val environment: AuthEnvironment = defaultAuthEnvironment(),
val clientId: String,
val clientSecret: String? = null,
val redirectUri: String = "com.quran.oauth://callback",
val postLogoutRedirectUri: String = "com.quran.oauth://callback",
val scopes: List<String> = listOf("openid", "offline_access", "content", "user", "bookmark", "sync", "collection", "reading_session", "preference", "note")
) {
val baseUrl: String = if (usePreProduction) {
"https://prelive-oauth2.quran.foundation"
} else {
"https://oauth2.quran.foundation"
}

val baseUrl: String = environment.baseUrl
val authorizationEndpoint = "$baseUrl/oauth2/auth"
val tokenEndpoint = "$baseUrl/oauth2/token"
val userinfoEndpoint = "$baseUrl/userinfo"
val endSessionEndpoint = "$baseUrl/oauth2/sessions/logout"
val revocationEndpoint = "$baseUrl/oauth2/revoke"
}
}

fun defaultAuthEnvironment(): AuthEnvironment {
return if (BuildKonfig.IS_DEBUG) AuthEnvironment.PRELIVE else AuthEnvironment.PRODUCTION
}

fun defaultAuthConfig(environment: AuthEnvironment = defaultAuthEnvironment()): AuthConfig {
return AuthConfig(
environment = environment,
clientId = BuildKonfig.CLIENT_ID,
clientSecret = BuildKonfig.CLIENT_SECRET
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.quran.shared.auth.model

import kotlin.test.Test
import kotlin.test.assertEquals

class AuthConfigTest {

@Test
fun `prelive environment uses prelive base url`() {
val config = AuthConfig(
environment = AuthEnvironment.PRELIVE,
clientId = "client-id"
)

assertEquals("https://prelive-oauth2.quran.foundation", config.baseUrl)
assertEquals("https://prelive-oauth2.quran.foundation/oauth2/token", config.tokenEndpoint)
}

@Test
fun `production environment uses production base url`() {
val config = AuthConfig(
environment = AuthEnvironment.PRODUCTION,
clientId = "client-id"
)

assertEquals("https://oauth2.quran.foundation", config.baseUrl)
assertEquals("https://oauth2.quran.foundation/oauth2/token", config.tokenEndpoint)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import androidx.activity.compose.setContent
import com.quran.shared.auth.di.AuthFlowFactoryProvider
import com.quran.shared.demo.android.ui.auth.AuthScreen
import com.quran.shared.persistence.DriverFactory
import com.quran.shared.pipeline.AppEnvironment
import com.quran.shared.pipeline.di.SharedDependencyGraph
import com.quran.shared.demo.android.ui.SyncViewModel

import com.quran.shared.syncengine.SynchronizationEnvironment
import org.publicvalue.multiplatform.oidc.appsupport.AndroidCodeAuthFlowFactory

/**
Expand All @@ -29,9 +28,11 @@ class MainActivity : ComponentActivity() {
private val codeAuthFlowFactory = AndroidCodeAuthFlowFactory(useWebView = false)

private val mainViewModel: SyncViewModel by lazy {
val environment = SynchronizationEnvironment(endPointURL = "https://apis-prelive.quran.foundation/auth")
val driverFactory = DriverFactory(context = this.applicationContext)
val graph = SharedDependencyGraph.init(driverFactory, environment)
val graph = SharedDependencyGraph.init(
driverFactory = driverFactory,
appEnvironment = AppEnvironment.PRELIVE
)

SyncViewModel(graph.authService, graph.syncService)
}
Expand All @@ -55,4 +56,4 @@ class MainActivity : ComponentActivity() {
)
}
}
}
}
5 changes: 1 addition & 4 deletions demo/apple/QuranSyncDemo/QuranSyncDemo/AppContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,10 @@ final class AppContainer {
Shared.AuthFlowFactoryProvider.shared.doInitialize()

let driverFactory = DriverFactory()
let environment = SynchronizationEnvironment(
endPointURL: "https://apis-prelive.quran.foundation/auth"
)

self.graph = SharedDependencyGraph.shared.doInit(
driverFactory: driverFactory,
environment: environment
appEnvironment: AppEnvironment.prelive
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.quran.shared.pipeline

import com.quran.shared.auth.model.AuthEnvironment
import com.quran.shared.auth.model.defaultAuthEnvironment
import com.quran.shared.syncengine.SynchronizationEnvironment

enum class AppEnvironment(
val authEnvironment: AuthEnvironment,
private val syncBaseUrl: String
) {
PRELIVE(
authEnvironment = AuthEnvironment.PRELIVE,
syncBaseUrl = "https://apis-prelive.quran.foundation/auth"
),
PRODUCTION(
authEnvironment = AuthEnvironment.PRODUCTION,
syncBaseUrl = "https://apis.quran.foundation/auth"
);

fun synchronizationEnvironment(): SynchronizationEnvironment {
return SynchronizationEnvironment(endPointURL = syncBaseUrl)
}
}

fun defaultAppEnvironment(): AppEnvironment {
return when (defaultAuthEnvironment()) {
AuthEnvironment.PRELIVE -> AppEnvironment.PRELIVE
AuthEnvironment.PRODUCTION -> AppEnvironment.PRODUCTION
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.quran.shared.pipeline.di

import com.quran.shared.auth.di.AuthModule
import com.quran.shared.auth.model.AuthEnvironment
import com.quran.shared.auth.model.defaultAuthEnvironment
import com.quran.shared.auth.service.AuthService
import com.quran.shared.di.AppScope
import com.quran.shared.persistence.DriverFactory
Expand All @@ -10,7 +12,9 @@ import com.quran.shared.persistence.repository.collection.repository.Collections
import com.quran.shared.persistence.repository.collectionbookmark.repository.CollectionBookmarksRepository
import com.quran.shared.persistence.repository.note.repository.NotesRepository
import com.quran.shared.persistence.repository.recentpage.repository.RecentPagesRepository
import com.quran.shared.pipeline.AppEnvironment
import com.quran.shared.pipeline.SyncService
import com.quran.shared.pipeline.defaultAppEnvironment
import com.quran.shared.syncengine.SynchronizationEnvironment
import dev.zacsweers.metro.DependencyGraph
import dev.zacsweers.metro.Provides
Expand Down Expand Up @@ -46,7 +50,8 @@ interface AppGraph {
fun interface Factory {
fun create(
@Provides driverFactory: DriverFactory,
@Provides environment: SynchronizationEnvironment
@Provides environment: SynchronizationEnvironment,
@Provides authEnvironment: AuthEnvironment
): AppGraph
}
}
Expand All @@ -59,17 +64,34 @@ object SharedDependencyGraph {

private fun doInit(
driverFactory: DriverFactory,
environment: SynchronizationEnvironment
environment: SynchronizationEnvironment,
authEnvironment: AuthEnvironment
): AppGraph {
return createGraphFactory<AppGraph.Factory>()
.create(driverFactory, environment)
.create(driverFactory, environment, authEnvironment)
.also { instance = it }
}

@OptIn(InternalCoroutinesApi::class)
fun init(driverFactory: DriverFactory, environment: SynchronizationEnvironment): AppGraph {
fun init(
driverFactory: DriverFactory,
appEnvironment: AppEnvironment = defaultAppEnvironment()
): AppGraph {
return init(
driverFactory = driverFactory,
environment = appEnvironment.synchronizationEnvironment(),
authEnvironment = appEnvironment.authEnvironment
)
}

@OptIn(InternalCoroutinesApi::class)
fun init(
driverFactory: DriverFactory,
environment: SynchronizationEnvironment,
authEnvironment: AuthEnvironment = defaultAuthEnvironment()
): AppGraph {
return instance ?: synchronized(lock) {
instance ?: doInit(driverFactory, environment)
instance ?: doInit(driverFactory, environment, authEnvironment)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.quran.shared.pipeline

import kotlin.test.Test
import kotlin.test.assertEquals

class AppEnvironmentTest {

@Test
fun `prelive app environment maps to prelive sync endpoint`() {
assertEquals(
"https://apis-prelive.quran.foundation/auth",
AppEnvironment.PRELIVE.synchronizationEnvironment().endPointURL
)
}

@Test
fun `production app environment maps to production sync endpoint`() {
assertEquals(
"https://apis.quran.foundation/auth",
AppEnvironment.PRODUCTION.synchronizationEnvironment().endPointURL
)
}
}