Plans on site domain display updated card

Fixes #19807, #19808, #19809

Updates card with `upgrade to a plan` CTA, besides existing `get a domain` CTA on domains dashboard for those on free plan .

| Before | After |
|--------|--------|
| ![Screenshot_20231219_193130](https://github.com/wordpress-mobile/WordPress-Android/assets/990349/1a127106-0ed5-495e-ba72-22b9e91294e8) |![Screenshot_20231220_112652](https://github.com/wordpress-mobile/WordPress-Android/assets/990349/63988d4a-defd-4d28-b6ca-953d2470b362) |

-----

## To Test:

Test 1
- Launch JP app
- `Select` a site with a free plan, have admin role
- `Go` to **Domains** (My site -> domains or My site -> More... -> domain)
- Verify a new card is displayed as in second image above
- `Tap` on **Upgrade to a plan**
- `Verify` it launches plans purchase flow like plans card on dashboard
- Return to domains
- `Tap` on **Just search for a domain**
- `Verify` it launches domain purchase flow as before

Test 2
- For completeness, also verify with a site that has a paid plan
- `Verify` it doesn't display the card above, but the card as before

Test 3
- `Verify` analytics tracker in AS like
🔵 Tracked: domains_dashboard_get_plan_tapped, Properties: {"blog_id":196875124,"site_type":"blog","is_jetpack":false}
-----

## Regression Notes

1. Potential unintended areas of impact

    - None

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

    - Manual, and unit tests

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

    - Updated unit tests

-----

## 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.

[Android] Display a card on Site domain

[Android] Display a card on Site domain
Apply same conditions for displaying plans card on My site dashboard

![Image](https://github.com/wordpress-mobile/WordPress-Android/assets/990349/056ba2b3-cdc4-4d11-8ac1-155374b7c8cb)

      Fails

      :no_entry_sign:

  Please add a type label to this issue. e.g. '[Type] Enhancement'

  Generated by :no_entry_sign: dangerJS

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 6fd3d4672b24..1b72edaa59ee 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -3,6 +3,7 @@
 24.0
 -----
 * [*] Filter media types when sharing files to the editor [https://github.com/wordpress-mobile/WordPress-Android/pull/19754]
+* [***] [Jetpack-only] Plans: Upgrade to a Plan from domains dashboard in Jetpack app [https://github.com/wordpress-mobile/WordPress-Android/pull/19818]
 * [**] [internal] Removes unused resources and code [https://github.com/wordpress-mobile/WordPress-Android/pull/19788]

 23.9
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardAdapter.kt
index a160888254d9..5ca9b2d869cb 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardAdapter.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardAdapter.kt
@@ -10,12 +10,14 @@ import org.wordpress.android.ui.domains.DomainsDashboardItem.ManageDomains
 import org.wordpress.android.ui.domains.DomainsDashboardItem.PurchaseDomain
 import org.wordpress.android.ui.domains.DomainsDashboardItem.SiteDomains
 import org.wordpress.android.ui.domains.DomainsDashboardItem.SiteDomainsHeader
+import org.wordpress.android.ui.domains.DomainsDashboardItem.PurchasePlan
 import org.wordpress.android.ui.domains.DomainsDashboardItem.Type
 import org.wordpress.android.ui.domains.DomainsDashboardItem.Type.ADD_DOMAIN
 import org.wordpress.android.ui.domains.DomainsDashboardItem.Type.DOMAIN_BLURB
 import org.wordpress.android.ui.domains.DomainsDashboardItem.Type.MANAGE_DOMAINS
 import org.wordpress.android.ui.domains.DomainsDashboardItem.Type.PRIMARY_DOMAIN
 import org.wordpress.android.ui.domains.DomainsDashboardItem.Type.PURCHASE_DOMAIN
+import org.wordpress.android.ui.domains.DomainsDashboardItem.Type.PURCHASE_PLAN
 import org.wordpress.android.ui.domains.DomainsDashboardItem.Type.SITE_DOMAINS
 import org.wordpress.android.ui.domains.DomainsDashboardItem.Type.SITE_DOMAINS_HEADER
 import org.wordpress.android.ui.domains.DomainsDashboardViewHolder.AddDomainViewHolder
@@ -25,6 +27,7 @@ import org.wordpress.android.ui.domains.DomainsDashboardViewHolder.ManageDomains
 import org.wordpress.android.ui.domains.DomainsDashboardViewHolder.PurchaseDomainViewHolder
 import org.wordpress.android.ui.domains.DomainsDashboardViewHolder.SiteDomainsHeaderViewHolder
 import org.wordpress.android.ui.domains.DomainsDashboardViewHolder.SiteDomainsViewHolder
+import org.wordpress.android.ui.domains.DomainsDashboardViewHolder.PurchasePlanViewHolder
 import org.wordpress.android.ui.utils.UiHelpers
 import javax.inject.Inject

@@ -40,6 +43,7 @@ class DomainsDashboardAdapter @Inject constructor(
             MANAGE_DOMAINS -> ManageDomainsViewHolder(parent)
             PURCHASE_DOMAIN -> PurchaseDomainViewHolder(parent, uiHelpers)
             DOMAIN_BLURB -> DomainBlurbViewHolder(parent, uiHelpers)
+            PURCHASE_PLAN -> PurchasePlanViewHolder(parent, uiHelpers)
         }
     }

@@ -53,6 +57,7 @@ class DomainsDashboardAdapter @Inject constructor(
             is ManageDomainsViewHolder -> holder.onBind(item as ManageDomains)
             is PurchaseDomainViewHolder -> holder.onBind(item as PurchaseDomain)
             is DomainBlurbViewHolder -> holder.onBind(item as DomainBlurb)
+            is PurchasePlanViewHolder -> holder.onBind(item as PurchasePlan)
         }
     }

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardFragment.kt
index ce46ee60a279..bb60d9f91380 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardFragment.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardFragment.kt
@@ -21,8 +21,10 @@ import org.wordpress.android.ui.ActivityLauncher
 import org.wordpress.android.ui.RequestCodes.DOMAIN_REGISTRATION
 import org.wordpress.android.ui.domains.DomainRegistrationActivity.DomainRegistrationPurpose.CTA_DOMAIN_CREDIT_REDEMPTION
 import org.wordpress.android.ui.domains.DomainRegistrationActivity.DomainRegistrationPurpose.DOMAIN_PURCHASE
+import org.wordpress.android.ui.domains.DomainRegistrationActivity.DomainRegistrationPurpose.FREE_DOMAIN_WITH_ANNUAL_PLAN
 import org.wordpress.android.ui.domains.DomainsDashboardNavigationAction.ClaimDomain
 import org.wordpress.android.ui.domains.DomainsDashboardNavigationAction.GetDomain
+import org.wordpress.android.ui.domains.DomainsDashboardNavigationAction.GetPlan
 import org.wordpress.android.ui.utils.UiHelpers
 import org.wordpress.android.util.config.DomainManagementFeatureConfig
 import org.wordpress.android.util.extensions.getSerializableExtraCompat
@@ -103,6 +105,11 @@ class DomainsDashboardFragment : Fragment(R.layout.domains_dashboard_fragment),
             action.site,
             CTA_DOMAIN_CREDIT_REDEMPTION
         )
+        is GetPlan -> ActivityLauncher.viewDomainRegistrationActivityForResult(
+            this,
+            action.site,
+            FREE_DOMAIN_WITH_ANNUAL_PLAN
+        )
     }

     @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardItem.kt
index 6bf01edec646..20fc8e283c66 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardItem.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardItem.kt
@@ -8,6 +8,7 @@ import org.wordpress.android.ui.domains.DomainsDashboardItem.Type.DOMAIN_BLURB
 import org.wordpress.android.ui.domains.DomainsDashboardItem.Type.MANAGE_DOMAINS
 import org.wordpress.android.ui.domains.DomainsDashboardItem.Type.PRIMARY_DOMAIN
 import org.wordpress.android.ui.domains.DomainsDashboardItem.Type.PURCHASE_DOMAIN
+import org.wordpress.android.ui.domains.DomainsDashboardItem.Type.PURCHASE_PLAN
 import org.wordpress.android.ui.domains.DomainsDashboardItem.Type.SITE_DOMAINS
 import org.wordpress.android.ui.domains.DomainsDashboardItem.Type.SITE_DOMAINS_HEADER
 import org.wordpress.android.ui.utils.ListItemInteraction
@@ -21,7 +22,8 @@ sealed class DomainsDashboardItem(val type: Type) {
         ADD_DOMAIN,
         MANAGE_DOMAINS,
         PURCHASE_DOMAIN,
-        DOMAIN_BLURB
+        DOMAIN_BLURB,
+        PURCHASE_PLAN
     }

     data class FreeDomain(
@@ -49,6 +51,14 @@ sealed class DomainsDashboardItem(val type: Type) {
         val onClick: ListItemInteraction
     ) : DomainsDashboardItem(PURCHASE_DOMAIN)

+    data class PurchasePlan(
+        @DrawableRes val image: Int?,
+        val title: UiString,
+        val body: UiString,
+        val onUpgradeClick: ListItemInteraction,
+        val onDomainClick: ListItemInteraction
+    ) : DomainsDashboardItem(PURCHASE_PLAN)
+
     data class DomainBlurb(val blurb: UiString) : DomainsDashboardItem(DOMAIN_BLURB)

     enum class Action(@IdRes val itemId: Int) {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardNavigationAction.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardNavigationAction.kt
index 733ceff0ebe9..60d32b8105d5 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardNavigationAction.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardNavigationAction.kt
@@ -5,4 +5,5 @@ import org.wordpress.android.fluxc.model.SiteModel
 sealed class DomainsDashboardNavigationAction {
     data class GetDomain(val site: SiteModel) : DomainsDashboardNavigationAction()
     data class ClaimDomain(val site: SiteModel) : DomainsDashboardNavigationAction()
+    data class GetPlan(val site: SiteModel) : DomainsDashboardNavigationAction()
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardViewHolder.kt
index 9aaeca1cf0b4..02e5db36518b 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardViewHolder.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardViewHolder.kt
@@ -10,6 +10,7 @@ import org.wordpress.android.R
 import org.wordpress.android.databinding.DomainAddDomainCtaBinding
 import org.wordpress.android.databinding.DomainFreeSiteAddressCardBinding
 import org.wordpress.android.databinding.DomainManageDomainsCtaBinding
+import org.wordpress.android.databinding.DomainPlanPurchaseCardBinding
 import org.wordpress.android.databinding.DomainPurchaseCardBinding
 import org.wordpress.android.databinding.DomainSiteDomainsBlurbBinding
 import org.wordpress.android.databinding.DomainSiteDomainsCardBinding
@@ -21,6 +22,7 @@ import org.wordpress.android.ui.domains.DomainsDashboardItem.ManageDomains
 import org.wordpress.android.ui.domains.DomainsDashboardItem.PurchaseDomain
 import org.wordpress.android.ui.domains.DomainsDashboardItem.SiteDomains
 import org.wordpress.android.ui.domains.DomainsDashboardItem.SiteDomainsHeader
+import org.wordpress.android.ui.domains.DomainsDashboardItem.PurchasePlan
 import org.wordpress.android.ui.utils.UiHelpers
 import org.wordpress.android.util.extensions.viewBinding

@@ -109,6 +111,21 @@ sealed class DomainsDashboardViewHolder<T : ViewBinding>(
         }
     }

+    class PurchasePlanViewHolder(
+        parent: ViewGroup,
+        private val uiHelpers: UiHelpers
+    ) : DomainsDashboardViewHolder<DomainPlanPurchaseCardBinding>(
+        parent.viewBinding(DomainPlanPurchaseCardBinding::inflate)
+    ) {
+        fun onBind(item: PurchasePlan) = with(binding) {
+            uiHelpers.setImageOrHide(purchasePlanImage, item.image)
+            uiHelpers.setTextOrHide(purchasePlanTitle, item.title)
+            uiHelpers.setTextOrHide(purchasePlanCaption, item.body)
+            upgradePlanButton.setOnClickListener { item.onUpgradeClick.click() }
+            justSearchDomainButton.setOnClickListener { item.onDomainClick.click() }
+        }
+    }
+
     class DomainBlurbViewHolder(
         parent: ViewGroup,
         private val uiHelpers: UiHelpers
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardViewModel.kt
index 2fb9f16c5e69..2f2305005aa9 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardViewModel.kt
@@ -7,6 +7,7 @@ import kotlinx.coroutines.async
 import org.wordpress.android.R
 import org.wordpress.android.analytics.AnalyticsTracker.Stat.DOMAINS_DASHBOARD_ADD_DOMAIN_TAPPED
 import org.wordpress.android.analytics.AnalyticsTracker.Stat.DOMAINS_DASHBOARD_GET_DOMAIN_TAPPED
+import org.wordpress.android.analytics.AnalyticsTracker.Stat.DOMAINS_DASHBOARD_GET_PLAN_TAPPED
 import org.wordpress.android.analytics.AnalyticsTracker.Stat.DOMAINS_DASHBOARD_VIEWED
 import org.wordpress.android.analytics.AnalyticsTracker.Stat.DOMAIN_CREDIT_REDEMPTION_TAPPED
 import org.wordpress.android.fluxc.model.PlanModel
@@ -22,8 +23,10 @@ import org.wordpress.android.ui.domains.DomainsDashboardItem.FreeDomain
 import org.wordpress.android.ui.domains.DomainsDashboardItem.PurchaseDomain
 import org.wordpress.android.ui.domains.DomainsDashboardItem.SiteDomains
 import org.wordpress.android.ui.domains.DomainsDashboardItem.SiteDomainsHeader
+import org.wordpress.android.ui.domains.DomainsDashboardItem.PurchasePlan
 import org.wordpress.android.ui.domains.DomainsDashboardNavigationAction.ClaimDomain
 import org.wordpress.android.ui.domains.DomainsDashboardNavigationAction.GetDomain
+import org.wordpress.android.ui.domains.DomainsDashboardNavigationAction.GetPlan
 import org.wordpress.android.ui.domains.usecases.FetchPlansUseCase
 import org.wordpress.android.ui.plans.isDomainCreditAvailable
 import org.wordpress.android.ui.utils.HtmlMessageUtils
@@ -156,15 +159,11 @@ class DomainsDashboardViewModel @Inject constructor(
                     ListItemInteraction.create(this::onGetDomainClick)
                 )
             } else {
-                PurchaseDomain(
-                    R.drawable.img_illustration_domains_card_header,
+                PurchasePlan(
+                    R.drawable.browser_address_bar,
                     UiStringRes(R.string.domains_free_plan_get_your_domain_title),
-                    UiStringText(
-                        htmlMessageUtils.getHtmlMessageFromStringFormatResId(
-                            R.string.domains_free_plan_get_your_domain_caption,
-                            freeDomainUrl
-                        )
-                    ),
+                    UiStringRes(R.string.domains_upgrade_to_plan_caption),
+                    ListItemInteraction.create(this::onGetPlanClick),
                     ListItemInteraction.create(this::onGetDomainClick)
                 )
             }
@@ -207,6 +206,11 @@ class DomainsDashboardViewModel @Inject constructor(
         _onNavigation.value = Event(GetDomain(site))
     }

+    private fun onGetPlanClick() {
+        analyticsTrackerWrapper.track(DOMAINS_DASHBOARD_GET_PLAN_TAPPED, site)
+        _onNavigation.value = Event(GetPlan(site))
+    }
+
     private fun onClaimDomainClick() {
         analyticsTrackerWrapper.track(DOMAIN_CREDIT_REDEMPTION_TAPPED, site)
         _onNavigation.value = Event(ClaimDomain(site))
diff --git a/WordPress/src/main/res/layout/domain_plan_purchase_card.xml b/WordPress/src/main/res/layout/domain_plan_purchase_card.xml
new file mode 100644
index 000000000000..7f1f651e50b2
--- /dev/null
+++ b/WordPress/src/main/res/layout/domain_plan_purchase_card.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/purchase_plan_card"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_margin="@dimen/margin_extra_large"
+    app:cardCornerRadius="@dimen/margin_medium_large"
+    app:cardElevation="0dp"
+    app:layout_constraintBottom_toBottomOf="parent"
+    app:layout_constraintEnd_toEndOf="parent"
+    app:layout_constraintStart_toStartOf="parent"
+    app:layout_constraintTop_toTopOf="parent"
+    app:strokeColor="@color/on_surface_disabled_primary_selector"
+    app:strokeWidth="1dp">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="@dimen/margin_extra_large">
+
+        <ImageView
+            android:id="@+id/purchase_plan_image"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:adjustViewBounds="true"
+            android:contentDescription="@null"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:src="@drawable/browser_address_bar" />
+
+        <com.google.android.material.textview.MaterialTextView
+            android:id="@+id/purchase_plan_title"
+            style="@style/DomainsPlanPurchaseCardTitle"
+            android:layout_marginTop="@dimen/domain_plan_card_title_top_margin"
+            android:text="@string/domains_free_plan_get_your_domain_title"
+            app:layout_constraintBottom_toTopOf="@id/purchase_plan_caption"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="@id/purchase_plan_image" />
+
+        <com.google.android.material.textview.MaterialTextView
+            android:id="@+id/purchase_plan_caption"
+            style="@style/DomainsPlanPurchaseCardSubTitle"
+            android:layout_marginTop="@dimen/margin_small"
+            android:text="@string/domains_upgrade_to_plan_caption"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/purchase_plan_title" />
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/upgrade_plan_button"
+            style="@style/DomainsPlanPurchaseCardPrimaryButton"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="@dimen/margin_medium_large"
+            android:text="@string/domains_upgrade_to_a_plan_button"
+            app:layout_constraintBottom_toTopOf="@id/just_search_domain_button"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/purchase_plan_caption" />
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/just_search_domain_button"
+            style="@style/DomainsPlanPurchaseCardSecondaryButton"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/domains_just_search_for_a_domain_button"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/upgrade_plan_button" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</com.google.android.material.card.MaterialCardView>
+
diff --git a/WordPress/src/main/res/values-land/dimens.xml b/WordPress/src/main/res/values-land/dimens.xml
index c32c067b9c5d..0a2ec43b918c 100644
--- a/WordPress/src/main/res/values-land/dimens.xml
+++ b/WordPress/src/main/res/values-land/dimens.xml
@@ -14,4 +14,6 @@
     <dimen name="dashboard_card_plans_top_margin">16dp</dimen>
     <dimen name="dashboard_card_plans_done_button_bottom_margin">12dp</dimen>

+    <dimen name="domain_plan_card_title_top_margin">160dp</dimen>
+
 </resources>
diff --git a/WordPress/src/main/res/values/dimens.xml b/WordPress/src/main/res/values/dimens.xml
index 68d8991ff905..c2d8a90dadce 100644
--- a/WordPress/src/main/res/values/dimens.xml
+++ b/WordPress/src/main/res/values/dimens.xml
@@ -571,6 +571,8 @@
     <dimen name="domain_suggestions_intro_header_bottom_padding">8dp</dimen>
     <dimen name="domain_suggestions_progress_indicator_size">24dp</dimen>

+    <dimen name="domain_plan_card_title_top_margin">78dp</dimen>
+
     <dimen name="keyline_second">72dp</dimen>

     <dimen name="toolbar_navigation_ripple_radius">20dp</dimen>
diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml
index b7c35003e809..b036fa7c7437 100644
--- a/WordPress/src/main/res/values/strings.xml
+++ b/WordPress/src/main/res/values/strings.xml
@@ -2709,6 +2709,9 @@
     <string name="domains_free_plan_get_your_domain_title">Get your domain</string>
     <string name="domains_free_plan_get_your_domain_caption">Domains purchased on this site will redirect visitors to <b>%s</b></string>
     <string name="domains_search_for_a_domain_button">Search for a domain</string>
+    <string name="domains_upgrade_to_plan_caption">Get a free one-year domain registration or transfer with any annual paid plan.</string>
+    <string name="domains_upgrade_to_a_plan_button">Upgrade to a plan</string>
+    <string name="domains_just_search_for_a_domain_button">Just search for a domain</string>

     <!-- Domain Management -->
     <string name="domain_management_expires">Expires %1$s</string>
diff --git a/WordPress/src/main/res/values/styles.xml b/WordPress/src/main/res/values/styles.xml
index 72fd2d6e1719..c02c05e96af6 100644
--- a/WordPress/src/main/res/values/styles.xml
+++ b/WordPress/src/main/res/values/styles.xml
@@ -1608,6 +1608,38 @@
         <item name="android:textAlignment">viewStart</item>
     </style>

+    <style name="DomainsPlanPurchaseCardTitle">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:textAlignment">center</item>
+        <item name="android:layout_gravity">center</item>
+        <item name="android:textAppearance">?attr/textAppearanceHeadline6</item>
+        <item name="android:textColor">?attr/colorOnSurface</item>
+    </style>
+
+    <style name="DomainsPlanPurchaseCardSubTitle">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:textAlignment">center</item>
+        <item name="android:layout_gravity">center</item>
+        <item name="android:textAppearance">?attr/textAppearanceBody2</item>
+        <item name="android:textColor">?attr/wpColorOnSurfaceMedium</item>
+    </style>
+
+    <style name="DomainsPlanPurchaseCardPrimaryButton" parent="WordPress.Button.Primary.Unelevated">
+        <item name="materialThemeOverlay">@style/ThemeOverlay.WordPress.Domains.Button.Primary.Unelevated</item>
+        <item name="cornerRadius">5dp</item>
+    </style>
+
+    <style name="ThemeOverlay.WordPress.Domains.Button.Primary.Unelevated" parent="">
+        <item name="colorPrimary">?attr/colorOnSurface</item>
+    </style>
+
+    <style name="DomainsPlanPurchaseCardSecondaryButton" parent="Widget.Material3.Button.TextButton">
+        <item name="android:textSize">@dimen/text_sz_medium</item>
+        <item name="cornerRadius">5dp</item>
+    </style>
+
     <!-- Blogging prompts onboarding dialog styles -->
     <style name="BloggingPromptsOnboardingTitle">
         <item name="android:textSize">@dimen/text_sz_double_extra_large</item>
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 56ff41c58fa8..77eec02ca812 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
@@ -718,6 +718,7 @@ public enum Stat {
         DOMAIN_CREDIT_NAME_SELECTED,
         DOMAINS_DASHBOARD_VIEWED,
         DOMAINS_DASHBOARD_GET_DOMAIN_TAPPED,
+        DOMAINS_DASHBOARD_GET_PLAN_TAPPED,
         DOMAINS_DASHBOARD_ADD_DOMAIN_TAPPED,
         DOMAINS_SEARCH_SELECT_DOMAIN_TAPPED,
         DOMAINS_REGISTRATION_FORM_VIEWED,
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 6be5f8310383..76e1853782a9 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
@@ -1929,6 +1929,8 @@ public static String getEventNameForStat(AnalyticsTracker.Stat stat) {
                 return "domains_dashboard_viewed";
             case DOMAINS_DASHBOARD_GET_DOMAIN_TAPPED:
                 return "domains_dashboard_get_domain_tapped";
+            case DOMAINS_DASHBOARD_GET_PLAN_TAPPED:
+                return "domains_dashboard_get_plan_tapped";
             case DOMAINS_DASHBOARD_ADD_DOMAIN_TAPPED:
                 return "domains_dashboard_add_domain_tapped";
             case DOMAINS_SEARCH_SELECT_DOMAIN_TAPPED:

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/domains/DomainsDashboardViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/domains/DomainsDashboardViewModelTest.kt
index 2f474eb5c510..e7dc96542b70 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/domains/DomainsDashboardViewModelTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/domains/DomainsDashboardViewModelTest.kt
@@ -4,8 +4,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.Before
 import org.junit.Test
-import org.mockito.kotlin.any
-import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 import org.wordpress.android.BaseUnitTest
@@ -20,6 +18,7 @@ import org.wordpress.android.ui.domains.DomainsDashboardItem.AddDomain
 import org.wordpress.android.ui.domains.DomainsDashboardItem.DomainBlurb
 import org.wordpress.android.ui.domains.DomainsDashboardItem.FreeDomain
 import org.wordpress.android.ui.domains.DomainsDashboardItem.PurchaseDomain
+import org.wordpress.android.ui.domains.DomainsDashboardItem.PurchasePlan
 import org.wordpress.android.ui.domains.DomainsDashboardItem.SiteDomains
 import org.wordpress.android.ui.domains.DomainsDashboardItem.SiteDomainsHeader
 import org.wordpress.android.ui.domains.usecases.FetchPlansUseCase
@@ -33,9 +32,7 @@ import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
 class DomainsDashboardViewModelTest : BaseUnitTest() {
     private val siteStore: SiteStore = mock()
     private val analyticsTracker: AnalyticsTrackerWrapper = mock()
-    private val htmlMessageUtils: HtmlMessageUtils = mock {
-        on { getHtmlMessageFromStringFormatResId(any(), any()) } doReturn ""
-    }
+    private val htmlMessageUtils: HtmlMessageUtils = mock()
     private val fetchPlansUseCase: FetchPlansUseCase = mock()

     private lateinit var viewModel: DomainsDashboardViewModel
@@ -87,8 +84,8 @@ class DomainsDashboardViewModelTest : BaseUnitTest() {
         assertThat(dashboardItems).hasSize(2)

         assertThat(dashboardItems[0]).isInstanceOf(FreeDomain::class.java)
-        assertThat(dashboardItems[1]).isInstanceOf(PurchaseDomain::class.java)
-        assertThat((dashboardItems[1] as PurchaseDomain).title)
+        assertThat(dashboardItems[1]).isInstanceOf(PurchasePlan::class.java)
+        assertThat((dashboardItems[1] as PurchasePlan).title)
             .isEqualTo(UiStringRes(R.string.domains_free_plan_get_your_domain_title))
     }


[Dynamic Cards] Hide dynamic card through a card menu

Fixes #19776

This PR includes a dynamic card's dropdown menu UI and local hiding dynamic cards logic.

![ezgif-2-d23e3d0a64](https://github.com/wordpress-mobile/WordPress-Android/assets/16563318/cc6ac271-4a52-4562-8737-ae547f1375ae)

-----

## To Test:

- Apply this patch: [cardmockspatch.patch](https://github.com/wordpress-mobile/WordPress-Android/files/13690106/cardmockspatch.patch) and run the app.
- Make sure you can see two dynamic cards on the top and bottom of the dashboard.
- Tap on a card's menu and select "Hide it" option.
- Make sure the card disappears permanently - it's not shown after restarting the app.

-----

## Regression Notes

1. Potential unintended areas of impact

    - My Site Dashboard screen and a dynamic dashboard card

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

    - Manual testing

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

    - N/A

-----

## 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)

🤖 [Dynamic Cards] Dismissing card from a dropdown menu
After tapping on the ••• menu, a dropdown menu should appear with a "hiding the card" option. After tapping on the card it has to be removed from the screen permanently.

![Image](https://github.com/wordpress-mobile/WordPress-Android/assets/16563318/260eec1f-03fd-491d-a39c-8a7349527f0e)

Hi,
I want to work on this issue.
> Hi,
> I want to work on this issue.

Thank you for offering to help on this @malirauf 🙇
Since this issue is part of a feature that is still work in progress it might be hard to coordinate on the implementation. Would you like to have a look at [an issue from this list ](https://github.com/wordpress-mobile/WordPress-Android/issues?q=is%3Aissue+is%3Aopen+label%3A%22Good+First+Issue%22)?

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 68af2a267ddf..1d6db4d0c792 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
@@ -250,6 +250,7 @@ class MySiteViewModel @Inject constructor(
             activityLogCardViewModelSlice.refresh,
             bloganuaryNudgeCardViewModelSlice.refresh,
             sotw2023NudgeCardViewModelSlice.refresh,
+            dynamicCardsViewModelSlice.refresh,
         )

     private var shouldMarkUpdateSiteTitleTaskComplete = false
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardComposable.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardComposable.kt
index ab9556785b06..3ed80d4e2c1b 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardComposable.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardComposable.kt
@@ -26,7 +26,7 @@ fun DynamicDashboardCard(
             },
         content = {
             Column(
-                Modifier.padding(top = 8.dp, bottom = 8.dp)
+                Modifier.padding(bottom = 8.dp)
             ) {
                 val isCtaInvisible = card.action !is ActionSource.Button
                 DynamicCardHeader(
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardHeader.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardHeader.kt
index f6dcbb711854..be2838e43c7d 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardHeader.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardHeader.kt
@@ -1,23 +1,16 @@
 package org.wordpress.android.ui.mysite.cards.dynamiccard

 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.ContentAlpha
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.MoreVert
-import androidx.compose.material.Icon
-import androidx.compose.material.IconButton
-import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import org.wordpress.android.R
 import org.wordpress.android.ui.compose.styles.DashboardCardTypography
+import org.wordpress.android.ui.mysite.cards.compose.MySiteCardToolbar
+import org.wordpress.android.ui.mysite.cards.compose.MySiteCardToolbarContextMenuItem

 @Composable
 fun DynamicCardHeader(
@@ -25,12 +18,16 @@ fun DynamicCardHeader(
     onHideMenuClicked: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
-    Row(
-        modifier = modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp),
-        verticalAlignment = Alignment.CenterVertically
+    MySiteCardToolbar(
+        modifier = modifier.padding(bottom = 8.dp),
+        contextMenuItems = listOf(
+            MySiteCardToolbarContextMenuItem.Option(
+                text = stringResource(R.string.my_site_dashboard_card_more_menu_hide_card),
+                onClick = onHideMenuClicked,
+            )
+        ),
     ) {
         Title(title = title, modifier = Modifier.weight(1f))
-        Menu(onHideMenuClicked)
     }
 }

@@ -45,17 +42,3 @@ private fun Title(title: String?, modifier: Modifier = Modifier) {
         }
     )
 }
-
-@Composable
-private fun Menu(onHideMenuClicked: () -> Unit) {
-    IconButton(
-        modifier = Modifier.size(32.dp),
-        onClick = onHideMenuClicked
-    ) {
-        Icon(
-            imageVector = Icons.Rounded.MoreVert,
-            contentDescription = stringResource(id = R.string.more),
-            tint = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium),
-        )
-    }
-}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsViewModelSlice.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsViewModelSlice.kt
index 8def20febd51..e1bff394e03a 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsViewModelSlice.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsViewModelSlice.kt
@@ -5,11 +5,14 @@ import androidx.lifecycle.MutableLiveData
 import org.wordpress.android.fluxc.model.dashboard.CardModel
 import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DynamicCardsBuilderParams
 import org.wordpress.android.ui.mysite.SiteNavigationAction
+import org.wordpress.android.ui.prefs.AppPrefsWrapper
 import org.wordpress.android.util.AppLog
 import org.wordpress.android.viewmodel.Event
 import javax.inject.Inject

-class DynamicCardsViewModelSlice @Inject constructor() {
+class DynamicCardsViewModelSlice @Inject constructor(
+    private val appPrefsWrapper: AppPrefsWrapper
+) {
     private val _onNavigation = MutableLiveData<Event<SiteNavigationAction>>()
     val onNavigation = _onNavigation as LiveData<Event<SiteNavigationAction>>

@@ -17,7 +20,7 @@ class DynamicCardsViewModelSlice @Inject constructor() {
     val refresh = _refresh as LiveData<Event<Boolean>>
     fun getBuilderParams(dynamicCards: CardModel.DynamicCardsModel?): DynamicCardsBuilderParams {
         return DynamicCardsBuilderParams(
-            dynamicCards = dynamicCards,
+            dynamicCards = dynamicCards?.filterVisible(),
             onActionClick = this::onActionClick,
             onMoreMenuClick = this::onMoreMenuClick,
             onHideMenuItemClick = this::onHideMenuItemClick
@@ -35,7 +38,10 @@ class DynamicCardsViewModelSlice @Inject constructor() {
     }

     private fun onHideMenuItemClick(cardId: String) {
-        AppLog.d(AppLog.T.MY_SITE_DASHBOARD, "Dynamic dashboard card action hide menu click: $cardId")
-        // TODO
+        appPrefsWrapper.setShouldHideDynamicCard(cardId, true)
+        _refresh.value = Event(true)
     }
+
+    private fun CardModel.DynamicCardsModel.filterVisible(): CardModel.DynamicCardsModel =
+        copy(dynamicCards = dynamicCards.filterNot { appPrefsWrapper.getShouldHideDynamicCard(it.id) })
 }
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 f66e0418093e..a9d199817ab2 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
@@ -210,6 +210,7 @@ public enum DeletablePrefKey implements PrefKey {
         SHOULD_SHOW_DEFAULT_QUICK_LINK_IN_DASHBOARD,
         SHOULD_HIDE_BLOGANUARY_NUDGE_CARD,
         SHOULD_HIDE_SOTW2023_NUDGE_CARD,
+        SHOULD_HIDE_DYNAMIC_CARD,
     }

     /**
@@ -1836,4 +1837,12 @@ public static void setShouldHideSotw2023NudgeCard(boolean isHidden) {
     public static boolean getShouldHideSotw2023NudgeCard() {
         return prefs().getBoolean(DeletablePrefKey.SHOULD_HIDE_SOTW2023_NUDGE_CARD.name(), false);
     }
+
+    public static void setShouldHideDynamicCard(@NonNull final String id, final boolean isHidden) {
+        prefs().edit().putBoolean(DeletablePrefKey.SHOULD_HIDE_DYNAMIC_CARD.name() + id, isHidden).apply();
+    }
+
+    public static boolean getShouldHideDynamicCard(@NonNull final String id) {
+        return prefs().getBoolean(DeletablePrefKey.SHOULD_HIDE_DYNAMIC_CARD.name() + id, 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 307e6772e1ff..10b8ef20cd04 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
@@ -434,6 +434,12 @@ class AppPrefsWrapper @Inject constructor() {
     fun getShouldHideSotw2023NudgeCard(): Boolean =
         AppPrefs.getShouldHideSotw2023NudgeCard()

+    fun setShouldHideDynamicCard(id: String, isHidden: Boolean): Unit =
+        AppPrefs.setShouldHideDynamicCard(id, isHidden)
+
+    fun getShouldHideDynamicCard(id: String, ): Boolean =
+        AppPrefs.getShouldHideDynamicCard(id)
+
     fun getAllPrefs(): Map<String, Any?> = AppPrefs.getAllPrefs()

     fun setString(prefKey: PrefKey, value: String) {

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsViewModelSliceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsViewModelSliceTest.kt
index 2a0628a5429b..2528fad23c30 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsViewModelSliceTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsViewModelSliceTest.kt
@@ -4,13 +4,19 @@ 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.verify
+import org.mockito.kotlin.whenever
 import org.wordpress.android.BaseUnitTest
 import org.wordpress.android.fluxc.model.dashboard.CardModel
-import org.wordpress.android.fluxc.model.dashboard.CardModel.DynamicCardsModel.DynamicCardRowModel
 import org.wordpress.android.fluxc.model.dashboard.CardModel.DynamicCardsModel.DynamicCardModel
+import org.wordpress.android.fluxc.model.dashboard.CardModel.DynamicCardsModel.DynamicCardRowModel
+import org.wordpress.android.ui.prefs.AppPrefsWrapper

 /* DYNAMIC CARDS */
-private const val DYNAMIC_CARD_ID = "year_in_review_2023"
+private const val DYNAMIC_CARD_ID_FIRST = "year_in_review_2023"
+private const val DYNAMIC_CARD_ID_SECOND = "domain_management"
 private const val DYNAMIC_CARD_TITLE = "News"
 private const val DYNAMIC_CARD_REMOTE_FEATURE_FLAG = "dynamic_dashboard_cards"
 private const val DYNAMIC_CARD_FEATURED_IMAGE = "https://path/to/image"
@@ -21,39 +27,72 @@ private const val DYNAMIC_CARD_ROW_ICON = "https://path/to/image"
 private const val DYNAMIC_CARD_ROW_TITLE = "Row title"
 private const val DYNAMIC_CARD_ROW_DESCRIPTION = "Row description"

-private val DYNAMIC_CARD_ROW_MODEL = DynamicCardRowModel(
+private val dynamicCardRowModel = DynamicCardRowModel(
     icon = DYNAMIC_CARD_ROW_ICON,
     title = DYNAMIC_CARD_ROW_TITLE,
-    description = DYNAMIC_CARD_ROW_DESCRIPTION
+    description = DYNAMIC_CARD_ROW_DESCRIPTION,
 )

-private val DYNAMIC_CARD_MODEL = DynamicCardModel(
-    id = DYNAMIC_CARD_ID,
+private val firstDynamicCardModel = DynamicCardModel(
+    id = DYNAMIC_CARD_ID_FIRST,
     title = DYNAMIC_CARD_TITLE,
     remoteFeatureFlag = DYNAMIC_CARD_REMOTE_FEATURE_FLAG,
     featuredImage = DYNAMIC_CARD_FEATURED_IMAGE,
     url = DYNAMIC_CARD_URL,
     action = DYNAMIC_CARD_ACTION,
     order = CardModel.DynamicCardsModel.CardOrder.fromString(DYNAMIC_CARD_ORDER),
-    rows = listOf(DYNAMIC_CARD_ROW_MODEL)
+    rows = listOf(dynamicCardRowModel),
+)
+
+private val secondDynamicCardModel = DynamicCardModel(
+    id = DYNAMIC_CARD_ID_SECOND,
+    title = null,
+    remoteFeatureFlag = DYNAMIC_CARD_REMOTE_FEATURE_FLAG,
+    featuredImage = null,
+    url = null,
+    action = null,
+    order = CardModel.DynamicCardsModel.CardOrder.fromString(DYNAMIC_CARD_ORDER),
+    rows = emptyList(),
+)
+
+private val dynamicCardsModel = CardModel.DynamicCardsModel(
+    dynamicCards = listOf(firstDynamicCardModel, secondDynamicCardModel),
 )

-private val DYNAMIC_CARDS_MODEL = CardModel.DynamicCardsModel(
-    dynamicCards = listOf(DYNAMIC_CARD_MODEL)
+private val filteredDynamicCardsModel = CardModel.DynamicCardsModel(
+    dynamicCards = listOf(firstDynamicCardModel),
 )

 @OptIn(ExperimentalCoroutinesApi::class)
 class DynamicCardsViewModelSliceTest : BaseUnitTest() {
-    lateinit var dynamicCardsViewModelSlice: DynamicCardsViewModelSlice
+    @Mock
+    private lateinit var appPrefsWrapper: AppPrefsWrapper
+    private lateinit var viewModelSlice: DynamicCardsViewModelSlice

     @Before
     fun setUp() {
-        dynamicCardsViewModelSlice = DynamicCardsViewModelSlice()
+        MockitoAnnotations.openMocks(this)
+        viewModelSlice = DynamicCardsViewModelSlice(appPrefsWrapper)
     }

     @Test
-    fun testGetBuilderParams() {
-        val builderParams = dynamicCardsViewModelSlice.getBuilderParams(DYNAMIC_CARDS_MODEL)
-        assertThat(builderParams.dynamicCards).isEqualTo(DYNAMIC_CARDS_MODEL)
+    fun `WHEN get builder params THEN return only not hidden cards only`() {
+        whenever(appPrefsWrapper.getShouldHideDynamicCard(DYNAMIC_CARD_ID_FIRST)).thenReturn(false)
+        whenever(appPrefsWrapper.getShouldHideDynamicCard(DYNAMIC_CARD_ID_SECOND)).thenReturn(true)
+
+        val builderParams = viewModelSlice.getBuilderParams(dynamicCardsModel)
+        assertThat(builderParams.dynamicCards).isEqualTo(filteredDynamicCardsModel)
+    }
+
+    @Test
+    fun `WHEN card hide menu item clicked THEN hide card locally and refresh`() {
+        whenever(appPrefsWrapper.getShouldHideDynamicCard(DYNAMIC_CARD_ID_FIRST)).thenReturn(false)
+        whenever(appPrefsWrapper.getShouldHideDynamicCard(DYNAMIC_CARD_ID_SECOND)).thenReturn(true)
+
+        val builderParams = viewModelSlice.getBuilderParams(dynamicCardsModel)
+        builderParams.onHideMenuItemClick.invoke(DYNAMIC_CARD_ID_FIRST)
+
+        verify(appPrefsWrapper).setShouldHideDynamicCard(DYNAMIC_CARD_ID_FIRST, true)
+        assertThat(viewModelSlice.refresh.value?.peekContent()).isTrue
     }
 }


Fixes #19797

## Description
This PR checks the remote feature flag key of each card returned from the endpoint and:
* If the key exists in the remote feature flags endpoint response, the remote flag value controls the card visibility
* If the key doesn’t exist, it means there are no restrictions, and the card is shown

-----

## To Test:
- Apply the [remotecards.patch](https://github.com/wordpress-mobile/WordPress-Android/files/13703174/remotecards.patch) and run the app.
- **Verify** that the cards with the following titles are visible:
  - `Enabled remote feature flag`
  - `Non existent remote feature flag`
  - `Empty feature flag`
 - **Verify** that the card with title `Disabled by remote feature flag` is not visible

https://github.com/wordpress-mobile/WordPress-Android/assets/304044/a297d83f-c43b-416f-b78b-6f8298792a5a

-----

## Regression Notes

1. Potential unintended areas of impact

    My Site Dashboard screen and a dynamic dashboard card

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

    Manual testing

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

    Added new test cases in the `DynamicCardsBuilderTest`

-----

## 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)

🤖 [Dynamic Cards] Implement card display logic according to the remote feature flags
When parsing the remote cards data, we will check the value of the remote-feature-flag against the remote feature flags endpoint response. If the key exists in the remote feature flags endpoint response, we will respect the value returned. If the key doesn’t exist, it means there are no restrictions, and we’ll show the card to all users. To control a certain card, we’ll add a corresponding remote feature flag similar to how we treat normal feature flags. The flag’s key will match the value of remote-feature-flag outlined in the cards data.

ref pc8HXX-1nA-p2

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSource.kt
index 9339b1d9abe8..33b3aa292daa 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSource.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSource.kt
@@ -118,7 +118,7 @@ class CardsSource @Inject constructor(
     }

     private fun shouldRequestDynamicCards(): Boolean {
-        return dynamicDashboardCardsFeatureConfig.isEnabled() // TODO We should also add check for hidden cards here
+        return dynamicDashboardCardsFeatureConfig.isEnabled()
     }

     private fun MediatorLiveData<CardsUpdate>.postErrorState() {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsBuilder.kt
index 14bf2e8c55a9..403248cd48aa 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsBuilder.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsBuilder.kt
@@ -1,6 +1,7 @@
 package org.wordpress.android.ui.mysite.cards.dynamiccard

 import org.wordpress.android.fluxc.model.dashboard.CardModel
+import org.wordpress.android.fluxc.model.dashboard.CardModel.DynamicCardsModel.DynamicCardModel
 import org.wordpress.android.fluxc.model.dashboard.CardModel.DynamicCardsModel.CardOrder
 import org.wordpress.android.ui.deeplinks.handlers.DeepLinkHandlers
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.Dynamic
@@ -8,26 +9,29 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DynamicCar
 import org.wordpress.android.ui.utils.ListItemInteraction.Companion.create
 import org.wordpress.android.util.UriWrapper
 import org.wordpress.android.util.UrlUtilsWrapper
+import org.wordpress.android.util.config.DynamicDashboardCardsFeatureConfig
+import org.wordpress.android.util.config.FeatureFlagConfig
 import javax.inject.Inject

 class DynamicCardsBuilder @Inject constructor(
     private val urlUtils: UrlUtilsWrapper,
-    private val deepLinkHandlers: DeepLinkHandlers
+    private val deepLinkHandlers: DeepLinkHandlers,
+    private val dynamicDashboardCardsFeatureConfig: DynamicDashboardCardsFeatureConfig,
+    private val featureFlagConfig: FeatureFlagConfig,
 ) {
     fun build(params: DynamicCardsBuilderParams, order: CardOrder): List<Dynamic>? {
-        if (!shouldBuildCard(params, order)) {
+        if (!dynamicDashboardCardsFeatureConfig.isEnabled() || !shouldBuildCard(params, order)) {
             return null
         }
         return convertToDynamicCards(params, order)
     }

     private fun shouldBuildCard(params: DynamicCardsBuilderParams, order: CardOrder): Boolean {
-        if (params.dynamicCards == null || params.dynamicCards.dynamicCards.none { it.order == order }) return false
-        return true
+        return !(params.dynamicCards == null || params.dynamicCards.dynamicCards.none { it.order == order })
     }

     private fun convertToDynamicCards(params: DynamicCardsBuilderParams, order: CardOrder): List<Dynamic> {
-        val cards = params.dynamicCards?.dynamicCards?.filter { it.order == order } ?: emptyList()
+        val cards = params.dynamicCards?.dynamicCards?.filter { it.order == order && it.isEnabled()}.orEmpty()
         return cards.map { card ->
             Dynamic(
                 id = card.id,
@@ -46,6 +50,14 @@ class DynamicCardsBuilder @Inject constructor(
         }
     }

+    fun DynamicCardModel.isEnabled(): Boolean {
+        // If there is no feature flag or there is no such remote feature flag, then the card is enabled
+        if (remoteFeatureFlag.isNullOrEmpty() ||
+            featureFlagConfig.getString(requireNotNull(remoteFeatureFlag)).isEmpty()
+        ) return true
+        return featureFlagConfig.isEnabled(requireNotNull(remoteFeatureFlag))
+    }
+
     private fun getActionSource(
         params: DynamicCardsBuilderParams,
         card: CardModel.DynamicCardsModel.DynamicCardModel

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsBuilderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsBuilderTest.kt
index 301a867dabb3..f6e664ba74b3 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsBuilderTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsBuilderTest.kt
@@ -13,12 +13,13 @@ import org.wordpress.android.ui.deeplinks.handlers.DeepLinkHandlers
 import org.wordpress.android.ui.mysite.MySiteCardAndItem
 import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams
 import org.wordpress.android.util.UrlUtilsWrapper
+import org.wordpress.android.util.config.DynamicDashboardCardsFeatureConfig
+import org.wordpress.android.util.config.FeatureFlagConfig
 import kotlin.test.assertEquals

 /* DYNAMIC CARDS */
 private const val DYNAMIC_CARD_ID = "year_in_review_2023"
 private const val DYNAMIC_CARD_TITLE = "News"
-private const val DYNAMIC_CARD_REMOTE_FEATURE_FLAG = "dynamic_dashboard_cards"
 private const val DYNAMIC_CARD_FEATURED_IMAGE = "https://path/to/image"
 private const val DYNAMIC_CARD_URL = "https://wordpress.com"
 private const val DYNAMIC_CARD_ACTION = "Call to action"
@@ -27,6 +28,10 @@ private const val DYNAMIC_CARD_ROW_ICON = "https://path/to/image"
 private const val DYNAMIC_CARD_ROW_TITLE = "Row title"
 private const val DYNAMIC_CARD_ROW_DESCRIPTION = "Row description"

+private const val ENABLED_REMOTE_FEATURE_FLAG = "enabled_remote_feature_flag"
+private const val DISABLED_REMOTE_FEATURE_FLAG = "disabled_remote_feature_flag"
+private const val UNKNOWN_REMOTE_FEATURE_FLAG = "unknown_remote_feature_flag"
+
 private val DYNAMIC_CARD_ROW_MODEL = CardModel.DynamicCardsModel.DynamicCardRowModel(
     icon = DYNAMIC_CARD_ROW_ICON,
     title = DYNAMIC_CARD_ROW_TITLE,
@@ -36,7 +41,7 @@ private val DYNAMIC_CARD_ROW_MODEL = CardModel.DynamicCardsModel.DynamicCardRowM
 private val DYNAMIC_CARD_MODEL = CardModel.DynamicCardsModel.DynamicCardModel(
     id = DYNAMIC_CARD_ID,
     title = DYNAMIC_CARD_TITLE,
-    remoteFeatureFlag = DYNAMIC_CARD_REMOTE_FEATURE_FLAG,
+    remoteFeatureFlag = ENABLED_REMOTE_FEATURE_FLAG,
     featuredImage = DYNAMIC_CARD_FEATURED_IMAGE,
     url = DYNAMIC_CARD_URL,
     action = DYNAMIC_CARD_ACTION,
@@ -47,7 +52,7 @@ private val DYNAMIC_CARD_MODEL = CardModel.DynamicCardsModel.DynamicCardModel(
 private val DYNAMIC_CARD_MODEL_INVALID_ACTION_TITLE = CardModel.DynamicCardsModel.DynamicCardModel(
     id = DYNAMIC_CARD_ID,
     title = DYNAMIC_CARD_TITLE,
-    remoteFeatureFlag = DYNAMIC_CARD_REMOTE_FEATURE_FLAG,
+    remoteFeatureFlag = ENABLED_REMOTE_FEATURE_FLAG,
     featuredImage = DYNAMIC_CARD_FEATURED_IMAGE,
     url = DYNAMIC_CARD_URL,
     action = null,
@@ -58,7 +63,7 @@ private val DYNAMIC_CARD_MODEL_INVALID_ACTION_TITLE = CardModel.DynamicCardsMode
 private val DYNAMIC_CARD_MODEL_INVALID_ACTION_TITLE_AND_URL = CardModel.DynamicCardsModel.DynamicCardModel(
     id = DYNAMIC_CARD_ID,
     title = DYNAMIC_CARD_TITLE,
-    remoteFeatureFlag = DYNAMIC_CARD_REMOTE_FEATURE_FLAG,
+    remoteFeatureFlag = ENABLED_REMOTE_FEATURE_FLAG,
     featuredImage = DYNAMIC_CARD_FEATURED_IMAGE,
     url = "",
     action = null,
@@ -69,7 +74,7 @@ private val DYNAMIC_CARD_MODEL_INVALID_ACTION_TITLE_AND_URL = CardModel.DynamicC
 private val DYNAMIC_CARD_MODEL_INVALID_URL = CardModel.DynamicCardsModel.DynamicCardModel(
     id = DYNAMIC_CARD_ID,
     title = DYNAMIC_CARD_TITLE,
-    remoteFeatureFlag = DYNAMIC_CARD_REMOTE_FEATURE_FLAG,
+    remoteFeatureFlag = ENABLED_REMOTE_FEATURE_FLAG,
     featuredImage = DYNAMIC_CARD_FEATURED_IMAGE,
     url = null,
     action = DYNAMIC_CARD_ACTION,
@@ -77,6 +82,39 @@ private val DYNAMIC_CARD_MODEL_INVALID_URL = CardModel.DynamicCardsModel.Dynamic
     rows = listOf(DYNAMIC_CARD_ROW_MODEL)
 )

+private val DYNAMIC_CARD_MODEL_DISABLED_REMOTELY = CardModel.DynamicCardsModel.DynamicCardModel(
+    id = DYNAMIC_CARD_ID,
+    title = DYNAMIC_CARD_TITLE,
+    remoteFeatureFlag = DISABLED_REMOTE_FEATURE_FLAG,
+    featuredImage = DYNAMIC_CARD_FEATURED_IMAGE,
+    url = DYNAMIC_CARD_URL,
+    action = DYNAMIC_CARD_ACTION,
+    order = CardModel.DynamicCardsModel.CardOrder.fromString(DYNAMIC_CARD_ORDER),
+    rows = listOf(DYNAMIC_CARD_ROW_MODEL)
+)
+
+private val DYNAMIC_CARD_MODEL_WITH_EMPTY_REMOTE_FLAG = CardModel.DynamicCardsModel.DynamicCardModel(
+    id = DYNAMIC_CARD_ID,
+    title = DYNAMIC_CARD_TITLE,
+    remoteFeatureFlag = "",
+    featuredImage = DYNAMIC_CARD_FEATURED_IMAGE,
+    url = DYNAMIC_CARD_URL,
+    action = DYNAMIC_CARD_ACTION,
+    order = CardModel.DynamicCardsModel.CardOrder.fromString(DYNAMIC_CARD_ORDER),
+    rows = listOf(DYNAMIC_CARD_ROW_MODEL)
+)
+
+private val DYNAMIC_CARD_MODEL_WITH_UNKNOWN_REMOTE_FLAG = CardModel.DynamicCardsModel.DynamicCardModel(
+    id = DYNAMIC_CARD_ID,
+    title = DYNAMIC_CARD_TITLE,
+    remoteFeatureFlag = UNKNOWN_REMOTE_FEATURE_FLAG,
+    featuredImage = DYNAMIC_CARD_FEATURED_IMAGE,
+    url = DYNAMIC_CARD_URL,
+    action = DYNAMIC_CARD_ACTION,
+    order = CardModel.DynamicCardsModel.CardOrder.fromString(DYNAMIC_CARD_ORDER),
+    rows = listOf(DYNAMIC_CARD_ROW_MODEL)
+)
+
 @ExperimentalCoroutinesApi
 class DynamicCardsBuilderTest : BaseUnitTest() {
     private lateinit var dynamicCardsBuilder: DynamicCardsBuilder
@@ -87,14 +125,27 @@ class DynamicCardsBuilderTest : BaseUnitTest() {
     @Mock
     private lateinit var deepLinkHandlers: DeepLinkHandlers

+    @Mock
+    private lateinit var dynamicDashboardCardsFeatureConfig: DynamicDashboardCardsFeatureConfig
+
+    @Mock
+    private lateinit var featureFlagConfig: FeatureFlagConfig
+
     @Before
     fun setUp() {
-        dynamicCardsBuilder = DynamicCardsBuilder(urlUtils, deepLinkHandlers)
+        dynamicCardsBuilder =
+            DynamicCardsBuilder(urlUtils, deepLinkHandlers, dynamicDashboardCardsFeatureConfig, featureFlagConfig)
         whenever(urlUtils.isValidUrlAndHostNotNull(DYNAMIC_CARD_URL)).thenReturn(true)
+        whenever(dynamicDashboardCardsFeatureConfig.isEnabled()).thenReturn(true)
+        whenever(featureFlagConfig.isEnabled(ENABLED_REMOTE_FEATURE_FLAG)).thenReturn(true)
+        whenever(featureFlagConfig.getString(ENABLED_REMOTE_FEATURE_FLAG)).thenReturn(ENABLED_REMOTE_FEATURE_FLAG)
+        whenever(featureFlagConfig.isEnabled(DISABLED_REMOTE_FEATURE_FLAG)).thenReturn(false)
+        whenever(featureFlagConfig.getString(DISABLED_REMOTE_FEATURE_FLAG)).thenReturn(DISABLED_REMOTE_FEATURE_FLAG)
+        whenever(featureFlagConfig.getString(UNKNOWN_REMOTE_FEATURE_FLAG)).thenReturn("")
     }

     @Test
-    fun testBuild() {
+    fun `WHEN the parameters contain an eligible card THEN all the card fields are copied to the card ui`() {
         val expectedCards = listOf(
             MySiteCardAndItem.Card.Dynamic(
                 id = DYNAMIC_CARD_ID,
@@ -124,11 +175,11 @@ class DynamicCardsBuilderTest : BaseUnitTest() {
             onHideMenuItemClick = mock(),
         )
         val dynamicCards = dynamicCardsBuilder.build(builderParams, CardModel.DynamicCardsModel.CardOrder.TOP)
-        assertThat(requireNotNull(dynamicCards).size).isEqualTo(1)
+        assertEquals(requireNotNull(dynamicCards).size, 1)
         assertEquals(expectedCards[0].id, dynamicCards[0].id)
         assertEquals(expectedCards[0].title, dynamicCards[0].title)
         assertEquals(expectedCards[0].image, dynamicCards[0].image)
-        assertThat(expectedCards[0].rows.size).isEqualTo(1)
+        assertEquals(expectedCards[0].rows.size, 1)
         assertEquals(expectedCards[0].rows, dynamicCards[0].rows)
         assertThat(dynamicCards[0].action).isInstanceOf(MySiteCardAndItem.Card.Dynamic.ActionSource.Button::class.java)
         val expected = expectedCards[0].action as? MySiteCardAndItem.Card.Dynamic.ActionSource.Button
@@ -138,7 +189,7 @@ class DynamicCardsBuilderTest : BaseUnitTest() {
     }

     @Test
-    fun testBuildWithInvalidActionTitle() {
+    fun `WHEN the card has invalid action title THEN the card has no action button but is actionable`() {
         val builderParams = MySiteCardAndItemBuilderParams.DynamicCardsBuilderParams(
             dynamicCards = CardModel.DynamicCardsModel(
                 dynamicCards = listOf(DYNAMIC_CARD_MODEL_INVALID_ACTION_TITLE)
@@ -155,7 +206,7 @@ class DynamicCardsBuilderTest : BaseUnitTest() {
     }

     @Test
-    fun testBuildWithInvalidActionTitleAndUrl() {
+    fun `WHEN the card has invalid action title and url THEN the card is not actionable`() {
         val builderParams = MySiteCardAndItemBuilderParams.DynamicCardsBuilderParams(
             dynamicCards = CardModel.DynamicCardsModel(
                 dynamicCards = listOf(DYNAMIC_CARD_MODEL_INVALID_ACTION_TITLE_AND_URL)
@@ -170,7 +221,7 @@ class DynamicCardsBuilderTest : BaseUnitTest() {
     }

     @Test
-    fun testBuildWithInvalidUrl() {
+    fun `WHEN the card has invalid url THEN the card is not actionable`() {
         val builderParams = MySiteCardAndItemBuilderParams.DynamicCardsBuilderParams(
             dynamicCards = CardModel.DynamicCardsModel(
                 dynamicCards = listOf(DYNAMIC_CARD_MODEL_INVALID_URL)
@@ -185,10 +236,10 @@ class DynamicCardsBuilderTest : BaseUnitTest() {
     }

     @Test
-    fun testBuildWithEmptyPosition() {
+    fun `WHEN there are no cards for the requested order position THEN no cards are shown`() {
         val builderParams = MySiteCardAndItemBuilderParams.DynamicCardsBuilderParams(
             dynamicCards = CardModel.DynamicCardsModel(
-                dynamicCards = listOf(DYNAMIC_CARD_MODEL_INVALID_ACTION_TITLE_AND_URL)
+                dynamicCards = listOf(DYNAMIC_CARD_MODEL)
             ),
             onActionClick = mock(),
             onMoreMenuClick = mock(),
@@ -199,7 +250,7 @@ class DynamicCardsBuilderTest : BaseUnitTest() {
     }

     @Test
-    fun testBuildWithInvalidParams() {
+    fun `WHEN the dynamic cards are null THEN then no dynamic cards are shown`() {
         val builderParams = MySiteCardAndItemBuilderParams.DynamicCardsBuilderParams(
             dynamicCards = null,
             onActionClick = mock(),
@@ -209,4 +260,46 @@ class DynamicCardsBuilderTest : BaseUnitTest() {
         val dynamicCards = dynamicCardsBuilder.build(builderParams, CardModel.DynamicCardsModel.CardOrder.TOP)
         assertThat(dynamicCards).isNull()
     }
+
+    @Test
+    fun `WHEN the card remote feature flag is disabled THEN the card is not visible`() {
+        val builderParams = MySiteCardAndItemBuilderParams.DynamicCardsBuilderParams(
+            dynamicCards = CardModel.DynamicCardsModel(
+                dynamicCards = listOf(DYNAMIC_CARD_MODEL_DISABLED_REMOTELY)
+            ),
+            onActionClick = mock(),
+            onMoreMenuClick = mock(),
+            onHideMenuItemClick = mock(),
+        )
+        val dynamicCards = dynamicCardsBuilder.build(builderParams, CardModel.DynamicCardsModel.CardOrder.TOP)
+        assertThat(requireNotNull(dynamicCards)).isEmpty()
+    }
+
+    @Test
+    fun `WHEN the card remote feature flag is empty THEN the card is visible`() {
+        val builderParams = MySiteCardAndItemBuilderParams.DynamicCardsBuilderParams(
+            dynamicCards = CardModel.DynamicCardsModel(
+                dynamicCards = listOf(DYNAMIC_CARD_MODEL_WITH_EMPTY_REMOTE_FLAG)
+            ),
+            onActionClick = mock(),
+            onMoreMenuClick = mock(),
+            onHideMenuItemClick = mock(),
+        )
+        val dynamicCards = dynamicCardsBuilder.build(builderParams, CardModel.DynamicCardsModel.CardOrder.TOP)
+        assertEquals(requireNotNull(dynamicCards).size, 1)
+    }
+
+    @Test
+    fun `WHEN the card remote feature flag does not exist THEN the card is visible`() {
+        val builderParams = MySiteCardAndItemBuilderParams.DynamicCardsBuilderParams(
+            dynamicCards = CardModel.DynamicCardsModel(
+                dynamicCards = listOf(DYNAMIC_CARD_MODEL_WITH_UNKNOWN_REMOTE_FLAG)
+            ),
+            onActionClick = mock(),
+            onMoreMenuClick = mock(),
+            onHideMenuItemClick = mock(),
+        )
+        val dynamicCards = dynamicCardsBuilder.build(builderParams, CardModel.DynamicCardsModel.CardOrder.TOP)
+        assertEquals(requireNotNull(dynamicCards).size, 1)
+    }
 }