Repository: thunderbird/thunderbird-android (language: kotlin)

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



Discussion hints:
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.

Pull request: 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

Test command: ./gradlew :feature:funding:googleplay:testDebugUnitTest --tests "app.k9mail.feature.funding.googleplay.ui.contribution.ContributionScreenKtTest" --tests "app.k9mail.feature.funding.googleplay.ui.contribution.ContributionViewModelTest"

Fix patch:
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>


Test patch:
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,
+            ),
+        )
+    }
+}