├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── release │ ├── app-release.apk │ └── output-metadata.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── noteswithrestapi │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── noteswithrestapi │ │ │ ├── authentication_feature │ │ │ ├── data │ │ │ │ ├── remote │ │ │ │ │ ├── AccountsApiService.kt │ │ │ │ │ └── dto │ │ │ │ │ │ ├── EmailVerificationResendResultDataDto.kt │ │ │ │ │ │ ├── EmailVerificationResendResultDto.kt │ │ │ │ │ │ ├── EmailVerificationResultDto.kt │ │ │ │ │ │ ├── IsAuthenticatedResultDataDto.kt │ │ │ │ │ │ ├── IsAuthenticatedResultDto.kt │ │ │ │ │ │ ├── LoginResultDataDto.kt │ │ │ │ │ │ ├── LoginResultDto.kt │ │ │ │ │ │ ├── RegisterResultDataDto.kt │ │ │ │ │ │ ├── RegisterResultDto.kt │ │ │ │ │ │ ├── ResetPasswordConfirmResultDto.kt │ │ │ │ │ │ └── ResetPasswordResultDto.kt │ │ │ │ └── repository │ │ │ │ │ └── AuthenticationRepositoryImpl.kt │ │ │ ├── di │ │ │ │ └── AuthenticationModule.kt │ │ │ ├── domain │ │ │ │ ├── error │ │ │ │ │ └── AuthenticationAppError.kt │ │ │ │ ├── model │ │ │ │ │ └── credentials │ │ │ │ │ │ ├── LoginCredentials.kt │ │ │ │ │ │ ├── PasswordResetConfirmCredentials.kt │ │ │ │ │ │ ├── RegisterCredentials.kt │ │ │ │ │ │ ├── ResendEmailVerificationCredentials.kt │ │ │ │ │ │ ├── SendPasswordResetCodeCredentials.kt │ │ │ │ │ │ └── VerifyEmailCredentials.kt │ │ │ │ ├── repository │ │ │ │ │ └── AuthenticationRepository.kt │ │ │ │ └── usecase │ │ │ │ │ ├── ConfirmPasswordResetUseCase.kt │ │ │ │ │ ├── IsLoggedInUseCase.kt │ │ │ │ │ ├── LoginUseCase.kt │ │ │ │ │ ├── RegisterUseCase.kt │ │ │ │ │ ├── ResendEmailVerificationCodeUseCase.kt │ │ │ │ │ ├── SendPasswordResetCodeUseCase.kt │ │ │ │ │ └── VerifyEmailUseCase.kt │ │ │ └── presentation │ │ │ │ ├── AuthenticationGraph.kt │ │ │ │ ├── components │ │ │ │ └── OTPTextField.kt │ │ │ │ ├── confirm_password_reset │ │ │ │ ├── ConfirmPasswordResetEvent.kt │ │ │ │ ├── ConfirmPasswordResetScreen.kt │ │ │ │ ├── ConfirmPasswordResetState.kt │ │ │ │ ├── ConfirmPasswordResetUiEvent.kt │ │ │ │ └── ConfirmPasswordResetViewModel.kt │ │ │ │ ├── login │ │ │ │ ├── LoginEvent.kt │ │ │ │ ├── LoginScreen.kt │ │ │ │ ├── LoginState.kt │ │ │ │ ├── LoginUiEvent.kt │ │ │ │ └── LoginViewModel.kt │ │ │ │ ├── register │ │ │ │ ├── RegisterEvent.kt │ │ │ │ ├── RegisterScreen.kt │ │ │ │ ├── RegisterState.kt │ │ │ │ ├── RegisterUiEvent.kt │ │ │ │ └── RegisterViewModel.kt │ │ │ │ ├── reset_password │ │ │ │ ├── ResetPasswordEvent.kt │ │ │ │ ├── ResetPasswordScreen.kt │ │ │ │ ├── ResetPasswordState.kt │ │ │ │ ├── ResetPasswordUiEvent.kt │ │ │ │ └── ResetPasswordViewModel.kt │ │ │ │ └── verify_email │ │ │ │ ├── VerifyEmailEvent.kt │ │ │ │ ├── VerifyEmailScreen.kt │ │ │ │ ├── VerifyEmailState.kt │ │ │ │ ├── VerifyEmailUiEvent.kt │ │ │ │ └── VerifyEmailViewModel.kt │ │ │ ├── core │ │ │ ├── NoteApplication.kt │ │ │ ├── data │ │ │ │ ├── network │ │ │ │ │ ├── HttpCustomErrorCode.kt │ │ │ │ │ ├── NetworkConstants.kt │ │ │ │ │ └── NetworkUtil.kt │ │ │ │ └── token │ │ │ │ │ ├── TokenManager.kt │ │ │ │ │ └── TokenManagerEncryptedSharedPreferences.kt │ │ │ ├── di │ │ │ │ └── AppModule.kt │ │ │ ├── domain │ │ │ │ ├── model │ │ │ │ │ ├── AppError.kt │ │ │ │ │ └── AppResponse.kt │ │ │ │ └── paginator │ │ │ │ │ ├── DefaultPaginator.kt │ │ │ │ │ └── Paginator.kt │ │ │ ├── presentation │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MainScreen.kt │ │ │ │ ├── components │ │ │ │ │ ├── AppDrawerContent.kt │ │ │ │ │ ├── ButtonComponent.kt │ │ │ │ │ ├── InputFieldComponent.kt │ │ │ │ │ ├── LoadingFailedComponent.kt │ │ │ │ │ ├── OutlinedButtonComponent.kt │ │ │ │ │ ├── ProgressIndicatorComponent.kt │ │ │ │ │ └── pull_refresh │ │ │ │ │ │ ├── PullRefresh.kt │ │ │ │ │ │ ├── PullRefreshIndicator.kt │ │ │ │ │ │ ├── PullRefreshIndicatorTransform.kt │ │ │ │ │ │ └── RememberPullRefreshState.kt │ │ │ │ ├── navigation │ │ │ │ │ ├── Graph.kt │ │ │ │ │ ├── NavigationItem.kt │ │ │ │ │ ├── RootNavGraph.kt │ │ │ │ │ └── Screen.kt │ │ │ │ └── theme │ │ │ │ │ └── ui │ │ │ │ │ └── theme │ │ │ │ │ ├── Color.kt │ │ │ │ │ ├── Shapes.kt │ │ │ │ │ ├── Theme.kt │ │ │ │ │ └── Type.kt │ │ │ └── utils │ │ │ │ ├── Constants.kt │ │ │ │ └── TimeUtil.kt │ │ │ ├── note_feature │ │ │ ├── data │ │ │ │ ├── mapper │ │ │ │ │ └── NoteMapper.kt │ │ │ │ ├── remote │ │ │ │ │ ├── NoteApiService.kt │ │ │ │ │ └── dto │ │ │ │ │ │ ├── GetNotesResultDto.kt │ │ │ │ │ │ └── NoteDto.kt │ │ │ │ └── repository │ │ │ │ │ └── NoteRepositoryImpl.kt │ │ │ ├── di │ │ │ │ └── NoteModule.kt │ │ │ ├── domain │ │ │ │ ├── error │ │ │ │ │ └── NoteError.kt │ │ │ │ ├── model │ │ │ │ │ └── Note.kt │ │ │ │ ├── repository │ │ │ │ │ └── NoteRepository.kt │ │ │ │ └── usecase │ │ │ │ │ ├── AddNoteUseCase.kt │ │ │ │ │ ├── DeleteNoteUseCase.kt │ │ │ │ │ ├── EditNoteUseCase.kt │ │ │ │ │ ├── GetNoteUseCase.kt │ │ │ │ │ └── GetNotesUseCase.kt │ │ │ └── presentation │ │ │ │ ├── NoteGraph.kt │ │ │ │ ├── add_note │ │ │ │ ├── AddNoteEvent.kt │ │ │ │ ├── AddNoteScreen.kt │ │ │ │ ├── AddNoteState.kt │ │ │ │ ├── AddNoteUiEvent.kt │ │ │ │ └── AddNoteViewModel.kt │ │ │ │ ├── components │ │ │ │ └── NoteComponent.kt │ │ │ │ ├── edit_note │ │ │ │ ├── EditNoteEvent.kt │ │ │ │ ├── EditNoteScreen.kt │ │ │ │ ├── EditNoteState.kt │ │ │ │ ├── EditNoteUiEvent.kt │ │ │ │ └── EditNoteViewModel.kt │ │ │ │ ├── notes │ │ │ │ ├── NotesEvent.kt │ │ │ │ ├── NotesScreen.kt │ │ │ │ ├── NotesState.kt │ │ │ │ └── NotesViewModel.kt │ │ │ │ └── search_note │ │ │ │ ├── SearchNoteEvent.kt │ │ │ │ ├── SearchNoteScreen.kt │ │ │ │ ├── SearchNoteState.kt │ │ │ │ └── SearchNoteViewModel.kt │ │ │ └── profile_feature │ │ │ ├── data │ │ │ ├── mapper │ │ │ │ └── UserMapper.kt │ │ │ ├── remote │ │ │ │ ├── ProfileApiService.kt │ │ │ │ └── dto │ │ │ │ │ ├── GetUserResultDto.kt │ │ │ │ │ ├── LogoutResultDto.kt │ │ │ │ │ └── UserData.kt │ │ │ └── repository │ │ │ │ └── ProfileRepositoryImpl.kt │ │ │ ├── di │ │ │ └── ProfileModule.kt │ │ │ ├── domain │ │ │ ├── model │ │ │ │ └── user │ │ │ │ │ └── User.kt │ │ │ ├── repository │ │ │ │ └── ProfileRepository.kt │ │ │ └── usecase │ │ │ │ ├── GetUserUseCase.kt │ │ │ │ └── LogoutUseCase.kt │ │ │ └── presentation │ │ │ ├── ProfileGraph.kt │ │ │ └── profile │ │ │ ├── ProfileEvent.kt │ │ │ ├── ProfileScreen.kt │ │ │ ├── ProfileState.kt │ │ │ ├── ProfileUiEvent.kt │ │ │ └── ProfileViewModel.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── amico_confirm.png │ │ ├── amico_notes.png │ │ ├── amico_reset.png │ │ ├── amico_signin.png │ │ ├── amico_signup.png │ │ └── ic_launcher_background.xml │ │ ├── font │ │ ├── roboto_bold.ttf │ │ ├── roboto_light.ttf │ │ ├── roboto_medium.ttf │ │ ├── roboto_regular.ttf │ │ └── ud_digi_kyokasho_nk_b.ttf │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.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 │ └── example │ └── noteswithrestapi │ └── ExampleUnitTest.kt ├── art ├── dark │ ├── add_note_dark.png │ ├── confirm_email.png │ ├── edit_note_dark.png │ ├── login_dark.png │ ├── nav_drawer_dark.png │ ├── notes_dark.png │ ├── profile_dark.png │ ├── register_dark.png │ ├── reset_password_dark.png │ ├── reset_pswrd_confirm_dark.png │ └── search_notes_dark.png ├── light │ ├── add_note_light.png │ ├── confirm_email_light.png │ ├── edit_note_light.png │ ├── login_light.png │ ├── nav_drawer_light.png │ ├── notes_light.png │ ├── proifle_light.png │ ├── register_light.png │ ├── reset_password.png │ ├── reset_password_confirm_light.png │ └── search_notes_light.png └── thumbnail.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── 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 | Notes With REST API -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.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 | # Notes with REST API 2 | - Creade, Read, Update, Delete notes using REST API 3 | - [REST API](https://github.com/ItamiOWM/drf-notes) 4 | 5 | ![Notes Thumbnail](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/thumbnail.png) 6 | 7 | ## Tech Stack 8 | 9 | - [Android Architecture Components](https://developer.android.com/topic/architecture) 10 | 11 | - Android Support Libraries 12 | 13 | - [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) 14 | 15 | - [Jetpack Compose](https://developer.android.com/jetpack/compose/documentation) 16 | 17 | - [Jetpack Navigation Compose Animation](https://google.github.io/accompanist/navigation-animation/) 18 | 19 | - [Coroutines](https://developer.android.com/kotlin/coroutines) 20 | 21 | - [Dagger-Hilt](https://developer.android.com/training/dependency-injection/hilt-android) 22 | 23 | - [Retorift](https://square.github.io/retrofit/) 24 | 25 | - [Encrypted Shared Preferences](https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences) 26 | 27 | - [System ui conroller](https://google.github.io/accompanist/systemuicontroller/) 28 | 29 | - [Compose Material 3](https://developer.android.com/jetpack/androidx/releases/compose-material3) 30 | 31 | - [Notes REST API](https://github.com/ItamiOWM/drf-notes) 32 | 33 | 34 | ## Screenshots (Dark Theme): 35 | 36 | ![LoginDark](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/dark/login_dark.png) 37 | ![RegisterDark](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/dark/register_dark.png) 38 | ![ResetPasswordDark](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/dark/reset_password_dark.png) 39 | ![ResetPasswordConfirmDark](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/dark/reset_pswrd_confirm_dark.png) 40 | ![ConfirmEmailDark](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/dark/confirm_email.png) 41 | ![NotesDark](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/dark/notes_dark.png) 42 | ![AddNoteDark](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/dark/add_note_dark.png) 43 | ![EditNoteDark](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/dark/edit_note_dark.png) 44 | ![SearchNotesDark](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/dark/search_notes_dark.png) 45 | ![ProfileDark](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/dark/profile_dark.png) 46 | ![NavDrawerDark](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/dark/nav_drawer_dark.png) 47 | 48 | 49 | 50 | ## Screenshots (Light Theme): 51 | 52 | ![LoginLight](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/light/login_light.png) 53 | ![RegisterLight](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/light/register_light.png) 54 | ![ResetPasswordLight](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/light/reset_password.png) 55 | ![ResetPasswordConfirmLight](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/light/reset_password_confirm_light.png) 56 | ![ConfirmEmailLight](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/light/confirm_email_light.png) 57 | ![NotesLight](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/light/notes_light.png) 58 | ![AddNoteLight](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/light/add_note_light.png) 59 | ![EditNoteLight](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/light/edit_note_light.png) 60 | ![SearchNotesLight](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/light/search_notes_light.png) 61 | ![ProfileLight](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/light/proifle_light.png) 62 | ![NavDrawerLight](https://github.com/ItamiOWM/NotesWithRESTAPI/blob/master/art/light/nav_drawer_light.png) 63 | 64 | 65 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlin-kapt' 5 | id 'dagger.hilt.android.plugin' 6 | } 7 | 8 | android { 9 | namespace 'com.example.noteswithrestapi' 10 | compileSdk 33 11 | 12 | defaultConfig { 13 | applicationId "com.example.noteswithrestapi" 14 | minSdk 26 15 | targetSdk 33 16 | versionCode 1 17 | versionName "1.0" 18 | 19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 20 | vectorDrawables { 21 | useSupportLibrary true 22 | } 23 | } 24 | 25 | buildTypes { 26 | release { 27 | minifyEnabled false 28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 29 | } 30 | } 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_17 33 | targetCompatibility JavaVersion.VERSION_17 34 | } 35 | kotlinOptions { 36 | jvmTarget = '17' 37 | } 38 | buildFeatures { 39 | compose true 40 | } 41 | composeOptions { 42 | kotlinCompilerExtensionVersion '1.4.6' 43 | } 44 | packagingOptions { 45 | resources { 46 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 47 | } 48 | } 49 | } 50 | 51 | dependencies { 52 | 53 | implementation 'androidx.core:core-ktx:1.10.1' 54 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' 55 | implementation 'androidx.activity:activity-compose:1.7.1' 56 | implementation platform('androidx.compose:compose-bom:2023.04.01') 57 | implementation 'androidx.compose.ui:ui' 58 | implementation 'androidx.compose.ui:ui-graphics' 59 | implementation 'androidx.compose.ui:ui-tooling-preview' 60 | implementation 'androidx.compose.material3:material3' 61 | // implementation 'com.google.android.material:material:1.9.0' 62 | testImplementation 'junit:junit:4.13.2' 63 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 64 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 65 | androidTestImplementation platform('androidx.compose:compose-bom:2023.04.01') 66 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4' 67 | debugImplementation 'androidx.compose.ui:ui-tooling' 68 | debugImplementation 'androidx.compose.ui:ui-test-manifest' 69 | 70 | //Compose Navigation 71 | implementation("androidx.navigation:navigation-compose:$compose_nav_version") 72 | 73 | //Navigation animations 74 | implementation "com.google.accompanist:accompanist-navigation-animation:$accompanist_animation_version" 75 | 76 | //ViewModel Compose 77 | implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$compose_lifecycle_viewmodel_version" 78 | 79 | //Material Icons 80 | implementation "androidx.compose.material:material-icons-extended:$compose_material_icons_version" 81 | 82 | //Dagger-Hilt 83 | implementation "com.google.dagger:hilt-android:$dagger_hilt_version" 84 | kapt "com.google.dagger:hilt-android-compiler:$dagger_hilt_version" 85 | kapt "androidx.hilt:hilt-compiler:$hilt_compiler_version" 86 | implementation "androidx.hilt:hilt-navigation-compose:$hilt_nav_compose_version" 87 | 88 | //Splash Screen API 89 | implementation("androidx.core:core-splashscreen:$splash_screen_api_version") 90 | 91 | //Control system ui 92 | implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_systemui_controller_version" 93 | 94 | //Retrofit 95 | implementation "com.squareup.retrofit2:retrofit:$retrofit_version" 96 | implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" 97 | implementation "com.squareup.okhttp3:okhttp:$okhttp_version" 98 | implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version" 99 | 100 | //EncryptedSharedPreferences 101 | implementation "androidx.security:security-crypto:$crypto_version" 102 | } -------------------------------------------------------------------------------- /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. 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/release/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/release/app-release.apk -------------------------------------------------------------------------------- /app/release/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "com.example.noteswithrestapi", 8 | "variantName": "release", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "attributes": [], 14 | "versionCode": 1, 15 | "versionName": "1.0", 16 | "outputFile": "app-release.apk" 17 | } 18 | ], 19 | "elementType": "File" 20 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/noteswithrestapi/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi 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.example.noteswithrestapi", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/AccountsApiService.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.data.remote 2 | 3 | import com.example.noteswithrestapi.authentication_feature.data.remote.dto.EmailVerificationResendResultDto 4 | import com.example.noteswithrestapi.authentication_feature.data.remote.dto.EmailVerificationResultDto 5 | import com.example.noteswithrestapi.authentication_feature.data.remote.dto.IsAuthenticatedResultDto 6 | import com.example.noteswithrestapi.authentication_feature.data.remote.dto.LoginResultDto 7 | import com.example.noteswithrestapi.authentication_feature.data.remote.dto.RegisterResultDto 8 | import com.example.noteswithrestapi.authentication_feature.data.remote.dto.ResetPasswordConfirmResultDto 9 | import com.example.noteswithrestapi.authentication_feature.data.remote.dto.ResetPasswordResultDto 10 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.LoginCredentials 11 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.PasswordResetConfirmCredentials 12 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.RegisterCredentials 13 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.ResendEmailVerificationCredentials 14 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.SendPasswordResetCodeCredentials 15 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.VerifyEmailCredentials 16 | import retrofit2.Response 17 | import retrofit2.http.Body 18 | import retrofit2.http.POST 19 | 20 | interface AccountsApiService { 21 | 22 | @POST("accounts/login/") 23 | suspend fun login( 24 | @Body loginCredentials: LoginCredentials, 25 | ): Response 26 | 27 | 28 | @POST("accounts/check-token/") 29 | suspend fun isAuthenticated( 30 | @Body token: String, 31 | ): Response 32 | 33 | 34 | @POST("accounts/register/") 35 | suspend fun register( 36 | @Body registerCredentials: RegisterCredentials, 37 | ): Response 38 | 39 | 40 | @POST("accounts/verify-email/") 41 | suspend fun verifyEmail( 42 | @Body verifyEmailCredentials: VerifyEmailCredentials, 43 | ): Response 44 | 45 | 46 | @POST("accounts/verify-email/resend/") 47 | suspend fun resendEmailVerification( 48 | @Body resendEmailVerificationCredentials: ResendEmailVerificationCredentials, 49 | ): Response 50 | 51 | 52 | @POST("accounts/reset-password/") 53 | suspend fun sendPasswordResetCode( 54 | @Body sendPasswordResetCodeCredentials: SendPasswordResetCodeCredentials, 55 | ): Response 56 | 57 | 58 | @POST("accounts/reset-password/confirm/") 59 | suspend fun passwordResetConfirm( 60 | @Body passwordResetConfirmCredentials: PasswordResetConfirmCredentials, 61 | ): Response 62 | 63 | 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/EmailVerificationResendResultDataDto.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto 2 | 3 | data class EmailVerificationResendResultDataDto( 4 | val email: String? 5 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/EmailVerificationResendResultDto.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class EmailVerificationResendResultDto( 6 | @SerializedName("data") val emailVerificationResendResultDataDto: EmailVerificationResendResultDataDto?, 7 | @SerializedName("message") val message: String? 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/EmailVerificationResultDto.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class EmailVerificationResultDto( 6 | @SerializedName("message") val message: String? 7 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/IsAuthenticatedResultDataDto.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto 2 | 3 | data class IsAuthenticatedResultDataDto( 4 | val auth: String, 5 | val user: String 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/IsAuthenticatedResultDto.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class IsAuthenticatedResultDto( 6 | @SerializedName("data") val isAuthenticatedResultDataDto: IsAuthenticatedResultDataDto, 7 | val message: String 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/LoginResultDataDto.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto 2 | 3 | data class LoginResultDataDto( 4 | val email: String?, 5 | val token: String? 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/LoginResultDto.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class LoginResultDto( 6 | @SerializedName("data") val loginResultDataDto: LoginResultDataDto?, 7 | @SerializedName("message") val message: String? 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/RegisterResultDataDto.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto 2 | 3 | data class RegisterResultDataDto( 4 | val email: String?, 5 | val is_active: Boolean?, 6 | val password: String? 7 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/RegisterResultDto.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class RegisterResultDto( 6 | @SerializedName("data") val registerResultDataDto: RegisterResultDataDto, 7 | @SerializedName("message") val message: String 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/ResetPasswordConfirmResultDto.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ResetPasswordConfirmResultDto( 6 | @SerializedName("message") val message: String? 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/ResetPasswordResultDto.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ResetPasswordResultDto( 6 | @SerializedName("message") val message: String? 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/di/AuthenticationModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.di 2 | 3 | import com.example.noteswithrestapi.authentication_feature.data.remote.AccountsApiService 4 | import com.example.noteswithrestapi.authentication_feature.data.repository.AuthenticationRepositoryImpl 5 | import com.example.noteswithrestapi.authentication_feature.domain.repository.AuthenticationRepository 6 | import dagger.Binds 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.components.SingletonComponent 11 | import retrofit2.Retrofit 12 | import javax.inject.Singleton 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | interface AuthenticationModule { 17 | 18 | 19 | @Binds 20 | @Singleton 21 | fun bindAuthenticationRepository( 22 | authenticationRepositoryImpl: AuthenticationRepositoryImpl 23 | ): AuthenticationRepository 24 | 25 | 26 | companion object { 27 | 28 | @Provides 29 | @Singleton 30 | fun provideAccountsApiService( 31 | retrofit: Retrofit 32 | ): AccountsApiService { 33 | return retrofit.create(AccountsApiService::class.java) 34 | } 35 | 36 | } 37 | 38 | 39 | 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/error/AuthenticationAppError.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.domain.error 2 | 3 | import com.example.noteswithrestapi.core.domain.model.AppError 4 | 5 | open class AuthenticationAppError: AppError() { 6 | 7 | object EmptyEmailError: AuthenticationAppError() 8 | 9 | object EmptyPasswordError: AuthenticationAppError() 10 | 11 | object EmptyRepeatPasswordError: AuthenticationAppError() 12 | 13 | object InvalidEmailOrPasswordError: AuthenticationAppError() 14 | 15 | object InvalidVerificationCodeError: AuthenticationAppError() 16 | 17 | object PasswordsDoNotMatchError: AuthenticationAppError() 18 | 19 | object ShortPasswordError: AuthenticationAppError() 20 | 21 | object UserAlreadyExist: AuthenticationAppError() 22 | 23 | object UserDoesNotExist: AuthenticationAppError() 24 | 25 | object InvalidEmailError: AuthenticationAppError() 26 | 27 | object EmailNotVerifiedError: AuthenticationAppError() 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/model/credentials/LoginCredentials.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.domain.model.credentials 2 | 3 | data class LoginCredentials( 4 | val email: String, 5 | val password: String 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/model/credentials/PasswordResetConfirmCredentials.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.domain.model.credentials 2 | 3 | data class PasswordResetConfirmCredentials( 4 | val email: String, 5 | val new_password: String, 6 | val password_reset_code: String, 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/model/credentials/RegisterCredentials.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.domain.model.credentials 2 | 3 | data class RegisterCredentials( 4 | val email: String, 5 | val password: String, 6 | val confirmPassword: String, 7 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/model/credentials/ResendEmailVerificationCredentials.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.domain.model.credentials 2 | 3 | data class ResendEmailVerificationCredentials( 4 | val email: String 5 | ) 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/model/credentials/SendPasswordResetCodeCredentials.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.domain.model.credentials 2 | 3 | data class SendPasswordResetCodeCredentials( 4 | val email: String 5 | ) 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/model/credentials/VerifyEmailCredentials.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.domain.model.credentials 2 | 3 | data class VerifyEmailCredentials( 4 | val email: String, 5 | val verification_code: String 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/repository/AuthenticationRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.domain.repository 2 | 3 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.LoginCredentials 4 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.PasswordResetConfirmCredentials 5 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.SendPasswordResetCodeCredentials 6 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.RegisterCredentials 7 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.ResendEmailVerificationCredentials 8 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.VerifyEmailCredentials 9 | import com.example.noteswithrestapi.core.domain.model.AppResponse 10 | 11 | interface AuthenticationRepository { 12 | 13 | suspend fun login(loginCredentials: LoginCredentials): AppResponse //Return token 14 | 15 | suspend fun isLoggedIn(): AppResponse 16 | 17 | suspend fun register(registerCredentials: RegisterCredentials): AppResponse 18 | 19 | suspend fun verifyEmail(verifyEmailCredentials: VerifyEmailCredentials): AppResponse 20 | 21 | suspend fun resendEmailVerification( 22 | resendEmailVerificationCredentials: ResendEmailVerificationCredentials, 23 | ): AppResponse 24 | 25 | suspend fun sendPasswordResetCode(sendPasswordResetCodeCredentials: SendPasswordResetCodeCredentials): AppResponse 26 | 27 | suspend fun confirmPasswordReset(passwordResetConfirmCredentials: PasswordResetConfirmCredentials): AppResponse 28 | 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/usecase/ConfirmPasswordResetUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.domain.usecase 2 | 3 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.PasswordResetConfirmCredentials 4 | import com.example.noteswithrestapi.authentication_feature.domain.repository.AuthenticationRepository 5 | import com.example.noteswithrestapi.core.domain.model.AppResponse 6 | import com.example.noteswithrestapi.authentication_feature.domain.error.AuthenticationAppError 7 | import javax.inject.Inject 8 | 9 | class ConfirmPasswordResetUseCase @Inject constructor( 10 | private val authenticationRepository: AuthenticationRepository, 11 | ) { 12 | 13 | suspend operator fun invoke( 14 | email: String, 15 | password: String, 16 | confirmPassword: String, 17 | code: String, 18 | ): AppResponse { 19 | 20 | if (email.isEmpty()) { 21 | return AppResponse.failed(AuthenticationAppError.EmptyEmailError) 22 | } 23 | 24 | if (password.isEmpty()) { 25 | return AppResponse.failed(AuthenticationAppError.EmptyPasswordError) 26 | } 27 | 28 | if (password.length < 8) { 29 | return AppResponse.failed(AuthenticationAppError.ShortPasswordError) 30 | } 31 | 32 | if (password != confirmPassword) { 33 | return AppResponse.failed(AuthenticationAppError.PasswordsDoNotMatchError) 34 | } 35 | 36 | val passwordResetCodeCredentials = PasswordResetConfirmCredentials(email, password, code) 37 | 38 | return authenticationRepository.confirmPasswordReset(passwordResetCodeCredentials) 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/usecase/IsLoggedInUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.domain.usecase 2 | 3 | import com.example.noteswithrestapi.authentication_feature.domain.repository.AuthenticationRepository 4 | import com.example.noteswithrestapi.core.domain.model.AppResponse 5 | import javax.inject.Inject 6 | 7 | class IsLoggedInUseCase @Inject constructor( 8 | private val authenticationRepository: AuthenticationRepository 9 | ) { 10 | 11 | suspend operator fun invoke(): AppResponse { 12 | return authenticationRepository.isLoggedIn() 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/usecase/LoginUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.domain.usecase 2 | 3 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.LoginCredentials 4 | import com.example.noteswithrestapi.authentication_feature.domain.repository.AuthenticationRepository 5 | import com.example.noteswithrestapi.core.domain.model.AppResponse 6 | import com.example.noteswithrestapi.authentication_feature.domain.error.AuthenticationAppError 7 | import javax.inject.Inject 8 | 9 | class LoginUseCase @Inject constructor( 10 | private val authenticationRepository: AuthenticationRepository, 11 | ) { 12 | 13 | suspend operator fun invoke(email: String, password: String): AppResponse { 14 | if (email.isBlank()) { 15 | return AppResponse.failed(AuthenticationAppError.EmptyEmailError) 16 | } 17 | if (password.isBlank()) { 18 | return AppResponse.failed(AuthenticationAppError.EmptyPasswordError) 19 | } 20 | val loginCredentials = LoginCredentials(email, password) 21 | return authenticationRepository.login(loginCredentials) 22 | 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/usecase/RegisterUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.domain.usecase 2 | 3 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.RegisterCredentials 4 | import com.example.noteswithrestapi.authentication_feature.domain.repository.AuthenticationRepository 5 | import com.example.noteswithrestapi.core.domain.model.AppResponse 6 | import com.example.noteswithrestapi.authentication_feature.domain.error.AuthenticationAppError 7 | import javax.inject.Inject 8 | 9 | class RegisterUseCase @Inject constructor( 10 | private val authenticationRepository: AuthenticationRepository 11 | ) { 12 | 13 | suspend operator fun invoke( 14 | email: String, 15 | password: String, 16 | confirmPassword: String, 17 | ): AppResponse { 18 | if (email.isEmpty()) { 19 | return AppResponse.failed(AuthenticationAppError.EmptyEmailError) 20 | } 21 | if (password.isEmpty()) { 22 | return AppResponse.failed(AuthenticationAppError.EmptyPasswordError) 23 | } 24 | if (confirmPassword.isEmpty()) { 25 | return AppResponse.failed(AuthenticationAppError.EmptyRepeatPasswordError) 26 | } 27 | if (password.count() < 8) { 28 | return AppResponse.failed(AuthenticationAppError.ShortPasswordError) 29 | } 30 | if (password != confirmPassword) { 31 | return AppResponse.failed(AuthenticationAppError.PasswordsDoNotMatchError) 32 | } 33 | 34 | val registerCredentials = RegisterCredentials(email, password, confirmPassword) 35 | return authenticationRepository.register(registerCredentials) 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/usecase/ResendEmailVerificationCodeUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.domain.usecase 2 | 3 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.ResendEmailVerificationCredentials 4 | import com.example.noteswithrestapi.authentication_feature.domain.repository.AuthenticationRepository 5 | import com.example.noteswithrestapi.core.domain.model.AppResponse 6 | import com.example.noteswithrestapi.authentication_feature.domain.error.AuthenticationAppError 7 | import javax.inject.Inject 8 | 9 | class ResendEmailVerificationCodeUseCase @Inject constructor( 10 | private val authenticationRepository: AuthenticationRepository, 11 | ) { 12 | 13 | suspend operator fun invoke(email: String): AppResponse { 14 | if (email.isEmpty()) { 15 | return AppResponse.failed(AuthenticationAppError.EmptyEmailError) 16 | } 17 | 18 | val resendEmailVerificationCredentials = ResendEmailVerificationCredentials(email) 19 | 20 | return authenticationRepository.resendEmailVerification(resendEmailVerificationCredentials) 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/usecase/SendPasswordResetCodeUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.domain.usecase 2 | 3 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.SendPasswordResetCodeCredentials 4 | import com.example.noteswithrestapi.authentication_feature.domain.repository.AuthenticationRepository 5 | import com.example.noteswithrestapi.core.domain.model.AppResponse 6 | import com.example.noteswithrestapi.authentication_feature.domain.error.AuthenticationAppError 7 | import javax.inject.Inject 8 | 9 | class SendPasswordResetCodeUseCase @Inject constructor( 10 | private val authenticationRepository: AuthenticationRepository, 11 | ) { 12 | 13 | suspend operator fun invoke(email: String): AppResponse { 14 | if (email.isEmpty()) { 15 | return AppResponse.failed(AuthenticationAppError.EmptyEmailError) 16 | } 17 | 18 | val sendPasswordResetCodeCredentials = SendPasswordResetCodeCredentials(email) 19 | 20 | return authenticationRepository.sendPasswordResetCode(sendPasswordResetCodeCredentials) 21 | 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/usecase/VerifyEmailUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.domain.usecase 2 | 3 | import androidx.core.text.isDigitsOnly 4 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.VerifyEmailCredentials 5 | import com.example.noteswithrestapi.authentication_feature.domain.repository.AuthenticationRepository 6 | import com.example.noteswithrestapi.core.domain.model.AppResponse 7 | import com.example.noteswithrestapi.authentication_feature.domain.error.AuthenticationAppError 8 | import javax.inject.Inject 9 | 10 | class VerifyEmailUseCase @Inject constructor( 11 | private val authenticationRepository: AuthenticationRepository, 12 | ) { 13 | 14 | suspend operator fun invoke(email: String, code: String): AppResponse { 15 | if (email.isEmpty()) { 16 | return AppResponse.failed(AuthenticationAppError.EmptyEmailError) 17 | } 18 | 19 | if (code.length < 6) { 20 | return AppResponse.failed(AuthenticationAppError.InvalidVerificationCodeError) 21 | } 22 | 23 | if (!code.isDigitsOnly()) { 24 | return AppResponse.failed(AuthenticationAppError.InvalidVerificationCodeError) 25 | } 26 | 27 | val verifyEmailCredentials = VerifyEmailCredentials(email, code) 28 | 29 | return authenticationRepository.verifyEmail(verifyEmailCredentials) 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/components/OTPTextField.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.presentation.components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.width 11 | import androidx.compose.foundation.layout.wrapContentHeight 12 | import androidx.compose.foundation.text.BasicTextField 13 | import androidx.compose.foundation.text.KeyboardOptions 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.text.input.KeyboardType 21 | import androidx.compose.ui.text.style.TextAlign 22 | import androidx.compose.ui.unit.Dp 23 | import androidx.compose.ui.unit.TextUnit 24 | import androidx.compose.ui.unit.dp 25 | import androidx.compose.ui.unit.sp 26 | 27 | @Composable 28 | fun OTPTextField( 29 | modifier: Modifier = Modifier, 30 | textValue: () -> String = { "" }, 31 | onTextValueChange: (String) -> Unit, 32 | charColor: Color = MaterialTheme.colorScheme.tertiary, 33 | underlineColor: Color = MaterialTheme.colorScheme.primary, 34 | charBackground: Color = Color.Transparent, 35 | charSize: TextUnit = 26.sp, 36 | containerSize: Dp = charSize.value.dp * 2, 37 | length: Int = 6, 38 | enabled: Boolean = true, 39 | password: Boolean = false, 40 | passwordChar: String = "", 41 | keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), 42 | ) { 43 | BasicTextField( 44 | modifier = modifier, 45 | value = textValue(), 46 | onValueChange = { 47 | if (it.length <= length) { 48 | onTextValueChange.invoke(it) 49 | } 50 | }, 51 | enabled = enabled, 52 | keyboardOptions = keyboardOptions, 53 | decorationBox = { 54 | Row(horizontalArrangement = Arrangement.spacedBy(0.dp)) { 55 | repeat(length) { index -> 56 | CharView( 57 | index = index, 58 | text = textValue(), 59 | charColor = charColor, 60 | underlineColor = underlineColor, 61 | charSize = charSize, 62 | containerSize = containerSize, 63 | charBackground = charBackground, 64 | password = password, 65 | passwordChar = passwordChar, 66 | ) 67 | } 68 | } 69 | }) 70 | } 71 | 72 | @Composable 73 | private fun CharView( 74 | index: Int, 75 | text: String, 76 | charColor: Color, 77 | underlineColor: Color, 78 | charSize: TextUnit, 79 | containerSize: Dp, 80 | charBackground: Color = Color.Transparent, 81 | password: Boolean = false, 82 | passwordChar: String = "", 83 | ) { 84 | 85 | val modifier = Modifier 86 | .width(containerSize) 87 | .background(charBackground) 88 | 89 | Column( 90 | horizontalAlignment = Alignment.CenterHorizontally, 91 | verticalArrangement = Arrangement.Center, 92 | ) { 93 | val char = when { 94 | index >= text.length -> "" 95 | password -> passwordChar 96 | else -> text[index].toString() 97 | } 98 | Text( 99 | text = char, 100 | color = charColor, 101 | modifier = modifier.wrapContentHeight(), 102 | style = MaterialTheme.typography.bodyLarge, 103 | fontSize = charSize, 104 | textAlign = TextAlign.Center, 105 | ) 106 | Spacer(modifier = Modifier.height(2.dp)) 107 | Box( 108 | modifier = Modifier 109 | .background(underlineColor) 110 | .height(1.5.dp) 111 | .width(containerSize / 1.75f) 112 | ) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/confirm_password_reset/ConfirmPasswordResetEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.presentation.confirm_password_reset 2 | 3 | sealed class ConfirmPasswordResetEvent { 4 | 5 | data class OnPasswordInputValueChange(val newValue: String): ConfirmPasswordResetEvent() 6 | 7 | data class OnConfirmPasswordInputValueChange(val newValue: String): ConfirmPasswordResetEvent() 8 | 9 | object OnChangePasswordVisualTransformation: ConfirmPasswordResetEvent() 10 | 11 | object OnChangeConfirmPasswordVisualTransformation: ConfirmPasswordResetEvent() 12 | 13 | data class OnCodeInputValueChange(val newValue: String): ConfirmPasswordResetEvent() 14 | 15 | object OnConfirmClick: ConfirmPasswordResetEvent() 16 | 17 | object OnResendCodeClick: ConfirmPasswordResetEvent() 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/confirm_password_reset/ConfirmPasswordResetState.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.presentation.confirm_password_reset 2 | 3 | data class ConfirmPasswordResetState( 4 | val passwordInput: String = "", 5 | val passwordError: String? = null, 6 | val isPasswordShown: Boolean = false, 7 | 8 | val confirmPasswordInput: String = "", 9 | val confirmPasswordError: String? = null, 10 | val isConfirmPasswordShown: Boolean = false, 11 | 12 | val codeInput: String = "", 13 | 14 | val isLoading: Boolean = false, 15 | val errorMessage: String? = null, 16 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/confirm_password_reset/ConfirmPasswordResetUiEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.presentation.confirm_password_reset 2 | 3 | sealed class ConfirmPasswordResetUiEvent { 4 | 5 | object OnConfirmPasswordResetSuccess : ConfirmPasswordResetUiEvent() 6 | 7 | data class ShowSnackbar(val message: String) : ConfirmPasswordResetUiEvent() 8 | 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/login/LoginEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.presentation.login 2 | 3 | sealed class LoginEvent { 4 | 5 | object OnSignInClick: LoginEvent() 6 | 7 | data class OnPasswordInputValueChange(val newValue: String): LoginEvent() 8 | 9 | data class OnEmailInputValueChange(val newValue: String): LoginEvent() 10 | 11 | object OnChangePasswordVisualTransformation: LoginEvent() 12 | 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/login/LoginState.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.presentation.login 2 | 3 | data class LoginState( 4 | 5 | val emailInput: String = "", 6 | val emailErrorMessage: String? = null, 7 | 8 | val passwordInput: String = "", 9 | val isPasswordShown: Boolean = false, 10 | val passwordErrorMessage: String? = null, 11 | 12 | val errorMessage: String? = null, 13 | val isLoading: Boolean = false, 14 | ) 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/login/LoginUiEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.presentation.login 2 | 3 | sealed class LoginUiEvent { 4 | 5 | object SuccessfullyLoggedIn: LoginUiEvent() 6 | 7 | data class EmailNotVerified(val email: String): LoginUiEvent() 8 | 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/register/RegisterEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.presentation.register 2 | 3 | sealed class RegisterEvent { 4 | 5 | object OnSignUpClick: RegisterEvent() 6 | 7 | data class OnEmailInputValueChange(val newValue: String): RegisterEvent() 8 | 9 | data class OnPasswordInputValueChange(val newValue: String): RegisterEvent() 10 | 11 | data class OnConfirmPasswordInputValueChange(val newValue: String): RegisterEvent() 12 | 13 | object OnChangePasswordVisualTransformation: RegisterEvent() 14 | 15 | object OnChangeConfirmPasswordVisualTransformation: RegisterEvent() 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/register/RegisterState.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.presentation.register 2 | 3 | data class RegisterState( 4 | val emailInput: String = "", 5 | val emailErrorMessage: String? = null, 6 | 7 | val passwordInput: String = "", 8 | val isPasswordShown: Boolean = false, 9 | val passwordErrorMessage: String? = null, 10 | 11 | val confirmPasswordInput: String = "", 12 | val isConfirmPasswordShown: Boolean = false, 13 | val confirmPasswordErrorMessage: String? = null, 14 | 15 | val errorMessage: String? = null, 16 | val isLoading: Boolean = false, 17 | ) 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/register/RegisterUiEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.presentation.register 2 | 3 | sealed class RegisterUiEvent { 4 | 5 | data class OnRegisterSuccessful(val email: String): RegisterUiEvent() 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/reset_password/ResetPasswordEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.presentation.reset_password 2 | 3 | sealed class ResetPasswordEvent { 4 | 5 | data class OnEmailInputValueChange(val newValue: String): ResetPasswordEvent() 6 | 7 | object OnResetPasswordClick: ResetPasswordEvent() 8 | 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/reset_password/ResetPasswordState.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.presentation.reset_password 2 | 3 | data class ResetPasswordState( 4 | val emailInput: String = "", 5 | val emailError: String? = null, 6 | 7 | val errorMessage: String? = null, 8 | val isLoading: Boolean = false, 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/reset_password/ResetPasswordUiEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.presentation.reset_password 2 | 3 | sealed class ResetPasswordUiEvent { 4 | 5 | data class OnResetCodeSentSuccessfully(val email: String): ResetPasswordUiEvent() 6 | 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/reset_password/ResetPasswordViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.presentation.reset_password 2 | 3 | import android.app.Application 4 | import android.util.Log 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.setValue 8 | import androidx.lifecycle.ViewModel 9 | import androidx.lifecycle.viewModelScope 10 | import com.example.noteswithrestapi.R 11 | import com.example.noteswithrestapi.authentication_feature.domain.usecase.SendPasswordResetCodeUseCase 12 | import com.example.noteswithrestapi.core.domain.model.AppError 13 | import com.example.noteswithrestapi.core.domain.model.AppResponse 14 | import com.example.noteswithrestapi.authentication_feature.domain.error.AuthenticationAppError 15 | import dagger.hilt.android.lifecycle.HiltViewModel 16 | import kotlinx.coroutines.channels.Channel 17 | import kotlinx.coroutines.flow.receiveAsFlow 18 | import kotlinx.coroutines.launch 19 | import javax.inject.Inject 20 | 21 | @HiltViewModel 22 | class ResetPasswordViewModel @Inject constructor( 23 | private val sendPasswordResetCodeUseCase: SendPasswordResetCodeUseCase, 24 | private val application: Application, 25 | ) : ViewModel() { 26 | 27 | 28 | private val _uiEvent = Channel() 29 | val uiEvent = _uiEvent.receiveAsFlow() 30 | 31 | var resetPasswordState by mutableStateOf(ResetPasswordState()) 32 | private set 33 | 34 | 35 | fun onEvent(event: ResetPasswordEvent) { 36 | when (event) { 37 | is ResetPasswordEvent.OnResetPasswordClick -> { 38 | sendResetCode(resetPasswordState.emailInput) 39 | } 40 | 41 | is ResetPasswordEvent.OnEmailInputValueChange -> { 42 | resetPasswordState = resetPasswordState.copy( 43 | emailInput = event.newValue, 44 | emailError = null 45 | ) 46 | } 47 | } 48 | } 49 | 50 | 51 | private fun sendResetCode(email: String) { 52 | viewModelScope.launch { 53 | resetPasswordState = resetPasswordState.copy(isLoading = true) 54 | 55 | when (val result = sendPasswordResetCodeUseCase(email)) { 56 | is AppResponse.Success -> { 57 | resetPasswordState = resetPasswordState.copy(isLoading = false) 58 | _uiEvent.send(ResetPasswordUiEvent.OnResetCodeSentSuccessfully(email)) 59 | } 60 | 61 | is AppResponse.Failed -> { 62 | resetPasswordState = resetPasswordState.copy(isLoading = false) 63 | handleAppError(result.error) 64 | } 65 | } 66 | 67 | } 68 | } 69 | 70 | 71 | private fun handleAppError(error: AppError) { 72 | when (error) { 73 | 74 | is AppError.ServerError -> { 75 | resetPasswordState = resetPasswordState.copy( 76 | errorMessage = application.getString(R.string.error_server) 77 | ) 78 | } 79 | 80 | is AppError.PoorNetworkConnectionError -> { 81 | resetPasswordState = resetPasswordState.copy( 82 | errorMessage = application.getString(R.string.error_poor_network_connection), 83 | ) 84 | } 85 | 86 | is AppError.GeneralError -> { 87 | resetPasswordState = resetPasswordState.copy( 88 | errorMessage = application.getString(error.messageResId), 89 | ) 90 | } 91 | 92 | is AuthenticationAppError -> { 93 | handleAuthAppError(error) 94 | } 95 | 96 | else -> { 97 | resetPasswordState = resetPasswordState.copy( 98 | errorMessage = application.getString(R.string.error_unknown), 99 | ) 100 | Log.e("UNEXPECTED_ERROR", error.toString()) 101 | } 102 | } 103 | } 104 | 105 | private fun handleAuthAppError(error: AuthenticationAppError) { 106 | when (error) { 107 | is AuthenticationAppError.EmptyEmailError -> { 108 | resetPasswordState = resetPasswordState.copy( 109 | emailError = application.getString(R.string.error_empty_email), 110 | ) 111 | } 112 | 113 | is AuthenticationAppError.InvalidEmailError -> { 114 | resetPasswordState = resetPasswordState.copy( 115 | emailError = application.getString(R.string.error_invalid_email), 116 | ) 117 | } 118 | 119 | is AuthenticationAppError.UserDoesNotExist -> { 120 | resetPasswordState = resetPasswordState.copy( 121 | errorMessage = application.getString(R.string.error_user_does_not_exists), 122 | ) 123 | } 124 | 125 | else -> { 126 | resetPasswordState = resetPasswordState.copy( 127 | errorMessage = application.getString(R.string.error_unknown), 128 | ) 129 | Log.e("UNEXPECTED_ERROR", error.toString()) 130 | } 131 | } 132 | } 133 | 134 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/verify_email/VerifyEmailEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.presentation.verify_email 2 | 3 | sealed class VerifyEmailEvent { 4 | 5 | data class OnCodeInputValueChange(val newValue: String) : VerifyEmailEvent() 6 | 7 | object OnConfirmClick: VerifyEmailEvent() 8 | 9 | object OnResendCodeClick: VerifyEmailEvent() 10 | 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/verify_email/VerifyEmailState.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.presentation.verify_email 2 | 3 | data class VerifyEmailState( 4 | val codeInput: String = "", 5 | val codeError: String? = null, 6 | 7 | val isLoading: Boolean = false, 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/verify_email/VerifyEmailUiEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.presentation.verify_email 2 | 3 | sealed class VerifyEmailUiEvent { 4 | 5 | object OnEmailVerifiedSuccessfully: VerifyEmailUiEvent() 6 | 7 | data class ShowSnackbar(val message: String): VerifyEmailUiEvent() 8 | 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/verify_email/VerifyEmailViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.authentication_feature.presentation.verify_email 2 | 3 | import android.app.Application 4 | import android.util.Log 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.setValue 8 | import androidx.lifecycle.SavedStateHandle 9 | import androidx.lifecycle.ViewModel 10 | import androidx.lifecycle.viewModelScope 11 | import com.example.noteswithrestapi.R 12 | import com.example.noteswithrestapi.authentication_feature.domain.usecase.ResendEmailVerificationCodeUseCase 13 | import com.example.noteswithrestapi.authentication_feature.domain.usecase.VerifyEmailUseCase 14 | import com.example.noteswithrestapi.core.domain.model.AppError 15 | import com.example.noteswithrestapi.core.domain.model.AppResponse 16 | import com.example.noteswithrestapi.authentication_feature.domain.error.AuthenticationAppError 17 | import com.example.noteswithrestapi.core.presentation.navigation.Screen 18 | import dagger.hilt.android.lifecycle.HiltViewModel 19 | import kotlinx.coroutines.channels.Channel 20 | import kotlinx.coroutines.flow.receiveAsFlow 21 | import kotlinx.coroutines.launch 22 | import javax.inject.Inject 23 | 24 | @HiltViewModel 25 | class VerifyEmailViewModel @Inject constructor( 26 | private val verifyEmailUseCase: VerifyEmailUseCase, 27 | private val resendEmailVerificationCodeUseCase: ResendEmailVerificationCodeUseCase, 28 | private val savedStateHandle: SavedStateHandle, 29 | private val application: Application, 30 | ) : ViewModel() { 31 | 32 | 33 | private val _uiEvent = Channel() 34 | val uiEvent = _uiEvent.receiveAsFlow() 35 | 36 | var verifyEmailState by mutableStateOf(VerifyEmailState()) 37 | private set 38 | 39 | private lateinit var email: String 40 | 41 | init { 42 | viewModelScope.launch { 43 | savedStateHandle.get(Screen.EMAIL_ARG)?.let { email -> 44 | this@VerifyEmailViewModel.email = email 45 | } ?: throw RuntimeException("Email argument wasn't passed") 46 | } 47 | } 48 | 49 | 50 | fun onEvent(event: VerifyEmailEvent) { 51 | when (event) { 52 | is VerifyEmailEvent.OnCodeInputValueChange -> { 53 | verifyEmailState = verifyEmailState.copy( 54 | codeInput = event.newValue, 55 | codeError = null 56 | ) 57 | } 58 | 59 | VerifyEmailEvent.OnConfirmClick -> { 60 | verifyEmail(email, verifyEmailState.codeInput) 61 | } 62 | 63 | VerifyEmailEvent.OnResendCodeClick -> { 64 | resendEmail(email) 65 | } 66 | } 67 | } 68 | 69 | private fun verifyEmail(email: String, code: String) { 70 | viewModelScope.launch { 71 | verifyEmailState = verifyEmailState.copy(isLoading = true) 72 | 73 | when (val result = verifyEmailUseCase(email, code)) { 74 | is AppResponse.Success -> { 75 | verifyEmailState = verifyEmailState.copy(isLoading = false) 76 | _uiEvent.send(VerifyEmailUiEvent.OnEmailVerifiedSuccessfully) 77 | } 78 | 79 | is AppResponse.Failed -> { 80 | verifyEmailState = verifyEmailState.copy(isLoading = false) 81 | handleAppError(result.error) 82 | } 83 | } 84 | 85 | } 86 | } 87 | 88 | private fun resendEmail(email: String) { 89 | verifyEmailState = verifyEmailState.copy(isLoading = true) 90 | viewModelScope.launch { 91 | when (val result = resendEmailVerificationCodeUseCase(email)) { 92 | is AppResponse.Success -> { 93 | verifyEmailState = verifyEmailState.copy(isLoading = false, codeError = null) 94 | _uiEvent.send(VerifyEmailUiEvent.ShowSnackbar(application.getString(R.string.text_code_resent))) 95 | } 96 | 97 | is AppResponse.Failed -> { 98 | verifyEmailState = verifyEmailState.copy(isLoading = false, codeError = null) 99 | handleAppError(result.error) 100 | } 101 | } 102 | } 103 | } 104 | 105 | 106 | private fun handleAppError(error: AppError) { 107 | when (error) { 108 | is AppError.GeneralError -> { 109 | verifyEmailState = verifyEmailState.copy( 110 | codeError = application.getString(error.messageResId) 111 | ) 112 | } 113 | 114 | is AppError.PoorNetworkConnectionError -> { 115 | verifyEmailState = verifyEmailState.copy( 116 | codeError = application.getString(R.string.error_poor_network_connection) 117 | ) 118 | } 119 | 120 | is AppError.ServerError -> { 121 | verifyEmailState = verifyEmailState.copy( 122 | codeError = application.getString(R.string.error_server) 123 | ) 124 | } 125 | 126 | is AuthenticationAppError -> { 127 | handleAuthAppError(error) 128 | } 129 | 130 | else -> { 131 | verifyEmailState = verifyEmailState.copy( 132 | codeError = application.getString(R.string.error_unknown) 133 | ) 134 | Log.e("UNKNOWN_ERROR", error.toString()) 135 | } 136 | } 137 | } 138 | 139 | private fun handleAuthAppError(error: AuthenticationAppError) { 140 | when (error) { 141 | is AuthenticationAppError.InvalidVerificationCodeError -> { 142 | verifyEmailState = verifyEmailState.copy( 143 | codeError = application.getString(R.string.error_invalid_verification_code) 144 | ) 145 | } 146 | 147 | else -> { 148 | verifyEmailState = verifyEmailState.copy( 149 | codeError = application.getString(R.string.error_unknown) 150 | ) 151 | Log.e("UNKNOWN_ERROR", error.toString()) 152 | } 153 | } 154 | } 155 | 156 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/NoteApplication.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class NoteApplication : Application() 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/data/network/HttpCustomErrorCode.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.data.network 2 | 3 | enum class HttpCustomErrorCode(val code: String) { 4 | 5 | //Authentication errors 6 | InvalidToken("4001"), 7 | InvalidEmailOrPassword("4002"), 8 | InvalidEmail("4003"), 9 | EmailAlreadyVerified("4004"), 10 | EmailNotVerified("4005"), 11 | UsedDoesNotExist("4006"), 12 | UsedAlreadyExist("4007"), 13 | InvalidVerificationCode("4008"), 14 | InvalidPasswordResetCode("4009"), 15 | InvalidCredentials("4010"), 16 | ShortPassword("4011"), 17 | 18 | NotFound("404"), 19 | Unauthorized("401"), 20 | Forbidden("403"), 21 | 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/data/network/NetworkConstants.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.data.network 2 | 3 | object NetworkConstants { 4 | 5 | const val API_BASE_URL = "http://192.168.127.29:8000/" 6 | 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/data/network/NetworkUtil.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.data.network 2 | 3 | import com.example.noteswithrestapi.R 4 | import com.example.noteswithrestapi.core.domain.model.AppError 5 | import com.example.noteswithrestapi.authentication_feature.domain.error.AuthenticationAppError 6 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 7 | import okhttp3.RequestBody.Companion.toRequestBody 8 | import org.json.JSONObject 9 | import retrofit2.Response 10 | 11 | fun createRequestBody(vararg params: Pair) = 12 | JSONObject(mapOf(*params)).toString() 13 | .toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()) 14 | 15 | 16 | fun Response.getAppErrorByHttpErrorCode(): AppError { 17 | val code = this.getHttpErrorCode() ?: return AppError.GeneralError(R.string.error_unknown) 18 | if (code.toInt() in 500..599) { 19 | return AppError.ServerError 20 | } 21 | return parseHttpErrorCodeToAppError(code) 22 | } 23 | 24 | fun parseHttpErrorCodeToAppError(code: String): AppError { 25 | return when (code) { 26 | 27 | HttpCustomErrorCode.InvalidVerificationCode.code -> AuthenticationAppError.InvalidVerificationCodeError 28 | HttpCustomErrorCode.ShortPassword.code -> AuthenticationAppError.ShortPasswordError 29 | HttpCustomErrorCode.InvalidVerificationCode.code -> AuthenticationAppError.InvalidVerificationCodeError 30 | HttpCustomErrorCode.InvalidPasswordResetCode.code -> AuthenticationAppError.InvalidVerificationCodeError 31 | HttpCustomErrorCode.InvalidEmail.code -> AuthenticationAppError.InvalidEmailError 32 | HttpCustomErrorCode.UsedAlreadyExist.code -> AuthenticationAppError.UserAlreadyExist 33 | HttpCustomErrorCode.UsedDoesNotExist.code -> AuthenticationAppError.UserDoesNotExist 34 | HttpCustomErrorCode.InvalidEmailOrPassword.code -> AuthenticationAppError.InvalidEmailOrPasswordError 35 | HttpCustomErrorCode.EmailNotVerified.code -> AuthenticationAppError.EmailNotVerifiedError 36 | 37 | HttpCustomErrorCode.InvalidCredentials.code -> AppError.InvalidCredentialsError 38 | HttpCustomErrorCode.NotFound.code -> AppError.NotFoundError 39 | HttpCustomErrorCode.Unauthorized.code -> AppError.UnauthorizedError 40 | HttpCustomErrorCode.Forbidden.code -> AppError.ForbiddenError 41 | else -> AppError.GeneralError(R.string.error_unknown) 42 | } 43 | } 44 | 45 | 46 | fun Response.getHttpErrorCode(): String? { 47 | try { 48 | if (this.code() in 500..599) { 49 | return this.code().toString() 50 | } 51 | if (this.code() in 400..499) { 52 | return this.errorBody()?.string()?.let { jsonString -> 53 | JSONObject(jsonString).getString("code") 54 | } ?: this.code().toString() 55 | } 56 | return null 57 | } catch (e: Exception) { 58 | return this.code().toString() 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/data/token/TokenManager.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.data.token 2 | 3 | interface TokenManager { 4 | 5 | val authToken: String? 6 | 7 | fun saveToken(token: String) 8 | 9 | fun removeToken() 10 | 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/data/token/TokenManagerEncryptedSharedPreferences.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.data.token 2 | 3 | import android.content.Context 4 | import androidx.security.crypto.EncryptedSharedPreferences 5 | import androidx.security.crypto.MasterKey 6 | import dagger.hilt.android.qualifiers.ApplicationContext 7 | import javax.inject.Inject 8 | 9 | class TokenManagerEncryptedSharedPreferences @Inject constructor( 10 | @ApplicationContext context: Context, 11 | ): TokenManager { 12 | 13 | private val masterKey = MasterKey.Builder(context) 14 | .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) 15 | .build() 16 | 17 | private val sharedPreferences = EncryptedSharedPreferences.create( 18 | context, 19 | TOKEN_STORAGE_FILE_NAME, 20 | masterKey, 21 | EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, 22 | EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, 23 | ) 24 | 25 | 26 | override val authToken: String? 27 | get() = sharedPreferences.getString(AUTH_TOKEN_KEY, null) 28 | 29 | override fun saveToken(token: String) { 30 | sharedPreferences.edit() 31 | .putString(AUTH_TOKEN_KEY, token) 32 | .apply() 33 | } 34 | 35 | override fun removeToken() { 36 | sharedPreferences.edit() 37 | .remove(AUTH_TOKEN_KEY) 38 | .apply() 39 | } 40 | 41 | 42 | companion object { 43 | 44 | private const val TOKEN_STORAGE_FILE_NAME = "token_storage" 45 | 46 | private const val AUTH_TOKEN_KEY = "auth_token_key" 47 | 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.di 2 | 3 | import com.example.noteswithrestapi.core.data.token.TokenManager 4 | import com.example.noteswithrestapi.core.data.token.TokenManagerEncryptedSharedPreferences 5 | import com.example.noteswithrestapi.core.data.network.NetworkConstants 6 | import dagger.Binds 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.components.SingletonComponent 11 | import okhttp3.OkHttpClient 12 | import okhttp3.logging.HttpLoggingInterceptor 13 | import retrofit2.Retrofit 14 | import retrofit2.converter.gson.GsonConverterFactory 15 | import javax.inject.Singleton 16 | 17 | @Module 18 | @InstallIn(SingletonComponent::class) 19 | interface AppModule { 20 | 21 | @Binds 22 | @Singleton 23 | fun bindTokenManager( 24 | tokenManagerEncryptedSharedPreferences: TokenManagerEncryptedSharedPreferences 25 | ): TokenManager 26 | 27 | companion object { 28 | 29 | @Provides 30 | @Singleton 31 | fun provideNoteApiRetrofit( 32 | okHttpClient: OkHttpClient 33 | ): Retrofit { 34 | return Retrofit.Builder() 35 | .baseUrl(NetworkConstants.API_BASE_URL) 36 | .addConverterFactory(GsonConverterFactory.create()) 37 | .client(okHttpClient) 38 | .build() 39 | } 40 | 41 | @Singleton 42 | @Provides 43 | fun provideOkHttpClient(): OkHttpClient { 44 | return OkHttpClient.Builder() 45 | .addInterceptor(HttpLoggingInterceptor().apply { 46 | level = HttpLoggingInterceptor.Level.BODY 47 | }).build() 48 | } 49 | 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/domain/model/AppError.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.domain.model 2 | 3 | open class AppError( 4 | message: String? = null, 5 | cause: Exception? = null 6 | ): Error(message, cause) { 7 | 8 | //Common Errors 9 | data class GeneralError(val messageResId: Int): AppError() 10 | 11 | object PoorNetworkConnectionError: AppError() 12 | 13 | object ServerError: AppError() 14 | 15 | object InvalidCredentialsError: AppError() 16 | 17 | object NotFoundError: AppError() 18 | 19 | object UnauthorizedError: AppError() 20 | 21 | object ForbiddenError: AppError() 22 | 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/domain/model/AppResponse.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.domain.model 2 | 3 | 4 | sealed class AppResponse { 5 | 6 | //Success state 7 | data class Success(val data: T) : AppResponse() 8 | 9 | //Failed state 10 | data class Failed(val error: AppError) : AppResponse() 11 | 12 | 13 | companion object { 14 | 15 | //GET SUCCESS STATE 16 | fun success(data: T) = Success(data) 17 | 18 | //GET FAILED STATE 19 | fun failed(error: AppError) = Failed(error) 20 | 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/domain/paginator/DefaultPaginator.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.domain.paginator 2 | 3 | import com.example.noteswithrestapi.core.domain.model.AppError 4 | import com.example.noteswithrestapi.core.domain.model.AppResponse 5 | 6 | class DefaultPaginator( 7 | private val initialKey: Key, 8 | private inline val onLoadUpdated: (Boolean) -> Unit, 9 | private inline val onRequest: suspend (nextKey: Key) -> AppResponse>, 10 | private inline val getNextKey: suspend (List) -> Key, 11 | private inline val onError: suspend (AppError) -> Unit, 12 | private inline val onSuccess: suspend (items: List, newKey: Key) -> Unit, 13 | ) : Paginator { 14 | 15 | private var currentKey = initialKey 16 | private var isMakingRequest = false 17 | 18 | override suspend fun loadNextPage() { 19 | if (isMakingRequest) { 20 | return 21 | } 22 | isMakingRequest = true 23 | onLoadUpdated(true) 24 | val result = onRequest(currentKey) 25 | isMakingRequest = false 26 | when (result) { 27 | is AppResponse.Success -> { 28 | val items = result.data 29 | currentKey = getNextKey(items) 30 | onSuccess(items, currentKey) 31 | onLoadUpdated(false) 32 | return 33 | } 34 | 35 | is AppResponse.Failed -> { 36 | onError(result.error) 37 | onLoadUpdated(false) 38 | return 39 | } 40 | } 41 | } 42 | 43 | override fun reset() { 44 | currentKey = initialKey 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/domain/paginator/Paginator.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.domain.paginator 2 | 3 | interface Paginator { 4 | 5 | suspend fun loadNextPage() 6 | 7 | fun reset() 8 | 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/presentation/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.presentation 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.animation.ExperimentalAnimationApi 7 | import androidx.compose.material3.MaterialTheme 8 | import com.example.noteswithrestapi.core.presentation.theme.ui.theme.NotesWithRESTAPITheme 9 | import com.google.accompanist.navigation.animation.rememberAnimatedNavController 10 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 11 | import dagger.hilt.android.AndroidEntryPoint 12 | 13 | @AndroidEntryPoint 14 | class MainActivity : ComponentActivity() { 15 | 16 | @OptIn(ExperimentalAnimationApi::class) 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setContent { 20 | NotesWithRESTAPITheme() { 21 | val systemUiController = rememberSystemUiController().apply { 22 | this.setSystemBarsColor(MaterialTheme.colorScheme.surface) 23 | } 24 | val navController = rememberAnimatedNavController() 25 | MainScreen(navController = navController) 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/presentation/MainScreen.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.presentation 2 | 3 | import androidx.compose.material3.DrawerValue 4 | import androidx.compose.material3.ExperimentalMaterial3Api 5 | import androidx.compose.material3.ModalNavigationDrawer 6 | import androidx.compose.material3.rememberDrawerState 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.rememberCoroutineScope 9 | import androidx.navigation.NavHostController 10 | import androidx.navigation.compose.currentBackStackEntryAsState 11 | import com.example.noteswithrestapi.core.presentation.components.AppDrawerContent 12 | import com.example.noteswithrestapi.core.presentation.navigation.NavigationItem 13 | import com.example.noteswithrestapi.core.presentation.navigation.RootNavGraph 14 | import kotlinx.coroutines.launch 15 | 16 | 17 | @OptIn(ExperimentalMaterial3Api::class) 18 | @Composable 19 | fun MainScreen( 20 | navController: NavHostController, 21 | ) { 22 | 23 | val navBackstackEntry = navController.currentBackStackEntryAsState() 24 | 25 | val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) 26 | 27 | val scope = rememberCoroutineScope() 28 | 29 | ModalNavigationDrawer( 30 | drawerState = drawerState, 31 | gesturesEnabled = false, 32 | drawerContent = { 33 | navBackstackEntry.value?.destination?.parent?.route?.let { currentGraph -> 34 | AppDrawerContent( 35 | items = listOf( 36 | NavigationItem.NotesFeature, 37 | NavigationItem.ProfileFeature 38 | ), 39 | currentRoute = currentGraph, 40 | onItemClick = { navItem -> 41 | scope.launch { 42 | drawerState.close() 43 | } 44 | if (navItem.route != currentGraph) { 45 | navController.navigate(navItem.route) 46 | } 47 | } 48 | ) 49 | } 50 | }, 51 | content = { 52 | RootNavGraph( 53 | navController = navController, 54 | onShowNavigationDrawer = { 55 | scope.launch { 56 | drawerState.open() 57 | } 58 | } 59 | ) 60 | } 61 | ) 62 | 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/presentation/components/AppDrawerContent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.presentation.components 2 | 3 | import androidx.compose.foundation.layout.Spacer 4 | import androidx.compose.foundation.layout.height 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.ModalDrawerSheet 10 | import androidx.compose.material3.NavigationDrawerItem 11 | import androidx.compose.material3.NavigationDrawerItemDefaults 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.* 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.unit.dp 18 | import com.example.noteswithrestapi.core.presentation.navigation.NavigationItem 19 | 20 | 21 | @OptIn(ExperimentalMaterial3Api::class) 22 | @Composable 23 | fun AppDrawerContent( 24 | items: List, 25 | currentRoute: String, 26 | onItemClick: (NavigationItem) -> Unit, 27 | ) { 28 | 29 | ModalDrawerSheet { 30 | Spacer(Modifier.height(12.dp)) 31 | items.forEach { item -> 32 | NavigationDrawerItem( 33 | icon = { Icon(item.icon, contentDescription = null) }, 34 | label = { Text(stringResource(id = item.nameId)) }, 35 | selected = item.route == currentRoute, 36 | onClick = { 37 | onItemClick(item) 38 | }, 39 | colors = NavigationDrawerItemDefaults.colors( 40 | selectedContainerColor = MaterialTheme.colorScheme.secondary, 41 | unselectedContainerColor = Color.Transparent 42 | ), 43 | modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) 44 | ) 45 | } 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/presentation/components/ButtonComponent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.presentation.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.rounded.Login 11 | import androidx.compose.material3.Button 12 | import androidx.compose.material3.ButtonDefaults 13 | import androidx.compose.material3.Icon 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.graphics.Shape 21 | import androidx.compose.ui.graphics.vector.ImageVector 22 | import androidx.compose.ui.text.font.FontWeight 23 | import androidx.compose.ui.tooling.preview.Preview 24 | import androidx.compose.ui.unit.dp 25 | import com.example.noteswithrestapi.core.presentation.theme.ui.theme.NotesWithRESTAPITheme 26 | 27 | @Composable 28 | fun ButtonComponent( 29 | modifier: Modifier = Modifier, 30 | text: String, 31 | leadingIcon: ImageVector? = null, 32 | backgroundColor: Color = MaterialTheme.colorScheme.secondary, 33 | contentColor: Color = MaterialTheme.colorScheme.onSecondary, 34 | contentPadding: PaddingValues = PaddingValues(start = 32.dp, end = 40.dp, top = 14.dp, bottom = 14.dp), 35 | enabled: () -> Boolean, 36 | shape: Shape = MaterialTheme.shapes.large, 37 | onButtonClick: () -> Unit, 38 | ) { 39 | 40 | Button( 41 | modifier = modifier, 42 | colors = ButtonDefaults.buttonColors( 43 | containerColor = backgroundColor, 44 | contentColor = contentColor 45 | ), 46 | shape = shape, 47 | enabled = enabled(), 48 | contentPadding = contentPadding, 49 | onClick = { 50 | onButtonClick() 51 | }, 52 | ) { 53 | Row( 54 | modifier = Modifier, 55 | horizontalArrangement = Arrangement.Center, 56 | verticalAlignment = Alignment.CenterVertically 57 | ) { 58 | if (leadingIcon != null) { 59 | Icon( 60 | imageVector = leadingIcon, 61 | contentDescription = text, 62 | modifier = Modifier.size(18.dp), 63 | ) 64 | Spacer(modifier = Modifier.width(9.dp)) 65 | } 66 | Text( 67 | text = text, 68 | style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold) 69 | ) 70 | } 71 | } 72 | } 73 | 74 | @Preview(showBackground = false) 75 | @Composable 76 | fun ButtonComponentPreview() { 77 | NotesWithRESTAPITheme(darkTheme = true) { 78 | ButtonComponent( 79 | text = "Login", 80 | enabled = { true }, 81 | leadingIcon = Icons.Rounded.Login, 82 | onButtonClick = {} 83 | ) 84 | } 85 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/presentation/components/InputFieldComponent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.presentation.components 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.interaction.collectIsFocusedAsState 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.fillMaxHeight 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.text.KeyboardOptions 14 | import androidx.compose.material.icons.Icons 15 | import androidx.compose.material.icons.rounded.Email 16 | import androidx.compose.material3.ExperimentalMaterial3Api 17 | import androidx.compose.material3.Icon 18 | import androidx.compose.material3.IconButton 19 | import androidx.compose.material3.MaterialTheme 20 | import androidx.compose.material3.Text 21 | import androidx.compose.material3.TextField 22 | import androidx.compose.material3.TextFieldDefaults 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.remember 25 | import androidx.compose.ui.Alignment 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.graphics.Color 28 | import androidx.compose.ui.graphics.vector.ImageVector 29 | import androidx.compose.ui.res.stringResource 30 | import androidx.compose.ui.text.font.FontWeight 31 | import androidx.compose.ui.text.input.KeyboardType 32 | import androidx.compose.ui.text.input.VisualTransformation 33 | import androidx.compose.ui.tooling.preview.Preview 34 | import androidx.compose.ui.unit.dp 35 | import androidx.compose.ui.unit.sp 36 | import com.example.noteswithrestapi.R 37 | import com.example.noteswithrestapi.core.presentation.theme.ui.theme.NotesWithRESTAPITheme 38 | 39 | 40 | val inputFieldDefaultModifier = Modifier 41 | .padding(start = 25.dp, end = 25.dp) 42 | 43 | 44 | @OptIn(ExperimentalMaterial3Api::class) 45 | @Composable 46 | fun InputFieldComponent( 47 | modifier: Modifier = Modifier, 48 | textValue: () -> String, 49 | onValueChanged: (String) -> Unit, 50 | error: () -> String?, 51 | label: String, 52 | leadingIcon: ImageVector, 53 | trailingIcon: ImageVector? = null, 54 | onTrailingIconClicked: (() -> Unit)? = null, 55 | keyboardType: KeyboardType = KeyboardType.Ascii, 56 | visualTransformation: VisualTransformation = VisualTransformation.None, 57 | textColor: Color = MaterialTheme.colorScheme.primary, 58 | labelColor: Color = MaterialTheme.colorScheme.onBackground, 59 | selectedBackgroundColor: Color = MaterialTheme.colorScheme.secondary, 60 | unselectedBackgroundColor: Color = Color.Transparent, 61 | errorColor: Color = MaterialTheme.colorScheme.error, 62 | enabled: () -> Boolean = { true }, 63 | ) { 64 | 65 | val textFieldInteractionSource = remember { MutableInteractionSource() } 66 | 67 | val isFocused = textFieldInteractionSource.collectIsFocusedAsState() 68 | 69 | 70 | Column( 71 | modifier = modifier 72 | ) { 73 | AnimatedVisibility(visible = error() != null) { 74 | error()?.let { error -> 75 | Text( 76 | text = error, 77 | color = errorColor, 78 | style = MaterialTheme.typography.bodySmall.copy( 79 | fontSize = 12.sp, 80 | fontWeight = FontWeight.Medium 81 | ), 82 | modifier = Modifier.padding(start = 5.dp) 83 | ) 84 | } 85 | } 86 | TextField( 87 | modifier = Modifier 88 | .fillMaxWidth() 89 | .padding(top = 3.dp) 90 | .height(60.dp), 91 | value = textValue(), 92 | onValueChange = onValueChanged, 93 | textStyle = MaterialTheme.typography.bodyMedium.copy(color = textColor), 94 | visualTransformation = visualTransformation, 95 | keyboardOptions = KeyboardOptions(keyboardType = keyboardType), 96 | shape = MaterialTheme.shapes.medium, 97 | colors = TextFieldDefaults.textFieldColors( 98 | cursorColor = textColor, 99 | focusedIndicatorColor = Color.Transparent, 100 | unfocusedIndicatorColor = Color.Transparent, 101 | containerColor = if (isFocused.value) selectedBackgroundColor else unselectedBackgroundColor, 102 | disabledIndicatorColor = Color.Transparent, 103 | ), 104 | interactionSource = textFieldInteractionSource, 105 | maxLines = 1, 106 | enabled = enabled(), 107 | label = { 108 | Box(modifier = Modifier.fillMaxHeight(0.55f)) { 109 | Text( 110 | text = label, 111 | color = labelColor, 112 | style = MaterialTheme.typography.bodyMedium.copy( 113 | fontWeight = FontWeight.Medium, 114 | fontSize = if (isFocused.value) 12.sp else 14.sp, 115 | color = MaterialTheme.colorScheme.onBackground 116 | ), 117 | modifier = Modifier.align(Alignment.CenterStart) 118 | ) 119 | } 120 | 121 | }, 122 | leadingIcon = { 123 | Icon( 124 | imageVector = leadingIcon, 125 | contentDescription = stringResource(R.string.desc_leading_icon), 126 | modifier = Modifier.size(32.dp), 127 | tint = textColor 128 | ) 129 | }, 130 | trailingIcon = { 131 | IconButton( 132 | onClick = { 133 | onTrailingIconClicked?.invoke() 134 | } 135 | ) { 136 | if (trailingIcon != null) { 137 | Icon( 138 | imageVector = trailingIcon, 139 | contentDescription = stringResource(R.string.desc_leading_icon), 140 | modifier = Modifier.size(27.dp), 141 | tint = textColor 142 | ) 143 | } 144 | } 145 | } 146 | ) 147 | } 148 | } 149 | 150 | 151 | @Preview 152 | @Composable 153 | fun InputFieldComponentPreview() { 154 | NotesWithRESTAPITheme(darkTheme = true) { 155 | InputFieldComponent( 156 | textValue = { "" }, 157 | onValueChanged = {}, 158 | label = "EMAIL", 159 | error = { "Maga shooher" }, 160 | leadingIcon = Icons.Rounded.Email 161 | ) 162 | } 163 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/presentation/components/LoadingFailedComponent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.presentation.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.rounded.Replay 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.IconButton 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.tooling.preview.Preview 17 | import androidx.compose.ui.unit.dp 18 | import com.example.noteswithrestapi.R 19 | import com.example.noteswithrestapi.core.presentation.theme.ui.theme.NotesWithRESTAPITheme 20 | 21 | 22 | @Composable 23 | fun LoadingFailedComponent( 24 | errorMessage: String, 25 | onRetryClick: (() -> Unit)?, 26 | modifier: Modifier = Modifier, 27 | ) { 28 | 29 | Column( 30 | modifier = modifier, 31 | horizontalAlignment = Alignment.CenterHorizontally 32 | ) { 33 | Text( 34 | text = errorMessage, 35 | style = MaterialTheme.typography.bodyLarge, 36 | color = MaterialTheme.colorScheme.error 37 | ) 38 | if (onRetryClick != null) { 39 | Text( 40 | text = stringResource(R.string.text_try_again), 41 | style = MaterialTheme.typography.bodyLarge, 42 | color = MaterialTheme.colorScheme.primary 43 | ) 44 | IconButton( 45 | onClick = { 46 | onRetryClick() 47 | }, 48 | ) { 49 | Icon( 50 | imageVector = Icons.Rounded.Replay, 51 | contentDescription = stringResource(R.string.desc_retry_icon), 52 | tint = MaterialTheme.colorScheme.primary, 53 | modifier = Modifier.size(30.dp) 54 | ) 55 | } 56 | } 57 | } 58 | 59 | } 60 | 61 | 62 | @Preview 63 | @Composable 64 | fun LoadingFailedComponentPreview() { 65 | NotesWithRESTAPITheme { 66 | LoadingFailedComponent( 67 | errorMessage = "Failed to load notes", 68 | onRetryClick = { }, 69 | modifier = Modifier.fillMaxWidth() 70 | ) 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/presentation/components/OutlinedButtonComponent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.presentation.components 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.rounded.Login 12 | import androidx.compose.material3.ButtonDefaults 13 | import androidx.compose.material3.Icon 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.OutlinedButton 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.graphics.Color 21 | import androidx.compose.ui.graphics.Shape 22 | import androidx.compose.ui.graphics.vector.ImageVector 23 | import androidx.compose.ui.text.font.FontWeight 24 | import androidx.compose.ui.tooling.preview.Preview 25 | import androidx.compose.ui.unit.dp 26 | import com.example.noteswithrestapi.core.presentation.theme.ui.theme.NotesWithRESTAPITheme 27 | 28 | @Composable 29 | fun OutlinedButtonComponent( 30 | modifier: Modifier = Modifier, 31 | text: String, 32 | leadingIcon: ImageVector? = null, 33 | borderColor: Color = MaterialTheme.colorScheme.tertiary, 34 | contentColor: Color = MaterialTheme.colorScheme.tertiary, 35 | contentPadding: PaddingValues = PaddingValues(start = 32.dp, end = 40.dp, top = 14.dp, bottom = 14.dp), 36 | enabled: () -> Boolean, 37 | shape: Shape = MaterialTheme.shapes.large, 38 | onButtonClick: () -> Unit, 39 | ) { 40 | 41 | OutlinedButton( 42 | modifier = modifier, 43 | colors = ButtonDefaults.outlinedButtonColors( 44 | contentColor = contentColor 45 | ), 46 | border = BorderStroke(2.dp, borderColor), 47 | shape = shape, 48 | enabled = enabled(), 49 | contentPadding = contentPadding, 50 | onClick = { 51 | onButtonClick() 52 | }, 53 | ) { 54 | Row( 55 | modifier = Modifier, 56 | horizontalArrangement = Arrangement.Center, 57 | verticalAlignment = Alignment.CenterVertically 58 | ) { 59 | if (leadingIcon != null) { 60 | Icon( 61 | imageVector = leadingIcon, 62 | contentDescription = text, 63 | modifier = Modifier.size(18.dp), 64 | ) 65 | Spacer(modifier = Modifier.width(9.dp)) 66 | } 67 | Text( 68 | text = text, 69 | style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold) 70 | ) 71 | } 72 | } 73 | } 74 | 75 | @Preview(showBackground = false) 76 | @Composable 77 | fun OutlinedButtonComponentPreview() { 78 | NotesWithRESTAPITheme(darkTheme = false) { 79 | OutlinedButtonComponent( 80 | text = "Login", 81 | enabled = { true }, 82 | leadingIcon = Icons.Rounded.Login, 83 | onButtonClick = {} 84 | ) 85 | } 86 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/presentation/components/ProgressIndicatorComponent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.presentation.components 2 | 3 | import androidx.compose.material3.CircularProgressIndicator 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.Color 8 | 9 | 10 | @Composable 11 | fun ProgressIndicatorComponent( 12 | isLoading: () -> Boolean, 13 | modifier: Modifier = Modifier, 14 | color: Color = MaterialTheme.colorScheme.primary, 15 | ) { 16 | 17 | if (isLoading()) { 18 | CircularProgressIndicator( 19 | modifier = modifier, 20 | color = color 21 | ) 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/presentation/components/pull_refresh/PullRefresh.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.presentation.components.pull_refresh 2 | 3 | import androidx.compose.ui.Modifier 4 | import androidx.compose.ui.geometry.Offset 5 | import androidx.compose.ui.input.nestedscroll.NestedScrollConnection 6 | import androidx.compose.ui.input.nestedscroll.NestedScrollSource 7 | import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.Drag 8 | import androidx.compose.ui.input.nestedscroll.nestedScroll 9 | import androidx.compose.ui.platform.debugInspectorInfo 10 | import androidx.compose.ui.platform.inspectable 11 | import androidx.compose.ui.unit.Velocity 12 | 13 | 14 | fun Modifier.pullRefresh( 15 | state: PullRefreshState, 16 | enabled: Boolean = true, 17 | ) = inspectable(inspectorInfo = debugInspectorInfo { 18 | name = "pullRefresh" 19 | properties["state"] = state 20 | properties["enabled"] = enabled 21 | }) { 22 | Modifier.pullRefresh(state::onPull, state::onRelease, enabled) 23 | } 24 | 25 | 26 | fun Modifier.pullRefresh( 27 | onPull: (pullDelta: Float) -> Float, 28 | onRelease: suspend (flingVelocity: Float) -> Float, 29 | enabled: Boolean = true, 30 | ) = inspectable(inspectorInfo = debugInspectorInfo { 31 | name = "pullRefresh" 32 | properties["onPull"] = onPull 33 | properties["onRelease"] = onRelease 34 | properties["enabled"] = enabled 35 | }) { 36 | Modifier.nestedScroll(PullRefreshNestedScrollConnection(onPull, onRelease, enabled)) 37 | } 38 | 39 | private class PullRefreshNestedScrollConnection( 40 | private val onPull: (pullDelta: Float) -> Float, 41 | private val onRelease: suspend (flingVelocity: Float) -> Float, 42 | private val enabled: Boolean, 43 | ) : NestedScrollConnection { 44 | 45 | override fun onPreScroll( 46 | available: Offset, 47 | source: NestedScrollSource, 48 | ): Offset = when { 49 | !enabled -> Offset.Zero 50 | source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up 51 | else -> Offset.Zero 52 | } 53 | 54 | override fun onPostScroll( 55 | consumed: Offset, 56 | available: Offset, 57 | source: NestedScrollSource, 58 | ): Offset = when { 59 | !enabled -> Offset.Zero 60 | source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down 61 | else -> Offset.Zero 62 | } 63 | 64 | override suspend fun onPreFling(available: Velocity): Velocity { 65 | return Velocity(0f, onRelease(available.y)) 66 | } 67 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/presentation/components/pull_refresh/PullRefreshIndicatorTransform.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.presentation.components.pull_refresh 2 | 3 | import androidx.compose.animation.core.LinearOutSlowInEasing 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.draw.drawWithContent 6 | import androidx.compose.ui.graphics.drawscope.clipRect 7 | import androidx.compose.ui.graphics.graphicsLayer 8 | import androidx.compose.ui.platform.debugInspectorInfo 9 | import androidx.compose.ui.platform.inspectable 10 | 11 | 12 | fun Modifier.pullRefreshIndicatorTransform( 13 | state: PullRefreshState, 14 | scale: Boolean = false, 15 | ) = inspectable(inspectorInfo = debugInspectorInfo { 16 | name = "pullRefreshIndicatorTransform" 17 | properties["state"] = state 18 | properties["scale"] = scale 19 | }) { 20 | Modifier 21 | .drawWithContent { 22 | clipRect( 23 | top = 0f, 24 | left = -Float.MAX_VALUE, 25 | right = Float.MAX_VALUE, 26 | bottom = Float.MAX_VALUE 27 | ) { 28 | this@drawWithContent.drawContent() 29 | } 30 | } 31 | .graphicsLayer { 32 | translationY = state.position - size.height 33 | 34 | if (scale && !state.refreshing) { 35 | val scaleFraction = LinearOutSlowInEasing 36 | .transform(state.position / state.threshold) 37 | .coerceIn(0f, 1f) 38 | scaleX = scaleFraction 39 | scaleY = scaleFraction 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/presentation/navigation/Graph.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.presentation.navigation 2 | 3 | object Graph { 4 | 5 | const val ROOT = "root_graph" 6 | 7 | const val AUTH_GRAPH = "auth_graph" 8 | 9 | const val NOTE_GRAPH = "note_graph" 10 | 11 | const val PROFILE_GRAPH = "profile_graph" 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/presentation/navigation/NavigationItem.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.presentation.navigation 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.rounded.AccountCircle 6 | import androidx.compose.material.icons.rounded.Notes 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import com.example.noteswithrestapi.R 9 | 10 | sealed class NavigationItem( 11 | @StringRes val nameId: Int, 12 | @StringRes val descId: Int, 13 | val route: String, 14 | val icon: ImageVector, 15 | ) { 16 | 17 | object NotesFeature: NavigationItem( 18 | nameId = R.string.title_notes, 19 | descId = R.string.desc_notes_screen, 20 | route = Graph.NOTE_GRAPH, 21 | icon = Icons.Rounded.Notes 22 | ) 23 | 24 | object ProfileFeature: NavigationItem( 25 | nameId = R.string.title_profile, 26 | descId = R.string.desc_profile_screen, 27 | route = Graph.PROFILE_GRAPH, 28 | icon = Icons.Rounded.AccountCircle 29 | ) 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/presentation/navigation/RootNavGraph.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.presentation.navigation 2 | 3 | import androidx.compose.animation.ExperimentalAnimationApi 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.navigation.NavHostController 7 | import com.example.noteswithrestapi.authentication_feature.presentation.authGraph 8 | import com.example.noteswithrestapi.note_feature.presentation.noteGraph 9 | import com.example.noteswithrestapi.profile_feature.presentation.profileGraph 10 | import com.google.accompanist.navigation.animation.AnimatedNavHost 11 | 12 | 13 | @OptIn(ExperimentalAnimationApi::class) 14 | @Composable 15 | fun RootNavGraph( 16 | modifier: Modifier = Modifier, 17 | startGraphRoute: String = Graph.AUTH_GRAPH, 18 | navController: NavHostController, 19 | onShowNavigationDrawer: () -> Unit, 20 | ) { 21 | 22 | AnimatedNavHost( 23 | modifier = modifier, 24 | navController = navController, 25 | route = Graph.ROOT, 26 | startDestination = startGraphRoute, 27 | ) { 28 | authGraph(navController) 29 | noteGraph( 30 | navController = navController, 31 | onShowNavigationDrawer = onShowNavigationDrawer 32 | ) 33 | profileGraph( 34 | navController = navController, 35 | onShowNavigationDrawer = onShowNavigationDrawer 36 | ) 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/presentation/navigation/Screen.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.presentation.navigation 2 | 3 | sealed class Screen(protected val route: String, vararg params: String) { 4 | 5 | val fullRoute: String = if (params.isEmpty()) route else { 6 | val builder = StringBuilder(route) 7 | params.forEach { builder.append("/{${it}}") } 8 | builder.toString() 9 | } 10 | 11 | 12 | //Authentication feature 13 | object Login: Screen(LOGIN_SCREEN_ROUTE) 14 | 15 | object Register: Screen(REGISTER_SCREEN_ROUTE) 16 | 17 | object ResetPassword: Screen(RESET_PASSWORD_SCREEN_ROUTE) 18 | 19 | object ConfirmPasswordReset: Screen(CONFIRM_PASSWORD_RESET_SCREEN_ROUTE, EMAIL_ARG) { 20 | fun getRouteWithArgs(email: String) = route.appendParams( 21 | EMAIL_ARG to email 22 | ) 23 | } 24 | 25 | object VerifyEmail: Screen(VERIFY_EMAIL_SCREEN_ROUTE, EMAIL_ARG) { 26 | fun getRouteWithArgs(email: String) = route.appendParams( 27 | EMAIL_ARG to email 28 | ) 29 | } 30 | 31 | 32 | //Note feature 33 | object Notes: Screen(NOTES_SCREEN_ROUTE) 34 | 35 | object AddNote: Screen(ADD_NOTE_SCREEN_ROUTE) 36 | 37 | object EditNote: Screen(EDIT_NOTE_SCREEN_ROUTE, NOTE_ID_ARG) { 38 | fun getRouteWithArgs(noteId: Int) = route.appendParams( 39 | NOTE_ID_ARG to noteId 40 | ) 41 | } 42 | 43 | object SearchNote: Screen(SEARCH_NOTE_SCREEN_ROUTE) 44 | 45 | 46 | //Profile feature 47 | object Profile: Screen(PROFILE_SCREEN_ROUTE) 48 | 49 | 50 | companion object { 51 | 52 | //Authentication feature 53 | private const val LOGIN_SCREEN_ROUTE = "login" 54 | private const val REGISTER_SCREEN_ROUTE = "register" 55 | private const val RESET_PASSWORD_SCREEN_ROUTE = "reset_password" 56 | private const val CONFIRM_PASSWORD_RESET_SCREEN_ROUTE = "confirm_password_reset" 57 | private const val VERIFY_EMAIL_SCREEN_ROUTE = "confirm_email" 58 | 59 | const val EMAIL_ARG = "email_arg" 60 | 61 | //Note feature 62 | private const val NOTES_SCREEN_ROUTE = "notes" 63 | private const val EDIT_NOTE_SCREEN_ROUTE = "edit_notes" 64 | private const val ADD_NOTE_SCREEN_ROUTE = "add_note" 65 | private const val SEARCH_NOTE_SCREEN_ROUTE = "search_note" 66 | private const val SEARCH_NOTE_RESULT_SCREEN_ROUTE = "search_note_result" 67 | 68 | const val NOTE_ID_ARG = "note_id_arg" 69 | 70 | 71 | //Profile feature 72 | private const val PROFILE_SCREEN_ROUTE = "profile" 73 | 74 | } 75 | 76 | 77 | internal fun String.appendParams(vararg params: Pair): String { 78 | val builder = StringBuilder(this) 79 | 80 | params.forEach { 81 | it.second?.toString()?.let { arg -> 82 | builder.append("/$arg") 83 | } 84 | } 85 | 86 | return builder.toString() 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/presentation/theme/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.presentation.theme.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | 6 | val primary_dark = Color(0XFFC8C6CC) 7 | val surface_dark = Color(0XFF211F26) 8 | val onSurface_dark = Color(0XFFC8C6CC) 9 | val background_dark = Color(0XFF141218) 10 | val onBackground_dark = Color(0XFF47454D) 11 | val secondary_dark = Color(0XFF282438) 12 | val onSecondary_dark = Color(0XFFC8C6CC) 13 | val tertiary_dark = Color(0XFFAEA7CB) 14 | 15 | val primary_light = Color(0XFF353040) 16 | val surface_light = Color(0XFFD7D5DE) 17 | val onSurface_light = Color(0XFF353040) 18 | val background_light = Color(0XFFFAF7FF) 19 | val onBackground_light = Color(0XFF8C8C8C) 20 | val secondary_light = Color(0XFFAEA7CB) 21 | val onSecondary_light = Color(0XFF353040) 22 | val tertiary_light = Color(0XFF282438) 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/presentation/theme/ui/theme/Shapes.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.presentation.theme.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material3.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | large = RoundedCornerShape(30.dp), 9 | medium = RoundedCornerShape(20.dp), 10 | small = RoundedCornerShape(15.dp) 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/presentation/theme/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.presentation.theme.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.darkColorScheme 6 | import androidx.compose.material3.lightColorScheme 7 | import androidx.compose.runtime.Composable 8 | 9 | private val DarkColorScheme = darkColorScheme( 10 | primary = primary_dark, 11 | secondary = secondary_dark, 12 | onSecondary = onSecondary_dark, 13 | tertiary = tertiary_dark, 14 | surface = surface_dark, 15 | onSurface = onSurface_dark, 16 | background = background_dark, 17 | onBackground = onBackground_dark, 18 | ) 19 | 20 | private val LightColorScheme = lightColorScheme( 21 | primary = primary_light, 22 | secondary = secondary_light, 23 | onSecondary = onSecondary_light, 24 | tertiary = tertiary_light, 25 | surface = surface_light, 26 | onSurface = onSurface_light, 27 | background = background_light, 28 | onBackground = onBackground_light, 29 | ) 30 | 31 | @Composable 32 | fun NotesWithRESTAPITheme( 33 | darkTheme: Boolean = isSystemInDarkTheme(), 34 | content: @Composable () -> Unit 35 | ) { 36 | val colorScheme = when { 37 | darkTheme -> DarkColorScheme 38 | else -> LightColorScheme 39 | } 40 | 41 | MaterialTheme( 42 | colorScheme = colorScheme, 43 | typography = Typography, 44 | content = content, 45 | shapes = Shapes 46 | ) 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/presentation/theme/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.presentation.theme.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.Font 6 | import androidx.compose.ui.text.font.FontFamily 7 | import androidx.compose.ui.text.font.FontWeight 8 | import androidx.compose.ui.unit.sp 9 | import com.example.noteswithrestapi.R 10 | 11 | // Set of Material typography styles to start with 12 | val ud_digi_family = FontFamily(listOf(Font(R.font.ud_digi_kyokasho_nk_b, weight = FontWeight.Bold))) 13 | 14 | val roboto_family = FontFamily(listOf( 15 | Font(R.font.roboto_regular, weight = FontWeight.Normal), 16 | Font(R.font.roboto_bold, weight = FontWeight.Bold), 17 | Font(R.font.roboto_medium, weight = FontWeight.Medium), 18 | Font(R.font.roboto_light, weight = FontWeight.Light) 19 | )) 20 | 21 | val Typography = Typography( 22 | headlineMedium = TextStyle( 23 | fontFamily = ud_digi_family, 24 | fontWeight = FontWeight.Bold, 25 | lineHeight = 28.sp, 26 | fontSize = 28.sp 27 | ), 28 | titleLarge = TextStyle( 29 | fontFamily = ud_digi_family, 30 | fontWeight = FontWeight.Bold, 31 | letterSpacing = 0.5.sp, 32 | lineHeight = 28.sp, 33 | fontSize = 22.sp 34 | ), 35 | titleMedium = TextStyle( 36 | fontFamily = ud_digi_family, 37 | fontWeight = FontWeight.Bold, 38 | letterSpacing = 0.5.sp, 39 | lineHeight = 28.sp, 40 | fontSize = 20.sp 41 | ), 42 | bodyLarge = TextStyle( 43 | fontFamily = roboto_family, 44 | fontWeight = FontWeight.Normal, 45 | fontSize = 18.sp, 46 | lineHeight = 28.sp, 47 | letterSpacing = 0.5.sp 48 | ), 49 | bodyMedium = TextStyle( 50 | fontFamily = roboto_family, 51 | fontWeight = FontWeight.Normal, 52 | fontSize = 16.sp, 53 | lineHeight = 28.sp, 54 | letterSpacing = 0.4.sp 55 | ), 56 | bodySmall = TextStyle( 57 | fontFamily = roboto_family, 58 | fontWeight = FontWeight.Normal, 59 | fontSize = 14.sp, 60 | lineHeight = 28.sp, 61 | ) 62 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/utils/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.utils 2 | 3 | object Constants { 4 | 5 | const val UNKNOWN_ID = -1 6 | 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/core/utils/TimeUtil.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.core.utils 2 | 3 | import java.time.LocalDateTime 4 | 5 | 6 | fun formatStringDate(stringDate: String): String { 7 | val localDate = LocalDateTime.parse(stringDate) 8 | return localDate.toString() 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/data/mapper/NoteMapper.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.data.mapper 2 | 3 | import com.example.noteswithrestapi.note_feature.data.remote.dto.NoteDto 4 | import com.example.noteswithrestapi.note_feature.domain.model.Note 5 | 6 | 7 | fun NoteDto.toNote(): Note = Note( 8 | id = this.id, 9 | title = this.title, 10 | content = this.content, 11 | date_updated = this.date_updated, 12 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/data/remote/NoteApiService.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.data.remote 2 | 3 | import com.example.noteswithrestapi.note_feature.data.remote.dto.GetNotesResultDto 4 | import com.example.noteswithrestapi.note_feature.data.remote.dto.NoteDto 5 | import com.example.noteswithrestapi.note_feature.domain.model.Note 6 | import retrofit2.Response 7 | import retrofit2.http.Body 8 | import retrofit2.http.DELETE 9 | import retrofit2.http.GET 10 | import retrofit2.http.Header 11 | import retrofit2.http.POST 12 | import retrofit2.http.PUT 13 | import retrofit2.http.Path 14 | import retrofit2.http.Query 15 | 16 | interface NoteApiService { 17 | 18 | @GET("api/v1/list-note/") 19 | suspend fun getNotes( 20 | @Header("Authorization") token: String, 21 | @Query("search") query: String = "", 22 | @Query("page") page: Int, 23 | @Query("page_size") pageSize: Int = 20, 24 | ): Response 25 | 26 | 27 | @GET("api/v1/get-note/{pk}") 28 | suspend fun getNote( 29 | @Header("Authorization") token: String, 30 | @Path("pk") id: Int, 31 | ): Response 32 | 33 | 34 | @POST("api/v1/add-note/") 35 | suspend fun addNote( 36 | @Header("Authorization") token: String, 37 | @Body note: Note 38 | ): Response 39 | 40 | 41 | @PUT("api/v1/update-note/{pk}") 42 | suspend fun updateNote( 43 | @Header("Authorization") token: String, 44 | @Path("pk") id: Int, 45 | @Body note: Note 46 | ): Response 47 | 48 | 49 | @DELETE("api/v1/delete-note/{pk}") 50 | suspend fun deleteNote( 51 | @Header("Authorization") token: String, 52 | @Path("pk") id: Int, 53 | ): Response 54 | 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/data/remote/dto/GetNotesResultDto.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.data.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class GetNotesResultDto( 6 | val count: Int, 7 | val next: String, 8 | val previous: Any, 9 | @SerializedName("results") val noteDtos: List 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/data/remote/dto/NoteDto.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.data.remote.dto 2 | 3 | data class NoteDto( 4 | val id: Int, 5 | val user_id: Int, 6 | val title: String, 7 | val content: String, 8 | val date_updated: String, 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/data/repository/NoteRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.data.repository 2 | 3 | import com.example.noteswithrestapi.R 4 | import com.example.noteswithrestapi.core.data.network.getAppErrorByHttpErrorCode 5 | import com.example.noteswithrestapi.core.data.token.TokenManager 6 | import com.example.noteswithrestapi.core.domain.model.AppError 7 | import com.example.noteswithrestapi.core.domain.model.AppResponse 8 | import com.example.noteswithrestapi.note_feature.data.mapper.toNote 9 | import com.example.noteswithrestapi.note_feature.data.remote.NoteApiService 10 | import com.example.noteswithrestapi.note_feature.domain.model.Note 11 | import com.example.noteswithrestapi.note_feature.domain.repository.NoteRepository 12 | import retrofit2.HttpException 13 | import java.io.IOException 14 | import javax.inject.Inject 15 | 16 | class NoteRepositoryImpl @Inject constructor( 17 | private val noteApiService: NoteApiService, 18 | private val tokenManager: TokenManager, 19 | ) : NoteRepository { 20 | 21 | override suspend fun addNote(note: Note): AppResponse { 22 | try { 23 | val token = tokenManager.authToken 24 | ?: return AppResponse.failed(AppError.GeneralError(R.string.error_get_notes)) 25 | 26 | val result = noteApiService.addNote("Token $token", note) 27 | 28 | if (result.isSuccessful) { 29 | val noteResult = result.body()?.toNote() 30 | ?: return AppResponse.failed(AppError.GeneralError(R.string.error_get_notes)) 31 | return AppResponse.success(noteResult) 32 | } 33 | 34 | val error = result.getAppErrorByHttpErrorCode() 35 | return AppResponse.failed(error) 36 | 37 | } catch (e: IOException) { 38 | return AppResponse.failed(AppError.PoorNetworkConnectionError) 39 | } catch (e: HttpException) { 40 | return AppResponse.failed(AppError.GeneralError(R.string.error_get_notes)) 41 | } 42 | } 43 | 44 | 45 | override suspend fun deleteNote(noteId: Int): AppResponse { 46 | try { 47 | 48 | val token = tokenManager.authToken 49 | ?: return AppResponse.failed(AppError.GeneralError(R.string.error_delete_note)) 50 | 51 | val result = noteApiService.deleteNote("Token $token", noteId) 52 | 53 | if (result.isSuccessful) { 54 | return AppResponse.success(Unit) 55 | } 56 | 57 | val error = result.getAppErrorByHttpErrorCode() 58 | return AppResponse.failed(error) 59 | 60 | } catch (e: IOException) { 61 | return AppResponse.failed(AppError.PoorNetworkConnectionError) 62 | } catch (e: HttpException) { 63 | return AppResponse.failed(AppError.GeneralError(R.string.error_delete_note)) 64 | } 65 | } 66 | 67 | 68 | override suspend fun updateNote(note: Note): AppResponse { 69 | try { 70 | 71 | val token = tokenManager.authToken 72 | ?: return AppResponse.failed(AppError.GeneralError(R.string.error_delete_note)) 73 | 74 | val result = noteApiService.updateNote("Token $token", note.id, note) 75 | 76 | if (result.isSuccessful) { 77 | val noteModel = result.body()?.toNote() 78 | ?: return AppResponse.failed(AppError.GeneralError(R.string.error_delete_note)) 79 | return AppResponse.success(noteModel) 80 | } 81 | 82 | val error = result.getAppErrorByHttpErrorCode() 83 | return AppResponse.failed(error) 84 | 85 | } catch (e: IOException) { 86 | return AppResponse.failed(AppError.PoorNetworkConnectionError) 87 | } catch (e: HttpException) { 88 | return AppResponse.failed(AppError.GeneralError(R.string.error_delete_note)) 89 | } 90 | } 91 | 92 | 93 | override suspend fun getNotes(page: Int, query: String): AppResponse> { 94 | try { 95 | 96 | val token = tokenManager.authToken 97 | ?: return AppResponse.failed(AppError.GeneralError(R.string.error_get_notes)) 98 | 99 | val result = noteApiService.getNotes( 100 | token = "Token $token", 101 | page = page, 102 | query = query, 103 | ) 104 | 105 | if (result.isSuccessful) { 106 | val noteDtos = result.body()?.noteDtos ?: return AppResponse.success(emptyList()) 107 | val notes = noteDtos.map { dto -> dto.toNote() } 108 | return AppResponse.success(notes) 109 | } 110 | 111 | val error = result.getAppErrorByHttpErrorCode() 112 | return AppResponse.failed(error) 113 | 114 | } catch (e: IOException) { 115 | return AppResponse.failed(AppError.PoorNetworkConnectionError) 116 | } catch (e: HttpException) { 117 | return AppResponse.failed(AppError.GeneralError(R.string.error_get_notes)) 118 | } 119 | } 120 | 121 | 122 | override suspend fun getNote(id: Int): AppResponse { 123 | try { 124 | 125 | val token = tokenManager.authToken 126 | ?: return AppResponse.failed(AppError.GeneralError(R.string.error_get_notes)) 127 | 128 | val result = noteApiService.getNote(token = "Token $token", id = id) 129 | 130 | if (result.isSuccessful) { 131 | val note = result.body()?.toNote() 132 | ?: return AppResponse.failed(AppError.GeneralError(R.string.error_get_note)) 133 | return AppResponse.success(note) 134 | } 135 | 136 | val error = result.getAppErrorByHttpErrorCode() 137 | return AppResponse.failed(error) 138 | 139 | } catch (e: IOException) { 140 | return AppResponse.failed(AppError.PoorNetworkConnectionError) 141 | } catch (e: HttpException) { 142 | return AppResponse.failed(AppError.GeneralError(R.string.error_get_note)) 143 | } 144 | } 145 | 146 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/di/NoteModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.di 2 | 3 | import com.example.noteswithrestapi.note_feature.data.remote.NoteApiService 4 | import com.example.noteswithrestapi.note_feature.data.repository.NoteRepositoryImpl 5 | import com.example.noteswithrestapi.note_feature.domain.repository.NoteRepository 6 | import dagger.Binds 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.components.SingletonComponent 11 | import retrofit2.Retrofit 12 | import javax.inject.Singleton 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | interface NoteModule { 17 | 18 | @Binds 19 | @Singleton 20 | fun bindNoteRepository( 21 | noteRepository: NoteRepositoryImpl, 22 | ): NoteRepository 23 | 24 | 25 | companion object { 26 | 27 | @Provides 28 | @Singleton 29 | fun provideNoteApiService( 30 | retrofit: Retrofit, 31 | ): NoteApiService { 32 | return retrofit.create(NoteApiService::class.java) 33 | } 34 | 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/domain/error/NoteError.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.domain.error 2 | 3 | import com.example.noteswithrestapi.core.domain.model.AppError 4 | 5 | open class NoteError: AppError() { 6 | 7 | object EmptyTitleError: NoteError() 8 | 9 | object EmptyContentError: NoteError() 10 | 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/domain/model/Note.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.domain.model 2 | 3 | import com.example.noteswithrestapi.core.utils.Constants 4 | 5 | data class Note( 6 | val id: Int = Constants.UNKNOWN_ID, 7 | val title: String, 8 | val content: String, 9 | val date_updated: String = "", 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/domain/repository/NoteRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.domain.repository 2 | 3 | import com.example.noteswithrestapi.core.domain.model.AppResponse 4 | import com.example.noteswithrestapi.note_feature.domain.model.Note 5 | 6 | interface NoteRepository { 7 | 8 | suspend fun addNote(note: Note): AppResponse 9 | 10 | suspend fun deleteNote(noteId: Int): AppResponse 11 | 12 | suspend fun updateNote(note: Note): AppResponse 13 | 14 | suspend fun getNotes(page: Int, query: String): AppResponse> 15 | 16 | suspend fun getNote(id: Int): AppResponse 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/domain/usecase/AddNoteUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.domain.usecase 2 | 3 | import com.example.noteswithrestapi.core.domain.model.AppResponse 4 | import com.example.noteswithrestapi.note_feature.domain.error.NoteError 5 | import com.example.noteswithrestapi.note_feature.domain.model.Note 6 | import com.example.noteswithrestapi.note_feature.domain.repository.NoteRepository 7 | import javax.inject.Inject 8 | 9 | class AddNoteUseCase @Inject constructor( 10 | private val noteRepository: NoteRepository 11 | ) { 12 | 13 | suspend operator fun invoke(title: String, content: String): AppResponse { 14 | if (title.isEmpty()) { 15 | return AppResponse.failed(NoteError.EmptyTitleError) 16 | } 17 | if (content.isEmpty()) { 18 | return AppResponse.failed(NoteError.EmptyContentError) 19 | } 20 | 21 | val note = Note( 22 | title = title, 23 | content = content 24 | ) 25 | 26 | return noteRepository.addNote(note) 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/domain/usecase/DeleteNoteUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.domain.usecase 2 | 3 | import com.example.noteswithrestapi.core.domain.model.AppResponse 4 | import com.example.noteswithrestapi.note_feature.domain.repository.NoteRepository 5 | import javax.inject.Inject 6 | 7 | class DeleteNoteUseCase @Inject constructor( 8 | private val noteRepository: NoteRepository 9 | ) { 10 | 11 | suspend operator fun invoke(id: Int): AppResponse { 12 | return noteRepository.deleteNote(id) 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/domain/usecase/EditNoteUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.domain.usecase 2 | 3 | import com.example.noteswithrestapi.core.domain.model.AppResponse 4 | import com.example.noteswithrestapi.note_feature.domain.error.NoteError 5 | import com.example.noteswithrestapi.note_feature.domain.model.Note 6 | import com.example.noteswithrestapi.note_feature.domain.repository.NoteRepository 7 | import javax.inject.Inject 8 | 9 | class EditNoteUseCase @Inject constructor( 10 | private val noteRepository: NoteRepository 11 | ) { 12 | 13 | suspend operator fun invoke(title: String, content: String, id: Int): AppResponse { 14 | if (title.isEmpty()) { 15 | return AppResponse.failed(NoteError.EmptyTitleError) 16 | } 17 | if (content.isEmpty()) { 18 | return AppResponse.failed(NoteError.EmptyContentError) 19 | } 20 | 21 | val note = Note( 22 | id = id, 23 | title = title, 24 | content = content 25 | ) 26 | 27 | return noteRepository.updateNote(note) 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/domain/usecase/GetNoteUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.domain.usecase 2 | 3 | import com.example.noteswithrestapi.core.domain.model.AppResponse 4 | import com.example.noteswithrestapi.note_feature.domain.model.Note 5 | import com.example.noteswithrestapi.note_feature.domain.repository.NoteRepository 6 | import javax.inject.Inject 7 | 8 | class GetNoteUseCase @Inject constructor( 9 | private val noteRepository: NoteRepository 10 | ) { 11 | 12 | suspend operator fun invoke(noteId: Int): AppResponse { 13 | return noteRepository.getNote(noteId) 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/domain/usecase/GetNotesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.domain.usecase 2 | 3 | import com.example.noteswithrestapi.core.domain.model.AppResponse 4 | import com.example.noteswithrestapi.note_feature.domain.model.Note 5 | import com.example.noteswithrestapi.note_feature.domain.repository.NoteRepository 6 | import javax.inject.Inject 7 | 8 | class GetNotesUseCase @Inject constructor( 9 | private val noteRepository: NoteRepository 10 | ) { 11 | 12 | suspend operator fun invoke(page: Int, query: String = ""): AppResponse> { 13 | return noteRepository.getNotes(page, query) 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/add_note/AddNoteEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.presentation.add_note 2 | 3 | sealed class AddNoteEvent { 4 | 5 | data class OnTitleInputValueChange(val newValue: String): AddNoteEvent() 6 | 7 | data class OnContentInputValueChange(val newValue: String): AddNoteEvent() 8 | 9 | object OnSaveClick: AddNoteEvent() 10 | 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/add_note/AddNoteState.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.presentation.add_note 2 | 3 | data class AddNoteState( 4 | val titleInputValue: String = "", 5 | 6 | val contentInputValue: String = "", 7 | 8 | val isLoading: Boolean = false, 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/add_note/AddNoteUiEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.presentation.add_note 2 | 3 | sealed class AddNoteUiEvent { 4 | 5 | object OnNoteSaved: AddNoteUiEvent() 6 | 7 | data class OnShowSnackbar(val message: String): AddNoteUiEvent() 8 | 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/add_note/AddNoteViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.presentation.add_note 2 | 3 | import android.app.Application 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.viewModelScope 9 | import com.example.noteswithrestapi.R 10 | import com.example.noteswithrestapi.core.domain.model.AppError 11 | import com.example.noteswithrestapi.core.domain.model.AppResponse 12 | import com.example.noteswithrestapi.note_feature.domain.error.NoteError 13 | import com.example.noteswithrestapi.note_feature.domain.usecase.AddNoteUseCase 14 | import dagger.hilt.android.lifecycle.HiltViewModel 15 | import kotlinx.coroutines.channels.Channel 16 | import kotlinx.coroutines.flow.receiveAsFlow 17 | import kotlinx.coroutines.launch 18 | import javax.inject.Inject 19 | 20 | @HiltViewModel 21 | class AddNoteViewModel @Inject constructor( 22 | private val addNoteUseCase: AddNoteUseCase, 23 | private val application: Application, 24 | ) : ViewModel() { 25 | 26 | 27 | private val _uiEvent = Channel() 28 | val uiEvent = _uiEvent.receiveAsFlow() 29 | 30 | var state by mutableStateOf(AddNoteState()) 31 | private set 32 | 33 | 34 | fun onEvent(event: AddNoteEvent) { 35 | when (event) { 36 | is AddNoteEvent.OnContentInputValueChange -> { 37 | state = state.copy(contentInputValue = event.newValue) 38 | } 39 | 40 | is AddNoteEvent.OnTitleInputValueChange -> { 41 | state = state.copy(titleInputValue = event.newValue) 42 | } 43 | 44 | is AddNoteEvent.OnSaveClick -> { 45 | saveNote(state.titleInputValue, state.contentInputValue) 46 | } 47 | 48 | } 49 | } 50 | 51 | private fun saveNote( 52 | title: String, 53 | content: String, 54 | ) { 55 | viewModelScope.launch { 56 | state = state.copy(isLoading = true) 57 | when (val result = addNoteUseCase(title, content)) { 58 | is AppResponse.Success -> { 59 | state = state.copy(isLoading = false) 60 | _uiEvent.send(AddNoteUiEvent.OnNoteSaved) 61 | } 62 | 63 | is AppResponse.Failed -> { 64 | state = state.copy(isLoading = false) 65 | handleAppError(result.error) 66 | } 67 | } 68 | } 69 | } 70 | 71 | 72 | private fun handleAppError(error: AppError) { 73 | viewModelScope.launch { 74 | when (error) { 75 | is AppError.GeneralError -> { 76 | _uiEvent.send(AddNoteUiEvent.OnShowSnackbar(application.getString(error.messageResId))) 77 | } 78 | 79 | is AppError.PoorNetworkConnectionError -> { 80 | _uiEvent.send(AddNoteUiEvent.OnShowSnackbar(application.getString(R.string.error_poor_network_connection))) 81 | } 82 | 83 | is AppError.ServerError -> { 84 | _uiEvent.send(AddNoteUiEvent.OnShowSnackbar(application.getString(R.string.error_server))) 85 | } 86 | 87 | is NoteError -> { 88 | when (error) { 89 | is NoteError.EmptyTitleError -> { 90 | _uiEvent.send(AddNoteUiEvent.OnShowSnackbar(application.getString(R.string.error_empty_title))) 91 | } 92 | 93 | is NoteError.EmptyContentError -> { 94 | _uiEvent.send(AddNoteUiEvent.OnShowSnackbar(application.getString(R.string.error_empty_content))) 95 | } 96 | } 97 | } 98 | 99 | else -> { 100 | _uiEvent.send(AddNoteUiEvent.OnShowSnackbar(application.getString(R.string.error_unknown))) 101 | } 102 | } 103 | } 104 | 105 | } 106 | 107 | 108 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/components/NoteComponent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.presentation.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material3.Card 9 | import androidx.compose.material3.CardDefaults 10 | import androidx.compose.material3.ExperimentalMaterial3Api 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.graphics.Shape 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.text.style.TextOverflow 20 | import androidx.compose.ui.tooling.preview.Preview 21 | import androidx.compose.ui.unit.dp 22 | import com.example.noteswithrestapi.core.presentation.theme.ui.theme.NotesWithRESTAPITheme 23 | import com.example.noteswithrestapi.note_feature.domain.model.Note 24 | 25 | 26 | @OptIn(ExperimentalMaterial3Api::class) 27 | @Composable 28 | fun NoteComponent( 29 | note: Note, 30 | onClick: () -> Unit, 31 | modifier: Modifier = Modifier, 32 | shape: Shape = MaterialTheme.shapes.large, 33 | backgroundColor: Color = MaterialTheme.colorScheme.surface, 34 | contentColor: Color = MaterialTheme.colorScheme.onSurface, 35 | ) { 36 | 37 | Card( 38 | onClick = onClick, 39 | modifier = modifier, 40 | shape = shape, 41 | colors = CardDefaults.cardColors( 42 | contentColor = contentColor, 43 | containerColor = backgroundColor, 44 | ) 45 | ) { 46 | Column( 47 | modifier = Modifier 48 | .padding(start = 16.dp, end = 16.dp, top = 18.dp, bottom = 12.dp) 49 | .fillMaxWidth(), 50 | horizontalAlignment = Alignment.CenterHorizontally 51 | ) { 52 | Text( 53 | text = note.title, 54 | style = MaterialTheme.typography.titleMedium, 55 | maxLines = 1, 56 | overflow = TextOverflow.Ellipsis, 57 | modifier = Modifier.align(Alignment.CenterHorizontally) 58 | ) 59 | Spacer(modifier = Modifier.height(14.dp)) 60 | Text( 61 | text = note.content, 62 | style = MaterialTheme.typography.bodyLarge, 63 | modifier = Modifier.align(Alignment.Start), 64 | maxLines = 4, 65 | overflow = TextOverflow.Ellipsis 66 | ) 67 | Spacer(modifier = Modifier.height(8.dp)) 68 | Text( 69 | text = note.date_updated, 70 | style = MaterialTheme.typography.bodySmall, 71 | fontWeight = FontWeight.Light, 72 | modifier = Modifier.align(Alignment.End) 73 | ) 74 | Spacer(modifier = Modifier.height(5.dp)) 75 | } 76 | } 77 | 78 | } 79 | 80 | @Preview 81 | @Composable 82 | fun NoteComponentPreview() { 83 | NotesWithRESTAPITheme { 84 | NoteComponent( 85 | note = Note( 86 | 0, 87 | "Note Title", 88 | "Hi there! Just wanted to leave a quick note to say that I hope you're having a great day. Remember to take breaks when you need them and stay focused. That's all i wanted to tell you", 89 | date_updated = "April 30, 2023" 90 | ), 91 | onClick = {}, 92 | modifier = Modifier 93 | .padding(start = 16.dp, end = 16.dp) 94 | .fillMaxWidth() 95 | ) 96 | } 97 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/edit_note/EditNoteEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.presentation.edit_note 2 | 3 | sealed class EditNoteEvent { 4 | 5 | data class OnTitleInputValueChange(val newValue: String): EditNoteEvent() 6 | 7 | data class OnContentInputValueChange(val newValue: String): EditNoteEvent() 8 | 9 | object OnSaveClick: EditNoteEvent() 10 | 11 | object OnDeleteClick: EditNoteEvent() 12 | 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/edit_note/EditNoteState.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.presentation.edit_note 2 | 3 | data class EditNoteState( 4 | val titleInputValue: String = "", 5 | 6 | val contentInputValue: String = "", 7 | 8 | val isLoading: Boolean = false, 9 | val getNoteErrorMessage: String? = null, 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/edit_note/EditNoteUiEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.presentation.edit_note 2 | 3 | sealed class EditNoteUiEvent { 4 | 5 | object OnNoteSaved: EditNoteUiEvent() 6 | 7 | object OnNoteDeleted: EditNoteUiEvent() 8 | 9 | data class OnShowSnackbar(val message: String): EditNoteUiEvent() 10 | 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/notes/NotesEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.presentation.notes 2 | 3 | sealed class NotesEvent { 4 | 5 | object OnLoadNextItems: NotesEvent() 6 | 7 | object OnRefresh: NotesEvent() 8 | 9 | object OnRetry: NotesEvent() 10 | 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/notes/NotesState.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.presentation.notes 2 | 3 | import com.example.noteswithrestapi.note_feature.domain.model.Note 4 | 5 | data class NotesState( 6 | val notes: List = emptyList(), 7 | val page: Int = 1, 8 | val isLoading: Boolean = false, 9 | val endReached: Boolean = false, 10 | val errorMessage: String? = null, 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/notes/NotesViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.presentation.notes 2 | 3 | import android.app.Application 4 | import android.util.Log 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.setValue 8 | import androidx.lifecycle.ViewModel 9 | import androidx.lifecycle.viewModelScope 10 | import com.example.noteswithrestapi.R 11 | import com.example.noteswithrestapi.core.domain.model.AppError 12 | import com.example.noteswithrestapi.core.domain.paginator.DefaultPaginator 13 | import com.example.noteswithrestapi.note_feature.domain.usecase.GetNotesUseCase 14 | import dagger.hilt.android.lifecycle.HiltViewModel 15 | import kotlinx.coroutines.launch 16 | import javax.inject.Inject 17 | 18 | @HiltViewModel 19 | class NotesViewModel @Inject constructor( 20 | private val getNotesUseCase: GetNotesUseCase, 21 | private val application: Application, 22 | ) : ViewModel() { 23 | 24 | 25 | var state by mutableStateOf(NotesState()) 26 | private set 27 | 28 | 29 | private val paginator = DefaultPaginator( 30 | initialKey = state.page, 31 | onLoadUpdated = { state = state.copy(isLoading = it) }, 32 | onRequest = { nextPage -> getNotesUseCase.invoke(nextPage) }, 33 | getNextKey = { state.page + 1 }, 34 | onError = { appError -> handleAppError(appError) }, 35 | onSuccess = { items, newKey -> 36 | state = state.copy( 37 | notes = state.notes + items, 38 | page = newKey, 39 | endReached = items.isEmpty() 40 | ) 41 | } 42 | ) 43 | 44 | init { 45 | loadNextItems() 46 | } 47 | 48 | 49 | fun onEvent(event: NotesEvent) { 50 | when (event) { 51 | is NotesEvent.OnLoadNextItems -> { 52 | loadNextItems() 53 | } 54 | 55 | is NotesEvent.OnRefresh -> { 56 | refresh() 57 | } 58 | 59 | is NotesEvent.OnRetry -> { 60 | onRetryLoadNextItems() 61 | } 62 | } 63 | } 64 | 65 | private fun onRetryLoadNextItems() { 66 | viewModelScope.launch { 67 | state = state.copy(errorMessage = null) 68 | paginator.loadNextPage() 69 | } 70 | } 71 | 72 | private fun loadNextItems() { 73 | viewModelScope.launch { 74 | paginator.loadNextPage() 75 | } 76 | } 77 | 78 | private fun refresh() { 79 | viewModelScope.launch { 80 | state = state.copy(notes = emptyList(), page = 1, endReached = false, errorMessage = null) 81 | paginator.reset() 82 | paginator.loadNextPage() 83 | } 84 | } 85 | 86 | 87 | private fun handleAppError(error: AppError) { 88 | 89 | when (error) { 90 | is AppError.GeneralError -> { 91 | state = state.copy(errorMessage = application.getString(error.messageResId)) 92 | } 93 | 94 | is AppError.PoorNetworkConnectionError -> { 95 | state = state.copy(errorMessage = application.getString(R.string.error_poor_network_connection)) 96 | } 97 | 98 | is AppError.ServerError -> { 99 | state = state.copy(errorMessage = application.getString(R.string.error_unknown)) 100 | } 101 | 102 | is AppError.NotFoundError -> { 103 | state = state.copy(endReached = true) 104 | } 105 | 106 | else -> { 107 | state = state.copy( 108 | errorMessage = application.getString(R.string.error_unknown), 109 | ) 110 | Log.e("UNEXPECTED_ERROR", error.toString()) 111 | } 112 | } 113 | } 114 | 115 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/search_note/SearchNoteEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.presentation.search_note 2 | 3 | 4 | sealed class SearchNoteEvent() { 5 | 6 | data class OnSearchQueryValueChange(val newValue: String) : SearchNoteEvent() 7 | 8 | object OnClearQuery : SearchNoteEvent() 9 | 10 | object OnLoadNextItems: SearchNoteEvent() 11 | 12 | object OnRetryLoadNextItems: SearchNoteEvent() 13 | 14 | object OnRefresh: SearchNoteEvent() 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/search_note/SearchNoteState.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.presentation.search_note 2 | 3 | import com.example.noteswithrestapi.note_feature.domain.model.Note 4 | 5 | data class SearchNoteState( 6 | val searchQueryInputValue: String = "", 7 | 8 | val searchItems: List = emptyList(), 9 | 10 | val isLoadingNextItems: Boolean = false, 11 | val endReached: Boolean = false, 12 | val page: Int = 1, 13 | 14 | val errorMessage: String? = null, 15 | ) { 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/search_note/SearchNoteViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.note_feature.presentation.search_note 2 | 3 | import android.app.Application 4 | import android.util.Log 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.setValue 8 | import androidx.lifecycle.ViewModel 9 | import androidx.lifecycle.viewModelScope 10 | import com.example.noteswithrestapi.R 11 | import com.example.noteswithrestapi.core.domain.model.AppError 12 | import com.example.noteswithrestapi.core.domain.paginator.DefaultPaginator 13 | import com.example.noteswithrestapi.note_feature.domain.usecase.GetNotesUseCase 14 | import dagger.hilt.android.lifecycle.HiltViewModel 15 | import kotlinx.coroutines.Job 16 | import kotlinx.coroutines.delay 17 | import kotlinx.coroutines.launch 18 | import javax.inject.Inject 19 | 20 | @HiltViewModel 21 | class SearchNoteViewModel @Inject constructor( 22 | private val getNotesUseCase: GetNotesUseCase, 23 | private val application: Application, 24 | ) : ViewModel() { 25 | 26 | 27 | var state by mutableStateOf(SearchNoteState()) 28 | private set 29 | 30 | 31 | private val paginator = DefaultPaginator( 32 | initialKey = state.page, 33 | onLoadUpdated = { state = state.copy(isLoadingNextItems = it) }, 34 | onRequest = { nextPage -> getNotesUseCase.invoke(nextPage, query) }, 35 | getNextKey = { state.page + 1 }, 36 | onError = { appError -> handleAppError(appError) }, 37 | onSuccess = { items, newKey -> 38 | state = state.copy( 39 | searchItems = state.searchItems + items, 40 | page = newKey, 41 | endReached = items.isEmpty() 42 | ) 43 | } 44 | ) 45 | 46 | private var query: String = "" 47 | 48 | private var searchQueryJob: Job? = null 49 | 50 | 51 | fun onEvent(event: SearchNoteEvent) { 52 | when (event) { 53 | is SearchNoteEvent.OnClearQuery -> { 54 | state = state.copy(searchQueryInputValue = "") 55 | query = "" 56 | refresh() 57 | } 58 | 59 | is SearchNoteEvent.OnLoadNextItems -> { 60 | loadNextItems() 61 | } 62 | 63 | is SearchNoteEvent.OnRetryLoadNextItems -> { 64 | retryLoadNextItems() 65 | } 66 | 67 | is SearchNoteEvent.OnSearchQueryValueChange -> { 68 | state = state.copy(searchQueryInputValue = event.newValue) 69 | searchQueryJob?.cancel() 70 | searchQueryJob = viewModelScope.launch { 71 | delay(1000L) 72 | query = state.searchQueryInputValue 73 | refresh() 74 | } 75 | } 76 | 77 | is SearchNoteEvent.OnRefresh -> { 78 | refresh() 79 | } 80 | } 81 | } 82 | 83 | private fun retryLoadNextItems() { 84 | viewModelScope.launch { 85 | state = state.copy(errorMessage = null) 86 | paginator.loadNextPage() 87 | } 88 | } 89 | 90 | private fun loadNextItems() { 91 | viewModelScope.launch { 92 | paginator.loadNextPage() 93 | } 94 | } 95 | 96 | 97 | private fun refresh() { 98 | viewModelScope.launch { 99 | state = state.copy(searchItems = emptyList(), endReached = false, page = 1, errorMessage = null) 100 | paginator.reset() 101 | paginator.loadNextPage() 102 | } 103 | } 104 | 105 | 106 | private fun handleAppError(error: AppError) { 107 | 108 | when (error) { 109 | is AppError.GeneralError -> { 110 | state = state.copy(errorMessage = application.getString(error.messageResId)) 111 | } 112 | 113 | is AppError.PoorNetworkConnectionError -> { 114 | state = 115 | state.copy(errorMessage = application.getString(R.string.error_poor_network_connection)) 116 | } 117 | 118 | is AppError.ServerError -> { 119 | state = state.copy(errorMessage = application.getString(R.string.error_unknown)) 120 | } 121 | 122 | is AppError.NotFoundError -> { 123 | state = state.copy(endReached = true) 124 | } 125 | 126 | else -> { 127 | state = state.copy( 128 | errorMessage = application.getString(R.string.error_unknown), 129 | ) 130 | Log.e("UNEXPECTED_ERROR", error.toString()) 131 | } 132 | } 133 | } 134 | 135 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/profile_feature/data/mapper/UserMapper.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.profile_feature.data.mapper 2 | 3 | import com.example.noteswithrestapi.profile_feature.data.remote.dto.GetUserResultDto 4 | import com.example.noteswithrestapi.profile_feature.domain.model.user.User 5 | 6 | 7 | fun GetUserResultDto.toUser(): User = User(this.userData.email, this.userData.date_created) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/profile_feature/data/remote/ProfileApiService.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.profile_feature.data.remote 2 | 3 | import com.example.noteswithrestapi.profile_feature.data.remote.dto.GetUserResultDto 4 | import com.example.noteswithrestapi.profile_feature.data.remote.dto.LogoutResultDto 5 | import retrofit2.Response 6 | import retrofit2.http.GET 7 | import retrofit2.http.Header 8 | import retrofit2.http.POST 9 | 10 | interface ProfileApiService { 11 | 12 | @GET("accounts/login/") 13 | suspend fun getUser( 14 | @Header("Authorization") token: String, 15 | ): Response 16 | 17 | 18 | @POST("accounts/logout/") 19 | suspend fun logout( 20 | @Header("Authorization") token: String, 21 | ): Response 22 | 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/profile_feature/data/remote/dto/GetUserResultDto.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.profile_feature.data.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class GetUserResultDto( 6 | @SerializedName("data") val userData: UserData, 7 | val message: String? 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/profile_feature/data/remote/dto/LogoutResultDto.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.profile_feature.data.remote.dto 2 | 3 | data class LogoutResultDto( 4 | val message: String? 5 | ) 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/profile_feature/data/remote/dto/UserData.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.profile_feature.data.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class UserData( 6 | @SerializedName("email") val email: String, 7 | @SerializedName("date_created") val date_created: String, 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/profile_feature/data/repository/ProfileRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.profile_feature.data.repository 2 | 3 | import com.example.noteswithrestapi.R 4 | import com.example.noteswithrestapi.core.data.network.getAppErrorByHttpErrorCode 5 | import com.example.noteswithrestapi.core.data.token.TokenManager 6 | import com.example.noteswithrestapi.core.domain.model.AppError 7 | import com.example.noteswithrestapi.core.domain.model.AppResponse 8 | import com.example.noteswithrestapi.profile_feature.data.mapper.toUser 9 | import com.example.noteswithrestapi.profile_feature.data.remote.ProfileApiService 10 | import com.example.noteswithrestapi.profile_feature.domain.model.user.User 11 | import com.example.noteswithrestapi.profile_feature.domain.repository.ProfileRepository 12 | import retrofit2.HttpException 13 | import java.io.IOException 14 | import javax.inject.Inject 15 | 16 | class ProfileRepositoryImpl @Inject constructor( 17 | private val tokenManager: TokenManager, 18 | private val profileApiService: ProfileApiService, 19 | ) : ProfileRepository { 20 | 21 | 22 | //Independently on result remove token and navigate to authentication screen 23 | override suspend fun logout(): AppResponse { 24 | try { 25 | val token = tokenManager.authToken ?: return AppResponse.Success(Unit) 26 | tokenManager.removeToken() 27 | val result = profileApiService.logout("Token $token") 28 | 29 | if (result.isSuccessful) { 30 | return AppResponse.success(Unit) 31 | } 32 | 33 | val error = result.getAppErrorByHttpErrorCode() 34 | return AppResponse.failed(error) 35 | 36 | } catch (e: IOException) { 37 | return AppResponse.failed(AppError.PoorNetworkConnectionError) 38 | } catch (e: HttpException) { 39 | return AppResponse.failed(AppError.GeneralError(R.string.error_logout_failed)) 40 | } 41 | } 42 | 43 | override suspend fun getCurrentUser(): AppResponse { 44 | try { 45 | val token = tokenManager.authToken 46 | ?: return AppResponse.failed(AppError.GeneralError(R.string.error_logout_failed)) 47 | 48 | val result = profileApiService.getUser("Token $token") 49 | 50 | if (result.isSuccessful) { 51 | val user = result.body()?.toUser() ?: return AppResponse.failed(AppError.GeneralError(R.string.error_logout_failed)) 52 | return AppResponse.success(user) 53 | } 54 | 55 | val error = result.getAppErrorByHttpErrorCode() 56 | return AppResponse.failed(error) 57 | 58 | } catch (e: IOException) { 59 | return AppResponse.failed(AppError.PoorNetworkConnectionError) 60 | } catch (e: HttpException) { 61 | return AppResponse.failed(AppError.GeneralError(R.string.error_logout_failed)) 62 | } 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/profile_feature/di/ProfileModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.profile_feature.di 2 | 3 | import com.example.noteswithrestapi.profile_feature.data.remote.ProfileApiService 4 | import com.example.noteswithrestapi.profile_feature.data.repository.ProfileRepositoryImpl 5 | import com.example.noteswithrestapi.profile_feature.domain.repository.ProfileRepository 6 | import dagger.Binds 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.components.SingletonComponent 11 | import retrofit2.Retrofit 12 | import javax.inject.Singleton 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | interface ProfileModule { 17 | 18 | @Binds 19 | @Singleton 20 | fun bindProfileRepository( 21 | profileRepositoryImpl: ProfileRepositoryImpl 22 | ): ProfileRepository 23 | 24 | 25 | companion object { 26 | 27 | @Provides 28 | @Singleton 29 | fun provideProfileApiService( 30 | retrofit: Retrofit, 31 | ): ProfileApiService { 32 | return retrofit.create(ProfileApiService::class.java) 33 | } 34 | 35 | } 36 | 37 | 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/profile_feature/domain/model/user/User.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.profile_feature.domain.model.user 2 | 3 | data class User( 4 | val email: String, 5 | val date_created: String, 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/profile_feature/domain/repository/ProfileRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.profile_feature.domain.repository 2 | 3 | import com.example.noteswithrestapi.core.domain.model.AppResponse 4 | import com.example.noteswithrestapi.profile_feature.domain.model.user.User 5 | 6 | interface ProfileRepository { 7 | 8 | suspend fun logout(): AppResponse 9 | 10 | suspend fun getCurrentUser(): AppResponse 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/profile_feature/domain/usecase/GetUserUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.profile_feature.domain.usecase 2 | 3 | import com.example.noteswithrestapi.core.domain.model.AppResponse 4 | import com.example.noteswithrestapi.profile_feature.domain.model.user.User 5 | import com.example.noteswithrestapi.profile_feature.domain.repository.ProfileRepository 6 | import javax.inject.Inject 7 | 8 | class GetUserUseCase @Inject constructor( 9 | private val profileRepository: ProfileRepository 10 | ) { 11 | 12 | suspend operator fun invoke(): AppResponse = profileRepository.getCurrentUser() 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/profile_feature/domain/usecase/LogoutUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.profile_feature.domain.usecase 2 | 3 | import com.example.noteswithrestapi.core.domain.model.AppResponse 4 | import com.example.noteswithrestapi.profile_feature.domain.repository.ProfileRepository 5 | import javax.inject.Inject 6 | 7 | class LogoutUseCase @Inject constructor( 8 | private val profileRepository: ProfileRepository 9 | ) { 10 | 11 | suspend operator fun invoke(): AppResponse { 12 | return profileRepository.logout() 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/profile_feature/presentation/ProfileGraph.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.profile_feature.presentation 2 | 3 | import androidx.compose.animation.ExperimentalAnimationApi 4 | import androidx.hilt.navigation.compose.hiltViewModel 5 | import androidx.navigation.NavController 6 | import androidx.navigation.NavGraph.Companion.findStartDestination 7 | import androidx.navigation.NavGraphBuilder 8 | import com.example.noteswithrestapi.core.presentation.navigation.Graph 9 | import com.example.noteswithrestapi.core.presentation.navigation.Screen 10 | import com.example.noteswithrestapi.profile_feature.presentation.profile.ProfileScreen 11 | import com.example.noteswithrestapi.profile_feature.presentation.profile.ProfileViewModel 12 | import com.google.accompanist.navigation.animation.composable 13 | import com.google.accompanist.navigation.animation.navigation 14 | 15 | @OptIn(ExperimentalAnimationApi::class) 16 | fun NavGraphBuilder.profileGraph( 17 | navController: NavController, 18 | onShowNavigationDrawer: () -> Unit, 19 | ) { 20 | navigation( 21 | route = Graph.PROFILE_GRAPH, 22 | startDestination = Screen.Profile.fullRoute 23 | ) { 24 | composable(Screen.Profile.fullRoute) { 25 | val viewModel: ProfileViewModel = hiltViewModel() 26 | ProfileScreen( 27 | onNavigateToAuthentication = { 28 | navController.navigate( 29 | Graph.AUTH_GRAPH, 30 | ) { 31 | popUpTo(navController.graph.findStartDestination().id) { 32 | inclusive = true 33 | } 34 | } 35 | navController.graph.setStartDestination(Graph.AUTH_GRAPH) 36 | }, 37 | onShowNavigationDrawer = onShowNavigationDrawer, 38 | state = viewModel.state, 39 | uiEvent = viewModel.uiEvent, 40 | onEvent = viewModel::onEvent 41 | ) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/profile_feature/presentation/profile/ProfileEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.profile_feature.presentation.profile 2 | 3 | sealed class ProfileEvent { 4 | 5 | object OnLogout: ProfileEvent() 6 | 7 | object OnRetryLoadProfile: ProfileEvent() 8 | 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/profile_feature/presentation/profile/ProfileScreen.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.profile_feature.presentation.profile 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.rounded.Logout 12 | import androidx.compose.material.icons.rounded.Menu 13 | import androidx.compose.material3.ExperimentalMaterial3Api 14 | import androidx.compose.material3.Icon 15 | import androidx.compose.material3.IconButton 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.Scaffold 18 | import androidx.compose.material3.Text 19 | import androidx.compose.material3.TopAppBar 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.LaunchedEffect 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.res.stringResource 25 | import androidx.compose.ui.tooling.preview.Preview 26 | import androidx.compose.ui.unit.dp 27 | import com.example.noteswithrestapi.R 28 | import com.example.noteswithrestapi.core.presentation.components.ButtonComponent 29 | import com.example.noteswithrestapi.core.presentation.components.LoadingFailedComponent 30 | import com.example.noteswithrestapi.core.presentation.components.ProgressIndicatorComponent 31 | import com.example.noteswithrestapi.core.presentation.theme.ui.theme.NotesWithRESTAPITheme 32 | import com.example.noteswithrestapi.profile_feature.domain.model.user.User 33 | import kotlinx.coroutines.flow.Flow 34 | import kotlinx.coroutines.flow.flow 35 | 36 | 37 | @OptIn(ExperimentalMaterial3Api::class) 38 | @Composable 39 | fun ProfileScreen( 40 | onNavigateToAuthentication: () -> Unit, 41 | onShowNavigationDrawer: () -> Unit, 42 | state: ProfileState, 43 | uiEvent: Flow, 44 | onEvent: (ProfileEvent) -> Unit, 45 | ) { 46 | 47 | LaunchedEffect(key1 = Unit) { 48 | uiEvent.collect { event -> 49 | when (event) { 50 | ProfileUiEvent.OnLogoutComplete -> onNavigateToAuthentication() 51 | } 52 | } 53 | } 54 | 55 | 56 | Scaffold( 57 | topBar = { 58 | TopAppBar( 59 | title = { 60 | Text( 61 | text = stringResource(id = R.string.title_profile), 62 | style = MaterialTheme.typography.titleLarge, 63 | color = MaterialTheme.colorScheme.onSurface 64 | ) 65 | }, 66 | navigationIcon = { 67 | IconButton( 68 | onClick = onShowNavigationDrawer 69 | ) { 70 | Icon( 71 | imageVector = Icons.Rounded.Menu, 72 | contentDescription = stringResource(id = R.string.desc_menu_icon), 73 | modifier = Modifier.size(30.dp) 74 | ) 75 | } 76 | } 77 | ) 78 | } 79 | ) { 80 | Box( 81 | modifier = Modifier 82 | .padding(it) 83 | .fillMaxSize(), 84 | contentAlignment = Alignment.Center 85 | ) { 86 | if (state.errorMessage == null) { 87 | if (state.user != null) { 88 | Column( 89 | modifier = Modifier.fillMaxSize(), 90 | horizontalAlignment = Alignment.CenterHorizontally, 91 | ) { 92 | Spacer(modifier = Modifier.height(50.dp)) 93 | Text( 94 | text = state.user.email, 95 | style = MaterialTheme.typography.titleMedium, 96 | color = MaterialTheme.colorScheme.primary 97 | ) 98 | Spacer(modifier = Modifier.height(10.dp)) 99 | Text( 100 | text = stringResource(R.string.text_joined_at_with_param, state.user.date_created), 101 | style = MaterialTheme.typography.bodyLarge, 102 | color = MaterialTheme.colorScheme.onBackground 103 | ) 104 | Spacer(modifier = Modifier.height(30.dp)) 105 | ButtonComponent( 106 | text = stringResource(R.string.title_logout), 107 | enabled = { !state.isLoading }, 108 | leadingIcon = Icons.Rounded.Logout, 109 | onButtonClick = { 110 | onEvent(ProfileEvent.OnLogout) 111 | } 112 | ) 113 | } 114 | } 115 | ProgressIndicatorComponent(isLoading = { state.isLoading }) 116 | } else { 117 | LoadingFailedComponent( 118 | errorMessage = state.errorMessage, 119 | onRetryClick = { onEvent(ProfileEvent.OnRetryLoadProfile) } 120 | ) 121 | } 122 | } 123 | } 124 | 125 | } 126 | 127 | 128 | @Preview 129 | @Composable 130 | fun ProfileScreenPreview() { 131 | NotesWithRESTAPITheme { 132 | ProfileScreen( 133 | onNavigateToAuthentication = {}, 134 | onShowNavigationDrawer = {}, 135 | state = ProfileState(User("itamiomw@gmail.com", "May 18, 2023")), 136 | uiEvent = flow {}, 137 | onEvent = {} 138 | ) 139 | } 140 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/profile_feature/presentation/profile/ProfileState.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.profile_feature.presentation.profile 2 | 3 | import com.example.noteswithrestapi.profile_feature.domain.model.user.User 4 | 5 | data class ProfileState( 6 | val user: User? = null, 7 | 8 | val isLoading: Boolean = false, 9 | val errorMessage: String? = null 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/profile_feature/presentation/profile/ProfileUiEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.profile_feature.presentation.profile 2 | 3 | sealed class ProfileUiEvent { 4 | 5 | object OnLogoutComplete: ProfileUiEvent() 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteswithrestapi/profile_feature/presentation/profile/ProfileViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteswithrestapi.profile_feature.presentation.profile 2 | 3 | import android.app.Application 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.viewModelScope 9 | import com.example.noteswithrestapi.R 10 | import com.example.noteswithrestapi.core.domain.model.AppError 11 | import com.example.noteswithrestapi.core.domain.model.AppResponse 12 | import com.example.noteswithrestapi.profile_feature.domain.usecase.GetUserUseCase 13 | import com.example.noteswithrestapi.profile_feature.domain.usecase.LogoutUseCase 14 | import dagger.hilt.android.lifecycle.HiltViewModel 15 | import kotlinx.coroutines.channels.Channel 16 | import kotlinx.coroutines.flow.receiveAsFlow 17 | import kotlinx.coroutines.launch 18 | import javax.inject.Inject 19 | 20 | @HiltViewModel 21 | class ProfileViewModel @Inject constructor( 22 | private val logoutUseCase: LogoutUseCase, 23 | private val getUserUseCase: GetUserUseCase, 24 | private val application: Application, 25 | ) : ViewModel() { 26 | 27 | 28 | private val _uiEvent = Channel() 29 | val uiEvent = _uiEvent.receiveAsFlow() 30 | 31 | var state by mutableStateOf(ProfileState()) 32 | private set 33 | 34 | 35 | init { 36 | getUser() 37 | } 38 | 39 | 40 | fun onEvent(event: ProfileEvent) { 41 | when (event) { 42 | is ProfileEvent.OnLogout -> { 43 | logout() 44 | } 45 | 46 | is ProfileEvent.OnRetryLoadProfile -> { 47 | getUser() 48 | } 49 | } 50 | } 51 | 52 | 53 | private fun logout() { 54 | viewModelScope.launch { 55 | state = state.copy(isLoading = true) 56 | logoutUseCase.invoke() 57 | state = state.copy(isLoading = false) 58 | _uiEvent.send(ProfileUiEvent.OnLogoutComplete) 59 | } 60 | } 61 | 62 | private fun getUser() { 63 | viewModelScope.launch { 64 | state = state.copy(isLoading = true, errorMessage = null) 65 | when (val result = getUserUseCase()) { 66 | is AppResponse.Success -> { 67 | state = state.copy(isLoading = false, user = result.data) 68 | } 69 | 70 | is AppResponse.Failed -> { 71 | state = state.copy(isLoading = false) 72 | handleAppError(result.error) 73 | } 74 | } 75 | } 76 | } 77 | 78 | 79 | private fun handleAppError(error: AppError) { 80 | when (error) { 81 | is AppError.GeneralError -> { 82 | state = state.copy(errorMessage = application.getString(error.messageResId)) 83 | } 84 | 85 | is AppError.ServerError -> { 86 | state = state.copy(errorMessage = application.getString(R.string.error_server)) 87 | } 88 | 89 | is AppError.PoorNetworkConnectionError -> { 90 | state = state.copy(errorMessage = application.getString(R.string.error_poor_network_connection)) 91 | } 92 | 93 | else -> { 94 | state = state.copy(errorMessage = application.getString(R.string.error_unknown)) 95 | } 96 | } 97 | } 98 | 99 | 100 | } -------------------------------------------------------------------------------- /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/amico_confirm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/drawable/amico_confirm.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/amico_notes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/drawable/amico_notes.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/amico_reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/drawable/amico_reset.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/amico_signin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/drawable/amico_signin.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/amico_signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/drawable/amico_signup.png -------------------------------------------------------------------------------- /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/font/roboto_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/font/roboto_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/roboto_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/font/roboto_light.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/roboto_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/font/roboto_medium.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/roboto_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/font/roboto_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/ud_digi_kyokasho_nk_b.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/font/ud_digi_kyokasho_nk_b.ttf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Notes With REST API 3 | Leading Icon 4 | Failed to login 5 | Failed to verify email 6 | Failed to resend verification code 7 | Failed to send password reset code 8 | Failed to reset password 9 | Failed to logout 10 | Failed to get user 11 | Failed to register 12 | Unknown error occurred 13 | Sign in illustration 14 | Sign up illustration 15 | Password reset illustration 16 | Login 17 | Confirm Email 18 | Register 19 | Reset Password 20 | Reset Password 21 | Please fill the inputs below 22 | Please enter 6 digit code from your email 23 | Please enter your email 24 | EMAIL 25 | PASSWORD 26 | NEW PASSWORD 27 | CONFIRM PASSWORD 28 | Forgot Password? 29 | Already have an account? 30 | Don\'t have an account? 31 | Sign In 32 | Sign Up 33 | Confirm 34 | Resend code 35 | Poor network connection 36 | Invalid email or password 37 | Empty password 38 | Empty email 39 | Invalid email 40 | Short password 41 | Password do not match 42 | User with such an email already exists 43 | User with such an email doesn\'t exist 44 | Invalid verification code 45 | Go back 46 | Code re-sent successfully 47 | Server error, try again later 48 | Failed to get notes 49 | Failed to delete note 50 | Failed to update note 51 | Failed to get note 52 | Failed to add note 53 | Notes 54 | Note 55 | Profile 56 | Menu icon 57 | Add note 58 | Search icon 59 | Notes screen 60 | Profile screen 61 | Try again 62 | Retry icon 63 | Note title 64 | Type something… 65 | Save 66 | Cancel 67 | Empty title 68 | Empty content 69 | Not found 70 | Delete icon 71 | Search notes 72 | Clear query icon 73 | Logout 74 | Joined at %s 75 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |