diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a687cd2d79b6e52ff87a0a964b7619cd244299b2..c74673ca22d6b4da75c5de4689b785facffeb2dd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -88,6 +88,7 @@ dependencies { val koinVersion = "3.5.3" implementation("io.insert-koin:koin-core:$koinVersion") implementation("io.insert-koin:koin-android:$koinVersion") + testImplementation("io.insert-koin:koin-test-junit4:$koinVersion") // Room val roomVersion = "2.6.1" @@ -100,6 +101,10 @@ dependencies { val timberVersion = "5.0.1" implementation("com.jakewharton.timber:timber:$timberVersion") + // mockk + val mockkVersion = "1.13.10" + testImplementation("io.mockk:mockk:$mockkVersion") + 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/java/com/informatika/bondoman/MainActivity.kt b/app/src/main/java/com/informatika/bondoman/MainActivity.kt index bc87cfaa38d1b1ed626009d7e08bd61adde40a84..f0ff1371b3141acb2cc42d9b2f5e58844175d6bb 100644 --- a/app/src/main/java/com/informatika/bondoman/MainActivity.kt +++ b/app/src/main/java/com/informatika/bondoman/MainActivity.kt @@ -13,13 +13,15 @@ import com.google.android.material.bottomnavigation.BottomNavigationView import com.informatika.bondoman.databinding.ActivityMainBinding import com.informatika.bondoman.view.activity.LoginActivity import com.informatika.bondoman.prefdatastore.JWTManager +import com.informatika.bondoman.viewmodel.JWTViewModel import kotlinx.coroutines.launch +import org.koin.androidx.viewmodel.ext.android.viewModel class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding - private lateinit var jwtManager: JWTManager + private val jwtViewModel: JWTViewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -30,8 +32,6 @@ class MainActivity : AppCompatActivity() { binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) - jwtManager = JWTManager(applicationContext) - val navView: BottomNavigationView = binding.navView val navController = findNavController(R.id.nav_host_fragment_activity_main) @@ -54,8 +54,7 @@ class MainActivity : AppCompatActivity() { super.onStart() lifecycleScope.launch { - if (jwtManager.isExpired()) { - jwtManager.onLogout() + if (jwtViewModel.isExpired()) { val intent = Intent(this@MainActivity, LoginActivity::class.java) startActivity(intent) finish() diff --git a/app/src/main/java/com/informatika/bondoman/di/ApplicationModule.kt b/app/src/main/java/com/informatika/bondoman/di/ApplicationModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..ba6e0f378ed24cbfba23f2a2b3fddd28320ad147 --- /dev/null +++ b/app/src/main/java/com/informatika/bondoman/di/ApplicationModule.kt @@ -0,0 +1,17 @@ +package com.informatika.bondoman.di + +import com.informatika.bondoman.prefdatastore.JWTManager +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import org.koin.android.ext.koin.androidApplication +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module + +val applicationModule = module { + // Dependency: Moshi + single<Moshi> { + Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/di/DatabaseModule.kt b/app/src/main/java/com/informatika/bondoman/di/DatabaseModule.kt index 446f1b9e7500cc91bf46121220f8599d17eeed7f..561ed8fa2d157a5f8a87f881cd6b7b0f3f9b9255 100644 --- a/app/src/main/java/com/informatika/bondoman/di/DatabaseModule.kt +++ b/app/src/main/java/com/informatika/bondoman/di/DatabaseModule.kt @@ -1,20 +1,20 @@ package com.informatika.bondoman.di import androidx.room.Room -import com.informatika.bondoman.model.local.AppDB +import com.informatika.bondoman.model.local.AppDatabase import com.informatika.bondoman.model.local.DBConstants +import org.koin.android.ext.koin.androidApplication import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val databaseModule = module { // Dependency: AppDB single { - Room.databaseBuilder(androidContext(), AppDB::class.java, DBConstants.mName).build() + Room.databaseBuilder(androidContext(), AppDatabase::class.java, DBConstants.mName).build() } // Dependency: TransactionDao single { - val appDB: AppDB = get() - appDB.transactionDao() + get<AppDatabase>().transactionDao() } } \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/di/NetworkModule.kt b/app/src/main/java/com/informatika/bondoman/di/NetworkModule.kt index e75873f1765de8393edf1c4c5075f5f8391b62c1..09000d567e5ec7f644315ec0c83f9fdfbc3ff654 100644 --- a/app/src/main/java/com/informatika/bondoman/di/NetworkModule.kt +++ b/app/src/main/java/com/informatika/bondoman/di/NetworkModule.kt @@ -3,7 +3,6 @@ package com.informatika.bondoman.di import com.informatika.bondoman.BuildConfig import com.informatika.bondoman.model.remote.AuthService import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor @@ -31,15 +30,8 @@ val networkModule = module { .build() } - // Dependency: Moshi - single<Moshi> { - Moshi.Builder() - .add(KotlinJsonAdapterFactory()) - .build() - } - // Dependency: Retrofit - single { + single<Retrofit> { Retrofit.Builder() .baseUrl(BuildConfig.BASE_URL) .client(get()) @@ -48,8 +40,5 @@ val networkModule = module { } // Dependency: ApiService - single { - val retrofit: Retrofit = get() - retrofit.create(AuthService::class.java) - } + single<AuthService> {get<Retrofit>().create(AuthService::class.java)} } \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/di/RepositoryModule.kt b/app/src/main/java/com/informatika/bondoman/di/RepositoryModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..1b6c40cacc04c03fcbe65b87603aac5ad06c51d2 --- /dev/null +++ b/app/src/main/java/com/informatika/bondoman/di/RepositoryModule.kt @@ -0,0 +1,26 @@ +package com.informatika.bondoman.di + +import com.informatika.bondoman.model.repository.login.LoginRepository +import com.informatika.bondoman.model.repository.login.LoginRepositoryImpl +import com.informatika.bondoman.model.repository.token.TokenRepository +import com.informatika.bondoman.model.repository.token.TokenRepositoryImpl +import com.informatika.bondoman.model.repository.transaction.TransactionRepository +import com.informatika.bondoman.model.repository.transaction.TransactionRepositoryImpl +import org.koin.dsl.module + +val repositoryModule = module { + // Dependency: LoginRepository + single<LoginRepository> { + LoginRepositoryImpl(get()) + } + + single<TokenRepository> { + TokenRepositoryImpl(get()) + } + + // Dependency: TransactionRepository + single<TransactionRepository> { + TransactionRepositoryImpl(get()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/di/ViewModelModule.kt b/app/src/main/java/com/informatika/bondoman/di/ViewModelModule.kt index 4dd6ff5794bea66d83e644d8b35c88e59dddb6b7..acd0370c4c45a950fef492185177cb74c895f381 100644 --- a/app/src/main/java/com/informatika/bondoman/di/ViewModelModule.kt +++ b/app/src/main/java/com/informatika/bondoman/di/ViewModelModule.kt @@ -1,17 +1,26 @@ package com.informatika.bondoman.di -import com.informatika.bondoman.model.repository.LoginRepository +import com.informatika.bondoman.prefdatastore.JWTManager +import com.informatika.bondoman.prefdatastore.JWTManagerImpl +import com.informatika.bondoman.viewmodel.JWTViewModel import org.koin.androidx.viewmodel.dsl.viewModel import com.informatika.bondoman.viewmodel.login.LoginViewModel import com.informatika.bondoman.viewmodel.transaction.CreateTransactionViewModel import com.informatika.bondoman.viewmodel.transaction.DetailTransactionViewModel -import com.informatika.bondoman.viewmodel.transaction.UpdateTransactionViewModel import com.informatika.bondoman.viewmodel.transaction.ListTransactionViewModel +import com.informatika.bondoman.viewmodel.transaction.UpdateTransactionViewModel +import org.koin.android.ext.koin.androidContext +import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.dsl.module val viewModelModule = module { - // Dependency: LoginViewModel + // Dependency: JWTViewModel viewModel { + JWTViewModel(JWTManagerImpl(androidContext()), get()) + } + + // Dependency: LoginViewModel + viewModel<LoginViewModel> { LoginViewModel(get()) } @@ -22,12 +31,12 @@ val viewModelModule = module { // Dependency: DetailTransactionViewModel viewModel { - parameters -> DetailTransactionViewModel(get(), transaction = parameters.get()) + parameters -> DetailTransactionViewModel(get(), _id = parameters.get()) } // Dependency: UpdateTransactionViewModel viewModel { - parameters -> UpdateTransactionViewModel(get(), transaction = parameters.get()) + parameters -> UpdateTransactionViewModel(get(), _id = parameters.get()) } // Dependency: ListTransactionViewModel @@ -35,7 +44,4 @@ val viewModelModule = module { ListTransactionViewModel(get()) } - single { - LoginRepository(get()) - } } \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/model/local/AppDB.kt b/app/src/main/java/com/informatika/bondoman/model/local/AppDatabase.kt similarity index 65% rename from app/src/main/java/com/informatika/bondoman/model/local/AppDB.kt rename to app/src/main/java/com/informatika/bondoman/model/local/AppDatabase.kt index cef0784beba5dab9fa801ee55e98b2bde5cc0c40..bfd5facac987b653a1bca76ed644bc7f268dfd0f 100644 --- a/app/src/main/java/com/informatika/bondoman/model/local/AppDB.kt +++ b/app/src/main/java/com/informatika/bondoman/model/local/AppDatabase.kt @@ -3,9 +3,9 @@ package com.informatika.bondoman.model.local import androidx.room.Database import androidx.room.RoomDatabase import com.informatika.bondoman.model.local.dao.TransactionDao -import com.informatika.bondoman.model.local.entity.Transaction +import com.informatika.bondoman.model.local.entity.transaction.Transaction -@Database(entities = [Transaction::class], version = DBConstants.mVersion) -abstract class AppDB : RoomDatabase() { +@Database(entities = [Transaction::class], version = DBConstants.mVersion, exportSchema = false) +abstract class AppDatabase : RoomDatabase() { abstract fun transactionDao(): TransactionDao } \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/model/local/dao/TransactionDao.kt b/app/src/main/java/com/informatika/bondoman/model/local/dao/TransactionDao.kt index 6def362937b5d3e1c58a4558cada5ba71313ffe2..88a853a5599d66d9da2271c130868878441d4167 100644 --- a/app/src/main/java/com/informatika/bondoman/model/local/dao/TransactionDao.kt +++ b/app/src/main/java/com/informatika/bondoman/model/local/dao/TransactionDao.kt @@ -9,22 +9,22 @@ import com.informatika.bondoman.model.local.entity.transaction.Transaction @Dao interface TransactionDao { - @Query("SELECT * FROM " + DBConstants.mTableTransaction + " WHERE _id = :id") + @Query("SELECT * FROM `" + DBConstants.mTableTransaction + "` WHERE _id = :id") suspend fun get(id: Int): Transaction - @Query("SELECT * FROM " + DBConstants.mTableTransaction + " ORDER BY createdAt DESC") + @Query("SELECT * FROM `" + DBConstants.mTableTransaction + "` ORDER BY createdAt DESC") suspend fun getAll(): List<Transaction> - @Query("INSERT INTO " + DBConstants.mTableTransaction + " (title, category, amount, location) VALUES(:title, :category, :amount, :location)") + @Query("INSERT INTO `" + DBConstants.mTableTransaction + "` (title, category, amount, location) VALUES(:title, :category, :amount, :location)") suspend fun insert(title: String, category: Category, amount: Int, location: String) - @Query("INSERT INTO " + DBConstants.mTableTransaction + " (title, category, amount) VALUES(:title, :category, :amount)") + @Query("INSERT INTO `" + DBConstants.mTableTransaction + "` (title, category, amount) VALUES(:title, :category, :amount)") suspend fun insert(title: String, category: Category, amount: Int) - @Query("UPDATE " + DBConstants.mTableTransaction + " SET title = :title, amount = :amount, location = :location") + @Query("UPDATE `" + DBConstants.mTableTransaction + "` SET title = :title, amount = :amount, location = :location") suspend fun update(title: String, amount: Int, location: String) - @Query("UPDATE " + DBConstants.mTableTransaction + " SET title = :title, amount = :amount") + @Query("UPDATE `" + DBConstants.mTableTransaction + "` SET title = :title, amount = :amount") suspend fun update(title: String, amount: Int) @Delete diff --git a/app/src/main/java/com/informatika/bondoman/model/repository/login/LoginRepository.kt b/app/src/main/java/com/informatika/bondoman/model/repository/login/LoginRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..a188e80ffc347fda4ec38ce9c1845a27dbf81477 --- /dev/null +++ b/app/src/main/java/com/informatika/bondoman/model/repository/login/LoginRepository.kt @@ -0,0 +1,8 @@ +package com.informatika.bondoman.model.repository.login + +import com.informatika.bondoman.model.Resource +import com.informatika.bondoman.model.remote.AuthService + +interface LoginRepository { + suspend fun login(username: String, password: String): Resource<String> +} \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/model/repository/LoginRepository.kt b/app/src/main/java/com/informatika/bondoman/model/repository/login/LoginRepositoryImpl.kt similarity index 79% rename from app/src/main/java/com/informatika/bondoman/model/repository/LoginRepository.kt rename to app/src/main/java/com/informatika/bondoman/model/repository/login/LoginRepositoryImpl.kt index eea2c0c7c79c05adde969f83fc7ee4cef5ccd068..d732a5749c8fd1b6cc8f082c92bd423a57c3eb3a 100644 --- a/app/src/main/java/com/informatika/bondoman/model/repository/LoginRepository.kt +++ b/app/src/main/java/com/informatika/bondoman/model/repository/login/LoginRepositoryImpl.kt @@ -1,14 +1,13 @@ -package com.informatika.bondoman.model.repository +package com.informatika.bondoman.model.repository.login import com.informatika.bondoman.model.remote.request.LoginRequest -import com.informatika.bondoman.network.ApiClient import com.informatika.bondoman.model.Resource import com.informatika.bondoman.model.remote.AuthService import retrofit2.awaitResponse import java.io.IOException -class LoginRepository constructor(private val authService: AuthService) { - suspend fun login(username: String, password: String): Resource<String> { +class LoginRepositoryImpl constructor(private var authService: AuthService) : LoginRepository { + override suspend fun login(username: String, password: String): Resource<String> { try { val call = authService.login(LoginRequest(username, password)) val response = call.awaitResponse() diff --git a/app/src/main/java/com/informatika/bondoman/model/repository/token/TokenRepository.kt b/app/src/main/java/com/informatika/bondoman/model/repository/token/TokenRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..201ac8bd646643ba7403cd5d28204157cbb892cc --- /dev/null +++ b/app/src/main/java/com/informatika/bondoman/model/repository/token/TokenRepository.kt @@ -0,0 +1,7 @@ +package com.informatika.bondoman.model.repository.token + +import com.informatika.bondoman.model.Resource + +interface TokenRepository { + suspend fun token(token: String): Resource<Boolean> +} \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/model/repository/TokenRepository.kt b/app/src/main/java/com/informatika/bondoman/model/repository/token/TokenRepositoryImpl.kt similarity index 68% rename from app/src/main/java/com/informatika/bondoman/model/repository/TokenRepository.kt rename to app/src/main/java/com/informatika/bondoman/model/repository/token/TokenRepositoryImpl.kt index 19492589e7220a6e119239b547ef8ab51144adae..babff4e8fb5d9698209366e7fce19227ae0498b7 100644 --- a/app/src/main/java/com/informatika/bondoman/model/repository/TokenRepository.kt +++ b/app/src/main/java/com/informatika/bondoman/model/repository/token/TokenRepositoryImpl.kt @@ -1,15 +1,15 @@ -package com.informatika.bondoman.model.repository +package com.informatika.bondoman.model.repository.token -import android.util.Log import com.informatika.bondoman.network.ApiClient import com.informatika.bondoman.model.Resource +import com.informatika.bondoman.model.remote.AuthService import retrofit2.awaitResponse import timber.log.Timber -class TokenRepository { - suspend fun token(token: String): Resource<Boolean> { +class TokenRepositoryImpl(private var authService: AuthService) : TokenRepository { + override suspend fun token(token: String): Resource<Boolean> { try { - val call = ApiClient.authService.token("Bearer $token") + val call = authService.token("Bearer $token") val response = call.awaitResponse() Timber.tag("status").d(response.code().toString()) diff --git a/app/src/main/java/com/informatika/bondoman/model/repository/transaction/TransactionRepository.kt b/app/src/main/java/com/informatika/bondoman/model/repository/transaction/TransactionRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..5c2608a2c34675d7474a18ba0e9c4312631875fd --- /dev/null +++ b/app/src/main/java/com/informatika/bondoman/model/repository/transaction/TransactionRepository.kt @@ -0,0 +1,27 @@ +package com.informatika.bondoman.model.repository.transaction + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.informatika.bondoman.model.Resource +import com.informatika.bondoman.model.local.entity.transaction.Category +import com.informatika.bondoman.model.local.entity.transaction.Transaction + +interface TransactionRepository { + val _listTransactionLiveData: MutableLiveData<Resource<List<Transaction>>> + val listTransactionLiveData: LiveData<Resource<List<Transaction>>> + get() = listTransactionLiveData + + val _transactionLiveData: MutableLiveData<Resource<Transaction>> + val transactionLiveData: LiveData<Resource<Transaction>> + get() = transactionLiveData + + suspend fun getTransaction(id: Int) + suspend fun getAllTransaction() + suspend fun insertTransaction(title: String, category: Category, amount: Int, location: String) + suspend fun insertTransaction(title: String, category: Category, amount: Int) + suspend fun updateTransaction(title: String, amount: Int, location: String) + suspend fun updateTransaction(title: String, amount: Int) + suspend fun deleteTransaction(transaction: Transaction) + suspend fun refreshTransaction() + +} \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/model/repository/TransactionRepository.kt b/app/src/main/java/com/informatika/bondoman/model/repository/transaction/TransactionRepositoryImpl.kt similarity index 55% rename from app/src/main/java/com/informatika/bondoman/model/repository/TransactionRepository.kt rename to app/src/main/java/com/informatika/bondoman/model/repository/transaction/TransactionRepositoryImpl.kt index dc4c07462c52d8c5e24c306ae5e183cd25392f23..66f4a7d40755d43411fc8ea8a16ba6967c9241f3 100644 --- a/app/src/main/java/com/informatika/bondoman/model/repository/TransactionRepository.kt +++ b/app/src/main/java/com/informatika/bondoman/model/repository/transaction/TransactionRepositoryImpl.kt @@ -1,4 +1,4 @@ -package com.informatika.bondoman.model.repository +package com.informatika.bondoman.model.repository.transaction import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -7,14 +7,20 @@ import com.informatika.bondoman.model.local.dao.TransactionDao import com.informatika.bondoman.model.local.entity.transaction.Category import com.informatika.bondoman.model.local.entity.transaction.Transaction -class TransactionRepository(private var transactionDao: TransactionDao) { - private val _listTransactionLiveData = MutableLiveData<Resource<List<Transaction>>>() - private val listTransactionLiveData : LiveData<Resource<List<Transaction>>> = _listTransactionLiveData +class TransactionRepositoryImpl(private var transactionDao: TransactionDao) : TransactionRepository { + override var _listTransactionLiveData = MutableLiveData<Resource<List<Transaction>>>() + private set + override var listTransactionLiveData : LiveData<Resource<List<Transaction>>> = _listTransactionLiveData + private set + get() = _listTransactionLiveData - private val _transactionLiveData = MutableLiveData<Resource<Transaction>>() - private val transactionLiveData : LiveData<Resource<Transaction>> = _transactionLiveData + override var _transactionLiveData = MutableLiveData<Resource<Transaction>>() + private set + override var transactionLiveData : LiveData<Resource<Transaction>> = _transactionLiveData + private set + get() = _transactionLiveData - suspend fun getTransaction(id: Int) { + override suspend fun getTransaction(id: Int) { _transactionLiveData.postValue(Resource.Loading()) try { val transaction = transactionDao.get(id) @@ -24,7 +30,7 @@ class TransactionRepository(private var transactionDao: TransactionDao) { } } - suspend fun getAllTransaction() { + override suspend fun getAllTransaction() { _listTransactionLiveData.postValue(Resource.Loading()) try { val transactionList = transactionDao.getAll() @@ -34,27 +40,27 @@ class TransactionRepository(private var transactionDao: TransactionDao) { } } - suspend fun insertTransaction(title: String, category: Category, amount: Int, location: String) { + override suspend fun insertTransaction(title: String, category: Category, amount: Int, location: String) { transactionDao.insert(title, category, amount, location) } - suspend fun insertTransaction(title: String, category: Category, amount: Int) { + override suspend fun insertTransaction(title: String, category: Category, amount: Int) { transactionDao.insert(title, category, amount) } - suspend fun updateTransaction(title: String, amount: Int, location: String) { + override suspend fun updateTransaction(title: String, amount: Int, location: String) { transactionDao.update(title, amount, location) } - suspend fun updateTransaction(title: String, amount: Int) { + override suspend fun updateTransaction(title: String, amount: Int) { transactionDao.update(title, amount) } - suspend fun deleteTransaction(transaction: Transaction) { + override suspend fun deleteTransaction(transaction: Transaction) { transactionDao.delete(transaction) } - suspend fun refreshTransaction() { + override suspend fun refreshTransaction() { _listTransactionLiveData.postValue(Resource.Loading()) try { val transactionList = transactionDao.getAll() @@ -63,8 +69,4 @@ class TransactionRepository(private var transactionDao: TransactionDao) { _listTransactionLiveData.postValue(Resource.Error(e)) } } - - fun getListTransactionLiveData() = listTransactionLiveData - - fun getTransactionLiveData() = transactionLiveData } \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/prefdatastore/JWTManager.kt b/app/src/main/java/com/informatika/bondoman/prefdatastore/JWTManager.kt index 1e232f6c42159a29a5da184fef6c29f6f0bd810e..e7f9a281c36388738505699a060af2cc5a48f078 100644 --- a/app/src/main/java/com/informatika/bondoman/prefdatastore/JWTManager.kt +++ b/app/src/main/java/com/informatika/bondoman/prefdatastore/JWTManager.kt @@ -1,65 +1,13 @@ package com.informatika.bondoman.prefdatastore -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import com.informatika.bondoman.model.Resource -import com.informatika.bondoman.model.repository.TokenRepository import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.map -const val KEY_TOKEN = "jwt_token" -val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = KEY_TOKEN) +interface JWTManager { + fun isAuthenticated(): Flow<Boolean> -class JWTManager(private val context: Context) { - private val tokenRepository = TokenRepository() + suspend fun getToken(): String - // returns a flow of is authenticated state - fun isAuthenticated(): Flow<Boolean> { - // flow of token existence from dataStore - return context.dataStore.data.map { - it.contains(JWT_TOKEN) - } - } + suspend fun saveToken(token: String) - suspend fun getToken(): String { - return context.dataStore.data - .map { it[JWT_TOKEN] } // get a flow of token from dataStore - .firstOrNull() // transform flow to suspend - ?: throw IllegalArgumentException("no token stored") - } - - // store new token after sign in or token refresh - suspend fun saveToken(token: String) { - context.dataStore.edit { - it[JWT_TOKEN] = token - } - } - - suspend fun isExpired(): Boolean { - return when (val result = tokenRepository.token(getToken())) { - is Resource.Success -> { - !result.data - } - - else -> { - true - } - } - } - - // to call when user logs out or when refreshing the token has failed - suspend fun onLogout() { - context.dataStore.edit { - it.remove(JWT_TOKEN) - } - } - - companion object { - val JWT_TOKEN = stringPreferencesKey(KEY_TOKEN) - } + suspend fun onLogout() } \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/prefdatastore/JWTManagerImpl.kt b/app/src/main/java/com/informatika/bondoman/prefdatastore/JWTManagerImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..c7a5016406659601ff929240a4223f9b72349b37 --- /dev/null +++ b/app/src/main/java/com/informatika/bondoman/prefdatastore/JWTManagerImpl.kt @@ -0,0 +1,50 @@ +package com.informatika.bondoman.prefdatastore + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map + +const val KEY_TOKEN = "jwt_token" +val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = KEY_TOKEN) + +class JWTManagerImpl(private val context: Context) : JWTManager { + + // returns a flow of is authenticated state + override fun isAuthenticated(): Flow<Boolean> { + // flow of token existence from dataStore + return context.dataStore.data.map { + it.contains(JWT_TOKEN) + } + } + + override suspend fun getToken(): String { + return context.dataStore.data + .map { it[JWT_TOKEN] } // get a flow of token from dataStore + .firstOrNull() // transform flow to suspend + ?: throw IllegalArgumentException("no token stored") + } + + // store new token after sign in or token refresh + override suspend fun saveToken(token: String) { + context.dataStore.edit { + it[JWT_TOKEN] = token + } + } + + // to call when user logs out or when refreshing the token has failed + override suspend fun onLogout() { + context.dataStore.edit { + it.remove(JWT_TOKEN) + } + } + + companion object { + val JWT_TOKEN = stringPreferencesKey(KEY_TOKEN) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/view/activity/LoginActivity.kt b/app/src/main/java/com/informatika/bondoman/view/activity/LoginActivity.kt index 38fbe0422733f516cde3f7604a51ec2824764156..dfb2b29be7f71c3dd0010d4e4f1dc1a36a7d4b08 100644 --- a/app/src/main/java/com/informatika/bondoman/view/activity/LoginActivity.kt +++ b/app/src/main/java/com/informatika/bondoman/view/activity/LoginActivity.kt @@ -12,23 +12,33 @@ import android.view.View import android.view.inputmethod.EditorInfo import android.widget.EditText import android.widget.Toast -import androidx.activity.viewModels import com.informatika.bondoman.MainActivity import com.informatika.bondoman.databinding.ActivityLoginBinding import com.informatika.bondoman.R import com.informatika.bondoman.prefdatastore.JWTManager +import com.informatika.bondoman.prefdatastore.JWTManagerImpl +import com.informatika.bondoman.viewmodel.JWTViewModel import com.informatika.bondoman.viewmodel.login.LoginViewModel import kotlinx.coroutines.runBlocking +import org.koin.android.ext.koin.androidContext +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf +import org.koin.core.context.startKoin +import timber.log.Timber class LoginActivity : AppCompatActivity() { - private val loginViewModel: LoginViewModel by viewModels<LoginViewModel>() + private val loginViewModel: LoginViewModel by viewModel() private lateinit var binding: ActivityLoginBinding - private lateinit var jwtManager: JWTManager + private val jwtViewModel: JWTViewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) +// initTimber() +// initKoin() + // koin set android context + binding = ActivityLoginBinding.inflate(layoutInflater) setContentView(binding.root) @@ -37,7 +47,6 @@ class LoginActivity : AppCompatActivity() { val login = binding.login val loading = binding.loading - jwtManager = JWTManager(applicationContext) loginViewModel.loginFormState.observe(this@LoginActivity, Observer { val loginState = it ?: return@Observer @@ -59,9 +68,8 @@ class LoginActivity : AppCompatActivity() { } else { if (loginResult.jwtToken != null) { runBlocking { - jwtManager.saveToken(loginResult.jwtToken) + jwtViewModel.jwtManager.saveToken(loginResult.jwtToken) } - updateUiWithUser() val intent = Intent(this@LoginActivity, MainActivity::class.java) startActivity(intent) @@ -129,4 +137,5 @@ class LoginActivity : AppCompatActivity() { override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} }) } + } \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/view/activity/MainEmptyActivity.kt b/app/src/main/java/com/informatika/bondoman/view/activity/MainEmptyActivity.kt index 9e732be57ea30f1a21c1a8f3e4db9f76caa570df..3c7f5b61c20b7c7c69a887fc09c300ef0dcec212 100644 --- a/app/src/main/java/com/informatika/bondoman/view/activity/MainEmptyActivity.kt +++ b/app/src/main/java/com/informatika/bondoman/view/activity/MainEmptyActivity.kt @@ -10,17 +10,21 @@ import androidx.lifecycle.lifecycleScope import com.informatika.bondoman.BuildConfig import com.informatika.bondoman.MainActivity import com.informatika.bondoman.databinding.ActivityMainEmptyBinding +import com.informatika.bondoman.di.applicationModule import com.informatika.bondoman.di.databaseModule import com.informatika.bondoman.di.networkModule +import com.informatika.bondoman.di.repositoryModule import com.informatika.bondoman.di.viewModelModule import com.informatika.bondoman.prefdatastore.JWTManager +import com.informatika.bondoman.viewmodel.JWTViewModel import kotlinx.coroutines.launch import org.koin.android.ext.koin.androidContext +import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.context.startKoin import timber.log.Timber class MainEmptyActivity : AppCompatActivity() { - private lateinit var jwtManager: JWTManager + private val jwtViewModel: JWTViewModel by viewModel() private lateinit var binding: ActivityMainEmptyBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -38,30 +42,36 @@ class MainEmptyActivity : AppCompatActivity() { installSplashScreen() enableEdgeToEdge() + } - jwtManager = JWTManager(applicationContext) - lifecycleScope.launch { - jwtManager.isAuthenticated().collect { - if (it) { - val intent = Intent(this@MainEmptyActivity, MainActivity::class.java) - startActivity(intent) - finish() + override fun onStart() { + super.onStart() + if (BuildConfig.DEBUG) { + Timber.d("onStart") + } - } else { - val intent = Intent(this@MainEmptyActivity, LoginActivity::class.java) - startActivity(intent) - finish() - } + lifecycleScope.launch { + if (jwtViewModel.isExpired()) { + val intent = Intent(this@MainEmptyActivity, LoginActivity::class.java) + startActivity(intent) + finish() + } else { + val intent = Intent(this@MainEmptyActivity, MainActivity::class.java) + startActivity(intent) + finish() } } } private fun initKoin() { startKoin { + androidContext(this@MainEmptyActivity) modules( listOf( + applicationModule, databaseModule, networkModule, + repositoryModule, viewModelModule ) ) diff --git a/app/src/main/java/com/informatika/bondoman/view/fragment/TransactionFragment.kt b/app/src/main/java/com/informatika/bondoman/view/fragment/TransactionFragment.kt index 9cfd7105084e731ac1bc1ef76015662c707d7bbe..deb050402b8843d9b32ee8db37cdc66fc19bc327 100644 --- a/app/src/main/java/com/informatika/bondoman/view/fragment/TransactionFragment.kt +++ b/app/src/main/java/com/informatika/bondoman/view/fragment/TransactionFragment.kt @@ -5,18 +5,26 @@ import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.viewModels +import org.koin.androidx.viewmodel.ext.android.viewModel import com.informatika.bondoman.R +import com.informatika.bondoman.viewmodel.transaction.CreateTransactionViewModel +import com.informatika.bondoman.viewmodel.transaction.DetailTransactionViewModel import com.informatika.bondoman.viewmodel.transaction.ListTransactionViewModel +import com.informatika.bondoman.viewmodel.transaction.UpdateTransactionViewModel +import org.koin.core.parameter.parametersOf class TransactionFragment : Fragment() { + val _id: Int = 0 + // TODO: delete unused viewModels just dummy + private val listTransactionViewModel: ListTransactionViewModel by viewModel() + private val detailTransactionViewModel: DetailTransactionViewModel by viewModel { parametersOf(_id) } + private val updateTransactionViewModel: UpdateTransactionViewModel by viewModel { parametersOf(_id) } + private val createTransactionViewModel: CreateTransactionViewModel by viewModel() companion object { fun newInstance() = TransactionFragment() } - private val viewModel: ListTransactionViewModel by viewModels() - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? diff --git a/app/src/main/java/com/informatika/bondoman/viewmodel/JWTViewModel.kt b/app/src/main/java/com/informatika/bondoman/viewmodel/JWTViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..71fcb3440f7df81f8bccfb1254cf9a2a7eee715c --- /dev/null +++ b/app/src/main/java/com/informatika/bondoman/viewmodel/JWTViewModel.kt @@ -0,0 +1,28 @@ +package com.informatika.bondoman.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.informatika.bondoman.model.Resource +import com.informatika.bondoman.model.repository.token.TokenRepository +import com.informatika.bondoman.prefdatastore.JWTManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import timber.log.Timber + +class JWTViewModel(var jwtManager: JWTManager, private var tokenRepository: TokenRepository): ViewModel() { + suspend fun isExpired(): Boolean { + var isExpired = true + try { + val result = tokenRepository.token(jwtManager.getToken()) + if (result is Resource.Success) { + isExpired = false + } else { + jwtManager.onLogout() + } + } catch (e: Exception) { + Timber.tag("JWT").d("No token found") + } + return isExpired + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/viewmodel/login/LoginViewModel.kt b/app/src/main/java/com/informatika/bondoman/viewmodel/login/LoginViewModel.kt index 355b4847a3861d6c1ce8de2e0e5b2a41f9c339a8..0b3bf85d3d61bebf592008addde2b6caf8b49579 100644 --- a/app/src/main/java/com/informatika/bondoman/viewmodel/login/LoginViewModel.kt +++ b/app/src/main/java/com/informatika/bondoman/viewmodel/login/LoginViewModel.kt @@ -4,12 +4,12 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import android.util.Patterns -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.informatika.bondoman.model.repository.LoginRepository +import com.informatika.bondoman.model.repository.login.LoginRepositoryImpl import com.informatika.bondoman.model.Resource import com.informatika.bondoman.R +import com.informatika.bondoman.model.repository.login.LoginRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/CreateTransactionViewModel.kt b/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/CreateTransactionViewModel.kt index e8296e5b8eb9700e74aed1007de5f22a156af15a..6453553cc45c3060682cbfd77ae3bab6cc887cde 100644 --- a/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/CreateTransactionViewModel.kt +++ b/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/CreateTransactionViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.informatika.bondoman.R import com.informatika.bondoman.model.local.entity.transaction.Category -import com.informatika.bondoman.model.repository.TransactionRepository +import com.informatika.bondoman.model.repository.transaction.TransactionRepository import com.informatika.bondoman.viewmodel.transaction.helper.TransactionFormState import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/DetailTransactionViewModel.kt b/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/DetailTransactionViewModel.kt index de71945aa0b0007158e459e7cf28d9d9c683e0f7..eda6c439fea853bbed8d427f13c7c58a9d9916de 100644 --- a/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/DetailTransactionViewModel.kt +++ b/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/DetailTransactionViewModel.kt @@ -5,24 +5,24 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.informatika.bondoman.model.Resource import com.informatika.bondoman.model.local.entity.transaction.Transaction -import com.informatika.bondoman.model.repository.TransactionRepository +import com.informatika.bondoman.model.repository.transaction.TransactionRepository import kotlinx.coroutines.launch -class DetailTransactionViewModel(private var transactionRepository: TransactionRepository, transaction: Transaction) : ViewModel() { +class DetailTransactionViewModel(private var transactionRepository: TransactionRepository, _id: Int) : ViewModel() { val transactionLiveData = MutableLiveData<Resource<Transaction>>() private val observer = androidx.lifecycle.Observer<Resource<Transaction>> { transactionLiveData.postValue(it) } init { - transactionRepository.getTransactionLiveData().observeForever(observer) + transactionRepository.transactionLiveData.observeForever(observer) viewModelScope.launch { - transactionRepository.getTransaction(transaction._id) + transactionRepository.getTransaction(_id) } } override fun onCleared() { super.onCleared() - transactionRepository.getTransactionLiveData().removeObserver(observer) + transactionRepository.transactionLiveData.removeObserver(observer) } } \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/ListTransactionViewModel.kt b/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/ListTransactionViewModel.kt index 279d0e1e07c4335e8f6095583107863e0b987579..99501a6043c50bdaaf083b27611b3f83ad4911cc 100644 --- a/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/ListTransactionViewModel.kt +++ b/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/ListTransactionViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.informatika.bondoman.model.Resource import com.informatika.bondoman.model.local.entity.transaction.Transaction -import com.informatika.bondoman.model.repository.TransactionRepository +import com.informatika.bondoman.model.repository.transaction.TransactionRepository import kotlinx.coroutines.launch class ListTransactionViewModel(private var transactionRepository: TransactionRepository) : ViewModel() { @@ -15,7 +15,7 @@ class ListTransactionViewModel(private var transactionRepository: TransactionRep } init { - transactionRepository.getListTransactionLiveData().observeForever(observer) + transactionRepository.listTransactionLiveData.observeForever(observer) } fun getAllTransaction() { @@ -26,7 +26,7 @@ class ListTransactionViewModel(private var transactionRepository: TransactionRep override fun onCleared() { super.onCleared() - transactionRepository.getListTransactionLiveData().removeObserver(observer) + transactionRepository.listTransactionLiveData.removeObserver(observer) } diff --git a/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/UpdateTransactionViewModel.kt b/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/UpdateTransactionViewModel.kt index 16da0cb251f8038ea0a46eb038429c5ab6eea4fd..6576ced9fa836595198ba40de03438244b5838b6 100644 --- a/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/UpdateTransactionViewModel.kt +++ b/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/UpdateTransactionViewModel.kt @@ -5,27 +5,28 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.informatika.bondoman.model.Resource import com.informatika.bondoman.model.local.entity.transaction.Transaction -import com.informatika.bondoman.model.repository.TransactionRepository +import com.informatika.bondoman.model.repository.transaction.TransactionRepository import com.informatika.bondoman.viewmodel.transaction.helper.TransactionFormState import kotlinx.coroutines.launch -class UpdateTransactionViewModel(private var transactionRepository: TransactionRepository, transaction: Transaction) : ViewModel() { +class UpdateTransactionViewModel(private var transactionRepository: TransactionRepository, _id: Int) : ViewModel() { val transactionLiveData = MutableLiveData<Resource<Transaction>>() private val observer = androidx.lifecycle.Observer<Resource<Transaction>> { transactionLiveData.postValue(it) } + // TODO: Implement updateTransaction function private val _updateTransactionForm = MutableLiveData<TransactionFormState>() init { - transactionRepository.getTransactionLiveData().observeForever(observer) + transactionRepository.transactionLiveData.observeForever(observer) viewModelScope.launch { - transactionRepository.getTransaction(transaction._id) + transactionRepository.getTransaction(_id) } } override fun onCleared() { super.onCleared() - transactionRepository.getTransactionLiveData().removeObserver(observer) + transactionRepository.transactionLiveData.removeObserver(observer) } } \ No newline at end of file diff --git a/app/src/test/java/com/informatika/bondoman/CheckAllModules.kt b/app/src/test/java/com/informatika/bondoman/CheckAllModules.kt new file mode 100644 index 0000000000000000000000000000000000000000..9f1ea55c8e71cfd658ece845911be59cc2ab2aa4 --- /dev/null +++ b/app/src/test/java/com/informatika/bondoman/CheckAllModules.kt @@ -0,0 +1,28 @@ +package com.informatika.bondoman + +import android.app.Application +import com.informatika.bondoman.di.applicationModule +import com.informatika.bondoman.di.databaseModule +import com.informatika.bondoman.di.networkModule +import com.informatika.bondoman.di.repositoryModule +import com.informatika.bondoman.di.viewModelModule +import io.mockk.mockk +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin +import org.koin.dsl.koinApplication +import org.koin.test.KoinTest +import org.koin.test.check.checkModules +import kotlin.test.Test + +class CheckAllModules: KoinTest { + private val mockApplication: Application = mockk() + @Test + fun checkAllModules() { + // Check all modules + startKoin { + androidContext(mockApplication) + modules(listOf(applicationModule, databaseModule, networkModule, repositoryModule, viewModelModule)) + }.checkModules() + + } +} \ No newline at end of file