Login logic

This commit is contained in:
2021-09-07 14:15:25 +02:00
parent 1a71262008
commit 326cf267d2
20 changed files with 24759 additions and 67 deletions
@@ -3,6 +3,7 @@ package com.ericampire.android.androidstudycase
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
@@ -14,10 +15,11 @@ import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@ExperimentalMaterialApi
@ExperimentalPagerApi
@ExperimentalPermissionsApi
@ExperimentalAnimationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
@@ -1,6 +1,8 @@
package com.ericampire.android.androidstudycase.app
import android.app.Application
import android.content.Context
import android.content.Intent
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
@@ -8,4 +10,15 @@ class App : Application() {
override fun onCreate() {
super.onCreate()
}
companion object {
fun restart(context: Context) {
context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
addCategory(Intent.CATEGORY_HOME)
context.startActivity(this)
}
}
}
}
@@ -1,5 +1,6 @@
package com.ericampire.android.androidstudycase.app
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.material.ExperimentalMaterialApi
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
@@ -11,6 +12,7 @@ import com.ericampire.android.androidstudycase.util.Destination
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@ExperimentalAnimationApi
@ExperimentalMaterialApi
fun NavGraphBuilder.addHomeScreen(navController: NavController) {
composable(Destination.Home.route) {
@@ -0,0 +1,67 @@
package com.ericampire.android.androidstudycase.presentation.custom
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Facebook
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.ericampire.android.androidstudycase.R
import com.ericampire.android.androidstudycase.presentation.theme.AppColor
@Composable
fun CustomFacebookButton(
modifier: Modifier = Modifier,
enabled: Boolean = true,
onClick: () -> Unit
) {
Button(
enabled = enabled,
colors = ButtonDefaults.buttonColors(backgroundColor = AppColor.BlueFacebook),
modifier = modifier
.fillMaxWidth()
.height(50.dp),
onClick = onClick,
content = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
content = {
Icon(
tint = Color.White,
imageVector = Icons.Rounded.Facebook,
contentDescription = null,
)
Text(
color = Color.White,
style = MaterialTheme.typography.h5,
text = stringResource(R.string.txt_connect_with_facebook)
)
Icon(
tint = Color.Transparent,
imageVector = Icons.Rounded.Facebook,
contentDescription = null,
)
}
)
}
)
}
@Preview
@Composable
fun CustomFacebookButtonPreview() {
CustomFacebookButton() {
}
}
@@ -0,0 +1,67 @@
package com.ericampire.android.androidstudycase.presentation.custom
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Facebook
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.ericampire.android.androidstudycase.R
@Composable
fun CustomGoogleButton(
modifier: Modifier = Modifier,
enabled: Boolean = true,
onClick: () -> Unit
) {
OutlinedButton(
enabled = enabled,
colors = ButtonDefaults.buttonColors(backgroundColor = Color.White),
modifier = modifier
.fillMaxWidth()
.height(50.dp),
onClick = onClick,
content = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
content = {
Icon(
tint = Color.Unspecified,
painter = painterResource(id = R.drawable.ic_google),
contentDescription = null,
)
Text(
style = MaterialTheme.typography.h5,
color = MaterialTheme.colors.surface,
text = stringResource(R.string.txt_connect_with_google)
)
Icon(
tint = Color.Transparent,
imageVector = Icons.Rounded.Facebook,
contentDescription = null,
)
}
)
}
)
}
@Preview
@Composable
fun CustomGoogleButtonPreview() {
CustomGoogleButton {
}
}
@@ -140,8 +140,16 @@ fun ExploreScreen(
}
is Success -> {
val animations = it.invoke()
HorizontalPager(state = pagerState) {
ExploreContent(files = animations)
if (animations.isEmpty()) {
// Todo: Empty instead of Loading View
LoadingAnimation(
waveColor = MaterialTheme.colors.primary.copy(alpha = 0.5f),
arcColor = MaterialTheme.colors.primaryVariant
)
} else {
HorizontalPager(state = pagerState) {
ExploreContent(files = animations)
}
}
}
is Fail -> {
@@ -2,4 +2,6 @@ package com.ericampire.android.androidstudycase.presentation.screen.home.busines
sealed interface HomeAction {
object FetchData : HomeAction
object Login : HomeAction
object FetchCurrentUser : HomeAction
}
@@ -3,17 +3,17 @@ package com.ericampire.android.androidstudycase.presentation.screen.home.busines
import com.airbnb.mvrx.MavericksViewModelFactory
import com.ericampire.android.androidstudycase.app.hilt.AssistedViewModelFactory
import com.ericampire.android.androidstudycase.app.hilt.hiltMavericksViewModelFactory
import com.ericampire.android.androidstudycase.domain.usecase.FindFeaturedAnimatorUseCase
import com.ericampire.android.androidstudycase.domain.usecase.FindFeaturedBlogUseCase
import com.ericampire.android.androidstudycase.domain.usecase.FindFeaturedLottieFileUseCase
import com.ericampire.android.androidstudycase.domain.usecase.FindUsersUseCase
import com.ericampire.android.androidstudycase.domain.usecase.*
import com.ericampire.android.androidstudycase.util.PreviewData
import com.ericampire.android.androidstudycase.util.data
import com.ericampire.android.androidstudycase.util.mvi.BaseViewModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
class HomeViewModel @AssistedInject constructor(
@@ -21,7 +21,8 @@ class HomeViewModel @AssistedInject constructor(
private val findFeaturedBlogUseCase: FindFeaturedBlogUseCase,
private val findFeaturedAnimatorUseCase: FindFeaturedAnimatorUseCase,
private val findUsersUseCase: FindUsersUseCase,
private val findFeaturedLottieFileUseCase: FindFeaturedLottieFileUseCase
private val findFeaturedLottieFileUseCase: FindFeaturedLottieFileUseCase,
private val saveUserUseCase: SaveUserUseCase
) : BaseViewModel<HomeViewState, HomeAction>(initialState) {
@@ -30,25 +31,45 @@ class HomeViewModel @AssistedInject constructor(
pendingAction.collectLatest { action ->
when (action) {
HomeAction.FetchData -> fetchData()
HomeAction.Login -> login()
HomeAction.FetchCurrentUser -> fetchCurrentUser()
}
}
}
}
private fun login() {
viewModelScope.launch {
suspend {
delay(2000)
saveUserUseCase(PreviewData.User.data.first()).data!!
}.execute {
copy(login = it)
}
}
}
private fun fetchCurrentUser() {
viewModelScope.launch {
findUsersUseCase(Unit).map {
it.data?.firstOrNull()
}.execute {
copy(currentUser = it)
}
}
}
private fun fetchData() {
val userFlow = findUsersUseCase(Unit)
val storiesFlow = findFeaturedBlogUseCase(Unit)
val animatorsFlow = findFeaturedAnimatorUseCase(Unit)
val animationsFlow = findFeaturedLottieFileUseCase(Unit)
combine(
userFlow,
storiesFlow,
animationsFlow,
animatorsFlow
) { user, stories, anim, animators ->
) { stories, anim, animators ->
HomeContentData(
user = user.data?.firstOrNull(),
blog = stories.data ?: emptyList(),
featuredAnimators = animators.data ?: emptyList(),
featuredLottieFile = anim.data ?: emptyList(),
@@ -9,12 +9,13 @@ import com.ericampire.android.androidstudycase.domain.entity.Lottiefile
import com.ericampire.android.androidstudycase.domain.entity.User
data class HomeViewState(
val contentData: Async<HomeContentData> = Uninitialized
val contentData: Async<HomeContentData> = Uninitialized,
val login: Async<Unit> = Uninitialized,
val currentUser: Async<User?> = Uninitialized,
) : MavericksState
data class HomeContentData(
val blog: List<Blog> = emptyList(),
val featuredAnimators: List<Animator> = emptyList(),
val featuredLottieFile: List<Lottiefile> = emptyList(),
val user: User? = null,
) : MavericksState
@@ -1,6 +1,7 @@
package com.ericampire.android.androidstudycase.presentation.screen.home.ui
import androidx.compose.animation.Crossfade
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@@ -9,16 +10,16 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
@@ -29,6 +30,8 @@ import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel
import com.ericampire.android.androidstudycase.R
import com.ericampire.android.androidstudycase.app.App
import com.ericampire.android.androidstudycase.domain.entity.User
import com.ericampire.android.androidstudycase.presentation.custom.CustomImageView
import com.ericampire.android.androidstudycase.presentation.custom.LoadingAnimation
import com.ericampire.android.androidstudycase.presentation.custom.TopActionBar
@@ -37,9 +40,10 @@ import com.ericampire.android.androidstudycase.presentation.screen.home.business
import com.ericampire.android.androidstudycase.presentation.screen.home.business.HomeViewModel
import com.ericampire.android.androidstudycase.presentation.screen.home.business.HomeViewState
import com.ericampire.android.androidstudycase.presentation.theme.AppColor
import com.ericampire.android.androidstudycase.util.Destination
import kotlinx.coroutines.launch
import timber.log.Timber
@ExperimentalAnimationApi
@ExperimentalMaterialApi
@Composable
fun HomeScreen(
@@ -48,56 +52,87 @@ fun HomeScreen(
) {
val state by viewModel.collectAsState(HomeViewState::contentData)
val loginState by viewModel.collectAsState(HomeViewState::login)
val userState by viewModel.collectAsState(HomeViewState::currentUser)
val bottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
LaunchedEffect(viewModel) {
viewModel.submitAction(HomeAction.FetchData)
viewModel.submitAction(HomeAction.FetchCurrentUser)
}
Scaffold(
topBar = {
Column(
modifier = Modifier
.fillMaxWidth()
.background(color = AppColor.Black001),
content = {
TopActionBar()
}
LaunchedEffect(loginState) {
if (loginState is Success) {
App.restart(context)
}
}
ModalBottomSheetLayout(
sheetShape = RoundedCornerShape(topEnd = 24.dp, topStart = 24.dp),
sheetState = bottomSheetState,
sheetContent = {
LoginBottomSheet(
onLoginClick = {
viewModel.submitAction(HomeAction.Login)
},
isLoading = loginState is Loading
)
},
content = { contentPadding ->
Crossfade(modifier = Modifier.padding(contentPadding), targetState = state) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
content = {
when (it) {
Uninitialized -> {
LoadingAnimation(
waveColor = MaterialTheme.colors.primary.copy(alpha = 0.5f),
arcColor = MaterialTheme.colors.primaryVariant
)
}
is Loading -> {
LoadingAnimation(
waveColor = MaterialTheme.colors.primary.copy(alpha = 0.5f),
arcColor = MaterialTheme.colors.primaryVariant
)
}
is Success -> {
HomeContent(
state = it.invoke(),
onLoginClick = {
navController.navigate(Destination.Login.route)
}
)
}
is Fail -> {
Timber.e(it.error.localizedMessage)
}
content = {
Scaffold(
topBar = {
Column(
modifier = Modifier
.fillMaxWidth()
.background(color = AppColor.Black001),
content = {
TopActionBar()
}
)
},
content = { contentPadding ->
Crossfade(modifier = Modifier.padding(contentPadding), targetState = state) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
content = {
when (it) {
Uninitialized -> {
LoadingAnimation(
waveColor = MaterialTheme.colors.primary.copy(alpha = 0.5f),
arcColor = MaterialTheme.colors.primaryVariant
)
}
is Loading -> {
LoadingAnimation(
waveColor = MaterialTheme.colors.primary.copy(alpha = 0.5f),
arcColor = MaterialTheme.colors.primaryVariant
)
}
is Success -> {
HomeContent(
state = it.invoke(),
currentUser = userState.invoke(),
onLoginClick = {
coroutineScope.launch {
bottomSheetState.animateTo(targetValue = ModalBottomSheetValue.Expanded)
}
}
)
}
is Fail -> {
Timber.e(it.error.localizedMessage)
}
}
}
)
}
)
}
}
)
}
)
}
@@ -107,6 +142,7 @@ fun HomeScreen(
fun HomeContent(
modifier: Modifier = Modifier,
state: HomeContentData,
currentUser: User? = null,
onLoginClick: () -> Unit
) {
LazyColumn(
@@ -117,10 +153,10 @@ fun HomeContent(
verticalArrangement = Arrangement.spacedBy(16.dp),
content = {
item {
if (state.user == null) {
if (currentUser == null) {
UnLoggedUserHeaderView(onLoginClick = onLoginClick)
} else {
LoggedUserHeaderView(user = state.user)
LoggedUserHeaderView(user = currentUser)
}
}
@@ -42,7 +42,7 @@ fun LoggedUserHeaderView(
content = {
Text(
maxLines = 1,
text = "Monday September 4",
text = "Monday 4 September",
style = MaterialTheme.typography.button.copy(
color = Color.Gray
),
@@ -0,0 +1,117 @@
package com.ericampire.android.androidstudycase.presentation.screen.home.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.compose.*
import com.ericampire.android.androidstudycase.R
import com.ericampire.android.androidstudycase.presentation.custom.CustomFacebookButton
import com.ericampire.android.androidstudycase.presentation.custom.CustomGoogleButton
import com.ericampire.android.androidstudycase.presentation.theme.AndroidStudyCaseTheme
import com.ericampire.android.androidstudycase.presentation.theme.AppColor
@ExperimentalAnimationApi
@Composable
fun LoginBottomSheet(
modifier: Modifier = Modifier,
isLoading: Boolean = false,
onLoginClick: () -> Unit,
) {
val composition by rememberLottieComposition(
spec = LottieCompositionSpec.RawRes(R.raw.people_communicating)
)
val progress by animateLottieCompositionAsState(
composition = composition,
iterations = LottieConstants.IterateForever,
)
Column(
modifier = modifier
.clip(MaterialTheme.shapes.medium)
.background(AppColor.Black001),
content = {
AnimatedVisibility(isLoading) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
Column(
modifier = Modifier.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
content = {
Text(
textAlign = TextAlign.Center,
text = stringResource(id = R.string.txt_login_title),
style = MaterialTheme.typography.h4.copy(
color = MaterialTheme.colors.onSurface
),
)
Text(
textAlign = TextAlign.Center,
text = stringResource(id = R.string.txt_login_description),
style = MaterialTheme.typography.body1.copy(
color = MaterialTheme.colors.onSurface
),
)
LottieAnimation(
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
composition = composition,
progress = progress,
)
Text(
textAlign = TextAlign.Center,
text = stringResource(id = R.string.txt_get_started),
style = MaterialTheme.typography.body1.copy(
color = MaterialTheme.colors.onSurface
),
)
CustomFacebookButton(
modifier = Modifier.fillMaxWidth(),
onClick = onLoginClick
)
CustomGoogleButton(
modifier = Modifier.fillMaxWidth(),
onClick = onLoginClick
)
}
)
}
)
}
@ExperimentalAnimationApi
@ExperimentalMaterialApi
@Preview()
@Composable
fun LoginDialogViewPreview() {
AndroidStudyCaseTheme(darkTheme = true) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
content = {
LoginBottomSheet(
onLoginClick = {}
)
}
)
}
}
@@ -44,7 +44,7 @@ fun UnLoggedUserHeaderView(
content = {
Text(
maxLines = 1,
text = "Monday September 4",
text = "Monday 4 September",
style = MaterialTheme.typography.button.copy(
color = Color.Gray
),
@@ -1,5 +1,6 @@
package com.ericampire.android.androidstudycase.presentation.screen.main.ui
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.padding
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
@@ -29,7 +30,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
@ExperimentalMaterialApi
@ExperimentalPagerApi
@ExperimentalPermissionsApi
@ExperimentalAnimationApi
@Composable
fun MainScreen() {
val navController = rememberNavController()
@@ -12,7 +12,8 @@ object AppColor {
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
val Black001 = Color(0xFF222222)
val BlackOverlay = Color(0x4D000000)
val BlueFacebook = Color(0xFF3B5998)
val BlackOverlay = Color(0x4DFFFFFF)
val BlackOverlay001 = Color(0x1A000000)
val WhiteTransparent = Color(0x80FFFFFF)
}
+18
View File
@@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M23.745,12.27C23.745,11.48 23.675,10.73 23.555,10L12.255,10L12.255,14.51L18.725,14.51C18.435,15.99 17.585,17.24 16.325,18.09L16.325,21.09L20.185,21.09C22.445,19 23.745,15.92 23.745,12.27Z"
android:fillColor="#4285F4" />
<path
android:pathData="M12.255,24C15.495,24 18.205,22.92 20.185,21.09L16.325,18.09C15.245,18.81 13.875,19.25 12.255,19.25C9.125,19.25 6.475,17.14 5.525,14.29L1.545,14.29L1.545,17.38C3.515,21.3 7.565,24 12.255,24Z"
android:fillColor="#34A853" />
<path
android:pathData="M5.525,14.29C5.275,13.57 5.145,12.8 5.145,12C5.145,11.2 5.285,10.43 5.525,9.71L5.525,6.62L1.545,6.62C0.725,8.24 0.255,10.06 0.255,12C0.255,13.94 0.725,15.76 1.545,17.38L5.525,14.29Z"
android:fillColor="#FBBC05" />
<path
android:pathData="M12.255,4.75C14.025,4.75 15.605,5.36 16.855,6.55L20.275,3.13C18.205,1.19 15.495,0 12.255,0C7.565,0 3.515,2.7 1.545,6.62L5.525,9.71C6.475,6.86 9.125,4.75 12.255,4.75Z"
android:fillColor="#EA4335" />
</vector>
File diff suppressed because it is too large Load Diff
+3
View File
@@ -2,4 +2,7 @@
<string name="txt_featured_animation">Featured Animations</string>
<string name="txt_open_setting">Open Settings</string>
<string name="txt_request">Request permission</string>
<string name="txt_login_title">1000s of lottie animations</string>
<string name="txt_login_description">from top creators waiting to be discover, save and share from the palm of your hand</string>
<string name="txt_connect_with_facebook">Continue with Facebook</string>
</resources>
@@ -2,6 +2,7 @@ package com.ericampire.android.androidstudycase.data.room
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy.REPLACE
import androidx.room.Query
import com.ericampire.android.androidstudycase.domain.entity.User
import kotlinx.coroutines.flow.Flow
@@ -9,7 +10,7 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface UserDao {
@Insert
@Insert(onConflict = REPLACE)
fun save(user: User)
@Query("SELECT * FROM User")
+2
View File
@@ -21,4 +21,6 @@
<string name="txt_feature_not_available">Feature not available</string>
<string name="txt_camera_permssion_required">The camera is important for this app. Please grant the permission.</string>
<string name="txt_permission_denied">Camera permission denied. See this FAQ with information about why we need this permission. Please, grant us access on the Settings screen.</string>
<string name="txt_connect_with_google">Continue with Google</string>
<string name="txt_get_started">Get started for free</string>
</resources>