redesign the welcome pages

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
@@ -3,6 +3,7 @@ package sh.sar.basedbank.ui.onboarding
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.os.CountDownTimer
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
@@ -17,6 +18,7 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
private lateinit var binding: ActivityOnboardingBinding private lateinit var binding: ActivityOnboardingBinding
private lateinit var prefs: SharedPreferences private lateinit var prefs: SharedPreferences
private var countDownTimer: CountDownTimer? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -29,34 +31,31 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
TabLayoutMediator(binding.dotsIndicator, binding.viewPager) { _, _ -> }.attach() 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) val savedLang = prefs.getString("language", null)
binding.languageChipGroup.setOnCheckedStateChangeListener(null) binding.languageToggle.clearOnButtonCheckedListeners()
when (savedLang) { when (savedLang) {
"en" -> binding.chipEnglish.isChecked = true "en" -> binding.btnLangEnglish.isChecked = true
"dv" -> binding.chipDhivehi.isChecked = true "dv" -> binding.btnLangDhivehi.isChecked = true
} }
binding.languageChipGroup.setOnCheckedStateChangeListener { _, checkedIds -> binding.languageToggle.addOnButtonCheckedListener { _, checkedId, isChecked ->
if (checkedIds.isNotEmpty()) { if (isChecked) selectLanguage(if (checkedId == R.id.btnLangEnglish) "en" else "dv")
selectLanguage(if (checkedIds[0] == R.id.chipEnglish) "en" else "dv")
}
} }
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
binding.languageChipGroup.visibility = if (position == 0) View.VISIBLE else View.GONE binding.languageSection.visibility = if (position == 0) View.VISIBLE else View.GONE
// Block forward swipe on slide 1 until security is set up binding.viewPager.isUserInputEnabled = when {
if (position == 1) { position > 2 -> false
binding.viewPager.isUserInputEnabled = position == 1 -> prefs.getString("security_method", null) != null
prefs.getString("security_method", null) != null else -> true
} else {
binding.viewPager.isUserInputEnabled = true
} }
updateButtons(position, adapter.itemCount) updateButtons(position, adapter.itemCount)
if (position == adapter.itemCount - 1) startGetStartedCountdown()
} }
}) })
binding.languageChipGroup.visibility = View.VISIBLE binding.languageSection.visibility = View.VISIBLE
updateButtons(0, adapter.itemCount) updateButtons(0, adapter.itemCount)
binding.btnNext.setOnClickListener { 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 // Called by SecuritySetupFragment when setup is complete
override fun onSecuritySetupComplete() { override fun onSecuritySetupComplete() {
binding.viewPager.isUserInputEnabled = true 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) { private fun selectLanguage(lang: String) {
@@ -83,6 +93,21 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
updateButtons(binding.viewPager.currentItem, binding.viewPager.adapter?.itemCount ?: 3) 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) { private fun updateButtons(position: Int, count: Int) {
val langSelected = prefs.getString("language", null) != null val langSelected = prefs.getString("language", null) != null
val securityDone = prefs.getString("security_method", 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 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 = if (isLast) View.GONE else View.VISIBLE
binding.btnNext.visibility = when {
isLast -> View.GONE
position == 1 && !securityDone -> View.GONE
else -> View.VISIBLE
}
binding.btnNext.isEnabled = when (position) { binding.btnNext.isEnabled = when (position) {
0 -> langSelected 0 -> langSelected
1 -> securityDone 1 -> securityDone
else -> true else -> true // position 2 (configure) has no gate
} }
} }
} }
@@ -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
}
}
@@ -19,14 +19,16 @@ class OnboardingFragment : Fragment() {
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val title = requireArguments().getString(ARG_TITLE, "") val titleRes = requireArguments().getInt(ARG_TITLE)
val desc = requireArguments().getString(ARG_DESC, "") val descRes = requireArguments().getInt(ARG_DESC)
val icon = requireArguments().getInt(ARG_ICON, 0) val icon = requireArguments().getInt(ARG_ICON, 0)
val isFirst = requireArguments().getBoolean(ARG_IS_FIRST, false) val isFirst = requireArguments().getBoolean(ARG_IS_FIRST, false)
val isLast = requireArguments().getBoolean(ARG_IS_LAST, false)
binding.icon.setImageResource(icon) binding.icon.setImageResource(icon)
binding.title.text = title binding.title.text = getString(titleRes)
binding.description.text = desc 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 // On the first slide, show the two placeholder cards for upcoming banks
binding.placeholderCards.visibility = if (isFirst) View.VISIBLE else View.GONE 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_DESC = "desc"
private const val ARG_ICON = "icon" private const val ARG_ICON = "icon"
private const val ARG_IS_FIRST = "is_first" private const val ARG_IS_FIRST = "is_first"
private const val ARG_IS_LAST = "is_last"
fun newInstance(slide: OnboardingSlide) = OnboardingFragment().apply { fun newInstance(slide: OnboardingSlide) = OnboardingFragment().apply {
arguments = bundleOf( arguments = bundleOf(
ARG_TITLE to slide.title, ARG_TITLE to slide.titleRes,
ARG_DESC to slide.description, ARG_DESC to slide.descRes,
ARG_ICON to slide.iconRes, ARG_ICON to slide.iconRes,
ARG_IS_FIRST to slide.isFirst ARG_IS_FIRST to slide.isFirst,
ARG_IS_LAST to slide.isLast
) )
} }
} }
@@ -9,36 +9,39 @@ class OnboardingPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(
private val slides = listOf( private val slides = listOf(
OnboardingSlide( OnboardingSlide(
title = activity.getString(R.string.onboarding_title_1), titleRes = R.string.onboarding_title_1,
description = activity.getString(R.string.onboarding_desc_1), descRes = R.string.onboarding_desc_1,
iconRes = R.drawable.ic_launcher_foreground, iconRes = R.drawable.ic_launcher_foreground,
isFirst = true isFirst = true
), ),
OnboardingSlide( OnboardingSlide(
title = activity.getString(R.string.onboarding_title_2), titleRes = R.string.onboarding_title_2,
description = activity.getString(R.string.onboarding_desc_2), descRes = R.string.onboarding_desc_2,
iconRes = R.drawable.ic_launcher_foreground, iconRes = R.drawable.ic_launcher_foreground,
isFirst = false isFirst = false
), ),
OnboardingSlide( OnboardingSlide(
title = activity.getString(R.string.onboarding_title_3), titleRes = R.string.onboarding_title_3,
description = activity.getString(R.string.onboarding_desc_3), descRes = R.string.onboarding_desc_3,
iconRes = R.drawable.ic_launcher_foreground, 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) { override fun createFragment(position: Int): Fragment = when (position) {
1 -> SecuritySetupFragment() 1 -> SecuritySetupFragment()
else -> OnboardingFragment.newInstance(slides[position]) 2 -> OnboardingConfigureFragment()
else -> OnboardingFragment.newInstance(slides[position - if (position > 2) 1 else 0])
} }
} }
data class OnboardingSlide( data class OnboardingSlide(
val title: String, val titleRes: Int,
val description: String, val descRes: Int,
val iconRes: Int, val iconRes: Int,
val isFirst: Boolean val isFirst: Boolean,
val isLast: Boolean = false
) )
@@ -86,17 +86,21 @@ class PatternView @JvmOverloads constructor(
if (errorState) return false if (errorState) return false
when (event.action) { when (event.action) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
parent?.requestDisallowInterceptTouchEvent(true)
recording = true; selected.clear() recording = true; selected.clear()
hit(event.x, event.y) hit(event.x, event.y)
} }
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
parent?.requestDisallowInterceptTouchEvent(true)
touchX = event.x; touchY = event.y touchX = event.x; touchY = event.y
hit(event.x, event.y) hit(event.x, event.y)
} }
MotionEvent.ACTION_UP -> { MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
parent?.requestDisallowInterceptTouchEvent(false)
recording = false recording = false
invalidate() invalidate()
onPatternComplete?.invoke(selected.map { it.index }) if (event.action == MotionEvent.ACTION_UP)
onPatternComplete?.invoke(selected.map { it.index })
} }
} }
invalidate() invalidate()
@@ -7,7 +7,6 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.biometric.BiometricManager
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import sh.sar.basedbank.R import sh.sar.basedbank.R
@@ -21,6 +20,7 @@ class SecuritySetupFragment : Fragment() {
interface Callback { interface Callback {
fun onSecuritySetupComplete() fun onSecuritySetupComplete()
fun onSecuritySetupReset()
} }
companion object { companion object {
@@ -33,7 +33,7 @@ class SecuritySetupFragment : Fragment() {
private var _b: FragmentSecuritySetupBinding? = null private var _b: FragmentSecuritySetupBinding? = null
private val b get() = _b!! 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 var step = Step.CHOOSE
private val pinDigits = mutableListOf<Int>() private val pinDigits = mutableListOf<Int>()
@@ -48,9 +48,6 @@ class SecuritySetupFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE) val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
val changeMode = arguments?.getBoolean(ARG_CHANGE_MODE, false) ?: false 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.cardPin.setOnClickListener { goTo(Step.PIN_ENTER) }
b.cardPattern.setOnClickListener { goTo(Step.PATTERN_ENTER) } b.cardPattern.setOnClickListener { goTo(Step.PATTERN_ENTER) }
@@ -62,20 +59,21 @@ class SecuritySetupFragment : Fragment() {
} }
} }
b.btnPatternBack.setOnClickListener { goTo(Step.CHOOSE) } 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.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() buildNumpad()
goTo(Step.CHOOSE)
if (!changeMode && prefs.getString("security_method", null) != null) {
goTo(Step.CONFIGURED)
} else {
goTo(Step.CHOOSE)
}
} }
private fun buildNumpad() { private fun buildNumpad() {
@@ -144,7 +142,7 @@ class SecuritySetupFragment : Fragment() {
Step.PIN_CONFIRM -> { Step.PIN_CONFIRM -> {
if (entered == firstPin) { if (entered == firstPin) {
saveCredential("pin", entered) saveCredential("pin", entered)
goToBiometricOrFinish() finishSetup()
} else { } else {
b.tvPinDots.text = getString(R.string.pin_no_match) b.tvPinDots.text = getString(R.string.pin_no_match)
pinDigits.clear() pinDigits.clear()
@@ -172,7 +170,7 @@ class SecuritySetupFragment : Fragment() {
Step.PATTERN_CONFIRM -> { Step.PATTERN_CONFIRM -> {
if (pattern == firstPattern) { if (pattern == firstPattern) {
saveCredential("pattern", pattern.joinToString("")) saveCredential("pattern", pattern.joinToString(""))
goToBiometricOrFinish() finishSetup()
} else { } else {
b.patternView.showError() b.patternView.showError()
b.tvPatternStatus.text = getString(R.string.pattern_no_match) 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) { private fun goTo(s: Step) {
step = s 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.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.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.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) { when (s) {
Step.PIN_ENTER -> { Step.PIN_ENTER -> {
@@ -256,6 +237,7 @@ class SecuritySetupFragment : Fragment() {
private fun finishSetup() { private fun finishSetup() {
val cb = activity as? Callback val cb = activity as? Callback
if (cb != null) { if (cb != null) {
goTo(Step.CONFIGURED)
cb.onSecuritySetupComplete() cb.onSecuritySetupComplete()
} else { } else {
parentFragmentManager.popBackStack() parentFragmentManager.popBackStack()
@@ -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>
+35 -17
View File
@@ -27,31 +27,49 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.chip.ChipGroup <LinearLayout
android:id="@+id/languageChipGroup" android:id="@+id/languageSection"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:orientation="vertical"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:visibility="gone" android:visibility="gone">
app:singleSelection="true"
app:selectionRequired="false">
<com.google.android.material.chip.Chip <TextView
android:id="@+id/chipEnglish"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="English" android:layout_gravity="center_horizontal"
style="@style/Widget.Material3.Chip.Filter" /> android:text="@string/select_language"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurface"
android:layout_marginBottom="8dp" />
<com.google.android.material.chip.Chip <com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/chipDhivehi" android:id="@+id/languageToggle"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="ދިވެހި" app:singleSelection="true"
style="@style/Widget.Material3.Chip.Filter" /> 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 <com.google.android.material.tabs.TabLayout
android:id="@+id/dotsIndicator" android:id="@+id/dotsIndicator"
@@ -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>
@@ -40,53 +40,46 @@
android:gravity="center" android:gravity="center"
android:lineSpacingMultiplier="1.4" /> android:lineSpacingMultiplier="1.4" />
<!-- Bank logo cards shown only on first slide --> <!-- Supported services shown only on first slide -->
<LinearLayout <LinearLayout
android:id="@+id/placeholderCards" android:id="@+id/placeholderCards"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="vertical"
android:layout_marginTop="40dp" android:layout_marginTop="40dp"
android:weightSum="2"
android:visibility="gone"> android:visibility="gone">
<com.google.android.material.card.MaterialCardView <TextView
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="80dp" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_gravity="center_horizontal"
android:layout_marginEnd="8dp" android:text="@string/onboarding_supported_services"
app:cardCornerRadius="12dp" android:textAppearance="?attr/textAppearanceLabelMedium"
app:strokeWidth="1dp" android:textColor="?attr/colorOnSurfaceVariant"
app:strokeColor="?attr/colorOutline"> android:layout_marginBottom="16dp" />
<ImageView <ImageView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="40dp"
android:src="@drawable/mib_faisanet_logo" android:src="@drawable/mib_faisanet_logo"
android:scaleType="centerInside" android:scaleType="centerInside"
android:padding="12dp" android:contentDescription="@string/mib_name" />
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 <ImageView
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="80dp" android:layout_height="40dp"
android:layout_weight="1" android:layout_marginTop="24dp"
android:layout_marginStart="8dp" android:src="@drawable/fahipay_logo_long"
app:cardCornerRadius="12dp" android:scaleType="centerInside"
app:strokeWidth="1dp" android:contentDescription="@string/fahipay_name" />
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>
</LinearLayout> </LinearLayout>
@@ -19,11 +19,12 @@
android:paddingBottom="24dp" android:paddingBottom="24dp"
android:gravity="center_horizontal"> android:gravity="center_horizontal">
<TextView <ImageView
android:layout_width="wrap_content" android:layout_width="80dp"
android:layout_height="wrap_content" android:layout_height="80dp"
android:text="🔒" android:src="@drawable/ic_lock"
android:textSize="56sp" android:tint="?attr/colorPrimary"
android:contentDescription="@null"
android:layout_marginBottom="16dp" /> android:layout_marginBottom="16dp" />
<TextView <TextView
@@ -255,73 +256,49 @@
</LinearLayout> </LinearLayout>
<!-- Step: Biometric prompt -->
<!-- Step: Already configured -->
<LinearLayout <LinearLayout
android:id="@+id/viewBiometric" android:id="@+id/viewConfigured"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center"
android:paddingHorizontal="24dp" android:paddingHorizontal="24dp"
android:paddingTop="40dp"
android:paddingBottom="24dp"
android:gravity="center_horizontal"
android:visibility="gone"> 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 <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="🔐" android:text="@string/security_already_configured"
android:textSize="72sp"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/biometric_title"
android:textAppearance="?attr/textAppearanceHeadlineSmall" android:textAppearance="?attr/textAppearanceHeadlineSmall"
android:textColor="?attr/colorOnSurface" android:textColor="?attr/colorOnSurface"
android:gravity="center" android:gravity="center"
android:layout_marginBottom="12dp" /> android:layout_marginBottom="8dp" />
<TextView <TextView
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/biometric_desc" android:text="@string/security_already_configured_desc"
android:textAppearance="?attr/textAppearanceBodyMedium" android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant" android:textColor="?attr/colorOnSurfaceVariant"
android:gravity="center" android:gravity="center"
android:layout_marginBottom="16dp" /> android:layout_marginBottom="32dp" />
<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>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btnEnableBiometrics" android:id="@+id/btnChangeLock"
android:layout_width="match_parent" style="@style/Widget.Material3.Button.OutlinedButton"
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:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/skip_biometrics" /> android:text="@string/settings_change_lock" />
</LinearLayout> </LinearLayout>
+2
View File
@@ -3,6 +3,8 @@
<string name="app_name">BasedBank</string> <string name="app_name">BasedBank</string>
<!-- Onboarding --> <!-- Onboarding -->
<string name="onboarding_supported_services">ހިދުމަތްތައް</string>
<string name="select_language">ބަސް ހިޔާލު ކުރޭ</string>
<string name="onboarding_title_1">ތިޔަ ބޭންކްތައް، އެއް އެޕެއްގައި</string> <string name="onboarding_title_1">ތިޔަ ބޭންކްތައް، އެއް އެޕެއްގައި</string>
<string name="onboarding_desc_1">BasedBank ގެ ސަބަބުން ތިޔަ ދިވެހި ބޭންކު އެކައުންޓްތައް، ހަމައެއް ތަނަކުން ބެލޭ. ބެލެންސް ބެލޭ، ތަފާތު ތަންތަން ބެލޭ — ތަފާތު އެޕްތަކަށް ބަދަލު ނުވެ.</string> <string name="onboarding_desc_1">BasedBank ގެ ސަބަބުން ތިޔަ ދިވެހި ބޭންކު އެކައުންޓްތައް، ހަމައެއް ތަނަކުން ބެލޭ. ބެލެންސް ބެލޭ، ތަފާތު ތަންތަން ބެލޭ — ތަފާތު އެޕްތަކަށް ބަދަލު ނުވެ.</string>
<string name="onboarding_title_2">އިތުރު ބޭންކްތައް ހިމެނެނީ</string> <string name="onboarding_title_2">އިތުރު ބޭންކްތައް ހިމެނެނީ</string>
+7 -2
View File
@@ -2,12 +2,14 @@
<string name="app_name">BasedBank</string> <string name="app_name">BasedBank</string>
<!-- Onboarding --> <!-- 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_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_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_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_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_title_3">Before You Begin</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_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="coming_soon">Coming Soon</string>
<string name="next">Next</string> <string name="next">Next</string>
<string name="get_started">Get Started</string> <string name="get_started">Get Started</string>
@@ -49,6 +51,9 @@
<!-- Security setup --> <!-- Security setup -->
<string name="security_setup">Secure Your App</string> <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_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">PIN Code</string>
<string name="method_pin_desc">48 digit numeric PIN</string> <string name="method_pin_desc">48 digit numeric PIN</string>
<string name="method_pattern">Draw Pattern</string> <string name="method_pattern">Draw Pattern</string>