diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9adfcd81be5dfc08ad6ede46e813d0d750393f6e..1c1e3726accbfd644fb93cc96b67bd8154e8063c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -48,6 +48,8 @@ dependencies { implementation("androidx.navigation:navigation-fragment-ktx:$navVersion") implementation("androidx.navigation:navigation-ui-ktx:$navVersion") implementation("com.google.android.material:material:1.9.0") + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e105b0df59996aa47658435961e0d68ccf14db54..8beb467f97e42f1076585f61954e3ac9ff8c3419 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,10 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> + <uses-permission android:name="android.permission.INTERNET" /> + <application + android:name=".BondowowoApp" android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" @@ -14,8 +17,12 @@ tools:targetApi="31"> <activity android:name=".MainActivity" - android:label="@string/app_name" - android:exported="true"> + android:exported="false" + android:theme="@style/AppTheme.NoActionBar"/> + <activity + android:name=".LoginActivity" + android:exported="true" + android:theme="@style/AppTheme.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> diff --git a/app/src/main/java/com/atm/bondowowo/BondowowoApp.kt b/app/src/main/java/com/atm/bondowowo/BondowowoApp.kt new file mode 100644 index 0000000000000000000000000000000000000000..c52591accbab03846ed52308e2396d774068895a --- /dev/null +++ b/app/src/main/java/com/atm/bondowowo/BondowowoApp.kt @@ -0,0 +1,41 @@ +package com.atm.bondowowo; + +import android.app.Activity +import android.app.Application +import android.content.Context +import android.content.Intent +import com.atm.bondowowo.utils.BackgroundJWTChecker + +class BondowowoApp : Application() { + + private var jwtChecker: BackgroundJWTChecker? = null + + override fun onCreate() { + super.onCreate() + initBackgroundJWTChecker(this) + } + + private fun initBackgroundJWTChecker(context: Context) { + jwtChecker = BackgroundJWTChecker() + jwtChecker?.startChecking(context, object : BackgroundJWTChecker.Callback { + override fun onTokenVerified() { + // Continu app + } + + override fun onTokenInvalid() { + val intent = Intent(context, LoginActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + context.startActivity(intent) + if (context is Activity) { + context.finish() + } + } + + override fun onNetworkError() { + // Harusnya ini coba cek isi JWT pake library + } + }) + } + + +} diff --git a/app/src/main/java/com/atm/bondowowo/LoginActivity.kt b/app/src/main/java/com/atm/bondowowo/LoginActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..bdb25211c3ed5051a44c170f116ded610661d2ac --- /dev/null +++ b/app/src/main/java/com/atm/bondowowo/LoginActivity.kt @@ -0,0 +1,107 @@ +package com.atm.bondowowo + +import android.content.Intent +import android.os.Bundle +import android.widget.Button +import android.widget.EditText +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.atm.bondowowo.data.model.LoginRequest +import com.atm.bondowowo.utils.NetworkUtils.apiService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class LoginActivity : AppCompatActivity() { + + private lateinit var etUsername: EditText + private lateinit var etPassword: EditText + private lateinit var btnLogin: Button + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_login) + + CoroutineScope(Dispatchers.Main).launch { + if (isUserAuthenticated()) { + navigateToMainActivity() + } else { + etUsername = findViewById(R.id.etUsername) + etPassword = findViewById(R.id.etPassword) + btnLogin = findViewById(R.id.btnLogin) + + btnLogin.setOnClickListener { + performLogin() + } + } + } + } + + private fun performLogin() { + val email = etUsername.text.toString() + val password = etPassword.text.toString() + + val loginRequest = LoginRequest(email, password) + + CoroutineScope(Dispatchers.IO).launch { + try { + val response = apiService.login(loginRequest) + if (response.isSuccessful) { + val loginResponse = response.body() + val token = loginResponse?.token + + saveTokenSecurely(token) + + startActivity(Intent(this@LoginActivity, MainActivity::class.java)) + finish() + } else { + withContext(Dispatchers.Main) { + Toast.makeText(this@LoginActivity, "Invalid Credentials", Toast.LENGTH_SHORT) + .show() + } + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + Toast.makeText(this@LoginActivity, "An error occurred", Toast.LENGTH_SHORT) + .show() + } + } + } + } + + private fun saveTokenSecurely(token: String?) { + val sharedPreferences = getSharedPreferences("AUTH_PREFS", MODE_PRIVATE) + val editor = sharedPreferences.edit() + editor.putString("JWT_TOKEN", token) + editor.apply() + } + + private suspend fun isUserAuthenticated(): Boolean { + val sharedPreferences = getSharedPreferences("AUTH_PREFS", MODE_PRIVATE) + val token = sharedPreferences.getString("JWT_TOKEN", null) + val isValid = token?.let { isTokenValid(it) } + + return token != null && isValid == true + } + + private fun navigateToMainActivity() { + startActivity(Intent(this, MainActivity::class.java)) + finish() + } + + private suspend fun isTokenValid(token: String): Boolean { + return try { + val response = apiService.verifyToken("Bearer $token") + if (response.isSuccessful) { + val responseBody = response.body() + val isString = responseBody is String + return !isString + } else { + false + } + } catch (e: Exception) { + false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/atm/bondowowo/MainActivity.kt b/app/src/main/java/com/atm/bondowowo/MainActivity.kt index 2a4bb9959cf0656d911052dfba5c02217a31875d..8c31aacfddb0787b6425bfe60d37ffe0dd402dd1 100644 --- a/app/src/main/java/com/atm/bondowowo/MainActivity.kt +++ b/app/src/main/java/com/atm/bondowowo/MainActivity.kt @@ -1,13 +1,17 @@ package com.atm.bondowowo +import android.content.Intent import android.os.Bundle +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.navigation.findNavController import androidx.navigation.fragment.NavHostFragment -import androidx.navigation.ui.AppBarConfiguration -import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController import com.atm.bondowowo.databinding.ActivityMainBinding +import com.atm.bondowowo.utils.NetworkUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class MainActivity : AppCompatActivity() { @@ -18,20 +22,57 @@ class MainActivity : AppCompatActivity() { binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) - // Navigation Component - val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment - val navController = navHostFragment.navController - val appBarConfiguration = AppBarConfiguration( - setOf( - R.id.transactionFragment, R.id.scanFragment, R.id.graphFragment, R.id.settingsFragment - ) - ) - setupActionBarWithNavController(navController, appBarConfiguration) - binding.bottomNavLayout.bottomNavigation.setupWithNavController(navController) + CoroutineScope(Dispatchers.Main).launch { + if (!isUserAuthenticated()) { + navigateToLoginActivity() + } else { + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment + val navController = navHostFragment.navController + binding.bottomNavLayout.bottomNavigation.setupWithNavController(navController) + } + } } override fun onSupportNavigateUp(): Boolean { val navController = findNavController(R.id.nav_host_fragment) return navController.navigateUp() || super.onSupportNavigateUp() } + + private suspend fun isUserAuthenticated(): Boolean { + val sharedPreferences = getSharedPreferences("AUTH_PREFS", MODE_PRIVATE) + val token = sharedPreferences.getString("JWT_TOKEN", null) + + return token != null && isTokenValid(token) + } + + private fun navigateToLoginActivity() { + startActivity(Intent(this, LoginActivity::class.java)) + finish() + } + + private suspend fun isTokenValid(token: String): Boolean { + return try { + val response = NetworkUtils.apiService.verifyToken("Bearer $token") + if (response.isSuccessful) { + val responseBody = response.body() + val isString = responseBody is String + return !isString + } else { + Toast.makeText( + this@MainActivity, + "Session Expired, Please Re login", + Toast.LENGTH_SHORT + ).show() + false + } + } catch (e: Exception) { + Toast.makeText( + this@MainActivity, + "An error occurred, Please Re login", + Toast.LENGTH_SHORT + ).show() + false + } + } } diff --git a/app/src/main/java/com/atm/bondowowo/data/model/LoginRequest.kt b/app/src/main/java/com/atm/bondowowo/data/model/LoginRequest.kt new file mode 100644 index 0000000000000000000000000000000000000000..f3aedb66a3a98445b76452ac8e7a5eb27131b3b0 --- /dev/null +++ b/app/src/main/java/com/atm/bondowowo/data/model/LoginRequest.kt @@ -0,0 +1,6 @@ +package com.atm.bondowowo.data.model + +data class LoginRequest( + val email: String, + val password: String +) \ No newline at end of file diff --git a/app/src/main/java/com/atm/bondowowo/data/model/LoginResponse.kt b/app/src/main/java/com/atm/bondowowo/data/model/LoginResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..4d9a220d14f37a0e1798bb723925f0b71e446e82 --- /dev/null +++ b/app/src/main/java/com/atm/bondowowo/data/model/LoginResponse.kt @@ -0,0 +1,5 @@ +package com.atm.bondowowo.data.model + +data class LoginResponse( + val token: String // JWT +) \ No newline at end of file diff --git a/app/src/main/java/com/atm/bondowowo/data/model/VerifyResponse.kt b/app/src/main/java/com/atm/bondowowo/data/model/VerifyResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..7d5e9641f408f7021aee7010628f1a3df00c9a76 --- /dev/null +++ b/app/src/main/java/com/atm/bondowowo/data/model/VerifyResponse.kt @@ -0,0 +1,7 @@ +package com.atm.bondowowo.data.model + +data class VerifyResponse( + val nim: String, + val iat: Int, + val exp: Int +) diff --git a/app/src/main/java/com/atm/bondowowo/data/remote/ApiService.kt b/app/src/main/java/com/atm/bondowowo/data/remote/ApiService.kt new file mode 100644 index 0000000000000000000000000000000000000000..a1a54404537b435ace213c5fa017bd0f236b989e --- /dev/null +++ b/app/src/main/java/com/atm/bondowowo/data/remote/ApiService.kt @@ -0,0 +1,17 @@ +package com.atm.bondowowo.data.remote + +import com.atm.bondowowo.data.model.LoginRequest +import com.atm.bondowowo.data.model.LoginResponse +import com.atm.bondowowo.data.model.VerifyResponse +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.Header +import retrofit2.http.POST + +interface ApiService { + @POST("api/auth/login") + suspend fun login(@Body loginRequest: LoginRequest): Response<LoginResponse> + + @POST("api/auth/token") + suspend fun verifyToken(@Header("Authorization") jwtToken: String): Response<Any> +} diff --git a/app/src/main/java/com/atm/bondowowo/utils/BackgroundJWTCheckerUtil.kt b/app/src/main/java/com/atm/bondowowo/utils/BackgroundJWTCheckerUtil.kt new file mode 100644 index 0000000000000000000000000000000000000000..5af4f8c09a09732e962262245b768c1eb5e47ccb --- /dev/null +++ b/app/src/main/java/com/atm/bondowowo/utils/BackgroundJWTCheckerUtil.kt @@ -0,0 +1,50 @@ +package com.atm.bondowowo.utils + +import androidx.appcompat.app.AppCompatActivity +import android.content.Context +import kotlinx.coroutines.* + +class BackgroundJWTChecker { + + private var job: Job? = null + + interface Callback { + fun onTokenVerified() + fun onTokenInvalid() + fun onNetworkError() + } + + @OptIn(DelicateCoroutinesApi::class) + fun startChecking(context: Context, callback: Callback) { + job = GlobalScope.launch(Dispatchers.IO) { + while (isActive) { + val sharedPreferences = context.getSharedPreferences( + "AUTH_PREFS", + AppCompatActivity.MODE_PRIVATE + ) + val token = sharedPreferences.getString("JWT_TOKEN", null) + if (token != null) { + verifyToken(token, callback) + } else { + callback.onTokenInvalid() + } + delay(3 * 60 * 1000) + } + } + } + + + private suspend fun verifyToken(token: String, callback: Callback) { + try { + val response = NetworkUtils.apiService.verifyToken("Bearer $token") + if (response.isSuccessful && response.body() !is String) { + callback.onTokenVerified() + } else { + callback.onTokenInvalid() + } + } catch (e: Exception) { + callback.onNetworkError() + } + } + +} diff --git a/app/src/main/java/com/atm/bondowowo/utils/NetworkUtils.kt b/app/src/main/java/com/atm/bondowowo/utils/NetworkUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..41395daa2d346845ad92f488fb7330dd953b3a4e --- /dev/null +++ b/app/src/main/java/com/atm/bondowowo/utils/NetworkUtils.kt @@ -0,0 +1,16 @@ +package com.atm.bondowowo.utils + +// NetworkUtils.kt + +import com.atm.bondowowo.data.remote.ApiService +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object NetworkUtils { + private val retrofit = Retrofit.Builder() + .baseUrl("https://pbd-backend-2024.vercel.app/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + + val apiService: ApiService = retrofit.create(ApiService::class.java) +} diff --git a/app/src/main/res/color/bottom_nav_icon_color.xml b/app/src/main/res/color/bottom_nav_icon_color.xml new file mode 100644 index 0000000000000000000000000000000000000000..ce755c13eefc72615179bf365e86ff0ce8ede0e4 --- /dev/null +++ b/app/src/main/res/color/bottom_nav_icon_color.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@color/black" android:state_checked="true" /> + <item android:color="@color/grey" android:state_checked="false" /> +</selector> \ No newline at end of file diff --git a/app/src/main/res/color/bottom_nav_text_color.xml b/app/src/main/res/color/bottom_nav_text_color.xml new file mode 100644 index 0000000000000000000000000000000000000000..ce755c13eefc72615179bf365e86ff0ce8ede0e4 --- /dev/null +++ b/app/src/main/res/color/bottom_nav_text_color.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@color/black" android:state_checked="true" /> + <item android:color="@color/grey" android:state_checked="false" /> +</selector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_logo.png b/app/src/main/res/drawable/ic_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ec727bb458122581b1579079bc02db305b2a7492 Binary files /dev/null and b/app/src/main/res/drawable/ic_logo.png differ diff --git a/app/src/main/res/drawable/ic_scan.png b/app/src/main/res/drawable/ic_scan.png index 5bc7a45d202c7b5f24ea36464b312bfd8d1eb3d1..cc17501d3eeb34b3eaf3cbd12d48561aa0145290 100644 Binary files a/app/src/main/res/drawable/ic_scan.png and b/app/src/main/res/drawable/ic_scan.png differ diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000000000000000000000000000000000000..f4c3510fa82e5c378fba2e78c7d49147ef853d83 --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="23dp"> + + <ImageView + android:id="@+id/ivLogo" + android:layout_width="350dp" + android:layout_height="150dp" + android:layout_marginTop="80dp" + android:src="@drawable/ic_logo" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.466" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + android:contentDescription="Logo" /> + + <EditText + android:id="@+id/etUsername" + android:layout_width="0dp" + android:layout_height="60dp" + android:layout_marginTop="84dp" + android:hint="Enter your email" + android:inputType="text" + android:background="@color/textfield" + android:paddingStart="20dp" + android:paddingEnd="20dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/ivLogo" + android:autofillHints="emailAddress" /> + + <EditText + android:id="@+id/etPassword" + android:layout_width="0dp" + android:layout_height="75dp" + android:layout_marginTop="16dp" + android:hint="Enter your password" + android:inputType="textPassword" + android:background="@color/textfield" + android:paddingStart="20dp" + android:paddingEnd="20dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/etUsername" + android:autofillHints="password" /> + + <Button + android:id="@+id/btnLogin" + android:layout_width="0dp" + android:layout_height="70dp" + android:layout_marginTop="104dp" + android:backgroundTint="@color/primary1" + + android:text="Login" + android:textAllCaps="false" + android:textColor="#FFFFFF" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/etPassword" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_navigation_layout.xml b/app/src/main/res/layout/bottom_navigation_layout.xml index c31cae370d6934b0cdb7814cd8aecbfe6dccd145..ea6f4c7ba26a0a9950874998902286b23ffb6de9 100644 --- a/app/src/main/res/layout/bottom_navigation_layout.xml +++ b/app/src/main/res/layout/bottom_navigation_layout.xml @@ -3,5 +3,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/bottomNavigation" android:layout_width="match_parent" - android:layout_height="wrap_content" - app:menu="@menu/bottom_navigation_menu" /> \ No newline at end of file + android:layout_height="70dp" + android:background="@color/white" + app:itemIconTint="@color/bottom_nav_icon_color" + app:itemTextColor="@color/bottom_nav_text_color" + app:menu="@menu/bottom_navigation_menu" /> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127d327620c93d2b2d00342a68e97b98a48d..14750cdafd804111205aed7dd59c2fb2faac95d3 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,4 +7,10 @@ <color name="teal_700">#FF018786</color> <color name="black">#FF000000</color> <color name="white">#FFFFFFFF</color> + <color name="grey">#808080</color> + <color name="primary1">#012B39</color> + <color name="primary2">#1F7A8C</color> + <color name="primary3">#C0DBF8</color> + <color name="textfield">#F7F8F9</color> + </resources> \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000000000000000000000000000000000..9c62daf94e2a79bf1708a5a8c3a59991de5e5050 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> + </style> + + <style name="AppTheme.NoActionBar"> + <item name="windowActionBar">false</item> + <item name="windowNoTitle">true</item> + <item name="colorPrimary">@color/white</item> + <item name="colorPrimaryDark">@color/primary1</item> + <item name="colorAccent">@color/primary2</item> + </style> +</resources> \ No newline at end of file