diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2df39f1240544c6d3460e77341b980cf2048d9c4..7d760a5e0cb43c980fe1c071a02760b50157b7ba 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -41,13 +41,29 @@ dependencies { implementation("androidx.appcompat:appcompat:1.5.1") implementation("com.google.android.material:material:1.9.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1") + implementation("androidx.navigation:navigation-fragment-ktx:2.5.3") implementation("androidx.navigation:navigation-ui-ktx:2.5.3") + implementation("androidx.legacy:legacy-support-v4:1.0.0") + implementation("androidx.fragment:fragment-ktx:1.5.7") implementation("androidx.annotation:annotation:1.6.0") + + implementation("com.squareup.retrofit2:retrofit:2.9.0") + + implementation(platform("com.squareup.okhttp3:okhttp-bom:4.12.0")) + implementation("com.squareup.okhttp3:okhttp") + implementation("com.squareup.okhttp3:logging-interceptor") + + implementation("com.squareup.moshi:moshi:1.15.0") + implementation("com.squareup.moshi:moshi-kotlin:1.15.0") + implementation("com.squareup.retrofit2:converter-moshi:2.9.0") + implementation("androidx.preference:preference:1.2.1") + 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 4190e448b161c2f9bcf48d3b2a49d372657c9b1a..d03d4fa9d491bbdeb66240931f2e94074a13a73e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ <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:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" @@ -13,15 +15,19 @@ android:theme="@style/Theme.BondoYap" tools:targetApi="31"> <activity - android:name=".MainActivity" - android:exported="true" - android:label="@string/app_name"> + android:name=".ui.login.LoginActivity" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> + + <activity + android:name=".MainActivity" + android:exported="true"> + </activity> + </application> </manifest> \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ApiClient.kt b/app/src/main/java/com/example/bondoyap/ApiClient.kt new file mode 100644 index 0000000000000000000000000000000000000000..d1858bc3d5c3c4d5ec7b2a3de8f17f3427e5f6eb --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ApiClient.kt @@ -0,0 +1,41 @@ +package com.example.bondoyap + +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST + + +object Constants { + const val BASE_URL: String = "https://pbd-backend-2024.vercel.app/api/" + const val SHARED_PREFS_NAME = "Prefs" +} + +interface ApiClient{ + @POST("auth/login") + fun login( + @Body loginRequest: LoginRequest + ): Call<LoginResponse> + + @POST("auth/token") + fun verifyToken( + ): Call<TokenResponse> + + @POST("bill/upload") + fun uploadPicture( + ) +} + +data class LoginRequest( + val email: String, + val password: String +) + +data class LoginResponse( + val token: String +) + +data class TokenResponse( + val nim: String, + val iat: Number, + val exp: Number +) \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/MainActivity.kt b/app/src/main/java/com/example/bondoyap/MainActivity.kt index 1517b69c822338dbbd87d5d5c06398ab414e484b..7002b983d3102e2c405b8e123671fa18da86b984 100644 --- a/app/src/main/java/com/example/bondoyap/MainActivity.kt +++ b/app/src/main/java/com/example/bondoyap/MainActivity.kt @@ -1,6 +1,7 @@ package com.example.bondoyap import android.content.Context +import android.content.Intent import android.content.SharedPreferences import android.os.Bundle import com.google.android.material.bottomnavigation.BottomNavigationView @@ -9,17 +10,25 @@ import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController +import com.example.bondoyap.Constants.SHARED_PREFS_NAME import com.example.bondoyap.databinding.ActivityMainBinding +import com.example.bondoyap.ui.login.LoginActivity class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var sharedPreferences: SharedPreferences - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + sharedPreferences = getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE) + + if (!isLoggedIn()) { + val intent = Intent(this, LoginActivity::class.java) + startActivity(intent) + finish() + } binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) @@ -27,29 +36,14 @@ class MainActivity : AppCompatActivity() { val navController = findNavController(R.id.nav_host_fragment_activity_main) - sharedPreferences = getSharedPreferences("MyPrefs", Context.MODE_PRIVATE) - - if (!isLoggedIn()) { - navController.navigate(R.id.navigation_login) - } val appBarConfiguration = AppBarConfiguration(setOf( R.id.navigation_settings, R.id.navigation_transactions, R.id.navigation_scanner, - R.id.navigation_graph, R.id.navigation_login)) + R.id.navigation_graph)) setupActionBarWithNavController(navController, appBarConfiguration) navView.setupWithNavController(navController) - - navController.addOnDestinationChangedListener { _, destination, _ -> - // Disable bottom navigation when on the login screen - if (destination.id == R.id.navigation_login) { - navView.visibility = BottomNavigationView.GONE - } else { - navView.visibility = BottomNavigationView.VISIBLE - } - } } - private fun isLoggedIn(): Boolean { return sharedPreferences.getBoolean("isLoggedIn", false) } diff --git a/app/src/main/java/com/example/bondoyap/ui/login/LoggedInUserView.kt b/app/src/main/java/com/example/bondoyap/ui/login/LoggedInUserView.kt index 127052153ac667ee17007bf12a87861dfd93308f..106a5999d4bc31f2babf13d5b526b17ff648626e 100644 --- a/app/src/main/java/com/example/bondoyap/ui/login/LoggedInUserView.kt +++ b/app/src/main/java/com/example/bondoyap/ui/login/LoggedInUserView.kt @@ -4,6 +4,6 @@ package com.example.bondoyap.ui.login * User details post authentication that is exposed to the UI */ data class LoggedInUserView( - val displayName: String + val email: String //... other data fields that may be accessible to the UI ) \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/login/LoginActivity.kt b/app/src/main/java/com/example/bondoyap/ui/login/LoginActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..dc379ed4e8b62e97c645bf7be327eab0c35480ad --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/login/LoginActivity.kt @@ -0,0 +1,157 @@ +package com.example.bondoyap.ui.login + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.annotation.StringRes +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.example.bondoyap.Constants.SHARED_PREFS_NAME +import com.example.bondoyap.MainActivity +import com.example.bondoyap.R +import com.example.bondoyap.databinding.ActivityLoginBinding + +class LoginActivity : AppCompatActivity() { + + private lateinit var loginViewModel: LoginViewModel + private lateinit var binding: ActivityLoginBinding + private lateinit var sharedPreferences: SharedPreferences + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + sharedPreferences = getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE) + + + + if (isLoggedIn()) { + navigateToMainActivity() + finish() + } else { + binding = ActivityLoginBinding.inflate(layoutInflater) + setContentView(binding.root) + + val emailEditText = binding.email + val passwordEditText = binding.password + val loginButton = binding.login + val loadingProgressBar = binding.loading + + val factory = LoginViewModelFactory(this) + loginViewModel = ViewModelProvider(this, factory)[LoginViewModel::class.java] + + // skip login + + loginViewModel.loginFormState.observe(this@LoginActivity, Observer { + val loginState = it ?: return@Observer + + // disable login button unless both username / password is valid + loginButton.isEnabled = loginState.isDataValid + + if (loginState.emailError != null) { + emailEditText.error = getString(loginState.emailError) + } + if (loginState.passwordError != null) { + passwordEditText.error = getString(loginState.passwordError) + } + }) + + loginViewModel.loginResult.observe(this@LoginActivity, Observer { + val loginResult = it ?: return@Observer + + loadingProgressBar.visibility = View.GONE + if (loginResult.error != null) { + showLoginFailed(loginResult.error) + } + if (loginResult.success != null) { + updateUiWithUser(loginResult.success) + setResult(RESULT_OK) + finish() + } + }) + + emailEditText.afterTextChanged { + loginViewModel.loginDataChanged( + emailEditText.text.toString(), + passwordEditText.text.toString() + ) + } + + passwordEditText.apply { + afterTextChanged { + loginViewModel.loginDataChanged( + emailEditText.text.toString(), + passwordEditText.text.toString() + ) + } + + setOnEditorActionListener { _, actionId, _ -> + when (actionId) { + EditorInfo.IME_ACTION_DONE -> + loginViewModel.login( + emailEditText.text.toString(), + passwordEditText.text.toString() + ) + } + false + } + + loginButton.setOnClickListener { + loadingProgressBar.visibility = View.VISIBLE + loginViewModel.login( + emailEditText.text.toString(), + passwordEditText.text.toString() + ) + } + } + } + } + + private fun updateUiWithUser(model: LoggedInUserView) { + val welcome = getString(R.string.welcome) + "\n" + model.email + // TODO : initiate successful logged in experience + Toast.makeText( + applicationContext, + welcome, + Toast.LENGTH_SHORT + ).show() + navigateToMainActivity() + } + + private fun showLoginFailed(@StringRes errorString: Int) { + Toast.makeText(applicationContext, errorString, Toast.LENGTH_SHORT).show() + } + + private fun navigateToMainActivity() { + val intent = Intent(this, MainActivity::class.java) + startActivity(intent) + finish() + } + + private fun isLoggedIn(): Boolean { + return sharedPreferences.getBoolean("isLoggedIn", false) + } +} + +/** + * Extension function to simplify setting an afterTextChanged action to EditText components. + */ +fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) { + this.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(editable: Editable?) { + afterTextChanged.invoke(editable.toString()) + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + + 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/example/bondoyap/ui/login/LoginFragment.kt b/app/src/main/java/com/example/bondoyap/ui/login/LoginFragment.kt deleted file mode 100644 index 2fa73d7b619d82ec9bcd99dd44731ea250e8830a..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/example/bondoyap/ui/login/LoginFragment.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.example.bondoyap.ui.login - -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider -import androidx.annotation.StringRes -import androidx.fragment.app.Fragment -import android.os.Bundle -import android.text.Editable -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import android.widget.Toast -import androidx.navigation.fragment.findNavController -import com.example.bondoyap.R -import com.example.bondoyap.databinding.FragmentLoginBinding - -class LoginFragment : Fragment() { - - private lateinit var loginViewModel: LoginViewModel - private var _binding: FragmentLoginBinding? = null - - // This property is only valid between onCreateView and - // onDestroyView. - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - - _binding = FragmentLoginBinding.inflate(inflater, container, false) - return binding.root - - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - loginViewModel = ViewModelProvider(this, LoginViewModelFactory()) - .get(LoginViewModel::class.java) - - val emailEditText = binding.email - val passwordEditText = binding.password - val loginButton = binding.login - val loadingProgressBar = binding.loading - - loginViewModel.loginFormState.observe(viewLifecycleOwner, - Observer { loginFormState -> - if (loginFormState == null) { - return@Observer - } - loginButton.isEnabled = loginFormState.isDataValid - loginFormState.emailError?.let { - emailEditText.error = getString(it) - } - loginFormState.passwordError?.let { - passwordEditText.error = getString(it) - } - }) - - loginViewModel.loginResult.observe(viewLifecycleOwner, - Observer { loginResult -> - loginResult ?: return@Observer - loadingProgressBar.visibility = View.GONE - loginResult.error?.let { - showLoginFailed(it) - } - loginResult.success?.let { - updateUiWithUser(it) - requireActivity().actionBar?.setDisplayHomeAsUpEnabled(false) - navigateToTransactionFragment() - } - }) - - val afterTextChangedListener = object : TextWatcher { - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { - // ignore - } - - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - // ignore - } - - override fun afterTextChanged(s: Editable) { - loginViewModel.loginDataChanged( - emailEditText.text.toString(), - passwordEditText.text.toString() - ) - } - } - emailEditText.addTextChangedListener(afterTextChangedListener) - passwordEditText.addTextChangedListener(afterTextChangedListener) - passwordEditText.setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_DONE) { - loginViewModel.login( - emailEditText.text.toString(), - passwordEditText.text.toString() - ) - } - false - } - - loginButton.setOnClickListener { - loadingProgressBar.visibility = View.VISIBLE - loginViewModel.login( - emailEditText.text.toString(), - passwordEditText.text.toString() - ) - } - } - - private fun updateUiWithUser(model: LoggedInUserView) { - val welcome = getString(R.string.welcome) + model.displayName - // TODO : initiate successful logged in experience - val appContext = context?.applicationContext ?: return - Toast.makeText(appContext, welcome, Toast.LENGTH_LONG).show() - } - - private fun showLoginFailed(@StringRes errorString: Int) { - val appContext = context?.applicationContext ?: return - Toast.makeText(appContext, errorString, Toast.LENGTH_LONG).show() - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - private fun navigateToTransactionFragment() { - findNavController().navigate(R.id.navigation_transactions) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModel.kt b/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModel.kt index 11feb87bc1487a0f9624658311d6fe0bfc11b1c6..fedbf7d9cd2c6686bf8dc47cdf25d6cf84422a40 100644 --- a/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModel.kt +++ b/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModel.kt @@ -4,9 +4,11 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import android.util.Patterns +import androidx.lifecycle.viewModelScope import com.example.bondoyap.R import com.example.bondoyap.ui.login.data.LoginRepository import com.example.bondoyap.ui.login.data.Result +import kotlinx.coroutines.launch class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() { @@ -17,13 +19,14 @@ class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() val loginResult: LiveData<LoginResult> = _loginResult fun login(email: String, password: String) { - val result = loginRepository.login(email, password) - - if (result is Result.Success) { - _loginResult.value = - LoginResult(success = LoggedInUserView(displayName = result.data.displayName)) - } else { - _loginResult.value = LoginResult(error = R.string.login_failed) + viewModelScope.launch { + val result = loginRepository.login(email, password) + if (result is Result.Success) { + _loginResult.value = + LoginResult(success = LoggedInUserView(email = result.data.email)) + } else { + _loginResult.value = LoginResult(error = R.string.login_failed) + } } } diff --git a/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModelFactory.kt b/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModelFactory.kt index de741ade55806d4f3612cf2ef705e3d13874dc9d..b0e9039da4e6694319d086bdc6fca269138c6c17 100644 --- a/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModelFactory.kt +++ b/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModelFactory.kt @@ -1,22 +1,21 @@ package com.example.bondoyap.ui.login +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.example.bondoyap.ui.login.data.LoginDataSource import com.example.bondoyap.ui.login.data.LoginRepository -/** - * ViewModel provider factory to instantiate LoginViewModel. - * Required given LoginViewModel has a non-empty constructor - */ -class LoginViewModelFactory : ViewModelProvider.Factory { +class LoginViewModelFactory(private val context: Context) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { if (modelClass.isAssignableFrom(LoginViewModel::class.java)) { return LoginViewModel( loginRepository = LoginRepository( - dataSource = LoginDataSource() + dataSource = LoginDataSource(), + context = context + ) ) as T } diff --git a/app/src/main/java/com/example/bondoyap/ui/login/data/LoginDataSource.kt b/app/src/main/java/com/example/bondoyap/ui/login/data/LoginDataSource.kt index 3e221c0a19f9dc26f316fee64c9cfae5734e84f1..cf54c3009a664f4756b194be6d01189ef6d85582 100644 --- a/app/src/main/java/com/example/bondoyap/ui/login/data/LoginDataSource.kt +++ b/app/src/main/java/com/example/bondoyap/ui/login/data/LoginDataSource.kt @@ -1,6 +1,15 @@ package com.example.bondoyap.ui.login.data +import com.example.bondoyap.ApiClient +import com.example.bondoyap.Constants +import com.example.bondoyap.LoginRequest import com.example.bondoyap.ui.login.data.model.LoggedInUser +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory import java.io.IOException /** @@ -8,17 +17,49 @@ import java.io.IOException */ class LoginDataSource { - fun login(email: String, password: String): Result<LoggedInUser> { + suspend fun login(email: String, password: String): Result<LoggedInUser> { return try { - // TODO: handle loggedInUser authentication - val fakeUser = LoggedInUser(java.util.UUID.randomUUID().toString(), "Jane Doe") - Result.Success(fakeUser) + + val token = postLogin(email, password) + + val parts = email.split("@") + val nim = parts[0] + + val user = LoggedInUser(nim, email, token) + + Result.Success(user) } catch (e: Throwable) { Result.Error(IOException("Error logging in", e)) } } - fun logout() { - // TODO: revoke authentication + private suspend fun postLogin(email: String, password: String):String { + + val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + + val api = Retrofit.Builder() + .baseUrl(Constants.BASE_URL) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + + val apiClient = api.create(ApiClient::class.java) + + return withContext(Dispatchers.IO) { + try { + val loginRequest = LoginRequest(email, password) + val response = apiClient.login(loginRequest).execute() + + if (response.isSuccessful) { + val loginResponse = response.body() + loginResponse?.token ?: throw IOException("Token not received") + } else { + throw IOException("${response.code()} ${response.message()}") + } + } catch (e: Exception) { + throw IOException("Login failed: ${e.message}") + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/login/data/LoginRepository.kt b/app/src/main/java/com/example/bondoyap/ui/login/data/LoginRepository.kt index 840ffe51ce4b5e3a307ad9c7c0442c11d14642ee..bcc2777cdcfc8f2fb2f901308341f767067d0a7b 100644 --- a/app/src/main/java/com/example/bondoyap/ui/login/data/LoginRepository.kt +++ b/app/src/main/java/com/example/bondoyap/ui/login/data/LoginRepository.kt @@ -1,34 +1,29 @@ package com.example.bondoyap.ui.login.data import com.example.bondoyap.ui.login.data.model.LoggedInUser +import android.content.Context +import android.content.SharedPreferences +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory /** * Class that requests authentication and user information from the remote data source and * maintains an in-memory cache of login status and user credentials information. */ -class LoginRepository(val dataSource: LoginDataSource) { +class LoginRepository(val dataSource: LoginDataSource, context: Context) { - // in-memory cache of the loggedInUser object - var user: LoggedInUser? = null - private set + private val sharedPreferences: SharedPreferences = context.getSharedPreferences("Prefs", Context.MODE_PRIVATE) + private val editor: SharedPreferences.Editor = sharedPreferences.edit() - val isLoggedIn: Boolean - get() = user != null - - init { - // If user credentials will be cached in local storage, it is recommended it be encrypted - // @see https://developer.android.com/training/articles/keystore - user = null - } + private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + private val userAdapter: JsonAdapter<LoggedInUser> = moshi.adapter(LoggedInUser::class.java) - fun logout() { - user = null - dataSource.logout() - } + val isLoggedIn: Boolean + get() = sharedPreferences.getBoolean("isLoggedIn", false) - fun login(email: String, password: String): Result<LoggedInUser> { - // handle login + suspend fun login(email: String, password: String): Result<LoggedInUser> { val result = dataSource.login(email, password) if (result is Result.Success) { @@ -38,8 +33,22 @@ class LoginRepository(val dataSource: LoginDataSource) { return result } + fun logout(){ + editor.clear() + editor.apply() + } + + fun getUser(): LoggedInUser? { + val userJson = sharedPreferences.getString("loggedInUser", null) + return userJson?.let { + userAdapter.fromJson(it) + } + } + private fun setLoggedInUser(loggedInUser: LoggedInUser) { - this.user = loggedInUser + val userJson = userAdapter.toJson(loggedInUser) + editor.putString("loggedInUser", userJson).apply() + editor.putBoolean("isLoggedIn", true).apply() // If user credentials will be cached in local storage, it is recommended it be encrypted // @see https://developer.android.com/training/articles/keystore } diff --git a/app/src/main/java/com/example/bondoyap/ui/login/data/model/LoggedInUser.kt b/app/src/main/java/com/example/bondoyap/ui/login/data/model/LoggedInUser.kt index 10a9aa93f64d280b3861bd282307ab0213e011df..78d7e033176d0673dfb6016ebacf1d2c54c256e0 100644 --- a/app/src/main/java/com/example/bondoyap/ui/login/data/model/LoggedInUser.kt +++ b/app/src/main/java/com/example/bondoyap/ui/login/data/model/LoggedInUser.kt @@ -5,5 +5,6 @@ package com.example.bondoyap.ui.login.data.model */ data class LoggedInUser( val userId: String, - val displayName: String + val email: String, + val token: String ) \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/settings/SettingsFragment.kt b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsFragment.kt index 9433c1071c0e9bc4cd8fcac9326e5c7b3acc43cc..8018684cc1dabee268479d5c16e1e2029ee1dba6 100644 --- a/app/src/main/java/com/example/bondoyap/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsFragment.kt @@ -1,38 +1,78 @@ package com.example.bondoyap.ui.settings +import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView +import android.widget.Toast import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import com.example.bondoyap.databinding.FragmentSettingsBinding +import com.example.bondoyap.ui.login.LoginActivity class SettingsFragment : Fragment() { private var _binding: FragmentSettingsBinding? = null - - // This property is only valid between onCreateView and - // onDestroyView. private val binding get() = _binding!! + private lateinit var settingsViewModel: SettingsViewModel override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? ): View { - val settingsViewModel = - ViewModelProvider(this).get(SettingsViewModel::class.java) - _binding = FragmentSettingsBinding.inflate(inflater, container, false) - val root: View = binding.root + return binding.root + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val factory = context?.let { SettingsViewModelFactory(it) } + if (factory != null) { + settingsViewModel = ViewModelProvider(this, factory)[SettingsViewModel::class.java] + } else { + throw IllegalStateException("Context is null. Cannot create SettingsViewModelFactory.") + } + val textView = binding.textSettings + val logoutButton = binding.logoutButton + val randomButton = binding.randomTransactions + val saveButton = binding.saveTransactions + val sendButton = binding.sendTransactions + + settingsViewModel.getUser()?.let { user -> + val loggedInUserText = "Masuk dengan akun:\n ${user.email}" + textView.text = loggedInUserText + } + val appContext = context?.applicationContext + + logoutButton.setOnClickListener { + settingsViewModel.getUser() + settingsViewModel.logout() +// findNavController().navigate(R.id.navigation_login) - val textView: TextView = binding.textSettings - settingsViewModel.text.observe(viewLifecycleOwner) { - textView.text = it + val activity = requireActivity() + val intent = Intent(activity, LoginActivity::class.java) + Toast.makeText(appContext, "Logout Sukses!", Toast.LENGTH_SHORT).show() + activity.startActivity(intent) + activity.finish() } - return root + + randomButton.setOnClickListener { + Toast.makeText(appContext, "Membuat transaksi random ...", Toast.LENGTH_SHORT).show() + //todo + } + + saveButton.setOnClickListener{ + Toast.makeText(appContext, "Menyimpan transaksi...", Toast.LENGTH_SHORT).show() + //todo + } + + sendButton.setOnClickListener{ + Toast.makeText(appContext, "Mengirimkan transaksi ...", Toast.LENGTH_SHORT).show() + //todo + } + } override fun onDestroyView() { diff --git a/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModel.kt index 02f1e625446ea7b7ced397b4d01dea34bc08185c..6038adcbe40a12ade0663f68d32d295401f2690a 100644 --- a/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModel.kt @@ -3,11 +3,21 @@ package com.example.bondoyap.ui.settings import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import com.example.bondoyap.ui.login.data.LoginRepository +import com.example.bondoyap.ui.login.data.model.LoggedInUser -class SettingsViewModel : ViewModel() { +class SettingsViewModel(private val loginRepository: LoginRepository) : ViewModel() { private val _text = MutableLiveData<String>().apply { value = "This is Settings Fragment" } val text: LiveData<String> = _text + + fun logout(){ + loginRepository.logout() + } + + fun getUser(): LoggedInUser? { + return loginRepository.getUser() + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModelFactory.kt b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModelFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..b3b7f9747a79b29fc94a46c775d0ee604a767c45 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModelFactory.kt @@ -0,0 +1,23 @@ +package com.example.bondoyap.ui.settings + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.bondoyap.ui.login.data.LoginDataSource +import com.example.bondoyap.ui.login.data.LoginRepository + +class SettingsViewModelFactory(private val context: Context) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create(modelClass: Class<T>): T { + if (modelClass.isAssignableFrom(SettingsViewModel::class.java)) { + return SettingsViewModel( + loginRepository = LoginRepository( + dataSource = LoginDataSource(), + context = context + ) + ) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/Transactions.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/Transactions.kt new file mode 100644 index 0000000000000000000000000000000000000000..152e4636c4a171aecc4e5ee1c76438ab253c8e97 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/Transactions.kt @@ -0,0 +1,21 @@ +package com.example.bondoyap.ui.transactions + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "transactions") +data class Transactions( + @ColumnInfo(name = "judul") + val judul: String, + + @ColumnInfo(name = "nominal") + val nominal: Double, + + @ColumnInfo(name = "is_pemasukan") + val isPemasukan: Boolean, + + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "id") + val id: Int = 0 +) \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsDao.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..3e38008bd4e23137e0fe1a42b11a3c7a4b97c06e --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsDao.kt @@ -0,0 +1,23 @@ +package com.example.bondoyap.ui.transactions + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Query +import androidx.room.Upsert + +@Dao +interface TransactionsDao { + + @Upsert + suspend fun upsertTransaction(transaksi: Transactions) + + @Delete + suspend fun deleteTransaction(transaksi: Transactions) + + @Query("SELECT * FROM transactions") + fun getTransaction(): LiveData<List<Transactions>> + + @Query("SELECT * FROM transactions WHERE transactions.id == :transactionsId") + fun getTransactionById(transactionsId: Int): Transactions +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsDatabase.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsDatabase.kt new file mode 100644 index 0000000000000000000000000000000000000000..3eed5518b23481bcfd4f01339fdc67a25b5aeb55 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsDatabase.kt @@ -0,0 +1,13 @@ +package com.example.bondoyap.ui.transactions + +import androidx.room.Database +import androidx.room.RoomDatabase + +@Database( + entities = [Transactions::class], + version = 1 +) +abstract class TransactionsDatabase: RoomDatabase() { + + abstract val dao: TransactionsDao +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsFragment.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsFragment.kt index dbce163a4779b3b0d8184a624dd5c233ded521a3..d3ae405e2a6d4269a8a55df39b32bbc1baee418f 100644 --- a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsFragment.kt +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsFragment.kt @@ -7,8 +7,11 @@ import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope import com.example.bondoyap.databinding.FragmentTransactionsBinding +import kotlinx.coroutines.launch class TransactionsFragment : Fragment() { @@ -23,9 +26,6 @@ class TransactionsFragment : Fragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { - val transactionsViewModel = - ViewModelProvider(this).get(TransactionsViewModel::class.java) - _binding = FragmentTransactionsBinding.inflate(inflater, container, false) val categories = arrayOf("Pemasukan", "Pengeluaran") @@ -34,6 +34,20 @@ class TransactionsFragment : Fragment() { adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) binding.spinnerKategori.adapter = adapter + binding.buttonSimpan.setOnClickListener { + val judul: String = binding.editTextJudul.text.toString() + val nominal: Double? = binding.editTextNominal.text.toString().toDoubleOrNull() + val isPemasukan: Boolean = binding.spinnerKategori.selectedItemPosition == 0 + + viewModel.viewModelScope.launch { + viewModel.onSaveButtonClicked(judul, nominal, isPemasukan) + } + + binding.editTextJudul.text.clear() + binding.editTextNominal.text.clear() + binding.spinnerKategori.setSelection(0) + } + return binding.root } diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsViewModel.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsViewModel.kt index 1a790c6f8709dc3c2f42d091b860cde6389501c3..123d0ef560874a060b9099916c2a741dcb65ce91 100644 --- a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsViewModel.kt +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsViewModel.kt @@ -1,13 +1,31 @@ package com.example.bondoyap.ui.transactions -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update -class TransactionsViewModel : ViewModel() { +class TransactionsViewModel( + private val dao: TransactionsDao +): ViewModel() { + suspend fun onSaveButtonClicked( + judul: String = "", + nominal: Double? = 0.0, + isPemasukan: Boolean = true + ) { + if(nominal == null) { + return + } + if(judul.isBlank() || nominal.isNaN()) { + return + } - private val _text = MutableLiveData<String>().apply { - value = "This is Transactions Fragment" + val transaction = Transactions( + judul = judul, + nominal = nominal, + isPemasukan = isPemasukan + ) + + dao.upsertTransaction(transaction) } - val text: LiveData<String> = _text + } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout-w1240dp/activity_login.xml similarity index 81% rename from app/src/main/res/layout/fragment_login.xml rename to app/src/main/res/layout-w1240dp/activity_login.xml index 3835370f0dd243f7d2448961484710a245cdf4d5..cf194090807d97abade273046c604fba5498297e 100644 --- a/app/src/main/res/layout/fragment_login.xml +++ b/app/src/main/res/layout-w1240dp/activity_login.xml @@ -2,21 +2,20 @@ <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" - android:paddingLeft="@dimen/fragment_horizontal_margin" - android:paddingTop="@dimen/fragment_vertical_margin" - android:paddingRight="@dimen/fragment_horizontal_margin" - android:paddingBottom="@dimen/fragment_vertical_margin" - tools:context=".ui.login.LoginFragment"> + android:paddingLeft="@dimen/activity_horizontal_margin" + android:paddingTop="@dimen/activity_vertical_margin" + android:paddingRight="@dimen/activity_horizontal_margin" + android:paddingBottom="@dimen/activity_vertical_margin" + tools:context=".ui.login.LoginActivity"> <EditText android:id="@+id/email" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="24dp" android:layout_marginTop="96dp" - android:layout_marginEnd="24dp" android:autofillHints="@string/prompt_email" android:hint="@string/prompt_email" android:inputType="textEmailAddress" @@ -29,9 +28,7 @@ android:id="@+id/password" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="24dp" android:layout_marginTop="8dp" - android:layout_marginEnd="24dp" android:autofillHints="@string/prompt_password" android:hint="@string/prompt_password" android:imeActionLabel="@string/action_sign_in_short" @@ -47,9 +44,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="start" - android:layout_marginStart="48dp" android:layout_marginTop="16dp" - android:layout_marginEnd="48dp" android:layout_marginBottom="64dp" android:enabled="false" android:text="@string/action_sign_in" @@ -64,9 +59,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:layout_marginStart="32dp" android:layout_marginTop="64dp" - android:layout_marginEnd="32dp" android:layout_marginBottom="64dp" android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" @@ -74,4 +67,5 @@ app:layout_constraintStart_toStartOf="@+id/password" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.3" /> + </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout-w936dp/activity_login.xml b/app/src/main/res/layout-w936dp/activity_login.xml new file mode 100644 index 0000000000000000000000000000000000000000..0b647885c5c88180f2fe880b915c8bf1214a9911 --- /dev/null +++ b/app/src/main/res/layout-w936dp/activity_login.xml @@ -0,0 +1,78 @@ +<?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" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingLeft="@dimen/activity_horizontal_margin" + android:paddingTop="@dimen/activity_vertical_margin" + android:paddingRight="@dimen/activity_horizontal_margin" + android:paddingBottom="@dimen/activity_vertical_margin" + tools:context=".ui.login.LoginActivity"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="840dp" + android:layout_height="match_parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent"> + + <EditText + android:id="@+id/email" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="96dp" + android:autofillHints="@string/prompt_email" + android:hint="@string/prompt_email" + android:inputType="textEmailAddress" + android:selectAllOnFocus="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <EditText + android:id="@+id/password" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:autofillHints="@string/prompt_password" + android:hint="@string/prompt_password" + android:imeActionLabel="@string/action_sign_in_short" + android:imeOptions="actionDone" + android:inputType="textPassword" + android:selectAllOnFocus="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/email" /> + + <Button + android:id="@+id/login" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:layout_marginTop="16dp" + android:layout_marginBottom="64dp" + android:enabled="false" + android:text="@string/action_sign_in" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/password" + app:layout_constraintVertical_bias="0.2" /> + + <ProgressBar + android:id="@+id/loading" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginTop="64dp" + android:layout_marginBottom="64dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="@+id/password" + app:layout_constraintStart_toStartOf="@+id/password" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.3" /> + + </androidx.constraintlayout.widget.ConstraintLayout> +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file 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..cf194090807d97abade273046c604fba5498297e --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,71 @@ +<?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" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingLeft="@dimen/activity_horizontal_margin" + android:paddingTop="@dimen/activity_vertical_margin" + android:paddingRight="@dimen/activity_horizontal_margin" + android:paddingBottom="@dimen/activity_vertical_margin" + tools:context=".ui.login.LoginActivity"> + + <EditText + android:id="@+id/email" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="96dp" + android:autofillHints="@string/prompt_email" + android:hint="@string/prompt_email" + android:inputType="textEmailAddress" + android:selectAllOnFocus="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <EditText + android:id="@+id/password" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:autofillHints="@string/prompt_password" + android:hint="@string/prompt_password" + android:imeActionLabel="@string/action_sign_in_short" + android:imeOptions="actionDone" + android:inputType="textPassword" + android:selectAllOnFocus="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/email" /> + + <Button + android:id="@+id/login" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:layout_marginTop="16dp" + android:layout_marginBottom="64dp" + android:enabled="false" + android:text="@string/action_sign_in" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/password" + app:layout_constraintVertical_bias="0.2" /> + + <ProgressBar + android:id="@+id/loading" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginTop="64dp" + android:layout_marginBottom="64dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="@+id/password" + app:layout_constraintStart_toStartOf="@+id/password" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.3" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index 5f90bc6adda3319a6073dd79512e10f163e686f2..9689b01e558f04f59984c293faac2e1e771ad55c 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -15,8 +15,51 @@ android:layout_marginEnd="8dp" android:textAlignment="center" android:textSize="20sp" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"/> + + <Button + android:id="@+id/random_transactions" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/random_transactions" + app:layout_constraintTop_toBottomOf="@+id/text_settings" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:layout_marginTop="16dp" + /> + + <Button + android:id="@+id/save_transactions" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/save_transactions" + app:layout_constraintTop_toBottomOf="@+id/random_transactions" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" + android:layout_marginTop="16dp" + /> + + <Button + android:id="@+id/send_transactions" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/send_transactions" + app:layout_constraintTop_toBottomOf="@+id/save_transactions" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:layout_marginTop="16dp" + /> + + <Button + android:id="@+id/logout_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/logout" + app:layout_constraintTop_toBottomOf="@+id/send_transactions" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintEnd_toEndOf="parent" + android:layout_marginTop="16dp"/> + </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index efa5003d6cdb2778fc6190e8d2ef6edc22bf9d79..4412b02a53bd550175ae936468dceed611158b3d 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -5,11 +5,11 @@ android:id="@+id/mobile_navigation" app:startDestination="@+id/navigation_transactions"> - <fragment - android:id="@+id/navigation_login" - android:name="com.example.bondoyap.ui.login.LoginFragment" - android:label="@string/title_login" - tools:layout="@layout/fragment_transactions" /> +<!-- <fragment--> +<!-- android:id="@+id/navigation_login"--> +<!-- android:name="com.example.bondoyap.ui.login.LoginActivity"--> +<!-- android:label="@string/title_login"--> +<!-- tools:layout="@layout/fragment_transactions" />--> <fragment android:id="@+id/navigation_transactions" diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f681ae11694396c167bee5b520246962495c34d --- /dev/null +++ b/app/src/main/res/values-land/dimens.xml @@ -0,0 +1,3 @@ +<resources> + <dimen name="activity_horizontal_margin">48dp</dimen> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-w1240dp/dimens.xml b/app/src/main/res/values-w1240dp/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..7e065116699dfedbfa503eb28fe432bf1447dd73 --- /dev/null +++ b/app/src/main/res/values-w1240dp/dimens.xml @@ -0,0 +1,3 @@ +<resources> + <dimen name="activity_horizontal_margin">200dp</dimen> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-w600dp/dimens.xml b/app/src/main/res/values-w600dp/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f681ae11694396c167bee5b520246962495c34d --- /dev/null +++ b/app/src/main/res/values-w600dp/dimens.xml @@ -0,0 +1,3 @@ +<resources> + <dimen name="activity_horizontal_margin">48dp</dimen> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127d327620c93d2b2d00342a68e97b98a48d..4ee6d8ffce1e4f269fc67238f23da6eb0a022be2 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,4 +7,5 @@ <color name="teal_700">#FF018786</color> <color name="black">#FF000000</color> <color name="white">#FFFFFFFF</color> + <color name="red">#FF0000</color> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a238740602ff63a2bf9ab121221d1f8573851554..4522aea76e6192deae69830c132b1819180725df 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,17 +4,24 @@ <string name="title_scanner">Scanner</string> <string name="title_graph">Graph</string> <string name="title_settings">Pengaturan</string> - <!-- Strings related to login --> + <string name="title_login">Login</string> <string name="prompt_email">Email</string> <string name="prompt_password">Password</string> <string name="action_sign_in">Sign in</string> <string name="action_sign_in_short">Sign in</string> - <string name="welcome">"Welcome!"</string> - <string name="invalid_username">Not a valid email</string> - <string name="invalid_password">Password must be >5 characters</string> - <string name="login_failed">"Login failed"</string> - <!-- Strings related to transaction --> + <string name="welcome">"Selamat Datang!"</string> + <string name="logged_in">Masuk dengan akun:</string> + <string name="invalid_username">Bukan email yang valid</string> + <string name="invalid_password">Password tidak boleh kosong</string> + <string name="login_failed">"Login gagal"</string> + <string name="logout">Logout</string> + <string name="title_activity_login">Login</string> + + <string name="random_transactions">Membuat transaksi random</string> + <string name="save_transactions">Simpan daftar transaksi</string> + <string name="send_transactions">Kirim daftar transaksi</string> + <string name="textfield_label_judul">Judul</string> <string name="textfield_label_nominal">Nominal</string> <string name="textfield_label_kategori">Kategori</string> @@ -24,4 +31,6 @@ <string name="hint_nominal">Enter Nominal</string> <string name="hint_kategori">Enter Kategori</string> <string name="hint_lokasi">Enter Lokasi</string> + <string name="pembelian">Pembelian</string> + <string name="pengeluaran">Pengeluaran</string> </resources> \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 31ed43cc99341f5b95f5b15fd54f84d14a585002..96e26d4e6b245ef5a2d5b0bba7d68c7d716a61d4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,4 +2,5 @@ plugins { id("com.android.application") version "8.3.0" apply false id("org.jetbrains.kotlin.android") version "1.9.22" apply false + id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false } \ No newline at end of file