From 335fd306fac3579cfc0efa13a08bf87d7eb1cb9a Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 29 Mar 2024 14:39:54 +0100 Subject: [PATCH 1/9] Add a confetti deep link for Android As specified here https://developer.android.com/training/app-links Handles links that look like `confetti://conference/{conferenceId}`. --- androidApp/src/main/AndroidManifest.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index 16a48d6d4..a5372f080 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -74,6 +74,16 @@ + + + + + + + From 8f7016a7ed61bb3581347fa17ec42fa2cf3e4f2e Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 29 Mar 2024 14:41:01 +0100 Subject: [PATCH 2/9] Add initial conference ID to AppComponent As described in the decompose docs about deep links https://arkivanov.github.io/Decompose/navigation/stack/deeplinking/#handling-deep-links --- .../confetti/decompose/AppComponent.kt | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) 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) + } } } From e284f8d034fd985c28a5e33b8a05736afc2c5ada Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 29 Mar 2024 14:50:33 +0100 Subject: [PATCH 3/9] Extract the conferenceId from the deep link and pass to DefaultAppComponent --- .../java/dev/johnoreilly/confetti/MainActivity.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/androidApp/src/main/java/dev/johnoreilly/confetti/MainActivity.kt b/androidApp/src/main/java/dev/johnoreilly/confetti/MainActivity.kt index 4a70effeb..b81fec964 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 @@ -57,6 +58,7 @@ class MainActivity : ComponentActivity() { val appComponent = DefaultAppComponent( componentContext = defaultComponentContext(), + initialConferenceId = intent.data?.extractConferenceIdOrNull(), onSignOut = { lifecycleScope.launch { credentialManager.clearCredentialState(ClearCredentialStateRequest()) @@ -86,6 +88,18 @@ class MainActivity : ComponentActivity() { } } } + + /** + * From a deep link like `confetti://conference/devfeststockholm2023` extracts `devfeststockholm2023` + */ + private fun Uri.extractConferenceIdOrNull(): String? { + if (host != "conference") return null + val path = path ?: return null + if (path.firstOrNull() != '/') return null + val conferenceId = path.substring(1) + if (!conferenceId.all { it.isLetterOrDigit() }) return null + return conferenceId + } } @Composable From 6e0602f906d9ccfee1231ee19bd09d9b134bab27 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 29 Mar 2024 14:50:50 +0100 Subject: [PATCH 4/9] Pass a nil initialConferenceId for the iOS client --- iosApp/iosApp/AppDelegate.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 ) } } From 4e3f302512683519a181d6e0acb9d399e6fac166 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Fri, 29 Mar 2024 17:38:10 +0100 Subject: [PATCH 5/9] add .well_known file --- androidApp/src/main/AndroidManifest.xml | 1 - landing-page/public/.well_known/assetlinks.json | 11 +++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 landing-page/public/.well_known/assetlinks.json diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index a5372f080..323952a0f 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -81,7 +81,6 @@ diff --git a/landing-page/public/.well_known/assetlinks.json b/landing-page/public/.well_known/assetlinks.json new file mode 100644 index 000000000..492e34b6b --- /dev/null +++ b/landing-page/public/.well_known/assetlinks.json @@ -0,0 +1,11 @@ +[{ + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "dev.johnoreilly.confetti", + "sha256_cert_fingerprints": + [ + "13:42:59:D2:F2:27:A3:BC:A9:A6:42:BD:CE:B9:FE:A8:60:84:F6:5C:F6:4A:4F:22:A7:EE:D8:31:AC:C1:A1:10" + ] + } +}] From 2d4535fcbf36f3bcb27c6313abb358747fc10231 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Fri, 29 Mar 2024 17:40:07 +0100 Subject: [PATCH 6/9] Revert "add .well_known file" I'll make a separate PR This reverts commit 4e3f302512683519a181d6e0acb9d399e6fac166. --- androidApp/src/main/AndroidManifest.xml | 1 + landing-page/public/.well_known/assetlinks.json | 11 ----------- 2 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 landing-page/public/.well_known/assetlinks.json diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index 323952a0f..a5372f080 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -81,6 +81,7 @@ diff --git a/landing-page/public/.well_known/assetlinks.json b/landing-page/public/.well_known/assetlinks.json deleted file mode 100644 index 492e34b6b..000000000 --- a/landing-page/public/.well_known/assetlinks.json +++ /dev/null @@ -1,11 +0,0 @@ -[{ - "relation": ["delegate_permission/common.handle_all_urls"], - "target": { - "namespace": "android_app", - "package_name": "dev.johnoreilly.confetti", - "sha256_cert_fingerprints": - [ - "13:42:59:D2:F2:27:A3:BC:A9:A6:42:BD:CE:B9:FE:A8:60:84:F6:5C:F6:4A:4F:22:A7:EE:D8:31:AC:C1:A1:10" - ] - } -}] From ea69b1a14c3476f2fd9f3cd12e4e96639f2280fa Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 29 Mar 2024 18:40:20 +0100 Subject: [PATCH 7/9] Update the deep link to use `https://confetti-app.dev/` as the base url Also turn autoVerify to "true", which along with this PR should not require anyone to manually do anything for Confetti to handle those links. --- androidApp/src/main/AndroidManifest.xml | 7 +++---- .../main/java/dev/johnoreilly/confetti/MainActivity.kt | 9 ++++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index a5372f080..9d8bfae10 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -75,14 +75,13 @@ - + + android:scheme="https" + android:host="confetti-app.dev" /> diff --git a/androidApp/src/main/java/dev/johnoreilly/confetti/MainActivity.kt b/androidApp/src/main/java/dev/johnoreilly/confetti/MainActivity.kt index b81fec964..d81c7d8d8 100644 --- a/androidApp/src/main/java/dev/johnoreilly/confetti/MainActivity.kt +++ b/androidApp/src/main/java/dev/johnoreilly/confetti/MainActivity.kt @@ -90,13 +90,16 @@ class MainActivity : ComponentActivity() { } /** - * From a deep link like `confetti://conference/devfeststockholm2023` extracts `devfeststockholm2023` + * From a deep link like `https://confetti-app.dev/conference/devfeststockholm2023` extracts `devfeststockholm2023` */ private fun Uri.extractConferenceIdOrNull(): String? { - if (host != "conference") return null + if (host != "confetti-app.dev") return null val path = path ?: return null if (path.firstOrNull() != '/') return null - val conferenceId = path.substring(1) + 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 } From e3cc965748c859e7a13fb2820914baf7e46fd054 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Sun, 7 Apr 2024 15:32:48 +0200 Subject: [PATCH 8/9] Discard last saved state if there was a deep link --- .../src/main/java/dev/johnoreilly/confetti/MainActivity.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/androidApp/src/main/java/dev/johnoreilly/confetti/MainActivity.kt b/androidApp/src/main/java/dev/johnoreilly/confetti/MainActivity.kt index d81c7d8d8..4c84be66b 100644 --- a/androidApp/src/main/java/dev/johnoreilly/confetti/MainActivity.kt +++ b/androidApp/src/main/java/dev/johnoreilly/confetti/MainActivity.kt @@ -55,10 +55,13 @@ class MainActivity : ComponentActivity() { // including IME animations WindowCompat.setDecorFitsSystemWindows(window, false) + val initialConferenceId = intent.data?.extractConferenceIdOrNull() val appComponent = DefaultAppComponent( - componentContext = defaultComponentContext(), - initialConferenceId = intent.data?.extractConferenceIdOrNull(), + componentContext = defaultComponentContext( + discardSavedState = initialConferenceId != null, + ), + initialConferenceId = initialConferenceId, onSignOut = { lifecycleScope.launch { credentialManager.clearCredentialState(ClearCredentialStateRequest()) From cd9cbb48d6286b7cb92f4edb7c59dfb6a0fea9b0 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Sun, 7 Apr 2024 20:40:53 +0200 Subject: [PATCH 9/9] Store if we've handled the deep link in savedState This prevents the problem of trying to handle the deep link again after this scenario: We open the app with a deep link We navigate somewhere else in the app Then we go home The process is killed in the background Then we go to the recents screen and we open the app again In this case, the original deep link is delivered again to the app This was breaking the behavior of restoring the state properly when coming back into the app after a process death after having opened the app with a deep link prior to that --- .../dev/johnoreilly/confetti/MainActivity.kt | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/androidApp/src/main/java/dev/johnoreilly/confetti/MainActivity.kt b/androidApp/src/main/java/dev/johnoreilly/confetti/MainActivity.kt index 4c84be66b..0ac634dee 100644 --- a/androidApp/src/main/java/dev/johnoreilly/confetti/MainActivity.kt +++ b/androidApp/src/main/java/dev/johnoreilly/confetti/MainActivity.kt @@ -33,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) @@ -55,7 +57,12 @@ class MainActivity : ComponentActivity() { // including IME animations WindowCompat.setDecorFitsSystemWindows(window, false) - val initialConferenceId = intent.data?.extractConferenceIdOrNull() + 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( @@ -95,7 +102,10 @@ class MainActivity : ComponentActivity() { /** * From a deep link like `https://confetti-app.dev/conference/devfeststockholm2023` extracts `devfeststockholm2023` */ - private fun Uri.extractConferenceIdOrNull(): String? { + 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 @@ -106,6 +116,15 @@ class MainActivity : ComponentActivity() { 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