diff --git a/app/src/internal/AndroidManifest.xml b/app/src/internal/AndroidManifest.xml
index b89dff89ff6f..82091edb2c82 100644
--- a/app/src/internal/AndroidManifest.xml
+++ b/app/src/internal/AndroidManifest.xml
@@ -7,6 +7,10 @@
android:name="com.duckduckgo.app.dev.settings.DevSettingsActivity"
android:label="@string/devSettingsTitle"
android:parentActivityName="com.duckduckgo.app.settings.SettingsActivity" />
+
+ when (command) {
+ is TriggerNotification -> addNotification(id = command.notificationItem.id, notification = command.notificationItem.notification)
+ }
+ }.launchIn(lifecycleScope)
+ }
+
+ private fun render(viewState: ViewState) {
+ viewState.notificationItems.forEach { notificationItem ->
+ TwoLineListItem(this).apply {
+
+ setPrimaryText(notificationItem.title)
+ setSecondaryText(notificationItem.subtitle)
+ setOnClickListener {
+ viewModel.onNotificationItemClick(notificationItem)
+ }
+ }.also {
+ binding.notificationsContainer.addView(it)
+ }
+ }
+ }
+
+ private fun addNotification(
+ id: Int,
+ notification: Notification
+ ) {
+ NotificationManagerCompat.from(this)
+ .checkPermissionAndNotify(context = this, id = id, notification = notification)
+ }
+
+ companion object {
+
+ fun intent(context: Context): Intent {
+ return Intent(context, NotificationsActivity::class.java)
+ }
+ }
+}
diff --git a/app/src/internal/java/com/duckduckgo/app/dev/settings/notifications/NotificationsViewModel.kt b/app/src/internal/java/com/duckduckgo/app/dev/settings/notifications/NotificationsViewModel.kt
new file mode 100644
index 000000000000..f5ce059426b1
--- /dev/null
+++ b/app/src/internal/java/com/duckduckgo/app/dev/settings/notifications/NotificationsViewModel.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2024 DuckDuckGo
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.duckduckgo.app.dev.settings.notifications
+
+import android.app.Notification
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.duckduckgo.anvil.annotations.ContributesViewModel
+import com.duckduckgo.app.dev.settings.notifications.NotificationViewModel.ViewState.NotificationItem
+import com.duckduckgo.app.notification.NotificationFactory
+import com.duckduckgo.app.notification.model.SchedulableNotificationPlugin
+import com.duckduckgo.app.survey.api.SurveyRepository
+import com.duckduckgo.app.survey.model.Survey
+import com.duckduckgo.app.survey.model.Survey.Status.SCHEDULED
+import com.duckduckgo.app.survey.notification.SurveyAvailableNotification
+import com.duckduckgo.common.utils.DispatcherProvider
+import com.duckduckgo.common.utils.plugins.PluginPoint
+import com.duckduckgo.di.scopes.ActivityScope
+import javax.inject.Inject
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+@ContributesViewModel(ActivityScope::class)
+class NotificationViewModel @Inject constructor(
+ private val dispatcher: DispatcherProvider,
+ private val schedulableNotificationPluginPoint: PluginPoint,
+ private val factory: NotificationFactory,
+ private val surveyRepository: SurveyRepository,
+) : ViewModel() {
+
+ data class ViewState(
+ val notificationItems: List = emptyList(),
+ ) {
+
+ data class NotificationItem(
+ val id: Int,
+ val title: String,
+ val subtitle: String,
+ val notification: Notification
+ )
+ }
+
+ sealed class Command {
+ data class TriggerNotification(val notificationItem: NotificationItem) : Command()
+ }
+
+ private val _viewState = MutableStateFlow(ViewState())
+ val viewState = _viewState.asStateFlow()
+
+ private val _command = Channel(1, BufferOverflow.DROP_OLDEST)
+ val command = _command.receiveAsFlow()
+
+ init {
+ viewModelScope.launch {
+ val notificationItems = schedulableNotificationPluginPoint.getPlugins().map { plugin ->
+
+ // The survey notification will crash if we do not have a survey in the database
+ if (plugin.getSchedulableNotification().javaClass == SurveyAvailableNotification::class.java) {
+ withContext(dispatcher.io()) {
+ addTestSurvey()
+ }
+ }
+
+ // the survey intent hits the DB, so we need to do this on IO
+ val launchIntent = withContext(dispatcher.io()) { plugin.getLaunchIntent() }
+
+ NotificationItem(
+ id = plugin.getSpecification().systemId,
+ title = plugin.getSpecification().title,
+ subtitle = plugin.getSpecification().description,
+ notification = factory.createNotification(plugin.getSpecification(), launchIntent, null),
+ )
+ }
+
+ _viewState.update { it.copy(notificationItems = notificationItems) }
+ }
+ }
+
+ private fun addTestSurvey() {
+ surveyRepository.persistSurvey(
+ Survey(
+ "testSurveyId",
+ "https://youtu.be/dQw4w9WgXcQ?si=iztopgFbXoWUnoOE",
+ daysInstalled = 1,
+ status = SCHEDULED,
+ ),
+ )
+ }
+
+ fun onNotificationItemClick(notificationItem: NotificationItem) {
+ viewModelScope.launch {
+ _command.send(Command.TriggerNotification(notificationItem))
+ }
+ }
+}
diff --git a/app/src/internal/res/layout/activity_notifications.xml b/app/src/internal/res/layout/activity_notifications.xml
new file mode 100644
index 000000000000..2de682d5dd28
--- /dev/null
+++ b/app/src/internal/res/layout/activity_notifications.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file