From 23498dd3c3e9ad3328fe2e285cab1943df58bda6 Mon Sep 17 00:00:00 2001
From: Mohamed <mohamed.metwalli@sap.com>
Date: Thu, 4 Mar 2021 16:35:34 +0100
Subject: [PATCH] QR Code scanning to check-in (EXPOSUREAPP-5305) (#2507)

* Setup deep-linking to confirmation

* Update ConfirmCheckInFragment.kt

* Update fragment_confrim_check_in.xml

* Create Uri.kt

* Add navUri extension

* Handle deep-linking in MainActivity

* Check event decoding

* Add scan check-in qr code fragment

* Create viewModel and DI modules

* lint

* Create ViewModel

* Delegate work to view model

* use viewmodel

* Add unit test for navUri

* Adjust data

* Add unit tests for deep-linking

* lint

* Remove + from id references

* Add TODO

* Add unit tests

* Add tests

* add ToDo

* Fix test

* Deep linking for Event Registration (EXPOSUREAPP-5305) (#2502)

* Handle deep-linking and shortcuts

* lint

* Fillin intent in Onboarding

* Lint

* Delete ConfirmCheckInFragment.kt

* Fix test
---
 Corona-Warn-App/build.gradle                  |   1 -
 .../ui/launcher/LauncherActivityTest.kt       | 105 +++++++++++++
 .../java/de/rki/coronawarnapp/util/UriTest.kt |  45 ++++++
 .../java/testhelpers/TestAppComponent.kt      |   4 +-
 .../ui/EventRegistrationTestFragment.kt       |   9 ++
 .../fragment_test_eventregistration.xml       |   6 +
 Corona-Warn-App/src/main/AndroidManifest.xml  |  18 +++
 .../checkins/qrcode/EventQRCode.kt            |  12 +-
 .../EventRegistrationUIModule.kt              |  18 +++
 .../checkin/ConfirmCheckInEvent.kt            |   6 +
 .../checkin/ConfirmCheckInFragment.kt         |  51 ++++++
 .../checkin/ConfirmCheckInModule.kt           |  18 +++
 .../checkin/ConfirmCheckInViewModel.kt        |  44 ++++++
 .../scan/ScanCheckInQrCodeEvent.kt            |   6 +
 .../scan/ScanCheckInQrCodeFragment.kt         | 147 ++++++++++++++++++
 .../scan/ScanCheckInQrCodeModule.kt           |  18 +++
 .../scan/ScanCheckInQrCodeViewModel.kt        |  25 +++
 .../ui/launcher/LauncherActivity.kt           |   5 +-
 .../rki/coronawarnapp/ui/main/MainActivity.kt |  52 ++++---
 .../ui/main/MainActivityModule.kt             |   4 +-
 .../ui/onboarding/OnboardingActivity.kt       |  22 +--
 .../onboarding/OnboardingLoadingFragment.kt   |   2 +-
 .../java/de/rki/coronawarnapp/util/Uri.kt     |  16 ++
 .../util/shortcuts/AppShortcutsHelper.kt      |   9 +-
 .../res/layout/fragment_confrim_check_in.xml  |  54 +++++++
 .../layout/fragment_scan_check_in_qr_code.xml |  96 ++++++++++++
 .../src/main/res/navigation/nav_graph.xml     |  23 ++-
 .../durationpicker/DurationExtensionKtTest.kt |   2 +-
 .../checkin/ConfirmCheckInViewModelTest.kt    |  49 ++++++
 .../scan/ScanCheckInQrCodeViewModelTest.kt    |  42 +++++
 30 files changed, 851 insertions(+), 58 deletions(-)
 create mode 100644 Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityTest.kt
 create mode 100644 Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/UriTest.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInEvent.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInFragment.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInModule.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModel.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeEvent.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeFragment.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeModule.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeViewModel.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/Uri.kt
 create mode 100644 Corona-Warn-App/src/main/res/layout/fragment_confrim_check_in.xml
 create mode 100644 Corona-Warn-App/src/main/res/layout/fragment_scan_check_in_qr_code.xml
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModelTest.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeViewModelTest.kt

diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle
index e7fda93ad..dff24f01a 100644
--- a/Corona-Warn-App/build.gradle
+++ b/Corona-Warn-App/build.gradle
@@ -382,7 +382,6 @@ dependencies {
     androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
     androidTestImplementation 'androidx.test:rules:1.3.0'
     androidTestImplementation 'androidx.test.ext:truth:1.3.0'
-    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
     androidTestImplementation 'androidx.work:work-testing:2.5.0'
     androidTestImplementation "io.mockk:mockk-android:1.10.4"
     debugImplementation 'androidx.fragment:fragment-testing:1.2.5'
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityTest.kt
new file mode 100644
index 000000000..dcc17b0f7
--- /dev/null
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityTest.kt
@@ -0,0 +1,105 @@
+package de.rki.coronawarnapp.ui.launcher
+
+import android.content.Intent
+import android.net.Uri
+import androidx.test.core.app.launchActivity
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import dagger.Module
+import dagger.android.ContributesAndroidInjector
+import de.rki.coronawarnapp.main.CWASettings
+import de.rki.coronawarnapp.storage.LocalData
+import de.rki.coronawarnapp.ui.onboarding.OnboardingActivity
+import de.rki.coronawarnapp.update.UpdateChecker
+import de.rki.coronawarnapp.util.ui.SingleLiveEvent
+import io.mockk.MockKAnnotations
+import io.mockk.Runs
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.spyk
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import testhelpers.BaseUITest
+import testhelpers.TestDispatcherProvider
+
+@RunWith(AndroidJUnit4::class)
+class LauncherActivityTest : BaseUITest() {
+
+    @MockK lateinit var updateChecker: UpdateChecker
+    @MockK lateinit var cwaSettings: CWASettings
+    lateinit var viewModel: LauncherActivityViewModel
+
+    @Before
+    fun setup() {
+        MockKAnnotations.init(this)
+        mockkObject(LocalData)
+        coEvery { updateChecker.checkForUpdate() } returns UpdateChecker.Result(isUpdateNeeded = false)
+        every { LocalData.isOnboarded() } returns false
+        viewModel = launcherActivityViewModel()
+        setupMockViewModel(
+            object : LauncherActivityViewModel.Factory {
+                override fun create(): LauncherActivityViewModel = viewModel
+            }
+        )
+
+        every { viewModel.events } returns mockk<SingleLiveEvent<LauncherEvent>>().apply {
+            every { observe(any(), any()) } just Runs
+        }
+    }
+
+    @After
+    fun teardown() {
+        clearAllViewModels()
+    }
+
+    @Test
+    fun testDeepLinkLowercase() {
+        val uri = Uri.parse("https://coronawarn.app/E1/SOME_PATH_GOES_HERE")
+        launchActivity<LauncherActivity>(getIntent(uri))
+    }
+
+    @Test
+    fun testDeepLinkLowercaseWww() {
+        val uri = Uri.parse("https://www.coronawarn.app/E1/SOME_PATH_GOES_HERE")
+        launchActivity<LauncherActivity>(getIntent(uri))
+    }
+
+    @Test(expected = RuntimeException::class)
+    fun testDeepLinkDoNotOpenOtherLinks() {
+        val uri = Uri.parse("https://www.rki.de")
+        launchActivity<LauncherActivity>(getIntent(uri))
+    }
+
+    @Test(expected = RuntimeException::class)
+    fun testDeepLinkUppercase() {
+        // Host is case sensitive and it should be only in lowercase
+        val uri = Uri.parse("HTTPS://CORONAWARN.APP/E1/SOME_PATH_GOES_HERE")
+        launchActivity<LauncherActivity>(getIntent(uri))
+    }
+
+    private fun getIntent(uri: Uri) = Intent(Intent.ACTION_VIEW, uri).apply {
+        setPackage(InstrumentationRegistry.getInstrumentation().targetContext.packageName)
+        addCategory(Intent.CATEGORY_BROWSABLE)
+        addCategory(Intent.CATEGORY_DEFAULT)
+    }
+
+    private fun launcherActivityViewModel() = spyk(
+        LauncherActivityViewModel(
+            updateChecker,
+            TestDispatcherProvider(),
+            cwaSettings
+        )
+    )
+}
+
+@Module
+abstract class LauncherActivityTestModule {
+    @ContributesAndroidInjector
+    abstract fun launcherActivity(): LauncherActivity
+}
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/UriTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/UriTest.kt
new file mode 100644
index 000000000..300fc6c32
--- /dev/null
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/UriTest.kt
@@ -0,0 +1,45 @@
+package de.rki.coronawarnapp.util
+
+import androidx.core.net.toUri
+import io.kotest.matchers.shouldBe
+import org.junit.Test
+import testhelpers.BaseTestInstrumentation
+
+class UriTest : BaseTestInstrumentation() {
+
+    @Test
+    fun navUriConvertsSchemeAndAuthorityToLowercase() {
+        val uri = "HTTPS://CORONAWARN.APP/E1/SOME_PATH_GOES_HERE".toUri()
+        uri.navUri.toString() shouldBe "https://coronawarn.app/E1/SOME_PATH_GOES_HERE"
+
+        val uri2 = "HTTPS://CORONAWARN.APP/e1/some_path_goes_here".toUri()
+        uri2.navUri.toString() shouldBe "https://coronawarn.app/e1/some_path_goes_here"
+    }
+
+    @Test
+    fun navUriDoesNotChangePath() {
+        val uri = "https://coronawarn.app/E1/SOME_PATH_GOES_HERE".toUri()
+        uri.navUri.toString() shouldBe "https://coronawarn.app/E1/SOME_PATH_GOES_HERE"
+
+        val uri2 = "https://coronawarn.app/e1/some_path_goes_here".toUri()
+        uri2.navUri.toString() shouldBe "https://coronawarn.app/e1/some_path_goes_here"
+    }
+
+    @Test
+    fun navUriConvertsSchemeAndAuthorityToLowercaseWithWWW() {
+        val uri = "HTTPS://WWW.CORONAWARN.APP/E1/SOME_PATH_GOES_HERE".toUri()
+        uri.navUri.toString() shouldBe "https://www.coronawarn.app/E1/SOME_PATH_GOES_HERE"
+
+        val uri2 = "HTTPS://WWW.CORONAWARN.APP/e1/some_path_goes_here".toUri()
+        uri2.navUri.toString() shouldBe "https://www.coronawarn.app/e1/some_path_goes_here"
+    }
+
+    @Test
+    fun navUriDoesNotChangePathWithWWW() {
+        val uri = "https://www.coronawarn.app/E1/SOME_PATH_GOES_HERE".toUri()
+        uri.navUri.toString() shouldBe "https://www.coronawarn.app/E1/SOME_PATH_GOES_HERE"
+
+        val uri2 = "https://www.coronawarn.app/e1/some_path_goes_here".toUri()
+        uri2.navUri.toString() shouldBe "https://www.coronawarn.app/e1/some_path_goes_here"
+    }
+}
diff --git a/Corona-Warn-App/src/androidTest/java/testhelpers/TestAppComponent.kt b/Corona-Warn-App/src/androidTest/java/testhelpers/TestAppComponent.kt
index 67c9eb031..a5975ccf6 100644
--- a/Corona-Warn-App/src/androidTest/java/testhelpers/TestAppComponent.kt
+++ b/Corona-Warn-App/src/androidTest/java/testhelpers/TestAppComponent.kt
@@ -4,6 +4,7 @@ import dagger.BindsInstance
 import dagger.Component
 import dagger.android.AndroidInjector
 import dagger.android.support.AndroidSupportInjectionModule
+import de.rki.coronawarnapp.ui.launcher.LauncherActivityTestModule
 import de.rki.coronawarnapp.ui.main.MainActivityTestModule
 import testhelpers.viewmodels.MockViewModelModule
 import javax.inject.Singleton
@@ -14,7 +15,8 @@ import javax.inject.Singleton
         MockViewModelModule::class,
         FragmentTestModuleRegistrar::class,
         TestAndroidModule::class,
-        MainActivityTestModule::class
+        MainActivityTestModule::class,
+        LauncherActivityTestModule::class,
     ]
 )
 @Singleton
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt
index de69184c9..0079ee6f3 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt
@@ -1,7 +1,10 @@
 package de.rki.coronawarnapp.test.eventregistration.ui
 
 import android.annotation.SuppressLint
+import android.os.Bundle
+import android.view.View
 import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.databinding.FragmentTestEventregistrationBinding
 import de.rki.coronawarnapp.test.menu.ui.TestMenuItem
@@ -19,6 +22,12 @@ class EventRegistrationTestFragment : Fragment(R.layout.fragment_test_eventregis
 
     private val binding: FragmentTestEventregistrationBinding by viewBindingLazy()
 
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        binding.scanCheckInQrCode.setOnClickListener {
+            findNavController().navigate(R.id.scanCheckInQrCodeFragment)
+        }
+    }
+
     companion object {
         val MENU_ITEM = TestMenuItem(
             title = "Event Registration",
diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml
index a80d0f83d..3fc1d7d83 100644
--- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml
+++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml
@@ -27,6 +27,12 @@
                 android:layout_height="wrap_content"
                 android:text="Event registration" />
 
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/scanCheckInQrCode"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="Scan check in QR code" />
+
         </LinearLayout>
 
     </LinearLayout>
diff --git a/Corona-Warn-App/src/main/AndroidManifest.xml b/Corona-Warn-App/src/main/AndroidManifest.xml
index 8c7a9babc..e70468167 100644
--- a/Corona-Warn-App/src/main/AndroidManifest.xml
+++ b/Corona-Warn-App/src/main/AndroidManifest.xml
@@ -54,12 +54,30 @@
 
         <activity
             android:name=".ui.launcher.LauncherActivity"
+            android:exported="true"
             android:screenOrientation="portrait"
             android:theme="@style/AppTheme.Launcher">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
+
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+
+                <data
+                    android:host="coronawarn.app"
+                    android:pathPrefix="/"
+                    android:scheme="https" />
+
+                <data
+                    android:host="www.coronawarn.app"
+                    android:pathPrefix="/"
+                    android:scheme="https" />
+            </intent-filter>
         </activity>
         <activity
             android:name=".ui.main.MainActivity"
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/EventQRCode.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/EventQRCode.kt
index 20abf0a9b..69edb248e 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/EventQRCode.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/EventQRCode.kt
@@ -1,6 +1,10 @@
 package de.rki.coronawarnapp.eventregistration.checkins.qrcode
 
-@Suppress("EmptyClassBlock")
-interface EventQRCode {
-    // TODO
-}
+import org.joda.time.Instant
+
+data class EventQRCode(
+    val guid: String,
+    val description: String? = null,
+    val start: Instant? = null,
+    val end: Instant? = null,
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt
new file mode 100644
index 000000000..e4e97fea4
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt
@@ -0,0 +1,18 @@
+package de.rki.coronawarnapp.ui.eventregistration
+
+import dagger.Module
+import dagger.android.ContributesAndroidInjector
+import de.rki.coronawarnapp.ui.eventregistration.checkin.ConfirmCheckInFragment
+import de.rki.coronawarnapp.ui.eventregistration.checkin.ConfirmCheckInModule
+import de.rki.coronawarnapp.ui.eventregistration.scan.ScanCheckInQrCodeFragment
+import de.rki.coronawarnapp.ui.eventregistration.scan.ScanCheckInQrCodeModule
+
+@Module
+internal abstract class EventRegistrationUIModule {
+
+    @ContributesAndroidInjector(modules = [ScanCheckInQrCodeModule::class])
+    abstract fun scanCheckInQrCodeFragment(): ScanCheckInQrCodeFragment
+
+    @ContributesAndroidInjector(modules = [ConfirmCheckInModule::class])
+    abstract fun confirmCheckInFragment(): ConfirmCheckInFragment
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInEvent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInEvent.kt
new file mode 100644
index 000000000..120c3ce22
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInEvent.kt
@@ -0,0 +1,6 @@
+package de.rki.coronawarnapp.ui.eventregistration.checkin
+
+sealed class ConfirmCheckInEvent {
+    object BackEvent : ConfirmCheckInEvent()
+    object ConfirmEvent : ConfirmCheckInEvent()
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInFragment.kt
new file mode 100644
index 000000000..c1389bd41
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInFragment.kt
@@ -0,0 +1,51 @@
+package de.rki.coronawarnapp.ui.eventregistration.checkin
+
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.navArgs
+import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.databinding.FragmentConfrimCheckInBinding
+import de.rki.coronawarnapp.util.di.AutoInject
+import de.rki.coronawarnapp.util.ui.observe2
+import de.rki.coronawarnapp.util.ui.popBackStack
+import de.rki.coronawarnapp.util.ui.viewBindingLazy
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
+import de.rki.coronawarnapp.util.viewmodel.cwaViewModels
+import javax.inject.Inject
+
+class ConfirmCheckInFragment : Fragment(R.layout.fragment_confrim_check_in), AutoInject {
+
+    @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
+    private val viewModel: ConfirmCheckInViewModel by cwaViewModels { viewModelFactory }
+
+    private val binding: FragmentConfrimCheckInBinding by viewBindingLazy()
+    private val args by navArgs<ConfirmCheckInFragmentArgs>()
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        with(binding) {
+            toolbar.setNavigationOnClickListener { viewModel.onClose() }
+            confirmButton.setOnClickListener { viewModel.onConfirmEvent() }
+        }
+
+        viewModel.decodeEvent(args.encodedEvent)
+        viewModel.navigationEvents.observe2(this) { navEvent ->
+            when (navEvent) {
+                ConfirmCheckInEvent.BackEvent -> popBackStack()
+                ConfirmCheckInEvent.ConfirmEvent -> popBackStack() // TODO Do something else
+            }
+        }
+
+        // TODO bind data to actual UI
+        viewModel.eventData.observe2(this) {
+            with(binding) {
+                eventGuid.text = "GUID: %s".format(it.guid)
+                startTime.text = "Start time: %s".format(it.start)
+                endTime.text = "End time: %s".format(it.end)
+                description.text = "Description: %s".format(it.description)
+            }
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInModule.kt
new file mode 100644
index 000000000..46d8d5282
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInModule.kt
@@ -0,0 +1,18 @@
+package de.rki.coronawarnapp.ui.eventregistration.checkin
+
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey
+
+@Module
+abstract class ConfirmCheckInModule {
+    @Binds
+    @IntoMap
+    @CWAViewModelKey(ConfirmCheckInViewModel::class)
+    abstract fun confirmCheckInFragment(
+        factory: ConfirmCheckInViewModel.Factory
+    ): CWAViewModelFactory<out CWAViewModel>
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModel.kt
new file mode 100644
index 000000000..5bcac425f
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModel.kt
@@ -0,0 +1,44 @@
+package de.rki.coronawarnapp.ui.eventregistration.checkin
+
+import androidx.lifecycle.MutableLiveData
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.EventQRCode
+import de.rki.coronawarnapp.eventregistration.common.decodeBase32
+import de.rki.coronawarnapp.server.protocols.internal.evreg.EventOuterClass
+import de.rki.coronawarnapp.ui.SingleLiveEvent
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
+import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
+import org.joda.time.Instant
+
+class ConfirmCheckInViewModel @AssistedInject constructor() : CWAViewModel() {
+    private val eventLiveData = MutableLiveData<EventQRCode>()
+    val eventData = eventLiveData
+    val navigationEvents = SingleLiveEvent<ConfirmCheckInEvent>()
+
+    fun decodeEvent(encodedEvent: String) = launch {
+        // TODO Verify event(EXPOSUREAPP-5423)
+        //  and finalise event parsing logic
+        val decodedEventString = encodedEvent.split(".")[0].decodeBase32()
+        val parseEvent = EventOuterClass.Event.parseFrom(decodedEventString.toByteArray())
+        eventLiveData.postValue(parseEvent.toEventQrCode())
+    }
+
+    fun onClose() {
+        navigationEvents.value = ConfirmCheckInEvent.BackEvent
+    }
+
+    fun onConfirmEvent() {
+        navigationEvents.value = ConfirmCheckInEvent.ConfirmEvent
+    }
+
+    private fun EventOuterClass.Event.toEventQrCode() = EventQRCode(
+        guid = String(guid.toByteArray()),
+        description = description,
+        start = Instant.ofEpochMilli(start.toLong()),
+        end = Instant.ofEpochMilli(end.toLong())
+    )
+
+    @AssistedFactory
+    interface Factory : SimpleCWAViewModelFactory<ConfirmCheckInViewModel>
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeEvent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeEvent.kt
new file mode 100644
index 000000000..d833a241f
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeEvent.kt
@@ -0,0 +1,6 @@
+package de.rki.coronawarnapp.ui.eventregistration.scan
+
+sealed class ScanCheckInQrCodeEvent {
+    object BackEvent : ScanCheckInQrCodeEvent()
+    data class ConfirmCheckInEvent(val url: String) : ScanCheckInQrCodeEvent()
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeFragment.kt
new file mode 100644
index 000000000..cdf5b1045
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeFragment.kt
@@ -0,0 +1,147 @@
+package de.rki.coronawarnapp.ui.eventregistration.scan
+
+import android.Manifest
+import android.content.pm.PackageManager
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import android.view.View
+import android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT
+import androidx.core.net.toUri
+import androidx.navigation.fragment.findNavController
+import com.google.zxing.BarcodeFormat
+import com.journeyapps.barcodescanner.DefaultDecoderFactory
+import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.databinding.FragmentScanCheckInQrCodeBinding
+import de.rki.coronawarnapp.util.CameraPermissionHelper
+import de.rki.coronawarnapp.util.DialogHelper
+import de.rki.coronawarnapp.util.di.AutoInject
+import de.rki.coronawarnapp.util.navUri
+import de.rki.coronawarnapp.util.ui.observe2
+import de.rki.coronawarnapp.util.ui.popBackStack
+import de.rki.coronawarnapp.util.ui.viewBindingLazy
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
+import de.rki.coronawarnapp.util.viewmodel.cwaViewModels
+import javax.inject.Inject
+
+class ScanCheckInQrCodeFragment :
+    Fragment(R.layout.fragment_scan_check_in_qr_code),
+    AutoInject {
+
+    @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
+    private val viewModel: ScanCheckInQrCodeViewModel by cwaViewModels { viewModelFactory }
+
+    private val binding: FragmentScanCheckInQrCodeBinding by viewBindingLazy()
+    private var showsPermissionDialog = false
+
+    override fun onViewCreated(
+        view: View,
+        savedInstanceState: Bundle?
+    ) {
+        with(binding) {
+            checkInQrCodeScanTorch.setOnCheckedChangeListener { _, isChecked ->
+                binding.checkInQrCodeScanPreview.setTorch(isChecked)
+            }
+            checkInQrCodeScanClose.setOnClickListener { viewModel.onNavigateUp() }
+            checkInQrCodeScanPreview.decoderFactory = DefaultDecoderFactory(listOf(BarcodeFormat.QR_CODE))
+            checkInQrCodeScanViewfinderView.setCameraPreview(binding.checkInQrCodeScanPreview)
+        }
+
+        viewModel.navigationEvents.observe2(this) { navEvent ->
+            when (navEvent) {
+                is ScanCheckInQrCodeEvent.BackEvent -> popBackStack()
+                is ScanCheckInQrCodeEvent.ConfirmCheckInEvent -> findNavController().navigate(
+                    navEvent.url.toUri().navUri
+                )
+            }
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        binding.checkInQrCodeScanContainer.sendAccessibilityEvent(TYPE_ANNOUNCEMENT)
+        if (CameraPermissionHelper.hasCameraPermission(requireActivity())) {
+            binding.checkInQrCodeScanPreview.resume()
+            startDecode()
+            return
+        }
+        if (showsPermissionDialog) return
+
+        requestCameraPermission()
+    }
+
+    override fun onRequestPermissionsResult(
+        requestCode: Int,
+        permissions: Array<String>,
+        grantResults: IntArray
+    ) {
+        if (requestCode == REQUEST_CAMERA_PERMISSION_CODE &&
+            grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_DENIED
+        ) {
+            if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
+                showCameraPermissionRationaleDialog()
+            } else {
+                // User permanently denied access to the camera
+                showCameraPermissionDeniedDialog()
+            }
+        }
+    }
+
+    private fun startDecode() = binding.checkInQrCodeScanPreview
+        .decodeSingle { barcodeResult ->
+            viewModel.onScanResult(barcodeResult)
+        }
+
+    private fun showCameraPermissionDeniedDialog() {
+        val permissionDeniedDialog = DialogHelper.DialogInstance(
+            requireActivity(),
+            // TODO use strings for this screen
+            R.string.submission_qr_code_scan_permission_denied_dialog_headline,
+            R.string.submission_qr_code_scan_permission_denied_dialog_body,
+            R.string.submission_qr_code_scan_permission_denied_dialog_button,
+            cancelable = false,
+            positiveButtonFunction = {
+                showsPermissionDialog = false
+                viewModel.onNavigateUp()
+            }
+        )
+        showsPermissionDialog = true
+        DialogHelper.showDialog(permissionDeniedDialog)
+    }
+
+    private fun showCameraPermissionRationaleDialog() {
+        val cameraPermissionRationaleDialogInstance = DialogHelper.DialogInstance(
+            requireActivity(),
+            // TODO use strings for this screen
+            R.string.submission_qr_code_scan_permission_rationale_dialog_headline,
+            R.string.submission_qr_code_scan_permission_rationale_dialog_body,
+            R.string.submission_qr_code_scan_permission_rationale_dialog_button_positive,
+            R.string.submission_qr_code_scan_permission_rationale_dialog_button_negative,
+            false,
+            {
+                showsPermissionDialog = false
+                requestCameraPermission()
+            },
+            {
+                showsPermissionDialog = false
+                viewModel.onNavigateUp()
+            }
+        )
+
+        showsPermissionDialog = true
+        DialogHelper.showDialog(cameraPermissionRationaleDialogInstance)
+    }
+
+    private fun requestCameraPermission() = requestPermissions(
+        arrayOf(Manifest.permission.CAMERA),
+        REQUEST_CAMERA_PERMISSION_CODE
+    )
+
+    override fun onPause() {
+        super.onPause()
+        binding.checkInQrCodeScanPreview.pause()
+    }
+
+    companion object {
+        private const val REQUEST_CAMERA_PERMISSION_CODE = 4000
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeModule.kt
new file mode 100644
index 000000000..64d7a0813
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeModule.kt
@@ -0,0 +1,18 @@
+package de.rki.coronawarnapp.ui.eventregistration.scan
+
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey
+
+@Module
+abstract class ScanCheckInQrCodeModule {
+    @Binds
+    @IntoMap
+    @CWAViewModelKey(ScanCheckInQrCodeViewModel::class)
+    abstract fun scanCheckInQrCodeFragment(
+        factory: ScanCheckInQrCodeViewModel.Factory
+    ): CWAViewModelFactory<out CWAViewModel>
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeViewModel.kt
new file mode 100644
index 000000000..a9f29b9d3
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeViewModel.kt
@@ -0,0 +1,25 @@
+package de.rki.coronawarnapp.ui.eventregistration.scan
+
+import com.journeyapps.barcodescanner.BarcodeResult
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import de.rki.coronawarnapp.ui.SingleLiveEvent
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
+import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
+
+class ScanCheckInQrCodeViewModel @AssistedInject constructor() : CWAViewModel() {
+    val navigationEvents = SingleLiveEvent<ScanCheckInQrCodeEvent>()
+
+    fun onNavigateUp() {
+        navigationEvents.value = ScanCheckInQrCodeEvent.BackEvent
+    }
+
+    fun onScanResult(barcodeResult: BarcodeResult) {
+        navigationEvents.value = ScanCheckInQrCodeEvent.ConfirmCheckInEvent(
+            barcodeResult.result.text
+        )
+    }
+
+    @AssistedFactory
+    interface Factory : SimpleCWAViewModelFactory<ScanCheckInQrCodeViewModel>
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivity.kt
index 90c3fe4bc..27a61dabf 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivity.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivity.kt
@@ -9,7 +9,6 @@ import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.ui.main.MainActivity
 import de.rki.coronawarnapp.ui.onboarding.OnboardingActivity
 import de.rki.coronawarnapp.util.di.AppInjector
-import de.rki.coronawarnapp.util.shortcuts.AppShortcutsHelper
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
 import de.rki.coronawarnapp.util.viewmodel.cwaViewModels
 import javax.inject.Inject
@@ -30,12 +29,12 @@ class LauncherActivity : AppCompatActivity() {
         vm.events.observe(this) {
             when (it) {
                 LauncherEvent.GoToOnboarding -> {
-                    OnboardingActivity.start(this, AppShortcutsHelper.getShortcutType(intent))
+                    OnboardingActivity.start(this, intent)
                     this.overridePendingTransition(0, 0)
                     finish()
                 }
                 LauncherEvent.GoToMainActivity -> {
-                    MainActivity.start(this, AppShortcutsHelper.getShortcutType(intent))
+                    MainActivity.start(this, intent)
                     this.overridePendingTransition(0, 0)
                     finish()
                 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt
index f6984c21e..4c5cc0b68 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt
@@ -31,11 +31,14 @@ import de.rki.coronawarnapp.util.ConnectivityHelper
 import de.rki.coronawarnapp.util.DialogHelper
 import de.rki.coronawarnapp.util.device.PowerManagement
 import de.rki.coronawarnapp.util.di.AppInjector
+import de.rki.coronawarnapp.util.navUri
+import de.rki.coronawarnapp.util.shortcuts.AppShortcutsHelper.Companion.getShortcutExtra
 import de.rki.coronawarnapp.util.ui.findNavController
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
 import de.rki.coronawarnapp.util.viewmodel.cwaViewModels
 import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
 import org.joda.time.LocalDate
+import timber.log.Timber
 import javax.inject.Inject
 
 /**
@@ -47,24 +50,14 @@ import javax.inject.Inject
  */
 class MainActivity : AppCompatActivity(), HasAndroidInjector {
     companion object {
-        private const val EXTRA_DATA = "shortcut"
-
-        fun start(context: Context, shortcut: AppShortcuts? = null) {
-            val intent = Intent(context, MainActivity::class.java).apply {
-                if (shortcut != null) {
-                    putExtra(EXTRA_DATA, shortcut.toString())
-                    flags = flags or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
-                }
+        fun start(context: Context, launchIntent: Intent) {
+            Intent(context, MainActivity::class.java).apply {
+                flags = flags or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
+                Timber.i("launchIntent:$launchIntent")
+                fillIn(launchIntent, Intent.FILL_IN_DATA)
+                Timber.i("filledIntent:$this")
+                context.startActivity(this)
             }
-            context.startActivity(intent)
-        }
-
-        private fun getShortcutFromIntent(intent: Intent): AppShortcuts? {
-            val extra = intent.getStringExtra(EXTRA_DATA)
-            if (extra != null) {
-                return AppShortcuts.valueOf(extra)
-            }
-            return null
         }
     }
 
@@ -80,6 +73,8 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector {
     private val FragmentManager.currentNavigationFragment: Fragment?
         get() = primaryNavigationFragment?.childFragmentManager?.fragments?.first()
 
+    private val navController by lazy { supportFragmentManager.findNavController(R.id.nav_host_fragment) }
+
     @Inject lateinit var powerManagement: PowerManagement
     @Inject lateinit var deadmanScheduler: DeadmanNotificationScheduler
     @Inject lateinit var contactDiaryWorkScheduler: ContactDiaryWorkScheduler
@@ -105,7 +100,6 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector {
             showEnergyOptimizedEnabledForBackground()
         }
 
-        val navController = supportFragmentManager.findNavController(R.id.nav_host_fragment)
         binding.mainBottomNavigation.setupWithNavController2(navController) {
             vm.onBottomNavSelected()
         }
@@ -118,16 +112,21 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector {
         }
     }
 
+    override fun onNewIntent(intent: Intent?) {
+        super.onNewIntent(intent)
+        Timber.i("onNewIntent:$intent")
+        navigateByIntentUri(intent)
+    }
+
     private fun processExtraParameters() {
-        when (getShortcutFromIntent(intent)) {
-            AppShortcuts.CONTACT_DIARY -> {
-                goToContactJournal()
-            }
+        when (intent.getShortcutExtra()) {
+            AppShortcuts.CONTACT_DIARY -> goToContactJournal()
         }
+
+        navigateByIntentUri(intent)
     }
 
     private fun goToContactJournal() {
-        val navController = supportFragmentManager.findNavController(R.id.nav_host_fragment)
         findViewById<BottomNavigationView>(R.id.main_bottom_navigation).selectedItemId =
             R.id.contact_diary_nav_graph
         val nestedGraph = navController.graph.findNode(R.id.contact_diary_nav_graph) as NavGraph
@@ -154,6 +153,13 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector {
         }
     }
 
+    private fun navigateByIntentUri(intent: Intent?) {
+        val uri = intent?.data ?: return
+        Timber.i("Uri:$uri")
+        Timber.i("NavUri:%s", uri.navUri)
+        navController.navigate(uri.navUri)
+    }
+
     /**
      * Register callbacks.
      */
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt
index 72222d127..dc98543bb 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt
@@ -8,6 +8,7 @@ import de.rki.coronawarnapp.datadonation.analytics.ui.AnalyticsUIModule
 import de.rki.coronawarnapp.release.NewReleaseInfoFragment
 import de.rki.coronawarnapp.release.NewReleaseInfoFragmentModule
 import de.rki.coronawarnapp.tracing.ui.details.TracingDetailsFragmentModule
+import de.rki.coronawarnapp.ui.eventregistration.EventRegistrationUIModule
 import de.rki.coronawarnapp.ui.information.InformationFragmentModule
 import de.rki.coronawarnapp.ui.interoperability.InteroperabilityConfigurationFragment
 import de.rki.coronawarnapp.ui.interoperability.InteroperabilityConfigurationFragmentModule
@@ -32,7 +33,8 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey
         SubmissionFragmentModule::class,
         InformationFragmentModule::class,
         NewReleaseInfoFragmentModule::class,
-        AnalyticsUIModule::class
+        AnalyticsUIModule::class,
+        EventRegistrationUIModule::class,
     ]
 )
 abstract class MainActivityModule {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt
index fe8a9252f..a020dfe53 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt
@@ -15,8 +15,8 @@ import de.rki.coronawarnapp.environment.BuildConfigWrap
 import de.rki.coronawarnapp.main.CWASettings
 import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.ui.main.MainActivity
-import de.rki.coronawarnapp.util.AppShortcuts
 import de.rki.coronawarnapp.util.di.AppInjector
+import timber.log.Timber
 import javax.inject.Inject
 
 /**
@@ -26,22 +26,16 @@ import javax.inject.Inject
  */
 class OnboardingActivity : AppCompatActivity(), LifecycleObserver, HasAndroidInjector {
     companion object {
-        private val TAG: String? = OnboardingActivity::class.simpleName
-        private const val EXTRA_DATA = "shortcut"
 
-        fun start(context: Context, shortcut: AppShortcuts? = null) {
-            val intent = Intent(context, OnboardingActivity::class.java).apply {
-                putExtra(EXTRA_DATA, shortcut?.toString())
+        fun start(context: Context, launchIntent: Intent? = null) {
+            val intent = Intent(context, OnboardingActivity::class.java)
+            Timber.i("launchIntent:$launchIntent")
+            launchIntent?.let {
+                intent.fillIn(it, Intent.FILL_IN_DATA)
+                Timber.i("filledIntent:$intent")
             }
             context.startActivity(intent)
         }
-
-        fun getShortcutFromIntent(intent: Intent?): AppShortcuts? {
-            intent?.getStringExtra(EXTRA_DATA)?.let {
-                return AppShortcuts.valueOf(it)
-            }
-            return null
-        }
     }
 
     @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
@@ -74,7 +68,7 @@ class OnboardingActivity : AppCompatActivity(), LifecycleObserver, HasAndroidInj
         LocalData.isOnboarded(true)
         LocalData.onboardingCompletedTimestamp(System.currentTimeMillis())
         settings.lastChangelogVersion.update { BuildConfigWrap.VERSION_CODE }
-        MainActivity.start(this)
+        MainActivity.start(this, intent)
         finish()
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingLoadingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingLoadingFragment.kt
index 7d7c6475f..e4d0d9a7a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingLoadingFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingLoadingFragment.kt
@@ -41,7 +41,7 @@ class OnboardingLoadingFragment : Fragment(R.layout.onboaring_loading_layout), A
                             .actionLoadingFragmentToOnboardingFragment()
                     )
                 OnboardingFragmentEvents.OnboardingDone -> {
-                    MainActivity.start(requireContext(), OnboardingActivity.getShortcutFromIntent(activity?.intent))
+                    MainActivity.start(requireContext(), requireActivity().intent)
                     activity?.overridePendingTransition(0, 0)
                     activity?.finish()
                 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/Uri.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/Uri.kt
new file mode 100644
index 000000000..93ab36f1c
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/Uri.kt
@@ -0,0 +1,16 @@
+package de.rki.coronawarnapp.util
+
+import android.net.Uri
+import androidx.navigation.NavController
+import java.util.Locale
+
+/**
+ * [NavController.navigate] by Uri is case sensitive. When authority and/or scheme are
+ * in Uppercase letter an Exception will thrown.
+ * To avoid such cases [navUri] is converting Uri schema and authority to lowercase always.
+ */
+val Uri.navUri: Uri
+    get() = Uri.Builder()
+        .authority(authority?.toLowerCase(Locale.ROOT))
+        .scheme(scheme?.toLowerCase(Locale.ROOT))
+        .path(path).build()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/shortcuts/AppShortcutsHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/shortcuts/AppShortcutsHelper.kt
index 0aeb1345d..a12907e37 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/shortcuts/AppShortcutsHelper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/shortcuts/AppShortcutsHelper.kt
@@ -36,18 +36,17 @@ class AppShortcutsHelper @Inject constructor(@AppContext private val context: Co
 
     private fun createContactDiaryIntent() = Intent(context, LauncherActivity::class.java).apply {
         action = Intent.ACTION_VIEW
-        putExtra(SHORTCUT_EXTRA_ID, AppShortcuts.CONTACT_DIARY.toString())
+        putExtra(SHORTCUT_EXTRA, AppShortcuts.CONTACT_DIARY.toString())
     }
 
     companion object {
         private const val CONTACT_DIARY_SHORTCUT_ID = "contact_diary_id"
-        private const val SHORTCUT_EXTRA_ID = "shortcut_extra"
+        const val SHORTCUT_EXTRA = "shortcut_extra"
 
-        fun getShortcutType(intent: Intent): AppShortcuts? {
-            intent.getStringExtra(SHORTCUT_EXTRA_ID)?.let {
+        fun Intent.getShortcutExtra(): AppShortcuts? {
+            getStringExtra(SHORTCUT_EXTRA)?.let {
                 return AppShortcuts.valueOf(it)
             }
-
             return null
         }
     }
diff --git a/Corona-Warn-App/src/main/res/layout/fragment_confrim_check_in.xml b/Corona-Warn-App/src/main/res/layout/fragment_confrim_check_in.xml
new file mode 100644
index 000000000..5f80dbeff
--- /dev/null
+++ b/Corona-Warn-App/src/main/res/layout/fragment_confrim_check_in.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".ui.eventregistration.checkin.ConfirmCheckInFragment">
+
+    <androidx.appcompat.widget.Toolbar
+        android:id="@+id/toolbar"
+        style="@style/CWAToolbar.Close"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <!-- TODO implement actual UI -->
+    <LinearLayout
+        style="@style/Card"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="10dp"
+        android:orientation="vertical"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/toolbar">
+        <TextView
+            android:id="@+id/eventGuid"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+        <TextView
+            android:id="@+id/startTime"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+        <TextView
+            android:id="@+id/endTime"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+        <TextView
+            android:id="@+id/description"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/confirmButton"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Confirm" />
+    </LinearLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/Corona-Warn-App/src/main/res/layout/fragment_scan_check_in_qr_code.xml b/Corona-Warn-App/src/main/res/layout/fragment_scan_check_in_qr_code.xml
new file mode 100644
index 000000000..aa0768d4a
--- /dev/null
+++ b/Corona-Warn-App/src/main/res/layout/fragment_scan_check_in_qr_code.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/check_in_qr_code_scan_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:contentDescription="@string/submission_qr_code_scan_title">
+
+    <com.journeyapps.barcodescanner.BarcodeView
+        android:id="@+id/check_in_qr_code_scan_preview"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:zxing_framing_rect_height="@dimen/submission_scan_qr_code_viewfinder_size"
+        app:zxing_framing_rect_width="@dimen/submission_scan_qr_code_viewfinder_size">
+
+    </com.journeyapps.barcodescanner.BarcodeView>
+
+    <com.journeyapps.barcodescanner.ViewfinderView
+        android:id="@+id/check_in_qr_code_scan_viewfinder_view"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:zxing_viewfinder_laser_visibility="false" />
+
+    <TextView
+        android:id="@+id/check_in_qr_code_scan_body"
+        style="@style/registrationQRCodeScanBody"
+        android:layout_width="@dimen/submission_scan_qr_code_viewfinder_size"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/submission_scan_qr_code_viewfinder_center_offset"
+        android:text="@string/submission_qr_code_scan_body"
+        app:layout_constraintEnd_toEndOf="@id/check_in_qr_code_scan_preview"
+        app:layout_constraintStart_toStartOf="@id/check_in_qr_code_scan_preview"
+        app:layout_constraintTop_toBottomOf="@id/check_in_qr_code_scan_guideline_center" />
+
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/check_in_qr_code_scan_close"
+        style="@style/buttonIcon"
+        android:layout_width="@dimen/icon_size_button"
+        android:layout_height="@dimen/icon_size_button"
+        app:layout_constraintBottom_toTopOf="@id/check_in_qr_code_scan_guideline_top"
+        app:layout_constraintEnd_toStartOf="@id/guideline_start"
+        app:layout_constraintStart_toStartOf="@id/guideline_start"
+        app:layout_constraintTop_toTopOf="@id/check_in_qr_code_scan_guideline_top">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            style="@style/iconStable"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:contentDescription="@string/accessibility_close"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:srcCompat="@drawable/ic_close" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+    <ToggleButton
+        android:id="@+id/check_in_qr_code_scan_torch"
+        android:layout_width="@dimen/icon_size_button"
+        android:layout_height="@dimen/icon_size_button"
+        android:background="@drawable/ic_registration_qr_code_scan_torch_toggle"
+        android:backgroundTint="@color/colorStableLight"
+        android:textOff=""
+        android:textOn=""
+        app:layout_constraintBottom_toTopOf="@id/check_in_qr_code_scan_guideline_top"
+        app:layout_constraintEnd_toStartOf="@id/guideline_end"
+        app:layout_constraintStart_toStartOf="@id/guideline_end"
+        app:layout_constraintTop_toTopOf="@id/check_in_qr_code_scan_guideline_top" />
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/check_in_qr_code_scan_guideline_top"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintGuide_begin="@dimen/spacing_normal" />
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/check_in_qr_code_scan_guideline_center"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintGuide_percent="0.5" />
+
+    <include layout="@layout/merge_guidelines_side" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
index b4fe423e3..7dbf6fe4d 100644
--- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
+++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
@@ -534,8 +534,7 @@
         android:id="@+id/surveyConsentDetailFragment"
         android:name="de.rki.coronawarnapp.datadonation.survey.consent.SurveyConsentDetailFragment"
         android:label="survey_consent_detail_fragment"
-        tools:layout="@layout/survey_consent_detail_fragment">
-    </fragment>
+        tools:layout="@layout/survey_consent_detail_fragment" />
     <fragment
         android:id="@+id/analyticsUserInputFragment"
         android:name="de.rki.coronawarnapp.datadonation.analytics.ui.input.AnalyticsUserInputFragment"
@@ -561,8 +560,7 @@
         android:id="@+id/ppaMoreInfoFragment"
         android:name="de.rki.coronawarnapp.datadonation.analytics.ui.PpaMoreInfoFragment"
         android:label="PpaMoreInfoFragment"
-        tools:layout="@layout/fragment_ppa_more_info">
-    </fragment>
+        tools:layout="@layout/fragment_ppa_more_info" />
     <fragment
         android:id="@+id/settingsPrivacyPreservingAnalyticsFragment"
         android:name="de.rki.coronawarnapp.ui.settings.analytics.SettingsPrivacyPreservingAnalyticsFragment"
@@ -575,4 +573,21 @@
             android:id="@+id/action_settingsPrivacyPreservingAnalyticsFragment_to_ppaMoreInfoFragment"
             app:destination="@id/ppaMoreInfoFragment" />
     </fragment>
+
+    <fragment
+        android:id="@+id/verifyEventFragment"
+        android:name="de.rki.coronawarnapp.ui.eventregistration.checkin.ConfirmCheckInFragment"
+        android:label="fragment_verify_event"
+        tools:layout="@layout/fragment_confrim_check_in">
+        <deepLink app:uri="https://coronawarn.app/E1/{encodedEvent}" />
+        <argument
+            android:name="encodedEvent"
+            app:argType="string" />
+
+    </fragment>
+    <fragment
+        android:id="@+id/scanCheckInQrCodeFragment"
+        android:name="de.rki.coronawarnapp.ui.eventregistration.scan.ScanCheckInQrCodeFragment"
+        android:label="ScanCheckInQrCodeFragment"
+        tools:layout="@layout/fragment_scan_check_in_qr_code" />
 </navigation>
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/durationpicker/DurationExtensionKtTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/durationpicker/DurationExtensionKtTest.kt
index 4bfa961ca..84d647ea2 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/durationpicker/DurationExtensionKtTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/durationpicker/DurationExtensionKtTest.kt
@@ -54,4 +54,4 @@ internal class DurationExtensionKtTest {
         val suffix: String?,
         val expectedReadableDuration: String
     )
-}
\ No newline at end of file
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModelTest.kt
new file mode 100644
index 000000000..bb02e9962
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModelTest.kt
@@ -0,0 +1,49 @@
+package de.rki.coronawarnapp.ui.eventregistration.checkin
+
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.EventQRCode
+import io.kotest.matchers.shouldBe
+import org.joda.time.Instant
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+import org.junit.jupiter.api.extension.ExtendWith
+import testhelpers.BaseTest
+import testhelpers.extensions.InstantExecutorExtension
+import testhelpers.extensions.getOrAwaitValue
+
+@ExtendWith(InstantExecutorExtension::class)
+class ConfirmCheckInViewModelTest : BaseTest() {
+
+    private lateinit var viewModel: ConfirmCheckInViewModel
+
+    @BeforeEach
+    fun setUp() {
+        viewModel = ConfirmCheckInViewModel()
+    }
+
+    @Test
+    fun decodeEvent() {
+        val decodedEvent =
+            "BIPEY33SMVWSA2LQON2W2IDEN5WG64RAONUXIIDBNVSXILBAMNXRBCM4UQARRKM6UQASAHRKCC7CTDWGQ4JCO7RVZSWVIMQK4UPA" +
+                ".GBCAEIA7TEORBTUA25QHBOCWT26BCA5PORBS2E4FFWMJ3UU3P6SXOL7SHUBCA7UEZBDDQ2R6VRJH7WBJKVF7GZYJA6YMRN27IPEP7NKGGJSWX3XQ"
+        viewModel.decodeEvent(decodedEvent)
+        viewModel.eventData.getOrAwaitValue() shouldBe EventQRCode(
+            guid = "Lorem ipsum dolor sit amet, co",
+            description = "",
+            start = Instant.parse("1970-01-01T00:44:50.857Z"),
+            end = Instant.parse("1970-01-01T00:00:00.030Z")
+        )
+    }
+
+    @Test
+    fun onClose() {
+        viewModel.onClose()
+        viewModel.navigationEvents.getOrAwaitValue() shouldBe ConfirmCheckInEvent.BackEvent
+    }
+
+    @Test
+    fun onConfirmEvent() {
+        viewModel.onConfirmEvent()
+        viewModel.navigationEvents.getOrAwaitValue() shouldBe ConfirmCheckInEvent.ConfirmEvent
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeViewModelTest.kt
new file mode 100644
index 000000000..e855bf055
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeViewModelTest.kt
@@ -0,0 +1,42 @@
+package de.rki.coronawarnapp.ui.eventregistration.scan
+
+import com.google.zxing.Result
+import com.journeyapps.barcodescanner.BarcodeResult
+import io.kotest.matchers.shouldBe
+import io.mockk.every
+import io.mockk.mockk
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import testhelpers.BaseTest
+import testhelpers.extensions.InstantExecutorExtension
+import testhelpers.extensions.getOrAwaitValue
+
+@ExtendWith(InstantExecutorExtension::class)
+class ScanCheckInQrCodeViewModelTest : BaseTest() {
+
+    private lateinit var viewModel: ScanCheckInQrCodeViewModel
+
+    @BeforeEach
+    fun setup() {
+        viewModel = ScanCheckInQrCodeViewModel()
+    }
+
+    @Test
+    fun `onNavigateUp goes back`() {
+        viewModel.onNavigateUp()
+        viewModel.navigationEvents.getOrAwaitValue() shouldBe ScanCheckInQrCodeEvent.BackEvent
+    }
+
+    @Test
+    fun `onScanResult results in navigation url`() {
+        val mockedResult = mockk<BarcodeResult>().apply {
+            every { result } returns mockk<Result>().apply {
+                every { text } returns "https://coronawarn.app/E1/SOME_PATH_GOES_HERE"
+            }
+        }
+        viewModel.onScanResult(mockedResult)
+        viewModel.navigationEvents.getOrAwaitValue() shouldBe
+            ScanCheckInQrCodeEvent.ConfirmCheckInEvent("https://coronawarn.app/E1/SOME_PATH_GOES_HERE")
+    }
+}
-- 
GitLab