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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+