├── .gitignore ├── .idea ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── jarRepositories.xml ├── misc.xml └── runConfigurations.xml ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── net │ │ └── simplifiedcoding │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── net │ │ │ └── simplifiedcoding │ │ │ ├── MainActivity.kt │ │ │ ├── MyApplication.kt │ │ │ ├── data │ │ │ ├── UserPreferences.kt │ │ │ ├── network │ │ │ │ ├── AuthApi.kt │ │ │ │ ├── BaseApi.kt │ │ │ │ ├── RemoteDataSource.kt │ │ │ │ ├── Resource.kt │ │ │ │ ├── SafeApiCall.kt │ │ │ │ ├── TokenAuthenticator.kt │ │ │ │ ├── TokenRefreshApi.kt │ │ │ │ └── UserApi.kt │ │ │ ├── repository │ │ │ │ ├── AuthRepository.kt │ │ │ │ ├── BaseRepository.kt │ │ │ │ └── UserRepository.kt │ │ │ └── responses │ │ │ │ ├── LoginResponse.kt │ │ │ │ ├── TokenResponse.kt │ │ │ │ └── User.kt │ │ │ ├── di │ │ │ └── AppModule.kt │ │ │ └── ui │ │ │ ├── Utils.kt │ │ │ ├── auth │ │ │ ├── AuthActivity.kt │ │ │ ├── AuthViewModel.kt │ │ │ ├── LoginFragment.kt │ │ │ └── RegisterFragment.kt │ │ │ ├── base │ │ │ └── BaseViewModel.kt │ │ │ └── home │ │ │ ├── HomeActivity.kt │ │ │ ├── HomeFragment.kt │ │ │ └── HomeViewModel.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── app_logo.png │ │ ├── bg_button_primary.xml │ │ ├── bg_login_page1.xml │ │ └── ic_launcher_background.xml │ │ ├── font │ │ ├── poppins_bold.ttf │ │ ├── poppins_light.ttf │ │ ├── poppins_regular.ttf │ │ └── poppins_semibold.ttf │ │ ├── layout │ │ ├── activity_auth.xml │ │ ├── activity_home.xml │ │ ├── activity_main.xml │ │ ├── fragment_home.xml │ │ ├── fragment_login.xml │ │ └── fragment_register.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ ├── nav_auth.xml │ │ └── nav_home.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── net │ └── simplifiedcoding │ └── ExampleUnitTest.kt ├── 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 | 16 | 17 | # Built application files 18 | *.apk 19 | *.aar 20 | *.ap_ 21 | *.aab 22 | 23 | # Files for the ART/Dalvik VM 24 | *.dex 25 | 26 | # Java class files 27 | *.class 28 | 29 | # Generated files 30 | bin/ 31 | gen/ 32 | out/ 33 | # Uncomment the following line in case you need and you don't have the release build type files in your app 34 | # release/ 35 | 36 | # Gradle files 37 | .gradle/ 38 | build/ 39 | 40 | # Local configuration file (sdk path, etc) 41 | local.properties 42 | 43 | # Proguard folder generated by Eclipse 44 | proguard/ 45 | 46 | # Log Files 47 | *.log 48 | 49 | # Android Studio Navigation editor temp files 50 | .navigation/ 51 | 52 | # Android Studio captures folder 53 | captures/ 54 | 55 | # IntelliJ 56 | .idea/workspace.xml 57 | .idea/tasks.xml 58 | .idea/gradle.xml 59 | .idea/assetWizardSettings.xml 60 | .idea/dictionaries 61 | .idea/libraries 62 | # Android Studio 3 in .gitignore file. 63 | .idea/caches 64 | .idea/modules.xml 65 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 66 | .idea/navEditor.xml 67 | 68 | # Keystore files 69 | # Uncomment the following lines if you do not want to check your keystore files in. 70 | #*.jks 71 | #*.keystore 72 | 73 | # External native build folder generated in Android Studio 2.2 and later 74 | .cxx/ 75 | 76 | # Google Services (e.g. APIs or Firebase) 77 | # google-services.json 78 | 79 | # Freeline 80 | freeline.py 81 | freeline/ 82 | freeline_project_description.json 83 | 84 | # fastlane 85 | fastlane/report.xml 86 | fastlane/Preview.html 87 | fastlane/screenshots 88 | fastlane/test_output 89 | fastlane/readme.md 90 | 91 | # Version control 92 | vcs.xml 93 | 94 | # lint 95 | lint/intermediates/ 96 | lint/generated/ 97 | lint/outputs/ 98 | lint/tmp/ 99 | # lint/reports/ 100 | 101 | # Android Profiling 102 | *.hprof -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Login Sample -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | xmlns:android 17 | 18 | ^$ 19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | xmlns:.* 28 | 29 | ^$ 30 | 31 | 32 | BY_NAME 33 | 34 |
35 |
36 | 37 | 38 | 39 | .*:id 40 | 41 | http://schemas.android.com/apk/res/android 42 | 43 | 44 | 45 |
46 |
47 | 48 | 49 | 50 | .*:name 51 | 52 | http://schemas.android.com/apk/res/android 53 | 54 | 55 | 56 |
57 |
58 | 59 | 60 | 61 | name 62 | 63 | ^$ 64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | style 73 | 74 | ^$ 75 | 76 | 77 | 78 |
79 |
80 | 81 | 82 | 83 | .* 84 | 85 | ^$ 86 | 87 | 88 | BY_NAME 89 | 90 |
91 |
92 | 93 | 94 | 95 | .* 96 | 97 | http://schemas.android.com/apk/res/android 98 | 99 | 100 | ANDROID_ATTRIBUTE_ORDER 101 | 102 |
103 |
104 | 105 | 106 | 107 | .* 108 | 109 | .* 110 | 111 | 112 | BY_NAME 113 | 114 |
115 |
116 |
117 |
118 | 119 | 121 |
122 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | apply plugin: 'dagger.hilt.android.plugin' 6 | apply plugin: "androidx.navigation.safeargs" 7 | 8 | android { 9 | compileSdkVersion 30 10 | buildToolsVersion "30.0.2" 11 | 12 | defaultConfig { 13 | applicationId "net.simplifiedcoding" 14 | minSdkVersion 16 15 | targetSdkVersion 30 16 | versionCode 1 17 | versionName "1.0" 18 | 19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 20 | } 21 | 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | 29 | buildFeatures { 30 | viewBinding true 31 | } 32 | 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_1_8 35 | targetCompatibility JavaVersion.VERSION_1_8 36 | } 37 | 38 | kotlinOptions { 39 | jvmTarget = JavaVersion.VERSION_1_8.toString() 40 | } 41 | } 42 | 43 | dependencies { 44 | implementation fileTree(dir: "libs", include: ["*.jar"]) 45 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 46 | implementation 'androidx.core:core-ktx:1.3.2' 47 | implementation 'androidx.appcompat:appcompat:1.2.0' 48 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 49 | implementation 'androidx.annotation:annotation:1.1.0' 50 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 51 | testImplementation 'junit:junit:4.13' 52 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 53 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 54 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' 55 | implementation 'com.squareup.retrofit2:retrofit:2.7.2' 56 | implementation 'com.squareup.retrofit2:converter-gson:2.7.2' 57 | implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" 58 | implementation 'com.google.android.material:material:1.3.0-beta01' 59 | implementation "androidx.navigation:navigation-fragment-ktx:2.3.2" 60 | implementation "androidx.navigation:navigation-ui-ktx:2.3.2" 61 | implementation "com.squareup.okhttp3:logging-interceptor:4.9.0" 62 | implementation "androidx.datastore:datastore-preferences:1.0.0-alpha05" 63 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0" 64 | implementation "com.google.dagger:hilt-android:2.28-alpha" 65 | implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02' 66 | implementation "androidx.fragment:fragment-ktx:1.2.5" 67 | kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02' 68 | kapt "com.google.dagger:hilt-android-compiler:2.28-alpha" 69 | } -------------------------------------------------------------------------------- /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/src/androidTest/java/net/simplifiedcoding/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding 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("net.simplifiedcoding", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.lifecycle.Observer 6 | import androidx.lifecycle.asLiveData 7 | import dagger.hilt.android.AndroidEntryPoint 8 | import net.simplifiedcoding.data.UserPreferences 9 | import net.simplifiedcoding.ui.auth.AuthActivity 10 | import net.simplifiedcoding.ui.home.HomeActivity 11 | import net.simplifiedcoding.ui.startNewActivity 12 | 13 | @AndroidEntryPoint 14 | class MainActivity : AppCompatActivity() { 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | setContentView(R.layout.activity_main) 19 | val userPreferences = UserPreferences(this) 20 | 21 | userPreferences.accessToken.asLiveData().observe(this, Observer { 22 | val activity = if (it == null) AuthActivity::class.java else HomeActivity::class.java 23 | startNewActivity(activity) 24 | }) 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/MyApplication.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class MyApplication : Application() -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/UserPreferences.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.core.clear 7 | import androidx.datastore.preferences.core.edit 8 | import androidx.datastore.preferences.core.preferencesKey 9 | import androidx.datastore.preferences.createDataStore 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.map 12 | import javax.inject.Inject 13 | 14 | class UserPreferences @Inject constructor(context: Context) { 15 | 16 | private val applicationContext = context.applicationContext 17 | private val dataStore: DataStore 18 | 19 | init { 20 | dataStore = applicationContext.createDataStore( 21 | name = "my_data_store" 22 | ) 23 | } 24 | 25 | val accessToken: Flow 26 | get() = dataStore.data.map { preferences -> 27 | preferences[ACCESS_TOKEN] 28 | } 29 | 30 | val refreshToken: Flow 31 | get() = dataStore.data.map { preferences -> 32 | preferences[REFRESH_TOKEN] 33 | } 34 | 35 | suspend fun saveAccessTokens(accessToken: String, refreshToken: String) { 36 | dataStore.edit { preferences -> 37 | preferences[ACCESS_TOKEN] = accessToken 38 | preferences[REFRESH_TOKEN] = refreshToken 39 | } 40 | } 41 | 42 | suspend fun clear() { 43 | dataStore.edit { preferences -> 44 | preferences.clear() 45 | } 46 | } 47 | 48 | companion object { 49 | private val ACCESS_TOKEN = preferencesKey("key_access_token") 50 | private val REFRESH_TOKEN = preferencesKey("key_refresh_token") 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/network/AuthApi.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.network 2 | 3 | import net.simplifiedcoding.data.responses.LoginResponse 4 | import net.simplifiedcoding.data.responses.TokenResponse 5 | import retrofit2.http.Field 6 | import retrofit2.http.FormUrlEncoded 7 | import retrofit2.http.POST 8 | 9 | interface AuthApi : BaseApi { 10 | 11 | @FormUrlEncoded 12 | @POST("auth/login") 13 | suspend fun login( 14 | @Field("email") email: String, 15 | @Field("password") password: String 16 | ): LoginResponse 17 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/network/BaseApi.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.network 2 | 3 | import okhttp3.ResponseBody 4 | import retrofit2.http.POST 5 | 6 | interface BaseApi { 7 | @POST("logout") 8 | suspend fun logout(): ResponseBody 9 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/network/RemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.network 2 | 3 | import android.content.Context 4 | import net.simplifiedcoding.BuildConfig 5 | import okhttp3.Authenticator 6 | import okhttp3.OkHttpClient 7 | import okhttp3.logging.HttpLoggingInterceptor 8 | import retrofit2.Retrofit 9 | import retrofit2.converter.gson.GsonConverterFactory 10 | 11 | class RemoteDataSource { 12 | 13 | companion object { 14 | private const val BASE_URL = "http://simplifiedcoding.tech/mywebapp/public/api/" 15 | } 16 | 17 | fun buildApi( 18 | api: Class, 19 | context: Context 20 | ): Api { 21 | val authenticator = TokenAuthenticator(context, buildTokenApi()) 22 | return Retrofit.Builder() 23 | .baseUrl(BASE_URL) 24 | .client(getRetrofitClient(authenticator)) 25 | .addConverterFactory(GsonConverterFactory.create()) 26 | .build() 27 | .create(api) 28 | } 29 | 30 | private fun buildTokenApi(): TokenRefreshApi { 31 | return Retrofit.Builder() 32 | .baseUrl(BASE_URL) 33 | .client(getRetrofitClient()) 34 | .addConverterFactory(GsonConverterFactory.create()) 35 | .build() 36 | .create(TokenRefreshApi::class.java) 37 | } 38 | 39 | private fun getRetrofitClient(authenticator: Authenticator? = null): OkHttpClient { 40 | return OkHttpClient.Builder() 41 | .addInterceptor { chain -> 42 | chain.proceed(chain.request().newBuilder().also { 43 | it.addHeader("Accept", "application/json") 44 | }.build()) 45 | }.also { client -> 46 | authenticator?.let { client.authenticator(it) } 47 | if (BuildConfig.DEBUG) { 48 | val logging = HttpLoggingInterceptor() 49 | logging.setLevel(HttpLoggingInterceptor.Level.BODY) 50 | client.addInterceptor(logging) 51 | } 52 | }.build() 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/network/Resource.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.network 2 | 3 | import okhttp3.ResponseBody 4 | 5 | sealed class Resource { 6 | data class Success(val value: T) : Resource() 7 | data class Failure( 8 | val isNetworkError: Boolean, 9 | val errorCode: Int?, 10 | val errorBody: ResponseBody? 11 | ) : Resource() 12 | object Loading : Resource() 13 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/network/SafeApiCall.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.network 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | import retrofit2.HttpException 6 | 7 | interface SafeApiCall { 8 | suspend fun safeApiCall( 9 | apiCall: suspend () -> T 10 | ): Resource { 11 | return withContext(Dispatchers.IO) { 12 | try { 13 | Resource.Success(apiCall.invoke()) 14 | } catch (throwable: Throwable) { 15 | when (throwable) { 16 | is HttpException -> { 17 | Resource.Failure(false, throwable.code(), throwable.response()?.errorBody()) 18 | } 19 | else -> { 20 | Resource.Failure(true, null, null) 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/network/TokenAuthenticator.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.network 2 | 3 | import android.content.Context 4 | import kotlinx.coroutines.flow.first 5 | import kotlinx.coroutines.runBlocking 6 | import net.simplifiedcoding.data.UserPreferences 7 | import net.simplifiedcoding.data.repository.BaseRepository 8 | import net.simplifiedcoding.data.responses.TokenResponse 9 | import okhttp3.Authenticator 10 | import okhttp3.Request 11 | import okhttp3.Response 12 | import okhttp3.Route 13 | import javax.inject.Inject 14 | 15 | class TokenAuthenticator @Inject constructor( 16 | context: Context, 17 | private val tokenApi: TokenRefreshApi 18 | ) : Authenticator, BaseRepository(tokenApi) { 19 | 20 | private val appContext = context.applicationContext 21 | private val userPreferences = UserPreferences(appContext) 22 | 23 | override fun authenticate(route: Route?, response: Response): Request? { 24 | return runBlocking { 25 | when (val tokenResponse = getUpdatedToken()) { 26 | is Resource.Success -> { 27 | userPreferences.saveAccessTokens( 28 | tokenResponse.value.access_token!!, 29 | tokenResponse.value.refresh_token!! 30 | ) 31 | response.request.newBuilder() 32 | .header("Authorization", "Bearer ${tokenResponse.value.access_token}") 33 | .build() 34 | } 35 | else -> null 36 | } 37 | } 38 | } 39 | 40 | private suspend fun getUpdatedToken(): Resource { 41 | val refreshToken = userPreferences.refreshToken.first() 42 | return safeApiCall { tokenApi.refreshAccessToken(refreshToken) } 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/network/TokenRefreshApi.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.network 2 | 3 | import net.simplifiedcoding.data.responses.TokenResponse 4 | import retrofit2.http.Field 5 | import retrofit2.http.FormUrlEncoded 6 | import retrofit2.http.POST 7 | 8 | interface TokenRefreshApi : BaseApi { 9 | @FormUrlEncoded 10 | @POST("auth/refresh-token") 11 | suspend fun refreshAccessToken( 12 | @Field("refresh_token") refreshToken: String? 13 | ): TokenResponse 14 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/network/UserApi.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.network 2 | 3 | import net.simplifiedcoding.data.responses.LoginResponse 4 | import okhttp3.ResponseBody 5 | import retrofit2.http.GET 6 | import retrofit2.http.POST 7 | 8 | interface UserApi : BaseApi{ 9 | @GET("user") 10 | suspend fun getUser(): LoginResponse 11 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/repository/AuthRepository.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.repository 2 | 3 | import net.simplifiedcoding.data.UserPreferences 4 | import net.simplifiedcoding.data.network.AuthApi 5 | import javax.inject.Inject 6 | 7 | class AuthRepository @Inject constructor( 8 | private val api: AuthApi, 9 | private val preferences: UserPreferences 10 | ) : BaseRepository(api) { 11 | 12 | suspend fun login( 13 | email: String, 14 | password: String 15 | ) = safeApiCall { 16 | api.login(email, password) 17 | } 18 | 19 | suspend fun saveAccessTokens(accessToken: String, refreshToken: String) { 20 | preferences.saveAccessTokens(accessToken, refreshToken) 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/repository/BaseRepository.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.repository 2 | 3 | import net.simplifiedcoding.data.network.BaseApi 4 | import net.simplifiedcoding.data.network.SafeApiCall 5 | 6 | abstract class BaseRepository(private val api: BaseApi) : SafeApiCall { 7 | 8 | suspend fun logout() = safeApiCall { 9 | api.logout() 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/repository/UserRepository.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.repository 2 | 3 | import net.simplifiedcoding.data.network.UserApi 4 | import javax.inject.Inject 5 | 6 | class UserRepository @Inject constructor( 7 | private val api: UserApi 8 | ) : BaseRepository(api) { 9 | 10 | suspend fun getUser() = safeApiCall { api.getUser() } 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/responses/LoginResponse.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.responses 2 | 3 | data class LoginResponse( 4 | val user: User 5 | ) -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/responses/TokenResponse.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.responses 2 | 3 | data class TokenResponse( 4 | val access_token: String?, 5 | val refresh_token: String? 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/responses/User.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.responses 2 | 3 | data class User( 4 | val access_token: String?, 5 | val refresh_token: String?, 6 | val created_at: String, 7 | val email: String, 8 | val email_verified_at: Any, 9 | val id: Int, 10 | val name: String, 11 | val updated_at: String 12 | ) -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.di 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.components.ApplicationComponent 8 | import dagger.hilt.android.qualifiers.ApplicationContext 9 | import net.simplifiedcoding.data.UserPreferences 10 | import net.simplifiedcoding.data.network.* 11 | import net.simplifiedcoding.data.repository.AuthRepository 12 | import net.simplifiedcoding.data.repository.UserRepository 13 | import javax.inject.Singleton 14 | 15 | @Module 16 | @InstallIn(ApplicationComponent::class) 17 | object AppModule { 18 | 19 | @Singleton 20 | @Provides 21 | fun provideRemoteDataSource(): RemoteDataSource { 22 | return RemoteDataSource() 23 | } 24 | 25 | @Singleton 26 | @Provides 27 | fun provideAuthApi( 28 | remoteDataSource: RemoteDataSource, 29 | @ApplicationContext context: Context 30 | ): AuthApi { 31 | return remoteDataSource.buildApi(AuthApi::class.java, context) 32 | } 33 | 34 | @Singleton 35 | @Provides 36 | fun provideUserApi( 37 | remoteDataSource: RemoteDataSource, 38 | @ApplicationContext context: Context 39 | ): UserApi { 40 | return remoteDataSource.buildApi(UserApi::class.java, context) 41 | } 42 | 43 | @Singleton 44 | @Provides 45 | fun provideUserPreferences(@ApplicationContext context: Context): UserPreferences { 46 | return UserPreferences(context) 47 | } 48 | 49 | @Provides 50 | fun provideAuthRepository( 51 | authApi: AuthApi, 52 | userPreferences: UserPreferences 53 | ): AuthRepository { 54 | return AuthRepository(authApi, userPreferences) 55 | } 56 | 57 | @Provides 58 | fun provideUserRepository(userApi: UserApi): UserRepository { 59 | return UserRepository(userApi) 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/Utils.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.view.View 6 | import androidx.fragment.app.Fragment 7 | import androidx.lifecycle.lifecycleScope 8 | import com.google.android.material.snackbar.Snackbar 9 | import kotlinx.coroutines.launch 10 | import net.simplifiedcoding.data.network.Resource 11 | import net.simplifiedcoding.ui.auth.LoginFragment 12 | import net.simplifiedcoding.ui.home.HomeActivity 13 | 14 | fun Activity.startNewActivity(activity: Class) { 15 | Intent(this, activity).also { 16 | it.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 17 | startActivity(it) 18 | } 19 | } 20 | 21 | fun View.visible(isVisible: Boolean) { 22 | visibility = if (isVisible) View.VISIBLE else View.GONE 23 | } 24 | 25 | fun View.enable(enabled: Boolean) { 26 | isEnabled = enabled 27 | alpha = if (enabled) 1f else 0.5f 28 | } 29 | 30 | fun View.snackbar(message: String, action: (() -> Unit)? = null) { 31 | val snackbar = Snackbar.make(this, message, Snackbar.LENGTH_LONG) 32 | action?.let { 33 | snackbar.setAction("Retry") { 34 | it() 35 | } 36 | } 37 | snackbar.show() 38 | } 39 | 40 | fun Fragment.logout() = lifecycleScope.launch { 41 | if (activity is HomeActivity) { 42 | (activity as HomeActivity).performLogout() 43 | } 44 | } 45 | 46 | fun Fragment.handleApiError( 47 | failure: Resource.Failure, 48 | retry: (() -> Unit)? = null 49 | ) { 50 | when { 51 | failure.isNetworkError -> requireView().snackbar( 52 | "Please check your internet connection", 53 | retry 54 | ) 55 | failure.errorCode == 401 -> { 56 | if (this is LoginFragment) { 57 | requireView().snackbar("You've entered incorrect email or password") 58 | } else { 59 | logout() 60 | } 61 | } 62 | else -> { 63 | val error = failure.errorBody?.string().toString() 64 | requireView().snackbar(error) 65 | } 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/auth/AuthActivity.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui.auth 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import android.util.Log 6 | import android.widget.Toast 7 | import dagger.hilt.android.AndroidEntryPoint 8 | import kotlinx.coroutines.runBlocking 9 | import net.simplifiedcoding.R 10 | import net.simplifiedcoding.data.UserPreferences 11 | import javax.inject.Inject 12 | 13 | @AndroidEntryPoint 14 | class AuthActivity : AppCompatActivity() { 15 | 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setContentView(R.layout.activity_auth) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/auth/AuthViewModel.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui.auth 2 | 3 | import androidx.hilt.lifecycle.ViewModelInject 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.viewModelScope 7 | import kotlinx.coroutines.launch 8 | import net.simplifiedcoding.data.network.Resource 9 | import net.simplifiedcoding.data.repository.AuthRepository 10 | import net.simplifiedcoding.data.responses.LoginResponse 11 | import net.simplifiedcoding.ui.base.BaseViewModel 12 | 13 | class AuthViewModel @ViewModelInject constructor( 14 | private val repository: AuthRepository 15 | ) : BaseViewModel(repository) { 16 | 17 | private val _loginResponse: MutableLiveData> = MutableLiveData() 18 | val loginResponse: LiveData> 19 | get() = _loginResponse 20 | 21 | fun login( 22 | email: String, 23 | password: String 24 | ) = viewModelScope.launch { 25 | _loginResponse.value = Resource.Loading 26 | _loginResponse.value = repository.login(email, password) 27 | } 28 | 29 | suspend fun saveAccessTokens(accessToken: String, refreshToken: String) { 30 | repository.saveAccessTokens(accessToken, refreshToken) 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/auth/LoginFragment.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui.auth 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.core.widget.addTextChangedListener 6 | import androidx.fragment.app.Fragment 7 | import androidx.fragment.app.viewModels 8 | import androidx.lifecycle.Observer 9 | import androidx.lifecycle.lifecycleScope 10 | import dagger.hilt.android.AndroidEntryPoint 11 | import kotlinx.coroutines.launch 12 | import net.simplifiedcoding.R 13 | import net.simplifiedcoding.data.network.Resource 14 | import net.simplifiedcoding.databinding.FragmentLoginBinding 15 | import net.simplifiedcoding.ui.enable 16 | import net.simplifiedcoding.ui.handleApiError 17 | import net.simplifiedcoding.ui.home.HomeActivity 18 | import net.simplifiedcoding.ui.startNewActivity 19 | import net.simplifiedcoding.ui.visible 20 | 21 | 22 | @AndroidEntryPoint 23 | class LoginFragment: Fragment(R.layout.fragment_login) { 24 | 25 | private lateinit var binding: FragmentLoginBinding 26 | private val viewModel by viewModels() 27 | 28 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 29 | super.onActivityCreated(savedInstanceState) 30 | binding = FragmentLoginBinding.bind(view) 31 | binding.progressbar.visible(false) 32 | binding.buttonLogin.enable(false) 33 | 34 | viewModel.loginResponse.observe(viewLifecycleOwner, Observer { 35 | binding.progressbar.visible(it is Resource.Loading) 36 | when (it) { 37 | is Resource.Success -> { 38 | lifecycleScope.launch { 39 | viewModel.saveAccessTokens( 40 | it.value.user.access_token!!, 41 | it.value.user.refresh_token!! 42 | ) 43 | requireActivity().startNewActivity(HomeActivity::class.java) 44 | } 45 | } 46 | is Resource.Failure -> handleApiError(it) { login() } 47 | } 48 | }) 49 | 50 | binding.editTextTextPassword.addTextChangedListener { 51 | val email = binding.editTextTextEmailAddress.text.toString().trim() 52 | binding.buttonLogin.enable(email.isNotEmpty() && it.toString().isNotEmpty()) 53 | } 54 | 55 | binding.buttonLogin.setOnClickListener { 56 | login() 57 | } 58 | } 59 | 60 | private fun login() { 61 | val email = binding.editTextTextEmailAddress.text.toString().trim() 62 | val password = binding.editTextTextPassword.text.toString().trim() 63 | viewModel.login(email, password) 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/auth/RegisterFragment.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui.auth 2 | 3 | import android.os.Bundle 4 | import androidx.fragment.app.Fragment 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import net.simplifiedcoding.R 9 | 10 | class RegisterFragment : Fragment() { 11 | 12 | override fun onCreateView( 13 | inflater: LayoutInflater, container: ViewGroup?, 14 | savedInstanceState: Bundle? 15 | ): View? { 16 | return inflater.inflate(R.layout.fragment_register, container, false) 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/base/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui.base 2 | 3 | import androidx.lifecycle.ViewModel 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.withContext 6 | import net.simplifiedcoding.data.network.UserApi 7 | import net.simplifiedcoding.data.repository.BaseRepository 8 | 9 | abstract class BaseViewModel( 10 | private val repository: BaseRepository 11 | ) : ViewModel() { 12 | 13 | suspend fun logout() = withContext(Dispatchers.IO) { repository.logout() } 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/home/HomeActivity.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui.home 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import androidx.activity.viewModels 6 | import androidx.lifecycle.lifecycleScope 7 | import dagger.hilt.android.AndroidEntryPoint 8 | import kotlinx.coroutines.launch 9 | import net.simplifiedcoding.R 10 | import net.simplifiedcoding.data.UserPreferences 11 | import net.simplifiedcoding.data.network.AuthApi 12 | import net.simplifiedcoding.ui.auth.AuthActivity 13 | import net.simplifiedcoding.ui.startNewActivity 14 | import javax.inject.Inject 15 | 16 | @AndroidEntryPoint 17 | class HomeActivity : AppCompatActivity() { 18 | 19 | @Inject 20 | lateinit var userPreferences: UserPreferences 21 | 22 | private val viewModel by viewModels() 23 | 24 | override fun onCreate(savedInstanceState: Bundle?) { 25 | super.onCreate(savedInstanceState) 26 | setContentView(R.layout.activity_home) 27 | } 28 | 29 | fun performLogout() = lifecycleScope.launch { 30 | viewModel.logout() 31 | userPreferences.clear() 32 | startNewActivity(AuthActivity::class.java) 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/home/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui.home 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.Fragment 6 | import androidx.fragment.app.viewModels 7 | import androidx.lifecycle.Observer 8 | import dagger.hilt.android.AndroidEntryPoint 9 | import net.simplifiedcoding.R 10 | import net.simplifiedcoding.data.network.Resource 11 | import net.simplifiedcoding.data.responses.User 12 | import net.simplifiedcoding.databinding.FragmentHomeBinding 13 | import net.simplifiedcoding.ui.handleApiError 14 | import net.simplifiedcoding.ui.logout 15 | import net.simplifiedcoding.ui.visible 16 | 17 | @AndroidEntryPoint 18 | class HomeFragment : Fragment(R.layout.fragment_home) { 19 | 20 | private lateinit var binding: FragmentHomeBinding 21 | private val viewModel by viewModels() 22 | 23 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 24 | super.onViewCreated(view, savedInstanceState) 25 | binding = FragmentHomeBinding.bind(view) 26 | binding.progressbar.visible(false) 27 | 28 | viewModel.getUser() 29 | 30 | viewModel.user.observe(viewLifecycleOwner, Observer { 31 | when (it) { 32 | is Resource.Success -> { 33 | binding.progressbar.visible(false) 34 | updateUI(it.value.user) 35 | } 36 | is Resource.Loading -> { 37 | binding.progressbar.visible(true) 38 | } 39 | is Resource.Failure -> { 40 | handleApiError(it) 41 | } 42 | } 43 | }) 44 | 45 | binding.buttonLogout.setOnClickListener { 46 | logout() 47 | } 48 | } 49 | 50 | private fun updateUI(user: User) { 51 | with(binding) { 52 | textViewId.text = user.id.toString() 53 | textViewName.text = user.name 54 | textViewEmail.text = user.email 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui.home 2 | 3 | import androidx.hilt.lifecycle.ViewModelInject 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.viewModelScope 7 | import kotlinx.coroutines.launch 8 | import net.simplifiedcoding.data.network.Resource 9 | import net.simplifiedcoding.data.repository.UserRepository 10 | import net.simplifiedcoding.data.responses.LoginResponse 11 | import net.simplifiedcoding.ui.base.BaseViewModel 12 | 13 | class HomeViewModel @ViewModelInject constructor( 14 | private val repository: UserRepository 15 | ) : BaseViewModel(repository) { 16 | 17 | private val _user: MutableLiveData> = MutableLiveData() 18 | val user: LiveData> 19 | get() = _user 20 | 21 | fun getUser() = viewModelScope.launch { 22 | _user.value = Resource.Loading 23 | _user.value = repository.getUser() 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /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/app_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probelalkhan/Retrofit-Token-Authenticator-Example/46cdeefee91856cd3d88254e129812890393a6ea/app/src/main/res/drawable/app_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_button_primary.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_login_page1.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /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/poppins_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probelalkhan/Retrofit-Token-Authenticator-Example/46cdeefee91856cd3d88254e129812890393a6ea/app/src/main/res/font/poppins_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probelalkhan/Retrofit-Token-Authenticator-Example/46cdeefee91856cd3d88254e129812890393a6ea/app/src/main/res/font/poppins_light.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probelalkhan/Retrofit-Token-Authenticator-Example/46cdeefee91856cd3d88254e129812890393a6ea/app/src/main/res/font/poppins_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probelalkhan/Retrofit-Token-Authenticator-Example/46cdeefee91856cd3d88254e129812890393a6ea/app/src/main/res/font/poppins_semibold.ttf -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_auth.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 17 | 18 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 47 | 48 | 52 | 53 | 54 | 55 | 56 | 57 |