Add funding UI - part 5

This finishes adding the main UI parts. Integration with the actual billing manager will be in a separate pull-request. Given the time constraints, it doesn't include fake data to render the contribution items.

![Contribution Screen](https://github.com/user-attachments/assets/2e5a9a32-c27e-4a30-82d9-dfdf45dbc6a0)

Resolves #8163

Implement subscription ui flow

Implement subscription ui flow
Design:

![Support -_ Choose amount v01 1](https://github.com/user-attachments/assets/bcf35dc3-ac5c-4ffa-a8d5-530918771509)

Will this also be enabled for first time installs like the desktop version of Thunderbird does? And will it pop up regularly or will it just be there?
We haven't worked out the cadence yet, but the first time install might be a bit too early. Folks need to have a chance to use the app before they make this decision. On desktop it works because there is more screen space and the message does not get in the way.

diff --git a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/organism/TopAppBar.kt b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/organism/TopAppBar.kt
index ae78238d63b..3d39d176cea 100644
--- a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/organism/TopAppBar.kt
+++ b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/organism/TopAppBar.kt
@@ -5,6 +5,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
 import app.k9mail.core.ui.compose.designsystem.atom.button.ButtonIcon
 import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
 import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleLarge
@@ -44,6 +45,7 @@ fun TopAppBarWithMenuButton(
             ButtonIcon(
                 onClick = onMenuClick,
                 imageVector = Icons.Outlined.Menu,
+                modifier = Modifier.testTag("TopAppBarMenuButton"),
             )
         },
         actions = actions,
@@ -64,6 +66,7 @@ fun TopAppBarWithBackButton(
             ButtonIcon(
                 onClick = onBackClick,
                 imageVector = Icons.Outlined.ArrowBack,
+                modifier = Modifier.testTag("TopAppBarBackButton"),
             )
         },
         actions = actions,
diff --git a/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionContentPreview.kt b/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionContentPreview.kt
new file mode 100644
index 00000000000..d0f1a72aaf0
--- /dev/null
+++ b/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionContentPreview.kt
@@ -0,0 +1,36 @@
+package app.k9mail.feature.funding.googleplay.ui.contribution
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.Preview
+import app.k9mail.core.ui.compose.common.annotation.PreviewDevicesWithBackground
+import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
+import app.k9mail.feature.funding.googleplay.ui.contribution.ContributionContract.State
+
+@Composable
+@PreviewDevicesWithBackground
+fun ContributionContentPreview() {
+    PreviewWithTheme {
+        ContributionContent(
+            state = State(
+                recurringContributions = FakeData.recurringContributions,
+                oneTimeContributions = FakeData.oneTimeContributions,
+                selectedContribution = FakeData.recurringContributions.first(),
+            ),
+            onEvent = {},
+            contentPadding = PaddingValues(),
+        )
+    }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun ContributionContentEmptyPreview() {
+    PreviewWithTheme {
+        ContributionContent(
+            state = State(),
+            onEvent = {},
+            contentPadding = PaddingValues(),
+        )
+    }
+}
diff --git a/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionFooterPreview.kt b/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionFooterPreview.kt
index 75b41c6f202..d991d8bc5a9 100644
--- a/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionFooterPreview.kt
+++ b/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionFooterPreview.kt
@@ -9,7 +9,9 @@ import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
 fun ContributionFooterPreview() {
     PreviewWithTheme {
         ContributionFooter(
-            onClick = {},
+            onPurchaseClick = {},
+            onManagePurchaseClick = {},
+            purchasedContribution = null,
             isPurchaseEnabled = true,
         )
     }
@@ -20,8 +22,23 @@ fun ContributionFooterPreview() {
 fun ContributionFooterDisabledPreview() {
     PreviewWithTheme {
         ContributionFooter(
-            onClick = {},
+            onPurchaseClick = {},
+            onManagePurchaseClick = {},
+            purchasedContribution = null,
             isPurchaseEnabled = false,
         )
     }
 }
+
+@Composable
+@Preview(showBackground = true)
+fun ContributionFooterWithRecurringContributionPreview() {
+    PreviewWithTheme {
+        ContributionFooter(
+            onPurchaseClick = {},
+            onManagePurchaseClick = {},
+            purchasedContribution = FakeData.recurringContribution,
+            isPurchaseEnabled = true,
+        )
+    }
+}
diff --git a/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionHeaderPreview.kt b/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionHeaderPreview.kt
index 3d3955700b9..e95de64aeed 100644
--- a/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionHeaderPreview.kt
+++ b/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionHeaderPreview.kt
@@ -8,6 +8,26 @@ import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
 @Preview(showBackground = true)
 internal fun ContributionHeaderPreview() {
     PreviewWithTheme {
-        ContributionHeader()
+        ContributionHeader(purchasedContribution = null)
+    }
+}
+
+@Composable
+@Preview(showBackground = true)
+internal fun ContributionHeaderWithPurchasedOneTimeContributionPreview() {
+    PreviewWithTheme {
+        ContributionHeader(
+            purchasedContribution = FakeData.oneTimeContribution,
+        )
+    }
+}
+
+@Composable
+@Preview(showBackground = true)
+internal fun ContributionHeaderWithPurchasedRecurringContributionPreview() {
+    PreviewWithTheme {
+        ContributionHeader(
+            purchasedContribution = FakeData.recurringContribution,
+        )
     }
 }
diff --git a/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionListPreview.kt b/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionListPreview.kt
index 9d12de47ce7..5c231bbe86e 100644
--- a/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionListPreview.kt
+++ b/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionListPreview.kt
@@ -3,13 +3,15 @@ package app.k9mail.feature.funding.googleplay.ui.contribution
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.tooling.preview.Preview
 import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
+import kotlinx.collections.immutable.persistentListOf

 @Composable
 @Preview(showBackground = true)
 internal fun ContributionListPreview() {
     PreviewWithTheme {
         ContributionList(
-            contributions = FakeData.recurringContributions,
+            oneTimeContributions = FakeData.oneTimeContributions,
+            recurringContributions = FakeData.recurringContributions,
             isRecurringContributionSelected = true,
             selectedItem = FakeData.recurringContributions.first(),
             onOneTimeContributionTypeClick = {},
@@ -24,7 +26,8 @@ internal fun ContributionListPreview() {
 internal fun ContributionListRecurringPreview() {
     PreviewWithTheme {
         ContributionList(
-            contributions = FakeData.oneTimeContributions,
+            oneTimeContributions = FakeData.oneTimeContributions,
+            recurringContributions = FakeData.recurringContributions,
             selectedItem = FakeData.oneTimeContributions.last(),
             isRecurringContributionSelected = false,
             onOneTimeContributionTypeClick = {},
@@ -33,3 +36,51 @@ internal fun ContributionListRecurringPreview() {
         )
     }
 }
+
+@Composable
+@Preview(showBackground = true)
+internal fun ContributionListOneTimeOnlyPreview() {
+    PreviewWithTheme {
+        ContributionList(
+            oneTimeContributions = FakeData.oneTimeContributions,
+            recurringContributions = persistentListOf(),
+            selectedItem = null,
+            isRecurringContributionSelected = false,
+            onOneTimeContributionTypeClick = {},
+            onRecurringContributionTypeClick = {},
+            onItemClick = {},
+        )
+    }
+}
+
+@Composable
+@Preview(showBackground = true)
+internal fun ContributionListRecurringOnlyPreview() {
+    PreviewWithTheme {
+        ContributionList(
+            oneTimeContributions = persistentListOf(),
+            recurringContributions = FakeData.recurringContributions,
+            selectedItem = null,
+            isRecurringContributionSelected = true,
+            onOneTimeContributionTypeClick = {},
+            onRecurringContributionTypeClick = {},
+            onItemClick = {},
+        )
+    }
+}
+
+@Composable
+@Preview(showBackground = true)
+internal fun ContributionListEmptyPreview() {
+    PreviewWithTheme {
+        ContributionList(
+            oneTimeContributions = persistentListOf(),
+            recurringContributions = persistentListOf(),
+            selectedItem = null,
+            isRecurringContributionSelected = false,
+            onOneTimeContributionTypeClick = {},
+            onRecurringContributionTypeClick = {},
+            onItemClick = {},
+        )
+    }
+}
diff --git a/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionScreenPreview.kt b/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionScreenPreview.kt
new file mode 100644
index 00000000000..ee68ac75b63
--- /dev/null
+++ b/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionScreenPreview.kt
@@ -0,0 +1,23 @@
+package app.k9mail.feature.funding.googleplay.ui.contribution
+
+import androidx.compose.runtime.Composable
+import app.k9mail.core.ui.compose.common.annotation.PreviewDevicesWithBackground
+import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
+import app.k9mail.feature.funding.googleplay.ui.contribution.ContributionContract.State
+
+@Composable
+@PreviewDevicesWithBackground
+fun ContributionScreenPreview() {
+    PreviewWithTheme {
+        ContributionScreen(
+            onBack = {},
+            viewModel = FakeContributionViewModel(
+                initialState = State(
+                    recurringContributions = FakeData.recurringContributions,
+                    oneTimeContributions = FakeData.oneTimeContributions,
+                    selectedContribution = FakeData.recurringContributions.first(),
+                ),
+            ),
+        )
+    }
+}
diff --git a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/domain/entity/Contribution.kt b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/domain/entity/Contribution.kt
index 9db4befb318..9d94655846b 100644
--- a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/domain/entity/Contribution.kt
+++ b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/domain/entity/Contribution.kt
@@ -3,5 +3,7 @@ package app.k9mail.feature.funding.googleplay.domain.entity
 interface Contribution {
     val id: String
     val title: String
-    val price: String
+    val description: String
+    val price: Long
+    val priceFormatted: String
 }
diff --git a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/domain/entity/OneTimeContribution.kt b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/domain/entity/OneTimeContribution.kt
index 09149262a32..26b951c83b6 100644
--- a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/domain/entity/OneTimeContribution.kt
+++ b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/domain/entity/OneTimeContribution.kt
@@ -3,5 +3,7 @@ package app.k9mail.feature.funding.googleplay.domain.entity
 class OneTimeContribution(
     override val id: String,
     override val title: String,
-    override val price: String,
+    override val description: String,
+    override val price: Long,
+    override val priceFormatted: String,
 ) : Contribution
diff --git a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/domain/entity/RecurringContribution.kt b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/domain/entity/RecurringContribution.kt
index 24f02b22347..1c63a9e146d 100644
--- a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/domain/entity/RecurringContribution.kt
+++ b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/domain/entity/RecurringContribution.kt
@@ -3,5 +3,7 @@ package app.k9mail.feature.funding.googleplay.domain.entity
 data class RecurringContribution(
     override val id: String,
     override val title: String,
-    override val price: String,
+    override val description: String,
+    override val price: Long,
+    override val priceFormatted: String,
 ) : Contribution
diff --git a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionContent.kt b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionContent.kt
index 21bcdd815b3..931f0a2affb 100644
--- a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionContent.kt
+++ b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionContent.kt
@@ -1,14 +1,19 @@
 package app.k9mail.feature.funding.googleplay.ui.contribution

-import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium
-import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleLarge
+import androidx.compose.ui.platform.testTag
+import app.k9mail.core.ui.compose.common.activity.LocalActivity
+import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
+import app.k9mail.core.ui.compose.theme2.MainTheme
 import app.k9mail.feature.funding.googleplay.ui.contribution.ContributionContract.Event
 import app.k9mail.feature.funding.googleplay.ui.contribution.ContributionContract.State

@@ -19,18 +24,48 @@ internal fun ContributionContent(
     contentPadding: PaddingValues,
     modifier: Modifier = Modifier,
 ) {
-    Column(
-        modifier = modifier.fillMaxSize()
-            .padding(contentPadding)
-            .clickable(
-                enabled = false,
-                onClick = { onEvent(Event.OnPurchaseClicked) },
-            ),
+    ResponsiveWidthContainer(
+        modifier = modifier
+            .testTag("ContributionContent")
+            .padding(contentPadding),
     ) {
-        TextTitleLarge(
-            text = "ContributionScreen",
-        )
+        val activity = LocalActivity.current
+        val scrollState = rememberScrollState()

-        TextBodyMedium(text = "is recurring selected: ${state.isRecurringContributionSelected}")
+        Column(
+            modifier = Modifier
+                .fillMaxSize()
+                .padding(horizontal = MainTheme.spacings.quadruple)
+                .verticalScroll(scrollState),
+            horizontalAlignment = Alignment.Start,
+            verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.triple),
+        ) {
+            ContributionHeader(
+                purchasedContribution = state.purchasedContribution,
+            )
+
+            ContributionList(
+                oneTimeContributions = state.oneTimeContributions,
+                recurringContributions = state.recurringContributions,
+                selectedItem = state.selectedContribution,
+                isRecurringContributionSelected = state.isRecurringContributionSelected,
+                onOneTimeContributionTypeClick = {
+                    onEvent(Event.OnOneTimeContributionSelected)
+                },
+                onRecurringContributionTypeClick = {
+                    onEvent(Event.OnRecurringContributionSelected)
+                },
+                onItemClick = {
+                    onEvent(Event.OnContributionItemClicked(it))
+                },
+            )
+
+            ContributionFooter(
+                onPurchaseClick = { onEvent(Event.OnPurchaseClicked(activity)) },
+                onManagePurchaseClick = { onEvent(Event.OnManagePurchaseClicked(it)) },
+                purchasedContribution = state.purchasedContribution,
+                isPurchaseEnabled = state.selectedContribution != null,
+            )
+        }
     }
 }
diff --git a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionContract.kt b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionContract.kt
index 2475d08815c..bcd8604b459 100644
--- a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionContract.kt
+++ b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionContract.kt
@@ -1,5 +1,6 @@
 package app.k9mail.feature.funding.googleplay.ui.contribution

+import android.app.Activity
 import androidx.compose.runtime.Stable
 import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
 import app.k9mail.feature.funding.googleplay.domain.entity.Contribution
@@ -14,16 +15,27 @@ internal class ContributionContract {

     @Stable
     data class State(
-        val recurringContributions: ImmutableList<RecurringContribution> = persistentListOf(),
         val oneTimeContributions: ImmutableList<OneTimeContribution> = persistentListOf(),
+        val recurringContributions: ImmutableList<RecurringContribution> = persistentListOf(),
+        val purchasedContribution: Contribution? = null,
         val selectedContribution: Contribution? = null,
         val isRecurringContributionSelected: Boolean = false,
     )

     sealed interface Event {
-        data object OnOneTimeContributionClicked : Event
-        data object OnRecurringContributionClicked : Event
-        data class OnContributionClicked(val item: Contribution) : Event
-        data object OnPurchaseClicked : Event
+        data object OnOneTimeContributionSelected : Event
+        data object OnRecurringContributionSelected : Event
+
+        data class OnContributionItemClicked(
+            val item: Contribution,
+        ) : Event
+
+        data class OnPurchaseClicked(
+            val activity: Activity,
+        ) : Event
+
+        data class OnManagePurchaseClicked(
+            val contribution: Contribution,
+        ) : Event
     }
 }
diff --git a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionFooter.kt b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionFooter.kt
index 54670bbdfd7..150641aa869 100644
--- a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionFooter.kt
+++ b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionFooter.kt
@@ -1,29 +1,43 @@
 package app.k9mail.feature.funding.googleplay.ui.contribution

-import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
 import app.k9mail.core.ui.compose.designsystem.atom.button.ButtonFilled
 import app.k9mail.feature.funding.googleplay.R
+import app.k9mail.feature.funding.googleplay.domain.entity.Contribution
+import app.k9mail.feature.funding.googleplay.domain.entity.RecurringContribution

 @Composable
 internal fun ContributionFooter(
-    onClick: () -> Unit,
+    purchasedContribution: Contribution?,
+    onPurchaseClick: () -> Unit,
+    onManagePurchaseClick: (Contribution) -> Unit,
     isPurchaseEnabled: Boolean,
     modifier: Modifier = Modifier,
 ) {
-    Row(
+    Column(
         modifier = modifier,
     ) {
-        ButtonFilled(
-            text = stringResource(
-                R.string.funding_googleplay_contribution_footer_payment_button,
-            ),
-            onClick = onClick,
-            enabled = isPurchaseEnabled,
-            modifier = Modifier.fillMaxWidth(),
-        )
+        if (purchasedContribution != null && purchasedContribution is RecurringContribution) {
+            ButtonFilled(
+                text = stringResource(
+                    R.string.funding_googleplay_contribution_footer_manage_button,
+                ),
+                onClick = { onManagePurchaseClick(purchasedContribution) },
+                modifier = Modifier.fillMaxWidth(),
+            )
+        } else {
+            ButtonFilled(
+                text = stringResource(
+                    R.string.funding_googleplay_contribution_footer_payment_button,
+                ),
+                onClick = onPurchaseClick,
+                enabled = isPurchaseEnabled,
+                modifier = Modifier.fillMaxWidth(),
+            )
+        }
     }
 }
diff --git a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionHeader.kt b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionHeader.kt
index 260a6f39a5b..2e87bbf4552 100644
--- a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionHeader.kt
+++ b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionHeader.kt
@@ -10,15 +10,19 @@ 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.text.style.TextAlign
 import app.k9mail.core.ui.compose.designsystem.atom.image.FixedScaleImage
 import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium
 import app.k9mail.core.ui.compose.designsystem.atom.text.TextHeadlineSmall
 import app.k9mail.core.ui.compose.theme2.MainTheme
 import app.k9mail.feature.funding.googleplay.R
+import app.k9mail.feature.funding.googleplay.domain.entity.Contribution
+import app.k9mail.feature.funding.googleplay.ui.contribution.image.GoldenHearthSunburst
 import app.k9mail.feature.funding.googleplay.ui.contribution.image.HearthSunburst

 @Composable
 internal fun ContributionHeader(
+    purchasedContribution: Contribution?,
     modifier: Modifier = Modifier,
 ) {
     Column(
@@ -33,7 +37,7 @@ internal fun ContributionHeader(
                 .height(MainTheme.sizes.large),
         ) {
             FixedScaleImage(
-                imageVector = HearthSunburst,
+                imageVector = if (purchasedContribution != null) GoldenHearthSunburst else HearthSunburst,
                 contentDescription = null,
                 alignment = Alignment.TopCenter,
                 allowOverflow = true,
@@ -41,12 +45,15 @@ internal fun ContributionHeader(
         }

         TextHeadlineSmall(
-            text = stringResource(R.string.funding_googleplay_contribution_header_title),
+            text = purchasedContribution?.title
+                ?: stringResource(R.string.funding_googleplay_contribution_header_title),
             color = MainTheme.colors.primary,
+            textAlign = TextAlign.Center,
         )

         TextBodyMedium(
-            text = stringResource(R.string.funding_googleplay_contribution_header_description),
+            text = purchasedContribution?.description
+                ?: stringResource(R.string.funding_googleplay_contribution_header_description),
         )
     }
 }
diff --git a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionList.kt b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionList.kt
index 0c90e5c97e6..31bd2e27574 100644
--- a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionList.kt
+++ b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionList.kt
@@ -16,12 +16,14 @@ import app.k9mail.core.ui.compose.designsystem.atom.text.TextLabelLarge
 import app.k9mail.core.ui.compose.theme2.MainTheme
 import app.k9mail.feature.funding.googleplay.R
 import app.k9mail.feature.funding.googleplay.domain.entity.Contribution
+import app.k9mail.feature.funding.googleplay.domain.entity.OneTimeContribution
+import app.k9mail.feature.funding.googleplay.domain.entity.RecurringContribution
 import kotlinx.collections.immutable.ImmutableList

-@OptIn(ExperimentalLayoutApi::class)
 @Composable
 internal fun ContributionList(
-    contributions: ImmutableList<Contribution>,
+    oneTimeContributions: ImmutableList<OneTimeContribution>,
+    recurringContributions: ImmutableList<RecurringContribution>,
     selectedItem: Contribution?,
     isRecurringContributionSelected: Boolean,
     onOneTimeContributionTypeClick: () -> Unit,
@@ -42,18 +44,61 @@ internal fun ContributionList(
             TextLabelLarge(
                 text = stringResource(R.string.funding_googleplay_contribution_list_title),
             )
-            Row(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .padding(top = MainTheme.spacings.default),
-                horizontalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
-            ) {
+
+            TypeSelectionRow(
+                oneTimeContributions = oneTimeContributions,
+                recurringContributions = recurringContributions,
+                isRecurringContributionSelected = isRecurringContributionSelected,
+                onOneTimeContributionTypeClick = onOneTimeContributionTypeClick,
+                onRecurringContributionTypeClick = onRecurringContributionTypeClick,
+            )
+
+            ChoicesRow(
+                contributions = if (isRecurringContributionSelected) recurringContributions else oneTimeContributions,
+                selectedItem = selectedItem,
+                onItemClick = onItemClick,
+            )
+
+            TextBodyMedium(
+                text = stringResource(R.string.funding_googleplay_contribution_list_disclaimer),
+                modifier = Modifier.padding(top = MainTheme.spacings.default),
+            )
+        }
+    }
+}
+
+@Composable
+private fun TypeSelectionRow(
+    oneTimeContributions: ImmutableList<OneTimeContribution>,
+    recurringContributions: ImmutableList<RecurringContribution>,
+    isRecurringContributionSelected: Boolean,
+    onOneTimeContributionTypeClick: () -> Unit,
+    onRecurringContributionTypeClick: () -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    Row(
+        modifier = modifier
+            .fillMaxWidth()
+            .padding(top = MainTheme.spacings.default),
+        horizontalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
+    ) {
+        if (oneTimeContributions.isEmpty() && recurringContributions.isEmpty()) {
+            ContributionListItem(
+                text = stringResource(R.string.funding_googleplay_contribution_list_type_none_available),
+                onClick = {},
+                isSelected = true,
+                modifier = Modifier.weight(1f),
+            )
+        } else {
+            if (oneTimeContributions.isNotEmpty()) {
                 ContributionListItem(
                     text = stringResource(R.string.funding_googleplay_contribution_list_type_one_time),
                     onClick = onOneTimeContributionTypeClick,
                     isSelected = !isRecurringContributionSelected,
                     modifier = Modifier.weight(1f),
                 )
+            }
+            if (recurringContributions.isNotEmpty()) {
                 ContributionListItem(
                     text = stringResource(R.string.funding_googleplay_contribution_list_type_recurring),
                     onClick = onRecurringContributionTypeClick,
@@ -61,24 +106,29 @@ internal fun ContributionList(
                     modifier = Modifier.weight(1f),
                 )
             }
+        }
+    }
+}

-            FlowRow(
-                horizontalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
-                verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
-            ) {
-                contributions.forEach {
-                    ContributionListItem(
-                        text = it.price,
-                        onClick = { onItemClick(it) },
-                        isSelected = it == selectedItem,
-                        modifier = Modifier.weight(1f),
-                    )
-                }
-            }
-
-            TextBodyMedium(
-                text = stringResource(R.string.funding_googleplay_contribution_list_disclaimer),
-                modifier = Modifier.padding(top = MainTheme.spacings.default),
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+private fun ChoicesRow(
+    contributions: ImmutableList<Contribution>,
+    onItemClick: (Contribution) -> Unit,
+    selectedItem: Contribution?,
+    modifier: Modifier = Modifier,
+) {
+    FlowRow(
+        horizontalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
+        verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
+        modifier = modifier,
+    ) {
+        contributions.forEach {
+            ContributionListItem(
+                text = it.priceFormatted,
+                onClick = { onItemClick(it) },
+                isSelected = it == selectedItem,
+                modifier = Modifier.weight(1f),
             )
         }
     }
diff --git a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionScreen.kt b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionScreen.kt
index ce8d76b3ed7..1be57894164 100644
--- a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionScreen.kt
+++ b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionScreen.kt
@@ -3,8 +3,11 @@ package app.k9mail.feature.funding.googleplay.ui.contribution
 import androidx.activity.compose.BackHandler
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
 import app.k9mail.core.ui.compose.common.mvi.observe
+import app.k9mail.core.ui.compose.designsystem.organism.TopAppBarWithBackButton
 import app.k9mail.core.ui.compose.designsystem.template.Scaffold
+import app.k9mail.feature.funding.googleplay.R
 import app.k9mail.feature.funding.googleplay.ui.contribution.ContributionContract.ViewModel
 import org.koin.androidx.compose.koinViewModel

@@ -22,6 +25,12 @@ internal fun ContributionScreen(

     Scaffold(
         modifier = modifier,
+        topBar = {
+            TopAppBarWithBackButton(
+                title = stringResource(R.string.funding_googleplay_contribution_title),
+                onBackClick = onBack,
+            )
+        },
     ) { innerPadding ->
         ContributionContent(
             state = state.value,
diff --git a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionViewModel.kt b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionViewModel.kt
index 4585a9ee771..fc964212534 100644
--- a/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionViewModel.kt
+++ b/feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionViewModel.kt
@@ -1,6 +1,8 @@
 package app.k9mail.feature.funding.googleplay.ui.contribution

+import android.app.Activity
 import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
+import app.k9mail.feature.funding.googleplay.domain.entity.Contribution
 import app.k9mail.feature.funding.googleplay.ui.contribution.ContributionContract.Event
 import app.k9mail.feature.funding.googleplay.ui.contribution.ContributionContract.State
 import app.k9mail.feature.funding.googleplay.ui.contribution.ContributionContract.ViewModel
@@ -12,14 +14,47 @@ internal class ContributionViewModel(

     override fun event(event: Event) {
         when (event) {
-            is Event.OnContributionClicked -> TODO()
+            Event.OnOneTimeContributionSelected -> onOneTimeContributionSelected()
+            Event.OnRecurringContributionSelected -> onRecurringContributionSelected()
+            is Event.OnContributionItemClicked -> onContributionItemClicked(event.item)
+            is Event.OnPurchaseClicked -> onPurchaseClicked(event.activity)
+            is Event.OnManagePurchaseClicked -> onManagePurchaseClicked(event.contribution)
+        }
+    }

-            Event.OnOneTimeContributionClicked -> TODO()
-            Event.OnPurchaseClicked -> {
-                // TODO
-            }
+    private fun onOneTimeContributionSelected() {
+        updateState {
+            it.copy(
+                isRecurringContributionSelected = false,
+                selectedContribution = it.oneTimeContributions.firstOrNull(),
+            )
+        }
+    }

-            Event.OnRecurringContributionClicked -> TODO()
+    private fun onRecurringContributionSelected() {
+        updateState {
+            it.copy(
+                isRecurringContributionSelected = true,
+                selectedContribution = it.recurringContributions.firstOrNull(),
+            )
         }
     }
+
+    private fun onContributionItemClicked(item: Contribution) {
+        updateState {
+            it.copy(
+                selectedContribution = item,
+            )
+        }
+    }
+
+    @Suppress("UnusedParameter")
+    private fun onPurchaseClicked(activity: Activity) {
+        // TODO: Implement purchase logic
+    }
+
+    @Suppress("UnusedParameter")
+    private fun onManagePurchaseClicked(contribution: Contribution) {
+        // TODO: Implement manage purchase logic
+    }
 }
diff --git a/feature/funding/googleplay/src/main/res/values/strings.xml b/feature/funding/googleplay/src/main/res/values/strings.xml
index c96668dfb46..6e1f72faa22 100644
--- a/feature/funding/googleplay/src/main/res/values/strings.xml
+++ b/feature/funding/googleplay/src/main/res/values/strings.xml
@@ -1,13 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
+    <string name="funding_googleplay_contribution_title">Settings</string>
+
     <string name="funding_googleplay_contribution_header_title">Support Thunderbird</string>
     <string name="funding_googleplay_contribution_header_description">We never show advertisements or sell your data. We are fully funded by financial contributions from our users. If you’re enjoying Thunderbird, please help support it.  We can’t do this without you!</string>

     <string name="funding_googleplay_contribution_list_title">Secure contribution</string>
     <string name="funding_googleplay_contribution_list_type_one_time">Give once</string>
     <string name="funding_googleplay_contribution_list_type_recurring">Monthly</string>
+    <string name="funding_googleplay_contribution_list_type_none_available">None available</string>
     <!-- The disclaimer could be found at https://www.thunderbird.net/donate/mobile/?form=tfa for other languages -->
     <string name="funding_googleplay_contribution_list_disclaimer">Contributions are not tax-deductible as charitable donations.</string>

     <string name="funding_googleplay_contribution_footer_payment_button">Continue to payment</string>
+    <string name="funding_googleplay_contribution_footer_manage_button">Modify monthly payment</string>
 </resources>

diff --git a/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/FakeContributionViewModel.kt b/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/FakeContributionViewModel.kt
new file mode 100644
index 00000000000..62da2560890
--- /dev/null
+++ b/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/FakeContributionViewModel.kt
@@ -0,0 +1,21 @@
+package app.k9mail.feature.funding.googleplay.ui.contribution
+
+import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
+import app.k9mail.feature.funding.googleplay.ui.contribution.ContributionContract.Event
+import app.k9mail.feature.funding.googleplay.ui.contribution.ContributionContract.State
+import app.k9mail.feature.funding.googleplay.ui.contribution.ContributionContract.ViewModel
+
+internal class FakeContributionViewModel(
+    initialState: State,
+) : BaseViewModel<State, Event, Nothing>(initialState = initialState), ViewModel {
+
+    val events = mutableListOf<Event>()
+
+    override fun event(event: Event) {
+        events.add(event)
+    }
+
+    fun applyState(state: State) {
+        updateState { state }
+    }
+}
diff --git a/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/FakeData.kt b/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/FakeData.kt
index 904dbc29e79..e125a6cfa0f 100644
--- a/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/FakeData.kt
+++ b/feature/funding/googleplay/src/debug/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/FakeData.kt
@@ -6,69 +6,109 @@ import kotlinx.collections.immutable.persistentListOf

 internal object FakeData {

+    val recurringContribution = RecurringContribution(
+        id = "monthly_subscription_50",
+        title = "Monthly subscription: $50",
+        description = "Monthly subscription for $50",
+        price = 5000L,
+        priceFormatted = "50 $",
+    )
+
     val recurringContributions = persistentListOf(
         RecurringContribution(
             id = "monthly_subscription_250",
             title = "Monthly subscription: $250",
-            price = "$250",
+            description = "Monthly subscription for $250",
+            price = 25000L,
+            priceFormatted = "250 $",
         ),
         RecurringContribution(
             id = "monthly_subscription_140",
             title = "Monthly subscription: $140",
-            price = "$140",
+            description = "Monthly subscription for $140",
+            price = 14000L,
+            priceFormatted = "140 $",
         ),
         RecurringContribution(
             id = "monthly_subscription_80",
             title = "Monthly subscription: $80",
-            price = "$80",
+            description = "Monthly subscription for $80",
+            price = 8000L,
+            priceFormatted = "80 $",
         ),
         RecurringContribution(
             id = "monthly_subscription_50",
             title = "Monthly subscription: $50",
-            price = "$50",
+            description = "Monthly subscription for $50",
+            price = 5000L,
+            priceFormatted = "50 $",
         ),
         RecurringContribution(
             id = "monthly_subscription_25",
             title = "Monthly subscription: $25",
-            price = "$25",
+            description = "Monthly subscription for $25",
+            price = 2500L,
+            priceFormatted = "25 $",
         ),
         RecurringContribution(
             id = "monthly_subscription_15",
             title = "Monthly subscription: $15",
-            price = "$15",
+            description = "Monthly subscription for $15",
+            price = 1500L,
+            priceFormatted = "15 $",
         ),
     )

+    val oneTimeContribution = OneTimeContribution(
+        id = "one_time_payment_50",
+        title = "One time payment: $50",
+        description = "One time payment for $50",
+        price = 5000L,
+        priceFormatted = "50 $",
+    )
+
     val oneTimeContributions = persistentListOf(
         OneTimeContribution(
             id = "one_time_payment_250",
             title = "One time payment: $250",
-            price = "$250",
+            description = "One time payment for $250",
+            price = 25000L,
+            priceFormatted = "250 $",
         ),
         OneTimeContribution(
             id = "one_time_payment_140",
             title = "One time payment: $140",
-            price = "$140",
+            description = "One time payment for $140",
+            price = 14000L,
+            priceFormatted = "140 $",
         ),
         OneTimeContribution(
             id = "one_time_payment_80",
             title = "One time payment: $80",
-            price = "$80",
+            description = "One time payment for $80",
+            price = 8000L,
+            priceFormatted = "80 $",
         ),
         OneTimeContribution(
             id = "one_time_payment_50",
             title = "One time payment: $50",
-            price = "$50",
+            description = "One time payment for $50",
+            price = 5000L,
+            priceFormatted = "50 $",
         ),
         OneTimeContribution(
             id = "one_time_payment_25",
             title = "One time payment: $25",
-            price = "$25",
+            description = "One time payment for $25",
+            price = 2500L,
+            priceFormatted = "25 $",
         ),
         OneTimeContribution(
             id = "one_time_payment_15",
             title = "One time payment: $15",
-            price = "$15",
+            description = "One time payment for $15",
+            price = 1500L,
+            priceFormatted = "15 $",
         ),
     )
 }
diff --git a/feature/funding/googleplay/src/test/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionScreenKtTest.kt b/feature/funding/googleplay/src/test/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionScreenKtTest.kt
index 7d525ff92ab..02c978b15ee 100644
--- a/feature/funding/googleplay/src/test/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionScreenKtTest.kt
+++ b/feature/funding/googleplay/src/test/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionScreenKtTest.kt
@@ -1,6 +1,10 @@
 package app.k9mail.feature.funding.googleplay.ui.contribution

+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.test.performClick
+import app.k9mail.core.ui.compose.common.activity.LocalActivity
 import app.k9mail.core.ui.compose.testing.ComposeTest
+import app.k9mail.core.ui.compose.testing.onNodeWithTag
 import app.k9mail.core.ui.compose.testing.pressBack
 import app.k9mail.core.ui.compose.testing.setContentWithTheme
 import app.k9mail.feature.funding.googleplay.ui.contribution.ContributionContract.State
@@ -8,20 +12,23 @@ import assertk.assertThat
 import assertk.assertions.isEqualTo
 import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
+import org.mockito.Mockito.mock

 internal class ContributionScreenKtTest : ComposeTest() {

     @Test
-    fun `should observe state and send events to view model`() = runTest {
+    fun `should call onBack when back button is pressed`() = runTest {
         val initialState = State()
         val viewModel = FakeContributionViewModel(initialState)
         var onBackCounter = 0

         setContentWithTheme {
-            ContributionScreen(
-                onBack = { onBackCounter++ },
-                viewModel = viewModel,
-            )
+            CompositionLocalProvider(LocalActivity provides mock()) {
+                ContributionScreen(
+                    onBack = { onBackCounter++ },
+                    viewModel = viewModel,
+                )
+            }
         }

         assertThat(onBackCounter).isEqualTo(0)
@@ -29,5 +36,9 @@ internal class ContributionScreenKtTest : ComposeTest() {
         pressBack()

         assertThat(onBackCounter).isEqualTo(1)
+
+        onNodeWithTag("TopAppBarBackButton").performClick()
+
+        assertThat(onBackCounter).isEqualTo(2)
     }
 }
diff --git a/feature/funding/googleplay/src/test/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionViewModelTest.kt b/feature/funding/googleplay/src/test/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionViewModelTest.kt
new file mode 100644
index 00000000000..5bb02a5b7f9
--- /dev/null
+++ b/feature/funding/googleplay/src/test/kotlin/app/k9mail/feature/funding/googleplay/ui/contribution/ContributionViewModelTest.kt
@@ -0,0 +1,118 @@
+package app.k9mail.feature.funding.googleplay.ui.contribution
+
+import app.k9mail.core.ui.compose.testing.MainDispatcherRule
+import app.k9mail.core.ui.compose.testing.mvi.MviContext
+import app.k9mail.core.ui.compose.testing.mvi.MviTurbines
+import app.k9mail.core.ui.compose.testing.mvi.runMviTest
+import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
+import app.k9mail.feature.funding.googleplay.domain.entity.Contribution
+import app.k9mail.feature.funding.googleplay.ui.contribution.ContributionContract.Event
+import app.k9mail.feature.funding.googleplay.ui.contribution.ContributionContract.State
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import kotlin.test.Test
+import org.junit.Rule
+
+class ContributionViewModelTest {
+
+    @get:Rule
+    val mainDispatcherRule = MainDispatcherRule()
+
+    @Test
+    fun `should change selected contribution and selected type when on time contribution selected`() = runMviTest {
+        val initialState = State(
+            isRecurringContributionSelected = true,
+            oneTimeContributions = FakeData.oneTimeContributions,
+        )
+
+        contributionRobot(initialState) {
+            selectOneTimeContribution()
+            verifyOneTimeContributionSelected()
+        }
+    }
+
+    @Test
+    fun `should change selected contribution and selected type when recurring contribution selected`() = runMviTest {
+        val initialState = State(
+            isRecurringContributionSelected = false,
+            recurringContributions = FakeData.recurringContributions,
+        )
+
+        contributionRobot(initialState) {
+            selectRecurringContribution()
+            verifyRecurringContributionSelected()
+        }
+    }
+
+    @Test
+    fun `should change selected contribution when contribution item clicked`() = runMviTest {
+        val initialState = State(
+            isRecurringContributionSelected = true,
+            recurringContributions = FakeData.recurringContributions,
+        )
+        val selectedContribution = FakeData.recurringContributions[2]
+
+        contributionRobot(initialState) {
+            selectContributionItem(selectedContribution)
+            verifyContributionItemSelected(selectedContribution)
+        }
+    }
+}
+
+private suspend fun MviContext.contributionRobot(
+    initialState: State = State(),
+    interaction: suspend ContributionRobot.() -> Unit,
+) = ContributionRobot(this, initialState).apply {
+    initialize()
+    interaction()
+}
+
+private class ContributionRobot(
+    private val mviContext: MviContext,
+    private val initialState: State = State(),
+) {
+    private val viewModel = ContributionViewModel(initialState)
+    private lateinit var turbines: MviTurbines<State, Nothing>
+
+    suspend fun initialize() {
+        turbines = mviContext.turbinesWithInitialStateCheck(viewModel, initialState)
+    }
+
+    fun selectOneTimeContribution() {
+        viewModel.event(Event.OnOneTimeContributionSelected)
+    }
+
+    suspend fun verifyOneTimeContributionSelected() {
+        assertThat(turbines.awaitStateItem()).isEqualTo(
+            initialState.copy(
+                isRecurringContributionSelected = false,
+                selectedContribution = initialState.oneTimeContributions.first(),
+            ),
+        )
+    }
+
+    fun selectRecurringContribution() {
+        viewModel.event(Event.OnRecurringContributionSelected)
+    }
+
+    suspend fun verifyRecurringContributionSelected() {
+        assertThat(turbines.awaitStateItem()).isEqualTo(
+            initialState.copy(
+                isRecurringContributionSelected = true,
+                selectedContribution = initialState.recurringContributions.first(),
+            ),
+        )
+    }
+
+    fun selectContributionItem(item: Contribution) {
+        viewModel.event(Event.OnContributionItemClicked(item))
+    }
+
+    suspend fun verifyContributionItemSelected(item: Contribution) {
+        assertThat(turbines.awaitStateItem()).isEqualTo(
+            initialState.copy(
+                selectedContribution = item,
+            ),
+        )
+    }
+}


fix(logs): debug logs ignored when user enable it

Fixes #9561.

- Fixes the "Enable debug logging" preference not logging any useful data outside debuggable builds
- Refactor:
  - Move `CompositeLogSinkManager` to `:core:logging:api`
  - Replaced hardcoded string qualifier `syncDebug` with the `LogSync.Qualifier.SyncDebug` enum qualifier for improved discoverability.

Debug logging does not include IMAP/POP3 commands in beta versions
### Checklist

- [x] I have used the search function to see if someone else has already submitted the same bug report.
- [x] I will describe the problem with as much detail as possible.

### App

Thunderbird for Android

### App version

12.0b4

### Where did you get the app from?

Other

### Android version

13 (xiaomi.eu 14.0.3.0.TJUMIXM)

### Device model

Xiaomi POCO X3 Pro (global)

### Steps to reproduce

1. Go to Settings -> "General settings" -> Debugging
2. Enable all options (debug logging, sync debug logging, log sensitive information)
3. Refresh a folder
4. Export logs or observe logcat output

### Expected behavior

Logs include IMAP/POP3 commands sent to the mail server and received responses, as well as other debugging information (number of unsynced messages, etc.)

### Actual behavior

There is no useful debugging information in debug logs (see below for an example). This makes debugging problems with message synchronisation much harder.

This problem does not exist in debug builds, but most people who submit bug reports will most likely run a release version. In my case a debug build (from beta branch) synchronises correctly, but the release (12.0b4) downloaded from archive.mozilla.org doesn't, so I only have debug logs from a version where everything is ok...

#9250 introduced logging changes that prevented messages with level lower than INFO (VERBOSE and DEBUG) from being logged at all. #9359 changed the minimum log level to VERBOSE, but only for debug builds. Most log messages enabled by the "Enable debug logging" option are DEBUG or VERBOSE, so enabling this option does almost nothing.

### Logs

```
08-04 14:22:58.350 32664 14487 I MessagingController: Running command 'synchronizeMailbox', seq = 119 (background priority)
08-04 14:22:58.352 32664 14487 I ImapSync: Synchronizing folder konto.pl:INBOX
08-04 14:22:58.731 32664 14458 I rd.android.beta: This is non sticky GC, maxfree is 8388608 minfree is 2097152
08-04 14:23:03.167 32664 14458 I rd.android.beta: This is sticky GC, maxfree is 8388608 minfree is 2097152
08-04 14:23:03.668 32664 14487 I ImapSync: Done synchronizing folder konto.pl:INBOX
08-04 14:23:03.668 32664 14487 I MessagingController:  Command 'synchronizeMailbox' completed
08-04 14:23:03.668 32664 14487 I MessagingController: Running command 'sendPendingMessages', seq = 120 (background priority)
08-04 14:23:03.669 32664 14487 I MessagingController:  Command 'sendPendingMessages' completed
```

diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/core/AppCommonCoreModule.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/core/AppCommonCoreModule.kt
index 92e1c60d6b3..c9d1d85c7d9 100644
--- a/app-common/src/main/kotlin/net/thunderbird/app/common/core/AppCommonCoreModule.kt
+++ b/app-common/src/main/kotlin/net/thunderbird/app/common/core/AppCommonCoreModule.kt
@@ -2,9 +2,13 @@ package net.thunderbird.app.common.core

 import android.content.Context
 import kotlin.time.ExperimentalTime
-import net.thunderbird.app.common.BuildConfig
+import net.thunderbird.app.common.core.logging.DefaultLogLevelManager
+import net.thunderbird.core.common.inject.getList
+import net.thunderbird.core.common.inject.singleListOf
 import net.thunderbird.core.logging.DefaultLogger
 import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelManager
+import net.thunderbird.core.logging.LogLevelProvider
 import net.thunderbird.core.logging.LogSink
 import net.thunderbird.core.logging.Logger
 import net.thunderbird.core.logging.composite.CompositeLogSink
@@ -13,25 +17,22 @@ import net.thunderbird.core.logging.file.AndroidFileSystemManager
 import net.thunderbird.core.logging.file.FileLogSink
 import org.koin.core.module.Module
 import org.koin.core.qualifier.named
+import org.koin.dsl.bind
 import org.koin.dsl.module

 val appCommonCoreModule: Module = module {
-    single<LogLevel> {
-        if (BuildConfig.DEBUG) LogLevel.VERBOSE else LogLevel.INFO
-    }
+    single<LogLevelManager> {
+        DefaultLogLevelManager()
+    }.bind<LogLevelProvider>()

-    single<List<LogSink>> {
-        listOf(
-            ConsoleLogSink(
-                level = get(),
-            ),
-        )
-    }
+    singleListOf<LogSink>(
+        { ConsoleLogSink(level = LogLevel.VERBOSE) },
+    )

     single<CompositeLogSink> {
         CompositeLogSink(
-            level = get(),
-            sinks = get(),
+            logLevelProvider = get(),
+            sinks = getList(),
         )
     }

@@ -44,8 +45,8 @@ val appCommonCoreModule: Module = module {

     single<CompositeLogSink>(named(SYNC_DEBUG_LOG)) {
         CompositeLogSink(
-            level = get(),
-            sinks = get(),
+            logLevelProvider = get(),
+            sinks = getList(),
         )
     }

@@ -58,7 +59,7 @@ val appCommonCoreModule: Module = module {
         )
     }

-    single<Logger> (named(SYNC_DEBUG_LOG)) {
+    single<Logger>(named(SYNC_DEBUG_LOG)) {
         @OptIn(ExperimentalTime::class)
         DefaultLogger(
             sink = get<CompositeLogSink>(named(SYNC_DEBUG_LOG)),
diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/core/logging/DefaultLogLevelManager.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/core/logging/DefaultLogLevelManager.kt
new file mode 100644
index 00000000000..9dc4951226b
--- /dev/null
+++ b/app-common/src/main/kotlin/net/thunderbird/app/common/core/logging/DefaultLogLevelManager.kt
@@ -0,0 +1,22 @@
+package net.thunderbird.app.common.core.logging
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import net.thunderbird.app.common.BuildConfig
+import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelManager
+
+class DefaultLogLevelManager : LogLevelManager {
+    private val defaultLevel = if (BuildConfig.DEBUG) LogLevel.VERBOSE else LogLevel.INFO
+    private val logLevel = MutableStateFlow(defaultLevel)
+
+    override fun override(level: LogLevel) {
+        logLevel.update { level }
+    }
+
+    override fun restoreDefault() {
+        override(defaultLevel)
+    }
+
+    override fun current(): LogLevel = logLevel.value
+}
diff --git a/core/common/src/commonMain/kotlin/net/thunderbird/core/common/inject/KoinMultibindingCollection.kt b/core/common/src/commonMain/kotlin/net/thunderbird/core/common/inject/KoinMultibindingCollection.kt
new file mode 100644
index 00000000000..4263f311885
--- /dev/null
+++ b/core/common/src/commonMain/kotlin/net/thunderbird/core/common/inject/KoinMultibindingCollection.kt
@@ -0,0 +1,64 @@
+package net.thunderbird.core.common.inject
+
+import org.koin.core.definition.Definition
+import org.koin.core.module.KoinDslMarker
+import org.koin.core.module.Module
+import org.koin.core.parameter.parametersOf
+import org.koin.core.qualifier.Qualifier
+import org.koin.core.qualifier.named
+import org.koin.core.scope.Scope
+
+// This file must be deleted once https://github.com/InsertKoinIO/koin/pull/1951 is merged to
+// Koin and released on 4.2.0
+
+/**
+ * Defines a singleton list of elements of type [T].
+ *
+ * This function creates a singleton definition for a mutable list of elements.
+ * Each element in the list is resolved from the provided [items] definitions.
+ *
+ * @param T The type of elements in the list.
+ * @param items Vararg of [Definition]s that will be resolved and added to the list.
+ * @param qualifier Optional [Qualifier] to distinguish this list from others of the same type.
+ *                  If null, a default qualifier based on the type [T] will be used.
+ */
+@KoinDslMarker
+inline fun <reified T> Module.singleListOf(vararg items: Definition<T>, qualifier: Qualifier? = null) {
+    single(qualifier ?: defaultListQualifier<T>(), createdAtStart = true) {
+        items.map { definition -> definition(this, parametersOf()) }
+    }
+}
+
+/**
+ * Resolves a [List] of instances of type [T].
+ * This is a helper function for Koin's multibinding feature.
+ *
+ * It uses the [defaultListQualifier] if no [qualifier] is provided.
+ *
+ * @param T The type of instances in the list.
+ * @param qualifier An optional [Qualifier] to distinguish between different lists of the same type.
+ * @return The resolved [MutableList] of instances of type [T].
+ */
+inline fun <reified T> Scope.getList(qualifier: Qualifier? = null) =
+    get<List<T>>(qualifier ?: defaultListQualifier<T>())
+
+/**
+ * Creates a qualifier for a set of a specific type.
+ *
+ * This is used to differentiate between different sets of the same type when injecting dependencies.
+ *
+ * @param T The type of the elements in the set.
+ * @return A qualifier for the set.
+ */
+inline fun <reified T> defaultListQualifier() =
+    defaultCollectionQualifier<List<T>, T>()
+
+/**
+ * Creates a default [Qualifier] for a collection binding.
+ *
+ * @param TCollection The type of the collection (e.g., `List`, `List`).
+ * @param T The type of the elements in the collection.
+ * @return A [Qualifier] that can be used to identify the specific collection binding.
+ */
+inline fun <reified TCollection : Collection<T>, reified T> defaultCollectionQualifier() =
+    named("${TCollection::class.qualifiedName}<${T::class.qualifiedName}>")
diff --git a/core/logging/api/src/commonMain/kotlin/net/thunderbird/core/logging/LogLevelManager.kt b/core/logging/api/src/commonMain/kotlin/net/thunderbird/core/logging/LogLevelManager.kt
new file mode 100644
index 00000000000..1391f4352b6
--- /dev/null
+++ b/core/logging/api/src/commonMain/kotlin/net/thunderbird/core/logging/LogLevelManager.kt
@@ -0,0 +1,28 @@
+package net.thunderbird.core.logging
+
+/**
+ * Manages the log level for the application.
+ *
+ * This interface provides a way to update the log level dynamically.
+ * Implementations of this interface are responsible for persisting the log level
+ * and notifying listeners of changes.
+ */
+interface LogLevelManager : LogLevelProvider {
+    /**
+     * Overrides the current log level.
+     *
+     * This function allows for a temporary change in the log level,
+     * typically for debugging or specific operational needs.
+     * The original log level can be restored by calling [restoreDefault] function
+     *
+     * @param level The new log level to set
+     */
+    fun override(level: LogLevel)
+
+    /**
+     * Restores the log level to its default value.
+     *
+     * The default log level is defined by the specific implementation of this interface.
+     */
+    fun restoreDefault()
+}
diff --git a/core/logging/api/src/commonMain/kotlin/net/thunderbird/core/logging/LogLevelProvider.kt b/core/logging/api/src/commonMain/kotlin/net/thunderbird/core/logging/LogLevelProvider.kt
new file mode 100644
index 00000000000..34e46be9272
--- /dev/null
+++ b/core/logging/api/src/commonMain/kotlin/net/thunderbird/core/logging/LogLevelProvider.kt
@@ -0,0 +1,15 @@
+package net.thunderbird.core.logging
+
+/**
+ * Provides the current [LogLevel].
+ *
+ * This can be used to dynamically change the log level during runtime.
+ */
+fun interface LogLevelProvider {
+    /**
+     * Gets the current log level.
+     *
+     * @return The current log level.
+     */
+    fun current(): LogLevel
+}
diff --git a/core/logging/impl-composite/src/commonMain/kotlin/net/thunderbird/core/logging/composite/CompositeLogSink.kt b/core/logging/impl-composite/src/commonMain/kotlin/net/thunderbird/core/logging/composite/CompositeLogSink.kt
index 5db2a7ba766..77b981daa68 100644
--- a/core/logging/impl-composite/src/commonMain/kotlin/net/thunderbird/core/logging/composite/CompositeLogSink.kt
+++ b/core/logging/impl-composite/src/commonMain/kotlin/net/thunderbird/core/logging/composite/CompositeLogSink.kt
@@ -1,6 +1,7 @@
 package net.thunderbird.core.logging.composite

 import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelProvider
 import net.thunderbird.core.logging.LogSink

 /**
@@ -22,15 +23,15 @@ interface CompositeLogSink : LogSink {
 /**
  * Creates a [CompositeLogSink] with the specified log level and manager.
  *
- * @param level The minimum [LogLevel] for messages to be logged.
+ * @param logLevelProvider The minimum [LogLevel] for messages to be logged.
  * @param manager The [CompositeLogSinkManager] that manages the collection of sinks.
  * @param sinks A list of [LogSink] instances to be managed by this composite sink.
  * @return A new instance of [CompositeLogSink].
  */
 fun CompositeLogSink(
-    level: LogLevel,
+    logLevelProvider: LogLevelProvider,
     manager: CompositeLogSinkManager = DefaultLogSinkManager(),
     sinks: List<LogSink> = emptyList(),
 ): CompositeLogSink {
-    return DefaultCompositeLogSink(level, manager, sinks)
+    return DefaultCompositeLogSink(logLevelProvider, manager, sinks)
 }
diff --git a/core/logging/impl-composite/src/commonMain/kotlin/net/thunderbird/core/logging/composite/DefaultCompositeLogSink.kt b/core/logging/impl-composite/src/commonMain/kotlin/net/thunderbird/core/logging/composite/DefaultCompositeLogSink.kt
index 481cc27e51f..5f398aed402 100644
--- a/core/logging/impl-composite/src/commonMain/kotlin/net/thunderbird/core/logging/composite/DefaultCompositeLogSink.kt
+++ b/core/logging/impl-composite/src/commonMain/kotlin/net/thunderbird/core/logging/composite/DefaultCompositeLogSink.kt
@@ -2,13 +2,15 @@ package net.thunderbird.core.logging.composite

 import net.thunderbird.core.logging.LogEvent
 import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelProvider
 import net.thunderbird.core.logging.LogSink

 internal class DefaultCompositeLogSink(
-    override val level: LogLevel,
+    private val logLevelProvider: LogLevelProvider,
     override val manager: CompositeLogSinkManager = DefaultLogSinkManager(),
     sinks: List<LogSink> = emptyList(),
 ) : CompositeLogSink {
+    override val level: LogLevel get() = logLevelProvider.current()

     init {
         manager.addAll(sinks)
diff --git a/core/preference/impl/src/commonMain/kotlin/net/thunderbird/core/preference/debugging/DefaultDebuggingSettingsPreferenceManager.kt b/core/preference/impl/src/commonMain/kotlin/net/thunderbird/core/preference/debugging/DefaultDebuggingSettingsPreferenceManager.kt
index 44756a57923..77cb628c942 100644
--- a/core/preference/impl/src/commonMain/kotlin/net/thunderbird/core/preference/debugging/DefaultDebuggingSettingsPreferenceManager.kt
+++ b/core/preference/impl/src/commonMain/kotlin/net/thunderbird/core/preference/debugging/DefaultDebuggingSettingsPreferenceManager.kt
@@ -10,6 +10,8 @@ import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
+import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelManager
 import net.thunderbird.core.logging.Logger
 import net.thunderbird.core.preference.storage.Storage
 import net.thunderbird.core.preference.storage.StorageEditor
@@ -20,6 +22,7 @@ class DefaultDebuggingSettingsPreferenceManager(
     private val logger: Logger,
     private val storage: Storage,
     private val storageEditor: StorageEditor,
+    private val logLevelManager: LogLevelManager,
     private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
     private var scope: CoroutineScope = CoroutineScope(SupervisorJob()),
 ) : DebuggingSettingsPreferenceManager {
@@ -32,7 +35,7 @@ class DefaultDebuggingSettingsPreferenceManager(
     override fun save(config: DebuggingSettings) {
         logger.debug(TAG) { "save() called with: config = $config" }
         writeConfig(config)
-        configState.update { config }
+        configState.update { config.also(::updateDebugLogLevel) }
     }

     private fun loadConfig(): DebuggingSettings = DebuggingSettings(
@@ -44,7 +47,7 @@ class DefaultDebuggingSettingsPreferenceManager(
             KEY_ENABLE_SYNC_DEBUG_LOGGING,
             DEBUGGING_SETTINGS_DEFAULT_IS_SYNC_LOGGING_ENABLED,
         ),
-    )
+    ).also(::updateDebugLogLevel)

     private fun writeConfig(config: DebuggingSettings) {
         logger.debug(TAG) { "writeConfig() called with: config = $config" }
@@ -58,4 +61,12 @@ class DefaultDebuggingSettingsPreferenceManager(
             }
         }
     }
+
+    private fun updateDebugLogLevel(config: DebuggingSettings) {
+        if (config.isDebugLoggingEnabled) {
+            logLevelManager.override(LogLevel.DEBUG)
+        } else {
+            logLevelManager.restoreDefault()
+        }
+    }
 }
diff --git a/legacy/core/src/main/java/com/fsck/k9/preferences/KoinModule.kt b/legacy/core/src/main/java/com/fsck/k9/preferences/KoinModule.kt
index 2cda0b84a69..52c4d1fc170 100644
--- a/legacy/core/src/main/java/com/fsck/k9/preferences/KoinModule.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/preferences/KoinModule.kt
@@ -71,6 +71,7 @@ val preferencesModule = module {
             logger = get(),
             storage = get<Preferences>().storage,
             storageEditor = get<Preferences>().createStorageEditor(),
+            logLevelManager = get(),
         )
     }
     single<DebugLogConfigurator> {

diff --git a/core/logging/impl-composite/src/commonTest/kotlin/net/thunderbird/core/logging/composite/DefaultCompositeLogSinkTest.kt b/core/logging/impl-composite/src/commonTest/kotlin/net/thunderbird/core/logging/composite/DefaultCompositeLogSinkTest.kt
index ec3fed6ff8d..7b2f8aae487 100644
--- a/core/logging/impl-composite/src/commonTest/kotlin/net/thunderbird/core/logging/composite/DefaultCompositeLogSinkTest.kt
+++ b/core/logging/impl-composite/src/commonTest/kotlin/net/thunderbird/core/logging/composite/DefaultCompositeLogSinkTest.kt
@@ -19,7 +19,7 @@ class DefaultCompositeLogSinkTest {

         // Act
         DefaultCompositeLogSink(
-            level = LogLevel.INFO,
+            logLevelProvider = { LogLevel.INFO },
             manager = sinkManager,
             sinks = listOf(sink1, sink2),
         )
@@ -38,7 +38,7 @@ class DefaultCompositeLogSinkTest {
         val sinkManager = FakeCompositeLogSinkManager(mutableListOf(sink1, sink2))

         val testSubject = DefaultCompositeLogSink(
-            level = LogLevel.INFO,
+            logLevelProvider = { LogLevel.INFO },
             manager = sinkManager,
         )

@@ -60,7 +60,7 @@ class DefaultCompositeLogSinkTest {
         val sinkManager = FakeCompositeLogSinkManager(mutableListOf(sink1, sink2))

         val testSubject = DefaultCompositeLogSink(
-            level = LogLevel.WARN,
+            logLevelProvider = { LogLevel.WARN },
             manager = sinkManager,
         )

@@ -80,7 +80,7 @@ class DefaultCompositeLogSinkTest {
         val sinkManager = FakeCompositeLogSinkManager(mutableListOf(sink1, sink2))

         val testSubject = DefaultCompositeLogSink(
-            level = LogLevel.INFO,
+            logLevelProvider = { LogLevel.INFO },
             manager = sinkManager,
         )

diff --git a/core/logging/testing/src/commonMain/kotlin/net/thunderbird/core/logging/testing/TestLogLevelManager.kt b/core/logging/testing/src/commonMain/kotlin/net/thunderbird/core/logging/testing/TestLogLevelManager.kt
new file mode 100644
index 00000000000..97b73330fe2
--- /dev/null
+++ b/core/logging/testing/src/commonMain/kotlin/net/thunderbird/core/logging/testing/TestLogLevelManager.kt
@@ -0,0 +1,17 @@
+package net.thunderbird.core.logging.testing
+
+import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelManager
+
+class TestLogLevelManager : LogLevelManager {
+    var logLevel = LogLevel.VERBOSE
+    override fun override(level: LogLevel) {
+        logLevel = level
+    }
+
+    override fun restoreDefault() {
+        logLevel = LogLevel.VERBOSE
+    }
+
+    override fun current(): LogLevel = logLevel
+}
diff --git a/legacy/core/src/test/java/com/fsck/k9/TestApp.kt b/legacy/core/src/test/java/com/fsck/k9/TestApp.kt
index c3772111eae..5d005e62b95 100644
--- a/legacy/core/src/test/java/com/fsck/k9/TestApp.kt
+++ b/legacy/core/src/test/java/com/fsck/k9/TestApp.kt
@@ -17,15 +17,19 @@ import net.thunderbird.core.featureflag.FeatureFlag
 import net.thunderbird.core.featureflag.FeatureFlagProvider
 import net.thunderbird.core.featureflag.InMemoryFeatureFlagProvider
 import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelManager
+import net.thunderbird.core.logging.LogLevelProvider
 import net.thunderbird.core.logging.Logger
 import net.thunderbird.core.logging.composite.CompositeLogSink
 import net.thunderbird.core.logging.composite.CompositeLogSinkManager
 import net.thunderbird.core.logging.file.FileLogSink
 import net.thunderbird.core.logging.legacy.Log
+import net.thunderbird.core.logging.testing.TestLogLevelManager
 import net.thunderbird.core.logging.testing.TestLogger
 import net.thunderbird.core.preference.storage.StoragePersister
 import net.thunderbird.legacy.core.FakeAccountDefaultsProvider
 import org.koin.core.qualifier.named
+import org.koin.dsl.bind
 import org.koin.dsl.module
 import org.mockito.kotlin.mock

@@ -52,7 +56,7 @@ class TestApp : Application() {
         val fileSink: FileLogSink = mock<FileLogSink>()

         val compositeSink: CompositeLogSink = CompositeLogSink(
-            level = LogLevel.DEBUG,
+            logLevelProvider = { LogLevel.DEBUG },
             manager = sinkManager,
             sinks = listOf(fileSink),
         )
@@ -61,6 +65,7 @@ class TestApp : Application() {

 val testModule = module {
     single<Logger> { TestApp.logger }
+    single<LogLevelManager> { TestLogLevelManager() }.bind<LogLevelProvider>()
     single(named("syncDebug")) { TestApp.logger }
     single(named("syncDebug")) { TestApp.compositeSink }
     single(named("syncDebug")) { TestApp.fileSink }
diff --git a/legacy/storage/src/test/java/com/fsck/k9/storage/TestApp.kt b/legacy/storage/src/test/java/com/fsck/k9/storage/TestApp.kt
index f7f9bf7d323..943cc1be6e1 100644
--- a/legacy/storage/src/test/java/com/fsck/k9/storage/TestApp.kt
+++ b/legacy/storage/src/test/java/com/fsck/k9/storage/TestApp.kt
@@ -17,14 +17,18 @@ import net.thunderbird.core.featureflag.FeatureFlag
 import net.thunderbird.core.featureflag.FeatureFlagProvider
 import net.thunderbird.core.featureflag.InMemoryFeatureFlagProvider
 import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelManager
+import net.thunderbird.core.logging.LogLevelProvider
 import net.thunderbird.core.logging.Logger
 import net.thunderbird.core.logging.composite.CompositeLogSink
 import net.thunderbird.core.logging.composite.CompositeLogSinkManager
 import net.thunderbird.core.logging.file.FileLogSink
 import net.thunderbird.core.logging.legacy.Log
+import net.thunderbird.core.logging.testing.TestLogLevelManager
 import net.thunderbird.core.logging.testing.TestLogger
 import net.thunderbird.core.preference.storage.StoragePersister
 import org.koin.core.qualifier.named
+import org.koin.dsl.bind
 import org.koin.dsl.module
 import org.mockito.kotlin.mock

@@ -50,7 +54,7 @@ class TestApp : Application() {
         val fileSink: FileLogSink = mock<FileLogSink>()

         val compositeSink: CompositeLogSink = CompositeLogSink(
-            level = LogLevel.DEBUG,
+            logLevelProvider = { LogLevel.DEBUG },
             manager = sinkManager,
             sinks = listOf(fileSink),
         )
@@ -59,6 +63,7 @@ class TestApp : Application() {

 val testModule = module {
     single<Logger> { TestApp.logger }
+    single<LogLevelManager> { TestLogLevelManager() }.bind<LogLevelProvider>()
     single(named("syncDebug")) { TestApp.logger }
     single(named("syncDebug")) { TestApp.compositeSink }
     single(named("syncDebug")) { TestApp.fileSink }
diff --git a/legacy/ui/legacy/src/test/java/com/fsck/k9/TestApp.kt b/legacy/ui/legacy/src/test/java/com/fsck/k9/TestApp.kt
index 644112cd9ae..1fb618518ca 100644
--- a/legacy/ui/legacy/src/test/java/com/fsck/k9/TestApp.kt
+++ b/legacy/ui/legacy/src/test/java/com/fsck/k9/TestApp.kt
@@ -10,14 +10,18 @@ import net.thunderbird.core.featureflag.FeatureFlag
 import net.thunderbird.core.featureflag.FeatureFlagProvider
 import net.thunderbird.core.featureflag.InMemoryFeatureFlagProvider
 import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelManager
+import net.thunderbird.core.logging.LogLevelProvider
 import net.thunderbird.core.logging.Logger
 import net.thunderbird.core.logging.composite.CompositeLogSink
 import net.thunderbird.core.logging.composite.CompositeLogSinkManager
 import net.thunderbird.core.logging.file.FileLogSink
 import net.thunderbird.core.logging.legacy.Log
+import net.thunderbird.core.logging.testing.TestLogLevelManager
 import net.thunderbird.core.logging.testing.TestLogger
 import net.thunderbird.core.preference.storage.StoragePersister
 import org.koin.core.qualifier.named
+import org.koin.dsl.bind
 import org.koin.dsl.module
 import org.mockito.Mockito.mock

@@ -44,7 +48,7 @@ class TestApp : Application() {
         val fileSink: FileLogSink = org.mockito.kotlin.mock<FileLogSink>()

         val compositeSink: CompositeLogSink = CompositeLogSink(
-            level = LogLevel.DEBUG,
+            logLevelProvider = { LogLevel.DEBUG },
             manager = sinkManager,
             sinks = listOf(fileSink),
         )
@@ -53,6 +57,7 @@ class TestApp : Application() {

 val testModule = module {
     single<Logger> { TestApp.logger }
+    single<LogLevelManager> { TestLogLevelManager() }.bind<LogLevelProvider>()
     single(named("syncDebug")) { TestApp.logger }
     single(named("syncDebug")) { TestApp.compositeSink }
     single(named("syncDebug")) { TestApp.fileSink }


[Jetpack Social] Hide Jetpack Social container if PostStatus is PRIVATE

Fixes #18927

To test:
You should not be able to reproduce the bug anymore. Steps to reproduce can be found in https://github.com/wordpress-mobile/WordPress-Android/issues/18927

## Regression Notes
1. Potential unintended areas of impact
Jetpack Social containers should continue to work as expected.

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

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

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)

Jetpack Social section is visible on private posts
Refs pctCYC-W4-p2#comment-989

### Expected behavior
The Jetpack Social section should be hidden from the pre-publishing sheet and post settings when the post is set to private.

### Actual behavior
The Jetpack Social section is shown for private posts, but the post is not auto-shared.

### Steps to reproduce the behavior

- Launch the Jetpack app.
- Create a new blog post.
- Set it to private.
- Observe that the Jetpack Social section is visible in post settings and pre-publishing sheet.

##### Tested on Jetpack Android 23.0-rc-1

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java
index fcc44f840f07..17a68c9c050c 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java
@@ -938,6 +938,7 @@ void updatePostStatus(PostStatus postStatus) {
                     postImmutableModel -> {
                         updatePostStatusRelatedViews(postImmutableModel);
                         updateSaveButton();
+                        mJetpackSocialViewModel.onPostStatusChanged();
                         return null;
                     });
         }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorJetpackSocialViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorJetpackSocialViewModel.kt
index a2eaf0135a5e..8716b4e72dff 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorJetpackSocialViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorJetpackSocialViewModel.kt
@@ -85,7 +85,11 @@ class EditorJetpackSocialViewModel @Inject constructor(
         this.siteModel = siteModel
         this.editPostRepository = editPostRepository

-        if (jetpackSocialFeatureConfig.isEnabled() && !editPostRepository.isPage) {
+        startJetpackSocial()
+    }
+
+    private fun startJetpackSocial() {
+        if (shouldShowJetpackSocial()) {
             launch {
                 shareLimit = getJetpackSocialShareLimitStatusUseCase.execute(siteModel)
                 loadConnections()
@@ -164,8 +168,7 @@ class EditorJetpackSocialViewModel @Inject constructor(
     }

     private suspend fun loadJetpackSocialIfSupported() {
-        val showJetpackSocial = jetpackSocialFeatureConfig.isEnabled() && siteModel.supportsPublicize()
-        if (!showJetpackSocial) {
+        if (!shouldShowJetpackSocial()) {
             _jetpackSocialContainerVisibility.postValue(JetpackSocialContainerVisibility.ALL_HIDDEN)
             return
         }
@@ -187,6 +190,11 @@ class EditorJetpackSocialViewModel @Inject constructor(
         }
     }

+    private fun shouldShowJetpackSocial() = jetpackSocialFeatureConfig.isEnabled()
+            && !editPostRepository.isPage
+            && siteModel.supportsPublicize()
+            && currentPost?.status != PostStatus.PRIVATE.toString()
+
     private suspend fun mapLoaded(): JetpackSocialUiState.Loaded {
         val shareMessage = jetpackSocialShareMessage.ifEmpty {
             getJetpackSocialShareMessageUseCase.execute(currentPost?.id ?: -1)
@@ -383,6 +391,10 @@ class EditorJetpackSocialViewModel @Inject constructor(
         if (trackEvent) jetpackSocialSharingTracker.trackAddConnectionCtaDisplayed(jetpackSocialFlow)
     }

+    fun onPostStatusChanged() {
+        startJetpackSocial()
+    }
+
     data class JetpackSocialContainerVisibility(
         val showInPrepublishingSheet: Boolean,
         val showInPostSettings: Boolean,

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/EditorJetpackSocialViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/EditorJetpackSocialViewModelTest.kt
index 68e92a6e3a22..1e05b7d4f486 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/posts/EditorJetpackSocialViewModelTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/EditorJetpackSocialViewModelTest.kt
@@ -16,7 +16,9 @@ import org.wordpress.android.R
 import org.wordpress.android.datasets.wrappers.PublicizeTableWrapper
 import org.wordpress.android.fluxc.Dispatcher
 import org.wordpress.android.fluxc.model.AccountModel
+import org.wordpress.android.fluxc.model.PostModel
 import org.wordpress.android.fluxc.model.SiteModel
+import org.wordpress.android.fluxc.model.post.PostStatus
 import org.wordpress.android.fluxc.store.AccountStore
 import org.wordpress.android.fluxc.store.SiteStore
 import org.wordpress.android.models.PublicizeConnection
@@ -116,6 +118,27 @@ class EditorJetpackSocialViewModelTest : BaseUnitTest() {
         verify(getPublicizeConnectionsForUserUseCase, never()).execute(any(), any(), any())
     }

+    @Test
+    fun `Should NOT load jetpack social if post is page`() = test {
+        whenever(jetpackSocialFeatureConfig.isEnabled())
+            .thenReturn(true)
+        whenever(editPostRepository.isPage).thenReturn(true)
+        classToTest.start(FAKE_SITE_MODEL, editPostRepository)
+        verify(getPublicizeConnectionsForUserUseCase, never()).execute(any(), any(), any())
+    }
+
+    @Test
+    fun `Should NOT load jetpack social if PostStatus is PRIVATE`() = test {
+        whenever(jetpackSocialFeatureConfig.isEnabled())
+            .thenReturn(true)
+        whenever(editPostRepository.isPage).thenReturn(false)
+        whenever(editPostRepository.getPost()).thenReturn(PostModel().apply {
+            setStatus(PostStatus.PRIVATE.toString())
+        })
+        classToTest.start(FAKE_SITE_MODEL, editPostRepository)
+        verify(getPublicizeConnectionsForUserUseCase, never()).execute(any(), any(), any())
+    }
+
     @Test
     fun `Should hide jetpack social container if FF is disabled`() = test {
         whenever(jetpackSocialFeatureConfig.isEnabled())
@@ -525,6 +548,12 @@ class EditorJetpackSocialViewModelTest : BaseUnitTest() {
         verify(jetpackSocialSharingTracker).trackConnectionToggled(JetpackSocialFlow.PRE_PUBLISHING, false)
     }

+    @Test
+    fun `Should start jetpack social if onPostVisibilityChanged is called`() {
+        classToTest.onPostStatusChanged()
+        verify(showJetpackSocialContainerObserver).onChanged(any())
+    }
+
     private fun fakeSiteModel(supportsPublicize: Boolean = false): SiteModel = FAKE_SITE_MODEL.apply {
         siteId = FAKE_REMOTE_SITE_ID
         url = "mysite.blog"