Repository: wordpress-mobile/WordPress-Android (language: kotlin)

Issue:
SoTW 2023 card in the WordPress app
Internal ref: pdb1SS-3F9-p2

Since we currently don't have a way of showing informational nudges/cards in the WordPress app, we will add a static card for inviting the users to check the State of the Word keynote event recording.


We'll add a simple card for SoTW in the WordPress app:

![image](https://github.com/wordpress-mobile/WordPress-Android/assets/5091503/9bf21b77-7e3e-4bbf-8790-ed7b351cbda3)


## Overall Specs
- The card uses a remote feature flag: `wp_sotw_2023_nudge`
- The card is displayed on top of the My Sites menu.
- Tapping `Watch now` should open a link to the event landing page.
- The text does not need localization. It's only displayed in English.
- If we have enough time, allow the user to dismiss the card (once and forever).
  - In the card, I used the ellipsis menu, but we can use the `X` icon and directly dismiss the image if it's any easier.

## Analytics
### Card shown
`sotw_2023_nudge_post_event_card_shown`

### Card hidden ("hide this" button tapped)
`sotw_2023_nudge_post_event_card_hide_tapped`

### CTA tapped
`sotw_2023_nudge_post_event_card_cta_tapped`


Discussion hints:
nan

Pull request: [SotW 2023] Add State of the Word post-event card to WordPress
Fixes #19709

More info in the parent issue. The card is only visible for the following requirements:
- The app is WordPress (part of the `WpSotw2023NudgeFeatureConfig` logic)
- The `wp_sotw_2023_nudge` flag is ON (`WpSotw2023NudgeFeatureConfig`)
- The user did not hide the card (App Pref set by user interaction with `hide this` card button)
- The device's current date is at least one day after the event date (2023-12-11)
- The user app language is English (`en`)


https://github.com/wordpress-mobile/WordPress-Android/assets/5091503/c976d05a-7b33-4f42-964b-7b12de4e8045

-----

## To Test:

Note: tracking is sent for all card interactions, check the analytics PR (https://github.com/wordpress-mobile/WordPress-Android/pull/19719) for testing instructions.

1. Install and log into the WordPress app
3. Set up app/device to follow the requirements above (some tips: https://github.com/wordpress-mobile/WordPress-Android/pull/19718#issue-2021569561)
4. Go to `My Site`
5. **Verify** the SotW card is shown above the Site Menu items
6. Tap `Watch Now`
7. **Verify** the SotW 2023 event page is opened on an external browser
8. Go back
9. Tap `Hde this`
10. **Verify** the card is hidden from the dashboard (all sites)

Note: the card only shows again if the user reinstalls the app, clears app data, or log out and log in again.

-----

## Regression Notes

1. Potential unintended areas of impact

    - Wrong behavior in the site dashboard
    - Card showing in Jetpack

12. What I did to test those areas of impact (or what existing automated tests I relied on)

    - Manual tests and Unit tests

13. What automated tests I added (or what prevented me from doing so)

    - Unit tests for all new code introduced
    - Updated unit tests for code that was changed

-----

## 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.
- [x] 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)
- [x] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)


Test command: ./gradlew :WordPress:testWordpressVanillaDebugUnitTest --tests "org.wordpress.android.ui.mysite.cards.sotw2023.WpSotw2023NudgeCardAnalyticsTrackerTest" --tests "org.wordpress.android.ui.mysite.cards.sotw2023.WpSotw2023NudgeCardViewModelSliceTest" --tests "org.wordpress.android.ui.mysite.cards.dashboard.todaysstats.TodaysStatsViewModelSliceTest" --tests "org.wordpress.android.ui.mysite.MySiteViewModelTest"

Fix patch:
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapter.kt
index 913f4e1557aa..6453339350c6 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapter.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapter.kt
@@ -24,6 +24,7 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.PostCard
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.QuickLinksItem
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.QuickStartCard
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.TodaysStatsCard.TodaysStatsCardWithData
+import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.WpSotw2023NudgeCardModel
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.CategoryEmptyHeaderItem
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.CategoryHeaderItem
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.InfoItem
@@ -51,6 +52,7 @@ import org.wordpress.android.ui.mysite.cards.nocards.NoCardsMessageViewHolder
 import org.wordpress.android.ui.mysite.cards.personalize.PersonalizeCardViewHolder
 import org.wordpress.android.ui.mysite.cards.quicklinksitem.QuickLinkRibbonViewHolder
 import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartCardViewHolder
+import org.wordpress.android.ui.mysite.cards.sotw2023.WpSotw2023NudgeCardViewHolder
 import org.wordpress.android.ui.mysite.items.categoryheader.MySiteCategoryItemEmptyViewHolder
 import org.wordpress.android.ui.mysite.items.categoryheader.MySiteCategoryItemViewHolder
 import org.wordpress.android.ui.mysite.items.infoitem.MySiteInfoItemViewHolder
@@ -123,6 +125,7 @@ class MySiteAdapter(
             )
             MySiteCardAndItem.Type.NO_CARDS_MESSAGE.ordinal -> NoCardsMessageViewHolder(parent)
             MySiteCardAndItem.Type.PERSONALIZE_CARD.ordinal -> PersonalizeCardViewHolder(parent)
+            MySiteCardAndItem.Type.WP_SOTW_2023_NUDGE_CARD.ordinal -> WpSotw2023NudgeCardViewHolder(parent)
             else -> throw IllegalArgumentException("Unexpected view type")
         }
     }
@@ -156,6 +159,7 @@ class MySiteAdapter(
             is JetpackInstallFullPluginCardViewHolder -> holder.bind(getItem(position) as JetpackInstallFullPluginCard)
             is NoCardsMessageViewHolder -> holder.bind(getItem(position) as MySiteCardAndItem.Card.NoCardsMessage)
             is PersonalizeCardViewHolder -> holder.bind(getItem(position) as PersonalizeCardModel)
+            is WpSotw2023NudgeCardViewHolder -> holder.bind(getItem(position) as WpSotw2023NudgeCardModel)
         }
     }
 
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapterDiffCallback.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapterDiffCallback.kt
index 0a728059f46a..0e7c90394bbd 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapterDiffCallback.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapterDiffCallback.kt
@@ -20,6 +20,7 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.PostCard.PostCardW
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.QuickLinksItem
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.QuickStartCard
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.TodaysStatsCard.TodaysStatsCardWithData
+import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.WpSotw2023NudgeCardModel
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.CategoryEmptyHeaderItem
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.CategoryHeaderItem
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.InfoItem
@@ -64,6 +65,7 @@ object MySiteAdapterDiffCallback : DiffUtil.ItemCallback<MySiteCardAndItem>() {
             oldItem is MySiteCardAndItem.Card.NoCardsMessage && updatedItem is
                     MySiteCardAndItem.Card.NoCardsMessage -> true
             oldItem is PersonalizeCardModel && updatedItem is PersonalizeCardModel -> true
+            oldItem is WpSotw2023NudgeCardModel && updatedItem is WpSotw2023NudgeCardModel -> true
             else -> throw UnsupportedOperationException("Diff not implemented yet")
         }
     }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItem.kt
index f70f84389aae..aef4980fc62f 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItem.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItem.kt
@@ -59,6 +59,7 @@ sealed class MySiteCardAndItem(open val type: Type, open val activeQuickStartIte
         JETPACK_INSTALL_FULL_PLUGIN_CARD,
         NO_CARDS_MESSAGE,
         PERSONALIZE_CARD,
+        WP_SOTW_2023_NUDGE_CARD,
     }
 
     data class SiteInfoHeaderCard(
@@ -380,6 +381,14 @@ sealed class MySiteCardAndItem(open val type: Type, open val activeQuickStartIte
             val onMoreMenuClick: ListItemInteraction,
         ) : Card(type = Type.DASHBOARD_PLANS_CARD)
 
+        data class WpSotw2023NudgeCardModel(
+            val title: UiString,
+            val text: UiString,
+            val ctaText: UiString,
+            val onHideMenuItemClick: ListItemInteraction,
+            val onCtaClick: ListItemInteraction,
+        ) : Card(type = Type.WP_SOTW_2023_NUDGE_CARD)
+
         data class NoCardsMessage(val title: UiString, val message: UiString)  : Card(Type.NO_CARDS_MESSAGE)
         data class PersonalizeCardModel(val onClick: () -> Unit) : Card(Type.PERSONALIZE_CARD)
     }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt
index 740deed94cca..3a9d04129a03 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt
@@ -704,7 +704,7 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
             ActivityLauncher.viewCurrentBlogPostsOfType(requireActivity(), action.site, PostListType.SCHEDULED)
         is SiteNavigationAction.OpenStatsInsights ->
             ActivityLauncher.viewBlogStatsForTimeframe(requireActivity(), action.site, StatsTimeframe.INSIGHTS)
-        is SiteNavigationAction.OpenTodaysStatsGetMoreViewsExternalUrl ->
+        is SiteNavigationAction.OpenExternalUrl ->
             ActivityLauncher.openUrlExternal(requireActivity(), action.url)
         is SiteNavigationAction.OpenJetpackPoweredBottomSheet -> showJetpackPoweredBottomSheet()
         is SiteNavigationAction.OpenJetpackMigrationDeleteWP -> showJetpackMigrationDeleteWP()
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 b92685992b0f..9f6f0fcf4404 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
@@ -84,6 +84,7 @@ import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository
 import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository.QuickStartCategory
 import org.wordpress.android.ui.mysite.cards.siteinfo.SiteInfoHeaderCardBuilder
 import org.wordpress.android.ui.mysite.cards.siteinfo.SiteInfoHeaderCardViewModelSlice
+import org.wordpress.android.ui.mysite.cards.sotw2023.WpSotw2023NudgeCardViewModelSlice
 import org.wordpress.android.ui.mysite.items.infoitem.MySiteInfoItemBuilder
 import org.wordpress.android.ui.mysite.items.listitem.SiteItemsBuilder
 import org.wordpress.android.ui.mysite.items.listitem.SiteItemsViewModelSlice
@@ -169,6 +170,7 @@ class MySiteViewModel @Inject constructor(
     private val siteInfoHeaderCardViewModelSlice: SiteInfoHeaderCardViewModelSlice,
     private val quickLinksItemViewModelSlice: QuickLinksItemViewModelSlice,
     private val bloganuaryNudgeCardViewModelSlice: BloganuaryNudgeCardViewModelSlice,
+    private val sotw2023NudgeCardViewModelSlice: WpSotw2023NudgeCardViewModelSlice,
 ) : ScopedViewModel(mainDispatcher) {
     private val _onSnackbarMessage = MutableLiveData<Event<SnackbarMessageHolder>>()
     private val _onNavigation = MutableLiveData<Event<SiteNavigationAction>>()
@@ -231,7 +233,8 @@ class MySiteViewModel @Inject constructor(
         bloganuaryNudgeCardViewModelSlice.onNavigation,
         personalizeCardViewModelSlice.onNavigation,
         siteInfoHeaderCardViewModelSlice.onNavigation,
-        quickLinksItemViewModelSlice.navigation
+        quickLinksItemViewModelSlice.navigation,
+        sotw2023NudgeCardViewModelSlice.onNavigation,
     )
 
     val onMediaUpload = siteInfoHeaderCardViewModelSlice.onMediaUpload
@@ -247,6 +250,7 @@ class MySiteViewModel @Inject constructor(
             postsCardViewModelSlice.refresh,
             activityLogCardViewModelSlice.refresh,
             bloganuaryNudgeCardViewModelSlice.refresh,
+            sotw2023NudgeCardViewModelSlice.refresh,
         )
     val domainTransferCardRefresh = domainTransferCardViewModel.refresh
 
@@ -322,6 +326,7 @@ class MySiteViewModel @Inject constructor(
         siteInfoHeaderCardViewModelSlice.initialize(viewModelScope)
         quickLinksItemViewModelSlice.initialization(viewModelScope)
         quickLinksItemViewModelSlice.start()
+        sotw2023NudgeCardViewModelSlice.initialize(viewModelScope)
     }
 
     @Suppress("LongParameterList")
@@ -423,8 +428,11 @@ class MySiteViewModel @Inject constructor(
 
         val siteItems = getSiteItems(site, activeTask, backupAvailable, scanAvailable)
 
+        val sotw2023Card = sotw2023NudgeCardViewModelSlice.buildCard()
+
         return mutableListOf<MySiteCardAndItem>().apply {
             infoItem?.let { add(infoItem) }
+            sotw2023Card?.let { add(it) }
             addAll(siteItems)
             jetpackSwitchMenu?.let { add(jetpackSwitchMenu) }
             if (jetpackFeatureCardHelper.shouldShowFeatureCardAtTop())
@@ -988,6 +996,8 @@ class MySiteViewModel @Inject constructor(
             .forEach { personalizeCardViewModelSlice.trackShown(it.type) }
         siteSelected.dashboardData.filterIsInstance<MySiteCardAndItem.Card.NoCardsMessage>()
             .forEach { noCardsMessageViewModelSlice.trackShown(it.type) }
+        siteSelected.dashboardData.filterIsInstance<MySiteCardAndItem.Card.WpSotw2023NudgeCardModel>()
+            .forEach { _ -> sotw2023NudgeCardViewModelSlice.trackShown() }
     }
 
     private fun resetShownTrackers() {
@@ -997,6 +1007,7 @@ class MySiteViewModel @Inject constructor(
         jetpackFeatureCardShownTracker.resetShown()
         jetpackInstallFullPluginShownTracker.resetShown()
         personalizeCardViewModelSlice.resetShown()
+        sotw2023NudgeCardViewModelSlice.resetShown()
     }
 
     // FluxC events
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt
index 0ab91f96784e..a9b593bf2ad4 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt
@@ -85,7 +85,7 @@ sealed class SiteNavigationAction {
     data class EditDraftPost(val site: SiteModel, val postId: Int) : SiteNavigationAction()
     data class EditScheduledPost(val site: SiteModel, val postId: Int) : SiteNavigationAction()
     data class OpenStatsInsights(val site: SiteModel) : SiteNavigationAction()
-    data class OpenTodaysStatsGetMoreViewsExternalUrl(val url: String) : SiteNavigationAction()
+    data class OpenExternalUrl(val url: String) : SiteNavigationAction()
     object OpenJetpackPoweredBottomSheet : SiteNavigationAction()
     object OpenJetpackMigrationDeleteWP : SiteNavigationAction()
     data class OpenJetpackFeatureOverlay(val source: JetpackFeatureCollectionOverlaySource) : SiteNavigationAction()
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSlice.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSlice.kt
index 0a6b9f64a56d..c1d3f4f34ce8 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSlice.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSlice.kt
@@ -50,7 +50,7 @@ class TodaysStatsViewModelSlice @Inject constructor(
             _onNavigation.value = Event(SiteNavigationAction.ShowJetpackRemovalStaticPostersView)
         } else {
             _onNavigation.value = Event(
-                SiteNavigationAction.OpenTodaysStatsGetMoreViewsExternalUrl(
+                SiteNavigationAction.OpenExternalUrl(
                     TodaysStatsCardBuilder.URL_GET_MORE_VIEWS_AND_TRAFFIC
                 )
             )
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCard.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCard.kt
new file mode 100644
index 000000000000..350448ad9d03
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCard.kt
@@ -0,0 +1,101 @@
+package org.wordpress.android.ui.mysite.cards.sotw2023
+
+import android.content.res.Configuration
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import org.wordpress.android.R
+import org.wordpress.android.ui.compose.components.card.UnelevatedCard
+import org.wordpress.android.ui.compose.styles.DashboardCardTypography
+import org.wordpress.android.ui.compose.theme.AppTheme
+import org.wordpress.android.ui.compose.unit.Margin
+import org.wordpress.android.ui.compose.utils.uiStringText
+import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.WpSotw2023NudgeCardModel
+import org.wordpress.android.ui.mysite.cards.compose.MySiteCardToolbar
+import org.wordpress.android.ui.mysite.cards.compose.MySiteCardToolbarContextMenuItem
+import org.wordpress.android.ui.utils.ListItemInteraction
+import org.wordpress.android.ui.utils.UiString.UiStringRes
+
+@Composable
+fun WpSotw2023NudgeCard(
+    model: WpSotw2023NudgeCardModel,
+    modifier: Modifier = Modifier,
+) {
+    UnelevatedCard(
+        modifier = modifier.semantics(mergeDescendants = true) {},
+    ) {
+        Column(
+            modifier = Modifier.padding(bottom = Margin.Medium.value)
+        ) {
+            CardToolbar(model)
+
+            Spacer(Modifier.height(Margin.Medium.value))
+
+            Text(
+                text = uiStringText(model.text),
+                style = DashboardCardTypography.detailText,
+                modifier = Modifier.padding(horizontal = Margin.ExtraLarge.value),
+            )
+
+            Spacer(Modifier.height(Margin.Small.value))
+
+            TextButton(
+                onClick = { model.onCtaClick.click() },
+                modifier = Modifier.padding(horizontal = Margin.Small.value)
+            ) {
+                Text(
+                    text = uiStringText(model.ctaText),
+                    style = DashboardCardTypography.footerCTA,
+                )
+            }
+        }
+    }
+}
+
+@Composable
+private fun CardToolbar(
+    model: WpSotw2023NudgeCardModel
+) {
+    MySiteCardToolbar(
+        contextMenuItems = listOf(
+            MySiteCardToolbarContextMenuItem.Option(
+                text = stringResource(R.string.my_site_dashboard_card_more_menu_hide_card),
+                onClick = { model.onHideMenuItemClick.click() },
+            )
+        ),
+    ) {
+        Text(
+            text = uiStringText(uiString = model.title),
+            style = DashboardCardTypography.smallTitle,
+            textAlign = TextAlign.Start,
+            fontWeight = FontWeight.Medium,
+        )
+    }
+}
+
+@Preview(name = "Light Mode")
+@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+fun WpSotw2023NudgeCardPreview() {
+    AppTheme {
+        WpSotw2023NudgeCard(
+            model = WpSotw2023NudgeCardModel(
+                title = UiStringRes(R.string.wp_sotw_2023_dashboard_nudge_title),
+                text = UiStringRes(R.string.wp_sotw_2023_dashboard_nudge_text),
+                ctaText = UiStringRes(R.string.wp_sotw_2023_dashboard_nudge_cta),
+                onHideMenuItemClick = ListItemInteraction.create {},
+                onCtaClick = ListItemInteraction.create {},
+            )
+        )
+    }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardAnalyticsTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardAnalyticsTracker.kt
new file mode 100644
index 000000000000..be84de153368
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardAnalyticsTracker.kt
@@ -0,0 +1,30 @@
+package org.wordpress.android.ui.mysite.cards.sotw2023
+
+import org.wordpress.android.analytics.AnalyticsTracker
+import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
+import javax.inject.Inject
+
+class WpSotw2023NudgeCardAnalyticsTracker @Inject constructor(
+    private val analyticsTracker: AnalyticsTrackerWrapper,
+) {
+    private var cardShownTracked: Boolean = false
+
+    fun resetShown() {
+        cardShownTracked = false
+    }
+
+    fun trackShown() {
+        if (!cardShownTracked) {
+            cardShownTracked = true
+            analyticsTracker.track(AnalyticsTracker.Stat.SOTW_2023_NUDGE_POST_EVENT_CARD_SHOWN)
+        }
+    }
+
+    fun trackCtaTapped() {
+        analyticsTracker.track(AnalyticsTracker.Stat.SOTW_2023_NUDGE_POST_EVENT_CARD_CTA_TAPPED)
+    }
+
+    fun trackHideTapped() {
+        analyticsTracker.track(AnalyticsTracker.Stat.SOTW_2023_NUDGE_POST_EVENT_CARD_HIDE_TAPPED)
+    }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardViewHolder.kt
new file mode 100644
index 000000000000..e924cd6ff113
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardViewHolder.kt
@@ -0,0 +1,27 @@
+package org.wordpress.android.ui.mysite.cards.sotw2023
+
+import android.view.ViewGroup
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import org.wordpress.android.databinding.WpSotw20223NudgeCardBinding
+import org.wordpress.android.ui.compose.theme.AppTheme
+import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.WpSotw2023NudgeCardModel
+import org.wordpress.android.ui.mysite.MySiteCardAndItemViewHolder
+import org.wordpress.android.util.extensions.viewBinding
+
+class WpSotw2023NudgeCardViewHolder(parent: ViewGroup) :
+    MySiteCardAndItemViewHolder<WpSotw20223NudgeCardBinding>(parent.viewBinding(WpSotw20223NudgeCardBinding::inflate)) {
+    fun bind(cardModel: WpSotw2023NudgeCardModel) = with(binding.wpSotw2023NudgeCard) {
+        // Dispose of the Composition when the view's LifecycleOwner is destroyed
+        setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool)
+        setContent {
+            AppTheme {
+                WpSotw2023NudgeCard(
+                    model = cardModel,
+                    modifier = Modifier.fillMaxWidth(),
+                )
+            }
+        }
+    }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardViewModelSlice.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardViewModelSlice.kt
new file mode 100644
index 000000000000..c38ed0766037
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardViewModelSlice.kt
@@ -0,0 +1,86 @@
+package org.wordpress.android.ui.mysite.cards.sotw2023
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import kotlinx.coroutines.CoroutineScope
+import org.wordpress.android.R
+import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.WpSotw2023NudgeCardModel
+import org.wordpress.android.ui.mysite.SiteNavigationAction
+import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenExternalUrl
+import org.wordpress.android.ui.prefs.AppPrefsWrapper
+import org.wordpress.android.ui.utils.ListItemInteraction
+import org.wordpress.android.ui.utils.UiString.UiStringRes
+import org.wordpress.android.util.DateTimeUtilsWrapper
+import org.wordpress.android.util.LocaleManagerWrapper
+import org.wordpress.android.util.config.WpSotw2023NudgeFeatureConfig
+import org.wordpress.android.viewmodel.Event
+import java.time.Instant
+import javax.inject.Inject
+
+class WpSotw2023NudgeCardViewModelSlice @Inject constructor(
+    private val featureConfig: WpSotw2023NudgeFeatureConfig,
+    private val appPrefsWrapper: AppPrefsWrapper,
+    private val dateTimeUtilsWrapper: DateTimeUtilsWrapper,
+    private val localeManagerWrapper: LocaleManagerWrapper,
+    private val tracker: WpSotw2023NudgeCardAnalyticsTracker,
+) {
+    private val _onNavigation = MutableLiveData<Event<SiteNavigationAction>>()
+    val onNavigation = _onNavigation as LiveData<Event<SiteNavigationAction>>
+
+    private val _refresh = MutableLiveData<Event<Boolean>>()
+    val refresh = _refresh as LiveData<Event<Boolean>>
+
+    private lateinit var scope: CoroutineScope
+
+    fun initialize(scope: CoroutineScope) {
+        this.scope = scope
+    }
+
+    fun buildCard(): WpSotw2023NudgeCardModel? = WpSotw2023NudgeCardModel(
+        title = UiStringRes(R.string.wp_sotw_2023_dashboard_nudge_title),
+        text = UiStringRes(R.string.wp_sotw_2023_dashboard_nudge_text),
+        ctaText = UiStringRes(R.string.wp_sotw_2023_dashboard_nudge_cta),
+        onHideMenuItemClick = ListItemInteraction.create(::onHideMenuItemClick),
+        onCtaClick = ListItemInteraction.create(::onCtaClick),
+    ).takeIf { isEligible() }
+
+    fun trackShown() {
+        tracker.trackShown()
+    }
+
+    fun resetShown() {
+        tracker.resetShown()
+    }
+
+    private fun onHideMenuItemClick() {
+        tracker.trackHideTapped()
+        appPrefsWrapper.setShouldHideSotw2023NudgeCard(true)
+        _refresh.value = Event(true)
+    }
+
+    private fun onCtaClick() {
+        tracker.trackCtaTapped()
+        _onNavigation.value = Event(OpenExternalUrl(URL))
+    }
+
+    private fun isEligible(): Boolean {
+        val eventTime = Instant.parse(POST_EVENT_START)
+        val now = dateTimeUtilsWrapper.getInstantNow()
+        val isDateEligible = now.isAfter(eventTime)
+
+        val currentLanguage = localeManagerWrapper.getLanguage()
+        val isLanguageEligible = currentLanguage.startsWith(TARGET_LANGUAGE, ignoreCase = true)
+
+        return featureConfig.isEnabled() &&
+                !appPrefsWrapper.getShouldHideSotw2023NudgeCard() &&
+                isDateEligible &&
+                isLanguageEligible
+    }
+
+    companion object {
+        private const val URL = "https://wordpress.org/state-of-the-word/" +
+                "?utm_source=mobile&utm_medium=appnudge&utm_campaign=sotw2023"
+        private const val POST_EVENT_START = "2023-12-12T00:00:00.00Z"
+        private const val TARGET_LANGUAGE = "en"
+    }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
index ebbc64b69f60..d39eadbbfb90 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
@@ -207,6 +207,7 @@ public enum DeletablePrefKey implements PrefKey {
         SHOULD_SHOW_SITE_ITEM_AS_QUICK_LINK_IN_DASHBOARD,
         SHOULD_SHOW_DEFAULT_QUICK_LINK_IN_DASHBOARD,
         SHOULD_HIDE_BLOGANUARY_NUDGE_CARD,
+        SHOULD_HIDE_SOTW2023_NUDGE_CARD,
     }
 
     /**
@@ -1821,4 +1822,12 @@ public static void setShouldHideBloganuaryNudgeCard(final long siteId, final boo
     public static boolean getShouldHideBloganuaryNudgeCard(final long siteId) {
         return prefs().getBoolean(getSiteIdHideBloganuaryNudgeCardKey(siteId), false);
     }
+
+    public static void setShouldHideSotw2023NudgeCard(boolean isHidden) {
+        prefs().edit().putBoolean(DeletablePrefKey.SHOULD_HIDE_SOTW2023_NUDGE_CARD.name(), isHidden).apply();
+    }
+
+    public static boolean getShouldHideSotw2023NudgeCard() {
+        return prefs().getBoolean(DeletablePrefKey.SHOULD_HIDE_SOTW2023_NUDGE_CARD.name(), false);
+    }
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt
index 6a02a0c17915..190794b1d0d7 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt
@@ -418,6 +418,12 @@ class AppPrefsWrapper @Inject constructor() {
     fun getShouldHideBloganuaryNudgeCard(siteId: Long): Boolean =
         AppPrefs.getShouldHideBloganuaryNudgeCard(siteId)
 
+    fun setShouldHideSotw2023NudgeCard(isHidden: Boolean): Unit =
+        AppPrefs.setShouldHideSotw2023NudgeCard(isHidden)
+
+    fun getShouldHideSotw2023NudgeCard(): Boolean =
+        AppPrefs.getShouldHideSotw2023NudgeCard()
+
     fun getAllPrefs(): Map<String, Any?> = AppPrefs.getAllPrefs()
 
     fun setString(prefKey: PrefKey, value: String) {
diff --git a/WordPress/src/main/java/org/wordpress/android/util/DateTimeUtilsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/util/DateTimeUtilsWrapper.kt
index e06a74f95802..8e0a065963e5 100644
--- a/WordPress/src/main/java/org/wordpress/android/util/DateTimeUtilsWrapper.kt
+++ b/WordPress/src/main/java/org/wordpress/android/util/DateTimeUtilsWrapper.kt
@@ -10,6 +10,7 @@ import java.util.Date
 import java.util.Locale
 import javax.inject.Inject
 import android.text.format.DateUtils
+import java.time.Instant
 
 class DateTimeUtilsWrapper @Inject constructor(
     private val localeManagerWrapper: LocaleManagerWrapper,
@@ -58,4 +59,6 @@ class DateTimeUtilsWrapper @Inject constructor(
     fun getCalendarInstance(): Calendar {
         return Calendar.getInstance()
     }
+
+    fun getInstantNow(): Instant = Instant.now()
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/WpSotw2023NudgeFeatureConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/WpSotw2023NudgeFeatureConfig.kt
new file mode 100644
index 000000000000..d39086ea2e12
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/config/WpSotw2023NudgeFeatureConfig.kt
@@ -0,0 +1,20 @@
+package org.wordpress.android.util.config
+
+import org.wordpress.android.BuildConfig
+import org.wordpress.android.annotation.Feature
+import javax.inject.Inject
+
+private const val WP_SOTW_2023_NUDGE_REMOTE_FIELD = "wp_sotw_2023_nudge"
+
+@Feature(WP_SOTW_2023_NUDGE_REMOTE_FIELD, true)
+class WpSotw2023NudgeFeatureConfig @Inject constructor(
+    appConfig: AppConfig
+) : FeatureConfig(
+    appConfig,
+    BuildConfig.BLOGANUARY_DASHBOARD_NUDGE,
+    WP_SOTW_2023_NUDGE_REMOTE_FIELD
+) {
+    override fun isEnabled(): Boolean {
+        return super.isEnabled() && !BuildConfig.IS_JETPACK_APP
+    }
+}
diff --git a/WordPress/src/main/res/layout/wp_sotw_20223_nudge_card.xml b/WordPress/src/main/res/layout/wp_sotw_20223_nudge_card.xml
new file mode 100644
index 000000000000..ee8b5955c2c1
--- /dev/null
+++ b/WordPress/src/main/res/layout/wp_sotw_20223_nudge_card.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <androidx.compose.ui.platform.ComposeView
+        android:id="@+id/wp_sotw_2023_nudge_card"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        tools:composableName="org.wordpress.android.ui.mysite.cards.sotw2023.WpSotw2023NudgeCardKt.WpSotw2023NudgeCardPreview"
+        tools:ignore="RtlSymmetry" />
+
+</LinearLayout>
diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml
index 8f3eec407ae5..53ecf31b0eb5 100644
--- a/WordPress/src/main/res/values/strings.xml
+++ b/WordPress/src/main/res/values/strings.xml
@@ -4833,4 +4833,10 @@ translators: %s: Select control option value e.g: "Auto, 25%". -->
     <string name="bloganuary_dashboard_nudge_overlay_note_prompts_enabled">Bloganuary will use Daily Blogging Prompts to send you topics for the month of January.</string>
     <string name="bloganuary_dashboard_nudge_overlay_action_turn_on_prompts">Turn on blogging prompts</string>
     <string name="bloganuary_dashboard_nudge_overlay_action_dismiss">Let’s go!</string>
+
+
+    <!-- State of the Word 2023 -->
+    <string name="wp_sotw_2023_dashboard_nudge_title" translatable="false">State of the Word 2023</string>
+    <string name="wp_sotw_2023_dashboard_nudge_text" translatable="false">Check out WordPress co-founder Matt Mullenweg’s annual keynote to stay on top of what’s coming in 2024 and beyond.</string>
+    <string name="wp_sotw_2023_dashboard_nudge_cta" translatable="false">Watch now</string>
 </resources>
diff --git a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java
index fecb45182c71..d10e8c891011 100644
--- a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java
+++ b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java
@@ -1100,6 +1100,9 @@ public enum Stat {
         BLOGANUARY_NUDGE_LEARN_MORE_MODAL_SHOWN,
         BLOGANUARY_NUDGE_LEARN_MORE_MODAL_DISMISSED,
         BLOGANUARY_NUDGE_LEARN_MORE_MODAL_ACTION_TAPPED,
+        SOTW_2023_NUDGE_POST_EVENT_CARD_SHOWN,
+        SOTW_2023_NUDGE_POST_EVENT_CARD_HIDE_TAPPED,
+        SOTW_2023_NUDGE_POST_EVENT_CARD_CTA_TAPPED,
     }
 
     private static final List<Tracker> TRACKERS = new ArrayList<>();
diff --git a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java
index de149b5979a0..bc80347c9fc3 100644
--- a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java
+++ b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java
@@ -2685,6 +2685,12 @@ public static String getEventNameForStat(AnalyticsTracker.Stat stat) {
                 return "bloganuary_nudge_learn_more_modal_dismissed";
             case BLOGANUARY_NUDGE_LEARN_MORE_MODAL_ACTION_TAPPED:
                 return "bloganuary_nudge_learn_more_modal_action_tapped";
+            case SOTW_2023_NUDGE_POST_EVENT_CARD_SHOWN:
+                return "sotw_2023_nudge_post_event_card_shown";
+            case SOTW_2023_NUDGE_POST_EVENT_CARD_HIDE_TAPPED:
+                return "sotw_2023_nudge_post_event_card_hide_tapped";
+            case SOTW_2023_NUDGE_POST_EVENT_CARD_CTA_TAPPED:
+                return "sotw_2023_nudge_post_event_card_cta_tapped";
         }
         return null;
     }


Test patch:
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 74610d76f6ff..54beb07c7607 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
@@ -51,6 +51,7 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.ErrorCard
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.JetpackFeatureCard
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.QuickStartCard
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.QuickStartCard.QuickStartTaskTypeItem
+import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.WpSotw2023NudgeCardModel
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.InfoItem
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.ListItem
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.SingleActionCard
@@ -97,6 +98,7 @@ import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository
 import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository.QuickStartCategory
 import org.wordpress.android.ui.mysite.cards.siteinfo.SiteInfoHeaderCardBuilder
 import org.wordpress.android.ui.mysite.cards.siteinfo.SiteInfoHeaderCardViewModelSlice
+import org.wordpress.android.ui.mysite.cards.sotw2023.WpSotw2023NudgeCardViewModelSlice
 import org.wordpress.android.ui.mysite.items.infoitem.MySiteInfoItemBuilder
 import org.wordpress.android.ui.mysite.items.listitem.ListItemAction
 import org.wordpress.android.ui.mysite.items.listitem.SiteItemsBuilder
@@ -281,6 +283,9 @@ class MySiteViewModelTest : BaseUnitTest() {
     @Mock
     lateinit var bloganuaryNudgeViewModelSlice: BloganuaryNudgeCardViewModelSlice
 
+    @Mock
+    lateinit var wpSotw2023NudgeCardViewModelSlice: WpSotw2023NudgeCardViewModelSlice
+
     private lateinit var viewModel: MySiteViewModel
     private lateinit var uiModels: MutableList<MySiteViewModel.State>
     private lateinit var snackbars: MutableList<SnackbarMessageHolder>
@@ -420,6 +425,7 @@ class MySiteViewModelTest : BaseUnitTest() {
         whenever(bloggingPromptCardViewModelSlice.getBuilderParams(anyOrNull())).thenReturn(mock())
         whenever(quickLinksItemViewModelSlice.uiState).thenReturn(mock())
         whenever(quickStartRepository.quickStartMenuStep).thenReturn(mock())
+        whenever(wpSotw2023NudgeCardViewModelSlice.buildCard()).thenReturn(null)
 
         viewModel = MySiteViewModel(
             testDispatcher(),
@@ -474,6 +480,7 @@ class MySiteViewModelTest : BaseUnitTest() {
             siteInfoHeaderCardViewModelSlice,
             quickLinksItemViewModelSlice,
             bloganuaryNudgeViewModelSlice,
+            wpSotw2023NudgeCardViewModelSlice,
         )
         uiModels = mutableListOf()
         snackbars = mutableListOf()
@@ -1430,6 +1437,24 @@ class MySiteViewModelTest : BaseUnitTest() {
             assertThat(viewModel.onShowJetpackIndividualPluginOverlay.value?.peekContent()).isNull()
         }
 
+    @Test
+    fun `when sotw card is not null then it is shown in the site menu`() {
+        whenever(wpSotw2023NudgeCardViewModelSlice.buildCard()).thenReturn(mock())
+
+        initSelectedSite()
+
+        assertThat(getSiteMenuTabLastItems().filterIsInstance(WpSotw2023NudgeCardModel::class.java)).isNotEmpty
+    }
+
+    @Test
+    fun `when sotw card is null then it is not shown in the site menu`() {
+        whenever(wpSotw2023NudgeCardViewModelSlice.buildCard()).thenReturn(null)
+
+        initSelectedSite()
+
+        assertThat(getSiteMenuTabLastItems().filterIsInstance(WpSotw2023NudgeCardModel::class.java)).isEmpty()
+    }
+
     private fun findDomainRegistrationCard() =
         getLastItems().find { it is DomainRegistrationCard } as DomainRegistrationCard?
 
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt
index dc057216dcdd..21880f63df13 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt
@@ -87,7 +87,7 @@ class TodaysStatsViewModelSliceTest : BaseUnitTest() {
 
             assertThat(navigationActions)
                 .containsOnly(
-                    SiteNavigationAction.OpenTodaysStatsGetMoreViewsExternalUrl(
+                    SiteNavigationAction.OpenExternalUrl(
                         TodaysStatsCardBuilder.URL_GET_MORE_VIEWS_AND_TRAFFIC
                     )
                 )
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardAnalyticsTrackerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardAnalyticsTrackerTest.kt
new file mode 100644
index 000000000000..64f442fa6cbc
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardAnalyticsTrackerTest.kt
@@ -0,0 +1,62 @@
+package org.wordpress.android.ui.mysite.cards.sotw2023
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.wordpress.android.BaseUnitTest
+import org.wordpress.android.analytics.AnalyticsTracker.Stat
+import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class WpSotw2023NudgeCardAnalyticsTrackerTest : BaseUnitTest() {
+    @Mock
+    lateinit var analyticsTracker: AnalyticsTrackerWrapper
+
+    lateinit var tracker: WpSotw2023NudgeCardAnalyticsTracker
+
+    @Before
+    fun setUp() {
+        tracker = WpSotw2023NudgeCardAnalyticsTracker(analyticsTracker)
+    }
+
+    @Test
+    fun `WHEN card is shown THEN trackShown is called`() {
+        tracker.trackShown()
+
+        verify(analyticsTracker).track(Stat.SOTW_2023_NUDGE_POST_EVENT_CARD_SHOWN)
+    }
+
+    @Test
+    fun `WHEN card is shown multiple times without resetting THEN trackShown is called once`() {
+        tracker.trackShown()
+        tracker.trackShown()
+
+        verify(analyticsTracker, times(1)).track(Stat.SOTW_2023_NUDGE_POST_EVENT_CARD_SHOWN)
+    }
+
+    @Test
+    fun `WHEN card is shown multiple times with resetting THEN trackShown is called multiple times`() {
+        tracker.trackShown()
+        tracker.resetShown()
+        tracker.trackShown()
+
+        verify(analyticsTracker, times(2)).track(Stat.SOTW_2023_NUDGE_POST_EVENT_CARD_SHOWN)
+    }
+
+    @Test
+    fun `WHEN card CTA is tapped THEN trackCtaTapped is called`() {
+        tracker.trackCtaTapped()
+
+        verify(analyticsTracker).track(Stat.SOTW_2023_NUDGE_POST_EVENT_CARD_CTA_TAPPED)
+    }
+
+    @Test
+    fun `WHEN card hide is tapped THEN trackHideTapped is called`() {
+        tracker.trackHideTapped()
+
+        verify(analyticsTracker).track(Stat.SOTW_2023_NUDGE_POST_EVENT_CARD_HIDE_TAPPED)
+    }
+}
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardViewModelSliceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardViewModelSliceTest.kt
new file mode 100644
index 000000000000..19cb2f2f4330
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardViewModelSliceTest.kt
@@ -0,0 +1,166 @@
+package org.wordpress.android.ui.mysite.cards.sotw2023
+
+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.Mockito
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.wordpress.android.BaseUnitTest
+import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenExternalUrl
+import org.wordpress.android.ui.prefs.AppPrefsWrapper
+import org.wordpress.android.util.DateTimeUtilsWrapper
+import org.wordpress.android.util.LocaleManagerWrapper
+import org.wordpress.android.util.config.WpSotw2023NudgeFeatureConfig
+import java.time.Instant
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class WpSotw2023NudgeCardViewModelSliceTest : BaseUnitTest() {
+    @Mock
+    lateinit var featureConfig: WpSotw2023NudgeFeatureConfig
+
+    @Mock
+    lateinit var appPrefsWrapper: AppPrefsWrapper
+
+    @Mock
+    lateinit var dateTimeUtilsWrapper: DateTimeUtilsWrapper
+
+    @Mock
+    lateinit var localeManagerWrapper: LocaleManagerWrapper
+
+    @Mock
+    lateinit var tracker: WpSotw2023NudgeCardAnalyticsTracker
+
+    private lateinit var viewModelSlice: WpSotw2023NudgeCardViewModelSlice
+
+    @Before
+    fun setUp() {
+        viewModelSlice = WpSotw2023NudgeCardViewModelSlice(
+            featureConfig,
+            appPrefsWrapper,
+            dateTimeUtilsWrapper,
+            localeManagerWrapper,
+            tracker,
+        )
+        viewModelSlice.initialize(testScope())
+    }
+
+    @Test
+    fun `WHEN feature is disabled THEN buildCard returns null `() {
+        mockCardRequisites(isFeatureEnabled = false)
+
+        val card = viewModelSlice.buildCard()
+
+        assertThat(card).isNull()
+    }
+
+    @Test
+    fun `WHEN card is hidden in app prefs THEN buildCard returns null`() {
+        mockCardRequisites(isCardHidden = true)
+
+        val card = viewModelSlice.buildCard()
+
+        assertThat(card).isNull()
+    }
+
+    @Test
+    fun `WHEN date is before event THEN buildCard returns null`() {
+        mockCardRequisites(isDateAfterEvent = false)
+
+        val card = viewModelSlice.buildCard()
+
+        assertThat(card).isNull()
+    }
+
+    @Test
+    fun `WHEN language is not english THEN buildCard returns null`() {
+        mockCardRequisites(isLanguageEnglish = false)
+
+        val card = viewModelSlice.buildCard()
+
+        assertThat(card).isNull()
+    }
+
+    @Test
+    fun `WHEN requisites are met THEN buildCard returns card `() {
+        mockCardRequisites()
+
+        val card = viewModelSlice.buildCard()
+
+        assertThat(card).isNotNull
+    }
+
+    @Test
+    fun `WHEN card onCtaClick is clicked THEN navigate to URL`() {
+        mockCardRequisites()
+
+        val card = viewModelSlice.buildCard()!!
+        card.onCtaClick.click()
+
+        assertThat(viewModelSlice.onNavigation.value?.peekContent()).isEqualTo(OpenExternalUrl(EXPECTED_URL))
+    }
+
+    @Test
+    fun `WHEN card onHideMenuItemClick is clicked THEN hide card in app prefs and refresh`() {
+        mockCardRequisites()
+
+        val card = viewModelSlice.buildCard()!!
+        card.onHideMenuItemClick.click()
+
+        verify(appPrefsWrapper).setShouldHideSotw2023NudgeCard(true)
+        assertThat(viewModelSlice.refresh.value?.peekContent()).isTrue
+    }
+
+    // region Analytics
+    @Test
+    fun `WHEN card is shown THEN analytics is tracked`() {
+        mockCardRequisites()
+
+        viewModelSlice.trackShown()
+
+        verify(tracker).trackShown()
+    }
+
+    @Test
+    fun `WHEN card onCtaClick is clicked THEN analytics is tracked`() {
+        mockCardRequisites()
+
+        val card = viewModelSlice.buildCard()!!
+        card.onCtaClick.click()
+        verify(tracker).trackCtaTapped()
+    }
+
+    @Test
+    fun `WHEN card onHideMenuItemClick is clicked THEN analytics is tracked`() {
+        mockCardRequisites()
+
+        val card = viewModelSlice.buildCard()!!
+        card.onHideMenuItemClick.click()
+
+        verify(tracker).trackHideTapped()
+    }
+    // endregion Analytics
+
+    private fun mockCardRequisites(
+        isFeatureEnabled: Boolean = true,
+        isCardHidden: Boolean = false,
+        isDateAfterEvent: Boolean = true,
+        isLanguageEnglish: Boolean = true
+    ) {
+        with(Mockito.lenient()) {
+            whenever(featureConfig.isEnabled()).thenReturn(isFeatureEnabled)
+            whenever(appPrefsWrapper.getShouldHideSotw2023NudgeCard()).thenReturn(isCardHidden)
+            val date = if (isDateAfterEvent) "2023-12-12T00:00:01Z" else "2021-12-11T00:00:00Z"
+            whenever(dateTimeUtilsWrapper.getInstantNow()).thenReturn(Instant.parse(date))
+            val language = if (isLanguageEnglish) "en_US" else "fr_FR"
+            whenever(localeManagerWrapper.getLanguage()).thenReturn(language)
+        }
+    }
+
+    companion object {
+        private const val EXPECTED_URL = "https://wordpress.org/state-of-the-word/" +
+                "?utm_source=mobile&utm_medium=appnudge&utm_campaign=sotw2023"
+    }
+}