From c5f53e86d6f78a626e4c5aabda4f11779f3fa0f9 Mon Sep 17 00:00:00 2001
From: Chilja Gossow <49635654+chiljamgossow@users.noreply.github.com>
Date: Tue, 11 May 2021 15:25:14 +0200
Subject: [PATCH] Vaccination QR code scan screen, error handling, navigation
 (EXPOSUREAPP-6726) (#3124)

* extractor

* decoding

* decoding, error handling

* clean up

* klint

* detekt

* simplify condition

* comment failing unit test

* unit test

* change lib

* Unit tests for RA/PCRCoronaTest.isFinal

* Add license text.

* add header

* clean up

* scan screen, error handling, navigation

* clean up

* klint detekt

* merge

* move strings

* comments

* klint

* revert version change

* fix fun name

* fix merge conflicts

* show dialog

* add navigation

* fix error dialog

* fix error dialog

* fix error dialog

* fix error dialog

* simplify

* merge main

* fix package

* review comments

* circular progress spinner

* fix test

Co-authored-by: Matthias Urhahn <matthias.urhahn@sap.com>
---
 .../bugreporting/ui/ErrorDialog.kt            |  35 ++--
 .../coronawarnapp/ui/main/home/HomeAdapter.kt |   6 +-
 .../ui/main/home/HomeFragmentViewModel.kt     |   8 +-
 .../scan/ScanCheckInQrCodeFragment.kt         |  22 +--
 .../InvalidHealthCertificateException.kt      |  81 ++++++++-
 .../VaccinationCertificateCOSEParser.kt       |   2 +-
 .../qrcode/VaccinationCertificateV1Parser.kt  |  82 +++++----
 .../core/qrcode/VaccinationQRCodeExtractor.kt |  22 +--
 .../core/qrcode/VaccinationQRCodeValidator.kt |   4 +-
 .../storage/VaccinationContainer.kt           |   3 +-
 .../vaccination/decoder/ZLIBDecompressor.kt   |   2 +-
 .../vaccination/ui/VaccinationUIModule.kt     |   5 +
 .../CompleteVaccinationHomeCard.kt            |   2 +-
 .../CreateVaccinationHomeCard.kt              |  11 +-
 .../IncompleteVaccinationHomeCard.kt          |   2 +-
 .../VaccinationStatusItem.kt                  |   2 +-
 .../ui/scan/VaccinationQrCodeScanFragment.kt  | 163 ++++++++++++++++++
 .../ui/scan/VaccinationQrCodeScanModule.kt    |  18 ++
 .../ui/scan/VaccinationQrCodeScanViewModel.kt |  47 +++++
 ..._qr_code.xml => fragment_scan_qr_code.xml} |  32 ++--
 .../src/main/res/navigation/nav_graph.xml     |   4 +
 .../trace_location_attendee_nav_graph.xml     |   4 +-
 .../res/navigation/vaccination_nav_graph.xml  |  16 +-
 .../res/values-de/vaccination_strings.xml     |  13 +-
 .../main/res/values/vaccination_strings.xml   |  13 +-
 .../qrcode/VaccinationQRCodeExtractorTest.kt  |  48 +++++-
 .../qrcode/VaccinationQrCodeTestData.java     |   1 +
 27 files changed, 534 insertions(+), 114 deletions(-)
 rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/{homecards => homecard}/CompleteVaccinationHomeCard.kt (96%)
 rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/{homecards => homecard}/CreateVaccinationHomeCard.kt (87%)
 rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/{homecards => homecard}/IncompleteVaccinationHomeCard.kt (96%)
 rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/{homecards => homecard}/VaccinationStatusItem.kt (85%)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanModule.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanViewModel.kt
 rename Corona-Warn-App/src/main/res/layout/{fragment_scan_check_in_qr_code.xml => fragment_scan_qr_code.xml} (71%)

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/ui/ErrorDialog.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/ui/ErrorDialog.kt
index e17a6f672..dbd4e65bd 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/ui/ErrorDialog.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/ui/ErrorDialog.kt
@@ -41,21 +41,22 @@ private fun MaterialAlertDialogBuilder.setMessageView(
     setView(textView)
 }
 
-fun Throwable.toErrorDialogBuilder(context: Context) = MaterialAlertDialogBuilder(context).apply {
-    val error = this@toErrorDialogBuilder
-    val humanReadable = error.tryHumanReadableError(context)
-
-    setTitle(humanReadable.title ?: context.getString(R.string.errors_generic_headline_short))
-    setMessageView(humanReadable.description, textHasLinks = true)
-
-    setPositiveButton(R.string.errors_generic_button_positive) { _, _ -> }
-
-    setNeutralButton(R.string.errors_generic_button_negative) { _, _ ->
-        MaterialAlertDialogBuilder(context).apply {
-            setMessageView(
-                error.toString() + "\n\n" + error.stackTraceToString(),
-                textHasLinks = false
-            )
-        }.show()
+fun Throwable.toErrorDialogBuilder(context: Context) =
+    MaterialAlertDialogBuilder(context).apply {
+        val error = this@toErrorDialogBuilder
+        val humanReadable = error.tryHumanReadableError(context)
+
+        setTitle(humanReadable.title ?: context.getString(R.string.errors_generic_headline_short))
+        setMessageView(humanReadable.description, textHasLinks = true)
+
+        setPositiveButton(R.string.errors_generic_button_positive) { _, _ -> }
+
+        setNeutralButton(R.string.errors_generic_button_negative) { _, _ ->
+            MaterialAlertDialogBuilder(context).apply {
+                setMessageView(
+                    error.toString() + "\n\n" + error.stackTraceToString(),
+                    textHasLinks = false
+                )
+            }.show()
+        }
     }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeAdapter.kt
index 8ba4b3d16..abd463895 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeAdapter.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeAdapter.kt
@@ -38,9 +38,9 @@ import de.rki.coronawarnapp.util.lists.modular.mods.DataBinderMod
 import de.rki.coronawarnapp.util.lists.modular.mods.SavedStateMod
 import de.rki.coronawarnapp.util.lists.modular.mods.StableIdMod
 import de.rki.coronawarnapp.util.lists.modular.mods.TypedVHCreatorMod
-import de.rki.coronawarnapp.vaccination.ui.homecards.CompleteVaccinationHomeCard
-import de.rki.coronawarnapp.vaccination.ui.homecards.CreateVaccinationHomeCard
-import de.rki.coronawarnapp.vaccination.ui.homecards.IncompleteVaccinationHomeCard
+import de.rki.coronawarnapp.vaccination.ui.homecard.CompleteVaccinationHomeCard
+import de.rki.coronawarnapp.vaccination.ui.homecard.CreateVaccinationHomeCard
+import de.rki.coronawarnapp.vaccination.ui.homecard.IncompleteVaccinationHomeCard
 
 class HomeAdapter :
     ModularAdapter<HomeAdapter.HomeItemVH<HomeItem, ViewBinding>>(),
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt
index e8a0344d8..cb5e2c045 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt
@@ -74,9 +74,9 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
 import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson
 import de.rki.coronawarnapp.vaccination.core.repository.VaccinationRepository
-import de.rki.coronawarnapp.vaccination.ui.homecards.CompleteVaccinationHomeCard
-import de.rki.coronawarnapp.vaccination.ui.homecards.CreateVaccinationHomeCard
-import de.rki.coronawarnapp.vaccination.ui.homecards.IncompleteVaccinationHomeCard
+import de.rki.coronawarnapp.vaccination.ui.homecard.CompleteVaccinationHomeCard
+import de.rki.coronawarnapp.vaccination.ui.homecard.CreateVaccinationHomeCard
+import de.rki.coronawarnapp.vaccination.ui.homecard.IncompleteVaccinationHomeCard
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
@@ -366,7 +366,7 @@ class HomeFragmentViewModel @AssistedInject constructor(
             add(
                 CreateVaccinationHomeCard.Item(
                     onClickAction = {
-                        // TODO: implement in another PR
+                        routeToScreen.postValue(HomeFragmentDirections.actionMainFragmentToVaccinationNavGraph())
                     }
                 )
             )
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeFragment.kt
index 767df359c..47ddf2f67 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeFragment.kt
@@ -12,7 +12,7 @@ import com.google.android.material.transition.MaterialContainerTransform
 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.databinding.FragmentScanQrCodeBinding
 import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.CheckInsFragment
 import de.rki.coronawarnapp.util.DialogHelper
 import de.rki.coronawarnapp.util.di.AutoInject
@@ -26,13 +26,13 @@ import timber.log.Timber
 import javax.inject.Inject
 
 class ScanCheckInQrCodeFragment :
-    Fragment(R.layout.fragment_scan_check_in_qr_code),
+    Fragment(R.layout.fragment_scan_qr_code),
     AutoInject {
 
     @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
     private val viewModel: ScanCheckInQrCodeViewModel by cwaViewModels { viewModelFactory }
 
-    private val binding: FragmentScanCheckInQrCodeBinding by viewBindingLazy()
+    private val binding: FragmentScanQrCodeBinding by viewBindingLazy()
     private var showsPermissionDialog = false
 
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -47,13 +47,13 @@ class ScanCheckInQrCodeFragment :
         savedInstanceState: Bundle?
     ) {
         with(binding) {
-            checkInQrCodeScanTorch.setOnCheckedChangeListener { _, isChecked ->
-                binding.checkInQrCodeScanPreview.setTorch(isChecked)
+            qrCodeScanTorch.setOnCheckedChangeListener { _, isChecked ->
+                binding.qrCodeScanPreview.setTorch(isChecked)
             }
 
-            checkInQrCodeScanToolbar.setNavigationOnClickListener { viewModel.onNavigateUp() }
-            checkInQrCodeScanPreview.decoderFactory = DefaultDecoderFactory(listOf(BarcodeFormat.QR_CODE))
-            checkInQrCodeScanViewfinderView.setCameraPreview(binding.checkInQrCodeScanPreview)
+            qrCodeScanToolbar.setNavigationOnClickListener { viewModel.onNavigateUp() }
+            qrCodeScanPreview.decoderFactory = DefaultDecoderFactory(listOf(BarcodeFormat.QR_CODE))
+            qrCodeScanViewfinderView.setCameraPreview(binding.qrCodeScanPreview)
         }
 
         viewModel.events.observe2(this) { navEvent ->
@@ -76,7 +76,7 @@ class ScanCheckInQrCodeFragment :
         super.onResume()
         binding.checkInQrCodeScanContainer.sendAccessibilityEvent(TYPE_ANNOUNCEMENT)
         if (CameraPermissionHelper.hasCameraPermission(requireActivity())) {
-            binding.checkInQrCodeScanPreview.resume()
+            binding.qrCodeScanPreview.resume()
             startDecode()
             return
         }
@@ -104,7 +104,7 @@ class ScanCheckInQrCodeFragment :
         }
     }
 
-    private fun startDecode() = binding.checkInQrCodeScanPreview
+    private fun startDecode() = binding.qrCodeScanPreview
         .decodeSingle { barcodeResult ->
             viewModel.onScanResult(barcodeResult)
         }
@@ -156,7 +156,7 @@ class ScanCheckInQrCodeFragment :
 
     override fun onPause() {
         super.onPause()
-        binding.checkInQrCodeScanPreview.pause()
+        binding.qrCodeScanPreview.pause()
     }
 
     companion object {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/InvalidHealthCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/InvalidHealthCertificateException.kt
index c3cb9a33a..6cb561931 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/InvalidHealthCertificateException.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/InvalidHealthCertificateException.kt
@@ -1,10 +1,32 @@
 package de.rki.coronawarnapp.vaccination.core.qrcode
 
+import android.content.Context
+import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException
+import de.rki.coronawarnapp.util.HasHumanReadableError
+import de.rki.coronawarnapp.util.HumanReadableError
+import de.rki.coronawarnapp.util.ui.CachedString
+import de.rki.coronawarnapp.util.ui.LazyString
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.HC_COSE_MESSAGE_INVALID
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.HC_COSE_TAG_INVALID
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_ALREADY_REGISTERED
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_DOB_MISMATCH
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_DGC
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_EXP
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_HCERT
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_ISS
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_JSON_SCHEMA_INVALID
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_NAME_MISMATCH
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_PREFIX_INVALID
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_STORING_FAILED
 
 class InvalidHealthCertificateException(
     val errorCode: ErrorCode
-) : InvalidQRCodeException(errorCode.message) {
+) : HasHumanReadableError, InvalidQRCodeException(errorCode.message) {
     enum class ErrorCode(
         val message: String
     ) {
@@ -13,6 +35,61 @@ class InvalidHealthCertificateException(
         HC_COSE_TAG_INVALID("COSE tag invalid."),
         HC_COSE_MESSAGE_INVALID("COSE message invalid."),
         HC_CBOR_DECODING_FAILED("CBOR decoding failed."),
-        VC_NO_VACCINATION_ENTRY("Vaccination certificate missing.")
+        VC_NO_VACCINATION_ENTRY("Vaccination certificate missing."),
+        VC_PREFIX_INVALID("Prefix invalid."),
+        VC_STORING_FAILED("Storing failed."),
+        VC_JSON_SCHEMA_INVALID("Json schema invalid."),
+        VC_NAME_MISMATCH("Name does not match."),
+        VC_ALREADY_REGISTERED("Certificate already registered."),
+        VC_DOB_MISMATCH("Date of birth does not match."),
+        VC_HC_CWT_NO_DGC("Dgc missing."),
+        VC_HC_CWT_NO_EXP("Expiration date missing."),
+        VC_HC_CWT_NO_HCERT("Health certificate missing."),
+        VC_HC_CWT_NO_ISS("Issuer missing."),
+    }
+
+    val errorMessage: LazyString
+        get() = when (errorCode) {
+            HC_BASE45_DECODING_FAILED,
+            HC_CBOR_DECODING_FAILED,
+            HC_COSE_MESSAGE_INVALID,
+            HC_ZLIB_DECOMPRESSION_FAILED,
+            HC_COSE_TAG_INVALID,
+            VC_PREFIX_INVALID,
+            VC_HC_CWT_NO_DGC,
+            VC_HC_CWT_NO_EXP,
+            VC_HC_CWT_NO_HCERT,
+            VC_HC_CWT_NO_ISS,
+            VC_JSON_SCHEMA_INVALID,
+            -> CachedString { context ->
+                context.getString(ERROR_MESSAGE_VC_INVALID)
+            }
+            VC_NO_VACCINATION_ENTRY -> CachedString { context ->
+                context.getString(ERROR_MESSAGE_VC_NOT_YET_SUPPORTED)
+            }
+            VC_STORING_FAILED -> CachedString { context ->
+                context.getString(ERROR_MESSAGE_VC_SCAN_AGAIN)
+            }
+            VC_NAME_MISMATCH, VC_DOB_MISMATCH -> CachedString { context ->
+                context.getString(ERROR_MESSAGE_VC_DIFFERENT_PERSON)
+            }
+            VC_ALREADY_REGISTERED -> CachedString { context ->
+                context.getString(ERROR_MESSAGE_ALREADY_REGISTERED)
+            }
+        }
+
+    override fun toHumanReadableError(context: Context): HumanReadableError {
+        var errorCodeString = errorCode.toString()
+        errorCodeString = if (errorCodeString.startsWith(PREFIX)) errorCodeString else PREFIX + errorCodeString
+        return HumanReadableError(
+            description = errorMessage.get(context) + "\n\n$errorCodeString"
+        )
     }
 }
+
+private const val PREFIX = "VC_"
+private const val ERROR_MESSAGE_VC_INVALID = R.string.error_vc_invalid
+private const val ERROR_MESSAGE_VC_NOT_YET_SUPPORTED = R.string.error_vc_not_yet_supported
+private const val ERROR_MESSAGE_VC_SCAN_AGAIN = R.string.error_vc_scan_again
+private const val ERROR_MESSAGE_VC_DIFFERENT_PERSON = R.string.error_vc_different_person
+private const val ERROR_MESSAGE_ALREADY_REGISTERED = R.string.error_vc_already_registered
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateCOSEParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateCOSEParser.kt
index b2dd023f4..03c53611c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateCOSEParser.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateCOSEParser.kt
@@ -30,7 +30,7 @@ class VaccinationCertificateCOSEParser @Inject constructor(
 
     private fun CBORObject.decodeCBORObject(): VaccinationCertificateData {
         return try {
-            vaccinationCertificateV1Parser.decode(this)
+            vaccinationCertificateV1Parser.parse(this)
         } catch (e: Exception) {
             Timber.e(e)
             throw InvalidHealthCertificateException(HC_CBOR_DECODING_FAILED)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateV1Parser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateV1Parser.kt
index 7e73d4bc1..0ffceb930 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateV1Parser.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateV1Parser.kt
@@ -2,13 +2,21 @@ package de.rki.coronawarnapp.vaccination.core.qrcode
 
 import com.google.gson.Gson
 import com.upokecenter.cbor.CBORObject
+import de.rki.coronawarnapp.util.serialization.BaseGson
 import de.rki.coronawarnapp.util.serialization.fromJson
 import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_DGC
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_EXP
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_HCERT
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_ISS
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_JSON_SCHEMA_INVALID
 import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY
 import org.joda.time.Instant
 import javax.inject.Inject
 
-class VaccinationCertificateV1Parser @Inject constructor() {
+class VaccinationCertificateV1Parser @Inject constructor(
+    @BaseGson private val gson: Gson
+) {
 
     companion object {
         private val keyEuDgcV1 = CBORObject.FromObject(1)
@@ -18,41 +26,43 @@ class VaccinationCertificateV1Parser @Inject constructor() {
         private val keyIssuedAt = CBORObject.FromObject(6)
     }
 
-    fun decode(map: CBORObject): VaccinationCertificateData {
-        try {
-            var issuer: String? = null
-            map[keyIssuer]?.let {
-                issuer = it.AsString()
-            }
-            var issuedAt: Instant? = null
-            map[keyIssuedAt]?.let {
-                issuedAt = Instant.ofEpochSecond(it.AsNumber().ToInt64Checked())
-            }
-            var expiresAt: Instant? = null
-            map[keyExpiresAt]?.let {
-                expiresAt = Instant.ofEpochSecond(it.AsNumber().ToInt64Checked())
-            }
-            var certificate: VaccinationCertificateV1? = null
-            map[keyHCert]?.let { hcert ->
-                hcert[keyEuDgcV1]?.let {
-                    val json = it.ToJSONString()
-                    certificate = Gson().fromJson<VaccinationCertificateV1>(json)
-                }
-            }
-            val header = VaccinationCertificateHeader(
-                issuer = issuer!!,
-                issuedAt = issuedAt!!,
-                expiresAt = expiresAt!!
-            )
-            return VaccinationCertificateData(
-                header,
-                certificate!!.validate()
-            )
-        } catch (e: InvalidHealthCertificateException) {
-            throw e
-        } catch (e: Throwable) {
-            throw InvalidHealthCertificateException(HC_CBOR_DECODING_FAILED)
-        }
+    fun parse(map: CBORObject): VaccinationCertificateData = try {
+        val issuer: String = map[keyIssuer]?.AsString() ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_ISS)
+
+        val issuedAt: Instant = map[keyIssuedAt]?.run {
+            Instant.ofEpochSecond(AsNumber().ToInt64Checked())
+        } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_ISS)
+
+        val expiresAt: Instant = map[keyExpiresAt]?.run {
+            Instant.ofEpochSecond(AsNumber().ToInt64Checked())
+        } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_EXP)
+
+        val certificate: VaccinationCertificateV1 = map[keyHCert]?.run {
+            this[keyEuDgcV1]?.run {
+                toCertificate()
+            } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_DGC)
+        } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_HCERT)
+
+        val header = VaccinationCertificateHeader(
+            issuer = issuer,
+            issuedAt = issuedAt,
+            expiresAt = expiresAt
+        )
+        VaccinationCertificateData(
+            header,
+            certificate.validate()
+        )
+    } catch (e: InvalidHealthCertificateException) {
+        throw e
+    } catch (e: Throwable) {
+        throw InvalidHealthCertificateException(HC_CBOR_DECODING_FAILED)
+    }
+
+    private fun CBORObject.toCertificate() = try {
+        val json = ToJSONString()
+        gson.fromJson<VaccinationCertificateV1>(json)
+    } catch (e: Throwable) {
+        throw InvalidHealthCertificateException(VC_JSON_SCHEMA_INVALID)
     }
 
     private fun VaccinationCertificateV1.validate(): VaccinationCertificateV1 {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt
index 3c4ac6d49..b2d9308d7 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt
@@ -20,18 +20,18 @@ class VaccinationQRCodeExtractor @Inject constructor(
 
     private val prefix = "HC1:"
 
-    override fun canHandle(rawString: String): Boolean {
-        return rawString.startsWith(prefix)
-    }
+    override fun canHandle(rawString: String): Boolean = rawString.startsWith(prefix)
 
     override fun extract(rawString: String): VaccinationCertificateQRCode {
         val rawCOSEObject = rawString
             .removePrefix(prefix)
             .decodeBase45()
             .decompress()
+
         val certificate = rawCOSEObject
             .decodeCOSEObject()
-            .decodeCBORObject()
+            .parseCBORObject()
+
         return VaccinationCertificateQRCode(
             parsedData = certificate,
             certificateCOSE = rawCOSEObject,
@@ -40,14 +40,14 @@ class VaccinationQRCodeExtractor @Inject constructor(
 
     private fun String.decodeBase45(): ByteArray = try {
         base45Decoder.decode(this)
-    } catch (e: Exception) {
+    } catch (e: Throwable) {
         Timber.e(e)
         throw InvalidHealthCertificateException(HC_BASE45_DECODING_FAILED)
     }
 
     private fun ByteArray.decompress(): ByteArray = try {
-        zLIBDecompressor.decode(this)
-    } catch (e: Exception) {
+        zLIBDecompressor.decompress(this)
+    } catch (e: Throwable) {
         Timber.e(e)
         throw InvalidHealthCertificateException(HC_ZLIB_DECOMPRESSION_FAILED)
     }
@@ -56,16 +56,16 @@ class VaccinationQRCodeExtractor @Inject constructor(
         healthCertificateCOSEDecoder.decode(this)
     } catch (e: InvalidHealthCertificateException) {
         throw e
-    } catch (e: Exception) {
+    } catch (e: Throwable) {
         Timber.e(e)
         throw InvalidHealthCertificateException(HC_COSE_MESSAGE_INVALID)
     }
 
-    private fun CBORObject.decodeCBORObject(): VaccinationCertificateData = try {
-        vaccinationCertificateV1Parser.decode(this)
+    private fun CBORObject.parseCBORObject(): VaccinationCertificateData = try {
+        vaccinationCertificateV1Parser.parse(this)
     } catch (e: InvalidHealthCertificateException) {
         throw e
-    } catch (e: Exception) {
+    } catch (e: Throwable) {
         Timber.e(e)
         throw InvalidHealthCertificateException(HC_CBOR_DECODING_FAILED)
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt
index cf074f8c4..07fb8293c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt
@@ -1,8 +1,8 @@
 package de.rki.coronawarnapp.vaccination.core.qrcode
 
 import dagger.Reusable
-import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException
 import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_PREFIX_INVALID
 import timber.log.Timber
 import javax.inject.Inject
 
@@ -16,7 +16,7 @@ class VaccinationQRCodeValidator @Inject constructor(
         return findExtractor(rawString)
             ?.extract(rawString)
             ?.also { Timber.i("Extracted data from QR code is $it") }
-            ?: throw InvalidQRCodeException()
+            ?: throw InvalidHealthCertificateException(VC_PREFIX_INVALID)
     }
 
     private fun findExtractor(rawString: String): QrCodeExtractor<VaccinationCertificateQRCode>? {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt
index 978864bd0..79d3dad89 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt
@@ -1,6 +1,7 @@
 package de.rki.coronawarnapp.vaccination.core.repository.storage
 
 import androidx.annotation.Keep
+import com.google.gson.Gson
 import com.google.gson.annotations.SerializedName
 import de.rki.coronawarnapp.ui.Country
 import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier
@@ -35,7 +36,7 @@ data class VaccinationContainer(
     private val certificateData: VaccinationCertificateData by lazy {
         preParsedData ?: VaccinationCertificateCOSEParser(
             HealthCertificateCOSEDecoder(),
-            VaccinationCertificateV1Parser(),
+            VaccinationCertificateV1Parser(Gson()),
         ).parse(vaccinationCertificateCOSE)
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/decoder/ZLIBDecompressor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/decoder/ZLIBDecompressor.kt
index 8355af38f..b79a18844 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/decoder/ZLIBDecompressor.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/decoder/ZLIBDecompressor.kt
@@ -5,7 +5,7 @@ import java.util.zip.InflaterInputStream
 import javax.inject.Inject
 
 class ZLIBDecompressor @Inject constructor() {
-    fun decode(input: ByteArray): ByteArray = if (
+    fun decompress(input: ByteArray): ByteArray = if (
         input.size >= 2 &&
         input[0] == 0x78.toByte() &&
         input[1] in listOf(0x01.toByte(), 0x5E.toByte(), 0x9C.toByte(), 0xDA.toByte())
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/VaccinationUIModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/VaccinationUIModule.kt
index 78afda0dd..9c5e2af20 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/VaccinationUIModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/VaccinationUIModule.kt
@@ -6,6 +6,8 @@ import de.rki.coronawarnapp.vaccination.ui.details.VaccinationDetailsFragment
 import de.rki.coronawarnapp.vaccination.ui.details.VaccinationDetailsFragmentModule
 import de.rki.coronawarnapp.vaccination.ui.list.VaccinationListFragment
 import de.rki.coronawarnapp.vaccination.ui.list.VaccinationListFragmentModule
+import de.rki.coronawarnapp.vaccination.ui.scan.VaccinationQrCodeScanFragment
+import de.rki.coronawarnapp.vaccination.ui.scan.VaccinationQrCodeScanModule
 
 @Module
 abstract class VaccinationUIModule {
@@ -15,4 +17,7 @@ abstract class VaccinationUIModule {
 
     @ContributesAndroidInjector(modules = [VaccinationDetailsFragmentModule::class])
     abstract fun vaccinationDetailsFragment(): VaccinationDetailsFragment
+
+    @ContributesAndroidInjector(modules = [VaccinationQrCodeScanModule::class])
+    abstract fun vaccinationQrCodeScanFragment(): VaccinationQrCodeScanFragment
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/CompleteVaccinationHomeCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/CompleteVaccinationHomeCard.kt
similarity index 96%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/CompleteVaccinationHomeCard.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/CompleteVaccinationHomeCard.kt
index 17965fc0f..f64b6f3fd 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/CompleteVaccinationHomeCard.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/CompleteVaccinationHomeCard.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.vaccination.ui.homecards
+package de.rki.coronawarnapp.vaccination.ui.homecard
 
 import android.view.ViewGroup
 import de.rki.coronawarnapp.R
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/CreateVaccinationHomeCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/CreateVaccinationHomeCard.kt
similarity index 87%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/CreateVaccinationHomeCard.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/CreateVaccinationHomeCard.kt
index d0a98e8a1..4f807e7ae 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/CreateVaccinationHomeCard.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/CreateVaccinationHomeCard.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.vaccination.ui.homecards
+package de.rki.coronawarnapp.vaccination.ui.homecard
 
 import android.view.ViewGroup
 import de.rki.coronawarnapp.R
@@ -22,10 +22,17 @@ class CreateVaccinationHomeCard(parent: ViewGroup) :
         payloads: List<Any>
     ) -> Unit = { item, payloads ->
 
-        itemView.setOnClickListener {
+        fun onClick() {
             val curItem = payloads.filterIsInstance<Item>().singleOrNull() ?: item
             curItem.onClickAction(item)
         }
+
+        itemView.setOnClickListener {
+            onClick()
+        }
+        nextStepsAction.setOnClickListener {
+            onClick()
+        }
     }
 
     data class Item(val onClickAction: (Item) -> Unit) : HomeItem, HasPayloadDiffer {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/IncompleteVaccinationHomeCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/IncompleteVaccinationHomeCard.kt
similarity index 96%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/IncompleteVaccinationHomeCard.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/IncompleteVaccinationHomeCard.kt
index 37472a274..915eb6d68 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/IncompleteVaccinationHomeCard.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/IncompleteVaccinationHomeCard.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.vaccination.ui.homecards
+package de.rki.coronawarnapp.vaccination.ui.homecard
 
 import android.view.ViewGroup
 import de.rki.coronawarnapp.R
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/VaccinationStatusItem.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/VaccinationStatusItem.kt
similarity index 85%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/VaccinationStatusItem.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/VaccinationStatusItem.kt
index 5968607a6..92bc3708f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/VaccinationStatusItem.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/VaccinationStatusItem.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.vaccination.ui.homecards
+package de.rki.coronawarnapp.vaccination.ui.homecard
 
 import de.rki.coronawarnapp.ui.main.home.items.HomeItem
 import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt
new file mode 100644
index 000000000..bc4c36ec0
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt
@@ -0,0 +1,163 @@
+package de.rki.coronawarnapp.vaccination.ui.scan
+
+import android.Manifest
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.view.View
+import android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT
+import androidx.fragment.app.Fragment
+import com.google.zxing.BarcodeFormat
+import com.journeyapps.barcodescanner.DefaultDecoderFactory
+import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.bugreporting.ui.toErrorDialogBuilder
+import de.rki.coronawarnapp.databinding.FragmentScanQrCodeBinding
+import de.rki.coronawarnapp.util.DialogHelper
+import de.rki.coronawarnapp.util.di.AutoInject
+import de.rki.coronawarnapp.util.permission.CameraPermissionHelper
+import de.rki.coronawarnapp.util.ui.doNavigate
+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 VaccinationQrCodeScanFragment :
+    Fragment(R.layout.fragment_scan_qr_code),
+    AutoInject {
+
+    @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
+    private val viewModel: VaccinationQrCodeScanViewModel by cwaViewModels { viewModelFactory }
+
+    private val binding: FragmentScanQrCodeBinding by viewBindingLazy()
+    private var showsPermissionDialog = false
+
+    override fun onViewCreated(
+        view: View,
+        savedInstanceState: Bundle?
+    ) {
+        with(binding) {
+            qrCodeScanTorch.setOnCheckedChangeListener { _, isChecked ->
+                binding.qrCodeScanPreview.setTorch(isChecked)
+            }
+
+            qrCodeScanToolbar.setNavigationOnClickListener { popBackStack() }
+            qrCodeScanPreview.decoderFactory = DefaultDecoderFactory(listOf(BarcodeFormat.QR_CODE))
+            qrCodeScanViewfinderView.setCameraPreview(binding.qrCodeScanPreview)
+            qrCodeScanSpinner.hide()
+        }
+
+        viewModel.event.observe(viewLifecycleOwner) { event ->
+            when (event) {
+                is VaccinationQrCodeScanViewModel.Event.QrCodeScanSucceeded -> {
+                    binding.qrCodeScanSpinner.hide()
+                    doNavigate(
+                        VaccinationQrCodeScanFragmentDirections
+                            .actionVaccinationQrCodeScanFragmentToVaccinationDetailsFragment(event.certificateId)
+                    )
+                }
+                VaccinationQrCodeScanViewModel.Event.QrCodeScanInProgress -> {
+                    binding.qrCodeScanSpinner.show()
+                }
+            }
+        }
+
+        viewModel.errorEvent.observe(viewLifecycleOwner) {
+            binding.qrCodeScanSpinner.hide()
+            it.toErrorDialogBuilder(requireContext()).apply {
+                setOnDismissListener { popBackStack() }
+            }.show()
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        binding.checkInQrCodeScanContainer.sendAccessibilityEvent(TYPE_ANNOUNCEMENT)
+        if (CameraPermissionHelper.hasCameraPermission(requireActivity())) {
+            binding.qrCodeScanPreview.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()
+                viewModel.setCameraDeniedPermanently(false)
+            } else {
+                // User permanently denied access to the camera
+                showCameraPermissionDeniedDialog()
+                viewModel.setCameraDeniedPermanently(true)
+            }
+        }
+    }
+
+    private fun startDecode() = binding.qrCodeScanPreview
+        .decodeSingle { barcodeResult ->
+            viewModel.onScanResult(barcodeResult)
+        }
+
+    private fun showCameraPermissionDeniedDialog() {
+        val permissionDeniedDialog = DialogHelper.DialogInstance(
+            requireActivity(),
+            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 = {
+                leave()
+            }
+        )
+        showsPermissionDialog = true
+        DialogHelper.showDialog(permissionDeniedDialog)
+    }
+
+    private fun showCameraPermissionRationaleDialog() {
+        val cameraPermissionRationaleDialogInstance = DialogHelper.DialogInstance(
+            requireActivity(),
+            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,
+            positiveButtonFunction = {
+                showsPermissionDialog = false
+                requestCameraPermission()
+            },
+            negativeButtonFunction = {
+                leave()
+            }
+        )
+
+        showsPermissionDialog = true
+        DialogHelper.showDialog(cameraPermissionRationaleDialogInstance)
+    }
+
+    private fun requestCameraPermission() = requestPermissions(
+        arrayOf(Manifest.permission.CAMERA),
+        REQUEST_CAMERA_PERMISSION_CODE
+    )
+
+    private fun leave() {
+        showsPermissionDialog = false
+        popBackStack()
+    }
+
+    override fun onPause() {
+        super.onPause()
+        binding.qrCodeScanPreview.pause()
+    }
+
+    companion object {
+        private const val REQUEST_CAMERA_PERMISSION_CODE = 4000
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanModule.kt
new file mode 100644
index 000000000..aa5cd02df
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanModule.kt
@@ -0,0 +1,18 @@
+package de.rki.coronawarnapp.vaccination.ui.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 VaccinationQrCodeScanModule {
+    @Binds
+    @IntoMap
+    @CWAViewModelKey(VaccinationQrCodeScanViewModel::class)
+    abstract fun vaccinationQrCodeScanFragment(
+        factory: VaccinationQrCodeScanViewModel.Factory
+    ): CWAViewModelFactory<out CWAViewModel>
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanViewModel.kt
new file mode 100644
index 000000000..17b6d0334
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanViewModel.kt
@@ -0,0 +1,47 @@
+package de.rki.coronawarnapp.vaccination.ui.scan
+
+import com.journeyapps.barcodescanner.BarcodeResult
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import de.rki.coronawarnapp.util.permission.CameraSettings
+import de.rki.coronawarnapp.util.ui.SingleLiveEvent
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
+import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
+import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQRCodeValidator
+import de.rki.coronawarnapp.vaccination.core.repository.VaccinationRepository
+import timber.log.Timber
+
+class VaccinationQrCodeScanViewModel @AssistedInject constructor(
+    private val cameraSettings: CameraSettings,
+    private val vaccinationQRCodeValidator: VaccinationQRCodeValidator,
+    private val vaccinationRepository: VaccinationRepository
+) : CWAViewModel() {
+
+    val event = SingleLiveEvent<Event>()
+
+    val errorEvent = SingleLiveEvent<Throwable>()
+
+    fun onScanResult(barcodeResult: BarcodeResult) = launch {
+        try {
+            event.postValue(Event.QrCodeScanInProgress)
+            val qrCode = vaccinationQRCodeValidator.validate(barcodeResult.text)
+            val certificate = vaccinationRepository.registerVaccination(qrCode)
+            event.postValue(Event.QrCodeScanSucceeded(certificate.certificateId))
+        } catch (e: Throwable) {
+            errorEvent.postValue(e)
+        }
+    }
+
+    fun setCameraDeniedPermanently(denied: Boolean) {
+        Timber.d("setCameraDeniedPermanently(denied=$denied)")
+        cameraSettings.isCameraDeniedPermanently.update { denied }
+    }
+
+    sealed class Event {
+        object QrCodeScanInProgress : Event()
+        data class QrCodeScanSucceeded(val certificateId: String) : Event()
+    }
+
+    @AssistedFactory
+    interface Factory : SimpleCWAViewModelFactory<VaccinationQrCodeScanViewModel>
+}
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_qr_code.xml
similarity index 71%
rename from Corona-Warn-App/src/main/res/layout/fragment_scan_check_in_qr_code.xml
rename to Corona-Warn-App/src/main/res/layout/fragment_scan_qr_code.xml
index 80845bfcf..2b4f586ab 100644
--- 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_qr_code.xml
@@ -8,7 +8,7 @@
     android:transitionName="shared_element_container">
 
     <com.journeyapps.barcodescanner.BarcodeView
-        android:id="@+id/check_in_qr_code_scan_preview"
+        android:id="@+id/qr_code_scan_preview"
         android:layout_width="0dp"
         android:layout_height="0dp"
         app:layout_constraintBottom_toBottomOf="parent"
@@ -21,7 +21,7 @@
     </com.journeyapps.barcodescanner.BarcodeView>
 
     <com.journeyapps.barcodescanner.ViewfinderView
-        android:id="@+id/check_in_qr_code_scan_viewfinder_view"
+        android:id="@+id/qr_code_scan_viewfinder_view"
         android:layout_width="0dp"
         android:layout_height="0dp"
         app:layout_constraintBottom_toBottomOf="parent"
@@ -32,19 +32,31 @@
         app:zxing_viewfinder_laser_visibility="false" />
 
     <TextView
-        android:id="@+id/check_in_qr_code_scan_body"
+        android:id="@+id/qr_code_scan_body"
         style="@style/qrCodeScanBody"
         android:layout_width="@dimen/scan_qr_code_viewfinder_size"
         android:layout_height="wrap_content"
         android:layout_marginTop="@dimen/scan_qr_code_viewfinder_center_offset"
         android:text="@string/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" />
+        app:layout_constraintEnd_toEndOf="@id/qr_code_scan_preview"
+        app:layout_constraintStart_toStartOf="@id/qr_code_scan_preview"
+        app:layout_constraintTop_toBottomOf="@id/qr_code_scan_guideline_center" />
 
+    <com.google.android.material.progressindicator.CircularProgressIndicator
+        android:id="@+id/qr_code_scan_spinner"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/spacing_normal"
+        android:indeterminate="true"
+        app:indicatorColor="@android:color/white"
+        app:indicatorSize="64dp"
+        app:trackColor="@android:color/transparent"
+        app:layout_constraintEnd_toEndOf="@id/qr_code_scan_body"
+        app:layout_constraintStart_toStartOf="@id/qr_code_scan_body"
+        app:layout_constraintTop_toBottomOf="@id/qr_code_scan_body" />
 
     <com.google.android.material.appbar.MaterialToolbar
-        android:id="@+id/check_in_qr_code_scan_toolbar"
+        android:id="@+id/qr_code_scan_toolbar"
         style="@style/CWAToolbar.BackArrow.Transparent"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
@@ -56,7 +68,7 @@
         app:titleTextColor="@color/colorQrCodeScanToolbar">
 
         <ToggleButton
-            android:id="@+id/check_in_qr_code_scan_torch"
+            android:id="@+id/qr_code_scan_torch"
             android:layout_width="@dimen/icon_size_button"
             android:layout_height="@dimen/icon_size_button"
             android:layout_gravity="end"
@@ -69,10 +81,10 @@
     </com.google.android.material.appbar.MaterialToolbar>
 
     <androidx.constraintlayout.widget.Guideline
-        android:id="@+id/check_in_qr_code_scan_guideline_center"
+        android:id="@+id/qr_code_scan_guideline_center"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:orientation="horizontal"
         app:layout_constraintGuide_percent="0.5" />
 
-</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
+</androidx.constraintlayout.widget.ConstraintLayout>
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 a7b1ec503..c5c8028c7 100644
--- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
+++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
@@ -76,6 +76,9 @@
         <action
             android:id="@+id/action_mainFragment_to_submissionTestResultKeysSharedFragment"
             app:destination="@id/submissionTestResultKeysSharedFragment" />
+        <action
+            android:id="@+id/action_mainFragment_to_vaccinationNavGraph"
+            app:destination="@id/vaccination_nav_graph" />
     </fragment>
 
     <fragment
@@ -809,4 +812,5 @@
             android:name="testType"
             app:argType="de.rki.coronawarnapp.coronatest.type.CoronaTest$Type" />
     </fragment>
+
 </navigation>
diff --git a/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml
index b202c89c6..494e80cbe 100644
--- a/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml
+++ b/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml
@@ -59,7 +59,7 @@
         android:id="@+id/scanCheckInQrCodeFragment"
         android:name="de.rki.coronawarnapp.ui.presencetracing.attendee.scan.ScanCheckInQrCodeFragment"
         android:label="ScanCheckInQrCodeFragment"
-        tools:layout="@layout/fragment_scan_check_in_qr_code" />
+        tools:layout="@layout/fragment_scan_qr_code" />
 
     <fragment
         android:id="@+id/checkInsFragment"
@@ -97,4 +97,4 @@
             app:destination="@id/checkInOnboardingFragment" />
     </fragment>
 
-</navigation>
\ No newline at end of file
+</navigation>
diff --git a/Corona-Warn-App/src/main/res/navigation/vaccination_nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/vaccination_nav_graph.xml
index 5046d21d0..a0c4e7a1a 100644
--- a/Corona-Warn-App/src/main/res/navigation/vaccination_nav_graph.xml
+++ b/Corona-Warn-App/src/main/res/navigation/vaccination_nav_graph.xml
@@ -3,7 +3,19 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/vaccination_nav_graph"
-    app:startDestination="@id/vaccinationDetailsFragment">
+    app:startDestination="@id/vaccinationQrCodeScanFragment">
+
+    <fragment
+        android:id="@+id/vaccinationQrCodeScanFragment"
+        android:name="de.rki.coronawarnapp.vaccination.ui.scan.VaccinationQrCodeScanFragment"
+        android:label="VaccinationQrCodeScanFragment"
+        tools:layout="@layout/fragment_scan_qr_code">
+        <action
+            android:id="@+id/action_vaccinationQrCodeScanFragment_to_vaccinationDetailsFragment"
+            app:destination="@id/vaccinationDetailsFragment"
+            app:popUpTo="@id/vaccinationQrCodeScanFragment"
+            app:popUpToInclusive="true" />
+    </fragment>
 
     <fragment
         android:id="@+id/vaccinationListFragment"
@@ -28,4 +40,4 @@
             android:name="vaccinationCertificateId"
             app:argType="string" />
     </fragment>
-</navigation>
\ No newline at end of file
+</navigation>
diff --git a/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml
index f40c3a635..54a86d4e3 100644
--- a/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml
+++ b/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml
@@ -70,4 +70,15 @@
     <string name="vaccination_card_status_vaccination_name">SARS-CoV-2 Impfschutz</string>
     <!-- XTXT: Homescreen vaccination status card vaccination status label -->
     <string name="vaccination_card_status_vaccination_incomplete">Unvollständiger Impfschutz</string>
-</resources>
\ No newline at end of file
+    <!-- XTXT: Vaccination QR code scan error message-->
+    <string name="error_vc_invalid">Dieser QR-Code ist kein gültiges Impfzertifikat.</string>
+    <!-- XTXT: Vaccination QR code scan error message-->
+    <string name="error_vc_not_yet_supported">Dieses Impfzertifikat wird in Ihrer App-Version noch nicht unterstützt. Bitte aktualisieren Sie Ihre App oder wenden Sie sich an die technische Hotline unter „App-Informationen“.</string>
+    <!-- XTXT: Vaccination QR code scan error message-->
+    <string name="error_vc_scan_again">Das Impfzertifikat konnte nicht auf Ihrem Smartphone gespeichert werden. Bitte versuchen Sie es später noch einmal oder wenden Sie sich an die technische Hotline unter „App-Informationen“.</string>
+    <!-- XTXT: Vaccination QR code scan error message-->
+    <string name="error_vc_already_registered">Das Impfzertifikat ist bereits in Ihrer App registriert.</string>
+    <!-- XTXT: Vaccination QR code scan error message-->
+    <string name="error_vc_different_person">Die persönlichen Daten dieses Impfzertifikats stimmen nicht mit denen der bereits registrierten Zertifikate überein. Sie können in der App nur Zertifikate einer Person registrieren.</string>
+
+</resources>
diff --git a/Corona-Warn-App/src/main/res/values/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values/vaccination_strings.xml
index 5bc2d27d1..ca5e6519b 100644
--- a/Corona-Warn-App/src/main/res/values/vaccination_strings.xml
+++ b/Corona-Warn-App/src/main/res/values/vaccination_strings.xml
@@ -70,4 +70,15 @@
     <string name="vaccination_card_status_vaccination_name">SARS-CoV-2 Impfschutz</string>
     <!-- XTXT: Homescreen card vaccination status label -->
     <string name="vaccination_card_status_vaccination_incomplete">Unvollständiger Impfschutz</string>
-</resources>
\ No newline at end of file
+
+    <!-- XTXT: Vaccination QR code scan error message-->
+    <string name="error_vc_invalid"></string>
+    <!-- XTXT: Vaccination QR code scan error message-->
+    <string name="error_vc_not_yet_supported"></string>
+    <!-- XTXT: Vaccination QR code scan error message-->
+    <string name="error_vc_scan_again"></string>
+    <!-- XTXT: Vaccination QR code scan error message-->
+    <string name="error_vc_already_registered"></string>
+    <!-- XTXT: Vaccination QR code scan error message-->
+    <string name="error_vc_different_person"></string>
+</resources>
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt
index 1ebc70a0d..9f5ac095f 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt
@@ -1,13 +1,16 @@
 package de.rki.coronawarnapp.vaccination.core.qrcode
 
+import com.google.gson.Gson
 import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED
-import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED
 import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED
+import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_ISS
 import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY
 import de.rki.coronawarnapp.vaccination.decoder.Base45Decoder
 import de.rki.coronawarnapp.vaccination.decoder.ZLIBDecompressor
 import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.matchers.shouldBe
+import org.joda.time.Instant
+import org.joda.time.LocalDate
 import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
 
@@ -16,7 +19,7 @@ class VaccinationQRCodeExtractorTest : BaseTest() {
     private val base45Decoder = Base45Decoder()
     private val ZLIBDecompressor = ZLIBDecompressor()
     private val healthCertificateCOSEDecoder = HealthCertificateCOSEDecoder()
-    private val vaccinationCertificateV1Decoder = VaccinationCertificateV1Parser()
+    private val vaccinationCertificateV1Decoder = VaccinationCertificateV1Parser(Gson())
 
     private val extractor = VaccinationQRCodeExtractor(
         base45Decoder,
@@ -36,10 +39,47 @@ class VaccinationQRCodeExtractorTest : BaseTest() {
     }
 
     @Test
-    fun `valid encoding but not a health certificate fails with HC_CBOR_DECODING_FAILED`() {
+    fun `happy path extraction with data`() {
+        val qrCode = extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode3)
+
+        with(qrCode.parsedData.header) {
+            issuer shouldBe "AT"
+            issuedAt shouldBe Instant.ofEpochSecond(1620392021)
+            expiresAt shouldBe Instant.ofEpochSecond(1620564821)
+        }
+
+        with(qrCode.parsedData.vaccinationCertificate) {
+            with(nameData) {
+                familyName shouldBe "Musterfrau-Gößinger"
+                familyNameStandardized shouldBe "MUSTERFRAU<GOESSINGER"
+                givenName shouldBe "Gabriele"
+                givenNameStandardized shouldBe "GABRIELE"
+            }
+            dob shouldBe "1998-02-26"
+            dateOfBirth shouldBe LocalDate.parse("1998-02-26")
+            version shouldBe "1.0.0"
+
+            with(vaccinationDatas[0]) {
+                uniqueCertificateIdentifier shouldBe "urn:uvci:01:AT:10807843F94AEE0EE5093FBC254BD813P"
+                countryOfVaccination shouldBe "AT"
+                doseNumber shouldBe 1
+                dt shouldBe "2021-02-18"
+                certificateIssuer shouldBe "BMSGPK Austria"
+                marketAuthorizationHolderId shouldBe "ORG-100030215"
+                medicalProductId shouldBe "EU/1/20/1528"
+                totalSeriesOfDoses shouldBe 2
+                targetId shouldBe "840539006"
+                vaccineId shouldBe "1119305005"
+                vaccinatedAt shouldBe LocalDate.parse("2021-02-18")
+            }
+        }
+    }
+
+    @Test
+    fun `valid encoding but not a health certificate fails with VC_HC_CWT_NO_ISS`() {
         shouldThrow<InvalidHealthCertificateException> {
             extractor.extract(VaccinationQrCodeTestData.validEncoded)
-        }.errorCode shouldBe HC_CBOR_DECODING_FAILED
+        }.errorCode shouldBe VC_HC_CWT_NO_ISS
     }
 
     @Test
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQrCodeTestData.java b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQrCodeTestData.java
index 2bd0706ce..7db220536 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQrCodeTestData.java
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQrCodeTestData.java
@@ -5,4 +5,5 @@ public class VaccinationQrCodeTestData {
     static public String validVaccinationQrCode2 = "HC1:NCFOXN%TS3DHZN4HAF*PQFKKGTNA.Q/R8WRU2FC6L9N*CH PC.IU:N AJPJPC%OQHIZC4.OI1RM8ZA.A53XHMKN4NN3F85QNCY0O%0VZ001HOC9JU0D0HT0HO1PM:K$$09B9LW4T*8+DC%H0PZBITH$*SBAKYE9*FJTJAHD4UDADPSDJIM4KF/B0C2SFIH:9$GCQOS62PR6WPHN6D7LLK*2HG%89UV-0LZ 2ZJJ4FF86O:HO73SM1IO-O.Z80GHS-O:S9UZ4+FJE 4Y3LL/II 07LPMIH-O9XZQSH9R$FXQGDVBK*RZP3:*DG1W7SGT$7S%RMSG2UQYI9*FGCPAXRQ3E2N+E .1:L7O:7X/5Q+MSA7G6MBYO+JQLHP71RJW63X7VUONC6V35HW6SZ6FT5D75W9AV88E34+V4YC5/HQWOQ6$S4N4N31SHPO3Q0E447H9VAK:6.5G$N3ZF7W2SBJT7QG+8UJII3MACIBG2U76MGX3$YB.S7PIJRVOBTN6DTEUIOS7ZKJJEL%.B PT2LO36KT8SP50M/O$4";
     static public String validEncoded = "6BFB 9B8OYK3DR3D92BSQAQAHSOMEQ3%1GEVQT4H4O8G3.13G$H6+DH.157SWEV21SD7F2OPY1O-9LRFG0NGCUEPS5LLKJ:1CEJTLA2SADI887A/P3UHL20FTA9ZTRPSVUXO19LEZBQF3VJE$77D5FFC91ZFKCPP%90VS09P2QDQBCMY7-AE0/RW1R:ICP76XRS5UGC82WDNRJ9R7SX331MI9C7WNE5ZL1795NTA/P-35.N65O65ZQ8SU2:KY:C9K9PKD6+K%DI$YQ-9A:CKZ+5HPQNIF7N3K UEU6GEKHCO03MC%QN+LN+C5TTB1B94EC$38QC5O5DP262N:X7JYR/XH/A8%-1KZFTODRY3I 859G-IS9TMY4JM21TAV$N2NK3%BW8K7GI6%O8DUKUT036EF$8:32RBK*0IHJISK5SLTT21KYE7 U/316$I08A/XBU4IZYAGD3UVOJQI2YH3JMXHS1IPE%FOJN$HOV%B3FWCDCP65/%RKP2W2M4A9X7GETNASOXZ0Q/Q5LUNMJ QH+-2:4FW$33+4 +AY7GV-15/717GXY4H4O.:RM/USWV70PV8NGL5XP15NQ3K217GC:1WQEJNBK1RU6J.4K9/J%VQOHA+EW I0YMQ 0";
     static public String certificateMissing = "HC1:NCFNA0%00FFWTWGVLKJ99K83X4C8DTTMMX*4P8B3XK2F3$8JVJG2F3$%IQJG/IC6TAY50.FK6ZK6:ETPCBEC8ZKW.CNWE.Y92OAGY82+8UB8-R7/0A1OA1C9K09UIAW.CE$E7%E7WE KEVKER EB39W4N*6K3/D5$CMPCG/DA8DBB85IAAY8WY8I3DA8D0EC*KE: CZ CO/EZKEZ96446C56GVC*JC1A6NA73W5KF6TF627BSKL*8F.MLCM6$-I99MG$8THRJSCJVM/*V:0EY1QU 77*D9KR$SKIP5S-I2-RA1CC06+CHPYQX96*SUF3WZ36NM3XPK1P8.MAFZ6SHB";
+    static public String validVaccinationQrCode3 = "HC1:NCFOXN%TS3DH3ZSUZK+.V0ETD%65NL-AH%TAIOOW%I-1W0658WA/UAN9AAT4V22F/8X*G3M9JUPY0BX/KR96R/S09T./0LWTKD33236J3TA3M*4VV2 73-E3ND3DAJ-43%*48YIB73A*G3W19UEBY5:PI0EGSP4*2D$43B+2SEB7:I/2DY73CIBC:G 7376BXBJBAJ UNFMJCRN0H3PQN*E33H3OA70M3FMJIJN523S+0B/S7-SN2H N37J3JFTULJ5CB3ZCIATULV:SNS8F-67N%21Q21$48X2+36D-I/2DBAJDAJCNB-43SZ4RZ4E%5B/9OK53:UCT16DEZIE IE9.M CVCT1+9V*QERU1MK93P5 U02Y9.G9/G9F:QQ28R3U6/V.*NT*QM.SY$N-P1S29 34S0BYBRC.UYS1U%O6QKN*Q5-QFRMLNKNM8JI0EUGP$I/XK$M8-L9KDI:ZH2E4EVS6O0FVAQNJT:EZ6Q%D0*T1.XSDYV0.VI2OKSNODA.BOD:C.OTXS02:M5OGJIF4LHJW7FFJ2NLGFL/EE%CJF+KM%V$AUS:H+NARLK IBMMG";
 }
-- 
GitLab