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

Issue:
Ask for contacts permission during onboarding
Instead of always requesting the contacts permission in the message list screen, ask during initial app setup (after the first account was successfully created). We want to avoid running into the problem where with broken Android versions repeatedly denying the permission prompt won't get rid of the prompt.

See #6261


Discussion hints:
this is really annoying
We're currently rewriting the account setup code and are not looking for external contributions implementing this feature.

Pull request: Add onboarding screen to ask for Android permissions
This removes the old onboarding module and creates several onboarding sub modules.
- `:feature:onboarding:main` contains the navigation logic and binds the various onboarding modules together
- `:feature:onboarding:welcome` contains the welcome screen (`strings.xml` files have moved; so we need to update Weblate to point to the new location)
- `:feature:onboarding:permission` contains the new permission screen (needs a new component on Weblate)



<img src="https://user-images.githubusercontent.com/218061/278472767-127df045-6cdd-4d57-b222-658ae5e170ec.png" alt="image" width=400>

Closes #6262

Test command: ./gradlew :core:android:permissions:testDebugUnitTest --tests "app.k9mail.core.android.permissions.AndroidPermissionCheckerTest"

Fix patch:
diff --git a/app-feature-preview/build.gradle.kts b/app-feature-preview/build.gradle.kts
index 2d1beda7e45..6566d944c12 100644
--- a/app-feature-preview/build.gradle.kts
+++ b/app-feature-preview/build.gradle.kts
@@ -60,7 +60,7 @@ dependencies {
     implementation(projects.core.common)
     implementation(projects.mail.common)
 
-    implementation(projects.feature.onboarding)
+    implementation(projects.feature.onboarding.main)
     implementation(projects.feature.account.setup)
     implementation(projects.feature.account.edit)
 
diff --git a/app-feature-preview/src/main/java/app/k9mail/feature/preview/FeatureModule.kt b/app-feature-preview/src/main/java/app/k9mail/feature/preview/FeatureModule.kt
index aa80e7390e6..a5b1186ff2d 100644
--- a/app-feature-preview/src/main/java/app/k9mail/feature/preview/FeatureModule.kt
+++ b/app-feature-preview/src/main/java/app/k9mail/feature/preview/FeatureModule.kt
@@ -6,6 +6,7 @@ import app.k9mail.feature.account.edit.AccountEditExternalContract
 import app.k9mail.feature.account.edit.featureAccountEditModule
 import app.k9mail.feature.account.setup.AccountSetupExternalContract
 import app.k9mail.feature.account.setup.featureAccountSetupModule
+import app.k9mail.feature.onboarding.main.featureOnboardingModule
 import app.k9mail.feature.preview.account.AccountOwnerNameProvider
 import app.k9mail.feature.preview.account.InMemoryAccountStore
 import app.k9mail.feature.preview.auth.AndroidKeyStoreDirectoryProvider
@@ -18,6 +19,7 @@ import com.fsck.k9.mail.ssl.LocalKeyStore
 import com.fsck.k9.mail.ssl.TrustManagerFactory
 import com.fsck.k9.mail.ssl.TrustedSocketFactory
 import org.koin.core.module.Module
+import org.koin.core.qualifier.named
 import org.koin.dsl.binds
 import org.koin.dsl.module
 
@@ -41,9 +43,12 @@ val featureModule: Module = module {
     single { TrustManagerFactory.createInstance(get()) }
     single<TrustedSocketFactory> { DefaultTrustedSocketFactory(get(), get()) }
     single<OAuth2TokenProviderFactory> { RealOAuth2TokenProviderFactory(context = get()) }
+    single(named("ClientIdAppName")) { "App Name" }
+    single(named("ClientIdAppVersion")) { "App Version" }
 
     includes(
         accountModule,
+        featureOnboardingModule,
         featureAccountSetupModule,
         featureAccountEditModule,
     )
diff --git a/app-feature-preview/src/main/java/app/k9mail/feature/preview/navigation/FeatureNavHost.kt b/app-feature-preview/src/main/java/app/k9mail/feature/preview/navigation/FeatureNavHost.kt
index 5f1a99e19c4..20e0e70181b 100644
--- a/app-feature-preview/src/main/java/app/k9mail/feature/preview/navigation/FeatureNavHost.kt
+++ b/app-feature-preview/src/main/java/app/k9mail/feature/preview/navigation/FeatureNavHost.kt
@@ -7,10 +7,9 @@ import androidx.navigation.compose.NavHost
 import app.k9mail.feature.account.edit.navigation.accountEditRoute
 import app.k9mail.feature.account.edit.navigation.navigateToAccountEditIncomingServerSettings
 import app.k9mail.feature.account.setup.navigation.accountSetupRoute
-import app.k9mail.feature.account.setup.navigation.navigateToAccountSetup
-import app.k9mail.feature.onboarding.navigation.NAVIGATION_ROUTE_ONBOARDING
-import app.k9mail.feature.onboarding.navigation.navigateToOnboarding
-import app.k9mail.feature.onboarding.navigation.onboardingRoute
+import app.k9mail.feature.onboarding.main.navigation.NAVIGATION_ROUTE_ONBOARDING
+import app.k9mail.feature.onboarding.main.navigation.navigateToOnboarding
+import app.k9mail.feature.onboarding.main.navigation.onboardingRoute
 
 @Composable
 fun FeatureNavHost(
@@ -24,8 +23,11 @@ fun FeatureNavHost(
         modifier = modifier,
     ) {
         onboardingRoute(
-            onStart = { navController.navigateToAccountSetup() },
             onImport = { /* TODO */ },
+            onBack = navController::popBackStack,
+            onFinish = { accountUuid ->
+                navController.navigateToAccountEditIncomingServerSettings(accountUuid)
+            },
         )
         accountSetupRoute(
             onBack = navController::popBackStack,
diff --git a/core/android/permissions/build.gradle.kts b/core/android/permissions/build.gradle.kts
new file mode 100644
index 00000000000..41bb4a224c7
--- /dev/null
+++ b/core/android/permissions/build.gradle.kts
@@ -0,0 +1,13 @@
+plugins {
+    id(ThunderbirdPlugins.Library.android)
+}
+
+android {
+    namespace = "app.k9mail.core.android.permissions"
+}
+
+dependencies {
+    testImplementation(libs.androidx.test.core)
+    testImplementation(libs.robolectric)
+    testImplementation(libs.assertk)
+}
diff --git a/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/AndroidPermissionChecker.kt b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/AndroidPermissionChecker.kt
new file mode 100644
index 00000000000..371bf2db34d
--- /dev/null
+++ b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/AndroidPermissionChecker.kt
@@ -0,0 +1,38 @@
+package app.k9mail.core.android.permissions
+
+import android.Manifest
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.core.content.ContextCompat
+
+/**
+ * Checks if a [Permission] has been granted to the app.
+ */
+class AndroidPermissionChecker(
+    private val context: Context,
+) : PermissionChecker {
+
+    override fun checkPermission(permission: Permission): PermissionState {
+        return when (permission) {
+            Permission.Contacts -> {
+                checkSelfPermission(Manifest.permission.READ_CONTACTS)
+            }
+            Permission.Notifications -> {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                    checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
+                } else {
+                    PermissionState.GrantedImplicitly
+                }
+            }
+        }
+    }
+
+    private fun checkSelfPermission(permission: String): PermissionState {
+        return if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) {
+            PermissionState.Granted
+        } else {
+            PermissionState.Denied
+        }
+    }
+}
diff --git a/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/CorePermissionsAndroidModule.kt b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/CorePermissionsAndroidModule.kt
new file mode 100644
index 00000000000..dc1885345f1
--- /dev/null
+++ b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/CorePermissionsAndroidModule.kt
@@ -0,0 +1,8 @@
+package app.k9mail.core.android.permissions
+
+import org.koin.core.module.Module
+import org.koin.dsl.module
+
+val corePermissionsAndroidModule: Module = module {
+    factory<PermissionChecker> { AndroidPermissionChecker(context = get()) }
+}
diff --git a/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/Permission.kt b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/Permission.kt
new file mode 100644
index 00000000000..4ffaa43c3b1
--- /dev/null
+++ b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/Permission.kt
@@ -0,0 +1,9 @@
+package app.k9mail.core.android.permissions
+
+/**
+ * System permissions we ask for during onboarding.
+ */
+enum class Permission {
+    Contacts,
+    Notifications,
+}
diff --git a/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/PermissionChecker.kt b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/PermissionChecker.kt
new file mode 100644
index 00000000000..6713b34a53b
--- /dev/null
+++ b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/PermissionChecker.kt
@@ -0,0 +1,8 @@
+package app.k9mail.core.android.permissions
+
+/**
+ * Checks if a [Permission] has been granted to the app.
+ */
+interface PermissionChecker {
+    fun checkPermission(permission: Permission): PermissionState
+}
diff --git a/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/PermissionState.kt b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/PermissionState.kt
new file mode 100644
index 00000000000..fc123ae9f78
--- /dev/null
+++ b/core/android/permissions/src/main/kotlin/app/k9mail/core/android/permissions/PermissionState.kt
@@ -0,0 +1,10 @@
+package app.k9mail.core.android.permissions
+
+enum class PermissionState {
+    /**
+     * The permission is not a runtime permission in the Android version we're running on.
+     */
+    GrantedImplicitly,
+    Granted,
+    Denied,
+}
diff --git a/core/ui/compose/common/src/main/kotlin/app/k9mail/core/ui/compose/common/image/ImageWithOverlayCoordinate.kt b/core/ui/compose/common/src/main/kotlin/app/k9mail/core/ui/compose/common/image/ImageWithOverlayCoordinate.kt
new file mode 100644
index 00000000000..e64bf2b39df
--- /dev/null
+++ b/core/ui/compose/common/src/main/kotlin/app/k9mail/core/ui/compose/common/image/ImageWithOverlayCoordinate.kt
@@ -0,0 +1,18 @@
+package app.k9mail.core.ui.compose.common.image
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.Dp
+
+/**
+ * An image with a coordinate to draw a (smaller) overlay icon on top of it.
+ *
+ * Example: An icon representing an Android permission with an overlay icon to indicate whether the permission has been
+ * granted.
+ */
+@Immutable
+data class ImageWithOverlayCoordinate(
+    val image: ImageVector,
+    val overlayOffsetX: Dp,
+    val overlayOffsetY: Dp,
+)
diff --git a/core/ui/compose/common/src/main/kotlin/app/k9mail/core/ui/compose/common/visibility/VisibilityModifiers.kt b/core/ui/compose/common/src/main/kotlin/app/k9mail/core/ui/compose/common/visibility/VisibilityModifiers.kt
new file mode 100644
index 00000000000..2425ecc4f74
--- /dev/null
+++ b/core/ui/compose/common/src/main/kotlin/app/k9mail/core/ui/compose/common/visibility/VisibilityModifiers.kt
@@ -0,0 +1,9 @@
+package app.k9mail.core.ui.compose.common.visibility
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+
+/**
+ * Sets a composable to be fully transparent when it should be hidden (but still present for layout purposes).
+ */
+fun Modifier.hide(hide: Boolean) = alpha(if (hide) 0f else 1f)
diff --git a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/DelayedCircularProgressIndicator.kt b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/DelayedCircularProgressIndicator.kt
new file mode 100644
index 00000000000..2a84d192224
--- /dev/null
+++ b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/DelayedCircularProgressIndicator.kt
@@ -0,0 +1,43 @@
+package app.k9mail.core.ui.compose.designsystem.atom
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import app.k9mail.core.ui.compose.common.visibility.hide
+import app.k9mail.core.ui.compose.theme.MainTheme
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+private const val LOADING_INDICATOR_DELAY = 500L
+
+/**
+ * Only show a [CircularProgressIndicator] after [LOADING_INDICATOR_DELAY] ms.
+ *
+ * Use this to avoid flashing a loading indicator for loads that are usually very fast.
+ */
+@Composable
+fun DelayedCircularProgressIndicator(
+    modifier: Modifier = Modifier,
+    color: Color = MainTheme.colors.secondary,
+) {
+    var progressIndicatorVisible by remember { mutableStateOf(false) }
+
+    LaunchedEffect(key1 = Unit) {
+        launch {
+            delay(LOADING_INDICATOR_DELAY)
+            progressIndicatorVisible = true
+        }
+    }
+
+    CircularProgressIndicator(
+        modifier = Modifier
+            .hide(!progressIndicatorVisible)
+            .then(modifier),
+        color = color,
+    )
+}
diff --git a/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/Icons.kt b/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/Icons.kt
index 5ff0bf0464f..c20a75a8d06 100644
--- a/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/Icons.kt
+++ b/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/Icons.kt
@@ -1,6 +1,9 @@
 package app.k9mail.core.ui.compose.theme
 
 import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material.icons.filled.Cancel
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.CheckCircle
 import androidx.compose.material.icons.filled.Error
 import androidx.compose.material.icons.filled.MoveToInbox
 import androidx.compose.material.icons.filled.Notifications
@@ -48,6 +51,12 @@ object Icons {
 
         val user: ImageVector
             get() = MaterialIcons.Filled.AccountCircle
+
+        val check: ImageVector
+            get() = MaterialIcons.Filled.CheckCircle
+
+        val cancel: ImageVector
+            get() = MaterialIcons.Filled.Cancel
     }
 
     object Outlined {
diff --git a/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/IconsWithBottomRightOverlay.kt b/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/IconsWithBottomRightOverlay.kt
new file mode 100644
index 00000000000..220d926c2e3
--- /dev/null
+++ b/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/IconsWithBottomRightOverlay.kt
@@ -0,0 +1,27 @@
+package app.k9mail.core.ui.compose.theme
+
+import androidx.compose.material.icons.filled.Notifications
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.ui.unit.dp
+import app.k9mail.core.ui.compose.common.image.ImageWithOverlayCoordinate
+import androidx.compose.material.icons.Icons as MaterialIcons
+
+// We're using "by lazy" so not all icons are loaded into memory as soon as the object is accessed. But once a property
+// is accessed we want to retain the `ImageWithOverlayCoordinate` instance.
+object IconsWithBottomRightOverlay {
+    val person: ImageWithOverlayCoordinate by lazy {
+        ImageWithOverlayCoordinate(
+            image = MaterialIcons.Filled.Person,
+            overlayOffsetX = 24.dp,
+            overlayOffsetY = 20.dp,
+        )
+    }
+
+    val notification: ImageWithOverlayCoordinate by lazy {
+        ImageWithOverlayCoordinate(
+            image = MaterialIcons.Filled.Notifications,
+            overlayOffsetX = 23.dp,
+            overlayOffsetY = 19.dp,
+        )
+    }
+}
diff --git a/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/Sizes.kt b/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/Sizes.kt
index 148270cf7b2..fc5b60c70cc 100644
--- a/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/Sizes.kt
+++ b/core/ui/compose/theme/src/main/java/app/k9mail/core/ui/compose/theme/Sizes.kt
@@ -16,6 +16,7 @@ data class Sizes(
     val huger: Dp = 384.dp,
 
     val icon: Dp = 24.dp,
+    val largeIcon: Dp = 32.dp,
 
     val topBarHeight: Dp = 56.dp,
     val bottomBarHeight: Dp = 56.dp,
diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/navigation/AccountSetupNavigation.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/navigation/AccountSetupNavigation.kt
index 5d4eb2ce88c..74083567529 100644
--- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/navigation/AccountSetupNavigation.kt
+++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/navigation/AccountSetupNavigation.kt
@@ -3,6 +3,7 @@ package app.k9mail.feature.account.setup.navigation
 import androidx.navigation.NavController
 import androidx.navigation.NavGraphBuilder
 import androidx.navigation.NavOptions
+import androidx.navigation.compose.composable
 import app.k9mail.core.ui.compose.common.navigation.deepLinkComposable
 import app.k9mail.feature.account.setup.ui.AccountSetupScreen
 
@@ -23,3 +24,16 @@ fun NavGraphBuilder.accountSetupRoute(
         )
     }
 }
+
+fun NavGraphBuilder.nestedAccountSetupRoute(
+    route: String,
+    onBack: () -> Unit,
+    onFinish: (String) -> Unit,
+) {
+    composable(route) {
+        AccountSetupScreen(
+            onBack = onBack,
+            onFinish = onFinish,
+        )
+    }
+}
diff --git a/feature/launcher/build.gradle.kts b/feature/launcher/build.gradle.kts
index c7d08c7ade1..72ce81fb321 100644
--- a/feature/launcher/build.gradle.kts
+++ b/feature/launcher/build.gradle.kts
@@ -19,7 +19,7 @@ android {
 dependencies {
     implementation(projects.core.ui.compose.designsystem)
     implementation(projects.app.ui.base)
-    implementation(projects.feature.onboarding)
+    implementation(projects.feature.onboarding.main)
     implementation(projects.feature.account.setup)
     implementation(projects.feature.account.edit)
 
diff --git a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/FeatureLauncherActivity.kt b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/FeatureLauncherActivity.kt
index af2050bb921..0b9dd7106b1 100644
--- a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/FeatureLauncherActivity.kt
+++ b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/FeatureLauncherActivity.kt
@@ -11,7 +11,7 @@ import app.k9mail.feature.account.edit.navigation.NAVIGATION_ROUTE_ACCOUNT_EDIT_
 import app.k9mail.feature.account.edit.navigation.withAccountUuid
 import app.k9mail.feature.account.setup.navigation.NAVIGATION_ROUTE_ACCOUNT_SETUP
 import app.k9mail.feature.launcher.ui.FeatureLauncherApp
-import app.k9mail.feature.onboarding.navigation.NAVIGATION_ROUTE_ONBOARDING
+import app.k9mail.feature.onboarding.main.navigation.NAVIGATION_ROUTE_ONBOARDING
 import com.fsck.k9.ui.base.K9Activity
 
 class FeatureLauncherActivity : K9Activity() {
diff --git a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/di/FeatureLauncherModule.kt b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/di/FeatureLauncherModule.kt
index 1e5a5e47355..ddab819657b 100644
--- a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/di/FeatureLauncherModule.kt
+++ b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/di/FeatureLauncherModule.kt
@@ -2,10 +2,12 @@ package app.k9mail.feature.launcher.di
 
 import app.k9mail.feature.account.edit.featureAccountEditModule
 import app.k9mail.feature.account.setup.featureAccountSetupModule
+import app.k9mail.feature.onboarding.main.featureOnboardingModule
 import org.koin.dsl.module
 
 val featureLauncherModule = module {
     includes(
+        featureOnboardingModule,
         featureAccountSetupModule,
         featureAccountEditModule,
     )
diff --git a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/navigation/FeatureLauncherNavHost.kt b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/navigation/FeatureLauncherNavHost.kt
index 5de11098ba7..0ca3213dbe3 100644
--- a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/navigation/FeatureLauncherNavHost.kt
+++ b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/navigation/FeatureLauncherNavHost.kt
@@ -7,11 +7,10 @@ import androidx.navigation.compose.NavHost
 import app.k9mail.core.ui.compose.common.activity.LocalActivity
 import app.k9mail.feature.account.edit.navigation.accountEditRoute
 import app.k9mail.feature.account.setup.navigation.accountSetupRoute
-import app.k9mail.feature.account.setup.navigation.navigateToAccountSetup
 import app.k9mail.feature.launcher.FeatureLauncherExternalContract.AccountSetupFinishedLauncher
 import app.k9mail.feature.launcher.FeatureLauncherExternalContract.ImportSettingsLauncher
-import app.k9mail.feature.onboarding.navigation.NAVIGATION_ROUTE_ONBOARDING
-import app.k9mail.feature.onboarding.navigation.onboardingRoute
+import app.k9mail.feature.onboarding.main.navigation.NAVIGATION_ROUTE_ONBOARDING
+import app.k9mail.feature.onboarding.main.navigation.onboardingRoute
 import org.koin.compose.koinInject
 
 @Composable
@@ -30,8 +29,9 @@ fun FeatureLauncherNavHost(
         modifier = modifier,
     ) {
         onboardingRoute(
-            onStart = { navController.navigateToAccountSetup() },
             onImport = { importSettingsLauncher.launch() },
+            onBack = onBack,
+            onFinish = { accountUuid -> accountSetupFinishedLauncher.launch(accountUuid) },
         )
         accountSetupRoute(
             onBack = onBack,
diff --git a/feature/onboarding/main/build.gradle.kts b/feature/onboarding/main/build.gradle.kts
new file mode 100644
index 00000000000..9b807013746
--- /dev/null
+++ b/feature/onboarding/main/build.gradle.kts
@@ -0,0 +1,24 @@
+plugins {
+    id(ThunderbirdPlugins.Library.androidCompose)
+}
+
+android {
+    namespace = "app.k9mail.feature.onboarding.main"
+    resourcePrefix = "onboarding_main_"
+
+    buildTypes {
+        debug {
+            manifestPlaceholders["appAuthRedirectScheme"] = "FIXME: override this in your app project"
+        }
+        release {
+            manifestPlaceholders["appAuthRedirectScheme"] = "FIXME: override this in your app project"
+        }
+    }
+}
+
+dependencies {
+    implementation(projects.core.ui.compose.designsystem)
+    implementation(projects.feature.onboarding.welcome)
+    implementation(projects.feature.account.setup)
+    implementation(projects.feature.onboarding.permissions)
+}
diff --git a/feature/onboarding/main/src/main/kotlin/app/k9mail/feature/onboarding/main/OnboardingModule.kt b/feature/onboarding/main/src/main/kotlin/app/k9mail/feature/onboarding/main/OnboardingModule.kt
new file mode 100644
index 00000000000..fc02cc33380
--- /dev/null
+++ b/feature/onboarding/main/src/main/kotlin/app/k9mail/feature/onboarding/main/OnboardingModule.kt
@@ -0,0 +1,11 @@
+package app.k9mail.feature.onboarding.main
+
+import app.k9mail.feature.onboarding.permissions.featureOnboardingPermissionsModule
+import org.koin.core.module.Module
+import org.koin.dsl.module
+
+val featureOnboardingModule: Module = module {
+    includes(
+        featureOnboardingPermissionsModule,
+    )
+}
diff --git a/feature/onboarding/main/src/main/kotlin/app/k9mail/feature/onboarding/main/navigation/OnboardingNavHost.kt b/feature/onboarding/main/src/main/kotlin/app/k9mail/feature/onboarding/main/navigation/OnboardingNavHost.kt
new file mode 100644
index 00000000000..3d49d1da3c3
--- /dev/null
+++ b/feature/onboarding/main/src/main/kotlin/app/k9mail/feature/onboarding/main/navigation/OnboardingNavHost.kt
@@ -0,0 +1,63 @@
+package app.k9mail.feature.onboarding.main.navigation
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.navigation.NavController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import app.k9mail.feature.account.setup.navigation.nestedAccountSetupRoute
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsScreen
+import app.k9mail.feature.onboarding.welcome.ui.OnboardingScreen
+
+private const val NESTED_NAVIGATION_ROUTE_WELCOME = "welcome"
+private const val NESTED_NAVIGATION_ROUTE_ACCOUNT_SETUP = "account_setup"
+private const val NESTED_NAVIGATION_ROUTE_PERMISSIONS = "permissions"
+
+private fun NavController.navigateToAccountSetup() {
+    navigate(NESTED_NAVIGATION_ROUTE_ACCOUNT_SETUP)
+}
+
+private fun NavController.navigateToPermissions() {
+    navigate(NESTED_NAVIGATION_ROUTE_PERMISSIONS)
+}
+
+@Composable
+fun OnboardingNavHost(
+    onImport: () -> Unit,
+    onBack: () -> Unit,
+    onFinish: (String) -> Unit,
+) {
+    val navController = rememberNavController()
+    var accountUuid by rememberSaveable { mutableStateOf<String?>(null) }
+
+    NavHost(
+        navController = navController,
+        startDestination = NESTED_NAVIGATION_ROUTE_WELCOME,
+    ) {
+        composable(route = NESTED_NAVIGATION_ROUTE_WELCOME) {
+            OnboardingScreen(
+                onStartClick = { navController.navigateToAccountSetup() },
+                onImportClick = onImport,
+            )
+        }
+
+        nestedAccountSetupRoute(
+            route = NESTED_NAVIGATION_ROUTE_ACCOUNT_SETUP,
+            onBack = onBack,
+            onFinish = { createdAccountUuid ->
+                accountUuid = createdAccountUuid
+                navController.navigateToPermissions()
+            },
+        )
+
+        composable(route = NESTED_NAVIGATION_ROUTE_PERMISSIONS) {
+            PermissionsScreen(
+                onNext = { onFinish(requireNotNull(accountUuid)) },
+            )
+        }
+    }
+}
diff --git a/feature/onboarding/src/main/kotlin/app/k9mail/feature/onboarding/navigation/OnboardingNavigation.kt b/feature/onboarding/main/src/main/kotlin/app/k9mail/feature/onboarding/main/navigation/OnboardingNavigation.kt
similarity index 69%
rename from feature/onboarding/src/main/kotlin/app/k9mail/feature/onboarding/navigation/OnboardingNavigation.kt
rename to feature/onboarding/main/src/main/kotlin/app/k9mail/feature/onboarding/main/navigation/OnboardingNavigation.kt
index a9e2263afbb..16cd0b3e78b 100644
--- a/feature/onboarding/src/main/kotlin/app/k9mail/feature/onboarding/navigation/OnboardingNavigation.kt
+++ b/feature/onboarding/main/src/main/kotlin/app/k9mail/feature/onboarding/main/navigation/OnboardingNavigation.kt
@@ -1,10 +1,9 @@
-package app.k9mail.feature.onboarding.navigation
+package app.k9mail.feature.onboarding.main.navigation
 
 import androidx.navigation.NavController
 import androidx.navigation.NavGraphBuilder
 import androidx.navigation.NavOptions
 import app.k9mail.core.ui.compose.common.navigation.deepLinkComposable
-import app.k9mail.feature.onboarding.ui.OnboardingScreen
 
 const val NAVIGATION_ROUTE_ONBOARDING = "onboarding"
 
@@ -15,13 +14,15 @@ fun NavController.navigateToOnboarding(
 }
 
 fun NavGraphBuilder.onboardingRoute(
-    onStart: () -> Unit,
     onImport: () -> Unit,
+    onBack: () -> Unit,
+    onFinish: (String) -> Unit,
 ) {
     deepLinkComposable(route = NAVIGATION_ROUTE_ONBOARDING) {
-        OnboardingScreen(
-            onStartClick = onStart,
-            onImportClick = onImport,
+        OnboardingNavHost(
+            onImport = onImport,
+            onBack = onBack,
+            onFinish = onFinish,
         )
     }
 }
diff --git a/feature/onboarding/permissions/build.gradle.kts b/feature/onboarding/permissions/build.gradle.kts
new file mode 100644
index 00000000000..240d12f9f46
--- /dev/null
+++ b/feature/onboarding/permissions/build.gradle.kts
@@ -0,0 +1,14 @@
+plugins {
+    id(ThunderbirdPlugins.Library.androidCompose)
+}
+
+android {
+    namespace = "app.k9mail.feature.onboarding.permissions"
+    resourcePrefix = "onboarding_permissions_"
+}
+
+dependencies {
+    implementation(projects.core.ui.compose.designsystem)
+    implementation(projects.core.android.permissions)
+    implementation(projects.feature.account.common)
+}
diff --git a/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/PermissionsModule.kt b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/PermissionsModule.kt
new file mode 100644
index 00000000000..7a4b1b7e773
--- /dev/null
+++ b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/PermissionsModule.kt
@@ -0,0 +1,21 @@
+package app.k9mail.feature.onboarding.permissions
+
+import app.k9mail.core.android.permissions.corePermissionsAndroidModule
+import app.k9mail.feature.onboarding.permissions.domain.PermissionsDomainContract.UseCase
+import app.k9mail.feature.onboarding.permissions.domain.usecase.CheckPermission
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsViewModel
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.core.module.Module
+import org.koin.dsl.module
+
+val featureOnboardingPermissionsModule: Module = module {
+    includes(corePermissionsAndroidModule)
+
+    factory<UseCase.CheckPermission> { CheckPermission(permissionChecker = get()) }
+
+    viewModel {
+        PermissionsViewModel(
+            checkPermission = get(),
+        )
+    }
+}
diff --git a/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/domain/PermissionsDomainContract.kt b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/domain/PermissionsDomainContract.kt
new file mode 100644
index 00000000000..ef1fb950bcb
--- /dev/null
+++ b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/domain/PermissionsDomainContract.kt
@@ -0,0 +1,14 @@
+package app.k9mail.feature.onboarding.permissions.domain
+
+import app.k9mail.core.android.permissions.Permission
+import app.k9mail.core.android.permissions.PermissionState
+
+interface PermissionsDomainContract {
+
+    interface UseCase {
+
+        fun interface CheckPermission {
+            operator fun invoke(permission: Permission): PermissionState
+        }
+    }
+}
diff --git a/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/domain/usecase/CheckPermission.kt b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/domain/usecase/CheckPermission.kt
new file mode 100644
index 00000000000..5f53d40fdd5
--- /dev/null
+++ b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/domain/usecase/CheckPermission.kt
@@ -0,0 +1,18 @@
+package app.k9mail.feature.onboarding.permissions.domain.usecase
+
+import app.k9mail.core.android.permissions.Permission
+import app.k9mail.core.android.permissions.PermissionChecker
+import app.k9mail.core.android.permissions.PermissionState
+import app.k9mail.feature.onboarding.permissions.domain.PermissionsDomainContract.UseCase
+
+/**
+ * Checks if a [Permission] has been granted to the app.
+ */
+class CheckPermission(
+    private val permissionChecker: PermissionChecker,
+) : UseCase.CheckPermission {
+
+    override fun invoke(permission: Permission): PermissionState {
+        return permissionChecker.checkPermission(permission)
+    }
+}
diff --git a/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionBox.kt b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionBox.kt
new file mode 100644
index 00000000000..e16efd40658
--- /dev/null
+++ b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionBox.kt
@@ -0,0 +1,174 @@
+package app.k9mail.feature.onboarding.permissions.ui
+
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import app.k9mail.core.ui.compose.common.image.ImageWithOverlayCoordinate
+import app.k9mail.core.ui.compose.designsystem.atom.Icon
+import app.k9mail.core.ui.compose.designsystem.atom.button.Button
+import app.k9mail.core.ui.compose.designsystem.atom.text.TextBody2
+import app.k9mail.core.ui.compose.designsystem.atom.text.TextHeadline6
+import app.k9mail.core.ui.compose.theme.Icons
+import app.k9mail.core.ui.compose.theme.IconsWithBottomRightOverlay
+import app.k9mail.core.ui.compose.theme.K9Theme
+import app.k9mail.core.ui.compose.theme.MainTheme
+import app.k9mail.feature.onboarding.permissions.R
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.UiPermissionState
+
+private val MAX_WIDTH = 500.dp
+
+@Composable
+internal fun PermissionBox(
+    icon: ImageWithOverlayCoordinate,
+    permissionState: UiPermissionState,
+    title: String,
+    description: String,
+    onAllowClick: () -> Unit,
+) {
+    Column(
+        modifier = Modifier
+            .width(MAX_WIDTH)
+            .padding(horizontal = MainTheme.spacings.double),
+    ) {
+        Row {
+            Box(
+                modifier = Modifier.padding(
+                    end = MainTheme.spacings.double,
+                    top = MainTheme.spacings.default,
+                    bottom = MainTheme.spacings.default,
+                ),
+            ) {
+                IconWithPermissionStateOverlay(icon, permissionState)
+            }
+            Column {
+                TextHeadline6(text = title)
+                TextBody2(text = description)
+            }
+        }
+
+        val buttonAlpha by animateFloatAsState(
+            targetValue = if (permissionState == UiPermissionState.Granted) 0f else 1f,
+            label = "AllowButtonAlpha",
+        )
+
+        Column(
+            modifier = Modifier.fillMaxWidth(),
+            horizontalAlignment = Alignment.End,
+        ) {
+            Spacer(modifier = Modifier.height(MainTheme.spacings.default))
+
+            Button(
+                text = stringResource(R.string.onboarding_permissions_allow_button),
+                onClick = onAllowClick,
+                modifier = Modifier.alpha(buttonAlpha),
+            )
+        }
+    }
+}
+
+@Composable
+private fun IconWithPermissionStateOverlay(
+    icon: ImageWithOverlayCoordinate,
+    permissionState: UiPermissionState,
+) {
+    Box {
+        val iconSize = MainTheme.sizes.largeIcon
+        val overlayIconSize = iconSize / 2
+        val overlayIconOffset = overlayIconSize / 2
+        val scalingFactor = iconSize / icon.image.defaultHeight
+        val overlayOffsetX = (icon.overlayOffsetX * scalingFactor) - overlayIconOffset
+        val overlayOffsetY = (icon.overlayOffsetY * scalingFactor) - overlayIconOffset
+
+        Icon(
+            imageVector = icon.image,
+            modifier = Modifier.size(iconSize),
+        )
+
+        when (permissionState) {
+            UiPermissionState.Unknown -> Unit
+            UiPermissionState.Granted -> {
+                Icon(
+                    imageVector = Icons.Filled.check,
+                    tint = MainTheme.colors.success,
+                    modifier = Modifier
+                        .size(overlayIconSize)
+                        .offset(
+                            x = overlayOffsetX,
+                            y = overlayOffsetY,
+                        ),
+                )
+            }
+
+            UiPermissionState.Denied -> {
+                Icon(
+                    imageVector = Icons.Filled.cancel,
+                    tint = MainTheme.colors.warning,
+                    modifier = Modifier
+                        .size(overlayIconSize)
+                        .offset(
+                            x = overlayOffsetX,
+                            y = overlayOffsetY,
+                        ),
+                )
+            }
+        }
+    }
+}
+
+@Preview(showBackground = true)
+@Composable
+internal fun PermissionBoxUnknownStatePreview() {
+    K9Theme {
+        PermissionBox(
+            icon = IconsWithBottomRightOverlay.person,
+            permissionState = UiPermissionState.Unknown,
+            title = "Contacts",
+            description = "Allow access to be able to display contact names and photos.",
+            onAllowClick = {},
+        )
+    }
+}
+
+@Preview(showBackground = true)
+@Composable
+internal fun PermissionBoxGrantedStatePreview() {
+    K9Theme {
+        PermissionBox(
+            icon = IconsWithBottomRightOverlay.person,
+            permissionState = UiPermissionState.Granted,
+            title = "Contacts",
+            description = "Allow access to be able to display contact names and photos.",
+            onAllowClick = {},
+        )
+    }
+}
+
+@Preview(showBackground = true)
+@Composable
+internal fun PermissionBoxDeniedStatePreview() {
+    K9Theme {
+        PermissionBox(
+            icon = IconsWithBottomRightOverlay.person,
+            permissionState = UiPermissionState.Denied,
+            title = "Contacts",
+            description = "Allow access to be able to display contact names and photos.",
+            onAllowClick = {},
+        )
+    }
+}
diff --git a/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsContent.kt b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsContent.kt
new file mode 100644
index 00000000000..127331b1aa5
--- /dev/null
+++ b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsContent.kt
@@ -0,0 +1,198 @@
+package app.k9mail.feature.onboarding.permissions.ui
+
+import androidx.compose.animation.Crossfade
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import app.k9mail.core.ui.compose.common.PreviewDevices
+import app.k9mail.core.ui.compose.common.visibility.hide
+import app.k9mail.core.ui.compose.designsystem.atom.DelayedCircularProgressIndicator
+import app.k9mail.core.ui.compose.designsystem.atom.Surface
+import app.k9mail.core.ui.compose.designsystem.atom.button.Button
+import app.k9mail.core.ui.compose.designsystem.atom.button.ButtonText
+import app.k9mail.core.ui.compose.designsystem.atom.text.TextHeadline5
+import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
+import app.k9mail.core.ui.compose.designsystem.template.Scaffold
+import app.k9mail.core.ui.compose.theme.IconsWithBottomRightOverlay
+import app.k9mail.core.ui.compose.theme.K9Theme
+import app.k9mail.core.ui.compose.theme.MainTheme
+import app.k9mail.feature.account.common.ui.AppTitleTopHeader
+import app.k9mail.feature.onboarding.permissions.R
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.Event
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.State
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.UiPermissionState
+import app.k9mail.feature.account.common.R as CommonR
+
+@Composable
+internal fun PermissionsContent(
+    state: State,
+    onEvent: (Event) -> Unit,
+) {
+    val scrollState = rememberScrollState()
+
+    Scaffold(
+        bottomBar = {
+            BottomBar(state, onEvent, scrollState)
+        },
+    ) { innerPadding ->
+        ResponsiveWidthContainer(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(innerPadding),
+        ) {
+            Column(
+                verticalArrangement = Arrangement.SpaceBetween,
+                horizontalAlignment = Alignment.CenterHorizontally,
+                modifier = Modifier
+                    .fillMaxHeight()
+                    .verticalScroll(state = scrollState),
+            ) {
+                HeaderArea()
+
+                ContentArea(state, onEvent)
+
+                // This provides some bottom padding but is also necessary to make the vertical arrangement have the
+                // desired effect of putting ContentArea() in the middle.
+                Spacer(modifier = Modifier.height(MainTheme.spacings.double))
+            }
+        }
+    }
+}
+
+@Composable
+private fun HeaderArea() {
+    Column(
+        horizontalAlignment = Alignment.CenterHorizontally,
+    ) {
+        AppTitleTopHeader()
+
+        TextHeadline5(
+            text = stringResource(R.string.onboarding_permissions_screen_title),
+            modifier = Modifier.padding(horizontal = MainTheme.spacings.double),
+        )
+
+        Spacer(modifier = Modifier.height(MainTheme.spacings.double))
+    }
+}
+
+@Composable
+private fun ContentArea(state: State, onEvent: (Event) -> Unit) {
+    Column(
+        verticalArrangement = Arrangement.Center,
+        modifier = Modifier
+            .fillMaxHeight()
+            .padding(MainTheme.spacings.double),
+    ) {
+        if (state.isLoading) {
+            DelayedCircularProgressIndicator()
+        } else {
+            PermissionBoxes(state, onEvent)
+        }
+    }
+}
+
+@Composable
+private fun PermissionBoxes(
+    state: State,
+    onEvent: (Event) -> Unit,
+) {
+    PermissionBox(
+        icon = IconsWithBottomRightOverlay.person,
+        permissionState = state.contactsPermissionState,
+        title = stringResource(R.string.onboarding_permissions_contacts_title),
+        description = stringResource(R.string.onboarding_permissions_contacts_description),
+        onAllowClick = { onEvent(Event.AllowContactsPermissionClicked) },
+    )
+
+    if (state.isNotificationsPermissionVisible) {
+        Spacer(modifier = Modifier.height(MainTheme.spacings.quadruple))
+
+        PermissionBox(
+            icon = IconsWithBottomRightOverlay.notification,
+            permissionState = state.notificationsPermissionState,
+            title = stringResource(R.string.onboarding_permissions_notifications_title),
+            description = stringResource(R.string.onboarding_permissions_notifications_description),
+            onAllowClick = { onEvent(Event.AllowNotificationsPermissionClicked) },
+        )
+    }
+}
+
+@Composable
+private fun BottomBar(
+    state: State,
+    onEvent: (Event) -> Unit,
+    scrollState: ScrollState,
+) {
+    // Elevate the bottom bar when some scrollable content is "underneath" it
+    val elevation by animateDpAsState(
+        targetValue = if (scrollState.canScrollForward) 8.dp else 0.dp,
+        label = "BottomBarElevation",
+    )
+
+    Surface(elevation = elevation) {
+        ResponsiveWidthContainer(
+            modifier = Modifier.fillMaxWidth(),
+        ) {
+            Row(
+                modifier = Modifier
+                    .padding(
+                        start = MainTheme.spacings.double,
+                        end = MainTheme.spacings.double,
+                        top = MainTheme.spacings.half,
+                        bottom = MainTheme.spacings.half,
+                    )
+                    .fillMaxWidth(),
+                horizontalArrangement = Arrangement.End,
+            ) {
+                Crossfade(
+                    targetState = state.isNextButtonVisible,
+                    label = "NextButton",
+                ) { isNextButtonVisible ->
+                    Button(
+                        text = stringResource(CommonR.string.account_common_button_next),
+                        onClick = { onEvent(Event.NextClicked) },
+                        modifier = Modifier.hide(!isNextButtonVisible),
+                    )
+
+                    ButtonText(
+                        text = stringResource(R.string.onboarding_permissions_skip_button),
+                        onClick = { onEvent(Event.NextClicked) },
+                        modifier = Modifier.hide(isNextButtonVisible),
+                    )
+                }
+            }
+        }
+    }
+}
+
+@PreviewDevices
+@Composable
+internal fun PermissionContentPreview() {
+    K9Theme {
+        PermissionsContent(
+            state = State(
+                isLoading = false,
+                contactsPermissionState = UiPermissionState.Granted,
+                notificationsPermissionState = UiPermissionState.Denied,
+                isNotificationsPermissionVisible = true,
+                isNextButtonVisible = false,
+            ),
+            onEvent = {},
+        )
+    }
+}
diff --git a/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsContract.kt b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsContract.kt
new file mode 100644
index 00000000000..51b84771cb9
--- /dev/null
+++ b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsContract.kt
@@ -0,0 +1,40 @@
+package app.k9mail.feature.onboarding.permissions.ui
+
+import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
+
+interface PermissionsContract {
+    interface ViewModel : UnidirectionalViewModel<State, Event, Effect>
+
+    data class State(
+        val isLoading: Boolean = true,
+        val contactsPermissionState: UiPermissionState = UiPermissionState.Unknown,
+        val notificationsPermissionState: UiPermissionState = UiPermissionState.Unknown,
+        val isNotificationsPermissionVisible: Boolean = false,
+        val isNextButtonVisible: Boolean = false,
+    )
+
+    sealed interface Event {
+        data object LoadPermissionState : Event
+
+        data object AllowContactsPermissionClicked : Event
+        data object AllowNotificationsPermissionClicked : Event
+
+        data class ContactsPermissionResult(val success: Boolean) : Event
+        data class NotificationsPermissionResult(val success: Boolean) : Event
+
+        data object NextClicked : Event
+    }
+
+    sealed interface Effect {
+        data object RequestContactsPermission : Effect
+        data object RequestNotificationsPermission : Effect
+
+        data object NavigateNext : Effect
+    }
+
+    enum class UiPermissionState {
+        Unknown,
+        Granted,
+        Denied,
+    }
+}
diff --git a/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsScreen.kt b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsScreen.kt
new file mode 100644
index 00000000000..4b8423987be
--- /dev/null
+++ b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsScreen.kt
@@ -0,0 +1,72 @@
+package app.k9mail.feature.onboarding.permissions.ui
+
+import android.Manifest
+import android.os.Build
+import androidx.activity.compose.ManagedActivityResultLauncher
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import app.k9mail.core.android.permissions.PermissionState
+import app.k9mail.core.ui.compose.common.PreviewDevices
+import app.k9mail.core.ui.compose.common.mvi.observe
+import app.k9mail.core.ui.compose.theme.K9Theme
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.Effect
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.Event
+import kotlinx.coroutines.Dispatchers
+import org.koin.androidx.compose.koinViewModel
+
+@Composable
+fun PermissionsScreen(
+    viewModel: PermissionsContract.ViewModel = koinViewModel<PermissionsViewModel>(),
+    onNext: () -> Unit,
+) {
+    val contactsPermissionLauncher = rememberLauncherForActivityResult(RequestPermission()) { success ->
+        viewModel.event(Event.ContactsPermissionResult(success))
+    }
+
+    val notificationsPermissionLauncher = rememberLauncherForActivityResult(RequestPermission()) { success ->
+        viewModel.event(Event.NotificationsPermissionResult(success))
+    }
+
+    val (state, dispatch) = viewModel.observe { effect ->
+        when (effect) {
+            Effect.RequestContactsPermission -> contactsPermissionLauncher.requestContactsPermission()
+            Effect.RequestNotificationsPermission -> notificationsPermissionLauncher.requestNotificationsPermission()
+            Effect.NavigateNext -> onNext()
+        }
+    }
+
+    LaunchedEffect(key1 = Unit) {
+        dispatch(Event.LoadPermissionState)
+    }
+
+    PermissionsContent(
+        state = state.value,
+        onEvent = dispatch,
+    )
+}
+
+private fun ManagedActivityResultLauncher<String, Boolean>.requestContactsPermission() {
+    launch(Manifest.permission.READ_CONTACTS)
+}
+
+private fun ManagedActivityResultLauncher<String, Boolean>.requestNotificationsPermission() {
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+        launch(Manifest.permission.POST_NOTIFICATIONS)
+    }
+}
+
+@PreviewDevices
+@Composable
+internal fun PermissionScreenPreview() {
+    K9Theme {
+        PermissionsScreen(
+            viewModel = PermissionsViewModel(
+                checkPermission = { PermissionState.Denied },
+                backgroundDispatcher = Dispatchers.Main.immediate,
+            ),
+            onNext = {},
+        )
+    }
+}
diff --git a/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsViewModel.kt b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsViewModel.kt
new file mode 100644
index 00000000000..7803a127aac
--- /dev/null
+++ b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionsViewModel.kt
@@ -0,0 +1,108 @@
+package app.k9mail.feature.onboarding.permissions.ui
+
+import androidx.lifecycle.viewModelScope
+import app.k9mail.core.android.permissions.Permission
+import app.k9mail.core.android.permissions.PermissionState
+import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
+import app.k9mail.feature.onboarding.permissions.domain.PermissionsDomainContract.UseCase
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.Effect
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.Event
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.State
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.UiPermissionState
+import app.k9mail.feature.onboarding.permissions.ui.PermissionsContract.ViewModel
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class PermissionsViewModel(
+    private val checkPermission: UseCase.CheckPermission,
+    private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO,
+) : BaseViewModel<State, Event, Effect>(initialState = State(isLoading = true)), ViewModel {
+
+    override fun event(event: Event) {
+        when (event) {
+            Event.LoadPermissionState -> handleOneTimeEvent(event, ::loadPermissionState)
+            Event.AllowContactsPermissionClicked -> handleAllowContactsPermissionClicked()
+            Event.AllowNotificationsPermissionClicked -> handleAllowNotificationsPermissionClicked()
+            is Event.ContactsPermissionResult -> handleContactsPermissionResult(event.success)
+            is Event.NotificationsPermissionResult -> handleNotificationsPermissionResult(event.success)
+            Event.NextClicked -> handleNextClicked()
+        }
+    }
+
+    private fun loadPermissionState() {
+        viewModelScope.launch {
+            val (contactsPermissionState, notificationsPermissionState) = withContext(backgroundDispatcher) {
+                arrayOf(
+                    checkPermission(Permission.Contacts),
+                    checkPermission(Permission.Notifications),
+                )
+            }
+
+            val contactsUiPermissionState = when (contactsPermissionState) {
+                PermissionState.GrantedImplicitly -> error("Unexpected case")
+                PermissionState.Granted -> UiPermissionState.Granted
+                PermissionState.Denied -> UiPermissionState.Unknown
+            }
+            val notificationsUiPermissionState = when (notificationsPermissionState) {
+                PermissionState.GrantedImplicitly -> UiPermissionState.Unknown
+                PermissionState.Granted -> UiPermissionState.Granted
+                PermissionState.Denied -> UiPermissionState.Unknown
+            }
+            val isNotificationsPermissionVisible = notificationsPermissionState != PermissionState.GrantedImplicitly
+
+            updateState { state ->
+                state.copy(
+                    isLoading = false,
+                    contactsPermissionState = contactsUiPermissionState,
+                    notificationsPermissionState = notificationsUiPermissionState,
+                    isNotificationsPermissionVisible = isNotificationsPermissionVisible,
+                )
+            }
+            updateNextButtonState()
+        }
+    }
+
+    private fun handleAllowContactsPermissionClicked() {
+        emitEffect(Effect.RequestContactsPermission)
+    }
+
+    private fun handleAllowNotificationsPermissionClicked() {
+        emitEffect(Effect.RequestNotificationsPermission)
+    }
+
+    private fun handleContactsPermissionResult(success: Boolean) {
+        updateState { state ->
+            state.copy(
+                contactsPermissionState = if (success) UiPermissionState.Granted else UiPermissionState.Denied,
+            )
+        }
+        updateNextButtonState()
+    }
+
+    private fun handleNotificationsPermissionResult(success: Boolean) {
+        updateState { state ->
+            state.copy(
+                notificationsPermissionState = if (success) UiPermissionState.Granted else UiPermissionState.Denied,
+            )
+        }
+        updateNextButtonState()
+    }
+
+    private fun updateNextButtonState() {
+        updateState { state ->
+            val isContactsPermissionGranted = state.contactsPermissionState == UiPermissionState.Granted
+            val isNotificationsPermissionGrantedOrHidden = !state.isNotificationsPermissionVisible ||
+                state.notificationsPermissionState == UiPermissionState.Granted
+
+            state.copy(
+                isNextButtonVisible = isContactsPermissionGranted && isNotificationsPermissionGrantedOrHidden,
+            )
+        }
+    }
+
+    private fun handleNextClicked() {
+        emitEffect(Effect.NavigateNext)
+    }
+}
diff --git a/feature/onboarding/permissions/src/main/res/values/strings.xml b/feature/onboarding/permissions/src/main/res/values/strings.xml
new file mode 100644
index 00000000000..096560516ea
--- /dev/null
+++ b/feature/onboarding/permissions/src/main/res/values/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Title of the onboarding screen to ask for Android permissions -->
+    <string name="onboarding_permissions_screen_title">Improve the experience</string>
+    <!-- Title of the section asking for the permission to read contacts -->
+    <string name="onboarding_permissions_contacts_title">Contacts</string>
+    <!-- Description of why we ask for the permission to read contacts -->
+    <string name="onboarding_permissions_contacts_description">Allow access to be able to provide contact suggestions and to display contact names and photos.</string>
+    <!-- Title of the section asking for permission to post notifications -->
+    <string name="onboarding_permissions_notifications_title">Notifications</string>
+    <!-- Description of why we ask the permission to post notifications -->
+    <string name="onboarding_permissions_notifications_description">Turn on notifications so you don\'t miss any emails.</string>
+    <!-- Text of the button that will trigger the system dialog to ask for an Android permission -->
+    <string name="onboarding_permissions_allow_button">Allow</string>
+    <!-- Text of the button to skip the onboarding screen to ask for Android permissions -->
+    <string name="onboarding_permissions_skip_button">Skip</string>
+</resources>
diff --git a/feature/onboarding/build.gradle.kts b/feature/onboarding/welcome/build.gradle.kts
similarity index 60%
rename from feature/onboarding/build.gradle.kts
rename to feature/onboarding/welcome/build.gradle.kts
index fd113b5223c..e620724e191 100644
--- a/feature/onboarding/build.gradle.kts
+++ b/feature/onboarding/welcome/build.gradle.kts
@@ -3,8 +3,8 @@ plugins {
 }
 
 android {
-    namespace = "app.k9mail.feature.onboarding"
-    resourcePrefix = "onboarding_"
+    namespace = "app.k9mail.feature.onboarding.welcome"
+    resourcePrefix = "onboarding_welcome_"
 }
 
 dependencies {
diff --git a/feature/onboarding/src/main/kotlin/app/k9mail/feature/onboarding/ui/OnboardingContent.kt b/feature/onboarding/welcome/src/main/kotlin/app/k9mail/feature/onboarding/welcome/ui/OnboardingContent.kt
similarity index 98%
rename from feature/onboarding/src/main/kotlin/app/k9mail/feature/onboarding/ui/OnboardingContent.kt
rename to feature/onboarding/welcome/src/main/kotlin/app/k9mail/feature/onboarding/welcome/ui/OnboardingContent.kt
index d008a2bad21..1da819eec69 100644
--- a/feature/onboarding/src/main/kotlin/app/k9mail/feature/onboarding/ui/OnboardingContent.kt
+++ b/feature/onboarding/welcome/src/main/kotlin/app/k9mail/feature/onboarding/welcome/ui/OnboardingContent.kt
@@ -1,4 +1,4 @@
-package app.k9mail.feature.onboarding.ui
+package app.k9mail.feature.onboarding.welcome.ui
 
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.Arrangement
@@ -23,7 +23,7 @@ import app.k9mail.core.ui.compose.designsystem.template.ResponsiveContent
 import app.k9mail.core.ui.compose.theme.K9Theme
 import app.k9mail.core.ui.compose.theme.MainTheme
 import app.k9mail.core.ui.compose.theme.ThunderbirdTheme
-import app.k9mail.feature.onboarding.R
+import app.k9mail.feature.onboarding.welcome.R
 
 @Composable
 internal fun OnboardingContent(
diff --git a/feature/onboarding/src/main/kotlin/app/k9mail/feature/onboarding/ui/OnboardingScreen.kt b/feature/onboarding/welcome/src/main/kotlin/app/k9mail/feature/onboarding/welcome/ui/OnboardingScreen.kt
similarity index 90%
rename from feature/onboarding/src/main/kotlin/app/k9mail/feature/onboarding/ui/OnboardingScreen.kt
rename to feature/onboarding/welcome/src/main/kotlin/app/k9mail/feature/onboarding/welcome/ui/OnboardingScreen.kt
index e1972a069e7..a274d9506fe 100644
--- a/feature/onboarding/src/main/kotlin/app/k9mail/feature/onboarding/ui/OnboardingScreen.kt
+++ b/feature/onboarding/welcome/src/main/kotlin/app/k9mail/feature/onboarding/welcome/ui/OnboardingScreen.kt
@@ -1,4 +1,4 @@
-package app.k9mail.feature.onboarding.ui
+package app.k9mail.feature.onboarding.welcome.ui
 
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.tooling.preview.Preview
diff --git a/feature/onboarding/src/main/res/drawable/onboarding_welcome_logo.xml b/feature/onboarding/welcome/src/main/res/drawable/onboarding_welcome_logo.xml
similarity index 100%
rename from feature/onboarding/src/main/res/drawable/onboarding_welcome_logo.xml
rename to feature/onboarding/welcome/src/main/res/drawable/onboarding_welcome_logo.xml
diff --git a/feature/onboarding/src/main/res/values-cs/strings.xml b/feature/onboarding/welcome/src/main/res/values-ar/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-cs/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-ar/strings.xml
diff --git a/feature/onboarding/src/main/res/values-bg/strings.xml b/feature/onboarding/welcome/src/main/res/values-az/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-bg/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-az/strings.xml
diff --git a/feature/onboarding/src/main/res/values-es/strings.xml b/feature/onboarding/welcome/src/main/res/values-be/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-es/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-be/strings.xml
diff --git a/feature/onboarding/src/main/res/values-et/strings.xml b/feature/onboarding/welcome/src/main/res/values-bg/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-et/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-bg/strings.xml
diff --git a/feature/onboarding/src/main/res/values-eu/strings.xml b/feature/onboarding/welcome/src/main/res/values-br/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-eu/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-br/strings.xml
diff --git a/feature/onboarding/src/main/res/values-ca/strings.xml b/feature/onboarding/welcome/src/main/res/values-ca/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-ca/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-ca/strings.xml
diff --git a/feature/onboarding/src/main/res/values-fi/strings.xml b/feature/onboarding/welcome/src/main/res/values-cs/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-fi/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-cs/strings.xml
diff --git a/feature/onboarding/src/main/res/values-fr/strings.xml b/feature/onboarding/welcome/src/main/res/values-cy/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-fr/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-cy/strings.xml
diff --git a/feature/onboarding/src/main/res/values-da/strings.xml b/feature/onboarding/welcome/src/main/res/values-da/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-da/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-da/strings.xml
diff --git a/feature/onboarding/src/main/res/values-de/strings.xml b/feature/onboarding/welcome/src/main/res/values-de/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-de/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-de/strings.xml
diff --git a/feature/onboarding/src/main/res/values-fy/strings.xml b/feature/onboarding/welcome/src/main/res/values-el/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-fy/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-el/strings.xml
diff --git a/feature/onboarding/src/main/res/values-it/strings.xml b/feature/onboarding/welcome/src/main/res/values-en-rGB/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-it/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-en-rGB/strings.xml
diff --git a/feature/onboarding/src/main/res/values-nl/strings.xml b/feature/onboarding/welcome/src/main/res/values-eo/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-nl/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-eo/strings.xml
diff --git a/feature/onboarding/src/main/res/values-pt-rBR/strings.xml b/feature/onboarding/welcome/src/main/res/values-es/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-pt-rBR/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-es/strings.xml
diff --git a/feature/onboarding/src/main/res/values-sv/strings.xml b/feature/onboarding/welcome/src/main/res/values-et/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-sv/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-et/strings.xml
diff --git a/feature/onboarding/src/main/res/values-zh-rTW/strings.xml b/feature/onboarding/welcome/src/main/res/values-eu/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-zh-rTW/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-eu/strings.xml
diff --git a/feature/onboarding/src/main/res/values-ar/strings.xml b/feature/onboarding/welcome/src/main/res/values-fa/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-ar/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-fa/strings.xml
diff --git a/feature/onboarding/src/main/res/values-az/strings.xml b/feature/onboarding/welcome/src/main/res/values-fi/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-az/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-fi/strings.xml
diff --git a/feature/onboarding/src/main/res/values-be/strings.xml b/feature/onboarding/welcome/src/main/res/values-fr/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-be/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-fr/strings.xml
diff --git a/feature/onboarding/src/main/res/values-br/strings.xml b/feature/onboarding/welcome/src/main/res/values-fy/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-br/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-fy/strings.xml
diff --git a/feature/onboarding/src/main/res/values-cy/strings.xml b/feature/onboarding/welcome/src/main/res/values-gd/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-cy/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-gd/strings.xml
diff --git a/feature/onboarding/src/main/res/values-el/strings.xml b/feature/onboarding/welcome/src/main/res/values-gl/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-el/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-gl/strings.xml
diff --git a/feature/onboarding/src/main/res/values-hi/strings.xml b/feature/onboarding/welcome/src/main/res/values-hi/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-hi/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-hi/strings.xml
diff --git a/feature/onboarding/src/main/res/values-en-rGB/strings.xml b/feature/onboarding/welcome/src/main/res/values-hr/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-en-rGB/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-hr/strings.xml
diff --git a/feature/onboarding/src/main/res/values-hu/strings.xml b/feature/onboarding/welcome/src/main/res/values-hu/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-hu/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-hu/strings.xml
diff --git a/feature/onboarding/src/main/res/values-eo/strings.xml b/feature/onboarding/welcome/src/main/res/values-hy/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-eo/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-hy/strings.xml
diff --git a/feature/onboarding/src/main/res/values-fa/strings.xml b/feature/onboarding/welcome/src/main/res/values-in/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-fa/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-in/strings.xml
diff --git a/feature/onboarding/src/main/res/values-is/strings.xml b/feature/onboarding/welcome/src/main/res/values-is/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-is/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-is/strings.xml
diff --git a/feature/onboarding/src/main/res/values-gd/strings.xml b/feature/onboarding/welcome/src/main/res/values-it/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-gd/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-it/strings.xml
diff --git a/feature/onboarding/src/main/res/values-gl/strings.xml b/feature/onboarding/welcome/src/main/res/values-iw/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-gl/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-iw/strings.xml
diff --git a/feature/onboarding/src/main/res/values-ja/strings.xml b/feature/onboarding/welcome/src/main/res/values-ja/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-ja/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-ja/strings.xml
diff --git a/feature/onboarding/src/main/res/values-hr/strings.xml b/feature/onboarding/welcome/src/main/res/values-ka/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-hr/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-ka/strings.xml
diff --git a/feature/onboarding/src/main/res/values-hy/strings.xml b/feature/onboarding/welcome/src/main/res/values-ko/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-hy/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-ko/strings.xml
diff --git a/feature/onboarding/src/main/res/values-in/strings.xml b/feature/onboarding/welcome/src/main/res/values-lt/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-in/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-lt/strings.xml
diff --git a/feature/onboarding/src/main/res/values-iw/strings.xml b/feature/onboarding/welcome/src/main/res/values-lv/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-iw/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-lv/strings.xml
diff --git a/feature/onboarding/src/main/res/values-ka/strings.xml b/feature/onboarding/welcome/src/main/res/values-ml/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-ka/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-ml/strings.xml
diff --git a/feature/onboarding/src/main/res/values-nb-rNO/strings.xml b/feature/onboarding/welcome/src/main/res/values-nb-rNO/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-nb-rNO/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-nb-rNO/strings.xml
diff --git a/feature/onboarding/src/main/res/values-ko/strings.xml b/feature/onboarding/welcome/src/main/res/values-nl/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-ko/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-nl/strings.xml
diff --git a/feature/onboarding/src/main/res/values-lt/strings.xml b/feature/onboarding/welcome/src/main/res/values-nn/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-lt/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-nn/strings.xml
diff --git a/feature/onboarding/src/main/res/values-pl/strings.xml b/feature/onboarding/welcome/src/main/res/values-pl/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-pl/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-pl/strings.xml
diff --git a/feature/onboarding/src/main/res/values-lv/strings.xml b/feature/onboarding/welcome/src/main/res/values-pt-rBR/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-lv/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-pt-rBR/strings.xml
diff --git a/feature/onboarding/src/main/res/values-ml/strings.xml b/feature/onboarding/welcome/src/main/res/values-pt-rPT/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-ml/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-pt-rPT/strings.xml
diff --git a/feature/onboarding/src/main/res/values-ro/strings.xml b/feature/onboarding/welcome/src/main/res/values-ro/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-ro/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-ro/strings.xml
diff --git a/feature/onboarding/src/main/res/values-nn/strings.xml b/feature/onboarding/welcome/src/main/res/values-ru/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-nn/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-ru/strings.xml
diff --git a/feature/onboarding/src/main/res/values-pt-rPT/strings.xml b/feature/onboarding/welcome/src/main/res/values-sk/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-pt-rPT/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-sk/strings.xml
diff --git a/feature/onboarding/src/main/res/values-ru/strings.xml b/feature/onboarding/welcome/src/main/res/values-sl/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-ru/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-sl/strings.xml
diff --git a/feature/onboarding/src/main/res/values-sq/strings.xml b/feature/onboarding/welcome/src/main/res/values-sq/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-sq/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-sq/strings.xml
diff --git a/feature/onboarding/src/main/res/values-sk/strings.xml b/feature/onboarding/welcome/src/main/res/values-sr/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-sk/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-sr/strings.xml
diff --git a/feature/onboarding/src/main/res/values-sl/strings.xml b/feature/onboarding/welcome/src/main/res/values-sv/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-sl/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-sv/strings.xml
diff --git a/feature/onboarding/src/main/res/values-sr/strings.xml b/feature/onboarding/welcome/src/main/res/values-ta/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-sr/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-ta/strings.xml
diff --git a/feature/onboarding/src/main/res/values-tr/strings.xml b/feature/onboarding/welcome/src/main/res/values-tr/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-tr/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-tr/strings.xml
diff --git a/feature/onboarding/src/main/res/values-ta/strings.xml b/feature/onboarding/welcome/src/main/res/values-uk/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-ta/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-uk/strings.xml
diff --git a/feature/onboarding/src/main/res/values-uk/strings.xml b/feature/onboarding/welcome/src/main/res/values-vi/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-uk/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-vi/strings.xml
diff --git a/feature/onboarding/src/main/res/values-zh-rCN/strings.xml b/feature/onboarding/welcome/src/main/res/values-zh-rCN/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-zh-rCN/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-zh-rCN/strings.xml
diff --git a/feature/onboarding/src/main/res/values-vi/strings.xml b/feature/onboarding/welcome/src/main/res/values-zh-rTW/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values-vi/strings.xml
rename to feature/onboarding/welcome/src/main/res/values-zh-rTW/strings.xml
diff --git a/feature/onboarding/src/main/res/values/strings.xml b/feature/onboarding/welcome/src/main/res/values/strings.xml
similarity index 100%
rename from feature/onboarding/src/main/res/values/strings.xml
rename to feature/onboarding/welcome/src/main/res/values/strings.xml
diff --git a/settings.gradle.kts b/settings.gradle.kts
index eb170d99c42..1969a84ab70 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -39,7 +39,12 @@ include(
 
 include(
     ":feature:launcher",
-    ":feature:onboarding",
+)
+
+include(
+    ":feature:onboarding:main",
+    ":feature:onboarding:welcome",
+    ":feature:onboarding:permissions",
 )
 
 include(
@@ -64,6 +69,7 @@ include(
     ":core:featureflags",
     ":core:testing",
     ":core:android:common",
+    ":core:android:permissions",
     ":core:android:testing",
     ":core:ui:compose:common",
     ":core:ui:compose:designsystem",


Test patch:
diff --git a/core/android/permissions/src/test/kotlin/app/k9mail/core/android/permissions/AndroidPermissionCheckerTest.kt b/core/android/permissions/src/test/kotlin/app/k9mail/core/android/permissions/AndroidPermissionCheckerTest.kt
new file mode 100644
index 00000000000..af2ec34c383
--- /dev/null
+++ b/core/android/permissions/src/test/kotlin/app/k9mail/core/android/permissions/AndroidPermissionCheckerTest.kt
@@ -0,0 +1,66 @@
+package app.k9mail.core.android.permissions
+
+import android.Manifest
+import android.app.Application
+import android.os.Build
+import androidx.test.core.app.ApplicationProvider
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+class AndroidPermissionCheckerTest {
+    private val application: Application = ApplicationProvider.getApplicationContext()
+    private val shadowApplication = Shadows.shadowOf(application)
+
+    private val permissionChecker = AndroidPermissionChecker(application)
+
+    @Test
+    fun `granted READ_CONTACTS permission`() {
+        shadowApplication.grantPermissions(Manifest.permission.READ_CONTACTS)
+
+        val result = permissionChecker.checkPermission(Permission.Contacts)
+
+        assertThat(result).isEqualTo(PermissionState.Granted)
+    }
+
+    @Test
+    fun `denied READ_CONTACTS permission`() {
+        shadowApplication.denyPermissions(Manifest.permission.READ_CONTACTS)
+
+        val result = permissionChecker.checkPermission(Permission.Contacts)
+
+        assertThat(result).isEqualTo(PermissionState.Denied)
+    }
+
+    @Test
+    fun `granted POST_NOTIFICATIONS permission`() {
+        shadowApplication.grantPermissions(Manifest.permission.POST_NOTIFICATIONS)
+
+        val result = permissionChecker.checkPermission(Permission.Notifications)
+
+        assertThat(result).isEqualTo(PermissionState.Granted)
+    }
+
+    @Test
+    fun `denied POST_NOTIFICATIONS permission`() {
+        shadowApplication.denyPermissions(Manifest.permission.POST_NOTIFICATIONS)
+
+        val result = permissionChecker.checkPermission(Permission.Notifications)
+
+        assertThat(result).isEqualTo(PermissionState.Denied)
+    }
+
+    @Test
+    @Config(minSdk = Build.VERSION_CODES.S_V2, maxSdk = Build.VERSION_CODES.S_V2)
+    fun `POST_NOTIFICATIONS permission not available`() {
+        val result = permissionChecker.checkPermission(Permission.Notifications)
+
+        assertThat(result).isEqualTo(PermissionState.GrantedImplicitly)
+    }
+}