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) + } } }