diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml
index 16a48d6d4..9d8bfae10 100644
--- a/androidApp/src/main/AndroidManifest.xml
+++ b/androidApp/src/main/AndroidManifest.xml
@@ -74,6 +74,15 @@
+
+
+
+
+
+
+
diff --git a/androidApp/src/main/java/dev/johnoreilly/confetti/MainActivity.kt b/androidApp/src/main/java/dev/johnoreilly/confetti/MainActivity.kt
index 4a70effeb..0ac634dee 100644
--- a/androidApp/src/main/java/dev/johnoreilly/confetti/MainActivity.kt
+++ b/androidApp/src/main/java/dev/johnoreilly/confetti/MainActivity.kt
@@ -2,6 +2,7 @@
package dev.johnoreilly.confetti
+import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -32,6 +33,8 @@ import org.koin.android.ext.android.inject
class MainActivity : ComponentActivity() {
+ private var isDeepLinkHandledPreviously = false
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -54,9 +57,18 @@ class MainActivity : ComponentActivity() {
// including IME animations
WindowCompat.setDecorFitsSystemWindows(window, false)
+ isDeepLinkHandledPreviously = savedInstanceState?.getBoolean(KEY_DEEP_LINK_HANDLED) ?: false
+ val initialConferenceId = intent.data?.extractConferenceIdOrNull(isDeepLinkHandledPreviously)
+ if (initialConferenceId != null) {
+ intent.setData(null)
+ isDeepLinkHandledPreviously = true
+ }
val appComponent =
DefaultAppComponent(
- componentContext = defaultComponentContext(),
+ componentContext = defaultComponentContext(
+ discardSavedState = initialConferenceId != null,
+ ),
+ initialConferenceId = initialConferenceId,
onSignOut = {
lifecycleScope.launch {
credentialManager.clearCredentialState(ClearCredentialStateRequest())
@@ -86,6 +98,33 @@ class MainActivity : ComponentActivity() {
}
}
}
+
+ /**
+ * From a deep link like `https://confetti-app.dev/conference/devfeststockholm2023` extracts `devfeststockholm2023`
+ */
+ private fun Uri.extractConferenceIdOrNull(isDeepLinkHandledPreviously: Boolean): String? {
+ if (isDeepLinkHandledPreviously) {
+ return null
+ }
+ if (host != "confetti-app.dev") return null
+ val path = path ?: return null
+ if (path.firstOrNull() != '/') return null
+ val parts = path.substring(1).split('/')
+ if (parts.size != 2) return null
+ if (parts[0] != "conference") return null
+ val conferenceId = parts[1]
+ if (!conferenceId.all { it.isLetterOrDigit() }) return null
+ return conferenceId
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putBoolean(KEY_DEEP_LINK_HANDLED, isDeepLinkHandledPreviously)
+ }
+
+ companion object {
+ const val KEY_DEEP_LINK_HANDLED: String = "dev.johnoreilly.confetti.KEY_DEEP_LINK_HANDLED"
+ }
}
@Composable
diff --git a/iosApp/iosApp/AppDelegate.swift b/iosApp/iosApp/AppDelegate.swift
index 09aea5aa2..fdf96bd38 100644
--- a/iosApp/iosApp/AppDelegate.swift
+++ b/iosApp/iosApp/AppDelegate.swift
@@ -20,7 +20,8 @@ class AppDelegate : NSObject, UIApplicationDelegate {
componentContext: DefaultComponentContext(lifecycle: ApplicationLifecycle()),
onSignOut: {},
onSignIn: {},
- isMultiPane: UIDevice.current.userInterfaceIdiom != UIUserInterfaceIdiom.phone
+ isMultiPane: UIDevice.current.userInterfaceIdiom != UIUserInterfaceIdiom.phone,
+ initialConferenceId: nil
)
}
}
diff --git a/shared/src/commonMain/kotlin/dev/johnoreilly/confetti/decompose/AppComponent.kt b/shared/src/commonMain/kotlin/dev/johnoreilly/confetti/decompose/AppComponent.kt
index 3a8599c68..14a804d5c 100644
--- a/shared/src/commonMain/kotlin/dev/johnoreilly/confetti/decompose/AppComponent.kt
+++ b/shared/src/commonMain/kotlin/dev/johnoreilly/confetti/decompose/AppComponent.kt
@@ -34,6 +34,7 @@ class DefaultAppComponent(
private val onSignOut: () -> Unit,
private val onSignIn: () -> Unit,
private val isMultiPane: Boolean = false,
+ initialConferenceId: String? = null,
) : AppComponent, KoinComponent, ComponentContext by componentContext {
private val coroutineScope = coroutineScope()
@@ -53,15 +54,22 @@ class DefaultAppComponent(
init {
coroutineScope.launch {
- val conference: String = repository.getConference()
- if (conference == AppSettings.CONFERENCE_NOT_SET) {
- showConferences()
+ if (initialConferenceId != null) {
+ // todo, consider changing how conference theme colors are decided so that only knowing the conference
+ // ID is enough to also get the right color
+ repository.setConference(initialConferenceId, null)
+ showConference(conference = initialConferenceId, conferenceThemeColor = null)
} else {
- val conferenceThemeColor = repository.getConferenceThemeColor()
- showConference(conference = conference, conferenceThemeColor = conferenceThemeColor)
-
- // Take the opportunity to update any listeners of the conference
- repository.updateConfenceListeners(conference, conferenceThemeColor)
+ val conference: String = repository.getConference()
+ if (conference == AppSettings.CONFERENCE_NOT_SET) {
+ showConferences()
+ } else {
+ val conferenceThemeColor = repository.getConferenceThemeColor()
+ showConference(conference = conference, conferenceThemeColor = conferenceThemeColor)
+
+ // Take the opportunity to update any listeners of the conference
+ repository.updateConfenceListeners(conference, conferenceThemeColor)
+ }
}
}