diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b004171..828b0f5 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -32,9 +32,16 @@ jobs:
instrumented_tests:
runs-on: ubuntu-latest
+
strategy:
matrix:
- api-level: [29]
+ include:
+ - api-level: 29
+ arch: x86
+ - api-level: 33
+ arch: x86_64
+ fail-fast: false
+
steps:
- uses: actions/checkout@v4
@@ -44,12 +51,18 @@ jobs:
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- - name: Grant execute permission for gradlew
- run: chmod +x gradlew
+ - name: Set up JDK 21
+ uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'temurin'
- name: Gradle cache
uses: gradle/actions/setup-gradle@v3
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
- name: AVD cache
uses: actions/cache@v4
id: avd-cache
@@ -57,23 +70,27 @@ jobs:
path: |
~/.android/avd/*
~/.android/adb*
- key: avd-${{ matrix.api-level }}
+ key: avd-${{ matrix.api-level }}-${{ matrix.arch }}
- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
- force-avd-creation: false
+ arch: ${{ matrix.arch }}
+ target: google_apis
+ force-avd-creation: true
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
- disable-animations: false
- script: echo "Generated AVD snapshot for caching."
+ disable-animations: true
+ script: echo "AVD snapshot generated for API ${{ matrix.api-level }} - ${{ matrix.arch }}."
- name: Run tests with AVD
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
+ arch: ${{ matrix.arch }}
+ target: google_apis
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
- script: ./gradlew connectedAndroidTest --daemon && killall -INT crashpad_handler || true
+ script: ./gradlew connectedDebugAndroidTest --daemon
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 886c6bc..c807562 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -102,6 +102,7 @@ dependencies {
implementation(libs.hilt.navigation.compose)
implementation(libs.androidx.splash.screen)
implementation(libs.androidx.junit.ktx)
+ implementation(libs.accompanist.permissions)
ksp(libs.dagger.hilt.compiler)
diff --git a/app/src/androidTest/java/com/notifier/app/core/presentation/notification/FakeNotificationPermissionState.kt b/app/src/androidTest/java/com/notifier/app/core/presentation/notification/FakeNotificationPermissionState.kt
new file mode 100644
index 0000000..b1c9940
--- /dev/null
+++ b/app/src/androidTest/java/com/notifier/app/core/presentation/notification/FakeNotificationPermissionState.kt
@@ -0,0 +1,18 @@
+package com.notifier.app.core.presentation.notification
+
+/**
+ * A fake implementation of [NotificationPermissionState] for use in tests.
+ *
+ * @param isGranted whether the notification permission is granted.
+ * @param shouldShowRationale whether the system should show a rationale for the permission.
+ *
+ * This implementation is used to simulate various permission states in UI tests without
+ * requiring actual system permission dialogs.
+ */
+class FakeNotificationPermissionState(
+ override val isGranted: Boolean,
+ override val shouldShowRationale: Boolean
+) : NotificationPermissionState {
+ /** No-op implementation since this is only used in tests. */
+ override fun requestPermission() { /* No-op for tests */ }
+}
diff --git a/app/src/androidTest/java/com/notifier/app/core/presentation/notification/WithNotificationPermissionTest.kt b/app/src/androidTest/java/com/notifier/app/core/presentation/notification/WithNotificationPermissionTest.kt
new file mode 100644
index 0000000..6cac515
--- /dev/null
+++ b/app/src/androidTest/java/com/notifier/app/core/presentation/notification/WithNotificationPermissionTest.kt
@@ -0,0 +1,112 @@
+package com.notifier.app.core.presentation.notification
+
+import androidx.compose.material3.Text
+import androidx.compose.ui.test.assertCountEquals
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onAllNodesWithText
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.filters.SdkSuppress
+import com.notifier.app.ui.theme.GitHubNotifierTheme
+import org.junit.Rule
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 33)
+class WithNotificationPermissionTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun permissionNotGranted_shouldShowRationale_showsRationalePrompt() {
+ composeTestRule.setContent {
+ GitHubNotifierTheme {
+ WithNotificationPermission(
+ content = { Text("Main Content") },
+ permissionState = FakeNotificationPermissionState(
+ isGranted = false,
+ shouldShowRationale = true
+ )
+ )
+ }
+ }
+
+ // Check that rationale message is shown
+ composeTestRule
+ .onNodeWithText(
+ "To deliver GitHub notifications like pull requests, issues, and mentions, " +
+ "we need notification access. Please allow it."
+ )
+ .assertIsDisplayed()
+
+ // Check that 'Allow' button is shown
+ composeTestRule
+ .onNodeWithText("Allow Notifications")
+ .assertIsDisplayed()
+
+ // Check main content is still shown
+ composeTestRule
+ .onNodeWithText("Main Content")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun permissionNotGranted_shouldNotShowRationale_showsDeniedPrompt() {
+ composeTestRule.setContent {
+ GitHubNotifierTheme {
+ WithNotificationPermission(
+ content = { Text("Main Content") },
+ permissionState = FakeNotificationPermissionState(
+ isGranted = false,
+ shouldShowRationale = false
+ )
+ )
+ }
+ }
+
+ // Check that denied message is shown
+ composeTestRule
+ .onNodeWithText(
+ "Notification access is required to show updates from your GitHub activity. " +
+ "Enable it from system settings."
+ )
+ .assertIsDisplayed()
+
+ // Check that 'Open Settings' button is shown
+ composeTestRule
+ .onNodeWithText("Open Settings")
+ .assertIsDisplayed()
+
+ // Check main content is still shown
+ composeTestRule
+ .onNodeWithText("Main Content")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun permissionGranted_doesNotShowPrompt() {
+ composeTestRule.setContent {
+ GitHubNotifierTheme {
+ WithNotificationPermission(
+ content = { Text("Main Content") },
+ permissionState = FakeNotificationPermissionState(
+ isGranted = true,
+ shouldShowRationale = false
+ )
+ )
+ }
+ }
+
+ // Should only show main content
+ composeTestRule
+ .onNodeWithText("Main Content")
+ .assertIsDisplayed()
+
+ composeTestRule
+ .onAllNodesWithText("Allow Notifications")
+ .assertCountEquals(0)
+
+ composeTestRule
+ .onAllNodesWithText("Open Settings")
+ .assertCountEquals(0)
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8f685c1..52d0b87 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">
+
{
- NotificationRoute()
+ WithNotificationPermission {
+ NotificationRoute()
+ }
}
}
}
diff --git a/app/src/main/java/com/notifier/app/core/domain/notification/AppNotificationChannel.kt b/app/src/main/java/com/notifier/app/core/domain/notification/AppNotificationChannel.kt
new file mode 100644
index 0000000..b9c3561
--- /dev/null
+++ b/app/src/main/java/com/notifier/app/core/domain/notification/AppNotificationChannel.kt
@@ -0,0 +1,33 @@
+package com.notifier.app.core.domain.notification
+
+/**
+ * Represents the various notification channels used within the app.
+ *
+ * Each enum entry defines the [id], [displayName], and [description]
+ * associated with a specific type of notification channel.
+ *
+ * These channels are used to categorize and manage notifications for the entire app.
+ */
+enum class AppNotificationChannel(
+ /**
+ * The unique ID for the notification channel.
+ * Used when creating and referencing the channel in the notification system.
+ */
+ val id: String,
+
+ /** The user-visible name of the notification channel. */
+ val displayName: String,
+
+ /** The user-visible description of what types of notifications this channel delivers. */
+ val description: String,
+) {
+ /**
+ * Channel for delivering notifications related to GitHub activity,
+ * such as pull requests, issues, and mentions.
+ */
+ GITHUB(
+ id = "github_channel",
+ displayName = "GitHub Notifications",
+ description = "Channel for GitHub related notifications"
+ ),
+}
diff --git a/app/src/main/java/com/notifier/app/core/domain/notification/NotificationHandler.kt b/app/src/main/java/com/notifier/app/core/domain/notification/NotificationHandler.kt
new file mode 100644
index 0000000..d11516b
--- /dev/null
+++ b/app/src/main/java/com/notifier/app/core/domain/notification/NotificationHandler.kt
@@ -0,0 +1,90 @@
+package com.notifier.app.core.domain.notification
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import androidx.core.app.NotificationCompat
+import androidx.core.net.toUri
+import com.notifier.app.R
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Handles the creation and display of in-app notifications.
+ *
+ * This class is responsible for:
+ * - Creating required [NotificationChannel]s.
+ * - Showing notifications using [NotificationCompat.Builder].
+ *
+ * @property applicationContext The application-level context used to access system services.
+ */
+@Singleton
+class NotificationHandler @Inject constructor(
+ @ApplicationContext private val applicationContext: Context,
+) {
+ /**
+ * Displays a notification with the given details.
+ *
+ * @param id The unique ID for this notification (used to update or cancel it).
+ * @param title The title displayed in the notification.
+ * @param message The body content of the notification.
+ * @param channel The [AppNotificationChannel] that defines the channel for this notification.
+ * @param url The URL to open in the browser when the notification is clicked.
+ */
+ fun showNotification(
+ id: Int,
+ title: String,
+ message: String,
+ channel: AppNotificationChannel,
+ url: String,
+ ) {
+ val pendingIntent = PendingIntent.getActivity(
+ /* context = */ applicationContext,
+ /* requestCode = */ id,
+ /* intent = */ Intent(Intent.ACTION_VIEW, url.toUri()),
+ /* flags = */ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val notification = NotificationCompat.Builder(applicationContext, channel.id)
+ .setContentTitle(title)
+ .setContentText(message)
+ .setSmallIcon(R.drawable.ic_launcher_foreground)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setContentIntent(pendingIntent)
+ .setAutoCancel(true)
+ .build()
+
+ val notificationManager = applicationContext.getSystemService(
+ Context.NOTIFICATION_SERVICE
+ ) as NotificationManager
+
+ notificationManager.notify(id, notification)
+ }
+
+ /**
+ * Creates all required notification channels used by the app.
+ *
+ * This must be called during app startup on Android O+ (API 26+) to register
+ * channels with the system before sending notifications.
+ */
+ fun createNotificationChannels() {
+ val notificationManager = applicationContext.getSystemService(
+ Context.NOTIFICATION_SERVICE
+ ) as NotificationManager
+
+ AppNotificationChannel.entries.forEach { channel ->
+ val notificationChannel = NotificationChannel(
+ channel.id,
+ channel.displayName,
+ NotificationManager.IMPORTANCE_DEFAULT
+ ).apply {
+ description = channel.description
+ }
+
+ notificationManager.createNotificationChannel(notificationChannel)
+ }
+ }
+}
diff --git a/app/src/main/java/com/notifier/app/core/presentation/notification/NotificationPermissionState.kt b/app/src/main/java/com/notifier/app/core/presentation/notification/NotificationPermissionState.kt
new file mode 100644
index 0000000..8983924
--- /dev/null
+++ b/app/src/main/java/com/notifier/app/core/presentation/notification/NotificationPermissionState.kt
@@ -0,0 +1,30 @@
+package com.notifier.app.core.presentation.notification
+
+/**
+ * Represents the current state of the notification permission.
+ *
+ * This interface is used to check whether notification permission has been granted,
+ * whether a rationale should be shown to the user, and to trigger the permission request flow.
+ */
+interface NotificationPermissionState {
+ /**
+ * Indicates whether the app should display a rationale for requesting the notification permission.
+ *
+ * This is typically true if the user has previously denied the permission without selecting
+ * "Don't ask again."
+ */
+ val shouldShowRationale: Boolean
+
+ /**
+ * Indicates whether the notification permission has been granted.
+ */
+ val isGranted: Boolean
+
+ /**
+ * Requests the notification permission from the user.
+ *
+ * This should be called when the app determines that it needs permission
+ * and wants to prompt the user for it.
+ */
+ fun requestPermission()
+}
diff --git a/app/src/main/java/com/notifier/app/core/presentation/notification/WithNotificationPermission.kt b/app/src/main/java/com/notifier/app/core/presentation/notification/WithNotificationPermission.kt
new file mode 100644
index 0000000..effae59
--- /dev/null
+++ b/app/src/main/java/com/notifier/app/core/presentation/notification/WithNotificationPermission.kt
@@ -0,0 +1,202 @@
+package com.notifier.app.core.presentation.notification
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.provider.Settings
+import android.util.Log
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.PreviewDynamicColors
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.compose.ui.unit.dp
+import com.notifier.app.R
+import com.notifier.app.ui.theme.GitHubNotifierTheme
+
+/**
+ * Wraps a screen or composable with logic to check and request notification permission on
+ * Android 13+.
+ *
+ * If the permission is not granted, it will prompt the user appropriately—either by showing a
+ * system dialog or a fallback message guiding them to enable it manually from settings.
+ *
+ * @param permissionState The [NotificationPermissionState] to use, primarily for testability.
+ * If null, the default state from [rememberNotificationPermissionState] is used.
+ * @param content The main content to be displayed alongside the permission prompt.
+ */
+@Composable
+fun WithNotificationPermission(
+ permissionState: NotificationPermissionState? = null,
+ content: @Composable () -> Unit,
+) {
+ Column {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ NotificationPermissionHandler(
+ permissionState = permissionState ?: rememberNotificationPermissionState()
+ )
+ }
+ content()
+ }
+}
+
+/**
+ * Displays the appropriate notification permission prompt if the permission isn't granted.
+ *
+ * - If permission can still be requested, it shows a rationale and a retry button.
+ * - If permanently denied, it shows a message guiding the user to system settings.
+ *
+ * @param permissionState Current state of the notification permission.
+ */
+@Composable
+private fun NotificationPermissionHandler(
+ permissionState: NotificationPermissionState
+) {
+ val context = LocalContext.current
+ var hasRequested by rememberSaveable { mutableStateOf(false) }
+
+ LaunchedEffect(permissionState.isGranted, permissionState.shouldShowRationale) {
+ if (!hasRequested && !permissionState.isGranted && !permissionState.shouldShowRationale) {
+ hasRequested = true
+ permissionState.requestPermission()
+ }
+ }
+
+ if (!permissionState.isGranted) {
+ NotificationPermissionPrompt(
+ shouldShowRationale = permissionState.shouldShowRationale,
+ onRequestPermission = {
+ hasRequested = true
+ permissionState.requestPermission()
+ },
+ onOpenSettings = {
+ runCatching {
+ Intent(
+ Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+ Uri.fromParts(
+ /* scheme = */ "package",
+ /* ssp = */ context.packageName,
+ /* fragment = */ null
+ )
+ ).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }.let(context::startActivity)
+ }.onFailure { throwable ->
+ Log.e(
+ "WithNotificationPermission",
+ "Unable to open app settings",
+ throwable
+ )
+ }
+ }
+ )
+ }
+}
+
+/**
+ * A UI prompt to guide the user for notification permission.
+ *
+ * If rationale should be shown, it allows retrying the permission request.
+ * Otherwise, it suggests navigating to system settings to enable permission manually.
+ *
+ * @param shouldShowRationale Whether to explain the need for permission again.
+ * @param onRequestPermission Callback for retrying the permission request.
+ * @param onOpenSettings Callback to open the app's settings screen.
+ */
+@Composable
+private fun NotificationPermissionPrompt(
+ shouldShowRationale: Boolean,
+ onRequestPermission: () -> Unit,
+ onOpenSettings: () -> Unit,
+) {
+ val message = if (shouldShowRationale) {
+ stringResource(R.string.notification_permission_rationale_message)
+ } else {
+ stringResource(R.string.notification_permission_denied_message)
+ }
+
+ val actionButtonText = if (shouldShowRationale) {
+ stringResource(R.string.notification_permission_allow_button_text)
+ } else {
+ stringResource(R.string.notification_permission_settings_button_text)
+ }
+
+ val onClick = if (shouldShowRationale) onRequestPermission else onOpenSettings
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.surfaceVariant)
+ .padding(16.dp)
+ ) {
+ Text(
+ text = message,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ TextButton(
+ modifier = Modifier
+ .padding(top = 8.dp)
+ .align(Alignment.End),
+ onClick = onClick
+ ) {
+ Text(
+ text = actionButtonText,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+}
+
+/**
+ * Preview parameter provider for displaying different notification permission states in previews.
+ *
+ * Provides sample values for the `shouldShowRationale` flag to simulate UI scenarios
+ * where the user has either denied permission once or permanently.
+ */
+class ShouldShowRationaleProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ true, // Should show rationale (e.g., user denied once)
+ false // Should go to app settings (e.g., denied permanently)
+ )
+}
+
+/**
+ * Preview of the [NotificationPermissionPrompt] composable with dynamic colors and support for light/dark themes.
+ *
+ * This preview allows visualization of the permission prompt in both rationale and permanent denial states.
+ */
+@PreviewLightDark
+@PreviewDynamicColors
+@Composable
+private fun NotificationPermissionPromptPreview(
+ @PreviewParameter(ShouldShowRationaleProvider::class)
+ shouldShowRationale: Boolean,
+) {
+ GitHubNotifierTheme {
+ NotificationPermissionPrompt(
+ shouldShowRationale,
+ onRequestPermission = {},
+ onOpenSettings = {}
+ )
+ }
+}
diff --git a/app/src/main/java/com/notifier/app/core/presentation/notification/rememberNotificationPermissionState.kt b/app/src/main/java/com/notifier/app/core/presentation/notification/rememberNotificationPermissionState.kt
new file mode 100644
index 0000000..bf3a267
--- /dev/null
+++ b/app/src/main/java/com/notifier/app/core/presentation/notification/rememberNotificationPermissionState.kt
@@ -0,0 +1,42 @@
+package com.notifier.app.core.presentation.notification
+
+import android.Manifest
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.isGranted
+import com.google.accompanist.permissions.rememberPermissionState
+import com.google.accompanist.permissions.shouldShowRationale
+
+/**
+ * Remembers and provides the current state of the notification permission using
+ * [Manifest.permission.POST_NOTIFICATIONS] (available from Android 13/TIRAMISU).
+ *
+ * This function wraps the Accompanist [rememberPermissionState] for notifications into
+ * a [NotificationPermissionState] interface for simplified use in UI components.
+ *
+ * @return a [NotificationPermissionState] instance containing the current permission status,
+ * rationale flag, and a method to launch the permission request.
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+@OptIn(ExperimentalPermissionsApi::class)
+@Composable
+fun rememberNotificationPermissionState(): NotificationPermissionState {
+ val state = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
+
+ return remember(state) {
+ object : NotificationPermissionState {
+ override val shouldShowRationale: Boolean
+ get() = state.status.shouldShowRationale
+
+ override val isGranted: Boolean
+ get() = state.status.isGranted
+
+ override fun requestPermission() {
+ state.launchPermissionRequest()
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ef71109..5ded50b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -14,4 +14,8 @@
Connected successfully!
Continue
Connection failed. Please try again.
+ To deliver GitHub notifications like pull requests, issues, and mentions, we need notification access. Please allow it.
+ Notification access is required to show updates from your GitHub activity. Enable it from system settings.
+ Allow Notifications
+ Open Settings
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 300103e..af8c98b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -18,6 +18,7 @@ navigation = "2.8.9"
hiltNavigationCompose = "1.2.0"
datastore = "1.1.4"
splashScreen = "1.0.1"
+accompanistPermissions = "0.37.3"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -51,6 +52,7 @@ dagger-hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compi
dagger-hilt-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "daggerHilt" }
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
androidx-splash-screen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashScreen" }
+accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanistPermissions" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }