[Bloganuary] Add attribution property to dashboard card tracking

Fixes #19594

-----

## To Test:

- Install Jetpack and log in;
- Open "My Site" -> "HOME" with a blog selected;
- Verify the Blogging Prompts card is visible;
- Verify the track log is shown: `🔵 Tracked: blogging_prompts_my_site_card_viewed` **with the property** `"attribution"`;
- Tap `"Answer prompt` button: verify the track log is shown  `🔵 Tracked: blogging_prompts_my_site_card_answer_prompt_tapped` **with the property** `"attribution"`;
- Tap on `"My Site"` FAB and then tap the `"Answer prompt"` action: verify the track log is shown: `🔵 Tracked: my_site_create_sheet_answer_prompt_tapped` **with the property** `"attribution"`.

> [!WARNING]
> It's not clear to me how to test all the possibilities for `"attribution"` (none, `dayone` or `bloganuary`)

-----

## Regression Notes

1. Potential unintended areas of impact

    - Blogging prompts cards

2. What I did to test those areas of impact (or what existing automated tests I relied on)

    - Manual testing + updated unit tests

3. What automated tests I added (or what prevented me from doing so)

    - Updated `BloggingPromptsCardTrackHelperTest`, `BloggingPromptCardViewModelSliceTest`, `BloggingPromptsCardAnalyticsTrackerTest`, `WPMainActivityViewModelTest` and `MySiteViewModelTest`.

-----

## PR Submission Checklist:

- [x] I have completed the Regression Notes.
- [x] I have considered adding accessibility improvements for my changes.
- [x] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

-----

## UI Changes Testing Checklist:

- [ ] Portrait and landscape orientations.
- [ ] Light and dark modes.
- [ ] Fonts: Larger, smaller and bold text.
- [ ] High contrast.
- [ ] Talkback.
- [ ] Languages with large words or with letters/accents not frequently used in English.
- [ ] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [ ] Large and small screen sizes. (Tablet and smaller phones)
- [ ] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)

[Prompts] Add attribution property to dashboard card tracking
- update `blogging_prompts_my_site_card_viewed`
  - add property for `attribution` (none, `dayone`, `bloganuary`)
- update `blogging_prompts_my_site_card_answer_prompt_tapped`
  - add property for `attribution` (none, `dayone`, `bloganuary`)
- update `my_site_create_sheet_answer_prompt_tapped`
  - add property for `attribution` (none, `dayone`, `bloganuary`)

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/CompactBloggingPromptCardViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/CompactBloggingPromptCardViewHolder.kt
index b6793405407a..8d9fb79b03ed 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/main/CompactBloggingPromptCardViewHolder.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/CompactBloggingPromptCardViewHolder.kt
@@ -24,7 +24,7 @@ class CompactBloggingPromptCardViewHolder(
         setupAttributionContainer(action.attribution)

         answerButton.setOnClickListener {
-            action.onClickAction?.invoke(action.promptId)
+            action.onClickAction?.invoke(action.promptId, action.attribution)
         }
         promptHelpButtonContainer.setOnClickListener {
             action.onHelpAction?.invoke()
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MainActionListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/MainActionListItem.kt
index f7fe0d24a730..cfbac905fc29 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/main/MainActionListItem.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/MainActionListItem.kt
@@ -31,7 +31,7 @@ sealed class MainActionListItem {
         val isAnswered: Boolean,
         val promptId: Int,
         val attribution: BloggingPromptAttribution,
-        val onClickAction: ((promptId: Int) -> Unit)?,
+        val onClickAction: ((promptId: Int, attribution: BloggingPromptAttribution) -> Unit)?,
         val onHelpAction: (() -> Unit)?
     ) : MainActionListItem()
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/BloggingPromptsCardTrackHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/BloggingPromptsCardTrackHelper.kt
index 6fa89ea3b74d..08dc088750e3 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/BloggingPromptsCardTrackHelper.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/BloggingPromptsCardTrackHelper.kt
@@ -30,9 +30,14 @@ class BloggingPromptsCardTrackHelper @Inject constructor(
     private val waitingToTrack = AtomicBoolean(false)
     private val currentSite = AtomicReference<Int?>(null)

-    private fun onDashboardRefreshed() {
+    private fun onDashboardRefreshed(state: MySiteViewModel.State.SiteSelected?) {
+        val bloggingPromptCards = state?.dashboardData
+            ?.filterIsInstance<BloggingPromptCard>()
+            ?: listOf()
+
         latestPromptCardVisible.get()?.let { isPromptCardVisible ->
-            if (isPromptCardVisible) tracker.trackMySiteCardViewed()
+            val attribution = bloggingPromptCards.firstOrNull()?.attribution
+            if (isPromptCardVisible) tracker.trackMySiteCardViewed(attribution)
             waitingToTrack.set(false)
         } ?: run {
             waitingToTrack.set(true)
@@ -40,7 +45,11 @@ class BloggingPromptsCardTrackHelper @Inject constructor(
     }

-    fun onDashboardCardsUpdated(scope: CoroutineScope, bloggingPromptCards: List<BloggingPromptCard>) {
+    fun onDashboardCardsUpdated(scope: CoroutineScope, state: MySiteViewModel.State.SiteSelected?) {
+        val bloggingPromptCards = state?.dashboardData
+            ?.filterIsInstance<BloggingPromptCard>()
+            ?: listOf()
+
         // cancel any existing job (debouncing mechanism)
         dashboardUpdateDebounceJob?.cancel()

@@ -52,7 +61,8 @@ class BloggingPromptsCardTrackHelper @Inject constructor(

             latestPromptCardVisible.set(isVisible)
             if (isVisible && waitingToTrack.getAndSet(false)) {
-                tracker.trackMySiteCardViewed()
+                val attribution = bloggingPromptCards.firstOrNull()?.attribution
+                tracker.trackMySiteCardViewed(attribution)
             }
         }.also {
             it.invokeOnCompletion { cause ->
@@ -62,17 +72,22 @@ class BloggingPromptsCardTrackHelper @Inject constructor(
         }
     }

-    fun onResume() {
-        onDashboardRefreshed()
+    fun onResume(state: MySiteViewModel.State.SiteSelected?) {
+        onDashboardRefreshed(state)
     }

-    fun onSiteChanged(siteId: Int?) {
+    fun onSiteChanged(siteId: Int?, state: MySiteViewModel.State.SiteSelected?) {
         if (currentSite.getAndSet(siteId) != siteId) {
             latestPromptCardVisible.set(null)
-            onDashboardRefreshed()
+            onDashboardRefreshed(state)
         }
     }

+    private val BloggingPromptCard.attribution: String?
+        get() {
+            return (this as? BloggingPromptCard.BloggingPromptCardWithData)?.attribution?.value
+        }
+
     companion object {
         private const val PROMPT_CARD_VISIBLE_DEBOUNCE = 500L
     }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt
index 8b05a07112a9..dfa72c20e01d 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt
@@ -288,13 +288,13 @@ class MySiteViewModel @Inject constructor(

                 bloggingPromptCardViewModelSlice.onDashboardCardsUpdated(
                     viewModelScope,
-                    state.dashboardData.filterIsInstance<MySiteCardAndItem.Card.BloggingPromptCard>()
+                    state as? SiteSelected
                 )
                 state
             } else {
                 buildNoSiteState(currentAvatarUrl, avatarName)
             }
-            bloggingPromptCardViewModelSlice.onSiteChanged(site?.id)
+            bloggingPromptCardViewModelSlice.onSiteChanged(site?.id, state as? SiteSelected)

             dashboardCardPlansUtils.onSiteChanged(site?.id, state as? SiteSelected)

@@ -695,7 +695,7 @@ class MySiteViewModel @Inject constructor(
         isSiteSelected = false
         checkAndShowJetpackFullPluginInstallOnboarding()
         checkAndShowQuickStartNotice()
-        bloggingPromptCardViewModelSlice.onResume()
+        bloggingPromptCardViewModelSlice.onResume(uiModel.value as? SiteSelected)
         dashboardCardPlansUtils.onResume(uiModel.value as? SiteSelected)
         quickLinksItemViewModelSlice.onResume()
     }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptAttribution.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptAttribution.kt
index 453c3bca8c5e..2d678e1d15f9 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptAttribution.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptAttribution.kt
@@ -5,24 +5,25 @@ import androidx.annotation.StringRes
 import org.wordpress.android.R

 enum class BloggingPromptAttribution(
+    val value: String,
     @StringRes val contentRes: Int,
     @DrawableRes val iconRes: Int,
 ) {
-    NO_ATTRIBUTION(-1, -1),
+    NO_ATTRIBUTION("", -1, -1),
     DAY_ONE(
+        "dayone",
         R.string.my_site_blogging_prompt_card_attribution_dayone,
         R.drawable.ic_dayone_24dp,
     ),
     BLOGANUARY(
+        "bloganuary",
         R.string.my_site_blogging_prompt_card_attribution_bloganuary,
         R.drawable.ic_bloganuary_24dp,
     );

     companion object {
-        fun fromString(value: String): BloggingPromptAttribution = when (value) {
-            "dayone" -> DAY_ONE
-            "bloganuary" -> BLOGANUARY
-            else -> NO_ATTRIBUTION
-        }
+        fun fromString(value: String): BloggingPromptAttribution = entries
+            .firstOrNull { it.value == value }
+            ?: NO_ATTRIBUTION
     }
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptCardViewModelSlice.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptCardViewModelSlice.kt
index a71e95adccf2..93bc45fe6f0a 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptCardViewModelSlice.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptCardViewModelSlice.kt
@@ -11,10 +11,10 @@ import org.wordpress.android.ui.bloggingprompts.BloggingPromptsPostTagProvider
 import org.wordpress.android.ui.bloggingprompts.BloggingPromptsSettingsHelper
 import org.wordpress.android.ui.mysite.BloggingPromptCardNavigationAction
 import org.wordpress.android.ui.mysite.BloggingPromptsCardTrackHelper
-import org.wordpress.android.ui.mysite.MySiteCardAndItem
 import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams
 import org.wordpress.android.ui.mysite.MySiteSourceManager
 import org.wordpress.android.ui.mysite.MySiteUiState
+import org.wordpress.android.ui.mysite.MySiteViewModel
 import org.wordpress.android.ui.mysite.SelectedSiteRepository
 import org.wordpress.android.ui.mysite.SiteNavigationAction
 import org.wordpress.android.ui.pages.SnackbarMessageHolder
@@ -53,7 +53,7 @@ class BloggingPromptCardViewModelSlice @Inject constructor(
         return MySiteCardAndItemBuilderParams.BloggingPromptCardBuilderParams(
             bloggingPrompt = bloggingPromptUpdate?.promptModel,
             onShareClick = this::onBloggingPromptShareClick,
-            onAnswerClick = this::onBloggingPromptAnswerClick,
+            onAnswerClick = { id -> onBloggingPromptAnswerClick(id, bloggingPromptUpdate?.promptModel?.attribution) },
             onSkipClick = this::onBloggingPromptSkipClick,
             onViewMoreClick = this::onBloggingPromptViewMoreClick,
             onViewAnswersClick = this::onBloggingPromptViewAnswersClick,
@@ -65,8 +65,8 @@ class BloggingPromptCardViewModelSlice @Inject constructor(
         _onNavigation.value = Event(BloggingPromptCardNavigationAction.SharePrompt(message))
     }

-    private fun onBloggingPromptAnswerClick(promptId: Int) {
-        bloggingPromptsCardAnalyticsTracker.trackMySiteCardAnswerPromptClicked()
+    private fun onBloggingPromptAnswerClick(promptId: Int, attribution: String?) {
+        bloggingPromptsCardAnalyticsTracker.trackMySiteCardAnswerPromptClicked(attribution)
         val selectedSite = requireNotNull(selectedSiteRepository.getSelectedSite())
         _onNavigation.value = Event(BloggingPromptCardNavigationAction.AnswerPrompt(selectedSite, promptId))
     }
@@ -106,8 +106,12 @@ class BloggingPromptCardViewModelSlice @Inject constructor(
     private fun onBloggingPromptRemoveClick() {
         scope.launch(bgDispatcher) {
             updatePromptsCardEnabled(isEnabled = false).join()
-            _onNavigation.postValue(Event(BloggingPromptCardNavigationAction
-                .CardRemoved(this@BloggingPromptCardViewModelSlice::onBloggingPromptUndoClick)))
+            _onNavigation.postValue(
+                Event(
+                    BloggingPromptCardNavigationAction
+                        .CardRemoved(this@BloggingPromptCardViewModelSlice::onBloggingPromptUndoClick)
+                )
+            )
         }
     }

@@ -123,16 +127,18 @@ class BloggingPromptCardViewModelSlice @Inject constructor(
         }
     }

-    fun onDashboardCardsUpdated(scope: CoroutineScope,
-                                bloggingPromptCard: List<MySiteCardAndItem.Card.BloggingPromptCard>) {
-        bloggingPromptsCardTrackHelper.onDashboardCardsUpdated(scope, bloggingPromptCard)
+    fun onDashboardCardsUpdated(
+        scope: CoroutineScope,
+        state: MySiteViewModel.State.SiteSelected?
+    ) {
+        bloggingPromptsCardTrackHelper.onDashboardCardsUpdated(scope, state)
     }

-    fun onSiteChanged(siteId: Int?) {
-        bloggingPromptsCardTrackHelper.onSiteChanged(siteId)
+    fun onSiteChanged(siteId: Int?, state: MySiteViewModel.State.SiteSelected?) {
+        bloggingPromptsCardTrackHelper.onSiteChanged(siteId, state)
     }

-    fun onResume() {
-        bloggingPromptsCardTrackHelper.onResume()
+    fun onResume(state: MySiteViewModel.State.SiteSelected?) {
+        bloggingPromptsCardTrackHelper.onResume(state)
     }
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptsCardAnalyticsTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptsCardAnalyticsTracker.kt
index 464dc55b16c4..697c939dc657 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptsCardAnalyticsTracker.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptsCardAnalyticsTracker.kt
@@ -7,9 +7,9 @@ import javax.inject.Inject
 class BloggingPromptsCardAnalyticsTracker @Inject constructor(
     private val analyticsTracker: AnalyticsTrackerWrapper
 ) {
-    fun trackMySiteCardAnswerPromptClicked() = analyticsTracker.track(
+    fun trackMySiteCardAnswerPromptClicked(attribution: String?) = analyticsTracker.track(
         Stat.BLOGGING_PROMPTS_MY_SITE_CARD_ANSWER_PROMPT_CLICKED,
-        emptyMap()
+        mapOf("attribution" to attribution).filterValues { !it.isNullOrBlank() }
     )

     fun trackMySiteCardShareClicked() = analyticsTracker.track(
@@ -57,8 +57,8 @@ class BloggingPromptsCardAnalyticsTracker @Inject constructor(
         emptyMap()
     )

-    fun trackMySiteCardViewed() = analyticsTracker.track(
+    fun trackMySiteCardViewed(attribution: String?) = analyticsTracker.track(
         Stat.BLOGGING_PROMPTS_MY_SITE_CARD_VIEWED,
-        emptyMap()
+        mapOf("attribution" to attribution).filterValues { !it.isNullOrBlank() }
     )
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt
index 951841cda068..5b833b9a0160 100644
--- a/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt
@@ -248,8 +248,11 @@ class WPMainActivityViewModel @Inject constructor(
         }
     }

-    private fun onAnswerPromptActionClicked(promptId: Int) {
-        analyticsTracker.track(Stat.MY_SITE_CREATE_SHEET_ANSWER_PROMPT_TAPPED)
+    private fun onAnswerPromptActionClicked(promptId: Int, attribution: BloggingPromptAttribution) {
+        analyticsTracker.track(
+            Stat.MY_SITE_CREATE_SHEET_ANSWER_PROMPT_TAPPED,
+            mapOf("attribution" to attribution.value).filterValues { !it.isNullOrBlank() }
+        )
         _isBottomSheetShowing.postValue(Event(false))
         _createPostWithBloggingPrompt.postValue(promptId)
     }

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/BloggingPromptsCardTrackHelperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/BloggingPromptsCardTrackHelperTest.kt
index 158deabf14eb..253bc5a805a4 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/BloggingPromptsCardTrackHelperTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/BloggingPromptsCardTrackHelperTest.kt
@@ -11,7 +11,11 @@ import org.mockito.kotlin.mock
 import org.mockito.kotlin.never
 import org.mockito.kotlin.verify
 import org.wordpress.android.BaseUnitTest
+import org.wordpress.android.ui.avatars.TrainOfAvatarsItem
+import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptAttribution
 import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptsCardAnalyticsTracker
+import org.wordpress.android.ui.utils.ListItemInteraction
+import org.wordpress.android.ui.utils.UiString

 @ExperimentalCoroutinesApi
 class BloggingPromptsCardTrackHelperTest : BaseUnitTest() {
@@ -29,25 +33,23 @@ class BloggingPromptsCardTrackHelperTest : BaseUnitTest() {
     fun `given onResume was called in dashboard, when dashboard cards are received with prompts card, then track once`() =
         test {
             launch {
-                helper.onResume()
+                helper.onResume(siteSelected())

                 // with prompt card (transient state)
                 helper.onDashboardCardsUpdated(
-                    this,
-                    mock()
+                    this, siteSelected()
                 )

                 delay(10)

                 // again with prompt card (final state) to test debounce
                 helper.onDashboardCardsUpdated(
-                    this,
-                    mock()
+                    this, siteSelected()
                 )

                 advanceUntilIdle()

-                verify(bloggingPromptsCardAnalyticsTracker).trackMySiteCardViewed()
+                verify(bloggingPromptsCardAnalyticsTracker).trackMySiteCardViewed("bloganuary")

                 // need to cancel this internal job to finish the test
                 cancel()
@@ -59,13 +61,12 @@ class BloggingPromptsCardTrackHelperTest : BaseUnitTest() {
     fun `given onResume was called in dashboard, when dashboard cards are received without prompts card, then don't track`() =
         test {
             launch {
-                helper.onResume()
+                helper.onResume(siteSelected())

-                val bloggingPromptCards = mock<List<MySiteCardAndItem.Card.BloggingPromptCard>>()
                 // with prompt card (transient state)
                 helper.onDashboardCardsUpdated(
                     this,
-                    bloggingPromptCards
+                    siteSelected()
                 )

                 delay(10)
@@ -73,12 +74,12 @@ class BloggingPromptsCardTrackHelperTest : BaseUnitTest() {
                 // again without prompt card (final state) to test debounce
                 helper.onDashboardCardsUpdated(
                     this,
-                    listOf()
+                    siteSelected()
                 )

                 advanceUntilIdle()

-                verify(bloggingPromptsCardAnalyticsTracker, never()).trackMySiteCardViewed()
+                verify(bloggingPromptsCardAnalyticsTracker, never()).trackMySiteCardViewed("attribution")

                 // need to cancel this internal job to finish the test
                 cancel()
@@ -91,11 +92,10 @@ class BloggingPromptsCardTrackHelperTest : BaseUnitTest() {
     fun `given dashboard cards were received with prompts card, when onResume is called in dashboard, then track once`() =
         test {
             launch {
-                val bloggingPromptCards = mock<List<MySiteCardAndItem.Card.BloggingPromptCard>>()
                 // with prompt card (transient state)
                 helper.onDashboardCardsUpdated(
                     this,
-                    bloggingPromptCards
+                    siteSelected()
                 )

                 delay(10)
@@ -103,14 +103,14 @@ class BloggingPromptsCardTrackHelperTest : BaseUnitTest() {
                 // again with prompt card (final state) to test debounce
                 helper.onDashboardCardsUpdated(
                     this,
-                    bloggingPromptCards
+                    siteSelected()
                 )

                 advanceUntilIdle()

-                helper.onResume()
+                helper.onResume(siteSelected())

-                verify(bloggingPromptsCardAnalyticsTracker).trackMySiteCardViewed()
+                verify(bloggingPromptsCardAnalyticsTracker).trackMySiteCardViewed("bloganuary")

                 // need to cancel this internal job to finish the test
                 cancel()
@@ -122,11 +122,10 @@ class BloggingPromptsCardTrackHelperTest : BaseUnitTest() {
     fun `given dashboard cards were received without prompts card, when onResume is called in dashboard, then don't track`() =
         test {
             launch {
-                val bloggingPromptCards = mock<List<MySiteCardAndItem.Card.BloggingPromptCard>>()
                 // with prompt card (transient state)
                 helper.onDashboardCardsUpdated(
                     this,
-                    bloggingPromptCards
+                    siteSelected()
                 )

                 delay(10)
@@ -134,14 +133,14 @@ class BloggingPromptsCardTrackHelperTest : BaseUnitTest() {
                 // again without prompt card (final state) to test debounce
                 helper.onDashboardCardsUpdated(
                     this,
-                    listOf()
+                    siteSelected()
                 )

                 advanceUntilIdle()

-                helper.onResume()
+                helper.onResume(siteSelected())

-                verify(bloggingPromptsCardAnalyticsTracker, never()).trackMySiteCardViewed()
+                verify(bloggingPromptsCardAnalyticsTracker, never()).trackMySiteCardViewed("attribution")

                 // need to cancel this internal job to finish the test
                 cancel()
@@ -161,20 +160,19 @@ class BloggingPromptsCardTrackHelperTest : BaseUnitTest() {
             delay(1000L)

             // new site selected
-            helper.onSiteChanged(1)
+            helper.onSiteChanged(1, siteSelected())

             // screen resumed
-            helper.onResume()
+            helper.onResume(siteSelected())

             // dashboard cards updated with prompt card
             helper.onDashboardCardsUpdated(
-                this,
-                mock()
+                this, siteSelected()
             )

             advanceUntilIdle()

-            verify(bloggingPromptsCardAnalyticsTracker).trackMySiteCardViewed()
+            verify(bloggingPromptsCardAnalyticsTracker).trackMySiteCardViewed("bloganuary")

             // need to cancel this internal job to finish the test
             cancel()
@@ -184,35 +182,68 @@ class BloggingPromptsCardTrackHelperTest : BaseUnitTest() {
     @Test
     fun `given new site selected, when dashboard cards are updated without prompt card, then don't track`() = test {
         launch {
-            // old site had prompt card
-            val bloggingPromptCards = mock<List<MySiteCardAndItem.Card.BloggingPromptCard>>()
-
             helper.onDashboardCardsUpdated(
                 this,
-                bloggingPromptCards
+                siteSelected().copy(
+                    dashboardData = emptyList()
+                )
             )

             // simulate the user was here for a while
             delay(1000L)

             // new site selected
-            helper.onSiteChanged(1)
+            helper.onSiteChanged(1, siteSelected())

             // screen resumed
-            helper.onResume()
+            helper.onResume(siteSelected())

             // dashboard cards updated without prompt card
             helper.onDashboardCardsUpdated(
                 this,
-                listOf()
+                siteSelected()
             )

             advanceUntilIdle()

-            verify(bloggingPromptsCardAnalyticsTracker, never()).trackMySiteCardViewed()
+            verify(bloggingPromptsCardAnalyticsTracker, never()).trackMySiteCardViewed("attribution")

             // need to cancel this internal job to finish the test
             cancel()
         }
     }
+
+    private fun siteSelected() = MySiteViewModel.State.SiteSelected(
+        siteInfoHeader = MySiteCardAndItem.SiteInfoHeaderCard(
+            title = "title",
+            url = "url",
+            iconState = MySiteCardAndItem.SiteInfoHeaderCard.IconState.Progress,
+            showTitleFocusPoint = true,
+            showSubtitleFocusPoint = true,
+            showIconFocusPoint = true,
+            onTitleClick = ListItemInteraction.create {},
+            onUrlClick = ListItemInteraction.create {},
+            onIconClick = ListItemInteraction.create {},
+            onSwitchSiteClick = ListItemInteraction.create {},
+        ),
+        dashboardData = listOf(
+            MySiteCardAndItem.Card.BloggingPromptCard.BloggingPromptCardWithData(
+                prompt = UiString.UiStringText("prompt"),
+                respondents = listOf(
+                    TrainOfAvatarsItem.AvatarItem("url")
+                ),
+                numberOfAnswers = 5,
+                isAnswered = true,
+                promptId = 123,
+                tagUrl = "tagUrl",
+                attribution = BloggingPromptAttribution.BLOGANUARY,
+                onShareClick = {},
+                onAnswerClick = {},
+                onSkipClick = {},
+                onViewMoreClick = {},
+                onViewAnswersClick = {},
+                onRemoveClick = {},
+            )
+        ),
+    )
 }
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteViewModelTest.kt
index 715823280275..d7d4b77ff827 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteViewModelTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteViewModelTest.kt
@@ -905,11 +905,13 @@ class MySiteViewModelTest : BaseUnitTest() {
     fun `given blogging prompt card, when resuming dashboard, then tracker helper called as expected`() = test {
         initSelectedSite()

-        verify(bloggingPromptCardViewModelSlice, atLeastOnce()).onSiteChanged(siteLocalId)
+        val siteSelected = uiModels.last() as SiteSelected
+
+        verify(bloggingPromptCardViewModelSlice, atLeastOnce()).onSiteChanged(siteLocalId, siteSelected)

         viewModel.onResume()

-        verify(bloggingPromptCardViewModelSlice).onResume()
+        verify(bloggingPromptCardViewModelSlice).onResume(siteSelected)
         verify(bloggingPromptCardViewModelSlice, atLeastOnce())
             .onDashboardCardsUpdated(
                 any(),
@@ -921,11 +923,13 @@ class MySiteViewModelTest : BaseUnitTest() {
     fun `given no blogging prompt card, when resuming dashboard, then tracker helper called as expected`() = test {
         initSelectedSite()

-        verify(bloggingPromptCardViewModelSlice, atLeastOnce()).onSiteChanged(siteLocalId)
+        val siteSelected = uiModels.last() as SiteSelected
+
+        verify(bloggingPromptCardViewModelSlice, atLeastOnce()).onSiteChanged(siteLocalId, siteSelected)

         viewModel.onResume()

-        verify(bloggingPromptCardViewModelSlice).onResume()
+        verify(bloggingPromptCardViewModelSlice).onResume(siteSelected)
         verify(bloggingPromptCardViewModelSlice, atMost(1))
             .onDashboardCardsUpdated(
                 any(),
@@ -937,11 +941,13 @@ class MySiteViewModelTest : BaseUnitTest() {
     fun `given blogging prompt card, when resuming menu, then tracker helper called as expected`() = test {
         initSelectedSite()

-        verify(bloggingPromptCardViewModelSlice, atLeastOnce()).onSiteChanged(siteLocalId)
+        val siteSelected = uiModels.last() as SiteSelected
+
+        verify(bloggingPromptCardViewModelSlice, atLeastOnce()).onSiteChanged(siteLocalId, siteSelected)

         viewModel.onResume()

-        verify(bloggingPromptCardViewModelSlice).onResume()
+        verify(bloggingPromptCardViewModelSlice).onResume(siteSelected)
         verify(bloggingPromptCardViewModelSlice, atLeastOnce())
             .onDashboardCardsUpdated(
                 any(),
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptCardViewModelSliceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptCardViewModelSliceTest.kt
index 69cd81d89eea..d97bb8a4fec9 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptCardViewModelSliceTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptCardViewModelSliceTest.kt
@@ -17,12 +17,14 @@ import org.mockito.kotlin.whenever
 import org.wordpress.android.BaseUnitTest
 import org.wordpress.android.R
 import org.wordpress.android.fluxc.model.SiteModel
+import org.wordpress.android.fluxc.model.bloggingprompts.BloggingPromptModel
 import org.wordpress.android.models.ReaderTag
 import org.wordpress.android.ui.bloggingprompts.BloggingPromptsPostTagProvider
 import org.wordpress.android.ui.bloggingprompts.BloggingPromptsSettingsHelper
 import org.wordpress.android.ui.mysite.BloggingPromptCardNavigationAction
 import org.wordpress.android.ui.mysite.BloggingPromptsCardTrackHelper
 import org.wordpress.android.ui.mysite.MySiteSourceManager
+import org.wordpress.android.ui.mysite.MySiteUiState
 import org.wordpress.android.ui.mysite.SelectedSiteRepository
 import org.wordpress.android.ui.mysite.SiteNavigationAction
 import org.wordpress.android.ui.pages.SnackbarMessageHolder
@@ -111,11 +113,17 @@ class BloggingPromptCardViewModelSliceTest : BaseUnitTest() {

     @Test
     fun `given blogging prompt card, when answer button is clicked, answer action is called`() = test {
-        val params = viewModelSlice.getBuilderParams(mock())
+        val attribution = "attribution"
+        val bloggingPromptUpdate = mock<MySiteUiState.PartialState.BloggingPromptUpdate>().apply {
+            val mockPromptModel = mock<BloggingPromptModel>()
+            whenever(mockPromptModel.attribution).thenReturn(attribution)
+            whenever(promptModel).thenReturn(mockPromptModel)
+        }
+        val params = viewModelSlice.getBuilderParams(bloggingPromptUpdate)

         params.onAnswerClick(123)

-        verify(bloggingPromptsCardAnalyticsTracker).trackMySiteCardAnswerPromptClicked()
+        verify(bloggingPromptsCardAnalyticsTracker).trackMySiteCardAnswerPromptClicked(attribution)
         assertThat(navigationActions).containsOnly(BloggingPromptCardNavigationAction.AnswerPrompt(site, 123))
     }

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptsCardAnalyticsTrackerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptsCardAnalyticsTrackerTest.kt
index 61949dbcf00e..97dad4d5a867 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptsCardAnalyticsTrackerTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptsCardAnalyticsTrackerTest.kt
@@ -12,10 +12,11 @@ class BloggingPromptsCardAnalyticsTrackerTest {

     @Test
     fun `Should track my site card answer prompt clicked`() {
-        classToTest.trackMySiteCardAnswerPromptClicked()
+        val attribution = "attribution"
+        classToTest.trackMySiteCardAnswerPromptClicked(attribution)
         verify(analyticsTracker).track(
             Stat.BLOGGING_PROMPTS_MY_SITE_CARD_ANSWER_PROMPT_CLICKED,
-            emptyMap()
+            mapOf("attribution" to attribution)
         )
     }

diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt
index 0e2d8bad965c..36562fb40408 100644
--- a/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt
@@ -48,6 +48,7 @@ import org.wordpress.android.ui.main.MainActionListItem.AnswerBloggingPromptActi
 import org.wordpress.android.ui.main.MainActionListItem.CreateAction
 import org.wordpress.android.ui.main.MainFabUiState
 import org.wordpress.android.ui.mysite.SelectedSiteRepository
+import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptAttribution
 import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository
 import org.wordpress.android.ui.prefs.AppPrefsWrapper
 import org.wordpress.android.ui.prefs.privacy.banner.domain.ShouldAskPrivacyConsent
@@ -356,7 +357,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() {

         val promptId = 123

-        action!!.onClickAction?.invoke(promptId)
+        action!!.onClickAction?.invoke(promptId, BloggingPromptAttribution.BLOGANUARY)
         assertThat(viewModel.createPostWithBloggingPrompt.value).isEqualTo(promptId)
     }


Handle webview checkout result

Fixes #19544

### Description

This PR adds completion handling for the domain purchase flow (webview checkout). When the checkout result is an error state, a generic error screen is shown (same as the existing screen for cart failure). On success, the "all domains" screen is shown (and activities in the back stack above that one should be cleared).

To test:

#### Success

1. Enter the domain management flow (Me -> Domains)
2. Tap the "+" sign
3. Search for a new domain
4. Tap a result to purchase (please use an obscure domain if testing a real purchase to not waste a good domain)
   * Adding a timestamp or random numbers / letters, nonce, etc. should suffice
5. Continue with the purchase (via standalone, or connected with a site flows)
6. Finish the purchase
7. Expect to be brought back to the domain management screen (aka all domains screen)
8. Expect that the newly purchased domain is visible in the list (or searchable if it is a long list already 😅 )

#### Failure

1. Perform steps 1-5 from above
2. Tap the back button in the web view before completing the purchase
3. Expect to see the error screen

Note: I am still investigating a better way to introduce an error in the web view checkout process.

## Regression Notes
1. Potential unintended areas of impact
Domain management feature

10. What I did to test those areas of impact (or what existing automated tests I relied on)
Manual and automated tests

11. What automated tests I added (or what prevented me from doing so)
Unit tests were added for the checkout completion events

PR submission checklist:

- [x] I have completed the Regression Notes.
- [x] I have considered adding accessibility improvements for my changes.
- [x] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

UI Changes testing checklist:

- [ ] Portrait and landscape orientations.
- [ ] Light and dark modes.
- [ ] Fonts: Larger, smaller and bold text.
- [ ] High contrast.
- [ ] Talkback.
- [ ] Languages with large words or with letters/accents not frequently used in English.
- [ ] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [ ] Large and small screen sizes. (Tablet and smaller phones)
- [ ] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)

🤖 Handle webview checkout result

🤖 Handle webview checkout result
When the domain purchase is complete in the domain management flow:
* On success the flow returns to a refreshed domain management list
* On failure an error is shown

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java
index b789b372eb11..6722836cd815 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java
@@ -1927,6 +1927,7 @@ public static void showJetpackStaticPoster(@NonNull Context context) {

     public static void openDomainManagement(@NonNull Context context) {
         Intent intent = new Intent(context, DomainManagementActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
         context.startActivity(intent);
     }

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/purchasedomain/PurchaseDomainActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/purchasedomain/PurchaseDomainActivity.kt
index 5d5730f1b2c0..a7df53d7ec35 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/purchasedomain/PurchaseDomainActivity.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/purchasedomain/PurchaseDomainActivity.kt
@@ -12,6 +12,7 @@ import dagger.hilt.android.AndroidEntryPoint
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import org.wordpress.android.fluxc.store.SiteStore
+import org.wordpress.android.ui.ActivityLauncher
 import org.wordpress.android.ui.domains.DomainRegistrationCheckoutWebViewActivity
 import org.wordpress.android.ui.domains.management.M3Theme
 import org.wordpress.android.ui.domains.management.purchasedomain.PurchaseDomainViewModel.ActionEvent.GoBack
@@ -19,6 +20,7 @@ import org.wordpress.android.ui.domains.management.purchasedomain.PurchaseDomain
 import org.wordpress.android.ui.domains.management.purchasedomain.PurchaseDomainViewModel.ActionEvent.GoToSitePicker
 import org.wordpress.android.ui.domains.management.purchasedomain.PurchaseDomainViewModel.ActionEvent.GoToExistingSiteCheckout
 import org.wordpress.android.ui.domains.management.purchasedomain.PurchaseDomainViewModel.ActionEvent.GoToExistingSitePlans
+import org.wordpress.android.ui.domains.management.purchasedomain.PurchaseDomainViewModel.ActionEvent.OpenDomainManagement
 import org.wordpress.android.ui.domains.management.purchasedomain.composable.PurchaseDomainScreen
 import org.wordpress.android.ui.main.SitePickerContract
 import javax.inject.Inject
@@ -108,6 +110,9 @@ class PurchaseDomainActivity : AppCompatActivity() {
                 )
             }
             GoBack -> onBackPressedDispatcher.onBackPressed()
+            OpenDomainManagement -> {
+                ActivityLauncher.openDomainManagement(this)
+            }
         }
     }

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/purchasedomain/PurchaseDomainViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/purchasedomain/PurchaseDomainViewModel.kt
index 0755288cc91f..649dc90b9983 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/purchasedomain/PurchaseDomainViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/purchasedomain/PurchaseDomainViewModel.kt
@@ -67,9 +67,12 @@ class PurchaseDomainViewModel @AssistedInject constructor(
         }
     }

-    @Suppress("UNUSED_PARAMETER")
-    fun onDomainRegistrationComplete(event: DomainRegistrationCompletedEvent?) {
-        // TODO Handle domain registration complete
+    fun onDomainRegistrationComplete(event: DomainRegistrationCompletedEvent?) = event?.also {
+        launch {
+            _actionEvents.emit(ActionEvent.OpenDomainManagement)
+        }
+    } ?: run {
+        _uiStateFlow.value = UiState.ErrorInCheckout
     }

     private val SiteModel.shouldOfferPlans
@@ -112,6 +115,7 @@ class PurchaseDomainViewModel @AssistedInject constructor(
         object SubmittingJustDomainCart : UiState
         object SubmittingSiteDomainCart : UiState
         object ErrorSubmittingCart : UiState
+        object ErrorInCheckout : UiState
     }

     sealed class ActionEvent {
@@ -120,6 +124,7 @@ class PurchaseDomainViewModel @AssistedInject constructor(
         data class GoToSitePicker(val domain: String) : ActionEvent()
         data class GoToExistingSiteCheckout(val domain: String, val siteModel: SiteModel) : ActionEvent()
         data class GoToExistingSitePlans(val domain: String, val siteModel: SiteModel) : ActionEvent()
+        object OpenDomainManagement : ActionEvent()
     }

     @AssistedFactory
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/purchasedomain/composable/PurchaseDomainScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/purchasedomain/composable/PurchaseDomainScreen.kt
index 1241e68bd43e..86fe6d1df044 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/purchasedomain/composable/PurchaseDomainScreen.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/purchasedomain/composable/PurchaseDomainScreen.kt
@@ -67,7 +67,7 @@ fun PurchaseDomainScreen(
             )
         },
         content = {
-            if (uiState == UiState.ErrorSubmittingCart) {
+            if (uiState == UiState.ErrorSubmittingCart || uiState == UiState.ErrorInCheckout) {
                 ErrorScreen(onButtonTapped = onErrorButtonTapped)
             } else {
                 Box(

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/domains/management/purchasedomain/PurchaseDomainViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/domains/management/purchasedomain/PurchaseDomainViewModelTest.kt
index d21bbbb02c47..2fd151dd855b 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/domains/management/purchasedomain/PurchaseDomainViewModelTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/domains/management/purchasedomain/PurchaseDomainViewModelTest.kt
@@ -22,6 +22,7 @@ import org.wordpress.android.fluxc.network.rest.wpcom.transactions.TransactionsR
 import org.wordpress.android.fluxc.network.rest.wpcom.transactions.TransactionsRestClient.CreateShoppingCartResponse.Extra
 import org.wordpress.android.fluxc.network.rest.wpcom.transactions.TransactionsRestClient.CreateShoppingCartResponse.Product
 import org.wordpress.android.fluxc.store.TransactionsStore
+import org.wordpress.android.ui.domains.DomainRegistrationCompletedEvent
 import org.wordpress.android.ui.domains.management.purchasedomain.PurchaseDomainViewModel.ActionEvent
 import org.wordpress.android.ui.domains.management.purchasedomain.PurchaseDomainViewModel.ActionEvent.GoBack
 import org.wordpress.android.ui.domains.management.purchasedomain.PurchaseDomainViewModel.ActionEvent.GoToDomainPurchasing
@@ -30,6 +31,7 @@ import org.wordpress.android.ui.domains.management.purchasedomain.PurchaseDomain
 import org.wordpress.android.ui.domains.management.purchasedomain.PurchaseDomainViewModel.UiState.SubmittingJustDomainCart
 import org.wordpress.android.ui.domains.management.purchasedomain.PurchaseDomainViewModel.UiState.SubmittingSiteDomainCart
 import org.wordpress.android.ui.domains.management.purchasedomain.PurchaseDomainViewModel.UiState.ErrorSubmittingCart
+import org.wordpress.android.ui.domains.management.purchasedomain.PurchaseDomainViewModel.UiState.ErrorInCheckout
 import org.wordpress.android.ui.domains.usecases.CreateCartUseCase
 import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper

@@ -119,6 +121,33 @@ class PurchaseDomainViewModelTest : BaseUnitTest() {
         assertThat(viewModel.uiStateFlow.value).isEqualTo(ErrorSubmittingCart)
     }

+    @Test
+    fun `WHEN check out fails THEN the ui is set to the ErrorInCheckout state`() = test {
+        viewModel.onDomainRegistrationComplete(null)
+        assertThat(viewModel.uiStateFlow.value).isEqualTo(ErrorInCheckout)
+    }
+
+    @Test
+    fun `WHEN check out is successful THEN go to the all domains screen`() = testWithActionEvents { events ->
+        viewModel.onDomainRegistrationComplete(
+            DomainRegistrationCompletedEvent("example.com", "joe@schmo.co")
+        )
+        advanceUntilIdle()
+
+        // Then
+        assertThat(events.last()).isEqualTo(ActionEvent.OpenDomainManagement)
+    }
+
+    @Test
+    fun `WHEN check out is cancelled THEN go to the all domains screen`() = testWithActionEvents { events ->
+        viewModel.onDomainRegistrationComplete(
+            DomainRegistrationCompletedEvent("example.com", "joe@schmo.co", true)
+        )
+        advanceUntilIdle()
+
+        // Then
+        assertThat(events.last()).isEqualTo(ActionEvent.OpenDomainManagement)
+    }

     @Test
     fun `WHEN the error button is tapped THEN the ui is set to the Initial state`() = test {


[Bloganuary] Show prompts bloganuary attribution UI

Fixes #19592

When the prompts for today contains `bloganuary` in the `attribution` field we should show the proper icon and text in the attribution UI (the same we use for Day One).

**Note:** This still does not implement the updated UI, just reuses the attribution logic for the card.

![image](https://github.com/wordpress-mobile/WordPress-Android/assets/5091503/87103df0-9053-45f1-9ed9-dd808c970461)

## To test
The testing setup is the same as the one explained here: https://github.com/wordpress-mobile/WordPress-Android/pull/19590 (Charles Setup and mocked json response)

### Test Steps
1. Download, install, and log in the Jetpack version from this PR
3. **Disable** the `Map Local` setting in Charles (so we hit the actual endpoint)
4. Refresh the app dashboard
5. **Verify** the prompt card either shows no attribution UI or the `Day One` attribution (if today's prompt is a Day One prompt)
6. **Enable** the `Map Local` setting in Charles (so we hit our mocked json response)
7. Refresh the app dashboard
8. **Verify** the prompt card shows the attribution UI for `Bloganuary`

## Regression Notes
1. Potential unintended areas of impact
Incorrect attribution UI being shown.

2. What I did to test those areas of impact (or what existing automated tests I relied on)
Manual and Unit testing.

3. What automated tests I added (or what prevented me from doing so)
Test for the possible attribution types and proper mapping.

PR submission checklist:

- [x] I have completed the Regression Notes.
- [x] I have considered adding accessibility improvements for my changes.
- [x] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

UI Changes testing checklist:

- [x] Portrait and landscape orientations.
- [x] Light and dark modes.
- [x] Fonts: Larger, smaller and bold text.
- [x] High contrast.
- [ ] Talkback.
- [ ] Languages with large words or with letters/accents not frequently used in English.
- [ ] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [ ] Large and small screen sizes. (Tablet and smaller phones)
- [ ] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)

[Bloganuary] Implement the Bloganuary attribution UI in Prompts card
We are internally talking about returning the value `bloganuary` in the `attribution` field of the API response as we do for Day One (which returns `dayone`).

Internal ref: pe7F0s-1iI-p2#comment-1738

We should implement/reuse that `attribution` logic to show the "From Bloganuary" attribution UI in the card.

![Image](https://github.com/wordpress-mobile/WordPress-Android/assets/5091503/d4125038-21af-45d8-925a-71526616e849)

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/CompactBloggingPromptCardViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/CompactBloggingPromptCardViewHolder.kt
index bf5cedf0e23e..b6793405407a 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/main/CompactBloggingPromptCardViewHolder.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/CompactBloggingPromptCardViewHolder.kt
@@ -1,10 +1,9 @@
 package org.wordpress.android.ui.main

 import android.view.ViewGroup
-import org.wordpress.android.R
 import org.wordpress.android.databinding.BloggingPromptCardCompactBinding
 import org.wordpress.android.ui.main.MainActionListItem.AnswerBloggingPromptAction
-import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptAttribution.DAY_ONE
+import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptAttribution
 import org.wordpress.android.ui.utils.UiHelpers
 import org.wordpress.android.util.HtmlCompatWrapper
 import org.wordpress.android.util.extensions.viewBinding
@@ -21,11 +20,8 @@ class CompactBloggingPromptCardViewHolder(
             uiHelpers.getTextOfUiString(promptContent.context, action.promptTitle).toString()
         )
         uiHelpers.setTextOrHide(promptContent, cardPrompt)
-        uiHelpers.updateVisibility(attributionContainer, action.attribution == DAY_ONE)

-        attributionContent.text = htmlCompatWrapper.fromHtml(
-            attributionContent.context.getString(R.string.my_site_blogging_prompt_card_attribution_dayone)
-        )
+        setupAttributionContainer(action.attribution)

         answerButton.setOnClickListener {
             action.onClickAction?.invoke(action.promptId)
@@ -36,4 +32,25 @@ class CompactBloggingPromptCardViewHolder(
         uiHelpers.updateVisibility(answeredButton, action.isAnswered)
         uiHelpers.updateVisibility(answerButton, !action.isAnswered)
     }
+
+    private fun BloggingPromptCardCompactBinding.setupAttributionContainer(
+        attribution: BloggingPromptAttribution
+    ) {
+        uiHelpers.updateVisibility(attributionContainer, attribution != BloggingPromptAttribution.NO_ATTRIBUTION)
+
+        val context = attributionContainer.context
+
+        attribution.contentRes
+            .takeIf { it != -1 }
+            ?.let { context.getString(it) }
+            ?.let { content ->
+                attributionContent.text = htmlCompatWrapper.fromHtml(content)
+            }
+
+        attribution.iconRes
+            .takeIf { it != -1 }
+            ?.let { iconRes ->
+                attributionIcon.setImageResource(iconRes)
+            }
+    }
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptAttribution.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptAttribution.kt
index a543dcc24c06..453c3bca8c5e 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptAttribution.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptAttribution.kt
@@ -1,13 +1,27 @@
 package org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts

-enum class BloggingPromptAttribution {
-    NO_ATTRIBUTION,
-    DAY_ONE;
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import org.wordpress.android.R
+
+enum class BloggingPromptAttribution(
+    @StringRes val contentRes: Int,
+    @DrawableRes val iconRes: Int,
+) {
+    NO_ATTRIBUTION(-1, -1),
+    DAY_ONE(
+        R.string.my_site_blogging_prompt_card_attribution_dayone,
+        R.drawable.ic_dayone_24dp,
+    ),
+    BLOGANUARY(
+        R.string.my_site_blogging_prompt_card_attribution_bloganuary,
+        R.drawable.ic_bloganuary_24dp,
+    );

     companion object {
         fun fromString(value: String): BloggingPromptAttribution = when (value) {
             "dayone" -> DAY_ONE
-
+            "bloganuary" -> BLOGANUARY
             else -> NO_ATTRIBUTION
         }
     }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptCardViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptCardViewHolder.kt
index 2d648547333a..43850b31799a 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptCardViewHolder.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptCardViewHolder.kt
@@ -9,7 +9,7 @@ import org.wordpress.android.databinding.MySiteBloggingPromptCardBinding
 import org.wordpress.android.ui.avatars.TrainOfAvatarsItem
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.BloggingPromptCard.BloggingPromptCardWithData
 import org.wordpress.android.ui.mysite.MySiteCardAndItemViewHolder
-import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptAttribution.DAY_ONE
+import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptAttribution.NO_ATTRIBUTION
 import org.wordpress.android.ui.utils.UiHelpers
 import org.wordpress.android.util.HtmlCompatWrapper
 import org.wordpress.android.util.extensions.getColorStateListFromAttributeOrRes
@@ -31,11 +31,7 @@ class BloggingPromptCardViewHolder(
         uiHelpers.setTextOrHide(promptContent, cardPrompt)
         uiHelpers.updateVisibility(answerButton, !card.isAnswered)

-        uiHelpers.updateVisibility(attributionContainer, card.attribution == DAY_ONE)
-
-        attributionContent.text = htmlCompatWrapper.fromHtml(
-            attributionContent.context.getString(R.string.my_site_blogging_prompt_card_attribution_dayone)
-        )
+        setupAttributionContainer(card.attribution)

         bloggingPromptCardMenu.setOnClickListener {
             bloggingPromptsCardAnalyticsTracker.trackMySiteCardMenuClicked()
@@ -59,6 +55,27 @@ class BloggingPromptCardViewHolder(
         setupAnsweredUsersContainer(card)
     }

+    private fun MySiteBloggingPromptCardBinding.setupAttributionContainer(
+        attribution: BloggingPromptAttribution
+    ) {
+        uiHelpers.updateVisibility(attributionContainer, attribution != NO_ATTRIBUTION)
+
+        val context = attributionContainer.context
+
+        attribution.contentRes
+            .takeIf { it != -1 }
+            ?.let { context.getString(it) }
+            ?.let { content ->
+                attributionContent.text = htmlCompatWrapper.fromHtml(content)
+            }
+
+        attribution.iconRes
+            .takeIf { it != -1 }
+            ?.let { iconRes ->
+                attributionIcon.setImageResource(iconRes)
+            }
+    }
+
     @Suppress("NestedBlockDepth")
     private fun setupAnsweredUsersContainer(
         card: BloggingPromptCardWithData,
diff --git a/WordPress/src/main/res/drawable-night/ic_bloganuary_24dp.xml b/WordPress/src/main/res/drawable-night/ic_bloganuary_24dp.xml
new file mode 100644
index 000000000000..d9d06fe107e0
--- /dev/null
+++ b/WordPress/src/main/res/drawable-night/ic_bloganuary_24dp.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:width="22.285715dp"
+    android:height="24dp"
+    android:viewportHeight="14"
+    android:viewportWidth="13">
+    <path
+        android:fillColor="#FFFFFF"
+        android:pathData="M9.713,0.91H3.287C1.613,0.91 0.249,2.319 0.249,4.049V9.951C0.249,11.684 1.613,13.09 3.287,13.09H9.713C11.39,13.09 12.751,11.681 12.751,9.951V4.049C12.751,2.316 11.387,0.91 9.713,0.91ZM6.5,6.745C6.218,6.745 5.99,6.509 5.99,6.218C5.99,5.927 6.218,5.691 6.5,5.691C6.782,5.691 7.01,5.927 7.01,6.218C7.01,6.509 6.782,6.745 6.5,6.745ZM6.813,7.339C7.285,7.197 7.636,6.751 7.636,6.218C7.636,5.571 7.126,5.044 6.5,5.044C5.874,5.044 5.364,5.571 5.364,6.218C5.364,6.751 5.715,7.197 6.187,7.339V9.744C5.633,8.678 4.889,7.918 3.953,7.482V4.427C3.953,4.243 4.097,4.094 4.276,4.094H8.721C8.9,4.094 9.044,4.243 9.044,4.427V7.504C8.08,8.07 7.335,8.833 6.813,9.78V7.339ZM12.125,9.951C12.125,11.325 11.043,12.444 9.713,12.444H6.813V11.448C7.235,9.896 8.139,8.729 9.503,7.979L9.669,7.889V4.427C9.669,3.887 9.244,3.448 8.721,3.448H4.279C3.756,3.448 3.331,3.887 3.331,4.427V7.915L3.528,7.996C4.776,8.503 5.671,9.667 6.187,11.451V12.444H3.287C1.957,12.444 0.875,11.325 0.875,9.951V4.049C0.875,2.675 1.957,1.557 3.287,1.557H9.713C11.043,1.557 12.125,2.675 12.125,4.049V9.951Z"
+        tools:ignore="VectorPath" />
+</vector>
diff --git a/WordPress/src/main/res/drawable/ic_bloganuary_24dp.xml b/WordPress/src/main/res/drawable/ic_bloganuary_24dp.xml
new file mode 100644
index 000000000000..ac7773959434
--- /dev/null
+++ b/WordPress/src/main/res/drawable/ic_bloganuary_24dp.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:width="22.285715dp"
+    android:height="24dp"
+    android:viewportHeight="14"
+    android:viewportWidth="13">
+    <path
+        android:fillColor="#000000"
+        android:pathData="M9.713,0.91H3.287C1.613,0.91 0.249,2.319 0.249,4.049V9.951C0.249,11.684 1.613,13.09 3.287,13.09H9.713C11.39,13.09 12.751,11.681 12.751,9.951V4.049C12.751,2.316 11.387,0.91 9.713,0.91ZM6.5,6.745C6.218,6.745 5.99,6.509 5.99,6.218C5.99,5.927 6.218,5.691 6.5,5.691C6.782,5.691 7.01,5.927 7.01,6.218C7.01,6.509 6.782,6.745 6.5,6.745ZM6.813,7.339C7.285,7.197 7.636,6.751 7.636,6.218C7.636,5.571 7.126,5.044 6.5,5.044C5.874,5.044 5.364,5.571 5.364,6.218C5.364,6.751 5.715,7.197 6.187,7.339V9.744C5.633,8.678 4.889,7.918 3.953,7.482V4.427C3.953,4.243 4.097,4.094 4.276,4.094H8.721C8.9,4.094 9.044,4.243 9.044,4.427V7.504C8.08,8.07 7.335,8.833 6.813,9.78V7.339ZM12.125,9.951C12.125,11.325 11.043,12.444 9.713,12.444H6.813V11.448C7.235,9.896 8.139,8.729 9.503,7.979L9.669,7.889V4.427C9.669,3.887 9.244,3.448 8.721,3.448H4.279C3.756,3.448 3.331,3.887 3.331,4.427V7.915L3.528,7.996C4.776,8.503 5.671,9.667 6.187,11.451V12.444H3.287C1.957,12.444 0.875,11.325 0.875,9.951V4.049C0.875,2.675 1.957,1.557 3.287,1.557H9.713C11.043,1.557 12.125,2.675 12.125,4.049V9.951Z"
+        tools:ignore="VectorPath" />
+</vector>
diff --git a/WordPress/src/main/res/layout/blogging_prompt_card_compact.xml b/WordPress/src/main/res/layout/blogging_prompt_card_compact.xml
index 2d90535788a2..b9d9295cb8fd 100644
--- a/WordPress/src/main/res/layout/blogging_prompt_card_compact.xml
+++ b/WordPress/src/main/res/layout/blogging_prompt_card_compact.xml
@@ -76,10 +76,11 @@
             tools:visibility="visible">

             <ImageView
+                android:id="@+id/attribution_icon"
                 android:layout_width="wrap_content"
                 android:layout_height="16dp"
                 android:importantForAccessibility="no"
-                android:src="@drawable/ic_dayone_24dp" />
+                tools:src="@drawable/ic_dayone_24dp" />

             <TextView
                 android:id="@+id/attribution_content"
diff --git a/WordPress/src/main/res/layout/my_site_blogging_prompt_card.xml b/WordPress/src/main/res/layout/my_site_blogging_prompt_card.xml
index 9af805b37681..aac5f191f805 100644
--- a/WordPress/src/main/res/layout/my_site_blogging_prompt_card.xml
+++ b/WordPress/src/main/res/layout/my_site_blogging_prompt_card.xml
@@ -63,10 +63,11 @@
             tools:visibility="visible">

             <ImageView
+                android:id="@+id/attribution_icon"
                 android:layout_width="wrap_content"
                 android:layout_height="16dp"
                 android:importantForAccessibility="no"
-                android:src="@drawable/ic_dayone_24dp" />
+                tools:src="@drawable/ic_dayone_24dp" />

             <TextView
                 android:id="@+id/attribution_content"
diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml
index 90146cab631f..edbed58bbb67 100644
--- a/WordPress/src/main/res/values/strings.xml
+++ b/WordPress/src/main/res/values/strings.xml
@@ -2399,6 +2399,7 @@
     <string name="my_site_blogging_prompt_card_menu_skip">Skip for today</string>
     <string name="my_site_blogging_prompt_card_menu_remove">Turn off prompts</string>
     <string name="my_site_blogging_prompt_card_attribution_dayone">From DayOne</string>
+    <string name="my_site_blogging_prompt_card_attribution_bloganuary">From Bloganuary</string>
     <string name="my_site_blogging_prompt_card_menu_learn_more">Learn more</string>
     <string name="my_site_blogging_prompt_card_skipped_snackbar">Skipped today\'s blogging prompt</string>
     <string name="my_site_blogging_prompt_card_removed_snackbar_title">Blogging Prompts hidden</string>

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptCardBuilderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptCardBuilderTest.kt
index a7c5381131f2..d109707fe366 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptCardBuilderTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/bloggingprompts/BloggingPromptCardBuilderTest.kt
@@ -43,35 +43,22 @@ private val RESPONDENTS = listOf(
 class BloggingPromptCardBuilderTest : BaseUnitTest() {
     private lateinit var builder: BloggingPromptCardBuilder

-    @Suppress("MaxLineLength")
-    private val bloggingPrompt = BloggingPromptModel(
-        id = 123,
-        text = PROMPT_TITLE,
-        date = Date(),
-        isAnswered = false,
-        attribution = "dayone",
-        respondentsCount = 5,
-        respondentsAvatarUrls = RESPONDENTS,
-        answeredLink = "https://wordpress.com/tag/dailyprompt-123"
-    )
-
     @Before
     fun setUp() {
         builder = BloggingPromptCardBuilder()
     }

     @Test
-    fun `given blogging prompt, when card is built, then return card`() {
-        val statCard = buildBloggingPromptCardBuilderParams(bloggingPrompt)
-
-        assertThat(statCard).isNotNull()
-    }
-
-    @Test
-    fun `given blogging prompt, when card is built without showing view answers action, then return matching card`() {
-        val statCard = buildBloggingPromptCardBuilderParams(bloggingPrompt)
-
-        assertThat(statCard).isEqualTo(bloggingPromptCard())
+    fun `given blogging prompt, when card is built then return matching card`() {
+        // lets test all possible attribution types to verify proper mapping
+        mapOf(
+            "dayone" to BloggingPromptAttribution.DAY_ONE,
+            "bloganuary" to BloggingPromptAttribution.BLOGANUARY,
+            "" to BloggingPromptAttribution.NO_ATTRIBUTION
+        ).forEach { (attributionString, attribution) ->
+            val promptCard = buildBloggingPromptCardBuilderParams(bloggingPromptModel(attributionString))
+            assertThat(promptCard).isEqualTo(bloggingPromptCard(attribution))
+        }
     }

     @Test
@@ -102,7 +89,21 @@ class BloggingPromptCardBuilderTest : BaseUnitTest() {
     private val onViewAnswersClick: (tagUrl: String) -> Unit = { }
     private val onRemoveClick: () -> Unit = { }

+    private fun bloggingPromptModel(
+        attribution: String
+    ) = BloggingPromptModel(
+        id = 123,
+        text = PROMPT_TITLE,
+        date = Date(),
+        isAnswered = false,
+        attribution = attribution,
+        respondentsCount = 5,
+        respondentsAvatarUrls = RESPONDENTS,
+        answeredLink = "https://wordpress.com/tag/dailyprompt-123"
+    )
+
     private fun bloggingPromptCard(
+        attribution: BloggingPromptAttribution
     ) = BloggingPromptCardWithData(
         prompt = UiStringText(PROMPT_TITLE),
         respondents = RESPONDENTS_IN_CARD_VIEW_ANSWERS,
@@ -110,7 +111,7 @@ class BloggingPromptCardBuilderTest : BaseUnitTest() {
         false,
         promptId = 123,
         tagUrl = "https://wordpress.com/tag/dailyprompt-123",
-        attribution = BloggingPromptAttribution.DAY_ONE,
+        attribution = attribution,
         onShareClick = onShareClick,
         onAnswerClick = onAnswerClick,
         onSkipClick = onSkipClick,


[Bloganuary] Add bloganuary tags to Bloganuary prompt answers

Fixes #19588

If the prompt returned by the API has the `bloganuary_id`, and therefore the `BloggingPromptModel` from FluxC has the `bloganuaryId` field, then we should also add the `bloganuary` and `bloganuary-YYYY-DD` tags (this second one IS the content of the `bloganuaryId` field).

## To test
Since this relies on the response from the backend and Bloganuary prompts will only be available in January, we need to mock the response for testing it, for that you will need to setup Charles and use the `Map Local` feature.

### Setup
- Download and setup [Charles proxy](https://www.charlesproxy.com/)
- With Charles running, set up the test device WiFi connection to use Charles (don't forget to setup the certificates so it can intercept HTTPS requests)
- Setup the `SSL Proxying` settings to include the `public-api.wordpress.com` host location
- Setup a `Map Local` entry as following:

The fields for the new `Map Local` entry should be:

**Map From**
Protocol: `https`
Host: `public-api.wordpress.com`
Path: `wpcom/v3/sites/*/blogging-prompts/`
Query: `per_page=20&after=*&force_year=2023&order=desc&locale=en_US`

**Map To**
Local path: [mocked_bloganuary_response.json](https://github.com/wordpress-mobile/WordPress-Android/files/13312455/mocked_bloganuary_response.json) (download and put this file somewhere and choose it for this field)

![image](https://github.com/wordpress-mobile/WordPress-Android/assets/5091503/1fde758d-564b-45e0-a0e6-37de6e558cf5)

#### Notes
1. This file should be OK for testing up to November 28, it makes all entries there have the `bloganuary_id` field as if it was January.
2. I redacted user-sensitive information (avatar URLs) so it is expected that the Prompt card will only show placeholders for the respondent avatars.

### Test steps
1. Download, install, and log in the Jetpack version from this PR
3. **Disable** the `Map Local` setting in Charles (so we hit the actual endpoint)
4. Refresh the app dashboard
5. Tap `Answer Prompt` in the prompts card
6. Go to the Post Settings
7. **Verify** the tags section only show the regular `dailyprompt`-related tags
8. Go back to the Dashboard
9. **Enable** the `Map Local` setting in Charles (so we hit our mocked json response)
10. Refresh the app dashboard
11. Tap `Answer Prompt` in the prompts card
12. Go to the Post Settings
13. **Verify** the tags section now show the `dailyprompt` AND `bloganuary` tags (4 in total)

You can also test the prompt answer from the "View more prompts" list by doing this:
1. Keep the `Map Local` setting on in Charles
2. Go back to the Dashboard
3. Tap the overflow menu in the Prompts card
4. Tap "View more prompts"
5. Tap Today's prompt
6. Go to Post Settings
7. **Verify** the tags section show the `dailyprompt` AND `bloganuary` tags (4 in total)
8. Do steps 2-4 again
9. Tap a prompt **before November 9**
10. Go to Post Settings
11. **Verify** the tags section only show the regular `dailyprompt`-related tags

## Regression Notes
1. Potential unintended areas of impact
Adding tags when `bloganuaryId` not present

17. What I did to test those areas of impact (or what existing automated tests I relied on)
Manual testing + Unit tests

18. What automated tests I added (or what prevented me from doing so)
Unit tests to assert tags are only added in the correct scenario.

PR submission checklist:

- [x] I have completed the Regression Notes.
- [x] I have considered adding accessibility improvements for my changes.
- [x] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

UI Changes testing checklist:
No UI changes

[Bloganuary] Add Bloganuary tags to prompt answers
Use `bloganuaryId` field from `BloggingPromptModel` to get one of the bloganuary tags for the post, the other tag should be `bloganuary` and should be added if the `bloganuaryId` field is present.

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProvider.kt
index 14319a5319ba..0600bbcff449 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProvider.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProvider.kt
@@ -23,5 +23,6 @@ class BloggingPromptsPostTagProvider @Inject constructor(

     companion object {
         const val BLOGGING_PROMPT_TAG = "dailyprompt"
+        const val BLOGANUARY_TAG = "bloganuary"
     }
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorBloggingPromptsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorBloggingPromptsViewModel.kt
index 8c8380d783cb..d3c2edfceeb5 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorBloggingPromptsViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorBloggingPromptsViewModel.kt
@@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.first
 import org.wordpress.android.fluxc.model.SiteModel
+import org.wordpress.android.fluxc.model.bloggingprompts.BloggingPromptModel
 import org.wordpress.android.fluxc.store.bloggingprompts.BloggingPromptsStore
 import org.wordpress.android.modules.BG_THREAD
 import org.wordpress.android.ui.bloggingprompts.BloggingPromptsPostTagProvider
@@ -45,16 +46,20 @@ class EditorBloggingPromptsViewModel
                     EditorLoadedPrompt(
                         promptId,
                         content,
-                        createPromptTags(prompt.answeredLink)
+                        createPromptTags(prompt)
                     )
                 )
             )
         }
     }

-    private fun createPromptTags(tagUrl: String): List<String> = mutableListOf<String>().apply {
+    private fun createPromptTags(prompt: BloggingPromptModel): List<String> = mutableListOf<String>().apply {
         add(BloggingPromptsPostTagProvider.BLOGGING_PROMPT_TAG)
-        add(bloggingPromptsPostTagProvider.promptIdTag(tagUrl))
+        add(bloggingPromptsPostTagProvider.promptIdTag(prompt.answeredLink))
+        prompt.bloganuaryId?.let { bloganuaryIdTag ->
+            add(BloggingPromptsPostTagProvider.BLOGANUARY_TAG)
+            add(bloganuaryIdTag)
+        }
     }

     data class EditorLoadedPrompt(val promptId: Int, val content: String, val tags: List<String>)

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/editor/EditorBloggingPromptsViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/editor/EditorBloggingPromptsViewModelTest.kt
index f79de2779589..4c8c5ee0cfe8 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/editor/EditorBloggingPromptsViewModelTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/editor/EditorBloggingPromptsViewModelTest.kt
@@ -7,6 +7,7 @@ import org.junit.Before
 import org.junit.Test
 import org.mockito.Mock
 import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
 import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.times
@@ -31,20 +32,37 @@ class EditorBloggingPromptsViewModelTest : BaseUnitTest() {
     private lateinit var viewModel: EditorBloggingPromptsViewModel
     private var loadedPrompt: EditorLoadedPrompt? = null

-    private val bloggingPrompt = BloggingPromptsResult(
-        model = BloggingPromptModel(
-            id = 123,
-            text = "title",
-            date = Date(),
-            isAnswered = false,
-            attribution = "",
-            respondentsCount = 5,
-            respondentsAvatarUrls = listOf(),
-            answeredLink = "https://wordpress.com/tag/dailyprompt-123",
+    private val bloggingPrompt = listOf(
+        BloggingPromptsResult(
+            model = BloggingPromptModel(
+                id = 123,
+                text = "title",
+                date = Date(),
+                isAnswered = false,
+                attribution = "",
+                respondentsCount = 5,
+                respondentsAvatarUrls = listOf(),
+                answeredLink = "https://wordpress.com/tag/dailyprompt-123",
+            )
+        ),
+        BloggingPromptsResult(
+            model = BloggingPromptModel(
+                id = 321,
+                text = "title",
+                date = Date(),
+                isAnswered = false,
+                attribution = "",
+                respondentsCount = 10,
+                respondentsAvatarUrls = listOf(),
+                answeredLink = "https://wordpress.com/tag/dailyprompt-321",
+                bloganuaryId = "bloganuaryTag"
+            )
         )
     )
     private val bloggingPromptsStore: BloggingPromptsStore = mock {
-        onBlocking { getPromptById(any(), any()) } doReturn flowOf(bloggingPrompt)
+        onBlocking { getPromptById(any(), any()) } doAnswer { mock ->
+            flowOf(bloggingPrompt.first { it.model?.id == mock.arguments[1] })
+        }
     }
     private val bloggingPromptsBlock = "blogging_prompts_block"
     private val bloggingPromptsEditorBlockMapper: BloggingPromptsEditorBlockMapper = mock {
@@ -72,7 +90,7 @@ class EditorBloggingPromptsViewModelTest : BaseUnitTest() {
     fun `starting VM fetches a prompt and posts it to onBloggingPromptLoaded`() = test {
         viewModel.start(siteModel, 123)

-        assertThat(loadedPrompt?.promptId).isEqualTo(bloggingPrompt.model?.id)
+        assertThat(loadedPrompt?.promptId).isEqualTo(123)

         verify(bloggingPromptsStore, times(1)).getPromptById(any(), any())
     }
@@ -98,4 +116,16 @@ class EditorBloggingPromptsViewModelTest : BaseUnitTest() {
             "promptIdTag"
         )
     }
+
+    @Test
+    fun `should add bloganuary tags`() {
+        whenever(bloggingPromptsPostTagProvider.promptIdTag(any())).thenReturn("promptIdTag")
+        viewModel.start(siteModel, 321)
+        assertThat(loadedPrompt?.tags).containsOnly(
+            BloggingPromptsPostTagProvider.BLOGGING_PROMPT_TAG,
+            "promptIdTag",
+            BloggingPromptsPostTagProvider.BLOGANUARY_TAG,
+            "bloganuaryTag"
+        )
+    }
 }


🤖 Prevent navigation outside the domain details webview enhancements

Fixes #19501

## Description
This PR enhances the implementation of https://github.com/wordpress-mobile/WordPress-Android/pull/19570 aligning with our internal discussion (ref p1699462442594229-slack-C05NS0YV7HS). With this:
* Navigation is only allowed in the `https://wordpress.com/domains/manage/all/some.domain/edit/` scope
* All other urls are loaded in the external browser
* Due to limitations with intercepting url loading (in `shouldOverrideUrlLoading`) when there is no actual page loading (e.g. with JS) it monitors url changes and redirects in those cases too https://github.com/wordpress-mobile/WordPress-Android/pull/19585/commits/715966fd42bb0e1d2862221dc13fe004e032a958

## To test:
1. Enable the `domain_management` feature flag
2. Open the domain management (`Me>Domains` or `My Site>More>Domains>ALL)
3. Tap on a domain
4. **Verify** that the details page opens
5. Try to browse outside this page
6. **Verify** that the link opens in an external browser

https://github.com/wordpress-mobile/WordPress-Android/assets/304044/84e998d5-2b4a-482b-8177-7564b8bf06e2

## Regression Notes
1. Potential unintended areas of impact
N/A

2. What I did to test those areas of impact (or what existing automated tests I relied on)
N/A

3. What automated tests I added (or what prevented me from doing so)
Updated existing tests and added `DomainManagementDetailsWebViewClientTest`

PR submission checklist:

- [ ] I have completed the Regression Notes.
- [ ] I have considered adding accessibility improvements for my changes.
- [ ] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

UI Changes testing checklist:

- [ ] Portrait and landscape orientations.
- [ ] Light and dark modes.
- [ ] Fonts: Larger, smaller and bold text.
- [ ] High contrast.
- [ ] Talkback.
- [ ] Languages with large words or with letters/accents not frequently used in English.
- [ ] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [ ] Large and small screen sizes. (Tablet and smaller phones)
- [ ] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)

🤖 Prevent navigation outside the domain details webview
As documented in pc8HXX-1gR-p2:
Users should not be able to navigate to URLs outside of the domain details context. These URLs should be intercepted and opened in the device’s browser instead.

related PR: https://github.com/wordpress-mobile/WordPress-Android/pull/19447

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainManagementActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainManagementActivity.kt
index 6574c602cb0f..f439e6baf1db 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainManagementActivity.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainManagementActivity.kt
@@ -41,7 +41,13 @@ class DomainManagementActivity : AppCompatActivity() {
     private fun handleActionEvents(actionEvent: ActionEvent) {
         when (actionEvent) {
             is ActionEvent.DomainTapped -> {
-                startActivity(DomainManagementDetailsActivity.createIntent(this, actionEvent.detailUrl))
+                startActivity(
+                    DomainManagementDetailsActivity.createIntent(
+                        this,
+                        actionEvent.domain,
+                        actionEvent.detailUrl
+                    )
+                )
             }
             is ActionEvent.AddDomainTapped -> ActivityLauncher.openNewDomainSearch(this)
             is ActionEvent.NavigateBackTapped -> onBackPressedDispatcher.onBackPressed()
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainManagementViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainManagementViewModel.kt
index ec08a99881d5..91f3cae374a1 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainManagementViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainManagementViewModel.kt
@@ -83,10 +83,10 @@ class DomainManagementViewModel @Inject constructor(
         }
     }

-    fun onDomainTapped(detailUrl: String) {
+    fun onDomainTapped(domain: String, detailUrl: String) {
         analyticsTracker.track(Stat.DOMAIN_MANAGEMENT_MY_DOMAINS_SCREEN_DOMAIN_TAPPED)
         launch {
-            _actionEvents.emit(ActionEvent.DomainTapped(detailUrl))
+            _actionEvents.emit(ActionEvent.DomainTapped(domain, detailUrl))
         }
     }

@@ -117,7 +117,7 @@ class DomainManagementViewModel @Inject constructor(
     }

     sealed class ActionEvent {
-        data class DomainTapped(val detailUrl: String): ActionEvent()
+        data class DomainTapped(val domain: String, val detailUrl: String): ActionEvent()
         object AddDomainTapped: ActionEvent()
         object NavigateBackTapped: ActionEvent()
     }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainsListCard.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainsListCard.kt
index 4c593f1bab72..2bc7ebab5e00 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainsListCard.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainsListCard.kt
@@ -35,14 +35,14 @@ import java.util.Date
 @Composable
 fun DomainListCard(
     uiState: DomainCardUiState,
-    onDomainTapped: (detailUrl: String) -> Unit = {},
+    onDomainTapped: (domain: String, detailUrl: String) -> Unit = { _: String, _: String -> },
     ) {
     OutlinedCard(
         modifier = Modifier.fillMaxWidth(),
         border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
         onClick = {
             if (uiState is DomainCardUiState.Loaded && uiState.detailUrl != null) {
-                onDomainTapped(uiState.detailUrl)
+                onDomainTapped(uiState.domain ?: "", uiState.detailUrl)
             }
         },
     ) {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/MyDomainsScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/MyDomainsScreen.kt
index dbc9347b68d3..28d247f1a482 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/MyDomainsScreen.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/MyDomainsScreen.kt
@@ -47,7 +47,7 @@ import org.wordpress.android.ui.domains.management.composable.PrimaryButton
 fun MyDomainsScreen(
     uiState: UiState,
     onSearchQueryChanged: (String) -> Unit,
-    onDomainTapped: (detailUrl: String) -> Unit,
+    onDomainTapped: (String, String) -> Unit,
     onAddDomainTapped: () -> Unit,
     onFindDomainTapped: () -> Unit,
     onBackTapped: () -> Unit,
@@ -158,7 +158,7 @@ fun MyDomainsSearchInput(
 fun MyDomainsList(
     listUiState: PopulatedList,
     listState: LazyListState,
-    onDomainTapped: (detailUrl: String) -> Unit,
+    onDomainTapped: (domain: String, detailUrl: String) -> Unit,
 ) {
     LazyColumn(
         verticalArrangement = Arrangement.spacedBy(16.dp),
@@ -198,11 +198,10 @@ fun PreviewMyDomainsScreen() {
             uiState = PopulatedList.Initial,
             onSearchQueryChanged = {},
             onAddDomainTapped = {},
-            onDomainTapped = {},
+            onDomainTapped = { _, _ ->},
             onFindDomainTapped = {},
             onBackTapped = {},
-            onRefresh = {},
-        )
+        ) {}
     }
 }

@@ -215,11 +214,10 @@ fun PreviewMyDomainsScreenError() {
             uiState = Error,
             onSearchQueryChanged = {},
             onAddDomainTapped = {},
-            onDomainTapped = {},
+            onDomainTapped = { _, _ ->},
             onFindDomainTapped = {},
             onBackTapped = {},
-            onRefresh = {},
-        )
+        ) {}
     }
 }

@@ -232,10 +230,9 @@ fun PreviewMyDomainsScreenEmpty() {
             uiState = Empty,
             onSearchQueryChanged = {},
             onAddDomainTapped = {},
-            onDomainTapped = {},
+            onDomainTapped = { _, _ ->},
             onFindDomainTapped = {},
             onBackTapped = {},
-            onRefresh = {},
-        )
+        ) {}
     }
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsActivity.kt
index 2f43f918a0fc..657ce7e8217e 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsActivity.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsActivity.kt
@@ -9,6 +9,8 @@ import org.wordpress.android.ui.WPWebViewActivity

 class DomainManagementDetailsActivity : WPWebViewActivity(),
     DomainManagementDetailsWebViewClient.DomainManagementWebViewClientListener {
+    private val domainArg: String get() = intent.getStringExtra(PICKED_DOMAIN_KEY) ?: error("Domain cannot be null.")
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         toggleNavbarVisibility(false)
@@ -19,18 +21,23 @@ class DomainManagementDetailsActivity : WPWebViewActivity(),
         return true
     }

-    override fun createWebViewClient(allowedURL: List<String>?) = DomainManagementDetailsWebViewClient(this)
+    override fun createWebViewClient(allowedURL: List<String>?) =
+        DomainManagementDetailsWebViewClient(DomainManagementDetailsWebViewNavigationDelegate(domainArg), this)

     override fun onRedirectToExternalBrowser(url: String) {
         ActivityLauncher.openUrlExternal(this, url)
+        onBackPressedDispatcher.onBackPressed()
     }

     companion object {
-        fun createIntent(context: Context, domainDetailUrl: String): Intent =
+        const val PICKED_DOMAIN_KEY: String = "picked_domain_key"
+
+        fun createIntent(context: Context, domain: String, domainDetailUrl: String): Intent =
             Intent(context, DomainManagementDetailsActivity::class.java).apply {
                 putExtra(USE_GLOBAL_WPCOM_USER, true)
                 putExtra(AUTHENTICATION_URL, WPCOM_LOGIN_URL)
                 putExtra(URL_TO_LOAD, domainDetailUrl)
+                putExtra(PICKED_DOMAIN_KEY, domain)
             }
     }
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsWebViewClient.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsWebViewClient.kt
index aa0638719a67..b613d899983b 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsWebViewClient.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsWebViewClient.kt
@@ -2,20 +2,25 @@ package org.wordpress.android.ui.domains.management.details

 import android.webkit.WebResourceRequest
 import android.webkit.WebView
-import org.wordpress.android.ui.domains.management.details.DomainManagementDetailsWebViewNavigationDelegate.toUrl
 import org.wordpress.android.util.ErrorManagedWebViewClient

 class DomainManagementDetailsWebViewClient(
+    private val navigationDelegate: DomainManagementDetailsWebViewNavigationDelegate,
     private val listener: DomainManagementWebViewClientListener
 ) : ErrorManagedWebViewClient(listener) {
-    private val navigationDelegate = DomainManagementDetailsWebViewNavigationDelegate
-
     interface DomainManagementWebViewClientListener : ErrorManagedWebViewClientListener {
         fun onRedirectToExternalBrowser(url: String)
     }

+    override fun doUpdateVisitedHistory(view: WebView?, url: String?, isReload: Boolean) {
+        super.doUpdateVisitedHistory(view, url, isReload)
+        if (url != null && url != "about:blank" && !navigationDelegate.canNavigateTo(url)) {
+            listener.onRedirectToExternalBrowser(url)
+        }
+    }
+
     override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest) : Boolean {
-        if (navigationDelegate.canNavigateTo(request.url.toUrl())) return false
+        if (navigationDelegate.canNavigateTo(request.url)) return false
         listener.onRedirectToExternalBrowser(request.url.toString())
         return true
     }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsWebViewNavigationDelegate.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsWebViewNavigationDelegate.kt
index 3d031e49b236..0b0569b476d9 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsWebViewNavigationDelegate.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsWebViewNavigationDelegate.kt
@@ -2,11 +2,12 @@ package org.wordpress.android.ui.domains.management.details

 import org.wordpress.android.ui.utils.AbstractAllowedUrlsWebViewNavigationDelegate

-object DomainManagementDetailsWebViewNavigationDelegate: AbstractAllowedUrlsWebViewNavigationDelegate() {
+class DomainManagementDetailsWebViewNavigationDelegate(domain: String) :
+    AbstractAllowedUrlsWebViewNavigationDelegate() {
     override val allowedUrls = listOf(
         UrlMatcher(
             "wordpress.com".toRegex(),
-            listOf("/domains.*".toRegex())
+            listOf("/domains/manage/all/$domain/edit/.*".toRegex())
         )
     )
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/utils/AbstractAllowedUrlsWebViewNavigationDelegate.kt b/WordPress/src/main/java/org/wordpress/android/ui/utils/AbstractAllowedUrlsWebViewNavigationDelegate.kt
index f2dfa32e885c..ec49ad9483da 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/utils/AbstractAllowedUrlsWebViewNavigationDelegate.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/utils/AbstractAllowedUrlsWebViewNavigationDelegate.kt
@@ -7,6 +7,10 @@ abstract class AbstractAllowedUrlsWebViewNavigationDelegate {

     fun canNavigateTo(url: Url) = allowedUrls.any { it.matches(url) }

+    fun canNavigateTo(uri: Uri) = canNavigateTo(uri.toUrl())
+
+    fun canNavigateTo(urlString: String) = canNavigateTo(Uri.parse(urlString))
+
     data class UrlMatcher(
         private val host: Regex,
         private val paths: List<Regex>

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/domains/management/DomainManagementViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/domains/management/DomainManagementViewModelTest.kt
index 310dc09d36f2..4650821f6a1f 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/domains/management/DomainManagementViewModelTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/domains/management/DomainManagementViewModelTest.kt
@@ -58,15 +58,15 @@ class DomainManagementViewModelTest : BaseUnitTest() {
     @Test
     fun `WHEN a domain is tapped THEN track DOMAIN_MANAGEMENT_MY_DOMAINS_SCREEN_DOMAIN_TAPPED event`() = test {
         initializeViewModel()
-        viewModel.onDomainTapped(testDomain)
+        viewModel.onDomainTapped(testDomain, testDomainDetailUrl)
         verify(analyticsTracker).track(DOMAIN_MANAGEMENT_MY_DOMAINS_SCREEN_DOMAIN_TAPPED)
     }

     @Test
     fun `WHEN a domain is tapped THEN send DomainTapped action event`() = testWithActionEvents { events ->
-        viewModel.onDomainTapped(testDomain)
+        viewModel.onDomainTapped(testDomain, testDomainDetailUrl)
         advanceUntilIdle()
-        assertThat(events.last()).isEqualTo(ActionEvent.DomainTapped(testDomain))
+        assertThat(events.last()).isEqualTo(ActionEvent.DomainTapped(testDomain, testDomainDetailUrl))
     }

     @Test
@@ -175,5 +175,6 @@ class DomainManagementViewModelTest : BaseUnitTest() {

     companion object {
         private const val testDomain = "domain"
+        private const val testDomainDetailUrl = "domainDetailUrl"
     }
 }
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsWebViewClientTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsWebViewClientTest.kt
new file mode 100644
index 000000000000..32f73268fdb1
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsWebViewClientTest.kt
@@ -0,0 +1,93 @@
+package org.wordpress.android.ui.domains.management.details
+
+import android.net.Uri
+import android.webkit.WebResourceRequest
+import android.webkit.WebView
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.wordpress.android.BaseUnitTest
+
+@ExperimentalCoroutinesApi
+class DomainManagementDetailsWebViewClientTest : BaseUnitTest() {
+    @Mock
+    private lateinit var navigationDelegate: DomainManagementDetailsWebViewNavigationDelegate
+
+    @Mock
+    private lateinit var listener: DomainManagementDetailsWebViewClient.DomainManagementWebViewClientListener
+
+    @Mock
+    private lateinit var webView: WebView
+
+    @Mock
+    private lateinit var request: WebResourceRequest
+
+    @Mock
+    private lateinit var uri: Uri
+
+    private lateinit var client: DomainManagementDetailsWebViewClient
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.openMocks(this)
+        client = DomainManagementDetailsWebViewClient(navigationDelegate, listener)
+    }
+
+    @Test
+    fun `WHEN browsing to a not allowed domain THEN redirect to the external browser`() {
+        val url = "https://some.domain"
+        whenever(navigationDelegate.canNavigateTo(url)).thenReturn(false)
+        client.doUpdateVisitedHistory(null, url, false)
+        verify(listener, times(1)).onRedirectToExternalBrowser(url)
+    }
+
+    @Test
+    fun `WHEN browsing to an allowed domain THEN do not redirect to the external browser`() {
+        val url = "https://some.domain"
+        whenever(navigationDelegate.canNavigateTo(url)).thenReturn(true)
+        client.doUpdateVisitedHistory(null, url, false)
+        verify(listener, times(0)).onRedirectToExternalBrowser(url)
+    }
+
+    @Test
+    fun `WHEN browsing to the blank domain THEN do not redirect to the external browser`() {
+        val url = "about:blank"
+        client.doUpdateVisitedHistory(null, url, false)
+        verify(listener, times(0)).onRedirectToExternalBrowser(url)
+    }
+
+    @Test
+    fun `WHEN browsing to a null domain THEN do not redirect to the external browser`() {
+        val url = null
+        client.doUpdateVisitedHistory(null, url, false)
+        verify(listener, times(0)).onRedirectToExternalBrowser(any())
+    }
+
+    @Test
+    fun `WHEN preparing to browse to a not allowed domain THEN redirect to the external browser`() {
+        val url = "https://some.domain"
+        whenever(uri.toString()).thenReturn(url)
+        whenever(request.url).thenReturn(uri)
+        whenever(navigationDelegate.canNavigateTo(uri)).thenReturn(false)
+        val actual = client.shouldOverrideUrlLoading(webView, request)
+        verify(listener, times(1)).onRedirectToExternalBrowser(url)
+        assertThat(actual).isTrue
+    }
+
+    @Test
+    fun `WHEN preparing to browse to an allowed domain THEN do not redirect to the external browser`() {
+        val url = "https://some.domain"
+        whenever(request.url).thenReturn(uri)
+        whenever(navigationDelegate.canNavigateTo(uri)).thenReturn(true)
+        val actual = client.shouldOverrideUrlLoading(webView, request)
+        verify(listener, times(0)).onRedirectToExternalBrowser(url)
+        assertThat(actual).isFalse
+    }
+}
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsWebViewNavigationDelegateTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsWebViewNavigationDelegateTest.kt
index f0db5052645f..f69352caa8ae 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsWebViewNavigationDelegateTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/domains/management/details/DomainManagementDetailsWebViewNavigationDelegateTest.kt
@@ -8,14 +8,13 @@ import org.wordpress.android.ui.utils.AbstractAllowedUrlsWebViewNavigationDelega

 @ExperimentalCoroutinesApi
 class DomainManagementDetailsWebViewNavigationDelegateTest : BaseUnitTest() {
-    private val navigationDelegate = DomainManagementDetailsWebViewNavigationDelegate
+    private val navigationDelegate = DomainManagementDetailsWebViewNavigationDelegate("some.domain")

     @Test
-    fun `when browsing in the domains path, then the web view can navigate`() {
+    fun `when browsing in the domain details edit path, then the web view can navigate`() {
         Assertions.assertThat(
             buildUrls(
-                "/domains/manage/all/some.domain/edit/some.domain", // standard details page
-                "/domains/mapping/some.domain/setup/some.domain?step=&show-errors=false&firstVisit=false" // fix errors
+                "/domains/manage/all/some.domain/edit/some.slug", // standard details page
             )
         ).allMatch {
             navigationDelegate.canNavigateTo(it)
@@ -23,10 +22,12 @@ class DomainManagementDetailsWebViewNavigationDelegateTest : BaseUnitTest() {
     }

     @Test
-    fun `when browsing outside the domains path, then the web view cannot navigate`() {
+    fun `when browsing outside the domain details edit path, then the web view cannot navigate`() {
         Assertions.assertThat(
             buildUrls(
-                "/email/antonis.me/manage/some.domain", // setup email
+                "/domains/manage/all/some.domain/dns/some.slug", // dns page
+                "/domains/mapping/some.domain/setup/some.domain?step=&show-errors=false&firstVisit=false",// some errors
+                "/email/some.domain/manage/some.domain", // setup email
                 "/support/domains/https-ssl/" // support
             )
         ).noneMatch {