diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 99f0217f..b5a9a71c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,6 +9,8 @@ + + diff --git a/app/src/main/java/com/twofasapp/AppModule.kt b/app/src/main/java/com/twofasapp/AppModule.kt index 70244024..504f18c3 100644 --- a/app/src/main/java/com/twofasapp/AppModule.kt +++ b/app/src/main/java/com/twofasapp/AppModule.kt @@ -11,6 +11,7 @@ import com.twofasapp.environment.AppConfig import com.twofasapp.externalimport.domain.GoogleAuthenticatorImporter import com.twofasapp.permissions.CameraPermissionRequest import com.twofasapp.permissions.CameraPermissionRequestFlow +import com.twofasapp.permissions.NotificationsPermissionRequestFlow import com.twofasapp.services.analytics.AnalyticsServiceFirebase import com.twofasapp.services.googledrive.GoogleDriveService import com.twofasapp.services.googledrive.GoogleDriveServiceImpl @@ -43,6 +44,7 @@ val applicationModule = module { factory { CameraPermissionRequest(activityContext()) } factory { CameraPermissionRequestFlow(activityContext()) } + factory { NotificationsPermissionRequestFlow(activityContext()) } single { AppDispatchers() } diff --git a/app/src/main/java/com/twofasapp/navigation/SettingsRouterImpl.kt b/app/src/main/java/com/twofasapp/navigation/SettingsRouterImpl.kt index ca7f0af0..b32bb0ad 100644 --- a/app/src/main/java/com/twofasapp/navigation/SettingsRouterImpl.kt +++ b/app/src/main/java/com/twofasapp/navigation/SettingsRouterImpl.kt @@ -6,6 +6,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.composable import com.twofasapp.browserextension.ui.browser.BrowserDetailsScreenFactory import com.twofasapp.browserextension.ui.main.BrowserExtensionScreenFactory +import com.twofasapp.browserextension.ui.main.permission.BrowserExtensionPermissionScreenFactory import com.twofasapp.browserextension.ui.pairing.progress.PairingProgressScreenFactory import com.twofasapp.browserextension.ui.pairing.scan.PairingScanScreenFactory import com.twofasapp.settings.ui.main.SettingsMainScreenFactory @@ -19,6 +20,7 @@ class SettingsRouterImpl( private val pairingProgressScreenFactory: PairingProgressScreenFactory, private val pairingScanScreenFactory: PairingScanScreenFactory, private val browserDetailsScreenFactory: BrowserDetailsScreenFactory, + private val browserExtensionPermissionScreenFactory: BrowserExtensionPermissionScreenFactory, ) : SettingsRouter() { companion object { @@ -29,6 +31,7 @@ class SettingsRouterImpl( private const val BROWSER_EXTENSION = "browser_extension" private const val BROWSER_DETAILS = "browser_details/{$ARG_EXTENSION_ID}" private const val PAIRING_SCAN = "pairing_scan" + private const val PERMISSION = "permission" private const val PAIRING_PROGRESS = "pairing_progress/{$ARG_EXTENSION_ID}" } @@ -40,6 +43,7 @@ class SettingsRouterImpl( browserDetailsScreenFactory.create(it.arguments?.getString(ARG_EXTENSION_ID).orEmpty()) }) builder.composable(route = PAIRING_SCAN, content = { pairingScanScreenFactory.create() }) + builder.composable(route = PERMISSION, content = { browserExtensionPermissionScreenFactory.create() }) builder.composable(route = PAIRING_PROGRESS, content = { pairingProgressScreenFactory.create(it.arguments?.getString(ARG_EXTENSION_ID).orEmpty()) }) @@ -59,6 +63,7 @@ class SettingsRouterImpl( SettingsDirections.Theme -> navController.navigate(THEME) SettingsDirections.BrowserExtension -> navController.navigate(BROWSER_EXTENSION) SettingsDirections.PairingScan -> navController.navigate(PAIRING_SCAN) { popUpTo(BROWSER_EXTENSION) } + SettingsDirections.Permission -> navController.navigate(PERMISSION) { popUpTo(BROWSER_EXTENSION) } is SettingsDirections.PairingProgress -> navController.navigate( PAIRING_PROGRESS.replace("{$ARG_EXTENSION_ID}", direction.extensionId) diff --git a/browserextension/build.gradle.kts b/browserextension/build.gradle.kts index 3ff38222..16731ba2 100644 --- a/browserextension/build.gradle.kts +++ b/browserextension/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(libs.bundles.fastAdapter) implementation(libs.bundles.rxJava) implementation(libs.bundles.appCompat) + implementation(libs.bundles.accompanist) implementation(libs.lottie) implementation(libs.timber) implementation(libs.kotlinCoroutines) diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/BrowserExtensionModule.kt b/browserextension/src/main/java/com/twofasapp/browserextension/BrowserExtensionModule.kt index 84f3db93..1197cc8f 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/BrowserExtensionModule.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/BrowserExtensionModule.kt @@ -4,7 +4,20 @@ import com.twofasapp.browserextension.data.BrowserExtensionLocalData import com.twofasapp.browserextension.data.BrowserExtensionLocalDataImpl import com.twofasapp.browserextension.data.BrowserExtensionRemoteData import com.twofasapp.browserextension.data.BrowserExtensionRemoteDataImpl -import com.twofasapp.browserextension.domain.* +import com.twofasapp.browserextension.domain.ApproveLoginRequestCase +import com.twofasapp.browserextension.domain.DeletePairedBrowserCase +import com.twofasapp.browserextension.domain.DenyLoginRequestCase +import com.twofasapp.browserextension.domain.EncryptCodeCase +import com.twofasapp.browserextension.domain.FetchPairedBrowsersCase +import com.twofasapp.browserextension.domain.FetchTokenRequestsCase +import com.twofasapp.browserextension.domain.FetchTokenRequestsCaseImpl +import com.twofasapp.browserextension.domain.ObserveMobileDeviceCase +import com.twofasapp.browserextension.domain.ObserveMobileDeviceCaseImpl +import com.twofasapp.browserextension.domain.ObservePairedBrowsersCase +import com.twofasapp.browserextension.domain.ObservePairedBrowsersCaseImpl +import com.twofasapp.browserextension.domain.PairBrowserCase +import com.twofasapp.browserextension.domain.RegisterMobileDeviceCase +import com.twofasapp.browserextension.domain.UpdateMobileDeviceCase import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepositoryImpl import com.twofasapp.browserextension.notification.ShowBrowserExtensionRequestNotificationCaseImpl @@ -12,6 +25,8 @@ import com.twofasapp.browserextension.ui.browser.BrowserDetailsScreenFactory import com.twofasapp.browserextension.ui.browser.BrowserDetailsViewModel import com.twofasapp.browserextension.ui.main.BrowserExtensionScreenFactory import com.twofasapp.browserextension.ui.main.BrowserExtensionViewModel +import com.twofasapp.browserextension.ui.main.permission.BrowserExtensionPermissionScreenFactory +import com.twofasapp.browserextension.ui.main.permission.BrowserExtensionPermissionViewModel import com.twofasapp.browserextension.ui.pairing.progress.PairingProgressScreenFactory import com.twofasapp.browserextension.ui.pairing.progress.PairingProgressViewModel import com.twofasapp.browserextension.ui.pairing.scan.PairingScanScreenFactory @@ -46,6 +61,7 @@ class BrowserExtensionModule : KoinModule { singleOf(::DeletePairedBrowserCase) viewModelOf(::BrowserExtensionViewModel) + viewModelOf(::BrowserExtensionPermissionViewModel) viewModelOf(::PairingScanViewModel) viewModelOf(::PairingProgressViewModel) viewModelOf(::BrowserExtensionRequestViewModel) @@ -55,5 +71,6 @@ class BrowserExtensionModule : KoinModule { singleOf(::PairingProgressScreenFactory) singleOf(::PairingScanScreenFactory) singleOf(::BrowserDetailsScreenFactory) + singleOf(::BrowserExtensionPermissionScreenFactory) } } \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionScreen.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionScreen.kt index 4c82d666..b31eadf2 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionScreen.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionScreen.kt @@ -1,16 +1,38 @@ package com.twofasapp.browserextension.ui.main +import android.Manifest import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* +import androidx.compose.material.Button +import androidx.compose.material.Divider +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier @@ -22,25 +44,71 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ConstraintLayout -import com.twofasapp.resources.R -import com.twofasapp.design.compose.* +import androidx.core.content.ContextCompat +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.twofasapp.design.compose.ButtonShape +import com.twofasapp.design.compose.ButtonTextColor +import com.twofasapp.design.compose.HeaderEntry +import com.twofasapp.design.compose.SimpleEntry +import com.twofasapp.design.compose.Toolbar import com.twofasapp.design.compose.dialogs.InputDialog import com.twofasapp.design.compose.dialogs.RationaleDialog import com.twofasapp.extensions.openBrowserApp import com.twofasapp.navigation.SettingsDirections import com.twofasapp.navigation.SettingsRouter +import com.twofasapp.resources.R import kotlinx.coroutines.launch import org.koin.androidx.compose.get +@Composable +internal fun RequestPermission( + permission: String, + rationaleTitle: String = "", + rationaleText: String = "", + onGranted: () -> Unit = {}, + onDismiss: () -> Unit = {}, +) { + var showRationale by remember { mutableStateOf(false) } + + val launcher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + onGranted() + } else { + showRationale = true + } + } + + val permissionCheckResult = ContextCompat.checkSelfPermission(LocalContext.current, permission) + if (permissionCheckResult == PackageManager.PERMISSION_GRANTED) { + onGranted() + } else { + LaunchedEffect(Unit) { + launcher.launch(permission) + } + } + + if (showRationale) { + RationaleDialog( + title = rationaleTitle, text = rationaleText, onDismiss = onDismiss + ) + } +} + @Composable internal fun BrowserExtensionScreen( viewModel: BrowserExtensionViewModel = get(), - router: SettingsRouter = get(), + router: SettingsRouter = get() ) { val uiState = viewModel.uiState.collectAsState().value val scaffoldState = rememberScaffoldState() val scope = rememberCoroutineScope() + var askForPermission by remember { mutableStateOf(false) } Scaffold( scaffoldState = scaffoldState, @@ -49,17 +117,28 @@ internal fun BrowserExtensionScreen( if (uiState.isLoading) return@Scaffold if (uiState.pairedBrowsers.isEmpty()) { - EmptyScreen(viewModel, padding) + EmptyScreen(router = router, padding = padding, onAddClick = { askForPermission = true }) } else { ContentScreen( viewModel = viewModel, uiState = uiState, router = router, - padding = padding + onAddClick = { askForPermission = true }, + padding = padding, ) } } + if (askForPermission) { + RequestPermission( + permission = Manifest.permission.CAMERA, + onGranted = { router.navigate(SettingsDirections.PairingScan) }, + onDismiss = { askForPermission = false }, + rationaleTitle = stringResource(id = R.string.permissions__camera_permission), + rationaleText = stringResource(id = R.string.permissions__camera_permission_description), + ) + } + uiState.getMostRecentEvent()?.let { when (it) { is BrowserExtensionUiState.Event.ShowSnackbarError -> { @@ -73,32 +152,33 @@ internal fun BrowserExtensionScreen( } } +@OptIn(ExperimentalPermissionsApi::class) @Composable internal fun ContentScreen( viewModel: BrowserExtensionViewModel, uiState: BrowserExtensionUiState, router: SettingsRouter, + onAddClick: () -> Unit, padding: PaddingValues, ) { + val activity = LocalContext.current as Activity + val permissionState = rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS) + LazyColumn(modifier = Modifier.padding(padding)) { item { HeaderEntry(text = stringResource(id = R.string.browser__paired_devices_browser_title)) } items(uiState.pairedBrowsers, key = { it.id }) { - SimpleEntry( - title = it.name, + SimpleEntry(title = it.name, iconVisibleWhenNotSet = true, subtitle = it.formatPairedAt(), - click = { router.navigate(SettingsDirections.BrowserDetails(extensionId = it.id)) } - ) + click = { router.navigate(SettingsDirections.BrowserDetails(extensionId = it.id)) }) } item { Button( - onClick = { viewModel.onPairBrowserClick() }, - shape = ButtonShape(), - modifier = Modifier.padding(start = 72.dp, top = 6.dp, bottom = 2.dp) + onClick = { onAddClick() }, shape = ButtonShape(), modifier = Modifier.padding(start = 72.dp, top = 6.dp, bottom = 2.dp) ) { Text(text = stringResource(id = R.string.browser__add_new).uppercase(), color = ButtonTextColor()) } @@ -116,14 +196,30 @@ internal fun ContentScreen( iconEndClick = { viewModel.onEditDeviceClick() }, ) } - } - if (uiState.showRationaleDialog) { - RationaleDialog( - title = stringResource(id = R.string.permissions__camera_permission), - text = stringResource(id = R.string.permissions__camera_permission_description), - onDismiss = { viewModel.onRationaleDialogDismiss() } - ) + if (permissionState.status.isGranted.not()) { + item { + Divider(Modifier.padding(top = 24.dp, bottom = 24.dp)) + Text( + text = stringResource(id = R.string.browser__push_notifications_content), + style = MaterialTheme.typography.body2.copy(fontSize = 14.sp, color = MaterialTheme.colors.primary), + modifier = Modifier.padding(start = 72.dp, bottom = 8.dp, end = 16.dp) + ) + } + item { + Button( + onClick = { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", activity.packageName, null)) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + activity.startActivity(intent) + }, + shape = ButtonShape(), + modifier = Modifier.padding(start = 72.dp, top = 6.dp, bottom = 2.dp) + ) { + Text(text = "Enable Notifications".uppercase(), color = ButtonTextColor()) + } + } + } } if (uiState.showEditDeviceDialog) { @@ -140,28 +236,27 @@ internal fun ContentScreen( @Composable internal fun EmptyScreen( - viewModel: BrowserExtensionViewModel, + router: SettingsRouter, padding: PaddingValues, + onAddClick: () -> Unit, ) { val activity = (LocalContext.current as? Activity) - ConstraintLayout(modifier = Modifier - .fillMaxSize() - .padding(padding)) { + ConstraintLayout( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { val (content, pair) = createRefs() - Column( - verticalArrangement = Arrangement.spacedBy(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .constrainAs(content) { - top.linkTo(parent.top) - bottom.linkTo(pair.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - } - .padding(vertical = 16.dp) - ) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp), horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier + .constrainAs(content) { + top.linkTo(parent.top) + bottom.linkTo(pair.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + .padding(vertical = 16.dp)) { Image( painter = painterResource(id = R.drawable.browser_extension_start_image), contentDescription = null, @@ -184,28 +279,29 @@ internal fun EmptyScreen( modifier = Modifier.padding(horizontal = 16.dp) ) - Text(text = buildAnnotatedString { - append(stringResource(id = R.string.browser__more_info) + " ") - withStyle(style = SpanStyle(MaterialTheme.colors.primary)) { - append(stringResource(id = R.string.browser__more_info_link_title)) - } - }, style = MaterialTheme.typography.body2, modifier = Modifier - .padding(horizontal = 16.dp) - .align(CenterHorizontally) - .clickable { - activity?.openBrowserApp(url = "https://2fas.com/be") - }, textAlign = TextAlign.Center) + Text( + text = buildAnnotatedString { + append(stringResource(id = R.string.browser__more_info) + " ") + withStyle(style = SpanStyle(MaterialTheme.colors.primary)) { + append(stringResource(id = R.string.browser__more_info_link_title)) + } + }, style = MaterialTheme.typography.body2, modifier = Modifier + .padding(horizontal = 16.dp) + .align(CenterHorizontally) + .clickable { + activity?.openBrowserApp(url = "https://2fas.com/be") + }, textAlign = TextAlign.Center + ) } - Button(onClick = { viewModel.onPairBrowserClick() }, - shape = ButtonShape(), - modifier = Modifier - .height(48.dp) - .constrainAs(pair) { - bottom.linkTo(parent.bottom, margin = 16.dp) - start.linkTo(parent.start) - end.linkTo(parent.end) - }) { + + Button(onClick = { onAddClick() }, shape = ButtonShape(), modifier = Modifier + .height(48.dp) + .constrainAs(pair) { + bottom.linkTo(parent.bottom, margin = 16.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }) { Text(text = stringResource(id = R.string.browser__pair_with_web_browser).uppercase(), color = ButtonTextColor()) } } diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionUiState.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionUiState.kt index 2245b400..839db070 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionUiState.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionUiState.kt @@ -9,8 +9,6 @@ internal data class BrowserExtensionUiState( val isLoading: Boolean = true, val pairedBrowsers: List = emptyList(), val mobileDevice: MobileDevice? = null, - val isCameraPermissionGranted: Boolean = false, - val showRationaleDialog: Boolean = false, val showEditDeviceDialog: Boolean = false, override val events: List = emptyList(), ) : UiState { diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionViewModel.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionViewModel.kt index c83e41be..16fbf63a 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionViewModel.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionViewModel.kt @@ -7,17 +7,15 @@ import com.twofasapp.browserextension.domain.FetchPairedBrowsersCase import com.twofasapp.browserextension.domain.ObserveMobileDeviceCase import com.twofasapp.browserextension.domain.ObservePairedBrowsersCase import com.twofasapp.browserextension.domain.UpdateMobileDeviceCase -import com.twofasapp.navigation.SettingsDirections -import com.twofasapp.navigation.SettingsRouter -import com.twofasapp.permissions.CameraPermissionRequestFlow -import com.twofasapp.permissions.PermissionStatus -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch internal class BrowserExtensionViewModel( private val dispatchers: Dispatchers, - private val settingsRouter: SettingsRouter, - private val cameraPermissionRequest: CameraPermissionRequestFlow, private val observeMobileDeviceCase: ObserveMobileDeviceCase, private val observePairedBrowsersCase: ObservePairedBrowsersCase, private val updateMobileDeviceCase: UpdateMobileDeviceCase, @@ -51,24 +49,6 @@ internal class BrowserExtensionViewModel( } } - fun onPairBrowserClick() { - cameraPermissionRequest.execute() - .take(1) - .onEach { - when (it) { - PermissionStatus.GRANTED -> settingsRouter.navigate(SettingsDirections.PairingScan) - PermissionStatus.DENIED -> Unit - PermissionStatus.DENIED_NEVER_ASK -> _uiState.update { state -> - state.copy(showRationaleDialog = true) - } - } - }.launchIn(viewModelScope) - } - - fun onRationaleDialogDismiss() { - _uiState.update { it.copy(showRationaleDialog = false) } - } - fun onEditDeviceClick() { _uiState.update { it.copy(showEditDeviceDialog = true) } } diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/permission/BrowserExtensionPermissionScreen.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/permission/BrowserExtensionPermissionScreen.kt new file mode 100644 index 00000000..e0a1cf57 --- /dev/null +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/permission/BrowserExtensionPermissionScreen.kt @@ -0,0 +1,121 @@ +package com.twofasapp.browserextension.ui.main.permission + +import android.Manifest +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import com.twofasapp.browserextension.ui.main.RequestPermission +import com.twofasapp.design.compose.ButtonHeight +import com.twofasapp.design.compose.ButtonShape +import com.twofasapp.design.compose.ButtonTextColor +import com.twofasapp.design.compose.Toolbar +import com.twofasapp.navigation.SettingsDirections +import com.twofasapp.navigation.SettingsRouter +import com.twofasapp.resources.R +import org.koin.androidx.compose.get + +@Composable +internal fun BrowserExtensionPermissionScreen( + router: SettingsRouter = get(), +) { + // Launched only on >= TIRAMISU + var askForPermission by remember { mutableStateOf(false) } + + Scaffold( + topBar = { + Toolbar(title = stringResource(id = R.string.browser__browser_extension)) { + router.navigate(SettingsDirections.GoBack) + } + } + ) { padding -> + + ConstraintLayout( + modifier = Modifier + .fillMaxHeight() + .padding(horizontal = 16.dp) + ) { + val (content, pair) = createRefs() + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .constrainAs(content) { + top.linkTo(parent.top) + bottom.linkTo(pair.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + .padding(vertical = 16.dp) + ) { + Image( + painter = painterResource(id = R.drawable.browser_extension_permission_image), + contentDescription = null, + modifier = Modifier + .height(130.dp) + .offset(y = (-16).dp) + ) + + Text( + text = stringResource(id = R.string.browser__push_notifications_title), + style = MaterialTheme.typography.h6, + textAlign = TextAlign.Center, + modifier = Modifier + ) + + Text( + text = stringResource(id = R.string.browser__push_notifications_content), + style = MaterialTheme.typography.body1, + textAlign = TextAlign.Center, + modifier = Modifier + ) + } + + Button( + onClick = { askForPermission = true }, + shape = ButtonShape(), + modifier = Modifier + .height(ButtonHeight()) + .constrainAs(pair) { + bottom.linkTo(parent.bottom, margin = 16.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }) { + Text(text = stringResource(id = R.string.commons__continue).uppercase(), color = ButtonTextColor()) + } + + if (askForPermission) { + RequestPermission( + permission = Manifest.permission.POST_NOTIFICATIONS, + onGranted = { router.navigateBack() }, + onDismiss = { + askForPermission = false + router.navigateBack() + }, + rationaleTitle = stringResource(id = R.string.browser__push_notifications_title), + rationaleText = stringResource(id = R.string.browser__push_notifications_content), + ) + } + } + } +} \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/permission/BrowserExtensionPermissionScreenFactory.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/permission/BrowserExtensionPermissionScreenFactory.kt new file mode 100644 index 00000000..36a6d0cc --- /dev/null +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/permission/BrowserExtensionPermissionScreenFactory.kt @@ -0,0 +1,11 @@ +package com.twofasapp.browserextension.ui.main.permission + +import androidx.compose.runtime.Composable + +class BrowserExtensionPermissionScreenFactory { + + @Composable + fun create() { + BrowserExtensionPermissionScreen() + } +} \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/permission/BrowserExtensionPermissionViewModel.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/permission/BrowserExtensionPermissionViewModel.kt new file mode 100644 index 00000000..a10be459 --- /dev/null +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/permission/BrowserExtensionPermissionViewModel.kt @@ -0,0 +1,28 @@ +package com.twofasapp.browserextension.ui.main.permission + +import androidx.lifecycle.viewModelScope +import com.twofasapp.base.BaseViewModel +import com.twofasapp.permissions.NotificationsPermissionRequestFlow +import com.twofasapp.permissions.PermissionStatus +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +internal class BrowserExtensionPermissionViewModel( + private val notificationsPermissionRequestFlow: NotificationsPermissionRequestFlow, +) : BaseViewModel() { + + val permissionStatus = MutableStateFlow(null) + + fun askForPermission() { + viewModelScope.launch { + notificationsPermissionRequestFlow.execute() + .take(1) + .collect { status -> + println("dupa: $status") + permissionStatus.update { status } + } + } + } +} diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressScreen.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressScreen.kt index c14d9dc2..925f2f85 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressScreen.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressScreen.kt @@ -1,7 +1,14 @@ package com.twofasapp.browserextension.ui.pairing.progress +import android.app.NotificationManager +import android.os.Build import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding import androidx.compose.material.Button import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold @@ -16,11 +23,19 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout -import com.airbnb.lottie.compose.* -import com.twofasapp.resources.R -import com.twofasapp.design.compose.* +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition +import com.twofasapp.design.compose.AnimatedContent +import com.twofasapp.design.compose.ButtonHeight +import com.twofasapp.design.compose.ButtonShape +import com.twofasapp.design.compose.ButtonTextColor +import com.twofasapp.design.compose.Toolbar import com.twofasapp.navigation.SettingsDirections import com.twofasapp.navigation.SettingsRouter +import com.twofasapp.resources.R import org.koin.androidx.compose.get @Composable @@ -28,6 +43,7 @@ internal fun PairingProgressScreen( extensionId: String, viewModel: PairingProgressViewModel = get(), router: SettingsRouter = get(), + notificationManager: NotificationManager = get() ) { val uiState = viewModel.uiState.collectAsState() viewModel.pairBrowser(extensionId) @@ -42,7 +58,7 @@ internal fun PairingProgressScreen( AnimatedContent( condition = uiState.value.isPairing, contentWhenTrue = { ProgressContent() }, - contentWhenFalse = { ResultContent(uiState.value.isPairingSuccess, uiState.value.code, router) } + contentWhenFalse = { ResultContent(uiState.value.isPairingSuccess, uiState.value.code, router, notificationManager) } ) } } @@ -60,6 +76,7 @@ internal fun ResultContent( isSuccess: Boolean, code: Int? = null, router: SettingsRouter, + notificationManager: NotificationManager, ) { val image = if (isSuccess) R.drawable.browser_extension_success_image else R.drawable.browser_extension_error_image @@ -78,7 +95,15 @@ internal fun ResultContent( val cta = if (isSuccess) R.string.commons__continue else R.string.browser__result_error_cta val ctaAction: () -> Unit = if (isSuccess) { - { router.navigate(SettingsDirections.GoBack) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (notificationManager.areNotificationsEnabled()) { + { router.navigate(SettingsDirections.GoBack) } + } else { + { router.navigate(SettingsDirections.Permission) } + } + } else { + { router.navigate(SettingsDirections.GoBack) } + } } else { { router.navigate(SettingsDirections.PairingScan) } } diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressViewModel.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressViewModel.kt index 7c6c9602..229b629c 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressViewModel.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressViewModel.kt @@ -6,6 +6,7 @@ import com.twofasapp.base.dispatcher.Dispatchers import com.twofasapp.browserextension.domain.PairBrowserCase import com.twofasapp.browserextension.domain.RegisterMobileDeviceCase import com.twofasapp.network.exception.BrowserAlreadyPairedException +import com.twofasapp.permissions.NotificationsPermissionRequestFlow import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -16,6 +17,7 @@ internal class PairingProgressViewModel( private val dispatchers: Dispatchers, private val registerMobileDeviceCase: RegisterMobileDeviceCase, private val pairBrowserCase: PairBrowserCase, + private val notificationsPermissionRequestFlow: NotificationsPermissionRequestFlow, ) : BaseViewModel() { private val _uiState = MutableStateFlow(PairingProgressUiState()) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a34a55cd..92b94df7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,6 +27,7 @@ accompanistPlaceholder = { module = "com.google.accompanist:accompanist-placehol accompanistSystemUi = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } accompanistPager = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanist" } accompanistPagerIndicators = { module = "com.google.accompanist:accompanist-pager-indicators", version.ref = "accompanist" } +accompanistPermissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } coil = { module = "io.coil-kt:coil", version.ref = "coil" } coilCompose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } composeActivity = { module = "androidx.activity:activity-compose", version.ref = "composeActivity" } @@ -153,6 +154,7 @@ accompanist = [ "accompanistSystemUi", "accompanistPager", "accompanistPagerIndicators", + "accompanistPermissions" ] coil = [ "coil", diff --git a/navigation/src/main/java/com/twofasapp/navigation/SettingsDirections.kt b/navigation/src/main/java/com/twofasapp/navigation/SettingsDirections.kt index f09e66f9..50a72964 100644 --- a/navigation/src/main/java/com/twofasapp/navigation/SettingsDirections.kt +++ b/navigation/src/main/java/com/twofasapp/navigation/SettingsDirections.kt @@ -1,5 +1,7 @@ package com.twofasapp.navigation +import android.os.Build +import androidx.annotation.RequiresApi import com.twofasapp.navigation.base.Directions sealed interface SettingsDirections : Directions { @@ -9,5 +11,6 @@ sealed interface SettingsDirections : Directions { object BrowserExtension : SettingsDirections class BrowserDetails(val extensionId: String) : SettingsDirections object PairingScan : SettingsDirections + object Permission : SettingsDirections class PairingProgress(val extensionId: String) : SettingsDirections } \ No newline at end of file diff --git a/permissions/src/main/java/com/twofasapp/permissions/NotificationsPermissionRequestFlow.kt b/permissions/src/main/java/com/twofasapp/permissions/NotificationsPermissionRequestFlow.kt new file mode 100644 index 00000000..3aaeaaa7 --- /dev/null +++ b/permissions/src/main/java/com/twofasapp/permissions/NotificationsPermissionRequestFlow.kt @@ -0,0 +1,14 @@ +package com.twofasapp.permissions + +import android.Manifest +import android.app.Activity +import android.os.Build +import androidx.annotation.RequiresApi +import com.twofasapp.permissions.internal.PermissionRequest +import com.twofasapp.permissions.internal.PermissionRequestFlow + +class NotificationsPermissionRequestFlow(activity: Activity) : PermissionRequestFlow(activity) { + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + override val permission: String = Manifest.permission.POST_NOTIFICATIONS +} \ No newline at end of file diff --git a/resources/src/main/res/drawable-night/browser_extension_permission_image.xml b/resources/src/main/res/drawable-night/browser_extension_permission_image.xml new file mode 100644 index 00000000..f29a4dba --- /dev/null +++ b/resources/src/main/res/drawable-night/browser_extension_permission_image.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/src/main/res/drawable/browser_extension_permission_image.xml b/resources/src/main/res/drawable/browser_extension_permission_image.xml new file mode 100644 index 00000000..72cd1d65 --- /dev/null +++ b/resources/src/main/res/drawable/browser_extension_permission_image.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +