Ask for battery optimizations at most three times.

Fixes #1801

Dismissing battery optimization toast permanently
Version 7.0.6

Hello,

My phone uses the AOSP implementation of battery optimization which respects apps just fine, I don't need to disable it. Voice is kept alive as long as it's playing.
Please allow me to keep it from prompting me every I press play.

Your app is lovely, thank you.

./gradlew :playbackScreen:testDebugUnitTest --tests "voice.playbackScreen.batteryOptimization.BatteryOptimizationTest"

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 77e470e25a..6022cdd8bb 100755
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -134,6 +134,7 @@ android {

 dependencies {
   implementation(projects.strings)
+  implementation(projects.datastore)
   implementation(projects.common)
   implementation(projects.data)
   implementation(projects.playback)
diff --git a/app/src/main/kotlin/voice/app/injection/PrefsModule.kt b/app/src/main/kotlin/voice/app/injection/PrefsModule.kt
index 8817c1a590..4d648335e4 100644
--- a/app/src/main/kotlin/voice/app/injection/PrefsModule.kt
+++ b/app/src/main/kotlin/voice/app/injection/PrefsModule.kt
@@ -17,7 +17,6 @@ import kotlinx.serialization.builtins.ListSerializer
 import kotlinx.serialization.builtins.nullable
 import kotlinx.serialization.builtins.serializer
 import voice.app.BuildConfig
-import voice.app.serialization.SerializableDataStoreFactory
 import voice.app.serialization.UriSerializer
 import voice.bookOverview.BookMigrationExplanationQualifier
 import voice.bookOverview.BookMigrationExplanationShown
@@ -29,6 +28,7 @@ import voice.common.pref.PrefKeys
 import voice.common.pref.RootAudiobookFolders
 import voice.common.pref.SingleFileAudiobookFolders
 import voice.common.pref.SingleFolderAudiobookFolders
+import voice.datastore.VoiceDataStoreFactory
 import javax.inject.Named
 import javax.inject.Singleton

@@ -100,28 +100,28 @@ object PrefsModule {
   @Provides
   @Singleton
   @RootAudiobookFolders
-  fun audiobookFolders(factory: SerializableDataStoreFactory): DataStore<List<Uri>> {
+  fun audiobookFolders(factory: VoiceDataStoreFactory): DataStore<List<Uri>> {
     return factory.createUriList("audiobookFolders")
   }

   @Provides
   @Singleton
   @SingleFolderAudiobookFolders
-  fun singleFolderAudiobookFolders(factory: SerializableDataStoreFactory): DataStore<List<Uri>> {
+  fun singleFolderAudiobookFolders(factory: VoiceDataStoreFactory): DataStore<List<Uri>> {
     return factory.createUriList("SingleFolderAudiobookFolders")
   }

   @Provides
   @Singleton
   @SingleFileAudiobookFolders
-  fun singleFileAudiobookFolders(factory: SerializableDataStoreFactory): DataStore<List<Uri>> {
+  fun singleFileAudiobookFolders(factory: VoiceDataStoreFactory): DataStore<List<Uri>> {
     return factory.createUriList("SingleFileAudiobookFolders")
   }

   @Provides
   @Singleton
   @CurrentBook
-  fun currentBook(factory: SerializableDataStoreFactory): DataStore<BookId?> {
+  fun currentBook(factory: VoiceDataStoreFactory): DataStore<BookId?> {
     return factory.create(
       serializer = BookId.serializer().nullable,
       fileName = "currentBook",
@@ -132,12 +132,12 @@ object PrefsModule {
   @Provides
   @Singleton
   @BookMigrationExplanationQualifier
-  fun bookMigrationExplanationShown(factory: SerializableDataStoreFactory): BookMigrationExplanationShown {
+  fun bookMigrationExplanationShown(factory: VoiceDataStoreFactory): BookMigrationExplanationShown {
     return factory.create(Boolean.serializer(), false, "bookMigrationExplanationShown2")
   }
 }

-private fun SerializableDataStoreFactory.createUriList(
+private fun VoiceDataStoreFactory.createUriList(
   name: String,
 ): DataStore<List<Uri>> = create(
   serializer = ListSerializer(UriSerializer),
diff --git a/datastore/build.gradle.kts b/datastore/build.gradle.kts
new file mode 100644
index 0000000000..82bf9def88
--- /dev/null
+++ b/datastore/build.gradle.kts
@@ -0,0 +1,14 @@
+plugins {
+  id("voice.library")
+  alias(libs.plugins.anvil)
+}
+
+anvil {
+  generateDaggerFactories.set(true)
+}
+
+dependencies {
+  implementation(libs.dagger.core)
+  api(libs.serialization.json)
+  api(libs.datastore)
+}
diff --git a/app/src/main/kotlin/voice/app/serialization/KotlinxDataStoreSerializer.kt b/datastore/src/main/kotlin/voice/datastore/KotlinxDataStoreSerializer.kt
similarity index 95%
rename from app/src/main/kotlin/voice/app/serialization/KotlinxDataStoreSerializer.kt
rename to datastore/src/main/kotlin/voice/datastore/KotlinxDataStoreSerializer.kt
index 07e3649d08..bae6571116 100644
--- a/app/src/main/kotlin/voice/app/serialization/KotlinxDataStoreSerializer.kt
+++ b/datastore/src/main/kotlin/voice/datastore/KotlinxDataStoreSerializer.kt
@@ -1,4 +1,4 @@
-package voice.app.serialization
+package voice.datastore

 import androidx.datastore.core.Serializer
 import kotlinx.serialization.ExperimentalSerializationApi
diff --git a/app/src/main/kotlin/voice/app/serialization/SerializableDataStoreFactory.kt b/datastore/src/main/kotlin/voice/datastore/VoiceDataStoreFactory.kt
similarity index 69%
rename from app/src/main/kotlin/voice/app/serialization/SerializableDataStoreFactory.kt
rename to datastore/src/main/kotlin/voice/datastore/VoiceDataStoreFactory.kt
index 9d4ba38b59..d55e7a8526 100644
--- a/app/src/main/kotlin/voice/app/serialization/SerializableDataStoreFactory.kt
+++ b/datastore/src/main/kotlin/voice/datastore/VoiceDataStoreFactory.kt
@@ -1,14 +1,15 @@
-package voice.app.serialization
+package voice.datastore

 import android.content.Context
 import androidx.datastore.core.DataStore
 import androidx.datastore.core.DataStoreFactory
 import androidx.datastore.dataStoreFile
 import kotlinx.serialization.KSerializer
+import kotlinx.serialization.builtins.serializer
 import kotlinx.serialization.json.Json
 import javax.inject.Inject

-class SerializableDataStoreFactory
+class VoiceDataStoreFactory
 @Inject constructor(
   private val json: Json,
   private val context: Context,
@@ -29,4 +30,15 @@ class SerializableDataStoreFactory
       context.dataStoreFile(fileName)
     }
   }
+
+  fun int(
+    fileName: String,
+    defaultValue: Int,
+  ): DataStore<Int> {
+    return create(
+      serializer = Int.serializer(),
+      defaultValue = defaultValue,
+      fileName = fileName,
+    )
+  }
 }
diff --git a/playbackScreen/build.gradle.kts b/playbackScreen/build.gradle.kts
index a9711c47d1..535a76d5e1 100644
--- a/playbackScreen/build.gradle.kts
+++ b/playbackScreen/build.gradle.kts
@@ -19,6 +19,7 @@ dependencies {
   implementation(projects.strings)
   implementation(projects.playback)
   implementation(projects.data)
+  implementation(projects.datastore)
   implementation(projects.sleepTimer)

   implementation(libs.datastore)
diff --git a/playbackScreen/src/main/kotlin/voice/playbackScreen/BatteryOptimization.kt b/playbackScreen/src/main/kotlin/voice/playbackScreen/BatteryOptimization.kt
deleted file mode 100644
index 468f6777ae..0000000000
--- a/playbackScreen/src/main/kotlin/voice/playbackScreen/BatteryOptimization.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package voice.playbackScreen
-
-import android.app.Application
-import android.os.PowerManager
-import androidx.core.content.getSystemService
-import javax.inject.Inject
-
-class BatteryOptimization
-@Inject constructor(
-  private val context: Application,
-) {
-
-  fun isIgnoringBatteryOptimizations(): Boolean {
-    val powerManager = context.getSystemService<PowerManager>()
-      ?: return true
-    return powerManager.isIgnoringBatteryOptimizations(context.packageName)
-  }
-}
diff --git a/playbackScreen/src/main/kotlin/voice/playbackScreen/BookPlayViewModel.kt b/playbackScreen/src/main/kotlin/voice/playbackScreen/BookPlayViewModel.kt
index b50d7ec082..ef03e1e9d7 100644
--- a/playbackScreen/src/main/kotlin/voice/playbackScreen/BookPlayViewModel.kt
+++ b/playbackScreen/src/main/kotlin/voice/playbackScreen/BookPlayViewModel.kt
@@ -22,6 +22,7 @@ import voice.playback.PlayerController
 import voice.playback.misc.Decibel
 import voice.playback.misc.VolumeGain
 import voice.playback.playstate.PlayStateManager
+import voice.playbackScreen.batteryOptimization.BatteryOptimization
 import voice.sleepTimer.SleepTimer
 import javax.inject.Inject
 import kotlin.time.Duration
@@ -41,8 +42,6 @@ class BookPlayViewModel
   private val batteryOptimization: BatteryOptimization,
 ) {

-  private var askedToIgnoreBatteryOptimization = false
-
   private val scope = MainScope()

   private val _viewEffects = MutableSharedFlow<BookPlayViewEffect>(extraBufferCapacity = 1)
@@ -102,11 +101,13 @@ class BookPlayViewModel
   }

   fun playPause() {
-    if (playStateManager.playState != PlayStateManager.PlayState.Playing && !askedToIgnoreBatteryOptimization) {
-      if (!batteryOptimization.isIgnoringBatteryOptimizations()) {
-        _viewEffects.tryEmit(BookPlayViewEffect.RequestIgnoreBatteryOptimization)
+    if (playStateManager.playState != PlayStateManager.PlayState.Playing) {
+      scope.launch {
+        if (batteryOptimization.shouldRequest()) {
+          _viewEffects.tryEmit(BookPlayViewEffect.RequestIgnoreBatteryOptimization)
+          batteryOptimization.onBatteryOptimizationsRequested()
+        }
       }
-      askedToIgnoreBatteryOptimization = true
     }
     player.playPause()
   }
diff --git a/playbackScreen/src/main/kotlin/voice/playbackScreen/batteryOptimization/BatteryOptimization.kt b/playbackScreen/src/main/kotlin/voice/playbackScreen/batteryOptimization/BatteryOptimization.kt
new file mode 100644
index 0000000000..e9629de45d
--- /dev/null
+++ b/playbackScreen/src/main/kotlin/voice/playbackScreen/batteryOptimization/BatteryOptimization.kt
@@ -0,0 +1,23 @@
+package voice.playbackScreen.batteryOptimization
+
+import android.os.Build
+import androidx.datastore.core.DataStore
+import kotlinx.coroutines.flow.first
+import javax.inject.Inject
+
+class BatteryOptimization
+@Inject constructor(
+  private val isIgnoringBatteryOptimizations: IsIgnoringBatteryOptimizations,
+  private val amountOfBatteryOptimizationsRequested: DataStore<Int>,
+) {
+
+  suspend fun shouldRequest(): Boolean = when {
+    isIgnoringBatteryOptimizations() -> false
+    Build.MANUFACTURER.equals("Google", ignoreCase = true) -> false
+    else -> amountOfBatteryOptimizationsRequested.data.first() <= 3
+  }
+
+  suspend fun onBatteryOptimizationsRequested() {
+    amountOfBatteryOptimizationsRequested.updateData { it + 1 }
+  }
+}
diff --git a/playbackScreen/src/main/kotlin/voice/playbackScreen/batteryOptimization/BatteryOptimizationModule.kt b/playbackScreen/src/main/kotlin/voice/playbackScreen/batteryOptimization/BatteryOptimizationModule.kt
new file mode 100644
index 0000000000..91328d97b5
--- /dev/null
+++ b/playbackScreen/src/main/kotlin/voice/playbackScreen/batteryOptimization/BatteryOptimizationModule.kt
@@ -0,0 +1,20 @@
+package voice.playbackScreen.batteryOptimization
+
+import androidx.datastore.core.DataStore
+import com.squareup.anvil.annotations.ContributesTo
+import dagger.Module
+import dagger.Provides
+import voice.common.AppScope
+import voice.datastore.VoiceDataStoreFactory
+import javax.inject.Singleton
+
+@Module
+@ContributesTo(AppScope::class)
+object BatteryOptimizationModule {
+
+  @Provides
+  @Singleton
+  fun amountOfBatteryOptimizationsRequestedStore(factory: VoiceDataStoreFactory): DataStore<Int> {
+    return factory.int("amountOfBatteryOptimizationsRequestedStore", 0)
+  }
+}
diff --git a/playbackScreen/src/main/kotlin/voice/playbackScreen/batteryOptimization/IsIgnoringBatteryOptimizations.kt b/playbackScreen/src/main/kotlin/voice/playbackScreen/batteryOptimization/IsIgnoringBatteryOptimizations.kt
new file mode 100644
index 0000000000..1d7fa5ba47
--- /dev/null
+++ b/playbackScreen/src/main/kotlin/voice/playbackScreen/batteryOptimization/IsIgnoringBatteryOptimizations.kt
@@ -0,0 +1,24 @@
+package voice.playbackScreen.batteryOptimization
+
+import android.app.Application
+import android.os.PowerManager
+import androidx.core.content.getSystemService
+import com.squareup.anvil.annotations.ContributesBinding
+import voice.common.AppScope
+import javax.inject.Inject
+
+fun interface IsIgnoringBatteryOptimizations {
+  operator fun invoke(): Boolean
+}
+
+@ContributesBinding(AppScope::class)
+class IsIgnoringBatteryOptimizationsImpl
+@Inject constructor(
+  private val context: Application,
+) : IsIgnoringBatteryOptimizations {
+  override fun invoke(): Boolean {
+    val powerManager = context.getSystemService<PowerManager>()
+      ?: return true
+    return powerManager.isIgnoringBatteryOptimizations(context.packageName)
+  }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 30111f0bc0..91459e2594 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -54,6 +54,7 @@ include(":sleepTimer")
 include(":settings")
 include(":search")
 include(":cover")
+include(":datastore")
 include(":folderPicker")
 include(":bookOverview")
 include(":migration")

diff --git a/playbackScreen/src/test/kotlin/voice/playbackScreen/batteryOptimization/BatteryOptimizationTest.kt b/playbackScreen/src/test/kotlin/voice/playbackScreen/batteryOptimization/BatteryOptimizationTest.kt
new file mode 100644
index 0000000000..b9024d911a
--- /dev/null
+++ b/playbackScreen/src/test/kotlin/voice/playbackScreen/batteryOptimization/BatteryOptimizationTest.kt
@@ -0,0 +1,56 @@
+package voice.playbackScreen.batteryOptimization
+
+import androidx.datastore.core.DataStore
+import io.kotest.matchers.booleans.shouldBeFalse
+import io.kotest.matchers.booleans.shouldBeTrue
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.updateAndGet
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class BatteryOptimizationTest {
+
+  @Test
+  fun `requests at most three times`() = runTest {
+    val batteryOptimization = BatteryOptimization(
+      isIgnoringBatteryOptimizations = { false },
+      amountOfBatteryOptimizationsRequested = MemoryDataStore(0),
+    )
+
+    batteryOptimization.shouldRequest().shouldBeTrue()
+
+    batteryOptimization.onBatteryOptimizationsRequested()
+    batteryOptimization.shouldRequest().shouldBeTrue()
+
+    batteryOptimization.onBatteryOptimizationsRequested()
+    batteryOptimization.shouldRequest().shouldBeTrue()
+
+    batteryOptimization.onBatteryOptimizationsRequested()
+    batteryOptimization.shouldRequest().shouldBeTrue()
+
+    batteryOptimization.onBatteryOptimizationsRequested()
+    batteryOptimization.shouldRequest().shouldBeFalse()
+  }
+
+  @Test
+  fun `does not request when optimizations are ignored`() = runTest {
+    val batteryOptimization = BatteryOptimization(
+      isIgnoringBatteryOptimizations = { true },
+      amountOfBatteryOptimizationsRequested = MemoryDataStore(0),
+    )
+
+    batteryOptimization.shouldRequest().shouldBeFalse()
+  }
+}
+
+private class MemoryDataStore<T>(initial: T) : DataStore<T> {
+
+  private val value = MutableStateFlow(initial)
+
+  override val data: Flow<T> get() = value
+
+  override suspend fun updateData(transform: suspend (t: T) -> T): T {
+    return value.updateAndGet { transform(it) }
+  }
+}


Fixes #20908

- Implement `reader_floating_button` remote feature flag
- Show the "create" FAB in Reader based on the state of the `reader_floating_button` feature flag
- Add analytics (see #20908 for details)
- Move some FAB/Sheet related code to `MainCreateSheetHelper` for better organization
- Some minor refactor

-----

## To Test:

### Showing FAB on Reader page
1. Open Jetpack
2. Go to `Debug Settings`
3. **Disable** the `reader_floating_buitton` remote flag
4. Go to Reader
5. **Verify** the FAB does not appear on Reader
6. Go to `Debug Settings`
7. **Enable** the `reader_floating_button` remote flag
8. Go to Reader
9. **Verify** the FAB appears on Reader

### Analytics
1. Open Jetpack
2. Go to `Debug Settings`
3. **Enable** the `reader_floating_button` remote flag
4. Go to Reader
5. **Verify** tracked `reader_create_fab_shown`
6. Tap the FAB
7. **Verify** tracked `reader_create_sheet_shown`
8. Tap the "?" button in the Prompts item (if a prompt is available for the tested site)
9. **Verify** tracked `reader_create_sheet_prompt_help_tapped`
10. Go back
11. Tap `Answer prompt` (if a prompt is available for the tested site)
12. **Verify** tracked `reader_create_sheet_answer_prompt_tapped`
13. Go back to Reader
14. Tap the FAB
15. Tap `Blog post`
16. **Verify** tracked `reader_create_sheet_action_tapped, Properties: {"action":"create_new_post"}`

### Sanity
Please also test the FAB works normally on "My Site" page as expected, and the FAB is not shown in other pages, nor on the Reader when the `reader_floating_button` is disabled.

-----

## Regression Notes

1. Potential unintended areas of impact

    - FAB behavior in other pages

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

    - Manual Tests
    - Unit tests in the Activity ViewModel

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

    - Updated Activity ViewModel tests
    - Added unit tests to the `MainCreateSheetHelper`

-----

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

-----

## Testing Checklist (strike-out the not-applying and unnecessary ones):

- [ ] WordPress.com sites and self-hosted Jetpack sites.
- [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)
- [ ] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)

[Reader] Show "Create Post" FAB in Reader
As an initial experiment towards the Quick Post in Reader feature we want to enable the existing "create content" FAB (from the My Site page) in the Reader, but only showing "Create Post" actions in the bottom sheet.

| Button | Sheet |
|--------|--------|
| ![Image](https://github.com/wordpress-mobile/WordPress-Android/assets/5091503/95a0139b-5a98-465b-b5ba-585a4d105367) | ![Image](https://github.com/wordpress-mobile/WordPress-Android/assets/5091503/0d857da0-9eee-4738-914a-7242b0bb709e) |

### Feature Flag

We want to target just a small amount of users with this initial feature, since we want to gauge its usage to decide on the best implementation for Quick Post in Reader, so this feature will be behind a remote feature flag that we will control the rollout percentage remotely.

The feature flag name is: `reader_floating_button`

### Analytics

To properly measure and analyze the usage and impact of this, we will set some specific events, similar to the existing ones for the FAB and Creation Sheet:
- `reader_create_sheet_shown` after tapping FAB
- `reader_create_sheet_action_tapped` when tapping a sheet item
   - `action`: `create_new_post` and `answer_blogging_prompt`
- `reader_create_sheet_answer_prompt_tapped` when tapping Answer Prompt
- `reader_create_sheet_prompt_help_tapped`  when tapping the “?” icon in the Prompts item
- `reader_create_fab_shown` when the fab is shown in the Reader page
- `my_site_create_fab_shown` when the fab is shown in the My Site page

./gradlew :WordPress:testWordpressVanillaDebugUnitTest --tests "org.wordpress.android.ui.main.analytics.MainCreateSheetTrackerTest" --tests "org.wordpress.android.ui.main.utils.MainCreateSheetHelperTest" --tests "org.wordpress.android.viewmodel.main.WPMainActivityViewModelTest"

diff --git a/WordPress/build.gradle b/WordPress/build.gradle
index ffb8fa889646..04dd425afdf4 100644
--- a/WordPress/build.gradle
+++ b/WordPress/build.gradle
@@ -154,6 +154,7 @@ android {
         buildConfigField "boolean", "READER_TAGS_FEED", "false"
         buildConfigField "boolean", "READER_ANNOUNCEMENT_CARD", "false"
         buildConfigField "boolean", "VOICE_TO_CONTENT", "false"
+        buildConfigField "boolean", "READER_FLOATING_BUTTON", "false"

         // Override these constants in jetpack product flavor to enable/ disable features
         buildConfigField "boolean", "ENABLE_SITE_CREATION", "true"
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java
index 8b4281d9cb94..934d6ba346b9 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java
@@ -760,14 +760,17 @@ private void initViewModel() {
                     .show(getSupportFragmentManager(), FeatureAnnouncementDialogFragment.TAG);
         });

-        mFloatingActionButton.setOnClickListener(v -> mViewModel.onFabClicked(getSelectedSite()));
+        mFloatingActionButton.setOnClickListener(v -> {
+            PageType selectedPage = getSelectedPage();
+            if (selectedPage != null) mViewModel.onFabClicked(getSelectedSite(), selectedPage);
+        });

         mFloatingActionButton.setOnLongClickListener(v -> {
             if (v.isHapticFeedbackEnabled()) {
                 v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
             }

-            int messageId = mViewModel.getCreateContentMessageId(getSelectedSite());
+            int messageId = mViewModel.getCreateContentMessageId(getSelectedSite(), getSelectedPage());

             Toast.makeText(v.getContext(), messageId, Toast.LENGTH_SHORT).show();
             return true;
@@ -844,7 +847,7 @@ private void initViewModel() {
         // initialized with the most restrictive rights case. This is OK and will be frequently checked
         // to normalize the UI state whenever mSelectedSite changes.
         // It also means that the ViewModel must accept a nullable SiteModel.
-        mViewModel.start(getSelectedSite());
+        mViewModel.start(getSelectedSite(), mBottomNav.getCurrentSelectedPage());
     }

     private void triggerCreatePageFlow(ActionType actionType) {
@@ -1204,8 +1207,8 @@ protected void onResume() {

         mViewModel.onResume(
                 getSelectedSite(),
-                mSelectedSiteRepository.hasSelectedSite() && mBottomNav != null
-                && mBottomNav.getCurrentSelectedPage() == PageType.MY_SITE
+                mSelectedSiteRepository.hasSelectedSite(),
+                getSelectedPage()
         );

         checkForInAppUpdate();
@@ -1291,8 +1294,9 @@ public void onPageChanged(int position) {
         }

         mViewModel.onPageChanged(
-                mSiteStore.hasSite() && pageType == PageType.MY_SITE,
-                getSelectedSite()
+                getSelectedSite(),
+                mSiteStore.hasSite(),
+                pageType
         );
     }

@@ -2007,4 +2011,9 @@ private void showOpenPageMessageIfNeeded() {
             }
         }
     }
+
+    @Nullable
+    private PageType getSelectedPage() {
+        return mBottomNav != null ? mBottomNav.getCurrentSelectedPage() : null;
+    }
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/analytics/MainCreateSheetTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/analytics/MainCreateSheetTracker.kt
new file mode 100644
index 000000000000..05d6e57d6c4c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/analytics/MainCreateSheetTracker.kt
@@ -0,0 +1,66 @@
+package org.wordpress.android.ui.main.analytics
+
+import org.wordpress.android.analytics.AnalyticsTracker
+import org.wordpress.android.ui.main.MainActionListItem
+import org.wordpress.android.ui.main.WPMainNavigationView
+import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptAttribution
+import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
+import java.util.Locale
+import javax.inject.Inject
+
+class MainCreateSheetTracker @Inject constructor(
+    private val analyticsTracker: AnalyticsTrackerWrapper,
+) {
+    fun trackActionTapped(page: WPMainNavigationView.PageType, actionType: MainActionListItem.ActionType) {
+        val stat = when (page) {
+            WPMainNavigationView.PageType.MY_SITE -> AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_ACTION_TAPPED
+            WPMainNavigationView.PageType.READER -> AnalyticsTracker.Stat.READER_CREATE_SHEET_ACTION_TAPPED
+            else -> return
+        }
+        val properties = mapOf("action" to actionType.name.lowercase(Locale.ROOT))
+        analyticsTracker.track(stat, properties)
+    }
+
+    fun trackAnswerPromptActionTapped(page: WPMainNavigationView.PageType, attribution: BloggingPromptAttribution) {
+        val properties = mapOf("attribution" to attribution.value).filterValues { it.isNotBlank() }
+        val stat = when (page) {
+            WPMainNavigationView.PageType.MY_SITE -> AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_ANSWER_PROMPT_TAPPED
+            WPMainNavigationView.PageType.READER -> AnalyticsTracker.Stat.READER_CREATE_SHEET_ANSWER_PROMPT_TAPPED
+            else -> return
+        }
+        analyticsTracker.track(stat, properties)
+    }
+
+    fun trackHelpPromptActionTapped(page: WPMainNavigationView.PageType) {
+        val stat = when (page) {
+            WPMainNavigationView.PageType.MY_SITE -> AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_PROMPT_HELP_TAPPED
+            WPMainNavigationView.PageType.READER -> AnalyticsTracker.Stat.READER_CREATE_SHEET_PROMPT_HELP_TAPPED
+            else -> return
+        }
+        analyticsTracker.track(stat)
+    }
+
+    fun trackSheetShown(page: WPMainNavigationView.PageType) {
+        val stat = when (page) {
+            WPMainNavigationView.PageType.MY_SITE -> AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_SHOWN
+            WPMainNavigationView.PageType.READER -> AnalyticsTracker.Stat.READER_CREATE_SHEET_SHOWN
+            else -> return
+        }
+        analyticsTracker.track(stat)
+    }
+
+    fun trackFabShown(page: WPMainNavigationView.PageType) {
+        val stat = when (page) {
+            WPMainNavigationView.PageType.MY_SITE -> AnalyticsTracker.Stat.MY_SITE_CREATE_FAB_SHOWN
+            WPMainNavigationView.PageType.READER -> AnalyticsTracker.Stat.READER_CREATE_FAB_SHOWN
+            else -> return
+        }
+        analyticsTracker.track(stat)
+    }
+
+    fun trackCreateActionsSheetCard(actions: List<MainActionListItem>) {
+        if (actions.any { it is MainActionListItem.AnswerBloggingPromptAction }) {
+            analyticsTracker.track(AnalyticsTracker.Stat.BLOGGING_PROMPTS_CREATE_SHEET_CARD_VIEWED)
+        }
+    }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/utils/MainCreateSheetHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/utils/MainCreateSheetHelper.kt
new file mode 100644
index 000000000000..7255a908245a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/utils/MainCreateSheetHelper.kt
@@ -0,0 +1,37 @@
+package org.wordpress.android.ui.main.utils
+
+import org.wordpress.android.fluxc.model.SiteModel
+import org.wordpress.android.ui.bloggingprompts.BloggingPromptsSettingsHelper
+import org.wordpress.android.ui.main.WPMainNavigationView.PageType
+import org.wordpress.android.ui.voicetocontent.VoiceToContentFeatureUtils
+import org.wordpress.android.util.BuildConfigWrapper
+import org.wordpress.android.util.SiteUtilsWrapper
+import org.wordpress.android.util.config.ReaderFloatingButtonFeatureConfig
+import javax.inject.Inject
+
+class MainCreateSheetHelper @Inject constructor(
+    private val voiceToContentFeatureUtils: VoiceToContentFeatureUtils,
+    private val readerFloatingButtonFeatureConfig: ReaderFloatingButtonFeatureConfig,
+    private val bloggingPromptsSettingsHelper: BloggingPromptsSettingsHelper,
+    private val buildConfig: BuildConfigWrapper,
+    private val siteUtils: SiteUtilsWrapper,
+) {
+    fun shouldShowFabForPage(page: PageType?): Boolean {
+        val enabledForPage = page == PageType.MY_SITE ||
+                (page == PageType.READER && readerFloatingButtonFeatureConfig.isEnabled())
+        return buildConfig.isCreateFabEnabled && enabledForPage
+    }
+
+    @Suppress("FunctionOnlyReturningConstant")
+    fun canCreatePost(): Boolean = true // for completeness
+
+    fun canCreatePage(site: SiteModel?, page: PageType?): Boolean {
+        return siteUtils.hasFullAccessToContent(site) && page == PageType.MY_SITE
+    }
+
+    fun canCreatePostFromAudio(site: SiteModel?): Boolean {
+        return voiceToContentFeatureUtils.isVoiceToContentEnabled() && siteUtils.hasFullAccessToContent(site)
+    }
+
+    suspend fun canCreatePromptAnswer(): Boolean = bloggingPromptsSettingsHelper.shouldShowPromptsFeature()
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/util/SiteUtilsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/util/SiteUtilsWrapper.kt
index 5a4df6403e6e..a44a13192627 100644
--- a/WordPress/src/main/java/org/wordpress/android/util/SiteUtilsWrapper.kt
+++ b/WordPress/src/main/java/org/wordpress/android/util/SiteUtilsWrapper.kt
@@ -27,4 +27,5 @@ class SiteUtilsWrapper @Inject constructor(private val appContext: Context) {
     fun getSiteIconUrlOfResourceSize(site: SiteModel, @DimenRes sizeRes: Int): String {
         return SiteUtils.getSiteIconUrl(site, appContext.resources.getDimensionPixelSize(sizeRes))
     }
+    fun hasFullAccessToContent(site: SiteModel?): Boolean = SiteUtils.hasFullAccessToContent(site)
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/ReaderFloatingButtonFeatureConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/ReaderFloatingButtonFeatureConfig.kt
new file mode 100644
index 000000000000..c04c1ddb287e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/config/ReaderFloatingButtonFeatureConfig.kt
@@ -0,0 +1,16 @@
+package org.wordpress.android.util.config
+
+import org.wordpress.android.BuildConfig
+import org.wordpress.android.annotation.Feature
+import javax.inject.Inject
+
+private const val READER_FLOATING_BUTTON_REMOTE_FIELD = "reader_floating_button"
+
+@Feature(READER_FLOATING_BUTTON_REMOTE_FIELD, false)
+class ReaderFloatingButtonFeatureConfig @Inject constructor(
+    appConfig: AppConfig
+) : FeatureConfig(
+    appConfig,
+    BuildConfig.READER_FLOATING_BUTTON,
+    READER_FLOATING_BUTTON_REMOTE_FIELD
+)
diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt
index 8c278ea462a5..cb36800bbedd 100644
--- a/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt
@@ -18,7 +18,6 @@ import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask
 import org.wordpress.android.fluxc.store.SiteStore
 import org.wordpress.android.fluxc.store.bloggingprompts.BloggingPromptsStore
 import org.wordpress.android.modules.UI_THREAD
-import org.wordpress.android.ui.bloggingprompts.BloggingPromptsSettingsHelper
 import org.wordpress.android.ui.debug.preferences.DebugPrefs
 import org.wordpress.android.ui.main.MainActionListItem
 import org.wordpress.android.ui.main.MainActionListItem.ActionType
@@ -30,27 +29,28 @@ import org.wordpress.android.ui.main.MainActionListItem.ActionType.NO_ACTION
 import org.wordpress.android.ui.main.MainActionListItem.AnswerBloggingPromptAction
 import org.wordpress.android.ui.main.MainActionListItem.CreateAction
 import org.wordpress.android.ui.main.MainFabUiState
+import org.wordpress.android.ui.main.WPMainNavigationView.PageType
+import org.wordpress.android.ui.main.analytics.MainCreateSheetTracker
+import org.wordpress.android.ui.main.utils.MainCreateSheetHelper
 import org.wordpress.android.ui.mysite.SelectedSiteRepository
 import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptAttribution
 import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository
 import org.wordpress.android.ui.prefs.AppPrefsWrapper
 import org.wordpress.android.ui.prefs.privacy.banner.domain.ShouldAskPrivacyConsent
 import org.wordpress.android.ui.utils.UiString.UiStringText
-import org.wordpress.android.ui.voicetocontent.VoiceToContentFeatureUtils
 import org.wordpress.android.ui.whatsnew.FeatureAnnouncementProvider
 import org.wordpress.android.util.BuildConfigWrapper
 import org.wordpress.android.util.FluxCUtils
 import org.wordpress.android.util.SiteUtils.hasFullAccessToContent
 import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
-import org.wordpress.android.util.mapSafe
 import org.wordpress.android.util.mapNullable
+import org.wordpress.android.util.mapSafe
 import org.wordpress.android.util.merge
 import org.wordpress.android.viewmodel.Event
 import org.wordpress.android.viewmodel.ScopedViewModel
 import org.wordpress.android.viewmodel.SingleLiveEvent
 import java.io.Serializable
 import java.util.Date
-import java.util.Locale
 import javax.inject.Inject
 import javax.inject.Named

@@ -65,11 +65,11 @@ class WPMainActivityViewModel @Inject constructor(
     private val selectedSiteRepository: SelectedSiteRepository,
     private val accountStore: AccountStore,
     private val siteStore: SiteStore,
-    private val bloggingPromptsSettingsHelper: BloggingPromptsSettingsHelper,
     private val bloggingPromptsStore: BloggingPromptsStore,
-    @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher,
     private val shouldAskPrivacyConsent: ShouldAskPrivacyConsent,
-    private val voiceToContentFeatureUtils: VoiceToContentFeatureUtils
+    private val mainCreateSheetHelper: MainCreateSheetHelper,
+    private val mainCreateSheetTracker: MainCreateSheetTracker,
+    @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher,
 ) : ScopedViewModel(mainDispatcher) {
     private var isStarted = false

@@ -149,7 +149,7 @@ class WPMainActivityViewModel @Inject constructor(
     val isSignedInWPComOrHasWPOrgSite: Boolean
         get() = FluxCUtils.isSignedInWPComOrHasWPOrgSite(accountStore, siteStore)

-    fun start(site: SiteModel?) {
+    fun start(site: SiteModel?, page: PageType) {
         if (isStarted) return
         isStarted = true

@@ -159,17 +159,17 @@ class WPMainActivityViewModel @Inject constructor(
             }
         }

-        setMainFabUiState(false, site)
+        setMainFabUiState(false, site, page)

-        launch { loadMainActions(site) }
+        launch { loadMainActions(site, page) }

         updateFeatureAnnouncements()
     }

     @Suppress("LongMethod")
-    private suspend fun loadMainActions(site: SiteModel?, onFabClicked: Boolean = false) {
+    private suspend fun loadMainActions(site: SiteModel?, page: PageType, onFabClicked: Boolean = false) {
         val actionsList = ArrayList<MainActionListItem>()
-        if (bloggingPromptsSettingsHelper.shouldShowPromptsFeature()) {
+        if (mainCreateSheetHelper.canCreatePromptAnswer()) {
             val prompt = site?.let {
                 bloggingPromptsStore.getPromptForDate(it, Date()).firstOrNull()?.model
             }
@@ -182,8 +182,14 @@ class WPMainActivityViewModel @Inject constructor(
                         isAnswered = prompt.isAnswered,
                         promptId = prompt.id,
                         attribution = BloggingPromptAttribution.fromPrompt(prompt),
-                        onClickAction = ::onAnswerPromptActionClicked,
-                        onHelpAction = ::onHelpPrompActionClicked
+                        onClickAction = { prompt, attribution ->
+                            onAnswerPromptActionClicked(
+                                prompt,
+                                attribution,
+                                page
+                            )
+                        },
+                        onHelpAction = { onHelpPromptActionClicked(page) }
                     )
                 )
             }
@@ -197,42 +203,46 @@ class WPMainActivityViewModel @Inject constructor(
                 onClickAction = null
             )
         )
-        actionsList.add(
-            CreateAction(
-                actionType = CREATE_NEW_POST,
-                iconRes = R.drawable.ic_posts_white_24dp,
-                labelRes = R.string.my_site_bottom_sheet_add_post,
-                onClickAction = ::onCreateActionClicked
+
+        if (mainCreateSheetHelper.canCreatePost()) {
+            actionsList.add(
+                CreateAction(
+                    actionType = CREATE_NEW_POST,
+                    iconRes = R.drawable.ic_posts_white_24dp,
+                    labelRes = R.string.my_site_bottom_sheet_add_post,
+                    onClickAction = { onCreateActionClicked(it, page) }
+                )
             )
-        )
-        if (voiceToContentFeatureUtils.isVoiceToContentEnabled() && hasFullAccessToContent(site)) {
+        }
+
+        if (mainCreateSheetHelper.canCreatePostFromAudio(site)) {
             actionsList.add(
                 CreateAction(
                     actionType = ActionType.CREATE_NEW_POST_FROM_AUDIO_AI,
                     iconRes = R.drawable.ic_mic_white_24dp,
                     labelRes = R.string.my_site_bottom_sheet_add_post_from_audio,
-                    onClickAction = ::onCreateActionClicked
+                    onClickAction = { onCreateActionClicked(it, page) }
                 )
             )
         }
-        if (hasFullAccessToContent(site)) {
+
+        if (mainCreateSheetHelper.canCreatePage(site, page)) {
             actionsList.add(
                 CreateAction(
                     actionType = CREATE_NEW_PAGE,
                     iconRes = R.drawable.ic_pages_white_24dp,
                     labelRes = R.string.my_site_bottom_sheet_add_page,
-                    onClickAction = ::onCreateActionClicked
+                    onClickAction = { onCreateActionClicked(it, page) }
                 )
             )
         }

         _mainActions.postValue(actionsList)
-        if (onFabClicked) trackCreateActionsSheetCard(actionsList)
+        if (onFabClicked) mainCreateSheetTracker.trackCreateActionsSheetCard(actionsList)
     }

-    private fun onCreateActionClicked(actionType: ActionType) {
-        val properties = mapOf("action" to actionType.name.lowercase(Locale.ROOT))
-        analyticsTracker.track(Stat.MY_SITE_CREATE_SHEET_ACTION_TAPPED, properties)
+    private fun onCreateActionClicked(actionType: ActionType, page: PageType) {
+        mainCreateSheetTracker.trackActionTapped(page, actionType)
         _isBottomSheetShowing.postValue(Event(false))
         _createAction.postValue(actionType)

@@ -244,29 +254,19 @@ class WPMainActivityViewModel @Inject constructor(
         }
     }

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

-    private fun onHelpPrompActionClicked() {
-        analyticsTracker.track(Stat.MY_SITE_CREATE_SHEET_PROMPT_HELP_TAPPED)
+    private fun onHelpPromptActionClicked(page: PageType) {
+        mainCreateSheetTracker.trackHelpPromptActionTapped(page)
         _openBloggingPromptsOnboarding.call()
     }

-    private fun trackCreateActionsSheetCard(actions: List<MainActionListItem>) {
-        if (actions.any { it is AnswerBloggingPromptAction }) {
-            analyticsTracker.track(Stat.BLOGGING_PROMPTS_CREATE_SHEET_CARD_VIEWED)
-        }
-    }
-
-    fun onFabClicked(site: SiteModel?) {
+    fun onFabClicked(site: SiteModel?, page: PageType) {
         appPrefsWrapper.setMainFabTooltipDisabled(true)
-        setMainFabUiState(true, site)

         _showQuickStarInBottomSheet.postValue(quickStartRepository.activeTask.value == PUBLISH_POST)

@@ -278,9 +278,9 @@ class WPMainActivityViewModel @Inject constructor(

                 // Reload main actions, since the first time this is initialized the SiteModel may not contain the
                 // latest info.
-                loadMainActions(site, onFabClicked = true)
+                loadMainActions(site, page, onFabClicked = true)

-                analyticsTracker.track(Stat.MY_SITE_CREATE_SHEET_SHOWN)
+                mainCreateSheetTracker.trackSheetShown(page)
                 _isBottomSheetShowing.postValue(Event(true))
             }
         } else {
@@ -289,18 +289,18 @@ class WPMainActivityViewModel @Inject constructor(
         }
     }

-    fun onPageChanged(isOnMySitePageWithValidSite: Boolean, site: SiteModel?) {
-        val showFab = if (buildConfigWrapper.isCreateFabEnabled) isOnMySitePageWithValidSite else false
-        setMainFabUiState(showFab, site)
+    fun onPageChanged(site: SiteModel?, hasValidSite: Boolean, page: PageType) {
+        val showFab = hasValidSite && mainCreateSheetHelper.shouldShowFabForPage(page)
+        setMainFabUiState(showFab, site, page)
     }

     fun onOpenLoginPage() = launch {
         _switchToMeTab.value = Event(Unit)
     }

-    fun onResume(site: SiteModel?, isOnMySitePageWithValidSite: Boolean) {
-        val showFab = if (buildConfigWrapper.isCreateFabEnabled) isOnMySitePageWithValidSite else false
-        setMainFabUiState(showFab, site)
+    fun onResume(site: SiteModel?, hasValidSite: Boolean, page: PageType?) {
+        val showFab = hasValidSite && mainCreateSheetHelper.shouldShowFabForPage(page)
+        setMainFabUiState(showFab, site, page)

         checkAndShowFeatureAnnouncement()
     }
@@ -327,18 +327,20 @@ class WPMainActivityViewModel @Inject constructor(
         }
     }

-    private fun setMainFabUiState(isFabVisible: Boolean, site: SiteModel?) {
+    private fun setMainFabUiState(isFabVisible: Boolean, site: SiteModel?, page: PageType?) {
+        if (isFabVisible && page != null) mainCreateSheetTracker.trackFabShown(page)
+
         val newState = MainFabUiState(
             isFabVisible = isFabVisible,
             isFabTooltipVisible = if (appPrefsWrapper.isMainFabTooltipDisabled()) false else isFabVisible,
-            CreateContentMessageId = getCreateContentMessageId(site)
+            CreateContentMessageId = getCreateContentMessageId(site, page)
         )

         _fabUiState.value = newState
     }

-    fun getCreateContentMessageId(site: SiteModel?): Int =
-        if (hasFullAccessToContent(site)) {
+    fun getCreateContentMessageId(site: SiteModel?, page: PageType?): Int =
+        if (mainCreateSheetHelper.canCreatePage(site, page)) {
             R.string.create_post_page_fab_tooltip
         } else {
             R.string.create_post_page_fab_tooltip_contributors
@@ -375,7 +377,7 @@ class WPMainActivityViewModel @Inject constructor(
         selectedSiteRepository.removeSite()
     }

-    fun triggerCreatePageFlow(){
+    fun triggerCreatePageFlow() {
         _createAction.postValue(CREATE_NEW_PAGE_FROM_PAGES_CARD)
     }

diff --git a/WordPress/src/main/res/layout/filtered_list_component.xml b/WordPress/src/main/res/layout/filtered_list_component.xml
index 307ea2dd3203..c5ba57c0a687 100644
--- a/WordPress/src/main/res/layout/filtered_list_component.xml
+++ b/WordPress/src/main/res/layout/filtered_list_component.xml
@@ -2,7 +2,7 @@
 <org.wordpress.android.widgets.NestedCoordinatorLayout 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/coordinator"
+    android:id="@+id/filtered_list_coordinator"
     android:layout_width="match_parent"
     android:layout_height="match_parent">

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 699f1078283e..3a7b95866b48 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
@@ -304,7 +304,6 @@ public enum Stat {
         MY_SITE_ICON_CROPPED,
         MY_SITE_ICON_UPLOADED,
         MY_SITE_ICON_UPLOAD_UNSUCCESSFUL,
-        MY_SITE_CREATE_SHEET_ANSWER_PROMPT_TAPPED,
         NOTIFICATIONS_DISABLED,
         NOTIFICATIONS_ENABLED,
         NOTIFICATIONS_ACCESSED,
@@ -831,7 +830,14 @@ public enum Stat {
         JETPACK_BACKUP_DOWNLOAD_SHARE_LINK_TAPPED,
         MY_SITE_CREATE_SHEET_SHOWN,
         MY_SITE_CREATE_SHEET_ACTION_TAPPED,
+        MY_SITE_CREATE_SHEET_ANSWER_PROMPT_TAPPED,
         MY_SITE_CREATE_SHEET_PROMPT_HELP_TAPPED,
+        MY_SITE_CREATE_FAB_SHOWN,
+        READER_CREATE_SHEET_SHOWN,
+        READER_CREATE_SHEET_ACTION_TAPPED,
+        READER_CREATE_SHEET_ANSWER_PROMPT_TAPPED,
+        READER_CREATE_SHEET_PROMPT_HELP_TAPPED,
+        READER_CREATE_FAB_SHOWN,
         BLOGGING_PROMPTS_CREATE_SHEET_CARD_VIEWED,
         MY_SITE_NO_SITES_VIEW_DISPLAYED,
         MY_SITE_NO_SITES_VIEW_ACTION_TAPPED,

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/main/analytics/MainCreateSheetTrackerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/main/analytics/MainCreateSheetTrackerTest.kt
new file mode 100644
index 000000000000..e2cec7f1df9f
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/main/analytics/MainCreateSheetTrackerTest.kt
@@ -0,0 +1,276 @@
+package org.wordpress.android.ui.main.analytics
+
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnitRunner
+import org.mockito.kotlin.argThat
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoInteractions
+import org.wordpress.android.analytics.AnalyticsTracker
+import org.wordpress.android.ui.main.MainActionListItem
+import org.wordpress.android.ui.main.WPMainNavigationView
+import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptAttribution
+import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
+
+@RunWith(MockitoJUnitRunner::class)
+class MainCreateSheetTrackerTest {
+    @Mock
+    private lateinit var analyticsTracker: AnalyticsTrackerWrapper
+
+    private lateinit var tracker: MainCreateSheetTracker
+
+    @Before
+    fun setUp() {
+        tracker = MainCreateSheetTracker(analyticsTracker)
+    }
+
+    // region trackActionTapped
+    @Test
+    fun `trackActionTapped tracks action tapped for my site page`() {
+        // Arrange
+        val page = WPMainNavigationView.PageType.MY_SITE
+        val actionType = MainActionListItem.ActionType.CREATE_NEW_POST
+        val expectedStat = AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_ACTION_TAPPED
+
+        // Act
+        tracker.trackActionTapped(page, actionType)
+
+        // Assert
+        verify(analyticsTracker).track(eq(expectedStat), argThat<Map<String, Any>> {
+            this["action"] == "create_new_post"
+        })
+    }
+
+    @Test
+    fun `trackActionTapped tracks action tapped for reader page`() {
+        // Arrange
+        val page = WPMainNavigationView.PageType.READER
+        val actionType = MainActionListItem.ActionType.CREATE_NEW_POST
+        val expectedStat = AnalyticsTracker.Stat.READER_CREATE_SHEET_ACTION_TAPPED
+
+        // Act
+        tracker.trackActionTapped(page, actionType)
+
+        // Assert
+        verify(analyticsTracker).track(eq(expectedStat), argThat<Map<String, Any>> {
+            this["action"] == "create_new_post"
+        })
+    }
+
+    @Test
+    fun `trackActionTapped does not track action tapped for other pages`() {
+        WPMainNavigationView.PageType.entries
+            .filterNot { it == WPMainNavigationView.PageType.MY_SITE || it == WPMainNavigationView.PageType.READER }
+            .forEach { page ->
+                // Arrange
+                val actionType = MainActionListItem.ActionType.CREATE_NEW_POST
+
+                // Act
+                tracker.trackActionTapped(page, actionType)
+
+                // Assert
+                verifyNoInteractions(analyticsTracker)
+            }
+    }
+    // endregion
+
+    // region trackAnswerPromptActionTapped
+    @Test
+    fun `trackAnswerPromptActionTapped tracks answer prompt action tapped for my site page`() {
+        // Arrange
+        val page = WPMainNavigationView.PageType.MY_SITE
+        val attribution = BloggingPromptAttribution.DAY_ONE
+        val expectedStat = AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_ANSWER_PROMPT_TAPPED
+
+        // Act
+        tracker.trackAnswerPromptActionTapped(page, attribution)
+
+        // Assert
+        verify(analyticsTracker).track(eq(expectedStat), argThat<Map<String, Any>> {
+            this["attribution"] == attribution.value
+        })
+    }
+
+    @Test
+    fun `trackAnswerPromptActionTapped tracks answer prompt action tapped for reader page`() {
+        // Arrange
+        val page = WPMainNavigationView.PageType.READER
+        val attribution = BloggingPromptAttribution.DAY_ONE
+        val expectedStat = AnalyticsTracker.Stat.READER_CREATE_SHEET_ANSWER_PROMPT_TAPPED
+
+        // Act
+        tracker.trackAnswerPromptActionTapped(page, attribution)
+
+        // Assert
+        verify(analyticsTracker).track(eq(expectedStat), argThat<Map<String, Any>> {
+            this["attribution"] == attribution.value
+        })
+    }
+
+    @Test
+    fun `trackAnswerPromptActionTapped does not track answer prompt action tapped for other pages`() {
+        WPMainNavigationView.PageType.entries
+            .filterNot { it == WPMainNavigationView.PageType.MY_SITE || it == WPMainNavigationView.PageType.READER }
+            .forEach { page ->
+                // Arrange
+                val attribution = BloggingPromptAttribution.DAY_ONE
+
+                // Act
+                tracker.trackAnswerPromptActionTapped(page, attribution)
+
+                // Assert
+                verifyNoInteractions(analyticsTracker)
+            }
+    }
+    // endregion
+
+    // region trackHelpPromptActionTapped
+    @Test
+    fun `trackHelpPromptActionTapped tracks help prompt action tapped for my site page`() {
+        // Arrange
+        val page = WPMainNavigationView.PageType.MY_SITE
+        val expectedStat = AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_PROMPT_HELP_TAPPED
+
+        // Act
+        tracker.trackHelpPromptActionTapped(page)
+
+        // Assert
+        verify(analyticsTracker).track(expectedStat)
+    }
+
+    @Test
+    fun `trackHelpPromptActionTapped tracks help prompt action tapped for reader page`() {
+        // Arrange
+        val page = WPMainNavigationView.PageType.READER
+        val expectedStat = AnalyticsTracker.Stat.READER_CREATE_SHEET_PROMPT_HELP_TAPPED
+
+        // Act
+        tracker.trackHelpPromptActionTapped(page)
+
+        // Assert
+        verify(analyticsTracker).track(expectedStat)
+    }
+
+    @Test
+    fun `trackHelpPromptActionTapped does not track help prompt action tapped for other pages`() {
+        WPMainNavigationView.PageType.entries
+            .filterNot { it == WPMainNavigationView.PageType.MY_SITE || it == WPMainNavigationView.PageType.READER }
+            .forEach { page ->
+                // Act
+                tracker.trackHelpPromptActionTapped(page)
+
+                // Assert
+                verifyNoInteractions(analyticsTracker)
+            }
+    }
+    // endregion
+
+    // region trackSheetShown
+    @Test
+    fun `trackSheetShown tracks sheet shown for my site page`() {
+        // Arrange
+        val page = WPMainNavigationView.PageType.MY_SITE
+        val expectedStat = AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_SHOWN
+
+        // Act
+        tracker.trackSheetShown(page)
+
+        // Assert
+        verify(analyticsTracker).track(expectedStat)
+    }
+
+    @Test
+    fun `trackSheetShown tracks sheet shown for reader page`() {
+        // Arrange
+        val page = WPMainNavigationView.PageType.READER
+        val expectedStat = AnalyticsTracker.Stat.READER_CREATE_SHEET_SHOWN
+
+        // Act
+        tracker.trackSheetShown(page)
+
+        // Assert
+        verify(analyticsTracker).track(expectedStat)
+    }
+
+    @Test
+    fun `trackSheetShown does not track sheet shown for other pages`() {
+        WPMainNavigationView.PageType.entries
+            .filterNot { it == WPMainNavigationView.PageType.MY_SITE || it == WPMainNavigationView.PageType.READER }
+            .forEach { page ->
+                // Act
+                tracker.trackSheetShown(page)
+
+                // Assert
+                verifyNoInteractions(analyticsTracker)
+            }
+    }
+    // endregion
+
+    // region trackFabShown
+    @Test
+    fun `trackFabShown tracks fab shown for my site page`() {
+        // Arrange
+        val page = WPMainNavigationView.PageType.MY_SITE
+        val expectedStat = AnalyticsTracker.Stat.MY_SITE_CREATE_FAB_SHOWN
+
+        // Act
+        tracker.trackFabShown(page)
+
+        // Assert
+        verify(analyticsTracker).track(expectedStat)
+    }
+
+    @Test
+    fun `trackFabShown tracks fab shown for reader page`() {
+        // Arrange
+        val page = WPMainNavigationView.PageType.READER
+        val expectedStat = AnalyticsTracker.Stat.READER_CREATE_FAB_SHOWN
+
+        // Act
+        tracker.trackFabShown(page)
+
+        // Assert
+        verify(analyticsTracker).track(expectedStat)
+    }
+
+    @Test
+    fun `trackFabShown does not track fab shown for other pages`() {
+        WPMainNavigationView.PageType.entries
+            .filterNot { it == WPMainNavigationView.PageType.MY_SITE || it == WPMainNavigationView.PageType.READER }
+            .forEach { page ->
+                // Act
+                tracker.trackFabShown(page)
+
+                // Assert
+                verifyNoInteractions(analyticsTracker)
+            }
+    }
+    // endregion
+
+    // region trackCreateActionsSheetCard
+    @Test
+    fun `trackCreateActionsSheetCard tracks bottom sheet when it is in the list`() {
+        val actionList = listOf(
+            mock<MainActionListItem.AnswerBloggingPromptAction>(),
+            mock<MainActionListItem.CreateAction>(),
+            mock<MainActionListItem.CreateAction>(),
+        )
+        tracker.trackCreateActionsSheetCard(actionList)
+        verify(analyticsTracker).track(AnalyticsTracker.Stat.BLOGGING_PROMPTS_CREATE_SHEET_CARD_VIEWED)
+    }
+
+    @Test
+    fun `trackCreateActionsSheetCard does not track bottom sheet when it is not in the list`() {
+        val actionList = listOf(
+            mock<MainActionListItem.CreateAction>(),
+            mock<MainActionListItem.CreateAction>(),
+        )
+        tracker.trackCreateActionsSheetCard(actionList)
+        verifyNoInteractions(analyticsTracker)
+    }
+    // endregion
+}
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/main/utils/MainCreateSheetHelperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/main/utils/MainCreateSheetHelperTest.kt
new file mode 100644
index 000000000000..fe473441fcfd
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/main/utils/MainCreateSheetHelperTest.kt
@@ -0,0 +1,260 @@
+package org.wordpress.android.ui.main.utils
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Before
+import org.mockito.Mock
+import org.mockito.kotlin.whenever
+import org.wordpress.android.BaseUnitTest
+import org.wordpress.android.fluxc.model.SiteModel
+import org.wordpress.android.ui.bloggingprompts.BloggingPromptsSettingsHelper
+import org.wordpress.android.ui.main.WPMainNavigationView.PageType
+import org.wordpress.android.ui.voicetocontent.VoiceToContentFeatureUtils
+import org.wordpress.android.util.BuildConfigWrapper
+import org.wordpress.android.util.SiteUtilsWrapper
+import org.wordpress.android.util.config.ReaderFloatingButtonFeatureConfig
+import kotlin.test.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class MainCreateSheetHelperTest : BaseUnitTest() {
+    @Mock
+    private lateinit var voiceToContentFeatureUtils: VoiceToContentFeatureUtils
+
+    @Mock
+    private lateinit var readerFloatingButtonFeatureConfig: ReaderFloatingButtonFeatureConfig
+
+    @Mock
+    private lateinit var bloggingPromptsSettingsHelper: BloggingPromptsSettingsHelper
+
+    @Mock
+    private lateinit var buildConfig: BuildConfigWrapper
+
+    @Mock
+    private lateinit var siteUtils: SiteUtilsWrapper
+
+    private lateinit var helper: MainCreateSheetHelper
+
+    @Before
+    fun setUp() {
+        helper = MainCreateSheetHelper(
+            voiceToContentFeatureUtils,
+            readerFloatingButtonFeatureConfig,
+            bloggingPromptsSettingsHelper,
+            buildConfig,
+            siteUtils,
+        )
+    }
+
+    // region shouldShowFabForPage
+    @Test
+    fun `shouldShowFabForPage returns true for my site page`() {
+        // Arrange
+        val page = PageType.MY_SITE
+        whenever(buildConfig.isCreateFabEnabled).thenReturn(true)
+
+        // Act
+        val result = helper.shouldShowFabForPage(page)
+
+        // Assert
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun `shouldShowFabForPage returns true for reader page when reader floating button feature is enabled`() {
+        // Arrange
+        val page = PageType.READER
+        whenever(buildConfig.isCreateFabEnabled).thenReturn(true)
+        whenever(readerFloatingButtonFeatureConfig.isEnabled()).thenReturn(true)
+
+        // Act
+        val result = helper.shouldShowFabForPage(page)
+
+        // Assert
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun `shouldShowFabForPage returns false for reader page when reader floating button feature is disabled`() {
+        // Arrange
+        val page = PageType.READER
+        whenever(buildConfig.isCreateFabEnabled).thenReturn(true)
+        whenever(readerFloatingButtonFeatureConfig.isEnabled()).thenReturn(false)
+
+        // Act
+        val result = helper.shouldShowFabForPage(page)
+
+        // Assert
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun `shouldShowFabForPage returns false for my site page when create fab is disabled`() {
+        // Arrange
+        val page = PageType.MY_SITE
+        whenever(buildConfig.isCreateFabEnabled).thenReturn(false)
+
+        // Act
+        val result = helper.shouldShowFabForPage(page)
+
+        // Assert
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun `shouldShowFabForPage returns false for reader page when create fab is disabled`() {
+        // Arrange
+        val page = PageType.READER
+        whenever(buildConfig.isCreateFabEnabled).thenReturn(false)
+        whenever(readerFloatingButtonFeatureConfig.isEnabled()).thenReturn(true)
+
+        // Act
+        val result = helper.shouldShowFabForPage(page)
+
+        // Assert
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun `shouldShowFabForPage returns false for other pages`() {
+        PageType.entries
+            .filterNot { it == PageType.MY_SITE || it == PageType.READER }
+            .forEach { page ->
+                // Arrange
+                whenever(buildConfig.isCreateFabEnabled).thenReturn(true)
+
+                // Act
+                val result = helper.shouldShowFabForPage(page)
+
+                // Assert
+                assertThat(result).isFalse()
+            }
+    }
+    // endregion
+
+    // region canCreatePost
+    @Test
+    fun `canCreatePost returns true`() {
+        // Act
+        val result = helper.canCreatePost()
+
+        // Assert
+        assertThat(result).isTrue()
+    }
+    // endregion
+
+    // region canCreatePage
+    @Test
+    fun `canCreatePage returns true for my site page with full access to content`() {
+        // Arrange
+        val site = SiteModel()
+        val page = PageType.MY_SITE
+        whenever(siteUtils.hasFullAccessToContent(site)).thenReturn(true)
+
+        // Act
+        val result = helper.canCreatePage(site, page)
+
+        // Assert
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun `canCreatePage returns false for my site page without full access to content`() {
+        // Arrange
+        val site = SiteModel()
+        val page = PageType.MY_SITE
+        whenever(siteUtils.hasFullAccessToContent(site)).thenReturn(false)
+
+        // Act
+        val result = helper.canCreatePage(site, page)
+
+        // Assert
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun `canCreatePage returns false for other pages with full access to content`() {
+        PageType.entries
+            .filterNot { it == PageType.MY_SITE }
+            .forEach { page ->
+                // Arrange
+                val site = SiteModel()
+                whenever(siteUtils.hasFullAccessToContent(site)).thenReturn(true)
+
+                // Act
+                val result = helper.canCreatePage(site, page)
+
+                // Assert
+                assertThat(result).isFalse()
+            }
+    }
+    // endregion
+
+    // region canCreatePostFromAudio
+    @Test
+    fun `canCreatePostFromAudio returns true when voice to content is enabled and site has full access to content`() {
+        // Arrange
+        val site = SiteModel()
+        whenever(voiceToContentFeatureUtils.isVoiceToContentEnabled()).thenReturn(true)
+        whenever(siteUtils.hasFullAccessToContent(site)).thenReturn(true)
+
+        // Act
+        val result = helper.canCreatePostFromAudio(site)
+
+        // Assert
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun `canCreatePostFromAudio returns false when voice to content is disabled`() {
+        // Arrange
+        val site = SiteModel()
+        whenever(voiceToContentFeatureUtils.isVoiceToContentEnabled()).thenReturn(false)
+
+        // Act
+        val result = helper.canCreatePostFromAudio(site)
+
+        // Assert
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun `canCreatePostFromAudio returns false when site does not have full access to content`() {
+        // Arrange
+        val site = SiteModel()
+        whenever(voiceToContentFeatureUtils.isVoiceToContentEnabled()).thenReturn(true)
+        whenever(siteUtils.hasFullAccessToContent(site)).thenReturn(false)
+
+        // Act
+        val result = helper.canCreatePostFromAudio(site)
+
+        // Assert
+        assertThat(result).isFalse()
+    }
+    // endregion
+
+    // region canCreatePromptAnswer
+    @Test
+    fun `canCreatePromptAnswer returns true when prompts feature should be shown`() = test {
+        // Arrange
+        whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(true)
+
+        // Act
+        val result = helper.canCreatePromptAnswer()
+
+        // Assert
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun `canCreatePromptAnswer returns false when prompts feature should not be shown`() = test {
+        // Arrange
+        whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(false)
+
+        // Act
+        val result = helper.canCreatePromptAnswer()
+
+        // Assert
+        assertThat(result).isFalse()
+    }
+    // endregion
+}
diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt
index 68f9e62a6174..f06d97206963 100644
--- a/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt
@@ -20,7 +20,6 @@ import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 import org.wordpress.android.BaseUnitTest
-import org.wordpress.android.analytics.AnalyticsTracker.Stat
 import org.wordpress.android.analytics.AnalyticsTracker.Stat.FEATURE_ANNOUNCEMENT_SHOWN_ON_APP_UPGRADE
 import org.wordpress.android.fluxc.model.SiteModel
 import org.wordpress.android.fluxc.model.bloggingprompts.BloggingPromptModel
@@ -35,21 +34,23 @@ import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask
 import org.wordpress.android.fluxc.store.SiteStore
 import org.wordpress.android.fluxc.store.bloggingprompts.BloggingPromptsStore
 import org.wordpress.android.fluxc.store.bloggingprompts.BloggingPromptsStore.BloggingPromptsResult
-import org.wordpress.android.ui.bloggingprompts.BloggingPromptsSettingsHelper
 import org.wordpress.android.ui.main.MainActionListItem.ActionType.ANSWER_BLOGGING_PROMPT
 import org.wordpress.android.ui.main.MainActionListItem.ActionType.CREATE_NEW_PAGE
 import org.wordpress.android.ui.main.MainActionListItem.ActionType.CREATE_NEW_POST
+import org.wordpress.android.ui.main.MainActionListItem.ActionType.CREATE_NEW_POST_FROM_AUDIO_AI
 import org.wordpress.android.ui.main.MainActionListItem.ActionType.NO_ACTION
 import org.wordpress.android.ui.main.MainActionListItem.AnswerBloggingPromptAction
 import org.wordpress.android.ui.main.MainActionListItem.CreateAction
 import org.wordpress.android.ui.main.MainFabUiState
+import org.wordpress.android.ui.main.WPMainNavigationView.PageType
+import org.wordpress.android.ui.main.analytics.MainCreateSheetTracker
+import org.wordpress.android.ui.main.utils.MainCreateSheetHelper
 import org.wordpress.android.ui.mysite.SelectedSiteRepository
 import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptAttribution
 import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository
 import org.wordpress.android.ui.prefs.AppPrefsWrapper
 import org.wordpress.android.ui.prefs.privacy.banner.domain.ShouldAskPrivacyConsent
 import org.wordpress.android.ui.quickstart.QuickStartType
-import org.wordpress.android.ui.voicetocontent.VoiceToContentFeatureUtils
 import org.wordpress.android.ui.whatsnew.FeatureAnnouncement
 import org.wordpress.android.ui.whatsnew.FeatureAnnouncementItem
 import org.wordpress.android.ui.whatsnew.FeatureAnnouncementProvider
@@ -95,9 +96,6 @@ class WPMainActivityViewModelTest : BaseUnitTest() {
     @Mock
     lateinit var siteStore: SiteStore

-    @Mock
-    lateinit var bloggingPromptsSettingsHelper: BloggingPromptsSettingsHelper
-
     @Mock
     lateinit var bloggingPromptsStore: BloggingPromptsStore

@@ -111,7 +109,10 @@ class WPMainActivityViewModelTest : BaseUnitTest() {
     private lateinit var shouldAskPrivacyConsent: ShouldAskPrivacyConsent

     @Mock
-    private lateinit var voiceToContentFeatureUtils: VoiceToContentFeatureUtils
+    private lateinit var mainCreateSheetHelper: MainCreateSheetHelper
+
+    @Mock
+    private lateinit var mainCreateSheetTracker: MainCreateSheetTracker

     private val featureAnnouncement = FeatureAnnouncement(
         "14.7",
@@ -157,10 +158,10 @@ class WPMainActivityViewModelTest : BaseUnitTest() {
         activeTask = MutableLiveData()
         externalFocusPointEvents = mutableListOf()
         whenever(quickStartRepository.activeTask).thenReturn(activeTask)
-        whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(false)
         whenever(bloggingPromptsStore.getPromptForDate(any(), any())).thenReturn(flowOf(bloggingPrompt))
         whenever(shouldAskPrivacyConsent()).thenReturn(false)
-        whenever(voiceToContentFeatureUtils.isVoiceToContentEnabled()).thenReturn(false)
+        whenever(mainCreateSheetHelper.canCreatePost()).thenReturn(true)
+        whenever(mainCreateSheetHelper.canCreatePromptAnswer()).thenReturn(false)
         viewModel = WPMainActivityViewModel(
             featureAnnouncementProvider,
             buildConfigWrapper,
@@ -170,11 +171,11 @@ class WPMainActivityViewModelTest : BaseUnitTest() {
             selectedSiteRepository,
             accountStore,
             siteStore,
-            bloggingPromptsSettingsHelper,
             bloggingPromptsStore,
-            NoDelayCoroutineDispatcher(),
             shouldAskPrivacyConsent,
-            voiceToContentFeatureUtils
+            mainCreateSheetHelper,
+            mainCreateSheetTracker,
+            NoDelayCoroutineDispatcher(),
         )
         viewModel.onFeatureAnnouncementRequested.observeForever(
             onFeatureAnnouncementRequestedObserver
@@ -195,73 +196,41 @@ class WPMainActivityViewModelTest : BaseUnitTest() {
     /* FAB VISIBILITY */

     @Test
-    fun `given fab enabled, when page changed to my site, then fab is visible`() {
+    fun `given fab enabled and page changed to supported page, then fab is visible`() {
         startViewModelWithDefaultParameters()
+        whenever(mainCreateSheetHelper.shouldShowFabForPage(any())).thenReturn(true)

-        viewModel.onPageChanged(isOnMySitePageWithValidSite = true, site = initSite(hasFullAccessToContent = true))
+        viewModel.onPageChanged(site = initSite(hasFullAccessToContent = true), hasValidSite = true, page = mock())

         assertThat(fabUiState?.isFabVisible).isTrue
     }

     @Test
-    fun `given fab enabled, when page changed away from my site, then fab is hidden`() {
+    fun `given fab disabled or page changed to non-supported page, then fab is hidden`() {
         startViewModelWithDefaultParameters()
+        whenever(mainCreateSheetHelper.shouldShowFabForPage(any())).thenReturn(false)

-        viewModel.onPageChanged(isOnMySitePageWithValidSite = false, site = initSite(hasFullAccessToContent = true))
+        viewModel.onPageChanged(site = initSite(hasFullAccessToContent = true), hasValidSite = true, page = mock())

-        assertThat(fabUiState?.isFabVisible).isFalse
+        assertThat(fabUiState?.isFabVisible).isFalse()
     }

     @Test
-    fun `given fab enabled, when my site page is resumed, then fab is visible`() {
+    fun `given fab enabled and supported page is resumed, then fab is visible`() {
         startViewModelWithDefaultParameters()
+        whenever(mainCreateSheetHelper.shouldShowFabForPage(any())).thenReturn(true)

-        viewModel.onResume(isOnMySitePageWithValidSite = true, site = initSite(hasFullAccessToContent = true))
+        viewModel.onResume(site = initSite(hasFullAccessToContent = true), hasValidSite = true, page = mock())

         assertThat(fabUiState?.isFabVisible).isTrue
     }

     @Test
-    fun `given fab enabled, when non my site page is resumed, then fab is hidden`() {
+    fun `given fab disabled or non-supported page is resumed, then fab is hidden`() {
         startViewModelWithDefaultParameters()
+        whenever(mainCreateSheetHelper.shouldShowFabForPage(any())).thenReturn(false)

-        viewModel.onResume(isOnMySitePageWithValidSite = false, site = initSite(hasFullAccessToContent = true))
-
-        assertThat(fabUiState?.isFabVisible).isFalse
-    }
-
-    @Test
-    fun `given fab disabled, when page changed to my site, then fab is hidden`() {
-        startViewModelWithDefaultParameters(isCreateFabEnabled = false)
-
-        viewModel.onPageChanged(isOnMySitePageWithValidSite = true, site = initSite(hasFullAccessToContent = true))
-
-        assertThat(fabUiState?.isFabVisible).isFalse
-    }
-
-    @Test
-    fun `given fab disabled, when page changed away from my site, then fab is hidden`() {
-        startViewModelWithDefaultParameters(isCreateFabEnabled = false)
-
-        viewModel.onPageChanged(isOnMySitePageWithValidSite = false, site = initSite(hasFullAccessToContent = true))
-
-        assertThat(fabUiState?.isFabVisible).isFalse
-    }
-
-    @Test
-    fun `given fab disabled, when my site page is resumed, then fab is hidden`() {
-        startViewModelWithDefaultParameters(isCreateFabEnabled = false)
-
-        viewModel.onResume(isOnMySitePageWithValidSite = true, site = initSite(hasFullAccessToContent = true))
-
-        assertThat(fabUiState?.isFabVisible).isFalse
-    }
-
-    @Test
-    fun `given fab disabled, when non my site page is resumed, then fab is hidden`() {
-        startViewModelWithDefaultParameters(isCreateFabEnabled = false)
-
-        viewModel.onResume(isOnMySitePageWithValidSite = false, site = initSite(hasFullAccessToContent = true))
+        viewModel.onResume(site = initSite(hasFullAccessToContent = true), hasValidSite = true, page = mock())

         assertThat(fabUiState?.isFabVisible).isFalse
     }
@@ -269,8 +238,10 @@ class WPMainActivityViewModelTest : BaseUnitTest() {
     @Test
     fun `fab focus point visible when active task is PUBLISH_POST`() {
         startViewModelWithDefaultParameters()
+        whenever(mainCreateSheetHelper.shouldShowFabForPage(any())).thenReturn(true)
+
         activeTask.value = PUBLISH_POST
-        viewModel.onPageChanged(isOnMySitePageWithValidSite = true, site = initSite(hasFullAccessToContent = true))
+        viewModel.onPageChanged(site = initSite(hasFullAccessToContent = true), hasValidSite = true, page = mock())

         assertThat(fabUiState?.isFocusPointVisible).isTrue
     }
@@ -279,7 +250,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() {
     fun `fab focus point gone when active task is different`() {
         startViewModelWithDefaultParameters()
         activeTask.value = UPDATE_SITE_TITLE
-        viewModel.onPageChanged(isOnMySitePageWithValidSite = true, site = initSite(hasFullAccessToContent = true))
+        viewModel.onPageChanged(site = initSite(hasFullAccessToContent = true), hasValidSite = true, page = mock())

         assertThat(fabUiState?.isFocusPointVisible).isFalse
     }
@@ -288,7 +259,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() {
     fun `fab focus point gone when active task is null`() {
         startViewModelWithDefaultParameters()
         activeTask.value = null
-        viewModel.onPageChanged(isOnMySitePageWithValidSite = true, site = initSite(hasFullAccessToContent = true))
+        viewModel.onPageChanged(site = initSite(hasFullAccessToContent = true), hasValidSite = true, page = mock())

         assertThat(fabUiState?.isFocusPointVisible).isFalse
     }
@@ -304,6 +275,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() {

     @Test
     fun `bottom sheet action is new page when new page is tapped`() {
+        whenever(mainCreateSheetHelper.canCreatePage(any(), any())).thenReturn(true)
         startViewModelWithDefaultParameters()
         val action = viewModel.mainActions.value?.first { it.actionType == CREATE_NEW_PAGE } as CreateAction
         assertThat(action).isNotNull
@@ -313,7 +285,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() {

     @Test
     fun `bottom sheet does not show prompt card when prompts feature is not active`() = test {
-        whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(false)
+        whenever(mainCreateSheetHelper.canCreatePromptAnswer()).thenReturn(false)
         startViewModelWithDefaultParameters()
         val hasBloggingPromptAction = viewModel.mainActions.value?.any { it.actionType == ANSWER_BLOGGING_PROMPT }
         assertThat(hasBloggingPromptAction).isFalse()
@@ -321,7 +293,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() {

     @Test
     fun `bottom sheet does show prompt card when prompts feature is active`() = test {
-        whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(true)
+        whenever(mainCreateSheetHelper.canCreatePromptAnswer()).thenReturn(true)
         startViewModelWithDefaultParameters()
         val hasBloggingPromptAction = viewModel.mainActions.value?.any { it.actionType == ANSWER_BLOGGING_PROMPT }
         assertThat(hasBloggingPromptAction).isTrue()
@@ -329,7 +301,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() {

     @Test
     fun `bottom sheet action is ANSWER_BLOGGING_PROMPT when the BP answer button is clicked`() = test {
-        whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(true)
+        whenever(mainCreateSheetHelper.canCreatePromptAnswer()).thenReturn(true)
         startViewModelWithDefaultParameters()
         val action = viewModel.mainActions.value?.firstOrNull {
             it.actionType == ANSWER_BLOGGING_PROMPT
@@ -345,7 +317,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() {
     @Test
     fun `bottom sheet does not show quick start focus point by default`() {
         startViewModelWithDefaultParameters()
-        viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true))
+        viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true), page = PageType.MY_SITE)
         assertThat(viewModel.isBottomSheetShowing.value!!.peekContent()).isTrue
         assertThat(viewModel.mainActions.value?.any { it is CreateAction && it.showQuickStartFocusPoint }).isEqualTo(
             false
@@ -356,7 +328,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() {
     fun `CREATE_NEW_POST action in bottom sheet with active Quick Start completes task and hides the focus point`() {
         startViewModelWithDefaultParameters()
         activeTask.value = PUBLISH_POST
-        viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true))
+        viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true), page = PageType.MY_SITE)
         assertThat(viewModel.isBottomSheetShowing.value!!.peekContent()).isTrue
         assertThat(viewModel.mainActions.value?.any { it is CreateAction && it.showQuickStartFocusPoint }).isEqualTo(
             true
@@ -376,7 +348,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() {
     fun `CREATE_NEW_POST action sets task as done in QuickStartRepository`() {
         startViewModelWithDefaultParameters()
         activeTask.value = PUBLISH_POST
-        viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true))
+        viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true), page = PageType.MY_SITE)

         val action = viewModel.mainActions.value?.first { it.actionType == CREATE_NEW_POST } as CreateAction
         assertThat(action).isNotNull
@@ -387,9 +359,11 @@ class WPMainActivityViewModelTest : BaseUnitTest() {

     @Test
     fun `actions that are not CREATE_NEW_POST will not complete quick start task`() {
+        whenever(mainCreateSheetHelper.canCreatePage(any(), any())).thenReturn(true)
         startViewModelWithDefaultParameters()
+
         activeTask.value = PUBLISH_POST
-        viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true))
+        viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true), page = PageType.MY_SITE)
         assertThat(viewModel.isBottomSheetShowing.value!!.peekContent()).isTrue
         assertThat(viewModel.mainActions.value?.any { it is CreateAction && it.showQuickStartFocusPoint }).isEqualTo(
             true
@@ -407,15 +381,29 @@ class WPMainActivityViewModelTest : BaseUnitTest() {
     @Test
     fun `new post action is triggered from FAB when no full access to content if stories unavailable`() {
         startViewModelWithDefaultParameters()
-        viewModel.onFabClicked(site = initSite(hasFullAccessToContent = false, isWpcomOrJpSite = false))
+        viewModel.onFabClicked(
+            site = initSite(hasFullAccessToContent = false, isWpcomOrJpSite = false),
+            page = PageType.MY_SITE
+        )
         assertThat(viewModel.isBottomSheetShowing.value).isNull()
         assertThat(viewModel.createAction.value).isEqualTo(CREATE_NEW_POST)
     }

     @Test
-    fun `bottom sheet is visualized when user has full access to content and has all 3 options`() {
+    fun `bottom sheet is visualized when user has full access to content and has 2 options`() {
+        whenever(mainCreateSheetHelper.canCreatePage(any(), any())).thenReturn(false)
+        startViewModelWithDefaultParameters()
+        viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true), page = PageType.MY_SITE)
+        assertThat(viewModel.createAction.value).isNull()
+        assertThat(viewModel.mainActions.value?.size).isEqualTo(2) // 1 option plus NO_ACTION, first in list
+        assertThat(viewModel.isBottomSheetShowing.value!!.peekContent()).isTrue
+    }
+
+    @Test
+    fun `bottom sheet is visualized when user has full access to content and has 3 options`() {
+        whenever(mainCreateSheetHelper.canCreatePage(any(), any())).thenReturn(true)
         startViewModelWithDefaultParameters()
-        viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true))
+        viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true), page = PageType.MY_SITE)
         assertThat(viewModel.createAction.value).isNull()
         assertThat(viewModel.mainActions.value?.size).isEqualTo(3) // 2 options plus NO_ACTION, first in list
         assertThat(viewModel.isBottomSheetShowing.value!!.peekContent()).isTrue
@@ -606,7 +594,20 @@ class WPMainActivityViewModelTest : BaseUnitTest() {
     }

     @Test
-    fun `bottom sheet actions are sorted in the correct order`() {
+    fun `bottom sheet actions are sorted in the correct order when can create post only`() {
+        startViewModelWithDefaultParameters()
+
+        val expectedOrder = listOf(
+            NO_ACTION,
+            CREATE_NEW_POST,
+        )
+
+        assertThat(viewModel.mainActions.value!!.map { it.actionType }).isEqualTo(expectedOrder)
+    }
+
+    @Test
+    fun `bottom sheet actions are sorted in the correct order when can create post, and page`() {
+        whenever(mainCreateSheetHelper.canCreatePage(any(), any())).thenReturn(true)
         startViewModelWithDefaultParameters()

         val expectedOrder = listOf(
@@ -618,6 +619,41 @@ class WPMainActivityViewModelTest : BaseUnitTest() {
         assertThat(viewModel.mainActions.value!!.map { it.actionType }).isEqualTo(expectedOrder)
     }

+    @Test
+    fun `bottom sheet actions are sorted in the correct order when can create post, prompts, and page`() = test {
+        whenever(mainCreateSheetHelper.canCreatePromptAnswer()).thenReturn(true)
+        whenever(mainCreateSheetHelper.canCreatePage(any(), any())).thenReturn(true)
+        startViewModelWithDefaultParameters()
+
+        val expectedOrder = listOf(
+            ANSWER_BLOGGING_PROMPT,
+            NO_ACTION,
+            CREATE_NEW_POST,
+            CREATE_NEW_PAGE
+        )
+
+        assertThat(viewModel.mainActions.value!!.map { it.actionType }).isEqualTo(expectedOrder)
+    }
+
+    @Test
+    fun `bottom sheet actions are sorted in the correct order when can create post, from audio, prompts, and page`() =
+        test {
+            whenever(mainCreateSheetHelper.canCreatePostFromAudio(any())).thenReturn(true)
+            whenever(mainCreateSheetHelper.canCreatePromptAnswer()).thenReturn(true)
+            whenever(mainCreateSheetHelper.canCreatePage(any(), any())).thenReturn(true)
+            startViewModelWithDefaultParameters()
+
+            val expectedOrder = listOf(
+                ANSWER_BLOGGING_PROMPT,
+                NO_ACTION,
+                CREATE_NEW_POST,
+                CREATE_NEW_POST_FROM_AUDIO_AI,
+                CREATE_NEW_PAGE
+            )
+
+            assertThat(viewModel.mainActions.value!!.map { it.actionType }).isEqualTo(expectedOrder)
+        }
+
     @Test
     fun `hasMultipleSites should be true when there are more than one site`() {
         whenever(siteStore.sitesCount).thenReturn(2)
@@ -656,18 +692,18 @@ class WPMainActivityViewModelTest : BaseUnitTest() {

     @Test
     fun `Should track analytics event when onHelpPromptActionClicked is called`() = test {
-        whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(true)
+        whenever(mainCreateSheetHelper.canCreatePromptAnswer()).thenReturn(true)
         startViewModelWithDefaultParameters()
         val action = viewModel.mainActions.value?.first {
             it.actionType == ANSWER_BLOGGING_PROMPT
         } as AnswerBloggingPromptAction
         action.onHelpAction?.invoke()
-        verify(analyticsTrackerWrapper).track(Stat.MY_SITE_CREATE_SHEET_PROMPT_HELP_TAPPED)
+        verify(mainCreateSheetTracker).trackHelpPromptActionTapped(any())
     }

     @Test
     fun `Should trigger openBloggingPromptsOnboarding when onHelpPromptActionClicked is called`() = test {
-        whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(true)
+        whenever(mainCreateSheetHelper.canCreatePromptAnswer()).thenReturn(true)
         startViewModelWithDefaultParameters()
         val action = viewModel.mainActions.value?.first {
             it.actionType == ANSWER_BLOGGING_PROMPT
@@ -677,14 +713,11 @@ class WPMainActivityViewModelTest : BaseUnitTest() {
     }

     @Test
-    @Suppress("MaxLineLength")
-    fun `Should track BLOGGING_PROMPTS_CREATE_SHEET_CARD_VIEWED when onFabClicked is called and actions contains AnswerBloggingPromptAction`() =
-        test {
-            whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(true)
-            startViewModelWithDefaultParameters()
-            viewModel.onFabClicked(initSite())
-            verify(analyticsTrackerWrapper).track(Stat.BLOGGING_PROMPTS_CREATE_SHEET_CARD_VIEWED)
-        }
+    fun `Should track card actions when onFabClicker is called`() {
+        startViewModelWithDefaultParameters()
+        viewModel.onFabClicked(initSite(), page = PageType.MY_SITE)
+        verify(mainCreateSheetTracker).trackCreateActionsSheetCard(any())
+    }

     @Test
     fun `it asks for privacy consent at the start when it should`() = test {
@@ -731,12 +764,14 @@ class WPMainActivityViewModelTest : BaseUnitTest() {

     private fun startViewModelWithDefaultParameters(
         isWhatsNewFeatureEnabled: Boolean = true,
-        isCreateFabEnabled: Boolean = true,
-        isWpcomOrJpSite: Boolean = true
+        isWpcomOrJpSite: Boolean = true,
+        pageType: PageType = PageType.MY_SITE,
     ) {
         whenever(buildConfigWrapper.isWhatsNewFeatureEnabled).thenReturn(isWhatsNewFeatureEnabled)
-        whenever(buildConfigWrapper.isCreateFabEnabled).thenReturn(isCreateFabEnabled)
-        viewModel.start(site = initSite(hasFullAccessToContent = true, isWpcomOrJpSite = isWpcomOrJpSite))
+        viewModel.start(
+            site = initSite(hasFullAccessToContent = true, isWpcomOrJpSite = isWpcomOrJpSite),
+            page = pageType
+        )
     }

     private fun setupObservers() {
@@ -748,7 +783,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() {
     }

     private fun resumeViewModelWithDefaultParameters() {
-        viewModel.onResume(site = initSite(hasFullAccessToContent = true), isOnMySitePageWithValidSite = true)
+        viewModel.onResume(site = initSite(hasFullAccessToContent = true), hasValidSite = true, page = PageType.MY_SITE)
     }

     private fun initSite(