├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── nativemobilebits │ │ └── loginflow │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── nativemobilebits │ │ │ └── loginflow │ │ │ ├── LoginFlowApp.kt │ │ │ ├── MainActivity.kt │ │ │ ├── app │ │ │ └── PostOfficeApp.kt │ │ │ ├── components │ │ │ └── AppComponents.kt │ │ │ ├── data │ │ │ ├── NavigationItem.kt │ │ │ ├── RegistrationUIState.kt │ │ │ ├── home │ │ │ │ └── HomeViewModel.kt │ │ │ ├── login │ │ │ │ ├── LoginUIEvent.kt │ │ │ │ ├── LoginUIState.kt │ │ │ │ └── LoginViewModel.kt │ │ │ ├── rules │ │ │ │ └── Validator.kt │ │ │ └── signup │ │ │ │ ├── SignupUIEvent.kt │ │ │ │ └── SignupViewModel.kt │ │ │ ├── navigation │ │ │ ├── PostOfficeAppRouter.kt │ │ │ └── SystemBackButtonHandler.kt │ │ │ ├── screens │ │ │ ├── HomeScreen.kt │ │ │ ├── LoginScreen.kt │ │ │ ├── SignUpScreen.kt │ │ │ └── TermsAndConditionsScreen.kt │ │ │ └── ui │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── ComponentsShape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ ├── ic_lock.xml │ │ ├── layout.png │ │ ├── lock.xml │ │ ├── message.xml │ │ └── profile.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-anydpi-v33 │ │ └── ic_launcher.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── nativemobilebits │ └── loginflow │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── first.png └── second.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | LoginFlow -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SuperApp built with JetpackCompose 2 | 3 | 4 | This is an in progress project built using JETPACK COMPOSE, all the implementation videos can be found at Native Mobile Bits. 5 | 6 | In this video, we will explore how to implement a login screen using Jetpack Compose, with a particular focus on optimizing keyboard options and actions.! 7 | 8 | 9 | 10 | Building a Complete Login Registration Flow in Jetpack Compose | Step-by-Step Tutorial 11 |
12 |
13 | Login Screen Implementation using Jetpack Compose | Keyboard Options & Actions. 14 |
15 |
16 | Architecting your Compose UI | Let's implement & learn | Jetpack Compose Series 17 |
18 |
19 | State Validation | How to manage Recomposition | Validator in Jetpack Compose 20 |
21 |
22 | Mastering Firebase Integration: Jetpack Compose App with Login and SignUp Features 23 |
24 |
25 | Scaffold in Jetpack Compose : Building Cool Apps Made Easy! 26 |
27 |
28 | Navigation Drawer in Jetpack Compose : Simplified Mastery with clear implementation 29 |
30 |
31 | -> Elegant Session Management in Jetpack Compose : Crafting an Elegant User Journey We are here, follow along 32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | If you love this repo, Drop a Star, Lets connect over here :) 40 | 41 | 42 | [![Linkedin Badge](https://img.shields.io/badge/-LinkedIn-0e76a8?style=flat-square&logo=Linkedin&logoColor=white)](https://www.linkedin.com/in/sachin-rajput-998b48105/) 43 | [![Website Badge](https://img.shields.io/badge/Medium-3b5998?style=flat-square&logo=google-chrome&logoColor=white)](https://droid-lover.medium.com/) 44 | [![Stackoverflow Badge](https://img.shields.io/badge/-Stackoverflow-FFA500?style=flat-square&logo=Stackoverflow&logoColor=orange)](https://stackoverflow.com/users/7193506/sachin) 45 | [![Twitter Badge](https://img.shields.io/twitter/follow/native_MB?style=social)](https://twitter.com/native_MB) 46 | 47 |
48 | 49 | [![Youtube Badge](https://img.shields.io/badge/YouTube-FF0000?style=for-the-badge&logo=youtube&logoColor=white)](https://www.youtube.com/channel/UCTjQSpx2waqXTC37AgM8qyA) 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Copyright (C) 2023 Sachin (Native Mobile Bits) 58 | 59 | 60 | ` You can fork it to use but not for commercial use` 61 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("kotlin-android") 4 | id("kotlin-kapt") 5 | id("com.google.gms.google-services") 6 | } 7 | 8 | android { 9 | compileSdk = 33 10 | compileSdkPreview = "UpsideDownCake" 11 | namespace = "com.nativemobilebits.loginflow" 12 | 13 | defaultConfig { 14 | applicationId = "com.nativemobilebits.loginflow" 15 | minSdk = 21 16 | targetSdk = 33 17 | versionCode = 1 18 | versionName = "1.0" 19 | 20 | vectorDrawables.useSupportLibrary = true 21 | } 22 | 23 | buildTypes { 24 | getByName("release") { 25 | isMinifyEnabled = false 26 | proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") 27 | } 28 | } 29 | flavorDimensions += "environment" 30 | productFlavors { 31 | create("dev") 32 | create("staging") 33 | create("prod") 34 | } 35 | compileOptions { 36 | sourceCompatibility = JavaVersion.VERSION_17 37 | targetCompatibility = JavaVersion.VERSION_17 38 | } 39 | kotlinOptions { 40 | jvmTarget = "17" 41 | } 42 | buildFeatures { 43 | compose = true 44 | buildConfig = true 45 | } 46 | composeOptions { 47 | kotlinCompilerExtensionVersion = "1.4.5" 48 | } 49 | } 50 | 51 | 52 | dependencies { 53 | val composeVersion = "1.4.2" 54 | 55 | implementation("androidx.core:core-ktx:1.10.0") 56 | implementation("androidx.appcompat:appcompat:1.6.1") 57 | implementation("com.google.android.material:material:1.9.0") 58 | implementation("androidx.compose.ui:ui:$composeVersion") 59 | implementation("androidx.activity:activity-compose:$composeVersion") 60 | implementation("androidx.compose.material:material:$composeVersion") 61 | implementation("androidx.compose.ui:ui-tooling-preview:$composeVersion") 62 | implementation("androidx.compose.material:material-icons-extended:$composeVersion") 63 | implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1") 64 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") 65 | 66 | implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1") 67 | 68 | implementation(platform("com.google.firebase:firebase-bom:32.0.0")) 69 | 70 | implementation("com.google.firebase:firebase-auth-ktx") 71 | } 72 | 73 | kapt { 74 | correctErrorTypes = true 75 | } 76 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/nativemobilebits/loginflow/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.nativemobilebits.loginflow", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/LoginFlowApp.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow 2 | 3 | import android.app.Application 4 | import com.google.firebase.FirebaseApp 5 | 6 | class LoginFlowApp : Application() { 7 | 8 | override fun onCreate() { 9 | super.onCreate() 10 | 11 | FirebaseApp.initializeApp(this) 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.tooling.preview.Preview 8 | import androidx.core.view.WindowCompat 9 | import com.google.firebase.FirebaseApp 10 | import com.google.firebase.auth.ktx.auth 11 | import com.google.firebase.ktx.Firebase 12 | import com.nativemobilebits.loginflow.app.PostOfficeApp 13 | 14 | class MainActivity : ComponentActivity() { 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | setContent { 19 | PostOfficeApp() 20 | } 21 | } 22 | } 23 | 24 | @Preview 25 | @Composable 26 | fun DefaultPreview(){ 27 | PostOfficeApp() 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/app/PostOfficeApp.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.app 2 | 3 | import androidx.activity.viewModels 4 | import androidx.compose.animation.Crossfade 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material.Surface 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.lifecycle.viewmodel.compose.viewModel 11 | import com.nativemobilebits.loginflow.data.home.HomeViewModel 12 | import com.nativemobilebits.loginflow.navigation.PostOfficeAppRouter 13 | import com.nativemobilebits.loginflow.navigation.Screen 14 | import com.nativemobilebits.loginflow.screens.HomeScreen 15 | import com.nativemobilebits.loginflow.screens.LoginScreen 16 | import com.nativemobilebits.loginflow.screens.SignUpScreen 17 | import com.nativemobilebits.loginflow.screens.TermsAndConditionsScreen 18 | 19 | @Composable 20 | fun PostOfficeApp(homeViewModel: HomeViewModel = viewModel()) { 21 | 22 | homeViewModel.checkForActiveSession() 23 | 24 | Surface( 25 | modifier = Modifier.fillMaxSize(), 26 | color = Color.White 27 | ) { 28 | 29 | if (homeViewModel.isUserLoggedIn.value == true) { 30 | PostOfficeAppRouter.navigateTo(Screen.HomeScreen) 31 | } 32 | 33 | Crossfade(targetState = PostOfficeAppRouter.currentScreen) { currentState -> 34 | when (currentState.value) { 35 | is Screen.SignUpScreen -> { 36 | SignUpScreen() 37 | } 38 | 39 | is Screen.TermsAndConditionsScreen -> { 40 | TermsAndConditionsScreen() 41 | } 42 | 43 | is Screen.LoginScreen -> { 44 | LoginScreen() 45 | } 46 | 47 | is Screen.HomeScreen -> { 48 | HomeScreen() 49 | } 50 | } 51 | } 52 | 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/components/AppComponents.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.components 2 | 3 | import android.util.Log 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.foundation.lazy.LazyColumn 8 | import androidx.compose.foundation.lazy.items 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.foundation.text.ClickableText 11 | import androidx.compose.foundation.text.KeyboardActions 12 | import androidx.compose.foundation.text.KeyboardOptions 13 | import androidx.compose.material.* 14 | import androidx.compose.material.icons.Icons 15 | import androidx.compose.material.icons.filled.Logout 16 | import androidx.compose.material.icons.filled.Menu 17 | import androidx.compose.material.icons.filled.Visibility 18 | import androidx.compose.material.icons.filled.VisibilityOff 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.draw.clip 25 | import androidx.compose.ui.geometry.Offset 26 | import androidx.compose.ui.graphics.Brush 27 | import androidx.compose.ui.graphics.Color 28 | import androidx.compose.ui.graphics.Shadow 29 | import androidx.compose.ui.graphics.painter.Painter 30 | import androidx.compose.ui.platform.LocalFocusManager 31 | import androidx.compose.ui.res.colorResource 32 | import androidx.compose.ui.res.stringResource 33 | import androidx.compose.ui.text.SpanStyle 34 | import androidx.compose.ui.text.TextStyle 35 | import androidx.compose.ui.text.buildAnnotatedString 36 | import androidx.compose.ui.text.font.FontStyle 37 | import androidx.compose.ui.text.font.FontWeight 38 | import androidx.compose.ui.text.input.ImeAction 39 | import androidx.compose.ui.text.input.KeyboardType 40 | import androidx.compose.ui.text.input.PasswordVisualTransformation 41 | import androidx.compose.ui.text.input.VisualTransformation 42 | import androidx.compose.ui.text.style.TextAlign 43 | import androidx.compose.ui.text.style.TextDecoration 44 | import androidx.compose.ui.text.withStyle 45 | import androidx.compose.ui.unit.TextUnit 46 | import androidx.compose.ui.unit.dp 47 | import androidx.compose.ui.unit.sp 48 | import com.nativemobilebits.loginflow.R 49 | import com.nativemobilebits.loginflow.data.NavigationItem 50 | import com.nativemobilebits.loginflow.ui.theme.* 51 | 52 | @Composable 53 | fun NormalTextComponent(value: String) { 54 | Text( 55 | text = value, 56 | modifier = Modifier 57 | .fillMaxWidth() 58 | .heightIn(min = 40.dp), 59 | style = TextStyle( 60 | fontSize = 24.sp, 61 | fontWeight = FontWeight.Normal, 62 | fontStyle = FontStyle.Normal 63 | ), color = colorResource(id = R.color.colorText), 64 | textAlign = TextAlign.Center 65 | ) 66 | } 67 | 68 | @Composable 69 | fun HeadingTextComponent(value: String) { 70 | Text( 71 | text = value, 72 | modifier = Modifier 73 | .fillMaxWidth() 74 | .heightIn(), 75 | style = TextStyle( 76 | fontSize = 30.sp, 77 | fontWeight = FontWeight.Bold, 78 | fontStyle = FontStyle.Normal 79 | ), color = colorResource(id = R.color.colorText), 80 | textAlign = TextAlign.Center 81 | ) 82 | } 83 | 84 | @Composable 85 | fun MyTextFieldComponent( 86 | labelValue: String, painterResource: Painter, 87 | onTextChanged: (String) -> Unit, 88 | errorStatus: Boolean = false 89 | ) { 90 | 91 | val textValue = remember { 92 | mutableStateOf("") 93 | } 94 | val localFocusManager = LocalFocusManager.current 95 | 96 | OutlinedTextField( 97 | modifier = Modifier 98 | .fillMaxWidth() 99 | .clip(componentShapes.small), 100 | label = { Text(text = labelValue) }, 101 | colors = TextFieldDefaults.outlinedTextFieldColors( 102 | focusedBorderColor = Primary, 103 | focusedLabelColor = Primary, 104 | cursorColor = Primary, 105 | backgroundColor = BgColor 106 | ), 107 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), 108 | singleLine = true, 109 | maxLines = 1, 110 | value = textValue.value, 111 | onValueChange = { 112 | textValue.value = it 113 | onTextChanged(it) 114 | }, 115 | leadingIcon = { 116 | Icon(painter = painterResource, contentDescription = "") 117 | }, 118 | isError = !errorStatus 119 | ) 120 | } 121 | 122 | 123 | @Composable 124 | fun PasswordTextFieldComponent( 125 | labelValue: String, painterResource: Painter, 126 | onTextSelected: (String) -> Unit, 127 | errorStatus: Boolean = false 128 | ) { 129 | 130 | val localFocusManager = LocalFocusManager.current 131 | val password = remember { 132 | mutableStateOf("") 133 | } 134 | 135 | val passwordVisible = remember { 136 | mutableStateOf(false) 137 | } 138 | 139 | OutlinedTextField( 140 | modifier = Modifier 141 | .fillMaxWidth() 142 | .clip(componentShapes.small), 143 | label = { Text(text = labelValue) }, 144 | colors = TextFieldDefaults.outlinedTextFieldColors( 145 | focusedBorderColor = Primary, 146 | focusedLabelColor = Primary, 147 | cursorColor = Primary, 148 | backgroundColor = BgColor 149 | ), 150 | keyboardOptions = KeyboardOptions( 151 | keyboardType = KeyboardType.Password, 152 | imeAction = ImeAction.Done 153 | ), 154 | singleLine = true, 155 | keyboardActions = KeyboardActions { 156 | localFocusManager.clearFocus() 157 | }, 158 | maxLines = 1, 159 | value = password.value, 160 | onValueChange = { 161 | password.value = it 162 | onTextSelected(it) 163 | }, 164 | leadingIcon = { 165 | Icon(painter = painterResource, contentDescription = "") 166 | }, 167 | trailingIcon = { 168 | 169 | val iconImage = if (passwordVisible.value) { 170 | Icons.Filled.Visibility 171 | } else { 172 | Icons.Filled.VisibilityOff 173 | } 174 | 175 | val description = if (passwordVisible.value) { 176 | stringResource(id = R.string.hide_password) 177 | } else { 178 | stringResource(id = R.string.show_password) 179 | } 180 | 181 | IconButton(onClick = { passwordVisible.value = !passwordVisible.value }) { 182 | Icon(imageVector = iconImage, contentDescription = description) 183 | } 184 | 185 | }, 186 | visualTransformation = if (passwordVisible.value) VisualTransformation.None else PasswordVisualTransformation(), 187 | isError = !errorStatus 188 | ) 189 | } 190 | 191 | @Composable 192 | fun CheckboxComponent( 193 | value: String, 194 | onTextSelected: (String) -> Unit, 195 | onCheckedChange: (Boolean) -> Unit 196 | ) { 197 | Row( 198 | modifier = Modifier 199 | .fillMaxWidth() 200 | .heightIn(56.dp), 201 | verticalAlignment = Alignment.CenterVertically, 202 | ) { 203 | 204 | val checkedState = remember { 205 | mutableStateOf(false) 206 | } 207 | 208 | Checkbox(checked = checkedState.value, 209 | onCheckedChange = { 210 | checkedState.value = !checkedState.value 211 | onCheckedChange.invoke(it) 212 | }) 213 | 214 | ClickableTextComponent(value = value, onTextSelected) 215 | } 216 | } 217 | 218 | @Composable 219 | fun ClickableTextComponent(value: String, onTextSelected: (String) -> Unit) { 220 | val initialText = "By continuing you accept our " 221 | val privacyPolicyText = "Privacy Policy" 222 | val andText = " and " 223 | val termsAndConditionsText = "Term of Use" 224 | 225 | val annotatedString = buildAnnotatedString { 226 | append(initialText) 227 | withStyle(style = SpanStyle(color = Primary)) { 228 | pushStringAnnotation(tag = privacyPolicyText, annotation = privacyPolicyText) 229 | append(privacyPolicyText) 230 | } 231 | append(andText) 232 | withStyle(style = SpanStyle(color = Primary)) { 233 | pushStringAnnotation(tag = termsAndConditionsText, annotation = termsAndConditionsText) 234 | append(termsAndConditionsText) 235 | } 236 | } 237 | 238 | ClickableText(text = annotatedString, onClick = { offset -> 239 | 240 | annotatedString.getStringAnnotations(offset, offset) 241 | .firstOrNull()?.also { span -> 242 | Log.d("ClickableTextComponent", "{${span.item}}") 243 | 244 | if ((span.item == termsAndConditionsText) || (span.item == privacyPolicyText)) { 245 | onTextSelected(span.item) 246 | } 247 | } 248 | 249 | }) 250 | } 251 | 252 | @Composable 253 | fun ButtonComponent(value: String, onButtonClicked: () -> Unit, isEnabled: Boolean = false) { 254 | Button( 255 | modifier = Modifier 256 | .fillMaxWidth() 257 | .heightIn(48.dp), 258 | onClick = { 259 | onButtonClicked.invoke() 260 | }, 261 | contentPadding = PaddingValues(), 262 | colors = ButtonDefaults.buttonColors(Color.Transparent), 263 | shape = RoundedCornerShape(50.dp), 264 | enabled = isEnabled 265 | ) { 266 | Box( 267 | modifier = Modifier 268 | .fillMaxWidth() 269 | .heightIn(48.dp) 270 | .background( 271 | brush = Brush.horizontalGradient(listOf(Secondary, Primary)), 272 | shape = RoundedCornerShape(50.dp) 273 | ), 274 | contentAlignment = Alignment.Center 275 | ) { 276 | Text( 277 | text = value, 278 | fontSize = 18.sp, 279 | color = Color.White, 280 | fontWeight = FontWeight.Bold 281 | ) 282 | 283 | } 284 | 285 | } 286 | } 287 | 288 | @Composable 289 | fun DividerTextComponent() { 290 | Row( 291 | modifier = Modifier.fillMaxWidth(), 292 | verticalAlignment = Alignment.CenterVertically 293 | ) { 294 | 295 | Divider( 296 | modifier = Modifier 297 | .fillMaxWidth() 298 | .weight(1f), 299 | color = GrayColor, 300 | thickness = 1.dp 301 | ) 302 | 303 | Text( 304 | modifier = Modifier.padding(8.dp), 305 | text = stringResource(R.string.or), 306 | fontSize = 18.sp, 307 | color = TextColor 308 | ) 309 | Divider( 310 | modifier = Modifier 311 | .fillMaxWidth() 312 | .weight(1f), 313 | color = GrayColor, 314 | thickness = 1.dp 315 | ) 316 | } 317 | } 318 | 319 | 320 | @Composable 321 | fun ClickableLoginTextComponent(tryingToLogin: Boolean = true, onTextSelected: (String) -> Unit) { 322 | val initialText = 323 | if (tryingToLogin) "Already have an account? " else "Don’t have an account yet? " 324 | val loginText = if (tryingToLogin) "Login" else "Register" 325 | 326 | val annotatedString = buildAnnotatedString { 327 | append(initialText) 328 | withStyle(style = SpanStyle(color = Primary)) { 329 | pushStringAnnotation(tag = loginText, annotation = loginText) 330 | append(loginText) 331 | } 332 | } 333 | 334 | ClickableText( 335 | modifier = Modifier 336 | .fillMaxWidth() 337 | .heightIn(min = 40.dp), 338 | style = TextStyle( 339 | fontSize = 21.sp, 340 | fontWeight = FontWeight.Normal, 341 | fontStyle = FontStyle.Normal, 342 | textAlign = TextAlign.Center 343 | ), 344 | text = annotatedString, 345 | onClick = { offset -> 346 | 347 | annotatedString.getStringAnnotations(offset, offset) 348 | .firstOrNull()?.also { span -> 349 | Log.d("ClickableTextComponent", "{${span.item}}") 350 | 351 | if (span.item == loginText) { 352 | onTextSelected(span.item) 353 | } 354 | } 355 | 356 | }, 357 | ) 358 | } 359 | 360 | @Composable 361 | fun UnderLinedTextComponent(value: String) { 362 | Text( 363 | text = value, 364 | modifier = Modifier 365 | .fillMaxWidth() 366 | .heightIn(min = 40.dp), 367 | style = TextStyle( 368 | fontSize = 16.sp, 369 | fontWeight = FontWeight.Normal, 370 | fontStyle = FontStyle.Normal 371 | ), color = colorResource(id = R.color.colorGray), 372 | textAlign = TextAlign.Center, 373 | textDecoration = TextDecoration.Underline 374 | ) 375 | 376 | } 377 | 378 | @Composable 379 | fun AppToolbar( 380 | toolbarTitle: String, logoutButtonClicked: () -> Unit, 381 | navigationIconClicked: () -> Unit 382 | ) { 383 | 384 | TopAppBar( 385 | backgroundColor = Primary, 386 | title = { 387 | Text( 388 | text = toolbarTitle, color = WhiteColor 389 | ) 390 | }, 391 | navigationIcon = { 392 | IconButton(onClick = { 393 | navigationIconClicked.invoke() 394 | }) { 395 | Icon( 396 | imageVector = Icons.Filled.Menu, 397 | contentDescription = stringResource(R.string.menu), 398 | tint = WhiteColor 399 | ) 400 | } 401 | 402 | }, 403 | actions = { 404 | IconButton(onClick = { 405 | logoutButtonClicked.invoke() 406 | }) { 407 | Icon( 408 | imageVector = Icons.Filled.Logout, 409 | contentDescription = stringResource(id = R.string.logout), 410 | ) 411 | } 412 | } 413 | ) 414 | } 415 | 416 | @Composable 417 | fun NavigationDrawerHeader(value: String?) { 418 | Box( 419 | modifier = Modifier 420 | .background( 421 | Brush.horizontalGradient( 422 | listOf(Primary, Secondary) 423 | ) 424 | ) 425 | .fillMaxWidth() 426 | .height(180.dp) 427 | .padding(32.dp) 428 | ) { 429 | 430 | NavigationDrawerText( 431 | title = value?:stringResource(R.string.navigation_header), 28.sp , AccentColor 432 | ) 433 | 434 | } 435 | } 436 | 437 | @Composable 438 | fun NavigationDrawerBody(navigationDrawerItems: List, 439 | onNavigationItemClicked:(NavigationItem) -> Unit) { 440 | LazyColumn(modifier = Modifier.fillMaxWidth()) { 441 | 442 | items(navigationDrawerItems) { 443 | NavigationItemRow(item = it,onNavigationItemClicked) 444 | } 445 | 446 | } 447 | } 448 | 449 | @Composable 450 | fun NavigationItemRow(item: NavigationItem, 451 | onNavigationItemClicked:(NavigationItem) -> Unit) { 452 | 453 | 454 | Row( 455 | modifier = Modifier 456 | .fillMaxWidth() 457 | .clickable { 458 | onNavigationItemClicked.invoke(item) 459 | }.padding(all = 16.dp) 460 | ) { 461 | 462 | Icon( 463 | imageVector = item.icon, 464 | contentDescription = item.description, 465 | ) 466 | 467 | Spacer(modifier = Modifier.width(18.dp)) 468 | 469 | NavigationDrawerText(title = item.title, 18.sp, Primary) 470 | 471 | 472 | } 473 | } 474 | 475 | @Composable 476 | fun NavigationDrawerText(title: String, textUnit: TextUnit,color: Color) { 477 | 478 | val shadowOffset = Offset(4f, 6f) 479 | 480 | Text( 481 | text = title, style = TextStyle( 482 | color = Color.Black, 483 | fontSize = textUnit, 484 | fontStyle = FontStyle.Normal, 485 | shadow = Shadow( 486 | color = Primary, 487 | offset = shadowOffset, 2f 488 | ) 489 | ) 490 | ) 491 | } 492 | 493 | 494 | 495 | 496 | 497 | -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/data/NavigationItem.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.data 2 | 3 | import androidx.compose.ui.graphics.vector.ImageVector 4 | 5 | data class NavigationItem( 6 | val title: String, 7 | val description: String, 8 | val itemId: String, 9 | val icon: ImageVector 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/data/RegistrationUIState.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.data 2 | 3 | data class RegistrationUIState( 4 | var firstName :String = "", 5 | var lastName :String = "", 6 | var email :String = "", 7 | var password :String = "", 8 | var privacyPolicyAccepted :Boolean = false, 9 | 10 | 11 | var firstNameError :Boolean = false, 12 | var lastNameError : Boolean = false, 13 | var emailError :Boolean = false, 14 | var passwordError : Boolean = false, 15 | var privacyPolicyError:Boolean = false 16 | 17 | 18 | ) 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/data/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.data.home 2 | 3 | import android.util.Log 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.filled.Favorite 6 | import androidx.compose.material.icons.filled.Home 7 | import androidx.compose.material.icons.filled.Settings 8 | import androidx.lifecycle.MutableLiveData 9 | import androidx.lifecycle.ViewModel 10 | import com.google.firebase.auth.FirebaseAuth 11 | import com.nativemobilebits.loginflow.data.NavigationItem 12 | import com.nativemobilebits.loginflow.data.signup.SignupViewModel 13 | import com.nativemobilebits.loginflow.navigation.PostOfficeAppRouter 14 | import com.nativemobilebits.loginflow.navigation.Screen 15 | 16 | class HomeViewModel : ViewModel() { 17 | 18 | private val TAG = HomeViewModel::class.simpleName 19 | 20 | val navigationItemsList = listOf( 21 | NavigationItem( 22 | title = "Home", 23 | icon = Icons.Default.Home, 24 | description = "Home Screen", 25 | itemId = "homeScreen" 26 | ), 27 | NavigationItem( 28 | title = "Settings", 29 | icon = Icons.Default.Settings, 30 | description = "Settings Screen", 31 | itemId = "settingsScreen" 32 | ), 33 | NavigationItem( 34 | title = "Favorite", 35 | icon = Icons.Default.Favorite, 36 | description = "Favorite Screen", 37 | itemId = "favoriteScreen" 38 | ) 39 | ) 40 | 41 | val isUserLoggedIn: MutableLiveData = MutableLiveData() 42 | 43 | fun logout() { 44 | 45 | val firebaseAuth = FirebaseAuth.getInstance() 46 | 47 | firebaseAuth.signOut() 48 | 49 | val authStateListener = FirebaseAuth.AuthStateListener { 50 | if (it.currentUser == null) { 51 | Log.d(TAG, "Inside sign outsuccess") 52 | PostOfficeAppRouter.navigateTo(Screen.LoginScreen) 53 | } else { 54 | Log.d(TAG, "Inside sign out is not complete") 55 | } 56 | } 57 | 58 | firebaseAuth.addAuthStateListener(authStateListener) 59 | 60 | } 61 | 62 | fun checkForActiveSession() { 63 | if (FirebaseAuth.getInstance().currentUser != null) { 64 | Log.d(TAG, "Valid session") 65 | isUserLoggedIn.value = true 66 | } else { 67 | Log.d(TAG, "User is not logged in") 68 | isUserLoggedIn.value = false 69 | } 70 | } 71 | 72 | 73 | val emailId: MutableLiveData = MutableLiveData() 74 | 75 | fun getUserData() { 76 | FirebaseAuth.getInstance().currentUser?.also { 77 | it.email?.also { email -> 78 | emailId.value = email 79 | } 80 | } 81 | } 82 | 83 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/data/login/LoginUIEvent.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.data.login 2 | 3 | sealed class LoginUIEvent{ 4 | 5 | data class EmailChanged(val email:String): LoginUIEvent() 6 | data class PasswordChanged(val password: String) : LoginUIEvent() 7 | 8 | object LoginButtonClicked : LoginUIEvent() 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/data/login/LoginUIState.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.data.login 2 | 3 | data class LoginUIState( 4 | var email :String = "", 5 | var password :String = "", 6 | 7 | var emailError :Boolean = false, 8 | var passwordError : Boolean = false 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/data/login/LoginViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.data.login 2 | 3 | import android.util.Log 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.lifecycle.ViewModel 6 | import com.google.firebase.auth.FirebaseAuth 7 | import com.nativemobilebits.loginflow.data.rules.Validator 8 | import com.nativemobilebits.loginflow.navigation.PostOfficeAppRouter 9 | import com.nativemobilebits.loginflow.navigation.Screen 10 | 11 | class LoginViewModel : ViewModel() { 12 | 13 | private val TAG = LoginViewModel::class.simpleName 14 | 15 | var loginUIState = mutableStateOf(LoginUIState()) 16 | 17 | var allValidationsPassed = mutableStateOf(false) 18 | 19 | var loginInProgress = mutableStateOf(false) 20 | 21 | 22 | fun onEvent(event: LoginUIEvent) { 23 | when (event) { 24 | is LoginUIEvent.EmailChanged -> { 25 | loginUIState.value = loginUIState.value.copy( 26 | email = event.email 27 | ) 28 | } 29 | 30 | is LoginUIEvent.PasswordChanged -> { 31 | loginUIState.value = loginUIState.value.copy( 32 | password = event.password 33 | ) 34 | } 35 | 36 | is LoginUIEvent.LoginButtonClicked -> { 37 | login() 38 | } 39 | } 40 | validateLoginUIDataWithRules() 41 | } 42 | 43 | private fun validateLoginUIDataWithRules() { 44 | val emailResult = Validator.validateEmail( 45 | email = loginUIState.value.email 46 | ) 47 | 48 | 49 | val passwordResult = Validator.validatePassword( 50 | password = loginUIState.value.password 51 | ) 52 | 53 | loginUIState.value = loginUIState.value.copy( 54 | emailError = emailResult.status, 55 | passwordError = passwordResult.status 56 | ) 57 | 58 | allValidationsPassed.value = emailResult.status && passwordResult.status 59 | 60 | } 61 | 62 | private fun login() { 63 | 64 | loginInProgress.value = true 65 | val email = loginUIState.value.email 66 | val password = loginUIState.value.password 67 | 68 | FirebaseAuth 69 | .getInstance() 70 | .signInWithEmailAndPassword(email, password) 71 | .addOnCompleteListener { 72 | Log.d(TAG,"Inside_login_success") 73 | Log.d(TAG,"${it.isSuccessful}") 74 | 75 | if(it.isSuccessful){ 76 | loginInProgress.value = false 77 | PostOfficeAppRouter.navigateTo(Screen.HomeScreen) 78 | } 79 | } 80 | .addOnFailureListener { 81 | Log.d(TAG,"Inside_login_failure") 82 | Log.d(TAG,"${it.localizedMessage}") 83 | 84 | loginInProgress.value = false 85 | 86 | } 87 | 88 | } 89 | 90 | } 91 | 92 | 93 | -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/data/rules/Validator.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.data.rules 2 | 3 | import android.util.Log 4 | 5 | object Validator { 6 | 7 | 8 | fun validateFirstName(fName: String): ValidationResult { 9 | return ValidationResult( 10 | (!fName.isNullOrEmpty() && fName.length >= 2) 11 | ) 12 | 13 | } 14 | 15 | fun validateLastName(lName: String): ValidationResult { 16 | return ValidationResult( 17 | (!lName.isNullOrEmpty() && lName.length >= 2) 18 | ) 19 | } 20 | 21 | fun validateEmail(email: String): ValidationResult { 22 | return ValidationResult( 23 | (!email.isNullOrEmpty()) 24 | ) 25 | } 26 | 27 | fun validatePassword(password: String): ValidationResult { 28 | return ValidationResult( 29 | (!password.isNullOrEmpty() && password.length >= 4) 30 | ) 31 | } 32 | 33 | fun validatePrivacyPolicyAcceptance(statusValue:Boolean):ValidationResult{ 34 | return ValidationResult( 35 | statusValue 36 | ) 37 | } 38 | 39 | } 40 | 41 | data class ValidationResult( 42 | val status: Boolean = false 43 | ) 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/data/signup/SignupUIEvent.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.data.signup 2 | 3 | sealed class SignupUIEvent{ 4 | 5 | data class FirstNameChanged(val firstName:String) : SignupUIEvent() 6 | data class LastNameChanged(val lastName:String) : SignupUIEvent() 7 | data class EmailChanged(val email:String): SignupUIEvent() 8 | data class PasswordChanged(val password: String) : SignupUIEvent() 9 | 10 | data class PrivacyPolicyCheckBoxClicked(val status:Boolean) : SignupUIEvent() 11 | 12 | object RegisterButtonClicked : SignupUIEvent() 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/data/signup/SignupViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.data.signup 2 | 3 | import android.util.Log 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.lifecycle.ViewModel 6 | import com.google.firebase.auth.FirebaseAuth 7 | import com.google.firebase.auth.FirebaseAuth.AuthStateListener 8 | import com.nativemobilebits.loginflow.data.RegistrationUIState 9 | import com.nativemobilebits.loginflow.data.rules.Validator 10 | import com.nativemobilebits.loginflow.navigation.PostOfficeAppRouter 11 | import com.nativemobilebits.loginflow.navigation.Screen 12 | 13 | 14 | class SignupViewModel : ViewModel() { 15 | 16 | private val TAG = SignupViewModel::class.simpleName 17 | 18 | 19 | var registrationUIState = mutableStateOf(RegistrationUIState()) 20 | 21 | var allValidationsPassed = mutableStateOf(false) 22 | 23 | var signUpInProgress = mutableStateOf(false) 24 | 25 | fun onEvent(event: SignupUIEvent) { 26 | when (event) { 27 | is SignupUIEvent.FirstNameChanged -> { 28 | registrationUIState.value = registrationUIState.value.copy( 29 | firstName = event.firstName 30 | ) 31 | printState() 32 | } 33 | 34 | is SignupUIEvent.LastNameChanged -> { 35 | registrationUIState.value = registrationUIState.value.copy( 36 | lastName = event.lastName 37 | ) 38 | printState() 39 | } 40 | 41 | is SignupUIEvent.EmailChanged -> { 42 | registrationUIState.value = registrationUIState.value.copy( 43 | email = event.email 44 | ) 45 | printState() 46 | 47 | } 48 | 49 | 50 | is SignupUIEvent.PasswordChanged -> { 51 | registrationUIState.value = registrationUIState.value.copy( 52 | password = event.password 53 | ) 54 | printState() 55 | 56 | } 57 | 58 | is SignupUIEvent.RegisterButtonClicked -> { 59 | signUp() 60 | } 61 | 62 | is SignupUIEvent.PrivacyPolicyCheckBoxClicked -> { 63 | registrationUIState.value = registrationUIState.value.copy( 64 | privacyPolicyAccepted = event.status 65 | ) 66 | } 67 | } 68 | validateDataWithRules() 69 | } 70 | 71 | 72 | private fun signUp() { 73 | Log.d(TAG, "Inside_signUp") 74 | printState() 75 | createUserInFirebase( 76 | email = registrationUIState.value.email, 77 | password = registrationUIState.value.password 78 | ) 79 | } 80 | 81 | private fun validateDataWithRules() { 82 | val fNameResult = Validator.validateFirstName( 83 | fName = registrationUIState.value.firstName 84 | ) 85 | 86 | val lNameResult = Validator.validateLastName( 87 | lName = registrationUIState.value.lastName 88 | ) 89 | 90 | val emailResult = Validator.validateEmail( 91 | email = registrationUIState.value.email 92 | ) 93 | 94 | 95 | val passwordResult = Validator.validatePassword( 96 | password = registrationUIState.value.password 97 | ) 98 | 99 | val privacyPolicyResult = Validator.validatePrivacyPolicyAcceptance( 100 | statusValue = registrationUIState.value.privacyPolicyAccepted 101 | ) 102 | 103 | 104 | Log.d(TAG, "Inside_validateDataWithRules") 105 | Log.d(TAG, "fNameResult= $fNameResult") 106 | Log.d(TAG, "lNameResult= $lNameResult") 107 | Log.d(TAG, "emailResult= $emailResult") 108 | Log.d(TAG, "passwordResult= $passwordResult") 109 | Log.d(TAG, "privacyPolicyResult= $privacyPolicyResult") 110 | 111 | registrationUIState.value = registrationUIState.value.copy( 112 | firstNameError = fNameResult.status, 113 | lastNameError = lNameResult.status, 114 | emailError = emailResult.status, 115 | passwordError = passwordResult.status, 116 | privacyPolicyError = privacyPolicyResult.status 117 | ) 118 | 119 | 120 | allValidationsPassed.value = fNameResult.status && lNameResult.status && 121 | emailResult.status && passwordResult.status && privacyPolicyResult.status 122 | 123 | } 124 | 125 | 126 | private fun printState() { 127 | Log.d(TAG, "Inside_printState") 128 | Log.d(TAG, registrationUIState.value.toString()) 129 | } 130 | 131 | 132 | private fun createUserInFirebase(email: String, password: String) { 133 | 134 | signUpInProgress.value = true 135 | 136 | FirebaseAuth 137 | .getInstance() 138 | .createUserWithEmailAndPassword(email, password) 139 | .addOnCompleteListener { 140 | Log.d(TAG, "Inside_OnCompleteListener") 141 | Log.d(TAG, " isSuccessful = ${it.isSuccessful}") 142 | 143 | signUpInProgress.value = false 144 | if (it.isSuccessful) { 145 | PostOfficeAppRouter.navigateTo(Screen.HomeScreen) 146 | } 147 | } 148 | .addOnFailureListener { 149 | Log.d(TAG, "Inside_OnFailureListener") 150 | Log.d(TAG, "Exception= ${it.message}") 151 | Log.d(TAG, "Exception= ${it.localizedMessage}") 152 | } 153 | } 154 | 155 | 156 | } 157 | -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/navigation/PostOfficeAppRouter.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.navigation 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.mutableStateOf 5 | 6 | sealed class Screen { 7 | 8 | object SignUpScreen : Screen() 9 | object TermsAndConditionsScreen : Screen() 10 | object LoginScreen : Screen() 11 | object HomeScreen : Screen() 12 | } 13 | 14 | 15 | object PostOfficeAppRouter { 16 | 17 | var currentScreen: MutableState = mutableStateOf(Screen.SignUpScreen) 18 | 19 | fun navigateTo(destination : Screen){ 20 | currentScreen.value = destination 21 | } 22 | 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/navigation/SystemBackButtonHandler.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.navigation 2 | 3 | import androidx.activity.ComponentActivity 4 | import androidx.activity.OnBackPressedCallback 5 | import androidx.activity.OnBackPressedDispatcherOwner 6 | import androidx.compose.runtime.* 7 | import androidx.compose.ui.platform.LocalLifecycleOwner 8 | 9 | private val LocalBackPressedDispatcher = 10 | staticCompositionLocalOf { null } 11 | 12 | 13 | private class ComposableBackNavigationHandler(enabled: Boolean) : OnBackPressedCallback(enabled) { 14 | lateinit var onBackPressed: () -> Unit 15 | 16 | override fun handleOnBackPressed() { 17 | onBackPressed() 18 | } 19 | 20 | } 21 | 22 | 23 | @Composable 24 | internal fun ComposableHandler( 25 | enabled: Boolean = true, 26 | onBackPressed: () -> Unit 27 | ) { 28 | val dispatcher = (LocalBackPressedDispatcher.current ?: return).onBackPressedDispatcher 29 | 30 | val handler = remember { ComposableBackNavigationHandler(enabled) } 31 | 32 | DisposableEffect(dispatcher) { 33 | dispatcher.addCallback(handler) 34 | 35 | 36 | onDispose { handler.remove() } 37 | } 38 | 39 | LaunchedEffect(enabled) { 40 | handler.isEnabled = enabled 41 | handler.onBackPressed = onBackPressed 42 | } 43 | } 44 | 45 | @Composable 46 | internal fun SystemBackButtonHandler(onBackPressed: () -> Unit) { 47 | CompositionLocalProvider( 48 | LocalBackPressedDispatcher provides LocalLifecycleOwner.current as ComponentActivity 49 | ) { 50 | ComposableHandler { 51 | onBackPressed() 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/screens/HomeScreen.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.screens 2 | 3 | import android.util.Log 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material.Scaffold 9 | import androidx.compose.material.Surface 10 | import androidx.compose.material.rememberScaffoldState 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.rememberCoroutineScope 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.tooling.preview.Preview 17 | import androidx.lifecycle.viewmodel.compose.viewModel 18 | import com.nativemobilebits.loginflow.R 19 | import com.nativemobilebits.loginflow.components.AppToolbar 20 | import com.nativemobilebits.loginflow.components.NavigationDrawerBody 21 | import com.nativemobilebits.loginflow.components.NavigationDrawerHeader 22 | import com.nativemobilebits.loginflow.data.home.HomeViewModel 23 | import com.nativemobilebits.loginflow.data.signup.SignupViewModel 24 | import kotlinx.coroutines.launch 25 | 26 | @Composable 27 | fun HomeScreen(homeViewModel: HomeViewModel = viewModel()) { 28 | 29 | val scaffoldState = rememberScaffoldState() 30 | val coroutineScope = rememberCoroutineScope() 31 | 32 | homeViewModel.getUserData() 33 | 34 | Scaffold( 35 | scaffoldState = scaffoldState, 36 | topBar = { 37 | AppToolbar(toolbarTitle = stringResource(id = R.string.home), 38 | logoutButtonClicked = { 39 | homeViewModel.logout() 40 | }, 41 | navigationIconClicked = { 42 | coroutineScope.launch { 43 | scaffoldState.drawerState.open() 44 | } 45 | } 46 | ) 47 | }, 48 | drawerGesturesEnabled = scaffoldState.drawerState.isOpen, 49 | drawerContent = { 50 | NavigationDrawerHeader(homeViewModel.emailId.value) 51 | NavigationDrawerBody(navigationDrawerItems = homeViewModel.navigationItemsList, 52 | onNavigationItemClicked = { 53 | Log.d("ComingHere","inside_NavigationItemClicked") 54 | Log.d("ComingHere","${it.itemId} ${it.title}") 55 | }) 56 | } 57 | 58 | ) { paddingValues -> 59 | 60 | Surface( 61 | modifier = Modifier 62 | .fillMaxSize() 63 | .background(Color.White) 64 | .padding(paddingValues) 65 | ) { 66 | Column(modifier = Modifier.fillMaxSize()) { 67 | 68 | 69 | } 70 | 71 | } 72 | } 73 | } 74 | 75 | @Preview 76 | @Composable 77 | fun HomeScreenPreview() { 78 | HomeScreen() 79 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/screens/LoginScreen.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.screens 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.CircularProgressIndicator 6 | import androidx.compose.material.Surface 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.res.painterResource 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.tooling.preview.Preview 14 | import androidx.compose.ui.unit.dp 15 | import androidx.lifecycle.viewmodel.compose.viewModel 16 | import com.nativemobilebits.loginflow.data.login.LoginViewModel 17 | import com.nativemobilebits.loginflow.R 18 | import com.nativemobilebits.loginflow.components.* 19 | import com.nativemobilebits.loginflow.data.login.LoginUIEvent 20 | import com.nativemobilebits.loginflow.navigation.PostOfficeAppRouter 21 | import com.nativemobilebits.loginflow.navigation.Screen 22 | import com.nativemobilebits.loginflow.navigation.SystemBackButtonHandler 23 | 24 | @Composable 25 | fun LoginScreen(loginViewModel: LoginViewModel = viewModel()) { 26 | 27 | Box( 28 | modifier = Modifier.fillMaxSize(), 29 | contentAlignment = Alignment.Center 30 | ) { 31 | 32 | Surface( 33 | modifier = Modifier 34 | .fillMaxSize() 35 | .background(Color.White) 36 | .padding(28.dp) 37 | ) { 38 | 39 | Column( 40 | modifier = Modifier 41 | .fillMaxSize() 42 | ) { 43 | 44 | NormalTextComponent(value = stringResource(id = R.string.login)) 45 | HeadingTextComponent(value = stringResource(id = R.string.welcome)) 46 | Spacer(modifier = Modifier.height(20.dp)) 47 | 48 | MyTextFieldComponent(labelValue = stringResource(id = R.string.email), 49 | painterResource(id = R.drawable.message), 50 | onTextChanged = { 51 | loginViewModel.onEvent(LoginUIEvent.EmailChanged(it)) 52 | }, 53 | errorStatus = loginViewModel.loginUIState.value.emailError 54 | ) 55 | 56 | PasswordTextFieldComponent( 57 | labelValue = stringResource(id = R.string.password), 58 | painterResource(id = R.drawable.lock), 59 | onTextSelected = { 60 | loginViewModel.onEvent(LoginUIEvent.PasswordChanged(it)) 61 | }, 62 | errorStatus = loginViewModel.loginUIState.value.passwordError 63 | ) 64 | 65 | Spacer(modifier = Modifier.height(40.dp)) 66 | UnderLinedTextComponent(value = stringResource(id = R.string.forgot_password)) 67 | 68 | Spacer(modifier = Modifier.height(40.dp)) 69 | 70 | ButtonComponent( 71 | value = stringResource(id = R.string.login), 72 | onButtonClicked = { 73 | loginViewModel.onEvent(LoginUIEvent.LoginButtonClicked) 74 | }, 75 | isEnabled = loginViewModel.allValidationsPassed.value 76 | ) 77 | 78 | Spacer(modifier = Modifier.height(20.dp)) 79 | 80 | DividerTextComponent() 81 | 82 | ClickableLoginTextComponent(tryingToLogin = false, onTextSelected = { 83 | PostOfficeAppRouter.navigateTo(Screen.SignUpScreen) 84 | }) 85 | } 86 | } 87 | 88 | if(loginViewModel.loginInProgress.value) { 89 | CircularProgressIndicator() 90 | } 91 | } 92 | 93 | 94 | SystemBackButtonHandler { 95 | PostOfficeAppRouter.navigateTo(Screen.SignUpScreen) 96 | } 97 | } 98 | 99 | @Preview 100 | @Composable 101 | fun LoginScreenPreview() { 102 | LoginScreen() 103 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/screens/SignUpScreen.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.screens 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.CircularProgressIndicator 6 | import androidx.compose.material.Surface 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.res.painterResource 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.tooling.preview.Preview 14 | import androidx.compose.ui.unit.dp 15 | import androidx.lifecycle.viewmodel.compose.viewModel 16 | import com.nativemobilebits.loginflow.R 17 | import com.nativemobilebits.loginflow.components.* 18 | import com.nativemobilebits.loginflow.data.signup.SignupViewModel 19 | import com.nativemobilebits.loginflow.data.signup.SignupUIEvent 20 | import com.nativemobilebits.loginflow.navigation.PostOfficeAppRouter 21 | import com.nativemobilebits.loginflow.navigation.Screen 22 | 23 | @Composable 24 | fun SignUpScreen(signupViewModel: SignupViewModel = viewModel()) { 25 | 26 | Box( 27 | modifier = Modifier.fillMaxSize(), 28 | contentAlignment = Alignment.Center 29 | ) { 30 | 31 | Surface( 32 | modifier = Modifier 33 | .fillMaxSize() 34 | .background(Color.White) 35 | .padding(28.dp) 36 | ) { 37 | Column(modifier = Modifier.fillMaxSize()) { 38 | 39 | NormalTextComponent(value = stringResource(id = R.string.hello)) 40 | HeadingTextComponent(value = stringResource(id = R.string.create_account)) 41 | Spacer(modifier = Modifier.height(20.dp)) 42 | 43 | MyTextFieldComponent( 44 | labelValue = stringResource(id = R.string.first_name), 45 | painterResource(id = R.drawable.profile), 46 | onTextChanged = { 47 | signupViewModel.onEvent(SignupUIEvent.FirstNameChanged(it)) 48 | }, 49 | errorStatus = signupViewModel.registrationUIState.value.firstNameError 50 | ) 51 | 52 | MyTextFieldComponent( 53 | labelValue = stringResource(id = R.string.last_name), 54 | painterResource = painterResource(id = R.drawable.profile), 55 | onTextChanged = { 56 | signupViewModel.onEvent(SignupUIEvent.LastNameChanged(it)) 57 | }, 58 | errorStatus = signupViewModel.registrationUIState.value.lastNameError 59 | ) 60 | 61 | MyTextFieldComponent( 62 | labelValue = stringResource(id = R.string.email), 63 | painterResource = painterResource(id = R.drawable.message), 64 | onTextChanged = { 65 | signupViewModel.onEvent(SignupUIEvent.EmailChanged(it)) 66 | }, 67 | errorStatus = signupViewModel.registrationUIState.value.emailError 68 | ) 69 | 70 | PasswordTextFieldComponent( 71 | labelValue = stringResource(id = R.string.password), 72 | painterResource = painterResource(id = R.drawable.ic_lock), 73 | onTextSelected = { 74 | signupViewModel.onEvent(SignupUIEvent.PasswordChanged(it)) 75 | }, 76 | errorStatus = signupViewModel.registrationUIState.value.passwordError 77 | ) 78 | 79 | CheckboxComponent(value = stringResource(id = R.string.terms_and_conditions), 80 | onTextSelected = { 81 | PostOfficeAppRouter.navigateTo(Screen.TermsAndConditionsScreen) 82 | }, 83 | onCheckedChange = { 84 | signupViewModel.onEvent(SignupUIEvent.PrivacyPolicyCheckBoxClicked(it)) 85 | } 86 | ) 87 | 88 | Spacer(modifier = Modifier.height(40.dp)) 89 | 90 | ButtonComponent( 91 | value = stringResource(id = R.string.register), 92 | onButtonClicked = { 93 | signupViewModel.onEvent(SignupUIEvent.RegisterButtonClicked) 94 | }, 95 | isEnabled = signupViewModel.allValidationsPassed.value 96 | ) 97 | 98 | Spacer(modifier = Modifier.height(20.dp)) 99 | 100 | DividerTextComponent() 101 | 102 | ClickableLoginTextComponent(tryingToLogin = true, onTextSelected = { 103 | PostOfficeAppRouter.navigateTo(Screen.LoginScreen) 104 | }) 105 | } 106 | 107 | } 108 | 109 | if(signupViewModel.signUpInProgress.value) { 110 | CircularProgressIndicator() 111 | } 112 | } 113 | 114 | 115 | } 116 | 117 | @Preview 118 | @Composable 119 | fun DefaultPreviewOfSignUpScreen() { 120 | SignUpScreen() 121 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/screens/TermsAndConditionsScreen.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.screens 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material.Surface 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.res.stringResource 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import androidx.compose.ui.unit.dp 13 | import com.nativemobilebits.loginflow.R 14 | import com.nativemobilebits.loginflow.components.HeadingTextComponent 15 | import com.nativemobilebits.loginflow.navigation.PostOfficeAppRouter 16 | import com.nativemobilebits.loginflow.navigation.Screen 17 | import com.nativemobilebits.loginflow.navigation.SystemBackButtonHandler 18 | 19 | @Composable 20 | fun TermsAndConditionsScreen() { 21 | Surface(modifier = Modifier 22 | .fillMaxSize() 23 | .background(color = Color.White) 24 | .padding(16.dp)) { 25 | 26 | HeadingTextComponent(value = stringResource(id = R.string.terms_and_conditions_header)) 27 | } 28 | 29 | SystemBackButtonHandler { 30 | PostOfficeAppRouter.navigateTo(Screen.SignUpScreen) 31 | } 32 | } 33 | 34 | @Preview 35 | @Composable 36 | fun TermsAndConditionsScreenPreview(){ 37 | TermsAndConditionsScreen() 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Primary = Color(0xFF92A3FD) 6 | val Secondary = Color(0xFF9DCEFF) 7 | val TextColor = Color(0xFF1D1617) 8 | val AccentColor = Color(0xFFC58BF2) 9 | val GrayColor = Color(0xFF7B6F72) 10 | val WhiteColor = Color(0xFFFFFFFF) 11 | val BgColor = Color(0xFFF7F8F8) 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/ui/theme/ComponentsShape.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val componentShapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(8.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | 9 | private val DarkColorPalette = darkColors( 10 | primary = Primary, 11 | primaryVariant = Primary, 12 | secondary = Secondary 13 | ) 14 | 15 | private val LightColorPalette = lightColors( 16 | primary = Primary, 17 | primaryVariant = Primary, 18 | secondary = Secondary 19 | 20 | /* Other default colors to override 21 | background = Color.White, 22 | surface = Color.White, 23 | onPrimary = Color.White, 24 | onSecondary = Color.Black, 25 | onBackground = Color.Black, 26 | onSurface = Color.Black, 27 | */ 28 | ) 29 | 30 | @Composable 31 | fun LoginFlowTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { 32 | val colors = if (darkTheme) { 33 | DarkColorPalette 34 | } else { 35 | LightColorPalette 36 | } 37 | 38 | MaterialTheme( 39 | colors = colors, 40 | typography = Typography, 41 | shapes = componentShapes, 42 | content = content 43 | ) 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nativemobilebits/loginflow/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow.ui.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ) 16 | /* Other default text styles to override 17 | button = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.W500, 20 | fontSize = 14.sp 21 | ), 22 | caption = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Normal, 25 | fontSize = 12.sp 26 | ) 27 | */ 28 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_lock.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 21 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droid-lover/AppsUsingJetpackCompose/0f93ff553fbfce9f73b8f88c5b795b9b231bac32/app/src/main/res/drawable/layout.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/lock.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 21 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/message.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/profile.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 14 | 15 | 16 | 19 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droid-lover/AppsUsingJetpackCompose/0f93ff553fbfce9f73b8f88c5b795b9b231bac32/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droid-lover/AppsUsingJetpackCompose/0f93ff553fbfce9f73b8f88c5b795b9b231bac32/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droid-lover/AppsUsingJetpackCompose/0f93ff553fbfce9f73b8f88c5b795b9b231bac32/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droid-lover/AppsUsingJetpackCompose/0f93ff553fbfce9f73b8f88c5b795b9b231bac32/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droid-lover/AppsUsingJetpackCompose/0f93ff553fbfce9f73b8f88c5b795b9b231bac32/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droid-lover/AppsUsingJetpackCompose/0f93ff553fbfce9f73b8f88c5b795b9b231bac32/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droid-lover/AppsUsingJetpackCompose/0f93ff553fbfce9f73b8f88c5b795b9b231bac32/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droid-lover/AppsUsingJetpackCompose/0f93ff553fbfce9f73b8f88c5b795b9b231bac32/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droid-lover/AppsUsingJetpackCompose/0f93ff553fbfce9f73b8f88c5b795b9b231bac32/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droid-lover/AppsUsingJetpackCompose/0f93ff553fbfce9f73b8f88c5b795b9b231bac32/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #92A3FD 4 | #9DCEFF 5 | #1D1617 6 | #C58BF2 7 | #FFFFFFFF 8 | #7B6F72 9 | #F7F8F8 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | LoginFlowApp 3 | Hey there, 4 | Create an Account 5 | Welcome Back 6 | First Name 7 | Last Name 8 | Email 9 | Password 10 | Hide password 11 | Show password 12 | By continuing you accept our Privacy Policy and Term of Use 13 | Term of Use 14 | Register 15 | Login 16 | or 17 | Already have an account? Login 18 | 19 | Forgot your password 20 | Home 21 | Logout 22 | Menu 23 | App Drawer 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/test/java/com/nativemobilebits/loginflow/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.nativemobilebits.loginflow 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") version "7.4.0" apply false 3 | id("com.android.library") version "7.4.0" apply false 4 | id("org.jetbrains.kotlin.android") version "1.8.20" apply false 5 | id("com.google.gms.google-services")version "4.3.15" apply false 6 | } 7 | 8 | tasks { 9 | register("clean", Delete::class) { 10 | delete(rootProject.buildDir) 11 | } 12 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droid-lover/AppsUsingJetpackCompose/0f93ff553fbfce9f73b8f88c5b795b9b231bac32/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat May 13 10:26:05 IST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /images/first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droid-lover/AppsUsingJetpackCompose/0f93ff553fbfce9f73b8f88c5b795b9b231bac32/images/first.png -------------------------------------------------------------------------------- /images/second.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droid-lover/AppsUsingJetpackCompose/0f93ff553fbfce9f73b8f88c5b795b9b231bac32/images/second.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "LoginFlow" 16 | include ':app' 17 | --------------------------------------------------------------------------------