redesign the welcome pages
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 5s

This commit is contained in:
2026-05-18 23:12:51 +05:00
parent ae307e3118
commit 33651ca107
13 changed files with 522 additions and 185 deletions

View File

@@ -3,6 +3,7 @@ package sh.sar.basedbank.ui.onboarding
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.os.CountDownTimer
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
@@ -17,6 +18,7 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
private lateinit var binding: ActivityOnboardingBinding
private lateinit var prefs: SharedPreferences
private var countDownTimer: CountDownTimer? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -29,34 +31,31 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
TabLayoutMediator(binding.dotsIndicator, binding.viewPager) { _, _ -> }.attach()
// Pre-select language chip without triggering the listener
// Pre-select language button without triggering the listener
val savedLang = prefs.getString("language", null)
binding.languageChipGroup.setOnCheckedStateChangeListener(null)
binding.languageToggle.clearOnButtonCheckedListeners()
when (savedLang) {
"en" -> binding.chipEnglish.isChecked = true
"dv" -> binding.chipDhivehi.isChecked = true
"en" -> binding.btnLangEnglish.isChecked = true
"dv" -> binding.btnLangDhivehi.isChecked = true
}
binding.languageChipGroup.setOnCheckedStateChangeListener { _, checkedIds ->
if (checkedIds.isNotEmpty()) {
selectLanguage(if (checkedIds[0] == R.id.chipEnglish) "en" else "dv")
}
binding.languageToggle.addOnButtonCheckedListener { _, checkedId, isChecked ->
if (isChecked) selectLanguage(if (checkedId == R.id.btnLangEnglish) "en" else "dv")
}
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
binding.languageChipGroup.visibility = if (position == 0) View.VISIBLE else View.GONE
// Block forward swipe on slide 1 until security is set up
if (position == 1) {
binding.viewPager.isUserInputEnabled =
prefs.getString("security_method", null) != null
} else {
binding.viewPager.isUserInputEnabled = true
binding.languageSection.visibility = if (position == 0) View.VISIBLE else View.GONE
binding.viewPager.isUserInputEnabled = when {
position > 2 -> false
position == 1 -> prefs.getString("security_method", null) != null
else -> true
}
updateButtons(position, adapter.itemCount)
if (position == adapter.itemCount - 1) startGetStartedCountdown()
}
})
binding.languageChipGroup.visibility = View.VISIBLE
binding.languageSection.visibility = View.VISIBLE
updateButtons(0, adapter.itemCount)
binding.btnNext.setOnClickListener {
@@ -71,10 +70,21 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
}
}
override fun onDestroy() {
super.onDestroy()
countDownTimer?.cancel()
}
// Called by SecuritySetupFragment when setup is complete
override fun onSecuritySetupComplete() {
binding.viewPager.isUserInputEnabled = true
updateButtons(binding.viewPager.currentItem, binding.viewPager.adapter?.itemCount ?: 3)
updateButtons(binding.viewPager.currentItem, binding.viewPager.adapter?.itemCount ?: 4)
}
// Called by SecuritySetupFragment when user resets to reconfigure
override fun onSecuritySetupReset() {
binding.viewPager.isUserInputEnabled = false
updateButtons(binding.viewPager.currentItem, binding.viewPager.adapter?.itemCount ?: 4)
}
private fun selectLanguage(lang: String) {
@@ -83,6 +93,21 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
updateButtons(binding.viewPager.currentItem, binding.viewPager.adapter?.itemCount ?: 3)
}
private fun startGetStartedCountdown() {
binding.btnGetStarted.isEnabled = false
countDownTimer?.cancel()
countDownTimer = object : CountDownTimer(5000, 1000) {
override fun onTick(millisUntilFinished: Long) {
val seconds = (millisUntilFinished / 1000 + 1).toInt()
binding.btnGetStarted.text = "${getString(R.string.get_started)} ($seconds)"
}
override fun onFinish() {
binding.btnGetStarted.text = getString(R.string.get_started)
binding.btnGetStarted.isEnabled = true
}
}.start()
}
private fun updateButtons(position: Int, count: Int) {
val langSelected = prefs.getString("language", null) != null
val securityDone = prefs.getString("security_method", null) != null
@@ -90,16 +115,11 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
binding.btnGetStarted.visibility = if (isLast) View.VISIBLE else View.GONE
// Hide Next on slide 1 until security is done (avoids a disabled-button-with-no-explanation)
binding.btnNext.visibility = when {
isLast -> View.GONE
position == 1 && !securityDone -> View.GONE
else -> View.VISIBLE
}
binding.btnNext.visibility = if (isLast) View.GONE else View.VISIBLE
binding.btnNext.isEnabled = when (position) {
0 -> langSelected
1 -> securityDone
else -> true
else -> true // position 2 (configure) has no gate
}
}
}

View File

@@ -0,0 +1,100 @@
package sh.sar.basedbank.ui.onboarding
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDelegate
import androidx.biometric.BiometricManager
import androidx.fragment.app.Fragment
import sh.sar.basedbank.R
import sh.sar.basedbank.databinding.FragmentOnboardingConfigureBinding
class OnboardingConfigureFragment : Fragment() {
private var _binding: FragmentOnboardingConfigureBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentOnboardingConfigureBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
// Navigation — default Drawer
val isBottom = prefs.getBoolean("bottom_nav", false)
binding.navModeToggle.check(if (isBottom) R.id.btnNavBottom else R.id.btnNavDrawer)
binding.navModeToggle.addOnButtonCheckedListener { _, checkedId, isChecked ->
if (!isChecked) return@addOnButtonCheckedListener
prefs.edit().putBoolean("bottom_nav", checkedId == R.id.btnNavBottom).apply()
}
// Theme — default System
val savedTheme = prefs.getString("theme", "system")
binding.themeToggle.check(when (savedTheme) {
"light" -> R.id.btnThemeLight
"dark" -> R.id.btnThemeDark
else -> R.id.btnThemeSystem
})
binding.themeToggle.addOnButtonCheckedListener { _, checkedId, isChecked ->
if (!isChecked) return@addOnButtonCheckedListener
val (key, mode) = when (checkedId) {
R.id.btnThemeLight -> "light" to AppCompatDelegate.MODE_NIGHT_NO
R.id.btnThemeDark -> "dark" to AppCompatDelegate.MODE_NIGHT_YES
else -> "system" to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
prefs.edit().putString("theme", key).apply()
AppCompatDelegate.setDefaultNightMode(mode)
}
// Biometrics
val canUseBiometrics = BiometricManager.from(requireContext())
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS
if (canUseBiometrics) {
val unlockEnabled = prefs.getBoolean("biometrics_enabled", false)
binding.switchBiometrics.isChecked = unlockEnabled
binding.switchBiometricsTransfer.isChecked = prefs.getBoolean("biometrics_transfer_confirm", false)
binding.switchBiometricsTransfer.isEnabled = unlockEnabled
binding.switchBiometrics.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean("biometrics_enabled", isChecked).apply()
binding.switchBiometricsTransfer.isEnabled = isChecked
if (!isChecked) {
binding.switchBiometricsTransfer.isChecked = false
prefs.edit().putBoolean("biometrics_transfer_confirm", false).apply()
}
}
binding.switchBiometricsTransfer.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean("biometrics_transfer_confirm", isChecked).apply()
}
} else {
binding.tvBiometricsHint.visibility = View.VISIBLE
binding.switchBiometrics.isEnabled = false
binding.switchBiometricsTransfer.isEnabled = false
}
// Block screenshots — default on
val blockScreenshots = prefs.getBoolean("block_screenshots", true)
binding.switchBlockScreenshots.isChecked = blockScreenshots
applyFlagSecure(blockScreenshots)
binding.switchBlockScreenshots.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean("block_screenshots", isChecked).apply()
applyFlagSecure(isChecked)
}
}
private fun applyFlagSecure(enabled: Boolean) {
val win = activity?.window ?: return
if (enabled) win.addFlags(android.view.WindowManager.LayoutParams.FLAG_SECURE)
else win.clearFlags(android.view.WindowManager.LayoutParams.FLAG_SECURE)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -19,14 +19,16 @@ class OnboardingFragment : Fragment() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val title = requireArguments().getString(ARG_TITLE, "")
val desc = requireArguments().getString(ARG_DESC, "")
val titleRes = requireArguments().getInt(ARG_TITLE)
val descRes = requireArguments().getInt(ARG_DESC)
val icon = requireArguments().getInt(ARG_ICON, 0)
val isFirst = requireArguments().getBoolean(ARG_IS_FIRST, false)
val isLast = requireArguments().getBoolean(ARG_IS_LAST, false)
binding.icon.setImageResource(icon)
binding.title.text = title
binding.description.text = desc
binding.title.text = getString(titleRes)
binding.description.text = getString(descRes)
binding.description.gravity = if (isLast) android.view.Gravity.START else android.view.Gravity.CENTER
// On the first slide, show the two placeholder cards for upcoming banks
binding.placeholderCards.visibility = if (isFirst) View.VISIBLE else View.GONE
@@ -42,13 +44,15 @@ class OnboardingFragment : Fragment() {
private const val ARG_DESC = "desc"
private const val ARG_ICON = "icon"
private const val ARG_IS_FIRST = "is_first"
private const val ARG_IS_LAST = "is_last"
fun newInstance(slide: OnboardingSlide) = OnboardingFragment().apply {
arguments = bundleOf(
ARG_TITLE to slide.title,
ARG_DESC to slide.description,
ARG_TITLE to slide.titleRes,
ARG_DESC to slide.descRes,
ARG_ICON to slide.iconRes,
ARG_IS_FIRST to slide.isFirst
ARG_IS_FIRST to slide.isFirst,
ARG_IS_LAST to slide.isLast
)
}
}

View File

@@ -9,36 +9,39 @@ class OnboardingPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(
private val slides = listOf(
OnboardingSlide(
title = activity.getString(R.string.onboarding_title_1),
description = activity.getString(R.string.onboarding_desc_1),
titleRes = R.string.onboarding_title_1,
descRes = R.string.onboarding_desc_1,
iconRes = R.drawable.ic_launcher_foreground,
isFirst = true
),
OnboardingSlide(
title = activity.getString(R.string.onboarding_title_2),
description = activity.getString(R.string.onboarding_desc_2),
titleRes = R.string.onboarding_title_2,
descRes = R.string.onboarding_desc_2,
iconRes = R.drawable.ic_launcher_foreground,
isFirst = false
),
OnboardingSlide(
title = activity.getString(R.string.onboarding_title_3),
description = activity.getString(R.string.onboarding_desc_3),
titleRes = R.string.onboarding_title_3,
descRes = R.string.onboarding_desc_3,
iconRes = R.drawable.ic_launcher_foreground,
isFirst = false
isFirst = false,
isLast = true
)
)
override fun getItemCount() = slides.size
override fun getItemCount() = slides.size + 1 // +1 for OnboardingConfigureFragment at position 2
override fun createFragment(position: Int): Fragment = when (position) {
1 -> SecuritySetupFragment()
else -> OnboardingFragment.newInstance(slides[position])
2 -> OnboardingConfigureFragment()
else -> OnboardingFragment.newInstance(slides[position - if (position > 2) 1 else 0])
}
}
data class OnboardingSlide(
val title: String,
val description: String,
val titleRes: Int,
val descRes: Int,
val iconRes: Int,
val isFirst: Boolean
val isFirst: Boolean,
val isLast: Boolean = false
)

View File

@@ -86,17 +86,21 @@ class PatternView @JvmOverloads constructor(
if (errorState) return false
when (event.action) {
MotionEvent.ACTION_DOWN -> {
parent?.requestDisallowInterceptTouchEvent(true)
recording = true; selected.clear()
hit(event.x, event.y)
}
MotionEvent.ACTION_MOVE -> {
parent?.requestDisallowInterceptTouchEvent(true)
touchX = event.x; touchY = event.y
hit(event.x, event.y)
}
MotionEvent.ACTION_UP -> {
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
parent?.requestDisallowInterceptTouchEvent(false)
recording = false
invalidate()
onPatternComplete?.invoke(selected.map { it.index })
if (event.action == MotionEvent.ACTION_UP)
onPatternComplete?.invoke(selected.map { it.index })
}
}
invalidate()

View File

@@ -7,7 +7,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.biometric.BiometricManager
import androidx.fragment.app.Fragment
import com.google.android.material.button.MaterialButton
import sh.sar.basedbank.R
@@ -21,6 +20,7 @@ class SecuritySetupFragment : Fragment() {
interface Callback {
fun onSecuritySetupComplete()
fun onSecuritySetupReset()
}
companion object {
@@ -33,7 +33,7 @@ class SecuritySetupFragment : Fragment() {
private var _b: FragmentSecuritySetupBinding? = null
private val b get() = _b!!
private enum class Step { CHOOSE, PIN_ENTER, PIN_CONFIRM, PATTERN_ENTER, PATTERN_CONFIRM, BIOMETRIC }
private enum class Step { CONFIGURED, CHOOSE, PIN_ENTER, PIN_CONFIRM, PATTERN_ENTER, PATTERN_CONFIRM }
private var step = Step.CHOOSE
private val pinDigits = mutableListOf<Int>()
@@ -48,9 +48,6 @@ class SecuritySetupFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
val changeMode = arguments?.getBoolean(ARG_CHANGE_MODE, false) ?: false
if (!changeMode && prefs.getString("security_method", null) != null) {
(activity as? Callback)?.onSecuritySetupComplete()
}
b.cardPin.setOnClickListener { goTo(Step.PIN_ENTER) }
b.cardPattern.setOnClickListener { goTo(Step.PATTERN_ENTER) }
@@ -62,20 +59,21 @@ class SecuritySetupFragment : Fragment() {
}
}
b.btnPatternBack.setOnClickListener { goTo(Step.CHOOSE) }
b.btnChangeLock.setOnClickListener {
prefs.edit().remove("security_method").apply()
(activity as? Callback)?.onSecuritySetupReset()
goTo(Step.CHOOSE)
}
b.patternView.onPatternComplete = { pattern -> handlePattern(pattern) }
b.btnEnableBiometrics.setOnClickListener {
prefs.edit().putBoolean("biometrics_enabled", true).apply()
finishSetup()
}
b.btnSkipBiometrics.setOnClickListener {
prefs.edit().putBoolean("biometrics_enabled", false).apply()
finishSetup()
}
buildNumpad()
goTo(Step.CHOOSE)
if (!changeMode && prefs.getString("security_method", null) != null) {
goTo(Step.CONFIGURED)
} else {
goTo(Step.CHOOSE)
}
}
private fun buildNumpad() {
@@ -144,7 +142,7 @@ class SecuritySetupFragment : Fragment() {
Step.PIN_CONFIRM -> {
if (entered == firstPin) {
saveCredential("pin", entered)
goToBiometricOrFinish()
finishSetup()
} else {
b.tvPinDots.text = getString(R.string.pin_no_match)
pinDigits.clear()
@@ -172,7 +170,7 @@ class SecuritySetupFragment : Fragment() {
Step.PATTERN_CONFIRM -> {
if (pattern == firstPattern) {
saveCredential("pattern", pattern.joinToString(""))
goToBiometricOrFinish()
finishSetup()
} else {
b.patternView.showError()
b.tvPatternStatus.text = getString(R.string.pattern_no_match)
@@ -187,29 +185,12 @@ class SecuritySetupFragment : Fragment() {
}
}
private fun goToBiometricOrFinish() {
// In change mode, biometrics is managed from Settings — skip this step
if (arguments?.getBoolean(ARG_CHANGE_MODE, false) == true) {
finishSetup()
return
}
val canAuth = BiometricManager.from(requireContext())
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
if (canAuth == BiometricManager.BIOMETRIC_SUCCESS) {
goTo(Step.BIOMETRIC)
} else {
requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
.edit().putBoolean("biometrics_enabled", false).apply()
finishSetup()
}
}
private fun goTo(s: Step) {
step = s
b.viewConfigured.visibility = if (s == Step.CONFIGURED) View.VISIBLE else View.GONE
b.viewChooseMethod.visibility = if (s == Step.CHOOSE) View.VISIBLE else View.GONE
b.viewPinSetup.visibility = if (s == Step.PIN_ENTER || s == Step.PIN_CONFIRM) View.VISIBLE else View.GONE
b.viewPatternSetup.visibility = if (s == Step.PATTERN_ENTER || s == Step.PATTERN_CONFIRM) View.VISIBLE else View.GONE
b.viewBiometric.visibility = if (s == Step.BIOMETRIC) View.VISIBLE else View.GONE
when (s) {
Step.PIN_ENTER -> {
@@ -256,6 +237,7 @@ class SecuritySetupFragment : Fragment() {
private fun finishSetup() {
val cb = activity as? Callback
if (cb != null) {
goTo(Step.CONFIGURED)
cb.onSecuritySetupComplete()
} else {
parentFragmentManager.popBackStack()

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm-2,15l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z" />
</vector>

View File

@@ -27,31 +27,49 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.chip.ChipGroup
android:id="@+id/languageChipGroup"
android:layout_width="wrap_content"
<LinearLayout
android:id="@+id/languageSection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:orientation="vertical"
android:layout_marginBottom="16dp"
android:visibility="gone"
app:singleSelection="true"
app:selectionRequired="false">
android:visibility="gone">
<com.google.android.material.chip.Chip
android:id="@+id/chipEnglish"
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="English"
style="@style/Widget.Material3.Chip.Filter" />
android:layout_gravity="center_horizontal"
android:text="@string/select_language"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurface"
android:layout_marginBottom="8dp" />
<com.google.android.material.chip.Chip
android:id="@+id/chipDhivehi"
android:layout_width="wrap_content"
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/languageToggle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="ދިވެހި"
style="@style/Widget.Material3.Chip.Filter" />
app:singleSelection="true"
app:selectionRequired="true">
</com.google.android.material.chip.ChipGroup>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnLangEnglish"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="English" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnLangDhivehi"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="ދިވެހި" />
</com.google.android.material.button.MaterialButtonToggleGroup>
</LinearLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/dotsIndicator"

View File

@@ -0,0 +1,218 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp"
android:paddingTop="40dp">
<!-- Appearance -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings_appearance"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings_navigation"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurface"
android:layout_marginBottom="8dp" />
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/navModeToggle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:singleSelection="true"
app:selectionRequired="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnNavDrawer"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/settings_nav_drawer" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnNavBottom"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/settings_nav_bottom" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/theme"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurface"
android:layout_marginBottom="8dp" />
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/themeToggle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:singleSelection="true"
app:selectionRequired="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnThemeSystem"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/theme_system" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnThemeLight"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/theme_light" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnThemeDark"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/theme_dark" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<!-- Privacy & Security -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings_privacy_security"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp" />
<!-- Biometrics -->
<LinearLayout
android:id="@+id/rowBiometrics"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings_biometrics"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:layout_marginBottom="4dp" />
<TextView
android:id="@+id/tvBiometricsHint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings_biometrics_unavailable"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant"
android:layout_marginBottom="8dp"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/settings_biometrics_unlock"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurface" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switchBiometrics"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/settings_biometrics_transfer"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurface" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switchBiometricsTransfer"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
<!-- Block screenshots -->
<LinearLayout
android:id="@+id/rowBlockScreenshots"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings_block_screenshots"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurface" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings_block_screenshots_desc"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switchBlockScreenshots"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -40,53 +40,46 @@
android:gravity="center"
android:lineSpacingMultiplier="1.4" />
<!-- Bank logo cards shown only on first slide -->
<!-- Supported services shown only on first slide -->
<LinearLayout
android:id="@+id/placeholderCards"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:orientation="vertical"
android:layout_marginTop="40dp"
android:weightSum="2"
android:visibility="gone">
<com.google.android.material.card.MaterialCardView
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
app:cardCornerRadius="12dp"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutline">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/onboarding_supported_services"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:layout_marginBottom="16dp" />
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/mib_faisanet_logo"
android:scaleType="centerInside"
android:padding="12dp"
android:contentDescription="@string/mib_name" />
<ImageView
android:layout_width="match_parent"
android:layout_height="40dp"
android:src="@drawable/mib_faisanet_logo"
android:scaleType="centerInside"
android:contentDescription="@string/mib_name" />
</com.google.android.material.card.MaterialCardView>
<ImageView
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="24dp"
android:src="@drawable/bml_logo_vector"
android:scaleType="centerInside"
android:contentDescription="@string/bml_name" />
<com.google.android.material.card.MaterialCardView
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_weight="1"
android:layout_marginStart="8dp"
app:cardCornerRadius="12dp"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutline">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/bml_logo_vector"
android:scaleType="centerInside"
android:padding="8dp"
android:contentDescription="@string/bml_name" />
</com.google.android.material.card.MaterialCardView>
<ImageView
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="24dp"
android:src="@drawable/fahipay_logo_long"
android:scaleType="centerInside"
android:contentDescription="@string/fahipay_name" />
</LinearLayout>

View File

@@ -19,11 +19,12 @@
android:paddingBottom="24dp"
android:gravity="center_horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🔒"
android:textSize="56sp"
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/ic_lock"
android:tint="?attr/colorPrimary"
android:contentDescription="@null"
android:layout_marginBottom="16dp" />
<TextView
@@ -255,73 +256,49 @@
</LinearLayout>
<!-- Step: Biometric prompt -->
<!-- Step: Already configured -->
<LinearLayout
android:id="@+id/viewBiometric"
android:id="@+id/viewConfigured"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:paddingHorizontal="24dp"
android:paddingTop="40dp"
android:paddingBottom="24dp"
android:gravity="center_horizontal"
android:visibility="gone">
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/ic_check_circle"
android:tint="?attr/colorPrimary"
android:contentDescription="@null"
android:layout_marginBottom="24dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🔐"
android:textSize="72sp"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/biometric_title"
android:text="@string/security_already_configured"
android:textAppearance="?attr/textAppearanceHeadlineSmall"
android:textColor="?attr/colorOnSurface"
android:gravity="center"
android:layout_marginBottom="12dp" />
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/biometric_desc"
android:text="@string/security_already_configured_desc"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:gravity="center"
android:layout_marginBottom="16dp" />
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
app:cardBackgroundColor="?attr/colorSecondaryContainer"
app:cardCornerRadius="12dp"
app:cardElevation="0dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="14dp"
android:text="@string/biometric_security_note"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSecondaryContainer"
android:gravity="center" />
</com.google.android.material.card.MaterialCardView>
android:layout_marginBottom="32dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnEnableBiometrics"
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="@string/enable_biometrics"
android:layout_marginBottom="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSkipBiometrics"
style="@style/Widget.Material3.Button.TextButton"
android:id="@+id/btnChangeLock"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/skip_biometrics" />
android:text="@string/settings_change_lock" />
</LinearLayout>

View File

@@ -3,6 +3,8 @@
<string name="app_name">BasedBank</string>
<!-- Onboarding -->
<string name="onboarding_supported_services">ހިދުމަތްތައް</string>
<string name="select_language">ބަސް ހިޔާލު ކުރޭ</string>
<string name="onboarding_title_1">ތިޔަ ބޭންކްތައް، އެއް އެޕެއްގައި</string>
<string name="onboarding_desc_1">BasedBank ގެ ސަބަބުން ތިޔަ ދިވެހި ބޭންކު އެކައުންޓްތައް، ހަމައެއް ތަނަކުން ބެލޭ. ބެލެންސް ބެލޭ، ތަފާތު ތަންތަން ބެލޭ — ތަފާތު އެޕްތަކަށް ބަދަލު ނުވެ.</string>
<string name="onboarding_title_2">އިތުރު ބޭންކްތައް ހިމެނެނީ</string>

View File

@@ -2,12 +2,14 @@
<string name="app_name">BasedBank</string>
<!-- Onboarding -->
<string name="onboarding_supported_services">Supported services</string>
<string name="select_language">Select Language</string>
<string name="onboarding_title_1">Your Banks, One App</string>
<string name="onboarding_desc_1">BasedBank brings all your Maldivian bank accounts together in one place. Check balances, view accounts, and more — without switching between apps.</string>
<string name="onboarding_title_2">More Banks Coming</string>
<string name="onboarding_desc_2">Support for additional banks is on the way. Stay tuned as we expand coverage across the Maldives.</string>
<string name="onboarding_title_3">Get Started</string>
<string name="onboarding_desc_3">Add your bank credentials and start viewing your accounts. Your data stays on your device.</string>
<string name="onboarding_title_3">Before You Begin</string>
<string name="onboarding_desc_3">BasedBank is an independent, third-party app. It is not affiliated with, endorsed by, or officially supported by any bank or financial institution.\n\nThis app works by logging into your internet banking services directly using your credentials and communicating with bank APIs. It is built using techniques derived from reverse-engineered official bank apps. Behaviour may change or break without notice if banks update their systems.\n\nDhiraagu and Ooredoo APIs are used for phone number and package lookups.\n\nBy tapping Get Started, you acknowledge and accept that:\n\n• Errors, failures, or service interruptions may occur at any time\n• Your bank may detect third-party access and apply restrictions or take other actions against your account\n• The developer of this app is not liable for any loss, damage, or consequences arising from your use of this app\n• You use this app entirely at your own risk</string>
<string name="coming_soon">Coming Soon</string>
<string name="next">Next</string>
<string name="get_started">Get Started</string>
@@ -49,6 +51,9 @@
<!-- Security setup -->
<string name="security_setup">Secure Your App</string>
<string name="security_setup_desc">Choose how you want to lock BasedBank when you\'re away.</string>
<string name="security_already_configured">App Lock Configured</string>
<string name="security_already_configured_desc">Your app lock is set up.</string>
<string name="method_pin">PIN Code</string>
<string name="method_pin_desc">48 digit numeric PIN</string>
<string name="method_pattern">Draw Pattern</string>