diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt
index 22cb9385bb4fd26efa3f9e293b467fabbd9d9ecd..ff5f2e959b4fb614a05e801f14c2c8eab8b63580 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt
@@ -24,65 +24,111 @@ import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
 import androidx.compose.material.AlertDialog
 import androidx.compose.material.Button
 import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Icon
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Text
 import androidx.compose.material.TextButton
 import androidx.compose.material.TextField
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Error
+import androidx.compose.material.icons.filled.Warning
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel.AddContactError
+import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel.AliasInvalidError
+import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel.ErrorContactAlreadyExists
+import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel.ErrorPendingAlreadyExists
+import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel.LinkInvalidError
+import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel.OwnLinkError
+import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel.PublicKeyInvalidError
+import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel.RemoteInvalidError
+import org.briarproject.briar.desktop.dialogs.DialogType.ERROR
+import org.briarproject.briar.desktop.dialogs.DialogType.WARNING
+import org.briarproject.briar.desktop.theme.Orange500
+import org.briarproject.briar.desktop.theme.Red500
+import org.briarproject.briar.desktop.ui.Constants.DIALOG_WIDTH
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nF
+import org.briarproject.briar.desktop.utils.PreviewUtils
 import org.briarproject.briar.desktop.utils.PreviewUtils.preview
 import org.briarproject.briar.desktop.viewmodel.viewModel
 
+const val link = "briar://ady23gvb2r76afe5zhxh5kvnh4b22zrcnxibn63tfknrdcwrw7zrs"
+
 fun main() = preview(
     "visible" to true,
     "remote link" to "",
-    "local link" to "briar://ady23gvb2r76afe5zhxh5kvnh4b22zrcnxibn63tfknrdcwrw7zrs",
+    "local link" to link,
     "alias" to "Alice",
-) {
-    if (getBooleanParameter("visible")) {
-        AddContactDialog(
-            onClose = { setBooleanParameter("visible", false) },
-            remoteHandshakeLink = getStringParameter("remote link"),
-            setRemoteHandshakeLink = { link -> setStringParameter("remote link", link) },
-            alias = getStringParameter("alias"),
-            setAddContactAlias = { alias -> setStringParameter("alias", alias) },
-            handshakeLink = getStringParameter("local link"),
-            onSubmitAddContactDialog = {}
+    "error visible" to false,
+    "error type" to PreviewUtils.Values(
+        0,
+        listOf(
+            OwnLinkError(link),
+            RemoteInvalidError(link),
+            AliasInvalidError(link, ""),
+            LinkInvalidError(link),
+            PublicKeyInvalidError(link),
+            ErrorContactAlreadyExists(link, "David", "chuck"),
+            ErrorPendingAlreadyExists(link, "Frank", "chuck"),
         )
-    }
+    ) { error -> error.javaClass.simpleName },
+) {
+    val localLink = getStringParameter("local link")
+    AddContactDialog(
+        onClose = { setBooleanParameter("visible", false) },
+        visible = getBooleanParameter("visible"),
+        remoteHandshakeLink = getStringParameter("remote link"),
+        setRemoteHandshakeLink = { link -> setStringParameter("remote link", link) },
+        alias = getStringParameter("alias"),
+        setAddContactAlias = { alias -> setStringParameter("alias", alias) },
+        handshakeLink = localLink,
+        onSubmitAddContactDialog = { setBooleanParameter("error visible", true) },
+        error = if (getBooleanParameter("error visible")) getGenericParameter("error type") else null,
+        onErrorDialogDismissed = { setBooleanParameter("error visible", false) },
+    )
 }
 
 @Composable
 fun AddContactDialog(
-    onClose: () -> Unit,
     viewModel: AddContactViewModel = viewModel(),
 ) = AddContactDialog(
-    onClose = onClose,
+    viewModel::dismissDialog,
+    viewModel.visible.value,
     viewModel.remoteHandshakeLink.value,
     viewModel::setRemoteHandshakeLink,
     viewModel.alias.value,
     viewModel::setAddContactAlias,
     viewModel.handshakeLink.value,
     viewModel::onSubmitAddContactDialog,
+    viewModel.error.value,
+    viewModel::clearError,
 )
 
 @OptIn(ExperimentalMaterialApi::class)
 @Composable
 fun AddContactDialog(
     onClose: () -> Unit,
+    visible: Boolean,
     remoteHandshakeLink: String,
     setRemoteHandshakeLink: (String) -> Unit,
     alias: String,
     setAddContactAlias: (String) -> Unit,
     handshakeLink: String,
     onSubmitAddContactDialog: () -> Unit,
+    error: AddContactError?,
+    onErrorDialogDismissed: () -> Unit,
 ) {
+    if (!visible) {
+        return
+    }
     AlertDialog(
         onDismissRequest = onClose,
         text = {
@@ -130,7 +176,11 @@ fun AddContactDialog(
             }
         },
         confirmButton = {
-            Button(onClick = { onSubmitAddContactDialog(); onClose() }) {
+            Button(
+                onClick = {
+                    onSubmitAddContactDialog()
+                }
+            ) {
                 Text("Add")
             }
         },
@@ -142,4 +192,55 @@ fun AddContactDialog(
             }
         },
     )
+    if (error != null) {
+        val (type, title, message) = errorMessage(error)
+        val (icon, color) = when (type) {
+            WARNING -> Icons.Filled.Warning to Orange500
+            ERROR -> Icons.Filled.Error to Red500
+        }
+        AlertDialog(
+            onDismissRequest = onErrorDialogDismissed,
+            confirmButton = {
+                TextButton(onErrorDialogDismissed) {
+                    Text(i18n("ok"))
+                }
+            },
+            modifier = Modifier.widthIn(min = DIALOG_WIDTH),
+            title = {
+                Row(
+                    verticalAlignment = Alignment.CenterVertically,
+                    horizontalArrangement = Arrangement.spacedBy(8.dp)
+                ) {
+                    Icon(
+                        imageVector = icon,
+                        contentDescription = title,
+                        tint = color
+                    )
+                    Text(title)
+                }
+            },
+            text = { Text(message) }
+        )
+    }
+}
+
+fun errorMessage(error: AddContactError) = when (error) {
+    is OwnLinkError -> Triple(ERROR, i18n("error"), i18n("introduction.error.own_link"))
+    is RemoteInvalidError -> Triple(ERROR, i18n("error"), i18n("introduction.error.remote_invalid"))
+    is AliasInvalidError -> Triple(ERROR, i18n("error"), i18n("introduction.error.alias_invalid"))
+    is LinkInvalidError -> Triple(ERROR, i18n("error"), i18nF("introduction.error.link_invalid", error.link))
+    is PublicKeyInvalidError -> Triple(
+        ERROR, i18n("error"),
+        i18nF("introduction.error.public_key_invalid", error.link)
+    )
+    is ErrorContactAlreadyExists -> {
+        val intro = i18nF("introduction.error.contact_already_exists", error.existingName)
+        var explanation = i18nF("introduction.error.duplicate_contact_explainer", error.existingName, error.alias)
+        Triple(WARNING, i18n("introduction.error.adding_failed"), (intro + "\n\n" + explanation))
+    }
+    is ErrorPendingAlreadyExists -> {
+        val intro = i18nF("introduction.error.pending_contact_already_exists", error.existingAlias)
+        var explanation = i18nF("introduction.error.duplicate_contact_explainer", error.existingAlias, error.alias)
+        Triple(WARNING, i18n("introduction.error.adding_failed"), (intro + "\n\n" + explanation))
+    }
 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactViewModel.kt
index c1b6773c63d129e9fff1827e1816116e66f6433b..235d1cec4261fbc4d7d07fd0b5fa8b9e97da2a47 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactViewModel.kt
@@ -48,18 +48,44 @@ constructor(
         private val LOG = KotlinLogging.logger {}
     }
 
+    sealed interface AddContactError
+
+    data class OwnLinkError(val link: String) : AddContactError
+    data class RemoteInvalidError(val link: String) : AddContactError
+    data class AliasInvalidError(val link: String, val alias: String) : AddContactError
+    data class LinkInvalidError(val link: String) : AddContactError
+    data class PublicKeyInvalidError(val link: String) : AddContactError
+
+    data class ErrorContactAlreadyExists(val link: String, val existingName: String, val alias: String) :
+        AddContactError
+
+    data class ErrorPendingAlreadyExists(val link: String, val existingAlias: String, val alias: String) :
+        AddContactError
+
     override fun onInit() {
         super.onInit()
         fetchHandshakeLink()
     }
 
+    private val _visible = mutableStateOf(false)
     private val _alias = mutableStateOf("")
     private val _remoteHandshakeLink = mutableStateOf("")
     private val _handshakeLink = mutableStateOf("")
+    private val _error = mutableStateOf<AddContactError?>(null)
 
+    val visible = _visible.asState()
     val alias = _alias.asState()
     val remoteHandshakeLink = _remoteHandshakeLink.asState()
     val handshakeLink = _handshakeLink.asState()
+    val error = _error.asState()
+
+    fun showDialog() {
+        _visible.value = true
+    }
+
+    fun dismissDialog() {
+        _visible.value = false
+    }
 
     fun setAddContactAlias(alias: String) {
         _alias.value = alias
@@ -80,13 +106,17 @@ constructor(
         addPendingContact(link, alias)
     }
 
+    fun clearError() {
+        _error.value = null
+    }
+
     private fun addPendingContact(link: String, alias: String) {
         // ignore preceding and trailing whitespace
         val matcher = HandshakeLinkConstants.LINK_REGEX.matcher(link.trim())
         // check if the link is well-formed
         if (!matcher.matches()) {
             LOG.warn { "Remote handshake link is invalid" }
-            // TODO: show message to user
+            _error.value = RemoteInvalidError(link)
             return
         }
         // compare with own link
@@ -94,25 +124,30 @@ constructor(
         val withSchema = "briar://$withoutSchema"
         if (_handshakeLink.value == withSchema) {
             LOG.warn { "Please enter contact's link, not your own" }
-            // TODO: show warning to user
+            _error.value = OwnLinkError(link)
             return
         }
 
         if (aliasIsInvalid(alias)) {
             LOG.warn { "Alias is invalid" }
-            // TODO: show message to user
+            _error.value = AliasInvalidError(link, alias)
             return
         }
 
         runOnDbThreadWithTransaction(false) { txn ->
             try {
                 contactManager.addPendingContact(txn, link, alias)
+                txn.attach {
+                    _visible.value = false
+                    _alias.value = ""
+                    _remoteHandshakeLink.value = ""
+                }
             } catch (e: FormatException) {
-                LOG.warn(e) { "Link is invalid" }
-                // TODO: show error to user
+                LOG.warn { "Link is invalid: $link" }
+                _error.value = LinkInvalidError(link)
             } catch (e: GeneralSecurityException) {
-                LOG.warn(e) { "Public key is invalid" }
-                // TODO: show error to user
+                LOG.warn { "Public key is invalid: $link" }
+                _error.value = PublicKeyInvalidError(link)
             }
             /*
             TODO: Warn user that the following two errors might be an attack
@@ -122,11 +157,11 @@ constructor(
 
             */
             catch (e: ContactExistsException) {
-                LOG.warn(e) { "Contact already exists" }
-                // TODO: show error to user
+                LOG.warn { "Contact already exists: $link" }
+                _error.value = ErrorContactAlreadyExists(link, e.remoteAuthor.name, alias)
             } catch (e: PendingContactExistsException) {
-                LOG.warn(e) { "Pending Contact already exists" }
-                // TODO: show error to user
+                LOG.warn { "Pending contact already exists: $link" }
+                _error.value = ErrorPendingAlreadyExists(link, e.pendingContact.alias, alias)
             }
         }
     }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt
index 5ac782b9fbc99d9ffd96cd3ba4d578c557882be3..aa94d0c9bd77334f8b83babe1a2e0302bbea6f5d 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt
@@ -37,10 +37,6 @@ import androidx.compose.material.Text
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.PersonAdd
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -51,6 +47,7 @@ import org.briarproject.briar.desktop.contact.ContactListViewModel
 import org.briarproject.briar.desktop.contact.PendingContactIdWrapper
 import org.briarproject.briar.desktop.contact.RealContactIdWrapper
 import org.briarproject.briar.desktop.contact.add.remote.AddContactDialog
+import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel
 import org.briarproject.briar.desktop.ui.BriarLogo
 import org.briarproject.briar.desktop.ui.Constants.PARAGRAPH_WIDTH
 import org.briarproject.briar.desktop.ui.VerticalDivider
@@ -60,12 +57,12 @@ import org.briarproject.briar.desktop.viewmodel.viewModel
 @Composable
 fun PrivateMessageScreen(
     viewModel: ContactListViewModel = viewModel(),
+    addContactViewModel: AddContactViewModel = viewModel(),
 ) {
-    var isContactDialogVisible by remember { mutableStateOf(false) }
-    if (isContactDialogVisible) AddContactDialog(onClose = { isContactDialogVisible = false })
+    AddContactDialog()
 
     if (viewModel.noContactsYet.value) {
-        NoContactsYet(onContactAdd = { isContactDialogVisible = true })
+        NoContactsYet(onContactAdd = { addContactViewModel.showDialog() })
         return
     }
 
@@ -76,7 +73,7 @@ fun PrivateMessageScreen(
             viewModel::selectContact,
             viewModel.filterBy.value,
             viewModel::setFilterBy,
-            onContactAdd = { isContactDialogVisible = true }
+            onContactAdd = { addContactViewModel.showDialog() }
         )
         VerticalDivider()
         Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/DialogType.kt b/src/main/kotlin/org/briarproject/briar/desktop/dialogs/DialogType.kt
new file mode 100644
index 0000000000000000000000000000000000000000..180850ff913308c796fcbc3552afc4b929e05d31
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/dialogs/DialogType.kt
@@ -0,0 +1,24 @@
+/*
+ * Briar Desktop
+ * Copyright (C) 2021-2022 The Briar Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package org.briarproject.briar.desktop.dialogs
+
+enum class DialogType {
+    ERROR,
+    WARNING
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/theme/Colors.kt b/src/main/kotlin/org/briarproject/briar/desktop/theme/Colors.kt
index 2cb6961e6323e663a009db8eae657e3a2ccab2c8..87d720e5931a511d69acae49de2ae869c89a3ba7 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/theme/Colors.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/theme/Colors.kt
@@ -39,6 +39,7 @@ val Gray950 = Color(0xff1f1f1f)
 val materialDarkBg = Color(0xff121212)
 
 val Red500 = Color(0xffdb3b21)
+val Orange500 = Color(0xfffc9403)
 
 val Blue400 = Color(0xff418cd8)
 val Blue500 = Color(0xff1f78d1) // todo: unused in Android
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/Constants.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/Constants.kt
index d860f80b8f4cdfe80313b67ff66793ca3c6bb38e..0101d6856398871a41711e66cf8691e126497f1b 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/ui/Constants.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/Constants.kt
@@ -25,4 +25,5 @@ object Constants {
     val COLUMN_WIDTH = 275.dp
     val STARTUP_FIELDS_WIDTH = 400.dp
     val PARAGRAPH_WIDTH = 540.dp
+    val DIALOG_WIDTH = 400.dp
 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt b/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt
index 0d0594748ba3be162ec6489b935f92e74911308f..d4337e68519e1b2e6af584b46e6c090b97496833 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt
@@ -24,10 +24,14 @@ import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.material.DropdownMenu
+import androidx.compose.material.DropdownMenuItem
 import androidx.compose.material.Icon
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Slider
@@ -39,8 +43,11 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
@@ -58,7 +65,7 @@ object PreviewUtils {
 
         val parameters = mutableMapOf<String, MutableState<Any>>()
 
-        private inline fun <reified T> getDatatype(name: String): T {
+        inline fun <reified T> getDatatype(name: String): T {
             val state = parameters[name] ?: throw IllegalArgumentException("No parameter found with name '$name'")
             if (state.value !is T) throw IllegalArgumentException("Parameter '$name' is not of type ${T::class.simpleName}")
             return state.value as T
@@ -90,6 +97,8 @@ object PreviewUtils {
 
         fun setFloatParameter(name: String, value: Float) = setDatatype(name, value)
 
+        inline fun <reified T : Any> getGenericParameter(name: String) = getDatatype<T>(name)
+
         fun getRandomId() = random.nextBytes(UniqueId.LENGTH)
 
         @Composable
@@ -142,8 +151,50 @@ object PreviewUtils {
     }
 
     @Composable
-    private fun PreviewScope.addFloatSliderParameter(name: String, initial: FloatSlider) = addParameter(name, initial.initial) { value ->
-        Slider(value.value, { value.value = it }, valueRange = initial.min..initial.max, modifier = Modifier.width(400.dp))
+    private fun PreviewScope.addFloatSliderParameter(name: String, initial: FloatSlider) =
+        addParameter(name, initial.initial) { value ->
+            Slider(
+                value.value,
+                { value.value = it },
+                valueRange = initial.min..initial.max,
+                modifier = Modifier.width(400.dp)
+            )
+        }
+
+    @Composable
+    private fun <T> PreviewScope.addDropDownParameter(
+        name: String,
+        initial: Values<T>,
+    ) {
+        var expanded by remember { mutableStateOf(false) }
+        val items = initial.values
+        val initialValue = items[initial.initial]
+        var selectedIndex by remember { mutableStateOf(initial.initial) }
+        addParameter(name, initialValue!!) { value ->
+            Box(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart)) {
+                Text(
+                    initial.toString(items[selectedIndex]),
+                    modifier = Modifier.fillMaxWidth().clickable(onClick = { expanded = true })
+                )
+                DropdownMenu(
+                    expanded = expanded,
+                    onDismissRequest = { expanded = false },
+                    modifier = Modifier.fillMaxWidth()
+                ) {
+                    items.forEachIndexed { index, s ->
+                        DropdownMenuItem(
+                            onClick = {
+                                selectedIndex = index
+                                expanded = false
+                                value.value = items[index]!!
+                            }
+                        ) {
+                            Text(text = initial.toString(s))
+                        }
+                    }
+                }
+            }
+        }
     }
 
     /**
@@ -171,6 +222,7 @@ object PreviewUtils {
                                 is Long -> scope.addLongParameter(name, initial)
                                 is Float -> scope.addFloatParameter(name, initial)
                                 is FloatSlider -> scope.addFloatSliderParameter(name, initial)
+                                is Values<*> -> scope.addDropDownParameter(name, initial)
                                 else -> throw IllegalArgumentException("Type ${initial::class.simpleName} is not supported for previewing.")
                             }
                         }
@@ -193,4 +245,10 @@ object PreviewUtils {
         val min: Float,
         val max: Float,
     )
+
+    data class Values<T>(
+        val initial: Int,
+        val values: List<T>,
+        val toString: (T) -> String,
+    )
 }